Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions hindsight-docs/docs-integrations/coding-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions hindsight-integrations/coding-agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 33 additions & 3 deletions hindsight-integrations/coding-agents/src/core/legacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
Expand Down Expand Up @@ -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");
});
});
43 changes: 38 additions & 5 deletions hindsight-integrations/coding-agents/src/core/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<agent>.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<string, string> = {
"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;
Expand All @@ -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<string, unknown>;
try {
Expand All @@ -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 } : {}),
Expand All @@ -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 } : {}),
Expand Down
2 changes: 2 additions & 0 deletions hindsight-integrations/coding-agents/src/installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
20 changes: 14 additions & 6 deletions hindsight-integrations/coding-agents/src/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof readLegacyEndpoint>;
/** Reads an old per-agent plugin's endpoint; injectable for tests. */
readLegacy?: (home: string, prefer: readonly string[]) => ReturnType<typeof readLegacyEndpoint>;
log?: (m: string) => void;
}

Expand Down Expand Up @@ -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(", ")}`);
Expand All @@ -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<string, unknown> = { ...existing, serverMode: legacy.serverMode };
if (legacy.apiUrl) carried.apiUrl = legacy.apiUrl;
Expand All @@ -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.`
);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading