threadnote 0.7.7 → 0.7.9

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.
@@ -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
  }
@@ -3678,33 +3756,111 @@ async function runCommand(executable, args, options = {}) {
3678
3756
  const child = (0, import_node_child_process.spawn)(executable, args, { cwd: options.cwd });
3679
3757
  const stdoutChunks = [];
3680
3758
  const stderrChunks = [];
3759
+ const maxOutputBytes = options.maxOutputBytes ?? defaultCommandMaxOutputBytes();
3760
+ let stdoutBytes = 0;
3761
+ let stderrBytes = 0;
3762
+ let finished = false;
3763
+ let failureResult;
3764
+ let killTimer;
3765
+ let sentTerminationSignal = false;
3766
+ const timeoutMs = options.timeoutMs ?? defaultCommandTimeoutMs();
3767
+ const finish = (result) => {
3768
+ if (finished) {
3769
+ return;
3770
+ }
3771
+ finished = true;
3772
+ if (killTimer) {
3773
+ clearTimeout(killTimer);
3774
+ }
3775
+ if (result.exitCode !== 0 && options.allowFailure !== true) {
3776
+ rejectPromise(new Error(`${formatShellCommand(executable, args)} failed: ${result.stderr || result.stdout}`));
3777
+ return;
3778
+ }
3779
+ resolvePromise(result);
3780
+ };
3781
+ const failAndKill = (message) => {
3782
+ if (failureResult) {
3783
+ return;
3784
+ }
3785
+ failureResult = { exitCode: 124, stderr: message, stdout: stdoutChunks.join("") };
3786
+ if (!sentTerminationSignal) {
3787
+ sentTerminationSignal = true;
3788
+ child.kill("SIGTERM");
3789
+ }
3790
+ setTimeout(() => {
3791
+ if (!finished) {
3792
+ child.kill("SIGKILL");
3793
+ }
3794
+ }, 1e3).unref?.();
3795
+ };
3796
+ if (timeoutMs > 0) {
3797
+ killTimer = setTimeout(() => {
3798
+ failAndKill(`${formatShellCommand(executable, args)} timed out after ${timeoutMs}ms`);
3799
+ }, timeoutMs);
3800
+ killTimer.unref?.();
3801
+ }
3681
3802
  child.stdout.on("data", (chunk) => {
3682
- stdoutChunks.push(String(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);
3683
3813
  });
3684
3814
  child.stderr.on("data", (chunk) => {
3685
- stderrChunks.push(String(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);
3686
3825
  });
3687
3826
  child.on("error", (err) => {
3827
+ if (finished) {
3828
+ return;
3829
+ }
3830
+ if (killTimer) {
3831
+ clearTimeout(killTimer);
3832
+ }
3688
3833
  if (options.allowFailure === true) {
3689
- resolvePromise({ exitCode: 127, stderr: errorMessage(err), stdout: "" });
3834
+ finish({ exitCode: 127, stderr: errorMessage(err), stdout: "" });
3690
3835
  } else {
3836
+ finished = true;
3691
3837
  rejectPromise(err);
3692
3838
  }
3693
3839
  });
3694
3840
  child.on("close", (code) => {
3695
- const result = {
3841
+ const result = failureResult ?? {
3696
3842
  exitCode: code ?? 1,
3697
3843
  stderr: stderrChunks.join(""),
3698
3844
  stdout: stdoutChunks.join("")
3699
3845
  };
3700
- if (result.exitCode !== 0 && options.allowFailure !== true) {
3701
- rejectPromise(new Error(`${formatShellCommand(executable, args)} failed: ${result.stderr || result.stdout}`));
3702
- return;
3703
- }
3704
- resolvePromise(result);
3846
+ finish(result);
3705
3847
  });
3706
3848
  });
3707
3849
  }
3850
+ function defaultCommandTimeoutMs() {
3851
+ return positiveIntegerFromEnv("THREADNOTE_COMMAND_TIMEOUT_MS") ?? 10 * 60 * 1e3;
3852
+ }
3853
+ function defaultCommandMaxOutputBytes() {
3854
+ return positiveIntegerFromEnv("THREADNOTE_COMMAND_MAX_OUTPUT_BYTES") ?? 5 * 1024 * 1024;
3855
+ }
3856
+ function positiveIntegerFromEnv(name) {
3857
+ const value = process.env[name];
3858
+ if (value === void 0) {
3859
+ return void 0;
3860
+ }
3861
+ const parsed = Number.parseInt(value, 10);
3862
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
3863
+ }
3708
3864
  async function gitValue(args, cwd = getInvocationCwd()) {
3709
3865
  const result = await runCommand("git", args, { allowFailure: true, cwd });
3710
3866
  if (result.exitCode !== 0) {
@@ -4090,12 +4246,12 @@ function firstLine(value) {
4090
4246
  }
4091
4247
  function formatStatus(status) {
4092
4248
  if (status === "ok") {
4093
- return "OK ";
4249
+ return success("OK ");
4094
4250
  }
4095
4251
  if (status === "warn") {
4096
- return "WARN";
4252
+ return warning("WARN");
4097
4253
  }
4098
- return "FAIL";
4254
+ return failure("FAIL");
4099
4255
  }
4100
4256
  function formatShellCommand(executable, args) {
4101
4257
  return redactText([executable, ...args].map(shellQuote).join(" "));
@@ -4152,7 +4308,7 @@ async function runMcpInstall(config, agent, options) {
4152
4308
  });
4153
4309
  return;
4154
4310
  }
4155
- const command = buildMcpInstallCommand(config, agent, name, {
4311
+ const command2 = buildMcpInstallCommand(config, agent, name, {
4156
4312
  bearerTokenEnvVar: options.bearerTokenEnvVar,
4157
4313
  nativeHttp,
4158
4314
  scope: options.scope,
@@ -4161,11 +4317,11 @@ async function runMcpInstall(config, agent, options) {
4161
4317
  const removeCommand = buildMcpRemoveCommand(agent, name);
4162
4318
  if (!apply) {
4163
4319
  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}`);
4320
+ if (removeCommand.cwd || command2.cwd) {
4321
+ console.log(`Command working directory: ${removeCommand.cwd ?? command2.cwd}`);
4166
4322
  }
4167
4323
  console.log(formatShellCommand(removeCommand.executable, removeCommand.args));
4168
- console.log(formatShellCommand(command.executable, command.args));
4324
+ console.log(formatShellCommand(command2.executable, command2.args));
4169
4325
  printMcpSnippet(config, agent, name, { nativeHttp, scope: options.scope, url });
4170
4326
  return;
4171
4327
  }
@@ -4173,7 +4329,7 @@ async function runMcpInstall(config, agent, options) {
4173
4329
  allowFailure: true,
4174
4330
  cwd: removeCommand.cwd
4175
4331
  });
4176
- await maybeRun(false, command.executable, command.args, { cwd: command.cwd });
4332
+ await maybeRun(false, command2.executable, command2.args, { cwd: command2.cwd });
4177
4333
  }
4178
4334
  async function runCursorMcpInstall(config, name, options) {
4179
4335
  const path = cursorMcpConfigPath();
@@ -4244,8 +4400,8 @@ async function removeMcpConfigs(value, dryRun) {
4244
4400
  await removeCopilotMcpConfig(OPENVIKING_MCP_NAME, dryRun);
4245
4401
  continue;
4246
4402
  }
4247
- const command = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
4248
- await maybeRun(dryRun, command.executable, command.args, { allowFailure: true, cwd: command.cwd });
4403
+ const command2 = buildMcpRemoveCommand(client, OPENVIKING_MCP_NAME);
4404
+ await maybeRun(dryRun, command2.executable, command2.args, { allowFailure: true, cwd: command2.cwd });
4249
4405
  }
4250
4406
  }
4251
4407
  async function removeMcpSnippets(config, dryRun) {
@@ -4280,17 +4436,17 @@ function buildMcpInstallCommand(config, agent, name, options) {
4280
4436
  const claudeCwd = getInvocationCwd();
4281
4437
  const claudeScope = options.scope ?? "user";
4282
4438
  if (!options.nativeHttp) {
4283
- const command = mcpAdapterCommand();
4439
+ const command2 = mcpAdapterCommand();
4284
4440
  const env = mcpEnvironment(config);
4285
4441
  if (agent === "codex") {
4286
4442
  return {
4287
4443
  executable: "codex",
4288
- args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", "/usr/bin/env", ...command]
4444
+ args: ["mcp", "add", ...env.flatMap((value) => ["--env", value]), name, "--", "/usr/bin/env", ...command2]
4289
4445
  };
4290
4446
  }
4291
4447
  return {
4292
4448
  executable: "claude",
4293
- args: ["mcp", "add", "--scope", claudeScope, name, "--", "/usr/bin/env", ...env, ...command],
4449
+ args: ["mcp", "add", "--scope", claudeScope, name, "--", "/usr/bin/env", ...env, ...command2],
4294
4450
  cwd: claudeCwd
4295
4451
  };
4296
4452
  }
@@ -4472,12 +4628,12 @@ function printMcpSnippet(config, agent, name, options) {
4472
4628
  return;
4473
4629
  }
4474
4630
  const snippetPath = (0, import_node_path2.join)(config.agentContextHome, "mcp", `${name}.${agent}.${agent === "codex" ? "toml" : "txt"}`);
4475
- const command = buildMcpInstallCommand(config, agent, name, {
4631
+ const command2 = buildMcpInstallCommand(config, agent, name, {
4476
4632
  nativeHttp: options.nativeHttp,
4477
4633
  scope: options.scope,
4478
4634
  url: options.url
4479
4635
  });
4480
- const snippet2 = `${formatShellCommand(command.executable, command.args)}
4636
+ const snippet2 = `${formatShellCommand(command2.executable, command2.args)}
4481
4637
  `;
4482
4638
  console.log(`
4483
4639
  Snippet (${snippetPath}):
@@ -7960,9 +8116,9 @@ async function syncSharedReposBeforeAgentRead(config) {
7960
8116
  const remainingBehind = new Set(state.behindTeams);
7961
8117
  for (const team of syncTeams) {
7962
8118
  try {
7963
- const warning = await runShareSyncQuiet(config, state, { team });
7964
- if (warning) {
7965
- warnings.push(warning);
8119
+ const warning2 = await runShareSyncQuiet(config, state, { team });
8120
+ if (warning2) {
8121
+ warnings.push(warning2);
7966
8122
  } else {
7967
8123
  remainingBehind.delete(team);
7968
8124
  syncedTeams.push(team);
@@ -8156,7 +8312,14 @@ Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or
8156
8312
  }
8157
8313
  }
8158
8314
  if (options.push !== false) {
8159
- await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
8315
+ const pushResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
8316
+ if (dryRun) {
8317
+ console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME])}`);
8318
+ } else if (pushResult && pushResult.exitCode !== 0) {
8319
+ throw new Error(
8320
+ `git push failed in ${worktree}: ${pushResult.stderr.trim() || pushResult.stdout.trim() || "unknown error"}`
8321
+ );
8322
+ }
8160
8323
  }
8161
8324
  }
8162
8325
  async function runShareSyncQuiet(config, state, options) {
@@ -8204,6 +8367,58 @@ async function stageShareableChanges(dryRun, git, worktree) {
8204
8367
  const pathspecs = [":(top)README.md", ":(top).gitignore", ...SHAREABLE_MEMORY_KIND_DIRS.map((dir) => `:(top)${dir}`)];
8205
8368
  await maybeRun(dryRun, git, ["-C", worktree, "add", "--", ...pathspecs], { allowFailure: true });
8206
8369
  }
8370
+ async function publishShareGitChange(worktree, relativePath, commitMessage, options = {}) {
8371
+ const dryRun = options.dryRun === true;
8372
+ const push = options.push !== false;
8373
+ const verb = options.verb ?? "add";
8374
+ const git = await requiredExecutable("git");
8375
+ const messages = [];
8376
+ const stageArgs = verb === "rm" ? ["-C", worktree, "rm", relativePath] : ["-C", worktree, "add", "--", relativePath];
8377
+ const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
8378
+ if (stageResult) {
8379
+ messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
8380
+ }
8381
+ if (dryRun) {
8382
+ console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "commit", "-m", commitMessage])}`);
8383
+ } else {
8384
+ const commitResult = await runCommand(git, ["-C", worktree, "commit", "-m", commitMessage], { allowFailure: true });
8385
+ if (commitResult.exitCode !== 0) {
8386
+ const detail = commitResult.stdout.trim() || commitResult.stderr.trim();
8387
+ if (/nothing to commit|no changes added/i.test(detail)) {
8388
+ messages.push("git commit: nothing to commit (file already in tree)");
8389
+ } else {
8390
+ throw new Error(`git commit failed: ${detail || "unknown error"}`);
8391
+ }
8392
+ } else {
8393
+ messages.push(`git commit: ${commitResult.stdout.trim().split("\n").slice(0, 2).join(" ")}`);
8394
+ }
8395
+ }
8396
+ if (!push) {
8397
+ messages.push("git push skipped (push=false)");
8398
+ return messages;
8399
+ }
8400
+ const pushResult = await runGitCommand(
8401
+ dryRun,
8402
+ git,
8403
+ ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME],
8404
+ "git push failed"
8405
+ );
8406
+ if (pushResult) {
8407
+ messages.push(`git push: ${pushResult.stdout.trim() || pushResult.stderr.trim() || "ok"}`);
8408
+ }
8409
+ return messages;
8410
+ }
8411
+ async function runGitCommand(dryRun, git, args, failureLabel) {
8412
+ if (dryRun) {
8413
+ console.log(`Would run: ${formatShellCommand(git, args)}`);
8414
+ return void 0;
8415
+ }
8416
+ const result = await runCommand(git, args, { allowFailure: true });
8417
+ if (result.exitCode !== 0) {
8418
+ throw new Error(`${failureLabel}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`);
8419
+ }
8420
+ return result;
8421
+ }
8207
8422
  async function runSharePublish(config, sourceUri, options) {
8208
8423
  assertVikingUri(sourceUri);
8209
8424
  const team = await resolveTeam(config, options.team);
@@ -8250,24 +8465,24 @@ async function runSharePublish(config, sourceUri, options) {
8250
8465
  }
8251
8466
  await ensureSharedDirectoryChain(config, ov, targetUri, dryRun);
8252
8467
  await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
8253
- await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "publish");
8254
- const git = await requiredExecutable("git");
8255
8468
  const worktree = team.config.worktree;
8256
8469
  const relativePath = vikingUriToWorktreeRelative(config, targetUri, team.name);
8257
8470
  const message = options.message ?? `share: publish ${relativePath}`;
8258
- await maybeRun(dryRun, git, ["-C", worktree, "add", "--", relativePath]);
8259
- await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
8260
- if (options.push !== false) {
8261
- const pushResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
8262
- if (dryRun) {
8263
- console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME])}`);
8264
- } else if (pushResult && pushResult.exitCode !== 0) {
8265
- const detail = pushResult.stderr.trim() || pushResult.stdout.trim() || "unknown error";
8266
- throw new Error(
8267
- `Memory was committed locally but git push failed: ${detail}
8268
- Resolve the remote issue (auth, network, branch protection), then run: threadnote share sync`
8269
- );
8270
- }
8471
+ const gitMessages = await publishShareGitChange(worktree, relativePath, message, {
8472
+ dryRun,
8473
+ push: options.push
8474
+ });
8475
+ for (const gitMessage of gitMessages) {
8476
+ console.log(gitMessage);
8477
+ }
8478
+ try {
8479
+ await removeMemoryUri(config, ov, sourceUri, dryRun);
8480
+ } catch (err) {
8481
+ throw new Error(
8482
+ `Published ${sourceUri} -> ${targetUri}, but could not remove the personal source. Retry cleanup later with: threadnote forget ${sourceUri}
8483
+ ${err instanceof Error ? err.message : String(err)}`,
8484
+ { cause: err }
8485
+ );
8271
8486
  }
8272
8487
  console.log(`Published ${sourceUri} -> ${targetUri}`);
8273
8488
  }
@@ -8287,15 +8502,25 @@ async function runShareUnpublish(config, sourceUri, options) {
8287
8502
  );
8288
8503
  }
