threadnote 1.0.0 → 1.1.0

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.
@@ -3616,6 +3616,41 @@ async function withSpinner(message, action) {
3616
3616
  (0, import_node_readline.cursorTo)(import_node_process.stdout, 0);
3617
3617
  }
3618
3618
  }
3619
+ function startProgress(message) {
3620
+ if (import_node_process.stdout.isTTY !== true || process.env.CI !== void 0 || process.env.THREADNOTE_NO_SPINNER !== void 0) {
3621
+ console.log(message);
3622
+ return {
3623
+ update(nextMessage) {
3624
+ console.log(nextMessage);
3625
+ },
3626
+ stop() {
3627
+ return;
3628
+ }
3629
+ };
3630
+ }
3631
+ const frames = ["-", "\\", "|", "/"];
3632
+ let frameIndex = 0;
3633
+ let currentMessage = message;
3634
+ const render = () => {
3635
+ (0, import_node_readline.clearLine)(import_node_process.stdout, 0);
3636
+ (0, import_node_readline.cursorTo)(import_node_process.stdout, 0);
3637
+ import_node_process.stdout.write(`${muted(frames[frameIndex])} ${currentMessage}`);
3638
+ frameIndex = (frameIndex + 1) % frames.length;
3639
+ };
3640
+ const timer = setInterval(render, 100);
3641
+ render();
3642
+ return {
3643
+ update(nextMessage) {
3644
+ currentMessage = nextMessage;
3645
+ render();
3646
+ },
3647
+ stop() {
3648
+ clearInterval(timer);
3649
+ (0, import_node_readline.clearLine)(import_node_process.stdout, 0);
3650
+ (0, import_node_readline.cursorTo)(import_node_process.stdout, 0);
3651
+ }
3652
+ };
3653
+ }
3619
3654
 
3620
3655
  // src/utils.ts
