threadnote 0.3.5 → 0.3.7

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.
@@ -552,13 +552,13 @@ var require_help = __commonJS({
552
552
  function callFormatItem(term, description) {
553
553
  return helper.formatItem(term, termWidth, description, helper);
554
554
  }
555
- let output = [
555
+ let output2 = [
556
556
  `${helper.styleTitle("Usage:")} ${helper.styleUsage(helper.commandUsage(cmd))}`,
557
557
  ""
558
558
  ];
559
559
  const commandDescription = helper.commandDescription(cmd);
560
560
  if (commandDescription.length > 0) {
561
- output = output.concat([
561
+ output2 = output2.concat([
562
562
  helper.boxWrap(
563
563
  helper.styleCommandDescription(commandDescription),
564
564
  helpWidth
@@ -572,7 +572,7 @@ var require_help = __commonJS({
572
572
  helper.styleArgumentDescription(helper.argumentDescription(argument))
573
573
  );
574
574
  });
575
- output = output.concat(
575
+ output2 = output2.concat(
576
576
  this.formatItemList("Arguments:", argumentList, helper)
577
577
  );
578
578
  const optionGroups = this.groupItems(
@@ -587,7 +587,7 @@ var require_help = __commonJS({
587
587
  helper.styleOptionDescription(helper.optionDescription(option))
588
588
  );
589
589
  });
590
- output = output.concat(this.formatItemList(group, optionList, helper));
590
+ output2 = output2.concat(this.formatItemList(group, optionList, helper));
591
591
  });
592
592
  if (helper.showGlobalOptions) {
593
593
  const globalOptionList = helper.visibleGlobalOptions(cmd).map((option) => {
@@ -596,7 +596,7 @@ var require_help = __commonJS({
596
596
  helper.styleOptionDescription(helper.optionDescription(option))
597
597
  );
598
598
  });
599
- output = output.concat(
599
+ output2 = output2.concat(
600
600
  this.formatItemList("Global Options:", globalOptionList, helper)
601
601
  );
602
602
  }
@@ -612,9 +612,9 @@ var require_help = __commonJS({
612
612
  helper.styleSubcommandDescription(helper.subcommandDescription(sub))
613
613
  );
614
614
  });
615
- output = output.concat(this.formatItemList(group, commandList, helper));
615
+ output2 = output2.concat(this.formatItemList(group, commandList, helper));
616
616
  });
617
- return output.join("\n");
617
+ return output2.join("\n");
618
618
  }
619
619
  /**
620
620
  * Return display width of string, ignoring ANSI escape sequences. Used in padding and wrapping calculations.
@@ -3564,7 +3564,7 @@ async function walkFiles(root) {
3564
3564
  return files;
3565
3565
  }
3566
3566
  function globToRegExp(glob) {
3567
- let output = "^";
3567
+ let output2 = "^";
3568
3568
  for (let index = 0; index < glob.length; index += 1) {
3569
3569
  const char = glob[index];
3570
3570
  if (char === "*") {
@@ -3572,23 +3572,23 @@ function globToRegExp(glob) {
3572
3572
  if (next === "*") {
3573
3573
  const afterNext = glob[index + 2];
3574
3574
  if (afterNext === "/") {
3575
- output += "(?:.*/)?";
3575
+ output2 += "(?:.*/)?";
3576
3576
  index += 2;
3577
3577
  } else {
3578
- output += ".*";
3578
+ output2 += ".*";
3579
3579
  index += 1;
3580
3580
  }
3581
3581
  } else {
3582
- output += "[^/]*";
3582
+ output2 += "[^/]*";
3583
3583
  }
3584
3584
  } else if (char === "?") {
3585
- output += "[^/]";
3585
+ output2 += "[^/]";
3586
3586
  } else {
3587
- output += escapeRegExp(char);
3587
+ output2 += escapeRegExp(char);
3588
3588
  }
3589
3589
  }
3590
- output += "$";
3591
- return new RegExp(output);
3590
+ output2 += "$";
3591
+ return new RegExp(output2);
3592
3592
  }
3593
3593
  function getGlobBase(pattern) {
3594
3594
  const parts = pattern.split("/");
@@ -3933,8 +3933,8 @@ function exactRecallTerms(query) {
3933
3933
  }
3934
3934
  return terms.sort((left, right) => exactRecallTermScore(right) - exactRecallTermScore(left)).slice(0, 4);
3935
3935
  }
