threadnote 0.3.5 → 0.3.6
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 +26 -2
- package/dist/mcp_server.cjs +24 -14
- package/dist/threadnote.cjs +32 -14
- package/docs/agent-instructions.md +6 -0
- package/docs/migration.md +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,28 @@ Run `threadnote seed-skills` to make local `SKILL.md` guidance discoverable thro
|
|
|
29
29
|
Agents are instructed to compact them into one concise replacement memory and forget only the clearly redundant originals,
|
|
30
30
|
keeping future recall sharper without losing useful detail.
|
|
31
31
|
|
|
32
|
+
**Still working on the same issue?**
|
|
33
|
+
Use `threadnote remember --replace <uri>` or `threadnote handoff --replace <uri>` to keep one current-state memory fresh
|
|
34
|
+
instead of accumulating near-duplicate progress notes.
|
|
35
|
+
|
|
36
|
+
## Why Not Just CLAUDE.md Or AGENTS.md?
|
|
37
|
+
|
|
38
|
+
Use them. Threadnote is not a replacement for checked-in instructions.
|
|
39
|
+
|
|
40
|
+
`CLAUDE.md`, `AGENTS.md`, Cursor rules, and repo docs are the right place for stable, canonical guidance: coding
|
|
41
|
+
standards, test commands, review rules, architecture notes, and anything the whole team should version and discuss.
|
|
42
|
+
|
|
43
|
+
They are less ideal for living context: what happened in the last agent session, which branch was halfway through a
|
|
44
|
+
refactor, what an on-call investigation concluded, which workaround was verified on one machine, or which duplicate
|
|
45
|
+
memories should be compacted. Putting that history into instruction files makes them noisy, stale, and expensive to load
|
|
46
|
+
into every context window.
|
|
47
|
+
|
|
48
|
+
Threadnote keeps that moving layer local, searchable, and shared across agents. Agents recall only the relevant memories,
|
|
49
|
+
handoffs, and skill/resource pointers when they need them, while the source files stay authoritative for project rules.
|
|
50
|
+
|
|
51
|
+
The split is simple: put durable repo policy in `CLAUDE.md`/`AGENTS.md`; put task history, handoffs, personal workflow
|
|
52
|
+
facts, and local cross-agent memory in Threadnote.
|
|
53
|
+
|
|
32
54
|
## Safety Model
|
|
33
55
|
|
|
34
56
|
- Machine writes stay **locally** under `THREADNOTE_HOME`, which defaults to `~/.openviking`.
|
|
@@ -208,14 +230,16 @@ This is it! Start working with your agents as usual. The agent will automaticall
|
|
|
208
230
|
- `seed-skills`: imports global and repo-local `SKILL.md` files as a searchable resource catalog so agents can discover
|
|
209
231
|
reusable workflow guidance. Use `seed-skills --native` only after configuring a working VLM provider.
|
|
210
232
|
- `mcp-install codex|claude|cursor`: installs or prints OpenViking MCP configuration for Codex, Claude, or Cursor.
|
|
211
|
-
- `remember`: stores a durable memory.
|
|
233
|
+
- `remember`: stores a durable memory. Use `--replace <uri>` to store an updated memory and remove a superseded one
|
|
234
|
+
after the new memory succeeds.
|
|
212
235
|
- `migrate-memories`: migrates legacy session-only `MEMORY` and `HANDOFF` records into durable memory files. Run
|
|
213
236
|
`migrate-memories --dry-run` first; use `--all-accounts` when importing from older local OpenViking accounts.
|
|
214
237
|
- `recall`: searches shared OpenViking context. It infers repo or skill scope from queries like
|
|
215
238
|
`skills for api service`; use `--uri` or `--no-infer-scope` to override.
|
|
216
239
|
- `read`: reads a `viking://` URI returned by `recall` or `list`.
|
|
217
240
|
- `list` / `ls`: lists a `viking://` directory.
|
|
218
|
-
- `handoff`: stores current git state and next-step notes as a durable handoff.
|
|
241
|
+
- `handoff`: stores current git state and next-step notes as a durable handoff. Use `--replace <uri>` to update the
|
|
242
|
+
current handoff for the same active issue.
|
|
219
243
|
- `forget`: removes a `viking://` URI.
|
|
220
244
|
- `export-pack` / `import-pack`: moves local context through `.ovpack` files.
|
|
221
245
|
|
package/dist/mcp_server.cjs
CHANGED
|
@@ -31095,6 +31095,7 @@ async function main() {
|
|
|
31095
31095
|
"Prefer `recall_context` to find candidate viking:// URIs, then `read_context` files or `list_context` directories.",
|
|
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
|
+
"When updating the same active issue, pass replaceUri to remember_context so the superseded memory is forgotten after the replacement is stored.",
|
|
31098
31099
|
"Do not store secrets, customer data, raw production logs, or credentials."
|
|
31099
31100
|
].join("\n")
|
|
31100
31101
|
}
|
|
@@ -31379,32 +31380,36 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
31379
31380
|
server.registerTool(
|
|
31380
31381
|
name,
|
|
31381
31382
|
{
|
|
31382
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
31383
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
31383
31384
|
description: `${description} Never store secrets, credentials, customer data, or raw logs.`,
|
|
31384
31385
|
inputSchema: {
|
|
31386
|
+
replaceUri: external_exports.string().optional().describe("Optional viking:// memory URI to forget after the new memory is safely stored"),
|
|
31385
31387
|
text: external_exports.string().optional().describe("Required memory text to store"),
|
|
31386
31388
|
sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, codex, or claude")
|
|
31387
31389
|
}
|
|
31388
31390
|
},
|
|
31389
|
-
async ({ sourceAgentClient, text }) => {
|
|
31391
|
+
async ({ replaceUri, sourceAgentClient, text }) => {
|
|
31390
31392
|
const checkedText = requiredText(text, name, "text", { text: "Durable engineering note..." });
|
|
31391
31393
|
if (!checkedText.ok) {
|
|
31392
31394
|
return checkedText.error;
|
|
31393
31395
|
}
|
|
31394
|
-
|
|
31395
|
-
|
|
31396
|
-
|
|
31397
|
-
|
|
31398
|
-
|
|
31399
|
-
|
|
31400
|
-
|
|
31401
|
-
|
|
31402
|
-
|
|
31403
|
-
)
|
|
31396
|
+
const checkedReplaceUri = optionalVikingUri(replaceUri, name);
|
|
31397
|
+
if (!checkedReplaceUri.ok) {
|
|
31398
|
+
return checkedReplaceUri.error;
|
|
31399
|
+
}
|
|
31400
|
+
const header = [
|
|
31401
|
+
"MEMORY",
|
|
31402
|
+
`source_agent_client: ${sourceAgentClient ?? "mcp"}`,
|
|
31403
|
+
`timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
31404
|
+
];
|
|
31405
|
+
if (checkedReplaceUri.value) {
|
|
31406
|
+
header.push(`supersedes: ${checkedReplaceUri.value}`);
|
|
31407
|
+
}
|
|
31408
|
+
return writeDurableMemory(config2, [...header, "", checkedText.value].join("\n"), checkedReplaceUri.value);
|
|
31404
31409
|
}
|
|
31405
31410
|
);
|
|
31406
31411
|
}
|
|
31407
|
-
async function writeDurableMemory(config2, memory) {
|
|
31412
|
+
async function writeDurableMemory(config2, memory, replaceUri) {
|
|
31408
31413
|
try {
|
|
31409
31414
|
const ov = await requiredOpenVikingCli();
|
|
31410
31415
|
const directoryUri = durableMemoryDirectoryUri(config2);
|
|
@@ -31427,8 +31432,13 @@ async function writeDurableMemory(config2, memory) {
|
|
|
31427
31432
|
memoryUri,
|
|
31428
31433
|
withIdentity(config2, ["write", memoryUri, "--content", memory, "--mode", "create", "--wait", "--timeout", "120"])
|
|
31429
31434
|
);
|
|
31435
|
+
const messages = [`Stored durable memory: ${memoryUri}`];
|
|
31436
|
+
if (replaceUri) {
|
|
31437
|
+
await runCommand(ov, withIdentity(config2, ["rm", replaceUri]));
|
|
31438
|
+
messages.push(`Forgot replaced memory: ${replaceUri}`);
|
|
31439
|
+
}
|
|
31430
31440
|
const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
31431
|
-
return { content: [{ type: "text", text: [
|
|
31441
|
+
return { content: [{ type: "text", text: [...messages, text].filter(Boolean).join("\n") }] };
|
|
31432
31442
|
} catch (err) {
|
|
31433
31443
|
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
31434
31444
|
}
|
package/dist/threadnote.cjs
CHANGED
|
@@ -7038,14 +7038,16 @@ async function runRemember(config, options) {
|
|
|
7038
7038
|
if (!text.trim()) {
|
|
7039
7039
|
throw new Error("Provide memory text with --text or --stdin.");
|
|
7040
7040
|
}
|
|
7041
|
-
const
|
|
7041
|
+
const header = [
|
|
7042
7042
|
"MEMORY",
|
|
7043
7043
|
`source_agent_client: ${options.sourceAgentClient ?? "codex"}`,
|
|
7044
|
-
`timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7044
|
+
`timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
7045
|
+
];
|
|
7046
|
+
if (options.replace) {
|
|
7047
|
+
header.push(`supersedes: ${options.replace}`);
|
|
7048
|
+
}
|
|
7049
|
+
const memory = [...header, "", text.trim()].join("\n");
|
|
7050
|
+
await storeMemory(config, memory, { dryRun: options.dryRun === true, replaceUri: options.replace });
|
|
7049
7051
|
}
|
|
7050
7052
|
async function runMigrateMemories(config, options) {
|
|
7051
7053
|
const dryRun = options.dryRun === true;
|
|
@@ -7160,7 +7162,7 @@ async function runList(config, uri, options) {
|
|
|
7160
7162
|
}
|
|
7161
7163
|
async function runHandoff(config, options) {
|
|
7162
7164
|
const handoff = await buildHandoff(options);
|
|
7163
|
-
await storeMemory(config, handoff, options.dryRun === true);
|
|
7165
|
+
await storeMemory(config, handoff, { dryRun: options.dryRun === true, replaceUri: options.replace });
|
|
7164
7166
|
}
|
|
7165
7167
|
async function runForget(config, uri, options) {
|
|
7166
7168
|
assertVikingUri(uri);
|
|
@@ -7231,11 +7233,14 @@ async function printExactMemoryMatches(config, ov, query, dryRun) {
|
|
|
7231
7233
|
console.log("\nExact durable memory matches:");
|
|
7232
7234
|
console.log(outputs.join("\n\n"));
|
|
7233
7235
|
}
|
|
7234
|
-
async function storeMemory(config, memory,
|
|
7235
|
-
|
|
7236
|
+
async function storeMemory(config, memory, options) {
|
|
7237
|
+
if (options.replaceUri) {
|
|
7238
|
+
assertVikingUri(options.replaceUri);
|
|
7239
|
+
}
|
|
7240
|
+
const ov = await openVikingCliForMode(options.dryRun);
|
|
7236
7241
|
const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
|
|
7237
7242
|
const memoryUri = durableMemoryUri(config, memory);
|
|
7238
|
-
if (dryRun) {
|
|
7243
|
+
if (options.dryRun) {
|
|
7239
7244
|
console.log(memory);
|
|
7240
7245
|
console.log("\nWould run:");
|
|
7241
7246
|
console.log(
|
|
@@ -7254,6 +7259,9 @@ async function storeMemory(config, memory, dryRun) {
|
|
|
7254
7259
|
])
|
|
7255
7260
|
)
|
|
7256
7261
|
);
|
|
7262
|
+
if (options.replaceUri) {
|
|
7263
|
+
console.log(formatShellCommand(ov, withIdentity(config, ["rm", options.replaceUri])));
|
|
7264
|
+
}
|
|
7257
7265
|
return;
|
|
7258
7266
|
}
|
|
7259
7267
|
await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
|
|
@@ -7261,6 +7269,10 @@ async function storeMemory(config, memory, dryRun) {
|
|
|
7261
7269
|
await ensureDurableMemoryDirectory(ov, config);
|
|
7262
7270
|
await writeDurableMemoryFile(ov, config, memoryUri, memoryPath);
|
|
7263
7271
|
console.log(`Stored durable memory: ${memoryUri}`);
|
|
7272
|
+
if (options.replaceUri) {
|
|
7273
|
+
await maybeRun(false, ov, withIdentity(config, ["rm", options.replaceUri]));
|
|
7274
|
+
console.log(`Forgot replaced memory: ${options.replaceUri}`);
|
|
7275
|
+
}
|
|
7264
7276
|
}
|
|
7265
7277
|
async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath) {
|
|
7266
7278
|
const args = withIdentity(config, [
|
|
@@ -7501,13 +7513,19 @@ async function buildHandoff(options) {
|
|
|
7501
7513
|
const status = await gitValue(["status", "--short"], repoRoot) ?? "";
|
|
7502
7514
|
const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
|
|
7503
7515
|
const touchedFiles = await gitTouchedFiles(repoRoot);
|
|
7504
|
-
|
|
7516
|
+
const header = [
|
|
7505
7517
|
"HANDOFF",
|
|
7506
7518
|
`repo: ${(0, import_node_path4.basename)(repoRoot)}`,
|
|
7507
7519
|
`repo_path: ${repoRoot}`,
|
|
7508
7520
|
`branch: ${branch || "unknown"}`,
|
|
7509
7521
|
`source_agent_client: ${options.sourceAgentClient ?? "codex"}`,
|
|
7510
|
-
`timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
7522
|
+
`timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
7523
|
+
];
|
|
7524
|
+
if (options.replace) {
|
|
7525
|
+
header.push(`supersedes: ${options.replace}`);
|
|
7526
|
+
}
|
|
7527
|
+
return [
|
|
7528
|
+
...header,
|
|
7511
7529
|
`task: ${options.task ?? "unspecified"}`,
|
|
7512
7530
|
"",
|
|
7513
7531
|
"files_touched:",
|
|
@@ -9019,7 +9037,7 @@ async function main() {
|
|
|
9019
9037
|
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) => {
|
|
9020
9038
|
await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
|
|
9021
9039
|
});
|
|
9022
|
-
program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").action(async (options) => {
|
|
9040
|
+
program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").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("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").action(async (options) => {
|
|
9023
9041
|
await runRemember(getRuntimeConfig(program2), options);
|
|
9024
9042
|
});
|
|
9025
9043
|
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(
|
|
@@ -9039,7 +9057,7 @@ async function main() {
|
|
|
9039
9057
|
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) => {
|
|
9040
9058
|
await runList(getRuntimeConfig(program2), uri, options);
|
|
9041
9059
|
});
|
|
9042
|
-
program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").action(async (options) => {
|
|
9060
|
+
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("--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").action(async (options) => {
|
|
9043
9061
|
await runHandoff(getRuntimeConfig(program2), options);
|
|
9044
9062
|
});
|
|
9045
9063
|
program2.command("forget").description("Remove a viking:// URI from local OpenViking context").argument("<uri>", "viking:// URI to remove").option("--dry-run", "Print ov command without deleting").action(async (uri, options) => {
|
|
@@ -38,6 +38,11 @@ canonical docs. Prefer updating checked-in docs for canonical repo rules.
|
|
|
38
38
|
|
|
39
39
|
## Memory Compaction
|
|
40
40
|
|
|
41
|
+
When working on the same active issue, prefer keeping one current-state memory updated instead of creating many small
|
|
42
|
+
progress memories. If an existing memory is clearly the current state for the issue, store the updated version with
|
|
43
|
+
`remember_context({"text":"...","replaceUri":"<uri>"})`, `threadnote remember --replace <uri>`, or
|
|
44
|
+
`threadnote handoff --replace <uri>` so the old memory is removed only after the replacement is stored.
|
|
45
|
+
|
|
41
46
|
When recall/read surfaces several memories that describe the same durable fact, incident, branch, or handoff, compact
|
|
42
47
|
them when it is safe:
|
|
43
48
|
|
|
@@ -72,6 +77,7 @@ threadnote recall --query "last handoff for this branch"
|
|
|
72
77
|
threadnote read viking://agent/threadnote/memories/.abstract.md
|
|
73
78
|
threadnote list viking://agent/threadnote/memories --all --recursive
|
|
74
79
|
threadnote remember --text "Durable engineering note..."
|
|
80
|
+
threadnote remember --replace viking://user/example/memories/events/current.md --text "Updated durable engineering note..."
|
|
75
81
|
threadnote forget viking://user/example/memories/events/duplicate.md
|
|
76
82
|
threadnote handoff --task "short task summary" --tests "checks run" --next-step "what to do next"
|
|
77
83
|
```
|
package/docs/migration.md
CHANGED
|
@@ -170,6 +170,8 @@ Preferred agent behavior is automatic after `threadnote install` has updated the
|
|
|
170
170
|
|
|
171
171
|
- On non-trivial task start, search OpenViking for recent handoffs and relevant repo guidance.
|
|
172
172
|
- 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.
|
|
173
175
|
- When recall surfaces clearly duplicate or stale memories, store one concise replacement memory and forget only the
|
|
174
176
|
redundant originals.
|
|
175
177
|
- Before pausing, switching agents, or finishing meaningful code changes, store a concise handoff with status, tests,
|
|
@@ -180,6 +182,7 @@ Manual CLI remains available for scripts and emergencies:
|
|
|
180
182
|
```bash
|
|
181
183
|
threadnote recall --query "last handoff for this branch"
|
|
182
184
|
threadnote remember --text "Durable engineering note..."
|
|
185
|
+
threadnote remember --replace viking://user/example/memories/events/current.md --text "Updated durable engineering note..."
|
|
183
186
|
threadnote handoff --task "short task summary" --tests "checks run" --next-step "what the next agent should do"
|
|
184
187
|
```
|
|
185
188
|
|