threadnote 0.3.7 → 0.3.8

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
@@ -7,12 +7,17 @@ and it does not index whole repositories by default.
7
7
  ## Real-World Uses
8
8
 
9
9
  **Want to continue work in a fresh agent session?**
10
- `threadnote install` adds user-level Codex, Claude, and Cursor instructions so new agents automatically recall recent handoffs and relevant memories before they start changing code.
10
+ `threadnote install` adds user-level Codex, Claude, Cursor, and Copilot instructions so new agents automatically recall recent handoffs and relevant memories before they start changing code.
11
+
12
+ **Working on a feature branch over several sessions?**
13
+ Agents recall the branch handoff for current status, then recall durable feature memories for the design, decisions,
14
+ interfaces, and gotchas behind the feature. As useful implementation knowledge appears, agents update the durable feature
15
+ memory instead of leaving everything buried in transient handoffs.
11
16
 
12
17
  **Implemented a feature a while ago and need to pick it up again?**
13
18
  Ask the agent to recall the feature, branch, or repo. Threadnote returns auditable `viking://` pointers that the agent can read before deciding what still matters.
14
19
 
15
- **Switching between Codex, Claude, and Cursor?**\
20
+ **Switching between Codex, Claude, Cursor, and Copilot?**\
16
21
  Install the MCP adapter for each agent you use. The user-level instructions tell agents to store a handoff before they pause, so the next agent can search the same local memory layer instead of reconstructing context
17
22
  from chat history.
18
23
 
@@ -49,6 +54,16 @@ Active memories with the same project/topic write to a stable lifecycle path, so
49
54
  version instead of creating another timestamped note. Untagged memories still use timestamped files when you want a
50
55
  historical trail.
51
56
 
57
+ For feature branches, keep a durable feature memory and an active handoff side by side with the same project/topic:
58
+
59
+ ```bash
60
+ threadnote remember --kind durable --project my-repo --topic feature-x --text "Feature knowledge: ..."
61
+ threadnote handoff --project my-repo --topic feature-x --task "Current status for feature X"
62
+ ```
63
+
64
+ Agents should update the durable feature memory when valuable implementation knowledge changes, and update the handoff
65
+ when current status, tests, blockers, or next steps change.
66
+
52
67
  Use `archive` when an old handoff is useful for provenance but should stop being treated as current working context:
53
68
 
54
69
  ```bash
@@ -72,11 +87,12 @@ refactor, what an on-call investigation concluded, which workaround was verified
72
87
  memories should be compacted. Putting that history into instruction files makes them noisy, stale, and expensive to load
73
88
  into every context window.
74
89
 
75
- Threadnote keeps that moving layer local, searchable, and shared across agents. Agents recall only the relevant memories,
76
- handoffs, and skill/resource pointers when they need them, while the source files stay authoritative for project rules.
90
+ Threadnote keeps that moving layer local, searchable, and shared across agents. Agents recall only the relevant durable
91
+ feature memories, handoffs, and skill/resource pointers when they need them, while the source files stay authoritative
92
+ for project rules.
77
93
 
78
- The split is simple: put durable repo policy in `CLAUDE.md`/`AGENTS.md`; put task history, handoffs, personal workflow
79
- facts, and local cross-agent memory in Threadnote.
94
+ The split is simple: put durable repo policy in `CLAUDE.md`/`AGENTS.md`; put feature knowledge, task history, handoffs,
95
+ personal workflow facts, and local cross-agent memory in Threadnote.
80
96
 
81
97
  ## Safety Model
82
98
 
@@ -87,8 +103,9 @@ facts, and local cross-agent memory in Threadnote.
87
103
  - Redaction: known config files such as `.mcp.json`, `config.toml`, and settings JSON are copied through a redactor
88
104
  before import.
89
105
  - Secret scanning: candidate files are skipped if common token or private-key patterns remain after redaction.
90
- - User instructions: `install` upserts a managed Threadnote block in `~/.codex/AGENTS.md`, `~/.claude/CLAUDE.md`, and
91
- `~/.cursor/rules/threadnote.md` without replacing existing personal instructions.
106
+ - User instructions: `install` upserts managed Threadnote guidance in `~/.codex/AGENTS.md`, `~/.claude/CLAUDE.md`,
107
+ `~/.cursor/rules/threadnote.md`, and `~/.copilot/instructions/threadnote.instructions.md` without replacing existing
108
+ personal instructions.
92
109
  - Agent config changes are explicit: `mcp-install` prints commands and snippets by default; use `--apply` to run them.
93
110
 
94
111
  ## Install
@@ -145,7 +162,7 @@ threadnote update --check
145
162
  threadnote update --dry-run
146
163
  ```
147
164
 
148
- After updating, restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.
165
+ After updating, restart Cursor, Copilot, Codex, Claude, or open a fresh agent session so MCP tools reload.
149
166
 
150
167
  Some releases include post-update memory migrations. `threadnote update` runs the new package's migration prompt after
151
168
  repair, explains what will change, and asks before applying it. Use `threadnote update --yes` for unattended local
@@ -167,6 +184,7 @@ Dry-run examples:
167
184
  threadnote mcp-install codex
168
185
  threadnote mcp-install claude
169
186
  threadnote mcp-install cursor
187
+ threadnote mcp-install copilot
170
188
  ```
171
189
 
172
190
  Apply after review:
@@ -175,11 +193,14 @@ Apply after review:
175
193
  threadnote mcp-install codex --apply
176
194
  threadnote mcp-install claude --apply
177
195
  threadnote mcp-install cursor --apply
196
+ threadnote mcp-install copilot --apply
178
197
  ```
179
198
 
180
199
  Claude installs at `user` scope by default so the same OpenViking MCP server is available from any repo or worktree.
181
200
  Use `--scope local` or `--scope project` only when you intentionally want repo-scoped Claude MCP config.
182
201
  Cursor installs by updating the global `~/.cursor/mcp.json` file.
202
+ Copilot installs by updating the VS Code user-profile `mcp.json` file. Set `THREADNOTE_COPILOT_MCP_CONFIG` if you use a
203
+ custom VS Code profile or want to test against a temporary file.
183
204
 
184
205
  If the package or checkout that originally installed `threadnote` has moved, run repair:
185
206
 
