threadnote 0.3.1 → 0.3.2
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 +25 -0
- package/dist/mcp_server.cjs +175 -4
- package/dist/threadnote.cjs +443 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,27 @@ threadnote start
|
|
|
68
68
|
threadnote doctor --dry-run
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
### Update
|
|
72
|
+
|
|
73
|
+
Threadnote occasionally checks npm for a newer published version when you run human-facing commands such as `doctor`,
|
|
74
|
+
`start`, `install`, or `repair`. The check is cached under `THREADNOTE_HOME` and never runs in CI or when
|
|
75
|
+
`THREADNOTE_NO_UPDATE_CHECK` or `NO_UPDATE_NOTIFIER` is set.
|
|
76
|
+
|
|
77
|
+
Update the package and refresh local shims, user instructions, and MCP config with one command:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
threadnote update
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Check without changing anything:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
threadnote update --check
|
|
87
|
+
threadnote update --dry-run
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
After updating, restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.
|
|
91
|
+
|
|
71
92
|
### MCP
|
|
72
93
|
|
|
73
94
|
Make the agents you use aware of Threadnote. Use only the MCP install lines for agents you actually use. Open a fresh
|
|
@@ -152,6 +173,8 @@ This is it! Start working with your agents as usual. The agent will automaticall
|
|
|
152
173
|
- `doctor`: checks prerequisites, the generated command shim, manifest shape, templates, and local OpenViking health.
|
|
153
174
|
- `install`: installs `openviking[local-embed]==0.3.12` if missing, creates `~/.openviking` config files if absent,
|
|
154
175
|
writes the command shim, and upserts user-level agent instructions.
|
|
176
|
+
- `update`: updates the published Threadnote package, then runs `repair` so shims and MCP config point at the new
|
|
177
|
+
version.
|
|
155
178
|
- `repair`: fixes install/config/shim/manifest/server health issues and rewrites Codex/Claude/Cursor MCP configs from the
|
|
156
179
|
current checkout.
|
|
157
180
|
- `start`: starts `openviking-server` on `127.0.0.1:1933`.
|
|
@@ -215,6 +238,8 @@ Environment variables:
|
|
|
215
238
|
- `THREADNOTE_USER`: OpenViking user value, default local username.
|
|
216
239
|
- `THREADNOTE_AGENT_ID`: shared agent identity, default `threadnote`.
|
|
217
240
|
- `THREADNOTE_OPENVIKING_VERSION`: package version to install, default `0.3.12`.
|
|
241
|
+
- `THREADNOTE_NPM_REGISTRY`: npm registry used by the installer and updater, default `https://registry.npmjs.org/`.
|
|
242
|
+
- `THREADNOTE_NO_UPDATE_CHECK`: disables opportunistic update notifications.
|
|
218
243
|
- `THREADNOTE_BIN_DIR`: directory for the `threadnote` shim, default `~/.local/bin`.
|
|
219
244
|
- `THREADNOTE_HOST`: local bind host, default `127.0.0.1`.
|
|
220
245
|
- `THREADNOTE_PORT`: local bind port, default `1933`.
|
package/dist/mcp_server.cjs
CHANGED
|
@@ -30961,6 +30961,7 @@ var DEFAULT_AGENT_ID = "threadnote";
|
|
|
30961
30961
|
|
|
30962
30962
|
// src/utils.ts
|
|
30963
30963
|
var import_node_child_process = require("node:child_process");
|
|
30964
|
+
var import_node_crypto = require("node:crypto");
|
|
30964
30965
|
function redactText(content) {
|
|
30965
30966
|
return content.replace(
|
|
30966
30967
|
/([A-Za-z0-9_.-]*(?:token|secret|password|api[_-]?key|authorization)[A-Za-z0-9_.-]*\s*[:=]\s*)("[^"]+"|'[^']+'|Bearer\s+[^'"\s]+|\S+)/gi,
|
|
@@ -31008,6 +31009,66 @@ async function runCommand(executable, args, options = {}) {
|
|
|
31008
31009
|
});
|
|
31009
31010
|
});
|
|
31010
31011
|
}
|
|
31012
|
+
async function sleep(ms) {
|
|
31013
|
+
return new Promise((resolvePromise) => {
|
|
31014
|
+
setTimeout(resolvePromise, ms);
|
|
31015
|
+
});
|
|
31016
|
+
}
|
|
31017
|
+
function sha256(content) {
|
|
31018
|
+
return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
|
|
31019
|
+
}
|
|
31020
|
+
function exactRecallTerms(query) {
|
|
31021
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
31022
|
+
"about",
|
|
31023
|
+
"after",
|
|
31024
|
+
"agent",
|
|
31025
|
+
"anything",
|
|
31026
|
+
"branch",
|
|
31027
|
+
"case",
|
|
31028
|
+
"current",
|
|
31029
|
+
"find",
|
|
31030
|
+
"handoff",
|
|
31031
|
+
"issue",
|
|
31032
|
+
"issues",
|
|
31033
|
+
"latest",
|
|
31034
|
+
"memory",
|
|
31035
|
+
"memories",
|
|
31036
|
+
"recall",
|
|
31037
|
+
"related",
|
|
31038
|
+
"search",
|
|
31039
|
+
"the",
|
|
31040
|
+
"this",
|
|
31041
|
+
"with"
|
|
31042
|
+
]);
|
|
31043
|
+
const seen = /* @__PURE__ */ new Set();
|
|
31044
|
+
const terms = [];
|
|
31045
|
+
for (const match of query.matchAll(/[A-Za-z0-9_.-]{4,}/g)) {
|
|
31046
|
+
const term = match[0];
|
|
31047
|
+
const normalized = term.toLowerCase();
|
|
31048
|
+
if (stopWords.has(normalized) || seen.has(normalized)) {
|
|
31049
|
+
continue;
|
|
31050
|
+
}
|
|
31051
|
+
seen.add(normalized);
|
|
31052
|
+
terms.push(term);
|
|
31053
|
+
}
|
|
31054
|
+
return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
|
|
31055
|
+
}
|
|
31056
|
+
function grepOutputHasMatches(output) {
|
|
31057
|
+
return !output.includes("matches []") && !output.includes('"matches":[]') && !output.includes("match_count 0");
|
|
31058
|
+
}
|
|
31059
|
+
function exactRecallTermScore(term) {
|
|
31060
|
+
let score = term.length;
|
|
31061
|
+
if (/[A-Z]/.test(term)) {
|
|
31062
|
+
score += 8;
|
|
31063
|
+
}
|
|
31064
|
+
if (/[0-9_.-]/.test(term)) {
|
|
31065
|
+
score += 6;
|
|
31066
|
+
}
|
|
31067
|
+
return score;
|
|
31068
|
+
}
|
|
31069
|
+
function safeTimestamp() {
|
|
31070
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
31071
|
+
}
|
|
31011
31072
|
function formatShellCommand(executable, args) {
|
|
31012
31073
|
return redactText([executable, ...args].map(shellQuote).join(" "));
|
|
31013
31074
|
}
|
|
@@ -31210,7 +31271,7 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
31210
31271
|
if (!checkedUri.ok) {
|
|
31211
31272
|
return checkedUri.error;
|
|
31212
31273
|
}
|
|
31213
|
-
return
|
|
31274
|
+
return runRecallTool(config2, [
|
|
31214
31275
|
"search",
|
|
31215
31276
|
checkedQuery.value,
|
|
31216
31277
|
...checkedUri.value ? ["--uri", checkedUri.value] : [],
|
|
@@ -31219,6 +31280,52 @@ function registerSearchTool(server, config2, name, description) {
|
|
|
31219
31280
|
}
|
|
31220
31281
|
);
|
|
31221
31282
|
}
|
|
31283
|
+
async function runRecallTool(config2, args) {
|
|
31284
|
+
const semanticResult = await runOpenVikingTool(config2, args);
|
|
31285
|
+
if (semanticResult.isError === true) {
|
|
31286
|
+
return semanticResult;
|
|
31287
|
+
}
|
|
31288
|
+
const query = args[1];
|
|
31289
|
+
const exactMatches = typeof query === "string" ? await exactMemoryMatchesText(config2, query) : void 0;
|
|
31290
|
+
if (!exactMatches) {
|
|
31291
|
+
return semanticResult;
|
|
31292
|
+
}
|
|
31293
|
+
const [firstContent] = semanticResult.content;
|
|
31294
|
+
if (firstContent?.type !== "text") {
|
|
31295
|
+
return semanticResult;
|
|
31296
|
+
}
|
|
31297
|
+
return {
|
|
31298
|
+
...semanticResult,
|
|
31299
|
+
content: [{ type: "text", text: `${firstContent.text}
|
|
31300
|
+
|
|
31301
|
+
Exact durable memory matches:
|
|
31302
|
+
${exactMatches}` }]
|
|
31303
|
+
};
|
|
31304
|
+
}
|
|
31305
|
+
async function exactMemoryMatchesText(config2, query) {
|
|
31306
|
+
const terms = exactRecallTerms(query);
|
|
31307
|
+
if (terms.length === 0) {
|
|
31308
|
+
return void 0;
|
|
31309
|
+
}
|
|
31310
|
+
const ov = await requiredOpenVikingCli();
|
|
31311
|
+
const scopes = [
|
|
31312
|
+
`viking://user/${uriSegment(config2.user)}/memories`,
|
|
31313
|
+
`viking://agent/${uriSegment(config2.agentId)}/memories`
|
|
31314
|
+
];
|
|
31315
|
+
const outputs = [];
|
|
31316
|
+
for (const term of terms) {
|
|
31317
|
+
for (const scope of scopes) {
|
|
31318
|
+
const result = await runCommand(ov, withIdentity(config2, ["grep", term, "--uri", scope, "--node-limit", "5"]), {
|
|
31319
|
+
allowFailure: true
|
|
31320
|
+
});
|
|
31321
|
+
const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
31322
|
+
if (result.exitCode === 0 && grepOutputHasMatches(output)) {
|
|
31323
|
+
outputs.push(output);
|
|
31324
|
+
}
|
|
31325
|
+
}
|
|
31326
|
+
}
|
|
31327
|
+
return outputs.length > 0 ? outputs.join("\n\n") : void 0;
|
|
31328
|
+
}
|
|
31222
31329
|
function registerReadTool(server, config2, name, description) {
|
|
31223
31330
|
server.registerTool(
|
|
31224
31331
|
name,
|
|
@@ -31284,8 +31391,8 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
31284
31391
|
if (!checkedText.ok) {
|
|
31285
31392
|
return checkedText.error;
|
|
31286
31393
|
}
|
|
31287
|
-
return
|
|
31288
|
-
|
|
31394
|
+
return writeDurableMemory(
|
|
31395
|
+
config2,
|
|
31289
31396
|
[
|
|
31290
31397
|
"MEMORY",
|
|
31291
31398
|
`source_agent_client: ${sourceAgentClient ?? "mcp"}`,
|
|
@@ -31293,10 +31400,74 @@ function registerStoreTool(server, config2, name, description) {
|
|
|
31293
31400
|
"",
|
|
31294
31401
|
checkedText.value
|
|
31295
31402
|
].join("\n")
|
|
31296
|
-
|
|
31403
|
+
);
|
|
31297
31404
|
}
|
|
31298
31405
|
);
|
|
31299
31406
|
}
|
|
31407
|
+
async function writeDurableMemory(config2, memory) {
|
|
31408
|
+
try {
|
|
31409
|
+
const ov = await requiredOpenVikingCli();
|
|
31410
|
+
const directoryUri = durableMemoryDirectoryUri(config2);
|
|
31411
|
+
const stat = await runCommand(ov, withIdentity(config2, ["stat", directoryUri]), { allowFailure: true });
|
|
31412
|
+
if (stat.exitCode !== 0) {
|
|
31413
|
+
await runCommand(
|
|
31414
|
+
ov,
|
|
31415
|
+
withIdentity(config2, [
|
|
31416
|
+
"mkdir",
|
|
31417
|
+
directoryUri,
|
|
31418
|
+
"--description",
|
|
31419
|
+
"Threadnote durable handoffs, memories, and cross-agent notes."
|
|
31420
|
+
])
|
|
31421
|
+
);
|
|
31422
|
+
}
|
|
31423
|
+
const memoryUri = durableMemoryUri(config2, memory);
|
|
31424
|
+
const result = await runOpenVikingWriteWithRetry(
|
|
31425
|
+
ov,
|
|
31426
|
+
config2,
|
|
31427
|
+
memoryUri,
|
|
31428
|
+
withIdentity(config2, ["write", memoryUri, "--content", memory, "--mode", "create", "--wait", "--timeout", "120"])
|
|
31429
|
+
);
|
|
31430
|
+
const text = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
31431
|
+
return { content: [{ type: "text", text: [`Stored durable memory: ${memoryUri}`, text].filter(Boolean).join("\n") }] };
|
|
31432
|
+
} catch (err) {
|
|
31433
|
+
return { content: [{ type: "text", text: errorMessage(err) }], isError: true };
|
|
31434
|
+
}
|
|
31435
|
+
}
|
|
31436
|
+
async function runOpenVikingWriteWithRetry(ov, config2, memoryUri, args) {
|
|
31437
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
31438
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
31439
|
+
if (result.exitCode === 0) {
|
|
31440
|
+
return result;
|
|
31441
|
+
}
|
|
31442
|
+
if (await vikingResourceExists(ov, config2, memoryUri)) {
|
|
31443
|
+
await runCommand(ov, withIdentity(config2, ["wait", "--timeout", "120"]), { allowFailure: true });
|
|
31444
|
+
return { exitCode: 0, stdout: "OpenViking accepted the memory and indexing wait completed.", stderr: "" };
|
|
31445
|
+
}
|
|
31446
|
+
if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
|
|
31447
|
+
throw new Error(`${ov} ${args.join(" ")} failed: ${result.stderr || result.stdout}`);
|
|
31448
|
+
}
|
|
31449
|
+
await sleep(1e3 * (attempt + 1));
|
|
31450
|
+
}
|
|
31451
|
+
throw new Error(`${ov} ${args.join(" ")} failed.`);
|
|
31452
|
+
}
|
|
31453
|
+
async function vikingResourceExists(ov, config2, uri) {
|
|
31454
|
+
const stat = await runCommand(ov, withIdentity(config2, ["stat", uri]), { allowFailure: true });
|
|
31455
|
+
return stat.exitCode === 0;
|
|
31456
|
+
}
|
|
31457
|
+
function isResourceBusy(stderr, stdout) {
|
|
31458
|
+
return `${stderr}
|
|
31459
|
+
${stdout}`.includes("resource is busy");
|
|
31460
|
+
}
|
|
31461
|
+
function durableMemoryUri(config2, memory) {
|
|
31462
|
+
return `${durableMemoryDirectoryUri(config2)}/threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
|
|
31463
|
+
}
|
|
31464
|
+
function durableMemoryDirectoryUri(config2) {
|
|
31465
|
+
return `viking://user/${uriSegment(config2.user)}/memories/events`;
|
|
31466
|
+
}
|
|
31467
|
+
function uriSegment(value) {
|
|
31468
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
31469
|
+
return normalized.length > 0 ? normalized : "unknown";
|
|
31470
|
+
}
|
|
31300
31471
|
function requiredText(value, toolName, fieldName, example) {
|
|
31301
31472
|
const normalized = value?.trim();
|
|
31302
31473
|
if (normalized) {
|
package/dist/threadnote.cjs
CHANGED
|
@@ -3897,6 +3897,55 @@ function parentVikingUri(uri) {
|
|
|
3897
3897
|
function sha256(content) {
|
|
3898
3898
|
return (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
|
|
3899
3899
|
}
|
|
3900
|
+
function exactRecallTerms(query) {
|
|
3901
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
3902
|
+
"about",
|
|
3903
|
+
"after",
|
|
3904
|
+
"agent",
|
|
3905
|
+
"anything",
|
|
3906
|
+
"branch",
|
|
3907
|
+
"case",
|
|
3908
|
+
"current",
|
|
3909
|
+
"find",
|
|
3910
|
+
"handoff",
|
|
3911
|
+
"issue",
|
|
3912
|
+
"issues",
|
|
3913
|
+
"latest",
|
|
3914
|
+
"memory",
|
|
3915
|
+
"memories",
|
|
3916
|
+
"recall",
|
|
3917
|
+
"related",
|
|
3918
|
+
"search",
|
|
3919
|
+
"the",
|
|
3920
|
+
"this",
|
|
3921
|
+
"with"
|
|
3922
|
+
]);
|
|
3923
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3924
|
+
const terms = [];
|
|
3925
|
+
for (const match of query.matchAll(/[A-Za-z0-9_.-]{4,}/g)) {
|
|
3926
|
+
const term = match[0];
|
|
3927
|
+
const normalized = term.toLowerCase();
|
|
3928
|
+
if (stopWords.has(normalized) || seen.has(normalized)) {
|
|
3929
|
+
continue;
|
|
3930
|
+
}
|
|
3931
|
+
seen.add(normalized);
|
|
3932
|
+
terms.push(term);
|
|
3933
|
+
}
|
|
3934
|
+
return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
|
|
3935
|
+
}
|
|
3936
|
+
function grepOutputHasMatches(output) {
|
|
3937
|
+
return !output.includes("matches []") && !output.includes('"matches":[]') && !output.includes("match_count 0");
|
|
3938
|
+
}
|
|
3939
|
+
function exactRecallTermScore(term) {
|
|
3940
|
+
let score = term.length;
|
|
3941
|
+
if (/[A-Z]/.test(term)) {
|
|
3942
|
+
score += 8;
|
|
3943
|
+
}
|
|
3944
|
+
if (/[0-9_.-]/.test(term)) {
|
|
3945
|
+
score += 6;
|
|
3946
|
+
}
|
|
3947
|
+
return score;
|
|
3948
|
+
}
|
|
3900
3949
|
function safeTimestamp() {
|
|
3901
3950
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3902
3951
|
}
|
|
@@ -7010,6 +7059,7 @@ async function runRecall(config, options) {
|
|
|
7010
7059
|
args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
|
|
7011
7060
|
}
|
|
7012
7061
|
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
7062
|
+
await printExactMemoryMatches(config, ov, options.query, options.dryRun === true);
|
|
7013
7063
|
}
|
|
7014
7064
|
async function runRead(config, uri, options) {
|
|
7015
7065
|
assertVikingUri(uri);
|
|
@@ -7082,18 +7132,141 @@ async function inferProjectFromQuery(config, normalizedQuery) {
|
|
|
7082
7132
|
return void 0;
|
|
7083
7133
|
}
|
|
7084
7134
|
}
|
|
7135
|
+
async function printExactMemoryMatches(config, ov, query, dryRun) {
|
|
7136
|
+
const terms = exactRecallTerms(query);
|
|
7137
|
+
if (terms.length === 0) {
|
|
7138
|
+
return;
|
|
7139
|
+
}
|
|
7140
|
+
const scopes = [
|
|
7141
|
+
`viking://user/${uriSegment(config.user)}/memories`,
|
|
7142
|
+
`viking://agent/${uriSegment(config.agentId)}/memories`
|
|
7143
|
+
];
|
|
7144
|
+
const outputs = [];
|
|
7145
|
+
for (const term of terms) {
|
|
7146
|
+
for (const scope of scopes) {
|
|
7147
|
+
const args = withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5"]);
|
|
7148
|
+
if (dryRun) {
|
|
7149
|
+
outputs.push(formatShellCommand(ov, args));
|
|
7150
|
+
continue;
|
|
7151
|
+
}
|
|
7152
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
7153
|
+
const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
7154
|
+
if (result.exitCode === 0 && grepOutputHasMatches(output)) {
|
|
7155
|
+
outputs.push(output);
|
|
7156
|
+
}
|
|
7157
|
+
}
|
|
7158
|
+
}
|
|
7159
|
+
if (outputs.length === 0) {
|
|
7160
|
+
return;
|
|
7161
|
+
}
|
|
7162
|
+
console.log("\nExact durable memory matches:");
|
|
7163
|
+
console.log(outputs.join("\n\n"));
|
|
7164
|
+
}
|
|
7085
7165
|
async function storeMemory(config, memory, dryRun) {
|
|
7086
7166
|
const ov = await openVikingCliForMode(dryRun);
|
|
7087
7167
|
const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
|
|
7168
|
+
const memoryUri = durableMemoryUri(config, memory);
|
|
7088
7169
|
if (dryRun) {
|
|
7089
7170
|
console.log(memory);
|
|
7090
7171
|
console.log("\nWould run:");
|
|
7091
|
-
console.log(
|
|
7172
|
+
console.log(
|
|
7173
|
+
formatShellCommand(
|
|
7174
|
+
ov,
|
|
7175
|
+
withIdentity(config, [
|
|
7176
|
+
"write",
|
|
7177
|
+
memoryUri,
|
|
7178
|
+
"--from-file",
|
|
7179
|
+
memoryPath,
|
|
7180
|
+
"--mode",
|
|
7181
|
+
"create",
|
|
7182
|
+
"--wait",
|
|
7183
|
+
"--timeout",
|
|
7184
|
+
"120"
|
|
7185
|
+
])
|
|
7186
|
+
)
|
|
7187
|
+
);
|
|
7092
7188
|
return;
|
|
7093
7189
|
}
|
|
7094
7190
|
await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
|
|
7095
7191
|
await (0, import_promises4.chmod)(memoryPath, 384);
|
|
7096
|
-
await
|
|
7192
|
+
await ensureDurableMemoryDirectory(ov, config);
|
|
7193
|
+
await writeDurableMemoryFile(ov, config, memoryUri, memoryPath);
|
|
7194
|
+
console.log(`Stored durable memory: ${memoryUri}`);
|
|
7195
|
+
}
|
|
7196
|
+
async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath) {
|
|
7197
|
+
const args = withIdentity(config, [
|
|
7198
|
+
"write",
|
|
7199
|
+
memoryUri,
|
|
7200
|
+
"--from-file",
|
|
7201
|
+
memoryPath,
|
|
7202
|
+
"--mode",
|
|
7203
|
+
"create",
|
|
7204
|
+
"--wait",
|
|
7205
|
+
"--timeout",
|
|
7206
|
+
"120"
|
|
7207
|
+
]);
|
|
7208
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
7209
|
+
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
7210
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
7211
|
+
if (result.exitCode === 0) {
|
|
7212
|
+
if (result.stdout.trim()) {
|
|
7213
|
+
console.log(result.stdout.trim());
|
|
7214
|
+
}
|
|
7215
|
+
if (result.stderr.trim()) {
|
|
7216
|
+
console.error(result.stderr.trim());
|
|
7217
|
+
}
|
|
7218
|
+
return;
|
|
7219
|
+
}
|
|
7220
|
+
if (await vikingResourceExists(ov, config, memoryUri)) {
|
|
7221
|
+
console.log("OpenViking accepted the memory but returned before the wait completed; waiting for indexing.");
|
|
7222
|
+
await waitForOpenVikingQueue(ov, config);
|
|
7223
|
+
return;
|
|
7224
|
+
}
|
|
7225
|
+
if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
|
|
7226
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
7227
|
+
}
|
|
7228
|
+
await sleep(1e3 * (attempt + 1));
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
async function waitForOpenVikingQueue(ov, config) {
|
|
7232
|
+
const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
|
|
7233
|
+
if (result.stdout.trim()) {
|
|
7234
|
+
console.log(result.stdout.trim());
|
|
7235
|
+
}
|
|
7236
|
+
if (result.stderr.trim()) {
|
|
7237
|
+
console.error(result.stderr.trim());
|
|
7238
|
+
}
|
|
7239
|
+
}
|
|
7240
|
+
async function vikingResourceExists(ov, config, uri) {
|
|
7241
|
+
const stat2 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
7242
|
+
return stat2.exitCode === 0;
|
|
7243
|
+
}
|
|
7244
|
+
async function ensureDurableMemoryDirectory(ov, config) {
|
|
7245
|
+
const directoryUri = durableMemoryDirectoryUri(config);
|
|
7246
|
+
const stat2 = await runCommand(ov, withIdentity(config, ["stat", directoryUri]), { allowFailure: true });
|
|
7247
|
+
if (stat2.exitCode === 0) {
|
|
7248
|
+
return;
|
|
7249
|
+
}
|
|
7250
|
+
await maybeRun(
|
|
7251
|
+
false,
|
|
7252
|
+
ov,
|
|
7253
|
+
withIdentity(config, [
|
|
7254
|
+
"mkdir",
|
|
7255
|
+
directoryUri,
|
|
7256
|
+
"--description",
|
|
7257
|
+
"Threadnote durable handoffs, memories, and cross-agent notes."
|
|
7258
|
+
])
|
|
7259
|
+
);
|
|
7260
|
+
}
|
|
7261
|
+
function durableMemoryUri(config, memory) {
|
|
7262
|
+
return `${durableMemoryDirectoryUri(config)}/threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
|
|
7263
|
+
}
|
|
7264
|
+
function durableMemoryDirectoryUri(config) {
|
|
7265
|
+
return `viking://user/${uriSegment(config.user)}/memories/events`;
|
|
7266
|
+
}
|
|
7267
|
+
function isResourceBusy(stderr, stdout) {
|
|
7268
|
+
return `${stderr}
|
|
7269
|
+
${stdout}`.includes("resource is busy");
|
|
7097
7270
|
}
|
|
7098
7271
|
async function buildHandoff(options) {
|
|
7099
7272
|
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]) ?? getInvocationCwd();
|
|
@@ -8284,25 +8457,289 @@ function parsePackageManager(value) {
|
|
|
8284
8457
|
throw new Error(`Invalid package manager: ${value}`);
|
|
8285
8458
|
}
|
|
8286
8459
|
|
|
8460
|
+
// src/update.ts
|
|
8461
|
+
var import_node_fs4 = require("node:fs");
|
|
8462
|
+
var import_promises7 = require("node:fs/promises");
|
|
8463
|
+
var import_node_os5 = require("node:os");
|
|
8464
|
+
var import_node_path7 = require("node:path");
|
|
8465
|
+
var NPM_PACKAGE_NAME = "threadnote";
|
|
8466
|
+
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
8467
|
+
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
8468
|
+
function parseUpdateRuntime(value) {
|
|
8469
|
+
if (value === "auto" || value === "npm" || value === "bun" || value === "deno") {
|
|
8470
|
+
return value;
|
|
8471
|
+
}
|
|
8472
|
+
throw new Error(`Invalid update runtime: ${value}. Expected auto, npm, bun, or deno.`);
|
|
8473
|
+
}
|
|
8474
|
+
async function maybeNotifyUpdate(config, options = {}) {
|
|
8475
|
+
if (isUpdateNotificationDisabled()) {
|
|
8476
|
+
return;
|
|
8477
|
+
}
|
|
8478
|
+
try {
|
|
8479
|
+
const info = await getUpdateInfo(config, {
|
|
8480
|
+
allowCacheWrite: options.dryRun !== true,
|
|
8481
|
+
preferFresh: false,
|
|
8482
|
+
registry: updateRegistry()
|
|
8483
|
+
});
|
|
8484
|
+
if (!info.isUpdateAvailable) {
|
|
8485
|
+
return;
|
|
8486
|
+
}
|
|
8487
|
+
console.log("");
|
|
8488
|
+
console.log(`Update available: threadnote ${info.currentVersion} -> ${info.latestVersion}`);
|
|
8489
|
+
console.log("Run: threadnote update");
|
|
8490
|
+
} catch (_err) {
|
|
8491
|
+
return;
|
|
8492
|
+
}
|
|
8493
|
+
}
|
|
8494
|
+
async function runUpdate(config, options) {
|
|
8495
|
+
const registry = normalizeRegistry(options.registry ?? updateRegistry());
|
|
8496
|
+
const info = await getUpdateInfo(config, {
|
|
8497
|
+
allowCacheWrite: options.dryRun !== true,
|
|
8498
|
+
preferFresh: true,
|
|
8499
|
+
registry
|
|
8500
|
+
});
|
|
8501
|
+
console.log(`Current version: ${info.currentVersion}`);
|
|
8502
|
+
console.log(`Latest version: ${info.latestVersion}`);
|
|
8503
|
+
console.log(`Registry: ${info.registry}`);
|
|
8504
|
+
if (options.check === true) {
|
|
8505
|
+
if (info.isUpdateAvailable) {
|
|
8506
|
+
console.log(`Update available. Run: threadnote update`);
|
|
8507
|
+
} else {
|
|
8508
|
+
console.log(
|
|
8509
|
+
compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
|
|
8510
|
+
);
|
|
8511
|
+
}
|
|
8512
|
+
return;
|
|
8513
|
+
}
|
|
8514
|
+
if (!info.isUpdateAvailable && options.force !== true) {
|
|
8515
|
+
console.log("Threadnote is up to date.");
|
|
8516
|
+
return;
|
|
8517
|
+
}
|
|
8518
|
+
const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
|
|
8519
|
+
const updateCommand = updatePackageCommand(runtime, registry);
|
|
8520
|
+
await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
|
|
8521
|
+
if (options.repair === false) {
|
|
8522
|
+
console.log("Skipping repair because --no-repair was provided.");
|
|
8523
|
+
return;
|
|
8524
|
+
}
|
|
8525
|
+
const threadnoteCommand = await installedThreadnoteCommand(runtime);
|
|
8526
|
+
await maybeRun(options.dryRun === true, threadnoteCommand, ["repair"]);
|
|
8527
|
+
console.log("Update complete. Restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.");
|
|
8528
|
+
}
|
|
8529
|
+
async function getUpdateInfo(config, options) {
|
|
8530
|
+
const currentVersion = await currentPackageVersion();
|
|
8531
|
+
const cached = options.preferFresh ? void 0 : await readFreshCache(config, options.registry);
|
|
8532
|
+
const latestVersion = cached?.latestVersion ?? await fetchLatestVersion(options.registry);
|
|
8533
|
+
if (!cached && options.allowCacheWrite) {
|
|
8534
|
+
await writeUpdateCache(config, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion, registry: options.registry });
|
|
8535
|
+
}
|
|
8536
|
+
return {
|
|
8537
|
+
currentVersion,
|
|
8538
|
+
isUpdateAvailable: compareVersions(currentVersion, latestVersion) < 0,
|
|
8539
|
+
latestVersion,
|
|
8540
|
+
registry: options.registry
|
|
8541
|
+
};
|
|
8542
|
+
}
|
|
8543
|
+
async function currentPackageVersion() {
|
|
8544
|
+
const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
|
|
8545
|
+
const parsed = JSON.parse(rawPackage);
|
|
8546
|
+
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8547
|
+
throw new Error("Could not read current threadnote package version.");
|
|
8548
|
+
}
|
|
8549
|
+
return parsed.version;
|
|
8550
|
+
}
|
|
8551
|
+
async function fetchLatestVersion(registry) {
|
|
8552
|
+
const url = new URL(`${NPM_PACKAGE_NAME}/latest`, normalizeRegistry(registry));
|
|
8553
|
+
const controller = new AbortController();
|
|
8554
|
+
const timeout = setTimeout(() => {
|
|
8555
|
+
controller.abort();
|
|
8556
|
+
}, 2500);
|
|
8557
|
+
try {
|
|
8558
|
+
const response = await fetch(url, { headers: { accept: "application/json" }, signal: controller.signal });
|
|
8559
|
+
if (!response.ok) {
|
|
8560
|
+
throw new Error(`npm registry returned HTTP ${response.status}`);
|
|
8561
|
+
}
|
|
8562
|
+
const parsed = await response.json();
|
|
8563
|
+
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8564
|
+
throw new Error("npm registry response did not include a version.");
|
|
8565
|
+
}
|
|
8566
|
+
return parsed.version;
|
|
8567
|
+
} catch (err) {
|
|
8568
|
+
throw new Error(`Could not check npm for updates: ${errorMessage(err)}`, { cause: err });
|
|
8569
|
+
} finally {
|
|
8570
|
+
clearTimeout(timeout);
|
|
8571
|
+
}
|
|
8572
|
+
}
|
|
8573
|
+
async function readFreshCache(config, registry) {
|
|
8574
|
+
const rawCache = await readFileIfExists(updateCachePath(config));
|
|
8575
|
+
if (!rawCache) {
|
|
8576
|
+
return void 0;
|
|
8577
|
+
}
|
|
8578
|
+
try {
|
|
8579
|
+
const parsed = JSON.parse(rawCache);
|
|
8580
|
+
if (!isJsonObject(parsed) || typeof parsed.checkedAt !== "string" || typeof parsed.latestVersion !== "string" || parsed.registry !== registry) {
|
|
8581
|
+
return void 0;
|
|
8582
|
+
}
|
|
8583
|
+
const checkedAt = Date.parse(parsed.checkedAt);
|
|
8584
|
+
if (!Number.isFinite(checkedAt) || Date.now() - checkedAt > UPDATE_CHECK_TTL_MS) {
|
|
8585
|
+
return void 0;
|
|
8586
|
+
}
|
|
8587
|
+
return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, registry };
|
|
8588
|
+
} catch (_err) {
|
|
8589
|
+
return void 0;
|
|
8590
|
+
}
|
|
8591
|
+
}
|
|
8592
|
+
async function writeUpdateCache(config, cache) {
|
|
8593
|
+
await ensureDirectory(config.agentContextHome, false);
|
|
8594
|
+
await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
|
|
8595
|
+
`, { encoding: "utf8", mode: 384 });
|
|
8596
|
+
}
|
|
8597
|
+
function updateCachePath(config) {
|
|
8598
|
+
return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
|
|
8599
|
+
}
|
|
8600
|
+
async function resolveUpdateRuntime(runtime) {
|
|
8601
|
+
if (runtime !== "auto") {
|
|
8602
|
+
await requireRuntime(runtime);
|
|
8603
|
+
return runtime;
|
|
8604
|
+
}
|
|
8605
|
+
for (const candidate of ["npm", "bun", "deno"]) {
|
|
8606
|
+
if (await findExecutable([candidate])) {
|
|
8607
|
+
return candidate;
|
|
8608
|
+
}
|
|
8609
|
+
}
|
|
8610
|
+
throw new Error("Install Node/npm, Bun, or Deno to update threadnote.");
|
|
8611
|
+
}
|
|
8612
|
+
async function requireRuntime(runtime) {
|
|
8613
|
+
if (!await findExecutable([runtime])) {
|
|
8614
|
+
throw new Error(`${runtime} was requested but was not found on PATH.`);
|
|
8615
|
+
}
|
|
8616
|
+
}
|
|
8617
|
+
async function installedThreadnoteCommand(runtime) {
|
|
8618
|
+
const runtimeBin = await runtimeThreadnoteBin(runtime);
|
|
8619
|
+
if (runtimeBin && await isExecutable(runtimeBin)) {
|
|
8620
|
+
return runtimeBin;
|
|
8621
|
+
}
|
|
8622
|
+
return await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
|
|
8623
|
+
}
|
|
8624
|
+
async function runtimeThreadnoteBin(runtime) {
|
|
8625
|
+
if (runtime === "npm") {
|
|
8626
|
+
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
8627
|
+
const prefix = result.stdout.trim();
|
|
8628
|
+
return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
8629
|
+
}
|
|
8630
|
+
if (runtime === "bun") {
|
|
8631
|
+
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
8632
|
+
const binDir = result.stdout.trim();
|
|
8633
|
+
return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
8634
|
+
}
|
|
8635
|
+
return (0, import_node_path7.join)(process.env.DENO_INSTALL ?? (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
|
|
8636
|
+
}
|
|
8637
|
+
async function isExecutable(path) {
|
|
8638
|
+
try {
|
|
8639
|
+
await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
|
|
8640
|
+
return true;
|
|
8641
|
+
} catch (_err) {
|
|
8642
|
+
return false;
|
|
8643
|
+
}
|
|
8644
|
+
}
|
|
8645
|
+
function updatePackageCommand(runtime, registry) {
|
|
8646
|
+
if (runtime === "npm") {
|
|
8647
|
+
return { executable: "npm", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
|
|
8648
|
+
}
|
|
8649
|
+
if (runtime === "bun") {
|
|
8650
|
+
return { executable: "bun", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
|
|
8651
|
+
}
|
|
8652
|
+
return {
|
|
8653
|
+
executable: "env",
|
|
8654
|
+
args: [
|
|
8655
|
+
`NPM_CONFIG_REGISTRY=${registry}`,
|
|
8656
|
+
"deno",
|
|
8657
|
+
"install",
|
|
8658
|
+
"--global",
|
|
8659
|
+
"--force",
|
|
8660
|
+
"--name",
|
|
8661
|
+
NPM_PACKAGE_NAME,
|
|
8662
|
+
"--allow-read",
|
|
8663
|
+
"--allow-write",
|
|
8664
|
+
"--allow-run",
|
|
8665
|
+
"--allow-env",
|
|
8666
|
+
"--allow-net",
|
|
8667
|
+
`npm:${NPM_PACKAGE_NAME}@latest`
|
|
8668
|
+
]
|
|
8669
|
+
};
|
|
8670
|
+
}
|
|
8671
|
+
function normalizeRegistry(registry) {
|
|
8672
|
+
return registry.endsWith("/") ? registry : `${registry}/`;
|
|
8673
|
+
}
|
|
8674
|
+
function updateRegistry() {
|
|
8675
|
+
return normalizeRegistry(process.env.THREADNOTE_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY);
|
|
8676
|
+
}
|
|
8677
|
+
function isUpdateNotificationDisabled() {
|
|
8678
|
+
return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
|
|
8679
|
+
}
|
|
8680
|
+
function compareVersions(currentVersion, latestVersion) {
|
|
8681
|
+
const current = parseVersion(currentVersion);
|
|
8682
|
+
const latest = parseVersion(latestVersion);
|
|
8683
|
+
for (let index = 0; index < 3; index += 1) {
|
|
8684
|
+
const difference = current.numbers[index] - latest.numbers[index];
|
|
8685
|
+
if (difference !== 0) {
|
|
8686
|
+
return difference;
|
|
8687
|
+
}
|
|
8688
|
+
}
|
|
8689
|
+
if (current.prerelease === latest.prerelease) {
|
|
8690
|
+
return 0;
|
|
8691
|
+
}
|
|
8692
|
+
if (current.prerelease === void 0) {
|
|
8693
|
+
return 1;
|
|
8694
|
+
}
|
|
8695
|
+
if (latest.prerelease === void 0) {
|
|
8696
|
+
return -1;
|
|
8697
|
+
}
|
|
8698
|
+
return current.prerelease.localeCompare(latest.prerelease);
|
|
8699
|
+
}
|
|
8700
|
+
function parseVersion(version) {
|
|
8701
|
+
const normalized = version.trim().replace(/^v/, "");
|
|
8702
|
+
const [core2, prerelease] = normalized.split("-", 2);
|
|
8703
|
+
const parts = core2.split(".").map((part) => Number(part));
|
|
8704
|
+
return {
|
|
8705
|
+
numbers: [safeVersionNumber(parts[0]), safeVersionNumber(parts[1]), safeVersionNumber(parts[2])],
|
|
8706
|
+
prerelease
|
|
8707
|
+
};
|
|
8708
|
+
}
|
|
8709
|
+
function safeVersionNumber(value) {
|
|
8710
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
8711
|
+
}
|
|
8712
|
+
|
|
8287
8713
|
// src/threadnote.ts
|
|
8288
8714
|
async function main() {
|
|
8289
8715
|
const program2 = new Command();
|
|
8290
8716
|
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);
|
|
8291
8717
|
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) => {
|
|
8292
|
-
|
|
8718
|
+
const config = getRuntimeConfig(program2);
|
|
8719
|
+
await runDoctor(config, options);
|
|
8720
|
+
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
8293
8721
|
});
|
|
8294
8722
|
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("--package-manager <manager>", "pipx, uv, or pip", parsePackageManager).action(async (options) => {
|
|
8295
|
-
|
|
8723
|
+
const config = getRuntimeConfig(program2);
|
|
8724
|
+
await runInstall(config, options);
|
|
8725
|
+
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
8726
|
+
});
|
|
8727
|
+
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").action(async (options) => {
|
|
8728
|
+
await runUpdate(getRuntimeConfig(program2), options);
|
|
8296
8729
|
});
|
|
8297
8730
|
program2.command("repair").description("Repair local OpenViking install, config files, server health, shim, manifest, and MCP config").option("--dry-run", "Print the repair actions without making changes").option(
|
|
8298
8731
|
"--mcp <clients>",
|
|
8299
8732
|
"MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
|
|
8300
8733
|
"available"
|
|
8301
8734
|
).option("--no-start", "Do not start OpenViking if health is failing").option("--package-manager <manager>", "pipx, uv, or pip", parsePackageManager).action(async (options) => {
|
|
8302
|
-
|
|
8735
|
+
const config = getRuntimeConfig(program2);
|
|
8736
|
+
await runRepair(config, options);
|
|
8737
|
+
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
8303
8738
|
});
|
|
8304
8739
|
program2.command("start").description("Start the local OpenViking server").option("--dry-run", "Print the start command without running it").option("--foreground", "Run in the foreground instead of detaching").option("--launchd", "Install and start a macOS LaunchAgent").action(async (options) => {
|
|
8305
|
-
|
|
8740
|
+
const config = getRuntimeConfig(program2);
|
|
8741
|
+
await runStart(config, options);
|
|
8742
|
+
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
8306
8743
|
});
|
|
8307
8744
|
program2.command("stop").description("Stop the local OpenViking server or LaunchAgent").option("--dry-run", "Print the stop actions without running them").action(async (options) => {
|
|
8308
8745
|
await runStop(getRuntimeConfig(program2), options);
|