threadnote 0.3.2 → 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 +2 -0
- package/dist/threadnote.cjs +239 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,6 +187,8 @@ This is it! Start working with your agents as usual. The agent will automaticall
|
|
|
187
187
|
`seed-skills --native` only after configuring a working VLM provider.
|
|
188
188
|
- `mcp-install codex|claude|cursor`: installs or prints OpenViking MCP configuration for Codex, Claude, or Cursor.
|
|
189
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.
|
|
190
192
|
- `recall`: searches shared OpenViking context. It infers repo or skill scope from queries like
|
|
191
193
|
`skills for api service`; use `--uri` or `--no-infer-scope` to override.
|
|
192
194
|
- `read`: reads a `viking://` URI returned by `recall` or `list`.
|
package/dist/threadnote.cjs
CHANGED
|
@@ -7047,6 +7047,75 @@ async function runRemember(config, options) {
|
|
|
7047
7047
|
].join("\n");
|
|
7048
7048
|
await storeMemory(config, memory, options.dryRun === true);
|
|
7049
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
|
+
}
|
|
7050
7119
|
async function runRecall(config, options) {
|
|
7051
7120
|
const ov = await openVikingCliForMode(options.dryRun === true);
|
|
7052
7121
|
const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, options.query));
|
|
@@ -7238,13 +7307,13 @@ async function waitForOpenVikingQueue(ov, config) {
|
|
|
7238
7307
|
}
|
|
7239
7308
|
}
|
|
7240
7309
|
async function vikingResourceExists(ov, config, uri) {
|
|
7241
|
-
const
|
|
7242
|
-
return
|
|
7310
|
+
const stat3 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
|
|
7311
|
+
return stat3.exitCode === 0;
|
|
7243
7312
|
}
|
|
7244
7313
|
async function ensureDurableMemoryDirectory(ov, config) {
|
|
7245
7314
|
const directoryUri = durableMemoryDirectoryUri(config);
|
|
7246
|
-
const
|
|
7247
|
-
if (
|
|
7315
|
+
const stat3 = await runCommand(ov, withIdentity(config, ["stat", directoryUri]), { allowFailure: true });
|
|
7316
|
+
if (stat3.exitCode === 0) {
|
|
7248
7317
|
return;
|
|
7249
7318
|
}
|
|
7250
7319
|
await maybeRun(
|
|
@@ -7264,6 +7333,164 @@ function durableMemoryUri(config, memory) {
|
|
|
7264
7333
|
function durableMemoryDirectoryUri(config) {
|
|
7265
7334
|
return `viking://user/${uriSegment(config.user)}/memories/events`;
|
|
7266
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
|
+
}
|
|
7267
7494
|
function isResourceBusy(stderr, stdout) {
|
|
7268
7495
|
return `${stderr}
|
|
7269
7496
|
${stdout}`.includes("resource is busy");
|
|
@@ -8766,6 +8993,14 @@ async function main() {
|
|
|
8766
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) => {
|
|
8767
8994
|
await runRemember(getRuntimeConfig(program2), options);
|
|
8768
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
|
+
});
|
|
8769
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) => {
|
|
8770
9005
|
await runRecall(getRuntimeConfig(program2), options);
|
|
8771
9006
|
});
|