@@ -199,6 +220,7 @@ claude mcp add threadnote -- threadnote-mcp-server
199
220
  ```
200
221
 
201
222
  Cursor uses the equivalent entry in `~/.cursor/mcp.json`.
223
+ Copilot uses the VS Code MCP `servers` entry in the user-profile `mcp.json`.
202
224
 
203
225
  If a future OpenViking build exposes a healthy native endpoint, install it explicitly:
204
226
 
@@ -255,8 +277,8 @@ This is it! Start working with your agents as usual. The agent will automaticall
255
277
  `--no-start` to skip the health check.
256
278
  - `update`: updates the published Threadnote package, then runs `repair` so shims and MCP config point at the new
257
279
  version.
258
- - `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude/Cursor MCP configs from the
259
- current checkout.
280
+ - `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude/Cursor/Copilot MCP configs
281
+ from the current checkout.
260
282
  - `start`: starts `openviking-server` on `127.0.0.1:1933`.
261
283
  - `stop`: stops the detached server pid or macOS LaunchAgent.
262
284
  - `uninstall`: removes Threadnote shims, MCP config, launchd config, and managed user instructions. Memories are
@@ -265,7 +287,8 @@ This is it! Start working with your agents as usual. The agent will automaticall
265
287
  - `seed`: imports curated repo guidance and docs from the manifest.
266
288
  - `seed-skills`: imports global and repo-local `SKILL.md` files as a searchable resource catalog so agents can discover
267
289
  reusable workflow guidance. Use `seed-skills --native` only after configuring a working VLM provider.
268
- - `mcp-install codex|claude|cursor`: installs or prints OpenViking MCP configuration for Codex, Claude, or Cursor.
290
+ - `mcp-install codex|claude|cursor|copilot`: installs or prints OpenViking MCP configuration for Codex, Claude, Cursor,
291
+ or GitHub Copilot in VS Code.
269
292
  - `remember`: stores a durable memory. Use `--replace <uri>` to store an updated memory and remove a superseded one
270
293
  after the new memory succeeds. Use `--kind`, `--project`, and `--topic` to store lifecycle-aware current knowledge.
271
294
  - `migrate-memories`: migrates legacy session-only `MEMORY` and `HANDOFF` records into durable memory files. Run
@@ -299,8 +322,9 @@ npm run threadnote -- install
299
322
  ```
300
323
 
301
324
  `install` writes a small command shim to `~/.local/bin/threadnote` by default and upserts user-level agent guidance in
302
- `~/.codex/AGENTS.md`, `~/.claude/CLAUDE.md`, and `~/.cursor/rules/threadnote.md`. After that, use the short command from
303
- any repo or working directory:
325
+ `~/.codex/AGENTS.md`, `~/.claude/CLAUDE.md`, `~/.cursor/rules/threadnote.md`, and
326
+ `~/.copilot/instructions/threadnote.instructions.md`. After that, use the short command from any repo or working
327
+ directory:
304
328
 
305
329
  ```bash
306
330
  threadnote doctor --dry-run
@@ -329,6 +353,7 @@ Environment variables:
329
353
  - `THREADNOTE_NPM_REGISTRY`: npm registry used by the installer and updater, default `https://registry.npmjs.org/`.
330
354
  - `THREADNOTE_NO_UPDATE_CHECK`: disables opportunistic update notifications.
331
355
  - `THREADNOTE_BIN_DIR`: directory for the `threadnote` shim, default `~/.local/bin`.
356
+ - `THREADNOTE_COPILOT_MCP_CONFIG`: explicit VS Code/Copilot `mcp.json` path for `mcp-install copilot`.
332
357
  - `THREADNOTE_HOST`: local bind host, default `127.0.0.1`.
333
358
  - `THREADNOTE_PORT`: local bind port, default `1933`.
334
359
 
@@ -31096,7 +31096,9 @@ async function main() {
31096
31096
  'Always pass JSON arguments. Example: recall_context({"query":"current repo latest handoff"}).',
31097
31097
  "Older clients may use the compatibility aliases `search`, `read`, and `list`.",
31098
31098
  'For durable facts, store kind="durable"; for current work logs, store kind="handoff" with project/topic so Threadnote keeps one active memory updated.',
31099
- "When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate handoffs do not accumulate.",
31099
+ "When a handoff describes an active branch or feature, recall durable feature memories for the same branch/topic before coding.",
31100
+ "During feature work, update durable feature knowledge when valuable implementation details, decisions, interfaces, or gotchas change.",
31101
+ "When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate durable memories or handoffs do not accumulate.",
31100
31102
  "Do not store secrets, customer data, raw production logs, or credentials."
31101
31103
  ].join("\n")
31102
31104
  }
@@ -31411,7 +31413,7 @@ function registerStoreTool(server, config2, name, description) {
31411
31413
  project: external_exports.string().optional().describe("Project/repo namespace, for example threadnote or mobile-native"),
31412
31414
  replaceUri: external_exports.string().optional().describe("Optional viking:// memory URI to forget after the new memory is safely stored"),
31413
31415
  text: external_exports.string().optional().describe("Required memory text to store"),
31414
- sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, codex, or claude"),
31416
+ sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, copilot, codex, or claude"),
31415
31417
  status: external_exports.enum(["active", "archived", "superseded"]).optional().describe("Memory lifecycle status"),
31416
31418
  topic: external_exports.string().optional().describe("Stable topic; active project/topic memories update one file")
31417
31419
  }
@@ -3489,9 +3489,14 @@ var USER_INSTRUCTIONS_START_MARKER = "<!-- BEGIN THREADNOTE USER INSTRUCTIONS --
3489
3489
  var USER_INSTRUCTIONS_END_MARKER = "<!-- END THREADNOTE USER INSTRUCTIONS -->";
3490
3490
  var USER_MANIFEST_NAME = "seed-manifest.yaml";
3491
3491
  var USER_AGENT_INSTRUCTION_TARGETS = [
3492
- { label: "codex user instructions", path: "~/.codex/AGENTS.md" },
3493
- { label: "claude user instructions", path: "~/.claude/CLAUDE.md" },
3494
- { label: "cursor user rule", path: "~/.cursor/rules/threadnote.md" }
3492
+ { kind: "block", label: "codex user instructions", path: "~/.codex/AGENTS.md" },
3493
+ { kind: "block", label: "claude user instructions", path: "~/.claude/CLAUDE.md" },
3494
+ { kind: "block", label: "cursor user rule", path: "~/.cursor/rules/threadnote.md" },
3495
+ {
3496
+ kind: "file",
3497
+ label: "copilot user instructions",
3498
+ path: "~/.copilot/instructions/threadnote.instructions.md"
3499
+ }
3495
3500
  ];
