threadnote 1.1.6 → 1.2.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.
package/.threadnoteignore CHANGED
@@ -1,5 +1,7 @@
1
1
  .git/
2
2
  node_modules/
3
+ .claude/worktrees/
4
+ .worktrees/
3
5
  bazel-*/
4
6
  dist/
5
7
  build/
@@ -36193,6 +36193,7 @@ function grepUrisFromJson(output) {
36193
36193
  }
36194
36194
  }
36195
36195
  var RECALL_CATEGORY_ORDER = ["memories", "resources", "skills"];
36196
+ var RECALL_PROMOTED_EXACT_SCORE = 0;
36196
36197
  function recallSnippet(value) {
36197
36198
  if (typeof value !== "string") {
36198
36199
  return "";
@@ -36244,11 +36245,14 @@ function parseRecallHits(output, options = {}) {
36244
36245
  return [];
36245
36246
  }
36246
36247
  }
36248
+ function stripAnchor(uri) {
36249
+ return uri.replace(/#.*$/, "");
36250
+ }
36247
36251
  function mergeRecallHits(passes) {
36248
36252
  const byDocument = /* @__PURE__ */ new Map();
36249
36253
  for (const pass of passes) {
36250
36254
  for (const hit of pass) {
36251
- const documentUri = hit.uri.replace(/#.*$/, "");
36255
+ const documentUri = stripAnchor(hit.uri);
36252
36256
  const existing = byDocument.get(documentUri);
36253
36257
  if (!existing || hit.score > existing.score) {
36254
36258
  byDocument.set(documentUri, { ...hit, uri: documentUri });
@@ -36256,23 +36260,120 @@ function mergeRecallHits(passes) {
36256
36260
  }
36257
36261
  }
36258
36262
  return [...byDocument.values()].sort(
36259
- (left, right) => RECALL_CATEGORY_ORDER.indexOf(left.category) - RECALL_CATEGORY_ORDER.indexOf(right.category) || right.score - left.score
36263
+ (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || right.score - left.score
36264
+ );
36265
+ }
36266
+ function recallCategoryRank(category) {
36267
+ const index = RECALL_CATEGORY_ORDER.indexOf(category);
36268
+ return index === -1 ? RECALL_CATEGORY_ORDER.length : index;
36269
+ }
36270
+ function dedupeByContent(hits) {
36271
+ const seen = /* @__PURE__ */ new Set();
36272
+ const kept = [];
36273
+ for (const hit of hits) {
36274
+ if (hit.category !== "memories" && hit.snippet.length > 0) {
36275
+ const key = `${hit.category}
36276
+ ${hit.snippet}`;
36277
+ if (seen.has(key)) {
36278
+ continue;
36279
+ }
36280
+ seen.add(key);
36281
+ }
36282
+ kept.push(hit);
36283
+ }
36284
+ return kept;
36285
+ }
36286
+ function categoryForUri(uri) {
36287
+ if (uri.includes("/memories/")) {
36288
+ return "memories";
36289
+ }
36290
+ if (uri.startsWith("viking://resources/agent-skills/")) {
36291
+ return "skills";
36292
+ }
36293
+ return "resources";
36294
+ }
36295
+ function contextTypeForCategory(category) {
36296
+ if (category === "memories") {
36297
+ return "memory";
36298
+ }
36299
+ return category === "skills" ? "skill" : "resource";
36300
+ }
36301
+ function applyExactMatchBoost(hits, exactMatches) {
36302
+ if (exactMatches.length === 0) {
36303
+ return hits;
36304
+ }
36305
+ const termsByUri = new Map(exactMatches.map((match) => [stripAnchor(match.uri), match.terms]));
36306
+ const annotated = hits.map((hit) => {
36307
+ const terms = termsByUri.get(stripAnchor(hit.uri));
36308
+ return terms ? { ...hit, exactTerms: terms } : hit;
36309
+ });
36310
+ const present = new Set(annotated.map((hit) => stripAnchor(hit.uri)));
36311
+ const promoted = [...termsByUri.keys()].filter((uri) => !present.has(uri)).map((uri) => {
36312
+ const category = categoryForUri(uri);
36313
+ return {
36314
+ category,
36315
+ contextType: contextTypeForCategory(category),
36316
+ exactTerms: termsByUri.get(uri) ?? [],
36317
+ score: RECALL_PROMOTED_EXACT_SCORE,
36318
+ snippet: "",
36319
+ uri
36320
+ };
36321
+ });
36322
+ return [...annotated, ...promoted].sort(
36323
+ (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (right.exactTerms?.length ?? 0) - (left.exactTerms?.length ?? 0) || right.score - left.score
36260
36324
  );
36261
36325
  }
36262
- function formatRecallHits(hits, maxHits) {
36263
- if (hits.length === 0) {
36326
+ function renderRecallHits(shown, overflow) {
36327
+ if (shown.length === 0) {
36264
36328
  return void 0;
36265
36329
  }
36266
- const shown = hits.slice(0, maxHits);
36267
36330
  const lines = shown.flatMap((hit, index) => {
36268
- const head = `${index + 1}. ${hit.contextType} \xB7 score ${hit.score.toFixed(2)} \xB7 ${hit.uri}`;
36331
+ const scorePart = hit.score > 0 ? `score ${hit.score.toFixed(2)}` : void 0;
36332
+ const exactPart = hit.exactTerms?.length ? `exact: ${hit.exactTerms.join(", ")}` : void 0;
36333
+ const head = `${index + 1}. ${[hit.contextType, scorePart, exactPart].filter(Boolean).join(" \xB7 ")} \xB7 ${hit.uri}`;
36269
36334
  return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
36270
36335
  });
36271
- if (hits.length > maxHits) {
36272
- lines.push(`(+${hits.length - maxHits} more \u2014 refine the query or read a URI above)`);
36336
+ if (overflow > 0) {
36337
+ lines.push(`(+${overflow} more \u2014 refine the query or read a URI above)`);
36273
36338
  }
36274
36339
  return lines.join("\n");
36275
36340
  }
36341
+ var RECALL_CATEGORY_RESERVE = 2;
36342
+ function selectShownHits(ranked, limit, reserve) {
36343
+ if (ranked.length <= limit) {
36344
+ return ranked;
36345
+ }
36346
+ const selected = /* @__PURE__ */ new Set();
36347
+ for (const category of RECALL_CATEGORY_ORDER) {
36348
+ let taken = 0;
36349
+ for (const hit of ranked) {
36350
+ if (selected.size >= limit || taken >= reserve) {
36351
+ break;
36352
+ }
36353
+ if (hit.category === category && !selected.has(hit.uri)) {
36354
+ selected.add(hit.uri);
36355
+ taken += 1;
36356
+ }
36357
+ }
36358
+ }
36359
+ for (const hit of ranked) {
36360
+ if (selected.size >= limit) {
36361
+ break;
36362
+ }
36363
+ selected.add(hit.uri);
36364
+ }
36365
+ return ranked.filter((hit) => selected.has(hit.uri));
36366
+ }
36367
+ function buildRecallSections(passes, exactMatches, limit) {
36368
+ const ranked = dedupeByContent(applyExactMatchBoost(mergeRecallHits(passes), exactMatches));
36369
+ const shown = selectShownHits(ranked, limit, RECALL_CATEGORY_RESERVE);
36370
+ const shownUris = new Set(shown.map((hit) => stripAnchor(hit.uri)));
36371
+ return {
36372
+ exactTail: formatExactMatchPointers(exactMatches.filter((match) => !shownUris.has(stripAnchor(match.uri)))),
36373
+ ranked,
36374
+ semanticSection: renderRecallHits(shown, ranked.length - shown.length)
36375
+ };
36376
+ }
36276
36377
  function exactMemoryScopeUris(params) {
36277
36378
  const { agentMemoriesUri, includeArchived, intents, projectName, projectResourceUri, userBase } = params;
36278
36379
  const durable = projectName ? `${userBase}/durable/projects/${projectName}` : `${userBase}/durable/projects`;
@@ -38457,14 +38558,14 @@ function registerTools(server, config2) {
38457
38558
  "grep",
38458
38559
  {
38459
38560
  annotations: { readOnlyHint: true, destructiveHint: false },
38460
- description: "Run exact text search in OpenViking.",
38561
+ description: "Run exact text search in OpenViking. Defaults to your memories subtree when uri is omitted (OpenViking grep requires a scope).",
38461
38562
  inputSchema: {
38462
38563
  caseInsensitive: external_exports.boolean().optional().describe("Case-insensitive search"),
38463
38564
  case_insensitive: external_exports.boolean().optional().describe("Case-insensitive search"),
38464
38565
  nodeLimit: external_exports.number().int().positive().max(1e3).optional().describe("Maximum result count"),
38465
38566
  node_limit: external_exports.number().int().positive().max(1e3).optional().describe("Maximum result count"),
38466
38567
  pattern: external_exports.string().optional().describe("Required text or regex pattern"),
38467
- uri: external_exports.string().optional().describe("Optional viking:// subtree")
38568
+ uri: external_exports.string().optional().describe("Optional viking:// subtree (defaults to your memories root)")
38468
38569
  }
38469
38570
  },
38470
38571
  async ({ caseInsensitive, case_insensitive, nodeLimit, node_limit, pattern, uri }) => {
@@ -38480,7 +38581,7 @@ function registerTools(server, config2) {
38480
38581
  case_insensitive: caseInsensitive ?? case_insensitive,
38481
38582
  node_limit: nodeLimit ?? node_limit,
38482
38583
  pattern: checkedPattern.value,
38483
- uri: checkedUri.value
38584
+ uri: checkedUri.value ?? `viking://user/${uriSegment2(config2.user)}/memories`
38484
38585
  });
38485
38586
  }
38486
38587
  );
@@ -39045,7 +39146,8 @@ async function runRecallTool(config2, params) {
39045
39146
  passes.push(seeded.hits);
39046
39147
  }
39047
39148
  const sections = [];
39048
- const semanticSection = formatRecallHits(mergeRecallHits(passes), params.nodeLimit ?? 12);
39149
+ const exactMatches = await collectExactMemoryMatches(config2, query, params.includeArchived, project);
39150
+ const { semanticSection, exactTail } = buildRecallSections(passes, exactMatches, params.nodeLimit ?? 12);
39049
39151
  if (semanticSection) {
39050
39152
  sections.push(semanticSection);
39051
39153
  } else if (!base.ok) {
@@ -39054,9 +39156,8 @@ async function runRecallTool(config2, params) {
39054
39156
  if (indexRepairMessages.length > 0) {
39055
39157
  sections.push(indexRepairMessages.join("\n"));
39056
39158
  }
39057
- const exactMatches = await exactMemoryMatchesText(config2, query, params.includeArchived, project);
39058
- if (exactMatches) {
39059
- sections.push(exactMatches);
39159
+ if (exactTail) {
39160
+ sections.push(exactTail);
39060
39161
  }
39061
39162
  const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
39062
39163
  if (hygieneHints) {
@@ -39103,14 +39204,14 @@ async function recallHygieneHintsSection(config2, recallText) {
39103
39204
  const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
39104
39205
  return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
39105
39206
  }
39106
- async function exactMemoryMatchesText(config2, query, includeArchived, project) {
39207
+ async function collectExactMemoryMatches(config2, query, includeArchived, project) {
39107
39208
  const terms = exactRecallTerms(query);
39108
39209
  if (terms.length === 0) {
39109
- return void 0;
39210
+ return [];
39110
39211
  }
39111
39212
  const ov = await requiredOpenVikingCli2();
39112
39213
  const scopes = exactMemoryScopes(config2, includeArchived, query, project);
39113
- const matches = await collectExactMatches(terms, scopes, async (term, scope) => {
39214
+ return collectExactMatches(terms, scopes, async (term, scope) => {
39114
39215
  const result = await runCommand(
39115
39216
  ov,
39116
39217
  withIdentity2(config2, ["grep", term, "--uri", scope, "--node-limit", "5", "--output", "json"]),
@@ -39118,7 +39219,6 @@ async function exactMemoryMatchesText(config2, query, includeArchived, project)
39118
39219
  );
39119
39220
  return result.exitCode === 0 ? result.stdout : void 0;
39120
39221
  });
39121
- return formatExactMatchPointers(matches);
39122
39222
  }
39123
39223
  function registerReadTool(server, config2, name, description) {
39124
39224
  server.registerTool(
@@ -4349,6 +4349,7 @@ function grepUrisFromJson(output2) {
4349
4349
  }
4350
4350
  }
4351
4351
  var RECALL_CATEGORY_ORDER = ["memories", "resources", "skills"];
4352
+ var RECALL_PROMOTED_EXACT_SCORE = 0;
4352
4353
  function recallSnippet(value) {
4353
4354
  if (typeof value !== "string") {
4354
4355
  return "";
@@ -4400,11 +4401,14 @@ function parseRecallHits(output2, options = {}) {
4400
4401
  return [];
4401
4402
  }
4402
4403
  }
4404
+ function stripAnchor(uri) {
4405
+ return uri.replace(/#.*$/, "");
4406
+ }
4403
4407
  function mergeRecallHits(passes) {
4404
4408
  const byDocument = /* @__PURE__ */ new Map();
4405
4409
  for (const pass of passes) {
4406
4410
  for (const hit of pass) {
4407
- const documentUri = hit.uri.replace(/#.*$/, "");
4411
+ const documentUri = stripAnchor(hit.uri);
4408
4412
  const existing = byDocument.get(documentUri);
4409
4413
  if (!existing || hit.score > existing.score) {
4410
4414
  byDocument.set(documentUri, { ...hit, uri: documentUri });
@@ -4412,23 +4416,120 @@ function mergeRecallHits(passes) {
4412
4416
  }
4413
4417
  }
4414
4418
  return [...byDocument.values()].sort(
4415
- (left, right) => RECALL_CATEGORY_ORDER.indexOf(left.category) - RECALL_CATEGORY_ORDER.indexOf(right.category) || right.score - left.score
4419
+ (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || right.score - left.score
4416
4420
  );
4417
4421
  }
4418
- function formatRecallHits(hits, maxHits) {
4419
- if (hits.length === 0) {
4422
+ function recallCategoryRank(category) {
4423
+ const index = RECALL_CATEGORY_ORDER.indexOf(category);
4424
+ return index === -1 ? RECALL_CATEGORY_ORDER.length : index;
4425
+ }
4426
+ function dedupeByContent(hits) {
4427
+ const seen = /* @__PURE__ */ new Set();
4428
+ const kept = [];
4429
+ for (const hit of hits) {
4430
+ if (hit.category !== "memories" && hit.snippet.length > 0) {
4431
+ const key = `${hit.category}
4432
+ ${hit.snippet}`;
4433
+ if (seen.has(key)) {
4434
+ continue;
4435
+ }
4436
+ seen.add(key);
4437
+ }
4438
+ kept.push(hit);
4439
+ }
4440
+ return kept;
4441
+ }
4442
+ function categoryForUri(uri) {
4443
+ if (uri.includes("/memories/")) {
4444
+ return "memories";
4445
+ }
4446
+ if (uri.startsWith("viking://resources/agent-skills/")) {
4447
+ return "skills";
4448
+ }
4449
+ return "resources";
4450
+ }
4451
+ function contextTypeForCategory(category) {
4452
+ if (category === "memories") {
4453
+ return "memory";
4454
+ }
4455
+ return category === "skills" ? "skill" : "resource";
4456
+ }
4457
+ function applyExactMatchBoost(hits, exactMatches) {
4458
+ if (exactMatches.length === 0) {
4459
+ return hits;
4460
+ }
4461
+ const termsByUri = new Map(exactMatches.map((match) => [stripAnchor(match.uri), match.terms]));
4462
+ const annotated = hits.map((hit) => {
4463
+ const terms = termsByUri.get(stripAnchor(hit.uri));
4464
+ return terms ? { ...hit, exactTerms: terms } : hit;
4465
+ });
4466
+ const present = new Set(annotated.map((hit) => stripAnchor(hit.uri)));
4467
+ const promoted = [...termsByUri.keys()].filter((uri) => !present.has(uri)).map((uri) => {
4468
+ const category = categoryForUri(uri);
4469
+ return {
4470
+ category,
4471
+ contextType: contextTypeForCategory(category),
4472
+ exactTerms: termsByUri.get(uri) ?? [],
4473
+ score: RECALL_PROMOTED_EXACT_SCORE,
4474
+ snippet: "",
4475
+ uri
4476
+ };
4477
+ });
4478
+ return [...annotated, ...promoted].sort(
4479
+ (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (right.exactTerms?.length ?? 0) - (left.exactTerms?.length ?? 0) || right.score - left.score
4480
+ );
4481
+ }
4482
+ function renderRecallHits(shown, overflow) {
4483
+ if (shown.length === 0) {
4420
4484
  return void 0;
4421
4485
  }
4422
- const shown = hits.slice(0, maxHits);
4423
4486
  const lines = shown.flatMap((hit, index) => {
4424
- const head = `${index + 1}. ${hit.contextType} \xB7 score ${hit.score.toFixed(2)} \xB7 ${hit.uri}`;
4487
+ const scorePart = hit.score > 0 ? `score ${hit.score.toFixed(2)}` : void 0;
4488
+ const exactPart = hit.exactTerms?.length ? `exact: ${hit.exactTerms.join(", ")}` : void 0;
4489
+ const head = `${index + 1}. ${[hit.contextType, scorePart, exactPart].filter(Boolean).join(" \xB7 ")} \xB7 ${hit.uri}`;
4425
4490
  return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
4426
4491
  });
4427
- if (hits.length > maxHits) {
4428
- lines.push(`(+${hits.length - maxHits} more \u2014 refine the query or read a URI above)`);
4492
+ if (overflow > 0) {
4493
+ lines.push(`(+${overflow} more \u2014 refine the query or read a URI above)`);
4429
4494
  }
4430
4495
  return lines.join("\n");
4431
4496
  }
4497
+ var RECALL_CATEGORY_RESERVE = 2;
4498
+ function selectShownHits(ranked, limit, reserve) {
4499
+ if (ranked.length <= limit) {
4500
+ return ranked;
4501
+ }
4502
+ const selected = /* @__PURE__ */ new Set();
4503
+ for (const category of RECALL_CATEGORY_ORDER) {
4504
+ let taken = 0;
4505
+ for (const hit of ranked) {
4506
+ if (selected.size >= limit || taken >= reserve) {
4507
+ break;
4508
+ }
4509
+ if (hit.category === category && !selected.has(hit.uri)) {
4510
+ selected.add(hit.uri);
4511
+ taken += 1;
4512
+ }
4513
+ }
4514
+ }
4515
+ for (const hit of ranked) {
4516
+ if (selected.size >= limit) {
4517
+ break;
4518
+ }
4519
+ selected.add(hit.uri);
4520
+ }
4521
+ return ranked.filter((hit) => selected.has(hit.uri));
4522
+ }
4523
+ function buildRecallSections(passes, exactMatches, limit) {
4524
+ const ranked = dedupeByContent(applyExactMatchBoost(mergeRecallHits(passes), exactMatches));
4525
+ const shown = selectShownHits(ranked, limit, RECALL_CATEGORY_RESERVE);
4526
+ const shownUris = new Set(shown.map((hit) => stripAnchor(hit.uri)));
4527
+ return {
4528
+ exactTail: formatExactMatchPointers(exactMatches.filter((match) => !shownUris.has(stripAnchor(match.uri)))),
4529
+ ranked,
4530
+ semanticSection: renderRecallHits(shown, ranked.length - shown.length)
4531
+ };
4532
+ }
4432
4533
  function exactMemoryScopeUris(params) {
4433
4534
  const { agentMemoriesUri, includeArchived, intents, projectName, projectResourceUri, userBase } = params;
4434
4535
  const durable = projectName ? `${userBase}/durable/projects/${projectName}` : `${userBase}/durable/projects`;
@@ -10478,19 +10579,17 @@ async function runRecall(config, options) {
10478
10579
  passes.push(await recallSearchHits(config, ov, searchArgs(seededUri), { dryRun, includeArchived }));
10479
10580
  }
10480
10581
  const recallOutputs = [];
10481
- const semanticSection = formatRecallHits(mergeRecallHits(passes), nodeLimit ?? 12);
10582
+ const exactMatches = await collectExactMemoryMatches(config, ov, query, { dryRun, includeArchived, project });
10583
+ const { semanticSection, exactTail } = buildRecallSections(passes, exactMatches, nodeLimit ?? 12);
10482
10584
  if (semanticSection) {
10483
10585
  console.log(`
10484
10586
  ${semanticSection}`);
10485
10587
  recallOutputs.push(semanticSection);
10486
10588
  }
10487
- const exactOutput = await printExactMemoryMatches(config, ov, query, {
10488
- dryRun,
10489
- includeArchived,
10490
- project
10491
- });
10492
- if (exactOutput) {
10493
- recallOutputs.push(exactOutput);
10589
+ if (exactTail) {
10590
+ console.log(`
10591
+ ${exactTail}`);
10592
+ recallOutputs.push(exactTail);
10494
10593
  }
10495
10594
  await printRecallHygieneNudges(config, recallOutputs.join("\n"));
10496
10595
  }
@@ -10854,10 +10953,10 @@ function hasAgentSkillCatalogIntent(query) {
10854
10953
  }
10855
10954
  return /\b(find|list|show|search|recall|use|choose|select)\b.{0,48}\bskills?\b/.test(normalized) || /\bskills?\b.{0,48}\b(for|to|that|which|about)\b/.test(normalized);
10856
10955
  }
10857
- async function printExactMemoryMatches(config, ov, query, options) {
10956
+ async function collectExactMemoryMatches(config, ov, query, options) {
10858
10957
  const terms = exactRecallTerms(query);
10859
10958
  if (terms.length === 0) {
10860
- return void 0;
10959
+ return [];
10861
10960
  }
10862
10961
  const scopes = exactMemoryScopes(config, options.includeArchived, query, options.project);
10863
10962
  const grepArgs = (term, scope) => withIdentity(config, ["grep", term, "--uri", scope, "--node-limit", "5", "--output", "json"]);
@@ -10865,19 +10964,12 @@ async function printExactMemoryMatches(config, ov, query, options) {
10865
10964
  const planned = terms.flatMap((term) => scopes.map((scope) => formatShellCommand(ov, grepArgs(term, scope))));
10866
10965
  console.log("\nExact memory/resource matches:");
10867
10966
  console.log(planned.join("\n"));
10868
- return planned.join("\n");
10967
+ return [];
10869
10968
  }
10870
- const matches = await collectExactMatches(terms, scopes, async (term, scope) => {
10969
+ return collectExactMatches(terms, scopes, async (term, scope) => {
10871
10970
  const result = await runCommand(ov, grepArgs(term, scope), { allowFailure: true });
10872
10971
  return result.exitCode === 0 ? result.stdout : void 0;
10873
10972
  });
10874
- const text = formatExactMatchPointers(matches);
10875
- if (!text) {
10876
- return void 0;
10877
- }
10878
- console.log(`
10879
- ${text}`);
10880
- return text;
10881
10973
  }
10882
10974
  async function storeMemory(config, options) {
10883
10975
  if (options.replaceUri) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",