8289
8504
  await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
8290
- await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "unpublish");
8291
- const git = await requiredExecutable("git");
8292
8505
  const worktree = team.config.worktree;
8293
8506
  const relativePath = vikingUriToWorktreeRelative(config, sourceUri, team.name);
8294
8507
  const message = options.message ?? `share: unpublish ${relativePath}`;
8295
- await maybeRun(dryRun, git, ["-C", worktree, "rm", relativePath], { allowFailure: true });
8296
- await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
8297
- if (options.push !== false) {
8298
- await maybeRun(dryRun, git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
8508
+ const gitMessages = await publishShareGitChange(worktree, relativePath, message, {
8509
+ dryRun,
8510
+ push: options.push,
8511
+ verb: "rm"
8512
+ });
8513
+ for (const gitMessage of gitMessages) {
8514
+ console.log(gitMessage);
8515
+ }
8516
+ try {
8517
+ await removeMemoryUri(config, ov, sourceUri, dryRun);
8518
+ } catch (err) {
8519
+ throw new Error(
8520
+ `Unpublished ${sourceUri} -> ${targetUri}, but could not remove the shared OpenViking source. Retry cleanup later with: threadnote forget ${sourceUri}
8521
+ ${err instanceof Error ? err.message : String(err)}`,
8522
+ { cause: err }
8523
+ );
8299
8524
  }
8300
8525
  console.log(`Unpublished ${sourceUri} -> ${targetUri}`);
8301
8526
  }
@@ -8315,9 +8540,98 @@ async function runShareList(config, _options) {
8315
8540
  console.log(` added: ${team.addedAt}`);
8316
8541
  }
8317
8542
  }
8543
+ async function runShareRename(config, options) {
8544
+ const oldTeam = await resolveTeam(config, options.team);
8545
+ const newName = normalizeTeamName(options.to);
8546
+ if (newName === oldTeam.name) {
8547
+ throw new Error(`Team is already named "${newName}".`);
8548
+ }
8549
+ const dryRun = options.dryRun === true;
8550
+ const teamsFile = await readTeamsFile(config);
8551
+ if (teamsFile.teams[newName]) {
8552
+ throw new Error(`Team "${newName}" is already configured.`);
8553
+ }
8554
+ const newWorktree = teamWorktreePath(config, newName);
8555
+ const newGitdir = teamGitdirPath(config, newName);
8556
+ await assertDestinationAbsent(newWorktree, "worktree");
8557
+ await assertDestinationAbsent(newGitdir, "gitdir");
8558
+ const updatedTeam = {
8559
+ ...oldTeam.config,
8560
+ gitdir: newGitdir,
8561
+ name: newName,
8562
+ worktree: newWorktree
8563
+ };
8564
+ const updatedTeams = {};
8565
+ for (const [name, value] of Object.entries(teamsFile.teams)) {
8566
+ if (name === oldTeam.name) {
8567
+ updatedTeams[newName] = updatedTeam;
8568
+ } else {
8569
+ updatedTeams[name] = value;
8570
+ }
8571
+ }
8572
+ const updatedFile = {
8573
+ defaultTeam: teamsFile.defaultTeam === oldTeam.name ? newName : teamsFile.defaultTeam,
8574
+ teams: updatedTeams,
8575
+ version: TEAMS_FILE_VERSION
8576
+ };
8577
+ if (dryRun) {
8578
+ console.log(`Would rename worktree: ${portablePath(oldTeam.config.worktree)} -> ${portablePath(newWorktree)}`);
8579
+ console.log(`Would rename gitdir: ${portablePath(oldTeam.config.gitdir)} -> ${portablePath(newGitdir)}`);
8580
+ console.log(`Would update git core.worktree for team "${newName}".`);
8581
+ console.log(`Would reindex shared memories under team "${newName}" and remove old shared URI tree.`);
8582
+ console.log(`Would write teams file: ${teamsFilePath(config)}`);
8583
+ return;
8584
+ }
8585
+ await (0, import_promises4.rename)(oldTeam.config.worktree, newWorktree);
8586
+ await (0, import_promises4.rename)(oldTeam.config.gitdir, newGitdir);
8587
+ await writeTeamsFile(config, updatedFile);
8588
+ const git = await requiredExecutable("git");
8589
+ await runCommand(git, ["-C", newWorktree, "config", "core.worktree", newWorktree]);
8590
+ const ingested = await ingestWorktreeFiles(config, updatedTeam, "replace");
8591
+ const ov = await openVikingCliForMode(false);
8592
+ await removeMemoryUri(
8593
+ config,
8594
+ ov,
8595
+ `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${oldTeam.name}`,
8596
+ false
8597
+ );
8598
+ console.log(`Renamed shared team "${oldTeam.name}" -> "${newName}".`);
8599
+ console.log(`Reindexed ${ingested} shared memory file(s).`);
8600
+ }
8601
+ async function runShareSetUrl(config, remoteUrl, options) {
8602
+ if (!remoteUrl.trim()) {
8603
+ throw new Error("Provide a git remote URL.");
8604
+ }
8605
+ const team = await resolveTeam(config, options.team);
8606
+ const dryRun = options.dryRun === true;
8607
+ const git = await requiredExecutable("git");
8608
+ if (dryRun) {
8609
+ console.log(
8610
+ `Would run: ${formatShellCommand(git, ["-C", team.config.worktree, "remote", "set-url", DEFAULT_GIT_REMOTE_NAME, remoteUrl])}`
8611
+ );
8612
+ console.log(
8613
+ `Would run: ${formatShellCommand(git, ["-C", team.config.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME])}`
8614
+ );
8615
+ console.log(`Would write teams file: ${teamsFilePath(config)}`);
8616
+ return;
8617
+ }
8618
+ await runCommand(git, ["-C", team.config.worktree, "remote", "set-url", DEFAULT_GIT_REMOTE_NAME, remoteUrl]);
8619
+ await runCommand(git, ["-C", team.config.worktree, "fetch", DEFAULT_GIT_REMOTE_NAME]);
8620
+ const teamsFile = await readTeamsFile(config);
8621
+ const updatedTeam = { ...team.config, remote: remoteUrl };
8622
+ await writeTeamsFile(config, {
8623
+ ...teamsFile,
8624
+ teams: { ...teamsFile.teams, [team.name]: updatedTeam }
8625
+ });
8626
+ console.log(`Updated shared team "${team.name}" remote: ${remoteUrl}`);
8627
+ }
8318
8628
  async function runShareRemove(config, options) {
8319
8629
  const team = await resolveTeam(config, options.team);
8320
8630
  const dryRun = options.dryRun === true;
8631
+ if (options.preserveLocal === true) {
8632
+ const preserved = await preserveSharedMemoriesLocally(config, team.config, dryRun);
8633
+ console.log(`${dryRun ? "Would preserve" : "Preserved"} ${preserved} shared durable memory file(s) locally.`);
8634
+ }
8321
8635
  const teamsFile = await readTeamsFile(config);
8322
8636
  const remaining = {};
8323
8637
  for (const [name, value] of Object.entries(teamsFile.teams)) {
@@ -8341,6 +8655,47 @@ async function runShareRemove(config, options) {
8341
8655
  console.log(`Keeping files at ${portablePath(team.config.worktree)} and ${portablePath(team.config.gitdir)}`);
8342
8656
  }
8343
8657
  }
8658
+ async function assertDestinationAbsent(path, label) {
8659
+ if (await exists(path)) {
8660
+ throw new Error(`Cannot rename share: destination ${label} already exists at ${path}.`);
8661
+ }
8662
+ }
8663
+ async function preserveSharedMemoriesLocally(config, team, dryRun) {
8664
+ const ov = await openVikingCliForMode(dryRun);
8665
+ const files = await walkMemoryFiles(team.worktree);
8666
+ let preserved = 0;
8667
+ for (const file of files) {
8668
+ const rel = (0, import_node_path5.relative)(team.worktree, file).split(import_node_path5.sep).join("/");
8669
+ if (!rel.startsWith("durable/")) {
8670
+ continue;
8671
+ }
8672
+ const targetUri = `viking://user/${uriSegment(config.user)}/memories/${rel}`;
8673
+ const content = await (0, import_promises4.readFile)(file, "utf8");
8674
+ if (dryRun) {
8675
+ console.log(`Would preserve ${rel} -> ${targetUri}`);
8676
+ } else {
8677
+ await ensurePersonalDirectoryChain(config, ov, parentUri(targetUri));
8678
+ await writeMemoryFile(config, ov, targetUri, content, "create", false);
8679
+ }
8680
+ preserved += 1;
8681
+ }
8682
+ return preserved;
8683
+ }
8684
+ async function ensurePersonalDirectoryChain(config, ov, directoryUri) {
8685
+ const prefix = "viking://";
8686
+ const parts = directoryUri.startsWith(prefix) ? directoryUri.slice(prefix.length).split("/").filter(Boolean) : [];
8687
+ const startIndex = parts[0] === "user" && parts.length > 2 ? 3 : 1;
8688
+ for (let index = startIndex; index <= parts.length; index += 1) {
8689
+ const uri = `${prefix}${parts.slice(0, index).join("/")}`;
8690
+ const statResult = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
8691
+ if (statResult.exitCode !== 0) {
8692
+ await runCommand(
8693
+ ov,
8694
+ withIdentity(config, ["mkdir", uri, "--description", "Threadnote lifecycle-aware local memories."])
8695
+ );
8696
+ }
8697
+ }
8698
+ }
8344
8699
  function normalizeTeamName(input2) {
8345
8700
  const candidate = (input2 ?? "default").trim();
8346
8701
  if (!candidate) {
@@ -8724,61 +9079,20 @@ async function waitForOvQueue(ov, config, options = {}) {
8724
9079
  console.error(result.stderr.trim());
8725
9080
  }
8726
9081
  }
8727
- function isTransientOvFailure(stderr, stdout) {
9082
+ function isTransientOvFailure(stderr, stdout2) {
8728
9083
  const output2 = `${stderr}
8729
- ${stdout}`.toLowerCase();
9084
+ ${stdout2}`.toLowerCase();
8730
9085
  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");
8731
9086
  }
8732
- function isResourceBusyFailure(stderr, stdout) {
9087
+ function isResourceBusyFailure(stderr, stdout2) {
8733
9088
  const output2 = `${stderr}
8734
- ${stdout}`.toLowerCase();
9089
+ ${stdout2}`.toLowerCase();
8735
9090
  return output2.includes("resource is busy") || output2.includes("resource is being processed");
8736
9091
  }
8737
9092
  async function ingestSingleFile(ov, config, uri, filePath, initialMode, options = {}) {
8738
9093
  const content = await (0, import_promises4.readFile)(filePath, "utf8");
8739
9094
  await writeMemoryFile(config, ov, uri, content, initialMode, false, options);
8740
9095
  }
8741
- async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
8742
- try {
8743
- await removeMemoryUri(config, ov, sourceUri, dryRun);
8744
- } catch (sourceErr) {
8745
- if (dryRun) {
8746
- throw sourceErr;
8747
- }
8748
- console.error(
8749
- `Source removal failed during ${label}; rolling back ${rollbackUri} so the system is back to the pre-${label} state.`
8750
- );
8751
- try {
8752
- await removeMemoryUri(config, ov, rollbackUri, false);
8753
- } catch (rollbackErr) {
8754
- console.error(
8755
- `Rollback of ${rollbackUri} also failed. Manual cleanup needed via: threadnote forget ${rollbackUri}
8756
- Rollback error: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`
8757
- );
8758
- }
8759
- await bestEffortRemoveWorktreeFile(rollbackUri, worktree, label);
8760
- throw sourceErr;
8761
- }
8762
- }
8763
- async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
8764
- if (label !== "publish") {
8765
- return;
8766
- }
8767
- const prefix = "viking://";
8768
- if (!rollbackUri.startsWith(prefix)) {
8769
- return;
8770
- }
8771
- const parts = rollbackUri.slice(prefix.length).split("/");
8772
- const sharedIndex = parts.indexOf("shared");
8773
- if (sharedIndex === -1 || sharedIndex + 2 >= parts.length) {
8774
- return;
8775
- }
8776
- const relative3 = parts.slice(sharedIndex + 2).join("/");
8777
- if (!relative3) {
8778
- return;
8779
- }
8780
- await (0, import_promises4.rm)((0, import_node_path5.join)(worktree, relative3), { force: true });
8781
- }
8782
9096
  async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
8783
9097
  const args = withIdentity(config, ["rm", uri]);
8784
9098
  if (dryRun) {
@@ -9166,8 +9480,8 @@ async function syncSharedReposAndLog(config) {
9166
9480
  if (syncResult.syncedTeams.length > 0) {
9167
9481
  console.error(`Auto-synced shared memories: ${syncResult.syncedTeams.join(", ")}`);
9168
9482
  }
9169
- for (const warning of syncResult.warnings) {
9170
- console.error(`Auto-sync warning: ${warning}`);
9483
+ for (const warning2 of syncResult.warnings) {
9484
+ console.error(`Auto-sync warning: ${warning2}`);
9171
9485
  }
9172
9486
  } catch (err) {
9173
9487
  console.error(`Auto-sync warning: ${err instanceof Error ? err.message : String(err)}`);
@@ -9308,11 +9622,11 @@ function localMemoryPathForUri(config, uri) {
9308
9622
  if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
9309
9623
  return void 0;
9310
9624
  }
9311
- const relative3 = uri.slice(prefix.length);
9312
- if (relative3.includes("..") || relative3.startsWith("/")) {
9625
+ const relative4 = uri.slice(prefix.length);
9626
+ if (relative4.includes("..") || relative4.startsWith("/")) {
9313
9627
  return void 0;
9314
9628
  }
9315
- return (0, import_node_path6.join)(localUserMemoriesRoot(config), ...relative3.split("/"));
9629
+ return (0, import_node_path6.join)(localUserMemoriesRoot(config), ...relative4.split("/"));
9316
9630
  }
9317
9631
  async function runList(config, uri, options) {
9318
9632
  assertVikingUri(uri);
@@ -9546,62 +9860,20 @@ async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
9546
9860
  }
9547
9861
  await ensureSharedDirectoryChain(config, ov, targetUri, options.dryRun);
9548
9862
  await writeMemoryFile(config, ov, targetUri, memory, "replace", options.dryRun);
9549
- const git = await requiredExecutable("git");
9550
- await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "add", "--", relativePath]);
9551
- await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "commit", "-m", `share: update ${relativePath}`], {
9552
- allowFailure: true
9553
- });
9554
- await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "push", DEFAULT_GIT_REMOTE_NAME], {
9555
- allowFailure: true
9863
+ const gitMessages = await publishShareGitChange(team.config.worktree, relativePath, `share: update ${relativePath}`, {
9864
+ dryRun: options.dryRun
9556
9865
  });
9866
+ for (const message of gitMessages) {
9867
+ console.log(message);
9868
+ }
9557
9869
  for (const redaction of scrub.redactions) {
9558
9870
  console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
9559
9871
  }
9560
9872
  console.log(`Updated shared memory: ${targetUri}`);
9561
9873
  }
