threadnote 0.7.8 → 0.7.10

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.
@@ -3756,33 +3756,111 @@ async function runCommand(executable, args, options = {}) {
3756
3756
  const child = (0, import_node_child_process.spawn)(executable, args, { cwd: options.cwd });
3757
3757
  const stdoutChunks = [];
3758
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
+ }
3759
3802
  child.stdout.on("data", (chunk) => {
3760
- 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);
3761
3813
  });
3762
3814
  child.stderr.on("data", (chunk) => {
3763
- 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);
3764
3825
  });
3765
3826
  child.on("error", (err) => {
3827
+ if (finished) {
3828
+ return;
3829
+ }
3830
+ if (killTimer) {
3831
+ clearTimeout(killTimer);
3832
+ }
3766
3833
  if (options.allowFailure === true) {
3767
- resolvePromise({ exitCode: 127, stderr: errorMessage(err), stdout: "" });
3834
+ finish({ exitCode: 127, stderr: errorMessage(err), stdout: "" });
3768
3835
  } else {
3836
+ finished = true;
3769
3837
  rejectPromise(err);
3770
3838
  }
3771
3839
  });
3772
3840
  child.on("close", (code) => {
3773
- const result = {
3841
+ const result = failureResult ?? {
3774
3842
  exitCode: code ?? 1,
3775
3843
  stderr: stderrChunks.join(""),
3776
3844
  stdout: stdoutChunks.join("")
3777
3845
  };
3778
- if (result.exitCode !== 0 && options.allowFailure !== true) {
3779
- rejectPromise(new Error(`${formatShellCommand(executable, args)} failed: ${result.stderr || result.stdout}`));
3780
- return;
3781
- }
3782
- resolvePromise(result);
3846
+ finish(result);
3783
3847
  });
3784
3848
  });
3785
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
+ }
3786
3864
  async function gitValue(args, cwd = getInvocationCwd()) {
3787
3865
  const result = await runCommand("git", args, { allowFailure: true, cwd });
3788
3866
  if (result.exitCode !== 0) {
@@ -8234,7 +8312,14 @@ Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or
8234
8312
  }
8235
8313
  }
8236
8314
  if (options.push !== false) {
8237
- 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
+ }
8238
8323
  }
8239
8324
  }
