threadnote 0.3.1 → 0.3.3

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
@@ -68,6 +68,27 @@ threadnote start
68
68
  threadnote doctor --dry-run
69
69
  ```
70
70
 
71
+ ### Update
72
+
73
+ Threadnote occasionally checks npm for a newer published version when you run human-facing commands such as `doctor`,
74
+ `start`, `install`, or `repair`. The check is cached under `THREADNOTE_HOME` and never runs in CI or when
75
+ `THREADNOTE_NO_UPDATE_CHECK` or `NO_UPDATE_NOTIFIER` is set.
76
+
77
+ Update the package and refresh local shims, user instructions, and MCP config with one command:
78
+
79
+ ```bash
80
+ threadnote update
81
+ ```
82
+
83
+ Check without changing anything:
84
+
85
+ ```bash
86
+ threadnote update --check
87
+ threadnote update --dry-run
88
+ ```
89
+
90
+ After updating, restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.
91
+
71
92
  ### MCP
72
93
 
73
94
  Make the agents you use aware of Threadnote. Use only the MCP install lines for agents you actually use. Open a fresh
@@ -152,6 +173,8 @@ This is it! Start working with your agents as usual. The agent will automaticall
152
173
  - `doctor`: checks prerequisites, the generated command shim, manifest shape, templates, and local OpenViking health.
153
174
  - `install`: installs `openviking[local-embed]==0.3.12` if missing, creates `~/.openviking` config files if absent,
154
175
  writes the command shim, and upserts user-level agent instructions.
176
+ - `update`: updates the published Threadnote package, then runs `repair` so shims and MCP config point at the new
177
+ version.
155
178
  - `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude/Cursor MCP configs from the
156
179
  current checkout.
157
180
  - `start`: starts `openviking-server` on `127.0.0.1:1933`.
@@ -164,6 +187,8 @@ This is it! Start working with your agents as usual. The agent will automaticall
164
187
  `seed-skills --native` only after configuring a working VLM provider.
165
188
  - `mcp-install codex|claude|cursor`: installs or prints OpenViking MCP configuration for Codex, Claude, or Cursor.
166
189
  - `remember`: stores a durable memory.
190
+ - `migrate-memories`: migrates legacy session-only `MEMORY` and `HANDOFF` records into durable memory files. Run
191
+ `migrate-memories --dry-run` first; use `--all-accounts` when importing from older local OpenViking accounts.
167
192
  - `recall`: searches shared OpenViking context. It infers repo or skill scope from queries like
168
193
  `skills for api service`; use `--uri` or `--no-infer-scope` to override.
169
194
  - `read`: reads a `viking://` URI returned by `recall` or `list`.
@@ -215,6 +240,8 @@ Environment variables:
215
240
  - `THREADNOTE_USER`: OpenViking user value, default local username.
216
241
  - `THREADNOTE_AGENT_ID`: shared agent identity, default `threadnote`.
217
242
  - `THREADNOTE_OPENVIKING_VERSION`: package version to install, default `0.3.12`.
243
+ - `THREADNOTE_NPM_REGISTRY`: npm registry used by the installer and updater, default `https://registry.npmjs.org/`.
244
+ - `THREADNOTE_NO_UPDATE_CHECK`: disables opportunistic update notifications.
218
245
  - `THREADNOTE_BIN_DIR`: directory for the `threadnote` shim, default `~/.local/bin`.
219
246
  - `THREADNOTE_HOST`: local bind host, default `127.0.0.1`.
220
247
  - `THREADNOTE_PORT`: local bind port, default `1933`.
@@ -30961,6 +30961,7 @@ var DEFAULT_AGENT_ID = "threadnote";
30961
30961
 
30962
30962
  // src/utils.ts
30963
30963
  var import_node_child_process = require("node:child_process");
30964
+ var import_node_crypto = require("node:crypto");
30964
30965
  function redactText(content) {
30965
30966
  return content.replace(
30966
30967
  /([A-Za-z0-9_.-]*(?:token|secret|password|api[_-]?key|authorization)[A-Za-z0-9_.-]*\s*[:=]\s*)("[^"]+"|'[^']+'|Bearer\s+[^'"\s]+|\S+)/gi,
@@ -31008,6 +31009,66 @@ async function runCommand(executable, args, options = {}) {
31008
31009
  });
31009
31010
  });
31010
31011
  }
31012
+ async function sleep(ms) {
31013
+ return new Promise((resolvePromise) => {
31014
+ setTimeout(resolvePromise, ms);
31015
+ });
31016
+ }
31017
+ function sha256(content) {
31018
+ return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
31019
+ }
31020
+ function exactRecallTerms(query) {
31021
+ const stopWords = /* @__PURE__ */ new Set([
31022
+ "about",
31023
+ "after",
31024
+ "agent",
31025
+ "anything",
31026
+ "branch",
31027
+ "case",
31028
+ "current",
31029
+ "find",
31030
+ "handoff",
31031
+ "issue",
31032
+ "issues",
31033
+ "latest",
31034
+ "memory",
31035
+ "memories",
31036
+ "recall",
31037
+ "related",
31038
+ "search",
31039
+ "the",
31040
+ "this",
31041
+ "with"
31042
+ ]);
31043
+ const seen = /* @__PURE__ */ new Set();
31044
+ const terms = [];
31045
+ for (const match of query.matchAll(/[A-Za-z0-9_.-]{4,}/g)) {
31046
+ const term = match[0];
31047
+ const normalized = term.toLowerCase();
31048
+ if (stopWords.has(normalized) || seen.has(normalized)) {
31049
+ continue;
31050
+ }
31051
+ seen.add(normalized);
31052
+ terms.push(term);
31053
+ }
31054
+ return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
31055
+ }
31056
+ function grepOutputHasMatches(output) {
31057
+ return !output.includes("matches []") && !output.includes('"matches":[]') && !output.includes("match_count 0");
31058
+ }
31059
+ function exactRecallTermScore(term) {
31060
+ let score = term.length;
31061
+ if (/[A-Z]/.test(term)) {
31062
+ score += 8;
31063
+ }
31064
+ if (/[0-9_.-]/.test(term)) {
31065
+ score += 6;
31066
+ }
31067
+ return score;
31068
+ }
31069
+ function safeTimestamp() {
31070
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
31071
+ }
31011
31072
  function formatShellCommand(executable, args) {
31012
31073
  return redactText([executable, ...args].map(shellQuote).join(" "));
31013
31074
  }