9562
9874
  async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode) {
9563
- const args = withIdentity(config, [
9564
- "write",
9565
- memoryUri,
9566
- "--from-file",
9567
- memoryPath,
9568
- "--mode",
9569
- writeMode,
9570
- "--wait",
9571
- "--timeout",
9572
- "120"
9573
- ]);
9574
- for (let attempt = 0; attempt < 4; attempt += 1) {
9575
- console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
9576
- const result = await runCommand(ov, args, { allowFailure: true });
9577
- if (result.exitCode === 0) {
9578
- if (result.stdout.trim()) {
9579
- console.log(result.stdout.trim());
9580
- }
9581
- if (result.stderr.trim()) {
9582
- console.error(result.stderr.trim());
9583
- }
9584
- return;
9585
- }
9586
- if (await vikingResourceExists2(ov, config, memoryUri)) {
9587
- console.log("OpenViking accepted the memory but returned before the wait completed; waiting for indexing.");
9588
- await waitForOpenVikingQueue(ov, config);
9589
- return;
9590
- }
9591
- if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
9592
- throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
9593
- }
9594
- await sleep(1e3 * (attempt + 1));
9595
- }
9596
- }
9597
- async function waitForOpenVikingQueue(ov, config) {
9598
- const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
9599
- if (result.stdout.trim()) {
9600
- console.log(result.stdout.trim());
9601
- }
9602
- if (result.stderr.trim()) {
9603
- console.error(result.stderr.trim());
9604
- }
9875
+ const content = await (0, import_promises5.readFile)(memoryPath, "utf8");
9876
+ await writeMemoryFile(config, ov, memoryUri, content, writeMode, false);
9605
9877
  }