3936
- function grepOutputHasMatches(output) {
3937
- return !output.includes("matches []") && !output.includes('"matches":[]') && !output.includes("match_count 0");
3936
+ function grepOutputHasMatches(output2) {
3937
+ return !output2.includes("matches []") && !output2.includes('"matches":[]') && !output2.includes("match_count 0");
3938
3938
  }
3939
3939
  function exactRecallTermScore(term) {
3940
3940
  let score = term.length;
@@ -4991,14 +4991,14 @@ function resolveYamlBinary(data) {
4991
4991
  return bitlen % 8 === 0;
4992
4992
  }
4993
4993
  function constructYamlBinary(data) {
4994
- var idx, tailbits, input = data.replace(/[\r\n=]/g, ""), max = input.length, map2 = BASE64_MAP, bits = 0, result = [];
4994
+ var idx, tailbits, input2 = data.replace(/[\r\n=]/g, ""), max = input2.length, map2 = BASE64_MAP, bits = 0, result = [];
4995
4995
  for (idx = 0; idx < max; idx++) {
4996
4996
  if (idx % 4 === 0 && idx) {
4997
4997
  result.push(bits >> 16 & 255);
4998
4998
  result.push(bits >> 8 & 255);
4999
4999
  result.push(bits & 255);
5000
5000
  }
5001
- bits = bits << 6 | map2.indexOf(input.charAt(idx));
5001
+ bits = bits << 6 | map2.indexOf(input2.charAt(idx));
5002
5002
  }
5003
5003
  tailbits = max % 4 * 6;
5004
5004
  if (tailbits === 0) {
@@ -5231,8 +5231,8 @@ for (i = 0; i < 256; i++) {
5231
5231
  simpleEscapeMap[i] = simpleEscapeSequence(i);
5232
5232
  }
5233
5233
  var i;
5234
- function State$1(input, options) {
5235
- this.input = input;
5234
+ function State$1(input2, options) {
5235
+ this.input = input2;
5236
5236
  this.filename = options["filename"] || null;
5237
5237
  this.schema = options["schema"] || _default;
5238
5238
  this.onWarning = options["onWarning"] || null;
@@ -5241,7 +5241,7 @@ function State$1(input, options) {
5241
5241
  this.listener = options["listener"] || null;
5242
5242
  this.implicitTypes = this.schema.compiledImplicit;
5243
5243
  this.typeMap = this.schema.compiledTypeMap;
5244
- this.length = input.length;
5244
+ this.length = input2.length;
5245
5245
  this.position = 0;
5246
5246
  this.line = 0;
5247
5247
  this.lineStart = 0;
@@ -6245,19 +6245,19 @@ function readDocument(state) {
6245
6245
  return;
6246
6246
  }
6247
6247
  }
6248
- function loadDocuments(input, options) {
6249
- input = String(input);
6248
+ function loadDocuments(input2, options) {
6249
+ input2 = String(input2);
6250
6250
  options = options || {};
6251
- if (input.length !== 0) {
6252
- if (input.charCodeAt(input.length - 1) !== 10 && input.charCodeAt(input.length - 1) !== 13) {
6253
- input += "\n";
6251
+ if (input2.length !== 0) {
6252
+ if (input2.charCodeAt(input2.length - 1) !== 10 && input2.charCodeAt(input2.length - 1) !== 13) {
6253
+ input2 += "\n";
6254
6254
  }
6255
- if (input.charCodeAt(0) === 65279) {
6256
- input = input.slice(1);
6255
+ if (input2.charCodeAt(0) === 65279) {
6256
+ input2 = input2.slice(1);
6257
6257
  }
6258
6258
  }
6259
- var state = new State$1(input, options);
6260
- var nullpos = input.indexOf("\0");
6259
+ var state = new State$1(input2, options);
6260
+ var nullpos = input2.indexOf("\0");
6261
6261
  if (nullpos !== -1) {
6262
6262
  state.position = nullpos;
6263
6263
  throwError(state, "null byte is not allowed in input");
@@ -6272,12 +6272,12 @@ function loadDocuments(input, options) {
6272
6272
  }
6273
6273
  return state.documents;
6274
6274
  }
6275
- function loadAll$1(input, iterator, options) {
6275
+ function loadAll$1(input2, iterator, options) {
6276
6276
  if (iterator !== null && typeof iterator === "object" && typeof options === "undefined") {
6277
6277
  options = iterator;
6278
6278
  iterator = null;
6279
6279
  }
6280
- var documents = loadDocuments(input, options);
6280
+ var documents = loadDocuments(input2, options);
6281
6281
  if (typeof iterator !== "function") {
6282
6282
  return documents;
6283
6283
  }
@@ -6285,8 +6285,8 @@ function loadAll$1(input, iterator, options) {
6285
6285
  iterator(documents[index]);
6286
6286
  }
6287
6287
  }
6288
- function load$1(input, options) {
6289
- var documents = loadDocuments(input, options);
6288
+ function load$1(input2, options) {
6289
+ var documents = loadDocuments(input2, options);
6290
6290
  if (documents.length === 0) {
6291
6291
  return void 0;
6292
6292
  } else if (documents.length === 1) {
@@ -6907,11 +6907,11 @@ function inspectNode(object, objects, duplicatesIndexes) {
6907
6907
  }
6908
6908
  }
6909
6909
  }
6910
- function dump$1(input, options) {
6910
+ function dump$1(input2, options) {
6911
6911
  options = options || {};
6912
6912
  var state = new State(options);
6913
- if (!state.noRefs) getDuplicateReferences(input, state);
6914
- var value = input;
6913
+ if (!state.noRefs) getDuplicateReferences(input2, state);
6914
+ var value = input2;
6915
6915
  if (state.replacer) {
6916
6916
  value = state.replacer.call({ "": value }, "", value);
6917
6917
  }
@@ -7033,19 +7033,34 @@ function readStringArray(object, key) {
7033
7033
  }
7034
7034
 
7035
7035
  // src/memory.ts
7036
+ function parseMemoryKind(value) {
7037
+ if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
7038
+ return value;
7039
+ }
7040
+ throw new Error(`Unsupported memory kind "${value}". Expected durable, handoff, incident, preference, or smoke.`);
7041
+ }
7042
+ function parseMemoryStatus(value) {
7043
+ if (["active", "archived", "superseded"].includes(value)) {
7044
+ return value;
7045
+ }
7046
+ throw new Error(`Unsupported memory status "${value}". Expected active, archived, or superseded.`);
7047
+ }
7036
7048
  async function runRemember(config, options) {
7037
7049
  const text = await getInputText(options.text, options.stdin === true);
7038
7050
  if (!text.trim()) {
7039
7051
  throw new Error("Provide memory text with --text or --stdin.");
7040
7052
  }
7041
- const memory = [
7042
- "MEMORY",
7043
- `source_agent_client: ${options.sourceAgentClient ?? "codex"}`,
7044
- `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`,
7045
- "",
7046
- text.trim()
7047
- ].join("\n");
7048
- await storeMemory(config, memory, options.dryRun === true);
7053
+ const metadata = {
7054
+ kind: options.kind ?? "durable",
7055
+ project: normalizeOptionalMetadata(options.project),
7056
+ sourceAgentClient: options.sourceAgentClient ?? "codex",
7057
+ status: options.status ?? "active",
7058
+ supersedes: options.replace,
7059
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7060
+ topic: normalizeOptionalMetadata(options.topic)
7061
+ };
7062
+ const memory = formatMemoryDocument("MEMORY", metadata, text.trim());
7063
+ await storeMemory(config, memory, { dryRun: options.dryRun === true, metadata, replaceUri: options.replace });
7049
7064
  }
7050
7065
  async function runMigrateMemories(config, options) {
7051
7066
  const dryRun = options.dryRun === true;
@@ -7096,7 +7111,7 @@ async function runMigrateMemories(config, options) {
7096
7111
  if (!dryRun) {
7097
7112
  await (0, import_promises4.writeFile)(migrationPath, candidate.text, { encoding: "utf8", mode: 384 });
7098
7113
  await (0, import_promises4.chmod)(migrationPath, 384);
7099
- await writeDurableMemoryFile(ov, config, memoryUri, migrationPath);
7114
+ await writeDurableMemoryFile(ov, config, memoryUri, migrationPath, "create");
7100
7115
  existingHashes.add(candidate.hash);
7101
7116
  }
7102
7117
  migratedCount += 1;
@@ -7116,6 +7131,62 @@ async function runMigrateMemories(config, options) {
7116
7131
  ].join("; ")
7117
7132
  );
7118
7133
  }
7134
+ async function runMigrateLifecycle(config, options) {
7135
+ const dryRun = options.dryRun === true || options.apply !== true;
7136
+ const limit = options.limit ? parsePositiveInteger(options.limit, "lifecycle migration limit") : void 0;
7137
+ const ov = await openVikingCliForMode(dryRun);
7138
+ const candidates = await legacyLifecycleHandoffCandidates(config);
7139
+ const migrationPath = (0, import_node_path4.join)(config.agentContextHome, "lifecycle-memory-migration.txt");
7140
+ let existingCount = 0;
7141
+ let migratedCount = 0;
7142
+ let skippedCount = 0;
7143
+ try {
7144
+ for (const candidate of candidates) {
7145
+ if (limit !== void 0 && migratedCount >= limit) {
7146
+ break;
7147
+ }
7148
+ const destinationUri = lifecycleMigrationUri(config, candidate.metadata, sha256(candidate.original.trim()));
7149
+ const migratedMemory = formatMemoryDocument(
7150
+ "HANDOFF",
7151
+ candidate.metadata,
7152
+ ["Migrated legacy handoff from the historical events trail.", "", candidate.original.trim()].join("\n")
7153
+ );
7154
+ console.log(`${dryRun ? "Would migrate" : "Migrating"} ${candidate.sourceUri} -> ${destinationUri}`);
7155
+ if (!dryRun) {
7156
+ if (await vikingResourceExists(ov, config, destinationUri)) {
7157
+ existingCount += 1;
7158
+ console.log(`Archived copy already exists; cleaning up legacy source: ${candidate.sourceUri}`);
7159
+ } else {
7160
+ await (0, import_promises4.writeFile)(migrationPath, migratedMemory, { encoding: "utf8", mode: 384 });
7161
+ await (0, import_promises4.chmod)(migrationPath, 384);
7162
+ await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, candidate.metadata));
7163
+ await writeDurableMemoryFile(ov, config, destinationUri, migrationPath, "create");
7164
+ }
7165
+ const removedOriginal = await removeVikingResourceWithRetry(ov, config, candidate.sourceUri);
7166
+ if (!removedOriginal) {
7167
+ console.error(
7168
+ `Migrated copy stored, but original is still processing. Retry later: threadnote forget ${candidate.sourceUri}`
7169
+ );
7170
+ skippedCount += 1;
7171
+ }
7172
+ }
7173
+ migratedCount += 1;
7174
+ }
7175
+ } finally {
7176
+ if (!dryRun) {
7177
+ await (0, import_promises4.rm)(migrationPath, { force: true });
7178
+ }
7179
+ }
7180
+ console.log(
7181
+ [
7182
+ `Lifecycle migration summary: ${migratedCount} clear legacy handoff(s) ${dryRun ? "would be migrated" : "migrated"}`,
7183
+ `${existingCount} existing archived copy/copies reused`,
7184
+ `${skippedCount} legacy source(s) still processing`,
7185
+ `${candidates.length} clear legacy handoff candidate(s) found`,
7186
+ dryRun ? "Run with --apply to perform this migration." : void 0
7187
+ ].filter((part) => part !== void 0).join("; ")
7188
+ );
7189
+ }
7119
7190
  async function runRecall(config, options) {
7120
7191
  const ov = await openVikingCliForMode(options.dryRun === true);
7121
7192
  const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, options.query));
@@ -7128,7 +7199,10 @@ async function runRecall(config, options) {
7128
7199
  args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
7129
7200
  }
7130
7201
  await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
7131
- await printExactMemoryMatches(config, ov, options.query, options.dryRun === true);
7202
+ await printExactMemoryMatches(config, ov, options.query, {
7203
+ dryRun: options.dryRun === true,
7204
+ includeArchived: options.includeArchived === true
7205
+ });
7132
7206
  }
7133
7207
  async function runRead(config, uri, options) {
7134
7208
  assertVikingUri(uri);
@@ -7159,13 +7233,70 @@ async function runList(config, uri, options) {
7159
7233
  await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
7160
7234
  }
7161
7235
  async function runHandoff(config, options) {
7162
- const handoff = await buildHandoff(options);
7163
- await storeMemory(config, handoff, options.dryRun === true);
7236
+ const { handoff, metadata } = await buildHandoff(options);
7237
+ await storeMemory(config, handoff, { dryRun: options.dryRun === true, metadata, replaceUri: options.replace });
7238
+ }
7239
+ async function runArchive(config, uri, options) {
7240
+ assertVikingUri(uri);
7241
+ const ov = await openVikingCliForMode(options.dryRun === true);
7242
+ const readResult = await maybeRun(options.dryRun === true, ov, withIdentity(config, ["read", uri]));
7243
+ const original = readResult?.stdout.trim();
7244
+ if (options.dryRun === true) {
7245
+ const fallbackMetadata = {
7246
+ archivedFrom: uri,
7247
+ kind: options.kind ?? "handoff",
7248
+ project: normalizeOptionalMetadata(options.project),
7249
+ sourceAgentClient: "threadnote",
7250
+ status: "archived",
7251
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7252
+ topic: normalizeOptionalMetadata(options.topic)
7253
+ };
7254
+ const archiveMemory2 = formatMemoryDocument(
7255
+ "MEMORY",
7256
+ fallbackMetadata,
7257
+ ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n")
7258
+ );
7259
+ await storeMemory(config, archiveMemory2, { dryRun: true, metadata: fallbackMetadata });
7260
+ console.log(formatShellCommand(ov, withIdentity(config, ["rm", uri])));
7261
+ return;
7262
+ }
7263
+ if (!original) {
7264
+ throw new Error(`Could not read ${uri} before archiving.`);
7265
+ }
7266
+ const inferredMetadata = inferMemoryMetadata(original);
7267
+ const metadata = {
7268
+ archivedFrom: uri,
7269
+ kind: options.kind ?? inferredMetadata.kind ?? "handoff",
7270
+ project: normalizeOptionalMetadata(options.project) ?? inferredMetadata.project,
7271
+ sourceAgentClient: "threadnote",
7272
+ status: "archived",
7273
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7274
+ topic: normalizeOptionalMetadata(options.topic) ?? inferredMetadata.topic
7275
+ };
7276
+ const archiveMemory = formatMemoryDocument(
7277
+ "MEMORY",
7278
+ metadata,
7279
+ ["Archived original Threadnote memory.", "", original].join("\n")
7280
+ );
7281
+ await storeMemory(config, archiveMemory, { dryRun: false, metadata });
7282
+ const removedOriginal = await removeVikingResourceWithRetry(ov, config, uri);
7283
+ if (removedOriginal) {
7284
+ console.log(`Archived original memory: ${uri}`);
7285
+ } else {
7286
+ console.error(`Archive stored, but original memory is still processing. Retry later: threadnote forget ${uri}`);
7287
+ }
7164
7288
  }
7165
7289
  async function runForget(config, uri, options) {
7166
7290
  assertVikingUri(uri);
7167
7291
  const ov = await openVikingCliForMode(options.dryRun === true);
7168
- await maybeRun(options.dryRun === true, ov, withIdentity(config, ["rm", uri]));
7292
+ if (options.dryRun === true) {
7293
+ await maybeRun(true, ov, withIdentity(config, ["rm", uri]));
7294
+ return;
7295
+ }
7296
+ const removed = await removeVikingResourceWithRetry(ov, config, uri);
7297
+ if (!removed) {
7298
+ throw new Error(`Resource is still being processed; retry later: threadnote forget ${uri}`);
7299
+ }
7169
7300
  }
7170
7301
  async function runExportPack(config, options) {
7171
7302
  const ov = await openVikingCliForMode(options.dryRun === true);
@@ -7201,27 +7332,24 @@ async function inferProjectFromQuery(config, normalizedQuery) {
7201
7332
  return void 0;
7202
7333
  }
7203
7334
  }
7204
- async function printExactMemoryMatches(config, ov, query, dryRun) {
7335
+ async function printExactMemoryMatches(config, ov, query, options) {
7205
7336
  const terms = exactRecallTerms(query);
7206
7337
  if (terms.length === 0) {
7207
7338
  return;
7208
7339
  }
7209
- const scopes = [
7210
- `viking://user/${uriSegment(config.user)}/memories`,
7211
- `viking://agent/${uriSegment(config.agentId)}/memories`
7212
- ];
7340
+ const scopes = exactMemoryScopes(config, options.includeArchived);
7213
7341
  const outputs = [];
7214
7342
  for (const term of terms) {
7215
7343
  for (const scope of scopes) {
7216
7344
  const args = withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5"]);
7217
- if (dryRun) {
7345
+ if (options.dryRun) {
7218
7346
  outputs.push(formatShellCommand(ov, args));
7219
7347
  continue;
7220
7348
  }
7221
7349
  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);
7350
+ const output2 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
7351
+ if (result.exitCode === 0 && grepOutputHasMatches(output2)) {
7352
+ outputs.push(output2);
7225
7353
  }
7226
7354
  }
7227
7355
  }
@@ -7231,11 +7359,15 @@ async function printExactMemoryMatches(config, ov, query, dryRun) {
7231
7359
  console.log("\nExact durable memory matches:");
7232
7360
  console.log(outputs.join("\n\n"));
7233
7361
  }
7234
- async function storeMemory(config, memory, dryRun) {
7235
- const ov = await openVikingCliForMode(dryRun);
7362
+ async function storeMemory(config, memory, options) {
7363
+ if (options.replaceUri) {
7364
+ assertVikingUri(options.replaceUri);
7365
+ }
7366
+ const ov = await openVikingCliForMode(options.dryRun);
7236
7367
  const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
7237
- const memoryUri = durableMemoryUri(config, memory);
7238
- if (dryRun) {
7368
+ const memoryUri = memoryUriFor(config, memory, options.metadata);
7369
+ const writeMode = await memoryWriteMode(ov, config, memoryUri, options.metadata);
7370
+ if (options.dryRun) {
7239
7371
  console.log(memory);
7240
7372
  console.log("\nWould run:");
7241
7373
  console.log(
@@ -7247,29 +7379,44 @@ async function storeMemory(config, memory, dryRun) {
7247
7379
  "--from-file",
7248
7380
  memoryPath,
7249
7381
  "--mode",
7250
- "create",
7382
+ writeMode,
7251
7383
  "--wait",
7252
7384
  "--timeout",
7253
7385
  "120"
7254
7386
  ])
7255
7387
  )
7256
7388
  );
7389
+ if (options.replaceUri && options.replaceUri !== memoryUri) {
7390
+ console.log(formatShellCommand(ov, withIdentity(config, ["rm", options.replaceUri])));
7391
+ }
7257
7392
  return;
7258
7393
  }
7259
7394
  await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
7260
7395
  await (0, import_promises4.chmod)(memoryPath, 384);
7261
- await ensureDurableMemoryDirectory(ov, config);
7262
- await writeDurableMemoryFile(ov, config, memoryUri, memoryPath);
7263
- console.log(`Stored durable memory: ${memoryUri}`);
7396
+ await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, options.metadata));
7397
+ await writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode);
7398
+ console.log(`Stored memory: ${memoryUri}`);
7399
+ if (options.replaceUri && options.replaceUri !== memoryUri) {
7400
+ const removedReplacedMemory = await removeVikingResourceWithRetry(ov, config, options.replaceUri);
7401
+ if (removedReplacedMemory) {
7402
+ console.log(`Forgot replaced memory: ${options.replaceUri}`);
7403
+ } else {
7404
+ console.error(
7405
+ `Replacement stored, but the superseded memory is still processing. Retry later: threadnote forget ${options.replaceUri}`
7406
+ );
7407
+ }
7408
+ } else if (options.replaceUri === memoryUri) {
7409
+ console.log(`Updated existing memory in place: ${memoryUri}`);
7410
+ }
7264
7411
  }
7265
- async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath) {
7412
+ async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode) {
7266
7413
  const args = withIdentity(config, [
7267
7414
  "write",
7268
7415
  memoryUri,
7269
7416
  "--from-file",
7270
7417
  memoryPath,
7271
7418
  "--mode",
7272
- "create",
7419
+ writeMode,
7273
7420
  "--wait",
7274
7421
  "--timeout",
7275
7422
  "120"
@@ -7306,29 +7453,49 @@ async function waitForOpenVikingQueue(ov, config) {
7306
7453
  console.error(result.stderr.trim());
7307
7454
  }
7308
7455
  }
7456
+ async function removeVikingResourceWithRetry(ov, config, uri) {
7457
+ const args = withIdentity(config, ["rm", uri]);
7458
+ for (let attempt = 0; attempt < 4; attempt += 1) {
7459
+ console.log(`${attempt === 0 ? "Running" : "Retrying"}: ${formatShellCommand(ov, args)}`);
7460
+ const result = await runCommand(ov, args, { allowFailure: true });
7461
+ if (result.exitCode === 0) {
7462
+ if (result.stdout.trim()) {
7463
+ console.log(result.stdout.trim());
7464
+ }
7465
+ if (result.stderr.trim()) {
7466
+ console.error(result.stderr.trim());
7467
+ }
7468
+ return true;
7469
+ }
7470
+ if (isResourceBusy(result.stderr, result.stdout) && attempt === 3) {
7471
+ return false;
7472
+ }
7473
+ if (!isResourceBusy(result.stderr, result.stdout)) {
7474
+ throw new Error(`${formatShellCommand(ov, args)} failed: ${result.stderr || result.stdout}`);
7475
+ }
7476
+ await sleep(1e3 * (attempt + 1));
7477
+ }
7478
+ return false;
7479
+ }
7309
7480
  async function vikingResourceExists(ov, config, uri) {
7310
7481
  const stat3 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
7311
7482
  return stat3.exitCode === 0;
7312
7483
  }
7313
7484
  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
- );
7485
+ await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
7329
7486
  }
7330
- function durableMemoryUri(config, memory) {
7331
- return `${durableMemoryDirectoryUri(config)}/threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
7487
+ async function ensureMemoryDirectory(ov, config, directoryUri) {
7488
+ for (const uri of vikingDirectoryChain(directoryUri)) {
7489
+ const statResult = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
7490
+ if (statResult.exitCode === 0) {
7491
+ continue;
7492
+ }
7493
+ await maybeRun(
7494
+ false,
7495
+ ov,
7496
+ withIdentity(config, ["mkdir", uri, "--description", "Threadnote lifecycle-aware local memories."])
7497
+ );
7498
+ }
7332
7499
  }
7333
7500
  function durableMemoryDirectoryUri(config) {
7334
7501
  return `viking://user/${uriSegment(config.user)}/memories/events`;
@@ -7336,6 +7503,185 @@ function durableMemoryDirectoryUri(config) {
7336
7503
  function migratedDurableMemoryUri(config, hash) {
7337
7504
  return `${durableMemoryDirectoryUri(config)}/threadnote-migrated-${hash.slice(0, 16)}.md`;
7338
7505
  }
7506
+ async function hasLegacyLifecycleHandoffCandidates(config) {
7507
+ return (await legacyLifecycleHandoffCandidates(config, 1)).length > 0;
7508
+ }
7509
+ async function legacyLifecycleHandoffCandidates(config, limit) {
7510
+ const eventsRoot = (0, import_node_path4.join)(localUserMemoriesRoot(config), "events");
7511
+ let entries;
7512
+ try {
7513
+ entries = await (0, import_promises4.readdir)(eventsRoot, { withFileTypes: true });
7514
+ } catch (_err) {
7515
+ return [];
7516
+ }
7517
+ const candidates = [];
7518
+ for (const entry of entries) {
7519
+ if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
7520
+ continue;
7521
+ }
7522
+ const sourcePath = (0, import_node_path4.join)(eventsRoot, entry.name);
7523
+ const original = await readTextIfExists(sourcePath);
7524
+ if (!original || !isClearLegacyHandoffMemory(original) || sensitiveMemoryReason(original)) {
7525
+ continue;
7526
+ }
7527
+ const sourceUri = `${durableMemoryDirectoryUri(config)}/${entry.name}`;
7528
+ candidates.push({
7529
+ metadata: {
7530
+ archivedFrom: sourceUri,
7531
+ kind: "handoff",
7532
+ project: inferLegacyProject(original),
7533
+ sourceAgentClient: "threadnote",
7534
+ status: "archived",
7535
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
7536
+ },
7537
+ original,
7538
+ sourceUri
7539
+ });
7540
+ if (limit !== void 0 && candidates.length >= limit) {
7541
+ break;
7542
+ }
7543
+ }
7544
+ return candidates;
7545
+ }
7546
+ function lifecycleMigrationUri(config, metadata, hash) {
7547
+ return `${memoryDirectoryUri(config, metadata)}/legacy-${hash.slice(0, 16)}.md`;
7548
+ }
7549
+ function exactMemoryScopes(config, includeArchived) {
7550
+ const userBase = `viking://user/${uriSegment(config.user)}/memories`;
7551
+ const scopes = [
7552
+ `${userBase}/preferences`,
7553
+ `${userBase}/durable/projects`,
7554
+ `${userBase}/handoffs/active`,
7555
+ `${userBase}/incidents/active`,
7556
+ `${userBase}/events`,
7557
+ `viking://agent/${uriSegment(config.agentId)}/memories`
7558
+ ];
7559
+ return includeArchived ? [...scopes, `${userBase}/durable/archived`, `${userBase}/handoffs/archived`, `${userBase}/incidents/archived`] : scopes;
7560
+ }
7561
+ function memoryUriFor(config, memory, metadata) {
7562
+ const filename = shouldUseStableMemoryUri(metadata) ? `${uriSegment(metadata.topic ?? "current")}.md` : `threadnote-${safeTimestamp()}-${sha256(memory).slice(0, 12)}.md`;
7563
+ return `${memoryDirectoryUri(config, metadata)}/${filename}`;
7564
+ }
7565
+ function memoryDirectoryUri(config, metadata) {
7566
+ const baseUri = `viking://user/${uriSegment(config.user)}/memories`;
7567
+ const projectSegment = uriSegment(metadata.project ?? "general");
7568
+ switch (metadata.kind) {
7569
+ case "preference":
7570
+ return metadata.status === "active" ? `${baseUri}/preferences` : `${baseUri}/preferences/${uriSegment(metadata.status)}`;
7571
+ case "handoff":
7572
+ return `${baseUri}/handoffs/${uriSegment(metadata.status)}/${projectSegment}`;
7573
+ case "incident":
7574
+ return `${baseUri}/incidents/${uriSegment(metadata.status)}/${projectSegment}`;
7575
+ case "smoke":
7576
+ return `${baseUri}/smoke/${uriSegment(metadata.status)}`;
7577
+ case "durable":
7578
+ return metadata.status === "active" ? `${baseUri}/durable/projects/${projectSegment}` : `${baseUri}/durable/${uriSegment(metadata.status)}/${projectSegment}`;
7579
+ }
7580
+ }
7581
+ function shouldUseStableMemoryUri(metadata) {
7582
+ return metadata.status === "active" && metadata.topic !== void 0 && metadata.kind !== "smoke";
7583
+ }
7584
+ async function memoryWriteMode(ov, config, memoryUri, metadata) {
7585
+ if (!shouldUseStableMemoryUri(metadata)) {
7586
+ return "create";
7587
+ }
7588
+ return await vikingResourceExists(ov, config, memoryUri) ? "replace" : "create";
7589
+ }
7590
+ function vikingDirectoryChain(directoryUri) {
7591
+ const prefix = "viking://";
7592
+ if (!directoryUri.startsWith(prefix)) {
7593
+ return [directoryUri];
7594
+ }
7595
+ const parts = directoryUri.slice(prefix.length).split("/").filter(Boolean);
7596
+ const startIndex = parts[0] === "user" && parts.length > 2 ? 3 : 1;
7597
+ const chain = [];
7598
+ for (let index = startIndex; index <= parts.length; index += 1) {
7599
+ chain.push(`${prefix}${parts.slice(0, index).join("/")}`);
7600
+ }
7601
+ return chain;
7602
+ }
7603
+ function formatMemoryDocument(title, metadata, body) {
7604
+ const header = [
7605
+ title,
7606
+ `kind: ${metadata.kind}`,
7607
+ `status: ${metadata.status}`,
7608
+ metadata.project ? `project: ${metadata.project}` : void 0,
7609
+ metadata.topic ? `topic: ${metadata.topic}` : void 0,
7610
+ `source_agent_client: ${metadata.sourceAgentClient}`,
7611
+ `timestamp: ${metadata.timestamp}`,
7612
+ metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
7613
+ metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
7614
+ ].filter((line) => line !== void 0);
7615
+ return [...header, "", body.trim()].join("\n");
7616
+ }
7617
+ function inferMemoryMetadata(memory) {
7618
+ const header = memory.slice(0, Math.max(0, memory.indexOf("\n\n")) || memory.length);
7619
+ const firstLine2 = header.split("\n")[0]?.trim();
7620
+ const kind = parseOptionalMemoryKind(parseHeaderValue(header, "kind")) ?? (firstLine2 === "HANDOFF" ? "handoff" : void 0);
7621
+ const status = parseOptionalMemoryStatus(parseHeaderValue(header, "status"));
7622
+ const project = normalizeOptionalMetadata(parseHeaderValue(header, "project")) ?? normalizeOptionalMetadata(parseHeaderValue(header, "repo"));
7623
+ const topic = normalizeOptionalMetadata(parseHeaderValue(header, "topic")) ?? normalizeOptionalMetadata(parseHeaderValue(header, "task"));
7624
+ return {
7625
+ kind,
7626
+ project,
7627
+ sourceAgentClient: parseHeaderValue(header, "source_agent_client") ?? void 0,
7628
+ status,
7629
+ topic
7630
+ };
7631
+ }
7632
+ function parseHeaderValue(header, key) {
7633
+ const prefix = `${key}:`;
7634
+ return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
7635
+ }
7636
+ function isClearLegacyHandoffMemory(memory) {
7637
+ if (/^kind:\s*/m.test(memory) || /^status:\s*/m.test(memory)) {
7638
+ return false;
7639
+ }
7640
+ const trimmed = memory.trim();
7641
+ if (trimmed.startsWith("HANDOFF\n")) {
7642
+ return true;
7643
+ }
7644
+ if (!trimmed.startsWith("MEMORY\n")) {
7645
+ return false;
7646
+ }
7647
+ return /^(?:#+\s*)?(?:final\s+)?handoff(?:\s+update)?\b/i.test(memoryBody(trimmed));
7648
+ }
7649
+ function memoryBody(memory) {
7650
+ const separatorIndex = memory.indexOf("\n\n");
7651
+ return separatorIndex === -1 ? "" : memory.slice(separatorIndex + 2).trim();
7652
+ }
7653
+ function inferLegacyProject(memory) {
7654
+ const explicit = parseHeaderValue(memory, "project") ?? parseHeaderValue(memory, "repo") ?? parseHeaderValue(memory, "repo_path") ?? /\brepo(?:_path)?\s+([~/A-Za-z0-9_.:/-]+)/.exec(memory)?.[1];
7655
+ if (!explicit) {
7656
+ return "general";
7657
+ }
7658
+ const trimmed = explicit.trim().replace(/[`.,;]+$/g, "");
7659
+ return trimmed.includes("/") ? (0, import_node_path4.basename)(trimmed) : trimmed;
7660
+ }
7661
+ function parseOptionalMemoryKind(value) {
7662
+ if (!value) {
7663
+ return void 0;
7664
+ }
7665
+ try {
7666
+ return parseMemoryKind(value);
7667
+ } catch (_err) {
7668
+ return void 0;
7669
+ }
7670
+ }
7671
+ function parseOptionalMemoryStatus(value) {
7672
+ if (!value) {
7673
+ return void 0;
7674
+ }
7675
+ try {
7676
+ return parseMemoryStatus(value);
7677
+ } catch (_err) {
7678
+ return void 0;
7679
+ }
7680
+ }
7681
+ function normalizeOptionalMetadata(value) {
7682
+ const trimmed = value?.trim();
7683
+ return trimmed ? trimmed : void 0;
7684
+ }
7339
7685
  async function legacySourceAccounts(config, options) {
7340
7686
  const explicitAccounts = options.sourceAccount?.filter((account) => account.trim().length > 0) ?? [];
7341
7687
  if (explicitAccounts.length > 0) {
@@ -7488,12 +7834,16 @@ function legacySourceLabel(candidate) {
7488
7834
  function localVikingDataRoot(config) {
7489
7835
  return (0, import_node_path4.join)(config.agentContextHome, "data", "viking");
7490
7836
  }
7837
+ function localUserMemoriesRoot(config) {
7838
+ return (0, import_node_path4.join)(localVikingDataRoot(config), config.account, "user", uriSegment(config.user), "memories");
7839
+ }
7491
7840
  function uniqueStrings(values) {
7492
7841
  return [...new Set(values)].sort();
7493
7842
  }
7494
7843
  function isResourceBusy(stderr, stdout) {
7495
- return `${stderr}
7496
- ${stdout}`.includes("resource is busy");
7844
+ const output2 = `${stderr}
7845
+ ${stdout}`.toLowerCase();
7846
+ return output2.includes("resource is busy") || output2.includes("resource is being processed");
7497
7847
  }
7498
7848
  async function buildHandoff(options) {
7499
7849
  const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]) ?? getInvocationCwd();
@@ -7501,13 +7851,20 @@ async function buildHandoff(options) {
7501
7851
  const status = await gitValue(["status", "--short"], repoRoot) ?? "";
7502
7852
  const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
7503
7853
  const touchedFiles = await gitTouchedFiles(repoRoot);
7504
- return [
7505
- "HANDOFF",
7506
- `repo: ${(0, import_node_path4.basename)(repoRoot)}`,
7854
+ const repoName = (0, import_node_path4.basename)(repoRoot);
7855
+ const metadata = {
7856
+ kind: "handoff",
7857
+ project: normalizeOptionalMetadata(options.project) ?? repoName,
7858
+ sourceAgentClient: options.sourceAgentClient ?? "codex",
7859
+ status: "active",
7860
+ supersedes: options.replace,
7861
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7862
+ topic: normalizeOptionalMetadata(options.topic)
7863
+ };
7864
+ const body = [
7865
+ `repo: ${repoName}`,
7507
7866
  `repo_path: ${repoRoot}`,
7508
7867
  `branch: ${branch || "unknown"}`,
7509
- `source_agent_client: ${options.sourceAgentClient ?? "codex"}`,
7510
- `timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}`,
7511
7868
  `task: ${options.task ?? "unspecified"}`,
7512
7869
  "",
7513
7870
  "files_touched:",
@@ -7528,6 +7885,7 @@ async function buildHandoff(options) {
7528
7885
  "next_step:",
7529
7886
  options.nextStep ?? "- inspect the current repo state and continue from this handoff"
7530
7887
  ].join("\n");
7888
+ return { handoff: formatMemoryDocument("HANDOFF", metadata, body), metadata };
7531
7889
  }
7532
7890
  async function gitTouchedFiles(cwd) {
7533
7891
  const changedFiles = await gitValue(["diff", "--name-only", "HEAD"], cwd);
@@ -7619,14 +7977,14 @@ async function runInitManifest(config, options) {
7619
7977
  uri: existingManifest.futureMonorepo.uri
7620
7978
  };
7621
7979
  }
7622
- const output = jsYaml.dump(outputManifest, { lineWidth: 120, noRefs: true });
7980
+ const output2 = jsYaml.dump(outputManifest, { lineWidth: 120, noRefs: true });
7623
7981
  if (options.dryRun === true) {
7624
7982
  console.log(`# Would write ${manifestPath}`);
7625
- console.log(output.trimEnd());
7983
+ console.log(output2.trimEnd());
7626
7984
  return;
7627
7985
  }
7628
7986
  await ensureDirectory((0, import_node_path5.dirname)(manifestPath), false);
7629
- await (0, import_promises5.writeFile)(manifestPath, output, { encoding: "utf8", mode: 384 });
7987
+ await (0, import_promises5.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
7630
7988
  await (0, import_promises5.chmod)(manifestPath, 384);
7631
7989
  console.log(`Wrote manifest: ${manifestPath}`);
7632
7990
  console.log("Seed with:");
@@ -7837,11 +8195,11 @@ function redactJsonValue(value) {
7837
8195
  if (!isJsonObject(value)) {
7838
8196
  return value;
7839
8197
  }
7840
- const output = {};
8198
+ const output2 = {};
7841
8199
  for (const [key, nestedValue] of Object.entries(value)) {
7842
- output[key] = isSensitiveKey(key) ? "[REDACTED]" : redactJsonValue(nestedValue);
8200
+ output2[key] = isSensitiveKey(key) ? "[REDACTED]" : redactJsonValue(nestedValue);
7843
8201
  }
7844
- return output;
8202
+ return output2;
7845
8203
  }
7846
8204
  function isSensitiveKey(key) {
7847
8205
  return /token|secret|password|credential|authorization|api[_-]?key|client[_-]?secret|bearer/i.test(key);
@@ -7865,15 +8223,476 @@ function detectSecretMatches(content) {
7865
8223
  }
7866
8224
 
7867
8225
  // src/lifecycle.ts
7868
- var import_node_child_process2 = require("node:child_process");
7869
- var import_node_fs3 = require("node:fs");
7870
- var import_promises6 = require("node:fs/promises");
7871
- var import_node_os4 = require("node:os");
7872
- var import_node_path6 = require("node:path");
8226
+ var import_node_child_process2 = require("node:child_process");
8227
+ var import_node_fs4 = require("node:fs");
8228
+ var import_promises8 = require("node:fs/promises");
8229
+ var import_node_os5 = require("node:os");
8230
+ var import_node_path7 = require("node:path");
8231
+
8232
+ // src/update.ts
8233
+ var import_node_fs3 = require("node:fs");
8234
+ var import_promises6 = require("node:fs/promises");
8235
+ var import_node_os4 = require("node:os");
8236
+ var import_node_path6 = require("node:path");
8237
+ var import_promises7 = require("node:readline/promises");
8238
+ var import_node_process = require("node:process");
8239
+ var NPM_PACKAGE_NAME = "threadnote";
8240
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
8241
+ var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
8242
+ var POST_UPDATE_MIGRATIONS_FILE = "post-update-migrations.json";
8243
+ var POST_UPDATE_STATE_FILE = "post-update-state.json";
8244
+ function parseUpdateRuntime(value) {
8245
+ if (value === "auto" || value === "npm" || value === "bun" || value === "deno") {
8246
+ return value;
8247
+ }
8248
+ throw new Error(`Invalid update runtime: ${value}. Expected auto, npm, bun, or deno.`);
8249
+ }
8250
+ async function maybeNotifyUpdate(config, options = {}) {
8251
+ if (isUpdateNotificationDisabled()) {
8252
+ return;
8253
+ }
8254
+ try {
8255
+ const info = await getUpdateInfo(config, {
8256
+ allowCacheWrite: options.dryRun !== true,
8257
+ preferFresh: false,
8258
+ registry: updateRegistry()
8259
+ });
8260
+ if (!info.isUpdateAvailable) {
8261
+ return;
8262
+ }
8263
+ console.log("");
8264
+ console.log(`Update available: threadnote ${info.currentVersion} -> ${info.latestVersion}`);
8265
+ console.log("Run: threadnote update");
8266
+ } catch (_err) {
8267
+ return;
8268
+ }
8269
+ }
8270
+ async function runUpdate(config, options) {
8271
+ const registry = normalizeRegistry(options.registry ?? updateRegistry());
8272
+ const info = await getUpdateInfo(config, {
8273
+ allowCacheWrite: options.dryRun !== true,
8274
+ preferFresh: true,
8275
+ registry
8276
+ });
8277
+ console.log(`Current version: ${info.currentVersion}`);
8278
+ console.log(`Latest version: ${info.latestVersion}`);
8279
+ console.log(`Registry: ${info.registry}`);
8280
+ if (options.check === true) {
8281
+ if (info.isUpdateAvailable) {
8282
+ console.log(`Update available. Run: threadnote update`);
8283
+ } else {
8284
+ console.log(
8285
+ compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
8286
+ );
8287
+ }
8288
+ return;
8289
+ }
8290
+ if (!info.isUpdateAvailable && options.force !== true) {
8291
+ console.log("Threadnote is up to date.");
8292
+ return;
8293
+ }
8294
+ const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
8295
+ const updateCommand = updatePackageCommand(runtime, registry);
8296
+ await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
8297
+ if (options.repair === false) {
8298
+ console.log("Skipping repair because --no-repair was provided.");
8299
+ return;
8300
+ }
8301
+ const threadnoteCommand = await installedThreadnoteCommand(runtime);
8302
+ await maybeRun(options.dryRun === true, threadnoteCommand, ["repair", "--no-post-update"]);
8303
+ if (options.postUpdate !== false) {
8304
+ const postUpdateArgs = [
8305
+ "post-update",
8306
+ "--from-version",
8307
+ info.currentVersion,
8308
+ "--to-version",
8309
+ info.latestVersion,
8310
+ ...options.yes === true ? ["--yes"] : []
8311
+ ];
8312
+ if (options.dryRun === true) {
8313
+ await maybeRun(true, threadnoteCommand, postUpdateArgs);
8314
+ } else {
8315
+ console.log(`Running: ${formatShellCommand(threadnoteCommand, postUpdateArgs)}`);
8316
+ const postUpdateExitCode = await runInteractive(threadnoteCommand, postUpdateArgs);
8317
+ if (postUpdateExitCode !== 0) {
8318
+ throw new Error(`${formatShellCommand(threadnoteCommand, postUpdateArgs)} exited with ${postUpdateExitCode}.`);
8319
+ }
8320
+ }
8321
+ } else {
8322
+ console.log("Skipping post-update migration prompts because --no-post-update was provided.");
8323
+ }
8324
+ console.log("Update complete. Restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.");
8325
+ }
8326
+ async function runPostUpdate(config, options) {
8327
+ if (!options.fromVersion || !options.toVersion) {
8328
+ throw new Error("Provide --from-version and --to-version for post-update.");
8329
+ }
8330
+ await runApplicablePostUpdateMigrations(config, {
8331
+ dryRun: options.dryRun === true,
8332
+ fromVersion: options.fromVersion,
8333
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
8334
+ markHandled: true,
8335
+ toVersion: options.toVersion,
8336
+ yes: options.yes === true
8337
+ });
8338
+ }
8339
+ async function maybeRunPostUpdateAfterRepair(config, options) {
8340
+ const toVersion = await currentPackageVersion();
8341
+ const state = await readPostUpdateState(config);
8342
+ const migrations = await applicablePostUpdateMigrations(config, {
8343
+ fromVersion: "0.0.0",
8344
+ handledMigrationIds: state.handledMigrationIds,
8345
+ toVersion
8346
+ });
8347
+ if (migrations.length === 0) {
8348
+ return;
8349
+ }
8350
+ console.log("");
8351
+ console.log("Repair found package post-update migrations.");
8352
+ console.log("This also covers updates launched by older Threadnote versions that only knew how to run repair.");
8353
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
8354
+ console.log(
8355
+ "This process is non-interactive, so Threadnote will print the manual migration command instead of prompting."
8356
+ );
8357
+ console.log(`Run the prompt manually with: threadnote post-update --from-version 0.0.0 --to-version ${toVersion}`);
8358
+ }
8359
+ await runApplicablePostUpdateMigrations(config, {
8360
+ dryRun: options.dryRun,
8361
+ fromVersion: "0.0.0",
8362
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
8363
+ markHandled: true,
8364
+ toVersion,
8365
+ yes: false
8366
+ });
8367
+ }
8368
+ async function getUpdateInfo(config, options) {
8369
+ const currentVersion = await currentPackageVersion();
8370
+ const cached = options.preferFresh ? void 0 : await readFreshCache(config, options.registry);
8371
+ const latestVersion = cached?.latestVersion ?? await fetchLatestVersion(options.registry);
8372
+ if (!cached && options.allowCacheWrite) {
8373
+ await writeUpdateCache(config, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion, registry: options.registry });
8374
+ }
8375
+ return {
8376
+ currentVersion,
8377
+ isUpdateAvailable: compareVersions(currentVersion, latestVersion) < 0,
8378
+ latestVersion,
8379
+ registry: options.registry
8380
+ };
8381
+ }
8382
+ async function currentPackageVersion() {
8383
+ const rawPackage = await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), "package.json"), "utf8");
8384
+ const parsed = JSON.parse(rawPackage);
8385
+ if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8386
+ throw new Error("Could not read current threadnote package version.");
8387
+ }
8388
+ return parsed.version;
8389
+ }
8390
+ async function fetchLatestVersion(registry) {
8391
+ const url = new URL(`${NPM_PACKAGE_NAME}/latest`, normalizeRegistry(registry));
8392
+ const controller = new AbortController();
8393
+ const timeout = setTimeout(() => {
8394
+ controller.abort();
8395
+ }, 2500);
8396
+ try {
8397
+ const response = await fetch(url, { headers: { accept: "application/json" }, signal: controller.signal });
8398
+ if (!response.ok) {
8399
+ throw new Error(`npm registry returned HTTP ${response.status}`);
8400
+ }
8401
+ const parsed = await response.json();
8402
+ if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8403
+ throw new Error("npm registry response did not include a version.");
8404
+ }
8405
+ return parsed.version;
8406
+ } catch (err) {
8407
+ throw new Error(`Could not check npm for updates: ${errorMessage(err)}`, { cause: err });
8408
+ } finally {
8409
+ clearTimeout(timeout);
8410
+ }
8411
+ }
8412
+ async function readFreshCache(config, registry) {
8413
+ const rawCache = await readFileIfExists(updateCachePath(config));
8414
+ if (!rawCache) {
8415
+ return void 0;
8416
+ }
8417
+ try {
8418
+ const parsed = JSON.parse(rawCache);
8419
+ if (!isJsonObject(parsed) || typeof parsed.checkedAt !== "string" || typeof parsed.latestVersion !== "string" || parsed.registry !== registry) {
8420
+ return void 0;
8421
+ }
8422
+ const checkedAt = Date.parse(parsed.checkedAt);
8423
+ if (!Number.isFinite(checkedAt) || Date.now() - checkedAt > UPDATE_CHECK_TTL_MS) {
8424
+ return void 0;
8425
+ }
8426
+ return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, registry };
8427
+ } catch (_err) {
8428
+ return void 0;
8429
+ }
8430
+ }
8431
+ async function writeUpdateCache(config, cache) {
8432
+ await ensureDirectory(config.agentContextHome, false);
8433
+ await (0, import_promises6.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
8434
+ `, { encoding: "utf8", mode: 384 });
8435
+ }
8436
+ function updateCachePath(config) {
8437
+ return (0, import_node_path6.join)(config.agentContextHome, "update-check.json");
8438
+ }
8439
+ async function runApplicablePostUpdateMigrations(config, options) {
8440
+ const state = await readPostUpdateState(config);
8441
+ const migrations = await applicablePostUpdateMigrations(config, {
8442
+ fromVersion: options.fromVersion,
8443
+ handledMigrationIds: state.handledMigrationIds,
8444
+ toVersion: options.toVersion
8445
+ });
8446
+ if (migrations.length === 0) {
8447
+ console.log("No post-update memory migrations apply.");
8448
+ return;
8449
+ }
8450
+ console.log("");
8451
+ console.log("Post-update memory migrations are available.");
8452
+ const threadnoteCommand = currentThreadnoteCommand() ?? await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
8453
+ const handledMigrationIds = new Set(state.handledMigrationIds);
8454
+ for (const migration of migrations) {
8455
+ printPostUpdateMigration(migration);
8456
+ const accepted = options.dryRun || options.yes || options.interactive && await confirmPostUpdateMigration("Apply this migration now? [y/N] ");
8457
+ if (!accepted) {
8458
+ console.log("Skipped. Run manually later:");
8459
+ console.log(` ${formatMigrationCommand(threadnoteCommand, migration.commandArgs)}`);
8460
+ continue;
8461
+ }
8462
+ await maybeRun(options.dryRun, threadnoteCommand, migration.commandArgs);
8463
+ if (!options.dryRun) {
8464
+ handledMigrationIds.add(migration.id);
8465
+ for (const instruction of migration.instructions) {
8466
+ console.log(instruction);
8467
+ }
8468
+ } else {
8469
+ console.log("After this migration succeeds, Threadnote will print:");
8470
+ for (const instruction of migration.instructions) {
8471
+ console.log(` ${instruction}`);
8472
+ }
8473
+ }
8474
+ }
8475
+ if (!options.dryRun && options.markHandled) {
8476
+ await writePostUpdateState(config, { handledMigrationIds: [...handledMigrationIds].sort() });
8477
+ }
8478
+ }
8479
+ async function applicablePostUpdateMigrations(config, options) {
8480
+ const migrations = await readPostUpdateMigrations();
8481
+ const handled = new Set(options.handledMigrationIds);
8482
+ const applicable = [];
8483
+ for (const migration of migrations) {
8484
+ if (handled.has(migration.id)) {
8485
+ continue;
8486
+ }
8487
+ if (compareVersions(options.fromVersion, migration.introducedIn) >= 0) {
8488
+ continue;
8489
+ }
8490
+ if (compareVersions(migration.introducedIn, options.toVersion) > 0) {
8491
+ continue;
8492
+ }
8493
+ if (migration.requiresLegacyHandoffs === true && !await hasLegacyLifecycleHandoffCandidates(config)) {
8494
+ continue;
8495
+ }
8496
+ applicable.push(migration);
8497
+ }
8498
+ return applicable;
8499
+ }
8500
+ async function readPostUpdateMigrations() {
8501
+ const raw = await readFileIfExists((0, import_node_path6.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
8502
+ if (!raw) {
8503
+ return [];
8504
+ }
8505
+ const parsed = JSON.parse(raw);
8506
+ if (!isJsonObject(parsed) || !Array.isArray(parsed.migrations)) {
8507
+ throw new Error(`${POST_UPDATE_MIGRATIONS_FILE} must contain a migrations array.`);
8508
+ }
8509
+ return parsed.migrations.map(parsePostUpdateMigration);
8510
+ }
8511
+ function parsePostUpdateMigration(value) {
8512
+ if (!isJsonObject(value) || typeof value.id !== "string" || typeof value.introducedIn !== "string" || typeof value.title !== "string" || !Array.isArray(value.description) || !Array.isArray(value.commandArgs) || !Array.isArray(value.instructions)) {
8513
+ throw new Error(`Invalid entry in ${POST_UPDATE_MIGRATIONS_FILE}.`);
8514
+ }
8515
+ return {
8516
+ commandArgs: stringArray(value, "commandArgs"),
8517
+ description: stringArray(value, "description"),
8518
+ id: value.id,
8519
+ instructions: stringArray(value, "instructions"),
8520
+ introducedIn: value.introducedIn,
8521
+ requiresLegacyHandoffs: value.requiresLegacyHandoffs === true,
8522
+ title: value.title
8523
+ };
8524
+ }
8525
+ function stringArray(value, key) {
8526
+ const raw = value[key];
8527
+ if (!Array.isArray(raw) || !raw.every((item) => typeof item === "string")) {
8528
+ throw new Error(`Invalid ${key} in ${POST_UPDATE_MIGRATIONS_FILE}.`);
8529
+ }
8530
+ return raw;
8531
+ }
8532
+ function printPostUpdateMigration(migration) {
8533
+ console.log("");
8534
+ console.log(`${migration.title} (${migration.introducedIn})`);
8535
+ for (const line of migration.description) {
8536
+ console.log(`- ${line}`);
8537
+ }
8538
+ }
8539
+ async function confirmPostUpdateMigration(prompt) {
8540
+ const readline = (0, import_promises7.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
8541
+ try {
8542
+ const answer = (await readline.question(prompt)).trim().toLowerCase();
8543
+ return answer === "y" || answer === "yes";
8544
+ } finally {
8545
+ readline.close();
8546
+ }
8547
+ }
8548
+ function formatMigrationCommand(executable, args) {
8549
+ return [executable, ...args].map((part) => /\s/.test(part) ? JSON.stringify(part) : part).join(" ");
8550
+ }
8551
+ function currentThreadnoteCommand() {
8552
+ const entrypoint = process.argv[1]?.trim();
8553
+ return entrypoint ? entrypoint : void 0;
8554
+ }
8555
+ async function readPostUpdateState(config) {
8556
+ const raw = await readFileIfExists(postUpdateStatePath(config));
8557
+ if (!raw) {
8558
+ return { handledMigrationIds: [] };
8559
+ }
8560
+ try {
8561
+ const parsed = JSON.parse(raw);
8562
+ if (!isJsonObject(parsed) || !Array.isArray(parsed.handledMigrationIds)) {
8563
+ return { handledMigrationIds: [] };
8564
+ }
8565
+ return { handledMigrationIds: parsed.handledMigrationIds.filter((id) => typeof id === "string") };
8566
+ } catch (_err) {
8567
+ return { handledMigrationIds: [] };
8568
+ }
8569
+ }
8570
+ async function writePostUpdateState(config, state) {
8571
+ await ensureDirectory(config.agentContextHome, false);
8572
+ await (0, import_promises6.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
8573
+ `, { encoding: "utf8", mode: 384 });
8574
+ }
8575
+ function postUpdateStatePath(config) {
8576
+ return (0, import_node_path6.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
8577
+ }
8578
+ async function resolveUpdateRuntime(runtime) {
8579
+ if (runtime !== "auto") {
8580
+ await requireRuntime(runtime);
8581
+ return runtime;
8582
+ }
8583
+ for (const candidate of ["npm", "bun", "deno"]) {
8584
+ if (await findExecutable([candidate])) {
8585
+ return candidate;
8586
+ }
8587
+ }
8588
+ throw new Error("Install Node/npm, Bun, or Deno to update threadnote.");
8589
+ }
8590
+ async function requireRuntime(runtime) {
8591
+ if (!await findExecutable([runtime])) {
8592
+ throw new Error(`${runtime} was requested but was not found on PATH.`);
8593
+ }
8594
+ }
8595
+ async function installedThreadnoteCommand(runtime) {
8596
+ const runtimeBin = await runtimeThreadnoteBin(runtime);
8597
+ if (runtimeBin && await isExecutable(runtimeBin)) {
8598
+ return runtimeBin;
8599
+ }
8600
+ return await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
8601
+ }
8602
+ async function runtimeThreadnoteBin(runtime) {
8603
+ if (runtime === "npm") {
8604
+ const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
8605
+ const prefix = result.stdout.trim();
8606
+ return prefix ? (0, import_node_path6.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
8607
+ }
8608
+ if (runtime === "bun") {
8609
+ const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
8610
+ const binDir = result.stdout.trim();
8611
+ return binDir ? (0, import_node_path6.join)(binDir, NPM_PACKAGE_NAME) : void 0;
8612
+ }
8613
+ return (0, import_node_path6.join)(process.env.DENO_INSTALL ?? (0, import_node_path6.join)((0, import_node_os4.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
8614
+ }
8615
+ async function isExecutable(path) {
8616
+ try {
8617
+ await (0, import_promises6.access)(path, import_node_fs3.constants.X_OK);
8618
+ return true;
8619
+ } catch (_err) {
8620
+ return false;
8621
+ }
8622
+ }
8623
+ function updatePackageCommand(runtime, registry) {
8624
+ if (runtime === "npm") {
8625
+ return { executable: "npm", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
8626
+ }
8627
+ if (runtime === "bun") {
8628
+ return { executable: "bun", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
8629
+ }
8630
+ return {
8631
+ executable: "env",
8632
+ args: [
8633
+ `NPM_CONFIG_REGISTRY=${registry}`,
8634
+ "deno",
8635
+ "install",
8636
+ "--global",
8637
+ "--force",
8638
+ "--name",
8639
+ NPM_PACKAGE_NAME,
8640
+ "--allow-read",
8641
+ "--allow-write",
8642
+ "--allow-run",
8643
+ "--allow-env",
8644
+ "--allow-net",
8645
+ `npm:${NPM_PACKAGE_NAME}@latest`
8646
+ ]
8647
+ };
8648
+ }
8649
+ function normalizeRegistry(registry) {
8650
+ return registry.endsWith("/") ? registry : `${registry}/`;
8651
+ }
8652
+ function updateRegistry() {
8653
+ return normalizeRegistry(process.env.THREADNOTE_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY);
8654
+ }
8655
+ function isUpdateNotificationDisabled() {
8656
+ return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
8657
+ }
8658
+ function compareVersions(currentVersion, latestVersion) {
8659
+ const current = parseVersion(currentVersion);
8660
+ const latest = parseVersion(latestVersion);
8661
+ for (let index = 0; index < 3; index += 1) {
8662
+ const difference = current.numbers[index] - latest.numbers[index];
8663
+ if (difference !== 0) {
8664
+ return difference;
8665
+ }
8666
+ }
8667
+ if (current.prerelease === latest.prerelease) {
8668
+ return 0;
8669
+ }
8670
+ if (current.prerelease === void 0) {
8671
+ return 1;
8672
+ }
8673
+ if (latest.prerelease === void 0) {
8674
+ return -1;
8675
+ }
8676
+ return current.prerelease.localeCompare(latest.prerelease);
8677
+ }
8678
+ function parseVersion(version) {
8679
+ const normalized = version.trim().replace(/^v/, "");
8680
+ const [core2, prerelease] = normalized.split("-", 2);
8681
+ const parts = core2.split(".").map((part) => Number(part));
8682
+ return {
8683
+ numbers: [safeVersionNumber(parts[0]), safeVersionNumber(parts[1]), safeVersionNumber(parts[2])],
8684
+ prerelease
8685
+ };
8686
+ }
8687
+ function safeVersionNumber(value) {
8688
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
8689
+ }
8690
+
8691
+ // src/lifecycle.ts
7873
8692
  async function runDoctor(config, options) {
7874
8693
  const checks = [];
7875
8694
  checks.push({ name: "mode", status: "ok", detail: options.dryRun ? "dry run; no writes" : "read-only checks" });
7876
- checks.push({ name: "platform", status: (0, import_node_os4.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os4.platform)() });
8695
+ checks.push({ name: "platform", status: (0, import_node_os5.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os5.platform)() });
7877
8696
  checks.push(await commandCheck("node", ["--version"]));
7878
8697
  checks.push(await commandCheck("python3", ["--version"]));
7879
8698
  checks.push(await commandCheck("openviking-server", ["--help"]));
@@ -7886,9 +8705,9 @@ async function runDoctor(config, options) {
7886
8705
  checks.push(await commandShimCheck());
7887
8706
  checks.push(...await userAgentInstructionsChecks());
7888
8707
  checks.push(await manifestCheck(config.manifestPath));
7889
- checks.push(await fileCheck((0, import_node_path6.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
7890
- checks.push(await fileCheck((0, import_node_path6.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
7891
- checks.push(await fileCheck((0, import_node_path6.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
8708
+ checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
8709
+ checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
8710
+ checks.push(await fileCheck((0, import_node_path7.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
7892
8711
  checks.push(await healthCheck(config));
7893
8712
  for (const check of checks) {
7894
8713
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -7905,9 +8724,9 @@ async function runInstall(config, options) {
7905
8724
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
7906
8725
  const dryRun = options.dryRun === true;
7907
8726
  await ensureDirectory(config.agentContextHome, dryRun);
7908
- await ensureDirectory((0, import_node_path6.join)(config.agentContextHome, "logs"), dryRun);
7909
- await ensureDirectory((0, import_node_path6.join)(config.agentContextHome, "redacted"), dryRun);
7910
- await ensureDirectory((0, import_node_path6.join)(config.agentContextHome, "mcp"), dryRun);
8727
+ await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "logs"), dryRun);
8728
+ await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "redacted"), dryRun);
8729
+ await ensureDirectory((0, import_node_path7.join)(config.agentContextHome, "mcp"), dryRun);
7911
8730
  await installCommandShim(dryRun);
7912
8731
  await installUserAgentInstructions(dryRun);
7913
8732
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
@@ -7933,17 +8752,17 @@ async function runInstall(config, options) {
7933
8752
  }
7934
8753
  await writeTemplateIfMissing({
7935
8754
  config,
7936
- destinationPath: (0, import_node_path6.join)(config.agentContextHome, "ov.conf"),
8755
+ destinationPath: (0, import_node_path7.join)(config.agentContextHome, "ov.conf"),
7937
8756
  dryRun,
7938
8757
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
7939
- templatePath: (0, import_node_path6.join)(toolRoot(), "config", "ov.conf.template.json")
8758
+ templatePath: (0, import_node_path7.join)(toolRoot(), "config", "ov.conf.template.json")
7940
8759
  });
7941
8760
  await writeTemplateIfMissing({
7942
8761
  config,
7943
- destinationPath: (0, import_node_path6.join)(config.agentContextHome, "ovcli.conf"),
8762
+ destinationPath: (0, import_node_path7.join)(config.agentContextHome, "ovcli.conf"),
7944
8763
  dryRun,
7945
8764
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
7946
- templatePath: (0, import_node_path6.join)(toolRoot(), "config", "ovcli.conf.template.json")
8765
+ templatePath: (0, import_node_path7.join)(toolRoot(), "config", "ovcli.conf.template.json")
7947
8766
  });
7948
8767
  if (options.start !== false) {
7949
8768
  const healthy = await repairServerHealth(config, dryRun);
@@ -7982,6 +8801,9 @@ async function runRepair(config, options) {
7982
8801
  }
7983
8802
  console.log("\nPost-repair doctor:");
7984
8803
  await runDoctor(config, { dryRun, strict: false });
8804
+ if (options.postUpdate !== false) {
8805
+ await maybeRunPostUpdateAfterRepair(config, { dryRun });
8806
+ }
7985
8807
  }
7986
8808
  async function runUninstall(config, options) {
7987
8809
  const dryRun = options.dryRun === true;
@@ -7990,7 +8812,7 @@ async function runUninstall(config, options) {
7990
8812
  }
7991
8813
  console.log("Uninstalling local Threadnote setup.");
7992
8814
  await runStop(config, { dryRun });
7993
- await removePathIfExists((0, import_node_path6.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
8815
+ await removePathIfExists((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
7994
8816
  await removeLaunchAgent(dryRun);
7995
8817
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
7996
8818
  await removeMcpSnippets(config, dryRun);
@@ -8025,7 +8847,7 @@ async function repairManifest(config, dryRun) {
8025
8847
  return;
8026
8848
  }
8027
8849
  const project = projectManifestForRepo(repoRoot, []);
8028
- const output = jsYaml.dump(
8850
+ const output2 = jsYaml.dump(
8029
8851
  {
8030
8852
  version: 1,
8031
8853
  projects: [
@@ -8041,19 +8863,19 @@ async function repairManifest(config, dryRun) {
8041
8863
  );
8042
8864
  if (dryRun) {
8043
8865
  console.log(`# Would write replacement manifest: ${config.manifestPath}`);
8044
- console.log(output.trimEnd());
8866
+ console.log(output2.trimEnd());
8045
8867
  return;
8046
8868
  }
8047
- await ensureDirectory((0, import_node_path6.dirname)(config.manifestPath), false);
8869
+ await ensureDirectory((0, import_node_path7.dirname)(config.manifestPath), false);
8048
8870
  const currentContent = await readFileIfExists(config.manifestPath);
8049
8871
  if (currentContent !== void 0) {
8050
8872
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
8051
- await (0, import_promises6.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
8052
- await (0, import_promises6.chmod)(backupPath, 384);
8873
+ await (0, import_promises8.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
8874
+ await (0, import_promises8.chmod)(backupPath, 384);
8053
8875
  console.log(`Backup: ${backupPath}`);
8054
8876
  }
8055
- await (0, import_promises6.writeFile)(config.manifestPath, output, { encoding: "utf8", mode: 384 });
8056
- await (0, import_promises6.chmod)(config.manifestPath, 384);
8877
+ await (0, import_promises8.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
8878
+ await (0, import_promises8.chmod)(config.manifestPath, 384);
8057
8879
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
8058
8880
  }
8059
8881
  async function repairServerHealth(config, dryRun) {
@@ -8094,16 +8916,16 @@ async function runStart(config, options) {
8094
8916
  );
8095
8917
  }
8096
8918
  const logPath = openVikingLogPath(config);
8097
- await ensureDirectory((0, import_node_path6.dirname)(logPath), false);
8919
+ await ensureDirectory((0, import_node_path7.dirname)(logPath), false);
8098
8920
  if (options.foreground === true) {
8099
8921
  const result = await runInteractive(server, args);
8100
8922
  process.exitCode = result;
8101
8923
  return;
8102
8924
  }
8103
- const logFd = (0, import_node_fs3.openSync)(logPath, "a");
8925
+ const logFd = (0, import_node_fs4.openSync)(logPath, "a");
8104
8926
  const child = spawnDetachedServerWithLog(server, args, logFd);
8105
8927
  child.unref();
8106
- await (0, import_promises6.writeFile)((0, import_node_path6.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
8928
+ await (0, import_promises8.writeFile)((0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
8107
8929
  `, "utf8");
8108
8930
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
8109
8931
  if (health) {
@@ -8118,7 +8940,7 @@ function spawnDetachedServerWithLog(server, args, logFd) {
8118
8940
  try {
8119
8941
  return spawnDetachedServer(server, args, logFd);
8120
8942
  } finally {
8121
- (0, import_node_fs3.closeSync)(logFd);
8943
+ (0, import_node_fs4.closeSync)(logFd);
8122
8944
  }
8123
8945
  }
8124
8946
  function spawnDetachedServer(server, args, logFd) {
@@ -8129,14 +8951,14 @@ function spawnDetachedServer(server, args, logFd) {
8129
8951
  }
8130
8952
  async function runStop(config, options) {
8131
8953
  const launchAgentPath = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
8132
- if ((0, import_node_os4.platform)() === "darwin") {
8954
+ if ((0, import_node_os5.platform)() === "darwin") {
8133
8955
  if (options.dryRun === true || await exists(launchAgentPath)) {
8134
8956
  await maybeRun(options.dryRun === true, "launchctl", ["unload", launchAgentPath], { allowFailure: true });
8135
8957
  } else {
8136
8958
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
8137
8959
  }
8138
8960
  }
8139
- const pidPath = (0, import_node_path6.join)(config.agentContextHome, "openviking-server.pid");
8961
+ const pidPath = (0, import_node_path7.join)(config.agentContextHome, "openviking-server.pid");
8140
8962
  const pidText = await readFileIfExists(pidPath);
8141
8963
  if (!pidText) {
8142
8964
  console.log("No pid file found for detached OpenViking server.");
@@ -8232,7 +9054,7 @@ async function pythonSystemCertificatesCheck() {
8232
9054
  };
8233
9055
  }
8234
9056
  async function commandShimCheck() {
8235
- const shimPath = (0, import_node_path6.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9057
+ const shimPath = (0, import_node_path7.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
8236
9058
  const content = await readFileIfExists(shimPath);
8237
9059
  if (content === void 0) {
8238
9060
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -8298,11 +9120,11 @@ async function hasPythonModule(serverPath, moduleName) {
8298
9120
  async function siblingPythonForExecutable(executablePath) {
8299
9121
  let resolvedPath;
8300
9122
  try {
8301
- resolvedPath = await (0, import_promises6.realpath)(executablePath);
9123
+ resolvedPath = await (0, import_promises8.realpath)(executablePath);
8302
9124
  } catch (_err) {
8303
9125
  return void 0;
8304
9126
  }
8305
- const pythonPath = (0, import_node_path6.join)((0, import_node_path6.dirname)(resolvedPath), "python");
9127
+ const pythonPath = (0, import_node_path7.join)((0, import_node_path7.dirname)(resolvedPath), "python");
8306
9128
  return await exists(pythonPath) ? pythonPath : void 0;
8307
9129
  }
8308
9130
  async function manifestCheck(path) {
@@ -8424,38 +9246,38 @@ function printInstallNextSteps(options) {
8424
9246
  }
8425
9247
  async function writeTemplateIfMissing(options) {
8426
9248
  if (await exists(options.destinationPath)) {
8427
- const currentContent = await (0, import_promises6.readFile)(options.destinationPath, "utf8");
9249
+ const currentContent = await (0, import_promises8.readFile)(options.destinationPath, "utf8");
8428
9250
  if (options.shouldRepair?.(currentContent) !== true) {
8429
9251
  console.log(`Already exists: ${options.destinationPath}`);
8430
9252
  return;
8431
9253
  }
8432
- const rendered2 = renderTemplate(await (0, import_promises6.readFile)(options.templatePath, "utf8"), options.config);
9254
+ const rendered2 = renderTemplate(await (0, import_promises8.readFile)(options.templatePath, "utf8"), options.config);
8433
9255
  if (options.dryRun) {
8434
9256
  console.log(`Would repair generated config: ${options.destinationPath}`);
8435
9257
  return;
8436
9258
  }
8437
9259
  const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
8438
- await (0, import_promises6.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
8439
- await (0, import_promises6.chmod)(backupPath, 384);
8440
- await (0, import_promises6.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
8441
- await (0, import_promises6.chmod)(options.destinationPath, 384);
9260
+ await (0, import_promises8.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
9261
+ await (0, import_promises8.chmod)(backupPath, 384);
9262
+ await (0, import_promises8.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
9263
+ await (0, import_promises8.chmod)(options.destinationPath, 384);
8442
9264
  console.log(`Repaired generated config: ${options.destinationPath}`);
8443
9265
  console.log(`Backup: ${backupPath}`);
8444
9266
  return;
8445
9267
  }
8446
- const rendered = renderTemplate(await (0, import_promises6.readFile)(options.templatePath, "utf8"), options.config);
9268
+ const rendered = renderTemplate(await (0, import_promises8.readFile)(options.templatePath, "utf8"), options.config);
8447
9269
  if (options.dryRun) {
8448
9270
  console.log(`Would write ${options.destinationPath}`);
8449
9271
  return;
8450
9272
  }
8451
- await ensureDirectory((0, import_node_path6.dirname)(options.destinationPath), false);
8452
- await (0, import_promises6.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
8453
- await (0, import_promises6.chmod)(options.destinationPath, 384);
9273
+ await ensureDirectory((0, import_node_path7.dirname)(options.destinationPath), false);
9274
+ await (0, import_promises8.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
9275
+ await (0, import_promises8.chmod)(options.destinationPath, 384);
8454
9276
  console.log(`Wrote ${options.destinationPath}`);
8455
9277
  }
8456
9278
  async function installCommandShim(dryRun) {
8457
9279
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
8458
- const shimPath = (0, import_node_path6.join)(binDir, "threadnote");
9280
+ const shimPath = (0, import_node_path7.join)(binDir, "threadnote");
8459
9281
  const existingContent = await readFileIfExists(shimPath);
8460
9282
  if (existingContent && !isManagedCommandShim(existingContent)) {
8461
9283
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -8471,12 +9293,12 @@ async function installCommandShim(dryRun) {
8471
9293
  return;
8472
9294
  }
8473
9295
  await ensureDirectory(binDir, false);
8474
- await (0, import_promises6.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
8475
- await (0, import_promises6.chmod)(shimPath, 493);
9296
+ await (0, import_promises8.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
9297
+ await (0, import_promises8.chmod)(shimPath, 493);
8476
9298
  console.log(`Wrote command shim: ${shimPath}`);
8477
9299
  }
8478
9300
  async function removeCommandShim(dryRun) {
8479
- const shimPath = (0, import_node_path6.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9301
+ const shimPath = (0, import_node_path7.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
8480
9302
  const content = await readFileIfExists(shimPath);
8481
9303
  if (content === void 0) {
8482
9304
  console.log(`Already absent: ${shimPath}`);
@@ -8506,8 +9328,8 @@ async function installUserAgentInstructions(dryRun) {
8506
9328
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
8507
9329
  continue;
8508
9330
  }
8509
- await ensureDirectory((0, import_node_path6.dirname)(targetPath), false);
8510
- await (0, import_promises6.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
9331
+ await ensureDirectory((0, import_node_path7.dirname)(targetPath), false);
9332
+ await (0, import_promises8.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
8511
9333
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
8512
9334
  }
8513
9335
  }
@@ -8536,12 +9358,12 @@ async function removeUserAgentInstructions(dryRun) {
8536
9358
  console.log(`Would update ${targetPath}`);
8537
9359
  continue;
8538
9360
  }
8539
- await (0, import_promises6.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
9361
+ await (0, import_promises8.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
8540
9362
  console.log(`Updated ${targetPath}`);
8541
9363
  }
8542
9364
  }
8543
9365
  async function renderUserAgentInstructionsBlock() {
8544
- const instructions = (await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
9366
+ const instructions = (await (0, import_promises8.readFile)((0, import_node_path7.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
8545
9367
  return `${USER_INSTRUCTIONS_START_MARKER}
8546
9368
  ${instructions}
8547
9369
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -8616,12 +9438,12 @@ function isManagedCommandShim(content) {
8616
9438
  return content.includes(SHIM_MARKER);
8617
9439
  }
8618
9440
  async function installLaunchAgent(config, dryRun) {
8619
- if ((0, import_node_os4.platform)() !== "darwin") {
9441
+ if ((0, import_node_os5.platform)() !== "darwin") {
8620
9442
  throw new Error("launchd autostart is only supported on macOS.");
8621
9443
  }
8622
- const source = (0, import_node_path6.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
9444
+ const source = (0, import_node_path7.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
8623
9445
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
8624
- const rendered = renderTemplate(await (0, import_promises6.readFile)(source, "utf8"), config);
9446
+ const rendered = renderTemplate(await (0, import_promises8.readFile)(source, "utf8"), config);
8625
9447
  if (dryRun) {
8626
9448
  console.log(`Would write ${destination}`);
8627
9449
  console.log(`Would run: launchctl unload ${destination}`);
@@ -8629,9 +9451,9 @@ async function installLaunchAgent(config, dryRun) {
8629
9451
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
8630
9452
  return;
8631
9453
  }
8632
- await ensureDirectory((0, import_node_path6.dirname)(destination), false);
8633
- await ensureDirectory((0, import_node_path6.dirname)(openVikingLogPath(config)), false);
8634
- await (0, import_promises6.writeFile)(destination, rendered, "utf8");
9454
+ await ensureDirectory((0, import_node_path7.dirname)(destination), false);
9455
+ await ensureDirectory((0, import_node_path7.dirname)(openVikingLogPath(config)), false);
9456
+ await (0, import_promises8.writeFile)(destination, rendered, "utf8");
8635
9457
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
8636
9458
  await maybeRun(false, "launchctl", ["load", destination]);
8637
9459
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
@@ -8694,7 +9516,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
8694
9516
  if (typeof parsed.default_user !== "string") {
8695
9517
  return false;
8696
9518
  }
8697
- if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path6.join)(config.agentContextHome, "data")) {
9519
+ if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path7.join)(config.agentContextHome, "data")) {
8698
9520
  return false;
8699
9521
  }
8700
9522
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -8713,259 +9535,6 @@ function parsePackageManager(value) {
8713
9535
  throw new Error(`Invalid package manager: ${value}`);
8714
9536
  }
8715
9537
 
8716
- // src/update.ts
8717
- var import_node_fs4 = require("node:fs");
8718
- var import_promises7 = require("node:fs/promises");
8719
- var import_node_os5 = require("node:os");
8720
- var import_node_path7 = require("node:path");
8721
- var NPM_PACKAGE_NAME = "threadnote";
8722
- var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
8723
- var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
8724
- function parseUpdateRuntime(value) {
8725
- if (value === "auto" || value === "npm" || value === "bun" || value === "deno") {
8726
- return value;
8727
- }
8728
- throw new Error(`Invalid update runtime: ${value}. Expected auto, npm, bun, or deno.`);
8729
- }
8730
- async function maybeNotifyUpdate(config, options = {}) {
8731
- if (isUpdateNotificationDisabled()) {
8732
- return;
8733
- }
8734
- try {
8735
- const info = await getUpdateInfo(config, {
8736
- allowCacheWrite: options.dryRun !== true,
8737
- preferFresh: false,
8738
- registry: updateRegistry()
8739
- });
8740
- if (!info.isUpdateAvailable) {
8741
- return;
8742
- }
8743
- console.log("");
8744
- console.log(`Update available: threadnote ${info.currentVersion} -> ${info.latestVersion}`);
8745
- console.log("Run: threadnote update");
8746
- } catch (_err) {
8747
- return;
8748
- }
8749
- }
8750
- async function runUpdate(config, options) {
8751
- const registry = normalizeRegistry(options.registry ?? updateRegistry());
8752
- const info = await getUpdateInfo(config, {
8753
- allowCacheWrite: options.dryRun !== true,
8754
- preferFresh: true,
8755
- registry
8756
- });
8757
- console.log(`Current version: ${info.currentVersion}`);
8758
- console.log(`Latest version: ${info.latestVersion}`);
8759
- console.log(`Registry: ${info.registry}`);
8760
- if (options.check === true) {
8761
- if (info.isUpdateAvailable) {
8762
- console.log(`Update available. Run: threadnote update`);
8763
- } else {
8764
- console.log(
8765
- compareVersions(info.currentVersion, info.latestVersion) > 0 ? "Current version is newer than npm latest." : "Threadnote is up to date."
8766
- );
8767
- }
8768
- return;
8769
- }
8770
- if (!info.isUpdateAvailable && options.force !== true) {
8771
- console.log("Threadnote is up to date.");
8772
- return;
8773
- }
8774
- const runtime = await resolveUpdateRuntime(options.runtime ?? "auto");
8775
- const updateCommand = updatePackageCommand(runtime, registry);
8776
- await maybeRun(options.dryRun === true, updateCommand.executable, updateCommand.args);
8777
- if (options.repair === false) {
8778
- console.log("Skipping repair because --no-repair was provided.");
8779
- return;
8780
- }
8781
- const threadnoteCommand = await installedThreadnoteCommand(runtime);
8782
- await maybeRun(options.dryRun === true, threadnoteCommand, ["repair"]);
8783
- console.log("Update complete. Restart Cursor, Codex, Claude, or open a fresh agent session so MCP tools reload.");
8784
- }
8785
- async function getUpdateInfo(config, options) {
8786
- const currentVersion = await currentPackageVersion();
8787
- const cached = options.preferFresh ? void 0 : await readFreshCache(config, options.registry);
8788
- const latestVersion = cached?.latestVersion ?? await fetchLatestVersion(options.registry);
8789
- if (!cached && options.allowCacheWrite) {
8790
- await writeUpdateCache(config, { checkedAt: (/* @__PURE__ */ new Date()).toISOString(), latestVersion, registry: options.registry });
8791
- }
8792
- return {
8793
- currentVersion,
8794
- isUpdateAvailable: compareVersions(currentVersion, latestVersion) < 0,
8795
- latestVersion,
8796
- registry: options.registry
8797
- };
8798
- }
8799
- async function currentPackageVersion() {
8800
- const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
8801
- const parsed = JSON.parse(rawPackage);
8802
- if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8803
- throw new Error("Could not read current threadnote package version.");
8804
- }
8805
- return parsed.version;
8806
- }
8807
- async function fetchLatestVersion(registry) {
8808
- const url = new URL(`${NPM_PACKAGE_NAME}/latest`, normalizeRegistry(registry));
8809
- const controller = new AbortController();
8810
- const timeout = setTimeout(() => {
8811
- controller.abort();
8812
- }, 2500);
8813
- try {
8814
- const response = await fetch(url, { headers: { accept: "application/json" }, signal: controller.signal });
8815
- if (!response.ok) {
8816
- throw new Error(`npm registry returned HTTP ${response.status}`);
8817
- }
8818
- const parsed = await response.json();
8819
- if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
8820
- throw new Error("npm registry response did not include a version.");
8821
- }
8822
- return parsed.version;
8823
- } catch (err) {
8824
- throw new Error(`Could not check npm for updates: ${errorMessage(err)}`, { cause: err });
8825
- } finally {
8826
- clearTimeout(timeout);
8827
- }
8828
- }
8829
- async function readFreshCache(config, registry) {
8830
- const rawCache = await readFileIfExists(updateCachePath(config));
8831
- if (!rawCache) {
8832
- return void 0;
8833
- }
8834
- try {
8835
- const parsed = JSON.parse(rawCache);
8836
- if (!isJsonObject(parsed) || typeof parsed.checkedAt !== "string" || typeof parsed.latestVersion !== "string" || parsed.registry !== registry) {
8837
- return void 0;
8838
- }
8839
- const checkedAt = Date.parse(parsed.checkedAt);
8840
- if (!Number.isFinite(checkedAt) || Date.now() - checkedAt > UPDATE_CHECK_TTL_MS) {
8841
- return void 0;
8842
- }
8843
- return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion, registry };
8844
- } catch (_err) {
8845
- return void 0;
8846
- }
8847
- }
8848
- async function writeUpdateCache(config, cache) {
8849
- await ensureDirectory(config.agentContextHome, false);
8850
- await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
8851
- `, { encoding: "utf8", mode: 384 });
8852
- }
8853
- function updateCachePath(config) {
8854
- return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
8855
- }
8856
- async function resolveUpdateRuntime(runtime) {
8857
- if (runtime !== "auto") {
8858
- await requireRuntime(runtime);
8859
- return runtime;
8860
- }
8861
- for (const candidate of ["npm", "bun", "deno"]) {
8862
- if (await findExecutable([candidate])) {
8863
- return candidate;
8864
- }
8865
- }
8866
- throw new Error("Install Node/npm, Bun, or Deno to update threadnote.");
8867
- }
8868
- async function requireRuntime(runtime) {
8869
- if (!await findExecutable([runtime])) {
8870
- throw new Error(`${runtime} was requested but was not found on PATH.`);
8871
- }
8872
- }
8873
- async function installedThreadnoteCommand(runtime) {
8874
- const runtimeBin = await runtimeThreadnoteBin(runtime);
8875
- if (runtimeBin && await isExecutable(runtimeBin)) {
8876
- return runtimeBin;
8877
- }
8878
- return await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
8879
- }
8880
- async function runtimeThreadnoteBin(runtime) {
8881
- if (runtime === "npm") {
8882
- const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
8883
- const prefix = result.stdout.trim();
8884
- return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
8885
- }
8886
- if (runtime === "bun") {
8887
- const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
8888
- const binDir = result.stdout.trim();
8889
- return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
8890
- }
8891
- 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);
8892
- }
8893
- async function isExecutable(path) {
8894
- try {
8895
- await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
8896
- return true;
8897
- } catch (_err) {
8898
- return false;
8899
- }
8900
- }
8901
- function updatePackageCommand(runtime, registry) {
8902
- if (runtime === "npm") {
8903
- return { executable: "npm", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
8904
- }
8905
- if (runtime === "bun") {
8906
- return { executable: "bun", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
8907
- }
8908
- return {
8909
- executable: "env",
8910
- args: [
8911
- `NPM_CONFIG_REGISTRY=${registry}`,
8912
- "deno",
8913
- "install",
8914
- "--global",
8915
- "--force",
8916
- "--name",
8917
- NPM_PACKAGE_NAME,
8918
- "--allow-read",
8919
- "--allow-write",
8920
- "--allow-run",
8921
- "--allow-env",
8922
- "--allow-net",
8923
- `npm:${NPM_PACKAGE_NAME}@latest`
8924
- ]
8925
- };
8926
- }
8927
- function normalizeRegistry(registry) {
8928
- return registry.endsWith("/") ? registry : `${registry}/`;
8929
- }
8930
- function updateRegistry() {
8931
- return normalizeRegistry(process.env.THREADNOTE_NPM_REGISTRY ?? DEFAULT_NPM_REGISTRY);
8932
- }
8933
- function isUpdateNotificationDisabled() {
8934
- return process.env.CI !== void 0 || process.env.NO_UPDATE_NOTIFIER !== void 0 || process.env.THREADNOTE_NO_UPDATE_CHECK !== void 0;
8935
- }
8936
- function compareVersions(currentVersion, latestVersion) {
8937
- const current = parseVersion(currentVersion);
8938
- const latest = parseVersion(latestVersion);
8939
- for (let index = 0; index < 3; index += 1) {
8940
- const difference = current.numbers[index] - latest.numbers[index];
8941
- if (difference !== 0) {
8942
- return difference;
8943
- }
8944
- }
8945
- if (current.prerelease === latest.prerelease) {
8946
- return 0;
8947
- }
8948
- if (current.prerelease === void 0) {
8949
- return 1;
8950
- }
8951
- if (latest.prerelease === void 0) {
8952
- return -1;
8953
- }
8954
- return current.prerelease.localeCompare(latest.prerelease);
8955
- }
8956
- function parseVersion(version) {
8957
- const normalized = version.trim().replace(/^v/, "");
8958
- const [core2, prerelease] = normalized.split("-", 2);
8959
- const parts = core2.split(".").map((part) => Number(part));
8960
- return {
8961
- numbers: [safeVersionNumber(parts[0]), safeVersionNumber(parts[1]), safeVersionNumber(parts[2])],
8962
- prerelease
8963
- };
8964
- }
8965
- function safeVersionNumber(value) {
8966
- return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
8967
- }
8968
-
8969
9538
  // src/threadnote.ts
8970
9539
  async function main() {
8971
9540
  const program2 = new Command();
@@ -8980,14 +9549,17 @@ async function main() {
8980
9549
  await runInstall(config, options);
8981
9550
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
8982
9551
  });
8983
- 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) => {
9552
+ program2.command("update").description("Update the published Threadnote package, then repair local shims and MCP config").option("--check", "Only check whether a newer version is available").option("--dry-run", "Print update and repair commands without running them").option("--force", "Run package-manager update even if this version is already current").option("--registry <url>", "npm registry URL", process.env.THREADNOTE_NPM_REGISTRY).option("--runtime <runtime>", "auto, npm, bun, or deno", parseUpdateRuntime, "auto").option("--no-repair", "Skip threadnote repair after updating the package").option("--no-post-update", "Skip post-update migration prompts").option("--yes", "Accept applicable post-update migrations without prompting").action(async (options) => {
8984
9553
  await runUpdate(getRuntimeConfig(program2), options);
8985
9554
  });
9555
+ program2.command("post-update", { hidden: true }).description("Run packaged post-update migration prompts").requiredOption("--from-version <version>", "Version before update").requiredOption("--to-version <version>", "Version after update").option("--dry-run", "Print post-update actions without running them").option("--yes", "Accept applicable post-update migrations without prompting").action(async (options) => {
9556
+ await runPostUpdate(getRuntimeConfig(program2), options);
9557
+ });
8986
9558
  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(
8987
9559
  "--mcp <clients>",
8988
9560
  "MCP clients to repair: available, all, none, codex, claude, cursor, or comma-separated list",
8989
9561
  "available"
8990
- ).option("--no-start", "Do not start OpenViking if health is failing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).action(async (options) => {
9562
+ ).option("--no-start", "Do not start OpenViking if health is failing").option("--no-post-update", "Skip post-update migration prompts after repair").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).action(async (options) => {
8991
9563
  const config = getRuntimeConfig(program2);
8992
9564
  await runRepair(config, options);
8993
9565
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
@@ -9019,7 +9591,7 @@ async function main() {
9019
9591
  program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex, claude, or cursor").option("--apply", "Actually modify the selected agent config").option("--name <name>", "MCP server name", OPENVIKING_MCP_NAME).option("--native-http", "Install OpenViking native HTTP MCP endpoint instead of the local stdio adapter").option("--scope <scope>", "Claude MCP config scope: user, local, or project", parseClaudeMcpScope, "user").option("--url <url>", "OpenViking native HTTP MCP URL").option("--bearer-token-env-var <name>", "Environment variable containing the local API key").action(async (agent, options) => {
9020
9592
  await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
9021
9593
  });
9022
- 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) => {
9594
+ program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind, "durable").option("--project <name>", "Project/repo/topic namespace for lifecycle-aware storage").option("--replace <uri>", "Supersede an existing viking:// memory after the new memory is stored").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--status <status>", "active, archived, or superseded", parseMemoryStatus, "active").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").option("--topic <name>", "Stable topic name; active memories with the same project/topic update one file").action(async (options) => {
9023
9595
  await runRemember(getRuntimeConfig(program2), options);
9024
9596
  });
9025
9597
  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(
@@ -9030,7 +9602,10 @@ async function main() {
9030
9602
  ).action(async (options) => {
9031
9603
  await runMigrateMemories(getRuntimeConfig(program2), options);
9032
9604
  });
9033
- 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) => {
9605
+ program2.command("migrate-lifecycle").description("Move clear legacy handoff memories into lifecycle-aware archive paths").option("--apply", "Perform the migration; without this, prints a dry run").option("--dry-run", "Print migration actions without writing or removing memories").option("--limit <count>", "Maximum number of legacy handoffs to migrate").action(async (options) => {
9606
+ await runMigrateLifecycle(getRuntimeConfig(program2), options);
9607
+ });
9608
+ program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in exact durable-memory matches").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) => {
9034
9609
  await runRecall(getRuntimeConfig(program2), options);
9035
9610
  });
9036
9611
  program2.command("read").description("Read a viking:// URI returned by recall or list").argument("<uri>", "viking:// URI to read").option("--dry-run", "Print ov command without reading").action(async (uri, options) => {
@@ -9039,9 +9614,12 @@ async function main() {
9039
9614
  program2.command("list").alias("ls").description("List a viking:// directory").argument("[uri]", "viking:// URI to list", "viking://").option("-a, --all", "Show hidden files such as .abstract.md and .overview.md").option("--dry-run", "Print ov command without listing").option("-n, --node-limit <count>", "Maximum number of nodes to list").option("-r, --recursive", "List subdirectories recursively").option("-s, --simple", "Print only paths").action(async (uri, options) => {
9040
9615
  await runList(getRuntimeConfig(program2), uri, options);
9041
9616
  });
9042
- program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").action(async (options) => {
9617
+ program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, gemini, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
9043
9618
  await runHandoff(getRuntimeConfig(program2), options);
9044
9619
  });
9620
+ program2.command("archive").description("Move a memory into the archived lifecycle tree, then remove the original after the archive is stored").argument("<uri>", "viking:// memory URI to archive").option("--dry-run", "Print archive content and ov commands without changing anything").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind).option("--project <name>", "Override inferred project/repo namespace").option("--topic <name>", "Override inferred topic").action(async (uri, options) => {
9621
+ await runArchive(getRuntimeConfig(program2), uri, options);
9622
+ });
9045
9623
  program2.command("forget").description("Remove a viking:// URI from local OpenViking context").argument("<uri>", "viking:// URI to remove").option("--dry-run", "Print ov command without deleting").action(async (uri, options) => {
9046
9624
  await runForget(getRuntimeConfig(program2), uri, options);
9047
9625
  });