threadnote 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,8 +28,8 @@ Threadnote keeps that memory local and searchable without editing unrelated repo
28
28
  - Redaction: known config files such as `.mcp.json`, `config.toml`, and settings JSON are copied through a redactor
29
29
  before import.
30
30
  - Secret scanning: candidate files are skipped if common token or private-key patterns remain after redaction.
31
- - User instructions: `install` upserts a managed Threadnote block in `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md`
32
- without replacing existing personal instructions.
31
+ - User instructions: `install` upserts a managed Threadnote block in `~/.codex/AGENTS.md`, `~/.claude/CLAUDE.md`, and
32
+ `~/.cursor/rules/threadnote.md` without replacing existing personal instructions.
33
33
  - Agent config changes are explicit: `mcp-install` prints commands and snippets by default; use `--apply` to run them.
34
34
 
35
35
  ## Install
@@ -70,13 +70,15 @@ threadnote doctor --dry-run
70
70
 
71
71
  ### MCP
72
72
 
73
- Make the agents you use aware of Threadnote. Use only the MCP install lines for agents you actually use. Open a fresh Codex or Claude session after installing MCP so the new server registration is loaded.
73
+ Make the agents you use aware of Threadnote. Use only the MCP install lines for agents you actually use. Open a fresh
74
+ agent session after installing MCP so the new server registration is loaded.
74
75
 
75
76
  Dry-run examples:
76
77
 
77
78
  ```bash
78
79
  threadnote mcp-install codex
79
80
  threadnote mcp-install claude
81
+ threadnote mcp-install cursor
80
82
  ```
81
83
 
82
84
  Apply after review:
@@ -84,10 +86,12 @@ Apply after review:
84
86
  ```bash
85
87
  threadnote mcp-install codex --apply
86
88
  threadnote mcp-install claude --apply
89
+ threadnote mcp-install cursor --apply
87
90
  ```
88
91
 
89
92
  Claude installs at `user` scope by default so the same OpenViking MCP server is available from any repo or worktree.
90
93
  Use `--scope local` or `--scope project` only when you intentionally want repo-scoped Claude MCP config.
94
+ Cursor installs by updating the global `~/.cursor/mcp.json` file.
91
95
 
92
96
  If the package or checkout that originally installed `threadnote` has moved, run repair:
93
97
 
@@ -106,6 +110,8 @@ codex mcp add threadnote -- threadnote-mcp-server
106
110
  claude mcp add threadnote -- threadnote-mcp-server
107
111
  ```
108
112
 
113
+ Cursor uses the equivalent entry in `~/.cursor/mcp.json`.
114
+
109
115
  If a future OpenViking build exposes a healthy native endpoint, install it explicitly:
110
116
 
111
117
  ```bash
@@ -146,7 +152,7 @@ This is it! Start working with your agents as usual. The agent will automaticall
146
152
  - `doctor`: checks prerequisites, the generated command shim, manifest shape, templates, and local OpenViking health.
147
153
  - `install`: installs `openviking[local-embed]==0.3.12` if missing, creates `~/.openviking` config files if absent,
148
154
  writes the command shim, and upserts user-level agent instructions.
149
- - `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude MCP configs from the
155
+ - `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude/Cursor MCP configs from the
150
156
  current checkout.
151
157
  - `start`: starts `openviking-server` on `127.0.0.1:1933`.
152
158
  - `stop`: stops the detached server pid or macOS LaunchAgent.
@@ -156,7 +162,7 @@ This is it! Start working with your agents as usual. The agent will automaticall
156
162
  - `seed`: imports curated repo guidance and docs from the manifest.
157
163
  - `seed-skills`: imports global and repo-local `SKILL.md` files as a searchable resource catalog. Use
158
164
  `seed-skills --native` only after configuring a working VLM provider.
159
- - `mcp-install codex|claude`: installs or prints OpenViking MCP configuration for Codex or Claude.
165
+ - `mcp-install codex|claude|cursor`: installs or prints OpenViking MCP configuration for Codex, Claude, or Cursor.
160
166
  - `remember`: stores a durable memory.
161
167
  - `recall`: searches shared OpenViking context. It infers repo or skill scope from queries like
162
168
  `skills for api service`; use `--uri` or `--no-infer-scope` to override.
@@ -178,7 +184,8 @@ npm run threadnote -- install
178
184
  ```
179
185
 
180
186
  `install` writes a small command shim to `~/.local/bin/threadnote` by default and upserts user-level agent guidance in
181
- `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md`. After that, use the short command from any repo or working directory:
187
+ `~/.codex/AGENTS.md`, `~/.claude/CLAUDE.md`, and `~/.cursor/rules/threadnote.md`. After that, use the short command from
188
+ any repo or working directory:
182
189
 
183
190
  ```bash
184
191
  threadnote doctor --dry-run
@@ -6143,7 +6143,8 @@ var USER_INSTRUCTIONS_END_MARKER = "<!-- END THREADNOTE USER INSTRUCTIONS -->";
6143
6143
  var USER_MANIFEST_NAME = "seed-manifest.yaml";
6144
6144
  var USER_AGENT_INSTRUCTION_TARGETS = [
6145
6145
  { label: "codex user instructions", path: "~/.codex/AGENTS.md" },
6146
- { label: "claude user instructions", path: "~/.claude/CLAUDE.md" }
6146
+ { label: "claude user instructions", path: "~/.claude/CLAUDE.md" },
6147
+ { label: "cursor user rule", path: "~/.cursor/rules/threadnote.md" }
6147
6148
  ];