9606
9878
  async function removeVikingResourceWithRetry(ov, config, uri) {
9607
9879
  const args = withIdentity(config, ["rm", uri]);
@@ -9628,8 +9900,8 @@ async function removeVikingResourceWithRetry(ov, config, uri) {
9628
9900
  return false;
9629
9901
  }
9630
9902
  async function vikingResourceExists2(ov, config, uri) {
9631
- const stat4 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
9632
- return stat4.exitCode === 0;
9903
+ const stat5 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
9904
+ return stat5.exitCode === 0;
9633
9905
  }
9634
9906
  async function ensureDurableMemoryDirectory(ov, config) {
9635
9907
  await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
@@ -9995,9 +10267,9 @@ function localUserMemoriesRoot(config) {
9995
10267
  function uniqueStrings(values) {
9996
10268
  return [...new Set(values)].sort();
9997
10269
  }
9998
- function isResourceBusy(stderr, stdout) {
10270
+ function isResourceBusy(stderr, stdout2) {
9999
10271
  const output2 = `${stderr}
10000
- ${stdout}`.toLowerCase();
10272
+ ${stdout2}`.toLowerCase();
10001
10273
  return output2.includes("resource is busy") || output2.includes("resource is being processed");
10002
10274
  }
10003
10275
  async function buildHandoff(options) {
@@ -10787,7 +11059,7 @@ var import_node_fs6 = require("node:fs");
10787
11059
  var import_promises11 = require("node:fs/promises");
10788
11060
  var import_node_os6 = require("node:os");
10789
11061
  var import_node_path12 = require("node:path");
10790
- var import_node_process2 = require("node:process");
11062
+ var import_node_process3 = require("node:process");
10791
11063
  var import_promises12 = require("node:readline/promises");
10792
11064
 
10793
11065
  // src/update.ts
@@ -10796,60 +11068,163 @@ var import_promises9 = require("node:fs/promises");
10796
11068
  var import_node_os5 = require("node:os");
10797
11069
  var import_node_path11 = require("node:path");
10798
11070
  var import_promises10 = require("node:readline/promises");
10799
- var import_node_process = require("node:process");
10800
- var NPM_PACKAGE_NAME = "threadnote";
10801
- var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
10802
- var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
10803
- var POST_UPDATE_MIGRATIONS_FILE = "post-update-migrations.json";
10804
- var POST_UPDATE_STATE_FILE = "post-update-state.json";
10805
- function parseUpdateRuntime(value) {
10806
- if (value === "auto" || value === "npm" || value === "bun" || value === "deno") {
10807
- return value;
11071
+ var import_node_process2 = require("node:process");
11072
+
11073
+ // src/release_notes.ts
11074
+ var GITHUB_RELEASES_URL = "https://api.github.com/repos/Kashkovsky/threadnote/releases?per_page=100";
11075
+ var RELEASE_FETCH_TIMEOUT_MS = 3500;
11076
+ async function fetchThreadnoteReleaseNotes() {
11077
+ const response = await fetch(GITHUB_RELEASES_URL, {
11078
+ headers: {
11079
+ accept: "application/vnd.github+json",
11080
+ "user-agent": "threadnote-cli"
11081
+ },
11082
+ signal: AbortSignal.timeout(RELEASE_FETCH_TIMEOUT_MS)
11083
+ });
11084
+ if (!response.ok) {
11085
+ throw new Error(`GitHub releases returned HTTP ${response.status}`);
10808
11086
  }
10809
- throw new Error(`Invalid update runtime: ${value}. Expected auto, npm, bun, or deno.`);
11087
+ const parsed = await response.json();
11088
+ if (!Array.isArray(parsed)) {
11089
+ throw new Error("GitHub releases response was not an array.");
11090
+ }
11091
+ return parsed.flatMap(parseGithubRelease);
10810
11092
  }
10811
- async function maybeNotifyUpdate(config, options = {}) {
10812
- if (isUpdateNotificationDisabled()) {
10813
- return;
11093
+ function releasesBetween(releases, currentVersion, latestVersion) {
11094
+ return releases.filter(
11095
+ (release) => compareVersions(release.version, currentVersion) > 0 && compareVersions(release.version, latestVersion) <= 0
11096
+ ).sort((left, right) => compareVersions(left.version, right.version));
11097
+ }
11098
+ function releaseForVersion(releases, version) {
11099
+ return releases.filter((release) => compareVersions(release.version, version) === 0).slice(0, 1);
11100
+ }
11101
+ function formatWhatsNew(releases) {
11102
+ if (releases.length === 0) {
11103
+ return [];
10814
11104
  }
10815
- try {
10816
- const info = await getUpdateInfo(config, {
10817
- allowCacheWrite: options.dryRun !== true,
10818
- preferFresh: false,
10819
- registry: updateRegistry()
10820
- });
10821
- if (!info.isUpdateAvailable) {
10822
- return;
11105
+ const lines = ["What's new:"];
11106
+ for (const release of releases) {
11107
+ lines.push(`${release.version}: ${release.title || "Release notes"}`);
11108
+ for (const line of bodyLines(release.body)) {
11109
+ lines.push(` ${line}`);
11110
+ }
11111
+ }
11112
+ return lines;
11113
+ }
11114
+ async function whatsNewLinesForVersion(version) {
11115
+ try {
11116
+ const releases = await fetchThreadnoteReleaseNotes();
11117
+ const lines = formatWhatsNew(releaseForVersion(releases, version));
11118
+ return lines.length > 0 ? lines : ["What's new:", `No GitHub release notes found for ${version}.`];
11119
+ } catch (err) {
11120
+ return [`What's new: unavailable (${errorMessage(err)})`];
11121
+ }
11122
+ }
11123
+ async function whatsNewLinesForVersionRange(currentVersion, latestVersion) {
11124
+ try {
11125
+ const releases = await fetchThreadnoteReleaseNotes();
11126
+ const lines = formatWhatsNew(releasesBetween(releases, currentVersion, latestVersion));
11127
+ return lines.length > 0 ? lines : ["What's new:", "No GitHub release notes found for this version range."];
11128
+ } catch (err) {
11129
+ return [`What's new: unavailable (${errorMessage(err)})`];
11130
+ }
11131
+ }
11132
+ function parseGithubRelease(value) {
11133
+ if (!isJsonObject(value) || value.draft === true || value.prerelease === true) {
11134
+ return [];
11135
+ }
11136
+ const tagName = typeof value.tag_name === "string" ? value.tag_name : "";
11137
+ const version = tagName.trim().replace(/^v/, "");
11138
+ if (!/^\d+\.\d+\.\d+(?:[-.A-Za-z0-9]+)?$/.test(version)) {
11139
+ return [];
11140
+ }
11141
+ const name = typeof value.name === "string" ? value.name : tagName;
11142
+ const body = typeof value.body === "string" ? value.body : "";
11143
+ const url = typeof value.html_url === "string" ? value.html_url : void 0;
11144
+ return [{ body, title: titleFromReleaseName(name, version), url, version }];
11145
+ }
11146
+ function titleFromReleaseName(name, version) {
11147
+ const trimmed = name.trim();
11148
+ const withoutPrefix = trimmed.replace(new RegExp(`^v?${escapeRegExp2(version)}:\\s*`, "i"), "");
11149
+ return withoutPrefix || trimmed || "Release notes";
11150
+ }
11151
+ function bodyLines(body) {
11152
+ const out = [];
11153
+ for (const rawLine of body.split("\n")) {
11154
+ const line = rawLine.trim();
11155
+ if (!line || /^#{1,6}\s+/.test(line)) {
11156
+ continue;
11157
+ }
11158
+ if (/^[-*]\s+/.test(line)) {
11159
+ out.push(`- ${line.replace(/^[-*]\s+/, "")}`);
11160
+ } else {
11161
+ out.push(line);
11162
+ }
11163
+ }
11164
+ return out;
11165
+ }
11166
+ function escapeRegExp2(value) {
11167
+ return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
11168
+ }
11169
+
11170
+ // src/update.ts
11171
+ var NPM_PACKAGE_NAME = "threadnote";
11172
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
11173
+ var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
11174
+ var POST_UPDATE_MIGRATIONS_FILE = "post-update-migrations.json";
11175
+ var POST_UPDATE_STATE_FILE = "post-update-state.json";
11176
+ function parseUpdateRuntime(value) {
11177
+ if (value === "auto" || value === "npm" || value === "bun" || value === "deno") {
11178
+ return value;
11179
+ }
11180
+ throw new Error(`Invalid update runtime: ${value}. Expected auto, npm, bun, or deno.`);
11181
+ }
11182
+ async function maybeNotifyUpdate(config, options = {}) {
11183
+ if (isUpdateNotificationDisabled()) {
11184
+ return;
11185
+ }
11186
+ try {
11187
+ const info2 = await getUpdateInfo(config, {
11188
+ allowCacheWrite: options.dryRun !== true,
11189
+ preferFresh: false,
11190
+ registry: updateRegistry()
11191
+ });
11192
+ if (!info2.isUpdateAvailable) {
11193
+ return;
10823
11194
  }
10824
11195
  console.log("");
10825
- console.log(`Update available: threadnote ${info.currentVersion} -> ${info.latestVersion}`);
10826
- console.log("Run: threadnote update");
11196
+ console.log(warning(`Update available: threadnote ${info2.currentVersion} -> ${info2.latestVersion}`));
11197
+ console.log(`Run: ${info("threadnote update")}`);
10827
11198
  } catch (_err) {
10828
11199
  return;
10829
11200
  }
10830
11201
  }
10831
11202
  async function runUpdate(config, options) {
10832
11203
  const registry = normalizeRegistry(options.registry ?? updateRegistry());
10833
- const info = await getUpdateInfo(config, {
10834
- allowCacheWrite: options.dryRun !== true,
10835
- preferFresh: true,
10836
- registry
10837
- });
10838
- console.log(`Current version: ${info.currentVersion}`);
10839
- console.log(`Latest version: ${info.latestVersion}`);
10840
- console.log(`Registry: ${info.registry}`);
11204
+ const info2 = await withSpinner(
11205
+ "Checking npm for latest threadnote version",
11206
+ () => getUpdateInfo(config, {
11207
+ allowCacheWrite: options.dryRun !== true,
11208
+ preferFresh: true,
11209
+ registry
11210
+ })
11211
+ );
11212
+ console.log(keyValue("Current version", info(info2.currentVersion)));
11213
+ console.log(keyValue("Latest version", info(info2.latestVersion)));
11214
+ console.log(keyValue("Registry", info2.registry));
10841
11215
  if (options.check === true) {
10842
- if (info.isUpdateAvailable) {
10843
- console.log(`Update available. Run: threadnote update`);
11216
+ if (info2.isUpdateAvailable) {
11217
+ console.log(warning("Update available. Run: threadnote update"));
11218
+ await printWhatsNewIfAvailable(info2);
10844
11219
  } else {
10845
11220
  console.log(
10846
- compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
11221
+ compareVersions(info2.currentVersion, info2.latestVersion) > 0 ? warning("Current version is newer than npm latest.") : success("Threadnote is up to date.")
10847
11222
  );
10848
11223
  }
10849
11224
  return;
10850
11225
  }
10851
- if (!info.isUpdateAvailable && options.force !== true) {
10852
- console.log("Threadnote is up to date.");
11226
+ if (!info2.isUpdateAvailable && options.force !== true) {
11227
+ console.log(success("Threadnote is up to date."));
10853
11228
  return;
10854
11229
  }
10855
11230
  const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
@@ -10857,6 +11232,7 @@ async function runUpdate(config, options) {
10857
11232
  await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
10858
11233
  if (options.repair === false) {
10859
11234
  console.log("Skipping repair because --no-repair was provided.");
11235
+ await printWhatsNewIfAvailable(info2);
10860
11236
  return;
10861
11237
  }
10862
11238
  const threadnoteCommand = await installedThreadnoteCommand(runtime);
@@ -10865,9 +11241,9 @@ async function runUpdate(config, options) {
10865
11241
  const postUpdateArgs = [
10866
11242
  "post-update",
10867
11243
  "--from-version",
10868
- info.currentVersion,
11244
+ info2.currentVersion,
10869
11245
  "--to-version",
10870
- info.latestVersion,
11246
+ info2.latestVersion,
10871
11247
  ...options.yes === true ? ["--yes"] : []
10872
11248
  ];
10873
11249
  if (options.dryRun === true) {
@@ -10885,6 +11261,20 @@ async function runUpdate(config, options) {
10885
11261
  console.log(
10886
11262
  "Update complete. Restart Cursor, Copilot, Codex, Claude, or open a fresh agent session so MCP tools reload."
10887
11263
  );
11264
+ await printWhatsNewIfAvailable(info2);
11265
+ }
11266
+ async function printWhatsNewIfAvailable(info2) {
11267
+ if (!info2.isUpdateAvailable) {
11268
+ return;
11269
+ }
11270
+ console.log("");
11271
+ const whatsNew = await withSpinner(
11272
+ "Fetching GitHub release notes",
11273
+ () => whatsNewLinesForVersionRange(info2.currentVersion, info2.latestVersion)
11274
+ );
11275
+ for (const line of whatsNew) {
11276
+ console.log(line === "What's new:" ? heading(line) : line);
11277
+ }
10888
11278
  }
10889
11279
  async function runPostUpdate(config, options) {
10890
11280
  if (!options.fromVersion || !options.toVersion) {
@@ -11202,7 +11592,7 @@ function printPostUpdateMigration(migration) {
11202
11592
  }
11203
11593
  }
11204
11594
  async function confirmPostUpdateMigration(prompt, defaultYes = false) {
11205
- const readline = (0, import_promises10.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
11595
+ const readline = (0, import_promises10.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
11206
11596
  try {
11207
11597
  const answer = (await readline.question(prompt)).trim().toLowerCase();
11208
11598
  if (answer === "") {
@@ -11318,6 +11708,19 @@ function isUpdateNotificationDisabled() {
11318
11708
 
11319
11709
  // src/lifecycle.ts
11320
11710
  async function runDoctor(config, options) {
11711
+ const checks = await collectDoctorChecks(config, options);
11712
+ for (const check of checks) {
11713
+ console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
11714
+ }
11715
+ const failureCount = checks.filter((check) => check.status === "fail").length;
11716
+ const warningCount = checks.filter((check) => check.status === "warn").length;
11717
+ console.log(`
11718
+ Summary: ${failureCount} failure(s), ${warningCount} warning(s)`);
11719
+ if (options.strict === true && failureCount > 0) {
11720
+ process.exitCode = 1;
11721
+ }
11722
+ }
11723
+ async function collectDoctorChecks(config, options = {}) {
11321
11724
  const checks = [];
11322
11725
  checks.push({ name: "mode", status: "ok", detail: options.dryRun ? "dry run; no writes" : "read-only checks" });
11323
11726
  checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
@@ -11337,16 +11740,7 @@ async function runDoctor(config, options) {
11337
11740
  checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
11338
11741
  checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
11339
11742
  checks.push(await healthCheck(config));
11340
- for (const check of checks) {
11341
- console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
11342
- }
11343
- const failureCount = checks.filter((check) => check.status === "fail").length;
11344
- const warningCount = checks.filter((check) => check.status === "warn").length;
11345
- console.log(`
11346
- Summary: ${failureCount} failure(s), ${warningCount} warning(s)`);
11347
- if (options.strict === true && failureCount > 0) {
11348
- process.exitCode = 1;
11349
- }
11743
+ return checks;
11350
11744
  }
11351
11745
  async function runInstall(config, options) {
11352
11746
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
@@ -11724,8 +12118,8 @@ async function commandPresenceCheck(name, args) {
11724
12118
  };
11725
12119
  }
11726
12120
  async function firstCommandCheck(name, commands, args) {
11727
- for (const command of commands) {
11728
- const executable = await findExecutable([command]);
12121
+ for (const command2 of commands) {
12122
+ const executable = await findExecutable([command2]);
11729
12123
  if (!executable) {
11730
12124
  continue;
11731
12125
  }
@@ -11733,7 +12127,7 @@ async function firstCommandCheck(name, commands, args) {
11733
12127
  return {
11734
12128
  name,
11735
12129
  status: result.exitCode === 0 ? "ok" : "warn",
11736
- detail: `${command}: ${firstLine(result.stdout || result.stderr) || executable}`
12130
+ detail: `${command2}: ${firstLine(result.stdout || result.stderr) || executable}`
11737
12131
  };
11738
12132
  }
11739
12133
  return { name, status: "fail", detail: `none found: ${commands.join(", ")}` };
@@ -11908,13 +12302,13 @@ async function runInstallCommands(config, preferred, force, dryRun) {
11908
12302
  }
11909
12303
  }
11910
12304
  async function offerToInstallUv() {
11911
- if (import_node_process2.stdin.isTTY !== true || import_node_process2.stdout.isTTY !== true) {
12305
+ if (import_node_process3.stdin.isTTY !== true || import_node_process3.stdout.isTTY !== true) {
11912
12306
  console.warn(
11913
12307
  "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."
11914
12308
  );
11915
12309
  return false;
11916
12310
  }
11917
- const readline = (0, import_promises12.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
12311
+ const readline = (0, import_promises12.createInterface)({ input: import_node_process3.stdin, output: import_node_process3.stdout });
11918
12312
  let answer;
11919
12313
  try {
11920
12314
  answer = (await readline.question(
@@ -12365,10 +12759,1028 @@ function parsePackageManager(value) {
12365
12759
  throw new Error(`Invalid package manager: ${value}`);
12366
12760
  }
12367
12761
 
12762
+ // src/version_command.ts
12763
+ async function runVersion(config, options) {
12764
+ const currentVersion = await currentPackageVersion();
12765
+ const registry = normalizeRegistry(options.registry ?? updateRegistry());
12766
+ let latestVersion;
12767
+ let latestWarning;
12768
+ try {
12769
+ latestVersion = await withSpinner("Checking npm for latest threadnote version", () => fetchLatestVersion2(registry));
12770
+ } catch (err) {
12771
+ latestWarning = errorMessage(err);
12772
+ }
12773
+ console.log(keyValue("Current version", info(currentVersion)));
12774
+ console.log(keyValue("Latest version", latestVersion ? info(latestVersion) : warning("unavailable")));
12775
+ if (latestWarning) {
12776
+ console.log(warning(`Warning: ${latestWarning}`));
12777
+ }
12778
+ if (!latestVersion) {
12779
+ return;
12780
+ }
12781
+ const comparison = compareVersions(currentVersion, latestVersion);
12782
+ if (comparison >= 0) {
12783
+ console.log(success(comparison > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."));
12784
+ console.log("");
12785
+ const whatsNew2 = await withSpinner("Fetching GitHub release notes", () => whatsNewLinesForVersion(currentVersion));
12786
+ printWhatsNew(whatsNew2);
12787
+ return;
12788
+ }
12789
+ console.log("");
12790
+ const whatsNew = await withSpinner(
12791
+ "Fetching GitHub release notes",
12792
+ () => whatsNewLinesForVersionRange(currentVersion, latestVersion)
12793
+ );
12794
+ printWhatsNew(whatsNew);
12795
+ }
12796
+ function printWhatsNew(whatsNew) {
12797
+ for (const line of whatsNew) {
12798
+ console.log(line === "What's new:" ? heading(line) : line);
12799
+ }
12800
+ }
12801
+
12802
+ // src/manager.ts
12803
+ var import_node_http2 = require("node:http");
12804
+ var import_node_crypto2 = require("node:crypto");
12805
+ var import_promises13 = require("node:fs/promises");
12806
+ var import_node_os7 = require("node:os");
12807
+ var import_node_path13 = require("node:path");
12808
+ var STATIC_FILES = {
12809
+ "/": { contentType: "text/html; charset=utf-8", path: "index.html" },
12810
+ "/index.html": { contentType: "text/html; charset=utf-8", path: "index.html" },
12811
+ "/app.css": { contentType: "text/css; charset=utf-8", path: "app.css" },
12812
+ "/app.js": { contentType: "text/javascript; charset=utf-8", path: "app.js" },
12813
+ "/threadnote-logo.svg": { contentType: "image/svg+xml; charset=utf-8", path: "threadnote-logo.svg", root: "docs" }
12814
+ };
12815
+ async function runManage(config, options) {
12816
+ const token = (0, import_node_crypto2.randomBytes)(24).toString("base64url");
12817
+ const server = createManagerServer({ config, jobs: /* @__PURE__ */ new Map(), token });
12818
+ const port = parseUiPort(options.uiPort);
12819
+ await listen(server, port);
12820
+ const address = server.address();
12821
+ const actualPort = typeof address === "object" && address ? address.port : port;
12822
+ const url = `http://127.0.0.1:${actualPort}/?token=${encodeURIComponent(token)}`;
12823
+ console.log(`Threadnote manager: ${url}`);
12824
+ console.log("Press Ctrl-C to stop the manager.");
12825
+ if (options.open !== false) {
12826
+ await runCommand("open", [url], { allowFailure: true });
12827
+ }
12828
+ await new Promise((resolve2, reject) => {
12829
+ const close = () => server.close((err) => err ? reject(err) : resolve2());
12830
+ process.once("SIGINT", close);
12831
+ process.once("SIGTERM", close);
12832
+ });
12833
+ }
12834
+ function createManagerServer(context) {
12835
+ return (0, import_node_http2.createServer)((request, response) => {
12836
+ void handleRequest(context, request, response).catch((err) => {
12837
+ writeJson(response, 500, { error: errorMessage(err) });
12838
+ });
12839
+ });
12840
+ }
12841
+ async function memoryTree(config) {
12842
+ const root = localMemoriesRoot(config);
12843
+ return readTree(config, root, `viking://user/${uriSegment(config.user)}/memories`, "");
12844
+ }
12845
+ async function readManagedMemory(config, uri) {
12846
+ assertVikingUri(uri);
12847
+ const path = localPathForMemoryUri(config, uri);
12848
+ if (!path) {
12849
+ throw new Error(`Manager can only read current-user memory URIs: ${uri}`);
12850
+ }
12851
+ const [content, pathStat] = await Promise.all([(0, import_promises13.readFile)(path, "utf8"), (0, import_promises13.stat)(path)]);
12852
+ const relativePath = (0, import_node_path13.relative)(localMemoriesRoot(config), path).split(import_node_path13.sep).join("/");
12853
+ const record = parseMemoryDocument(uri, content);
12854
+ return {
12855
+ content,
12856
+ node: {
12857
+ isDir: false,
12858
+ isShared: isInSharedNamespace(config, uri),
12859
+ isSystem: isSystemMemoryName(path.split(import_node_path13.sep).at(-1) ?? ""),
12860
+ metadata: record?.metadata,
12861
+ modTime: pathStat.mtime.toISOString(),
12862
+ name: path.split(import_node_path13.sep).at(-1) ?? uri,
12863
+ relativePath,
12864
+ sharedTeam: sharedTeamNameForUri(config, uri),
12865
+ size: pathStat.size,
12866
+ uri
12867
+ },
12868
+ record
12869
+ };
12870
+ }
12871
+ async function readContextUri(config, uri) {
12872
+ assertVikingUri(uri);
12873
+ try {
12874
+ const localMemory = await readManagedMemory(config, uri);
12875
+ return { content: localMemory.content, localMemory, output: localMemory.content };
12876
+ } catch {
12877
+ const result = await runCaptured(() => runRead(config, uri, {}));
12878
+ return { content: result.output, output: result.output };
12879
+ }
12880
+ }
12881
+ async function detectConsolidationAgents() {
12882
+ const [codex, claude, cursor, copilot] = await Promise.all([
12883
+ findExecutable(["codex"]),
12884
+ findExecutable(["claude"]),
12885
+ findExecutable(["cursor-agent"]),
12886
+ findExecutable(["copilot"])
12887
+ ]);
12888
+ return [
12889
+ { available: codex !== void 0, command: codex, id: "codex", label: "Codex" },
12890
+ { available: claude !== void 0, command: claude, id: "claude", label: "Claude" },
12891
+ { available: cursor !== void 0, command: cursor, id: "cursor", label: "Cursor" },
12892
+ { available: copilot !== void 0, command: copilot, id: "copilot", label: "Copilot" }
12893
+ ];
12894
+ }
12895
+ async function handleRequest(context, request, response) {
12896
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
12897
+ if (request.method === "GET" && STATIC_FILES[url.pathname]) {
12898
+ await serveStatic(context, url, response);
12899
+ return;
12900
+ }
12901
+ if (request.method === "GET" && url.pathname === "/favicon.ico") {
12902
+ response.writeHead(204, { "cache-control": "no-store" });
12903
+ response.end();
12904
+ return;
12905
+ }
12906
+ if (!isAuthorized(context, request)) {
12907
+ writeJson(response, 401, { error: "Unauthorized" });
12908
+ return;
12909
+ }
12910
+ if (request.method === "GET" && url.pathname === "/api/state") {
12911
+ const agents = await detectConsolidationAgents();
12912
+ const version = await currentPackageVersion();
12913
+ let latestVersion;
12914
+ try {
12915
+ latestVersion = await fetchLatestVersion2(normalizeRegistry(updateRegistry()));
12916
+ } catch {
12917
+ latestVersion = void 0;
12918
+ }
12919
+ writeJson(response, 200, {
12920
+ agents,
12921
+ config: publicConfig(context.config),
12922
+ latestVersion,
12923
+ openVikingLogPath: openVikingLogPath(context.config),
12924
+ version
12925
+ });
12926
+ return;
12927
+ }
12928
+ if (request.method === "GET" && url.pathname === "/api/tree") {
12929
+ writeJson(response, 200, { tree: await memoryTree(context.config) });
12930
+ return;
12931
+ }
12932
+ if (request.method === "GET" && url.pathname === "/api/memory") {
12933
+ writeJson(response, 200, await readManagedMemory(context.config, requiredQuery(url, "uri")));
12934
+ return;
12935
+ }
12936
+ if (request.method === "GET" && url.pathname === "/api/read") {
12937
+ writeJson(response, 200, await readContextUri(context.config, requiredQuery(url, "uri")));
12938
+ return;
12939
+ }
12940
+ if (request.method === "GET" && url.pathname === "/api/shares") {
12941
+ writeJson(response, 200, { shares: await shareSummaries(context.config) });
12942
+ return;
12943
+ }
12944
+ if (request.method === "GET" && url.pathname === "/api/doctor") {
12945
+ writeJson(response, 200, {
12946
+ checks: await collectManagerDoctorChecks(context.config),
12947
+ shares: await shareSummaries(context.config)
12948
+ });
12949
+ return;
12950
+ }
12951
+ if (request.method === "GET" && url.pathname.startsWith("/api/consolidations/")) {
12952
+ const id = url.pathname.split("/").at(-1) ?? "";
12953
+ const job = context.jobs.get(id);
12954
+ writeJson(response, job ? 200 : 404, job ? { job } : { error: "Consolidation job not found" });
12955
+ return;
12956
+ }
12957
+ if (request.method !== "POST") {
12958
+ writeJson(response, 404, { error: "Not found" });
12959
+ return;
12960
+ }
12961
+ const body = await readJsonBody(request);
12962
+ switch (url.pathname) {
12963
+ case "/api/memory/archive":
12964
+ requireConfirm(body);
12965
+ writeJson(
12966
+ response,
12967
+ 200,
12968
+ await runCaptured(() => runArchive(context.config, requireString(body.uri, "uri"), body))
12969
+ );
12970
+ return;
12971
+ case "/api/memory/forget":
12972
+ requireConfirm(body);
12973
+ writeJson(response, 200, await runCaptured(() => runForget(context.config, requireString(body.uri, "uri"), {})));
12974
+ return;
12975
+ case "/api/memory/save":
12976
+ writeJson(response, 200, await saveMemory(context.config, body));
12977
+ return;
12978
+ case "/api/memory/move":
12979
+ requireConfirm(body);
12980
+ writeJson(response, 200, await moveMemory(context.config, requireString(body.uri, "uri"), targetFromBody(body)));
12981
+ return;
12982
+ case "/api/memory/publish":
12983
+ requireConfirm(body);
12984
+ writeJson(
12985
+ response,
12986
+ 200,
12987
+ await runCaptured(
12988
+ () => runSharePublish(context.config, requireString(body.uri, "uri"), {
12989
+ redact: body.redact === true,
12990
+ team: optionalString(body.team)
12991
+ })
12992
+ )
12993
+ );
12994
+ return;
12995
+ case "/api/memory/unpublish":
12996
+ requireConfirm(body);
12997
+ writeJson(
12998
+ response,
12999
+ 200,
13000
+ await runCaptured(
13001
+ () => runShareUnpublish(context.config, requireString(body.uri, "uri"), { team: optionalString(body.team) })
13002
+ )
13003
+ );
13004
+ return;
13005
+ case "/api/folder/remove":
13006
+ requireConfirm(body);
13007
+ writeJson(
13008
+ response,
13009
+ 200,
13010
+ await runCaptured(() => removeManagedFolder(context.config, requireString(body.uri, "uri")))
13011
+ );
13012
+ return;
13013
+ case "/api/bulk":
13014
+ requireConfirm(body);
13015
+ writeJson(response, 200, await runBulk(context.config, body));
13016
+ return;
13017
+ case "/api/compact":
13018
+ if (body.apply === true) {
13019
+ requireConfirm(body);
13020
+ }
13021
+ writeJson(response, 200, await runCaptured(() => runCompact(context.config, body)));
13022
+ return;
13023
+ case "/api/recall":
13024
+ writeJson(
13025
+ response,
13026
+ 200,
13027
+ await runCaptured(
13028
+ () => runRecall(context.config, {
13029
+ query: requireString(body.query, "query"),
13030
+ nodeLimit: optionalString(body.nodeLimit)
13031
+ })
13032
+ )
13033
+ );
13034
+ return;
13035
+ case "/api/read":
13036
+ writeJson(response, 200, await readContextUri(context.config, requireString(body.uri, "uri")));
13037
+ return;
13038
+ case "/api/shares/init":
13039
+ requireConfirm(body);
13040
+ writeJson(
13041
+ response,
13042
+ 200,
13043
+ await runCaptured(
13044
+ () => runShareInit(context.config, requireString(body.remoteUrl, "remoteUrl"), { team: optionalString(body.team) })
13045
+ )
13046
+ );
13047
+ return;
13048
+ case "/api/shares/rename":
13049
+ requireConfirm(body);
13050
+ writeJson(
13051
+ response,
13052
+ 200,
13053
+ await runCaptured(
13054
+ () => runShareRename(context.config, { team: requireString(body.team, "team"), to: requireString(body.to, "to") })
13055
+ )
13056
+ );
13057
+ return;
13058
+ case "/api/shares/set-url":
13059
+ requireConfirm(body);
13060
+ writeJson(
13061
+ response,
13062
+ 200,
13063
+ await runCaptured(
13064
+ () => runShareSetUrl(context.config, requireString(body.remoteUrl, "remoteUrl"), { team: optionalString(body.team) })
13065
+ )
13066
+ );
13067
+ return;
13068
+ case "/api/shares/remove":
13069
+ requireConfirm(body);
13070
+ writeJson(
13071
+ response,
13072
+ 200,
13073
+ await runCaptured(
13074
+ () => runShareRemove(context.config, {
13075
+ keepFiles: body.keepFiles === true,
13076
+ preserveLocal: body.preserveLocal === true,
13077
+ team: optionalString(body.team)
13078
+ })
13079
+ )
13080
+ );
13081
+ return;
13082
+ case "/api/shares/sync":
13083
+ writeJson(
13084
+ response,
13085
+ 200,
13086
+ await runCaptured(() => runShareSync(context.config, { team: optionalString(body.team) }))
13087
+ );
13088
+ return;
13089
+ case "/api/doctor/start":
13090
+ writeJson(response, 200, await runCaptured(() => runStart(context.config, {})));
13091
+ return;
13092
+ case "/api/doctor/repair-dry-run":
13093
+ writeJson(response, 200, await runCaptured(() => runRepair(context.config, { dryRun: true })));
13094
+ return;
13095
+ case "/api/doctor/repair":
13096
+ requireConfirm(body);
13097
+ writeJson(response, 200, await runCaptured(() => runRepair(context.config, { dryRun: false })));
13098
+ return;
13099
+ case "/api/import-pack":
13100
+ requireConfirm(body);
13101
+ writeJson(
13102
+ response,
13103
+ 200,
13104
+ await runCaptured(() => runImportPack(context.config, { path: requireString(body.path, "path") }))
13105
+ );
13106
+ return;
13107
+ case "/api/export-pack":
13108
+ writeJson(
13109
+ response,
13110
+ 200,
13111
+ await runCaptured(
13112
+ () => runExportPack(context.config, { path: optionalString(body.path), uri: optionalString(body.uri) })
13113
+ )
13114
+ );
13115
+ return;
13116
+ case "/api/seed":
13117
+ requireConfirm(body);
13118
+ writeJson(
13119
+ response,
13120
+ 200,
13121
+ await runCaptured(
13122
+ () => body.skills === true ? runSeedSkills(context.config, { dryRun: body.dryRun === true }) : runSeed(context.config, { dryRun: body.dryRun === true })
13123
+ )
13124
+ );
13125
+ return;
13126
+ case "/api/consolidations":
13127
+ writeJson(response, 200, { job: await createConsolidation(context, body) });
13128
+ return;
13129
+ default:
13130
+ if (url.pathname.startsWith("/api/consolidations/") && url.pathname.endsWith("/apply")) {
13131
+ requireConfirm(body);
13132
+ const id = url.pathname.split("/").at(-2) ?? "";
13133
+ writeJson(response, 200, await applyConsolidation(context.config, context.jobs, id, body));
13134
+ return;
13135
+ }
13136
+ writeJson(response, 404, { error: "Not found" });
13137
+ }
13138
+ }
13139
+ async function serveStatic(context, url, response) {
13140
+ const file = STATIC_FILES[url.pathname] ?? STATIC_FILES["/"];
13141
+ const content = await (0, import_promises13.readFile)((0, import_node_path13.join)(toolRoot(), file.root ?? "manager", file.path));
13142
+ const headers = { "content-type": file.contentType };
13143
+ if (url.pathname === "/" || url.pathname === "/index.html") {
13144
+ headers["cache-control"] = "no-store";
13145
+ }
13146
+ response.writeHead(200, headers);
13147
+ response.end(content);
13148
+ }
13149
+ async function readTree(config, path, uri, relativePath) {
13150
+ const pathStat = await (0, import_promises13.stat)(path);
13151
+ const name = relativePath ? relativePath.split("/").at(-1) ?? relativePath : "memories";
13152
+ const isDir = pathStat.isDirectory();
13153
+ if (!isDir) {
13154
+ const content = await (0, import_promises13.readFile)(path, "utf8").catch(() => "");
13155
+ const record = parseMemoryDocument(uri, content);
13156
+ return {
13157
+ isDir: false,
13158
+ isShared: isInSharedNamespace(config, uri),
13159
+ isSystem: isSystemMemoryName(name),
13160
+ metadata: record?.metadata,
13161
+ modTime: pathStat.mtime.toISOString(),
13162
+ name,
13163
+ relativePath,
13164
+ sharedTeam: sharedTeamNameForUri(config, uri),
13165
+ size: pathStat.size,
13166
+ uri
13167
+ };
13168
+ }
13169
+ const entries = await (0, import_promises13.readdir)(path, { withFileTypes: true });
13170
+ const children = await Promise.all(
13171
+ entries.sort(
13172
+ (left, right) => Number(right.isDirectory()) - Number(left.isDirectory()) || left.name.localeCompare(right.name)
13173
+ ).map((entry) => {
13174
+ const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
13175
+ return readTree(config, (0, import_node_path13.join)(path, entry.name), `${uri}/${entry.name}`, childRelative);
13176
+ })
13177
+ );
13178
+ return {
13179
+ children,
13180
+ isDir: true,
13181
+ isShared: isInSharedNamespace(config, uri),
13182
+ isSystem: isSystemMemoryName(name),
13183
+ modTime: pathStat.mtime.toISOString(),
13184
+ name,
13185
+ relativePath,
13186
+ sharedTeam: sharedTeamNameForUri(config, uri),
13187
+ size: pathStat.size,
13188
+ uri
13189
+ };
13190
+ }
13191
+ async function saveMemory(config, body) {
13192
+ const text = requireString(body.text, "text");
13193
+ const replaceUri = optionalString(body.replaceUri);
13194
+ if (replaceUri && isRawMemoryDocument(text)) {
13195
+ return runCaptured(() => writeRawMemory(config, replaceUri, text));
13196
+ }
13197
+ return runCaptured(
13198
+ () => runRemember(config, {
13199
+ kind: memoryKind(body.kind) ?? "durable",
13200
+ project: optionalString(body.project),
13201
+ replace: replaceUri,
13202
+ sourceAgentClient: optionalString(body.sourceAgentClient) ?? "manager",
13203
+ status: memoryStatus(body.status) ?? "active",
13204
+ text,
13205
+ topic: optionalString(body.topic)
13206
+ })
13207
+ );
13208
+ }
13209
+ async function writeRawMemory(config, uri, content) {
13210
+ assertVikingUri(uri);
13211
+ const ov = await openVikingCliForMode(false);
13212
+ if (isInSharedNamespace(config, uri)) {
13213
+ const teamName = sharedTeamNameForUri(config, uri);
13214
+ if (!teamName) {
13215
+ throw new Error(`${uri} is not in a configured shared namespace.`);
13216
+ }
13217
+ const team = await resolveTeam(config, teamName);
13218
+ await ensureSharedDirectoryChain(config, ov, uri, false);
13219
+ await writeMemoryFile(config, ov, uri, content, "replace", false);
13220
+ const relativePath = vikingUriToWorktreeRelative(config, uri, team.name);
13221
+ await publishShareGitChange(team.config.worktree, relativePath, `share: update ${relativePath}`);
13222
+ return;
13223
+ }
13224
+ await ensurePersonalDirectoryChain2(config, ov, parentUri(uri));
13225
+ await writeMemoryFile(config, ov, uri, content, "replace", false);
13226
+ }
13227
+ async function moveMemory(config, sourceUri, target) {
13228
+ assertVikingUri(sourceUri);
13229
+ const source = await readManagedMemory(config, sourceUri);
13230
+ const sourceRecord = source.record;
13231
+ const text = sourceRecord?.body ?? source.content;
13232
+ const metadata = {
13233
+ kind: target.kind ?? sourceRecord?.metadata.kind ?? "durable",
13234
+ project: target.project ?? sourceRecord?.metadata.project ?? "general",
13235
+ sourceAgentClient: target.sourceAgentClient ?? "manager",
13236
+ status: target.status ?? sourceRecord?.metadata.status ?? "active",
13237
+ topic: target.topic ?? sourceRecord?.metadata.topic ?? "current"
13238
+ };
13239
+ const personalTargetUri = memoryUriFor2(config, metadata);
13240
+ if (target.team) {
13241
+ const targetTeam = target.team;
13242
+ if (isInSharedNamespace(config, sourceUri)) {
13243
+ const team = sharedTeamNameForUri(config, sourceUri);
13244
+ if (team !== targetTeam) {
13245
+ throw new Error(
13246
+ "Cross-team shared moves are not supported in V1. Copy/unpublish, then publish to the target team."
13247
+ );
13248
+ }
13249
+ const sharedTargetUri = sharedMemoryUriFor(config, targetTeam, metadata);
13250
+ const output3 = await runCaptured(
13251
+ () => moveSharedWithinTeam(config, sourceUri, sharedTargetUri, source.content, targetTeam)
13252
+ );
13253
+ return { ...output3, targetUri: sharedTargetUri };
13254
+ }
13255
+ const saved = await runCaptured(
13256
+ () => runRemember(config, {
13257
+ kind: metadata.kind,
13258
+ project: metadata.project,
13259
+ replace: sourceUri,
13260
+ sourceAgentClient: metadata.sourceAgentClient,
13261
+ status: metadata.status,
13262
+ text,
13263
+ topic: metadata.topic
13264
+ })
13265
+ );
13266
+ const published = await runCaptured(() => runSharePublish(config, personalTargetUri, { team: targetTeam }));
13267
+ return {
13268
+ output: [saved.output, published.output].filter(Boolean).join("\n"),
13269
+ targetUri: sharedMemoryUriFor(config, targetTeam, metadata)
13270
+ };
13271
+ }
13272
+ if (isInSharedNamespace(config, sourceUri)) {
13273
+ const saved = await runCaptured(
13274
+ () => runRemember(config, {
13275
+ kind: metadata.kind,
13276
+ project: metadata.project,
13277
+ sourceAgentClient: metadata.sourceAgentClient,
13278
+ status: metadata.status,
13279
+ text,
13280
+ topic: metadata.topic
13281
+ })
13282
+ );
13283
+ const removed = await runCaptured(() => removeSharedSource(config, sourceUri));
13284
+ return { output: [saved.output, removed.output].filter(Boolean).join("\n"), targetUri: personalTargetUri };
13285
+ }
13286
+ const output2 = await runCaptured(
13287
+ () => runRemember(config, {
13288
+ kind: metadata.kind,
13289
+ project: metadata.project,
13290
+ replace: sourceUri,
13291
+ sourceAgentClient: metadata.sourceAgentClient,
13292
+ status: metadata.status,
13293
+ text,
13294
+ topic: metadata.topic
13295
+ })
13296
+ );
13297
+ return { ...output2, targetUri: personalTargetUri };
13298
+ }
13299
+ async function moveSharedWithinTeam(config, sourceUri, targetUri, content, teamName) {
13300
+ const team = await resolveTeam(config, teamName);
13301
+ const ov = await openVikingCliForMode(false);
13302
+ await ensureSharedDirectoryChain(config, ov, targetUri, false);
13303
+ await writeMemoryFile(config, ov, targetUri, content, "create", false);
13304
+ await publishShareGitChange(
13305
+ team.config.worktree,
13306
+ vikingUriToWorktreeRelative(config, targetUri, team.name),
13307
+ `share: move ${vikingUriToWorktreeRelative(config, sourceUri, team.name)} to ${vikingUriToWorktreeRelative(config, targetUri, team.name)}`
13308
+ );
13309
+ await publishShareGitChange(
13310
+ team.config.worktree,
13311
+ vikingUriToWorktreeRelative(config, sourceUri, team.name),
13312
+ `share: remove ${vikingUriToWorktreeRelative(config, sourceUri, team.name)}`,
13313
+ {
13314
+ verb: "rm"
13315
+ }
13316
+ );
13317
+ await removeMemoryUri(config, ov, sourceUri, false);
13318
+ }
13319
+ async function removeSharedSource(config, sourceUri) {
13320
+ const teamName = sharedTeamNameForUri(config, sourceUri);
13321
+ if (!teamName) {
13322
+ throw new Error(`${sourceUri} is not a shared memory.`);
13323
+ }
13324
+ const team = await resolveTeam(config, teamName);
13325
+ const ov = await openVikingCliForMode(false);
13326
+ await publishShareGitChange(
13327
+ team.config.worktree,
13328
+ vikingUriToWorktreeRelative(config, sourceUri, team.name),
13329
+ `share: remove ${vikingUriToWorktreeRelative(config, sourceUri, team.name)}`,
13330
+ {
13331
+ verb: "rm"
13332
+ }
13333
+ );
13334
+ await removeMemoryUri(config, ov, sourceUri, false);
13335
+ }
13336
+ async function removeManagedFolder(config, uri) {
13337
+ assertVikingUri(uri);
13338
+ const rootUri = `viking://user/${uriSegment(config.user)}/memories`;
13339
+ if (uri === rootUri) {
13340
+ throw new Error("Refusing to remove the root memories folder.");
13341
+ }
13342
+ if (isInSharedNamespace(config, uri)) {
13343
+ throw new Error("Shared folders are managed from Sharing. Remove the share or unpublish selected memories.");
13344
+ }
13345
+ const path = localPathForMemoryUri(config, uri);
13346
+ if (!path) {
13347
+ throw new Error(`Manager can only remove current-user memory folders: ${uri}`);
13348
+ }
13349
+ const pathStat = await (0, import_promises13.stat)(path);
13350
+ if (!pathStat.isDirectory()) {
13351
+ throw new Error(`Not a folder: ${uri}`);
13352
+ }
13353
+ const relativePath = (0, import_node_path13.relative)(localMemoriesRoot(config), path);
13354
+ if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path13.sep).includes("..")) {
13355
+ throw new Error("Refusing to remove a folder outside the memories tree.");
13356
+ }
13357
+ const fileUris = await fileUrisUnderFolder(config, path);
13358
+ for (const fileUri of fileUris) {
13359
+ await runForget(config, fileUri, {});
13360
+ }
13361
+ await (0, import_promises13.rm)(path, { force: true, recursive: true });
13362
+ console.log(`Removed folder: ${uri}`);
13363
+ console.log(`Forgot ${fileUris.length} file${fileUris.length === 1 ? "" : "s"}.`);
13364
+ }
13365
+ async function fileUrisUnderFolder(config, folderPath) {
13366
+ const entries = await (0, import_promises13.readdir)(folderPath, { withFileTypes: true });
13367
+ const uris = [];
13368
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
13369
+ const path = (0, import_node_path13.join)(folderPath, entry.name);
13370
+ if (entry.isDirectory()) {
13371
+ uris.push(...await fileUrisUnderFolder(config, path));
13372
+ } else if (entry.isFile()) {
13373
+ uris.push(localPathToMemoryUri(config, path));
13374
+ }
13375
+ }
13376
+ return uris;
13377
+ }
13378
+ async function runBulk(config, body) {
13379
+ const action = requireString(body.action, "action");
13380
+ const uris = requireStringArray(body.uris, "uris");
13381
+ const results = [];
13382
+ for (const uri of uris) {
13383
+ try {
13384
+ let output2;
13385
+ if (action === "archive") {
13386
+ output2 = (await runCaptured(() => runArchive(config, uri, {}))).output;
13387
+ } else if (action === "forget") {
13388
+ output2 = (await runCaptured(() => runForget(config, uri, {}))).output;
13389
+ } else if (action === "publish") {
13390
+ output2 = (await runCaptured(() => runSharePublish(config, uri, { team: optionalString(body.team) }))).output;
13391
+ } else {
13392
+ throw new Error(`Unsupported bulk action: ${action}`);
13393
+ }
13394
+ results.push({ ok: true, output: output2, uri });
13395
+ } catch (err) {
13396
+ results.push({ error: errorMessage(err), ok: false, uri });
13397
+ }
13398
+ }
13399
+ return { results };
13400
+ }
13401
+ async function createConsolidation(context, body) {
13402
+ const agent = agentClient(requireString(body.agent, "agent"));
13403
+ const sourceUris = requireStringArray(body.uris, "uris");
13404
+ const target = targetFromBody(body);
13405
+ const job = {
13406
+ agent,
13407
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13408
+ id: (0, import_node_crypto2.randomUUID)(),
13409
+ sourceUris,
13410
+ status: "running",
13411
+ target
13412
+ };
13413
+ context.jobs.set(job.id, job);
13414
+ try {
13415
+ const sources = await Promise.all(sourceUris.map((uri) => readManagedMemory(context.config, uri)));
13416
+ job.draft = await runConsolidationAgent(agent, sources);
13417
+ job.status = "completed";
13418
+ } catch (err) {
13419
+ job.error = errorMessage(err);
13420
+ job.status = "failed";
13421
+ }
13422
+ return job;
13423
+ }
13424
+ async function applyConsolidation(config, jobs, id, body) {
13425
+ const job = jobs.get(id);
13426
+ if (!job) {
13427
+ throw new Error("Consolidation job not found.");
13428
+ }
13429
+ if (job.status !== "completed" || !job.draft) {
13430
+ throw new Error("Consolidation job is not completed.");
13431
+ }
13432
+ const draft = optionalString(body.draft) ?? job.draft;
13433
+ const target = targetFromBody({ ...job.target, ...body });
13434
+ const saved = await runCaptured(
13435
+ () => runRemember(config, {
13436
+ kind: target.kind ?? "durable",
13437
+ project: target.project,
13438
+ sourceAgentClient: target.sourceAgentClient ?? "manager",
13439
+ status: target.status ?? "active",
13440
+ text: draft,
13441
+ topic: target.topic
13442
+ })
13443
+ );
13444
+ const cleanup = cleanupMode(body.cleanup);
13445
+ const cleanupOutputs = [];
13446
+ if (cleanup !== "keep") {
13447
+ for (const uri of job.sourceUris) {
13448
+ if (isInSharedNamespace(config, uri) && body.cleanupShared !== true) {
13449
+ cleanupOutputs.push(`Skipped shared source cleanup: ${uri}`);
13450
+ continue;
13451
+ }
13452
+ const action = cleanup === "forget" ? () => runForget(config, uri, {}) : () => runArchive(config, uri, {});
13453
+ cleanupOutputs.push((await runCaptured(action)).output);
13454
+ }
13455
+ }
13456
+ return { output: [saved.output, ...cleanupOutputs].filter(Boolean).join("\n") };
13457
+ }
13458
+ async function runConsolidationAgent(agent, sources) {
13459
+ if (agent !== "codex" && agent !== "claude") {
13460
+ throw new Error(`${agent} does not expose a supported non-interactive consolidation mode.`);
13461
+ }
13462
+ const executable = await findExecutable([agent]);
13463
+ if (!executable) {
13464
+ throw new Error(`${agent} executable was not found.`);
13465
+ }
13466
+ const prompt = consolidationPrompt(sources);
13467
+ const stagingDir = await (0, import_promises13.mkdtemp)((0, import_node_path13.join)((0, import_node_os7.tmpdir)(), "threadnote-consolidate-"));
13468
+ const promptPath = (0, import_node_path13.join)(stagingDir, "prompt.txt");
13469
+ try {
13470
+ await (0, import_promises13.writeFile)(promptPath, prompt, { encoding: "utf8", mode: 384 });
13471
+ await (0, import_promises13.chmod)(promptPath, 384);
13472
+ const script = consolidationAgentScript(agent, executable);
13473
+ const result = await runCommand("sh", ["-lc", script, "threadnote-consolidate", promptPath], {
13474
+ allowFailure: true,
13475
+ maxOutputBytes: 1024 * 1024,
13476
+ timeoutMs: 10 * 60 * 1e3
13477
+ });
13478
+ if (result.exitCode !== 0) {
13479
+ throw new Error(result.stderr.trim() || result.stdout.trim() || `${agent} exited with ${result.exitCode}`);
13480
+ }
13481
+ const draft = result.stdout.trim();
13482
+ if (!draft) {
13483
+ throw new Error(`${agent} returned an empty consolidation draft.`);
13484
+ }
13485
+ return draft;
13486
+ } finally {
13487
+ await (0, import_promises13.rm)(stagingDir, { force: true, recursive: true });
13488
+ }
13489
+ }
13490
+ function consolidationAgentScript(agent, executable) {
13491
+ if (agent === "codex") {
13492
+ return `${shellQuote(executable)} exec --sandbox read-only --skip-git-repo-check - < "$1"`;
13493
+ }
13494
+ if (agent === "claude") {
13495
+ return `${shellQuote(executable)} --print --permission-mode default < "$1"`;
13496
+ }
13497
+ throw new Error(`${agent} does not expose a supported non-interactive consolidation mode.`);
13498
+ }
13499
+ function consolidationPrompt(sources) {
13500
+ return [
13501
+ "Consolidate these Threadnote memories into one concise memory.",
13502
+ "Return only the replacement memory body in Markdown. Do not include frontmatter.",
13503
+ "Preserve important facts, current status, decisions, blockers, and source viking:// URIs.",
13504
+ "",
13505
+ ...sources.flatMap((source) => [`--- SOURCE ${source.node.uri} ---`, source.content.trim(), ""])
13506
+ ].join("\n");
13507
+ }
13508
+ async function shareSummaries(config) {
13509
+ const teamsFile = await readTeamsFile(config);
13510
+ const git = await findExecutable(["git"]);
13511
+ const entries = await Promise.all(
13512
+ Object.values(teamsFile.teams).map(async (team) => {
13513
+ if (!git) {
13514
+ return { ...team, default: teamsFile.defaultTeam === team.name, warning: "git not found" };
13515
+ }
13516
+ const status = await runCommand(git, ["-C", team.worktree, "status", "--short", "--branch"], {
13517
+ allowFailure: true
13518
+ });
13519
+ const ahead = await gitCount(git, team.worktree, "@{u}..HEAD");
13520
+ const behind = await gitCount(git, team.worktree, "HEAD..@{u}");
13521
+ return {
13522
+ ...team,
13523
+ ahead,
13524
+ behind,
13525
+ default: teamsFile.defaultTeam === team.name,
13526
+ dirty: status.stdout.split("\n").some((line) => line.trim().length > 0 && !line.startsWith("##")),
13527
+ status: status.stdout.trim(),
13528
+ warning: status.exitCode === 0 ? void 0 : status.stderr.trim() || status.stdout.trim()
13529
+ };
13530
+ })
13531
+ );
13532
+ return entries.sort((left, right) => left.name.localeCompare(right.name));
13533
+ }
13534
+ async function collectManagerDoctorChecks(config) {
13535
+ const threadnote = await findExecutable(["threadnote"]);
13536
+ if (!threadnote) {
13537
+ return collectDoctorChecks(config, {});
13538
+ }
13539
+ const result = await runCommand(
13540
+ threadnote,
13541
+ [
13542
+ "--home",
13543
+ config.agentContextHome,
13544
+ "--manifest",
13545
+ config.manifestPath,
13546
+ "--host",
13547
+ config.host,
13548
+ "--port",
13549
+ String(config.port),
13550
+ "doctor"
13551
+ ],
13552
+ { allowFailure: true, maxOutputBytes: 1024 * 1024 }
13553
+ );
13554
+ const checks = parseDoctorChecksFromOutput([result.stdout, result.stderr].filter(Boolean).join("\n"));
13555
+ return checks.length > 0 ? checks : collectDoctorChecks(config, {});
13556
+ }
13557
+ function parseDoctorChecksFromOutput(output2) {
13558
+ const ansiEscape = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
13559
+ return output2.split(/\r?\n/).map((line) => line.replace(ansiEscape, "").trim()).map((line) => /^(OK|WARN|FAIL)\s+([^:]+):\s*(.*)$/.exec(line)).filter((match) => match !== null).map((match) => ({
13560
+ detail: match[3] ?? "",
13561
+ name: match[2] ?? "",
13562
+ status: doctorStatus(match[1] ?? "")
13563
+ }));
13564
+ }
13565
+ async function gitCount(git, worktree, range) {
13566
+ const result = await runCommand(git, ["-C", worktree, "rev-list", "--count", range], { allowFailure: true });
13567
+ if (result.exitCode !== 0) {
13568
+ return void 0;
13569
+ }
13570
+ return Number.parseInt(result.stdout.trim(), 10) || 0;
13571
+ }
13572
+ function doctorStatus(value) {
13573
+ if (value === "OK") {
13574
+ return "ok";
13575
+ }
13576
+ if (value === "FAIL") {
13577
+ return "fail";
13578
+ }
13579
+ return "warn";
13580
+ }
13581
+ async function runCaptured(action) {
13582
+ const lines = [];
13583
+ const originalLog = console.log;
13584
+ const originalWarn = console.warn;
13585
+ const originalError = console.error;
13586
+ console.log = (...args) => lines.push(args.map(String).join(" "));
13587
+ console.warn = (...args) => lines.push(args.map(String).join(" "));
13588
+ console.error = (...args) => lines.push(args.map(String).join(" "));
13589
+ try {
13590
+ await action();
13591
+ } finally {
13592
+ console.log = originalLog;
13593
+ console.warn = originalWarn;
13594
+ console.error = originalError;
13595
+ }
13596
+ return { output: lines.join("\n") };
13597
+ }
13598
+ function memoryUriFor2(config, metadata) {
13599
+ const project = uriSegment(metadata.project);
13600
+ const filename = metadata.status === "active" && metadata.kind !== "smoke" ? `${uriSegment(metadata.topic)}.md` : `threadnote-${safeTimestamp()}-${sha256(JSON.stringify(metadata)).slice(0, 12)}.md`;
13601
+ return `${memoryDirectoryUri2(config, metadata.kind, metadata.status, project)}/${filename}`;
13602
+ }
13603
+ function sharedMemoryUriFor(config, team, metadata) {
13604
+ if (metadata.kind !== "durable") {
13605
+ throw new Error("Only durable memories can be moved into shared team memory.");
13606
+ }
13607
+ return `viking://user/${uriSegment(config.user)}/memories/shared/${uriSegment(team)}/durable/projects/${uriSegment(metadata.project)}/${uriSegment(metadata.topic)}.md`;
13608
+ }
13609
+ function memoryDirectoryUri2(config, kind, status, projectSegment) {
13610
+ const base = `viking://user/${uriSegment(config.user)}/memories`;
13611
+ switch (kind) {
13612
+ case "preference":
13613
+ return status === "active" ? `${base}/preferences` : `${base}/preferences/${uriSegment(status)}`;
13614
+ case "handoff":
13615
+ return `${base}/handoffs/${uriSegment(status)}/${projectSegment}`;
13616
+ case "incident":
13617
+ return `${base}/incidents/${uriSegment(status)}/${projectSegment}`;
13618
+ case "smoke":
13619
+ return `${base}/smoke/${uriSegment(status)}`;
13620
+ case "durable":
13621
+ return status === "active" ? `${base}/durable/projects/${projectSegment}` : `${base}/durable/${uriSegment(status)}/${projectSegment}`;
13622
+ }
13623
+ }
13624
+ function localMemoriesRoot(config) {
13625
+ return (0, import_node_path13.join)(config.agentContextHome, "data", "viking", config.account, "user", uriSegment(config.user), "memories");
13626
+ }
13627
+ function localPathForMemoryUri(config, uri) {
13628
+ const prefix = `viking://user/${uriSegment(config.user)}/memories`;
13629
+ if (uri !== prefix && !uri.startsWith(`${prefix}/`)) {
13630
+ return void 0;
13631
+ }
13632
+ const relativePath = uri === prefix ? "" : uri.slice(prefix.length + 1);
13633
+ const segments = relativePath.split("/").filter(Boolean);
13634
+ if (segments.some((segment) => segment === "." || segment === "..")) {
13635
+ return void 0;
13636
+ }
13637
+ return (0, import_node_path13.join)(localMemoriesRoot(config), ...segments);
13638
+ }
13639
+ function localPathToMemoryUri(config, path) {
13640
+ const relativePath = (0, import_node_path13.relative)(localMemoriesRoot(config), path);
13641
+ if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path13.sep).includes("..")) {
13642
+ throw new Error(`Path is outside the memories tree: ${path}`);
13643
+ }
13644
+ return `viking://user/${uriSegment(config.user)}/memories/${relativePath.split(import_node_path13.sep).join("/")}`;
13645
+ }
13646
+ async function ensurePersonalDirectoryChain2(config, ov, directoryUri) {
13647
+ const prefix = "viking://";
13648
+ const parts = directoryUri.startsWith(prefix) ? directoryUri.slice(prefix.length).split("/").filter(Boolean) : [];
13649
+ const startIndex = parts[0] === "user" && parts.length > 2 ? 3 : 1;
13650
+ for (let index = startIndex; index <= parts.length; index += 1) {
13651
+ const uri = `${prefix}${parts.slice(0, index).join("/")}`;
13652
+ const statResult = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
13653
+ if (statResult.exitCode !== 0) {
13654
+ await runCommand(
13655
+ ov,
13656
+ withIdentity(config, ["mkdir", uri, "--description", "Threadnote lifecycle-aware local memories."])
13657
+ );
13658
+ }
13659
+ }
13660
+ }
13661
+ function publicConfig(config) {
13662
+ return {
13663
+ account: config.account,
13664
+ agentContextHome: config.agentContextHome,
13665
+ agentId: config.agentId,
13666
+ host: config.host,
13667
+ manifestPath: config.manifestPath,
13668
+ port: config.port,
13669
+ user: config.user
13670
+ };
13671
+ }
13672
+ function parseUiPort(value) {
13673
+ if (!value) {
13674
+ return 0;
13675
+ }
13676
+ const parsed = Number.parseInt(value, 10);
13677
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
13678
+ throw new Error(`Invalid --ui-port "${value}".`);
13679
+ }
13680
+ return parsed;
13681
+ }
13682
+ function isAuthorized(context, request) {
13683
+ const auth = request.headers.authorization;
13684
+ return auth === `Bearer ${context.token}` || request.headers["x-threadnote-token"] === context.token;
13685
+ }
13686
+ function isSystemMemoryName(name) {
13687
+ return name === ".abstract.md" || name === ".overview.md" || name === ".git" || name === ".gitignore";
13688
+ }
13689
+ async function listen(server, port) {
13690
+ await new Promise((resolve2, reject) => {
13691
+ server.once("error", reject);
13692
+ server.listen(port, "127.0.0.1", () => {
13693
+ server.off("error", reject);
13694
+ resolve2();
13695
+ });
13696
+ });
13697
+ }
13698
+ async function readJsonBody(request) {
13699
+ const chunks = [];
13700
+ for await (const chunk of request) {
13701
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
13702
+ }
13703
+ if (chunks.length === 0) {
13704
+ return {};
13705
+ }
13706
+ const raw = Buffer.concat(chunks).toString("utf8");
13707
+ const parsed = JSON.parse(raw);
13708
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13709
+ throw new Error("Expected a JSON object body.");
13710
+ }
13711
+ return parsed;
13712
+ }
13713
+ function writeJson(response, statusCode, body) {
13714
+ response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
13715
+ response.end(`${JSON.stringify(body)}
13716
+ `);
13717
+ }
13718
+ function requiredQuery(url, name) {
13719
+ const value = url.searchParams.get(name);
13720
+ if (!value) {
13721
+ throw new Error(`Missing query parameter: ${name}`);
13722
+ }
13723
+ return value;
13724
+ }
13725
+ function requireString(value, name) {
13726
+ if (typeof value !== "string" || value.trim().length === 0) {
13727
+ throw new Error(`Provide ${name}.`);
13728
+ }
13729
+ return value;
13730
+ }
13731
+ function optionalString(value) {
13732
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
13733
+ }
13734
+ function requireStringArray(value, name) {
13735
+ if (!Array.isArray(value) || value.length === 0 || !value.every((item) => typeof item === "string")) {
13736
+ throw new Error(`Provide ${name} as a non-empty string array.`);
13737
+ }
13738
+ return value;
13739
+ }
13740
+ function requireConfirm(body) {
13741
+ if (body.confirm !== true) {
13742
+ throw new Error("Set confirm=true for this action.");
13743
+ }
13744
+ }
13745
+ function targetFromBody(body) {
13746
+ return {
13747
+ kind: memoryKind(body.kind),
13748
+ project: optionalString(body.project),
13749
+ sourceAgentClient: optionalString(body.sourceAgentClient),
13750
+ status: memoryStatus(body.status),
13751
+ team: optionalString(body.team),
13752
+ topic: optionalString(body.topic)
13753
+ };
13754
+ }
13755
+ function memoryKind(value) {
13756
+ return value === "durable" || value === "handoff" || value === "incident" || value === "preference" || value === "smoke" ? value : void 0;
13757
+ }
13758
+ function memoryStatus(value) {
13759
+ return value === "active" || value === "archived" || value === "superseded" ? value : void 0;
13760
+ }
13761
+ function isRawMemoryDocument(text) {
13762
+ return text.startsWith("MEMORY\n") || text.startsWith("HANDOFF\n");
13763
+ }
13764
+ function agentClient(value) {
13765
+ if (value === "codex" || value === "claude" || value === "cursor" || value === "copilot") {
13766
+ return value;
13767
+ }
13768
+ throw new Error(`Unsupported consolidation agent: ${value}`);
13769
+ }
13770
+ function cleanupMode(value) {
13771
+ if (value === "forget" || value === "keep") {
13772
+ return value;
13773
+ }
13774
+ return "archive";
13775
+ }
13776
+
12368
13777
  // src/threadnote.ts
12369
13778
  async function main() {
12370
13779
  const program2 = new Command();
12371
13780
  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);
13781
+ program2.command("manage").description("Open the local Threadnote web manager").option("--ui-port <port>", "Port for the local manager UI; defaults to a random free port").option("--no-open", "Start the manager without opening a browser").action(async (options) => {
13782
+ await runManage(getRuntimeConfig(program2), options);
13783
+ });
12372
13784
  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) => {
12373
13785
  const config = getRuntimeConfig(program2);
12374
13786
  await runDoctor(config, options);
@@ -12390,6 +13802,9 @@ async function main() {
12390
13802
  }
12391
13803
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
12392
13804
  });
13805
+ 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) => {
13806
+ await runVersion(getRuntimeConfig(program2), options);
13807
+ });
12393
13808
  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) => {
12394
13809
  await runUpdate(getRuntimeConfig(program2), options);
12395
13810
  });
@@ -12515,7 +13930,13 @@ async function main() {
12515
13930
  share.command("list").description("List configured shared teams").option("--dry-run", "Print without side effects (no-op for list)").action(async (options) => {
12516
13931
  await runShareList(getRuntimeConfig(program2), options);
12517
13932
  });
12518
- share.command("remove").description("Forget a configured team and optionally delete its worktree/gitdir").option("--team <name>", "Team name; defaults to the configured default team").option("--keep-files", "Keep the worktree and gitdir on disk; only forget the team entry").option("--dry-run", "Print actions without running them").action(async (options) => {
13933
+ share.command("rename").description("Rename a configured shared team").requiredOption("--team <name>", "Existing team name").requiredOption("--to <name>", "New team name").option("--dry-run", "Print actions without running them").action(async (options) => {
13934
+ await runShareRename(getRuntimeConfig(program2), options);
13935
+ });
13936
+ share.command("set-url").description("Change the git remote URL for a configured shared team").argument("<remote-url>", "New git remote URL").option("--team <name>", "Team name; defaults to the configured default team").option("--dry-run", "Print actions without running them").action(async (remoteUrl, options) => {
13937
+ await runShareSetUrl(getRuntimeConfig(program2), remoteUrl, options);
13938
+ });
13939
+ share.command("remove").description("Forget a configured team and optionally delete its worktree/gitdir").option("--team <name>", "Team name; defaults to the configured default team").option("--keep-files", "Keep the worktree and gitdir on disk; only forget the team entry").option("--preserve-local", "Copy shared durable memories into the personal tree before removing the team").option("--dry-run", "Print actions without running them").action(async (options) => {
12519
13940
  await runShareRemove(getRuntimeConfig(program2), options);
12520
13941
  });
12521
13942
  program2.command("export-pack").description("Export local OpenViking context to an .ovpack").option("--dry-run", "Print ov command without exporting").option("--path <path>", "Output .ovpack path").option("--uri <uri>", "Source viking:// URI to export", "viking://").action(async (options) => {