threadnote 0.7.5 → 0.7.7

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.
@@ -3524,7 +3524,7 @@ var DEFAULT_SEED_PATTERNS = [
3524
3524
 
3525
3525
  // src/hooks.ts
3526
3526
  var import_promises7 = require("node:fs/promises");
3527
- var import_node_path8 = require("node:path");
3527
+ var import_node_path9 = require("node:path");
3528
3528
 
3529
3529
  // src/mcp.ts
3530
3530
  var import_node_fs2 = require("node:fs");
@@ -3958,6 +3958,57 @@ function portablePath(path) {
3958
3958
  function getInvocationCwd() {
3959
3959
  return process.env.THREADNOTE_CALLER_CWD ?? process.cwd();
3960
3960
  }
3961
+ function recallQueryRequestsWorkspaceContext(query) {
3962
+ const normalized = query.toLowerCase();
3963
+ return /\b(?:this|current)\s+(?:branch|repo|repository|workspace|worktree)\b/.test(normalized);
3964
+ }
3965
+ async function enrichRecallQueryWithWorkspaceContext(query, options = {}) {
3966
+ return enrichRecallQueryWithWorkspaceTerms(query, options, true);
3967
+ }
3968
+ async function enrichRecallQueryWithWorkspaceProjectContext(query, options = {}) {
3969
+ return enrichRecallQueryWithWorkspaceTerms(query, options, false);
3970
+ }
3971
+ async function enrichRecallQueryWithWorkspaceTerms(query, options, includeBranch) {
3972
+ if (!recallQueryRequestsWorkspaceContext(query)) {
3973
+ return query;
3974
+ }
3975
+ const terms = await currentWorkspaceRecallTerms(options, includeBranch);
3976
+ const additions = terms.filter((term) => !query.toLowerCase().includes(term.toLowerCase()));
3977
+ return additions.length > 0 ? `${query} ${additions.join(" ")}` : query;
3978
+ }
3979
+ async function currentWorkspaceRecallTerms(options, includeBranch) {
3980
+ const cwd = options.cwd ?? (options.includeProcessCwd === false ? void 0 : getInvocationCwd());
3981
+ if (!cwd || !(0, import_node_path.isAbsolute)(cwd)) {
3982
+ return [];
3983
+ }
3984
+ const repoRoot = await gitValue(["rev-parse", "--show-toplevel"], cwd);
3985
+ if (!repoRoot) {
3986
+ return [];
3987
+ }
3988
+ const branch = await gitValue(["branch", "--show-current"], repoRoot);
3989
+ const parent = (0, import_node_path.dirname)(repoRoot);
3990
+ return uniqueUsefulWorkspaceTerms([
3991
+ { source: "branch", value: includeBranch ? branch : void 0 },
3992
+ { source: "path", value: (0, import_node_path.basename)(repoRoot) },
3993
+ { source: "path", value: parent === (0, import_node_os.homedir)() ? void 0 : (0, import_node_path.basename)(parent) }
3994
+ ]);
3995
+ }
3996
+ function uniqueUsefulWorkspaceTerms(values) {
3997
+ const ignored = /* @__PURE__ */ new Set(["repos", "repositories", "workspaces", "worktrees"]);
3998
+ const seen = /* @__PURE__ */ new Set();
3999
+ const terms = [];
4000
+ for (const { source, value } of values) {
4001
+ const term = value?.trim();
4002
+ const normalized = term?.toLowerCase();
4003
+ const tooShort = source === "branch" ? false : (term?.length ?? 0) < 4;
4004
+ if (!term || !normalized || tooShort || ignored.has(normalized) || seen.has(normalized)) {
4005
+ continue;
4006
+ }
4007
+ seen.add(normalized);
4008
+ terms.push(term);
4009
+ }
4010
+ return terms;
4011
+ }
3961
4012
  function toPosixPath(path) {
3962
4013
  return path.split(import_node_path.sep).join("/");
3963
4014
  }
@@ -3981,19 +4032,29 @@ function exactRecallTerms(query) {
3981
4032
  "branch",
3982
4033
  "case",
3983
4034
  "current",
4035
+ "durable",
3984
4036
  "find",
4037
+ "feature",
4038
+ "features",
3985
4039
  "handoff",
3986
4040
  "issue",
3987
4041
  "issues",
4042
+ "knowledge",
3988
4043
  "latest",
3989
4044
  "memory",
3990
4045
  "memories",
4046
+ "project",
3991
4047
  "recall",
4048
+ "repo",
4049
+ "repository",
3992
4050
  "related",
3993
4051
  "search",
3994
- "the",
4052
+ "stored",
3995
4053
  "this",
3996
- "with"
4054
+ "the",
4055
+ "with",
4056
+ "workspace",
4057
+ "worktree"
3997
4058
  ]);
3998
4059
  const seen = /* @__PURE__ */ new Set();
3999
4060
  const terms = [];
@@ -4541,7 +4602,7 @@ function existsSyncDirectory(path) {
4541
4602
 
4542
4603
  // src/memory.ts
4543
4604
  var import_promises5 = require("node:fs/promises");
4544
- var import_node_path5 = require("node:path");
4605
+ var import_node_path6 = require("node:path");
4545
4606
 
4546
4607
  // src/manifest.ts
4547
4608
  var import_promises3 = require("node:fs/promises");
@@ -7239,10 +7300,445 @@ function readStringArray(object, key) {
7239
7300
  return value;
7240
7301
  }
7241
7302
 
7303
+ // src/memory_hygiene.ts
7304
+ var import_node_path3 = require("node:path");
7305
+ var HYGIENE_SOURCES_HEADING = "## Threadnote Hygiene Sources";
7306
+ var STALE_HANDOFF_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
7307
+ function parseMemoryDocument(uri, content) {
7308
+ const trimmed = content.trim();
7309
+ if (!trimmed) {
7310
+ return void 0;
7311
+ }
7312
+ const separatorIndex = trimmed.indexOf("\n\n");
7313
+ const header = separatorIndex === -1 ? trimmed : trimmed.slice(0, separatorIndex);
7314
+ const body = separatorIndex === -1 ? "" : trimmed.slice(separatorIndex + 2).trim();
7315
+ const firstLine2 = header.split("\n")[0]?.trim();
7316
+ if (firstLine2 !== "MEMORY" && firstLine2 !== "HANDOFF") {
7317
+ return void 0;
7318
+ }
7319
+ const kind = parseOptionalMemoryKind(headerValue(header, "kind")) ?? (firstLine2 === "HANDOFF" ? "handoff" : void 0);
7320
+ const status = parseOptionalMemoryStatus(headerValue(header, "status")) ?? "active";
7321
+ if (!kind) {
7322
+ return void 0;
7323
+ }
7324
+ return {
7325
+ body,
7326
+ content: trimmed,
7327
+ headerTitle: firstLine2,
7328
+ metadata: {
7329
+ archivedFrom: headerValue(header, "archived_from"),
7330
+ kind,
7331
+ project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
7332
+ sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
7333
+ status,
7334
+ supersedes: headerValue(header, "supersedes"),
7335
+ timestamp: headerValue(header, "timestamp") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
7336
+ topic: normalizeOptionalMetadata(headerValue(header, "topic"))
7337
+ },
7338
+ uri
7339
+ };
7340
+ }
7341
+ function buildCompactPlan(records, options) {
7342
+ const now = options.now ?? /* @__PURE__ */ new Date();
7343
+ const groupedRecords = records.map((record) => groupableRecord(record)).filter((item) => item !== void 0).filter((item) => item.project === options.project).filter((item) => options.topic === void 0 || item.topic === options.topic).filter((item) => options.kind === void 0 || item.record.metadata.kind === options.kind);
7344
+ const groups = /* @__PURE__ */ new Map();
7345
+ for (const item of groupedRecords) {
7346
+ groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
7347
+ }
7348
+ const keepUpdates = [];
7349
+ const archives = [];
7350
+ const forgets = [];
7351
+ const manualReview = [];
7352
+ for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
7353
+ const recordsInGroup = group.map((item) => item.record);
7354
+ const kind = recordsInGroup[0]?.metadata.kind;
7355
+ const topic = group[0]?.topic;
7356
+ const project = group[0]?.project;
7357
+ if (!kind || !project || !isCompactableKind(kind)) {
7358
+ continue;
7359
+ }
7360
+ const duplicateGroups = groupBy(recordsInGroup, (record) => comparableMemoryBody(record.body));
7361
+ const duplicateForgetUris = /* @__PURE__ */ new Set();
7362
+ const distinctBodyCount = duplicateGroups.size;
7363
+ for (const duplicateGroup of duplicateGroups.values()) {
7364
+ if (duplicateGroup.length < 2) {
7365
+ continue;
7366
+ }
7367
+ const duplicateKeep = preferredKeepRecord(duplicateGroup, topic);
7368
+ for (const duplicate of sortedNewestFirst(duplicateGroup).filter((record) => record.uri !== duplicateKeep.uri)) {
7369
+ duplicateForgetUris.add(duplicate.uri);
7370
+ forgets.push({ reason: `exact duplicate of ${duplicateKeep.uri}`, uri: duplicate.uri });
7371
+ }
7372
+ if (distinctBodyCount === 1 || kind !== "handoff") {
7373
+ keepUpdates.push({
7374
+ content: memoryContentWithHygieneSources(
7375
+ duplicateKeep,
7376
+ duplicateGroup.map((record) => record.uri)
7377
+ ),
7378
+ reason: "keep exact duplicate group with source URIs",
7379
+ sourceUris: duplicateGroup.map((record) => record.uri),
7380
+ uri: duplicateKeep.uri
7381
+ });
7382
+ }
7383
+ }
7384
+ const remainingRecords = recordsInGroup.filter((record) => !duplicateForgetUris.has(record.uri));
7385
+ if (recordsInGroup.length > 1 && distinctBodyCount === 1) {
7386
+ continue;
7387
+ }
7388
+ if (remainingRecords.length === 0) {
7389
+ continue;
7390
+ }
7391
+ if (remainingRecords.length === 1) {
7392
+ const [record] = remainingRecords;
7393
+ if (!record) {
7394
+ continue;
7395
+ }
7396
+ if (record.metadata.supersedes === record.uri) {
7397
+ keepUpdates.push({
7398
+ content: memoryContentWithHygieneSources(record, [record.uri]),
7399
+ reason: "strip self-supersedes header",
7400
+ sourceUris: [record.uri],
7401
+ uri: record.uri
7402
+ });
7403
+ }
7404
+ if (isStaleLookingHandoff(record, now)) {
7405
+ manualReview.push({ reason: "stale-looking active handoff", uri: record.uri });
7406
+ }
7407
+ continue;
7408
+ }
7409
+ if (kind === "handoff") {
7410
+ const keep = preferredKeepRecord(remainingRecords, topic);
7411
+ const sourceUris = recordsInGroup.map((record) => record.uri);
7412
+ keepUpdates.push({
7413
+ content: memoryContentWithHygieneSources(keep, sourceUris),
7414
+ reason: "keep latest handoff and preserve source URIs",
7415
+ sourceUris,
7416
+ uri: keep.uri
7417
+ });
7418
+ for (const record of sortedNewestFirst(remainingRecords).filter((item) => item.uri !== keep.uri)) {
7419
+ archives.push({
7420
+ kind,
7421
+ project,
7422
+ reason: `older handoff for ${project}/${topic ?? "unknown"}`,
7423
+ topic,
7424
+ uri: record.uri
7425
+ });
7426
+ }
7427
+ continue;
7428
+ }
7429
+ for (const record of sortedNewestFirst(remainingRecords)) {
7430
+ manualReview.push({ reason: `non-exact ${kind} memory in overlapping group`, uri: record.uri });
7431
+ }
7432
+ }
7433
+ return {
7434
+ archives: dedupeByUri(archives),
7435
+ forgets: dedupeByUri(forgets),
7436
+ keepUpdates: dedupeByUri(keepUpdates),
7437
+ manualReview: dedupeByUri(manualReview),
7438
+ options,
7439
+ recordsScanned: groupedRecords.length
7440
+ };
7441
+ }
7442
+ function memoryContentWithHygieneSources(record, sourceUris) {
7443
+ const body = stripHygieneSources(record.body);
7444
+ const uniqueSourceUris = [...new Set(sourceUris)].sort();
7445
+ const metadata = {
7446
+ ...record.metadata,
7447
+ supersedes: record.metadata.supersedes === record.uri ? void 0 : record.metadata.supersedes
7448
+ };
7449
+ return formatMemoryDocument(
7450
+ record.headerTitle,
7451
+ metadata,
7452
+ [body, "", HYGIENE_SOURCES_HEADING, "", ...uniqueSourceUris.map((uri) => `- ${uri}`)].join("\n")
7453
+ );
7454
+ }
7455
+ function formatCompactPlan(plan, options) {
7456
+ const scope = [
7457
+ `project ${plan.options.project}`,
7458
+ plan.options.topic ? `topic ${plan.options.topic}` : void 0,
7459
+ plan.options.kind ? `kind ${plan.options.kind}` : void 0
7460
+ ].filter((item) => item !== void 0).join(", ");
7461
+ const lines = [
7462
+ `${options.apply ? "Applying" : "Dry-run"} memory hygiene plan for ${scope}`,
7463
+ `Records scanned: ${plan.recordsScanned}`,
7464
+ "",
7465
+ formatPlanSection(
7466
+ "Keep/update",
7467
+ plan.keepUpdates.map((action) => `${action.uri} (${action.reason}; sources: ${action.sourceUris.length})`)
7468
+ ),
7469
+ formatPlanSection(
7470
+ "Archive old handoffs",
7471
+ plan.archives.map((action) => `${action.uri} (${action.reason})`)
7472
+ ),
7473
+ formatPlanSection(
7474
+ "Forget exact duplicates",
7475
+ plan.forgets.map((action) => `${action.uri} (${action.reason})`)
7476
+ ),
7477
+ formatPlanSection(
7478
+ "Manual review",
7479
+ plan.manualReview.map((item) => `${item.uri} (${item.reason})`)
7480
+ )
7481
+ ];
7482
+ if (!options.apply) {
7483
+ lines.push("", "No changes made. Re-run with --apply to execute this plan.");
7484
+ }
7485
+ return lines.join("\n");
7486
+ }
7487
+ function recallHygieneNudges(text, options) {
7488
+ const activeUris = activePersonalMemoryUrisFromText(text, options.user);
7489
+ const nudges = [];
7490
+ const returnedUriSet = new Set(activeUris);
7491
+ const returnedRecords = options.records?.filter((record) => returnedUriSet.has(record.uri)).map((record) => groupableRecord(record)) ?? [];
7492
+ const groups = /* @__PURE__ */ new Map();
7493
+ for (const item of returnedRecords) {
7494
+ if (!item) {
7495
+ continue;
7496
+ }
7497
+ groups.set(item.groupKey, [...groups.get(item.groupKey) ?? [], item]);
7498
+ }
7499
+ for (const group of [...groups.values()].sort(compareGroupedRecordLists)) {
7500
+ if (group.length < 3) {
7501
+ continue;
7502
+ }
7503
+ const first = group[0];
7504
+ if (!first || !isCompactableKind(first.record.metadata.kind)) {
7505
+ continue;
7506
+ }
7507
+ const topic = first.topic;
7508
+ if (!topic) {
7509
+ continue;
7510
+ }
7511
+ nudges.push(
7512
+ `${group.length} active ${memoryKindPlural(first.record.metadata.kind)} look overlapping for ${first.project}/${topic}; run compact_context({"project":"${first.project}","topic":"${topic}","dryRun":true}).`
7513
+ );
7514
+ }
7515
+ const projectCounts = /* @__PURE__ */ new Map();
7516
+ for (const uri of activeUris) {
7517
+ const parsed = parsePersonalMemoryUri(uri, options.user);
7518
+ if (!parsed || parsed.kind !== "handoff" || parsed.status !== "active") {
7519
+ continue;
7520
+ }
7521
+ projectCounts.set(parsed.project, (projectCounts.get(parsed.project) ?? 0) + 1);
7522
+ }
7523
+ for (const [project, count] of [...projectCounts.entries()].sort(([left], [right]) => left.localeCompare(right))) {
7524
+ if (count < 10) {
7525
+ continue;
7526
+ }
7527
+ nudges.push(
7528
+ `Many active handoffs surfaced for ${project}; run compact_context({"project":"${project}","dryRun":true}).`
7529
+ );
7530
+ }
7531
+ return [...new Set(nudges)];
7532
+ }
7533
+ function activePersonalMemoryUrisFromText(text, user) {
7534
+ const userSegment = uriSegment(user);
7535
+ const matches = text.matchAll(/viking:\/\/[^\s)]+/g);
7536
+ const uris = [];
7537
+ for (const match of matches) {
7538
+ const uri = match[0]?.replace(/[.,;:]+$/g, "");
7539
+ if (!uri || !parsePersonalMemoryUri(uri, userSegment)) {
7540
+ continue;
7541
+ }
7542
+ uris.push(uri);
7543
+ }
7544
+ return [...new Set(uris)];
7545
+ }
7546
+ function parsePersonalMemoryUri(uri, user) {
7547
+ const prefix = `viking://user/${uriSegment(user)}/memories/`;
7548
+ if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
7549
+ return void 0;
7550
+ }
7551
+ const rest = uri.slice(prefix.length);
7552
+ const parts = rest.split("/").filter(Boolean);
7553
+ if (parts.length < 4) {
7554
+ return void 0;
7555
+ }
7556
+ if (parts[0] === "handoffs" && parts[1] === "active" && parts[2] && parts[3]?.endsWith(".md")) {
7557
+ return { kind: "handoff", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
7558
+ }
7559
+ if (parts[0] === "durable" && parts[1] === "projects" && parts[2] && parts[3]?.endsWith(".md")) {
7560
+ return { kind: "durable", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
7561
+ }
7562
+ if (parts[0] === "incidents" && parts[1] === "active" && parts[2] && parts[3]?.endsWith(".md")) {
7563
+ return { kind: "incident", project: parts[2], status: "active", topic: parts[3].replace(/\.md$/, "") };
7564
+ }
7565
+ return void 0;
7566
+ }
7567
+ function handoffTopicForBranch(branch, options) {
7568
+ const topic = normalizeOptionalMetadata(options.topic);
7569
+ if (options.timestamped === true) {
7570
+ if (topic) {
7571
+ throw new Error("Cannot combine --timestamped with --topic.");
7572
+ }
7573
+ return void 0;
7574
+ }
7575
+ return topic ?? normalizeOptionalMetadata(branch) ?? "current";
7576
+ }
7577
+ function groupableRecord(record) {
7578
+ if (record.metadata.status !== "active" || !isCompactableKind(record.metadata.kind)) {
7579
+ return void 0;
7580
+ }
7581
+ const project = normalizeOptionalMetadata(record.metadata.project) ?? parseProjectFromUri(record.uri);
7582
+ if (!project) {
7583
+ return void 0;
7584
+ }
7585
+ const topic = topicForRecord(record);
7586
+ const groupKey = [record.metadata.kind, project, topic ?? record.uri].join("\0");
7587
+ return { groupKey, project, record, topic };
7588
+ }
7589
+ function topicForRecord(record) {
7590
+ return normalizeOptionalMetadata(record.metadata.topic) ?? normalizeOptionalMetadata(branchFromBody(record.body)) ?? topicFromUri(record.uri);
7591
+ }
7592
+ function branchFromBody(body) {
7593
+ const branch = /^branch:\s*(.+)$/m.exec(body)?.[1]?.trim();
7594
+ return branch?.split(/\s+/)[0]?.replace(/[.,;:]+$/g, "");
7595
+ }
7596
+ function topicFromUri(uri) {
7597
+ const name = (0, import_node_path3.basename)(uri).replace(/\.md$/, "");
7598
+ return name.startsWith("threadnote-") ? void 0 : name;
7599
+ }
7600
+ function parseProjectFromUri(uri) {
7601
+ const parts = uri.split("/memories/")[1]?.split("/").filter(Boolean) ?? [];
7602
+ if ((parts[0] === "handoffs" || parts[0] === "incidents") && parts[1] === "active") {
7603
+ return parts[2];
7604
+ }
7605
+ if (parts[0] === "durable" && parts[1] === "projects") {
7606
+ return parts[2];
7607
+ }
7608
+ return void 0;
7609
+ }
7610
+ function comparableMemoryBody(body) {
7611
+ return stripHygieneSources(body).trim().replace(/\s+/g, " ");
7612
+ }
7613
+ function stripHygieneSources(body) {
7614
+ const index = body.indexOf(`
7615
+ ${HYGIENE_SOURCES_HEADING}`);
7616
+ if (index !== -1) {
7617
+ return body.slice(0, index).trim();
7618
+ }
7619
+ return body.startsWith(HYGIENE_SOURCES_HEADING) ? "" : body.trim();
7620
+ }
7621
+ function preferredKeepRecord(records, topic) {
7622
+ const stableRecords = records.filter((record) => isStableRecord(record, topic));
7623
+ return sortedNewestFirst(stableRecords.length > 0 ? stableRecords : records)[0] ?? records[0];
7624
+ }
7625
+ function isStableRecord(record, topic) {
7626
+ const recordTopic = topic ?? topicForRecord(record);
7627
+ return recordTopic !== void 0 && (0, import_node_path3.basename)(record.uri) === `${uriSegment(recordTopic)}.md`;
7628
+ }
7629
+ function sortedNewestFirst(records) {
7630
+ return [...records].sort((left, right) => {
7631
+ const timestampDiff = timestampMs(right) - timestampMs(left);
7632
+ return timestampDiff === 0 ? left.uri.localeCompare(right.uri) : timestampDiff;
7633
+ });
7634
+ }
7635
+ function timestampMs(record) {
7636
+ const parsed = Date.parse(record.metadata.timestamp);
7637
+ return Number.isFinite(parsed) ? parsed : 0;
7638
+ }
7639
+ function isStaleLookingHandoff(record, now) {
7640
+ if (record.metadata.kind !== "handoff") {
7641
+ return false;
7642
+ }
7643
+ if (now.getTime() - timestampMs(record) < STALE_HANDOFF_AGE_MS) {
7644
+ return false;
7645
+ }
7646
+ return /\b(?:PR|pull request)\s+(?:OPEN|open|is open)|awaiting review|waiting for review|next steps?:\s*address PR review/i.test(
7647
+ record.body
7648
+ );
7649
+ }
7650
+ function groupBy(values, keyForValue) {
7651
+ const groups = /* @__PURE__ */ new Map();
7652
+ for (const value of values) {
7653
+ const key = keyForValue(value);
7654
+ groups.set(key, [...groups.get(key) ?? [], value]);
7655
+ }
7656
+ return groups;
7657
+ }
7658
+ function compareGroupedRecordLists(left, right) {
7659
+ return (left[0]?.groupKey ?? "").localeCompare(right[0]?.groupKey ?? "");
7660
+ }
7661
+ function dedupeByUri(items) {
7662
+ const seen = /* @__PURE__ */ new Set();
7663
+ const result = [];
7664
+ for (const item of items) {
7665
+ if (seen.has(item.uri)) {
7666
+ continue;
7667
+ }
7668
+ seen.add(item.uri);
7669
+ result.push(item);
7670
+ }
7671
+ return result;
7672
+ }
7673
+ function formatMemoryDocument(title, metadata, body) {
7674
+ const header = [
7675
+ title,
7676
+ `kind: ${metadata.kind}`,
7677
+ `status: ${metadata.status}`,
7678
+ metadata.project ? `project: ${metadata.project}` : void 0,
7679
+ metadata.topic ? `topic: ${metadata.topic}` : void 0,
7680
+ `source_agent_client: ${metadata.sourceAgentClient}`,
7681
+ `timestamp: ${metadata.timestamp}`,
7682
+ metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
7683
+ metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
7684
+ ].filter((line) => line !== void 0);
7685
+ return [...header, "", body.trim()].join("\n");
7686
+ }
7687
+ function headerValue(header, key) {
7688
+ const prefix = `${key}:`;
7689
+ return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
7690
+ }
7691
+ function parseOptionalMemoryKind(value) {
7692
+ if (!value) {
7693
+ return void 0;
7694
+ }
7695
+ if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
7696
+ return value;
7697
+ }
7698
+ return void 0;
7699
+ }
7700
+ function parseOptionalMemoryStatus(value) {
7701
+ if (!value) {
7702
+ return void 0;
7703
+ }
7704
+ if (["active", "archived", "superseded"].includes(value)) {
7705
+ return value;
7706
+ }
7707
+ return void 0;
7708
+ }
7709
+ function normalizeOptionalMetadata(value) {
7710
+ const trimmed = value?.trim();
7711
+ return trimmed ? trimmed : void 0;
7712
+ }
7713
+ function isCompactableKind(kind) {
7714
+ return kind === "durable" || kind === "handoff" || kind === "incident";
7715
+ }
7716
+ function memoryKindPlural(kind) {
7717
+ switch (kind) {
7718
+ case "handoff":
7719
+ return "handoffs";
7720
+ case "incident":
7721
+ return "incidents";
7722
+ case "durable":
7723
+ return "durable memories";
7724
+ case "preference":
7725
+ return "preferences";
7726
+ case "smoke":
7727
+ return "smoke memories";
7728
+ }
7729
+ }
7730
+ function formatPlanSection(title, lines) {
7731
+ if (lines.length === 0) {
7732
+ return `${title}:
7733
+ - none`;
7734
+ }
7735
+ return [`${title}:`, ...lines.map((line) => `- ${line}`)].join("\n");
7736
+ }
7737
+
7242
7738
  // src/runtime.ts
