threadnote 0.3.1 → 0.3.3
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 +27 -0
- package/dist/mcp_server.cjs +175 -4
- package/dist/threadnote.cjs +678 -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`.
|
|
@@ -164,6 +187,8 @@ This is it! Start working with your agents as usual. The agent will automaticall
|
|
|
164
187
|
`seed-skills --native` only after configuring a working VLM provider.
|
|
165
188
|
- `mcp-install codex|claude|cursor`: installs or prints OpenViking MCP configuration for Codex, Claude, or Cursor.
|
|
166
189
|
- `remember`: stores a durable memory.
|
|
190
|
+
- `migrate-memories`: migrates legacy session-only `MEMORY` and `HANDOFF` records into durable memory files. Run
|
|
191
|
+
`migrate-memories --dry-run` first; use `--all-accounts` when importing from older local OpenViking accounts.
|
|
167
192
|
- `recall`: searches shared OpenViking context. It infers repo or skill scope from queries like
|
|
168
193
|
`skills for api service`; use `--uri` or `--no-infer-scope` to override.
|
|
169
194
|
- `read`: reads a `viking://` URI returned by `recall` or `list`.
|
|
@@ -215,6 +240,8 @@ Environment variables:
|
|
|
215
240
|
- `THREADNOTE_USER`: OpenViking user value, default local username.
|
|
216
241
|
- `THREADNOTE_AGENT_ID`: shared agent identity, default `threadnote`.
|
|
217
242
|
- `THREADNOTE_OPENVIKING_VERSION`: package version to install, default `0.3.12`.
|
|
243
|
+
- `THREADNOTE_NPM_REGISTRY`: npm registry used by the installer and updater, default `https://registry.npmjs.org/`.
|
|
244
|
+
- `THREADNOTE_NO_UPDATE_CHECK`: disables opportunistic update notifications.
|
|
218
245
|
- `THREADNOTE_BIN_DIR`: directory for the `threadnote` shim, default `~/.local/bin`.
|
|
219
246
|
- `THREADNOTE_HOST`: local bind host, default `127.0.0.1`.
|
|
220
247
|
- `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
|
}
|
|
@@ -6998,6 +7047,75 @@ async function runRemember(config, options) {
|
|
|
6998
7047
|
].join("\n");
|
|
6999
7048
|
await storeMemory(config, memory, options.dryRun === true);
|
|
7000
7049
|
}
|
|
7050
|
+
async function runMigrateMemories(config, options) {
|
|
7051
|
+
const dryRun = options.dryRun === true;
|
|
7052
|
+
const limit = options.limit ? parsePositiveInteger(options.limit, "migration limit") : void 0;
|
|
7053
|
+
const sourceAccounts = await legacySourceAccounts(config, options);
|
|
7054
|
+
if (sourceAccounts.length === 0) {
|
|
7055
|
+
console.log("No local OpenViking accounts found to scan.");
|
|
7056
|
+
return;
|
|
7057
|
+
}
|
|
7058
|
+
const candidates = await legacyMemoryCandidates(config, sourceAccounts);
|
|
7059
|
+
const existingHashes = await existingDurableMemoryHashes(config);
|
|
7060
|
+
const ov = await openVikingCliForMode(dryRun);
|
|
7061
|
+
const migrationPath = (0, import_node_path4.join)(config.agentContextHome, "legacy-memory-migration.txt");
|
|
7062
|
+
let duplicateCount = 0;
|
|
7063
|
+
let migratedCount = 0;
|
|
7064
|
+
let sensitiveCount = 0;
|
|
7065
|
+
if (!dryRun && candidates.length > 0) {
|
|
7066
|
+
await ensureDurableMemoryDirectory(ov, config);
|
|
7067
|
+
}
|
|
7068
|
+
try {
|
|
7069
|
+
for (const candidate of candidates) {
|
|
7070
|
+
if (existingHashes.has(candidate.hash)) {
|
|
7071
|
+
duplicateCount += 1;
|
|
7072
|
+
continue;
|
|
7073
|
+
}
|
|
7074
|
+
if (existingHashes.has(candidate.comparableHash)) {
|
|
7075
|
+
duplicateCount += 1;
|
|
7076
|
+
continue;
|
|
7077
|
+
}
|
|
7078
|
+
const sensitiveReason = sensitiveMemoryReason(candidate.text);
|
|
7079
|
+
if (sensitiveReason) {
|
|
7080
|
+
sensitiveCount += 1;
|
|
7081
|
+
console.log(
|
|
7082
|
+
`SKIP ${legacySourceLabel(candidate)}: possible ${sensitiveReason}; inspect the source archive manually if needed.`
|
|
7083
|
+
);
|
|
7084
|
+
continue;
|
|
7085
|
+
}
|
|
7086
|
+
if (limit !== void 0 && migratedCount >= limit) {
|
|
7087
|
+
break;
|
|
7088
|
+
}
|
|
7089
|
+
const memoryUri = migratedDurableMemoryUri(config, candidate.hash);
|
|
7090
|
+
if (!dryRun && await vikingResourceExists(ov, config, memoryUri)) {
|
|
7091
|
+
duplicateCount += 1;
|
|
7092
|
+
existingHashes.add(candidate.hash);
|
|
7093
|
+
continue;
|
|
7094
|
+
}
|
|
7095
|
+
console.log(`${dryRun ? "Would migrate" : "Migrating"} ${legacySourceLabel(candidate)} -> ${memoryUri}`);
|
|
7096
|
+
if (!dryRun) {
|
|
7097
|
+
await (0, import_promises4.writeFile)(migrationPath, candidate.text, { encoding: "utf8", mode: 384 });
|
|
7098
|
+
await (0, import_promises4.chmod)(migrationPath, 384);
|
|
7099
|
+
await writeDurableMemoryFile(ov, config, memoryUri, migrationPath);
|
|
7100
|
+
existingHashes.add(candidate.hash);
|
|
7101
|
+
}
|
|
7102
|
+
migratedCount += 1;
|
|
7103
|
+
}
|
|
7104
|
+
} finally {
|
|
7105
|
+
if (!dryRun) {
|
|
7106
|
+
await (0, import_promises4.rm)(migrationPath, { force: true });
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
console.log(
|
|
7110
|
+
[
|
|
7111
|
+
`Migration summary: ${migratedCount} ${dryRun ? "would be migrated" : "migrated"}`,
|
|
7112
|
+
`${duplicateCount} duplicate(s) skipped`,
|
|
7113
|
+
`${sensitiveCount} sensitive-looking item(s) skipped`,
|
|
7114
|
+
`${candidates.length} legacy Threadnote item(s) scanned`,
|
|
7115
|
+
`source account(s): ${sourceAccounts.join(", ")}`
|
|
7116
|
+
].join("; ")
|
|
7117
|
+
);
|
|
7118
|
+
}
|
|
7001
7119
|
async function runRecall(config, options) {
|
|
7002
7120
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
7003
7121
|
const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, options.query));
|
|
@@ -7010,6 +7128,7 @@ async function runRecall(config, options) {
|
|
|
7010
7128
|
args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
|
|
7011
7129
|
}
|
|
7012
7130
|
await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
|
|
7131
|
+
await printExactMemoryMatches(config, ov, options.query, options.dryRun === true);
|
|
7013
7132
|
}
|
|
7014
7133
|
async function runRead(config, uri, options) {
|
|
7015
7134
|
assertVikingUri(uri);
|
|
@@ -7082,18 +7201,299 @@ async function inferProjectFromQuery(config, normalizedQuery) {
|
|
|
7082
7201
|
return void 0;
|
|
7083
7202
|
}
|
|
7084
7203
|
}
|
|
7204
|
+
async function printExactMemoryMatches(config, ov, query, dryRun) {
|
|
7205
|
+
const terms = exactRecallTerms(query);
|
|
7206
|
+
if (terms.length === 0) {
|
|
7207
|
+
return;
|
|
7208
|
+
}
|
|
7209
|
+
const scopes = [
|
|
7210
|
+
`viking://user/${uriSegment(config.user)}/memories`,
|
|
7211
|
+
`viking://agent/${uriSegment(config.agentId)}/memories`
|
|
7212
|
+
];
|
|
7213
|
+
const outputs = [];
|
|
7214
|
+
for (const term of terms) {
|
|
7215
|
+
for (const scope of scopes) {
|
|
7216
|
+
const args = withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5"]);
|
|
7217
|
+
if (dryRun) {
|
|
7218
|
+
outputs.push(formatShellCommand(ov, args));
|
|
7219
|
+
continue;
|
|
7220
|
+
}
|
|
7221
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
7222
|
+
const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
7223
|
+
if (result.exitCode === 0 && grepOutputHasMatches(output)) {
|
|
7224
|
+
outputs.push(output);
|
|
7225
|
+
}
|
|
7226
|
+
}
|
|
7227
|
+
}
|
|
7228
|
+
if (outputs.length === 0) {
|
|
7229
|
+
return;
|
|
7230
|
+
}
|
|
7231
|
+
console.log("\nExact durable memory matches:");
|
|
7232
|
+
console.log(outputs.join("\n\n"));
|
|
7233
|
+
}
|
|
7085
7234
|
async function storeMemory(config, memory, dryRun) {
|
|
7086
7235
|
const ov = await openVikingCliForMode(dryRun);
|
|
7087
7236
|
const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
|
|
7237
|
+
const memoryUri = durableMemoryUri(config, memory);
|
|
7088
7238
|
if (dryRun) {
|
|
7089
7239
|
console.log(memory);
|
|
7090
7240
|
console.log("\nWould run:");
|
|
7091
|
-
console.log(
|
|
7241
|
+
console.log(
|
|
7242
|
+
formatShellCommand(
|
|
7243
|
+
ov,
|
|
7244
|
+
withIdentity(config, [
|
|
7245
|
+
"write",
|
|
7246
|
+
memoryUri,
|
|
7247
|
+
"--from-file",
|
|
7248
|
+
memoryPath,
|
|
7249
|
+
"--mode",
|
|
7250
|
+
"create",
|
|
7251
|
+
"--wait",
|
|
7252
|
+
"--timeout",
|
|
7253
|
+
"120"
|
|
7254
|
+
])
|
|
7255
|
+
)
|
|
7256
|
+
);
|
|
7092
7257
|
return;
|
|
7093
7258
|
}
|
|
7094
7259
|
await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
|
|
7095
7260
|
await (0, import_promises4.chmod)(memoryPath, 384);
|
|
7096
|
-
await
|
|
7261
|
+
await ensureDurableMemoryDirectory(ov, config);
|
|
7262
|
+
await writeDurableMemoryFile(ov, config, memoryUri, memoryPath);
|
|
7263
|
+
console.log(`Stored durable memory: ${memoryUri}`);
|
|
7264
|
+
}
|
|
7265
|
+
async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath) {
|
|
7266
|
+
const args = withIdentity(config, [
|
|
7267
|
+
"write",
|
|
7268
|
+
memoryUri,
|
|
7269
|
+
"--from-file",
|
|
7270
|
+
memoryPath,
|
|
7271
|
+
"--mode",
|
|
7272
|
+
"create",
|
|
7273
|
+
"--wait",
|
|
7274
|
+
"--timeout",
|
|
7275
|
+
"120"
|
|
7276
|
+
]);
|
|
7277
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
7278
|
+
console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
|
|
7279
|
+
const result = await runCommand(ov, args, { allowFailure: true });
|
|
7280
|
+
if (result.exitCode === 0) {
|
|
7281
|
+
if (result.stdout.trim()) {
|
|
7282
|
+
console.log(result.stdout.trim());
|
|
7283
|
+
}
|
|
7284
|
+
if (result.stderr.trim()) {
|
|
7285
|
+
console.error(result.stderr.trim());
|
|
7286
|
+
}
|
|
7287
|
+
return;
|
|
7288
|
+
}
|
|
7289
|
+
if (await vikingResourceExists(ov, config, memoryUri)) {
|
|
7290
|
+
console.log("OpenViking accepted the memory but returned before the wait completed; waiting for indexing.");
|
|
7291
|
+
await waitForOpenVikingQueue(ov, config);
|
|
7292
|
+
return;
|
|
7293
|
+
}
|
|
7294
|
+
if (!isResourceBusy(result.stderr, result.stdout) || attempt === 3) {
|
|
7295
|
+
throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
|
|
7296
|
+
}
|
|
7297
|
+
await sleep(1e3 * (attempt + 1));
|
|
7298
|
+
}
|
|
7299
|
+
}
|
|
7300
|
+
async function waitForOpenVikingQueue(ov, config) {
|
|
7301
|
+
const result = await runCommand(ov, withIdentity(config, ["wait", "--timeout", "120"]), { allowFailure: true });
|
|
7302
|
+
if (result.stdout.trim()) {
|
|
7303
|
+
console.log(result.stdout.trim());
|
|
7304
|
+
}
|
|
7305
|
+
if (result.stderr.trim()) {
|
|
7306
|
+
console.error(result.stderr.trim());
|
|
7307
|
+
}
|
|
7308
|
+
}
|
|
7309
|
+
async function vikingResourceExists(ov, config, uri) {
|
|
7310
|
+
const stat3 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
7311
|
+
return stat3.exitCode === 0;
|
|
7312
|
+
}
|
|
7313
|
+
async function ensureDurableMemoryDirectory(ov, config) {
|
|
7314
|
+
const directoryUri = durableMemoryDirectoryUri(config);
|
|
7315
|
+
const stat3 = await runCommand(ov, withIdentity(config, ["stat", directoryUri]), { allowFailure: true });
|
|
7316
|
+
if (stat3.exitCode === 0) {
|
|
7317
|
+
return;
|
|
7318
|
+
}
|
|
7319
|
+
await maybeRun(
|
|
7320
|
+
false,
|
|
7321
|
+
ov,
|
|
7322
|
+
withIdentity(config, [
|
|
7323
|
+
"mkdir",
|
|
7324
|
+
directoryUri,
|
|
7325
|
+
"--description",
|
|
7326
|
+
"Threadnote durable handoffs, memories, and cross-agent notes."
|
|
7327
|
+
])
|
|
7328
|
+
);
|
|
7329
|
+
}
|
|
7330
|
+
function durableMemoryUri(config, memory) {
|
|
7331
|
+
return `${durableMemoryDirectoryUri(config)}/threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
|
|
7332
|
+
}
|
|
7333
|
+
function durableMemoryDirectoryUri(config) {
|
|
7334
|
+
return `viking://user/${uriSegment(config.user)}/memories/events`;
|
|
7335
|
+
}
|
|
7336
|
+
function migratedDurableMemoryUri(config, hash) {
|
|
7337
|
+
return `${durableMemoryDirectoryUri(config)}/threadnote-migrated-${hash.slice(0, 16)}.md`;
|
|
7338
|
+
}
|
|
7339
|
+
async function legacySourceAccounts(config, options) {
|
|
7340
|
+
const explicitAccounts = options.sourceAccount?.filter((account) => account.trim().length > 0) ?? [];
|
|
7341
|
+
if (explicitAccounts.length > 0) {
|
|
7342
|
+
return uniqueStrings(explicitAccounts);
|
|
7343
|
+
}
|
|
7344
|
+
if (options.allAccounts === true) {
|
|
7345
|
+
const accounts = await childDirectoryNames(localVikingDataRoot(config));
|
|
7346
|
+
return accounts.filter((account) => !account.startsWith("_"));
|
|
7347
|
+
}
|
|
7348
|
+
return [config.account];
|
|
7349
|
+
}
|
|
7350
|
+
async function legacyMemoryCandidates(config, sourceAccounts) {
|
|
7351
|
+
const candidates = [];
|
|
7352
|
+
for (const sourceAccount of sourceAccounts) {
|
|
7353
|
+
const sessionRoot = (0, import_node_path4.join)(localVikingDataRoot(config), sourceAccount, "session");
|
|
7354
|
+
for (const sourceSession of await childDirectoryNames(sessionRoot)) {
|
|
7355
|
+
const historyRoot = (0, import_node_path4.join)(sessionRoot, sourceSession, "history");
|
|
7356
|
+
for (const sourceArchive of await childDirectoryNames(historyRoot)) {
|
|
7357
|
+
if (!sourceArchive.startsWith("archive_")) {
|
|
7358
|
+
continue;
|
|
7359
|
+
}
|
|
7360
|
+
const sourcePath = (0, import_node_path4.join)(historyRoot, sourceArchive, "messages.jsonl");
|
|
7361
|
+
for (const text of await legacyMemoryTexts(sourcePath)) {
|
|
7362
|
+
candidates.push({
|
|
7363
|
+
comparableHash: sha256(comparableMemoryText(text)),
|
|
7364
|
+
hash: sha256(text),
|
|
7365
|
+
sourceAccount,
|
|
7366
|
+
sourceArchive,
|
|
7367
|
+
sourceSession,
|
|
7368
|
+
text
|
|
7369
|
+
});
|
|
7370
|
+
}
|
|
7371
|
+
}
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
return candidates.sort((left, right) => legacySourceLabel(left).localeCompare(legacySourceLabel(right)));
|
|
7375
|
+
}
|
|
7376
|
+
async function legacyMemoryTexts(sourcePath) {
|
|
7377
|
+
const raw = await readTextIfExists(sourcePath);
|
|
7378
|
+
if (!raw) {
|
|
7379
|
+
return [];
|
|
7380
|
+
}
|
|
7381
|
+
const memories = [];
|
|
7382
|
+
for (const line of raw.split("\n")) {
|
|
7383
|
+
const trimmedLine = line.trim();
|
|
7384
|
+
if (!trimmedLine) {
|
|
7385
|
+
continue;
|
|
7386
|
+
}
|
|
7387
|
+
try {
|
|
7388
|
+
const parsed = JSON.parse(trimmedLine);
|
|
7389
|
+
const text = legacyMessageText(parsed)?.trim();
|
|
7390
|
+
if (text && isLegacyThreadnoteMemory(text)) {
|
|
7391
|
+
memories.push(text);
|
|
7392
|
+
}
|
|
7393
|
+
} catch (_err) {
|
|
7394
|
+
continue;
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
return memories;
|
|
7398
|
+
}
|
|
7399
|
+
function legacyMessageText(value) {
|
|
7400
|
+
if (!isJsonObject(value)) {
|
|
7401
|
+
return void 0;
|
|
7402
|
+
}
|
|
7403
|
+
if (typeof value.content === "string") {
|
|
7404
|
+
return value.content;
|
|
7405
|
+
}
|
|
7406
|
+
if (!Array.isArray(value.parts)) {
|
|
7407
|
+
return void 0;
|
|
7408
|
+
}
|
|
7409
|
+
const parts = value.parts.map((part) => isJsonObject(part) && part.type === "text" && typeof part.text === "string" ? part.text : void 0).filter((text) => text !== void 0);
|
|
7410
|
+
return parts.length > 0 ? parts.join("\n") : void 0;
|
|
7411
|
+
}
|
|
7412
|
+
function isLegacyThreadnoteMemory(text) {
|
|
7413
|
+
return text.startsWith("MEMORY\n") || text.startsWith("HANDOFF\n");
|
|
7414
|
+
}
|
|
7415
|
+
async function existingDurableMemoryHashes(config) {
|
|
7416
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
7417
|
+
await collectDurableMemoryHashes(localVikingDataRoot(config), hashes);
|
|
7418
|
+
return hashes;
|
|
7419
|
+
}
|
|
7420
|
+
async function collectDurableMemoryHashes(root, hashes) {
|
|
7421
|
+
let entries;
|
|
7422
|
+
try {
|
|
7423
|
+
entries = await (0, import_promises4.readdir)(root, { withFileTypes: true });
|
|
7424
|
+
} catch (_err) {
|
|
7425
|
+
return;
|
|
7426
|
+
}
|
|
7427
|
+
for (const entry of entries) {
|
|
7428
|
+
const path = (0, import_node_path4.join)(root, entry.name);
|
|
7429
|
+
if (entry.isDirectory()) {
|
|
7430
|
+
await collectDurableMemoryHashes(path, hashes);
|
|
7431
|
+
continue;
|
|
7432
|
+
}
|
|
7433
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md") || !isDurableMemoryPath(path)) {
|
|
7434
|
+
continue;
|
|
7435
|
+
}
|
|
7436
|
+
const content = await readTextIfExists(path);
|
|
7437
|
+
if (content) {
|
|
7438
|
+
const trimmedContent = content.trim();
|
|
7439
|
+
hashes.add(sha256(trimmedContent));
|
|
7440
|
+
hashes.add(sha256(comparableMemoryText(trimmedContent)));
|
|
7441
|
+
}
|
|
7442
|
+
}
|
|
7443
|
+
}
|
|
7444
|
+
function isDurableMemoryPath(path) {
|
|
7445
|
+
return path.split(import_node_path4.sep).includes("memories");
|
|
7446
|
+
}
|
|
7447
|
+
async function childDirectoryNames(path) {
|
|
7448
|
+
let entries;
|
|
7449
|
+
try {
|
|
7450
|
+
entries = await (0, import_promises4.readdir)(path, { withFileTypes: true });
|
|
7451
|
+
} catch (_err) {
|
|
7452
|
+
return [];
|
|
7453
|
+
}
|
|
7454
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
7455
|
+
}
|
|
7456
|
+
async function readTextIfExists(path) {
|
|
7457
|
+
try {
|
|
7458
|
+
const pathStat = await (0, import_promises4.stat)(path);
|
|
7459
|
+
if (!pathStat.isFile()) {
|
|
7460
|
+
return void 0;
|
|
7461
|
+
}
|
|
7462
|
+
return await (0, import_promises4.readFile)(path, "utf8");
|
|
7463
|
+
} catch (_err) {
|
|
7464
|
+
return void 0;
|
|
7465
|
+
}
|
|
7466
|
+
}
|
|
7467
|
+
function sensitiveMemoryReason(text) {
|
|
7468
|
+
const patterns = [
|
|
7469
|
+
{ name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
7470
|
+
{ name: "API key", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
|
|
7471
|
+
{ name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
|
|
7472
|
+
{ name: "bearer token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/i },
|
|
7473
|
+
{ name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ }
|
|
7474
|
+
];
|
|
7475
|
+
return patterns.find((pattern) => pattern.regex.test(text))?.name;
|
|
7476
|
+
}
|
|
7477
|
+
function comparableMemoryText(text) {
|
|
7478
|
+
const trimmed = text.trim();
|
|
7479
|
+
if (!trimmed.startsWith("MEMORY\n")) {
|
|
7480
|
+
return trimmed;
|
|
7481
|
+
}
|
|
7482
|
+
const separatorIndex = trimmed.indexOf("\n\n");
|
|
7483
|
+
return separatorIndex === -1 ? trimmed : trimmed.slice(separatorIndex + 2).trim();
|
|
7484
|
+
}
|
|
7485
|
+
function legacySourceLabel(candidate) {
|
|
7486
|
+
return `${candidate.sourceAccount}/${candidate.sourceSession}/${candidate.sourceArchive}`;
|
|
7487
|
+
}
|
|
7488
|
+
function localVikingDataRoot(config) {
|
|
7489
|
+
return (0, import_node_path4.join)(config.agentContextHome, "data", "viking");
|
|
7490
|
+
}
|
|
7491
|
+
function uniqueStrings(values) {
|
|
7492
|
+
return [...new Set(values)].sort();
|
|
7493
|
+
}
|
|
7494
|
+
function isResourceBusy(stderr, stdout) {
|
|
7495
|
+
return `${stderr}
|
|
7496
|
+
${stdout}`.includes("resource is busy");
|
|
7097
7497
|
}
|
|
7098
7498
|
async function buildHandoff(options) {
|
|
7099
7499
|
const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]) ?? getInvocationCwd();
|
|
@@ -8284,25 +8684,289 @@ function parsePackageManager(value) {
|
|
|
8284
8684
|
throw new Error(`Invalid package manager: ${value}`);
|
|
8285
8685
|
}
|
|
8286
8686
|
|
|
8687
|
+
// src/update.ts
|
|
8688
|
+
var import_node_fs4 = require("node:fs");
|
|
8689
|
+
var import_promises7 = require("node:fs/promises");
|
|
8690
|
+
var import_node_os5 = require("node:os");
|
|
8691
|
+
var import_node_path7 = require("node:path");
|
|
8692
|
+
var NPM_PACKAGE_NAME = "threadnote";
|
|
8693
|
+
var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
8694
|
+
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
8695
|
+
function parseUpdateRuntime(value) {
|
|
8696
|
+
if (value === "auto" || value === "npm" || value === "bun" || value === "deno") {
|
|
8697
|
+
return value;
|
|
8698
|
+
}
|
|
8699
|
+
throw new Error(`Invalid update runtime: ${value}. Expected auto, npm, bun, or deno.`);
|
|
8700
|
+
}
|
|
8701
|
+
async function maybeNotifyUpdate(config, options = {}) {
|
|
8702
|
+
if (isUpdateNotificationDisabled()) {
|
|
8703
|
+
return;
|
|
8704
|
+
}
|
|
8705
|
+
try {
|
|
8706
|
+
const info = await getUpdateInfo(config, {
|
|
8707
|
+
allowCacheWrite: options.dryRun !== true,
|
|
8708
|
+
preferFresh: false,
|
|
8709
|
+
registry: updateRegistry()
|
|
8710
|
+
});
|
|
8711
|
+
if (!info.isUpdateAvailable) {
|
|
8712
|
+
return;
|
|
8713
|
+
}
|
|
8714
|
+
console.log("");
|
|
8715
|
+
console.log(`Update available: threadnote ${info.currentVersion} -> ${info.latestVersion}`);
|
|
8716
|
+
console.log("Run: threadnote update");
|
|
8717
|
+
} catch (_err) {
|
|
8718
|
+
return;
|
|
8719
|
+
}
|
|
8720
|
+
}
|
|
8721
|
+
async function runUpdate(config, options) {
|
|
8722
|
+
const registry = normalizeRegistry(options.registry ?? updateRegistry());
|
|
8723
|
+
const info = await getUpdateInfo(config, {
|
|
8724
|
+
allowCacheWrite: options.dryRun !== true,
|
|
8725
|
+
preferFresh: true,
|
|
8726
|
+
registry
|
|
8727
|
+
});
|
|
8728
|
+
console.log(`Current version: ${info.currentVersion}`);
|
|
8729
|
+
console.log(`Latest version: ${info.latestVersion}`);
|
|
8730
|
+
console.log(`Registry: ${info.registry}`);
|
|
8731
|
+
if (options.check === true) {
|
|
8732
|
+
if (info.isUpdateAvailable) {
|
|
8733
|
+
console.log(`Update available. Run: threadnote update`);
|
|
8734
|
+
} else {
|
|
8735
|
+
console.log(
|
|
8736
|
+
compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
|
|
8737
|
+
);
|
|
8738
|
+
}
|
|
8739
|
+
return;
|
|
8740
|
+
}
|
|
8741
|
+
if (!info.isUpdateAvailable && options.force !== true) {
|
|
8742
|
+
console.log("Threadnote is up to date.");
|
|
8743
|
+
return;
|
|
8744
|
+
}
|
|
8745
|
+
const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
|
|
8746
|
+
const updateCommand = updatePackageCommand(runtime, registry);
|
|
8747
|
+
await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
|
|
8748
|
+
if (options.repair === false) {
|
|
8749
|
+
console.log("Skipping repair because --no-repair was provided.");
|
|
8750
|
+
return;
|
|
8751
|
+
}
|
|
8752
|
+
const threadnoteCommand = await installedThreadnoteCommand(runtime);
|
|
8753
|
+
await maybeRun(options.dryRun === true, threadnoteCommand, ["repair"]);
|
|
8754
|
+
console.log("Update complete. Restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.");
|
|
8755
|
+
}
|
|
8756
|
+
async function getUpdateInfo(config, options) {
|
|
8757
|
+
const currentVersion = await currentPackageVersion();
|
|
8758
|
+
const cached = options.preferFresh ? void 0 : await readFreshCache(config, options.registry);
|
|
8759
|
+
const latestVersion = cached?.latestVersion ?? await fetchLatestVersion(options.registry);
|
|
8760
|
+
if (!cached && options.allowCacheWrite) {
|
|
8761
|
+
await writeUpdateCache(config, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion, registry: options.registry });
|
|
8762
|
+
}
|
|
8763
|
+
return {
|
|
8764
|
+
currentVersion,
|
|
8765
|
+
isUpdateAvailable: compareVersions(currentVersion, latestVersion) < 0,
|
|
8766
|
+
latestVersion,
|
|
8767
|
+
registry: options.registry
|
|
8768
|
+
};
|
|
8769
|
+
}
|
|
8770
|
+
async function currentPackageVersion() {
|
|
8771
|
+
const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
|
|
8772
|
+
const parsed = JSON.parse(rawPackage);
|
|
8773
|
+
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8774
|
+
throw new Error("Could not read current threadnote package version.");
|
|
8775
|
+
}
|
|
8776
|
+
return parsed.version;
|
|
8777
|
+
}
|
|
8778
|
+
async function fetchLatestVersion(registry) {
|
|
8779
|
+
const url = new URL(`${NPM_PACKAGE_NAME}/latest`, normalizeRegistry(registry));
|
|
8780
|
+
const controller = new AbortController();
|
|
8781
|
+
const timeout = setTimeout(() => {
|
|
8782
|
+
controller.abort();
|
|
8783
|
+
}, 2500);
|
|
8784
|
+
try {
|
|
8785
|
+
const response = await fetch(url, { headers: { accept: "application/json" }, signal: controller.signal });
|
|
8786
|
+
if (!response.ok) {
|
|
8787
|
+
throw new Error(`npm registry returned HTTP ${response.status}`);
|
|
8788
|
+
}
|
|
8789
|
+
const parsed = await response.json();
|
|
8790
|
+
if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
|
|
8791
|
+
throw new Error("npm registry response did not include a version.");
|
|
8792
|
+
}
|
|
8793
|
+
return parsed.version;
|
|
8794
|
+
} catch (err) {
|
|
8795
|
+
throw new Error(`Could not check npm for updates: ${errorMessage(err)}`, { cause: err });
|
|
8796
|
+
} finally {
|
|
8797
|
+
clearTimeout(timeout);
|
|
8798
|
+
}
|
|
8799
|
+
}
|
|
8800
|
+
async function readFreshCache(config, registry) {
|
|
8801
|
+
const rawCache = await readFileIfExists(updateCachePath(config));
|
|
8802
|
+
if (!rawCache) {
|
|
8803
|
+
return void 0;
|
|
8804
|
+
}
|
|
8805
|
+
try {
|
|
8806
|
+
const parsed = JSON.parse(rawCache);
|
|
8807
|
+
if (!isJsonObject(parsed) || typeof parsed.checkedAt !== "string" || typeof parsed.latestVersion !== "string" || parsed.registry !== registry) {
|
|
8808
|
+
return void 0;
|
|
8809
|
+
}
|
|
8810
|
+
const checkedAt = Date.parse(parsed.checkedAt);
|
|
8811
|
+
if (!Number.isFinite(checkedAt) || Date.now() - checkedAt > UPDATE_CHECK_TTL_MS) {
|
|
8812
|
+
return void 0;
|
|
8813
|
+
}
|
|
8814
|
+
return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, registry };
|
|
8815
|
+
} catch (_err) {
|
|
8816
|
+
return void 0;
|
|
8817
|
+
}
|
|
8818
|
+
}
|
|
8819
|
+
async function writeUpdateCache(config, cache) {
|
|
8820
|
+
await ensureDirectory(config.agentContextHome, false);
|
|
8821
|
+
await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
|
|
8822
|
+
`, { encoding: "utf8", mode: 384 });
|
|
8823
|
+
}
|
|
8824
|
+
function updateCachePath(config) {
|
|
8825
|
+
return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
|
|
8826
|
+
}
|
|
8827
|
+
async function resolveUpdateRuntime(runtime) {
|
|
8828
|
+
if (runtime !== "auto") {
|
|
8829
|
+
await requireRuntime(runtime);
|
|
8830
|
+
return runtime;
|
|
8831
|
+
}
|
|
8832
|
+
for (const candidate of ["npm", "bun", "deno"]) {
|
|
8833
|
+
if (await findExecutable([candidate])) {
|
|
8834
|
+
return candidate;
|
|
8835
|
+
}
|
|
8836
|
+
}
|
|
8837
|
+
throw new Error("Install Node/npm, Bun, or Deno to update threadnote.");
|
|
8838
|
+
}
|
|
8839
|
+
async function requireRuntime(runtime) {
|
|
8840
|
+
if (!await findExecutable([runtime])) {
|
|
8841
|
+
throw new Error(`${runtime} was requested but was not found on PATH.`);
|
|
8842
|
+
}
|
|
8843
|
+
}
|
|
8844
|
+
async function installedThreadnoteCommand(runtime) {
|
|
8845
|
+
const runtimeBin = await runtimeThreadnoteBin(runtime);
|
|
8846
|
+
if (runtimeBin && await isExecutable(runtimeBin)) {
|
|
8847
|
+
return runtimeBin;
|
|
8848
|
+
}
|
|
8849
|
+
return await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
|
|
8850
|
+
}
|
|
8851
|
+
async function runtimeThreadnoteBin(runtime) {
|
|
8852
|
+
if (runtime === "npm") {
|
|
8853
|
+
const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
|
|
8854
|
+
const prefix = result.stdout.trim();
|
|
8855
|
+
return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
|
|
8856
|
+
}
|
|
8857
|
+
if (runtime === "bun") {
|
|
8858
|
+
const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
|
|
8859
|
+
const binDir = result.stdout.trim();
|
|
8860
|
+
return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
|
|
8861
|
+
}
|
|
8862
|
+
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);
|
|
8863
|
+
}
|
|
8864
|
+
async function isExecutable(path) {
|
|
8865
|
+
try {
|
|
8866
|
+
await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
|
|
8867
|
+
return true;
|
|
8868
|
+
} catch (_err) {
|
|
8869
|
+
return false;
|
|
8870
|
+
}
|
|
8871
|
+
}
|
|
8872
|
+
function updatePackageCommand(runtime, registry) {
|
|
8873
|
+
if (runtime === "npm") {
|
|
8874
|
+
return { executable: "npm", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
|
|
8875
|
+
}
|
|
8876
|
+
if (runtime === "bun") {
|
|
8877
|
+
return { executable: "bun", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
|
|
8878
|
+
}
|
|
8879
|
+
return {
|
|
8880
|
+
executable: "env",
|
|
8881
|
+
args: [
|
|
8882
|
+
`NPM_CONFIG_REGISTRY=${registry}`,
|
|
8883
|
+
"deno",
|
|
8884
|
+
"install",
|
|
8885
|
+
"--global",
|
|
8886
|
+
"--force",
|
|
8887
|
+
"--name",
|
|
8888
|
+
NPM_PACKAGE_NAME,
|
|
8889
|
+
"--allow-read",
|
|
8890
|
+
"--allow-write",
|
|
8891
|
+
"--allow-run",
|
|
8892
|
+
"--allow-env",
|
|
8893
|
+
"--allow-net",
|
|
8894
|
+
`npm:${NPM_PACKAGE_NAME}@latest`
|
|
8895
|
+
]
|
|
8896
|
+
};
|
|
8897
|
+
}
|
|
8898
|
+
function normalizeRegistry(registry) {
|
|
8899
|
+
return registry.endsWith("/") ? registry : `${registry}/`;
|
|
8900
|
+
}
|
|
8901
|
+
function updateRegistry() {
|
|
8902
|
+
return normalizeRegistry(process.env.THREADNOTE_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY);
|
|
8903
|
+
}
|
|
8904
|
+
function isUpdateNotificationDisabled() {
|
|
8905
|
+
return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
|
|
8906
|
+
}
|
|
8907
|
+
function compareVersions(currentVersion, latestVersion) {
|
|
8908
|
+
const current = parseVersion(currentVersion);
|
|
8909
|
+
const latest = parseVersion(latestVersion);
|
|
8910
|
+
for (let index = 0; index < 3; index += 1) {
|
|
8911
|
+
const difference = current.numbers[index] - latest.numbers[index];
|
|
8912
|
+
if (difference !== 0) {
|
|
8913
|
+
return difference;
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
if (current.prerelease === latest.prerelease) {
|
|
8917
|
+
return 0;
|
|
8918
|
+
}
|
|
8919
|
+
if (current.prerelease === void 0) {
|
|
8920
|
+
return 1;
|
|
8921
|
+
}
|
|
8922
|
+
if (latest.prerelease === void 0) {
|
|
8923
|
+
return -1;
|
|
8924
|
+
}
|
|
8925
|
+
return current.prerelease.localeCompare(latest.prerelease);
|
|
8926
|
+
}
|
|
8927
|
+
function parseVersion(version) {
|
|
8928
|
+
const normalized = version.trim().replace(/^v/, "");
|
|
8929
|
+
const [core2, prerelease] = normalized.split("-", 2);
|
|
8930
|
+
const parts = core2.split(".").map((part) => Number(part));
|
|
8931
|
+
return {
|
|
8932
|
+
numbers: [safeVersionNumber(parts[0]), safeVersionNumber(parts[1]), safeVersionNumber(parts[2])],
|
|
8933
|
+
prerelease
|
|
8934
|
+
};
|
|
8935
|
+
}
|
|
8936
|
+
function safeVersionNumber(value) {
|
|
8937
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
8938
|
+
}
|
|
8939
|
+
|
|
8287
8940
|
// src/threadnote.ts
|
|
8288
8941
|
async function main() {
|
|
8289
8942
|
const program2 = new Command();
|
|
8290
8943
|
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
8944
|
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
|
-
|
|
8945
|
+
const config = getRuntimeConfig(program2);
|
|
8946
|
+
await runDoctor(config, options);
|
|
8947
|
+
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
8293
8948
|
});
|
|
8294
8949
|
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
|
-
|
|
8950
|
+
const config = getRuntimeConfig(program2);
|
|
8951
|
+
await runInstall(config, options);
|
|
8952
|
+
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
8953
|
+
});
|
|
8954
|
+
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) => {
|
|
8955
|
+
await runUpdate(getRuntimeConfig(program2), options);
|
|
8296
8956
|
});
|
|
8297
8957
|
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
8958
|
"--mcp <clients>",
|
|
8299
8959
|
"MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
|
|
8300
8960
|
"available"
|
|
8301
8961
|
).option("--no-start", "Do not start OpenViking if health is failing").option("--package-manager <manager>", "pipx, uv, or pip", parsePackageManager).action(async (options) => {
|
|
8302
|
-
|
|
8962
|
+
const config = getRuntimeConfig(program2);
|
|
8963
|
+
await runRepair(config, options);
|
|
8964
|
+
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
8303
8965
|
});
|
|
8304
8966
|
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
|
-
|
|
8967
|
+
const config = getRuntimeConfig(program2);
|
|
8968
|
+
await runStart(config, options);
|
|
8969
|
+
await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
|
|
8306
8970
|
});
|
|
8307
8971
|
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
8972
|
await runStop(getRuntimeConfig(program2), options);
|
|
@@ -8329,6 +8993,14 @@ async function main() {
|
|
|
8329
8993
|
program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").action(async (options) => {
|
|
8330
8994
|
await runRemember(getRuntimeConfig(program2), options);
|
|
8331
8995
|
});
|
|
8996
|
+
program2.command("migrate-memories").description("Migrate legacy session-only Threadnote memories into durable memory files").option("--all-accounts", "Scan all local OpenViking accounts under THREADNOTE_HOME").option("--dry-run", "Print migration actions without writing memories").option("--limit <count>", "Maximum number of memories to migrate").option(
|
|
8997
|
+
"--source-account <account>",
|
|
8998
|
+
"Source OpenViking account to scan; repeat for multiple accounts",
|
|
8999
|
+
collectOption,
|
|
9000
|
+
[]
|
|
9001
|
+
).action(async (options) => {
|
|
9002
|
+
await runMigrateMemories(getRuntimeConfig(program2), options);
|
|
9003
|
+
});
|
|
8332
9004
|
program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
|
|
8333
9005
|
await runRecall(getRuntimeConfig(program2), options);
|
|
8334
9006
|
});
|