threadnote 1.4.4 → 1.5.0

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.
@@ -35519,6 +35519,9 @@ function redactText(content) {
35519
35519
  "$1[REDACTED]"
35520
35520
  ).replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]").replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, "gh_[REDACTED]");
35521
35521
  }
35522
+ function escapeRegExp(value) {
35523
+ return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
35524
+ }
35522
35525
  async function requiredOpenVikingCli() {
35523
35526
  const command2 = await findOpenVikingCli();
35524
35527
  if (!command2) {
@@ -35906,14 +35909,19 @@ function exactRecallTerms(query) {
35906
35909
  "after",
35907
35910
  "agent",
35908
35911
  "anything",
35912
+ "been",
35909
35913
  "branch",
35910
35914
  "case",
35911
35915
  "current",
35916
+ "does",
35912
35917
  "durable",
35913
35918
  "find",
35914
35919
  "feature",
35915
35920
  "features",
35921
+ "from",
35916
35922
  "handoff",
35923
+ "have",
35924
+ "into",
35917
35925
  "issue",
35918
35926
  "issues",
35919
35927
  "knowledge",
@@ -35927,11 +35935,22 @@ function exactRecallTerms(query) {
35927
35935
  "related",
35928
35936
  "search",
35929
35937
  "stored",
35938
+ "than",
35939
+ "that",
35940
+ "them",
35941
+ "then",
35942
+ "they",
35930
35943
  "this",
35931
35944
  "the",
35945
+ "were",
35946
+ "what",
35947
+ "when",
35948
+ "which",
35949
+ "while",
35932
35950
  "with",
35933
35951
  "workspace",
35934
- "worktree"
35952
+ "worktree",
35953
+ "your"
35935
35954
  ]);
35936
35955
  const seen = /* @__PURE__ */ new Set();
35937
35956
  const terms = [];
@@ -35965,6 +35984,12 @@ function exactRecallScopeIntents(query) {
35965
35984
  function isSummarySidecarUri(uri) {
35966
35985
  return /\.(?:overview|abstract)\.md(?:#|$)/.test(uri);
35967
35986
  }
35987
+ function isAgentArtifactPackUri(uri) {
35988
+ return /\/agent-artifacts\/packs\//.test(uri);
35989
+ }
35990
+ function isExcludedRecallUri(uri) {
35991
+ return isSummarySidecarUri(uri) || isAgentArtifactPackUri(uri);
35992
+ }
35968
35993
  function grepUrisFromJson(output) {
35969
35994
  const start = output.search(/^\{/m);
35970
35995
  if (start < 0) {
@@ -35979,7 +36004,7 @@ function grepUrisFromJson(output) {
35979
36004
  }
35980
36005
  const uris = [];
35981
36006
  for (const match of matches) {
35982
- if (isJsonObject(match) && typeof match.uri === "string" && !isSummarySidecarUri(match.uri)) {
36007
+ if (isJsonObject(match) && typeof match.uri === "string" && !isExcludedRecallUri(match.uri)) {
35983
36008
  uris.push(match.uri);
35984
36009
  }
35985
36010
  }
@@ -36021,7 +36046,7 @@ function parseRecallHits(output, options = {}) {
36021
36046
  continue;
36022
36047
  }
36023
36048
  for (const item of items) {
36024
- if (!isJsonObject(item) || typeof item.uri !== "string" || isSummarySidecarUri(item.uri)) {
36049
+ if (!isJsonObject(item) || typeof item.uri !== "string" || isExcludedRecallUri(item.uri)) {
36025
36050
  continue;
36026
36051
  }
36027
36052
  if (options.includeArchived !== true && isArchivedMemoryUri(item.uri)) {
@@ -36080,6 +36105,9 @@ ${hit.snippet}`;
36080
36105
  return kept;
36081
36106
  }
36082
36107
  function categoryForUri(uri) {
36108
+ if (uri.includes("/agent-artifacts/")) {
36109
+ return "skills";
36110
+ }
36083
36111
  if (uri.includes("/memories/")) {
36084
36112
  return "memories";
36085
36113
  }
@@ -36094,6 +36122,36 @@ function contextTypeForCategory(category) {
36094
36122
  }
36095
36123
  return category === "skills" ? "skill" : "resource";
36096
36124
  }
36125
+ var RECALL_EXACT_SLUG_BONUS = 4;
36126
+ function uriSlug(uri) {
36127
+ const withoutExtension = stripAnchor(uri).replace(/\.[a-z0-9]+$/i, "");
36128
+ return withoutExtension.slice(withoutExtension.lastIndexOf("/") + 1).toLowerCase();
36129
+ }
36130
+ function exactTermDocumentFrequency(matches) {
36131
+ const frequency = /* @__PURE__ */ new Map();
36132
+ for (const match of matches) {
36133
+ for (const term of new Set(match.terms.map((term2) => term2.toLowerCase()))) {
36134
+ frequency.set(term, (frequency.get(term) ?? 0) + 1);
36135
+ }
36136
+ }
36137
+ return frequency;
36138
+ }
36139
+ function slugNamesTerm(slug, term) {
36140
+ return new RegExp(`(^|[^a-z0-9])${escapeRegExp(term)}([^a-z0-9]|$)`).test(slug);
36141
+ }
36142
+ function exactMatchStrength(hit, documentFrequency) {
36143
+ if (!hit.exactTerms?.length) {
36144
+ return 0;
36145
+ }
36146
+ const slug = uriSlug(hit.uri);
36147
+ let strength = 0;
36148
+ for (const term of hit.exactTerms) {
36149
+ const normalized = term.toLowerCase();
36150
+ const rarity = 1 / (documentFrequency.get(normalized) ?? 1);
36151
+ strength += rarity * (slugNamesTerm(slug, normalized) ? RECALL_EXACT_SLUG_BONUS : 1);
36152
+ }
36153
+ return strength;
36154
+ }
36097
36155
  function applyExactMatchBoost(hits, exactMatches) {
36098
36156
  if (exactMatches.length === 0) {
36099
36157
  return hits;
@@ -36115,24 +36173,33 @@ function applyExactMatchBoost(hits, exactMatches) {
36115
36173
  uri
36116
36174
  };
36117
36175
  });
36118
- return [...annotated, ...promoted].sort(
36119
- (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (right.exactTerms?.length ?? 0) - (left.exactTerms?.length ?? 0) || right.score - left.score
36176
+ const documentFrequency = exactTermDocumentFrequency(exactMatches);
36177
+ const merged = [...annotated, ...promoted];
36178
+ const combinedRelevanceByUri = /* @__PURE__ */ new Map();
36179
+ for (const hit of merged) {
36180
+ combinedRelevanceByUri.set(stripAnchor(hit.uri), exactMatchStrength(hit, documentFrequency) + hit.score);
36181
+ }
36182
+ return merged.sort(
36183
+ (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (combinedRelevanceByUri.get(stripAnchor(right.uri)) ?? 0) - (combinedRelevanceByUri.get(stripAnchor(left.uri)) ?? 0) || right.score - left.score
36120
36184
  );
36121
36185
  }
36186
+ var RECALL_LOW_CONFIDENCE_NOTE = "\u26A0 No semantically-relevant matches \u2014 the results below only contain the query words (the corpus may not cover this topic).";
36122
36187
  function renderRecallHits(shown, overflow) {
36123
36188
  if (shown.length === 0) {
36124
36189
  return void 0;
36125
36190
  }
36126
36191
  const lines = shown.flatMap((hit, index) => {
36127
36192
  const scorePart = hit.score > 0 ? `score ${hit.score.toFixed(2)}` : void 0;
36128
- const exactPart = hit.exactTerms?.length ? `exact: ${hit.exactTerms.join(", ")}` : void 0;
36193
+ const exactLabel = hit.score > 0 ? "exact" : "keyword-only";
36194
+ const exactPart = hit.exactTerms?.length ? `${exactLabel}: ${hit.exactTerms.join(", ")}` : void 0;
36129
36195
  const head = `${index + 1}. ${[hit.contextType, scorePart, exactPart].filter(Boolean).join(" \xB7 ")} \xB7 ${hit.uri}`;
36130
36196
  return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
36131
36197
  });
36132
36198
  if (overflow > 0) {
36133
36199
  lines.push(`(+${overflow} more \u2014 refine the query or read a URI above)`);
36134
36200
  }
36135
- return lines.join("\n");
36201
+ const noSemanticMatch = shown.every((hit) => hit.score === 0);
36202
+ return (noSemanticMatch ? [RECALL_LOW_CONFIDENCE_NOTE, ...lines] : lines).join("\n");
36136
36203
  }
36137
36204
  var RECALL_CATEGORY_RESERVE = 2;
36138
36205
  function selectShownHits(ranked, limit, reserve) {
@@ -37391,7 +37458,7 @@ async function collectPackMembers(manifestDir, manifest) {
37391
37458
  }
37392
37459
  return [...members.values()].sort((a, b) => compareStrings(a.relativePath, b.relativePath));
37393
37460
  }
37394
- function escapeRegExp(value) {
37461
+ function escapeRegExp2(value) {
37395
37462
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37396
37463
  }
37397
37464
  function tokenizePackPaths(text, rewriteRoots) {
@@ -37399,7 +37466,7 @@ function tokenizePackPaths(text, rewriteRoots) {
37399
37466
  for (const root of rewriteRoots) {
37400
37467
  if (root.length > 0) {
37401
37468
  out = out.replace(
37402
- new RegExp(`(?<![A-Za-z0-9/._~-])${escapeRegExp(root)}(?=[/\\\\]|["'\`\\s),>:\\]};=]|$)`, "g"),
37469
+ new RegExp(`(?<![A-Za-z0-9/._~-])${escapeRegExp2(root)}(?=[/\\\\]|["'\`\\s),>:\\]};=]|$)`, "g"),
37403
37470
  PACK_ROOT_TOKEN
37404
37471
  );
37405
37472
  }
@@ -4202,14 +4202,19 @@ function exactRecallTerms(query) {
4202
4202
  "after",
4203
4203
  "agent",
4204
4204
  "anything",
4205
+ "been",
4205
4206
  "branch",
4206
4207
  "case",
4207
4208
  "current",
4209
+ "does",
4208
4210
  "durable",
4209
4211
  "find",
4210
4212
  "feature",
4211
4213
  "features",
4214
+ "from",
4212
4215
  "handoff",
4216
+ "have",
4217
+ "into",
4213
4218
  "issue",
4214
4219
  "issues",
4215
4220
  "knowledge",
@@ -4223,11 +4228,22 @@ function exactRecallTerms(query) {
4223
4228
  "related",
4224
4229
  "search",
4225
4230
  "stored",
4231
+ "than",
4232
+ "that",
4233
+ "them",
4234
+ "then",
4235
+ "they",
4226
4236
  "this",
4227
4237
  "the",
4238
+ "were",
4239
+ "what",
4240
+ "when",
4241
+ "which",
4242
+ "while",
4228
4243
  "with",
4229
4244
  "workspace",
4230
- "worktree"
4245
+ "worktree",
4246
+ "your"
4231
4247
  ]);
4232
4248
  const seen = /* @__PURE__ */ new Set();
4233
4249
  const terms = [];
@@ -4261,6 +4277,34 @@ function exactRecallScopeIntents(query) {
4261
4277
  function isSummarySidecarUri(uri) {
4262
4278
  return /\.(?:overview|abstract)\.md(?:#|$)/.test(uri);
4263
4279
  }
4280
+ function isAgentArtifactPackUri(uri) {
4281
+ return /\/agent-artifacts\/packs\//.test(uri);
4282
+ }
4283
+ function isExcludedRecallUri(uri) {
4284
+ return isSummarySidecarUri(uri) || isAgentArtifactPackUri(uri);
4285
+ }
4286
+ function memoryUriProjectSegment(uri) {
4287
+ const marker = "/memories/";
4288
+ const at = uri.indexOf(marker);
4289
+ if (at < 0) {
4290
+ return void 0;
4291
+ }
4292
+ let segments = stripAnchor(uri).slice(at + marker.length).split("/").filter(Boolean);
4293
+ if (segments[0] === "shared") {
4294
+ segments = segments.slice(2);
4295
+ }
4296
+ const [kind, , project, ...rest] = segments;
4297
+ if ((kind === "durable" || kind === "handoffs" || kind === "incidents") && project && rest.length > 0) {
4298
+ return project;
4299
+ }
4300
+ return void 0;
4301
+ }
4302
+ function memoryFrontmatterField(content, field) {
4303
+ const blankLine = content.indexOf("\n\n");
4304
+ const header = blankLine < 0 ? content : content.slice(0, blankLine);
4305
+ const match = header.match(new RegExp(`^${escapeRegExp(field)}:[ \\t]*(.+)$`, "m"));
4306
+ return match?.[1]?.trim() || void 0;
4307
+ }
4264
4308
  function grepUrisFromJson(output2) {
4265
4309
  const start = output2.search(/^\{/m);
4266
4310
  if (start < 0) {
@@ -4275,7 +4319,7 @@ function grepUrisFromJson(output2) {
4275
4319
  }
4276
4320
  const uris = [];
4277
4321
  for (const match of matches) {
4278
- if (isJsonObject(match) && typeof match.uri === "string" && !isSummarySidecarUri(match.uri)) {
4322
+ if (isJsonObject(match) && typeof match.uri === "string" && !isExcludedRecallUri(match.uri)) {
4279
4323
  uris.push(match.uri);
4280
4324
  }
4281
4325
  }
@@ -4317,7 +4361,7 @@ function parseRecallHits(output2, options = {}) {
4317
4361
  continue;
4318
4362
  }
4319
4363
  for (const item of items) {
4320
- if (!isJsonObject(item) || typeof item.uri !== "string" || isSummarySidecarUri(item.uri)) {
4364
+ if (!isJsonObject(item) || typeof item.uri !== "string" || isExcludedRecallUri(item.uri)) {
4321
4365
  continue;
4322
4366
  }
4323
4367
  if (options.includeArchived !== true && isArchivedMemoryUri(item.uri)) {
@@ -4376,6 +4420,9 @@ ${hit.snippet}`;
4376
4420
  return kept;
4377
4421
  }
4378
4422
  function categoryForUri(uri) {
4423
+ if (uri.includes("/agent-artifacts/")) {
4424
+ return "skills";
4425
+ }
4379
4426
  if (uri.includes("/memories/")) {
4380
4427
  return "memories";
4381
4428
  }
@@ -4390,6 +4437,36 @@ function contextTypeForCategory(category) {
4390
4437
  }
4391
4438
  return category === "skills" ? "skill" : "resource";
4392
4439
  }
4440
+ var RECALL_EXACT_SLUG_BONUS = 4;
4441
+ function uriSlug(uri) {
4442
+ const withoutExtension = stripAnchor(uri).replace(/\.[a-z0-9]+$/i, "");
4443
+ return withoutExtension.slice(withoutExtension.lastIndexOf("/") + 1).toLowerCase();
4444
+ }
4445
+ function exactTermDocumentFrequency(matches) {
4446
+ const frequency = /* @__PURE__ */ new Map();
4447
+ for (const match of matches) {
4448
+ for (const term of new Set(match.terms.map((term2) => term2.toLowerCase()))) {
4449
+ frequency.set(term, (frequency.get(term) ?? 0) + 1);
4450
+ }
4451
+ }
4452
+ return frequency;
4453
+ }
4454
+ function slugNamesTerm(slug, term) {
4455
+ return new RegExp(`(^|[^a-z0-9])${escapeRegExp(term)}([^a-z0-9]|$)`).test(slug);
4456
+ }
4457
+ function exactMatchStrength(hit, documentFrequency) {
4458
+ if (!hit.exactTerms?.length) {
4459
+ return 0;
4460
+ }
4461
+ const slug = uriSlug(hit.uri);
4462
+ let strength = 0;
4463
+ for (const term of hit.exactTerms) {
4464
+ const normalized = term.toLowerCase();
4465
+ const rarity = 1 / (documentFrequency.get(normalized) ?? 1);
4466
+ strength += rarity * (slugNamesTerm(slug, normalized) ? RECALL_EXACT_SLUG_BONUS : 1);
4467
+ }
4468
+ return strength;
4469
+ }
4393
4470
  function applyExactMatchBoost(hits, exactMatches) {
4394
4471
  if (exactMatches.length === 0) {
4395
4472
  return hits;
@@ -4411,24 +4488,33 @@ function applyExactMatchBoost(hits, exactMatches) {
4411
4488
  uri
4412
4489
  };
4413
4490
  });
4414
- return [...annotated, ...promoted].sort(
4415
- (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (right.exactTerms?.length ?? 0) - (left.exactTerms?.length ?? 0) || right.score - left.score
4491
+ const documentFrequency = exactTermDocumentFrequency(exactMatches);
4492
+ const merged = [...annotated, ...promoted];
4493
+ const combinedRelevanceByUri = /* @__PURE__ */ new Map();
4494
+ for (const hit of merged) {
4495
+ combinedRelevanceByUri.set(stripAnchor(hit.uri), exactMatchStrength(hit, documentFrequency) + hit.score);
4496
+ }
4497
+ return merged.sort(
4498
+ (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (combinedRelevanceByUri.get(stripAnchor(right.uri)) ?? 0) - (combinedRelevanceByUri.get(stripAnchor(left.uri)) ?? 0) || right.score - left.score
4416
4499
  );
4417
4500
  }
4501
+ var RECALL_LOW_CONFIDENCE_NOTE = "\u26A0 No semantically-relevant matches \u2014 the results below only contain the query words (the corpus may not cover this topic).";
4418
4502
  function renderRecallHits(shown, overflow) {
4419
4503
  if (shown.length === 0) {
4420
4504
  return void 0;
4421
4505
  }
4422
4506
  const lines = shown.flatMap((hit, index) => {
4423
4507
  const scorePart = hit.score > 0 ? `score ${hit.score.toFixed(2)}` : void 0;
4424
- const exactPart = hit.exactTerms?.length ? `exact: ${hit.exactTerms.join(", ")}` : void 0;
4508
+ const exactLabel = hit.score > 0 ? "exact" : "keyword-only";
4509
+ const exactPart = hit.exactTerms?.length ? `${exactLabel}: ${hit.exactTerms.join(", ")}` : void 0;
4425
4510
  const head = `${index + 1}. ${[hit.contextType, scorePart, exactPart].filter(Boolean).join(" \xB7 ")} \xB7 ${hit.uri}`;
4426
4511
  return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
4427
4512
  });
4428
4513
  if (overflow > 0) {
4429
4514
  lines.push(`(+${overflow} more \u2014 refine the query or read a URI above)`);
4430
4515
  }
4431
- return lines.join("\n");
4516
+ const noSemanticMatch = shown.every((hit) => hit.score === 0);
4517
+ return (noSemanticMatch ? [RECALL_LOW_CONFIDENCE_NOTE, ...lines] : lines).join("\n");
4432
4518
  }
4433
4519
  var RECALL_CATEGORY_RESERVE = 2;
4434
4520
  function selectShownHits(ranked, limit, reserve) {
@@ -11768,6 +11854,13 @@ async function storeMemory(config, options) {
11768
11854
  console.log(`Updated existing memory in place: ${memoryUri}`);
11769
11855
  }
11770
11856
  }
11857
+ function warnOnSharedProjectDrift(metadata, inferred) {
11858
+ if (inferred?.project && metadata.project && uriSegment(metadata.project) !== inferred.project) {
11859
+ console.log(
11860
+ `WARN keeping shared memory project "${inferred.project}" from its storage path; ignoring requested "${metadata.project}". To change a shared memory's project, forget it and store a new one under the new project.`
11861
+ );
11862
+ }
11863
+ }
11771
11864
  async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
11772
11865
  if (options.metadata.kind !== "durable") {
11773
11866
  throw new Error("Shared memory replacement only supports durable memories.");
@@ -11778,9 +11871,10 @@ async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
11778
11871
  }
11779
11872
  const team = await resolveTeam(config, teamName);
11780
11873
  const inferred = sharedMemoryUriParts(config, targetUri);
11874
+ warnOnSharedProjectDrift(options.metadata, inferred);
11781
11875
  const metadata = {
11782
11876
  ...options.metadata,
11783
- project: options.metadata.project ?? inferred?.project,
11877
+ project: inferred?.project ?? options.metadata.project,
11784
11878
  topic: options.metadata.topic ?? inferred?.topic
11785
11879
  };
11786
11880
  const rawMemory = formatMemoryDocument2(options.title, metadata, options.bodyText);
@@ -13703,6 +13797,7 @@ function isUpdateNotificationDisabled() {
13703
13797
  }
13704
13798
 
13705
13799
  // src/lifecycle.ts
13800
+ var INSTALL_OUTPUT_TAIL_CHARS = 64e3;
13706
13801
  async function runDoctor(config, options) {
13707
13802
  const checks = await collectDoctorChecks(config, options);
13708
13803
  for (const check of checks) {
@@ -13735,6 +13830,7 @@ async function collectDoctorChecks(config, options = {}) {
13735
13830
  checks.push(...await userAgentInstructionsChecks());
13736
13831
  checks.push(await manifestCheck(config.manifestPath));
13737
13832
  checks.push(await recallIndexFreshnessCheck(config));
13833
+ checks.push(await memoryProjectConsistencyCheck(config));
13738
13834
  checks.push(await fileCheck((0, import_node_path14.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
13739
13835
  checks.push(await fileCheck((0, import_node_path14.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
13740
13836
  checks.push(await fileCheck((0, import_node_path14.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
@@ -14427,6 +14523,61 @@ async function recallIndexFreshnessCheck(config) {
14427
14523
  return { name: "recall index freshness", status: "warn", detail: errorMessage(err) };
14428
14524
  }
14429
14525
  }
14526
+ async function memoryProjectConsistencyCheck(config) {
14527
+ const name = "memory project consistency";
14528
+ const memoriesRoot = (0, import_node_path14.join)(
14529
+ config.agentContextHome,
14530
+ "data",
14531
+ "viking",
14532
+ config.account,
14533
+ "user",
14534
+ uriSegment(config.user),
14535
+ "memories"
14536
+ );
14537
+ try {
14538
+ let entries;
14539
+ try {
14540
+ entries = await (0, import_promises12.readdir)(memoriesRoot, { recursive: true });
14541
+ } catch {
14542
+ return { name, status: "ok", detail: "no memories directory yet" };
14543
+ }
14544
+ const mismatches = [];
14545
+ let checked = 0;
14546
+ for (const entry of entries) {
14547
+ if (!entry.endsWith(".md") || isSummarySidecarUri(entry)) {
14548
+ continue;
14549
+ }
14550
+ const uri = `viking://user/${uriSegment(config.user)}/memories/${entry.split(import_node_path14.sep).join("/")}`;
14551
+ const pathProject = memoryUriProjectSegment(uri);
14552
+ if (!pathProject) {
14553
+ continue;
14554
+ }
14555
+ let content;
14556
+ try {
14557
+ content = await (0, import_promises12.readFile)((0, import_node_path14.join)(memoriesRoot, entry), "utf8");
14558
+ } catch {
14559
+ continue;
14560
+ }
14561
+ checked += 1;
14562
+ const frontProject = memoryFrontmatterField(content, "project");
14563
+ if (frontProject && uriSegment(frontProject) !== pathProject) {
14564
+ mismatches.push(`${uri} (frontmatter "${frontProject}" vs path "${pathProject}")`);
14565
+ }
14566
+ }
14567
+ if (mismatches.length === 0) {
14568
+ return { name, status: "ok", detail: `${checked} project-scoped memories consistent` };
14569
+ }
14570
+ const sample = mismatches.slice(0, 3).join("; ");
14571
+ const extra = Math.max(0, mismatches.length - 3);
14572
+ return {
14573
+ name,
14574
+ status: "warn",
14575
+ detail: `${mismatches.length} memory(ies) whose frontmatter project differs from their storage path; re-store under the correct project to fix: ${sample}${extra > 0 ? `, +${extra} more` : ""}`
14576
+ };
14577
+ } catch (err) {
14578
+ return { name, status: "warn", detail: errorMessage(err) };
14579
+ }
14580
+ }
14430
14581
  async function fileCheck(path2, label) {
14431
14582
  return await exists(path2) ? { name: label, status: "ok", detail: path2 } : { name: label, status: "fail", detail: `${path2} missing` };
14432
14583
  }
@@ -14487,32 +14638,169 @@ async function runInstallCommands(config, preferred, force, dryRun) {
14487
14638
  continue;
14488
14639
  }
14489
14640
  console.log(`Running: ${formatShellCommand(installCommand.executable, installCommand.args)}`);
14490
- const exitCode = await runInteractive(installCommand.executable, installCommand.args);
14491
- if (exitCode !== 0) {
14492
- printOpenVikingInstallFailureHelp(installCommand);
14493
- throw new Error(`${formatShellCommand(installCommand.executable, installCommand.args)} exited with ${exitCode}.`);
14641
+ const result = await runInstallCommand(installCommand);
14642
+ if (result.exitCode !== 0) {
14643
+ const commandOutput = `${result.stderr}
14644
+ ${result.stdout}`;
14645
+ const retry = openVikingSourceBuildRetryForArchiveFailure(installCommand, commandOutput);
14646
+ if (retry) {
14647
+ console.error("");
14648
+ console.error(
14649
+ "The prebuilt llama-cpp-python wheel failed ZIP archive validation; retrying with a local source build."
14650
+ );
14651
+ console.error("This avoids the rejected wheel and can take 10-20 minutes.");
14652
+ console.log(`Running: ${formatInstallCommand(retry.command, retry.env)}`);
14653
+ const retryResult = await runInstallCommand(retry.command, retry.env);
14654
+ if (retryResult.exitCode === 0) {
14655
+ continue;
14656
+ }
14657
+ printOpenVikingInstallFailureHelp(retry.command, `${retryResult.stderr}
14658
+ ${retryResult.stdout}`);
14659
+ throw new Error(
14660
+ `${formatInstallCommand(retry.command, retry.env)} exited with ${retryResult.exitCode} after automatic source-build retry.`
14661
+ );
14662
+ }
14663
+ printOpenVikingInstallFailureHelp(installCommand, commandOutput);
14664
+ throw new Error(
14665
+ `${formatShellCommand(installCommand.executable, installCommand.args)} exited with ${result.exitCode}.`
14666
+ );
14494
14667
  }
14495
14668
  }
14496
14669
  }
14497
- function printOpenVikingInstallFailureHelp(failedCommand) {
14498
- console.error("");
14499
- console.error("OpenViking install did not complete.");
14500
- console.error(
14501
- "openviking[local-embed] includes llama-cpp-python, which compiles from source when no prebuilt wheel matches your Python/platform \u2014 that build can run 10-20 minutes and is memory-heavy, so it may be killed by the OS (out of memory) or look stuck."
14502
- );
14503
- console.error("Re-run it directly to see full output without any wrapper timeout:");
14504
- console.error(` ${formatShellCommand(failedCommand.executable, failedCommand.args)}`);
14505
- console.error("Cap memory use during the compile with: CMAKE_BUILD_PARALLEL_LEVEL=2 <command above>");
14670
+ async function runInstallCommand(command2, env = {}) {
14671
+ return new Promise((resolvePromise) => {
14672
+ let stdout2 = "";
14673
+ let stderr = "";
14674
+ let settled = false;
14675
+ const child = (0, import_node_child_process4.spawn)(command2.executable, command2.args, {
14676
+ env: Object.keys(env).length === 0 ? process.env : { ...process.env, ...env },
14677
+ stdio: ["inherit", "pipe", "pipe"]
14678
+ });
14679
+ child.stdout?.on("data", (chunk) => {
14680
+ const text = String(chunk);
14681
+ import_node_process4.stdout.write(text);
14682
+ stdout2 = appendInstallOutputTail(stdout2, text);
14683
+ });
14684
+ child.stderr?.on("data", (chunk) => {
14685
+ const text = String(chunk);
14686
+ import_node_process4.stderr.write(text);
14687
+ stderr = appendInstallOutputTail(stderr, text);
14688
+ });
14689
+ child.on("error", (err) => {
14690
+ if (settled) {
14691
+ return;
14692
+ }
14693
+ settled = true;
14694
+ const message = errorMessage(err);
14695
+ import_node_process4.stderr.write(`${message}
14696
+ `);
14697
+ resolvePromise({ exitCode: 1, stderr: appendInstallOutputTail(stderr, message), stdout: stdout2 });
14698
+ });
14699
+ child.on("close", (code) => {
14700
+ if (settled) {
14701
+ return;
14702
+ }
14703
+ settled = true;
14704
+ resolvePromise({ exitCode: code ?? 1, stderr, stdout: stdout2 });
14705
+ });
14706
+ });
14707
+ }
14708
+ function appendInstallOutputTail(current, chunk) {
14709
+ const next = `${current}${chunk}`;
14710
+ return next.length <= INSTALL_OUTPUT_TAIL_CHARS ? next : next.slice(next.length - INSTALL_OUTPUT_TAIL_CHARS);
14711
+ }
14712
+ function printOpenVikingInstallFailureHelp(failedCommand, commandOutput) {
14713
+ for (const line of openVikingInstallFailureHelpLines(failedCommand, commandOutput)) {
14714
+ console.error(line);
14715
+ }
14716
+ }
14717
+ function openVikingInstallFailureHelpLines(failedCommand, commandOutput = "") {
14718
+ if (isLlamaWheelArchiveExtractionFailure(commandOutput)) {
14719
+ return [
14720
+ "",
14721
+ "OpenViking install did not complete.",
14722
+ "The prebuilt llama-cpp-python wheel failed ZIP archive validation. Threadnote only falls back to a source build automatically when the failed command came from a prebuilt wheel index."
14723
+ ];
14724
+ }
14725
+ const lines = [
14726
+ "",
14727
+ "OpenViking install did not complete.",
14728
+ "openviking[local-embed] includes llama-cpp-python, which compiles from source when no prebuilt wheel matches your Python/platform \u2014 that build can run 10-20 minutes and is memory-heavy, so it may be killed by the OS (out of memory) or look stuck.",
14729
+ "The package-manager output above contains the underlying build or download error."
14730
+ ];
14506
14731
  if (failedCommand.executable === "uv") {
14507
14732
  if (failedCommand.args.includes("--python")) {
14508
- console.error(
14509
- "If uv could not fetch a managed CPython (offline or restricted network), drop the version pin and retry: THREADNOTE_OPENVIKING_PYTHON= threadnote install --force"
14733
+ lines.push(
14734
+ "If uv could not fetch managed CPython, Threadnote cannot complete the local install until that Python download is available."
14510
14735
  );
14511
14736
  }
14512
- console.error("If it was killed mid-build, clear the partial install first, then retry:");
14513
- console.error(" uv cache clean");
14514
- console.error(' rm -rf "$(uv tool dir)/openviking"');
14515
14737
  }
14738
+ return lines;
14739
+ }
14740
+ function isLlamaWheelArchiveExtractionFailure(output2) {
14741
+ const normalized = output2.toLowerCase();
14742
+ return (normalized.includes("llama-cpp-python") || normalized.includes("llama_cpp_python")) && (normalized.includes("failed to extract archive") || normalized.includes("zip file contains trailing contents after the end-of-central-directory record"));
14743
+ }
14744
+ function openVikingSourceBuildRetryForArchiveFailure(failedCommand, commandOutput) {
14745
+ if (!isLlamaWheelArchiveExtractionFailure(commandOutput)) {
14746
+ return void 0;
14747
+ }
14748
+ const command2 = withoutExtraIndexUrl(failedCommand);
14749
+ if (!command2) {
14750
+ return void 0;
14751
+ }
14752
+ return { command: command2, env: sourceBuildEnvironment(failedCommand) };
14753
+ }
14754
+ function withoutExtraIndexUrl(command2) {
14755
+ const args = [];
14756
+ let changed = false;
14757
+ for (let index = 0; index < command2.args.length; index += 1) {
14758
+ const arg = command2.args[index];
14759
+ const next = command2.args[index + 1];
14760
+ if (arg === "--extra-index-url" && next !== void 0) {
14761
+ changed = true;
14762
+ index += 1;
14763
+ continue;
14764
+ }
14765
+ if (arg === "--pip-args" && next?.includes("--extra-index-url") === true) {
14766
+ changed = true;
14767
+ index += 1;
14768
+ continue;
14769
+ }
14770
+ args.push(arg);
14771
+ }
14772
+ return changed ? { ...command2, args } : void 0;
14773
+ }
14774
+ function formatInstallCommand(command2, env = {}) {
14775
+ return [...formatEnvironmentAssignments(env), formatShellCommand(command2.executable, command2.args)].join(" ");
14776
+ }
14777
+ function formatEnvironmentAssignments(env) {
14778
+ return Object.entries(env).map(
14779
+ ([key, value]) => key === "CMAKE_ARGS" ? `${key}="${value.replaceAll('"', '\\"')}"` : `${key}=${shellQuote(value)}`
14780
+ );
14781
+ }
14782
+ function sourceBuildEnvironment(command2) {
14783
+ const env = {};
14784
+ if (extraIndexUrl(command2)?.replace(/\/+$/, "").toLowerCase().endsWith("/metal") === true) {
14785
+ env.CMAKE_ARGS = "-DGGML_METAL=on";
14786
+ }
14787
+ env.CMAKE_BUILD_PARALLEL_LEVEL = "2";
14788
+ return env;
14789
+ }
14790
+ function extraIndexUrl(command2) {
14791
+ for (let index = 0; index < command2.args.length; index += 1) {
14792
+ const arg = command2.args[index];
14793
+ if (arg === "--extra-index-url") {
14794
+ return command2.args[index + 1];
14795
+ }
14796
+ if (arg === "--pip-args") {
14797
+ const match = command2.args[index + 1]?.match(/(?:^|\s)--extra-index-url\s+(\S+)/);
14798
+ if (match) {
14799
+ return match[1];
14800
+ }
14801
+ }
14802
+ }
14803
+ return void 0;
14516
14804
  }
14517
14805
  async function offerToInstallUv() {
14518
14806
  if (import_node_process4.stdin.isTTY !== true || import_node_process4.stdout.isTTY !== true) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",
@@ -59,7 +59,7 @@
59
59
  "doctor": "npm run build --silent && node ./bin/threadnote.cjs doctor",
60
60
  "mcp-server": "npm run build --silent && node ./bin/threadnote-mcp-server.cjs",
61
61
  "pack:dry-run": "npm pack --dry-run",
62
- "typecheck": "tsc --noEmit && tsc -p test/tsconfig.json --noEmit"
62
+ "typecheck": "node ./node_modules/typescript-7/bin/tsc --noEmit && node ./node_modules/typescript-7/bin/tsc -p test/tsconfig.json --noEmit"
63
63
  },
64
64
  "engines": {
65
65
  "node": ">=20"
@@ -68,7 +68,7 @@
68
68
  "@eslint/js": "^10.0.1",
69
69
  "@modelcontextprotocol/sdk": "^1.29.0",
70
70
  "@types/js-yaml": "^4.0.9",
71
- "@types/node": "^25.6.0",
71
+ "@types/node": "^26.0.1",
72
72
  "@types/react": "^19.2.16",
73
73
  "@types/react-dom": "^19.2.3",
74
74
  "@vitest/coverage-v8": "^4.1.7",
@@ -83,6 +83,7 @@
83
83
  "react-dom": "^19.2.7",
84
84
  "tsx": "^4.21.0",
85
85
  "typescript": "^6.0.3",
86
+ "typescript-7": "npm:typescript@7.0.1-rc",
86
87
  "typescript-eslint": "^8.59.2",
87
88
  "vitest": "^4.1.7",
88
89
  "zod": "^4.4.3"