8240
8325
  async function runShareSyncQuiet(config, state, options) {
@@ -8282,6 +8367,58 @@ async function stageShareableChanges(dryRun, git, worktree) {
8282
8367
  const pathspecs = [":(top)README.md", ":(top).gitignore", ...SHAREABLE_MEMORY_KIND_DIRS.map((dir) => `:(top)${dir}`)];
8283
8368
  await maybeRun(dryRun, git, ["-C", worktree, "add", "--", ...pathspecs], { allowFailure: true });
8284
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
+ }
8285
8422
  async function runSharePublish(config, sourceUri, options) {
8286
8423
  assertVikingUri(sourceUri);
8287
8424
  const team = await resolveTeam(config, options.team);
@@ -8328,24 +8465,24 @@ async function runSharePublish(config, sourceUri, options) {
8328
8465
  }
8329
8466
  await ensureSharedDirectoryChain(config, ov, targetUri, dryRun);
8330
8467
  await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
8331
- await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "publish");
8332
- const git = await requiredExecutable("git");
8333
8468
  const worktree = team.config.worktree;
8334
8469
  const relativePath = vikingUriToWorktreeRelative(config, targetUri, team.name);
8335
8470
  const message = options.message ?? `share: publish ${relativePath}`;
8336
- await maybeRun(dryRun, git, ["-C", worktree, "add", "--", relativePath]);
8337
- await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
8338
- if (options.push !== false) {
8339
- const pushResult = dryRun ? void 0 : await runCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
8340
- if (dryRun) {
8341
- console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME])}`);
8342
- } else if (pushResult && pushResult.exitCode !== 0) {
8343
- const detail = pushResult.stderr.trim() || pushResult.stdout.trim() || "unknown error";
8344
- throw new Error(
8345
- `Memory was committed locally but git push failed: ${detail}
8346
- Resolve the remote issue (auth, network, branch protection), then run: threadnote share sync`
8347
- );
8348
- }
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
+ );
8349
8486
  }
8350
8487
  console.log(`Published ${sourceUri} -> ${targetUri}`);
8351
8488
  }
@@ -8365,15 +8502,25 @@ async function runShareUnpublish(config, sourceUri, options) {
8365
8502
  );
8366
8503
  }
8367
8504
  await writeMemoryFile(config, ov, targetUri, content, "create", dryRun);
8368
- await removeWithRollback(config, ov, sourceUri, targetUri, team.config.worktree, dryRun, "unpublish");
8369
- const git = await requiredExecutable("git");
8370
8505
  const worktree = team.config.worktree;
8371
8506
  const relativePath = vikingUriToWorktreeRelative(config, sourceUri, team.name);
8372
8507
  const message = options.message ?? `share: unpublish ${relativePath}`;
8373
- await maybeRun(dryRun, git, ["-C", worktree, "rm", relativePath], { allowFailure: true });
8374
- await maybeRun(dryRun, git, ["-C", worktree, "commit", "-m", message], { allowFailure: true });
8375
- if (options.push !== false) {
8376
- 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
+ );
8377
8524
  }
8378
8525
  console.log(`Unpublished ${sourceUri} -> ${targetUri}`);
8379
8526
  }
@@ -8393,9 +8540,98 @@ async function runShareList(config, _options) {
8393
8540
  console.log(` added: ${team.addedAt}`);
8394
8541
  }
8395
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
+ }
8396
8628
  async function runShareRemove(config, options) {
8397
8629
  const team = await resolveTeam(config, options.team);
8398
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
+ }
8399
8635
  const teamsFile = await readTeamsFile(config);
8400
8636
  const remaining = {};
8401
8637
  for (const [name, value] of Object.entries(teamsFile.teams)) {
@@ -8419,6 +8655,47 @@ async function runShareRemove(config, options) {
8419
8655
  console.log(`Keeping files at ${portablePath(team.config.worktree)} and ${portablePath(team.config.gitdir)}`);
8420
8656
  }
8421
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
+ }
8422
8699
  function normalizeTeamName(input2) {
8423
8700
  const candidate = (input2 ?? "default").trim();
8424
8701
  if (!candidate) {
@@ -8816,47 +9093,6 @@ async function ingestSingleFile(ov, config, uri, filePath, initialMode, options
8816
9093
  const content = await (0, import_promises4.readFile)(filePath, "utf8");
8817
9094
  await writeMemoryFile(config, ov, uri, content, initialMode, false, options);
8818
9095
  }
8819
- async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
8820
- try {
8821
- await removeMemoryUri(config, ov, sourceUri, dryRun);
8822
- } catch (sourceErr) {
8823
- if (dryRun) {
8824
- throw sourceErr;
8825
- }
8826
- console.error(
8827
- `Source removal failed during ${label}; rolling back ${rollbackUri} so the system is back to the pre-${label} state.`
8828
- );
8829
- try {
8830
- await removeMemoryUri(config, ov, rollbackUri, false);
8831
- } catch (rollbackErr) {
8832
- console.error(
8833
- `Rollback of ${rollbackUri} also failed. Manual cleanup needed via: threadnote forget ${rollbackUri}
8834
- Rollback error: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`
8835
- );
8836
- }
8837
- await bestEffortRemoveWorktreeFile(rollbackUri, worktree, label);
8838
- throw sourceErr;
8839
- }
8840
- }
8841
- async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
8842
- if (label !== "publish") {
8843
- return;
8844
- }
8845
- const prefix = "viking://";
8846
- if (!rollbackUri.startsWith(prefix)) {
8847
- return;
8848
- }
8849
- const parts = rollbackUri.slice(prefix.length).split("/");
8850
- const sharedIndex = parts.indexOf("shared");
8851
- if (sharedIndex === -1 || sharedIndex + 2 >= parts.length) {
8852
- return;
8853
- }
8854
- const relative3 = parts.slice(sharedIndex + 2).join("/");
8855
- if (!relative3) {
8856
- return;
8857
- }
8858
- await (0, import_promises4.rm)((0, import_node_path5.join)(worktree, relative3), { force: true });
8859
- }
8860
9096
  async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
8861
9097
  const args = withIdentity(config, ["rm", uri]);
8862
9098
  if (dryRun) {
@@ -9311,6 +9547,40 @@ async function runCompact(config, options) {
9311
9547
  await runForget(config, action.uri, { dryRun: false });
9312
9548
  }
9313
9549
  }
9550
+ async function runCompactDiagnostics(config, options) {
9551
+ const project = normalizeOptionalMetadata2(options.project);
9552
+ if (!project) {
9553
+ throw new Error("Provide --project for scoped memory hygiene.");
9554
+ }
9555
+ const topic = normalizeOptionalMetadata2(options.topic);
9556
+ const records = await scopedCompactRecords(config, {
9557
+ kind: options.kind,
9558
+ project
9559
+ });
9560
+ const activeRecords = records.filter((record) => record.metadata.status === "active");
9561
+ const matchingRecords = activeRecords.filter((record) => topic === void 0 || topicForRecord(record) === topic);
9562
+ const counts = /* @__PURE__ */ new Map();
9563
+ for (const record of matchingRecords) {
9564
+ const kind = record.metadata.kind;
9565
+ if (kind === "durable" || kind === "handoff" || kind === "incident") {
9566
+ counts.set(kind, (counts.get(kind) ?? 0) + 1);
9567
+ }
9568
+ }
9569
+ console.log(
9570
+ [
9571
+ "Scope summary:",
9572
+ `- project: ${project}`,
9573
+ `- topic: ${topic ?? "(all)"}`,
9574
+ `- kind: ${options.kind ?? "(handoff, durable, incident)"}`,
9575
+ `- stable records read: ${records.length}`,
9576
+ `- active records in project: ${activeRecords.length}`,
9577
+ `- active records matching topic: ${matchingRecords.length}`,
9578
+ `- matching by kind: ${formatKindCounts(counts)}`,
9579
+ "- skipped by design: archived memories, shared memories, preferences, smoke records, seeded resources, and non-stable timestamped/global paths",
9580
+ ""
9581
+ ].join("\n")
9582
+ );
9583
+ }
9314
9584
  async function scopedCompactRecords(config, options) {
9315
9585
  const kinds = options.kind ? [options.kind] : ["handoff", "durable", "incident"];
9316
9586
  const records = [];
@@ -9339,6 +9609,9 @@ async function scopedCompactRecords(config, options) {
9339
9609
  }
9340
9610
  return records;
9341
9611
  }
9612
+ function formatKindCounts(counts) {
9613
+ return ["handoff", "durable", "incident"].map((kind) => `${kind} ${counts.get(kind) ?? 0}`).join(", ");
9614
+ }
9342
9615
  async function readMemoryRecordsByUri(config, uris) {
9343
9616
  const records = [];
9344
9617
  for (const uri of uris) {
@@ -9386,11 +9659,11 @@ function localMemoryPathForUri(config, uri) {
9386
9659
  if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
9387
9660
  return void 0;
9388
9661
  }
9389
- const relative3 = uri.slice(prefix.length);
9390
- if (relative3.includes("..") || relative3.startsWith("/")) {
9662
+ const relative4 = uri.slice(prefix.length);
9663
+ if (relative4.includes("..") || relative4.startsWith("/")) {
9391
9664
  return void 0;
9392
9665
  }
9393
- return (0, import_node_path6.join)(localUserMemoriesRoot(config), ...relative3.split("/"));
9666
+ return (0, import_node_path6.join)(localUserMemoriesRoot(config), ...relative4.split("/"));
9394
9667
  }
9395
9668
  async function runList(config, uri, options) {
9396
9669
  assertVikingUri(uri);
@@ -9624,62 +9897,20 @@ async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
9624
9897
  }
9625
9898
  await ensureSharedDirectoryChain(config, ov, targetUri, options.dryRun);
9626
9899
  await writeMemoryFile(config, ov, targetUri, memory, "replace", options.dryRun);
9627
- const git = await requiredExecutable("git");
9628
- await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "add", "--", relativePath]);
9629
- await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "commit", "-m", `share: update ${relativePath}`], {
9630
- allowFailure: true
9631
- });
9632
- await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "push", DEFAULT_GIT_REMOTE_NAME], {
9633
- allowFailure: true
9900
+ const gitMessages = await publishShareGitChange(team.config.worktree, relativePath, `share: update ${relativePath}`, {
9901
+ dryRun: options.dryRun
9634
9902
  });
9903
+ for (const message of gitMessages) {
9904
+ console.log(message);
9905
+ }
9635
9906
  for (const redaction of scrub.redactions) {
9636
9907
  console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
9637
9908
  }
9638
9909
  console.log(`Updated shared memory: ${targetUri}`);
9639
9910
  }
9640
9911
  async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode) {
9641
- const args = withIdentity(config, [
9642
- "write",
9643
- memoryUri,
9644
- "--from-file",
9645
- memoryPath,
9646
- "--mode",
9647
- writeMode,
9648
- "--wait",
9649
- "--timeout",
9650
- "120"
9651
- ]);
9652
- for (let attempt = 0; attempt < 4; attempt += 1) {
9653
- console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
9654
- const result = await runCommand(ov, args, { allowFailure: true });
9655
- if (result.exitCode === 0) {
9656
- if (result.stdout.trim()) {
9657
- console.log(result.stdout.trim());
9658
- }
9659
- if (result.stderr.trim()) {
9660
- console.error(result.stderr.trim());
9661
- }
9662
- return;
9663
- }
9664
- if (await vikingResourceExists2(ov, config, memoryUri)) {
9665
- console.log("OpenViking accepted the memory but returned before the wait completed; waiting for indexing.");
9666
- await waitForOpenVikingQueue(ov, config);
9667
- return;
9668
- }
9669
- if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
9670
- throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
9671
- }
9672
- await sleep(1e3 * (attempt + 1));
9673
- }
9674
- }
9675
- async function waitForOpenVikingQueue(ov, config) {
9676
- const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
9677
- if (result.stdout.trim()) {
9678
- console.log(result.stdout.trim());
9679
- }
9680
- if (result.stderr.trim()) {
9681
- console.error(result.stderr.trim());
9682
- }
9912
+ const content = await (0, import_promises5.readFile)(memoryPath, "utf8");
9913
+ await writeMemoryFile(config, ov, memoryUri, content, writeMode, false);
9683
9914
  }
9684
9915
  async function removeVikingResourceWithRetry(ov, config, uri) {
9685
9916
  const args = withIdentity(config, ["rm", uri]);
@@ -9706,8 +9937,8 @@ async function removeVikingResourceWithRetry(ov, config, uri) {
9706
9937
  return false;
9707
9938
  }
9708
9939
  async function vikingResourceExists2(ov, config, uri) {
9709
- const stat4 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
9710
- return stat4.exitCode === 0;
9940
+ const stat5 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
9941
+ return stat5.exitCode === 0;
9711
9942
  }
9712
9943
  async function ensureDurableMemoryDirectory(ov, config) {
9713
9944
  await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
@@ -11514,6 +11745,19 @@ function isUpdateNotificationDisabled() {
11514
11745
 
11515
11746
  // src/lifecycle.ts
11516
11747
  async function runDoctor(config, options) {
11748
+ const checks = await collectDoctorChecks(config, options);
11749
+ for (const check of checks) {
11750
+ console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
11751
+ }
11752
+ const failureCount = checks.filter((check) => check.status === "fail").length;
11753
+ const warningCount = checks.filter((check) => check.status === "warn").length;
11754
+ console.log(`
11755
+ Summary: ${failureCount} failure(s), ${warningCount} warning(s)`);
11756
+ if (options.strict === true && failureCount > 0) {
11757
+ process.exitCode = 1;
11758
+ }
11759
+ }
11760
+ async function collectDoctorChecks(config, options = {}) {
11517
11761
  const checks = [];
11518
11762
  checks.push({ name: "mode", status: "ok", detail: options.dryRun ? "dry run; no writes" : "read-only checks" });
11519
11763
  checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
@@ -11533,16 +11777,7 @@ async function runDoctor(config, options) {
11533
11777
  checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
11534
11778
  checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
11535
11779
  checks.push(await healthCheck(config));
11536
- for (const check of checks) {
11537
- console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
11538
- }
11539
- const failureCount = checks.filter((check) => check.status === "fail").length;
11540
- const warningCount = checks.filter((check) => check.status === "warn").length;
11541
- console.log(`
11542
- Summary: ${failureCount} failure(s), ${warningCount} warning(s)`);
11543
- if (options.strict === true && failureCount > 0) {
11544
- process.exitCode = 1;
11545
- }
11780
+ return checks;
11546
11781
  }
11547
11782
  async function runInstall(config, options) {
11548
11783
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
@@ -12601,20 +12836,1010 @@ function printWhatsNew(whatsNew) {
12601
12836
  }
12602
12837
  }
12603
12838
 
12604
- // src/threadnote.ts
12605
- async function main() {
12606
- const program2 = new Command();
12607
- 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);
12608
- 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) => {
12609
- const config = getRuntimeConfig(program2);
12610
- await runDoctor(config, options);
12611
- await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
12839
+ // src/manager.ts
12840
+ var import_node_http2 = require("node:http");
12841
+ var import_node_crypto2 = require("node:crypto");
12842
+ var import_promises13 = require("node:fs/promises");
12843
+ var import_node_os7 = require("node:os");
12844
+ var import_node_path13 = require("node:path");
12845
+ var STATIC_FILES = {
12846
+ "/": { contentType: "text/html; charset=utf-8", path: "index.html" },
12847
+ "/index.html": { contentType: "text/html; charset=utf-8", path: "index.html" },
12848
+ "/app.css": { contentType: "text/css; charset=utf-8", path: "app.css" },
12849
+ "/app.js": { contentType: "text/javascript; charset=utf-8", path: "app.js" },
12850
+ "/threadnote-logo.svg": { contentType: "image/svg+xml; charset=utf-8", path: "threadnote-logo.svg", root: "docs" },
12851
+ "/threadnote-logo-inverted.svg": {
12852
+ contentType: "image/svg+xml; charset=utf-8",
12853
+ path: "threadnote-logo-inverted.svg",
12854
+ root: "docs"
12855
+ }
12856
+ };
12857
+ async function runManage(config, options) {
12858
+ const token = (0, import_node_crypto2.randomBytes)(24).toString("base64url");
12859
+ const server = createManagerServer({ config, jobs: /* @__PURE__ */ new Map(), token });
12860
+ const port = parseUiPort(options.uiPort);
12861
+ await listen(server, port);
12862
+ const address = server.address();
12863
+ const actualPort = typeof address === "object" && address ? address.port : port;
12864
+ const url = `http://127.0.0.1:${actualPort}/?token=${encodeURIComponent(token)}`;
12865
+ console.log(`Threadnote manager: ${url}`);
12866
+ console.log("Press Ctrl-C to stop the manager.");
12867
+ if (options.open !== false) {
12868
+ await runCommand("open", [url], { allowFailure: true });
12869
+ }
12870
+ await new Promise((resolve2, reject) => {
12871
+ const close = () => server.close((err) => err ? reject(err) : resolve2());
12872
+ process.once("SIGINT", close);
12873
+ process.once("SIGTERM", close);
12612
12874
  });
