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 +39 -14
- package/dist/mcp_server.cjs +4 -2
- package/dist/threadnote.cjs +225 -23
- package/docs/agent-instructions.md +25 -2
- package/docs/migration.md +26 -10
- package/docs/rollout.md +1 -1
- package/docs/security.md +5 -4
- package/docs/troubleshooting.md +11 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
76
|
-
handoffs, and skill/resource pointers when they need them, while the source files stay authoritative
|
|
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,
|
|
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
|
|
91
|
-
`~/.cursor/rules/threadnote.md` without replacing existing
|
|
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
|
|
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,
|
|
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`,
|
|
303
|
-
any repo or working
|
|
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
|
|
package/dist/mcp_server.cjs
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/threadnote.cjs
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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 !==
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
174
|
-
|
|
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
|
|
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
|
|
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`,
|
|
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
|
|
18
|
-
- `install` updates user-level Codex, Claude, and
|
|
19
|
-
personal instructions outside
|
|
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
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|