@@ -31210,7 +31271,7 @@ function registerSearchTool(server, config2, name, description) {
31210
31271
  if (!checkedUri.ok) {
31211
31272
  return checkedUri.error;
31212
31273
  }
31213
- return runOpenVikingTool(config2, [
31274
+ return runRecallTool(config2, [
31214
31275
  "search",
31215
31276
  checkedQuery.value,
31216
31277
  ...checkedUri.value ? ["--uri", checkedUri.value] : [],
@@ -31219,6 +31280,52 @@ function registerSearchTool(server, config2, name, description) {
31219
31280
  }
31220
31281
  );
31221
31282
  }
31283
+ async function runRecallTool(config2, args) {
31284
+ const semanticResult = await runOpenVikingTool(config2, args);
31285
+ if (semanticResult.isError === true) {
31286
+ return semanticResult;
31287
+ }
31288
+ const query = args[1];
31289
+ const exactMatches = typeof query === "string" ? await exactMemoryMatchesText(config2, query) : void 0;
31290
+ if (!exactMatches) {
31291
+ return semanticResult;
31292
+ }
31293
+ const [firstContent] = semanticResult.content;
31294
+ if (firstContent?.type !== "text") {
31295
+ return semanticResult;
31296
+ }
31297
+ return {
31298
+ ...semanticResult,
31299
+ content: [{ type: "text", text: `${firstContent.text}
31300
+
31301
+ Exact durable memory matches:
31302
+ ${exactMatches}` }]
31303
+ };
31304
+ }
31305
+ async function exactMemoryMatchesText(config2, query) {
31306
+ const terms = exactRecallTerms(query);
31307
+ if (terms.length === 0) {
31308
+ return void 0;
31309
+ }
31310
+ const ov = await requiredOpenVikingCli();
31311
+ const scopes = [
31312
+ `viking://user/${uriSegment(config2.user)}/memories`,
31313
+ `viking://agent/${uriSegment(config2.agentId)}/memories`
31314
+ ];
31315
+ const outputs = [];
31316
+ for (const term of terms) {
31317
+ for (const scope of scopes) {
31318
+ const result = await runCommand(ov, withIdentity(config2, ["grep", term, "--uri", scope, "--node-limit", "5"]), {
31319
+ allowFailure: true
31320
+ });
31321
+ const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
31322
+ if (result.exitCode === 0 && grepOutputHasMatches(output)) {
31323
+ outputs.push(output);
31324
+ }
31325
+ }
31326
+ }
31327
+ return outputs.length > 0 ? outputs.join("\n\n") : void 0;
31328
+ }
31222
31329
  function registerReadTool(server, config2, name, description) {
31223
31330
  server.registerTool(
31224
31331
  name,
@@ -31284,8 +31391,8 @@ function registerStoreTool(server, config2, name, description) {
31284
31391
  if (!checkedText.ok) {
31285
31392
  return checkedText.error;
31286
31393
  }
31287
- return runOpenVikingTool(config2, [
31288
- "add-memory",
31394
+ return writeDurableMemory(
31395
+ config2,
31289
31396
  [
31290
31397
  "MEMORY",
31291
31398
  `source_agent_client: ${sourceAgentClient ?? "mcp"}`,
@@ -31293,10 +31400,74 @@ function registerStoreTool(server, config2, name, description) {
31293
31400
  "",
31294
31401
  checkedText.value
31295
31402
  ].join("\n")
31296
- ]);
31403
+ );
31297
31404
  }
31298
31405
  );
31299
31406
  }
31407
+ async function writeDurableMemory(config2, memory) {
31408
+ try {
31409
+ const ov = await requiredOpenVikingCli();
31410
+ const directoryUri = durableMemoryDirectoryUri(config2);
31411
+ const stat = await runCommand(ov, withIdentity(config2, ["stat", directoryUri]), { allowFailure: true });
31412
+ if (stat.exitCode !== 0) {
31413
+ await runCommand(
31414
+ ov,
31415
+ withIdentity(config2, [
31416
+ "mkdir",
31417
+ directoryUri,
31418
+ "--description",
31419
+ "Threadnote durable handoffs, memories, and cross-agent notes."
31420
+ ])
31421
+ );
31422
+ }
31423
+ const memoryUri = durableMemoryUri(config2, memory);
31424
+ const result = await runOpenVikingWriteWithRetry(
31425
+ ov,
31426
+ config2,
31427
+ memoryUri,
31428
+ withIdentity(config2, ["write", memoryUri, "--content", memory, "--mode", "create", "--wait", "--timeout", "120"])
31429
+ );
31430
+ 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") }] };
31432
+ } catch (err) {
31433
+ return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
31434
+ }
31435
+ }
31436
+ async function runOpenVikingWriteWithRetry(ov, config2, memoryUri, args) {
31437
+ for (let attempt = 0; attempt < 4; attempt += 1) {
31438
+ const result = await runCommand(ov, args, { allowFailure: true });
31439
+ if (result.exitCode === 0) {
31440
+ return result;
31441
+ }
31442
+ if (await vikingResourceExists(ov, config2, memoryUri)) {
31443
+ await runCommand(ov, withIdentity(config2, ["wait", "--timeout", "120"]), { allowFailure: true });
31444
+ return { exitCode: 0, stdout: "OpenViking accepted the memory and indexing wait completed.", stderr: "" };
31445
+ }
31446
+ if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
31447
+ throw new Error(`${ov} ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
31448
+ }
31449
+ await sleep(1e3 * (attempt + 1));
31450
+ }
31451
+ throw new Error(`${ov} ${args.join(" ")} failed.`);
31452
+ }
31453
+ async function vikingResourceExists(ov, config2, uri) {
31454
+ const stat = await runCommand(ov, withIdentity(config2, ["stat", uri]), { allowFailure: true });
31455
+ return stat.exitCode === 0;
31456
+ }
31457
+ function isResourceBusy(stderr, stdout) {
31458
+ return `${stderr}
31459
+ ${stdout}`.includes("resource is busy");
31460
+ }
31461
+ function durableMemoryUri(config2, memory) {
31462
+ return `${durableMemoryDirectoryUri(config2)}/threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
31463
+ }
31464
+ function durableMemoryDirectoryUri(config2) {
31465
+ return `viking://user/${uriSegment(config2.user)}/memories/events`;
31466
+ }
31467
+ function uriSegment(value) {
31468
+ const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
31469
+ return normalized.length > 0 ? normalized : "unknown";
31470
+ }
31300
31471
  function requiredText(value, toolName, fieldName, example) {
31301
31472
  const normalized = value?.trim();
31302
31473
  if (normalized) {
@@ -3897,6 +3897,55 @@ function parentVikingUri(uri) {
3897
3897
  function sha256(content) {
3898
3898
  return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
3899
3899
  }
3900
+ function exactRecallTerms(query) {
3901
+ const stopWords = /* @__PURE__ */ new Set([
3902
+ "about",
3903
+ "after",
3904
+ "agent",
3905
+ "anything",
3906
+ "branch",
3907
+ "case",
3908
+ "current",
3909
+ "find",
3910
+ "handoff",
3911
+ "issue",
3912
+ "issues",
3913
+ "latest",
3914
+ "memory",
3915
+ "memories",
3916
+ "recall",
3917
+ "related",
3918
+ "search",
3919
+ "the",
3920
+ "this",
3921
+ "with"
3922
+ ]);
3923
+ const seen = /* @__PURE__ */ new Set();
3924
+ const terms = [];
3925
+ for (const match of query.matchAll(/[A-Za-z0-9_.-]{4,}/g)) {
3926
+ const term = match[0];
3927
+ const normalized = term.toLowerCase();
3928
+ if (stopWords.has(normalized) || seen.has(normalized)) {
3929
+ continue;
3930
+ }
3931
+ seen.add(normalized);
3932
+ terms.push(term);
3933
+ }
3934
+ return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
3935
+ }
3936
+ function grepOutputHasMatches(output) {
3937
+ return !output.includes("matches []") && !output.includes('"matches":[]') && !output.includes("match_count 0");
3938
+ }
3939
+ function exactRecallTermScore(term) {
3940
+ let score = term.length;
3941
+ if (/[A-Z]/.test(term)) {
3942
+ score += 8;
3943
+ }
3944
+ if (/[0-9_.-]/.test(term)) {
3945
+ score += 6;
3946
+ }
3947
+ return score;
3948
+ }
3900
3949
  function safeTimestamp() {
3901
3950
  return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3902
3951
  }
@@ -6998,6 +7047,75 @@ async function runRemember(config, options) {
6998
7047
  ].join("\n");
6999
7048
  await storeMemory(config, memory, options.dryRun === true);
7000
7049
  }
7050
+ async function runMigrateMemories(config, options) {
7051
+ const dryRun = options.dryRun === true;
7052
+ const limit = options.limit ? parsePositiveInteger(options.limit, "migration limit") : void 0;
7053
+ const sourceAccounts = await legacySourceAccounts(config, options);
7054
+ if (sourceAccounts.length === 0) {
7055
+ console.log("No local OpenViking accounts found to scan.");
7056
+ return;
7057
+ }
7058
+ const candidates = await legacyMemoryCandidates(config, sourceAccounts);
7059
+ const existingHashes = await existingDurableMemoryHashes(config);
7060
+ const ov = await openVikingCliForMode(dryRun);
7061
+ const migrationPath = (0, import_node_path4.join)(config.agentContextHome, "legacy-memory-migration.txt");
7062
+ let duplicateCount = 0;
7063
+ let migratedCount = 0;
7064
+ let sensitiveCount = 0;
7065
+ if (!dryRun && candidates.length > 0) {
7066
+ await ensureDurableMemoryDirectory(ov, config);
7067
+ }
7068
+ try {
7069
+ for (const candidate of candidates) {
7070
+ if (existingHashes.has(candidate.hash)) {
7071
+ duplicateCount += 1;
7072
+ continue;
7073
+ }
7074
+ if (existingHashes.has(candidate.comparableHash)) {
7075
+ duplicateCount += 1;
7076
+ continue;
7077
+ }
7078
+ const sensitiveReason = sensitiveMemoryReason(candidate.text);
7079
+ if (sensitiveReason) {
7080
+ sensitiveCount += 1;
7081
+ console.log(
7082
+ `SKIP ${legacySourceLabel(candidate)}: possible ${sensitiveReason}; inspect the source archive manually if needed.`
7083
+ );
7084
+ continue;
7085
+ }
7086
+ if (limit !== void 0 && migratedCount >= limit) {
7087
+ break;
7088
+ }
7089
+ const memoryUri = migratedDurableMemoryUri(config, candidate.hash);
7090
+ if (!dryRun && await vikingResourceExists(ov, config, memoryUri)) {
7091
+ duplicateCount += 1;
7092
+ existingHashes.add(candidate.hash);
7093
+ continue;
7094
+ }
7095
+ console.log(`${dryRun ? "Would migrate" : "Migrating"} ${legacySourceLabel(candidate)} -> ${memoryUri}`);
7096
+ if (!dryRun) {
7097
+ await (0, import_promises4.writeFile)(migrationPath, candidate.text, { encoding: "utf8", mode: 384 });
7098
+ await (0, import_promises4.chmod)(migrationPath, 384);
7099
+ await writeDurableMemoryFile(ov, config, memoryUri, migrationPath);
7100
+ existingHashes.add(candidate.hash);
7101
+ }
7102
+ migratedCount += 1;
7103
+ }
7104
+ } finally {
7105
+ if (!dryRun) {
7106
+ await (0, import_promises4.rm)(migrationPath, { force: true });
7107
+ }
7108
+ }
7109
+ console.log(
7110
+ [
7111
+ `Migration summary: ${migratedCount} ${dryRun ? "would be migrated" : "migrated"}`,
7112
+ `${duplicateCount} duplicate(s) skipped`,
7113
+ `${sensitiveCount} sensitive-looking item(s) skipped`,
7114
+ `${candidates.length} legacy Threadnote item(s) scanned`,
7115
+ `source account(s): ${sourceAccounts.join(", ")}`
7116
+ ].join("; ")
7117
+ );
7118
+ }
7001
7119
  async function runRecall(config, options) {
7002
7120
  const ov = await openVikingCliForMode(options.dryRun === true);
7003
7121
  const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, options.query));
@@ -7010,6 +7128,7 @@ async function runRecall(config, options) {
7010
7128
  args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
7011
7129
  }
7012
7130
  await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
7131
+ await printExactMemoryMatches(config, ov, options.query, options.dryRun === true);
7013
7132
  }
7014
7133
  async function runRead(config, uri, options) {
7015
7134
  assertVikingUri(uri);
@@ -7082,18 +7201,299 @@ async function inferProjectFromQuery(config, normalizedQuery) {
7082
7201
  return void 0;
7083
7202
  }
7084
7203
  }
7204
+ async function printExactMemoryMatches(config, ov, query, dryRun) {
7205
+ const terms = exactRecallTerms(query);
7206
+ if (terms.length === 0) {
7207
+ return;
7208
+ }
7209
+ const scopes = [
7210
+ `viking://user/${uriSegment(config.user)}/memories`,
7211
+ `viking://agent/${uriSegment(config.agentId)}/memories`
7212
+ ];
7213
+ const outputs = [];
7214
+ for (const term of terms) {
7215
+ for (const scope of scopes) {
7216
+ const args = withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5"]);
7217
+ if (dryRun) {
7218
+ outputs.push(formatShellCommand(ov, args));
7219
+ continue;
7220
+ }
7221
+ const result = await runCommand(ov, args, { allowFailure: true });
7222
+ const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
7223
+ if (result.exitCode === 0 && grepOutputHasMatches(output)) {
7224
+ outputs.push(output);
7225
+ }
7226
+ }
7227
+ }
7228
+ if (outputs.length === 0) {
7229
+ return;
7230
+ }
7231
+ console.log("\nExact durable memory matches:");
7232
+ console.log(outputs.join("\n\n"));
7233
+ }
7085
7234
  async function storeMemory(config, memory, dryRun) {
7086
7235
  const ov = await openVikingCliForMode(dryRun);
7087
7236
  const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
7237
+ const memoryUri = durableMemoryUri(config, memory);
7088
7238
  if (dryRun) {
7089
7239
  console.log(memory);
7090
7240
  console.log("\nWould run:");
7091
- console.log(formatShellCommand(ov, withIdentity(config, ["add-memory", memory])));
7241
+ console.log(
7242
+ formatShellCommand(
7243
+ ov,
7244
+ withIdentity(config, [
7245
+ "write",
7246
+ memoryUri,
7247
+ "--from-file",
7248
+ memoryPath,
7249
+ "--mode",
7250
+ "create",
7251
+ "--wait",
7252
+ "--timeout",
7253
+ "120"
7254
+ ])
7255
+ )
7256
+ );
7092
7257
  return;
7093
7258
  }
7094
7259
  await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
7095
7260
  await (0, import_promises4.chmod)(memoryPath, 384);
7096
- await maybeRun(false, ov, withIdentity(config, ["add-memory", memory]));
7261
+ await ensureDurableMemoryDirectory(ov, config);
7262
+ await writeDurableMemoryFile(ov, config, memoryUri, memoryPath);
7263
+ console.log(`Stored durable memory: ${memoryUri}`);
7264
+ }
7265
+ async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath) {
7266
+ const args = withIdentity(config, [
7267
+ "write",
7268
+ memoryUri,
7269
+ "--from-file",
7270
+ memoryPath,
7271
+ "--mode",
7272
+ "create",
7273
+ "--wait",
7274
+ "--timeout",
7275
+ "120"
7276
+ ]);
7277
+ for (let attempt = 0; attempt < 4; attempt += 1) {
7278
+ console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
7279
+ const result = await runCommand(ov, args, { allowFailure: true });
7280
+ if (result.exitCode === 0) {
7281
+ if (result.stdout.trim()) {
7282
+ console.log(result.stdout.trim());
7283
+ }
7284
+ if (result.stderr.trim()) {
7285
+ console.error(result.stderr.trim());
7286
+ }
7287
+ return;
7288
+ }
7289
+ if (await vikingResourceExists(ov, config, memoryUri)) {
7290
+ console.log("OpenViking accepted the memory but returned before the wait completed; waiting for indexing.");
7291
+ await waitForOpenVikingQueue(ov, config);
7292
+ return;
7293
+ }
7294
+ if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
7295
+ throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
7296
+ }
7297
+ await sleep(1e3 * (attempt + 1));
7298
+ }
7299
+ }
7300
+ async function waitForOpenVikingQueue(ov, config) {
7301
+ const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
7302
+ if (result.stdout.trim()) {
7303
+ console.log(result.stdout.trim());
7304
+ }
7305
+ if (result.stderr.trim()) {
7306
+ console.error(result.stderr.trim());
7307
+ }
7308
+ }
7309
+ async function vikingResourceExists(ov, config, uri) {
7310
+ const stat3 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
7311
+ return stat3.exitCode === 0;
7312
+ }
7313
+ async function ensureDurableMemoryDirectory(ov, config) {
7314
+ const directoryUri = durableMemoryDirectoryUri(config);
7315
+ const stat3 = await runCommand(ov, withIdentity(config, ["stat", directoryUri]), { allowFailure: true });
7316
+ if (stat3.exitCode === 0) {
7317
+ return;
7318
+ }
7319
+ await maybeRun(
7320
+ false,
7321
+ ov,
7322
+ withIdentity(config, [
7323
+ "mkdir",
7324
+ directoryUri,
7325
+ "--description",
7326
+ "Threadnote durable handoffs, memories, and cross-agent notes."
7327
+ ])
7328
+ );
7329
+ }
7330
+ function durableMemoryUri(config, memory) {
7331
+ return `${durableMemoryDirectoryUri(config)}/threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
7332
+ }
7333
+ function durableMemoryDirectoryUri(config) {
7334
+ return `viking://user/${uriSegment(config.user)}/memories/events`;
7335
+ }
7336
+ function migratedDurableMemoryUri(config, hash) {
7337
+ return `${durableMemoryDirectoryUri(config)}/threadnote-migrated-${hash.slice(0, 16)}.md`;
7338
+ }
7339
+ async function legacySourceAccounts(config, options) {
7340
+ const explicitAccounts = options.sourceAccount?.filter((account) => account.trim().length > 0) ?? [];
7341
+ if (explicitAccounts.length > 0) {
7342
+ return uniqueStrings(explicitAccounts);
7343
+ }
7344
+ if (options.allAccounts === true) {
7345
+ const accounts = await childDirectoryNames(localVikingDataRoot(config));
7346
+ return accounts.filter((account) => !account.startsWith("_"));
7347
+ }
7348
+ return [config.account];
7349
+ }
7350
+ async function legacyMemoryCandidates(config, sourceAccounts) {
7351
+ const candidates = [];
7352
+ for (const sourceAccount of sourceAccounts) {
7353
+ const sessionRoot = (0, import_node_path4.join)(localVikingDataRoot(config), sourceAccount, "session");
7354
+ for (const sourceSession of await childDirectoryNames(sessionRoot)) {
7355
+ const historyRoot = (0, import_node_path4.join)(sessionRoot, sourceSession, "history");
7356
+ for (const sourceArchive of await childDirectoryNames(historyRoot)) {
7357
+ if (!sourceArchive.startsWith("archive_")) {
7358
+ continue;
7359
+ }
7360
+ const sourcePath = (0, import_node_path4.join)(historyRoot, sourceArchive, "messages.jsonl");
7361
+ for (const text of await legacyMemoryTexts(sourcePath)) {
7362
+ candidates.push({
7363
+ comparableHash: sha256(comparableMemoryText(text)),
7364
+ hash: sha256(text),
7365
+ sourceAccount,
7366
+ sourceArchive,
7367
+ sourceSession,
7368
+ text
7369
+ });
7370
+ }
7371
+ }
7372
+ }
7373
+ }
7374
+ return candidates.sort((left, right) => legacySourceLabel(left).localeCompare(legacySourceLabel(right)));
7375
+ }
7376
+ async function legacyMemoryTexts(sourcePath) {
7377
+ const raw = await readTextIfExists(sourcePath);
7378
+ if (!raw) {
7379
+ return [];
7380
+ }
7381
+ const memories = [];
7382
+ for (const line of raw.split("\n")) {
7383
+ const trimmedLine = line.trim();
7384
+ if (!trimmedLine) {
7385
+ continue;
7386
+ }
7387
+ try {
7388
+ const parsed = JSON.parse(trimmedLine);
7389
+ const text = legacyMessageText(parsed)?.trim();
7390
+ if (text && isLegacyThreadnoteMemory(text)) {
7391
+ memories.push(text);
7392
+ }
7393
+ } catch (_err) {
7394
+ continue;
7395
+ }
7396
+ }
7397
+ return memories;
7398
+ }
7399
+ function legacyMessageText(value) {
7400
+ if (!isJsonObject(value)) {
7401
+ return void 0;
7402
+ }
7403
+ if (typeof value.content === "string") {
7404
+ return value.content;
7405
+ }
7406
+ if (!Array.isArray(value.parts)) {
7407
+ return void 0;
7408
+ }
7409
+ const parts = value.parts.map((part) => isJsonObject(part) && part.type === "text" && typeof part.text === "string" ? part.text : void 0).filter((text) => text !== void 0);
7410
+ return parts.length > 0 ? parts.join("\n") : void 0;
7411
+ }
7412
+ function isLegacyThreadnoteMemory(text) {
7413
+ return text.startsWith("MEMORY\n") || text.startsWith("HANDOFF\n");
7414
+ }
7415
+ async function existingDurableMemoryHashes(config) {
7416
+ const hashes = /* @__PURE__ */ new Set();
7417
+ await collectDurableMemoryHashes(localVikingDataRoot(config), hashes);
7418
+ return hashes;
7419
+ }
7420
+ async function collectDurableMemoryHashes(root, hashes) {
7421
+ let entries;
7422
+ try {
7423
+ entries = await (0, import_promises4.readdir)(root, { withFileTypes: true });
7424
+ } catch (_err) {
7425
+ return;
7426
+ }
7427
+ for (const entry of entries) {
7428
+ const path = (0, import_node_path4.join)(root, entry.name);
7429
+ if (entry.isDirectory()) {
7430
+ await collectDurableMemoryHashes(path, hashes);
7431
+ continue;
7432
+ }
7433
+ if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md") || !isDurableMemoryPath(path)) {
7434
+ continue;
7435
+ }
7436
+ const content = await readTextIfExists(path);
7437
+ if (content) {
7438
+ const trimmedContent = content.trim();
7439
+ hashes.add(sha256(trimmedContent));
7440
+ hashes.add(sha256(comparableMemoryText(trimmedContent)));
7441
+ }
7442
+ }
7443
+ }
7444
+ function isDurableMemoryPath(path) {
7445
+ return path.split(import_node_path4.sep).includes("memories");
7446
+ }
7447
+ async function childDirectoryNames(path) {
7448
+ let entries;
7449
+ try {
7450
+ entries = await (0, import_promises4.readdir)(path, { withFileTypes: true });
7451
+ } catch (_err) {
7452
+ return [];
7453
+ }
7454
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
7455
+ }
7456
+ async function readTextIfExists(path) {
7457
+ try {
7458
+ const pathStat = await (0, import_promises4.stat)(path);
7459
+ if (!pathStat.isFile()) {
7460
+ return void 0;
7461
+ }
7462
+ return await (0, import_promises4.readFile)(path, "utf8");
7463
+ } catch (_err) {
7464
+ return void 0;
7465
+ }
7466
+ }
7467
+ function sensitiveMemoryReason(text) {
7468
+ const patterns = [
7469
+ { name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
7470
+ { name: "API key", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
7471
+ { name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
7472
+ { name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
7473
+ { name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ }
7474
+ ];
7475
+ return patterns.find((pattern) => pattern.regex.test(text))?.name;
7476
+ }
7477
+ function comparableMemoryText(text) {
7478
+ const trimmed = text.trim();
7479
+ if (!trimmed.startsWith("MEMORY\n")) {
7480
+ return trimmed;
7481
+ }
7482
+ const separatorIndex = trimmed.indexOf("\n\n");
7483
+ return separatorIndex === -1 ? trimmed : trimmed.slice(separatorIndex + 2).trim();
7484
+ }
7485
+ function legacySourceLabel(candidate) {
7486
+ return `${candidate.sourceAccount}/${candidate.sourceSession}/${candidate.sourceArchive}`;
7487
+ }
7488
+ function localVikingDataRoot(config) {
7489
+ return (0, import_node_path4.join)(config.agentContextHome, "data", "viking");
7490
+ }
7491
+ function uniqueStrings(values) {
7492
+ return [...new Set(values)].sort();
7493
+ }
7494
+ function isResourceBusy(stderr, stdout) {
7495
+ return `${stderr}
7496
+ ${stdout}`.includes("resource is busy");
7097
7497
  }
7098
7498
  async function buildHandoff(options) {
7099
7499
  const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]) ?? getInvocationCwd();
@@ -8284,25 +8684,289 @@ function parsePackageManager(value) {
8284
8684
  throw new Error(`Invalid package manager: ${value}`);
8285
8685
  }
8286
8686
 
8687
+ // src/update.ts
8688
+ var import_node_fs4 = require("node:fs");
8689
+ var import_promises7 = require("node:fs/promises");
8690
+ var import_node_os5 = require("node:os");
8691
+ var import_node_path7 = require("node:path");
8692
+ var NPM_PACKAGE_NAME = "threadnote";
8693
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
8694
+ var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
8695
+ function parseUpdateRuntime(value) {
8696
+ if (value === "auto" || value === "npm" || value === "bun" || value === "deno") {
8697
+ return value;
8698
+ }
8699
+ throw new Error(`Invalid update runtime: ${value}. Expected auto, npm, bun, or deno.`);
8700
+ }
8701
+ async function maybeNotifyUpdate(config, options = {}) {
8702
+ if (isUpdateNotificationDisabled()) {
8703
+ return;
8704
+ }
8705
+ try {
8706
+ const info = await getUpdateInfo(config, {
8707
+ allowCacheWrite: options.dryRun !== true,
8708
+ preferFresh: false,
8709
+ registry: updateRegistry()
8710
+ });
8711
+ if (!info.isUpdateAvailable) {
8712
+ return;
8713
+ }
8714
+ console.log("");
8715
+ console.log(`Update available: threadnote ${info.currentVersion} -> ${info.latestVersion}`);
8716
+ console.log("Run: threadnote update");
8717
+ } catch (_err) {
8718
+ return;
8719
+ }
8720
+ }
8721
+ async function runUpdate(config, options) {
8722
+ const registry = normalizeRegistry(options.registry ?? updateRegistry());
8723
+ const info = await getUpdateInfo(config, {
8724
+ allowCacheWrite: options.dryRun !== true,
8725
+ preferFresh: true,
8726
+ registry
8727
+ });
8728
+ console.log(`Current version: ${info.currentVersion}`);
8729
+ console.log(`Latest version: ${info.latestVersion}`);
8730
+ console.log(`Registry: ${info.registry}`);
8731
+ if (options.check === true) {
8732
+ if (info.isUpdateAvailable) {
8733
+ console.log(`Update available. Run: threadnote update`);
8734
+ } else {
8735
+ console.log(
8736
+ compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
8737
+ );
8738
+ }
8739
+ return;
8740
+ }
8741
+ if (!info.isUpdateAvailable && options.force !== true) {
8742
+ console.log("Threadnote is up to date.");
8743
+ return;
8744
+ }
8745
+ const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
8746
+ const updateCommand = updatePackageCommand(runtime, registry);
8747
+ await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
8748
+ if (options.repair === false) {
8749
+ console.log("Skipping repair because --no-repair was provided.");
8750
+ return;
8751
+ }
8752
+ const threadnoteCommand = await installedThreadnoteCommand(runtime);
8753
+ await maybeRun(options.dryRun === true, threadnoteCommand, ["repair"]);
8754
+ console.log("Update complete. Restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.");
8755
+ }
8756
+ async function getUpdateInfo(config, options) {
8757
+ const currentVersion = await currentPackageVersion();
8758
+ const cached = options.preferFresh ? void 0 : await readFreshCache(config, options.registry);
8759
+ const latestVersion = cached?.latestVersion ?? await fetchLatestVersion(options.registry);
8760
+ if (!cached && options.allowCacheWrite) {
8761
+ await writeUpdateCache(config, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion, registry: options.registry });
8762
+ }
8763
+ return {
8764
+ currentVersion,
8765
+ isUpdateAvailable: compareVersions(currentVersion, latestVersion) < 0,
8766
+ latestVersion,
8767
+ registry: options.registry
8768
+ };
8769
+ }
8770
+ async function currentPackageVersion() {
8771
+ const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
8772
+ const parsed = JSON.parse(rawPackage);
8773
+ if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8774
+ throw new Error("Could not read current threadnote package version.");
8775
+ }
8776
+ return parsed.version;
8777
+ }
8778
+ async function fetchLatestVersion(registry) {
8779
+ const url = new URL(`${NPM_PACKAGE_NAME}/latest`, normalizeRegistry(registry));
8780
+ const controller = new AbortController();
8781
+ const timeout = setTimeout(() => {
8782
+ controller.abort();
8783
+ }, 2500);
8784
+ try {
8785
+ const response = await fetch(url, { headers: { accept: "application/json" }, signal: controller.signal });
8786
+ if (!response.ok) {
8787
+ throw new Error(`npm registry returned HTTP ${response.status}`);
8788
+ }
8789
+ const parsed = await response.json();
8790
+ if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8791
+ throw new Error("npm registry response did not include a version.");
8792
+ }
8793
+ return parsed.version;
8794
+ } catch (err) {
8795
+ throw new Error(`Could not check npm for updates: ${errorMessage(err)}`, { cause: err });
8796
+ } finally {
8797
+ clearTimeout(timeout);
8798
+ }
8799
+ }
8800
+ async function readFreshCache(config, registry) {
8801
+ const rawCache = await readFileIfExists(updateCachePath(config));
8802
+ if (!rawCache) {
8803
+ return void 0;
8804
+ }
8805
+ try {
8806
+ const parsed = JSON.parse(rawCache);
8807
+ if (!isJsonObject(parsed) || typeof parsed.checkedAt !== "string" || typeof parsed.latestVersion !== "string" || parsed.registry !== registry) {
8808
+ return void 0;
8809
+ }
8810
+ const checkedAt = Date.parse(parsed.checkedAt);
8811
+ if (!Number.isFinite(checkedAt) || Date.now() - checkedAt > UPDATE_CHECK_TTL_MS) {
8812
+ return void 0;
8813
+ }
8814
+ return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, registry };
8815
+ } catch (_err) {
8816
+ return void 0;
8817
+ }
8818
+ }
8819
+ async function writeUpdateCache(config, cache) {
8820
+ await ensureDirectory(config.agentContextHome, false);
8821
+ await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
8822
+ `, { encoding: "utf8", mode: 384 });
8823
+ }
8824
+ function updateCachePath(config) {
8825
+ return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
8826
+ }
8827
+ async function resolveUpdateRuntime(runtime) {
8828
+ if (runtime !== "auto") {
8829
+ await requireRuntime(runtime);
8830
+ return runtime;
8831
+ }
8832
+ for (const candidate of ["npm", "bun", "deno"]) {
8833
+ if (await findExecutable([candidate])) {
8834
+ return candidate;
8835
+ }
8836
+ }
8837
+ throw new Error("Install Node/npm, Bun, or Deno to update threadnote.");
8838
+ }
8839
+ async function requireRuntime(runtime) {
8840
+ if (!await findExecutable([runtime])) {
8841
+ throw new Error(`${runtime} was requested but was not found on PATH.`);
8842
+ }
8843
+ }
8844
+ async function installedThreadnoteCommand(runtime) {
8845
+ const runtimeBin = await runtimeThreadnoteBin(runtime);
8846
+ if (runtimeBin && await isExecutable(runtimeBin)) {
8847
+ return runtimeBin;
8848
+ }
8849
+ return await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
8850
+ }
8851
+ async function runtimeThreadnoteBin(runtime) {
8852
+ if (runtime === "npm") {
8853
+ const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
8854
+ const prefix = result.stdout.trim();
8855
+ return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
8856
+ }
8857
+ if (runtime === "bun") {
8858
+ const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
8859
+ const binDir = result.stdout.trim();
8860
+ return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
8861
+ }
8862
+ return (0, import_node_path7.join)(process.env.DENO_INSTALL ?? (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
8863
+ }
8864
+ async function isExecutable(path) {
8865
+ try {
8866
+ await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
8867
+ return true;
8868
+ } catch (_err) {
8869
+ return false;
8870
+ }
8871
+ }
8872
+ function updatePackageCommand(runtime, registry) {
8873
+ if (runtime === "npm") {
8874
+ return { executable: "npm", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
8875
+ }
8876
+ if (runtime === "bun") {
8877
+ return { executable: "bun", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
8878
+ }
8879
+ return {
8880
+ executable: "env",
8881
+ args: [
8882
+ `NPM_CONFIG_REGISTRY=${registry}`,
8883
+ "deno",
8884
+ "install",
8885
+ "--global",
8886
+ "--force",
8887
+ "--name",
8888
+ NPM_PACKAGE_NAME,
8889
+ "--allow-read",
8890
+ "--allow-write",
8891
+ "--allow-run",
8892
+ "--allow-env",
8893
+ "--allow-net",
8894
+ `npm:${NPM_PACKAGE_NAME}@latest`
8895
+ ]
8896
+ };
8897
+ }
8898
+ function normalizeRegistry(registry) {
8899
+ return registry.endsWith("/") ? registry : `${registry}/`;
8900
+ }
8901
+ function updateRegistry() {
8902
+ return normalizeRegistry(process.env.THREADNOTE_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY);
8903
+ }
8904
+ function isUpdateNotificationDisabled() {
8905
+ return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
8906
+ }
8907
+ function compareVersions(currentVersion, latestVersion) {
8908
+ const current = parseVersion(currentVersion);
8909
+ const latest = parseVersion(latestVersion);
8910
+ for (let index = 0; index < 3; index += 1) {
8911
+ const difference = current.numbers[index] - latest.numbers[index];
8912
+ if (difference !== 0) {
8913
+ return difference;
8914
+ }
8915
+ }
8916
+ if (current.prerelease === latest.prerelease) {
8917
+ return 0;
8918
+ }
8919
+ if (current.prerelease === void 0) {
8920
+ return 1;
8921
+ }
8922
+ if (latest.prerelease === void 0) {
8923
+ return -1;
8924
+ }
8925
+ return current.prerelease.localeCompare(latest.prerelease);
8926
+ }
8927
+ function parseVersion(version) {
8928
+ const normalized = version.trim().replace(/^v/, "");
8929
+ const [core2, prerelease] = normalized.split("-", 2);
8930
+ const parts = core2.split(".").map((part) => Number(part));
8931
+ return {
8932
+ numbers: [safeVersionNumber(parts[0]), safeVersionNumber(parts[1]), safeVersionNumber(parts[2])],
8933
+ prerelease
8934
+ };
8935
+ }
8936
+ function safeVersionNumber(value) {
8937
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
8938
+ }
8939
+
8287
8940
  // src/threadnote.ts
8288
8941
  async function main() {
8289
8942
  const program2 = new Command();
8290
8943
  program2.name("threadnote").description("Threadnote shared context workflow for development agents").showHelpAfterError().option("--home <path>", "Override THREADNOTE_HOME for this invocation").option("--manifest <path>", "Override THREADNOTE_MANIFEST for this invocation").option("--host <host>", "OpenViking host", DEFAULT_HOST).option("--port <port>", "OpenViking port", parsePort, DEFAULT_PORT);
8291
8944
  program2.command("doctor").description("Check local prerequisites, config files, manifest shape, and server health").option("--dry-run", "Show checks without writing anything").option("--strict", "Exit non-zero if any check fails").action(async (options) => {
8292
- await runDoctor(getRuntimeConfig(program2), options);
8945
+ const config = getRuntimeConfig(program2);
8946
+ await runDoctor(config, options);
8947
+ await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
8293
8948
  });
8294
8949
  program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--package-manager <manager>", "pipx, uv, or pip", parsePackageManager).action(async (options) => {
8295
- await runInstall(getRuntimeConfig(program2), options);
8950
+ const config = getRuntimeConfig(program2);
8951
+ await runInstall(config, options);
8952
+ await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
8953
+ });
8954
+ program2.command("update").description("Update the published Threadnote package, then repair local shims and MCP config").option("--check", "Only check whether a newer version is available").option("--dry-run", "Print update and repair commands without running them").option("--force", "Run package-manager update even if this version is already current").option("--registry <url>", "npm registry URL", process.env.THREADNOTE_NPM_REGISTRY).option("--runtime <runtime>", "auto, npm, bun, or deno", parseUpdateRuntime, "auto").option("--no-repair", "Skip threadnote repair after updating the package").action(async (options) => {
8955
+ await runUpdate(getRuntimeConfig(program2), options);
8296
8956
  });
8297
8957
  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(
8298
8958
  "--mcp <clients>",
8299
8959
  "MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
8300
8960
  "available"
8301
8961
  ).option("--no-start", "Do not start OpenViking if health is failing").option("--package-manager <manager>", "pipx, uv, or pip", parsePackageManager).action(async (options) => {
8302
- await runRepair(getRuntimeConfig(program2), options);
8962
+ const config = getRuntimeConfig(program2);
8963
+ await runRepair(config, options);
8964
+ await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
8303
8965
  });
8304
8966
  program2.command("start").description("Start the local OpenViking server").option("--dry-run", "Print the start command without running it").option("--foreground", "Run in the foreground instead of detaching").option("--launchd", "Install and start a macOS LaunchAgent").action(async (options) => {
8305
- await runStart(getRuntimeConfig(program2), options);
8967
+ const config = getRuntimeConfig(program2);
8968
+ await runStart(config, options);
8969
+ await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
8306
8970
  });
8307
8971
  program2.command("stop").description("Stop the local OpenViking server or LaunchAgent").option("--dry-run", "Print the stop actions without running them").action(async (options) => {
8308
8972
  await runStop(getRuntimeConfig(program2), options);
@@ -8329,6 +8993,14 @@ async function main() {
8329
8993
  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) => {
8330
8994
  await runRemember(getRuntimeConfig(program2), options);
8331
8995
  });
8996
+ 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(
8997
+ "--source-account <account>",
8998
+ "Source OpenViking account to scan; repeat for multiple accounts",
8999
+ collectOption,
9000
+ []
9001
+ ).action(async (options) => {
9002
+ await runMigrateMemories(getRuntimeConfig(program2), options);
9003
+ });
8332
9004
  program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
8333
9005
  await runRecall(getRuntimeConfig(program2), options);
8334
9006
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",