diff --git a/AGENTS.md b/AGENTS.md index 4311118..2a47c56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ This repository is a pi provider extension that registers Cursor SDK-backed mode - `src/cursor-provider-lazy.ts` owns the lazy `streamSimple` wrapper that defers Cursor provider runtime imports until the provider is invoked. - `src/cursor-session-scope.ts` owns pi session cwd, session file/id/name/generation scope keys, and `session_start` / `session_info_changed` registration for session-agent pooling, cloud agent names, and debug grouping. - `src/cursor-http1.ts` owns branch-scoped local HTTP/1.1 session state, global-preference override tracking, and extension-owned SDK configuration/null reset. +- `src/cursor-ripgrep-path.ts` owns bundled Cursor SDK platform ripgrep resolution and local-agent environment initialization. - `src/cursor-session-agent.ts` owns session-scoped SDK agent pooling, transport-aware pool identity, send-state commits, busy tracking for in-flight SDK `run.wait()` work, and scoped acquire/dispose state. - `src/cursor-session-agent-lifecycle.ts` owns lazy session-agent lifecycle invalidation on model select, compaction, tree navigation, shutdown, and scope changes, including shutdown-time HTTP transport reset before module reload. - `src/cursor-session-compaction-prep.ts` owns `prepareCursorSessionForCompaction()` (release scoped live runs, reset pooled agent) wired from `session_before_compact` in `src/index.ts`. diff --git a/src/cursor-provider-turn-prepare.ts b/src/cursor-provider-turn-prepare.ts index e258b34..b7cc573 100644 --- a/src/cursor-provider-turn-prepare.ts +++ b/src/cursor-provider-turn-prepare.ts @@ -2,6 +2,7 @@ import type { Context, SimpleStreamOptions } from "@earendil-works/pi-ai/compat" import type { AgentModeOption, ModelSelection, SDKAgent } from "@cursor/sdk"; import { configureCursorSdkHttp1 } from "./cursor-http1.js"; import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js"; +import { ensureCursorRipgrepPath } from "./cursor-ripgrep-path.js"; import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js"; import { acquireSessionCursorAgent, @@ -247,6 +248,7 @@ async function prepareCursorLocalProviderTurn( let completed = false; try { + ensureCursorRipgrepPath(); const localSafety = { autoReview: resolvedConfig.local.autoReview.value, sandboxEnabled: resolvedConfig.local.sandboxEnabled.value, diff --git a/src/cursor-ripgrep-path.ts b/src/cursor-ripgrep-path.ts new file mode 100644 index 0000000..a04f863 --- /dev/null +++ b/src/cursor-ripgrep-path.ts @@ -0,0 +1,27 @@ +import { accessSync, constants } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; + +const require = createRequire(import.meta.url); +const RIPGREP_ENV = "CURSOR_RIPGREP_PATH"; + +export function resolveBundledCursorRipgrepPath(): string | undefined { + try { + const platformPackage = `@cursor/sdk-${process.platform}-${process.arch}`; + const packageDirectory = dirname(require.resolve(`${platformPackage}/package.json`)); + const ripgrepPath = join(packageDirectory, "bin", process.platform === "win32" ? "rg.exe" : "rg"); + accessSync(ripgrepPath, constants.X_OK); + return ripgrepPath; + } catch { + return undefined; + } +} + +export function ensureCursorRipgrepPath(): string | undefined { + const configuredPath = process.env[RIPGREP_ENV]; + if (configuredPath) return configuredPath; + + const bundledPath = resolveBundledCursorRipgrepPath(); + if (bundledPath) process.env[RIPGREP_ENV] = bundledPath; + return bundledPath; +} diff --git a/test/cursor-ripgrep-path.test.ts b/test/cursor-ripgrep-path.test.ts new file mode 100644 index 0000000..f4cbd05 --- /dev/null +++ b/test/cursor-ripgrep-path.test.ts @@ -0,0 +1,33 @@ +import { accessSync, constants } from "node:fs"; +import { afterEach, describe, expect, it } from "vitest"; +import { + ensureCursorRipgrepPath, + resolveBundledCursorRipgrepPath, +} from "../src/cursor-ripgrep-path.js"; + +const originalRipgrepPath = process.env.CURSOR_RIPGREP_PATH; + +afterEach(() => { + if (originalRipgrepPath === undefined) delete process.env.CURSOR_RIPGREP_PATH; + else process.env.CURSOR_RIPGREP_PATH = originalRipgrepPath; +}); + +describe("Cursor ripgrep path", () => { + it("resolves the executable from the installed Cursor SDK platform package", () => { + const ripgrepPath = resolveBundledCursorRipgrepPath(); + + if (!ripgrepPath) throw new Error("Expected the installed Cursor SDK platform package to include ripgrep"); + expect(ripgrepPath.replaceAll("\\", "/")).toContain(`@cursor/sdk-${process.platform}-${process.arch}`); + expect(() => accessSync(ripgrepPath, constants.X_OK)).not.toThrow(); + }); + + it("configures an empty path without overriding an existing value", () => { + process.env.CURSOR_RIPGREP_PATH = ""; + const bundledPath = ensureCursorRipgrepPath(); + expect(process.env.CURSOR_RIPGREP_PATH).toBe(bundledPath); + + process.env.CURSOR_RIPGREP_PATH = "/custom/rg"; + expect(ensureCursorRipgrepPath()).toBe("/custom/rg"); + expect(process.env.CURSOR_RIPGREP_PATH).toBe("/custom/rg"); + }); +});