7243
7739
  var import_node_fs3 = require("node:fs");
7244
7740
  var import_node_os3 = require("node:os");
7245
- var import_node_path3 = require("node:path");
7741
+ var import_node_path4 = require("node:path");
7246
7742
  function getRuntimeConfig(program2, manifestOverride) {
7247
7743
  const options = program2.opts();
7248
7744
  const threadnoteHome = expandPath(options.home ?? process.env.THREADNOTE_HOME ?? "~/.openviking");
@@ -7261,20 +7757,20 @@ function getRuntimeConfig(program2, manifestOverride) {
7261
7757
  };
7262
7758
  }
7263
7759
  function defaultManifestPath(agentContextHome) {
7264
- const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
7760
+ const userManifest = (0, import_node_path4.join)(agentContextHome, USER_MANIFEST_NAME);
7265
7761
  return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
7266
7762
  }
7267
7763
  function builtInExampleManifestPath() {
7268
- return (0, import_node_path3.join)(toolRoot(), "config", "seed-manifest.example.yaml");
7764
+ return (0, import_node_path4.join)(toolRoot(), "config", "seed-manifest.example.yaml");
7269
7765
  }
7270
7766
  function openVikingHealthUrl(config) {
7271
7767
  return `http://${config.host}:${config.port}/health`;
7272
7768
  }
