From 788234527b18495e3eba9e2558d3f8a37d1d3555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Thu, 6 Aug 2026 12:43:09 +0200 Subject: [PATCH] feat(coding-agents): carry the Codex plugin's endpoint over too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The endpoint carry-over only read ~/.hindsight/claude-code.json, so someone migrating off the Codex plugin silently landed on Cloud despite having a server configured. Codex uses the same key names in ~/.hindsight/codex.json, so one reader serves both. The agent being installed is checked first: wiring Codex must take Codex's server even when a stale claude-code.json is still present. It then falls back to any known legacy config, since one server shared by both is the common case. These two are the only superseded plugins that shipped a user config — Cursor CLI, Copilot CLI, opencode and Cline have no endpoint to carry. --- .../docs-integrations/coding-agents.md | 13 +++--- .../coding-agents/README.md | 13 +++--- .../coding-agents/src/core/legacy.test.ts | 36 ++++++++++++++-- .../coding-agents/src/core/legacy.ts | 43 ++++++++++++++++--- .../coding-agents/src/installer.test.ts | 2 + .../coding-agents/src/installer.ts | 20 ++++++--- .../sdks/integrations/coding-agents.md | 13 +++--- 7 files changed, 111 insertions(+), 29 deletions(-) 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