12613
- program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--force", "Reinstall OpenViking at the pinned version even if a working install is already present").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).option(
12614
- "--with-hooks",
12615
- "Also install agent-side hooks (Claude PreCompact + SessionStart) for deterministic handoff snapshots and context preload"
12616
- ).action(async (options) => {
12617
- const config = getRuntimeConfig(program2);
12875
+ }
12876
+ function createManagerServer(context) {
12877
+ return (0, import_node_http2.createServer)((request, response) => {
12878
+ void handleRequest(context, request, response).catch((err) => {
12879
+ writeJson(response, 500, { error: errorMessage(err) });
12880
+ });
12881
+ });
12882
+ }
12883
+ async function memoryTree(config) {
12884
+ const root = localMemoriesRoot(config);
12885
+ return readTree(config, root, `viking://user/${uriSegment(config.user)}/memories`, "");
12886
+ }
12887
+ async function readManagedMemory(config, uri) {
12888
+ assertVikingUri(uri);
12889
+ const path = localPathForMemoryUri(config, uri);
12890
+ if (!path) {
12891
+ throw new Error(`Manager can only read current-user memory URIs: ${uri}`);
12892
+ }
12893
+ const [content, pathStat] = await Promise.all([(0, import_promises13.readFile)(path, "utf8"), (0, import_promises13.stat)(path)]);
12894
+ const relativePath = (0, import_node_path13.relative)(localMemoriesRoot(config), path).split(import_node_path13.sep).join("/");
12895
+ const record = parseMemoryDocument(uri, content);
12896
+ return {
12897
+ content,
12898
+ node: {
12899
+ isDir: false,
12900
+ isShared: isInSharedNamespace(config, uri),
12901
+ isSystem: isSystemMemoryName(path.split(import_node_path13.sep).at(-1) ?? ""),
12902
+ metadata: record?.metadata,
12903
+ modTime: pathStat.mtime.toISOString(),
12904
+ name: path.split(import_node_path13.sep).at(-1) ?? uri,
12905
+ relativePath,
12906
+ sharedTeam: sharedTeamNameForUri(config, uri),
12907
+ size: pathStat.size,
12908
+ uri
12909
+ },
12910
+ record
12911
+ };
12912
+ }
12913
+ async function readContextUri(config, uri) {
12914
+ assertVikingUri(uri);
12915
+ try {
12916
+ const localMemory = await readManagedMemory(config, uri);
12917
+ return { content: localMemory.content, localMemory, output: localMemory.content };
12918
+ } catch {
12919
+ const result = await runCaptured(() => runRead(config, uri, {}));
12920
+ return { content: result.output, output: result.output };
12921
+ }
12922
+ }
12923
+ async function detectConsolidationAgents() {
12924
+ const [codex, claude, cursor, copilot] = await Promise.all([
12925
+ findExecutable(["codex"]),
12926
+ findExecutable(["claude"]),
12927
+ findExecutable(["cursor-agent"]),
12928
+ findExecutable(["copilot"])
12929
+ ]);
12930
+ return [
12931
+ { available: codex !== void 0, command: codex, id: "codex", label: "Codex" },
12932
+ { available: claude !== void 0, command: claude, id: "claude", label: "Claude" },
12933
+ { available: cursor !== void 0, command: cursor, id: "cursor", label: "Cursor" },
12934
+ { available: copilot !== void 0, command: copilot, id: "copilot", label: "Copilot" }
12935
+ ];
12936
+ }
12937
+ async function handleRequest(context, request, response) {
12938
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
12939
+ if (request.method === "GET" && STATIC_FILES[url.pathname]) {
12940
+ await serveStatic(context, url, response);
12941
+ return;
12942
+ }
12943
+ if (request.method === "GET" && url.pathname === "/favicon.ico") {
12944
+ response.writeHead(204, { "cache-control": "no-store" });
12945
+ response.end();
12946
+ return;
12947
+ }
12948
+ if (!isAuthorized(context, request)) {
12949
+ writeJson(response, 401, { error: "Unauthorized" });
12950
+ return;
12951
+ }
12952
+ if (request.method === "GET" && url.pathname === "/api/state") {
12953
+ const agents = await detectConsolidationAgents();
12954
+ const version = await currentPackageVersion();
12955
+ let latestVersion;
12956
+ try {
12957
+ latestVersion = await fetchLatestVersion2(normalizeRegistry(updateRegistry()));
12958
+ } catch {
12959
+ latestVersion = void 0;
12960
+ }
12961
+ writeJson(response, 200, {
12962
+ agents,
12963
+ config: publicConfig(context.config),
12964
+ latestVersion,
12965
+ openVikingLogPath: openVikingLogPath(context.config),
12966
+ version
12967
+ });
12968
+ return;
12969
+ }
12970
+ if (request.method === "GET" && url.pathname === "/api/tree") {
12971
+ writeJson(response, 200, { tree: await memoryTree(context.config) });
12972
+ return;
12973
+ }
12974
+ if (request.method === "GET" && url.pathname === "/api/memory") {
12975
+ writeJson(response, 200, await readManagedMemory(context.config, requiredQuery(url, "uri")));
12976
+ return;
12977
+ }
12978
+ if (request.method === "GET" && url.pathname === "/api/read") {
12979
+ writeJson(response, 200, await readContextUri(context.config, requiredQuery(url, "uri")));
12980
+ return;
12981
+ }
12982
+ if (request.method === "GET" && url.pathname === "/api/shares") {
12983
+ writeJson(response, 200, { shares: await shareSummaries(context.config) });
12984
+ return;
12985
+ }
12986
+ if (request.method === "GET" && url.pathname === "/api/doctor") {
12987
+ writeJson(response, 200, {
12988
+ checks: await collectManagerDoctorChecks(context.config),
12989
+ shares: await shareSummaries(context.config)
12990
+ });
12991
+ return;
12992
+ }
12993
+ if (request.method === "GET" && url.pathname.startsWith("/api/consolidations/")) {
12994
+ const id = url.pathname.split("/").at(-1) ?? "";
12995
+ const job = context.jobs.get(id);
12996
+ writeJson(response, job ? 200 : 404, job ? { job } : { error: "Consolidation job not found" });
12997
+ return;
12998
+ }
12999
+ if (request.method !== "POST") {
13000
+ writeJson(response, 404, { error: "Not found" });
13001
+ return;
13002
+ }
13003
+ const body = await readJsonBody(request);
13004
+ switch (url.pathname) {
13005
+ case "/api/memory/archive":
13006
+ requireConfirm(body);
13007
+ writeJson(
13008
+ response,
13009
+ 200,
13010
+ await runCaptured(() => runArchive(context.config, requireString(body.uri, "uri"), body))
13011
+ );
13012
+ return;
13013
+ case "/api/memory/forget":
13014
+ requireConfirm(body);
13015
+ writeJson(response, 200, await runCaptured(() => runForget(context.config, requireString(body.uri, "uri"), {})));
13016
+ return;
13017
+ case "/api/memory/save":
13018
+ writeJson(response, 200, await saveMemory(context.config, body));
13019
+ return;
13020
+ case "/api/memory/move":
13021
+ requireConfirm(body);
13022
+ writeJson(response, 200, await moveMemory(context.config, requireString(body.uri, "uri"), targetFromBody(body)));
13023
+ return;
13024
+ case "/api/memory/publish":
13025
+ requireConfirm(body);
13026
+ writeJson(
13027
+ response,
13028
+ 200,
13029
+ await runCaptured(
13030
+ () => runSharePublish(context.config, requireString(body.uri, "uri"), {
13031
+ redact: body.redact === true,
13032
+ team: optionalString(body.team)
13033
+ })
13034
+ )
13035
+ );
13036
+ return;
13037
+ case "/api/memory/unpublish":
13038
+ requireConfirm(body);
13039
+ writeJson(
13040
+ response,
13041
+ 200,
13042
+ await runCaptured(
13043
+ () => runShareUnpublish(context.config, requireString(body.uri, "uri"), { team: optionalString(body.team) })
13044
+ )
13045
+ );
13046
+ return;
13047
+ case "/api/folder/remove":
13048
+ requireConfirm(body);
13049
+ writeJson(
13050
+ response,
13051
+ 200,
13052
+ await runCaptured(() => removeManagedFolder(context.config, requireString(body.uri, "uri")))
13053
+ );
13054
+ return;
13055
+ case "/api/bulk":
13056
+ requireConfirm(body);
13057
+ writeJson(response, 200, await runBulk(context.config, body));
13058
+ return;
13059
+ case "/api/compact":
13060
+ if (body.apply === true) {
13061
+ requireConfirm(body);
13062
+ }
13063
+ writeJson(
13064
+ response,
13065
+ 200,
13066
+ await runCaptured(async () => {
13067
+ await runCompactDiagnostics(context.config, body);
13068
+ await runCompact(context.config, body);
13069
+ })
13070
+ );
13071
+ return;
13072
+ case "/api/recall":
13073
+ writeJson(
13074
+ response,
13075
+ 200,
13076
+ await runCaptured(
13077
+ () => runRecall(context.config, {
13078
+ query: requireString(body.query, "query"),
13079
+ nodeLimit: optionalString(body.nodeLimit)
13080
+ })
13081
+ )
13082
+ );
13083
+ return;
13084
+ case "/api/read":
13085
+ writeJson(response, 200, await readContextUri(context.config, requireString(body.uri, "uri")));
13086
+ return;
13087
+ case "/api/shares/init":
13088
+ requireConfirm(body);
13089
+ writeJson(
13090
+ response,
13091
+ 200,
13092
+ await runCaptured(
13093
+ () => runShareInit(context.config, requireString(body.remoteUrl, "remoteUrl"), { team: optionalString(body.team) })
13094
+ )
13095
+ );
13096
+ return;
13097
+ case "/api/shares/rename":
13098
+ requireConfirm(body);
13099
+ writeJson(
13100
+ response,
13101
+ 200,
13102
+ await runCaptured(
13103
+ () => runShareRename(context.config, { team: requireString(body.team, "team"), to: requireString(body.to, "to") })
13104
+ )
13105
+ );
13106
+ return;
13107
+ case "/api/shares/set-url":
13108
+ requireConfirm(body);
13109
+ writeJson(
13110
+ response,
13111
+ 200,
13112
+ await runCaptured(
13113
+ () => runShareSetUrl(context.config, requireString(body.remoteUrl, "remoteUrl"), { team: optionalString(body.team) })
13114
+ )
13115
+ );
13116
+ return;
13117
+ case "/api/shares/remove":
13118
+ requireConfirm(body);
13119
+ writeJson(
13120
+ response,
13121
+ 200,
13122
+ await runCaptured(
13123
+ () => runShareRemove(context.config, {
13124
+ keepFiles: body.keepFiles === true,
13125
+ preserveLocal: body.preserveLocal === true,
13126
+ team: optionalString(body.team)
13127
+ })
13128
+ )
13129
+ );
13130
+ return;
13131
+ case "/api/shares/sync":
13132
+ writeJson(
13133
+ response,
13134
+ 200,
13135
+ await runCaptured(() => runShareSync(context.config, { team: optionalString(body.team) }))
13136
+ );
13137
+ return;
13138
+ case "/api/doctor/start":
13139
+ writeJson(response, 200, await runCaptured(() => runStart(context.config, {})));
13140
+ return;
13141
+ case "/api/doctor/repair-dry-run":
13142
+ writeJson(response, 200, await runCaptured(() => runRepair(context.config, { dryRun: true })));
13143
+ return;
13144
+ case "/api/doctor/repair":
13145
+ requireConfirm(body);
13146
+ writeJson(response, 200, await runCaptured(() => runRepair(context.config, { dryRun: false })));
13147
+ return;
13148
+ case "/api/import-pack":
13149
+ requireConfirm(body);
13150
+ writeJson(
13151
+ response,
13152
+ 200,
13153
+ await runCaptured(() => runImportPack(context.config, { path: requireString(body.path, "path") }))
13154
+ );
13155
+ return;
13156
+ case "/api/export-pack":
13157
+ writeJson(
13158
+ response,
13159
+ 200,
13160
+ await runCaptured(
13161
+ () => runExportPack(context.config, { path: optionalString(body.path), uri: optionalString(body.uri) })
13162
+ )
13163
+ );
13164
+ return;
13165
+ case "/api/seed":
13166
+ requireConfirm(body);
13167
+ writeJson(
13168
+ response,
13169
+ 200,
13170
+ await runCaptured(
13171
+ () => body.skills === true ? runSeedSkills(context.config, { dryRun: body.dryRun === true }) : runSeed(context.config, { dryRun: body.dryRun === true })
13172
+ )
13173
+ );
13174
+ return;
13175
+ case "/api/consolidations":
13176
+ writeJson(response, 200, { job: await createConsolidation(context, body) });
13177
+ return;
13178
+ default:
13179
+ if (url.pathname.startsWith("/api/consolidations/") && url.pathname.endsWith("/apply")) {
13180
+ requireConfirm(body);
13181
+ const id = url.pathname.split("/").at(-2) ?? "";
13182
+ writeJson(response, 200, await applyConsolidation(context.config, context.jobs, id, body));
13183
+ return;
13184
+ }
13185
+ writeJson(response, 404, { error: "Not found" });
13186
+ }
13187
+ }
13188
+ async function serveStatic(context, url, response) {
13189
+ const file = STATIC_FILES[url.pathname] ?? STATIC_FILES["/"];
13190
+ const content = await (0, import_promises13.readFile)((0, import_node_path13.join)(toolRoot(), file.root ?? "manager", file.path));
13191
+ const headers = { "content-type": file.contentType };
13192
+ if (url.pathname === "/" || url.pathname === "/index.html") {
13193
+ headers["cache-control"] = "no-store";
13194
+ }
13195
+ response.writeHead(200, headers);
13196
+ response.end(content);
13197
+ }
13198
+ async function readTree(config, path, uri, relativePath) {
13199
+ const pathStat = await (0, import_promises13.stat)(path);
13200
+ const name = relativePath ? relativePath.split("/").at(-1) ?? relativePath : "memories";
13201
+ const isDir = pathStat.isDirectory();
13202
+ if (!isDir) {
13203
+ const content = await (0, import_promises13.readFile)(path, "utf8").catch(() => "");
13204
+ const record = parseMemoryDocument(uri, content);
13205
+ return {
13206
+ isDir: false,
13207
+ isShared: isInSharedNamespace(config, uri),
13208
+ isSystem: isSystemMemoryName(name),
13209
+ metadata: record?.metadata,
13210
+ modTime: pathStat.mtime.toISOString(),
13211
+ name,
13212
+ relativePath,
13213
+ sharedTeam: sharedTeamNameForUri(config, uri),
13214
+ size: pathStat.size,
13215
+ uri
13216
+ };
13217
+ }
13218
+ const entries = await (0, import_promises13.readdir)(path, { withFileTypes: true });
13219
+ const children = await Promise.all(
13220
+ entries.sort(
13221
+ (left, right) => Number(right.isDirectory()) - Number(left.isDirectory()) || left.name.localeCompare(right.name)
13222
+ ).map((entry) => {
13223
+ const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
13224
+ return readTree(config, (0, import_node_path13.join)(path, entry.name), `${uri}/${entry.name}`, childRelative);
13225
+ })
13226
+ );
13227
+ return {
13228
+ children,
13229
+ isDir: true,
13230
+ isShared: isInSharedNamespace(config, uri),
13231
+ isSystem: isSystemMemoryName(name),
13232
+ modTime: pathStat.mtime.toISOString(),
13233
+ name,
13234
+ relativePath,
13235
+ sharedTeam: sharedTeamNameForUri(config, uri),
13236
+ size: pathStat.size,
13237
+ uri
13238
+ };
13239
+ }
13240
+ async function saveMemory(config, body) {
13241
+ const text = requireString(body.text, "text");
13242
+ const replaceUri = optionalString(body.replaceUri);
13243
+ if (replaceUri && isRawMemoryDocument(text)) {
13244
+ return runCaptured(() => writeRawMemory(config, replaceUri, text));
13245
+ }
13246
+ return runCaptured(
13247
+ () => runRemember(config, {
13248
+ kind: memoryKind(body.kind) ?? "durable",
13249
+ project: optionalString(body.project),
13250
+ replace: replaceUri,
13251
+ sourceAgentClient: optionalString(body.sourceAgentClient) ?? "manager",
13252
+ status: memoryStatus(body.status) ?? "active",
13253
+ text,
13254
+ topic: optionalString(body.topic)
13255
+ })
13256
+ );
13257
+ }
13258
+ async function writeRawMemory(config, uri, content) {
13259
+ assertVikingUri(uri);
13260
+ const ov = await openVikingCliForMode(false);
13261
+ if (isInSharedNamespace(config, uri)) {
13262
+ const teamName = sharedTeamNameForUri(config, uri);
13263
+ if (!teamName) {
13264
+ throw new Error(`${uri} is not in a configured shared namespace.`);
13265
+ }
13266
+ const team = await resolveTeam(config, teamName);
13267
+ await ensureSharedDirectoryChain(config, ov, uri, false);
13268
+ await writeMemoryFile(config, ov, uri, content, "replace", false);
13269
+ const relativePath = vikingUriToWorktreeRelative(config, uri, team.name);
13270
+ await publishShareGitChange(team.config.worktree, relativePath, `share: update ${relativePath}`);
13271
+ return;
13272
+ }
13273
+ await ensurePersonalDirectoryChain2(config, ov, parentUri(uri));
13274
+ await writeMemoryFile(config, ov, uri, content, "replace", false);
13275
+ }
13276
+ async function moveMemory(config, sourceUri, target) {
13277
+ assertVikingUri(sourceUri);
13278
+ const source = await readManagedMemory(config, sourceUri);
13279
+ const sourceRecord = source.record;
13280
+ const text = sourceRecord?.body ?? source.content;
13281
+ const metadata = {
13282
+ kind: target.kind ?? sourceRecord?.metadata.kind ?? "durable",
13283
+ project: target.project ?? sourceRecord?.metadata.project ?? "general",
13284
+ sourceAgentClient: target.sourceAgentClient ?? "manager",
13285
+ status: target.status ?? sourceRecord?.metadata.status ?? "active",
13286
+ topic: target.topic ?? sourceRecord?.metadata.topic ?? "current"
13287
+ };
13288
+ const personalTargetUri = memoryUriFor2(config, metadata);
13289
+ if (target.team) {
13290
+ const targetTeam = target.team;
13291
+ if (isInSharedNamespace(config, sourceUri)) {
13292
+ const team = sharedTeamNameForUri(config, sourceUri);
13293
+ if (team !== targetTeam) {
13294
+ throw new Error(
13295
+ "Cross-team shared moves are not supported in V1. Copy/unpublish, then publish to the target team."
13296
+ );
13297
+ }
13298
+ const sharedTargetUri = sharedMemoryUriFor(config, targetTeam, metadata);
13299
+ const output3 = await runCaptured(
13300
+ () => moveSharedWithinTeam(config, sourceUri, sharedTargetUri, source.content, targetTeam)
13301
+ );
13302
+ return { ...output3, targetUri: sharedTargetUri };
13303
+ }
13304
+ const saved = await runCaptured(
13305
+ () => runRemember(config, {
13306
+ kind: metadata.kind,
13307
+ project: metadata.project,
13308
+ replace: sourceUri,
13309
+ sourceAgentClient: metadata.sourceAgentClient,
13310
+ status: metadata.status,
13311
+ text,
13312
+ topic: metadata.topic
13313
+ })
13314
+ );
13315
+ const published = await runCaptured(() => runSharePublish(config, personalTargetUri, { team: targetTeam }));
13316
+ return {
13317
+ output: [saved.output, published.output].filter(Boolean).join("\n"),
13318
+ targetUri: sharedMemoryUriFor(config, targetTeam, metadata)
13319
+ };
13320
+ }
13321
+ if (isInSharedNamespace(config, sourceUri)) {
13322
+ const saved = await runCaptured(
13323
+ () => runRemember(config, {
13324
+ kind: metadata.kind,
13325
+ project: metadata.project,
13326
+ sourceAgentClient: metadata.sourceAgentClient,
13327
+ status: metadata.status,
13328
+ text,
13329
+ topic: metadata.topic
13330
+ })
13331
+ );
13332
+ const removed = await runCaptured(() => removeSharedSource(config, sourceUri));
13333
+ return { output: [saved.output, removed.output].filter(Boolean).join("\n"), targetUri: personalTargetUri };
13334
+ }
13335
+ const output2 = await runCaptured(
13336
+ () => runRemember(config, {
13337
+ kind: metadata.kind,
13338
+ project: metadata.project,
13339
+ replace: sourceUri,
13340
+ sourceAgentClient: metadata.sourceAgentClient,
13341
+ status: metadata.status,
13342
+ text,
13343
+ topic: metadata.topic
13344
+ })
13345
+ );
13346
+ return { ...output2, targetUri: personalTargetUri };
13347
+ }
13348
+ async function moveSharedWithinTeam(config, sourceUri, targetUri, content, teamName) {
13349
+ const team = await resolveTeam(config, teamName);
13350
+ const ov = await openVikingCliForMode(false);
13351
+ await ensureSharedDirectoryChain(config, ov, targetUri, false);
13352
+ await writeMemoryFile(config, ov, targetUri, content, "create", false);
13353
+ await publishShareGitChange(
13354
+ team.config.worktree,
13355
+ vikingUriToWorktreeRelative(config, targetUri, team.name),
13356
+ `share: move ${vikingUriToWorktreeRelative(config, sourceUri, team.name)} to ${vikingUriToWorktreeRelative(config, targetUri, team.name)}`
13357
+ );
13358
+ await publishShareGitChange(
13359
+ team.config.worktree,
13360
+ vikingUriToWorktreeRelative(config, sourceUri, team.name),
13361
+ `share: remove ${vikingUriToWorktreeRelative(config, sourceUri, team.name)}`,
13362
+ {
13363
+ verb: "rm"
13364
+ }
13365
+ );
13366
+ await removeMemoryUri(config, ov, sourceUri, false);
13367
+ }
13368
+ async function removeSharedSource(config, sourceUri) {
13369
+ const teamName = sharedTeamNameForUri(config, sourceUri);
13370
+ if (!teamName) {
13371
+ throw new Error(`${sourceUri} is not a shared memory.`);
13372
+ }
13373
+ const team = await resolveTeam(config, teamName);
13374
+ const ov = await openVikingCliForMode(false);
13375
+ await publishShareGitChange(
13376
+ team.config.worktree,
13377
+ vikingUriToWorktreeRelative(config, sourceUri, team.name),
13378
+ `share: remove ${vikingUriToWorktreeRelative(config, sourceUri, team.name)}`,
13379
+ {
13380
+ verb: "rm"
13381
+ }
13382
+ );
13383
+ await removeMemoryUri(config, ov, sourceUri, false);
13384
+ }
13385
+ async function removeManagedFolder(config, uri) {
13386
+ assertVikingUri(uri);
13387
+ const rootUri = `viking://user/${uriSegment(config.user)}/memories`;
13388
+ if (uri === rootUri) {
13389
+ throw new Error("Refusing to remove the root memories folder.");
13390
+ }
13391
+ if (isInSharedNamespace(config, uri)) {
13392
+ throw new Error("Shared folders are managed from Sharing. Remove the share or unpublish selected memories.");
13393
+ }
13394
+ const path = localPathForMemoryUri(config, uri);
13395
+ if (!path) {
13396
+ throw new Error(`Manager can only remove current-user memory folders: ${uri}`);
13397
+ }
13398
+ const pathStat = await (0, import_promises13.stat)(path);
13399
+ if (!pathStat.isDirectory()) {
13400
+ throw new Error(`Not a folder: ${uri}`);
13401
+ }
13402
+ const relativePath = (0, import_node_path13.relative)(localMemoriesRoot(config), path);
13403
+ if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path13.sep).includes("..")) {
13404
+ throw new Error("Refusing to remove a folder outside the memories tree.");
13405
+ }
13406
+ const fileUris = await fileUrisUnderFolder(config, path);
13407
+ for (const fileUri of fileUris) {
13408
+ await runForget(config, fileUri, {});
13409
+ }
13410
+ await (0, import_promises13.rm)(path, { force: true, recursive: true });
13411
+ console.log(`Removed folder: ${uri}`);
13412
+ console.log(`Forgot ${fileUris.length} file${fileUris.length === 1 ? "" : "s"}.`);
13413
+ }
13414
+ async function fileUrisUnderFolder(config, folderPath) {
13415
+ const entries = await (0, import_promises13.readdir)(folderPath, { withFileTypes: true });
13416
+ const uris = [];
13417
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
13418
+ const path = (0, import_node_path13.join)(folderPath, entry.name);
13419
+ if (entry.isDirectory()) {
13420
+ uris.push(...await fileUrisUnderFolder(config, path));
13421
+ } else if (entry.isFile()) {
13422
+ uris.push(localPathToMemoryUri(config, path));
13423
+ }
13424
+ }
13425
+ return uris;
13426
+ }
13427
+ async function runBulk(config, body) {
13428
+ const action = requireString(body.action, "action");
13429
+ const uris = requireStringArray(body.uris, "uris");
13430
+ const results = [];
13431
+ for (const uri of uris) {
13432
+ try {
13433
+ let output2;
13434
+ if (action === "archive") {
13435
+ output2 = (await runCaptured(() => runArchive(config, uri, {}))).output;
13436
+ } else if (action === "forget") {
13437
+ output2 = (await runCaptured(() => runForget(config, uri, {}))).output;
13438
+ } else if (action === "publish") {
13439
+ output2 = (await runCaptured(() => runSharePublish(config, uri, { team: optionalString(body.team) }))).output;
13440
+ } else {
13441
+ throw new Error(`Unsupported bulk action: ${action}`);
13442
+ }
13443
+ results.push({ ok: true, output: output2, uri });
13444
+ } catch (err) {
13445
+ results.push({ error: errorMessage(err), ok: false, uri });
13446
+ }
13447
+ }
13448
+ return { results };
13449
+ }
13450
+ async function createConsolidation(context, body) {
13451
+ const agent = agentClient(requireString(body.agent, "agent"));
13452
+ const sourceUris = requireStringArray(body.uris, "uris");
13453
+ const target = targetFromBody(body);
13454
+ const job = {
13455
+ agent,
13456
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13457
+ id: (0, import_node_crypto2.randomUUID)(),
13458
+ sourceUris,
13459
+ status: "running",
13460
+ target
13461
+ };
13462
+ context.jobs.set(job.id, job);
13463
+ try {
13464
+ const sources = await Promise.all(sourceUris.map((uri) => readManagedMemory(context.config, uri)));
13465
+ job.draft = await runConsolidationAgent(agent, sources);
13466
+ job.status = "completed";
13467
+ } catch (err) {
13468
+ job.error = errorMessage(err);
13469
+ job.status = "failed";
13470
+ }
13471
+ return job;
13472
+ }
13473
+ async function applyConsolidation(config, jobs, id, body) {
13474
+ const job = jobs.get(id);
13475
+ if (!job) {
13476
+ throw new Error("Consolidation job not found.");
13477
+ }
13478
+ if (job.status !== "completed" || !job.draft) {
13479
+ throw new Error("Consolidation job is not completed.");
13480
+ }
13481
+ const draft = optionalString(body.draft) ?? job.draft;
13482
+ const target = targetFromBody({ ...job.target, ...body });
13483
+ const saved = await runCaptured(
13484
+ () => runRemember(config, {
13485
+ kind: target.kind ?? "durable",
13486
+ project: target.project,
13487
+ sourceAgentClient: target.sourceAgentClient ?? "manager",
13488
+ status: target.status ?? "active",
13489
+ text: draft,
13490
+ topic: target.topic
13491
+ })
13492
+ );
13493
+ const cleanup = cleanupMode(body.cleanup);
13494
+ const cleanupOutputs = [];
13495
+ if (cleanup !== "keep") {
13496
+ for (const uri of job.sourceUris) {
13497
+ if (isInSharedNamespace(config, uri) && body.cleanupShared !== true) {
13498
+ cleanupOutputs.push(`Skipped shared source cleanup: ${uri}`);
13499
+ continue;
13500
+ }
13501
+ const action = cleanup === "forget" ? () => runForget(config, uri, {}) : () => runArchive(config, uri, {});
13502
+ cleanupOutputs.push((await runCaptured(action)).output);
13503
+ }
13504
+ }
13505
+ return { output: [saved.output, ...cleanupOutputs].filter(Boolean).join("\n") };
13506
+ }
13507
+ async function runConsolidationAgent(agent, sources) {
13508
+ if (agent !== "codex" && agent !== "claude") {
13509
+ throw new Error(`${agent} does not expose a supported non-interactive consolidation mode.`);
13510
+ }
13511
+ const executable = await findExecutable([agent]);
13512
+ if (!executable) {
13513
+ throw new Error(`${agent} executable was not found.`);
13514
+ }
13515
+ const prompt = consolidationPrompt(sources);
13516
+ const stagingDir = await (0, import_promises13.mkdtemp)((0, import_node_path13.join)((0, import_node_os7.tmpdir)(), "threadnote-consolidate-"));
13517
+ const promptPath = (0, import_node_path13.join)(stagingDir, "prompt.txt");
13518
+ try {
13519
+ await (0, import_promises13.writeFile)(promptPath, prompt, { encoding: "utf8", mode: 384 });
13520
+ await (0, import_promises13.chmod)(promptPath, 384);
13521
+ const script = consolidationAgentScript(agent, executable);
13522
+ const result = await runCommand("sh", ["-lc", script, "threadnote-consolidate", promptPath], {
13523
+ allowFailure: true,
13524
+ maxOutputBytes: 1024 * 1024,
13525
+ timeoutMs: 10 * 60 * 1e3
13526
+ });
13527
+ if (result.exitCode !== 0) {
13528
+ throw new Error(result.stderr.trim() || result.stdout.trim() || `${agent} exited with ${result.exitCode}`);
13529
+ }
13530
+ const draft = result.stdout.trim();
13531
+ if (!draft) {
13532
+ throw new Error(`${agent} returned an empty consolidation draft.`);
13533
+ }
13534
+ return draft;
13535
+ } finally {
13536
+ await (0, import_promises13.rm)(stagingDir, { force: true, recursive: true });
13537
+ }
13538
+ }
13539
+ function consolidationAgentScript(agent, executable) {
13540
+ if (agent === "codex") {
13541
+ return `${shellQuote(executable)} exec --sandbox read-only --skip-git-repo-check - < "$1"`;
13542
+ }
13543
+ if (agent === "claude") {
13544
+ return `${shellQuote(executable)} --print --permission-mode default < "$1"`;
13545
+ }
13546
+ throw new Error(`${agent} does not expose a supported non-interactive consolidation mode.`);
13547
+ }
13548
+ function consolidationPrompt(sources) {
13549
+ return [
13550
+ "Consolidate these Threadnote memories into one concise memory.",
13551
+ "Return only the replacement memory body in Markdown. Do not include frontmatter.",
13552
+ "Preserve important facts, current status, decisions, blockers, and source viking:// URIs.",
13553
+ "",
13554
+ ...sources.flatMap((source) => [`--- SOURCE ${source.node.uri} ---`, source.content.trim(), ""])
13555
+ ].join("\n");
13556
+ }
13557
+ async function shareSummaries(config) {
13558
+ const teamsFile = await readTeamsFile(config);
13559
+ const git = await findExecutable(["git"]);
13560
+ const entries = await Promise.all(
13561
+ Object.values(teamsFile.teams).map(async (team) => {
13562
+ if (!git) {
13563
+ return { ...team, default: teamsFile.defaultTeam === team.name, warning: "git not found" };
13564
+ }
13565
+ const status = await runCommand(git, ["-C", team.worktree, "status", "--short", "--branch"], {
13566
+ allowFailure: true
13567
+ });
13568
+ const ahead = await gitCount(git, team.worktree, "@{u}..HEAD");
13569
+ const behind = await gitCount(git, team.worktree, "HEAD..@{u}");
13570
+ return {
13571
+ ...team,
13572
+ ahead,
13573
+ behind,
13574
+ default: teamsFile.defaultTeam === team.name,
13575
+ dirty: status.stdout.split("\n").some((line) => line.trim().length > 0 && !line.startsWith("##")),
13576
+ status: status.stdout.trim(),
13577
+ warning: status.exitCode === 0 ? void 0 : status.stderr.trim() || status.stdout.trim()
13578
+ };
13579
+ })
13580
+ );
13581
+ return entries.sort((left, right) => left.name.localeCompare(right.name));
13582
+ }
13583
+ async function collectManagerDoctorChecks(config) {
13584
+ const threadnote = await findExecutable(["threadnote"]);
13585
+ if (!threadnote) {
13586
+ return collectDoctorChecks(config, {});
13587
+ }
13588
+ const result = await runCommand(
13589
+ threadnote,
13590
+ [
13591
+ "--home",
13592
+ config.agentContextHome,
13593
+ "--manifest",
13594
+ config.manifestPath,
13595
+ "--host",
13596
+ config.host,
13597
+ "--port",
13598
+ String(config.port),
13599
+ "doctor"
13600
+ ],
13601
+ { allowFailure: true, maxOutputBytes: 1024 * 1024 }
13602
+ );
13603
+ const checks = parseDoctorChecksFromOutput([result.stdout, result.stderr].filter(Boolean).join("\n"));
13604
+ return checks.length > 0 ? checks : collectDoctorChecks(config, {});
13605
+ }
13606
+ function parseDoctorChecksFromOutput(output2) {
13607
+ const ansiEscape = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
13608
+ 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) => ({
13609
+ detail: match[3] ?? "",
13610
+ name: match[2] ?? "",
13611
+ status: doctorStatus(match[1] ?? "")
13612
+ }));
13613
+ }
13614
+ async function gitCount(git, worktree, range) {
13615
+ const result = await runCommand(git, ["-C", worktree, "rev-list", "--count", range], { allowFailure: true });
13616
+ if (result.exitCode !== 0) {
13617
+ return void 0;
13618
+ }
13619
+ return Number.parseInt(result.stdout.trim(), 10) || 0;
13620
+ }
13621
+ function doctorStatus(value) {
13622
+ if (value === "OK") {
13623
+ return "ok";
13624
+ }
13625
+ if (value === "FAIL") {
13626
+ return "fail";
13627
+ }
13628
+ return "warn";
13629
+ }
13630
+ async function runCaptured(action) {
13631
+ const lines = [];
13632
+ const originalLog = console.log;
13633
+ const originalWarn = console.warn;
13634
+ const originalError = console.error;
13635
+ console.log = (...args) => lines.push(args.map(String).join(" "));
13636
+ console.warn = (...args) => lines.push(args.map(String).join(" "));
13637
+ console.error = (...args) => lines.push(args.map(String).join(" "));
13638
+ try {
13639
+ await action();
13640
+ } finally {
13641
+ console.log = originalLog;
13642
+ console.warn = originalWarn;
13643
+ console.error = originalError;
13644
+ }
13645
+ return { output: lines.join("\n") };
13646
+ }
13647
+ function memoryUriFor2(config, metadata) {
13648
+ const project = uriSegment(metadata.project);
13649
+ const filename = metadata.status === "active" && metadata.kind !== "smoke" ? `${uriSegment(metadata.topic)}.md` : `threadnote-${safeTimestamp()}-${sha256(JSON.stringify(metadata)).slice(0, 12)}.md`;
13650
+ return `${memoryDirectoryUri2(config, metadata.kind, metadata.status, project)}/${filename}`;
13651
+ }
13652
+ function sharedMemoryUriFor(config, team, metadata) {
13653
+ if (metadata.kind !== "durable") {
13654
+ throw new Error("Only durable memories can be moved into shared team memory.");
13655
+ }
13656
+ return `viking://user/${uriSegment(config.user)}/memories/shared/${uriSegment(team)}/durable/projects/${uriSegment(metadata.project)}/${uriSegment(metadata.topic)}.md`;
13657
+ }
13658
+ function memoryDirectoryUri2(config, kind, status, projectSegment) {
13659
+ const base = `viking://user/${uriSegment(config.user)}/memories`;
13660
+ switch (kind) {
13661
+ case "preference":
13662
+ return status === "active" ? `${base}/preferences` : `${base}/preferences/${uriSegment(status)}`;
13663
+ case "handoff":
13664
+ return `${base}/handoffs/${uriSegment(status)}/${projectSegment}`;
13665
+ case "incident":
13666
+ return `${base}/incidents/${uriSegment(status)}/${projectSegment}`;
13667
+ case "smoke":
13668
+ return `${base}/smoke/${uriSegment(status)}`;
13669
+ case "durable":
13670
+ return status === "active" ? `${base}/durable/projects/${projectSegment}` : `${base}/durable/${uriSegment(status)}/${projectSegment}`;
13671
+ }
13672
+ }
13673
+ function localMemoriesRoot(config) {
13674
+ return (0, import_node_path13.join)(config.agentContextHome, "data", "viking", config.account, "user", uriSegment(config.user), "memories");
13675
+ }
13676
+ function localPathForMemoryUri(config, uri) {
13677
+ const prefix = `viking://user/${uriSegment(config.user)}/memories`;
13678
+ if (uri !== prefix && !uri.startsWith(`${prefix}/`)) {
13679
+ return void 0;
13680
+ }
13681
+ const relativePath = uri === prefix ? "" : uri.slice(prefix.length + 1);
13682
+ const segments = relativePath.split("/").filter(Boolean);
13683
+ if (segments.some((segment) => segment === "." || segment === "..")) {
13684
+ return void 0;
13685
+ }
13686
+ return (0, import_node_path13.join)(localMemoriesRoot(config), ...segments);
13687
+ }
13688
+ function localPathToMemoryUri(config, path) {
13689
+ const relativePath = (0, import_node_path13.relative)(localMemoriesRoot(config), path);
13690
+ if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path13.sep).includes("..")) {
13691
+ throw new Error(`Path is outside the memories tree: ${path}`);
13692
+ }
13693
+ return `viking://user/${uriSegment(config.user)}/memories/${relativePath.split(import_node_path13.sep).join("/")}`;
13694
+ }
13695
+ async function ensurePersonalDirectoryChain2(config, ov, directoryUri) {
13696
+ const prefix = "viking://";
13697
+ const parts = directoryUri.startsWith(prefix) ? directoryUri.slice(prefix.length).split("/").filter(Boolean) : [];
13698
+ const startIndex = parts[0] === "user" && parts.length > 2 ? 3 : 1;
13699
+ for (let index = startIndex; index <= parts.length; index += 1) {
13700
+ const uri = `${prefix}${parts.slice(0, index).join("/")}`;
13701
+ const statResult = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
13702
+ if (statResult.exitCode !== 0) {
13703
+ await runCommand(
13704
+ ov,
13705
+ withIdentity(config, ["mkdir", uri, "--description", "Threadnote lifecycle-aware local memories."])
13706
+ );
13707
+ }
13708
+ }
13709
+ }
13710
+ function publicConfig(config) {
13711
+ return {
13712
+ account: config.account,
13713
+ agentContextHome: config.agentContextHome,
13714
+ agentId: config.agentId,
13715
+ host: config.host,
13716
+ manifestPath: config.manifestPath,
13717
+ port: config.port,
13718
+ user: config.user
13719
+ };
13720
+ }
13721
+ function parseUiPort(value) {
13722
+ if (!value) {
13723
+ return 0;
13724
+ }
13725
+ const parsed = Number.parseInt(value, 10);
13726
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
13727
+ throw new Error(`Invalid --ui-port "${value}".`);
13728
+ }
13729
+ return parsed;
13730
+ }
13731
+ function isAuthorized(context, request) {
13732
+ const auth = request.headers.authorization;
13733
+ return auth === `Bearer ${context.token}` || request.headers["x-threadnote-token"] === context.token;
13734
+ }
13735
+ function isSystemMemoryName(name) {
13736
+ return name === ".abstract.md" || name === ".overview.md" || name === ".git" || name === ".gitignore";
13737
+ }
13738
+ async function listen(server, port) {
13739
+ await new Promise((resolve2, reject) => {
13740
+ server.once("error", reject);
13741
+ server.listen(port, "127.0.0.1", () => {
13742
+ server.off("error", reject);
13743
+ resolve2();
13744
+ });
13745
+ });
13746
+ }
13747
+ async function readJsonBody(request) {
13748
+ const chunks = [];
13749
+ for await (const chunk of request) {
13750
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
13751
+ }
13752
+ if (chunks.length === 0) {
13753
+ return {};
13754
+ }
13755
+ const raw = Buffer.concat(chunks).toString("utf8");
13756
+ const parsed = JSON.parse(raw);
13757
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13758
+ throw new Error("Expected a JSON object body.");
13759
+ }
13760
+ return parsed;
13761
+ }
13762
+ function writeJson(response, statusCode, body) {
13763
+ response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
13764
+ response.end(`${JSON.stringify(body)}
13765
+ `);
13766
+ }
13767
+ function requiredQuery(url, name) {
13768
+ const value = url.searchParams.get(name);
13769
+ if (!value) {
13770
+ throw new Error(`Missing query parameter: ${name}`);
13771
+ }
13772
+ return value;
13773
+ }
13774
+ function requireString(value, name) {
13775
+ if (typeof value !== "string" || value.trim().length === 0) {
13776
+ throw new Error(`Provide ${name}.`);
13777
+ }
13778
+ return value;
13779
+ }
13780
+ function optionalString(value) {
13781
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
13782
+ }
13783
+ function requireStringArray(value, name) {
13784
+ if (!Array.isArray(value) || value.length === 0 || !value.every((item) => typeof item === "string")) {
13785
+ throw new Error(`Provide ${name} as a non-empty string array.`);
13786
+ }
13787
+ return value;
13788
+ }
13789
+ function requireConfirm(body) {
13790
+ if (body.confirm !== true) {
13791
+ throw new Error("Set confirm=true for this action.");
13792
+ }
13793
+ }
13794
+ function targetFromBody(body) {
13795
+ return {
13796
+ kind: memoryKind(body.kind),
13797
+ project: optionalString(body.project),
13798
+ sourceAgentClient: optionalString(body.sourceAgentClient),
13799
+ status: memoryStatus(body.status),
13800
+ team: optionalString(body.team),
13801
+ topic: optionalString(body.topic)
13802
+ };
13803
+ }
13804
+ function memoryKind(value) {
13805
+ return value === "durable" || value === "handoff" || value === "incident" || value === "preference" || value === "smoke" ? value : void 0;
13806
+ }
13807
+ function memoryStatus(value) {
13808
+ return value === "active" || value === "archived" || value === "superseded" ? value : void 0;
13809
+ }
13810
+ function isRawMemoryDocument(text) {
13811
+ return text.startsWith("MEMORY\n") || text.startsWith("HANDOFF\n");
13812
+ }
13813
+ function agentClient(value) {
13814
+ if (value === "codex" || value === "claude" || value === "cursor" || value === "copilot") {
13815
+ return value;
13816
+ }
13817
+ throw new Error(`Unsupported consolidation agent: ${value}`);
13818
+ }
13819
+ function cleanupMode(value) {
13820
+ if (value === "forget" || value === "keep") {
13821
+ return value;
13822
+ }
13823
+ return "archive";
13824
+ }
13825
+
13826
+ // src/threadnote.ts
13827
+ async function main() {
13828
+ const program2 = new Command();
13829
+ 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);
13830
+ 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) => {
13831
+ await runManage(getRuntimeConfig(program2), options);
13832
+ });
13833
+ 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) => {
13834
+ const config = getRuntimeConfig(program2);
13835
+ await runDoctor(config, options);
13836
+ await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
13837
+ });
13838
+ program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--force", "Reinstall OpenViking at the pinned version even if a working install is already present").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).option(
13839
+ "--with-hooks",
13840
+ "Also install agent-side hooks (Claude PreCompact + SessionStart) for deterministic handoff snapshots and context preload"
13841
+ ).action(async (options) => {
13842
+ const config = getRuntimeConfig(program2);
12618
13843
  await runInstall(config, options);
12619
13844
  if (options.withHooks === true) {
12620
13845
  const dryRun = options.dryRun === true;
@@ -12754,7 +13979,13 @@ async function main() {
12754
13979
  share.command("list").description("List configured shared teams").option("--dry-run", "Print without side effects (no-op for list)").action(async (options) => {
12755
13980
  await runShareList(getRuntimeConfig(program2), options);
12756
13981
  });
12757
- 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) => {
13982
+ 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) => {
13983
+ await runShareRename(getRuntimeConfig(program2), options);
13984
+ });
13985
+ 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) => {
13986
+ await runShareSetUrl(getRuntimeConfig(program2), remoteUrl, options);
13987
+ });
13988
+ 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) => {
12758
13989
  await runShareRemove(getRuntimeConfig(program2), options);
12759
13990
  });
12760
13991
  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) => {