threadnote 0.3.1 → 0.3.2

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`.
@@ -215,6 +238,8 @@ Environment variables:
215
238
  - `THREADNOTE_USER`: OpenViking user value, default local username.
216
239
  - `THREADNOTE_AGENT_ID`: shared agent identity, default `threadnote`.
217
240
  - `THREADNOTE_OPENVIKING_VERSION`: package version to install, default `0.3.12`.
241
+ - `THREADNOTE_NPM_REGISTRY`: npm registry used by the installer and updater, default `https://registry.npmjs.org/`.
242
+ - `THREADNOTE_NO_UPDATE_CHECK`: disables opportunistic update notifications.
218
243
  - `THREADNOTE_BIN_DIR`: directory for the `threadnote` shim, default `~/.local/bin`.
219
244
  - `THREADNOTE_HOST`: local bind host, default `127.0.0.1`.
220
245
  - `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
  }
@@ -7010,6 +7059,7 @@ async function runRecall(config, options) {
7010
7059
  args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
7011
7060
  }
7012
7061
  await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
7062
+ await printExactMemoryMatches(config, ov, options.query, options.dryRun === true);
7013
7063
  }
7014
7064
  async function runRead(config, uri, options) {
7015
7065
  assertVikingUri(uri);
@@ -7082,18 +7132,141 @@ async function inferProjectFromQuery(config, normalizedQuery) {
7082
7132
  return void 0;
7083
7133
  }
7084
7134
  }
7135
+ async function printExactMemoryMatches(config, ov, query, dryRun) {
7136
+ const terms = exactRecallTerms(query);
7137
+ if (terms.length === 0) {
7138
+ return;
7139
+ }
7140
+ const scopes = [
7141
+ `viking://user/${uriSegment(config.user)}/memories`,
7142
+ `viking://agent/${uriSegment(config.agentId)}/memories`
7143
+ ];
7144
+ const outputs = [];
7145
+ for (const term of terms) {
7146
+ for (const scope of scopes) {
7147
+ const args = withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5"]);
7148
+ if (dryRun) {
7149
+ outputs.push(formatShellCommand(ov, args));
7150
+ continue;
7151
+ }
7152
+ const result = await runCommand(ov, args, { allowFailure: true });
7153
+ const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
7154
+ if (result.exitCode === 0 && grepOutputHasMatches(output)) {
7155
+ outputs.push(output);
7156
+ }
7157
+ }
7158
+ }
7159
+ if (outputs.length === 0) {
7160
+ return;
7161
+ }
7162
+ console.log("\nExact durable memory matches:");
7163
+ console.log(outputs.join("\n\n"));
7164
+ }
7085
7165
  async function storeMemory(config, memory, dryRun) {
7086
7166
  const ov = await openVikingCliForMode(dryRun);
7087
7167
  const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
7168
+ const memoryUri = durableMemoryUri(config, memory);
7088
7169
  if (dryRun) {
7089
7170
  console.log(memory);
7090
7171
  console.log("\nWould run:");
7091
- console.log(formatShellCommand(ov, withIdentity(config, ["add-memory", memory])));
7172
+ console.log(
7173
+ formatShellCommand(
7174
+ ov,
7175
+ withIdentity(config, [
7176
+ "write",
7177
+ memoryUri,
7178
+ "--from-file",
7179
+ memoryPath,
7180
+ "--mode",
7181
+ "create",
7182
+ "--wait",
7183
+ "--timeout",
7184
+ "120"
7185
+ ])
7186
+ )
7187
+ );
7092
7188
  return;
7093
7189
  }
7094
7190
  await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
7095
7191
  await (0, import_promises4.chmod)(memoryPath, 384);
7096
- await maybeRun(false, ov, withIdentity(config, ["add-memory", memory]));
7192
+ await ensureDurableMemoryDirectory(ov, config);
7193
+ await writeDurableMemoryFile(ov, config, memoryUri, memoryPath);
7194
+ console.log(`Stored durable memory: ${memoryUri}`);
7195
+ }
7196
+ async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath) {
7197
+ const args = withIdentity(config, [
7198
+ "write",
7199
+ memoryUri,
7200
+ "--from-file",
7201
+ memoryPath,
7202
+ "--mode",
7203
+ "create",
7204
+ "--wait",
7205
+ "--timeout",
7206
+ "120"
7207
+ ]);
7208
+ for (let attempt = 0; attempt < 4; attempt += 1) {
7209
+ console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
7210
+ const result = await runCommand(ov, args, { allowFailure: true });
7211
+ if (result.exitCode === 0) {
7212
+ if (result.stdout.trim()) {
7213
+ console.log(result.stdout.trim());
7214
+ }
7215
+ if (result.stderr.trim()) {
7216
+ console.error(result.stderr.trim());
7217
+ }
7218
+ return;
7219
+ }
7220
+ if (await vikingResourceExists(ov, config, memoryUri)) {
7221
+ console.log("OpenViking accepted the memory but returned before the wait completed; waiting for indexing.");
7222
+ await waitForOpenVikingQueue(ov, config);
7223
+ return;
7224
+ }
7225
+ if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
7226
+ throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
7227
+ }
7228
+ await sleep(1e3 * (attempt + 1));
7229
+ }
7230
+ }
7231
+ async function waitForOpenVikingQueue(ov, config) {
7232
+ const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
7233
+ if (result.stdout.trim()) {
7234
+ console.log(result.stdout.trim());
7235
+ }
7236
+ if (result.stderr.trim()) {
7237
+ console.error(result.stderr.trim());
7238
+ }
7239
+ }
7240
+ async function vikingResourceExists(ov, config, uri) {
7241
+ const stat2 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
7242
+ return stat2.exitCode === 0;
7243
+ }
7244
+ async function ensureDurableMemoryDirectory(ov, config) {
7245
+ const directoryUri = durableMemoryDirectoryUri(config);
7246
+ const stat2 = await runCommand(ov, withIdentity(config, ["stat", directoryUri]), { allowFailure: true });
7247
+ if (stat2.exitCode === 0) {
7248
+ return;
7249
+ }
7250
+ await maybeRun(
7251
+ false,
7252
+ ov,
7253
+ withIdentity(config, [
7254
+ "mkdir",
7255
+ directoryUri,
7256
+ "--description",
7257
+ "Threadnote durable handoffs, memories, and cross-agent notes."
7258
+ ])
7259
+ );
7260
+ }
7261
+ function durableMemoryUri(config, memory) {
7262
+ return `${durableMemoryDirectoryUri(config)}/threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
7263
+ }
7264
+ function durableMemoryDirectoryUri(config) {
7265
+ return `viking://user/${uriSegment(config.user)}/memories/events`;
7266
+ }
7267
+ function isResourceBusy(stderr, stdout) {
7268
+ return `${stderr}
7269
+ ${stdout}`.includes("resource is busy");
7097
7270
  }