7273
7769
  function openVikingLogPath(config) {
7274
- return (0, import_node_path3.join)(config.agentContextHome, "logs", "server.log");
7770
+ return (0, import_node_path4.join)(config.agentContextHome, "logs", "server.log");
7275
7771
  }
7276
7772
  function openVikingServerArgs(config) {
7277
- return ["--config", (0, import_node_path3.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
7773
+ return ["--config", (0, import_node_path4.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
7278
7774
  }
7279
7775
  function withIdentity(config, args) {
7280
7776
  return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
@@ -7290,7 +7786,7 @@ function renderTemplate(template, config, extras = {}) {
7290
7786
  // src/share.ts
7291
7787
  var import_promises4 = require("node:fs/promises");
7292
7788
  var import_node_os4 = require("node:os");
7293
- var import_node_path4 = require("node:path");
7789
+ var import_node_path5 = require("node:path");
7294
7790
  var TEAMS_FILE_VERSION = 1;
7295
7791
  var SHARED_SEGMENT = "shared";
7296
7792
  var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
@@ -7359,8 +7855,8 @@ async function runShareInit(config, remoteUrl, options) {
7359
7855
  if (await exists(gitdir)) {
7360
7856
  throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
7361
7857
  }
7362
- await ensureDirectory((0, import_node_path4.dirname)(worktree), dryRun);
7363
- await ensureDirectory((0, import_node_path4.dirname)(gitdir), dryRun);
7858
+ await ensureDirectory((0, import_node_path5.dirname)(worktree), dryRun);
7859
+ await ensureDirectory((0, import_node_path5.dirname)(gitdir), dryRun);
7364
7860
  const git = await requiredExecutable("git");
7365
7861
  await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
7366
7862
  const newConfig = {
@@ -7391,7 +7887,7 @@ async function runShareInit(config, remoteUrl, options) {
7391
7887
  var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
7392
7888
  var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
7393
7889
  async function ensureSharedGitignore(worktree, git, push) {
7394
- const gitignorePath = (0, import_node_path4.join)(worktree, ".gitignore");
7890
+ const gitignorePath = (0, import_node_path5.join)(worktree, ".gitignore");
7395
7891
  const existing = await readFileIfExists(gitignorePath) ?? "";
7396
7892
  const lines = existing.split("\n").map((line) => line.trim());
7397
7893
  const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
@@ -7492,7 +7988,7 @@ function autoShareState(config) {
7492
7988
  return state;
7493
7989
  }
7494
7990
  function pendingReindexesPath(config) {
7495
- return (0, import_node_path4.join)(config.agentContextHome, "share", "auto-sync-pending-reindexes.json");
7991
+ return (0, import_node_path5.join)(config.agentContextHome, "share", "auto-sync-pending-reindexes.json");
7496
7992
  }
7497
7993
  async function loadPendingReindexes(config, state) {
7498
7994
  const raw = await readFileIfExists(pendingReindexesPath(config));
@@ -7536,7 +8032,7 @@ async function writePendingReindexes(config, state) {
7536
8032
  teams: Object.fromEntries(state.pendingReindexes),
7537
8033
  version: 1
7538
8034
  };
7539
- await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
8035
+ await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
7540
8036
  const tempPath = `${path}.${process.pid}.tmp`;
7541
8037
  await (0, import_promises4.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
7542
8038
  `, { encoding: "utf8", mode: 384 });
@@ -7629,7 +8125,7 @@ async function runShareSync(config, options) {
7629
8125
  if (dryRun) {
7630
8126
  console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
7631
8127
  } else if (pullResult && pullResult.exitCode !== 0) {
7632
- if (await exists((0, import_node_path4.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path4.join)(team.config.gitdir, "rebase-apply"))) {
8128
+ if (await exists((0, import_node_path5.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path5.join)(team.config.gitdir, "rebase-apply"))) {
7633
8129
  throw new Error(
7634
8130
  `git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
7635
8131
  Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
@@ -7684,7 +8180,7 @@ async function runShareSyncQuiet(config, state, options) {
7684
8180
  const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
7685
8181
  const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
7686
8182
  if (pullResult.exitCode !== 0) {
7687
- if (await exists((0, import_node_path4.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path4.join)(team.config.gitdir, "rebase-apply"))) {
8183
+ if (await exists((0, import_node_path5.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path5.join)(team.config.gitdir, "rebase-apply"))) {
7688
8184
  throw new Error(
7689
8185
  `Automatic share sync hit git conflicts in ${worktree}. Resolve them in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then rerun recall/read.`
7690
8186
  );
@@ -7858,10 +8354,10 @@ function normalizeTeamName(input2) {
7858
8354
  return candidate;
7859
8355
  }
7860
8356
  function teamsFilePath(config) {
7861
- return (0, import_node_path4.join)(config.agentContextHome, "share", "teams.json");
8357
+ return (0, import_node_path5.join)(config.agentContextHome, "share", "teams.json");
7862
8358
  }
7863
8359
  function teamWorktreePath(config, team) {
7864
- return (0, import_node_path4.join)(
8360
+ return (0, import_node_path5.join)(
7865
8361
  config.agentContextHome,
7866
8362
  "data",
7867
8363
  "viking",
@@ -7874,7 +8370,7 @@ function teamWorktreePath(config, team) {
7874
8370
  );
7875
8371
  }
7876
8372
  function teamGitdirPath(config, team) {
7877
- return (0, import_node_path4.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
8373
+ return (0, import_node_path5.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
7878
8374
  }
7879
8375
  async function readTeamsFile(config) {
7880
8376
  const path = teamsFilePath(config);
@@ -7917,7 +8413,7 @@ async function readTeamsFile(config) {
7917
8413
  }
7918
8414
  async function writeTeamsFile(config, contents) {
7919
8415
  const path = teamsFilePath(config);
7920
- await (0, import_promises4.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
8416
+ await (0, import_promises4.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
7921
8417
  const serializable = {
7922
8418
  defaultTeam: contents.defaultTeam,
7923
8419
  teams: contents.teams,
@@ -7986,7 +8482,7 @@ async function walkMemoryFiles(root) {
7986
8482
  if (entry.name === ".git") {
7987
8483
  continue;
7988
8484
  }
7989
- const full = (0, import_node_path4.join)(path, entry.name);
8485
+ const full = (0, import_node_path5.join)(path, entry.name);
7990
8486
  if (entry.isDirectory()) {
7991
8487
  if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
7992
8488
  continue;
@@ -8010,11 +8506,39 @@ async function walkMemoryFiles(root) {
8010
8506
  return out;
8011
8507
  }
8012
8508
  function workfileToVikingUri(config, team, filePath) {
8013
- const rel = (0, import_node_path4.relative)(team.worktree, filePath).split(import_node_path4.sep).join("/");
8509
+ const rel = (0, import_node_path5.relative)(team.worktree, filePath).split(import_node_path5.sep).join("/");
8014
8510
  return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
8015
8511
  }
8016
8512
  function isInSharedNamespace(config, uri) {
8017
- return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`);
8513
+ return sharedTeamNameForUri(config, uri) !== void 0;
8514
+ }
8515
+ function sharedTeamNameForUri(config, uri) {
8516
+ const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
8517
+ if (!uri.startsWith(prefix)) {
8518
+ return void 0;
8519
+ }
8520
+ const [team] = uri.slice(prefix.length).split("/");
8521
+ return team || void 0;
8522
+ }
8523
+ function sharedMemoryUriParts(config, uri) {
8524
+ const prefix = `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/`;
8525
+ if (!uri.startsWith(prefix)) {
8526
+ return void 0;
8527
+ }
8528
+ const [team, kind, scope, project, ...topicParts] = uri.slice(prefix.length).split("/");
8529
+ if (!team) {
8530
+ return void 0;
8531
+ }
8532
+ if (kind !== "durable" || scope !== "projects" || !project || topicParts.length === 0) {
8533
+ return { team };
8534
+ }
8535
+ const topicPath = topicParts.join("/");
8536
+ return {
8537
+ kind,
8538
+ project,
8539
+ team,
8540
+ topic: topicPath.endsWith(".md") ? topicPath.slice(0, -".md".length) : topicPath
8541
+ };
8018
8542
  }
8019
8543
  function isInTeamNamespace(config, uri, team) {
8020
8544
  return uri.startsWith(`viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team}/`);
@@ -8130,8 +8654,8 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun, op
8130
8654
  }
8131
8655
  return;
8132
8656
  }
8133
- const stagingDir = await (0, import_promises4.mkdtemp)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
8134
- const tempPath = (0, import_node_path4.join)(stagingDir, "body.txt");
8657
+ const stagingDir = await (0, import_promises4.mkdtemp)((0, import_node_path5.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
8658
+ const tempPath = (0, import_node_path5.join)(stagingDir, "body.txt");
8135
8659
  try {
8136
8660
  await (0, import_promises4.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
8137
8661
  await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode, options);
@@ -8253,7 +8777,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
8253
8777
  if (!relative3) {
8254
8778
  return;
8255
8779
  }
8256
- await (0, import_promises4.rm)((0, import_node_path4.join)(worktree, relative3), { force: true });
8780
+ await (0, import_promises4.rm)((0, import_node_path5.join)(worktree, relative3), { force: true });
8257
8781
  }
8258
8782
  async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
8259
8783
  const args = withIdentity(config, ["rm", uri]);
@@ -8317,8 +8841,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
8317
8841
  const oldRel = entries[index + 1];
8318
8842
  const newRel = entries[index + 2];
8319
8843
  if (oldRel && newRel) {
8320
- changes.push({ path: (0, import_node_path4.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
8321
- changes.push({ path: (0, import_node_path4.join)(worktree, newRel), relativePath: newRel, status: "added" });
8844
+ changes.push({ path: (0, import_node_path5.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
8845
+ changes.push({ path: (0, import_node_path5.join)(worktree, newRel), relativePath: newRel, status: "added" });
8322
8846
  }
8323
8847
  index += 3;
8324
8848
  continue;
@@ -8326,7 +8850,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
8326
8850
  const rel = entries[index + 1];
8327
8851
  if (rel) {
8328
8852
  const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
8329
- changes.push({ path: (0, import_node_path4.join)(worktree, rel), relativePath: rel, status });
8853
+ changes.push({ path: (0, import_node_path5.join)(worktree, rel), relativePath: rel, status });
8330
8854
  }
8331
8855
  index += 2;
8332
8856
  }
@@ -8410,6 +8934,12 @@ function parseMemoryStatus(value) {
8410
8934
  }
8411
8935
  throw new Error(`Unsupported memory status "${value}". Expected active, archived, or superseded.`);
8412
8936
  }
8937
+ function parseCompactKind(value) {
8938
+ if (["durable", "handoff", "incident"].includes(value)) {
8939
+ return value;
8940
+ }
8941
+ throw new Error(`Unsupported compact kind "${value}". Expected durable, handoff, or incident.`);
8942
+ }
8413
8943
  async function runRemember(config, options) {
8414
8944
  const text = await getInputText(options.text, options.stdin === true);
8415
8945
  if (!text.trim()) {
@@ -8417,11 +8947,11 @@ async function runRemember(config, options) {
8417
8947
  }
8418
8948
  const metadata = {
8419
8949
  kind: options.kind ?? "durable",
8420
- project: normalizeOptionalMetadata(options.project),
8950
+ project: normalizeOptionalMetadata2(options.project),
8421
8951
  sourceAgentClient: options.sourceAgentClient ?? "codex",
8422
8952
  status: options.status ?? "active",
8423
8953
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8424
- topic: normalizeOptionalMetadata(options.topic)
8954
+ topic: normalizeOptionalMetadata2(options.topic)
8425
8955
  };
8426
8956
  await storeMemory(config, {
8427
8957
  bodyText: text.trim(),
@@ -8442,7 +8972,7 @@ async function runMigrateMemories(config, options) {
8442
8972
  const candidates = await legacyMemoryCandidates(config, sourceAccounts);
8443
8973
  const existingHashes = await existingDurableMemoryHashes(config);
8444
8974
  const ov = await openVikingCliForMode(dryRun);
8445
- const migrationPath = (0, import_node_path5.join)(config.agentContextHome, "legacy-memory-migration.txt");
8975
+ const migrationPath = (0, import_node_path6.join)(config.agentContextHome, "legacy-memory-migration.txt");
8446
8976
  let duplicateCount = 0;
8447
8977
  let migratedCount = 0;
8448
8978
  let sensitiveCount = 0;
@@ -8505,7 +9035,7 @@ async function runMigrateLifecycle(config, options) {
8505
9035
  const limit = options.limit ? parsePositiveInteger(options.limit, "lifecycle migration limit") : void 0;
8506
9036
  const ov = await openVikingCliForMode(dryRun);
8507
9037
  const candidates = await legacyLifecycleHandoffCandidates(config);
8508
- const migrationPath = (0, import_node_path5.join)(config.agentContextHome, "lifecycle-memory-migration.txt");
9038
+ const migrationPath = (0, import_node_path6.join)(config.agentContextHome, "lifecycle-memory-migration.txt");
8509
9039
  let existingCount = 0;
8510
9040
  let migratedCount = 0;
8511
9041
  let skippedCount = 0;
@@ -8515,7 +9045,7 @@ async function runMigrateLifecycle(config, options) {
8515
9045
  break;
8516
9046
  }
8517
9047
  const destinationUri = lifecycleMigrationUri(config, candidate.metadata, sha256(candidate.original.trim()));
8518
- const migratedMemory = formatMemoryDocument(
9048
+ const migratedMemory = formatMemoryDocument2(
8519
9049
  "HANDOFF",
8520
9050
  candidate.metadata,
8521
9051
  ["Migrated legacy handoff from the historical events trail.", "", candidate.original.trim()].join("\n")
@@ -8561,8 +9091,10 @@ async function runRecall(config, options) {
8561
9091
  await syncSharedReposAndLog(config);
8562
9092
  }
8563
9093
  const ov = await openVikingCliForMode(options.dryRun === true);
8564
- const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, options.query));
8565
- const args = ["search", options.query];
9094
+ const query = await enrichRecallQueryWithWorkspaceContext(options.query);
9095
+ const projectQuery = await enrichRecallQueryWithWorkspaceProjectContext(options.query);
9096
+ const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, projectQuery));
9097
+ const args = ["search", query];
8566
9098
  if (inferredUri) {
8567
9099
  args.push("--uri", inferredUri);
8568
9100
  console.log(`Recall scope: ${inferredUri}`);
@@ -8570,24 +9102,41 @@ async function runRecall(config, options) {
8570
9102
  if (options.nodeLimit) {
8571
9103
  args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
8572
9104
  }
8573
- await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
8574
- await augmentRecallWithSeededResources(config, ov, options, inferredUri);
8575
- await printExactMemoryMatches(config, ov, options.query, {
9105
+ const recallOutputs = [];
9106
+ const baseResult = await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
9107
+ if (baseResult) {
9108
+ recallOutputs.push([baseResult.stdout.trim(), baseResult.stderr.trim()].filter(Boolean).join("\n"));
9109
+ }
9110
+ const seededOutput = await augmentRecallWithSeededResources(
9111
+ config,
9112
+ ov,
9113
+ { ...options, query },
9114
+ inferredUri,
9115
+ projectQuery
9116
+ );
9117
+ if (seededOutput) {
9118
+ recallOutputs.push(seededOutput);
9119
+ }
9120
+ const exactOutput = await printExactMemoryMatches(config, ov, query, {
8576
9121
  dryRun: options.dryRun === true,
8577
9122
  includeArchived: options.includeArchived === true
8578
9123
  });
9124
+ if (exactOutput) {
9125
+ recallOutputs.push(exactOutput);
9126
+ }
9127
+ await printRecallHygieneNudges(config, recallOutputs.join("\n"));
8579
9128
  }
8580
- async function augmentRecallWithSeededResources(config, ov, options, inferredUri) {
9129
+ async function augmentRecallWithSeededResources(config, ov, options, inferredUri, projectQuery = options.query) {
8581
9130
  if (options.uri || options.inferScope === false) {
8582
- return;
9131
+ return void 0;
8583
9132
  }
8584
- const project = await inferProjectFromQuery(config.manifestPath, options.query);
9133
+ const project = await inferProjectFromQuery(config.manifestPath, projectQuery);
8585
9134
  if (!project) {
8586
- return;
9135
+ return void 0;
8587
9136
  }
8588
9137
  const projectResourceUri = trimTrailingSlash(project.uri);
8589
9138
  if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
8590
- return;
9139
+ return void 0;
8591
9140
  }
8592
9141
  const args = ["search", options.query, "--uri", projectResourceUri];
8593
9142
  if (options.nodeLimit) {
@@ -8595,7 +9144,8 @@ async function augmentRecallWithSeededResources(config, ov, options, inferredUri
8595
9144
  }
8596
9145
  console.log(`
8597
9146
  Also searching seeded resources: ${projectResourceUri}`);
8598
- await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
9147
+ const result = await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
9148
+ return result ? [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n") : void 0;
8599
9149
  }
8600
9150
  async function runRead(config, uri, options) {
8601
9151
  assertVikingUri(uri);
@@ -8623,6 +9173,147 @@ async function syncSharedReposAndLog(config) {
8623
9173
  console.error(`Auto-sync warning: ${err instanceof Error ? err.message : String(err)}`);
8624
9174
  }
8625
9175
  }
9176
+ async function printRecallHygieneNudges(config, recallOutput) {
9177
+ const uris = activePersonalMemoryUrisFromText(recallOutput, config.user);
9178
+ if (uris.length === 0) {
9179
+ return;
9180
+ }
9181
+ const records = await readMemoryRecordsByUri(config, uris);
9182
+ const nudges = recallHygieneNudges(recallOutput, { records, user: config.user });
9183
+ if (nudges.length === 0) {
9184
+ return;
9185
+ }
9186
+ console.log("\nMemory hygiene hints:");
9187
+ for (const nudge of nudges) {
9188
+ console.log(`- ${nudge}`);
9189
+ }
9190
+ }
9191
+ async function runCompact(config, options) {
9192
+ const project = normalizeOptionalMetadata2(options.project);
9193
+ if (!project) {
9194
+ throw new Error("Provide --project for scoped memory hygiene.");
9195
+ }
9196
+ if (options.apply === true && options.dryRun === true) {
9197
+ throw new Error("Cannot combine --apply with --dry-run.");
9198
+ }
9199
+ const apply = options.apply === true;
9200
+ const records = await scopedCompactRecords(config, {
9201
+ kind: options.kind,
9202
+ project
9203
+ });
9204
+ const plan = buildCompactPlan(records, {
9205
+ kind: options.kind,
9206
+ project,
9207
+ topic: normalizeOptionalMetadata2(options.topic)
9208
+ });
9209
+ console.log(formatCompactPlan(plan, { apply }));
9210
+ if (!apply) {
9211
+ return;
9212
+ }
9213
+ const ov = await openVikingCliForMode(false);
9214
+ const updatePath = (0, import_node_path6.join)(config.agentContextHome, "compact-memory-update.txt");
9215
+ try {
9216
+ for (const action of plan.keepUpdates) {
9217
+ await (0, import_promises5.writeFile)(updatePath, action.content, { encoding: "utf8", mode: 384 });
9218
+ await (0, import_promises5.chmod)(updatePath, 384);
9219
+ await writeDurableMemoryFile(ov, config, action.uri, updatePath, "replace");
9220
+ }
9221
+ } finally {
9222
+ await (0, import_promises5.rm)(updatePath, { force: true });
9223
+ }
9224
+ for (const action of plan.archives) {
9225
+ await runArchive(config, action.uri, {
9226
+ dryRun: false,
9227
+ kind: action.kind,
9228
+ project: action.project,
9229
+ topic: action.topic
9230
+ });
9231
+ }
9232
+ for (const action of plan.forgets) {
9233
+ await runForget(config, action.uri, { dryRun: false });
9234
+ }
9235
+ }
9236
+ async function scopedCompactRecords(config, options) {
9237
+ const kinds = options.kind ? [options.kind] : ["handoff", "durable", "incident"];
9238
+ const records = [];
9239
+ for (const kind of kinds) {
9240
+ const directory = localMemoryDirectoryForCompact(config, kind, options.project);
9241
+ const uriDirectory = memoryUriDirectoryForCompact(config, kind, options.project);
9242
+ let entries;
9243
+ try {
9244
+ entries = await (0, import_promises5.readdir)(directory, { withFileTypes: true });
9245
+ } catch (_err) {
9246
+ continue;
9247
+ }
9248
+ for (const entry of entries) {
9249
+ if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
9250
+ continue;
9251
+ }
9252
+ const content = await readTextIfExists((0, import_node_path6.join)(directory, entry.name));
9253
+ if (!content) {
9254
+ continue;
9255
+ }
9256
+ const record = parseMemoryDocument(`${uriDirectory}/${entry.name}`, content);
9257
+ if (record) {
9258
+ records.push(record);
9259
+ }
9260
+ }
9261
+ }
9262
+ return records;
9263
+ }
9264
+ async function readMemoryRecordsByUri(config, uris) {
9265
+ const records = [];
9266
+ for (const uri of uris) {
9267
+ const localPath = localMemoryPathForUri(config, uri);
9268
+ if (!localPath) {
9269
+ continue;
9270
+ }
9271
+ const content = await readTextIfExists(localPath);
9272
+ if (!content) {
9273
+ continue;
9274
+ }
9275
+ const record = parseMemoryDocument(uri, content);
9276
+ if (record) {
9277
+ records.push(record);
9278
+ }
9279
+ }
9280
+ return records;
9281
+ }
9282
+ function localMemoryDirectoryForCompact(config, kind, project) {
9283
+ const root = localUserMemoriesRoot(config);
9284
+ const projectSegment = uriSegment(project);
9285
+ switch (kind) {
9286
+ case "durable":
9287
+ return (0, import_node_path6.join)(root, "durable", "projects", projectSegment);
9288
+ case "handoff":
9289
+ return (0, import_node_path6.join)(root, "handoffs", "active", projectSegment);
9290
+ case "incident":
9291
+ return (0, import_node_path6.join)(root, "incidents", "active", projectSegment);
9292
+ }
9293
+ }
9294
+ function memoryUriDirectoryForCompact(config, kind, project) {
9295
+ const base = `viking://user/${uriSegment(config.user)}/memories`;
9296
+ const projectSegment = uriSegment(project);
9297
+ switch (kind) {
9298
+ case "durable":
9299
+ return `${base}/durable/projects/${projectSegment}`;
9300
+ case "handoff":
9301
+ return `${base}/handoffs/active/${projectSegment}`;
9302
+ case "incident":
9303
+ return `${base}/incidents/active/${projectSegment}`;
9304
+ }
9305
+ }
9306
+ function localMemoryPathForUri(config, uri) {
9307
+ const prefix = `viking://user/${uriSegment(config.user)}/memories/`;
9308
+ if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
9309
+ return void 0;
9310
+ }
9311
+ const relative3 = uri.slice(prefix.length);
9312
+ if (relative3.includes("..") || relative3.startsWith("/")) {
9313
+ return void 0;
9314
+ }
9315
+ return (0, import_node_path6.join)(localUserMemoriesRoot(config), ...relative3.split("/"));
9316
+ }
8626
9317
  async function runList(config, uri, options) {
8627
9318
  assertVikingUri(uri);
8628
9319
  const ov = await openVikingCliForMode(options.dryRun === true);
@@ -8660,11 +9351,11 @@ async function runArchive(config, uri, options) {
8660
9351
  const fallbackMetadata = {
8661
9352
  archivedFrom: uri,
8662
9353
  kind: options.kind ?? "handoff",
8663
- project: normalizeOptionalMetadata(options.project),
9354
+ project: normalizeOptionalMetadata2(options.project),
8664
9355
  sourceAgentClient: "threadnote",
8665
9356
  status: "archived",
8666
9357
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8667
- topic: normalizeOptionalMetadata(options.topic)
9358
+ topic: normalizeOptionalMetadata2(options.topic)
8668
9359
  };
8669
9360
  await storeMemory(config, {
8670
9361
  bodyText: ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n"),
@@ -8682,11 +9373,11 @@ async function runArchive(config, uri, options) {
8682
9373
  const metadata = {
8683
9374
  archivedFrom: uri,
8684
9375
  kind: options.kind ?? inferredMetadata.kind ?? "handoff",
8685
- project: normalizeOptionalMetadata(options.project) ?? inferredMetadata.project,
9376
+ project: normalizeOptionalMetadata2(options.project) ?? inferredMetadata.project,
8686
9377
  sourceAgentClient: "threadnote",
8687
9378
  status: "archived",
8688
9379
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8689
- topic: normalizeOptionalMetadata(options.topic) ?? inferredMetadata.topic
9380
+ topic: normalizeOptionalMetadata2(options.topic) ?? inferredMetadata.topic
8690
9381
  };
8691
9382
  await storeMemory(config, {
8692
9383
  bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
@@ -8715,7 +9406,7 @@ async function runForget(config, uri, options) {
8715
9406
  }
8716
9407
  async function runExportPack(config, options) {
8717
9408
  const ov = await openVikingCliForMode(options.dryRun === true);
8718
- const defaultPath = (0, import_node_path5.join)(config.agentContextHome, `threadnote-${safeTimestamp()}.ovpack`);
9409
+ const defaultPath = (0, import_node_path6.join)(config.agentContextHome, `threadnote-${safeTimestamp()}.ovpack`);
8719
9410
  const outputPath = expandPath(options.path ?? defaultPath);
8720
9411
  await maybeRun(options.dryRun === true, ov, withIdentity(config, ["export", options.uri ?? "viking://", outputPath]));
8721
9412
  }
@@ -8740,7 +9431,7 @@ async function inferRecallUri(config, query) {
8740
9431
  async function printExactMemoryMatches(config, ov, query, options) {
8741
9432
  const terms = exactRecallTerms(query);
8742
9433
  if (terms.length === 0) {
8743
- return;
9434
+ return void 0;
8744
9435
  }
8745
9436
  const scopes = exactMemoryScopes(config, options.includeArchived);
8746
9437
  const outputs = [];
@@ -8752,30 +9443,36 @@ async function printExactMemoryMatches(config, ov, query, options) {
8752
9443
  continue;
8753
9444
  }
8754
9445
  const result = await runCommand(ov, args, { allowFailure: true });
8755
- const output2 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
8756
- if (result.exitCode === 0 && grepOutputHasMatches(output2)) {
8757
- outputs.push(output2);
9446
+ const output3 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
9447
+ if (result.exitCode === 0 && grepOutputHasMatches(output3)) {
9448
+ outputs.push(output3);
8758
9449
  }
8759
9450
  }
8760
9451
  }
8761
9452
  if (outputs.length === 0) {
8762
- return;
9453
+ return void 0;
8763
9454
  }
8764
- console.log("\nExact durable memory matches:");
8765
- console.log(outputs.join("\n\n"));
9455
+ console.log("\nExact memory/resource matches:");
9456
+ const output2 = outputs.join("\n\n");
9457
+ console.log(output2);
9458
+ return output2;
8766
9459
  }
8767
9460
  async function storeMemory(config, options) {
8768
9461
  if (options.replaceUri) {
8769
9462
  assertVikingUri(options.replaceUri);
8770
9463
  }
8771
9464
  const ov = await openVikingCliForMode(options.dryRun);
8772
- const memoryPath = (0, import_node_path5.join)(config.agentContextHome, "last-memory.txt");
9465
+ if (options.replaceUri && isInSharedNamespace(config, options.replaceUri)) {
9466
+ await storeSharedMemoryReplacement(config, ov, options, options.replaceUri);
9467
+ return;
9468
+ }
9469
+ const memoryPath = (0, import_node_path6.join)(config.agentContextHome, "last-memory.txt");
8773
9470
  const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
8774
- const candidateMemory = formatMemoryDocument(options.title, candidateMetadata, options.bodyText);
9471
+ const candidateMemory = formatMemoryDocument2(options.title, candidateMetadata, options.bodyText);
8775
9472
  const memoryUri = memoryUriFor(config, candidateMemory, candidateMetadata);
8776
9473
  const isInPlaceUpdate = options.replaceUri !== void 0 && options.replaceUri === memoryUri;
8777
9474
  const finalMetadata = isInPlaceUpdate ? { ...options.metadata, supersedes: void 0 } : candidateMetadata;
8778
- const memory = isInPlaceUpdate ? formatMemoryDocument(options.title, finalMetadata, options.bodyText) : candidateMemory;
9475
+ const memory = isInPlaceUpdate ? formatMemoryDocument2(options.title, finalMetadata, options.bodyText) : candidateMemory;
8779
9476
  const writeMode = await memoryWriteMode(ov, config, memoryUri, finalMetadata);
8780
9477
  if (options.dryRun) {
8781
9478
  console.log(memory);
@@ -8819,6 +9516,49 @@ async function storeMemory(config, options) {
8819
9516
  console.log(`Updated existing memory in place: ${memoryUri}`);
8820
9517
  }
8821
9518
  }
9519
+ async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
9520
+ if (options.metadata.kind !== "durable") {
9521
+ throw new Error("Shared memory replacement only supports durable memories.");
9522
+ }
9523
+ const teamName = sharedTeamNameForUri(config, targetUri);
9524
+ if (!teamName) {
9525
+ throw new Error(`Memory ${targetUri} is not in the shared namespace.`);
9526
+ }
9527
+ const team = await resolveTeam(config, teamName);
9528
+ const inferred = sharedMemoryUriParts(config, targetUri);
9529
+ const metadata = {
9530
+ ...options.metadata,
9531
+ project: options.metadata.project ?? inferred?.project,
9532
+ topic: options.metadata.topic ?? inferred?.topic
9533
+ };
9534
+ const rawMemory = formatMemoryDocument2(options.title, metadata, options.bodyText);
9535
+ const scrub = applyScrubber(stripPersonalProvenance(rawMemory), { redact: false });
9536
+ if (scrub.blocker) {
9537
+ throw new Error(
9538
+ `Refusing to update shared memory ${targetUri}: possible ${scrub.blocker}. Strip the sensitive value first.`
9539
+ );
9540
+ }
9541
+ const memory = scrub.cleaned;
9542
+ const relativePath = vikingUriToWorktreeRelative(config, targetUri, team.name);
9543
+ if (options.dryRun) {
9544
+ console.log(memory);
9545
+ console.log("\nWould run:");
9546
+ }
9547
+ await ensureSharedDirectoryChain(config, ov, targetUri, options.dryRun);
9548
+ await writeMemoryFile(config, ov, targetUri, memory, "replace", options.dryRun);
9549
+ const git = await requiredExecutable("git");
9550
+ await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "add", "--", relativePath]);
9551
+ await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "commit", "-m", `share: update ${relativePath}`], {
9552
+ allowFailure: true
9553
+ });
9554
+ await maybeRun(options.dryRun, git, ["-C", team.config.worktree, "push", DEFAULT_GIT_REMOTE_NAME], {
9555
+ allowFailure: true
9556
+ });
9557
+ for (const redaction of scrub.redactions) {
9558
+ console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before shared update.`);
9559
+ }
9560
+ console.log(`Updated shared memory: ${targetUri}`);
9561
+ }
8822
9562
  async function writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode) {
8823
9563
  const args = withIdentity(config, [
8824
9564
  "write",
@@ -8917,7 +9657,7 @@ async function hasLegacyLifecycleHandoffCandidates(config) {
8917
9657
  return (await legacyLifecycleHandoffCandidates(config, 1)).length > 0;
8918
9658
  }
8919
9659
  async function legacyLifecycleHandoffCandidates(config, limit) {
8920
- const eventsRoot = (0, import_node_path5.join)(localUserMemoriesRoot(config), "events");
9660
+ const eventsRoot = (0, import_node_path6.join)(localUserMemoriesRoot(config), "events");
8921
9661
  let entries;
8922
9662
  try {
8923
9663
  entries = await (0, import_promises5.readdir)(eventsRoot, { withFileTypes: true });
@@ -8929,7 +9669,7 @@ async function legacyLifecycleHandoffCandidates(config, limit) {
8929
9669
  if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
8930
9670
  continue;
8931
9671
  }
8932
- const sourcePath = (0, import_node_path5.join)(eventsRoot, entry.name);
9672
+ const sourcePath = (0, import_node_path6.join)(eventsRoot, entry.name);
8933
9673
  const original = await readTextIfExists(sourcePath);
8934
9674
  if (!original || !isClearLegacyHandoffMemory(original) || sensitiveMemoryReason(original)) {
8935
9675
  continue;
@@ -9015,7 +9755,7 @@ function vikingDirectoryChain(directoryUri) {
9015
9755
  }
9016
9756
  return chain;
9017
9757
  }
9018
- function formatMemoryDocument(title, metadata, body) {
9758
+ function formatMemoryDocument2(title, metadata, body) {
9019
9759
  const header = [
9020
9760
  title,
9021
9761
  `kind: ${metadata.kind}`,
@@ -9032,10 +9772,10 @@ function formatMemoryDocument(title, metadata, body) {
9032
9772
  function inferMemoryMetadata(memory) {
9033
9773
  const header = memory.slice(0, Math.max(0, memory.indexOf("\n\n")) || memory.length);
9034
9774
  const firstLine2 = header.split("\n")[0]?.trim();
9035
- const kind = parseOptionalMemoryKind(parseHeaderValue(header, "kind")) ?? (firstLine2 === "HANDOFF" ? "handoff" : void 0);
9036
- const status = parseOptionalMemoryStatus(parseHeaderValue(header, "status"));
9037
- const project = normalizeOptionalMetadata(parseHeaderValue(header, "project")) ?? normalizeOptionalMetadata(parseHeaderValue(header, "repo"));
9038
- const topic = normalizeOptionalMetadata(parseHeaderValue(header, "topic")) ?? normalizeOptionalMetadata(parseHeaderValue(header, "task"));
9775
+ const kind = parseOptionalMemoryKind2(parseHeaderValue(header, "kind")) ?? (firstLine2 === "HANDOFF" ? "handoff" : void 0);
9776
+ const status = parseOptionalMemoryStatus2(parseHeaderValue(header, "status"));
9777
+ const project = normalizeOptionalMetadata2(parseHeaderValue(header, "project")) ?? normalizeOptionalMetadata2(parseHeaderValue(header, "repo"));
9778
+ const topic = normalizeOptionalMetadata2(parseHeaderValue(header, "topic")) ?? normalizeOptionalMetadata2(parseHeaderValue(header, "task"));
9039
9779
  return {
9040
9780
  kind,
9041
9781
  project,
@@ -9071,9 +9811,9 @@ function inferLegacyProject(memory) {
9071
9811
  return "general";
9072
9812
  }
9073
9813
  const trimmed = explicit.trim().replace(/[`.,;]+$/g, "");
9074
- return trimmed.includes("/") ? (0, import_node_path5.basename)(trimmed) : trimmed;
9814
+ return trimmed.includes("/") ? (0, import_node_path6.basename)(trimmed) : trimmed;
9075
9815
  }
9076
- function parseOptionalMemoryKind(value) {
9816
+ function parseOptionalMemoryKind2(value) {
9077
9817
  if (!value) {
9078
9818
  return void 0;
9079
9819
  }
@@ -9083,7 +9823,7 @@ function parseOptionalMemoryKind(value) {
9083
9823
  return void 0;
9084
9824
  }
9085
9825
  }
9086
- function parseOptionalMemoryStatus(value) {
9826
+ function parseOptionalMemoryStatus2(value) {
9087
9827
  if (!value) {
9088
9828
  return void 0;
9089
9829
  }
@@ -9093,7 +9833,7 @@ function parseOptionalMemoryStatus(value) {
9093
9833
  return void 0;
9094
9834
  }
9095
9835
  }
9096
- function normalizeOptionalMetadata(value) {
9836
+ function normalizeOptionalMetadata2(value) {
9097
9837
  const trimmed = value?.trim();
9098
9838
  return trimmed ? trimmed : void 0;
9099
9839
  }
@@ -9111,14 +9851,14 @@ async function legacySourceAccounts(config, options) {
9111
9851
  async function legacyMemoryCandidates(config, sourceAccounts) {
9112
9852
  const candidates = [];
9113
9853
  for (const sourceAccount of sourceAccounts) {
9114
- const sessionRoot = (0, import_node_path5.join)(localVikingDataRoot(config), sourceAccount, "session");
9854
+ const sessionRoot = (0, import_node_path6.join)(localVikingDataRoot(config), sourceAccount, "session");
9115
9855
  for (const sourceSession of await childDirectoryNames(sessionRoot)) {
9116
- const historyRoot = (0, import_node_path5.join)(sessionRoot, sourceSession, "history");
9856
+ const historyRoot = (0, import_node_path6.join)(sessionRoot, sourceSession, "history");
9117
9857
  for (const sourceArchive of await childDirectoryNames(historyRoot)) {
9118
9858
  if (!sourceArchive.startsWith("archive_")) {
9119
9859
  continue;
9120
9860
  }
9121
- const sourcePath = (0, import_node_path5.join)(historyRoot, sourceArchive, "messages.jsonl");
9861
+ const sourcePath = (0, import_node_path6.join)(historyRoot, sourceArchive, "messages.jsonl");
9122
9862
  for (const text of await legacyMemoryTexts(sourcePath)) {
9123
9863
  candidates.push({
9124
9864
  comparableHash: sha256(comparableMemoryText(text)),
@@ -9186,7 +9926,7 @@ async function collectDurableMemoryHashes(root, hashes) {
9186
9926
  return;
9187
9927
  }
9188
9928
  for (const entry of entries) {
9189
- const path = (0, import_node_path5.join)(root, entry.name);
9929
+ const path = (0, import_node_path6.join)(root, entry.name);
9190
9930
  if (entry.isDirectory()) {
9191
9931
  await collectDurableMemoryHashes(path, hashes);
9192
9932
  continue;
@@ -9203,7 +9943,7 @@ async function collectDurableMemoryHashes(root, hashes) {
9203
9943
  }
9204
9944
  }
9205
9945
  function isDurableMemoryPath(path) {
9206
- return path.split(import_node_path5.sep).includes("memories");
9946
+ return path.split(import_node_path6.sep).includes("memories");
9207
9947
  }
9208
9948
  async function childDirectoryNames(path) {
9209
9949
  let entries;
@@ -9247,10 +9987,10 @@ function legacySourceLabel(candidate) {
9247
9987
  return `${candidate.sourceAccount}/${candidate.sourceSession}/${candidate.sourceArchive}`;
9248
9988
  }
9249
9989
  function localVikingDataRoot(config) {
9250
- return (0, import_node_path5.join)(config.agentContextHome, "data", "viking");
9990
+ return (0, import_node_path6.join)(config.agentContextHome, "data", "viking");
9251
9991
  }
9252
9992
  function localUserMemoriesRoot(config) {
9253
- return (0, import_node_path5.join)(localVikingDataRoot(config), config.account, "user", uriSegment(config.user), "memories");
9993
+ return (0, import_node_path6.join)(localVikingDataRoot(config), config.account, "user", uriSegment(config.user), "memories");
9254
9994
  }
9255
9995
  function uniqueStrings(values) {
9256
9996
  return [...new Set(values)].sort();
@@ -9266,14 +10006,15 @@ async function buildHandoff(options) {
9266
10006
  const status = await gitValue(["status", "--short"], repoRoot) ?? "";
9267
10007
  const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
9268
10008
  const touchedFiles = await gitTouchedFiles(repoRoot);
9269
- const repoName = (0, import_node_path5.basename)(repoRoot);
10009
+ const repoName = (0, import_node_path6.basename)(repoRoot);
10010
+ const topicBranch = branch && branch !== "unknown" ? branch : "current";
9270
10011
  const metadata = {
9271
10012
  kind: "handoff",
9272
- project: normalizeOptionalMetadata(options.project) ?? repoName,
10013
+ project: normalizeOptionalMetadata2(options.project) ?? repoName,
9273
10014
  sourceAgentClient: options.sourceAgentClient ?? "codex",
9274
10015
  status: "active",
9275
10016
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9276
- topic: normalizeOptionalMetadata(options.topic)
10017
+ topic: handoffTopicForBranch(topicBranch, { timestamped: options.timestamped, topic: options.topic })
9277
10018
  };
9278
10019
  const bodyText = [
9279
10020
  `repo: ${repoName}`,
@@ -9326,7 +10067,7 @@ function formatBlock(value, emptyValue) {
9326
10067
  // src/update-check.ts
9327
10068
  var import_node_child_process2 = require("node:child_process");
9328
10069
  var import_promises6 = require("node:fs/promises");
9329
- var import_node_path6 = require("node:path");
10070
+ var import_node_path7 = require("node:path");
9330
10071
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
9331
10072
  var NPM_LATEST_URL = "https://registry.npmjs.org/threadnote/latest";
9332
10073
  var FETCH_TIMEOUT_MS = 3e3;
@@ -9405,7 +10146,7 @@ async function readUpdateCache(cachePath) {
9405
10146
  }
9406
10147
  async function writeUpdateCache(cachePath, contents) {
9407
10148
  try {
9408
- await (0, import_promises6.mkdir)((0, import_node_path6.dirname)(cachePath), { recursive: true });
10149
+ await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(cachePath), { recursive: true });
9409
10150
  await (0, import_promises6.writeFile)(cachePath, `${JSON.stringify(contents)}
9410
10151
  `, { encoding: "utf8", mode: 384 });
9411
10152
  } catch {
@@ -9429,14 +10170,14 @@ async function fetchLatestVersion() {
9429
10170
 
9430
10171
  // src/version.ts
9431
10172
  var import_node_fs4 = require("node:fs");
9432
- var import_node_path7 = require("node:path");
10173
+ var import_node_path8 = require("node:path");
9433
10174
  var cachedVersion;
9434
10175
  function getThreadnoteVersion() {
9435
10176
  if (cachedVersion !== void 0) {
9436
10177
  return cachedVersion;
9437
10178
  }
9438
10179
  try {
9439
- const packageJsonPath = (0, import_node_path7.join)(__dirname, "..", "package.json");
10180
+ const packageJsonPath = (0, import_node_path8.join)(__dirname, "..", "package.json");
9440
10181
  const parsed = JSON.parse((0, import_node_fs4.readFileSync)(packageJsonPath, "utf8"));
9441
10182
  cachedVersion = typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : "unknown";
9442
10183
  } catch {
@@ -9495,7 +10236,7 @@ async function runClaudeHooksInstall(options) {
9495
10236
  console.log("\nRe-run with --apply to actually modify the file.");
9496
10237
  return;
9497
10238
  }
9498
- await (0, import_promises7.mkdir)((0, import_node_path8.dirname)(path), { recursive: true });
10239
+ await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path), { recursive: true });
9499
10240
  const serialized = `${JSON.stringify(next, void 0, 2)}
9500
10241
  `;
9501
10242
  await (0, import_promises7.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
@@ -9595,7 +10336,7 @@ async function hasManagedClaudeHooks() {
9595
10336
  async function runPreCompactHook(config, options = {}) {
9596
10337
  try {
9597
10338
  const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
9598
- const project = repoRoot ? (0, import_node_path8.basename)(repoRoot) : "general";
10339
+ const project = repoRoot ? (0, import_node_path9.basename)(repoRoot) : "general";
9599
10340
  await runHandoff(config, {
9600
10341
  blockers: "- none recorded",
9601
10342
  dryRun: options.dryRun === true,
@@ -9619,7 +10360,7 @@ async function runSessionStartHook(config, options = {}) {
9619
10360
  if (!repoRoot) {
9620
10361
  return;
9621
10362
  }
9622
- const project = (0, import_node_path8.basename)(repoRoot);
10363
+ const project = (0, import_node_path9.basename)(repoRoot);
9623
10364
  await emitUpdateBannerIfOutdated(config);
9624
10365
  process.stdout.write(`## Threadnote \u2014 latest context for ${project}
9625
10366
 
@@ -9628,7 +10369,8 @@ async function runSessionStartHook(config, options = {}) {
9628
10369
  dryRun: options.dryRun === true,
9629
10370
  inferScope: true,
9630
10371
  nodeLimit: "5",
9631
- query: `${project} latest handoff durable feature memory`
10372
+ // Keep "current branch" here so recall enriches the query with local git/workspace terms.
10373
+ query: `${project} current branch latest handoff durable feature memory`
9632
10374
  });
9633
10375
  } catch (err) {
9634
10376
  process.stderr.write(
@@ -9640,7 +10382,7 @@ async function runSessionStartHook(config, options = {}) {
9640
10382
  async function emitUpdateBannerIfOutdated(config) {
9641
10383
  try {
9642
10384
  const result = await checkForThreadnoteUpdate({
9643
- cachePath: (0, import_node_path8.join)(config.agentContextHome, ".update-state.json"),
10385
+ cachePath: (0, import_node_path9.join)(config.agentContextHome, ".update-state.json"),
9644
10386
  currentVersion: getThreadnoteVersion()
9645
10387
  });
9646
10388
  if (!result || !result.outdated) {
@@ -9666,12 +10408,12 @@ async function emitUpdateBannerIfOutdated(config) {
9666
10408
 
9667
10409
  // src/seeding.ts
9668
10410
  var import_promises8 = require("node:fs/promises");
9669
- var import_node_path9 = require("node:path");
10411
+ var import_node_path10 = require("node:path");
9670
10412
  async function runSeed(config, options) {
9671
10413
  const manifest = await readSeedManifest(config.manifestPath);
9672
10414
  const ignorePatterns = await loadIgnorePatterns();
9673
10415
  const ov = await openVikingCliForMode(options.dryRun === true);
9674
- const statePath = (0, import_node_path9.join)(config.agentContextHome, SEED_STATE_FILE);
10416
+ const statePath = (0, import_node_path10.join)(config.agentContextHome, SEED_STATE_FILE);
9675
10417
  const state = options.force === true ? { files: {}, version: 1 } : await readSeedState(statePath);
9676
10418
  const projects = filterProjects(manifest.projects, options.only);
9677
10419
  let importedCount = 0;
@@ -9751,13 +10493,13 @@ async function readSeedState(path) {
9751
10493
  }
9752
10494
  }
9753
10495
  async function writeSeedState(path, state) {
9754
- await ensureDirectory((0, import_node_path9.dirname)(path), false);
10496
+ await ensureDirectory((0, import_node_path10.dirname)(path), false);
9755
10497
  await (0, import_promises8.writeFile)(path, `${JSON.stringify(state, void 0, 2)}
9756
10498
  `, { encoding: "utf8", mode: 384 });
9757
10499
  }
9758
10500
  async function runInitManifest(config, options) {
9759
10501
  const manifestPath = expandPath(
9760
- options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path9.join)(config.agentContextHome, USER_MANIFEST_NAME)
10502
+ options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path10.join)(config.agentContextHome, USER_MANIFEST_NAME)
9761
10503
  );
9762
10504
  const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
9763
10505
  const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
@@ -9800,7 +10542,7 @@ async function runInitManifest(config, options) {
9800
10542
  console.log(output2.trimEnd());
9801
10543
  return;
9802
10544
  }
9803
- await ensureDirectory((0, import_node_path9.dirname)(manifestPath), false);
10545
+ await ensureDirectory((0, import_node_path10.dirname)(manifestPath), false);
9804
10546
  await (0, import_promises8.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
9805
10547
  await (0, import_promises8.chmod)(manifestPath, 384);
9806
10548
  console.log(`Wrote manifest: ${manifestPath}`);
@@ -9838,7 +10580,7 @@ async function projectIdentity(path) {
9838
10580
  }
9839
10581
  }
9840
10582
  function projectManifestForRepo(repoRoot, existingProjects) {
9841
- const baseName = uriSegment((0, import_node_path9.basename)(repoRoot));
10583
+ const baseName = uriSegment((0, import_node_path10.basename)(repoRoot));
9842
10584
  const usedNames = new Set(existingProjects.map((project) => project.name));
9843
10585
  const usedUris = new Set(existingProjects.map((project) => project.uri));
9844
10586
  let name = baseName;
@@ -9860,7 +10602,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
9860
10602
  for (const pattern of project.seed) {
9861
10603
  const files = await resolveProjectPattern(projectRoot, pattern);
9862
10604
  for (const filePath of files) {
9863
- const relativePath = toPosixPath((0, import_node_path9.relative)(projectRoot, filePath));
10605
+ const relativePath = toPosixPath((0, import_node_path10.relative)(projectRoot, filePath));
9864
10606
  if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
9865
10607
  continue;
9866
10608
  }
@@ -9878,17 +10620,17 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
9878
10620
  async function resolveProjectPattern(projectRoot, pattern) {
9879
10621
  const normalizedPattern = toPosixPath(pattern);
9880
10622
  if (!hasGlob(normalizedPattern)) {
9881
- const filePath = (0, import_node_path9.join)(projectRoot, normalizedPattern);
10623
+ const filePath = (0, import_node_path10.join)(projectRoot, normalizedPattern);
9882
10624
  return await isFile(filePath) ? [filePath] : [];
9883
10625
  }
9884
10626
  const globBase = getGlobBase(normalizedPattern);
9885
- const basePath = (0, import_node_path9.join)(projectRoot, globBase);
10627
+ const basePath = (0, import_node_path10.join)(projectRoot, globBase);
9886
10628
  if (!await exists(basePath)) {
9887
10629
  return [];
9888
10630
  }
9889
10631
  const regex = globToRegExp(normalizedPattern);
9890
10632
  const files = await walkFiles(basePath);
9891
- return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path9.relative)(projectRoot, filePath))));
10633
+ return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path10.relative)(projectRoot, filePath))));
9892
10634
  }
9893
10635
  async function prepareSeedFile(config, candidate, dryRun) {
9894
10636
  const content = await (0, import_promises8.readFile)(candidate.filePath, "utf8");
@@ -9903,12 +10645,12 @@ async function prepareSeedFile(config, candidate, dryRun) {
9903
10645
  if (redactedContent === content) {
9904
10646
  return candidate.filePath;
9905
10647
  }
9906
- const redactedPath = (0, import_node_path9.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
10648
+ const redactedPath = (0, import_node_path10.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
9907
10649
  if (dryRun) {
9908
10650
  console.log(`Would write redacted copy: ${redactedPath}`);
9909
10651
  return redactedPath;
9910
10652
  }
9911
- await ensureDirectory((0, import_node_path9.dirname)(redactedPath), false);
10653
+ await ensureDirectory((0, import_node_path10.dirname)(redactedPath), false);
9912
10654
  await (0, import_promises8.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
9913
10655
  await (0, import_promises8.chmod)(redactedPath, 384);
9914
10656
  return redactedPath;
@@ -9966,10 +10708,10 @@ async function resolveAbsolutePattern(pattern) {
9966
10708
  return files.filter((filePath) => regex.test(toPosixPath(filePath)));
9967
10709
  }
9968
10710
  function skillResourceUri(skill) {
9969
- return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0, import_node_path9.basename)((0, import_node_path9.dirname)(skill.filePath)))}-${skill.hash.slice(0, 12)}.md`;
10711
+ return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0, import_node_path10.basename)((0, import_node_path10.dirname)(skill.filePath)))}-${skill.hash.slice(0, 12)}.md`;
9970
10712
  }
9971
10713
  async function loadIgnorePatterns() {
9972
- const raw = await (0, import_promises8.readFile)((0, import_node_path9.join)(toolRoot(), ".threadnoteignore"), "utf8");
10714
+ const raw = await (0, import_promises8.readFile)((0, import_node_path10.join)(toolRoot(), ".threadnoteignore"), "utf8");
9973
10715
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
9974
10716
  }
9975
10717
  function matchesIgnore(relativePath, patterns) {
@@ -10044,7 +10786,7 @@ var import_node_child_process3 = require("node:child_process");
10044
10786
  var import_node_fs6 = require("node:fs");
10045
10787
  var import_promises11 = require("node:fs/promises");
10046
10788
  var import_node_os6 = require("node:os");
10047
- var import_node_path11 = require("node:path");
10789
+ var import_node_path12 = require("node:path");
10048
10790
  var import_node_process2 = require("node:process");
10049
10791
  var import_promises12 = require("node:readline/promises");
10050
10792
 
@@ -10052,7 +10794,7 @@ var import_promises12 = require("node:readline/promises");
10052
10794
  var import_node_fs5 = require("node:fs");
10053
10795
  var import_promises9 = require("node:fs/promises");
10054
10796
  var import_node_os5 = require("node:os");
10055
- var import_node_path10 = require("node:path");
10797
+ var import_node_path11 = require("node:path");
10056
10798
  var import_promises10 = require("node:readline/promises");
10057
10799
  var import_node_process = require("node:process");
10058
10800
  var NPM_PACKAGE_NAME = "threadnote";
@@ -10267,7 +11009,7 @@ async function waitForOpenVikingHealthy(config, timeoutMs) {
10267
11009
  return isOpenVikingHealthy(config);
10268
11010
  }
10269
11011
  function launchAgentPlistPath() {
10270
- return (0, import_node_path10.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
11012
+ return (0, import_node_path11.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
10271
11013
  }
10272
11014
  async function isLaunchAgentInstalled() {
10273
11015
  if (process.platform !== "darwin") {
@@ -10303,7 +11045,7 @@ async function getUpdateInfo(config, options) {
10303
11045
  };
10304
11046
  }
10305
11047
  async function currentPackageVersion() {
10306
- const rawPackage = await (0, import_promises9.readFile)((0, import_node_path10.join)(toolRoot(), "package.json"), "utf8");
11048
+ const rawPackage = await (0, import_promises9.readFile)((0, import_node_path11.join)(toolRoot(), "package.json"), "utf8");
10307
11049
  const parsed = JSON.parse(rawPackage);
10308
11050
  if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
10309
11051
  throw new Error("Could not read current threadnote package version.");
@@ -10357,7 +11099,7 @@ async function writeUpdateCache2(config, cache) {
10357
11099
  `, { encoding: "utf8", mode: 384 });
10358
11100
  }
10359
11101
  function updateCachePath(config) {
10360
- return (0, import_node_path10.join)(config.agentContextHome, "update-check.json");
11102
+ return (0, import_node_path11.join)(config.agentContextHome, "update-check.json");
10361
11103
  }
10362
11104
  async function runApplicablePostUpdateMigrations(config, options) {
10363
11105
  const state = await readPostUpdateState(config);
@@ -10421,7 +11163,7 @@ async function applicablePostUpdateMigrations(config, options) {
10421
11163
  return applicable;
10422
11164
  }
10423
11165
  async function readPostUpdateMigrations() {
10424
- const raw = await readFileIfExists((0, import_node_path10.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
11166
+ const raw = await readFileIfExists((0, import_node_path11.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
10425
11167
  if (!raw) {
10426
11168
  return [];
10427
11169
  }
@@ -10499,7 +11241,7 @@ async function writePostUpdateState(config, state) {
10499
11241
  `, { encoding: "utf8", mode: 384 });
10500
11242
  }
10501
11243
  function postUpdateStatePath(config) {
10502
- return (0, import_node_path10.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
11244
+ return (0, import_node_path11.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
10503
11245
  }
10504
11246
  async function resolveUpdateRuntime(runtime) {
10505
11247
  if (runtime !== "auto") {
@@ -10529,14 +11271,14 @@ async function runtimeThreadnoteBin(runtime) {
10529
11271
  if (runtime === "npm") {
10530
11272
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
10531
11273
  const prefix = result.stdout.trim();
10532
- return prefix ? (0, import_node_path10.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
11274
+ return prefix ? (0, import_node_path11.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
10533
11275
  }
10534
11276
  if (runtime === "bun") {
10535
11277
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
10536
11278
  const binDir = result.stdout.trim();
10537
- return binDir ? (0, import_node_path10.join)(binDir, NPM_PACKAGE_NAME) : void 0;
11279
+ return binDir ? (0, import_node_path11.join)(binDir, NPM_PACKAGE_NAME) : void 0;
10538
11280
  }
10539
- return (0, import_node_path10.join)(process.env.DENO_INSTALL ?? (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
11281
+ return (0, import_node_path11.join)(process.env.DENO_INSTALL ?? (0, import_node_path11.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
10540
11282
  }
10541
11283
  function updatePackageCommand(runtime, registry) {
10542
11284
  if (runtime === "npm") {
@@ -10591,9 +11333,9 @@ async function runDoctor(config, options) {
10591
11333
  checks.push(await commandShimCheck());
10592
11334
  checks.push(...await userAgentInstructionsChecks());
10593
11335
  checks.push(await manifestCheck(config.manifestPath));
10594
- checks.push(await fileCheck((0, import_node_path11.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
10595
- checks.push(await fileCheck((0, import_node_path11.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
10596
- checks.push(await fileCheck((0, import_node_path11.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
11336
+ checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
11337
+ checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
11338
+ checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
10597
11339
  checks.push(await healthCheck(config));
10598
11340
  for (const check of checks) {
10599
11341
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -10610,9 +11352,9 @@ async function runInstall(config, options) {
10610
11352
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
10611
11353
  const dryRun = options.dryRun === true;
10612
11354
  await ensureDirectory(config.agentContextHome, dryRun);
10613
- await ensureDirectory((0, import_node_path11.join)(config.agentContextHome, "logs"), dryRun);
10614
- await ensureDirectory((0, import_node_path11.join)(config.agentContextHome, "redacted"), dryRun);
10615
- await ensureDirectory((0, import_node_path11.join)(config.agentContextHome, "mcp"), dryRun);
11355
+ await ensureDirectory((0, import_node_path12.join)(config.agentContextHome, "logs"), dryRun);
11356
+ await ensureDirectory((0, import_node_path12.join)(config.agentContextHome, "redacted"), dryRun);
11357
+ await ensureDirectory((0, import_node_path12.join)(config.agentContextHome, "mcp"), dryRun);
10616
11358
  await installCommandShim(dryRun);
10617
11359
  await installUserAgentInstructions(dryRun);
10618
11360
  const serverPath = await findOpenVikingServer();
@@ -10649,17 +11391,17 @@ async function runInstall(config, options) {
10649
11391
  }
10650
11392
  await writeTemplateIfMissing({
10651
11393
  config,
10652
- destinationPath: (0, import_node_path11.join)(config.agentContextHome, "ov.conf"),
11394
+ destinationPath: (0, import_node_path12.join)(config.agentContextHome, "ov.conf"),
10653
11395
  dryRun,
10654
11396
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
10655
- templatePath: (0, import_node_path11.join)(toolRoot(), "config", "ov.conf.template.json")
11397
+ templatePath: (0, import_node_path12.join)(toolRoot(), "config", "ov.conf.template.json")
10656
11398
  });
10657
11399
  await writeTemplateIfMissing({
10658
11400
  config,
10659
- destinationPath: (0, import_node_path11.join)(config.agentContextHome, "ovcli.conf"),
11401
+ destinationPath: (0, import_node_path12.join)(config.agentContextHome, "ovcli.conf"),
10660
11402
  dryRun,
10661
11403
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
10662
- templatePath: (0, import_node_path11.join)(toolRoot(), "config", "ovcli.conf.template.json")
11404
+ templatePath: (0, import_node_path12.join)(toolRoot(), "config", "ovcli.conf.template.json")
10663
11405
  });
10664
11406
  if (options.start !== false) {
10665
11407
  const healthy = await repairServerHealth(config, dryRun);
@@ -10713,7 +11455,7 @@ async function runUninstall(config, options) {
10713
11455
  }
10714
11456
  console.log("Uninstalling local Threadnote setup.");
10715
11457
  await runStop(config, { dryRun });
10716
- await removePathIfExists((0, import_node_path11.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
11458
+ await removePathIfExists((0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
10717
11459
  await removeLaunchAgent(dryRun);
10718
11460
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
10719
11461
  await removeMcpSnippets(config, dryRun);
@@ -10770,7 +11512,7 @@ async function repairManifest(config, dryRun) {
10770
11512
  console.log(output2.trimEnd());
10771
11513
  return;
10772
11514
  }
10773
- await ensureDirectory((0, import_node_path11.dirname)(config.manifestPath), false);
11515
+ await ensureDirectory((0, import_node_path12.dirname)(config.manifestPath), false);
10774
11516
  const currentContent = await readFileIfExists(config.manifestPath);
10775
11517
  if (currentContent !== void 0) {
10776
11518
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
@@ -10788,7 +11530,7 @@ async function findOpenVikingServer() {
10788
11530
  return onPath;
10789
11531
  }
10790
11532
  for (const candidateDir of await openVikingServerCandidateDirs()) {
10791
- const candidate = (0, import_node_path11.join)(candidateDir, OPENVIKING_SERVER_COMMAND);
11533
+ const candidate = (0, import_node_path12.join)(candidateDir, OPENVIKING_SERVER_COMMAND);
10792
11534
  if (await isExecutable(candidate)) {
10793
11535
  return candidate;
10794
11536
  }
@@ -10834,7 +11576,7 @@ async function maybePrintOpenVikingPathHint(serverPath) {
10834
11576
  if (onPath) {
10835
11577
  return;
10836
11578
  }
10837
- const binDir = (0, import_node_path11.dirname)(serverPath);
11579
+ const binDir = (0, import_node_path12.dirname)(serverPath);
10838
11580
  const rcHint = suggestedShellRc(process.env.SHELL, (0, import_node_os6.platform)());
10839
11581
  console.log(
10840
11582
  `Note: ${serverPath} is installed but ${binDir} is not on this shell's PATH. Add \`export PATH="${binDir}:$PATH"\` to ${rcHint} so other tools can find openviking-server.`
@@ -10878,7 +11620,7 @@ async function runStart(config, options) {
10878
11620
  );
10879
11621
  }
10880
11622
  const logPath = openVikingLogPath(config);
10881
- await ensureDirectory((0, import_node_path11.dirname)(logPath), false);
11623
+ await ensureDirectory((0, import_node_path12.dirname)(logPath), false);
10882
11624
  if (options.foreground === true) {
10883
11625
  const result = await runInteractive(server, args);
10884
11626
  process.exitCode = result;
@@ -10887,7 +11629,7 @@ async function runStart(config, options) {
10887
11629
  const logFd = (0, import_node_fs6.openSync)(logPath, "a");
10888
11630
  const child = spawnDetachedServerWithLog(server, args, logFd);
10889
11631
  child.unref();
10890
- await (0, import_promises11.writeFile)((0, import_node_path11.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
11632
+ await (0, import_promises11.writeFile)((0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
10891
11633
  `, "utf8");
10892
11634
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
10893
11635
  if (health) {
@@ -10920,7 +11662,7 @@ async function runStop(config, options) {
10920
11662
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
10921
11663
  }
10922
11664
  }
10923
- const pidPath = (0, import_node_path11.join)(config.agentContextHome, "openviking-server.pid");
11665
+ const pidPath = (0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid");
10924
11666
  const pidText = await readFileIfExists(pidPath);
10925
11667
  if (!pidText) {
10926
11668
  console.log("No pid file found for detached OpenViking server.");
@@ -10962,7 +11704,7 @@ async function openVikingServerCheck() {
10962
11704
  }
10963
11705
  const result = await runCommand(executable, ["--help"], { allowFailure: true });
10964
11706
  const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
10965
- const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path11.dirname)(executable)} to PATH)`;
11707
+ const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path12.dirname)(executable)} to PATH)`;
10966
11708
  return {
10967
11709
  name,
10968
11710
  status: result.exitCode === 0 ? "ok" : "warn",
@@ -11031,7 +11773,7 @@ async function pythonSystemCertificatesCheck() {
11031
11773
  };
11032
11774
  }
11033
11775
  async function commandShimCheck() {
11034
- const shimPath = (0, import_node_path11.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
11776
+ const shimPath = (0, import_node_path12.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
11035
11777
  const content = await readFileIfExists(shimPath);
11036
11778
  if (content === void 0) {
11037
11779
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -11101,7 +11843,7 @@ async function siblingPythonForExecutable(executablePath) {
11101
11843
  } catch (_err) {
11102
11844
  return void 0;
11103
11845
  }
11104
- const pythonPath = (0, import_node_path11.join)((0, import_node_path11.dirname)(resolvedPath), "python");
11846
+ const pythonPath = (0, import_node_path12.join)((0, import_node_path12.dirname)(resolvedPath), "python");
11105
11847
  return await exists(pythonPath) ? pythonPath : void 0;
11106
11848
  }
11107
11849
  async function manifestCheck(path) {
@@ -11321,14 +12063,14 @@ async function writeTemplateIfMissing(options) {
11321
12063
  console.log(`Would write ${options.destinationPath}`);
11322
12064
  return;
11323
12065
  }
11324
- await ensureDirectory((0, import_node_path11.dirname)(options.destinationPath), false);
12066
+ await ensureDirectory((0, import_node_path12.dirname)(options.destinationPath), false);
11325
12067
  await (0, import_promises11.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
11326
12068
  await (0, import_promises11.chmod)(options.destinationPath, 384);
11327
12069
  console.log(`Wrote ${options.destinationPath}`);
11328
12070
  }
11329
12071
  async function installCommandShim(dryRun) {
11330
12072
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
11331
- const shimPath = (0, import_node_path11.join)(binDir, "threadnote");
12073
+ const shimPath = (0, import_node_path12.join)(binDir, "threadnote");
11332
12074
  const existingContent = await readFileIfExists(shimPath);
11333
12075
  if (existingContent && !isManagedCommandShim(existingContent)) {
11334
12076
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -11349,7 +12091,7 @@ async function installCommandShim(dryRun) {
11349
12091
  console.log(`Wrote command shim: ${shimPath}`);
11350
12092
  }
11351
12093
  async function removeCommandShim(dryRun) {
11352
- const shimPath = (0, import_node_path11.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
12094
+ const shimPath = (0, import_node_path12.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
11353
12095
  const content = await readFileIfExists(shimPath);
11354
12096
  if (content === void 0) {
11355
12097
  console.log(`Already absent: ${shimPath}`);
@@ -11383,7 +12125,7 @@ async function installUserAgentInstructions(dryRun) {
11383
12125
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
11384
12126
  continue;
11385
12127
  }
11386
- await ensureDirectory((0, import_node_path11.dirname)(targetPath), false);
12128
+ await ensureDirectory((0, import_node_path12.dirname)(targetPath), false);
11387
12129
  await (0, import_promises11.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
11388
12130
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
11389
12131
  }
@@ -11441,7 +12183,7 @@ async function renderUserAgentInstructions(target) {
11441
12183
  ].join("\n");
11442
12184
  }
11443
12185
  async function renderUserAgentInstructionsBlock() {
11444
- const instructions = (await (0, import_promises11.readFile)((0, import_node_path11.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
12186
+ const instructions = (await (0, import_promises11.readFile)((0, import_node_path12.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
11445
12187
  return `${USER_INSTRUCTIONS_START_MARKER}
11446
12188
  ${instructions}
11447
12189
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -11525,7 +12267,7 @@ async function installLaunchAgent(config, dryRun) {
11525
12267
  `Cannot install LaunchAgent: ${OPENVIKING_SERVER_COMMAND} was not found in PATH, uv tool bin dir, $UV_TOOL_BIN_DIR, or ~/.local/bin. Run \`threadnote install\` first.`
11526
12268
  );
11527
12269
  }
11528
- const source = (0, import_node_path11.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
12270
+ const source = (0, import_node_path12.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
11529
12271
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
11530
12272
  const rendered = renderTemplate(await (0, import_promises11.readFile)(source, "utf8"), config, {
11531
12273
  OPENVIKING_SERVER_PATH: resolvedServer ?? OPENVIKING_SERVER_COMMAND
@@ -11539,8 +12281,8 @@ async function installLaunchAgent(config, dryRun) {
11539
12281
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
11540
12282
  return;
11541
12283
  }
11542
- await ensureDirectory((0, import_node_path11.dirname)(destination), false);
11543
- await ensureDirectory((0, import_node_path11.dirname)(openVikingLogPath(config)), false);
12284
+ await ensureDirectory((0, import_node_path12.dirname)(destination), false);
12285
+ await ensureDirectory((0, import_node_path12.dirname)(openVikingLogPath(config)), false);
11544
12286
  await (0, import_promises11.writeFile)(destination, rendered, "utf8");
11545
12287
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
11546
12288
  await maybeRun(false, "launchctl", ["load", destination]);
@@ -11604,7 +12346,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
11604
12346
  if (typeof parsed.default_user !== "string") {
11605
12347
  return false;
11606
12348
  }
11607
- if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path11.join)(config.agentContextHome, "data")) {
12349
+ if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path12.join)(config.agentContextHome, "data")) {
11608
12350
  return false;
11609
12351
  }
11610
12352
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -11724,16 +12466,19 @@ async function main() {
11724
12466
  program2.command("migrate-lifecycle").description("Move clear legacy handoff memories into lifecycle-aware archive paths").option("--apply", "Perform the migration; without this, prints a dry run").option("--dry-run", "Print migration actions without writing or removing memories").option("--limit <count>", "Maximum number of legacy handoffs to migrate").action(async (options) => {
11725
12467
  await runMigrateLifecycle(getRuntimeConfig(program2), options);
11726
12468
  });
11727
- program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in exact durable-memory matches").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
12469
+ program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in exact memory/resource matches").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
11728
12470
  await runRecall(getRuntimeConfig(program2), options);
11729
12471
  });
12472
+ program2.command("compact").description("Plan or apply scoped memory hygiene for active personal memories").requiredOption("--project <name>", "Project/repo namespace to inspect").option("--apply", "Apply the compact plan; without this, prints a dry run").option("--dry-run", "Print the compact plan without changing anything").option("--kind <kind>", "durable, handoff, or incident", parseCompactKind).option("--topic <name>", "Stable topic name to inspect").action(async (options) => {
12473
+ await runCompact(getRuntimeConfig(program2), options);
12474
+ });
11730
12475
  program2.command("read").description("Read a viking:// URI returned by recall or list").argument("<uri>", "viking:// URI to read").option("--dry-run", "Print ov command without reading").action(async (uri, options) => {
11731
12476
  await runRead(getRuntimeConfig(program2), uri, options);
11732
12477
  });
11733
12478
  program2.command("list").alias("ls").description("List a viking:// directory").argument("[uri]", "viking:// URI to list", "viking://").option("-a, --all", "Show hidden files such as .abstract.md and .overview.md").option("--dry-run", "Print ov command without listing").option("-n, --node-limit <count>", "Maximum number of nodes to list").option("-r, --recursive", "List subdirectories recursively").option("-s, --simple", "Print only paths").action(async (uri, options) => {
11734
12479
  await runList(getRuntimeConfig(program2), uri, options);
11735
12480
  });
11736
- program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
12481
+ program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--timestamped", "Store a historical timestamped handoff instead of updating the current branch handoff").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
11737
12482
  await runHandoff(getRuntimeConfig(program2), options);
11738
12483
  });
11739
12484
  program2.command("archive").description("Move a memory into the archived lifecycle tree, then remove the original after the archive is stored").argument("<uri>", "viking:// memory URI to archive").option("--dry-run", "Print archive content and ov commands without changing anything").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind).option("--project <name>", "Override inferred project/repo namespace").option("--topic <name>", "Override inferred topic").action(async (uri, options) => {