threadnote 0.7.7 → 0.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/mcp_server.cjs +233 -196
- package/dist/threadnote.cjs +1673 -252
- 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/mcp_server.cjs
CHANGED
|
@@ -28477,25 +28477,25 @@ var Protocol = class {
|
|
|
28477
28477
|
});
|
|
28478
28478
|
}
|
|
28479
28479
|
_resetTimeout(messageId) {
|
|
28480
|
-
const
|
|
28481
|
-
if (!
|
|
28480
|
+
const info2 = this._timeoutInfo.get(messageId);
|
|
28481
|
+
if (!info2)
|
|
28482
28482
|
return false;
|
|
28483
|
-
const totalElapsed = Date.now() -
|
|
28484
|
-
if (
|
|
28483
|
+
const totalElapsed = Date.now() - info2.startTime;
|
|
28484
|
+
if (info2.maxTotalTimeout && totalElapsed >= info2.maxTotalTimeout) {
|
|
28485
28485
|
this._timeoutInfo.delete(messageId);
|
|
28486
28486
|
throw McpError.fromError(ErrorCode.RequestTimeout, "Maximum total timeout exceeded", {
|
|
28487
|
-
maxTotalTimeout:
|
|
28487
|
+
maxTotalTimeout: info2.maxTotalTimeout,
|
|
28488
28488
|
totalElapsed
|
|
28489
28489
|
});
|
|
28490
28490
|
}
|
|
28491
|
-
clearTimeout(
|
|
28492
|
-
|
|
28491
|
+
clearTimeout(info2.timeoutId);
|
|
28492
|
+
info2.timeoutId = setTimeout(info2.onTimeout, info2.timeout);
|
|
28493
28493
|
return true;
|
|
28494
28494
|
}
|
|
28495
28495
|
_cleanupTimeout(messageId) {
|
|
28496
|
-
const
|
|
28497
|
-
if (
|
|
28498
|
-
clearTimeout(
|
|
28496
|
+
const info2 = this._timeoutInfo.get(messageId);
|
|
28497
|
+
if (info2) {
|
|
28498
|
+
clearTimeout(info2.timeoutId);
|
|
28499
28499
|
this._timeoutInfo.delete(messageId);
|
|
28500
28500
|
}
|
|
28501
28501
|
}
|
|
@@ -28540,8 +28540,8 @@ var Protocol = class {
|
|
|
28540
28540
|
this._progressHandlers.clear();
|
|
28541
28541
|
this._taskProgressTokens.clear();
|
|
28542
28542
|
this._pendingDebouncedNotifications.clear();
|
|
28543
|
-
for (const
|
|
28544
|
-
clearTimeout(
|
|
28543
|
+
for (const info2 of this._timeoutInfo.values()) {
|
|
28544
|
+
clearTimeout(info2.timeoutId);
|
|
28545
28545
|
}
|
|
28546
28546
|
this._timeoutInfo.clear();
|
|
28547
28547
|
for (const controller of this._requestHandlerAbortControllers.values()) {
|
|
@@ -30036,8 +30036,8 @@ function validateToolName(name) {
|
|
|
30036
30036
|
function issueToolNameWarning(name, warnings) {
|
|
30037
30037
|
if (warnings.length > 0) {
|
|
30038
30038
|
console.warn(`Tool name validation warning for "${name}":`);
|
|
30039
|
-
for (const
|
|
30040
|
-
console.warn(` - ${
|
|
30039
|
+
for (const warning2 of warnings) {
|
|
30040
|
+
console.warn(` - ${warning2}`);
|
|
30041
30041
|
}
|
|
30042
30042
|
console.warn("Tool registration will proceed, but this may cause compatibility issues.");
|
|
30043
30043
|
console.warn("Consider updating the tool name to conform to the MCP tool naming standard.");
|
|
@@ -33591,6 +33591,39 @@ var import_node_crypto = require("node:crypto");
|
|
|
33591
33591
|
var import_promises = require("node:fs/promises");
|
|
33592
33592
|
var import_node_os = require("node:os");
|
|
33593
33593
|
var import_node_path = require("node:path");
|
|
33594
|
+
|
|
33595
|
+
// src/cli_ui.ts
|
|
33596
|
+
var import_node_process2 = require("node:process");
|
|
33597
|
+
var ANSI = {
|
|
33598
|
+
blue: "\x1B[34m",
|
|
33599
|
+
bold: "\x1B[1m",
|
|
33600
|
+
cyan: "\x1B[36m",
|
|
33601
|
+
dim: "\x1B[2m",
|
|
33602
|
+
green: "\x1B[32m",
|
|
33603
|
+
red: "\x1B[31m",
|
|
33604
|
+
reset: "\x1B[0m",
|
|
33605
|
+
yellow: "\x1B[33m"
|
|
33606
|
+
};
|
|
33607
|
+
function color(name, text) {
|
|
33608
|
+
if (!shouldUseColor()) {
|
|
33609
|
+
return text;
|
|
33610
|
+
}
|
|
33611
|
+
return `${ANSI[name]}${text}${ANSI.reset}`;
|
|
33612
|
+
}
|
|
33613
|
+
function command(text) {
|
|
33614
|
+
return color("cyan", text);
|
|
33615
|
+
}
|
|
33616
|
+
function info(text) {
|
|
33617
|
+
return color("cyan", text);
|
|
33618
|
+
}
|
|
33619
|
+
function warning(text) {
|
|
33620
|
+
return color("yellow", text);
|
|
33621
|
+
}
|
|
33622
|
+
function shouldUseColor() {
|
|
33623
|
+
return import_node_process2.stdout.isTTY === true && process.env.NO_COLOR === void 0 && process.env.CI === void 0 && process.env.TERM !== "dumb";
|
|
33624
|
+
}
|
|
33625
|
+
|
|
33626
|
+
// src/utils.ts
|
|
33594
33627
|
function isJsonObject(value) {
|
|
33595
33628
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33596
33629
|
}
|
|
@@ -33609,11 +33642,11 @@ function redactText(content) {
|
|
|
33609
33642
|
).replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]").replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, "gh_[REDACTED]");
|
|
33610
33643
|
}
|
|
33611
33644
|
async function requiredOpenVikingCli() {
|
|
33612
|
-
const
|
|
33613
|
-
if (!
|
|
33645
|
+
const command2 = await findExecutable(["ov", "openviking"]);
|
|
33646
|
+
if (!command2) {
|
|
33614
33647
|
throw new Error("Neither ov nor openviking was found in PATH. Run threadnote install first.");
|
|
33615
33648
|
}
|
|
33616
|
-
return
|
|
33649
|
+
return command2;
|
|
33617
33650
|
}
|
|
33618
33651
|
async function openVikingCliForMode(dryRun) {
|
|
33619
33652
|
if (dryRun) {
|
|
@@ -33621,16 +33654,16 @@ async function openVikingCliForMode(dryRun) {
|
|
|
33621
33654
|
}
|
|
33622
33655
|
return requiredOpenVikingCli();
|
|
33623
33656
|
}
|
|
33624
|
-
async function requiredExecutable(
|
|
33625
|
-
const executable = await findExecutable([
|
|
33657
|
+
async function requiredExecutable(command2) {
|
|
33658
|
+
const executable = await findExecutable([command2]);
|
|
33626
33659
|
if (!executable) {
|
|
33627
|
-
throw new Error(`${
|
|
33660
|
+
throw new Error(`${command2} was not found in PATH.`);
|
|
33628
33661
|
}
|
|
33629
33662
|
return executable;
|
|
33630
33663
|
}
|
|
33631
33664
|
async function findExecutable(commands) {
|
|
33632
|
-
for (const
|
|
33633
|
-
const result = await runCommand("which", [
|
|
33665
|
+
for (const command2 of commands) {
|
|
33666
|
+
const result = await runCommand("which", [command2], { allowFailure: true });
|
|
33634
33667
|
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
33635
33668
|
return result.stdout.trim();
|
|
33636
33669
|
}
|
|
@@ -33639,7 +33672,8 @@ async function findExecutable(commands) {
|
|
|
33639
33672
|
}
|
|
33640
33673
|
async function maybeRun(dryRun, executable, args, options = {}) {
|
|
33641
33674
|
const cwdSuffix = options.cwd ? ` (cwd: ${options.cwd})` : "";
|
|
33642
|
-
|
|
33675
|
+
const label = dryRun ? warning("Would run") : info("Running");
|
|
33676
|
+
console.log(`${label}: ${command(formatShellCommand(executable, args))}${cwdSuffix}`);
|
|
33643
33677
|
if (dryRun) {
|
|
33644
33678
|
return void 0;
|
|
33645
33679
|
}
|
|
@@ -33657,33 +33691,111 @@ async function runCommand(executable, args, options = {}) {
|
|
|
33657
33691
|
const child = (0, import_node_child_process.spawn)(executable, args, { cwd: options.cwd });
|
|
33658
33692
|
const stdoutChunks = [];
|
|
33659
33693
|
const stderrChunks = [];
|
|
33694
|
+
const maxOutputBytes = options.maxOutputBytes ?? defaultCommandMaxOutputBytes();
|
|
33695
|
+
let stdoutBytes = 0;
|
|
33696
|
+
let stderrBytes = 0;
|
|
33697
|
+
let finished = false;
|
|
33698
|
+
let failureResult;
|
|
33699
|
+
let killTimer;
|
|
33700
|
+
let sentTerminationSignal = false;
|
|
33701
|
+
const timeoutMs = options.timeoutMs ?? defaultCommandTimeoutMs();
|
|
33702
|
+
const finish = (result) => {
|
|
33703
|
+
if (finished) {
|
|
33704
|
+
return;
|
|
33705
|
+
}
|
|
33706
|
+
finished = true;
|
|
33707
|
+
if (killTimer) {
|
|
33708
|
+
clearTimeout(killTimer);
|
|
33709
|
+
}
|
|
33710
|
+
if (result.exitCode !== 0 && options.allowFailure !== true) {
|
|
33711
|
+
rejectPromise(new Error(`${formatShellCommand(executable, args)} failed: ${result.stderr || result.stdout}`));
|
|
33712
|
+
return;
|
|
33713
|
+
}
|
|
33714
|
+
resolvePromise(result);
|
|
33715
|
+
};
|
|
33716
|
+
const failAndKill = (message) => {
|
|
33717
|
+
if (failureResult) {
|
|
33718
|
+
return;
|
|
33719
|
+
}
|
|
33720
|
+
failureResult = { exitCode: 124, stderr: message, stdout: stdoutChunks.join("") };
|
|
33721
|
+
if (!sentTerminationSignal) {
|
|
33722
|
+
sentTerminationSignal = true;
|
|
33723
|
+
child.kill("SIGTERM");
|
|
33724
|
+
}
|
|
33725
|
+
setTimeout(() => {
|
|
33726
|
+
if (!finished) {
|
|
33727
|
+
child.kill("SIGKILL");
|
|
33728
|
+
}
|
|
33729
|
+
}, 1e3).unref?.();
|
|
33730
|
+
};
|
|
33731
|
+
if (timeoutMs > 0) {
|
|
33732
|
+
killTimer = setTimeout(() => {
|
|
33733
|
+
failAndKill(`${formatShellCommand(executable, args)} timed out after ${timeoutMs}ms`);
|
|
33734
|
+
}, timeoutMs);
|
|
33735
|
+
killTimer.unref?.();
|
|
33736
|
+
}
|
|
33660
33737
|
child.stdout.on("data", (chunk) => {
|
|
33661
|
-
|
|
33738
|
+
if (failureResult) {
|
|
33739
|
+
return;
|
|
33740
|
+
}
|
|
33741
|
+
const text = String(chunk);
|
|
33742
|
+
stdoutBytes += Buffer.byteLength(text);
|
|
33743
|
+
if (stdoutBytes + stderrBytes > maxOutputBytes) {
|
|
33744
|
+
failAndKill(`${formatShellCommand(executable, args)} exceeded output limit of ${maxOutputBytes} bytes`);
|
|
33745
|
+
return;
|
|
33746
|
+
}
|
|
33747
|
+
stdoutChunks.push(text);
|
|
33662
33748
|
});
|
|
33663
33749
|
child.stderr.on("data", (chunk) => {
|
|
33664
|
-
|
|
33750
|
+
if (failureResult) {
|
|
33751
|
+
return;
|
|
33752
|
+
}
|
|
33753
|
+
const text = String(chunk);
|
|
33754
|
+
stderrBytes += Buffer.byteLength(text);
|
|
33755
|
+
if (stdoutBytes + stderrBytes > maxOutputBytes) {
|
|
33756
|
+
failAndKill(`${formatShellCommand(executable, args)} exceeded output limit of ${maxOutputBytes} bytes`);
|
|
33757
|
+
return;
|
|
33758
|
+
}
|
|
33759
|
+
stderrChunks.push(text);
|
|
33665
33760
|
});
|
|
33666
33761
|
child.on("error", (err) => {
|
|
33762
|
+
if (finished) {
|
|
33763
|
+
return;
|
|
33764
|
+
}
|
|
33765
|
+
if (killTimer) {
|
|
33766
|
+
clearTimeout(killTimer);
|
|
33767
|
+
}
|
|
33667
33768
|
if (options.allowFailure === true) {
|
|
33668
|
-
|
|
33769
|
+
finish({ exitCode: 127, stderr: errorMessage(err), stdout: "" });
|
|
33669
33770
|
} else {
|
|
33771
|
+
finished = true;
|
|
33670
33772
|
rejectPromise(err);
|
|
33671
33773
|
}
|
|
33672
33774
|
});
|
|
33673
33775
|
child.on("close", (code) => {
|
|
33674
|
-
const result = {
|
|
33776
|
+
const result = failureResult ?? {
|
|
33675
33777
|
exitCode: code ?? 1,
|
|
33676
33778
|
stderr: stderrChunks.join(""),
|
|
33677
33779
|
stdout: stdoutChunks.join("")
|
|
33678
33780
|
};
|
|
33679
|
-
|
|
33680
|
-
rejectPromise(new Error(`${formatShellCommand(executable, args)} failed: ${result.stderr || result.stdout}`));
|
|
33681
|
-
return;
|
|
33682
|
-
}
|
|
33683
|
-
resolvePromise(result);
|
|
33781
|
+
finish(result);
|
|
33684
33782
|
});
|
|
33685
33783
|
});
|
|
33686
33784
|
}
|
|
33785
|
+
function defaultCommandTimeoutMs() {
|
|
33786
|
+
return positiveIntegerFromEnv("THREADNOTE_COMMAND_TIMEOUT_MS") ?? 10 * 60 * 1e3;
|
|
33787
|
+
}
|
|
33788
|
+
function defaultCommandMaxOutputBytes() {
|
|
33789
|
+
return positiveIntegerFromEnv("THREADNOTE_COMMAND_MAX_OUTPUT_BYTES") ?? 5 * 1024 * 1024;
|
|
33790
|
+
}
|
|
33791
|
+
function positiveIntegerFromEnv(name) {
|
|
33792
|
+
const value = process.env[name];
|
|
33793
|
+
if (value === void 0) {
|
|
33794
|
+
return void 0;
|
|
33795
|
+
}
|
|
33796
|
+
const parsed = Number.parseInt(value, 10);
|
|
33797
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
33798
|
+
}
|
|
33687
33799
|
async function gitValue(args, cwd = getInvocationCwd()) {
|
|
33688
33800
|
const result = await runCommand("git", args, { allowFailure: true, cwd });
|
|
33689
33801
|
if (result.exitCode !== 0) {
|
|
@@ -34445,9 +34557,9 @@ async function syncSharedReposBeforeAgentRead(config2) {
|
|
|
34445
34557
|
const remainingBehind = new Set(state.behindTeams);
|
|
34446
34558
|
for (const team of syncTeams) {
|
|
34447
34559
|
try {
|
|
34448
|
-
const
|
|
34449
|
-
if (
|
|
34450
|
-
warnings.push(
|
|
34560
|
+
const warning2 = await runShareSyncQuiet(config2, state, { team });
|
|
34561
|
+
if (warning2) {
|
|
34562
|
+
warnings.push(warning2);
|
|
34451
34563
|
} else {
|
|
34452
34564
|
remainingBehind.delete(team);
|
|
34453
34565
|
syncedTeams.push(team);
|
|
@@ -34529,8 +34641,8 @@ async function refreshShareUpdateState(config2, options) {
|
|
|
34529
34641
|
state,
|
|
34530
34642
|
async () => refreshShareUpdateStateLocked(config2, state, options)
|
|
34531
34643
|
);
|
|
34532
|
-
for (const
|
|
34533
|
-
console.error(
|
|
34644
|
+
for (const warning2 of warnings) {
|
|
34645
|
+
console.error(warning2);
|
|
34534
34646
|
}
|
|
34535
34647
|
}
|
|
34536
34648
|
async function refreshShareUpdateStateLocked(config2, state, options) {
|
|
@@ -34637,6 +34749,58 @@ async function runShareSyncQuiet(config2, state, options) {
|
|
|
34637
34749
|
}
|
|
34638
34750
|
return void 0;
|
|
34639
34751
|
}
|
|
34752
|
+
async function publishShareGitChange(worktree, relativePath, commitMessage, options = {}) {
|
|
34753
|
+
const dryRun = options.dryRun === true;
|
|
34754
|
+
const push = options.push !== false;
|
|
34755
|
+
const verb = options.verb ?? "add";
|
|
34756
|
+
const git = await requiredExecutable("git");
|
|
34757
|
+
const messages = [];
|
|
34758
|
+
const stageArgs = verb === "rm" ? ["-C", worktree, "rm", relativePath] : ["-C", worktree, "add", "--", relativePath];
|
|
34759
|
+
const stageResult = await runGitCommand(dryRun, git, stageArgs, `git ${verb} failed`);
|
|
34760
|
+
if (stageResult) {
|
|
34761
|
+
messages.push(`git ${verb}: ${stageResult.stdout.trim() || "ok"}`);
|
|
34762
|
+
}
|
|
34763
|
+
if (dryRun) {
|
|
34764
|
+
console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "commit", "-m", commitMessage])}`);
|
|
34765
|
+
} else {
|
|
34766
|
+
const commitResult = await runCommand(git, ["-C", worktree, "commit", "-m", commitMessage], { allowFailure: true });
|
|
34767
|
+
if (commitResult.exitCode !== 0) {
|
|
34768
|
+
const detail = commitResult.stdout.trim() || commitResult.stderr.trim();
|
|
34769
|
+
if (/nothing to commit|no changes added/i.test(detail)) {
|
|
34770
|
+
messages.push("git commit: nothing to commit (file already in tree)");
|
|
34771
|
+
} else {
|
|
34772
|
+
throw new Error(`git commit failed: ${detail || "unknown error"}`);
|
|
34773
|
+
}
|
|
34774
|
+
} else {
|
|
34775
|
+
messages.push(`git commit: ${commitResult.stdout.trim().split("\n").slice(0, 2).join(" ")}`);
|
|
34776
|
+
}
|
|
34777
|
+
}
|
|
34778
|
+
if (!push) {
|
|
34779
|
+
messages.push("git push skipped (push=false)");
|
|
34780
|
+
return messages;
|
|
34781
|
+
}
|
|
34782
|
+
const pushResult = await runGitCommand(
|
|
34783
|
+
dryRun,
|
|
34784
|
+
git,
|
|
34785
|
+
["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME],
|
|
34786
|
+
"git push failed"
|
|
34787
|
+
);
|
|
34788
|
+
if (pushResult) {
|
|
34789
|
+
messages.push(`git push: ${pushResult.stdout.trim() || pushResult.stderr.trim() || "ok"}`);
|
|
34790
|
+
}
|
|
34791
|
+
return messages;
|
|
34792
|
+
}
|
|
34793
|
+
async function runGitCommand(dryRun, git, args, failureLabel) {
|
|
34794
|
+
if (dryRun) {
|
|
34795
|
+
console.log(`Would run: ${formatShellCommand(git, args)}`);
|
|
34796
|
+
return void 0;
|
|
34797
|
+
}
|
|
34798
|
+
const result = await runCommand(git, args, { allowFailure: true });
|
|
34799
|
+
if (result.exitCode !== 0) {
|
|
34800
|
+
throw new Error(`${failureLabel}: ${result.stderr.trim() || result.stdout.trim() || "unknown error"}`);
|
|
34801
|
+
}
|
|
34802
|
+
return result;
|
|
34803
|
+
}
|
|
34640
34804
|
function normalizeTeamName(input) {
|
|
34641
34805
|
const candidate = (input ?? "default").trim();
|
|
34642
34806
|
if (!candidate) {
|
|
@@ -34917,61 +35081,20 @@ async function waitForOvQueue(ov, config2, options = {}) {
|
|
|
34917
35081
|
console.error(result.stderr.trim());
|
|
34918
35082
|
}
|
|
34919
35083
|
}
|
|
34920
|
-
function isTransientOvFailure(stderr,
|
|
35084
|
+
function isTransientOvFailure(stderr, stdout2) {
|
|
34921
35085
|
const output = `${stderr}
|
|
34922
|
-
${
|
|
35086
|
+
${stdout2}`.toLowerCase();
|
|
34923
35087
|
return output.includes("resource is busy") || output.includes("resource is being processed") || output.includes("network error") || output.includes("error sending request") || output.includes("http request failed") || output.includes("connection refused") || output.includes("connection reset") || output.includes("timed out");
|
|
34924
35088
|
}
|
|
34925
|
-
function isResourceBusyFailure(stderr,
|
|
35089
|
+
function isResourceBusyFailure(stderr, stdout2) {
|
|
34926
35090
|
const output = `${stderr}
|
|
34927
|
-
${
|
|
35091
|
+
${stdout2}`.toLowerCase();
|
|
34928
35092
|
return output.includes("resource is busy") || output.includes("resource is being processed");
|
|
34929
35093
|
}
|
|
34930
35094
|
async function ingestSingleFile(ov, config2, uri, filePath, initialMode, options = {}) {
|
|
34931
35095
|
const content = await (0, import_promises3.readFile)(filePath, "utf8");
|
|
34932
35096
|
await writeMemoryFile(config2, ov, uri, content, initialMode, false, options);
|
|
34933
35097
|
}
|
|
34934
|
-
async function removeWithRollback(config2, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
|
|
34935
|
-
try {
|
|
34936
|
-
await removeMemoryUri(config2, ov, sourceUri, dryRun);
|
|
34937
|
-
} catch (sourceErr) {
|
|
34938
|
-
if (dryRun) {
|
|
34939
|
-
throw sourceErr;
|
|
34940
|
-
}
|
|
34941
|
-
console.error(
|
|
34942
|
-
`Source removal failed during ${label}; rolling back ${rollbackUri} so the system is back to the pre-${label} state.`
|
|
34943
|
-
);
|
|
34944
|
-
try {
|
|
34945
|
-
await removeMemoryUri(config2, ov, rollbackUri, false);
|
|
34946
|
-
} catch (rollbackErr) {
|
|
34947
|
-
console.error(
|
|
34948
|
-
`Rollback of ${rollbackUri} also failed. Manual cleanup needed via: threadnote forget ${rollbackUri}
|
|
34949
|
-
Rollback error: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`
|
|
34950
|
-
);
|
|
34951
|
-
}
|
|
34952
|
-
await bestEffortRemoveWorktreeFile(rollbackUri, worktree, label);
|
|
34953
|
-
throw sourceErr;
|
|
34954
|
-
}
|
|
34955
|
-
}
|
|
34956
|
-
async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
|
|
34957
|
-
if (label !== "publish") {
|
|
34958
|
-
return;
|
|
34959
|
-
}
|
|
34960
|
-
const prefix = "viking://";
|
|
34961
|
-
if (!rollbackUri.startsWith(prefix)) {
|
|
34962
|
-
return;
|
|
34963
|
-
}
|
|
34964
|
-
const parts = rollbackUri.slice(prefix.length).split("/");
|
|
34965
|
-
const sharedIndex = parts.indexOf("shared");
|
|
34966
|
-
if (sharedIndex === -1 || sharedIndex + 2 >= parts.length) {
|
|
34967
|
-
return;
|
|
34968
|
-
}
|
|
34969
|
-
const relative2 = parts.slice(sharedIndex + 2).join("/");
|
|
34970
|
-
if (!relative2) {
|
|
34971
|
-
return;
|
|
34972
|
-
}
|
|
34973
|
-
await (0, import_promises3.rm)((0, import_node_path3.join)(worktree, relative2), { force: true });
|
|
34974
|
-
}
|
|
34975
35098
|
async function removeMemoryUri(config2, ov, uri, dryRun, options = {}) {
|
|
34976
35099
|
const args = withIdentity(config2, ["rm", uri]);
|
|
34977
35100
|
if (dryRun) {
|
|
@@ -35133,7 +35256,7 @@ async function main() {
|
|
|
35133
35256
|
"During feature work, update durable feature knowledge when valuable implementation details, decisions, interfaces, or gotchas change.",
|
|
35134
35257
|
"When updating the same active issue, pass project/topic or replaceUri to remember_context so duplicate durable memories or handoffs do not accumulate.",
|
|
35135
35258
|
"Use compact_context with dryRun=true for scoped memory hygiene when recall surfaces overlapping active memories.",
|
|
35136
|
-
"To share a durable memory with teammates, call `share_publish` with its viking:// URI. share_publish
|
|
35259
|
+
"To share a durable memory with teammates, call `share_publish` with its viking:// URI. share_publish scrubs for secrets, writes and pushes the shared copy first, then removes the personal copy after the push succeeds. Do not publish handoffs, preferences, or anything carrying machine-local paths or in-flight task context.",
|
|
35137
35260
|
"Do not store secrets, customer data, raw production logs, or credentials."
|
|
35138
35261
|
].join("\n")
|
|
35139
35262
|
}
|
|
@@ -35317,7 +35440,7 @@ function registerTools(server, config2) {
|
|
|
35317
35440
|
"share_publish",
|
|
35318
35441
|
{
|
|
35319
35442
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
35320
|
-
description: "Publish a personal memory into a team's shared memories git repo. Reads the memory, strips local-only provenance frontmatter (supersedes:, archived_from:), refuses publish if it matches secret patterns,
|
|
35443
|
+
description: "Publish a personal memory into a team's shared memories git repo. Reads the memory, strips local-only provenance frontmatter (supersedes:, archived_from:), refuses publish if it matches secret patterns, writes and pushes the shared copy first, then removes the personal original. Default team is used unless team is provided. Pass preview=true to return the would-be-published bytes without writing or committing.",
|
|
35321
35444
|
inputSchema: {
|
|
35322
35445
|
message: external_exports.string().optional().describe('Commit message override; defaults to "share: publish <path>"'),
|
|
35323
35446
|
preview: external_exports.boolean().optional().describe(
|
|
@@ -35428,8 +35551,8 @@ ${exactMatches}`);
|
|
|
35428
35551
|
if (syncedTeams.length > 0) {
|
|
35429
35552
|
sections.push(`Auto-synced shared memories: ${syncedTeams.join(", ")}`);
|
|
35430
35553
|
}
|
|
35431
|
-
for (const
|
|
35432
|
-
sections.push(`Auto-sync warning: ${
|
|
35554
|
+
for (const warning2 of syncWarnings) {
|
|
35555
|
+
sections.push(`Auto-sync warning: ${warning2}`);
|
|
35433
35556
|
}
|
|
35434
35557
|
if (sections.length <= 1) {
|
|
35435
35558
|
return semanticResult;
|
|
@@ -35527,7 +35650,7 @@ function registerReadTool(server, config2, name, description) {
|
|
|
35527
35650
|
}
|
|
35528
35651
|
const syncMessages = [
|
|
35529
35652
|
syncedTeams.length > 0 ? `Auto-synced shared memories: ${syncedTeams.join(", ")}` : void 0,
|
|
35530
|
-
...syncWarnings.map((
|
|
35653
|
+
...syncWarnings.map((warning2) => `Auto-sync warning: ${warning2}`)
|
|
35531
35654
|
].filter((part) => part !== void 0);
|
|
35532
35655
|
return {
|
|
35533
35656
|
...result,
|
|
@@ -35715,27 +35838,8 @@ function registerCompactTool(server, config2) {
|
|
|
35715
35838
|
const ov = await requiredOpenVikingCli2();
|
|
35716
35839
|
const appliedMessages = [];
|
|
35717
35840
|
for (const action of plan.keepUpdates) {
|
|
35718
|
-
|
|
35719
|
-
ov,
|
|
35720
|
-
config2,
|
|
35721
|
-
action.uri,
|
|
35722
|
-
withIdentity2(config2, [
|
|
35723
|
-
"write",
|
|
35724
|
-
action.uri,
|
|
35725
|
-
"--content",
|
|
35726
|
-
action.content,
|
|
35727
|
-
"--mode",
|
|
35728
|
-
"replace",
|
|
35729
|
-
"--wait",
|
|
35730
|
-
"--timeout",
|
|
35731
|
-
"120"
|
|
35732
|
-
])
|
|
35733
|
-
);
|
|
35841
|
+
await writeMemoryFile(config2, ov, action.uri, action.content, "replace", false, { quiet: true });
|
|
35734
35842
|
appliedMessages.push(`Updated kept memory: ${action.uri}`);
|
|
35735
|
-
const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
35736
|
-
if (output) {
|
|
35737
|
-
appliedMessages.push(output);
|
|
35738
|
-
}
|
|
35739
35843
|
}
|
|
35740
35844
|
for (const action of plan.archives) {
|
|
35741
35845
|
const archiveResult = await archiveMemoryForCompact(config2, ov, action);
|
|
@@ -35908,22 +36012,7 @@ async function writeDurableMemory(config2, params) {
|
|
|
35908
36012
|
const finalMetadata = isInPlaceUpdate ? { ...params.metadata, supersedes: void 0 } : candidateMetadata;
|
|
35909
36013
|
const memory = isInPlaceUpdate ? formatMemoryDocument2("MEMORY", finalMetadata, params.bodyText) : candidateMemory;
|
|
35910
36014
|
const writeMode = await memoryWriteMode(ov, config2, memoryUri, finalMetadata);
|
|
35911
|
-
|
|
35912
|
-
ov,
|
|
35913
|
-
config2,
|
|
35914
|
-
memoryUri,
|
|
35915
|
-
withIdentity2(config2, [
|
|
35916
|
-
"write",
|
|
35917
|
-
memoryUri,
|
|
35918
|
-
"--content",
|
|
35919
|
-
memory,
|
|
35920
|
-
"--mode",
|
|
35921
|
-
writeMode,
|
|
35922
|
-
"--wait",
|
|
35923
|
-
"--timeout",
|
|
35924
|
-
"120"
|
|
35925
|
-
])
|
|
35926
|
-
);
|
|
36015
|
+
await writeMemoryFile(config2, ov, memoryUri, memory, writeMode, false, { quiet: true });
|
|
35927
36016
|
const messages = [`Stored memory: ${memoryUri}`];
|
|
35928
36017
|
if (params.replaceUri && !isInPlaceUpdate) {
|
|
35929
36018
|
const removedReplacedMemory = await removeVikingResourceWithRetry(ov, config2, params.replaceUri);
|
|
@@ -35933,8 +36022,7 @@ async function writeDurableMemory(config2, params) {
|
|
|
35933
36022
|
} else if (isInPlaceUpdate) {
|
|
35934
36023
|
messages.push(`Updated existing memory in place: ${memoryUri}`);
|
|
35935
36024
|
}
|
|
35936
|
-
|
|
35937
|
-
return { content: [{ type: "text", text: [...messages, text].filter(Boolean).join("\n") }] };
|
|
36025
|
+
return { content: [{ type: "text", text: messages.join("\n") }] };
|
|
35938
36026
|
} catch (err) {
|
|
35939
36027
|
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
35940
36028
|
}
|
|
@@ -35969,27 +36057,10 @@ async function writeSharedMemoryReplacement(config2, ov, params, targetUri) {
|
|
|
35969
36057
|
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
|
|
35970
36058
|
}
|
|
35971
36059
|
messages.push(
|
|
35972
|
-
...await
|
|
36060
|
+
...await publishShareGitChange(resolved.config.worktree, relativePath, `share: update ${relativePath}`)
|
|
35973
36061
|
);
|
|
35974
36062
|
return { content: [{ type: "text", text: messages.join("\n") }] };
|
|
35975
36063
|
}
|
|
35976
|
-
async function runOpenVikingWriteWithRetry(ov, config2, memoryUri, args) {
|
|
35977
|
-
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
35978
|
-
const result = await runCommand(ov, args, { allowFailure: true });
|
|
35979
|
-
if (result.exitCode === 0) {
|
|
35980
|
-
return result;
|
|
35981
|
-
}
|
|
35982
|
-
if (await vikingResourceExists2(ov, config2, memoryUri)) {
|
|
35983
|
-
await runCommand(ov, withIdentity2(config2, ["wait", "--timeout", "120"]), { allowFailure: true });
|
|
35984
|
-
return { exitCode: 0, stdout: "OpenViking accepted the memory and indexing wait completed.", stderr: "" };
|
|
35985
|
-
}
|
|
35986
|
-
if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
|
|
35987
|
-
throw new Error(`${ov} ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
|
|
35988
|
-
}
|
|
35989
|
-
await sleep(1e3 * (attempt + 1));
|
|
35990
|
-
}
|
|
35991
|
-
throw new Error(`${ov} ${args.join(" ")} failed.`);
|
|
35992
|
-
}
|
|
35993
36064
|
async function vikingResourceExists2(ov, config2, uri) {
|
|
35994
36065
|
const stat2 = await runCommand(ov, withIdentity2(config2, ["stat", uri]), { allowFailure: true });
|
|
35995
36066
|
return stat2.exitCode === 0;
|
|
@@ -36011,9 +36082,9 @@ async function removeVikingResourceWithRetry(ov, config2, uri) {
|
|
|
36011
36082
|
}
|
|
36012
36083
|
return false;
|
|
36013
36084
|
}
|
|
36014
|
-
function isResourceBusy(stderr,
|
|
36085
|
+
function isResourceBusy(stderr, stdout2) {
|
|
36015
36086
|
const output = `${stderr}
|
|
36016
|
-
${
|
|
36087
|
+
${stdout2}`.toLowerCase();
|
|
36017
36088
|
return output.includes("resource is busy") || output.includes("resource is being processed");
|
|
36018
36089
|
}
|
|
36019
36090
|
async function ensureMemoryDirectory(ov, config2, directoryUri) {
|
|
@@ -36217,17 +36288,27 @@ async function runSharePublishTool(config2, sourceUri, options) {
|
|
|
36217
36288
|
}
|
|
36218
36289
|
await ensureSharedDirectoryChain(config2, ov, targetUri, false);
|
|
36219
36290
|
await writeMemoryFile(config2, ov, targetUri, content, "create", false);
|
|
36291
|
+
const messages = [`Published ${sourceUri} -> ${targetUri}`];
|
|
36292
|
+
for (const redaction of scrub.redactions) {
|
|
36293
|
+
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before publish.`);
|
|
36294
|
+
}
|
|
36295
|
+
const relativePath = vikingUriToWorktreeRelative(config2, targetUri, resolved.name);
|
|
36296
|
+
const commitMessage = options.message ?? `share: publish ${relativePath}`;
|
|
36297
|
+
const gitMessages = await publishShareGitChange(resolved.config.worktree, relativePath, commitMessage, {
|
|
36298
|
+
push: options.push
|
|
36299
|
+
});
|
|
36220
36300
|
try {
|
|
36221
|
-
await
|
|
36301
|
+
await removeMemoryUri(config2, ov, sourceUri, false, { quiet: true });
|
|
36222
36302
|
} catch (sourceErr) {
|
|
36223
36303
|
return {
|
|
36224
36304
|
content: [
|
|
36225
36305
|
{
|
|
36226
36306
|
type: "text",
|
|
36227
36307
|
text: [
|
|
36228
|
-
|
|
36229
|
-
|
|
36230
|
-
`
|
|
36308
|
+
...messages,
|
|
36309
|
+
...gitMessages,
|
|
36310
|
+
`Could not remove the personal source after publish: ${sourceUri}.`,
|
|
36311
|
+
`Retry cleanup later with: threadnote forget ${sourceUri}`,
|
|
36231
36312
|
sourceErr instanceof Error ? sourceErr.message : String(sourceErr)
|
|
36232
36313
|
].join("\n")
|
|
36233
36314
|
}
|
|
@@ -36235,18 +36316,6 @@ async function runSharePublishTool(config2, sourceUri, options) {
|
|
|
36235
36316
|
isError: true
|
|
36236
36317
|
};
|
|
36237
36318
|
}
|
|
36238
|
-
const messages = [`Published ${sourceUri} -> ${targetUri}`];
|
|
36239
|
-
for (const redaction of scrub.redactions) {
|
|
36240
|
-
messages.push(`Redacted ${redaction.count}\xD7 ${redaction.name} before publish.`);
|
|
36241
|
-
}
|
|
36242
|
-
const relativePath = vikingUriToWorktreeRelative(config2, targetUri, resolved.name);
|
|
36243
|
-
const commitMessage = options.message ?? `share: publish ${relativePath}`;
|
|
36244
|
-
const gitMessages = await gitPublishWorkflow(
|
|
36245
|
-
resolved.config.worktree,
|
|
36246
|
-
relativePath,
|
|
36247
|
-
commitMessage,
|
|
36248
|
-
options.push !== false
|
|
36249
|
-
);
|
|
36250
36319
|
return {
|
|
36251
36320
|
content: [{ type: "text", text: [...messages, ...gitMessages].join("\n") }],
|
|
36252
36321
|
isError: false
|
|
@@ -36255,47 +36324,15 @@ async function runSharePublishTool(config2, sourceUri, options) {
|
|
|
36255
36324
|
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
36256
36325
|
}
|
|
36257
36326
|
}
|
|
36258
|
-
async function gitPublishWorkflow(worktree, relativePath, commitMessage, push) {
|
|
36259
|
-
const messages = [];
|
|
36260
|
-
const add = await runCommand("git", ["-C", worktree, "add", relativePath], { allowFailure: true });
|
|
36261
|
-
if (add.exitCode !== 0) {
|
|
36262
|
-
messages.push(`git add failed: ${add.stderr.trim() || add.stdout.trim()}`);
|
|
36263
|
-
return messages;
|
|
36264
|
-
}
|
|
36265
|
-
const commit = await runCommand("git", ["-C", worktree, "commit", "-m", commitMessage], { allowFailure: true });
|
|
36266
|
-
if (commit.exitCode !== 0) {
|
|
36267
|
-
const detail = commit.stdout.trim() || commit.stderr.trim();
|
|
36268
|
-
if (/nothing to commit|no changes added/i.test(detail)) {
|
|
36269
|
-
messages.push("git commit: nothing to commit (file already in tree)");
|
|
36270
|
-
} else {
|
|
36271
|
-
messages.push(`git commit failed: ${detail}`);
|
|
36272
|
-
return messages;
|
|
36273
|
-
}
|
|
36274
|
-
} else {
|
|
36275
|
-
messages.push(`git commit: ${commit.stdout.trim().split("\n").slice(0, 2).join(" ")}`);
|
|
36276
|
-
}
|
|
36277
|
-
if (!push) {
|
|
36278
|
-
messages.push("git push skipped (push=false)");
|
|
36279
|
-
return messages;
|
|
36280
|
-
}
|
|
36281
|
-
const pushResult = await runCommand("git", ["-C", worktree, "push", DEFAULT_GIT_REMOTE_NAME], { allowFailure: true });
|
|
36282
|
-
const pushDetail = pushResult.stdout.trim() || pushResult.stderr.trim();
|
|
36283
|
-
if (pushResult.exitCode !== 0) {
|
|
36284
|
-
messages.push(`git push failed: ${pushDetail}`);
|
|
36285
|
-
} else {
|
|
36286
|
-
messages.push(`git push: ${pushDetail || "ok"}`);
|
|
36287
|
-
}
|
|
36288
|
-
return messages;
|
|
36289
|
-
}
|
|
36290
36327
|
function withIdentity2(config2, args) {
|
|
36291
36328
|
return [...args, "--account", config2.account, "--user", config2.user, "--agent-id", config2.agentId];
|
|
36292
36329
|
}
|
|
36293
36330
|
async function requiredOpenVikingCli2() {
|
|
36294
|
-
const
|
|
36295
|
-
if (!
|
|
36331
|
+
const command2 = process.env.THREADNOTE_OV ?? await findExecutable(["ov", "openviking"]) ?? await firstExistingPath([(0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "ov"), (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".local", "bin", "openviking")]);
|
|
36332
|
+
if (!command2) {
|
|
36296
36333
|
throw new Error("Neither ov nor openviking was found. Run threadnote install first.");
|
|
36297
36334
|
}
|
|
36298
|
-
return
|
|
36335
|
+
return command2;
|
|
36299
36336
|
}
|
|
36300
36337
|
async function firstExistingPath(paths) {
|
|
36301
36338
|
for (const path of paths) {
|