threadnote 0.7.6 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,7 +43,8 @@ which memory to keep/update, which old handoffs to archive, and which exact dupl
43
43
 
44
44
  **Still working on the same issue?**
45
45
  Use `threadnote remember --replace <uri>` or `threadnote handoff --replace <uri>` to keep one current-state memory fresh
46
- instead of accumulating near-duplicate progress notes.
46
+ instead of accumulating near-duplicate progress notes. Replacing a shared `durable/` URI updates that shared memory in
47
+ place and pushes the shared repo, so you do not need a separate `share publish` step.
47
48
 
48
49
  ## Memory Lifecycle
49
50
 
@@ -179,10 +180,15 @@ threadnote update
179
180
  Check without changing anything:
180
181
 
181
182
  ```bash
183
+ threadnote version
182
184
  threadnote update --check
183
185
  threadnote update --dry-run
184
186
  ```
185
187
 
188
+ `threadnote version` prints the installed version, latest npm version, and GitHub release notes. When no update is
189
+ available, it shows the current release notes; when an update is available, it shows every newer version. `threadnote
190
+ update` prints the same full "What's new" diff when an update is available.
191
+
186
192
  After updating, restart Cursor, Copilot, Codex, Claude, or open a fresh agent session so MCP tools reload.
187
193
 
188
194
  Some releases include post-update memory migrations. `threadnote update` runs the new package's migration prompt after
@@ -333,8 +339,9 @@ This is it! Start working with your agents as usual. The agent will automaticall
333
339
  - `install`: installs `openviking[local-embed]==0.3.12` if missing, creates `~/.openviking` config files if absent,
334
340
  writes the command shim, upserts user-level agent instructions, and starts/checks OpenViking health by default. Use
335
341
  `--no-start` to skip the health check.
342
+ - `version`: prints the installed Threadnote version, latest npm version, and release notes for newer GitHub releases.
336
343
  - `update`: updates the published Threadnote package, then runs `repair` so shims and MCP config point at the new
337
- version.
344
+ version. When an update is available, it prints release notes for the full version diff.
338
345
  - `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude/Cursor/Copilot MCP configs
339
346
  from the current checkout.
340
347
  - `start`: starts `openviking-server` on `127.0.0.1:1933`.
@@ -347,8 +354,9 @@ This is it! Start working with your agents as usual. The agent will automaticall
347
354
  reusable workflow guidance. Use `seed-skills --native` only after configuring a working VLM provider.
348
355
  - `mcp-install codex|claude|cursor|copilot`: installs or prints OpenViking MCP configuration for Codex, Claude, Cursor,
349
356
  or GitHub Copilot in VS Code.
350
- - `remember`: stores a durable memory. Use `--replace <uri>` to store an updated memory and remove a superseded one
351
- after the new memory succeeds. Use `--kind`, `--project`, and `--topic` to store lifecycle-aware current knowledge.
357
+ - `remember`: stores a durable memory. Use `--replace <uri>` to store an updated memory and remove a superseded personal
358
+ memory after the new memory succeeds; if `<uri>` is shared, Threadnote updates that shared memory in place and pushes
359
+ the shared repo. Use `--kind`, `--project`, and `--topic` to store lifecycle-aware current knowledge.
352
360
  - `migrate-memories`: migrates legacy session-only `MEMORY` and `HANDOFF` records into durable memory files. Run
353
361
  `migrate-memories --dry-run` first; use `--all-accounts` when importing from older local OpenViking accounts.
354
362
  - `migrate-lifecycle`: moves clear legacy handoff memories from the old events path into archived lifecycle handoff
@@ -28477,25 +28477,25 @@ var Protocol = class {
28477
28477
  });
28478
28478
  }
28479
28479
  _resetTimeout(messageId) {
28480
- const info = this._timeoutInfo.get(messageId);
28481
- if (!info)
28480
+ const info2 = this._timeoutInfo.get(messageId);
28481
+ if (!info2)
28482
28482
  return false;
28483
- const totalElapsed = Date.now() - info.startTime;
28484
- if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) {
28483
+ const totalElapsed = Date.now() - info2.startTime;
28484
+ if (info2.maxTotalTimeout && totalElapsed >= info2.maxTotalTimeout) {
28485
28485
  this._timeoutInfo.delete(messageId);
28486
28486
  throw McpError.fromError(ErrorCode.RequestTimeout, "Maximum total timeout exceeded", {
28487
- maxTotalTimeout: info.maxTotalTimeout,
28487
+ maxTotalTimeout: info2.maxTotalTimeout,
28488
28488
  totalElapsed
28489
28489
  });
28490
28490
  }
28491
- clearTimeout(info.timeoutId);
28492
- info.timeoutId = setTimeout(info.onTimeout, info.timeout);
28491
+ clearTimeout(info2.timeoutId);
28492
+ info2.timeoutId = setTimeout(info2.onTimeout, info2.timeout);
28493
28493
  return true;
28494
28494
  }
28495
28495
  _cleanupTimeout(messageId) {
28496
- const info = this._timeoutInfo.get(messageId);
28497
- if (info) {
28498
- clearTimeout(info.timeoutId);
28496
+ const info2 = this._timeoutInfo.get(messageId);
28497
+ if (info2) {
28498
+ clearTimeout(info2.timeoutId);
28499
28499
  this._timeoutInfo.delete(messageId);
28500
28500
  }
28501
28501
  }
@@ -28540,8 +28540,8 @@ var Protocol = class {
28540
28540
  this._progressHandlers.clear();
28541
28541
  this._taskProgressTokens.clear();
28542
28542
  this._pendingDebouncedNotifications.clear();
28543
- for (const info of this._timeoutInfo.values()) {
28544
- clearTimeout(info.timeoutId);
28543
+ for (const info2 of this._timeoutInfo.values()) {
28544
+ clearTimeout(info2.timeoutId);
28545
28545
  }
28546
28546
  this._timeoutInfo.clear();
28547
28547
  for (const controller of this._requestHandlerAbortControllers.values()) {
@@ -30036,8 +30036,8 @@ function validateToolName(name) {
30036
30036
  function issueToolNameWarning(name, warnings) {
30037
30037
  if (warnings.length > 0) {
30038
30038
  console.warn(`Tool name validation warning for "${name}":`);
30039
- for (const warning of warnings) {
30040
- console.warn(` - ${warning}`);
30039
+ for (const warning2 of warnings) {
30040
+ console.warn(` - ${warning2}`);
30041
30041
  }
30042
30042
  console.warn("Tool registration will proceed, but this may cause compatibility issues.");
30043
30043
  console.warn("Consider updating the tool name to conform to the MCP tool naming standard.");
@@ -33591,6 +33591,39 @@ var import_node_crypto = require("node:crypto");
33591
33591
  var import_promises = require("node:fs/promises");
33592
33592
  var import_node_os = require("node:os");
33593
33593
  var import_node_path = require("node:path");
33594
+
33595
+ // src/cli_ui.ts
33596
+ var import_node_process2 = require("node:process");
33597
+ var ANSI = {
33598
+ blue: "\x1B[34m",
33599
+ bold: "\x1B[1m",
33600
+ cyan: "\x1B[36m",
33601
+ dim: "\x1B[2m",
33602
+ green: "\x1B[32m",
33603
+ red: "\x1B[31m",
33604
+ reset: "\x1B[0m",
33605
+ yellow: "\x1B[33m"
33606
+ };
33607
+ function color(name, text) {
33608
+ if (!shouldUseColor()) {
33609
+ return text;
33610
+ }
33611
+ return `${ANSI[name]}${text}${ANSI.reset}`;
33612
+ }
33613
+ function command(text) {
33614
+ return color("cyan", text);
33615
+ }
33616
+ function info(text) {
33617
+ return color("cyan", text);
33618
+ }
33619
+ function warning(text) {
33620
+ return color("yellow", text);
33621
+ }
33622
+ function shouldUseColor() {
33623
+ return import_node_process2.stdout.isTTY === true && process.env.NO_COLOR === void 0 && process.env.CI === void 0 && process.env.TERM !== "dumb";
33624
+ }
33625
+
33626
+ // src/utils.ts
33594
33627
  function isJsonObject(value) {
33595
33628
  return typeof value === "object" && value !== null && !Array.isArray(value);
33596
33629
  }
@@ -33609,11 +33642,11 @@ function redactText(content) {
33609
33642
  ).replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]").replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, "gh_[REDACTED]");
33610
33643
  }
33611
33644
  async function requiredOpenVikingCli() {
33612
- const command = await findExecutable(["ov", "openviking"]);
33613
- if (!command) {
33645
+ const command2 = await findExecutable(["ov", "openviking"]);
33646
+ if (!command2) {
33614
33647
  throw new Error("Neither ov nor openviking was found in PATH. Run threadnote install first.");
33615
33648
  }
33616
- return command;
33649
+ return command2;
33617
33650
  }
33618
33651
  async function openVikingCliForMode(dryRun) {
33619
33652
  if (dryRun) {
@@ -33621,16 +33654,16 @@ async function openVikingCliForMode(dryRun) {
33621
33654
  }
33622
33655
  return requiredOpenVikingCli();
33623
33656
  }
33624
- async function requiredExecutable(command) {
33625
- const executable = await findExecutable([command]);
33657
+ async function requiredExecutable(command2) {
33658
+ const executable = await findExecutable([command2]);
33626
33659
  if (!executable) {
33627
- throw new Error(`${command} was not found in PATH.`);
33660
+ throw new Error(`${command2} was not found in PATH.`);
33628
33661
  }
33629
33662
  return executable;
33630
33663
  }
33631
33664
  async function findExecutable(commands) {
33632
- for (const command of commands) {
33633
- const result = await runCommand("which", [command], { allowFailure: true });
33665
+ for (const command2 of commands) {
33666
+ const result = await runCommand("which", [command2], { allowFailure: true });
33634
33667
  if (result.exitCode === 0 && result.stdout.trim()) {
33635
33668
  return result.stdout.trim();
33636
33669
  }
@@ -33639,7 +33672,8 @@ async function findExecutable(commands) {
33639
33672
  }
33640
33673
  async function maybeRun(dryRun, executable, args, options = {}) {
33641
33674
  const cwdSuffix = options.cwd ? ` (cwd: ${options.cwd})` : "";
33642
- console.log(`${dryRun ? "Would run" : "Running"}: ${formatShellCommand(executable, args)}${cwdSuffix}`);
33675
+ const label = dryRun ? warning("Would run") : info("Running");
33676
+ console.log(`${label}: ${command(formatShellCommand(executable, args))}${cwdSuffix}`);
33643
33677
  if (dryRun) {
33644
33678
  return void 0;
33645
33679
  }
@@ -34445,9 +34479,9 @@ async function syncSharedReposBeforeAgentRead(config2) {
34445
34479
  const remainingBehind = new Set(state.behindTeams);
34446
34480
  for (const team of syncTeams) {
34447
34481
  try {
34448
- const warning = await runShareSyncQuiet(config2, state, { team });
34449
- if (warning) {
34450
- warnings.push(warning);
34482
+ const warning2 = await runShareSyncQuiet(config2, state, { team });
34483
+ if (warning2) {
34484
+ warnings.push(warning2);
34451
34485
  } else {
34452
34486
  remainingBehind.delete(team);
34453
34487
  syncedTeams.push(team);
@@ -34529,8 +34563,8 @@ async function refreshShareUpdateState(config2, options) {
34529
34563
  state,
34530
34564
  async () => refreshShareUpdateStateLocked(config2, state, options)
34531
34565
  );
34532
- for (const warning of warnings) {
34533
- console.error(warning);
34566
+ for (const warning2 of warnings) {
34567
+ console.error(warning2);
34534
34568
  }
34535
34569
  }
34536
34570
  async function refreshShareUpdateStateLocked(config2, state, options) {
@@ -34726,7 +34760,35 @@ function workfileToVikingUri(config2, team, filePath) {
34726
34760
  return `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