6148
6149
  var DEFAULT_SEED_PATTERNS = [
6149
6150
  "AGENTS.md",
@@ -6172,7 +6173,7 @@ async function main() {
6172
6173
  });
6173
6174
  program2.command("repair").description("Repair local OpenViking install, config files, server health, shim, manifest, and MCP config").option("--dry-run", "Print the repair actions without making changes").option(
6174
6175
  "--mcp <clients>",
6175
- "MCP clients to repair: available, all, none, codex, claude, or comma-separated list",
6176
+ "MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
6176
6177
  "available"
6177
6178
  ).option("--no-start", "Do not start OpenViking if health is failing").option("--package-manager <manager>", "pipx, uv, or pip", parsePackageManager).action(async (options) => {
6178
6179
  await runRepair(getRuntimeConfig(program2), options);
@@ -6185,7 +6186,7 @@ async function main() {
6185
6186
  });
6186
6187
  program2.command("uninstall").description("Remove Threadnote setup and optionally erase local memories").option("--dry-run", "Print uninstall actions without making changes").option(
6187
6188
  "--mcp <clients>",
6188
- "MCP clients to remove: available, all, none, codex, claude, or comma-separated list",
6189
+ "MCP clients to remove: available, all, none, codex, claude, cursor, or comma-separated list",
6189
6190
  "available"
6190
6191
  ).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
6191
6192
  await runUninstall(getRuntimeConfig(program2), options);
@@ -6199,10 +6200,10 @@ async function main() {
6199
6200
  program2.command("seed-skills").description("Seed Codex, Claude, and repo-local SKILL.md files as a searchable catalog").option("--dry-run", "Print skill files and ov commands without importing").option("--manifest <path>", "Manifest path for repo-local skill discovery").option("--native", "Use native OpenViking skill ingestion; requires a working VLM config").action(async (options) => {
6200
6201
  await runSeedSkills(getRuntimeConfig(program2, options.manifest), options);
6201
6202
  });
6202
- program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex or claude").option("--apply", "Actually modify the selected agent config").option("--name <name>", "MCP server name", OPENVIKING_MCP_NAME).option("--native-http", "Install OpenViking native HTTP MCP endpoint instead of the local stdio adapter").option("--scope <scope>", "Claude MCP config scope: user, local, or project", parseClaudeMcpScope, "user").option("--url <url>", "OpenViking native HTTP MCP URL").option("--bearer-token-env-var <name>", "Environment variable containing the local API key").action(async (agent, options) => {
6203
+ program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex, claude, or cursor").option("--apply", "Actually modify the selected agent config").option("--name <name>", "MCP server name", OPENVIKING_MCP_NAME).option("--native-http", "Install OpenViking native HTTP MCP endpoint instead of the local stdio adapter").option("--scope <scope>", "Claude MCP config scope: user, local, or project", parseClaudeMcpScope, "user").option("--url <url>", "OpenViking native HTTP MCP URL").option("--bearer-token-env-var <name>", "Environment variable containing the local API key").action(async (agent, options) => {
6203
6204
  await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
6204
6205
  });
6205
- program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--source-agent-client <name>", "codex, claude, gemini, or another client name", "codex").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").action(async (options) => {
6206
+ program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").action(async (options) => {
6206
6207
  await runRemember(getRuntimeConfig(program2), options);
6207
6208
  });
6208
6209
  program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
@@ -6214,7 +6215,7 @@ async function main() {
6214
6215
  program2.command("list").alias("ls").description("List a viking:// directory").argument("[uri]", "viking:// URI to list", "viking://").option("-a, --all", "Show hidden files such as .abstract.md and .overview.md").option("--dry-run", "Print ov command without listing").option("-n, --node-limit <count>", "Maximum number of nodes to list").option("-r, --recursive", "List subdirectories recursively").option("-s, --simple", "Print only paths").action(async (uri, options) => {
6215
6216
  await runList(getRuntimeConfig(program2), uri, options);
6216
6217
  });
6217
- program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--source-agent-client <name>", "codex, claude, gemini, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").action(async (options) => {
6218
+ program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").action(async (options) => {
6218
6219
  await runHandoff(getRuntimeConfig(program2), options);
6219
6220
  });
