threadnote 1.5.0 → 1.6.1

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.
@@ -13,3 +13,12 @@ projects:
13
13
  - .claude/commands/*.md
14
14
  - .claude/skills/**/SKILL.md
15
15
  - docs/**/*.md
16
+
17
+ # Optional: worksets group related projects into one recall working set.
18
+ # `threadnote recall --workset <name>` (or a query mentioning the name) expands
19
+ # recall across every member's durable + seeded scope. Unknown member names are
20
+ # ignored. Inspect with `threadnote workset list` / `threadnote workset show`.
21
+ # worksets:
22
+ # - name: example-stack
23
+ # description: service plus its client
24
+ # projects: [example-service, example-client]
@@ -35520,7 +35520,7 @@ function redactText(content) {
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
35522
  function escapeRegExp(value) {
35523
- return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
35523
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
35524
35524
  }
35525
35525
  async function requiredOpenVikingCli() {
35526
35526
  const command2 = await findOpenVikingCli();
@@ -36368,6 +36368,35 @@ async function inferProjectFromQuery(manifestPath, query) {
36368
36368
  return void 0;
36369
36369
  }
36370
36370
  }
36371
+ function resolveWorksetProjects(manifest, workset) {
36372
+ const byName = new Map(manifest.projects.map((project) => [project.name.toLowerCase(), project]));
36373
+ const projects = workset.projects.map((name) => byName.get(name.toLowerCase())).filter((project) => project !== void 0);
36374
+ return { name: workset.name, projects };
36375
+ }
36376
+ async function requireWorkset(manifestPath, worksetName) {
36377
+ const manifest = await readSeedManifest(manifestPath);
36378
+ const workset = manifest.worksets?.find((entry) => entry.name.toLowerCase() === worksetName.toLowerCase());
36379
+ if (!workset) {
36380
+ throw new Error(`No workset named "${worksetName}" in ${manifestPath}.`);
36381
+ }
36382
+ return resolveWorksetProjects(manifest, workset);
36383
+ }
36384
+ async function inferWorksetFromQuery(manifestPath, query) {
36385
+ try {
36386
+ const manifest = await readSeedManifest(manifestPath);
36387
+ if (!manifest.worksets || manifest.worksets.length === 0) {
36388
+ return void 0;
36389
+ }
36390
+ const workset = manifest.worksets.find((entry) => containsNameToken(query, entry.name));
36391
+ return workset ? resolveWorksetProjects(manifest, workset) : void 0;
36392
+ } catch {
36393
+ return void 0;
36394
+ }
36395
+ }
36396
+ function containsNameToken(query, name) {
36397
+ const escaped = escapeRegExp(name.toLowerCase());
36398
+ return new RegExp(`(^|[^a-z0-9])${escaped}($|[^a-z0-9])`).test(query.toLowerCase());
36399
+ }
36371
36400
  async function readSeedManifest(path) {
36372
36401
  const raw = await (0, import_promises2.readFile)(path, "utf8");
36373
36402
  const loaded = index_vite_proxy_tmp_default.load(raw);
@@ -36399,7 +36428,33 @@ async function readSeedManifest(path) {
36399
36428
  uri: readString(loaded.future_monorepo, "uri")
36400
36429
  };
36401
36430
  }
36402
- return { futureMonorepo, projects, version: version2 };
36431
+ let worksets;
36432
+ if (loaded.worksets !== void 0) {
36433
+ if (!Array.isArray(loaded.worksets)) {
36434
+ throw new Error(`Manifest worksets must be an array: ${path}`);
36435
+ }
36436
+ worksets = loaded.worksets.map((worksetValue) => {
36437
+ if (!isJsonObject(worksetValue)) {
36438
+ throw new Error(`Manifest workset must be an object: ${path}`);
36439
+ }
36440
+ return {
36441
+ description: readOptionalString(worksetValue, "description"),
36442
+ name: readString(worksetValue, "name"),
36443
+ projects: readStringArray(worksetValue, "projects")
36444
+ };
36445
+ });
36446
+ }
36447
+ return { futureMonorepo, projects, version: version2, worksets };
36448
+ }
36449
+ function readOptionalString(object3, key) {
36450
+ const value = object3[key];
36451
+ if (value === void 0) {
36452
+ return void 0;
36453
+ }
36454
+ if (typeof value !== "string") {
36455
+ throw new Error(`Expected string for ${key}`);
36456
+ }
36457
+ return value;
36403
36458
  }