34727
34761
  }
34728
34762
  function isInSharedNamespace(config2, uri) {
34729
- return uri.startsWith(`viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`);
34763
+ return sharedTeamNameForUri(config2, uri) !== void 0;
34764
+ }
34765
+ function sharedTeamNameForUri(config2, uri) {
34766
+ const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
34767
+ if (!uri.startsWith(prefix)) {
34768
+ return void 0;
34769
+ }
34770
+ const [team] = uri.slice(prefix.length).split("/");
34771
+ return team || void 0;
34772
+ }
34773
+ function sharedMemoryUriParts(config2, uri) {
34774
+ const prefix = `viking://user/${uriSegment(config2.user)}/memories/${SHARED_SEGMENT}/`;
34775
+ if (!uri.startsWith(prefix)) {
34776
+ return void 0;
34777
+ }
34778
+ const [team, kind, scope, project, ...topicParts] = uri.slice(prefix.length).split("/");
34779
+ if (!team) {
34780
+ return void 0;
34781
+ }
34782
+ if (kind !== "durable" || scope !== "projects" || !project || topicParts.length === 0) {
34783
+ return { team };
34784
+ }
34785
+ const topicPath = topicParts.join("/");
34786
+ return {
34787
+ kind,
34788
+ project,
34789
+ team,
34790
+ topic: topicPath.endsWith(".md") ? topicPath.slice(0, -".md".length) : topicPath
34791
+ };
34730
34792
  }
34731
34793
  function sharedUriFor(config2, personalUri, team) {
34732
34794
  const prefix = `viking://user/${uriSegment(config2.user)}/memories/`;
@@ -34889,14 +34951,14 @@ async function waitForOvQueue(ov, config2, options = {}) {
34889
34951
  console.error(result.stderr.trim());
34890
34952
  }
34891
34953
  }