3621
3656
  function isJsonObject(value) {
@@ -3753,15 +3788,11 @@ async function maybeRun(dryRun, executable, args, options = {}) {
3753
3788
  }
3754
3789
  async function runCommand(executable, args, options = {}) {
3755
3790
  return new Promise((resolvePromise, rejectPromise) => {
3756
- const child = (0, import_node_child_process.spawn)(executable, args, { cwd: options.cwd });
3757
- const stdoutChunks = [];
3758
- const stderrChunks = [];
3759
3791
  const maxOutputBytes = options.maxOutputBytes ?? defaultCommandMaxOutputBytes();
3760
- let stdoutBytes = 0;
3761
- let stderrBytes = 0;
3762
3792
  let finished = false;
3763
- let failureResult;
3764
3793
  let killTimer;
3794
+ let killEscalationTimer;
3795
+ let failureMessage;
3765
3796
  let sentTerminationSignal = false;
3766
3797
  const timeoutMs = options.timeoutMs ?? defaultCommandTimeoutMs();
3767
3798
  const finish = (result) => {
@@ -3772,22 +3803,47 @@ async function runCommand(executable, args, options = {}) {
3772
3803
  if (killTimer) {
3773
3804
  clearTimeout(killTimer);
3774
3805
  }
3806
+ if (killEscalationTimer) {
3807
+ clearTimeout(killEscalationTimer);
3808
+ }
3775
3809
  if (result.exitCode !== 0 && options.allowFailure !== true) {
3776
3810
  rejectPromise(new Error(`${formatShellCommand(executable, args)} failed: ${result.stderr || result.stdout}`));
3777
3811
  return;
3778
3812
  }
3779
3813
  resolvePromise(result);
3780
3814
  };
3815
+ const child = (0, import_node_child_process.execFile)(
3816
+ executable,
3817
+ [...args],
3818
+ {
3819
+ cwd: options.cwd,
3820
+ encoding: "utf8",
3821
+ maxBuffer: maxOutputBytes
3822
+ },
3823
+ (err, stdout2, stderr) => {
3824
+ finish(
3825
+ commandResultFromExecFileCallback({
3826
+ args,
3827
+ err,
3828
+ executable,
3829
+ failureMessage,
3830
+ maxOutputBytes,
3831
+ stderr,
3832
+ stdout: stdout2
3833
+ })
3834
+ );
3835
+ }
3836
+ );
3781
3837
  const failAndKill = (message) => {
3782
- if (failureResult) {
3838
+ if (failureMessage) {
3783
3839
  return;
3784
3840
  }
3785
- failureResult = { exitCode: 124, stderr: message, stdout: stdoutChunks.join("") };
3841
+ failureMessage = message;
3786
3842
  if (!sentTerminationSignal) {
3787
3843
  sentTerminationSignal = true;
3788
3844
  child.kill("SIGTERM");
3789
3845
  }
3790
- setTimeout(() => {
3846
+ killEscalationTimer = setTimeout(() => {
3791
3847
  if (!finished) {
3792
3848
  child.kill("SIGKILL");
3793
3849
  }
@@ -3799,54 +3855,20 @@ async function runCommand(executable, args, options = {}) {
3799
3855
  }, timeoutMs);
3800
3856
  killTimer.unref?.();
3801
3857
  }
3802
- child.stdout.on("data", (chunk) => {
3803
- if (failureResult) {
3804
- return;
3805
- }
3806
- const text = String(chunk);
3807
- stdoutBytes += Buffer.byteLength(text);
3808
- if (stdoutBytes + stderrBytes > maxOutputBytes) {
3809
- failAndKill(`${formatShellCommand(executable, args)} exceeded output limit of ${maxOutputBytes} bytes`);
3810
- return;
3811
- }
3812
- stdoutChunks.push(text);
3813
- });
3814
- child.stderr.on("data", (chunk) => {
3815
- if (failureResult) {
3816
- return;
3817
- }
3818
- const text = String(chunk);
3819
- stderrBytes += Buffer.byteLength(text);
3820
- if (stdoutBytes + stderrBytes > maxOutputBytes) {
3821
- failAndKill(`${formatShellCommand(executable, args)} exceeded output limit of ${maxOutputBytes} bytes`);
3822
- return;
3823
- }
3824
- stderrChunks.push(text);
3825
- });
3826
- child.on("error", (err) => {
3827
- if (finished) {
3828
- return;
3829
- }
3830
- if (killTimer) {
3831
- clearTimeout(killTimer);
3832
- }
3833
- if (options.allowFailure === true) {
3834
- finish({ exitCode: 127, stderr: errorMessage(err), stdout: "" });
3835
- } else {
3836
- finished = true;
3837
- rejectPromise(err);
3838
- }
3839
- });
3840
- child.on("close", (code) => {
3841
- const result = failureResult ?? {
3842
- exitCode: code ?? 1,
3843
- stderr: stderrChunks.join(""),
3844
- stdout: stdoutChunks.join("")
3845
- };
3846
- finish(result);
3847
- });
3848
3858
  });
3849
3859
  }
3860
+ function commandResultFromExecFileCallback(params) {
3861
+ if (params.failureMessage) {
3862
+ return { exitCode: 124, stderr: params.failureMessage, stdout: params.stdout };
3863
+ }
3864
+ if (!params.err) {
3865
+ return { exitCode: 0, stderr: params.stderr, stdout: params.stdout };
3866
+ }
3867
+ const error = params.err;
3868
+ const exitCode = typeof error.code === "number" ? error.code : error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" ? 124 : 127;
3869
+ const stderr = error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" ? `${formatShellCommand(params.executable, params.args)} exceeded output limit of ${params.maxOutputBytes} bytes` : params.stderr || errorMessage(error);
3870
+ return { exitCode, stderr, stdout: params.stdout };
3871
+ }
3850
3872
  function defaultCommandTimeoutMs() {
3851
3873
  return positiveIntegerFromEnv("THREADNOTE_COMMAND_TIMEOUT_MS") ?? 10 * 60 * 1e3;
3852
3874
  }
@@ -7529,9 +7551,17 @@ var AUTO_REPAIR_STATE_FILE = "index-auto-repair.json";
7529
7551
  var AUTO_REPAIR_TTL_MS = 6 * 60 * 60 * 1e3;
7530
7552
  var MAX_SCAN_DEPTH = 5;
7531
7553
  var MAX_REPAIR_TARGETS = 4;
7532
- var MAINTENANCE_MAX_REPAIR_TARGETS = 16;
7554
+ var MAINTENANCE_COLLAPSE_DEPTH = 3;
7555
+ var MAINTENANCE_MAX_REPAIR_TARGETS = 512;
7533
7556
  async function repairStaleRecallIndex(config, ov, options = {}) {
7557
+ options.onProgress?.({ type: "scan-start" });
7534
7558
  const targets = await findStaleRecallIndexTargets(config, options);
7559
+ const repairTargets = targets.slice(0, options.maxTargets ?? MAX_REPAIR_TARGETS);
7560
+ options.onProgress?.({
7561
+ repairTargetCount: repairTargets.length,
7562
+ totalTargets: targets.length,
7563
+ type: "scan-complete"
7564
+ });
7535
7565
  if (targets.length === 0) {
7536
7566
  return { repairedUris: [], skippedRecentUris: [], warnings: [] };
7537
7567
  }
@@ -7540,16 +7570,20 @@ async function repairStaleRecallIndex(config, ov, options = {}) {
7540
7570
  const repairedUris = [];
7541
7571
  const skippedRecentUris = [];
7542
7572
  const warnings = [];
7543
- for (const target of targets.slice(0, options.maxTargets ?? MAX_REPAIR_TARGETS)) {
7573
+ for (const [targetIndex, target] of repairTargets.entries()) {
7574
+ const progressBase = { index: targetIndex + 1, target, total: repairTargets.length };
7544
7575
  const previous = state.entries[target.uri];
7545
7576
  if (previous?.signature === target.signature && now - Date.parse(previous.repairedAt) < AUTO_REPAIR_TTL_MS && options.dryRun !== true && options.ignoreBackoff !== true) {
7577
+ options.onProgress?.({ ...progressBase, type: "repair-skip-recent" });
7546
7578
  skippedRecentUris.push(target.uri);
7547
7579
  continue;
7548
7580
  }
7549
7581
  if (options.dryRun === true) {
7582
+ options.onProgress?.({ ...progressBase, type: "repair-dry-run" });
7550
7583
  repairedUris.push(target.uri);
7551
7584
  continue;
7552
7585
  }
7586
+ options.onProgress?.({ ...progressBase, type: "repair-start" });
7553
7587
  const result = await runCommand(
7554
7588
  ov,
7555
7589
  withIdentity(config, ["reindex", target.uri, "--mode", "semantic_and_vectors", "--wait", "true"]),
@@ -7560,6 +7594,7 @@ async function repairStaleRecallIndex(config, ov, options = {}) {
7560
7594
  state.entries[target.uri] = { repairedAt: new Date(now).toISOString(), signature: target.signature };
7561
7595
  } else {
7562
7596
  warnings.push(indexRepairWarning(target.uri, result));
7597
+ await options.onRepairFailure?.(target, result);
7563
7598
  }
7564
7599
  }
7565
7600
  if (options.dryRun !== true && repairedUris.length > 0) {
@@ -7576,16 +7611,14 @@ async function findStaleRecallIndexTargets(config, options = {}) {
7576
7611
  }
7577
7612
  const sidecars = await staleSidecars(root.path, root.uri);
7578
7613
  if (options.collapseToRoots === true) {
7579
- if (sidecars.length === 0) {
7580
- continue;
7581
- }
7582
- const parts = sidecars.map((sidecar) => `${sidecar.relativePath}
7614
+ for (const sidecar of sidecars) {
7615
+ const uri = collapseStaleSidecarUri(root.uri, sidecar.relativePath, options.collapseDepth);
7616
+ const current = byUri.get(uri) ?? { parts: [], staleCount: 0, uri };
7617
+ current.parts.push(`${sidecar.relativePath}
7583
7618
  ${sidecar.content}`);
7584
- byUri.set(root.uri, {
7585
- parts,
7586
- staleCount: sidecars.length,
7587
- uri: root.uri
7588
- });
7619
+ current.staleCount += 1;
7620
+ byUri.set(uri, current);
7621
+ }
7589
7622
  continue;
7590
7623
  }
7591
7624
  for (const sidecar of sidecars) {
@@ -7602,13 +7635,29 @@ ${sidecar.content}`);
7602
7635
  uri: target.uri
7603
7636
  }));
7604
7637
  }
7638
+ function collapseStaleSidecarUri(rootUri, relativePath, depth = 0) {
7639
+ const normalizedRootUri = trimLocalRootUri(rootUri);
7640
+ if (depth <= 0) {
7641
+ return normalizedRootUri;
7642
+ }
7643
+ const parentParts = relativePath.split("/").slice(0, -1);
7644
+ const collapsedParts = parentParts.slice(0, depth);
7645
+ return collapsedParts.length === 0 ? normalizedRootUri : `${normalizedRootUri}/${collapsedParts.join("/")}`;
7646
+ }
7605
7647
  function formatRecallIndexRepairMessages(result, options = {}) {
7606
7648
  const messages = [];
7607
- for (const uri of result.repairedUris) {
7649
+ const maxUris = options.maxUris ?? result.repairedUris.length;
7650
+ for (const uri of result.repairedUris.slice(0, maxUris)) {
7608
7651
  messages.push(
7609
7652
  `${options.dryRun === true ? "Would auto-reindex stale recall scope" : "Auto-reindexed stale recall scope"}: ${uri}`
7610
7653
  );
7611
7654
  }
7655
+ const hiddenUriCount = result.repairedUris.length - maxUris;
7656
+ if (hiddenUriCount > 0) {
7657
+ messages.push(
7658
+ options.dryRun === true ? `Would auto-reindex ${hiddenUriCount} more stale recall scope(s).` : `Auto-reindexed ${hiddenUriCount} more stale recall scope(s).`
7659
+ );
7660
+ }
7612
7661
  for (const warning2 of result.warnings) {
7613
7662
  messages.push(`Auto-index repair warning: ${warning2}`);
7614
7663
  }
@@ -11622,7 +11671,9 @@ async function runUpdate(config, options) {
11622
11671
  return;
11623
11672
  }
11624
11673
  const threadnoteCommand = await installedThreadnoteCommand(runtime);
11625
- await maybeRun(options.dryRun === true, threadnoteCommand, ["repair", "--no-post-update"]);
11674
+ console.log("");
11675
+ console.log("Repairing local Threadnote setup after package update.");
11676
+ await runThreadnoteSubcommand(options.dryRun === true, threadnoteCommand, ["repair", "--no-post-update"]);
11626
11677
  if (options.postUpdate !== false) {
11627
11678
  const postUpdateArgs = [
11628
11679
  "post-update",
@@ -11632,15 +11683,7 @@ async function runUpdate(config, options) {
11632
11683
  info2.latestVersion,
11633
11684
  ...options.yes === true ? ["--yes"] : []
11634
11685
  ];
11635
- if (options.dryRun === true) {
11636
- await maybeRun(true, threadnoteCommand, postUpdateArgs);
11637
- } else {
11638
- console.log(`Running: ${formatShellCommand(threadnoteCommand, postUpdateArgs)}`);
11639
- const postUpdateExitCode = await runInteractive(threadnoteCommand, postUpdateArgs);
11640
- if (postUpdateExitCode !== 0) {
11641
- throw new Error(`${formatShellCommand(threadnoteCommand, postUpdateArgs)} exited with ${postUpdateExitCode}.`);
11642
- }
11643
- }
11686
+ await runThreadnoteSubcommand(options.dryRun === true, threadnoteCommand, postUpdateArgs);
11644
11687
  } else {
11645
11688
  console.log("Skipping post-update migration prompts because --no-post-update was provided.");
11646
11689
  }
@@ -11742,9 +11785,11 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
11742
11785
  if (usingLaunchd) {
11743
11786
  const launchAgentPath = launchAgentPlistPath();
11744
11787
  await runCommand("launchctl", ["unload", launchAgentPath], { allowFailure: true });
11788
+ await waitForOpenVikingPortClosed(config, 15e3);
11745
11789
  await runCommand("launchctl", ["load", launchAgentPath], { allowFailure: true });
11746
11790
  } else {
11747
11791
  await maybeRun(false, threadnoteCommand, ["stop"]);
11792
+ await waitForOpenVikingPortClosed(config, 15e3);
11748
11793
  await maybeRun(false, threadnoteCommand, ["start"]);
11749
11794
  }
11750
11795
  const healthyAfter = await waitForOpenVikingHealthy(config, 1e4);
@@ -11755,6 +11800,17 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
11755
11800
  console.log("Check the server log or run: threadnote start");
11756
11801
  }
11757
11802
  }
11803
+ async function runThreadnoteSubcommand(dryRun, executable, args) {
11804
+ if (dryRun) {
11805
+ await maybeRun(true, executable, args);
11806
+ return;
11807
+ }
11808
+ console.log(`Running: ${formatShellCommand(executable, args)}`);
11809
+ const exitCode = await runInteractive(executable, args);
11810
+ if (exitCode !== 0) {
11811
+ throw new Error(`${formatShellCommand(executable, args)} exited with ${exitCode}.`);
11812
+ }
11813
+ }
11758
11814
  function openVikingHealthEndpoint(config) {
11759
11815
  return `http://${config.host}:${config.port}/health`;
11760
11816
  }
@@ -11778,12 +11834,27 @@ async function waitForOpenVikingHealthy(config, timeoutMs) {
11778
11834
  if (await isOpenVikingHealthy(config)) {
11779
11835
  return true;
11780
11836
  }
11781
- await new Promise((resolvePromise) => {
11782
- setTimeout(resolvePromise, 500);
11783
- });
11837
+ await sleep(500);
11784
11838
  }
11785
11839
  return isOpenVikingHealthy(config);
11786
11840
  }
11841
+ async function waitForOpenVikingPortClosed(config, timeoutMs) {
11842
+ console.log(`Waiting for OpenViking port ${config.host}:${config.port} to close before restart.`);
11843
+ const deadline = Date.now() + timeoutMs;
11844
+ while (Date.now() < deadline) {
11845
+ if (!await isTcpPortOpen(config.host, config.port, 300)) {
11846
+ return true;
11847
+ }
11848
+ await sleep(300);
11849
+ }
11850
+ if (!await isTcpPortOpen(config.host, config.port, 300)) {
11851
+ return true;
11852
+ }
11853
+ console.log(
11854
+ `Warning: OpenViking port ${config.host}:${config.port} is still in use after ${timeoutMs / 1e3}s; start may fail.`
11855
+ );
11856
+ return false;
11857
+ }
11787
11858
  function launchAgentPlistPath() {
11788
11859
  return (0, import_node_path12.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
11789
11860
  }
@@ -12314,16 +12385,44 @@ async function repairRecallIndex(config, dryRun) {
12314
12385
  console.log("Skipping recall index repair: neither ov nor openviking was found in PATH.");
12315
12386
  return;
12316
12387
  }
12388
+ const progress = startProgress("Scanning recall index freshness across memories and seeded resources.");
12317
12389
  try {
12318
12390
  const result = await repairStaleRecallIndex(config, ov, {
12391
+ collapseDepth: MAINTENANCE_COLLAPSE_DEPTH,
12319
12392
  collapseToRoots: true,
12320
12393
  dryRun,
12321
12394
  ignoreBackoff: true,
12322
12395
  includeAgentSkills: true,
12323
12396
  includeManifestResources: true,
12324
- maxTargets: MAINTENANCE_MAX_REPAIR_TARGETS
12397
+ maxTargets: MAINTENANCE_MAX_REPAIR_TARGETS,
12398
+ onRepairFailure: async () => {
12399
+ progress.update("A recall index reindex failed; checking OpenViking health before continuing.");
12400
+ await repairServerHealth(config, dryRun);
12401
+ },
12402
+ onProgress: (event) => {
12403
+ if (event.type === "scan-complete") {
12404
+ if (event.totalTargets === 0) {
12405
+ progress.update("No stale recall index scopes found.");
12406
+ } else {
12407
+ progress.update(
12408
+ `Found ${event.totalTargets} stale recall index scope(s); repairing ${event.repairTargetCount}.`
12409
+ );
12410
+ }
12411
+ } else if (event.type === "repair-start") {
12412
+ progress.update(
12413
+ `Reindexing ${event.index}/${event.total}: ${event.target.uri} (${event.target.staleCount} stale summaries).`
12414
+ );
12415
+ } else if (event.type === "repair-dry-run") {
12416
+ progress.update(
12417
+ `Planning reindex ${event.index}/${event.total}: ${event.target.uri} (${event.target.staleCount} stale summaries).`
12418
+ );
12419
+ } else if (event.type === "repair-skip-recent") {
12420
+ progress.update(`Skipping recently repaired scope ${event.index}/${event.total}: ${event.target.uri}.`);
12421
+ }
12422
+ }
12325
12423
  });
12326
- const messages = formatRecallIndexRepairMessages(result, { dryRun });
12424
+ progress.stop();
12425
+ const messages = formatRecallIndexRepairMessages(result, { dryRun, maxUris: 20 });
12327
12426
  if (messages.length === 0) {
12328
12427
  console.log("Recall index freshness OK.");
12329
12428
  return;
@@ -12332,6 +12431,7 @@ async function repairRecallIndex(config, dryRun) {
12332
12431
  console.log(message);
12333
12432
  }
12334
12433
  } catch (err) {
12434
+ progress.stop();
12335
12435
  console.log(`WARN could not repair recall index freshness: ${errorMessage(err)}`);
12336
12436
  }
12337
12437
  }
@@ -124,6 +124,10 @@ Threadnote uses its bundled stdio MCP adapter by default, even when the installe
124
124
  `/mcp`. The adapter adds Threadnote-specific tools and behavior such as shared-memory sync, exact recall fallback,
125
125
  seeded-resource recall augmentation, and recall-index repair.
126
126
 
127
+ The adapter also exposes raw OpenViking parity tools with `ov_*` names for native behaviors such as code symbol
128
+ navigation, watch management, raw search/read/list/store/remember, grep/glob, resource import, and forget. Prefer
129
+ Threadnote-named tools for memory workflows; use `ov_*` when you intentionally want native OpenViking behavior.
130
+
127
131
  Use the default stdio adapter:
128
132
 
129
133
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",