diff --git a/hindsight-docs/docs-integrations/coding-agents.md b/hindsight-docs/docs-integrations/coding-agents.md index 198ccf93a5..6edd9b311f 100644 --- a/hindsight-docs/docs-integrations/coding-agents.md +++ b/hindsight-docs/docs-integrations/coding-agents.md @@ -94,16 +94,19 @@ a `hookAdapter` in `src/harness/registry.ts`; persistent-plugin → implement `H The older per-agent integrations (`hindsight-claude-code`, `hindsight-cursor-cli`, `hindsight-codex`, …) are superseded by this package. Two things move; nothing else does. -**Your server moves automatically.** If `~/.hindsight/claude-code.json` exists, `install` adopts its -endpoint — `hindsightApiUrl` → `apiUrl`, `hindsightApiToken` → `apiToken`, and an empty URL means -the local daemon, as it did there. You already chose where your memory lives; defaulting to Cloud -instead would quietly send your prompts somewhere else. Pass `--server` to override. +**Your server moves automatically.** If `~/.hindsight/claude-code.json` or `~/.hindsight/codex.json` +exists, `install` adopts its endpoint — `hindsightApiUrl` → `apiUrl`, `hindsightApiToken` → +`apiToken`, and an empty URL means the local daemon, as it did there. The agent you are installing +is checked first, so wiring Codex takes Codex's server even if an old `claude-code.json` is still +lying around. You already chose where your memory lives; defaulting to Cloud instead would quietly +send your prompts somewhere else. Pass `--server` to override. (Those two are the only old plugins +that shipped a user config — Cursor CLI, Copilot CLI, opencode and Cline have no endpoint to carry.) **Your conversations are re-imported from local disk**, as new documents: ```bash cd /path/to/your/repo -hindsight-coding-agents install claude-code --import-conversations +hindsight-coding-agents install claude-code --import-conversations # or: install codex --import-conversations ``` This re-extracts the transcripts the agent already wrote, so it costs tokens roughly in proportion diff --git a/hindsight-integrations/coding-agents/README.md b/hindsight-integrations/coding-agents/README.md index 02a4067cbe..106c07668a 100644 --- a/hindsight-integrations/coding-agents/README.md +++ b/hindsight-integrations/coding-agents/README.md @@ -86,16 +86,19 @@ a `hookAdapter` in `src/harness/registry.ts`; persistent-plugin → implement `H The older per-agent integrations (`hindsight-claude-code`, `hindsight-cursor-cli`, `hindsight-codex`, …) are superseded by this package. Two things move; nothing else does. -**Your server moves automatically.** If `~/.hindsight/claude-code.json` exists, `install` adopts its -endpoint — `hindsightApiUrl` → `apiUrl`, `hindsightApiToken` → `apiToken`, and an empty URL means -the local daemon, as it did there. You already chose where your memory lives; defaulting to Cloud -instead would quietly send your prompts somewhere else. Pass `--server` to override. +**Your server moves automatically.** If `~/.hindsight/claude-code.json` or `~/.hindsight/codex.json` +exists, `install` adopts its endpoint — `hindsightApiUrl` → `apiUrl`, `hindsightApiToken` → +`apiToken`, and an empty URL means the local daemon, as it did there. The agent you are installing +is checked first, so wiring Codex takes Codex's server even if an old `claude-code.json` is still +lying around. You already chose where your memory lives; defaulting to Cloud instead would quietly +send your prompts somewhere else. Pass `--server` to override. (Those two are the only old plugins +that shipped a user config — Cursor CLI, Copilot CLI, opencode and Cline have no endpoint to carry.) **Your conversations are re-imported from local disk**, as new documents: ```bash cd /path/to/your/repo -hindsight-coding-agents install claude-code --import-conversations +hindsight-coding-agents install claude-code --import-conversations # or: install codex --import-conversations ``` This re-extracts the transcripts the agent already wrote, so it costs tokens roughly in proportion diff --git a/hindsight-integrations/coding-agents/src/core/legacy.test.ts b/hindsight-integrations/coding-agents/src/core/legacy.test.ts index 69d20a6f9d..a71003572a 100644 --- a/hindsight-integrations/coding-agents/src/core/legacy.test.ts +++ b/hindsight-integrations/coding-agents/src/core/legacy.test.ts @@ -6,19 +6,27 @@ import { readLegacyEndpoint } from "./legacy"; const homes: string[] = []; -function homeWith(config: unknown): string { +function homeWith(config: unknown, file = "claude-code.json"): string { const home = mkdtempSync(join(tmpdir(), "hindsight-legacy-")); homes.push(home); if (config !== undefined) { mkdirSync(join(home, ".hindsight"), { recursive: true }); writeFileSync( - join(home, ".hindsight", "claude-code.json"), + join(home, ".hindsight", file), typeof config === "string" ? config : JSON.stringify(config) ); } return home; } +/** Both old plugins that shipped a user config; same keys, different filename. */ +function homeWithBoth(claude: unknown, codex: unknown): string { + const home = homeWith(claude); + mkdirSync(join(home, ".hindsight"), { recursive: true }); + writeFileSync(join(home, ".hindsight", "codex.json"), JSON.stringify(codex)); + return home; +} + afterEach(() => { while (homes.length) rmSync(homes.pop()!, { recursive: true, force: true }); }); @@ -68,6 +76,28 @@ describe("readLegacyEndpoint", () => { const e = readLegacyEndpoint( homeWith({ hindsightApiUrl: "http://box:8888", recallBudget: "high", retainMode: "chunked" }) ); - expect(Object.keys(e ?? {}).sort()).toEqual(["apiUrl", "serverMode", "source"]); + expect(Object.keys(e ?? {}).sort()).toEqual(["apiUrl", "harness", "serverMode", "source"]); + }); + + it("reads the Codex plugin's config too", () => { + const e = readLegacyEndpoint(homeWith({ hindsightApiUrl: "http://cx:8888" }, "codex.json")); + expect(e?.harness).toBe("codex"); + expect(e?.apiUrl).toBe("http://cx:8888"); + }); + + // Installing Codex must not pick up a stale claude-code.json that points somewhere else. + it("prefers the config of the agent being installed", () => { + const home = homeWithBoth( + { hindsightApiUrl: "http://claude:8888" }, + { hindsightApiUrl: "http://codex:8888" } + ); + expect(readLegacyEndpoint(home, ["codex"])?.apiUrl).toBe("http://codex:8888"); + expect(readLegacyEndpoint(home, ["claude-code"])?.apiUrl).toBe("http://claude:8888"); + }); + + // A harness with no legacy plugin still benefits: the common case is one server for both. + it("falls back to any known legacy config", () => { + const home = homeWith({ hindsightApiUrl: "http://cx:8888" }, "codex.json"); + expect(readLegacyEndpoint(home, ["cursor-cli"])?.apiUrl).toBe("http://cx:8888"); }); }); diff --git a/hindsight-integrations/coding-agents/src/core/legacy.ts b/hindsight-integrations/coding-agents/src/core/legacy.ts index eeac1e927b..9df0dfd0f8 100644 --- a/hindsight-integrations/coding-agents/src/core/legacy.ts +++ b/hindsight-integrations/coding-agents/src/core/legacy.ts @@ -23,12 +23,26 @@ import { homedir } from "node:os"; import { join } from "node:path"; import { DEFAULT_DAEMON_PORT } from "./config"; -/** Where the old plugin told users to put their config (`~/.hindsight/claude-code.json`). */ -export function legacyConfigPath(home: string = homedir()): string { - return join(home, ".hindsight", "claude-code.json"); +/** + * The old per-agent plugins that shipped a user config, and its filename. + * + * Only these two ever adopted the `~/.hindsight/.json` convention — the other superseded + * integrations (Cursor CLI, Copilot CLI, opencode, Cline) have no such file, so there is no + * endpoint of theirs to carry. Both use IDENTICAL key names, which is why one reader serves both. + */ +export const LEGACY_CONFIG_FILES: Record = { + "claude-code": "claude-code.json", + codex: "codex.json", +}; + +/** Where an old plugin told users to put their config. */ +export function legacyConfigPath(harness: string, home: string = homedir()): string { + return join(home, ".hindsight", LEGACY_CONFIG_FILES[harness] ?? `${harness}.json`); } export interface LegacyEndpoint { + /** Which old plugin it came from, for the installer's output. */ + harness: string; serverMode: "cloud" | "self-hosted" | "daemon"; apiUrl?: string; apiToken?: string; @@ -47,8 +61,25 @@ const CLOUD_URL = "https://api.hindsight.vectorize.io"; * URL as "use the local daemon on `apiPort`" (`daemon.py:get_api_url`), so a config file that * never set one describes daemon mode, not an unconfigured user. */ -export function readLegacyEndpoint(home: string = homedir()): LegacyEndpoint | undefined { - const source = legacyConfigPath(home); +export function readLegacyEndpoint( + home: string = homedir(), + prefer: readonly string[] = [] +): LegacyEndpoint | undefined { + // Look at the agents being installed FIRST: someone wiring Codex should get Codex's endpoint even + // if a stale claude-code.json is also lying around. Falls back to any known legacy config, since + // the common case is one server shared by both. + const order = [...prefer, ...Object.keys(LEGACY_CONFIG_FILES)].filter( + (h, i, all) => h in LEGACY_CONFIG_FILES && all.indexOf(h) === i + ); + for (const harness of order) { + const found = readOne(harness, home); + if (found) return found; + } + return undefined; +} + +function readOne(harness: string, home: string): LegacyEndpoint | undefined { + const source = legacyConfigPath(harness, home); if (!existsSync(source)) return undefined; let raw: Record; try { @@ -63,6 +94,7 @@ export function readLegacyEndpoint(home: string = homedir()): LegacyEndpoint | u if (url) { return { + harness, serverMode: url.replace(/\/+$/, "") === CLOUD_URL ? "cloud" : "self-hosted", apiUrl: url, ...(apiToken ? { apiToken } : {}), @@ -71,6 +103,7 @@ export function readLegacyEndpoint(home: string = homedir()): LegacyEndpoint | u } const port = typeof raw.apiPort === "number" ? raw.apiPort : undefined; return { + harness, serverMode: "daemon", ...(apiToken ? { apiToken } : {}), ...(port && port !== DEFAULT_DAEMON_PORT ? { apiPort: port } : {}), diff --git a/hindsight-integrations/coding-agents/src/installer.test.ts b/hindsight-integrations/coding-agents/src/installer.test.ts index 7f6a830ac2..8308b84960 100644 --- a/hindsight-integrations/coding-agents/src/installer.test.ts +++ b/hindsight-integrations/coding-agents/src/installer.test.ts @@ -810,6 +810,7 @@ describe("server setup", () => { it("adopts the old plugin's endpoint instead of asking or defaulting to Cloud", () => { const ctx = makeCtx(); ctx.readLegacy = () => ({ + harness: "claude-code", serverMode: "self-hosted" as const, apiUrl: "http://legacy:8888", apiToken: "tok", @@ -829,6 +830,7 @@ describe("server setup", () => { it("an explicit --server still overrides what the old plugin used", () => { const ctx = makeCtx(); ctx.readLegacy = () => ({ + harness: "claude-code", serverMode: "self-hosted" as const, apiUrl: "http://legacy:8888", source: "/x", diff --git a/hindsight-integrations/coding-agents/src/installer.ts b/hindsight-integrations/coding-agents/src/installer.ts index 4a2eb279cc..3f2f97cec2 100644 --- a/hindsight-integrations/coding-agents/src/installer.ts +++ b/hindsight-integrations/coding-agents/src/installer.ts @@ -70,8 +70,8 @@ export interface InstallCtx { hasUvx?: () => boolean; detectLlm?: () => LlmChoice | undefined; hasRust?: () => boolean; - /** Reads the old per-agent plugin's endpoint; injectable for tests. */ - readLegacy?: (home: string) => ReturnType; + /** Reads an old per-agent plugin's endpoint; injectable for tests. */ + readLegacy?: (home: string, prefer: readonly string[]) => ReturnType; log?: (m: string) => void; } @@ -624,7 +624,7 @@ function readLineSync(prompt: string): string { * prerequisites are missing is still worth configuring, because `uv` or an API key can be * installed right after — unlike the harness preflights, which gate wiring that could never work. */ -function configureServer(c: InstallCtx, args: string[]): boolean { +function configureServer(c: InstallCtx, args: string[], installing: readonly string[]): boolean { const explicit = flagValue(args, "server"); if (explicit && !SERVER_MODES.includes(explicit as ServerMode)) { c.log?.(`unknown --server "${explicit}" — expected one of: ${SERVER_MODES.join(", ")}`); @@ -640,7 +640,7 @@ function configureServer(c: InstallCtx, args: string[]): boolean { // Someone coming from the old per-agent plugin already chose where their memory lives. // Adopt it rather than asking again — and above all rather than defaulting to Cloud, which // would quietly redirect their prompts and transcripts to a different server. - const legacy = (c.readLegacy ?? readLegacyEndpoint)(c.home); + const legacy = (c.readLegacy ?? readLegacyEndpoint)(c.home, installing); if (legacy) { const carried: Record = { ...existing, serverMode: legacy.serverMode }; if (legacy.apiUrl) carried.apiUrl = legacy.apiUrl; @@ -649,7 +649,7 @@ function configureServer(c: InstallCtx, args: string[]): boolean { writeJson(configPath, carried); c.log?.( `server: ${legacy.serverMode}${legacy.apiUrl ? ` (${legacy.apiUrl})` : ""} — carried over ` + - `from ${legacy.source}\n` + + `from the ${legacy.harness} plugin (${legacy.source})\n` + ` Only the endpoint moves; conversations do not. To bring this repo's history\n` + ` across, re-run here with --import-conversations.` ); @@ -1102,7 +1102,15 @@ export function run(argv: string[], ctx: InstallCtx): number { } // Which server the agents will talk to. Resolved BEFORE any harness is wired so the very first // session already has a config to read. - if (command === "install" && !configureServer(ctx, rawArgs)) return 1; + if ( + command === "install" && + !configureServer( + ctx, + rawArgs, + targets.map((t) => t.name) + ) + ) + return 1; // Preflight runs BEFORE any config is written, and only blocks the harness that failed: on // `install all` the other agents are still worth wiring. The non-zero exit keeps the failure diff --git a/skills/hindsight-docs/references/sdks/integrations/coding-agents.md b/skills/hindsight-docs/references/sdks/integrations/coding-agents.md index 1bfb5e8da3..f6532cbdd8 100644 --- a/skills/hindsight-docs/references/sdks/integrations/coding-agents.md +++ b/skills/hindsight-docs/references/sdks/integrations/coding-agents.md @@ -88,16 +88,19 @@ a `hookAdapter` in `src/harness/registry.ts`; persistent-plugin → implement `H The older per-agent integrations (`hindsight-claude-code`, `hindsight-cursor-cli`, `hindsight-codex`, …) are superseded by this package. Two things move; nothing else does. -**Your server moves automatically.** If `~/.hindsight/claude-code.json` exists, `install` adopts its -endpoint — `hindsightApiUrl` → `apiUrl`, `hindsightApiToken` → `apiToken`, and an empty URL means -the local daemon, as it did there. You already chose where your memory lives; defaulting to Cloud -instead would quietly send your prompts somewhere else. Pass `--server` to override. +**Your server moves automatically.** If `~/.hindsight/claude-code.json` or `~/.hindsight/codex.json` +exists, `install` adopts its endpoint — `hindsightApiUrl` → `apiUrl`, `hindsightApiToken` → +`apiToken`, and an empty URL means the local daemon, as it did there. The agent you are installing +is checked first, so wiring Codex takes Codex's server even if an old `claude-code.json` is still +lying around. You already chose where your memory lives; defaulting to Cloud instead would quietly +send your prompts somewhere else. Pass `--server` to override. (Those two are the only old plugins +that shipped a user config — Cursor CLI, Copilot CLI, opencode and Cline have no endpoint to carry.) **Your conversations are re-imported from local disk**, as new documents: ```bash cd /path/to/your/repo -hindsight-coding-agents install claude-code --import-conversations +hindsight-coding-agents install claude-code --import-conversations # or: install codex --import-conversations ``` This re-extracts the transcripts the agent already wrote, so it costs tokens roughly in proportion