34892
- function isTransientOvFailure(stderr, stdout) {
34954
+ function isTransientOvFailure(stderr, stdout2) {
34893
34955
  const output = `${stderr}
34894
- ${stdout}`.toLowerCase();
34956
+ ${stdout2}`.toLowerCase();
34895
34957
  return output.includes("resource is busy") || output.includes("resource is being processed") || output.includes("network error") || output.includes("error sending request") || output.includes("http request failed") || output.includes("connection refused") || output.includes("connection reset") || output.includes("timed out");
34896
34958
  }
34897
- function isResourceBusyFailure(stderr, stdout) {
34959
+ function isResourceBusyFailure(stderr, stdout2) {
34898
34960
  const output = `${stderr}
34899
- ${stdout}`.toLowerCase();
34961
+ ${stdout2}`.toLowerCase();
34900
34962
  return output.includes("resource is busy") || output.includes("resource is being processed");
34901
34963
  }
34902
34964
  async function ingestSingleFile(ov, config2, uri, filePath, initialMode, options = {}) {
@@ -35400,8 +35462,8 @@ ${exactMatches}`);
35400
35462
  if (syncedTeams.length > 0) {
35401
35463
  sections.push(`Auto-synced shared memories: ${syncedTeams.join(", ")}`);
35402
35464
  }
35403
- for (const warning of syncWarnings) {
35404
- sections.push(`Auto-sync warning: ${warning}`);
35465
+ for (const warning2 of syncWarnings) {
35466
+ sections.push(`Auto-sync warning: ${warning2}`);
35405
35467
  }
35406
35468
  if (sections.length <= 1) {
35407
35469
  return semanticResult;
@@ -35499,7 +35561,7 @@ function registerReadTool(server, config2, name, description) {
35499
35561
  }
35500
35562
  const syncMessages = [
35501
35563
  syncedTeams.length > 0 ? `Auto-synced shared memories: ${syncedTeams.join(", ")}` : void 0,
35502
- ...syncWarnings.map((warning) => `Auto-sync warning: ${warning}`)
35564
+ ...syncWarnings.map((warning2) => `Auto-sync warning: ${warning2}`)
35503
35565
  ].filter((part) => part !== void 0);
35504
35566
  return {
35505
35567
  ...result,
@@ -35547,7 +35609,9 @@ function registerStoreTool(server, config2, name, description) {
35547
35609
  inputSchema: {
35548
35610
  kind: external_exports.enum(["durable", "handoff", "incident", "preference", "smoke"]).optional().describe("Memory lifecycle kind; durable facts and handoffs are most common"),
35549
35611
  project: external_exports.string().optional().describe("Project/repo namespace, for example threadnote or mobile-native"),
35550
- replaceUri: external_exports.string().optional().describe("Optional viking:// memory URI to forget after the new memory is safely stored"),
35612
+ replaceUri: external_exports.string().optional().describe(
35613
+ "Optional viking:// memory URI to replace. Shared URIs are updated in place and pushed; personal URIs are forgotten after the replacement is safely stored."
35614
+ ),
35551
35615
  text: external_exports.string().optional().describe("Required memory text to store"),
35552
35616
  sourceAgentClient: external_exports.string().optional().describe("Originating client, for example cursor, copilot, codex, or claude"),
35553
35617
  status: external_exports.enum(["active", "archived", "superseded"]).optional().describe("Memory lifecycle status"),
@@ -35866,6 +35930,9 @@ async function readTextIfExists(path) {
35866
35930
  async function writeDurableMemory(config2, params) {
35867
35931
  try {
35868
35932
  const ov = await requiredOpenVikingCli2();
35933
+ if (params.replaceUri && isInSharedNamespace(config2, params.replaceUri)) {
35934
+ return await writeSharedMemoryReplacement(config2, ov, params, params.replaceUri);
35935
+ }
35869
35936
  const directoryUri = memoryDirectoryUri(config2, params.metadata);
35870
35937
  await ensureMemoryDirectory(ov, config2, directoryUri);
35871
35938
  const candidateMetadata = { ...params.metadata, supersedes: params.replaceUri };
@@ -35906,6 +35973,40 @@ async function writeDurableMemory(config2, params) {
35906
35973
  return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
35907
35974
  }
35908
35975
  }
35976
+ async function writeSharedMemoryReplacement(config2, ov, params, targetUri) {
35977
+ if (params.metadata.kind !== "durable") {
35978
+ return argumentError("Shared memory replacement only supports durable memories.");
35979
+ }
35980
+ const teamName = sharedTeamNameForUri(config2, targetUri);
35981
+ if (!teamName) {
35982
+ return argumentError(`Memory ${targetUri} is not in the shared namespace.`);
35983
+ }
35984
+ const resolved = await resolveTeam(config2, teamName);
35985
+ const inferred = sharedMemoryUriParts(config2, targetUri);
35986
+ const metadata = {
35987
+ ...params.metadata,
35988
+ project: params.metadata.project ?? inferred?.project,
35989
+ topic: params.metadata.topic ?? inferred?.topic
35990
+ };
35991
+ const rawMemory = formatMemoryDocument2("MEMORY", metadata, params.bodyText);
35992
+ const scrub = applyScrubber(stripPersonalProvenance(rawMemory), { redact: false });
35993
+ if (scrub.blocker) {
35994
+ return argumentError(
35995
+ `Refusing to update shared memory ${targetUri}: possible ${scrub.blocker}. Strip the sensitive value first.`
35996
+ );
35997
+ }
35998
+ await ensureSharedDirectoryChain(config2, ov, targetUri, false, { quiet: true });
35999
+ await writeMemoryFile(config2, ov, targetUri, scrub.cleaned, "replace", false, { quiet: true });
36000
+ const relativePath = vikingUriToWorktreeRelative(config2, targetUri, resolved.name);
36001
+ const messages = [`Updated shared memory: ${targetUri}`];
36002
+ for (const redaction of scrub.redactions) {
36003
+ messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
36004
+ }
36005
+ messages.push(
36006
+ ...await gitPublishWorkflow(resolved.config.worktree, relativePath, `share: update ${relativePath}`, true)
36007
+ );
36008
+ return { content: [{ type: "text", text: messages.join("\n") }] };
36009
+ }
35909
36010
  async function runOpenVikingWriteWithRetry(ov, config2, memoryUri, args) {
35910
36011
  for (let attempt = 0; attempt < 4; attempt += 1) {
35911
36012
  const result = await runCommand(ov, args, { allowFailure: true });
@@ -35944,9 +36045,9 @@ async function removeVikingResourceWithRetry(ov, config2, uri) {
35944
36045
  }
35945
36046
  return false;
35946
36047
  }
35947
- function isResourceBusy(stderr, stdout) {
36048
+ function isResourceBusy(stderr, stdout2) {
35948
36049
  const output = `${stderr}
35949
- ${stdout}`.toLowerCase();
36050
+ ${stdout2}`.toLowerCase();
35950
36051
  return output.includes("resource is busy") || output.includes("resource is being processed");
35951
36052
  }
35952
36053
  async function ensureMemoryDirectory(ov, config2, directoryUri) {
@@ -36224,11 +36325,11 @@ function withIdentity2(config2, args) {
36224
36325
  return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
36225
36326
  }
36226
36327
  async function requiredOpenVikingCli2() {
36227
- const command = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "ov"), (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "openviking")]);
36228
- if (!command) {
36328
+ const command2 = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "ov"), (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "openviking")]);
36329
+ if (!command2) {
36229
36330
  throw new Error("Neither ov nor openviking was found. Run threadnote install first.");
36230
36331
  }
36231
- return command;
36332
+ return command2;
36232
36333
  }
