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 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
 
@@ -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: false },
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
- return writeDurableMemory(
31395
- config2,
31396
- [
31397
- "MEMORY",
31398
- `source_agent_client: ${sourceAgentClient ?? "mcp"}`,
31399
- `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`,
31400
- "",
31401
- checkedText.value
31402
- ].join("\n")
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: [`Stored durable memory: ${memoryUri}`, text].filter(Boolean).join("\n") }] };
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
  }
@@ -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 memory = [
7041
+ const header = [
7042
7042
  "MEMORY",
7043
7043
  `source_agent_client: ${options.sourceAgentClient ?? "codex"}`,
7044
- `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`,
7045
- "",
7046
- text.trim()
7047
- ].join("\n");
7048
- await storeMemory(config, memory, options.dryRun === true);
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, dryRun) {
7235
- const ov = await openVikingCliForMode(dryRun);
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
- return [
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",