3496
3501
  var DEFAULT_SEED_PATTERNS = [
3497
3502
  "AGENTS.md",
@@ -3981,6 +3986,7 @@ function errorMessage(err) {
3981
3986
  }
3982
3987
 
3983
3988
  // src/mcp.ts
3989
+ var import_node_fs2 = require("node:fs");
3984
3990
  var import_promises2 = require("node:fs/promises");
3985
3991
  var import_node_path2 = require("node:path");
3986
3992
  var import_node_os2 = require("node:os");
@@ -4010,6 +4016,15 @@ async function runMcpInstall(config, agent, options) {
4010
4016
  });
4011
4017
  return;
4012
4018
  }
4019
+ if (agent === "copilot") {
4020
+ await runCopilotMcpInstall(config, name, {
4021
+ apply,
4022
+ bearerTokenEnvVar: options.bearerTokenEnvVar,
4023
+ nativeHttp,
4024
+ url
4025
+ });
4026
+ return;
4027
+ }
4013
4028
  const command = buildMcpInstallCommand(config, agent, name, {
4014
4029
  bearerTokenEnvVar: options.bearerTokenEnvVar,
4015
4030
  nativeHttp,
@@ -4059,6 +4074,34 @@ async function runCursorMcpInstall(config, name, options) {
4059
4074
  await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
4060
4075
  console.log(currentContent === void 0 ? `Wrote Cursor MCP config: ${path}` : `Updated Cursor MCP config: ${path}`);
4061
4076
  }
4077
+ async function runCopilotMcpInstall(config, name, options) {
4078
+ const path = copilotMcpConfigPath();
4079
+ const serverConfig = buildCopilotMcpServerConfig(config, {
4080
+ bearerTokenEnvVar: options.bearerTokenEnvVar,
4081
+ nativeHttp: options.nativeHttp,
4082
+ url: options.url
4083
+ });
4084
+ const currentContent = await readFileIfExists(path);
4085
+ const nextContent = renderCopilotMcpConfig(currentContent, name, serverConfig);
4086
+ if (!options.apply) {
4087
+ console.log("Dry run. Re-run with --apply to modify GitHub Copilot MCP config.");
4088
+ printCopilotMcpSnippet(config, name, {
4089
+ bearerTokenEnvVar: options.bearerTokenEnvVar,
4090
+ nativeHttp: options.nativeHttp,
4091
+ url: options.url
4092
+ });
4093
+ return;
4094
+ }
4095
+ if (currentContent === nextContent) {
4096
+ console.log(`Already configured: ${path}`);
4097
+ return;
4098
+ }
4099
+ await ensureDirectory((0, import_node_path2.dirname)(path), false);
4100
+ await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
4101
+ console.log(
4102
+ currentContent === void 0 ? `Wrote GitHub Copilot MCP config: ${path}` : `Updated GitHub Copilot MCP config: ${path}`
4103
+ );
4104
+ }
4062
4105
  async function removeMcpConfigs(value, dryRun) {
4063
4106
  const clients = await resolveMcpClients(value, "remove");
4064
4107
  if (clients.length === 0) {
@@ -4070,6 +4113,10 @@ async function removeMcpConfigs(value, dryRun) {
4070
4113
  await removeCursorMcpConfig(OPENVIKING_MCP_NAME, dryRun);
4071
4114
  continue;
4072
4115
  }
4116
+ if (client === "copilot") {
4117
+ await removeCopilotMcpConfig(OPENVIKING_MCP_NAME, dryRun);
4118
+ continue;
4119
+ }
4073
4120
  const command = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
4074
4121
  await maybeRun(dryRun, command.executable, command.args, { allowFailure: true, cwd: command.cwd });
4075
4122
  }
@@ -4090,11 +4137,19 @@ async function removeMcpSnippets(config, dryRun) {
4090
4137
  "MCP snippet",
4091
4138
  dryRun
4092
4139
  );
4140
+ await removePathIfExists(
4141
+ (0, import_node_path2.join)(config.agentContextHome, "mcp", `${OPENVIKING_MCP_NAME}.copilot.json`),
4142
+ "MCP snippet",
4143
+ dryRun
4144
+ );
4093
4145
  }
4094
4146
  function buildMcpInstallCommand(config, agent, name, options) {
4095
4147
  if (agent === "cursor") {
4096
4148
  throw new Error("Cursor MCP config is written directly to ~/.cursor/mcp.json.");
4097
4149
  }
4150
+ if (agent === "copilot") {
4151
+ throw new Error("GitHub Copilot MCP config is written directly to the VS Code user mcp.json file.");
4152
+ }
4098
4153
  const claudeCwd = getInvocationCwd();
4099
4154
  const claudeScope = options.scope ?? "user";
4100
4155
  if (!options.nativeHttp) {
@@ -4139,6 +4194,9 @@ function buildMcpRemoveCommand(agent, name) {
4139
4194
  if (agent === "cursor") {
4140
4195
  throw new Error("Cursor MCP config is removed directly from ~/.cursor/mcp.json.");
4141
4196
  }
4197
+ if (agent === "copilot") {
4198
+ throw new Error("GitHub Copilot MCP config is removed directly from the VS Code user mcp.json file.");
4199
+ }
4142
4200
  return agent === "codex" ? { executable: "codex", args: ["mcp", "remove", name] } : { executable: "claude", args: ["mcp", "remove", name], cwd: getInvocationCwd() };
4143
4201
  }
4144
4202
  function mcpEnvironment(config) {
@@ -4171,6 +4229,21 @@ function buildCursorMcpServerConfig(config, options) {
4171
4229
  env: mcpEnvironmentObject(config)
4172
4230
  };
4173
4231
  }
4232
+ function buildCopilotMcpServerConfig(config, options) {
4233
+ if (options.nativeHttp) {
4234
+ const server = { type: "http", url: options.url };
4235
+ if (options.bearerTokenEnvVar) {
4236
+ server.headers = { Authorization: `Bearer \${env:${options.bearerTokenEnvVar}}` };
4237
+ }
4238
+ return server;
4239
+ }
4240
+ return {
4241
+ args: [mcpAdapterCommand()[0]],
4242
+ command: "/usr/bin/env",
4243
+ env: mcpEnvironmentObject(config),
4244
+ type: "stdio"
4245
+ };
4246
+ }
4174
4247
  function renderCursorMcpConfig(currentContent, name, serverConfig) {
4175
4248
  const parsed = currentContent === void 0 ? {} : parseJsonConfigObject(currentContent);
4176
4249
  if (parsed === void 0) {
@@ -4186,6 +4259,21 @@ function renderCursorMcpConfig(currentContent, name, serverConfig) {
4186
4259
  return `${JSON.stringify(nextConfig, null, 2)}
4187
4260
  `;
4188
4261
  }
4262
+ function renderCopilotMcpConfig(currentContent, name, serverConfig) {
4263
+ const parsed = currentContent === void 0 ? {} : parseJsonConfigObject(currentContent);
4264
+ if (parsed === void 0) {
4265
+ throw new Error(`${copilotMcpConfigPath()} exists but is not a JSON object; not modifying it.`);
4266
+ }
4267
+ if (parsed.servers !== void 0 && !isJsonObject(parsed.servers)) {
4268
+ throw new Error(`${copilotMcpConfigPath()} has a non-object servers field; not modifying it.`);
4269
+ }
4270
+ const nextConfig = { ...parsed };
4271
+ const servers = isJsonObject(parsed.servers) ? { ...parsed.servers } : {};
4272
+ servers[name] = serverConfig;
4273
+ nextConfig.servers = servers;
4274
+ return `${JSON.stringify(nextConfig, null, 2)}
4275
+ `;
4276
+ }
4189
4277
  async function removeCursorMcpConfig(name, dryRun) {
4190
4278
  const path = cursorMcpConfigPath();
4191
4279
  const currentContent = await readFileIfExists(path);
@@ -4215,11 +4303,44 @@ async function removeCursorMcpConfig(name, dryRun) {
4215
4303
  await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
4216
4304
  console.log(`Updated Cursor MCP config: ${path}`);
4217
4305
  }
4306
+ async function removeCopilotMcpConfig(name, dryRun) {
4307
+ const path = copilotMcpConfigPath();
4308
+ const currentContent = await readFileIfExists(path);
4309
+ if (currentContent === void 0) {
4310
+ console.log(`Already absent: ${path}`);
4311
+ return;
4312
+ }
4313
+ const parsed = parseJsonConfigObject(currentContent);
4314
+ if (parsed === void 0) {
4315
+ console.log(`WARN ${path} exists but is not a JSON object; not modifying it.`);
4316
+ return;
4317
+ }
4318
+ if (!isJsonObject(parsed.servers) || parsed.servers[name] === void 0) {
4319
+ console.log(`No GitHub Copilot MCP config found: ${path}`);
4320
+ return;
4321
+ }
4322
+ const nextConfig = { ...parsed };
4323
+ const servers = { ...parsed.servers };
4324
+ delete servers[name];
4325
+ nextConfig.servers = servers;
4326
+ const nextContent = `${JSON.stringify(nextConfig, null, 2)}
4327
+ `;
4328
+ if (dryRun) {
4329
+ console.log(`Would update GitHub Copilot MCP config: ${path}`);
4330
+ return;
4331
+ }
4332
+ await (0, import_promises2.writeFile)(path, nextContent, { encoding: "utf8", mode: 420 });
4333
+ console.log(`Updated GitHub Copilot MCP config: ${path}`);
4334
+ }
4218
4335
  function printMcpSnippet(config, agent, name, options) {
4219
4336
  if (agent === "cursor") {
4220
4337
  printCursorMcpSnippet(config, name, { nativeHttp: options.nativeHttp, url: options.url });
4221
4338
  return;
4222
4339
  }
4340
+ if (agent === "copilot") {
4341
+ printCopilotMcpSnippet(config, name, { nativeHttp: options.nativeHttp, url: options.url });
4342
+ return;
4343
+ }
4223
4344
  const snippetPath = (0, import_node_path2.join)(config.agentContextHome, "mcp", `${name}.${agent}.${agent === "codex" ? "toml" : "txt"}`);
4224
4345
  const command = buildMcpInstallCommand(config, agent, name, {
4225
4346
  nativeHttp: options.nativeHttp,
@@ -4239,14 +4360,37 @@ function printCursorMcpSnippet(config, name, options) {
4239
4360
  Snippet (${snippetPath}; merge into ${cursorMcpConfigPath()}):
4240
4361
  ${snippet2}`);
4241
4362
  }
4363
+ function printCopilotMcpSnippet(config, name, options) {
4364
+ const snippetPath = (0, import_node_path2.join)(config.agentContextHome, "mcp", `${name}.copilot.json`);
4365
+ const snippet2 = JSON.stringify({ servers: { [name]: buildCopilotMcpServerConfig(config, options) } }, null, 2);
4366
+ console.log(`
4367
+ Snippet (${snippetPath}; merge into ${copilotMcpConfigPath()}):
4368
+ ${snippet2}`);
4369
+ }
4242
4370
  function cursorMcpConfigPath() {
4243
4371
  return expandPath("~/.cursor/mcp.json");
4244
4372
  }
4373
+ function copilotMcpConfigPath() {
4374
+ if (process.env.THREADNOTE_COPILOT_MCP_CONFIG) {
4375
+ return expandPath(process.env.THREADNOTE_COPILOT_MCP_CONFIG);
4376
+ }
4377
+ if ((0, import_node_os2.platform)() === "darwin") {
4378
+ const stablePath = expandPath("~/Library/Application Support/Code/User/mcp.json");
4379
+ const insidersPath = expandPath("~/Library/Application Support/Code - Insiders/User/mcp.json");
4380
+ return existsSyncDirectory((0, import_node_path2.dirname)(stablePath)) || !existsSyncDirectory((0, import_node_path2.dirname)(insidersPath)) ? stablePath : insidersPath;
4381
+ }
4382
+ if ((0, import_node_os2.platform)() === "win32") {
4383
+ const appData = process.env.APPDATA;
4384
+ return appData ? (0, import_node_path2.join)(appData, "Code", "User", "mcp.json") : expandPath("~/AppData/Roaming/Code/User/mcp.json");
4385
+ }
4386
+ const configHome = process.env.XDG_CONFIG_HOME ? expandPath(process.env.XDG_CONFIG_HOME) : expandPath("~/.config");
4387
+ return (0, import_node_path2.join)(configHome, "Code", "User", "mcp.json");
4388
+ }
4245
4389
  function parseAgentClient(value) {
4246
- if (value === "codex" || value === "claude" || value === "cursor") {
4390
+ if (value === "codex" || value === "claude" || value === "copilot" || value === "cursor") {
4247
4391
  return value;
4248
4392
  }
4249
- throw new Error(`Unsupported agent: ${value}. Expected codex, claude, or cursor.`);
4393
+ throw new Error(`Unsupported agent: ${value}. Expected codex, claude, copilot, or cursor.`);
4250
4394
  }
4251
4395
  function parseClaudeMcpScope(value) {
4252
4396
  if (value === "local" || value === "project" || value === "user") {
@@ -4261,7 +4405,7 @@ async function resolveMcpClients(value, action) {
4261
4405
  }
4262
4406
  let requested;
4263
4407
  if (normalized === "available" || normalized === "all") {
4264
- requested = ["codex", "claude", "cursor"];
4408
+ requested = ["codex", "claude", "cursor", "copilot"];
4265
4409
  } else {
4266
4410
  requested = normalized.split(",").map((part) => part.trim()).filter(Boolean).map(parseAgentClient);
4267
4411
  }
@@ -4277,6 +4421,16 @@ async function resolveMcpClients(value, action) {
4277
4421
  }
4278
4422
  continue;
4279
4423
  }
4424
+ if (client === "copilot") {
4425
+ if (!await isCopilotAvailable()) {
4426
+ console.log(`WARN VS Code/Copilot config not found; cannot ${action} copilot MCP config.`);
4427
+ continue;
4428
+ }
4429
+ if (!clients.includes(client)) {
4430
+ clients.push(client);
4431
+ }
4432
+ continue;
4433
+ }
4280
4434
  if (!await findExecutable([client])) {
4281
4435
  console.log(`WARN ${client} command not found; cannot ${action} ${client} MCP config.`);
4282
4436
  continue;
@@ -4296,9 +4450,28 @@ async function isCursorAvailable() {
4296
4450
  }
4297
4451
  return (0, import_node_os2.platform)() === "darwin" && await exists("/Applications/Cursor.app");
4298
4452
  }
4453
+ async function isCopilotAvailable() {
4454
+ if (process.env.THREADNOTE_COPILOT_MCP_CONFIG) {
4455
+ return true;
4456
+ }
4457
+ if (await exists((0, import_node_path2.dirname)(copilotMcpConfigPath()))) {
4458
+ return true;
4459
+ }
4460
+ if (await findExecutable(["code", "code-insiders"])) {
4461
+ return true;
4462
+ }
4463
+ return (0, import_node_os2.platform)() === "darwin" && await exists("/Applications/Visual Studio Code.app");
4464
+ }
4465
+ function existsSyncDirectory(path) {
4466
+ try {
4467
+ return (0, import_node_fs2.statSync)(path).isDirectory();
4468
+ } catch (_err) {
4469
+ return false;
4470
+ }
4471
+ }
4299
4472
 
4300
4473
  // src/runtime.ts
4301
- var import_node_fs2 = require("node:fs");
4474
+ var import_node_fs3 = require("node:fs");
4302
4475
  var import_node_os3 = require("node:os");
4303
4476
  var import_node_path3 = require("node:path");
4304
4477
  function getRuntimeConfig(program2, manifestOverride) {
@@ -4320,7 +4493,7 @@ function getRuntimeConfig(program2, manifestOverride) {
4320
4493
  }
4321
4494
  function defaultManifestPath(agentContextHome) {
4322
4495
  const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
4323
- return (0, import_node_fs2.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
4496
+ return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
4324
4497
  }
4325
4498
  function builtInExampleManifestPath() {
4326
4499
  return (0, import_node_path3.join)(toolRoot(), "config", "seed-manifest.example.yaml");
@@ -8224,13 +8397,13 @@ function detectSecretMatches(content) {
8224
8397
 
8225
8398
  // src/lifecycle.ts
8226
8399
  var import_node_child_process2 = require("node:child_process");
8227
- var import_node_fs4 = require("node:fs");
8400
+ var import_node_fs5 = require("node:fs");
8228
8401
  var import_promises8 = require("node:fs/promises");
8229
8402
  var import_node_os5 = require("node:os");
8230
8403
  var import_node_path7 = require("node:path");
8231
8404
 
8232
8405
  // src/update.ts
8233
- var import_node_fs3 = require("node:fs");
8406
+ var import_node_fs4 = require("node:fs");
8234
8407
  var import_promises6 = require("node:fs/promises");
8235
8408
  var import_node_os4 = require("node:os");
8236
8409
  var import_node_path6 = require("node:path");
@@ -8321,7 +8494,9 @@ async function runUpdate(config, options) {
8321
8494
  } else {
8322
8495
  console.log("Skipping post-update migration prompts because --no-post-update was provided.");
8323
8496
  }
8324
- console.log("Update complete. Restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.");
8497
+ console.log(
8498
+ "Update complete. Restart Cursor, Copilot, Codex, Claude, or open a fresh agent session so MCP tools reload."
8499
+ );
8325
8500
  }
8326
8501
  async function runPostUpdate(config, options) {
8327
8502
  if (!options.fromVersion || !options.toVersion) {
@@ -8614,7 +8789,7 @@ async function runtimeThreadnoteBin(runtime) {
8614
8789
  }
8615
8790
  async function isExecutable(path) {
8616
8791
  try {
8617
- await (0, import_promises6.access)(path, import_node_fs3.constants.X_OK);
8792
+ await (0, import_promises6.access)(path, import_node_fs4.constants.X_OK);
8618
8793
  return true;
8619
8794
  } catch (_err) {
8620
8795
  return false;
@@ -8922,7 +9097,7 @@ async function runStart(config, options) {
8922
9097
  process.exitCode = result;
8923
9098
  return;
8924
9099
  }
8925
- const logFd = (0, import_node_fs4.openSync)(logPath, "a");
9100
+ const logFd = (0, import_node_fs5.openSync)(logPath, "a");
8926
9101
  const child = spawnDetachedServerWithLog(server, args, logFd);
8927
9102
  child.unref();
8928
9103
  await (0, import_promises8.writeFile)((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
@@ -8940,7 +9115,7 @@ function spawnDetachedServerWithLog(server, args, logFd) {
8940
9115
  try {
8941
9116
  return spawnDetachedServer(server, args, logFd);
8942
9117
  } finally {
8943
- (0, import_node_fs4.closeSync)(logFd);
9118
+ (0, import_node_fs5.closeSync)(logFd);
8944
9119
  }
8945
9120
  }
8946
9121
  function spawnDetachedServer(server, args, logFd) {
@@ -9076,9 +9251,9 @@ async function commandShimCheck() {
9076
9251
  return { name: "threadnote shim", status: "ok", detail: shimPath };
9077
9252
  }
9078
9253
  async function userAgentInstructionsChecks() {
9079
- const expectedBlock = await renderUserAgentInstructionsBlock();
9080
9254
  return Promise.all(
9081
9255
  USER_AGENT_INSTRUCTION_TARGETS.map(async (target) => {
9256
+ const expectedInstructions = await renderUserAgentInstructions(target);
9082
9257
  const targetPath = expandPath(target.path);
9083
9258
  const content = await readFileIfExists(targetPath);
9084
9259
  if (content === void 0) {
@@ -9092,7 +9267,7 @@ async function userAgentInstructionsChecks() {
9092
9267
  detail: `${targetPath} missing threadnote block; install will add it`
9093
9268
  };
9094
9269
  }
9095
- if (existingBlock !== expectedBlock) {
9270
+ if (target.kind === "file" && content !== expectedInstructions || target.kind === "block" && existingBlock !== expectedInstructions) {
9096
9271
  return {
9097
9272
  name: target.label,
9098
9273
  status: "warn",
@@ -9311,11 +9486,15 @@ async function removeCommandShim(dryRun) {
9311
9486
  await removePath(shimPath, "command shim", dryRun);
9312
9487
  }
9313
9488
  async function installUserAgentInstructions(dryRun) {
9314
- const block = await renderUserAgentInstructionsBlock();
9315
9489
  for (const target of USER_AGENT_INSTRUCTION_TARGETS) {
9490
+ const instructions = await renderUserAgentInstructions(target);
9316
9491
  const targetPath = expandPath(target.path);
9317
9492
  const currentContent = await readFileIfExists(targetPath);
9318
- const nextContent = upsertManagedBlock(currentContent ?? "", block);
9493
+ if (target.kind === "file" && currentContent !== void 0 && extractManagedBlock(currentContent) === void 0) {
9494
+ console.log(`WARN ${targetPath} is not managed by threadnote; not modifying it`);
9495
+ continue;
9496
+ }
9497
+ const nextContent = target.kind === "file" ? instructions : upsertManagedBlock(currentContent ?? "", instructions);
9319
9498
  if (nextContent === void 0) {
9320
9499
  console.log(`WARN ${targetPath} has partial threadnote markers; not modifying it`);
9321
9500
  continue;
@@ -9341,6 +9520,14 @@ async function removeUserAgentInstructions(dryRun) {
9341
9520
  console.log(`Already absent: ${targetPath}`);
9342
9521
  continue;
9343
9522
  }
9523
+ if (target.kind === "file") {
9524
+ if (extractManagedBlock(currentContent) === void 0) {
9525
+ console.log(`WARN ${targetPath} is not managed by threadnote; not removing it`);
9526
+ continue;
9527
+ }
9528
+ await removePath(targetPath, target.label, dryRun);
9529
+ continue;
9530
+ }
9344
9531
  const nextContent = removeManagedBlock(currentContent);
9345
9532
  if (nextContent === void 0) {
9346
9533
  console.log(`WARN ${targetPath} has partial threadnote markers; not modifying it`);
@@ -9362,6 +9549,21 @@ async function removeUserAgentInstructions(dryRun) {
9362
9549
  console.log(`Updated ${targetPath}`);
9363
9550
  }
9364
9551
  }
9552
+ async function renderUserAgentInstructions(target) {
9553
+ const block = await renderUserAgentInstructionsBlock();
9554
+ if (target.kind === "block") {
9555
+ return block;
9556
+ }
9557
+ return [
9558
+ "---",
9559
+ "name: Threadnote",
9560
+ "description: Shared local context and handoffs through Threadnote",
9561
+ 'applyTo: "**"',
9562
+ "---",
9563
+ "",
9564
+ block
9565
+ ].join("\n");
9566
+ }
9365
9567
  async function renderUserAgentInstructionsBlock() {
9366
9568
  const instructions = (await (0, import_promises8.readFile)((0, import_node_path7.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
9367
9569
  return `${USER_INSTRUCTIONS_START_MARKER}
@@ -9557,7 +9759,7 @@ async function main() {
9557
9759
  });
9558
9760
  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(
9559
9761
  "--mcp <clients>",
9560
- "MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
9762
+ "MCP clients to repair: available, all, none, codex, claude, cursor, copilot, or comma-separated list",
9561
9763
  "available"
9562
9764
  ).option("--no-start", "Do not start OpenViking if health is failing").option("--no-post-update", "Skip post-update migration prompts after repair").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).action(async (options) => {
9563
9765
  const config = getRuntimeConfig(program2);
@@ -9574,7 +9776,7 @@ async function main() {
9574
9776
  });
9575
9777
  program2.command("uninstall").description("Remove Threadnote setup and optionally erase local memories").option("--dry-run", "Print uninstall actions without making changes").option(
9576
9778
  "--mcp <clients>",
9577
- "MCP clients to remove: available, all, none, codex, claude, cursor, or comma-separated list",
9779
+ "MCP clients to remove: available, all, none, codex, claude, cursor, copilot, or comma-separated list",
9578
9780
  "available"
9579
9781
  ).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
9580
9782
  await runUninstall(getRuntimeConfig(program2), options);
@@ -9588,10 +9790,10 @@ async function main() {
9588
9790
  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) => {
9589
9791
  await runSeedSkills(getRuntimeConfig(program2, options.manifest), options);
9590
9792
  });
9591
- 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) => {
9793
+ program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex, claude, cursor, or copilot").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) => {
9592
9794
  await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
9593
9795
  });
9594
- program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind, "durable").option("--project <name>", "Project/repo/topic namespace for lifecycle-aware storage").option("--replace <uri>", "Supersede an existing viking:// memory after the new memory is stored").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--status <status>", "active, archived, or superseded", parseMemoryStatus, "active").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").option("--topic <name>", "Stable topic name; active memories with the same project/topic update one file").action(async (options) => {
9796
+ program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind, "durable").option("--project <name>", "Project/repo/topic namespace for lifecycle-aware storage").option("--replace <uri>", "Supersede an existing viking:// memory after the new memory is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--status <status>", "active, archived, or superseded", parseMemoryStatus, "active").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").option("--topic <name>", "Stable topic name; active memories with the same project/topic update one file").action(async (options) => {
9595
9797
  await runRemember(getRuntimeConfig(program2), options);
9596
9798
  });
9597
9799
  program2.command("migrate-memories").description("Migrate legacy session-only Threadnote memories into durable memory files").option("--all-accounts", "Scan all local OpenViking accounts under THREADNOTE_HOME").option("--dry-run", "Print migration actions without writing memories").option("--limit <count>", "Maximum number of memories to migrate").option(
@@ -9614,7 +9816,7 @@ async function main() {
9614
9816
  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) => {
9615
9817
  await runList(getRuntimeConfig(program2), uri, options);
9616
9818
  });
9617
- 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("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").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").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
9819
+ 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("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
9618
9820
  await runHandoff(getRuntimeConfig(program2), options);
9619
9821
  });
9620
9822
  program2.command("archive").description("Move a memory into the archived lifecycle tree, then remove the original after the archive is stored").argument("<uri>", "viking:// memory URI to archive").option("--dry-run", "Print archive content and ov commands without changing anything").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind).option("--project <name>", "Override inferred project/repo namespace").option("--topic <name>", "Override inferred topic").action(async (uri, options) => {
@@ -1,6 +1,6 @@
1
1
  # Agent Instructions
2
2
 
3
- Threadnote installs this as user-level agent guidance for Codex, Claude, and Cursor.
3
+ Threadnote installs this as user-level agent guidance for Codex, Claude, Cursor, and Copilot.
4
4
 
5
5
  ## Shared Context
6
6
 
@@ -20,9 +20,14 @@ At the start of a non-trivial task, recall relevant context before making change
20
20
 
21
21
  - the current repo and branch;
22
22
  - recent handoffs;
23
+ - durable feature memories for the branch, feature name, project/topic, or issue;
23
24
  - task-specific skills or workflow guidance;
24
25
  - user or team preferences that may affect the work.
25
26
 
27
+ When a recalled handoff describes an active branch or feature, do a second recall for durable memories about that
28
+ feature before coding. The handoff tells you the current work state; durable feature memories tell you the design,
29
+ decisions, invariants, interfaces, and gotchas that should survive beyond one session.
30
+
26
31
  Skip proactive recall for tiny one-shot questions where context would add noise.
27
32
 
28
33
  Search results are not the final payload. Treat returned `viking://` URIs as pointers: read the relevant URI, or list a
@@ -36,6 +41,12 @@ logs, or other sensitive data.
36
41
  Also remember durable workflow facts you discover during work when they would help future agents and are not already in
37
42
  canonical docs. Prefer updating checked-in docs for canonical repo rules.
38
43
 
44
+ During feature work on a branch, maintain durable feature knowledge in addition to handoffs. Store or update a durable
45
+ memory when you learn something future agents should know about the feature itself: the intended behavior, design
46
+ decisions, API contracts, data flow, migration semantics, edge cases, known limitations, or why an implementation choice
47
+ was made. Do not wait until the end of the branch; update the feature memory whenever a valuable change lands or a
48
+ decision becomes clear.
49
+
39
50
  Use lifecycle metadata when storing memories:
40
51
 
41
52
  - `kind: durable` for facts future agents should use by default.
@@ -46,6 +57,14 @@ Use lifecycle metadata when storing memories:
46
57
  Archived handoffs are provenance, not default working context. Read them only when current durable memories or active
47
58
  handoffs are insufficient.
48
59
 
60
+ For branch feature work, prefer two stable memories with the same project and topic:
61
+
62
+ - `kind: durable` for the feature knowledge that should remain useful after the current agent session.
63
+ - `kind: handoff` for current status, files touched, tests, blockers, and next step.
64
+
65
+ If a durable feature memory already exists, update it in place with the same project/topic or with `--replace <uri>` /
66
+ `replaceUri`. Avoid creating a new timestamped durable memory for every small progress note.
67
+
49
68
  ## Memory Compaction
50
69
 
51
70
  When working on the same active issue, prefer keeping one current-state memory updated instead of creating many small
@@ -56,7 +75,8 @@ progress memories. If an existing memory is clearly the current state for the is
56
75
  When the issue has a stable name, prefer project/topic storage over timestamped memories:
57
76
 
58
77
  ```bash
59
- threadnote remember --kind durable --project my-repo --topic test-workflow --text "..."
78
+ threadnote recall --query "my-repo active-bug durable feature knowledge"
79
+ threadnote remember --kind durable --project my-repo --topic active-bug --text "Feature knowledge: ..."
60
80
  threadnote handoff --project my-repo --topic active-bug --task "..." --tests "..."
61
81
  ```
62
82
 
@@ -78,6 +98,7 @@ Never compact secrets, credentials, customer data, raw production logs, or check
78
98
  Before pausing, switching agents, or ending meaningful work with local changes, store a concise handoff. Include:
79
99
 
80
100
  - repo and branch;
101
+ - related durable feature memory URI or topic, when known;
81
102
  - files touched;
82
103
  - current status;
83
104
  - tests or checks run;
@@ -93,9 +114,11 @@ Use these only when MCP tools are not available:
93
114
  ```bash
94
115
  threadnote start
95
116
  threadnote recall --query "last handoff for this branch"
117
+ threadnote recall --query "durable feature knowledge for this branch"
96
118
  threadnote read viking://agent/threadnote/memories/.abstract.md
97
119
  threadnote list viking://agent/threadnote/memories --all --recursive
98
120
  threadnote remember --kind durable --project example --topic workflow --text "Durable engineering note..."
121
+ threadnote remember --kind durable --project example --topic active-issue --text "Feature knowledge..."
99
122
  threadnote remember --replace viking://user/example/memories/durable/projects/example/workflow.md --text "Updated durable engineering note..."
100
123
  threadnote archive viking://user/example/memories/handoffs/active/example/old-issue.md
101
124
  threadnote forget viking://user/example/memories/events/duplicate.md
package/docs/migration.md CHANGED
@@ -30,8 +30,8 @@ Developers should not need to run `recall`, `remember`, or `handoff` as a normal
30
30
 
31
31
  The intended workflow has three layers:
32
32
 
33
- - Agent-first: Codex, Claude, or another MCP-enabled agent calls OpenViking tools when the task calls for shared
34
- context.
33
+ - Agent-first: Codex, Claude, Copilot, or another MCP-enabled agent calls OpenViking tools when the task calls for
34
+ shared context.
35
35
  - Short CLI fallback: humans and scripts can run `threadnote recall`, `threadnote remember`, or
36
36
  `threadnote handoff` from any repo.
37
37
  - Checkout-local command: `npm run threadnote -- ...` is the bootstrap and debugging path before the short command shim
@@ -46,8 +46,8 @@ 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 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
49
+ to user-level Codex, Claude, Cursor, and Copilot instruction files. That guidance tells agents to recall context at task
50
+ start, store durable memories when explicitly asked or when a reusable workflow fact is learned, and create handoffs
51
51
  automatically before stopping meaningful work.
52
52
 
53
53
  ## Migration Steps
@@ -141,6 +141,16 @@ paths.
141
141
 
142
142
  Cursor installs by updating the global `~/.cursor/mcp.json` file.
143
143
 
144
+ For GitHub Copilot in VS Code:
145
+
146
+ ```bash
147
+ threadnote mcp-install copilot
148
+ threadnote mcp-install copilot --apply
149
+ ```
150
+
151
+ Copilot installs by updating the VS Code user-profile `mcp.json` file. If VS Code uses a custom profile path, set
152
+ `THREADNOTE_COPILOT_MCP_CONFIG` to that `mcp.json` path before running `mcp-install copilot`.
153
+
144
154
  Later, if the checkout that installed the MCP adapter is deleted or moved, repair it from any fresh checkout:
145
155
 
146
156
  ```bash
@@ -162,16 +172,21 @@ Preferred developer behavior is conversational:
162
172
 
163
173
  ```text
164
174
  Recall anything relevant for this branch before you start.
165
- Remember this workflow note for future agents: ...
175
+ Remember this feature decision for future agents: ...
166
176
  Create a handoff now.
167
177
  ```
168
178
 
169
179
  Preferred agent behavior is automatic after `threadnote install` has updated the user-level instruction files:
170
180
 
171
- - On non-trivial task start, search OpenViking for recent handoffs and relevant repo guidance.
181
+ - On non-trivial task start, search OpenViking for recent handoffs, durable feature memories for the branch/topic, and
182
+ relevant repo guidance.
172
183
  - When the user says "remember", store the memory after checking that it contains no secret or customer data.
173
- - When continuing the same active issue, update the current-state memory with `remember --replace <uri>` or
174
- `handoff --replace <uri>` instead of creating another near-duplicate progress memory.
184
+ - When continuing the same active issue, keep two current-state records when useful: a durable feature memory for design,
185
+ decisions, interfaces, and gotchas, plus a handoff for status, tests, blockers, and next steps.
186
+ - When valuable feature knowledge changes, update the durable feature memory with the same project/topic or
187
+ `remember --replace <uri>` instead of burying that knowledge only in a handoff.
188
+ - When current status changes, update the active handoff with the same project/topic or `handoff --replace <uri>` instead
189
+ of creating another near-duplicate progress memory.
175
190
  - When recall surfaces clearly duplicate or stale memories, store one concise replacement memory and forget only the
176
191
  redundant originals.
177
192
  - Before pausing, switching agents, or finishing meaningful code changes, store a concise handoff with status, tests,
@@ -181,9 +196,10 @@ Manual CLI remains available for scripts and emergencies:
181
196
 
182
197
  ```bash
183
198
  threadnote recall --query "last handoff for this branch"
184
- threadnote remember --text "Durable engineering note..."
199
+ threadnote recall --query "durable feature knowledge for this branch"
200
+ threadnote remember --kind durable --project example --topic active-feature --text "Feature knowledge..."
185
201
  threadnote remember --replace viking://user/example/memories/events/current.md --text "Updated durable engineering note..."
186
- threadnote handoff --task "short task summary" --tests "checks run" --next-step "what the next agent should do"
202
+ threadnote handoff --project example --topic active-feature --task "short task summary" --tests "checks run" --next-step "what the next agent should do"
187
203
  ```
188
204
 
189
205
  ## Repo Paths
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, Claude, or Cursor can store and recall a shared handoff after MCP setup.
21
+ - Codex, Claude, Cursor, or Copilot 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,8 @@
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`, `~/.claude/.credentials.json`, or Cursor account/session files.
9
+ - Local auth files such as `~/.codex/auth.json`, `~/.claude/.credentials.json`, Cursor account/session files, or VS Code
10
+ Copilot account/session files.
10
11
  - Local settings files that may contain secrets unless they go through redaction.
11
12
 
12
13
  ## Built-In Controls
@@ -14,9 +15,9 @@
14
15
  - `.threadnoteignore` excludes common secret and build-output paths.
15
16
  - `.mcp.json`, `config.toml`, and settings JSON are redacted before import.
16
17
  - Files are skipped if common secret patterns remain after redaction.
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.
18
+ - `mcp-install` requires `--apply` before it changes Codex, Claude, Cursor, or Copilot config.
19
+ - `install` updates user-level Codex, Claude, Cursor, and Copilot instruction files through managed Threadnote content.
20
+ Existing personal instructions outside managed blocks are preserved.
20
21
  - `uninstall` preserves local memories by default. `--erase-memories` is required before deleting `THREADNOTE_HOME`.
21
22
  - Config files created under `THREADNOTE_HOME` are written with user-only permissions.
22
23
 
@@ -147,7 +147,7 @@ threadnote repair
147
147
  ```
148
148
 
149
149
  `repair` reinstalls the `threadnote` shim, repairs generated config files, starts OpenViking if needed, and rewrites
150
- Codex/Claude/Cursor MCP configs to point at the current checkout.
150
+ Codex/Claude/Cursor/Copilot MCP configs to point at the current checkout.
151
151
 
152
152
  ## MCP Install Is Only Printing Commands
153
153
 
@@ -166,6 +166,16 @@ threadnote mcp-install cursor --apply
166
166
  This updates the global `~/.cursor/mcp.json` file. Restart Cursor or open a fresh agent session after changing MCP
167
167
  config.
168
168
 
169
+ For GitHub Copilot in VS Code:
170
+
171
+ ```bash
172
+ threadnote mcp-install copilot --apply
173
+ ```
174
+
175
+ This updates the VS Code user-profile `mcp.json` file. Restart VS Code or run `MCP: List Servers` from the Command
176
+ Palette after changing MCP config. If VS Code uses a custom profile path, set `THREADNOTE_COPILOT_MCP_CONFIG` to that
177
+ `mcp.json` path before running the command.
178
+
169
179
  ## Cursor MCP Tool Says Query Is Missing
170
180
 
171
181
  If Cursor shows an error like `expected string, received undefined` for Threadnote `search`, the MCP server started but
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",