36233
36334
  async function firstExistingPath(paths) {
36234
36335
  for (const path of paths) {
@@ -347,11 +347,11 @@ var require_help = __commonJS({
347
347
  * @returns {number}
348
348
  */
349
349
  longestSubcommandTermLength(cmd, helper) {
350
- return helper.visibleCommands(cmd).reduce((max, command) => {
350
+ return helper.visibleCommands(cmd).reduce((max, command2) => {
351
351
  return Math.max(
352
352
  max,
353
353
  this.displayWidth(
354
- helper.styleSubcommandTerm(helper.subcommandTerm(command))
354
+ helper.styleSubcommandTerm(helper.subcommandTerm(command2))
355
355
  )
356
356
  );
357
357
  }, 0);
@@ -512,9 +512,9 @@ var require_help = __commonJS({
512
512
  * @param {Help} helper
513
513
  * @returns string[]
514
514
  */
515
- formatItemList(heading, items, helper) {
515
+ formatItemList(heading2, items, helper) {
516
516
  if (items.length === 0) return [];
517
- return [helper.styleTitle(heading), ...items, ""];
517
+ return [helper.styleTitle(heading2), ...items, ""];
518
518
  }
519
519
  /**
520
520
  * Group items by their help group heading.
@@ -993,8 +993,8 @@ var require_option = __commonJS({
993
993
  * @param {string} heading
994
994
  * @return {Option}
995
995
  */
996
- helpGroup(heading) {
997
- this.helpGroupHeading = heading;
996
+ helpGroup(heading2) {
997
+ this.helpGroupHeading = heading2;
998
998
  return this;
999
999
  }
1000
1000
  /**
@@ -1284,8 +1284,8 @@ var require_command = __commonJS({
1284
1284
  */
1285
1285
  _getCommandAndAncestors() {
1286
1286
  const result = [];
1287
- for (let command = this; command; command = command.parent) {
1288
- result.push(command);
1287
+ for (let command2 = this; command2; command2 = command2.parent) {
1288
+ result.push(command2);
1289
1289
  }
1290
1290
  return result;
1291
1291
  }
@@ -1726,22 +1726,22 @@ Expecting one of '${allowedValues.join("', '")}'`);
1726
1726
  * @param {Command} command
1727
1727
  * @private
1728
1728
  */
1729
- _registerCommand(command) {
1729
+ _registerCommand(command2) {
1730
1730
  const knownBy = (cmd) => {
1731
1731
  return [cmd.name()].concat(cmd.aliases());
1732
1732
  };
1733
- const alreadyUsed = knownBy(command).find(
1733
+ const alreadyUsed = knownBy(command2).find(
1734
1734
  (name) => this._findCommand(name)
1735
1735
  );
1736
1736
  if (alreadyUsed) {
1737
1737
  const existingCmd = knownBy(this._findCommand(alreadyUsed)).join("|");
1738
- const newCmd = knownBy(command).join("|");
1738
+ const newCmd = knownBy(command2).join("|");
1739
1739
  throw new Error(
1740
1740
  `cannot add command '${newCmd}' as already have command '${existingCmd}'`
1741
1741
  );
1742
1742
  }
1743
- this._initCommandGroup(command);
1744
- this.commands.push(command);
1743
+ this._initCommandGroup(command2);
1744
+ this.commands.push(command2);
1745
1745
  }
1746
1746
  /**
1747
1747
  * Add an option.
@@ -2913,12 +2913,12 @@ Expecting one of '${allowedValues.join("', '")}'`);
2913
2913
  let suggestion = "";
2914
2914
  if (flag.startsWith("--") && this._showSuggestionAfterError) {
2915
2915
  let candidateFlags = [];
2916
- let command = this;
2916
+ let command2 = this;
2917
2917
  do {
2918
- const moreFlags = command.createHelp().visibleOptions(command).filter((option) => option.long).map((option) => option.long);
2918
+ const moreFlags = command2.createHelp().visibleOptions(command2).filter((option) => option.long).map((option) => option.long);
2919
2919
  candidateFlags = candidateFlags.concat(moreFlags);
2920
- command = command.parent;
2921
- } while (command && !command._enablePositionalOptions);
2920
+ command2 = command2.parent;
2921
+ } while (command2 && !command2._enablePositionalOptions);
2922
2922
  suggestion = suggestSimilar(flag, candidateFlags);
2923
2923
  }
2924
2924
  const message = `error: unknown option '${flag}'${suggestion}`;
@@ -2948,9 +2948,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2948
2948
  let suggestion = "";
2949
2949
  if (this._showSuggestionAfterError) {
2950
2950
  const candidateNames = [];
2951
- this.createHelp().visibleCommands(this).forEach((command) => {
2952
- candidateNames.push(command.name());
2953
- if (command.alias()) candidateNames.push(command.alias());
2951
+ this.createHelp().visibleCommands(this).forEach((command2) => {
2952
+ candidateNames.push(command2.name());
2953
+ if (command2.alias()) candidateNames.push(command2.alias());
2954
2954
  });
2955
2955
  suggestion = suggestSimilar(unknownName, candidateNames);
2956
2956
  }
@@ -3021,11 +3021,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
3021
3021
  */
3022
3022
  alias(alias) {
3023
3023
  if (alias === void 0) return this._aliases[0];
3024
- let command = this;
3024
+ let command2 = this;
3025
3025
  if (this.commands.length !== 0 && this.commands[this.commands.length - 1]._executableHandler) {
3026
- command = this.commands[this.commands.length - 1];
3026
+ command2 = this.commands[this.commands.length - 1];
3027
3027
  }
3028
- if (alias === command._name)
3028
+ if (alias === command2._name)
3029
3029
  throw new Error("Command alias can't be the same as its name");
3030
3030
  const matchingCommand = this.parent?._findCommand(alias);
3031
3031
  if (matchingCommand) {
@@ -3034,7 +3034,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3034
3034
  `cannot add alias '${alias}' to command '${this.name()}' as already have command '${existingCmd}'`
3035
3035
  );
3036
3036
  }
3037
- command._aliases.push(alias);
3037
+ command2._aliases.push(alias);
3038
3038
  return this;
3039
3039
  }
3040
3040
  /**
@@ -3088,9 +3088,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3088
3088
  * @param {string} [heading]
3089
3089
  * @return {Command | string}
3090
3090
  */
3091
- helpGroup(heading) {
3092
- if (heading === void 0) return this._helpGroupHeading ?? "";
3093
- this._helpGroupHeading = heading;
3091
+ helpGroup(heading2) {
3092
+ if (heading2 === void 0) return this._helpGroupHeading ?? "";
3093
+ this._helpGroupHeading = heading2;
3094
3094
  return this;
3095
3095
  }
3096
3096
  /**
@@ -3106,9 +3106,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3106
3106
  * @param {string} [heading]
3107
3107
  * @returns {Command | string}
3108
3108
  */
3109
- commandsGroup(heading) {
3110
- if (heading === void 0) return this._defaultCommandGroup ?? "";
3111
- this._defaultCommandGroup = heading;
3109
+ commandsGroup(heading2) {
3110
+ if (heading2 === void 0) return this._defaultCommandGroup ?? "";
3111
+ this._defaultCommandGroup = heading2;
3112
3112
  return this;
3113
3113
  }
3114
3114
  /**
@@ -3124,9 +3124,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
3124
3124
  * @param {string} [heading]
3125
3125
  * @returns {Command | string}
3126
3126
  */
3127
- optionsGroup(heading) {
3128
- if (heading === void 0) return this._defaultOptionGroup ?? "";
3129
- this._defaultOptionGroup = heading;
3127
+ optionsGroup(heading2) {
3128
+ if (heading2 === void 0) return this._defaultOptionGroup ?? "";
3129
+ this._defaultOptionGroup = heading2;
3130
3130
  return this;
3131
3131
  }
3132
3132
  /**
@@ -3246,7 +3246,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3246
3246
  write: outputContext.write,
3247
3247
  command: this
3248
3248
  };
3249
- this._getCommandAndAncestors().reverse().forEach((command) => command.emit("beforeAllHelp", eventContext));
3249
+ this._getCommandAndAncestors().reverse().forEach((command2) => command2.emit("beforeAllHelp", eventContext));
3250
3250
  this.emit("beforeHelp", eventContext);
3251
3251
  let helpInformation = this.helpInformation({ error: outputContext.error });
3252
3252
  if (deprecatedCallback) {
@@ -3261,7 +3261,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
3261
3261
  }
3262
3262
  this.emit("afterHelp", eventContext);
3263
3263
  this._getCommandAndAncestors().forEach(
3264
- (command) => command.emit("afterAllHelp", eventContext)
3264
+ (command2) => command2.emit("afterAllHelp", eventContext)
3265
3265
  );
3266
3266
  }
3267
3267
  /**
@@ -3541,6 +3541,83 @@ var import_node_http = require("node:http");
3541
3541
  var import_node_net = require("node:net");
3542
3542
  var import_node_os = require("node:os");
3543
3543
  var import_node_path = require("node:path");
3544
+
3545
+ // src/cli_ui.ts
3546
+ var import_node_readline = require("node:readline");
3547
+ var import_node_process = require("node:process");
3548
+ var ANSI = {
3549
+ blue: "\x1B[34m",
3550
+ bold: "\x1B[1m",
3551
+ cyan: "\x1B[36m",
3552
+ dim: "\x1B[2m",
3553
+ green: "\x1B[32m",
3554
+ red: "\x1B[31m",
3555
+ reset: "\x1B[0m",
3556
+ yellow: "\x1B[33m"
3557
+ };
3558
+ function color(name, text) {
3559
+ if (!shouldUseColor()) {
3560
+ return text;
3561
+ }
3562
+ return `${ANSI[name]}${text}${ANSI.reset}`;
3563
+ }
3564
+ function bold(text) {
3565
+ if (!shouldUseColor()) {
3566
+ return text;
3567
+ }
3568
+ return `${ANSI.bold}${text}${ANSI.reset}`;
3569
+ }
3570
+ function command(text) {
3571
+ return color("cyan", text);
3572
+ }
3573
+ function heading(text) {
3574
+ return bold(text);
3575
+ }
3576
+ function info(text) {
3577
+ return color("cyan", text);
3578
+ }
3579
+ function muted(text) {
3580
+ return color("dim", text);
3581
+ }
3582
+ function success(text) {
3583
+ return color("green", text);
3584
+ }
3585
+ function warning(text) {
3586
+ return color("yellow", text);
3587
+ }
3588
+ function failure(text) {
3589
+ return color("red", text);
3590
+ }
3591
+ function keyValue(label, value) {
3592
+ return `${label}: ${value}`;
3593
+ }
3594
+ function shouldUseColor() {
3595
+ return import_node_process.stdout.isTTY === true && process.env.NO_COLOR === void 0 && process.env.CI === void 0 && process.env.TERM !== "dumb";
3596
+ }
3597
+ async function withSpinner(message, action) {
3598
+ if (import_node_process.stdout.isTTY !== true || process.env.CI !== void 0 || process.env.THREADNOTE_NO_SPINNER !== void 0) {
3599
+ return action();
3600
+ }
3601
+ const frames = ["-", "\\", "|", "/"];
3602
+ let frameIndex = 0;
3603
+ const render = () => {
3604
+ (0, import_node_readline.clearLine)(import_node_process.stdout, 0);
3605
+ (0, import_node_readline.cursorTo)(import_node_process.stdout, 0);
3606
+ import_node_process.stdout.write(`${muted(frames[frameIndex])} ${message}`);
3607
+ frameIndex = (frameIndex + 1) % frames.length;
3608
+ };
3609
+ render();
3610
+ const timer = setInterval(render, 100);
3611
+ try {
3612
+ return await action();
3613
+ } finally {
3614
+ clearInterval(timer);
3615
+ (0, import_node_readline.clearLine)(import_node_process.stdout, 0);
3616
+ (0, import_node_readline.cursorTo)(import_node_process.stdout, 0);
3617
+ }
3618
+ }
3619
+
3620
+ // src/utils.ts
3544
3621
  function isJsonObject(value) {
3545
3622
  return typeof value === "object" && value !== null && !Array.isArray(value);
3546
3623
  }
@@ -3630,11 +3707,11 @@ function escapeRegExp(value) {
3630
3707
  return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
3631
3708
  }
3632
3709
  async function requiredOpenVikingCli() {
3633
- const command = await findExecutable(["ov", "openviking"]);
3634
- if (!command) {
3710
+ const command2 = await findExecutable(["ov", "openviking"]);
3711
+ if (!command2) {
3635
3712
  throw new Error("Neither ov nor openviking was found in PATH. Run threadnote install first.");
3636
3713
  }
3637
- return command;
3714
+ return command2;
3638
3715
  }
3639
3716
  async function openVikingCliForMode(dryRun) {
3640
3717
  if (dryRun) {
@@ -3642,16 +3719,16 @@ async function openVikingCliForMode(dryRun) {
3642
3719
  }
3643
3720
  return requiredOpenVikingCli();
3644
3721
  }
3645
- async function requiredExecutable(command) {
3646
- const executable = await findExecutable([command]);
3722
+ async function requiredExecutable(command2) {
3723
+ const executable = await findExecutable([command2]);
3647
3724
  if (!executable) {
3648
- throw new Error(`${command} was not found in PATH.`);
3725
+ throw new Error(`${command2} was not found in PATH.`);
3649
3726
  }
3650
3727
  return executable;
3651
3728
  }
3652
3729
  async function findExecutable(commands) {
3653
- for (const command of commands) {
3654
- const result = await runCommand("which", [command], { allowFailure: true });
3730
+ for (const command2 of commands) {
3731
+ const result = await runCommand("which", [command2], { allowFailure: true });
3655
3732
  if (result.exitCode === 0 && result.stdout.trim()) {
3656
3733
  return result.stdout.trim();
3657
3734
  }
@@ -3660,7 +3737,8 @@ async function findExecutable(commands) {
3660
3737
  }
3661
3738
  async function maybeRun(dryRun, executable, args, options = {}) {
3662
3739
  const cwdSuffix = options.cwd ? ` (cwd: ${options.cwd})` : "";
3663
- console.log(`${dryRun ? "Would run" : "Running"}: ${formatShellCommand(executable, args)}${cwdSuffix}`);
3740
+ const label = dryRun ? warning("Would run") : info("Running");
3741
+ console.log(`${label}: ${command(formatShellCommand(executable, args))}${cwdSuffix}`);
3664
3742
  if (dryRun) {
3665
3743
  return void 0;
3666
3744
  }
@@ -4090,12 +4168,12 @@ function firstLine(value) {
4090
4168
  }
4091
4169
  function formatStatus(status) {
4092
4170
  if (status === "ok") {
4093
- return "OK ";
4171
+ return success("OK ");
4094
4172
  }
4095
4173
  if (status === "warn") {
4096
- return "WARN";
4174
+ return warning("WARN");
4097
4175
  }
4098
- return "FAIL";
4176
+ return failure("FAIL");
4099
4177
  }
4100
4178
  function formatShellCommand(executable, args) {
4101
4179
  return redactText([executable, ...args].map(shellQuote).join(" "));
@@ -4152,7 +4230,7 @@ async function runMcpInstall(config, agent, options) {
4152
4230
  });
4153
4231
  return;
4154
4232
  }
4155
- const command = buildMcpInstallCommand(config, agent, name, {
4233
+ const command2 = buildMcpInstallCommand(config, agent, name, {
4156
4234
  bearerTokenEnvVar: options.bearerTokenEnvVar,
4157
4235
  nativeHttp,
4158
4236
  scope: options.scope,
@@ -4161,11 +4239,11 @@ async function runMcpInstall(config, agent, options) {
4161
4239
  const removeCommand = buildMcpRemoveCommand(agent, name);
4162
4240
  if (!apply) {
4163
4241
  console.log("Dry run. Re-run with --apply to modify the selected agent config.");
4164
- if (removeCommand.cwd || command.cwd) {
4165
- console.log(`Command working directory: ${removeCommand.cwd ?? command.cwd}`);
4242
+ if (removeCommand.cwd || command2.cwd) {
4243
+ console.log(`Command working directory: ${removeCommand.cwd ?? command2.cwd}`);
4166
4244
  }
4167
4245
  console.log(formatShellCommand(removeCommand.executable, removeCommand.args));
4168
- console.log(formatShellCommand(command.executable, command.args));
4246
+ console.log(formatShellCommand(command2.executable, command2.args));
4169
4247
  printMcpSnippet(config, agent, name, { nativeHttp, scope: options.scope, url });
4170
4248
  return;
4171
4249
  }
@@ -4173,7 +4251,7 @@ async function runMcpInstall(config, agent, options) {
4173
4251
  allowFailure: true,
4174
4252
  cwd: removeCommand.cwd
4175
4253
  });
4176
- await maybeRun(false, command.executable, command.args, { cwd: command.cwd });
4254
+ await maybeRun(false, command2.executable, command2.args, { cwd: command2.cwd });
4177
4255
  }
4178
4256
  async function runCursorMcpInstall(config, name, options) {
4179
4257
  const path = cursorMcpConfigPath();
@@ -4244,8 +4322,8 @@ async function removeMcpConfigs(value, dryRun) {
4244
4322
  await removeCopilotMcpConfig(OPENVIKING_MCP_NAME, dryRun);
4245
4323
  continue;
4246
4324
  }
4247
- const command = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
4248
- await maybeRun(dryRun, command.executable, command.args, { allowFailure: true, cwd: command.cwd });
4325
+ const command2 = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
4326
+ await maybeRun(dryRun, command2.executable, command2.args, { allowFailure: true, cwd: command2.cwd });
4249
4327
  }
4250
4328
  }
4251
4329
  async function removeMcpSnippets(config, dryRun) {
@@ -4280,17 +4358,17 @@ function buildMcpInstallCommand(config, agent, name, options) {
4280
4358
  const claudeCwd = getInvocationCwd();
4281
4359
  const claudeScope = options.scope ?? "user";
4282
4360
  if (!options.nativeHttp) {
4283
- const command = mcpAdapterCommand();
4361
+ const command2 = mcpAdapterCommand();
4284
4362
  const env = mcpEnvironment(config);
4285
4363
  if (agent === "codex") {
4286
4364
  return {
4287
4365
  executable: "codex",
4288
- args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", "/usr/bin/env", ...command]
4366
+ args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", "/usr/bin/env", ...command2]
4289
4367
  };
4290
4368
  }
4291
4369
  return {
4292
4370
  executable: "claude",
4293
- args: ["mcp", "add", "--scope", claudeScope, name, "--", "/usr/bin/env", ...env, ...command],
4371
+ args: ["mcp", "add", "--scope", claudeScope, name, "--", "/usr/bin/env", ...env, ...command2],
4294
4372
  cwd: claudeCwd
4295
4373
  };
4296
4374
  }
@@ -4472,12 +4550,12 @@ function printMcpSnippet(config, agent, name, options) {
4472
4550
  return;
4473
4551
  }
4474
4552
  const snippetPath = (0, import_node_path2.join)(config.agentContextHome, "mcp", `${name}.${agent}.${agent === "codex" ? "toml" : "txt"}`);
4475
- const command = buildMcpInstallCommand(config, agent, name, {
4553
+ const command2 = buildMcpInstallCommand(config, agent, name, {
4476
4554
  nativeHttp: options.nativeHttp,
4477
4555
  scope: options.scope,
4478
4556
  url: options.url
4479
4557
  });
4480
- const snippet2 = `${formatShellCommand(command.executable, command.args)}
4558
+ const snippet2 = `${formatShellCommand(command2.executable, command2.args)}
4481
4559
  `;
4482
4560
  console.log(`
4483
4561
  Snippet (${snippetPath}):
@@ -7960,9 +8038,9 @@ async function syncSharedReposBeforeAgentRead(config) {
7960
8038
  const remainingBehind = new Set(state.behindTeams);
7961
8039
  for (const team of syncTeams) {
7962
8040
  try {
7963
- const warning = await runShareSyncQuiet(config, state, { team });
7964
- if (warning) {
7965
- warnings.push(warning);
8041
+ const warning2 = await runShareSyncQuiet(config, state, { team });
8042
+ if (warning2) {
8043
+ warnings.push(warning2);
7966
8044
  } else {
7967
8045
  remainingBehind.delete(team);
7968
8046
  syncedTeams.push(team);
@@ -8510,7 +8588,35 @@ function workfileToVikingUri(config, team, filePath) {
8510
8588
  return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
8511
8589
  }
8512
8590
  function isInSharedNamespace(config, uri) {
8513
- return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`);
8591
+ return sharedTeamNameForUri(config, uri) !== void 0;
8592
+ }
8593
+ function sharedTeamNameForUri(config, uri) {
8594
+ const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
8595
+ if (!uri.startsWith(prefix)) {
8596
+ return void 0;
8597
+ }
8598
+ const [team] = uri.slice(prefix.length).split("/");
8599
+ return team || void 0;
8600
+ }
8601
+ function sharedMemoryUriParts(config, uri) {
8602
+ const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
8603
+ if (!uri.startsWith(prefix)) {
8604
+ return void 0;
8605
+ }
8606
+ const [team, kind, scope, project, ...topicParts] = uri.slice(prefix.length).split("/");
8607
+ if (!team) {
8608
+ return void 0;
8609
+ }
8610
+ if (kind !== "durable" || scope !== "projects" || !project || topicParts.length === 0) {
8611
+ return { team };
8612
+ }
8613
+ const topicPath = topicParts.join("/");
8614
+ return {
8615
+ kind,
8616
+ project,
8617
+ team,
8618
+ topic: topicPath.endsWith(".md") ? topicPath.slice(0, -".md".length) : topicPath
8619
+ };
8514
8620
  }
8515
8621
  function isInTeamNamespace(config, uri, team) {
8516
8622
  return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`);
@@ -8696,14 +8802,14 @@ async function waitForOvQueue(ov, config, options = {}) {
8696
8802
  console.error(result.stderr.trim());
8697
8803
  }
8698
8804
  }
8699
- function isTransientOvFailure(stderr, stdout) {
8805
+ function isTransientOvFailure(stderr, stdout2) {
8700
8806
  const output2 = `${stderr}
8701
- ${stdout}`.toLowerCase();
8807
+ ${stdout2}`.toLowerCase();
8702
8808
  return output2.includes("resource is busy") || output2.includes("resource is being processed") || output2.includes("network error") || output2.includes("error sending request") || output2.includes("http request failed") || output2.includes("connection refused") || output2.includes("connection reset") || output2.includes("timed out");
8703
8809
  }
8704
- function isResourceBusyFailure(stderr, stdout) {
8810
+ function isResourceBusyFailure(stderr, stdout2) {
8705
8811
  const output2 = `${stderr}
8706
- ${stdout}`.toLowerCase();
8812
+ ${stdout2}`.toLowerCase();
8707
8813
  return output2.includes("resource is busy") || output2.includes("resource is being processed");
8708
8814
  }
8709
8815
  async function ingestSingleFile(ov, config, uri, filePath, initialMode, options = {}) {
@@ -9138,8 +9244,8 @@ async function syncSharedReposAndLog(config) {
9138
9244
  if (syncResult.syncedTeams.length > 0) {
9139
9245
  console.error(`Auto-synced shared memories: ${syncResult.syncedTeams.join(", ")}`);
9140
9246
  }
9141
- for (const warning of syncResult.warnings) {
9142
- console.error(`Auto-sync warning: ${warning}`);
9247
+ for (const warning2 of syncResult.warnings) {
9248
+ console.error(`Auto-sync warning: ${warning2}`);
9143
9249
  }
9144
9250
  } catch (err) {
9145
9251
  console.error(`Auto-sync warning: ${err instanceof Error ? err.message : String(err)}`);
@@ -9434,6 +9540,10 @@ async function storeMemory(config, options) {
9434
9540
  assertVikingUri(options.replaceUri);
9435
9541
  }
9436
9542
  const ov = await openVikingCliForMode(options.dryRun);
9543
+ if (options.replaceUri && isInSharedNamespace(config, options.replaceUri)) {
9544
+ await storeSharedMemoryReplacement(config, ov, options, options.replaceUri);
9545
+ return;
9546
+ }
9437
9547
  const memoryPath = (0, import_node_path6.join)(config.agentContextHome, "last-memory.txt");
9438
9548
  const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
9439
9549
  const candidateMemory = formatMemoryDocument2(options.title, candidateMetadata, options.bodyText);
@@ -9484,6 +9594,49 @@ async function storeMemory(config, options) {
9484
9594
  console.log(`Updated existing memory in place: ${memoryUri}`);
9485
9595
  }
9486
9596
  }
9597
+ async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
9598
+ if (options.metadata.kind !== "durable") {
9599
+ throw new Error("Shared memory replacement only supports durable memories.");
9600
+ }
9601
+ const teamName = sharedTeamNameForUri(config, targetUri);
9602
+ if (!teamName) {
9603
+ throw new Error(`Memory ${targetUri} is not in the shared namespace.`);
9604
+ }
9605
+ const team = await resolveTeam(config, teamName);
9606
+ const inferred = sharedMemoryUriParts(config, targetUri);
9607
+ const metadata = {
9608
+ ...options.metadata,
9609
+ project: options.metadata.project ?? inferred?.project,
9610
+ topic: options.metadata.topic ?? inferred?.topic
9611
+ };
9612
+ const rawMemory = formatMemoryDocument2(options.title, metadata, options.bodyText);
9613
+ const scrub = applyScrubber(stripPersonalProvenance(rawMemory), { redact: false });
9614
+ if (scrub.blocker) {
9615
+ throw new Error(
9616
+ `Refusing to update shared memory ${targetUri}: possible ${scrub.blocker}. Strip the sensitive value first.`
9617
+ );
9618
+ }
9619
+ const memory = scrub.cleaned;
9620
+ const relativePath = vikingUriToWorktreeRelative(config, targetUri, team.name);
9621
+ if (options.dryRun) {
9622
+ console.log(memory);
9623
+ console.log("\nWould run:");
9624
+ }
9625
+ await ensureSharedDirectoryChain(config, ov, targetUri, options.dryRun);
9626
+ await writeMemoryFile(config, ov, targetUri, memory, "replace", options.dryRun);
9627
+ const git = await requiredExecutable("git");
9628
+ await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "add", "--", relativePath]);
9629
+ await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "commit", "-m", `share: update ${relativePath}`], {
9630
+ allowFailure: true
9631
+ });
9632
+ await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "push", DEFAULT_GIT_REMOTE_NAME], {
9633
+ allowFailure: true
9634
+ });
9635
+ for (const redaction of scrub.redactions) {
9636
+ console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
9637
+ }
9638
+ console.log(`Updated shared memory: ${targetUri}`);
9639
+ }
9487
9640
  async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode) {
9488
9641
  const args = withIdentity(config, [
9489
9642
  "write",
@@ -9920,9 +10073,9 @@ function localUserMemoriesRoot(config) {
9920
10073
  function uniqueStrings(values) {
9921
10074
  return [...new Set(values)].sort();
9922
10075
  }
9923
- function isResourceBusy(stderr, stdout) {
10076
+ function isResourceBusy(stderr, stdout2) {
9924
10077
  const output2 = `${stderr}
9925
- ${stdout}`.toLowerCase();
10078
+ ${stdout2}`.toLowerCase();
9926
10079
  return output2.includes("resource is busy") || output2.includes("resource is being processed");
9927
10080
  }
9928
10081
  async function buildHandoff(options) {
@@ -10712,7 +10865,7 @@ var import_node_fs6 = require("node:fs");
10712
10865
  var import_promises11 = require("node:fs/promises");
10713
10866
  var import_node_os6 = require("node:os");
10714
10867
  var import_node_path12 = require("node:path");
10715
- var import_node_process2 = require("node:process");
10868
+ var import_node_process3 = require("node:process");
10716
10869
  var import_promises12 = require("node:readline/promises");
10717
10870
 
10718
10871
  // src/update.ts
@@ -10721,7 +10874,106 @@ var import_promises9 = require("node:fs/promises");
10721
10874
  var import_node_os5 = require("node:os");
10722
10875
  var import_node_path11 = require("node:path");
10723
10876
  var import_promises10 = require("node:readline/promises");
10724
- var import_node_process = require("node:process");
10877
+ var import_node_process2 = require("node:process");
10878
+
10879
+ // src/release_notes.ts
10880
+ var GITHUB_RELEASES_URL = "https://api.github.com/repos/Kashkovsky/threadnote/releases?per_page=100";
10881
+ var RELEASE_FETCH_TIMEOUT_MS = 3500;
10882
+ async function fetchThreadnoteReleaseNotes() {
10883
+ const response = await fetch(GITHUB_RELEASES_URL, {
10884
+ headers: {
10885
+ accept: "application/vnd.github+json",
10886
+ "user-agent": "threadnote-cli"
10887
+ },
10888
+ signal: AbortSignal.timeout(RELEASE_FETCH_TIMEOUT_MS)
10889
+ });
10890
+ if (!response.ok) {
10891
+ throw new Error(`GitHub releases returned HTTP ${response.status}`);
10892
+ }
10893
+ const parsed = await response.json();
10894
+ if (!Array.isArray(parsed)) {
10895
+ throw new Error("GitHub releases response was not an array.");
10896
+ }
10897
+ return parsed.flatMap(parseGithubRelease);
10898
+ }
10899
+ function releasesBetween(releases, currentVersion, latestVersion) {
10900
+ return releases.filter(
10901
+ (release) => compareVersions(release.version, currentVersion) > 0 && compareVersions(release.version, latestVersion) <= 0
10902
+ ).sort((left, right) => compareVersions(left.version, right.version));
10903
+ }
10904
+ function releaseForVersion(releases, version) {
10905
+ return releases.filter((release) => compareVersions(release.version, version) === 0).slice(0, 1);
10906
+ }
10907
+ function formatWhatsNew(releases) {
10908
+ if (releases.length === 0) {
10909
+ return [];
10910
+ }
10911
+ const lines = ["What's new:"];
10912
+ for (const release of releases) {
10913
+ lines.push(`${release.version}: ${release.title || "Release notes"}`);
10914
+ for (const line of bodyLines(release.body)) {
10915
+ lines.push(` ${line}`);
10916
+ }
10917
+ }
10918
+ return lines;
10919
+ }
10920
+ async function whatsNewLinesForVersion(version) {
10921
+ try {
10922
+ const releases = await fetchThreadnoteReleaseNotes();
10923
+ const lines = formatWhatsNew(releaseForVersion(releases, version));
10924
+ return lines.length > 0 ? lines : ["What's new:", `No GitHub release notes found for ${version}.`];
10925
+ } catch (err) {
10926
+ return [`What's new: unavailable (${errorMessage(err)})`];
10927
+ }
10928
+ }
10929
+ async function whatsNewLinesForVersionRange(currentVersion, latestVersion) {
10930
+ try {
10931
+ const releases = await fetchThreadnoteReleaseNotes();
10932
+ const lines = formatWhatsNew(releasesBetween(releases, currentVersion, latestVersion));
10933
+ return lines.length > 0 ? lines : ["What's new:", "No GitHub release notes found for this version range."];
10934
+ } catch (err) {
10935
+ return [`What's new: unavailable (${errorMessage(err)})`];
10936
+ }
10937
+ }
10938
+ function parseGithubRelease(value) {
10939
+ if (!isJsonObject(value) || value.draft === true || value.prerelease === true) {
10940
+ return [];
10941
+ }
10942
+ const tagName = typeof value.tag_name === "string" ? value.tag_name : "";
10943
+ const version = tagName.trim().replace(/^v/, "");
10944
+ if (!/^\d+\.\d+\.\d+(?:[-.A-Za-z0-9]+)?$/.test(version)) {
10945
+ return [];
10946
+ }
10947
+ const name = typeof value.name === "string" ? value.name : tagName;
10948
+ const body = typeof value.body === "string" ? value.body : "";
10949
+ const url = typeof value.html_url === "string" ? value.html_url : void 0;
10950
+ return [{ body, title: titleFromReleaseName(name, version), url, version }];
10951
+ }
10952
+ function titleFromReleaseName(name, version) {
10953
+ const trimmed = name.trim();
10954
+ const withoutPrefix = trimmed.replace(new RegExp(`^v?${escapeRegExp2(version)}:\\s*`, "i"), "");
10955
+ return withoutPrefix || trimmed || "Release notes";
10956
+ }
10957
+ function bodyLines(body) {
10958
+ const out = [];
10959
+ for (const rawLine of body.split("\n")) {
10960
+ const line = rawLine.trim();
10961
+ if (!line || /^#{1,6}\s+/.test(line)) {
10962
+ continue;
10963
+ }
10964
+ if (/^[-*]\s+/.test(line)) {
10965
+ out.push(`- ${line.replace(/^[-*]\s+/, "")}`);
10966
+ } else {
10967
+ out.push(line);
10968
+ }
10969
+ }
10970
+ return out;
10971
+ }
10972
+ function escapeRegExp2(value) {
10973
+ return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
10974
+ }
10975
+
10976
+ // src/update.ts
10725
10977
  var NPM_PACKAGE_NAME = "threadnote";
10726
10978
  var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
10727
10979
  var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -10738,43 +10990,47 @@ async function maybeNotifyUpdate(config, options = {}) {
10738
10990
  return;
10739
10991
  }
10740
10992
  try {
10741
- const info = await getUpdateInfo(config, {
10993
+ const info2 = await getUpdateInfo(config, {
10742
10994
  allowCacheWrite: options.dryRun !== true,
10743
10995
  preferFresh: false,
10744
10996
  registry: updateRegistry()
10745
10997
  });
10746
- if (!info.isUpdateAvailable) {
10998
+ if (!info2.isUpdateAvailable) {
10747
10999
  return;
10748
11000
  }
10749
11001
  console.log("");
10750
- console.log(`Update available: threadnote ${info.currentVersion} -> ${info.latestVersion}`);
10751
- console.log("Run: threadnote update");
11002
+ console.log(warning(`Update available: threadnote ${info2.currentVersion} -> ${info2.latestVersion}`));
11003
+ console.log(`Run: ${info("threadnote update")}`);
10752
11004
  } catch (_err) {
10753
11005
  return;
10754
11006
  }
10755
11007
  }
10756
11008
  async function runUpdate(config, options) {
10757
11009
  const registry = normalizeRegistry(options.registry ?? updateRegistry());
10758
- const info = await getUpdateInfo(config, {
10759
- allowCacheWrite: options.dryRun !== true,
10760
- preferFresh: true,
10761
- registry
10762
- });
10763
- console.log(`Current version: ${info.currentVersion}`);
10764
- console.log(`Latest version: ${info.latestVersion}`);
10765
- console.log(`Registry: ${info.registry}`);
11010
+ const info2 = await withSpinner(
11011
+ "Checking npm for latest threadnote version",
11012
+ () => getUpdateInfo(config, {
11013
+ allowCacheWrite: options.dryRun !== true,
11014
+ preferFresh: true,
11015
+ registry
11016
+ })
11017
+ );
11018
+ console.log(keyValue("Current version", info(info2.currentVersion)));
11019
+ console.log(keyValue("Latest version", info(info2.latestVersion)));
11020
+ console.log(keyValue("Registry", info2.registry));
10766
11021
  if (options.check === true) {
10767
- if (info.isUpdateAvailable) {
10768
- console.log(`Update available. Run: threadnote update`);
11022
+ if (info2.isUpdateAvailable) {
11023
+ console.log(warning("Update available. Run: threadnote update"));
11024
+ await printWhatsNewIfAvailable(info2);
10769
11025
  } else {
10770
11026
  console.log(
10771
- compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
11027
+ compareVersions(info2.currentVersion, info2.latestVersion) > 0 ? warning("Current version is newer than npm latest.") : success("Threadnote is up to date.")
10772
11028
  );
10773
11029
  }
10774
11030
  return;
10775
11031
  }
10776
- if (!info.isUpdateAvailable && options.force !== true) {
10777
- console.log("Threadnote is up to date.");
11032
+ if (!info2.isUpdateAvailable && options.force !== true) {
11033
+ console.log(success("Threadnote is up to date."));
10778
11034
  return;
10779
11035
  }
10780
11036
  const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
@@ -10782,6 +11038,7 @@ async function runUpdate(config, options) {
10782
11038
  await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
10783
11039
  if (options.repair === false) {
10784
11040
  console.log("Skipping repair because --no-repair was provided.");
11041
+ await printWhatsNewIfAvailable(info2);
10785
11042
  return;
10786
11043
  }
10787
11044
  const threadnoteCommand = await installedThreadnoteCommand(runtime);
@@ -10790,9 +11047,9 @@ async function runUpdate(config, options) {
10790
11047
  const postUpdateArgs = [
10791
11048
  "post-update",
10792
11049
  "--from-version",
10793
- info.currentVersion,
11050
+ info2.currentVersion,
10794
11051
  "--to-version",
10795
- info.latestVersion,
11052
+ info2.latestVersion,
10796
11053
  ...options.yes === true ? ["--yes"] : []
10797
11054
  ];
10798
11055
  if (options.dryRun === true) {
@@ -10810,6 +11067,20 @@ async function runUpdate(config, options) {
10810
11067
  console.log(
10811
11068
  "Update complete. Restart Cursor, Copilot, Codex, Claude, or open a fresh agent session so MCP tools reload."
10812
11069
  );
11070
+ await printWhatsNewIfAvailable(info2);
11071
+ }
11072
+ async function printWhatsNewIfAvailable(info2) {
11073
+ if (!info2.isUpdateAvailable) {
11074
+ return;
11075
+ }
11076
+ console.log("");
11077
+ const whatsNew = await withSpinner(
11078
+ "Fetching GitHub release notes",
11079
+ () => whatsNewLinesForVersionRange(info2.currentVersion, info2.latestVersion)
11080
+ );
11081
+ for (const line of whatsNew) {
11082
+ console.log(line === "What's new:" ? heading(line) : line);
11083
+ }
10813
11084
  }
10814
11085
  async function runPostUpdate(config, options) {
10815
11086
  if (!options.fromVersion || !options.toVersion) {
@@ -11127,7 +11398,7 @@ function printPostUpdateMigration(migration) {
11127
11398
  }
11128
11399
  }
11129
11400
  async function confirmPostUpdateMigration(prompt, defaultYes = false) {
11130
- const readline = (0, import_promises10.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
11401
+ const readline = (0, import_promises10.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
11131
11402
  try {
11132
11403
  const answer = (await readline.question(prompt)).trim().toLowerCase();
11133
11404
  if (answer === "") {
@@ -11649,8 +11920,8 @@ async function commandPresenceCheck(name, args) {
11649
11920
  };
11650
11921
  }
11651
11922
  async function firstCommandCheck(name, commands, args) {
11652
- for (const command of commands) {
11653
- const executable = await findExecutable([command]);
11923
+ for (const command2 of commands) {
11924
+ const executable = await findExecutable([command2]);
11654
11925
  if (!executable) {
11655
11926
  continue;
11656
11927
  }
@@ -11658,7 +11929,7 @@ async function firstCommandCheck(name, commands, args) {
11658
11929
  return {
11659
11930
  name,
11660
11931
  status: result.exitCode === 0 ? "ok" : "warn",
11661
- detail: `${command}: ${firstLine(result.stdout || result.stderr) || executable}`
11932
+ detail: `${command2}: ${firstLine(result.stdout || result.stderr) || executable}`
11662
11933
  };
11663
11934
  }
11664
11935
  return { name, status: "fail", detail: `none found: ${commands.join(", ")}` };
@@ -11833,13 +12104,13 @@ async function runInstallCommands(config, preferred, force, dryRun) {
11833
12104
  }
11834
12105
  }
11835
12106
  async function offerToInstallUv() {
11836
- if (import_node_process2.stdin.isTTY !== true || import_node_process2.stdout.isTTY !== true) {
12107
+ if (import_node_process3.stdin.isTTY !== true || import_node_process3.stdout.isTTY !== true) {
11837
12108
  console.warn(
11838
12109
  "Neither uv nor pipx was found on PATH. Falling back to `python3 -m pip install --user`, which fails on PEP 668 (Homebrew/system) Python.\nRe-run with --package-manager uv after installing uv (brew install uv), or pass --package-manager pipx."
11839
12110
  );
11840
12111
  return false;
11841
12112
  }
11842
- const readline = (0, import_promises12.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
12113
+ const readline = (0, import_promises12.createInterface)({ input: import_node_process3.stdin, output: import_node_process3.stdout });
11843
12114
  let answer;
11844
12115
  try {
11845
12116
  answer = (await readline.question(
@@ -12290,6 +12561,46 @@ function parsePackageManager(value) {
12290
12561
  throw new Error(`Invalid package manager: ${value}`);
12291
12562
  }
12292
12563
 
12564
+ // src/version_command.ts
12565
+ async function runVersion(config, options) {
12566
+ const currentVersion = await currentPackageVersion();
12567
+ const registry = normalizeRegistry(options.registry ?? updateRegistry());
12568
+ let latestVersion;
12569
+ let latestWarning;
12570
+ try {
12571
+ latestVersion = await withSpinner("Checking npm for latest threadnote version", () => fetchLatestVersion2(registry));
12572
+ } catch (err) {
12573
+ latestWarning = errorMessage(err);
12574
+ }
12575
+ console.log(keyValue("Current version", info(currentVersion)));
12576
+ console.log(keyValue("Latest version", latestVersion ? info(latestVersion) : warning("unavailable")));
12577
+ if (latestWarning) {
12578
+ console.log(warning(`Warning: ${latestWarning}`));
12579
+ }
12580
+ if (!latestVersion) {
12581
+ return;
12582
+ }
12583
+ const comparison = compareVersions(currentVersion, latestVersion);
12584
+ if (comparison >= 0) {
12585
+ console.log(success(comparison > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."));
12586
+ console.log("");
12587
+ const whatsNew2 = await withSpinner("Fetching GitHub release notes", () => whatsNewLinesForVersion(currentVersion));
12588
+ printWhatsNew(whatsNew2);
12589
+ return;
12590
+ }
12591
+ console.log("");
12592
+ const whatsNew = await withSpinner(
12593
+ "Fetching GitHub release notes",
12594
+ () => whatsNewLinesForVersionRange(currentVersion, latestVersion)
12595
+ );
12596
+ printWhatsNew(whatsNew);
12597
+ }
12598
+ function printWhatsNew(whatsNew) {
12599
+ for (const line of whatsNew) {
12600
+ console.log(line === "What's new:" ? heading(line) : line);
12601
+ }
12602
+ }
12603
+
12293
12604
  // src/threadnote.ts
12294
12605
  async function main() {
12295
12606
  const program2 = new Command();
@@ -12315,6 +12626,9 @@ async function main() {
12315
12626
  }
12316
12627
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
12317
12628
  });
12629
+ program2.command("version").description("Print the installed Threadnote version, latest npm version, and release notes").option("--registry <url>", "npm registry URL", process.env.THREADNOTE_NPM_REGISTRY).action(async (options) => {
12630
+ await runVersion(getRuntimeConfig(program2), options);
12631
+ });
12318
12632
  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").option("--no-post-update", "Skip post-update migration prompts").option("--yes", "Accept applicable post-update migrations without prompting").action(async (options) => {
12319
12633
  await runUpdate(getRuntimeConfig(program2), options);
12320
12634
  });
@@ -73,14 +73,17 @@ For branch feature work, prefer two stable memories with the same project and to
73
73
  - `kind: handoff` for current status, files touched, tests, blockers, and next step.
74
74
 
75
75
  If a durable feature memory already exists, update it in place with the same project/topic or with `--replace <uri>` /
76
- `replaceUri`. Avoid creating a new timestamped durable memory for every small progress note.
76
+ `replaceUri`. When `replaceUri` points at a shared `durable/` URI, Threadnote updates that shared memory in place and
77
+ pushes the shared repo; do not create a personal replacement and then run `share_publish` again. Avoid creating a new
78
+ timestamped durable memory for every small progress note.
77
79
 
78
80
  ## Memory Compaction
79
81
 
80
82
  When working on the same active issue, prefer keeping one current-state memory updated instead of creating many small
81
83
  progress memories. If an existing memory is clearly the current state for the issue, store the updated version with
82
84
  `remember_context({"text":"...","replaceUri":"<uri>"})`, `threadnote remember --replace <uri>`, or
83
- `threadnote handoff --replace <uri>` so the old memory is removed only after the replacement is stored.
85
+ `threadnote handoff --replace <uri>` so the old personal memory is removed only after the replacement is stored. Shared
86
+ `durable/` replacements are updated in place and pushed instead.
84
87
 
85
88
  When the issue has a stable name, prefer project/topic storage over timestamped memories:
86
89
 
package/docs/index.html CHANGED
@@ -1341,7 +1341,7 @@ threadnote remember \
1341
1341
  <p class="muted" style="font-size: 0.9rem; margin-top: 0.6rem">
1342
1342
  Refuses to publish if the memory matches secret patterns (PEM, <code>sk-</code>, <code>gh*_</code>,
1343
1343
  <code>github_pat_</code>, <code>glpat-</code>, JWTs, AWS keys, Slack tokens). Refuses to silently
1344
- overwrite an existing shared URI.
1344
+ overwrite an existing shared URI; update shared memories with <code>remember --replace</code>.
1345
1345
  </p>
1346
1346
  </div>
1347
1347
 
package/docs/share.md CHANGED
@@ -71,6 +71,21 @@ lines from the memory's header block. Those pointers only resolve on the
71
71
  publisher's machine — teammates pull via git and cannot dereference them — so
72
72
  keeping them would just leak local-only provenance into team git history.
73
73
 
74
+ To update an existing shared durable memory, replace the shared URI directly:
75
+
76
+ ```bash
77
+ threadnote remember \
78
+ --replace viking://user/you/memories/shared/default/durable/projects/foo/bar.md \
79
+ --project foo \
80
+ --topic bar \
81
+ --text "Updated shared knowledge."
82
+ ```
83
+
84
+ For MCP, pass the same shared URI as `replaceUri` to `remember_context`.
85
+ Threadnote rewrites the shared memory in place, strips local-only provenance
86
+ headers, commits, and pushes the shared repo. You do not need to store a
87
+ personal replacement and run `share publish` again.
88
+
74
89
  ### Keep teammates' updates current
75
90
 
76
91
  Threadnote does a periodic background `git fetch` for configured share teams.
@@ -155,7 +170,9 @@ otherwise unpublished work is lost.
155
170
  keep both, copy the memory to a new URI first (`ov read` then
156
171
  `threadnote remember`).
157
172
  - `share publish` refuses to overwrite an existing shared memory at the same
158
- URI; forget the existing shared copy first or pick a different topic name.
173
+ URI; use `threadnote remember --replace <shared-uri>` or
174
+ `remember_context({replaceUri:"<shared-uri>"})` for updates, or pick a
175
+ different topic name.
159
176
 
160
177
  ## Conflict resolution
161
178
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",