36404
36459
  function readString(object3, key) {
36405
36460
  const value = object3[key];
@@ -38588,7 +38643,7 @@ function stripPersonalProvenance(content) {
38588
38643
  }
38589
38644
  const cleaned = [];
38590
38645
  for (let index = 0; index < headerEnd; index += 1) {
38591
- if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
38646
+ if (/^(?:supersedes|archived_from|references):\s/.test(lines[index])) {
38592
38647
  continue;
38593
38648
  }
38594
38649
  cleaned.push(lines[index]);
@@ -39002,6 +39057,7 @@ function parseMemoryDocument(uri, content) {
39002
39057
  archivedFrom: headerValue(header, "archived_from"),
39003
39058
  kind,
39004
39059
  project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
39060
+ references: headerValues(header, "references"),
39005
39061
  sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
39006
39062
  status,
39007
39063
  supersedes: headerValue(header, "supersedes"),
@@ -39203,6 +39259,24 @@ function recallHygieneNudges(text, options) {
39203
39259
  }
39204
39260
  return [...new Set(nudges)];
39205
39261
  }
39262
+ function referencedUrisFromRecords(records, recallOutput) {
39263
+ const seen = /* @__PURE__ */ new Set();
39264
+ const result = [];
39265
+ for (const record2 of records) {
39266
+ for (const uri of record2.metadata.references ?? []) {
39267
+ if (seen.has(uri) || recallOutput.includes(uri)) {
39268
+ continue;
39269
+ }
39270
+ seen.add(uri);
39271
+ result.push(uri);
39272
+ }
39273
+ }
39274
+ return result;
39275
+ }
39276
+ function referencedContextExcerpt(body, maxLines) {
39277
+ const lines = body.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(0, maxLines);
39278
+ return lines.map((line) => ` ${line}`).join("\n");
39279
+ }
39206
39280
  function activePersonalMemoryUrisFromText(text, user) {
39207
39281
  const userSegment = uriSegment(user);
39208
39282
  const matches = text.matchAll(/viking:\/\/[^\s)]+/g);
@@ -39343,7 +39417,8 @@ function formatMemoryDocument(title, metadata, body) {
39343
39417
  `source_agent_client: ${metadata.sourceAgentClient}`,
39344
39418
  `timestamp: ${metadata.timestamp}`,
39345
39419
  metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
39346
- metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
39420
+ metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0,
39421
+ ...(metadata.references ?? []).map((uri) => `references: ${uri}`)
39347
39422
  ].filter((line) => line !== void 0);
39348
39423
  return [...header, "", body.trim()].join("\n");
39349
39424
  }
@@ -39351,6 +39426,11 @@ function headerValue(header, key) {
39351
39426
  const prefix = `${key}:`;
39352
39427
  return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
39353
39428
  }
39429
+ function headerValues(header, key) {
39430
+ const prefix = `${key}:`;
39431
+ const values = header.split("\n").filter((line) => line.startsWith(prefix)).map((line) => line.slice(prefix.length).trim()).filter((value) => value.length > 0);
39432
+ return values.length > 0 ? values : void 0;
39433
+ }
39354
39434
  function parseOptionalMemoryKind(value) {
39355
39435
  if (!value) {
39356
39436
  return void 0;
@@ -40107,10 +40187,13 @@ function registerSearchTool(server, config2, name, description) {
40107
40187
  includeArchived: external_exports.boolean().optional().describe("Include archived memories in recall results"),
40108
40188
  threshold: external_exports.number().min(0).max(1).optional().describe(
40109
40189
  "Minimum relevance score 0-1 (default 0.5); lower it (toward 0) to broaden when a recall comes back empty"
40190
+ ),
40191
+ workset: external_exports.string().optional().describe(
40192
+ "Optional named workset (a set of related repos from the seed manifest) to recall across as one working set"
40110
40193
  )
40111
40194
  }
40112
40195
  },
40113
- async ({ callerCwd, includeArchived, nodeLimit, query, threshold, uri }) => {
40196
+ async ({ callerCwd, includeArchived, nodeLimit, query, threshold, uri, workset }) => {
40114
40197
  const checkedQuery = requiredText(query, name, "query", { query: "unity-ui-ccc latest handoff" });
40115
40198
  if (!checkedQuery.ok) {
40116
40199
  return checkedQuery.error;
@@ -40126,7 +40209,8 @@ function registerSearchTool(server, config2, name, description) {
40126
40209
  pinnedUri: checkedUri.value,
40127
40210
  nodeLimit,
40128
40211
  includeArchived: includeArchived === true,
40129
- threshold: threshold === void 0 ? void 0 : String(threshold)
40212
+ threshold: threshold === void 0 ? void 0 : String(threshold),
40213
+ workset: workset?.trim() || void 0
40130
40214
  })
40131
40215
  );
40132
40216
  }
@@ -40161,6 +40245,7 @@ async function runRecallTool(config2, params) {
40161
40245
  const project = params.pinnedUri ? void 0 : await inferProjectFromQuery(config2.manifestPath, projectQuery);
40162
40246
  const limitArgs = params.nodeLimit ? ["--node-limit", String(params.nodeLimit)] : [];
40163
40247
  const threshold = params.threshold ?? RECALL_SCORE_THRESHOLD;
40248
+ const explicitWorkset = params.workset ? await requireWorkset(config2.manifestPath, params.workset) : void 0;
40164
40249
  const pinnedArgs = params.pinnedUri ? ["--uri", params.pinnedUri] : [];
40165
40250
  const base = await recallSearchHits(
40166
40251
  config2,
@@ -40180,6 +40265,21 @@ async function runRecallTool(config2, params) {
40180
40265
  passes.push(seeded.hits);
40181
40266
  }
40182
40267
  const sections = [];
40268
+ const workset = params.pinnedUri ? void 0 : explicitWorkset ? explicitWorkset : await inferWorksetFromQuery(config2.manifestPath, projectQuery);
40269
+ if (workset && workset.projects.length > 0) {
40270
+ sections.push(`Workset scope: ${workset.name} (${workset.projects.map((member) => member.name).join(", ")})`);
40271
+ const alreadyScoped = new Set([params.pinnedUri, seededUri].filter((uri) => uri !== void 0));
40272
+ const worksetScopes = worksetScopeUris(config2, workset).filter((uri) => !alreadyScoped.has(uri)).slice(0, MAX_WORKSET_PASSES);
40273
+ for (const scope of worksetScopes) {
40274
+ const worksetPass = await recallSearchHits(
40275
+ config2,
40276
+ ["search", query, "--uri", scope, ...limitArgs],
40277
+ threshold,
40278
+ params.includeArchived
40279
+ );
40280
+ passes.push(worksetPass.hits);
40281
+ }
40282
+ }
40183
40283
  const exactMatches = await collectExactMemoryMatches(config2, query, params.includeArchived, project);
40184
40284
  const { semanticSection, exactTail } = buildRecallSections(passes, exactMatches, params.nodeLimit ?? 12);
40185
40285
  if (semanticSection) {
@@ -40193,6 +40293,10 @@ async function runRecallTool(config2, params) {
40193
40293
  if (exactTail) {
40194
40294
  sections.push(exactTail);
40195
40295
  }
40296
+ const referencedContext = await referencedContextSection(config2, sections.join("\n\n"));
40297
+ if (referencedContext) {
40298
+ sections.push(referencedContext);
40299
+ }
40196
40300
  const hygieneHints = await recallHygieneHintsSection(config2, sections.join("\n\n"));
40197
40301
  if (hygieneHints) {
40198
40302
  sections.push(hygieneHints);
@@ -40238,6 +40342,36 @@ async function recallHygieneHintsSection(config2, recallText) {
40238
40342
  const nudges = recallHygieneNudges(recallText, { records, user: config2.user });
40239
40343
  return nudges.length > 0 ? ["Memory hygiene hints:", ...nudges.map((nudge) => `- ${nudge}`)].join("\n") : void 0;
40240
40344
  }
40345
+ var MAX_REFERENCED_CONTEXT = 5;
40346
+ var REFERENCED_EXCERPT_LINES = 12;
40347
+ async function referencedContextSection(config2, recallText) {
40348
+ const surfacedUris = activePersonalMemoryUrisFromText(recallText, config2.user);
40349
+ if (surfacedUris.length === 0) {
40350
+ return void 0;
40351
+ }
40352
+ const surfaced = await readMemoryRecordsByUri(config2, surfacedUris);
40353
+ const referenced = referencedUrisFromRecords(surfaced, recallText);
40354
+ if (referenced.length === 0) {
40355
+ return void 0;
40356
+ }
40357
+ const capped = referenced.slice(0, MAX_REFERENCED_CONTEXT);
40358
+ const records = await readMemoryRecordsByUri(config2, capped);
40359
+ const byUri = new Map(records.map((record2) => [record2.uri, record2]));
40360
+ const lines = ["Referenced read-only context (one-way pointers from surfaced memories):"];
40361
+ for (const uri of capped) {
40362
+ const record2 = byUri.get(uri);
40363
+ if (record2) {
40364
+ lines.push(`- ${uri}`, referencedContextExcerpt(record2.body, REFERENCED_EXCERPT_LINES));
40365
+ } else {
40366
+ lines.push(`- ${uri} [reference unavailable locally]`);
40367
+ }
40368
+ }
40369
+ if (referenced.length > capped.length) {
40370
+ const omitted = referenced.length - capped.length;
40371
+ lines.push(`- \u2026 ${omitted} more referenced ${omitted === 1 ? "memory" : "memories"} omitted`);
40372
+ }
40373
+ return lines.join("\n");
40374
+ }
40241
40375
  async function collectExactMemoryMatches(config2, query, includeArchived, project) {
40242
40376
  const terms = exactRecallTerms(query);
40243
40377
  if (terms.length === 0) {
@@ -40330,6 +40464,9 @@ function registerStoreTool(server, config2, name, description) {
40330
40464
  inputSchema: {
40331
40465
  kind: external_exports.enum(["durable", "handoff", "incident", "preference", "smoke"]).optional().describe("Memory lifecycle kind; durable facts and handoffs are most common"),
40332
40466
  project: external_exports.string().optional().describe("Project/repo namespace, for example threadnote or mobile-native"),
40467
+ references: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional().describe(
40468
+ "Optional viking:// URI(s) to record as one-way, read-only prior context for this memory. Recall surfaces a short excerpt of each. Stripped from shared copies on publish."
40469
+ ),
40333
40470
  replaceUri: external_exports.string().optional().describe(
40334
40471
  "Optional viking:// memory URI to replace. Shared URIs are updated in place and pushed; personal URIs are forgotten after the replacement is safely stored."
40335
40472
  ),
@@ -40339,7 +40476,7 @@ function registerStoreTool(server, config2, name, description) {
40339
40476
  topic: external_exports.string().optional().describe("Stable topic; active project/topic memories update one file")
40340
40477
  }
40341
40478
  },
40342
- async ({ kind, project, replaceUri, sourceAgentClient, status, text, topic }) => {
40479
+ async ({ kind, project, references, replaceUri, sourceAgentClient, status, text, topic }) => {
40343
40480
  const checkedText = requiredText(text, name, "text", { text: "Durable engineering note..." });
40344
40481
  if (!checkedText.ok) {
40345
40482
  return checkedText.error;
@@ -40348,9 +40485,14 @@ function registerStoreTool(server, config2, name, description) {
40348
40485
  if (!checkedReplaceUri.ok) {
40349
40486
  return checkedReplaceUri.error;
40350
40487
  }
40488
+ const checkedReferences = optionalVikingUriList(references, name);
40489
+ if (!checkedReferences.ok) {
40490
+ return checkedReferences.error;
40491
+ }
40351
40492
  const metadata = {
40352
40493
  kind: kind ?? "durable",
40353
40494
  project: normalizeOptionalMetadata2(project),
40495
+ references: checkedReferences.value,
40354
40496
  sourceAgentClient: sourceAgentClient ?? "mcp",
40355
40497
  status: status ?? "active",
40356
40498
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -40801,6 +40943,18 @@ function exactMemoryScopes(config2, includeArchived, query, project) {
40801
40943
  userBase: `viking://user/${uriSegment2(config2.user)}/memories`
40802
40944
  });
40803
40945
  }
40946
+ var MAX_WORKSET_PASSES = 12;
40947
+ function worksetScopeUris(config2, workset) {
40948
+ const scopes = [];
40949
+ for (const member of workset.projects) {
40950
+ scopes.push(`viking://user/${uriSegment2(config2.user)}/memories/durable/projects/${uriSegment2(member.name)}`);
40951
+ const seeded = trimTrailingSlash(member.uri);
40952
+ if (seeded.startsWith("viking://")) {
40953
+ scopes.push(seeded);
40954
+ }
40955
+ }
40956
+ return [...new Set(scopes)];
40957
+ }
40804
40958
  function formatMemoryDocument2(title, metadata, body) {
40805
40959
  const header = [
40806
40960
  title,
@@ -40811,7 +40965,8 @@ function formatMemoryDocument2(title, metadata, body) {
40811
40965
  `source_agent_client: ${metadata.sourceAgentClient}`,
40812
40966
  `timestamp: ${metadata.timestamp}`,
40813
40967
  metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
40814
- metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
40968
+ metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0,
40969
+ ...(metadata.references ?? []).map((uri) => `references: ${uri}`)
40815
40970
  ].filter((line) => line !== void 0);
40816
40971
  return [...header, "", body.trim()].join("\n");
40817
40972
  }
@@ -40867,6 +41022,23 @@ function optionalVikingUri(value, toolName) {
40867
41022
  ok: false
40868
41023
  };
40869
41024
  }
41025
+ function optionalVikingUriList(value, toolName) {
41026
+ const rawValues = Array.isArray(value) ? value : value === void 0 ? [] : [value];
41027
+ const uris = rawValues.map((uri) => uri.trim()).filter(Boolean);
41028
+ if (uris.length === 0) {
41029
+ return { ok: true, value: void 0 };
41030
+ }
41031
+ const invalid = uris.find((uri) => !uri.startsWith("viking://"));
41032
+ if (invalid) {
41033
+ return {
41034
+ error: argumentError(
41035
+ `Threadnote MCP tool "${toolName}" needs viking:// URI values for "references". Received: ${invalid}`
41036
+ ),
41037
+ ok: false
41038
+ };
41039
+ }
41040
+ return { ok: true, value: [...new Set(uris)] };
41041
+ }
40870
41042
  function requiredVikingUriList(value, toolName, exampleUri) {
40871
41043
  const rawValues = Array.isArray(value) ? value : value === void 0 ? [] : [value];
40872
41044
  const uris = rawValues.map((uri) => uri.trim()).filter(Boolean);