6220
6221
  program2.command("forget").description("Remove a viking:// URI from local OpenViking context").argument("<uri>", "viking:// URI to remove").option("--dry-run", "Print ov command without deleting").action(async (uri, options) => {
@@ -6294,16 +6295,20 @@ async function runInstall(config, options) {
6294
6295
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
6295
6296
  if (serverPath) {
6296
6297
  console.log(`OpenViking server already installed: ${serverPath}`);
6297
- const repairReasons = [];
6298
- if (await hasLocalEmbeddingDependency(serverPath) === false) {
6298
+ const localEmbeddingMissing = await hasLocalEmbeddingDependency(serverPath) === false;
6299
+ const pythonSystemCertificatesMissing = await hasPythonSystemCertificatesPatch(serverPath) === false;
6300
+ if (localEmbeddingMissing) {
6301
+ const repairReasons = [];
6299
6302
  repairReasons.push("local embedding extra is missing");
6300
- }
6301
- if (await hasPythonSystemCertificatesPatch(serverPath) === false) {
6302
- repairReasons.push("Python system certificate bridge is missing");
6303
- }
6304
- if (repairReasons.length > 0) {
6303
+ if (pythonSystemCertificatesMissing) {
6304
+ repairReasons.push("Python system certificate bridge is missing");
6305
+ }
6305
6306
  console.log(`OpenViking install needs repair: ${repairReasons.join("; ")}.`);
6306
6307
  await runInstallCommands(config, options.packageManager, true, options.dryRun === true);
6308
+ } else if (pythonSystemCertificatesMissing) {
6309
+ console.log("OpenViking install needs repair: Python system certificate bridge is missing.");
6310
+ const installCommand = await getPythonSystemCertificatesInstallCommand(serverPath);
6311
+ await maybeRun(options.dryRun === true, installCommand.executable, installCommand.args);
6307
6312
  }
6308
6313
  } else {
6309
6314
  await runInstallCommands(config, options.packageManager, false, options.dryRun === true);
@@ -6651,13 +6656,6 @@ async function runMcpInstall(config, agent, options) {
6651
6656
  const url = options.url ?? `http://${config.host}:${config.port}/mcp`;
6652
6657
  const apply = options.apply === true;
6653
6658
  const nativeHttp = options.nativeHttp === true;
6654
- const command = buildMcpInstallCommand(config, agent, name, {
6655
- bearerTokenEnvVar: options.bearerTokenEnvVar,
6656
- nativeHttp,
6657
- scope: options.scope,
6658
- url
6659
- });
6660
- const removeCommand = buildMcpRemoveCommand(agent, name);
6661
6659
  if (nativeHttp) {
6662
6660
  const mcpStatus = await readHttpStatus(url, 1200);
6663
6661
  const unavailable = mcpStatus === void 0 || mcpStatus === 404;
@@ -6670,6 +6668,22 @@ async function runMcpInstall(config, agent, options) {
6670
6668
  console.log(`WARN OpenViking native MCP endpoint is not available at ${url}; default mcp-install uses stdio.`);
6671
6669
  }
6672
6670
  }
6671
+ if (agent === "cursor") {
6672
+ await runCursorMcpInstall(config, name, {
6673
+ apply,
6674
+ bearerTokenEnvVar: options.bearerTokenEnvVar,
6675
+ nativeHttp,
6676
+ url
6677
+ });
6678
+ return;
6679
+ }
6680
+ const command = buildMcpInstallCommand(config, agent, name, {
6681
+ bearerTokenEnvVar: options.bearerTokenEnvVar,
6682
+ nativeHttp,
6683
+ scope: options.scope,
6684
+ url
6685
+ });
6686
+ const removeCommand = buildMcpRemoveCommand(agent, name);
6673
6687
  if (!apply) {
6674
6688
  console.log("Dry run. Re-run with --apply to modify the selected agent config.");
6675
6689
  if (removeCommand.cwd || command.cwd) {
@@ -6686,6 +6700,32 @@ async function runMcpInstall(config, agent, options) {
6686
6700
  });
6687
6701
  await maybeRun(false, command.executable, command.args, { cwd: command.cwd });
6688
6702
  }
6703
+ async function runCursorMcpInstall(config, name, options) {
6704
+ const path = cursorMcpConfigPath();
6705
+ const serverConfig = buildCursorMcpServerConfig(config, {
6706
+ bearerTokenEnvVar: options.bearerTokenEnvVar,
6707
+ nativeHttp: options.nativeHttp,
6708
+ url: options.url
6709
+ });
6710
+ const currentContent = await readFileIfExists(path);
6711
+ const nextContent = renderCursorMcpConfig(currentContent, name, serverConfig);
6712
+ if (!options.apply) {
6713
+ console.log("Dry run. Re-run with --apply to modify Cursor MCP config.");
6714
+ printCursorMcpSnippet(config, name, {
6715
+ bearerTokenEnvVar: options.bearerTokenEnvVar,
6716
+ nativeHttp: options.nativeHttp,
6717
+ url: options.url
6718
+ });
6719
+ return;
6720
+ }
6721
+ if (currentContent === nextContent) {
6722
+ console.log(`Already configured: ${path}`);
6723
+ return;
6724
+ }
6725
+ await ensureDirectory((0, import_node_path2.dirname)(path), false);
6726
+ await (0, import_promises10.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
6727
+ console.log(currentContent === void 0 ? `Wrote Cursor MCP config: ${path}` : `Updated Cursor MCP config: ${path}`);
6728
+ }
6689
6729
  async function runRemember(config, options) {
6690
6730
  const text = await getInputText(options.text, options.stdin === true);
6691
6731
  if (!text.trim()) {
@@ -6980,6 +7020,20 @@ async function runInstallCommands(config, preferred, force, dryRun) {
6980
7020
  await maybeRun(dryRun, installCommand.executable, installCommand.args);
6981
7021
  }
6982
7022
  }
7023
+ async function getPythonSystemCertificatesInstallCommand(serverPath) {
7024
+ const pythonPath = await siblingPythonForExecutable(serverPath);
7025
+ if (!pythonPath) {
7026
+ throw new Error(`Could not find the OpenViking Python environment for ${serverPath}`);
7027
+ }
7028
+ const uvPath = await findExecutable(["uv"]);
7029
+ if (uvPath) {
7030
+ return {
7031
+ executable: uvPath,
7032
+ args: ["pip", "install", "--native-tls", "--python", pythonPath, PYTHON_SYSTEM_CERTS_PACKAGE]
7033
+ };
7034
+ }
7035
+ return { executable: pythonPath, args: ["-m", "pip", "install", PYTHON_SYSTEM_CERTS_PACKAGE] };
7036
+ }
6983
7037
  async function getInstallCommands(config, preferred, force) {
6984
7038
  const packageSpec = `${OPENVIKING_PACKAGE_NAME}==${config.openVikingVersion}`;
6985
7039
  const manager = preferred ?? await detectPackageManager();
@@ -7261,6 +7315,10 @@ async function removeMcpConfigs(value, dryRun) {
7261
7315
  return;
7262
7316
  }
7263
7317
  for (const client of clients) {
7318
+ if (client === "cursor") {
7319
+ await removeCursorMcpConfig(OPENVIKING_MCP_NAME, dryRun);
7320
+ continue;
7321
+ }
7264
7322
  const command = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
7265
7323
  await maybeRun(dryRun, command.executable, command.args, { allowFailure: true, cwd: command.cwd });
7266
7324
  }
@@ -7268,6 +7326,7 @@ async function removeMcpConfigs(value, dryRun) {
7268
7326
  async function removeMcpSnippets(config, dryRun) {
7269
7327
  await removePathIfExists((0, import_node_path4.join)(config.agentContextHome, "mcp", `${OPENVIKING_MCP_NAME}.codex.toml`), "MCP snippet", dryRun);
7270
7328
  await removePathIfExists((0, import_node_path4.join)(config.agentContextHome, "mcp", `${OPENVIKING_MCP_NAME}.claude.txt`), "MCP snippet", dryRun);
7329
+ await removePathIfExists((0, import_node_path4.join)(config.agentContextHome, "mcp", `${OPENVIKING_MCP_NAME}.cursor.json`), "MCP snippet", dryRun);
7271
7330
  }
7272
7331
  async function eraseThreadnoteHome(path, dryRun) {
7273
7332
  assertSafeThreadnoteHomeForErase(path);
@@ -7388,6 +7447,9 @@ async function resolveAbsolutePattern(pattern) {
7388
7447
  return files.filter((filePath) => regex.test(toPosixPath(filePath)));
7389
7448
  }
7390
7449
  function buildMcpInstallCommand(config, agent, name, options) {
7450
+ if (agent === "cursor") {
7451
+ throw new Error("Cursor MCP config is written directly to ~/.cursor/mcp.json.");
7452
+ }
7391
7453
  const claudeCwd = getInvocationCwd();
7392
7454
  const claudeScope = options.scope ?? "user";
7393
7455
  if (!options.nativeHttp) {
@@ -7429,6 +7491,9 @@ function mcpAdapterCommand() {
7429
7491
  return [(0, import_node_path4.join)(toolRoot(), "bin", "threadnote-mcp-server.cjs")];
7430
7492
  }
7431
7493
  function buildMcpRemoveCommand(agent, name) {
7494
+ if (agent === "cursor") {
7495
+ throw new Error("Cursor MCP config is removed directly from ~/.cursor/mcp.json.");
7496
+ }
7432
7497
  return agent === "codex" ? { executable: "codex", args: ["mcp", "remove", name] } : { executable: "claude", args: ["mcp", "remove", name], cwd: getInvocationCwd() };
7433
7498
  }
7434
7499
  function mcpEnvironment(config) {
@@ -7439,6 +7504,72 @@ function mcpEnvironment(config) {
7439
7504
  `THREADNOTE_AGENT_ID=${config.agentId}`
7440
7505
  ];
7441
7506
  }
7507
+ function mcpEnvironmentObject(config) {
7508
+ return {
7509
+ THREADNOTE_ACCOUNT: config.account,
7510
+ THREADNOTE_AGENT_ID: config.agentId,
7511
+ THREADNOTE_HOME: config.agentContextHome,
7512
+ THREADNOTE_USER: config.user
7513
+ };
7514
+ }
7515
+ function buildCursorMcpServerConfig(config, options) {
7516
+ if (options.nativeHttp) {
7517
+ const server = { url: options.url };
7518
+ if (options.bearerTokenEnvVar) {
7519
+ server.headers = { Authorization: `Bearer \${env:${options.bearerTokenEnvVar}}` };
7520
+ }
7521
+ return server;
7522
+ }
7523
+ return {
7524
+ args: [mcpAdapterCommand()[0]],
7525
+ command: "/usr/bin/env",
7526
+ env: mcpEnvironmentObject(config)
7527
+ };
7528
+ }
7529
+ function renderCursorMcpConfig(currentContent, name, serverConfig) {
7530
+ const parsed = currentContent === void 0 ? {} : parseJsonConfigObject(currentContent);
7531
+ if (parsed === void 0) {
7532
+ throw new Error(`${cursorMcpConfigPath()} exists but is not a JSON object; not modifying it.`);
7533
+ }
7534
+ if (parsed.mcpServers !== void 0 && !isJsonObject(parsed.mcpServers)) {
7535
+ throw new Error(`${cursorMcpConfigPath()} has a non-object mcpServers field; not modifying it.`);
7536
+ }
7537
+ const nextConfig = { ...parsed };
7538
+ const mcpServers = isJsonObject(parsed.mcpServers) ? { ...parsed.mcpServers } : {};
7539
+ mcpServers[name] = serverConfig;
7540
+ nextConfig.mcpServers = mcpServers;
7541
+ return `${JSON.stringify(nextConfig, null, 2)}
7542
+ `;
7543
+ }
7544
+ async function removeCursorMcpConfig(name, dryRun) {
7545
+ const path = cursorMcpConfigPath();
7546
+ const currentContent = await readFileIfExists(path);
7547
+ if (currentContent === void 0) {
7548
+ console.log(`Already absent: ${path}`);
7549
+ return;
7550
+ }
7551
+ const parsed = parseJsonConfigObject(currentContent);
7552
+ if (parsed === void 0) {
7553
+ console.log(`WARN ${path} exists but is not a JSON object; not modifying it.`);
7554
+ return;
7555
+ }
7556
+ if (!isJsonObject(parsed.mcpServers) || parsed.mcpServers[name] === void 0) {
7557
+ console.log(`No Cursor MCP config found: ${path}`);
7558
+ return;
7559
+ }
7560
+ const nextConfig = { ...parsed };
7561
+ const mcpServers = { ...parsed.mcpServers };
7562
+ delete mcpServers[name];
7563
+ nextConfig.mcpServers = mcpServers;
7564
+ const nextContent = `${JSON.stringify(nextConfig, null, 2)}
7565
+ `;
7566
+ if (dryRun) {
7567
+ console.log(`Would update Cursor MCP config: ${path}`);
7568
+ return;
7569
+ }
7570
+ await (0, import_promises10.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
7571
+ console.log(`Updated Cursor MCP config: ${path}`);
7572
+ }
7442
7573
  function withIdentity(config, args) {
7443
7574
  return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
7444
7575
  }
@@ -7450,6 +7581,10 @@ function uriSegment(value) {
7450
7581
  return normalized.length > 0 ? normalized : "unknown";
7451
7582
  }
7452
7583
  function printMcpSnippet(config, agent, name, options) {
7584
+ if (agent === "cursor") {
7585
+ printCursorMcpSnippet(config, name, { nativeHttp: options.nativeHttp, url: options.url });
7586
+ return;
7587
+ }
7453
7588
  const snippetPath = (0, import_node_path4.join)(config.agentContextHome, "mcp", `${name}.${agent}.${agent === "codex" ? "toml" : "txt"}`);
7454
7589
  const command = buildMcpInstallCommand(config, agent, name, {
7455
7590
  nativeHttp: options.nativeHttp,
@@ -7462,6 +7597,20 @@ function printMcpSnippet(config, agent, name, options) {
7462
7597
  Snippet (${snippetPath}):
7463
7598
  ${snippet2}`);
7464
7599
  }
7600
+ function printCursorMcpSnippet(config, name, options) {
7601
+ const snippetPath = (0, import_node_path4.join)(config.agentContextHome, "mcp", `${name}.cursor.json`);
7602
+ const snippet2 = JSON.stringify(
7603
+ { mcpServers: { [name]: buildCursorMcpServerConfig(config, options) } },
7604
+ null,
7605
+ 2
7606
+ );
7607
+ console.log(`
7608
+ Snippet (${snippetPath}; merge into ${cursorMcpConfigPath()}):
7609
+ ${snippet2}`);
7610
+ }
7611
+ function cursorMcpConfigPath() {
7612
+ return expandPath("~/.cursor/mcp.json");
7613
+ }
7465
7614
  async function storeMemory(config, memory, dryRun) {
7466
7615
  const ov = await openVikingCliForMode(dryRun);
7467
7616
  const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
@@ -8035,10 +8184,10 @@ function parsePackageManager(value) {
8035
8184
  throw new Error(`Invalid package manager: ${value}`);
8036
8185
  }
8037
8186
  function parseAgentClient(value) {
8038
- if (value === "codex" || value === "claude") {
8187
+ if (value === "codex" || value === "claude" || value === "cursor") {
8039
8188
  return value;
8040
8189
  }
8041
- throw new Error(`Unsupported agent: ${value}. Expected codex or claude.`);
8190
+ throw new Error(`Unsupported agent: ${value}. Expected codex, claude, or cursor.`);
8042
8191
  }
8043
8192
  function parseClaudeMcpScope(value) {
8044
8193
  if (value === "local" || value === "project" || value === "user") {
@@ -8053,12 +8202,22 @@ async function resolveMcpClients(value, action) {
8053
8202
  }
8054
8203
  let requested;
8055
8204
  if (normalized === "available" || normalized === "all") {
8056
- requested = ["codex", "claude"];
8205
+ requested = ["codex", "claude", "cursor"];
8057
8206
  } else {
8058
8207
  requested = normalized.split(",").map((part) => part.trim()).filter(Boolean).map(parseAgentClient);
8059
8208
  }
8060
8209
  const clients = [];
8061
8210
  for (const client of requested) {
8211
+ if (client === "cursor") {
8212
+ if (!await isCursorAvailable()) {
8213
+ console.log(`WARN Cursor config not found; cannot ${action} cursor MCP config.`);
8214
+ continue;
8215
+ }
8216
+ if (!clients.includes(client)) {
8217
+ clients.push(client);
8218
+ }
8219
+ continue;
8220
+ }
8062
8221
  if (!await findExecutable([client])) {
8063
8222
  console.log(`WARN ${client} command not found; cannot ${action} ${client} MCP config.`);
8064
8223
  continue;
@@ -8069,6 +8228,15 @@ async function resolveMcpClients(value, action) {
8069
8228
  }
8070
8229
  return clients;
8071
8230
  }
8231
+ async function isCursorAvailable() {
8232
+ if (await exists(expandPath("~/.cursor"))) {
8233
+ return true;
8234
+ }
8235
+ if (await findExecutable(["cursor", "cursor-agent"])) {
8236
+ return true;
8237
+ }
8238
+ return (0, import_node_os2.platform)() === "darwin" && await exists("/Applications/Cursor.app");
8239
+ }
8072
8240
  function assertVikingUri(uri) {
8073
8241
  if (!uri.startsWith("viking://")) {
8074
8242
  throw new Error(`Refusing non-viking URI: ${uri}`);
package/docs/migration.md CHANGED
@@ -46,9 +46,9 @@ Create a handoff for the next agent before you stop.
46
46
  ```
47
47
 
48
48
  For better continuity, run `threadnote install` so it can add the agent-side guidance from `docs/agent-instructions.md`
49
- to the user-level `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` files. That guidance tells agents to recall context at
50
- task start, store durable memories when explicitly asked or when a reusable workflow fact is learned, and create
51
- handoffs automatically before stopping meaningful work.
49
+ to user-level Codex, Claude, and Cursor instruction files. That guidance tells agents to recall context at task start,
50
+ store durable memories when explicitly asked or when a reusable workflow fact is learned, and create handoffs
51
+ automatically before stopping meaningful work.
52
52
 
53
53
  ## Migration Steps
54
54
 
@@ -133,6 +133,15 @@ paths.
133
133
  Claude installs at user scope by default so it works from every repo/worktree. Use `--scope local` only when a
134
134
  repo-specific Claude MCP entry is intentional.
135
135
 
136
+ For Cursor:
137
+
138
+ ```bash
139
+ threadnote mcp-install cursor
140
+ threadnote mcp-install cursor --apply
141
+ ```
142
+
143
+ Cursor installs by updating the global `~/.cursor/mcp.json` file.
144
+
136
145
  Later, if the checkout that installed the MCP adapter is deleted or moved, repair it from any fresh checkout:
137
146
 
138
147
  ```bash
@@ -158,8 +167,7 @@ Remember this workflow note for future agents: ...
158
167
  Create a handoff now.
159
168
  ```
160
169
 
161
- Preferred agent behavior is automatic after `threadnote install` has updated the user-level Codex and Claude instruction
162
- files:
170
+ Preferred agent behavior is automatic after `threadnote install` has updated the user-level instruction files:
163
171
 
164
172
  - On non-trivial task start, search OpenViking for recent handoffs and relevant repo guidance.
165
173
  - When the user says "remember", store the memory after checking that it contains no secret or customer data.
package/docs/rollout.md CHANGED
@@ -18,7 +18,7 @@ Start with a local-only pilot.
18
18
 
19
19
  - Install completes in under 10 minutes after prerequisites.
20
20
  - `doctor` reports clear actionable checks.
21
- - Codex and Claude can both store and recall a shared handoff.
21
+ - Codex, Claude, or Cursor can store and recall a shared handoff after MCP setup.
22
22
  - Seeding curated guidance does not import known secret patterns.
23
23
  - Fresh agents can recall repo testing guidance and discover relevant skills.
24
24
  - `uninstall --dry-run` previews removal, and `uninstall` leaves memories intact unless `--erase-memories` is explicit.
package/docs/security.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  - Credentials, access tokens, API keys, certificates, private keys, or shell history.
8
8
  - Customer data, production data, HIPAA data, or production logs without explicit approval and scrubbing.
9
- - Local auth files such as `~/.codex/auth.json` or `~/.claude/.credentials.json`.
9
+ - Local auth files such as `~/.codex/auth.json`, `~/.claude/.credentials.json`, or Cursor account/session files.
10
10
  - Local settings files that may contain secrets unless they go through redaction.
11
11
 
12
12
  ## Built-In Controls
@@ -14,9 +14,9 @@
14
14
  - `.threadnoteignore` excludes common secret and build-output paths.
15
15
  - `.mcp.json`, `config.toml`, and settings JSON are redacted before import.
16
16
  - Files are skipped if common secret patterns remain after redaction.
17
- - `mcp-install` requires `--apply` before it changes Codex or Claude config.
18
- - `install` updates user-level Codex and Claude instruction files through a managed Markdown block. Existing personal
19
- instructions outside that block are preserved.
17
+ - `mcp-install` requires `--apply` before it changes Codex, Claude, or Cursor config.
18
+ - `install` updates user-level Codex, Claude, and Cursor instruction files through a managed Markdown block. Existing
19
+ personal instructions outside that block are preserved.
20
20
  - `uninstall` preserves local memories by default. `--erase-memories` is required before deleting `THREADNOTE_HOME`.
21
21
  - Config files created under `THREADNOTE_HOME` are written with user-only permissions.
22
22
 
@@ -42,6 +42,14 @@ threadnote start
42
42
  Threadnote installs `pip-system-certs` into the OpenViking environment so Python `requests` can use certificates trusted
43
43
  by the operating system.
44
44
 
45
+ If an older Threadnote release tries to reinstall all of OpenViking and fails while fetching packages such as `openai`,
46
+ install the certificate bridge directly into the existing OpenViking environment:
47
+
48
+ ```bash
49
+ uv pip install --native-tls --python "$(dirname "$(realpath "$(which openviking-server)")")/python" pip-system-certs
50
+ threadnote start
51
+ ```
52
+
45
53
  ## Local Embedding Extra Missing
46
54
 
47
55
  The default OpenViking config uses the local embedding backend. If the server log says `llama-cpp-python` is missing,
@@ -139,7 +147,7 @@ threadnote repair
139
147
  ```
140
148
 
141
149
  `repair` reinstalls the `threadnote` shim, repairs generated config files, starts OpenViking if needed, and rewrites
142
- Codex/Claude MCP configs to point at the current checkout.
150
+ Codex/Claude/Cursor MCP configs to point at the current checkout.
143
151
 
144
152
  ## MCP Install Is Only Printing Commands
145
153
 
@@ -149,6 +157,15 @@ This is expected. Run with `--apply` after reviewing the command:
149
157
  threadnote mcp-install codex --apply
150
158
  ```
151
159
 
160
+ For Cursor:
161
+
162
+ ```bash
163
+ threadnote mcp-install cursor --apply
164
+ ```
165
+
166
+ This updates the global `~/.cursor/mcp.json` file. Restart Cursor or open a fresh agent session after changing MCP
167
+ config.
168
+
152
169
  ## Uninstall Without Losing Memories
153
170
 
154
171
  Run:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",
@@ -130,3 +130,4 @@ say " threadnote start"
130
130
  say " threadnote doctor --dry-run"
131
131
  say " threadnote mcp-install codex --apply # if you use Codex"
132
132
  say " threadnote mcp-install claude --apply # if you use Claude"
133
+ say " threadnote mcp-install cursor --apply # if you use Cursor"
package/docs/demo.md DELETED
@@ -1,279 +0,0 @@
1
- # Demo: Cross-Agent Development Context
2
-
3
- This demo shows `threadnote` as a shared local memory layer for engineering work. The story is intentionally simple:
4
- one agent starts a task in a worktree, stores a handoff, and another agent or worktree recalls it without the
5
- developer copy-pasting chat history, PR notes, or terminal output.
6
-
7
- ## Demo Goal
8
-
9
- Show engineers that:
10
-
11
- - repo instructions still live in `AGENTS.md`, `CLAUDE.md`, and checked-in docs;
12
- - OpenViking adds durable, searchable context across agent sessions, worktrees, and repos;
13
- - agents should use MCP directly, while `threadnote` is the human-readable fallback and diagnostic path;
14
- - memories survive branch merges and worktree deletion because they live under the developer's OpenViking home, not in
15
- the git worktree;
16
- - `repair` fixes the common stale-worktree and MCP launcher problems that show up during real local development.
17
-
18
- ## Use Case
19
-
20
- Use "continue a PR after switching agents" as the live scenario.
21
-
22
- The engineer has a PR that updates a local tool. Codex has already done some work and creates a handoff. The engineer
23
- opens Claude, or a fresh Codex session from another repo/worktree, and asks it to continue. The new agent
24
- recalls the handoff, reads the relevant `viking://` memory, discovers repo guidance, and stores an updated handoff when
25
- it finishes.
26
-
27
- This is the highest-signal demo because it exercises the behavior engineers actually feel every day: context transfer,
28
- stale local setup, and "what did the previous agent already learn?"
29
-
30
- ## Prep Checklist
31
-
32
- Run these before the meeting from this checkout:
33
-
34
- ```bash
35
- npm install
36
- npm run doctor -- --dry-run
37
- npm run threadnote -- install
38
- threadnote start
39
- threadnote doctor --dry-run
40
- threadnote init-manifest --repo "$(pwd)"
41
- threadnote seed --dry-run
42
- threadnote seed
43
- threadnote seed-skills --dry-run
44
- threadnote seed-skills
45
- threadnote mcp-install codex --apply
46
- threadnote mcp-install claude --apply
47
- ```
48
-
49
- Open a fresh Codex or Claude session after installing MCP. Existing sessions may not pick up newly registered MCP
50
- servers.
51
-
52
- Optional: create a deterministic demo handoff so the search result is easy to find.
53
-
54
- ```bash
55
- threadnote handoff \
56
- --task "Demo PR: continue the threadnote rollout after adding repair and docs" \
57
- --tests "doctor --dry-run, mcp-install dry runs, local TypeScript checks" \
58
- --next-step "Recall this handoff, read the most relevant viking URI, then summarize what changed and what to do next"
59
- ```
60
-
61
- ## Live Script
62
-
63
- ### 1. Open With The Problem
64
-
65
- Say:
66
-
67
- ```text
68
- When we switch from one agent session to another, we usually lose the useful middle: what was tried, what failed, what
69
- the branch is about, and what the next agent should avoid re-discovering. This tool gives agents a shared local context
70
- layer without replacing repo instructions or indexing entire repos.
71
- ```
72
-
73
- Show that the repo still has normal instructions:
74
-
75
- ```bash
76
- ls AGENTS.md CLAUDE.md
77
- ```
78
-
79
- ### 2. Show Local Health
80
-
81
- Run:
82
-
83
- ```bash
84
- threadnote doctor --dry-run
85
- threadnote start
86
- ```
87
-
88
- Say:
89
-
90
- ```text
91
- The server is local. The default is 127.0.0.1:1933, so it does not conflict with apps running on localhost:80 or
92
- localhost:443. If the exact port is taken, we can set THREADNOTE_PORT.
93
- ```
94
-
95
- ### 3. Show MCP Is Installed
96
-
97
- Run the client-specific check you want to demo:
98
-
99
- ```bash
100
- codex mcp list
101
- claude mcp list
102
- ```
103
-
104
- Say:
105
-
106
- ```text
107
- MCP is the expected agent path. The CLI exists so humans can inspect, repair, and reproduce what the agent is doing.
108
- Claude installs at user scope by default, so it works from other repos and worktrees.
109
- ```
110
-
111
- If the MCP server does not appear in an already-open agent session, restart that session after `mcp-install --apply`.
112
-
113
- ### 4. Recall The Previous Handoff
114
-
115
- In the agent UI, ask:
116
-
117
- ```text
118
- Before touching code, recall recent OpenViking context for this branch and read the most relevant handoff.
119
- ```
120
-
121
- For a terminal fallback, run:
122
-
123
- ```bash
124
- threadnote recall --query "last handoff for threadnote rollout"
125
- ```
126
-
127
- Point out that recall returns candidate `viking://` URIs and abstracts. Then read the best match:
128
-
129
- ```bash
130
- threadnote read "<paste the most relevant viking:// URI>"
131
- ```
132
-
133
- Say:
134
-
135
- ```text
136
- Search is intentionally a pointer step. The agent should read or list the selected URI before treating it as context.
137
- That keeps recall cheap and makes the result auditable.
138
- ```
139
-
140
- ### 5. Discover Repo Guidance
141
-
142
- Ask the agent:
143
-
144
- ```text
145
- Find any repo guidance or skills that matter for this task before you continue.
146
- ```
147
-
148
- Terminal fallback:
149
-
150
- ```bash
151
- threadnote recall --query "repo agent instructions testing guidance"
152
- threadnote recall --query "skills for this repo"
153
- ```
154
-
155
- Say:
156
-
157
- ```text
158
- This is not source navigation. It is a curated catalog of instructions, skills, and durable notes that helps the agent
159
- start in the right neighborhood.
160
- ```
161
-
162
- ### 6. Show Cross-Repo Or Cross-Worktree Continuity
163
-
164
- Switch to another repo or worktree and run the same recall:
165
-
166
- ```bash
167
- cd ~/work/another-repo
168
- threadnote recall --query "last handoff for threadnote rollout"
169
- ```
170
-
171
- Or ask a fresh Claude session from another repo:
172
-
173
- ```text
174
- Use OpenViking to recall the latest handoff for the threadnote rollout and summarize the next step.
175
- ```
176
-
177
- Say:
178
-
179
- ```text
180
- The repo list is developer-local and manifest-driven. Nothing assumes a fixed source path. Memories are stored in the
181
- local OpenViking home, so deleting a worktree removes launcher paths, not the remembered handoff.
182
- ```
183
-
184
- ### 7. Remember A Durable Fact
185
-
186
- Ask the agent:
187
-
188
- ```text
189
- Remember this workflow fact: threadnote MCP for Claude should be installed at user scope so it works from any repo or
190
- worktree.
191
- ```
192
-
193
- Terminal fallback:
194
-
195
- ```bash
196
- threadnote remember \
197
- --text "Workflow fact: install Claude OpenViking MCP at user scope so it works from any repo or worktree."
198
- ```
199
-
200
- Say:
201
-
202
- ```text
203
- The rule is: remember durable workflow facts, not secrets, customer data, raw logs, or long diffs. Canonical repo rules
204
- should still be checked in.
205
- ```
206
-
207
- ### 8. Create The Next Handoff
208
-
209
- Ask the agent:
210
-
211
- ```text
212
- Create a handoff for the next agent with the current status, tests run, blockers, and next step.
213
- ```
214
-
215
- Terminal fallback:
216
-
217
- ```bash
218
- threadnote handoff \
219
- --task "Demo: verified cross-agent recall for the threadnote rollout" \
220
- --tests "doctor --dry-run, recall, read, remember, handoff" \
221
- --next-step "Open a fresh agent session and ask it to recall this handoff before continuing"
222
- ```
223
-
224
- Say:
225
-
226
- ```text
227
- This is the part we want agents to do automatically before pausing or finishing meaningful code changes.
228
- ```
229
-
230
- ### 9. Show Repair
231
-
232
- Run:
233
-
234
- ```bash
235
- threadnote repair --dry-run
236
- ```
237
-
238
- Say:
239
-
240
- ```text
241
- This catches the ugly local cases: stale command shims, MCP launchers pointing at deleted worktrees, missing manifests,
242
- and a stopped local server. Repair rewrites paths from the current checkout.
243
- ```
244
-
245
- ### 10. Close
246
-
247
- Say:
248
-
249
- ```text
250
- The important design choice is that this does not replace the repo. It makes the useful working memory around the repo
251
- available to whichever agent session picks up the task next.
252
- ```
253
-
254
- ## Expected Signals
255
-
256
- - `doctor --dry-run` reports no failures or prints concrete repair steps.
257
- - `recall` returns `memory`, `resource`, or `skill` rows with `viking://` URIs.
258
- - `read` turns a selected URI into actual handoff or memory content.
259
- - A fresh agent session can use the OpenViking MCP tools after MCP install.
260
- - `repair --dry-run` explains what it would fix without mutating anything.
261
-
262
- ## Recovery Branches
263
-
264
- - MCP is installed but the current agent says no OpenViking tools are registered: open a fresh agent session.
265
- - `claude mcp list` does not show OpenViking: run `threadnote mcp-install claude --apply`; the default scope is
266
- `user`.
267
- - `/health` is OK but `/mcp` fails: this is expected for OpenViking `0.3.12`; use the default stdio adapter instead of
268
- `--native-http`.
269
- - The old checkout or worktree was deleted: run
270
- `npm run threadnote -- repair` from any fresh checkout.
271
- - Recall returns only overview nodes: read the URI, or list the directory with
272
- `threadnote list <uri> --all --recursive`.
273
-
274
- ## What Not To Demo
275
-
276
- - Do not seed an entire repo.
277
- - Do not store secrets, tokens, customer data, raw production logs, or long diffs.
278
- - Do not present CLI commands as the ideal daily workflow. For normal work, the developer should ask the agent in
279
- natural language and the agent should use MCP directly.