threadnote 1.1.5 → 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/
package/README.md CHANGED
@@ -12,6 +12,14 @@ It is intentionally scoped to curated docs, memories, skills, and handoffs.
12
12
  **Walkthrough:** https://kashkovsky.github.io/threadnote/
13
13
  **Wiki:** https://github.com/Kashkovsky/threadnote/wiki
14
14
 
15
+ ## Quickstart
16
+
17
+ ```bash
18
+ curl -fsSL https://raw.githubusercontent.com/Kashkovsky/threadnote/main/scripts/install.sh | sh
19
+ threadnote mcp-install claude --apply # or codex / cursor / copilot
20
+ threadnote doctor --dry-run
21
+ ```
22
+
15
23
  ## Real-World Uses
16
24
 
17
25
  **Want to continue work in a fresh agent session?**
@@ -36192,6 +36192,8 @@ function grepUrisFromJson(output) {
36192
36192
  return [];
36193
36193
  }
36194
36194
  }
36195
+ var RECALL_CATEGORY_ORDER = ["memories", "resources", "skills"];
36196
+ var RECALL_PROMOTED_EXACT_SCORE = 0;
36195
36197
  function recallSnippet(value) {
36196
36198
  if (typeof value !== "string") {
36197
36199
  return "";
@@ -36217,7 +36219,7 @@ function parseRecallHits(output, options = {}) {
36217
36219
  return [];
36218
36220
  }
36219
36221
  const hits = [];
36220
- for (const key of ["memories", "resources", "skills"]) {
36222
+ for (const key of RECALL_CATEGORY_ORDER) {
36221
36223
  const items = result[key];
36222
36224
  if (!Array.isArray(items)) {
36223
36225
  continue;
@@ -36230,6 +36232,7 @@ function parseRecallHits(output, options = {}) {
36230
36232
  continue;
36231
36233
  }
36232
36234
  hits.push({
36235
+ category: key,
36233
36236
  contextType: typeof item.context_type === "string" ? item.context_type : "result",
36234
36237
  score: typeof item.score === "number" ? item.score : 0,
36235
36238
  snippet: recallSnippet(item.abstract ?? item.overview),
@@ -36242,33 +36245,135 @@ function parseRecallHits(output, options = {}) {
36242
36245
  return [];
36243
36246
  }
36244
36247
  }
36248
+ function stripAnchor(uri) {
36249
+ return uri.replace(/#.*$/, "");
36250
+ }
36245
36251
  function mergeRecallHits(passes) {
36246
36252
  const byDocument = /* @__PURE__ */ new Map();
36247
36253
  for (const pass of passes) {
36248
36254
  for (const hit of pass) {
36249
- const documentUri = hit.uri.replace(/#.*$/, "");
36255
+ const documentUri = stripAnchor(hit.uri);
36250
36256
  const existing = byDocument.get(documentUri);
36251
36257
  if (!existing || hit.score > existing.score) {
36252
36258
  byDocument.set(documentUri, { ...hit, uri: documentUri });
36253
36259
  }
36254
36260
  }
36255
36261
  }
36256
- return [...byDocument.values()].sort((left, right) => right.score - left.score);
36262
+ return [...byDocument.values()].sort(
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";
36257
36294
  }
36258
- function formatRecallHits(hits, maxHits) {
36259
- if (hits.length === 0) {
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
36324
+ );
36325
+ }
36326
+ function renderRecallHits(shown, overflow) {
36327
+ if (shown.length === 0) {
36260
36328
  return void 0;
36261
36329
  }
36262
- const shown = hits.slice(0, maxHits);
36263
36330
  const lines = shown.flatMap((hit, index) => {
36264
- 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}`;
36265
36334
  return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
36266
36335
  });
36267
- if (hits.length > maxHits) {
36268
- 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)`);
36269
36338
  }
36270
36339
  return lines.join("\n");
36271
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
+ }
36272
36377
  function exactMemoryScopeUris(params) {
36273
36378
  const { agentMemoriesUri, includeArchived, intents, projectName, projectResourceUri, userBase } = params;
36274
36379
  const durable = projectName ? `${userBase}/durable/projects/${projectName}` : `${userBase}/durable/projects`;
@@ -38453,14 +38558,14 @@ function registerTools(server, config2) {
38453
38558
  "grep",
38454
38559
  {
38455
38560
  annotations: { readOnlyHint: true, destructiveHint: false },
38456
- 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).",
38457
38562
  inputSchema: {
38458
38563
  caseInsensitive: external_exports.boolean().optional().describe("Case-insensitive search"),
38459
38564
  case_insensitive: external_exports.boolean().optional().describe("Case-insensitive search"),
38460
38565
  nodeLimit: external_exports.number().int().positive().max(1e3).optional().describe("Maximum result count"),
38461
38566
  node_limit: external_exports.number().int().positive().max(1e3).optional().describe("Maximum result count"),
38462
38567
  pattern: external_exports.string().optional().describe("Required text or regex pattern"),
38463
- uri: external_exports.string().optional().describe("Optional viking:// subtree")
38568
+ uri: external_exports.string().optional().describe("Optional viking:// subtree (defaults to your memories root)")
38464
38569
  }
38465
38570
  },
38466
38571
  async ({ caseInsensitive, case_insensitive, nodeLimit, node_limit, pattern, uri }) => {
@@ -38476,7 +38581,7 @@ function registerTools(server, config2) {
38476
38581
  case_insensitive: caseInsensitive ?? case_insensitive,
38477
38582
  node_limit: nodeLimit ?? node_limit,
38478
38583
  pattern: checkedPattern.value,
38479
- uri: checkedUri.value
38584
+ uri: checkedUri.value ?? `viking://user/${uriSegment2(config2.user)}/memories`
38480
38585
  });
38481
38586
  }
38482
38587
  );
@@ -39041,7 +39146,8 @@ async function runRecallTool(config2, params) {
39041
39146
  passes.push(seeded.hits);
39042
39147
  }
39043
39148
  const sections = [];
39044
- 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);
39045
39151
  if (semanticSection) {
39046
39152
  sections.push(semanticSection);
39047
39153
  } else if (!base.ok) {
@@ -39050,9 +39156,8 @@ async function runRecallTool(config2, params) {
39050
39156
  if (indexRepairMessages.length > 0) {
39051
39157
  sections.push(indexRepairMessages.join("\n"));
39052
39158
  }
39053
- const exactMatches = await exactMemoryMatchesText(config2, query, params.includeArchived, project);
39054
- if (exactMatches) {
39055
- sections.push(exactMatches);
39159
+ if (exactTail) {
39160
+ sections.push(exactTail);
39056
39161
  }
39057
39162
  const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
39058
39163
  if (hygieneHints) {
@@ -39099,14 +39204,14 @@ async function recallHygieneHintsSection(config2, recallText) {
39099
39204
  const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
39100
39205
  return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
39101
39206
  }
39102
- async function exactMemoryMatchesText(config2, query, includeArchived, project) {
39207
+ async function collectExactMemoryMatches(config2, query, includeArchived, project) {
39103
39208
  const terms = exactRecallTerms(query);
39104
39209
  if (terms.length === 0) {
39105
- return void 0;
39210
+ return [];
39106
39211
  }
39107
39212
  const ov = await requiredOpenVikingCli2();
39108
39213
  const scopes = exactMemoryScopes(config2, includeArchived, query, project);
39109
- const matches = await collectExactMatches(terms, scopes, async (term, scope) => {
39214
+ return collectExactMatches(terms, scopes, async (term, scope) => {
39110
39215
  const result = await runCommand(
39111
39216
  ov,
39112
39217
  withIdentity2(config2, ["grep", term, "--uri", scope, "--node-limit", "5", "--output", "json"]),
@@ -39114,7 +39219,6 @@ async function exactMemoryMatchesText(config2, query, includeArchived, project)
39114
39219
  );
39115
39220
  return result.exitCode === 0 ? result.stdout : void 0;
39116
39221
  });
39117
- return formatExactMatchPointers(matches);
39118
39222
  }
39119
39223
  function registerReadTool(server, config2, name, description) {
39120
39224
  server.registerTool(