threadnote 0.7.8 → 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.
- package/README.md +4 -1
- package/dist/mcp_server.cjs +159 -156
- package/dist/threadnote.cjs +1347 -165
- package/docs/agent-instructions.md +2 -2
- package/docs/index.html +56 -32
- package/docs/share.md +20 -6
- package/manager/app.css +858 -0
- package/manager/app.js +37361 -0
- package/manager/index.html +14 -0
- package/package.json +15 -6
package/dist/threadnote.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
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
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
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) {
|
|
@@ -9386,11 +9622,11 @@ function localMemoryPathForUri(config, uri) {
|
|
|
9386
9622
|
if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
|
|
9387
9623
|
return void 0;
|
|
9388
9624
|
}
|
|
9389
|
-
const
|
|
9390
|
-
if (
|
|
9625
|
+
const relative4 = uri.slice(prefix.length);
|
|
9626
|
+
if (relative4.includes("..") || relative4.startsWith("/")) {
|
|
9391
9627
|
return void 0;
|
|
9392
9628
|
}
|
|
9393
|
-
return (0, import_node_path6.join)(localUserMemoriesRoot(config), ...
|
|
9629
|
+
return (0, import_node_path6.join)(localUserMemoriesRoot(config), ...relative4.split("/"));
|
|
9394
9630
|
}
|
|
9395
9631
|
async function runList(config, uri, options) {
|
|
9396
9632
|
assertVikingUri(uri);
|
|
@@ -9624,62 +9860,20 @@ async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
|
|
|
9624
9860
|
}
|
|
9625
9861
|
await ensureSharedDirectoryChain(config, ov, targetUri, options.dryRun);
|
|
9626
9862
|
await writeMemoryFile(config, ov, targetUri, memory, "replace", options.dryRun);
|
|
9627
|
-
const
|
|
9628
|
-
|
|
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
|
|
9863
|
+
const gitMessages = await publishShareGitChange(team.config.worktree, relativePath, `share: update ${relativePath}`, {
|
|
9864
|
+
dryRun: options.dryRun
|
|
9634
9865
|
});
|
|
9866
|
+
for (const message of gitMessages) {
|
|
9867
|
+
console.log(message);
|
|
9868
|
+
}
|
|
9635
9869
|
for (const redaction of scrub.redactions) {
|
|
9636
9870
|
console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
|
|
9637
9871
|
}
|
|
9638
9872
|
console.log(`Updated shared memory: ${targetUri}`);
|
|
9639
9873
|
}
|
|
9640
9874
|
async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode) {
|
|
9641
|
-
const
|
|
9642
|
-
|
|
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
|
-
}
|
|
9875
|
+
const content = await (0, import_promises5.readFile)(memoryPath, "utf8");
|
|
9876
|
+
await writeMemoryFile(config, ov, memoryUri, content, writeMode, false);
|
|
9683
9877
|
}
|
|
9684
9878
|
async function removeVikingResourceWithRetry(ov, config, uri) {
|
|
9685
9879
|
const args = withIdentity(config, ["rm", uri]);
|
|
@@ -9706,8 +9900,8 @@ async function removeVikingResourceWithRetry(ov, config, uri) {
|
|
|
9706
9900
|
return false;
|
|
9707
9901
|
}
|
|
9708
9902
|
async function vikingResourceExists2(ov, config, uri) {
|
|
9709
|
-
const
|
|
9710
|
-
return
|
|
9903
|
+
const stat5 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
9904
|
+
return stat5.exitCode === 0;
|
|
9711
9905
|
}
|
|
9712
9906
|
async function ensureDurableMemoryDirectory(ov, config) {
|
|
9713
9907
|
await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
|
|
@@ -11514,6 +11708,19 @@ function isUpdateNotificationDisabled() {
|
|
|
11514
11708
|
|
|
11515
11709
|
// src/lifecycle.ts
|
|
11516
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 = {}) {
|
|
11517
11724
|
const checks = [];
|
|
11518
11725
|
checks.push({ name: "mode", status: "ok", detail: options.dryRun ? "dry run; no writes" : "read-only checks" });
|
|
11519
11726
|
checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
|
|
@@ -11533,16 +11740,7 @@ async function runDoctor(config, options) {
|
|
|
11533
11740
|
checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
|
|
11534
11741
|
checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
|
|
11535
11742
|
checks.push(await healthCheck(config));
|
|
11536
|
-
|
|
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
|
-
}
|
|
11743
|
+
return checks;
|
|
11546
11744
|
}
|
|
11547
11745
|
async function runInstall(config, options) {
|
|
11548
11746
|
const repairInvalidConfigs = options.repairInvalidConfigs === true;
|
|
@@ -12601,36 +12799,1014 @@ function printWhatsNew(whatsNew) {
|
|
|
12601
12799
|
}
|
|
12602
12800
|
}
|
|
12603
12801
|
|
|
12604
|
-
// src/
|
|
12605
|
-
|
|
12606
|
-
|
|
12607
|
-
|
|
12608
|
-
|
|
12609
|
-
|
|
12610
|
-
|
|
12611
|
-
|
|
12612
|
-
}
|
|
12613
|
-
|
|
12614
|
-
|
|
12615
|
-
|
|
12616
|
-
|
|
12617
|
-
|
|
12618
|
-
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
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);
|
|
12628
12832
|
});
|
|
12629
|
-
|
|
12630
|
-
|
|
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
|
+
});
|
|
12631
12839
|
});
|
|
12632
|
-
|
|
12633
|
-
|
|
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
|
+
|
|
13777
|
+
// src/threadnote.ts
|
|
13778
|
+
async function main() {
|
|
13779
|
+
const program2 = new Command();
|
|
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
|
+
});
|
|
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) => {
|
|
13785
|
+
const config = getRuntimeConfig(program2);
|
|
13786
|
+
await runDoctor(config, options);
|
|
13787
|
+
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
13788
|
+
});
|
|
13789
|
+
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(
|
|
13790
|
+
"--with-hooks",
|
|
13791
|
+
"Also install agent-side hooks (Claude PreCompact + SessionStart) for deterministic handoff snapshots and context preload"
|
|
13792
|
+
).action(async (options) => {
|
|
13793
|
+
const config = getRuntimeConfig(program2);
|
|
13794
|
+
await runInstall(config, options);
|
|
13795
|
+
if (options.withHooks === true) {
|
|
13796
|
+
const dryRun = options.dryRun === true;
|
|
13797
|
+
for (const agent of ["claude", "codex", "cursor", "copilot"]) {
|
|
13798
|
+
console.log(`
|
|
13799
|
+
--- ${agent} hooks ---`);
|
|
13800
|
+
await runHooksInstall(config, agent, { apply: !dryRun, dryRun });
|
|
13801
|
+
}
|
|
13802
|
+
}
|
|
13803
|
+
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
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
|
+
});
|
|
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) => {
|
|
13809
|
+
await runUpdate(getRuntimeConfig(program2), options);
|
|
12634
13810
|
});
|
|
12635
13811
|
program2.command("post-update", { hidden: true }).description("Run packaged post-update migration prompts").requiredOption("--from-version <version>", "Version before update").requiredOption("--to-version <version>", "Version after update").option("--dry-run", "Print post-update actions without running them").option("--yes", "Accept applicable post-update migrations without prompting").action(async (options) => {
|
|
12636
13812
|
await runPostUpdate(getRuntimeConfig(program2), options);
|
|
@@ -12754,7 +13930,13 @@ async function main() {
|
|
|
12754
13930
|
share.command("list").description("List configured shared teams").option("--dry-run", "Print without side effects (no-op for list)").action(async (options) => {
|
|
12755
13931
|
await runShareList(getRuntimeConfig(program2), options);
|
|
12756
13932
|
});
|
|
12757
|
-
share.command("
|
|
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) => {
|
|
12758
13940
|
await runShareRemove(getRuntimeConfig(program2), options);
|
|
12759
13941
|
});
|
|
12760
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) => {
|