7098
7271
  async function buildHandoff(options) {
7099
7272
  const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]) ?? getInvocationCwd();
@@ -8284,25 +8457,289 @@ function parsePackageManager(value) {
8284
8457
  throw new Error(`Invalid package manager: ${value}`);
8285
8458
  }
8286
8459
 
8460
+ // src/update.ts
8461
+ var import_node_fs4 = require("node:fs");
8462
+ var import_promises7 = require("node:fs/promises");
8463
+ var import_node_os5 = require("node:os");
8464
+ var import_node_path7 = require("node:path");
8465
+ var NPM_PACKAGE_NAME = "threadnote";
8466
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
8467
+ var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
8468
+ function parseUpdateRuntime(value) {
8469
+ if (value === "auto" || value === "npm" || value === "bun" || value === "deno") {
8470
+ return value;
8471
+ }
8472
+ throw new Error(`Invalid update runtime: ${value}. Expected auto, npm, bun, or deno.`);
8473
+ }
8474
+ async function maybeNotifyUpdate(config, options = {}) {
8475
+ if (isUpdateNotificationDisabled()) {
8476
+ return;
8477
+ }
8478
+ try {
8479
+ const info = await getUpdateInfo(config, {
8480
+ allowCacheWrite: options.dryRun !== true,
8481
+ preferFresh: false,
8482
+ registry: updateRegistry()
8483
+ });
8484
+ if (!info.isUpdateAvailable) {
8485
+ return;
8486
+ }
8487
+ console.log("");
8488
+ console.log(`Update available: threadnote ${info.currentVersion} -> ${info.latestVersion}`);
8489
+ console.log("Run: threadnote update");
8490
+ } catch (_err) {
8491
+ return;
8492
+ }
8493
+ }
8494
+ async function runUpdate(config, options) {
8495
+ const registry = normalizeRegistry(options.registry ?? updateRegistry());
8496
+ const info = await getUpdateInfo(config, {
8497
+ allowCacheWrite: options.dryRun !== true,
8498
+ preferFresh: true,
8499
+ registry
8500
+ });
8501
+ console.log(`Current version: ${info.currentVersion}`);
8502
+ console.log(`Latest version: ${info.latestVersion}`);
8503
+ console.log(`Registry: ${info.registry}`);
8504
+ if (options.check === true) {
8505
+ if (info.isUpdateAvailable) {
8506
+ console.log(`Update available. Run: threadnote update`);
8507
+ } else {
8508
+ console.log(
8509
+ compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
8510
+ );
8511
+ }
8512
+ return;
8513
+ }
8514
+ if (!info.isUpdateAvailable && options.force !== true) {
8515
+ console.log("Threadnote is up to date.");
8516
+ return;
8517
+ }
8518
+ const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
8519
+ const updateCommand = updatePackageCommand(runtime, registry);
8520
+ await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
8521
+ if (options.repair === false) {
8522
+ console.log("Skipping repair because --no-repair was provided.");
8523
+ return;
8524
+ }
8525
+ const threadnoteCommand = await installedThreadnoteCommand(runtime);
8526
+ await maybeRun(options.dryRun === true, threadnoteCommand, ["repair"]);
8527
+ console.log("Update complete. Restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.");
8528
+ }
8529
+ async function getUpdateInfo(config, options) {
8530
+ const currentVersion = await currentPackageVersion();
8531
+ const cached = options.preferFresh ? void 0 : await readFreshCache(config, options.registry);
8532
+ const latestVersion = cached?.latestVersion ?? await fetchLatestVersion(options.registry);
8533
+ if (!cached && options.allowCacheWrite) {
8534
+ await writeUpdateCache(config, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion, registry: options.registry });
8535
+ }
8536
+ return {
8537
+ currentVersion,
8538
+ isUpdateAvailable: compareVersions(currentVersion, latestVersion) < 0,
8539
+ latestVersion,
8540
+ registry: options.registry
8541
+ };
8542
+ }
8543
+ async function currentPackageVersion() {
8544
+ const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
8545
+ const parsed = JSON.parse(rawPackage);
8546
+ if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8547
+ throw new Error("Could not read current threadnote package version.");
8548
+ }
8549
+ return parsed.version;
8550
+ }
8551
+ async function fetchLatestVersion(registry) {
8552
+ const url = new URL(`${NPM_PACKAGE_NAME}/latest`, normalizeRegistry(registry));
8553
+ const controller = new AbortController();
8554
+ const timeout = setTimeout(() => {
8555
+ controller.abort();
8556
+ }, 2500);
8557
+ try {
8558
+ const response = await fetch(url, { headers: { accept: "application/json" }, signal: controller.signal });
8559
+ if (!response.ok) {
8560
+ throw new Error(`npm registry returned HTTP ${response.status}`);
8561
+ }
8562
+ const parsed = await response.json();
8563
+ if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8564
+ throw new Error("npm registry response did not include a version.");
8565
+ }
8566
+ return parsed.version;
8567
+ } catch (err) {
8568
+ throw new Error(`Could not check npm for updates: ${errorMessage(err)}`, { cause: err });
8569
+ } finally {
8570
+ clearTimeout(timeout);
8571
+ }
8572
+ }
8573
+ async function readFreshCache(config, registry) {
8574
+ const rawCache = await readFileIfExists(updateCachePath(config));
8575
+ if (!rawCache) {
8576
+ return void 0;
8577
+ }
8578
+ try {
8579
+ const parsed = JSON.parse(rawCache);
8580
+ if (!isJsonObject(parsed) || typeof parsed.checkedAt !== "string" || typeof parsed.latestVersion !== "string" || parsed.registry !== registry) {
8581
+ return void 0;
8582
+ }
8583
+ const checkedAt = Date.parse(parsed.checkedAt);
8584
+ if (!Number.isFinite(checkedAt) || Date.now() - checkedAt > UPDATE_CHECK_TTL_MS) {
8585
+ return void 0;
8586
+ }
8587
+ return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, registry };
8588
+ } catch (_err) {
8589
+ return void 0;
8590
+ }
8591
+ }
8592
+ async function writeUpdateCache(config, cache) {
8593
+ await ensureDirectory(config.agentContextHome, false);
8594
+ await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
8595
+ `, { encoding: "utf8", mode: 384 });
8596
+ }
8597
+ function updateCachePath(config) {
8598
+ return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
8599
+ }
8600
+ async function resolveUpdateRuntime(runtime) {
8601
+ if (runtime !== "auto") {
8602
+ await requireRuntime(runtime);
8603
+ return runtime;
8604
+ }
8605
+ for (const candidate of ["npm", "bun", "deno"]) {
8606
+ if (await findExecutable([candidate])) {
8607
+ return candidate;
8608
+ }
8609
+ }
8610
+ throw new Error("Install Node/npm, Bun, or Deno to update threadnote.");
8611
+ }
8612
+ async function requireRuntime(runtime) {
8613
+ if (!await findExecutable([runtime])) {
8614
+ throw new Error(`${runtime} was requested but was not found on PATH.`);
8615
+ }
8616
+ }
8617
+ async function installedThreadnoteCommand(runtime) {
8618
+ const runtimeBin = await runtimeThreadnoteBin(runtime);
8619
+ if (runtimeBin && await isExecutable(runtimeBin)) {
8620
+ return runtimeBin;
8621
+ }
8622
+ return await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
8623
+ }
8624
+ async function runtimeThreadnoteBin(runtime) {
8625
+ if (runtime === "npm") {
8626
+ const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
8627
+ const prefix = result.stdout.trim();
8628
+ return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
8629
+ }
8630
+ if (runtime === "bun") {
8631
+ const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
8632
+ const binDir = result.stdout.trim();
8633
+ return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
8634
+ }
8635
+ 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);
8636
+ }
8637
+ async function isExecutable(path) {
8638
+ try {
8639
+ await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
8640
+ return true;
8641
+ } catch (_err) {
8642
+ return false;
8643
+ }
8644
+ }
8645
+ function updatePackageCommand(runtime, registry) {
8646
+ if (runtime === "npm") {
8647
+ return { executable: "npm", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
8648
+ }
8649
+ if (runtime === "bun") {
8650
+ return { executable: "bun", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
8651
+ }
8652
+ return {
8653
+ executable: "env",
8654
+ args: [
8655
+ `NPM_CONFIG_REGISTRY=${registry}`,
8656
+ "deno",
8657
+ "install",
8658
+ "--global",
8659
+ "--force",
8660
+ "--name",
8661
+ NPM_PACKAGE_NAME,
8662
+ "--allow-read",
8663
+ "--allow-write",
8664
+ "--allow-run",
8665
+ "--allow-env",
8666
+ "--allow-net",
8667
+ `npm:${NPM_PACKAGE_NAME}@latest`
8668
+ ]
8669
+ };
8670
+ }
8671
+ function normalizeRegistry(registry) {
8672
+ return registry.endsWith("/") ? registry : `${registry}/`;
8673
+ }
8674
+ function updateRegistry() {
8675
+ return normalizeRegistry(process.env.THREADNOTE_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY);
8676
+ }
8677
+ function isUpdateNotificationDisabled() {
8678
+ return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
8679
+ }
8680
+ function compareVersions(currentVersion, latestVersion) {
8681
+ const current = parseVersion(currentVersion);
8682
+ const latest = parseVersion(latestVersion);
8683
+ for (let index = 0; index < 3; index += 1) {
8684
+ const difference = current.numbers[index] - latest.numbers[index];
8685
+ if (difference !== 0) {
8686
+ return difference;
8687
+ }
8688
+ }
8689
+ if (current.prerelease === latest.prerelease) {
8690
+ return 0;
8691
+ }
8692
+ if (current.prerelease === void 0) {
8693
+ return 1;
8694
+ }
8695
+ if (latest.prerelease === void 0) {
8696
+ return -1;
8697
+ }
8698
+ return current.prerelease.localeCompare(latest.prerelease);
8699
+ }
8700
+ function parseVersion(version) {
8701
+ const normalized = version.trim().replace(/^v/, "");
8702
+ const [core2, prerelease] = normalized.split("-", 2);
8703
+ const parts = core2.split(".").map((part) => Number(part));
8704
+ return {
8705
+ numbers: [safeVersionNumber(parts[0]), safeVersionNumber(parts[1]), safeVersionNumber(parts[2])],
8706
+ prerelease
8707
+ };
8708
+ }
8709
+ function safeVersionNumber(value) {
8710
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
8711
+ }
8712
+
8287
8713
  // src/threadnote.ts
8288
8714
  async function main() {
8289
8715
  const program2 = new Command();
8290
8716
  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
8717
  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);
8718
+ const config = getRuntimeConfig(program2);
8719
+ await runDoctor(config, options);
8720
+ await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
8293
8721
  });
8294
8722
  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);
8723
+ const config = getRuntimeConfig(program2);
8724
+ await runInstall(config, options);
8725
+ await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
8726
+ });
8727
+ 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) => {
8728
+ await runUpdate(getRuntimeConfig(program2), options);
8296
8729
  });
8297
8730
  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
8731
  "--mcp <clients>",
8299
8732
  "MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
8300
8733
  "available"
8301
8734
  ).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);
8735
+ const config = getRuntimeConfig(program2);
8736
+ await runRepair(config, options);
8737
+ await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
8303
8738
  });
8304
8739
  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);
8740
+ const config = getRuntimeConfig(program2);
8741
+ await runStart(config, options);
8742
+ await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
8306
8743
  });
8307
8744
  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
8745
  await runStop(getRuntimeConfig(program2), options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",