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 +2 -0
- package/README.md +8 -0
- package/dist/mcp_server.cjs +124 -20
- package/dist/threadnote.cjs +3848 -3674
- package/package.json +2 -2
package/.threadnoteignore
CHANGED
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?**
|
package/dist/mcp_server.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
|
36259
|
-
if (
|
|
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
|
|
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 (
|
|
36268
|
-
lines.push(`(+${
|
|
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
|
|
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
|
-
|
|
39054
|
-
|
|
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
|
|
39207
|
+
async function collectExactMemoryMatches(config2, query, includeArchived, project) {
|
|
39103
39208
|
const terms = exactRecallTerms(query);
|
|
39104
39209
|
if (terms.length === 0) {
|
|
39105
|
-
return
|
|
39210
|
+
return [];
|
|
39106
39211
|
}
|
|
39107
39212
|
const ov = await requiredOpenVikingCli2();
|
|
39108
39213
|
const scopes = exactMemoryScopes(config2, includeArchived, query, project);
|
|
39109
|
-
|
|
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(
|