threadnote 0.7.5 → 0.7.6

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,7 +8506,7 @@ 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) {
@@ -8130,8 +8626,8 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun, op
8130
8626
  }
8131
8627
  return;
8132
8628
  }
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");
8629
+ const stagingDir = await (0, import_promises4.mkdtemp)((0, import_node_path5.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
8630
+ const tempPath = (0, import_node_path5.join)(stagingDir, "body.txt");
8135
8631
  try {
8136
8632
  await (0, import_promises4.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
8137
8633
  await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode, options);
@@ -8253,7 +8749,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
8253
8749
  if (!relative3) {
8254
8750
  return;
8255
8751
  }
8256
- await (0, import_promises4.rm)((0, import_node_path4.join)(worktree, relative3), { force: true });
8752
+ await (0, import_promises4.rm)((0, import_node_path5.join)(worktree, relative3), { force: true });
8257
8753
  }
8258
8754
  async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
8259
8755
  const args = withIdentity(config, ["rm", uri]);
@@ -8317,8 +8813,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
8317
8813
  const oldRel = entries[index + 1];
8318
8814
  const newRel = entries[index + 2];
8319
8815
  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" });
8816
+ changes.push({ path: (0, import_node_path5.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
8817
+ changes.push({ path: (0, import_node_path5.join)(worktree, newRel), relativePath: newRel, status: "added" });
8322
8818
  }
8323
8819
  index += 3;
8324
8820
  continue;
@@ -8326,7 +8822,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
8326
8822
  const rel = entries[index + 1];
8327
8823
  if (rel) {
8328
8824
  const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
8329
- changes.push({ path: (0, import_node_path4.join)(worktree, rel), relativePath: rel, status });
8825
+ changes.push({ path: (0, import_node_path5.join)(worktree, rel), relativePath: rel, status });
8330
8826
  }
8331
8827
  index += 2;
8332
8828
  }
@@ -8410,6 +8906,12 @@ function parseMemoryStatus(value) {
8410
8906
  }
8411
8907
  throw new Error(`Unsupported memory status "${value}". Expected active, archived, or superseded.`);
8412
8908
  }
8909
+ function parseCompactKind(value) {
8910
+ if (["durable", "handoff", "incident"].includes(value)) {
8911
+ return value;
8912
+ }
8913
+ throw new Error(`Unsupported compact kind "${value}". Expected durable, handoff, or incident.`);
8914
+ }
8413
8915
  async function runRemember(config, options) {
8414
8916
  const text = await getInputText(options.text, options.stdin === true);
8415
8917
  if (!text.trim()) {
@@ -8417,11 +8919,11 @@ async function runRemember(config, options) {
8417
8919
  }
8418
8920
  const metadata = {
8419
8921
  kind: options.kind ?? "durable",
8420
- project: normalizeOptionalMetadata(options.project),
8922
+ project: normalizeOptionalMetadata2(options.project),
8421
8923
  sourceAgentClient: options.sourceAgentClient ?? "codex",
8422
8924
  status: options.status ?? "active",
8423
8925
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8424
- topic: normalizeOptionalMetadata(options.topic)
8926
+ topic: normalizeOptionalMetadata2(options.topic)
8425
8927
  };
8426
8928
  await storeMemory(config, {
8427
8929
  bodyText: text.trim(),
@@ -8442,7 +8944,7 @@ async function runMigrateMemories(config, options) {
8442
8944
  const candidates = await legacyMemoryCandidates(config, sourceAccounts);
8443
8945
  const existingHashes = await existingDurableMemoryHashes(config);
8444
8946
  const ov = await openVikingCliForMode(dryRun);
8445
- const migrationPath = (0, import_node_path5.join)(config.agentContextHome, "legacy-memory-migration.txt");
8947
+ const migrationPath = (0, import_node_path6.join)(config.agentContextHome, "legacy-memory-migration.txt");
8446
8948
  let duplicateCount = 0;
8447
8949
  let migratedCount = 0;
8448
8950
  let sensitiveCount = 0;
@@ -8505,7 +9007,7 @@ async function runMigrateLifecycle(config, options) {
8505
9007
  const limit = options.limit ? parsePositiveInteger(options.limit, "lifecycle migration limit") : void 0;
8506
9008
  const ov = await openVikingCliForMode(dryRun);
8507
9009
  const candidates = await legacyLifecycleHandoffCandidates(config);
8508
- const migrationPath = (0, import_node_path5.join)(config.agentContextHome, "lifecycle-memory-migration.txt");
9010
+ const migrationPath = (0, import_node_path6.join)(config.agentContextHome, "lifecycle-memory-migration.txt");
8509
9011
  let existingCount = 0;
8510
9012
  let migratedCount = 0;
8511
9013
  let skippedCount = 0;
@@ -8515,7 +9017,7 @@ async function runMigrateLifecycle(config, options) {
8515
9017
  break;
8516
9018
  }
8517
9019
  const destinationUri = lifecycleMigrationUri(config, candidate.metadata, sha256(candidate.original.trim()));
8518
- const migratedMemory = formatMemoryDocument(
9020
+ const migratedMemory = formatMemoryDocument2(
8519
9021
  "HANDOFF",
8520
9022
  candidate.metadata,
8521
9023
  ["Migrated legacy handoff from the historical events trail.", "", candidate.original.trim()].join("\n")
@@ -8561,8 +9063,10 @@ async function runRecall(config, options) {
8561
9063
  await syncSharedReposAndLog(config);
8562
9064
  }
8563
9065
  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];
9066
+ const query = await enrichRecallQueryWithWorkspaceContext(options.query);
9067
+ const projectQuery = await enrichRecallQueryWithWorkspaceProjectContext(options.query);
9068
+ const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, projectQuery));
9069
+ const args = ["search", query];
8566
9070
  if (inferredUri) {
8567
9071
  args.push("--uri", inferredUri);
8568
9072
  console.log(`Recall scope: ${inferredUri}`);
@@ -8570,24 +9074,41 @@ async function runRecall(config, options) {
8570
9074
  if (options.nodeLimit) {
8571
9075
  args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
8572
9076
  }
8573
- await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
8574
- await augmentRecallWithSeededResources(config, ov, options, inferredUri);
8575
- await printExactMemoryMatches(config, ov, options.query, {
9077
+ const recallOutputs = [];
9078
+ const baseResult = await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
9079
+ if (baseResult) {
9080
+ recallOutputs.push([baseResult.stdout.trim(), baseResult.stderr.trim()].filter(Boolean).join("\n"));
9081
+ }
9082
+ const seededOutput = await augmentRecallWithSeededResources(
9083
+ config,
9084
+ ov,
9085
+ { ...options, query },
9086
+ inferredUri,
9087
+ projectQuery
9088
+ );
9089
+ if (seededOutput) {
9090
+ recallOutputs.push(seededOutput);
9091
+ }
9092
+ const exactOutput = await printExactMemoryMatches(config, ov, query, {
8576
9093
  dryRun: options.dryRun === true,
8577
9094
  includeArchived: options.includeArchived === true
8578
9095
  });
9096
+ if (exactOutput) {
9097
+ recallOutputs.push(exactOutput);
9098
+ }
9099
+ await printRecallHygieneNudges(config, recallOutputs.join("\n"));
8579
9100
  }
8580
- async function augmentRecallWithSeededResources(config, ov, options, inferredUri) {
9101
+ async function augmentRecallWithSeededResources(config, ov, options, inferredUri, projectQuery = options.query) {
8581
9102
  if (options.uri || options.inferScope === false) {
8582
- return;
9103
+ return void 0;
8583
9104
  }
8584
- const project = await inferProjectFromQuery(config.manifestPath, options.query);
9105
+ const project = await inferProjectFromQuery(config.manifestPath, projectQuery);
8585
9106
  if (!project) {
8586
- return;
9107
+ return void 0;
8587
9108
  }
8588
9109
  const projectResourceUri = trimTrailingSlash(project.uri);
8589
9110
  if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
8590
- return;
9111
+ return void 0;
8591
9112
  }
8592
9113
  const args = ["search", options.query, "--uri", projectResourceUri];
8593
9114
  if (options.nodeLimit) {
@@ -8595,7 +9116,8 @@ async function augmentRecallWithSeededResources(config, ov, options, inferredUri
8595
9116
  }
8596
9117
  console.log(`
8597
9118
  Also searching seeded resources: ${projectResourceUri}`);
8598
- await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
9119
+ const result = await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
9120
+ return result ? [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n") : void 0;
8599
9121
  }
8600
9122
  async function runRead(config, uri, options) {
8601
9123
  assertVikingUri(uri);
@@ -8623,6 +9145,147 @@ async function syncSharedReposAndLog(config) {
8623
9145
  console.error(`Auto-sync warning: ${err instanceof Error ? err.message : String(err)}`);
8624
9146
  }
8625
9147
  }
9148
+ async function printRecallHygieneNudges(config, recallOutput) {
9149
+ const uris = activePersonalMemoryUrisFromText(recallOutput, config.user);
9150
+ if (uris.length === 0) {
9151
+ return;
9152
+ }
9153
+ const records = await readMemoryRecordsByUri(config, uris);
9154
+ const nudges = recallHygieneNudges(recallOutput, { records, user: config.user });
9155
+ if (nudges.length === 0) {
9156
+ return;
9157
+ }
9158
+ console.log("\nMemory hygiene hints:");
9159
+ for (const nudge of nudges) {
9160
+ console.log(`- ${nudge}`);
9161
+ }
9162
+ }
9163
+ async function runCompact(config, options) {
9164
+ const project = normalizeOptionalMetadata2(options.project);
9165
+ if (!project) {
9166
+ throw new Error("Provide --project for scoped memory hygiene.");
9167
+ }
9168
+ if (options.apply === true && options.dryRun === true) {
9169
+ throw new Error("Cannot combine --apply with --dry-run.");
9170
+ }
9171
+ const apply = options.apply === true;
9172
+ const records = await scopedCompactRecords(config, {
9173
+ kind: options.kind,
9174
+ project
9175
+ });
9176
+ const plan = buildCompactPlan(records, {
9177
+ kind: options.kind,
9178
+ project,
9179
+ topic: normalizeOptionalMetadata2(options.topic)
9180
+ });
9181
+ console.log(formatCompactPlan(plan, { apply }));
9182
+ if (!apply) {
9183
+ return;
9184
+ }
9185
+ const ov = await openVikingCliForMode(false);
9186
+ const updatePath = (0, import_node_path6.join)(config.agentContextHome, "compact-memory-update.txt");
9187
+ try {
9188
+ for (const action of plan.keepUpdates) {
9189
+ await (0, import_promises5.writeFile)(updatePath, action.content, { encoding: "utf8", mode: 384 });
9190
+ await (0, import_promises5.chmod)(updatePath, 384);
9191
+ await writeDurableMemoryFile(ov, config, action.uri, updatePath, "replace");
9192
+ }
9193
+ } finally {
9194
+ await (0, import_promises5.rm)(updatePath, { force: true });
9195
+ }
9196
+ for (const action of plan.archives) {
9197
+ await runArchive(config, action.uri, {
9198
+ dryRun: false,
9199
+ kind: action.kind,
9200
+ project: action.project,
9201
+ topic: action.topic
9202
+ });
9203
+ }
9204
+ for (const action of plan.forgets) {
9205
+ await runForget(config, action.uri, { dryRun: false });
9206
+ }
9207
+ }
9208
+ async function scopedCompactRecords(config, options) {
9209
+ const kinds = options.kind ? [options.kind] : ["handoff", "durable", "incident"];
9210
+ const records = [];
9211
+ for (const kind of kinds) {
9212
+ const directory = localMemoryDirectoryForCompact(config, kind, options.project);
9213
+ const uriDirectory = memoryUriDirectoryForCompact(config, kind, options.project);
9214
+ let entries;
9215
+ try {
9216
+ entries = await (0, import_promises5.readdir)(directory, { withFileTypes: true });
9217
+ } catch (_err) {
9218
+ continue;
9219
+ }
9220
+ for (const entry of entries) {
9221
+ if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
9222
+ continue;
9223
+ }
9224
+ const content = await readTextIfExists((0, import_node_path6.join)(directory, entry.name));
9225
+ if (!content) {
9226
+ continue;
9227
+ }
9228
+ const record = parseMemoryDocument(`${uriDirectory}/${entry.name}`, content);
9229
+ if (record) {
9230
+ records.push(record);
9231
+ }
9232
+ }
9233
+ }
9234
+ return records;
9235
+ }
9236
+ async function readMemoryRecordsByUri(config, uris) {
9237
+ const records = [];
9238
+ for (const uri of uris) {
9239
+ const localPath = localMemoryPathForUri(config, uri);
9240
+ if (!localPath) {
9241
+ continue;
9242
+ }
9243
+ const content = await readTextIfExists(localPath);
9244
+ if (!content) {
9245
+ continue;
9246
+ }
9247
+ const record = parseMemoryDocument(uri, content);
9248
+ if (record) {
9249
+ records.push(record);
9250
+ }
9251
+ }
9252
+ return records;
9253
+ }
9254
+ function localMemoryDirectoryForCompact(config, kind, project) {
9255
+ const root = localUserMemoriesRoot(config);
9256
+ const projectSegment = uriSegment(project);
9257
+ switch (kind) {
9258
+ case "durable":
9259
+ return (0, import_node_path6.join)(root, "durable", "projects", projectSegment);
9260
+ case "handoff":
9261
+ return (0, import_node_path6.join)(root, "handoffs", "active", projectSegment);
9262
+ case "incident":
9263
+ return (0, import_node_path6.join)(root, "incidents", "active", projectSegment);
9264
+ }
9265
+ }
9266
+ function memoryUriDirectoryForCompact(config, kind, project) {
9267
+ const base = `viking://user/${uriSegment(config.user)}/memories`;
9268
+ const projectSegment = uriSegment(project);
9269
+ switch (kind) {
9270
+ case "durable":
9271
+ return `${base}/durable/projects/${projectSegment}`;
9272
+ case "handoff":
9273
+ return `${base}/handoffs/active/${projectSegment}`;
9274
+ case "incident":
9275
+ return `${base}/incidents/active/${projectSegment}`;
9276
+ }
9277
+ }
9278
+ function localMemoryPathForUri(config, uri) {
9279
+ const prefix = `viking://user/${uriSegment(config.user)}/memories/`;
9280
+ if (!uri.startsWith(prefix) || uri.includes("/shared/")) {
9281
+ return void 0;
9282
+ }
9283
+ const relative3 = uri.slice(prefix.length);
9284
+ if (relative3.includes("..") || relative3.startsWith("/")) {
9285
+ return void 0;
9286
+ }
9287
+ return (0, import_node_path6.join)(localUserMemoriesRoot(config), ...relative3.split("/"));
9288
+ }
8626
9289
  async function runList(config, uri, options) {
8627
9290
  assertVikingUri(uri);
8628
9291
  const ov = await openVikingCliForMode(options.dryRun === true);
@@ -8660,11 +9323,11 @@ async function runArchive(config, uri, options) {
8660
9323
  const fallbackMetadata = {
8661
9324
  archivedFrom: uri,
8662
9325
  kind: options.kind ?? "handoff",
8663
- project: normalizeOptionalMetadata(options.project),
9326
+ project: normalizeOptionalMetadata2(options.project),
8664
9327
  sourceAgentClient: "threadnote",
8665
9328
  status: "archived",
8666
9329
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8667
- topic: normalizeOptionalMetadata(options.topic)
9330
+ topic: normalizeOptionalMetadata2(options.topic)
8668
9331
  };
8669
9332
  await storeMemory(config, {
8670
9333
  bodyText: ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n"),
@@ -8682,11 +9345,11 @@ async function runArchive(config, uri, options) {
8682
9345
  const metadata = {
8683
9346
  archivedFrom: uri,
8684
9347
  kind: options.kind ?? inferredMetadata.kind ?? "handoff",
8685
- project: normalizeOptionalMetadata(options.project) ?? inferredMetadata.project,
9348
+ project: normalizeOptionalMetadata2(options.project) ?? inferredMetadata.project,
8686
9349
  sourceAgentClient: "threadnote",
8687
9350
  status: "archived",
8688
9351
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8689
- topic: normalizeOptionalMetadata(options.topic) ?? inferredMetadata.topic
9352
+ topic: normalizeOptionalMetadata2(options.topic) ?? inferredMetadata.topic
8690
9353
  };
8691
9354
  await storeMemory(config, {
8692
9355
  bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
@@ -8715,7 +9378,7 @@ async function runForget(config, uri, options) {
8715
9378
  }
8716
9379
  async function runExportPack(config, options) {
8717
9380
  const ov = await openVikingCliForMode(options.dryRun === true);
8718
- const defaultPath = (0, import_node_path5.join)(config.agentContextHome, `threadnote-${safeTimestamp()}.ovpack`);
9381
+ const defaultPath = (0, import_node_path6.join)(config.agentContextHome, `threadnote-${safeTimestamp()}.ovpack`);
8719
9382
  const outputPath = expandPath(options.path ?? defaultPath);
8720
9383
  await maybeRun(options.dryRun === true, ov, withIdentity(config, ["export", options.uri ?? "viking://", outputPath]));
8721
9384
  }
@@ -8740,7 +9403,7 @@ async function inferRecallUri(config, query) {
8740
9403
  async function printExactMemoryMatches(config, ov, query, options) {
8741
9404
  const terms = exactRecallTerms(query);
8742
9405
  if (terms.length === 0) {
8743
- return;
9406
+ return void 0;
8744
9407
  }
8745
9408
  const scopes = exactMemoryScopes(config, options.includeArchived);
8746
9409
  const outputs = [];
@@ -8752,30 +9415,32 @@ async function printExactMemoryMatches(config, ov, query, options) {
8752
9415
  continue;
8753
9416
  }
8754
9417
  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);
9418
+ const output3 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
9419
+ if (result.exitCode === 0 && grepOutputHasMatches(output3)) {
9420
+ outputs.push(output3);
8758
9421
  }
8759
9422
  }
8760
9423
  }
8761
9424
  if (outputs.length === 0) {
8762
- return;
9425
+ return void 0;
8763
9426
  }
8764
- console.log("\nExact durable memory matches:");
8765
- console.log(outputs.join("\n\n"));
9427
+ console.log("\nExact memory/resource matches:");
9428
+ const output2 = outputs.join("\n\n");
9429
+ console.log(output2);
9430
+ return output2;
8766
9431
  }
8767
9432
  async function storeMemory(config, options) {
8768
9433
  if (options.replaceUri) {
8769
9434
  assertVikingUri(options.replaceUri);
8770
9435
  }
8771
9436
  const ov = await openVikingCliForMode(options.dryRun);
8772
- const memoryPath = (0, import_node_path5.join)(config.agentContextHome, "last-memory.txt");
9437
+ const memoryPath = (0, import_node_path6.join)(config.agentContextHome, "last-memory.txt");
8773
9438
  const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
8774
- const candidateMemory = formatMemoryDocument(options.title, candidateMetadata, options.bodyText);
9439
+ const candidateMemory = formatMemoryDocument2(options.title, candidateMetadata, options.bodyText);
8775
9440
  const memoryUri = memoryUriFor(config, candidateMemory, candidateMetadata);
8776
9441
  const isInPlaceUpdate = options.replaceUri !== void 0 && options.replaceUri === memoryUri;
8777
9442
  const finalMetadata = isInPlaceUpdate ? { ...options.metadata, supersedes: void 0 } : candidateMetadata;
8778
- const memory = isInPlaceUpdate ? formatMemoryDocument(options.title, finalMetadata, options.bodyText) : candidateMemory;
9443
+ const memory = isInPlaceUpdate ? formatMemoryDocument2(options.title, finalMetadata, options.bodyText) : candidateMemory;
8779
9444
  const writeMode = await memoryWriteMode(ov, config, memoryUri, finalMetadata);
8780
9445
  if (options.dryRun) {
8781
9446
  console.log(memory);
@@ -8917,7 +9582,7 @@ async function hasLegacyLifecycleHandoffCandidates(config) {
8917
9582
  return (await legacyLifecycleHandoffCandidates(config, 1)).length > 0;
8918
9583
  }
8919
9584
  async function legacyLifecycleHandoffCandidates(config, limit) {
8920
- const eventsRoot = (0, import_node_path5.join)(localUserMemoriesRoot(config), "events");
9585
+ const eventsRoot = (0, import_node_path6.join)(localUserMemoriesRoot(config), "events");
8921
9586
  let entries;
8922
9587
  try {
8923
9588
  entries = await (0, import_promises5.readdir)(eventsRoot, { withFileTypes: true });
@@ -8929,7 +9594,7 @@ async function legacyLifecycleHandoffCandidates(config, limit) {
8929
9594
  if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
8930
9595
  continue;
8931
9596
  }
8932
- const sourcePath = (0, import_node_path5.join)(eventsRoot, entry.name);
9597
+ const sourcePath = (0, import_node_path6.join)(eventsRoot, entry.name);
8933
9598
  const original = await readTextIfExists(sourcePath);
8934
9599
  if (!original || !isClearLegacyHandoffMemory(original) || sensitiveMemoryReason(original)) {
8935
9600
  continue;
@@ -9015,7 +9680,7 @@ function vikingDirectoryChain(directoryUri) {
9015
9680
  }
9016
9681
  return chain;
9017
9682
  }
9018
- function formatMemoryDocument(title, metadata, body) {
9683
+ function formatMemoryDocument2(title, metadata, body) {
9019
9684
  const header = [
9020
9685
  title,
9021
9686
  `kind: ${metadata.kind}`,
@@ -9032,10 +9697,10 @@ function formatMemoryDocument(title, metadata, body) {
9032
9697
  function inferMemoryMetadata(memory) {
9033
9698
  const header = memory.slice(0, Math.max(0, memory.indexOf("\n\n")) || memory.length);
9034
9699
  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"));
9700
+ const kind = parseOptionalMemoryKind2(parseHeaderValue(header, "kind")) ?? (firstLine2 === "HANDOFF" ? "handoff" : void 0);
9701
+ const status = parseOptionalMemoryStatus2(parseHeaderValue(header, "status"));
9702
+ const project = normalizeOptionalMetadata2(parseHeaderValue(header, "project")) ?? normalizeOptionalMetadata2(parseHeaderValue(header, "repo"));
9703
+ const topic = normalizeOptionalMetadata2(parseHeaderValue(header, "topic")) ?? normalizeOptionalMetadata2(parseHeaderValue(header, "task"));
9039
9704
  return {
9040
9705
  kind,
9041
9706
  project,
@@ -9071,9 +9736,9 @@ function inferLegacyProject(memory) {
9071
9736
  return "general";
9072
9737
  }
9073
9738
  const trimmed = explicit.trim().replace(/[`.,;]+$/g, "");
9074
- return trimmed.includes("/") ? (0, import_node_path5.basename)(trimmed) : trimmed;
9739
+ return trimmed.includes("/") ? (0, import_node_path6.basename)(trimmed) : trimmed;
9075
9740
  }
9076
- function parseOptionalMemoryKind(value) {
9741
+ function parseOptionalMemoryKind2(value) {
9077
9742
  if (!value) {
9078
9743
  return void 0;
9079
9744
  }
@@ -9083,7 +9748,7 @@ function parseOptionalMemoryKind(value) {
9083
9748
  return void 0;
9084
9749
  }
9085
9750
  }
9086
- function parseOptionalMemoryStatus(value) {
9751
+ function parseOptionalMemoryStatus2(value) {
9087
9752
  if (!value) {
9088
9753
  return void 0;
9089
9754
  }
@@ -9093,7 +9758,7 @@ function parseOptionalMemoryStatus(value) {
9093
9758
  return void 0;
9094
9759
  }
9095
9760
  }
9096
- function normalizeOptionalMetadata(value) {
9761
+ function normalizeOptionalMetadata2(value) {
9097
9762
  const trimmed = value?.trim();
9098
9763
  return trimmed ? trimmed : void 0;
9099
9764
  }
@@ -9111,14 +9776,14 @@ async function legacySourceAccounts(config, options) {
9111
9776
  async function legacyMemoryCandidates(config, sourceAccounts) {
9112
9777
  const candidates = [];
9113
9778
  for (const sourceAccount of sourceAccounts) {
9114
- const sessionRoot = (0, import_node_path5.join)(localVikingDataRoot(config), sourceAccount, "session");
9779
+ const sessionRoot = (0, import_node_path6.join)(localVikingDataRoot(config), sourceAccount, "session");
9115
9780
  for (const sourceSession of await childDirectoryNames(sessionRoot)) {
9116
- const historyRoot = (0, import_node_path5.join)(sessionRoot, sourceSession, "history");
9781
+ const historyRoot = (0, import_node_path6.join)(sessionRoot, sourceSession, "history");
9117
9782
  for (const sourceArchive of await childDirectoryNames(historyRoot)) {
9118
9783
  if (!sourceArchive.startsWith("archive_")) {
9119
9784
  continue;
9120
9785
  }
9121
- const sourcePath = (0, import_node_path5.join)(historyRoot, sourceArchive, "messages.jsonl");
9786
+ const sourcePath = (0, import_node_path6.join)(historyRoot, sourceArchive, "messages.jsonl");
9122
9787
  for (const text of await legacyMemoryTexts(sourcePath)) {
9123
9788
  candidates.push({
9124
9789
  comparableHash: sha256(comparableMemoryText(text)),
@@ -9186,7 +9851,7 @@ async function collectDurableMemoryHashes(root, hashes) {
9186
9851
  return;
9187
9852
  }
9188
9853
  for (const entry of entries) {
9189
- const path = (0, import_node_path5.join)(root, entry.name);
9854
+ const path = (0, import_node_path6.join)(root, entry.name);
9190
9855
  if (entry.isDirectory()) {
9191
9856
  await collectDurableMemoryHashes(path, hashes);
9192
9857
  continue;
@@ -9203,7 +9868,7 @@ async function collectDurableMemoryHashes(root, hashes) {
9203
9868
  }
9204
9869
  }
9205
9870
  function isDurableMemoryPath(path) {
9206
- return path.split(import_node_path5.sep).includes("memories");
9871
+ return path.split(import_node_path6.sep).includes("memories");
9207
9872
  }
9208
9873
  async function childDirectoryNames(path) {
9209
9874
  let entries;
@@ -9247,10 +9912,10 @@ function legacySourceLabel(candidate) {
9247
9912
  return `${candidate.sourceAccount}/${candidate.sourceSession}/${candidate.sourceArchive}`;
9248
9913
  }
9249
9914
  function localVikingDataRoot(config) {
9250
- return (0, import_node_path5.join)(config.agentContextHome, "data", "viking");
9915
+ return (0, import_node_path6.join)(config.agentContextHome, "data", "viking");
9251
9916
  }
9252
9917
  function localUserMemoriesRoot(config) {
9253
- return (0, import_node_path5.join)(localVikingDataRoot(config), config.account, "user", uriSegment(config.user), "memories");
9918
+ return (0, import_node_path6.join)(localVikingDataRoot(config), config.account, "user", uriSegment(config.user), "memories");
9254
9919
  }
9255
9920
  function uniqueStrings(values) {
9256
9921
  return [...new Set(values)].sort();
@@ -9266,14 +9931,15 @@ async function buildHandoff(options) {
9266
9931
  const status = await gitValue(["status", "--short"], repoRoot) ?? "";
9267
9932
  const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
9268
9933
  const touchedFiles = await gitTouchedFiles(repoRoot);
9269
- const repoName = (0, import_node_path5.basename)(repoRoot);
9934
+ const repoName = (0, import_node_path6.basename)(repoRoot);
9935
+ const topicBranch = branch && branch !== "unknown" ? branch : "current";
9270
9936
  const metadata = {
9271
9937
  kind: "handoff",
9272
- project: normalizeOptionalMetadata(options.project) ?? repoName,
9938
+ project: normalizeOptionalMetadata2(options.project) ?? repoName,
9273
9939
  sourceAgentClient: options.sourceAgentClient ?? "codex",
9274
9940
  status: "active",
9275
9941
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9276
- topic: normalizeOptionalMetadata(options.topic)
9942
+ topic: handoffTopicForBranch(topicBranch, { timestamped: options.timestamped, topic: options.topic })
9277
9943
  };
9278
9944
  const bodyText = [
9279
9945
  `repo: ${repoName}`,
@@ -9326,7 +9992,7 @@ function formatBlock(value, emptyValue) {
9326
9992
  // src/update-check.ts
9327
9993
  var import_node_child_process2 = require("node:child_process");
9328
9994
  var import_promises6 = require("node:fs/promises");
9329
- var import_node_path6 = require("node:path");
9995
+ var import_node_path7 = require("node:path");
9330
9996
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
9331
9997
  var NPM_LATEST_URL = "https://registry.npmjs.org/threadnote/latest";
9332
9998
  var FETCH_TIMEOUT_MS = 3e3;
@@ -9405,7 +10071,7 @@ async function readUpdateCache(cachePath) {
9405
10071
  }
9406
10072
  async function writeUpdateCache(cachePath, contents) {
9407
10073
  try {
9408
- await (0, import_promises6.mkdir)((0, import_node_path6.dirname)(cachePath), { recursive: true });
10074
+ await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(cachePath), { recursive: true });
9409
10075
  await (0, import_promises6.writeFile)(cachePath, `${JSON.stringify(contents)}
9410
10076
  `, { encoding: "utf8", mode: 384 });
9411
10077
  } catch {
@@ -9429,14 +10095,14 @@ async function fetchLatestVersion() {
9429
10095
 
9430
10096
  // src/version.ts
9431
10097
  var import_node_fs4 = require("node:fs");
9432
- var import_node_path7 = require("node:path");
10098
+ var import_node_path8 = require("node:path");
9433
10099
  var cachedVersion;
9434
10100
  function getThreadnoteVersion() {
9435
10101
  if (cachedVersion !== void 0) {
9436
10102
  return cachedVersion;
9437
10103
  }
9438
10104
  try {
9439
- const packageJsonPath = (0, import_node_path7.join)(__dirname, "..", "package.json");
10105
+ const packageJsonPath = (0, import_node_path8.join)(__dirname, "..", "package.json");
9440
10106
  const parsed = JSON.parse((0, import_node_fs4.readFileSync)(packageJsonPath, "utf8"));
9441
10107
  cachedVersion = typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : "unknown";
9442
10108
  } catch {
@@ -9495,7 +10161,7 @@ async function runClaudeHooksInstall(options) {
9495
10161
  console.log("\nRe-run with --apply to actually modify the file.");
9496
10162
  return;
9497
10163
  }
9498
- await (0, import_promises7.mkdir)((0, import_node_path8.dirname)(path), { recursive: true });
10164
+ await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path), { recursive: true });
9499
10165
  const serialized = `${JSON.stringify(next, void 0, 2)}
9500
10166
  `;
9501
10167
  await (0, import_promises7.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
@@ -9595,7 +10261,7 @@ async function hasManagedClaudeHooks() {
9595
10261
  async function runPreCompactHook(config, options = {}) {
9596
10262
  try {
9597
10263
  const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
9598
- const project = repoRoot ? (0, import_node_path8.basename)(repoRoot) : "general";
10264
+ const project = repoRoot ? (0, import_node_path9.basename)(repoRoot) : "general";
9599
10265
  await runHandoff(config, {
9600
10266
  blockers: "- none recorded",
9601
10267
  dryRun: options.dryRun === true,
@@ -9619,7 +10285,7 @@ async function runSessionStartHook(config, options = {}) {
9619
10285
  if (!repoRoot) {
9620
10286
  return;
9621
10287
  }
9622
- const project = (0, import_node_path8.basename)(repoRoot);
10288
+ const project = (0, import_node_path9.basename)(repoRoot);
9623
10289
  await emitUpdateBannerIfOutdated(config);
9624
10290
  process.stdout.write(`## Threadnote \u2014 latest context for ${project}
9625
10291
 
@@ -9628,7 +10294,8 @@ async function runSessionStartHook(config, options = {}) {
9628
10294
  dryRun: options.dryRun === true,
9629
10295
  inferScope: true,
9630
10296
  nodeLimit: "5",
9631
- query: `${project} latest handoff durable feature memory`
10297
+ // Keep "current branch" here so recall enriches the query with local git/workspace terms.
10298
+ query: `${project} current branch latest handoff durable feature memory`
9632
10299
  });
9633
10300
  } catch (err) {
9634
10301
  process.stderr.write(
@@ -9640,7 +10307,7 @@ async function runSessionStartHook(config, options = {}) {
9640
10307
  async function emitUpdateBannerIfOutdated(config) {
9641
10308
  try {
9642
10309
  const result = await checkForThreadnoteUpdate({
9643
- cachePath: (0, import_node_path8.join)(config.agentContextHome, ".update-state.json"),
10310
+ cachePath: (0, import_node_path9.join)(config.agentContextHome, ".update-state.json"),
9644
10311
  currentVersion: getThreadnoteVersion()
9645
10312
  });
9646
10313
  if (!result || !result.outdated) {
@@ -9666,12 +10333,12 @@ async function emitUpdateBannerIfOutdated(config) {
9666
10333
 
9667
10334
  // src/seeding.ts
9668
10335
  var import_promises8 = require("node:fs/promises");
9669
- var import_node_path9 = require("node:path");
10336
+ var import_node_path10 = require("node:path");
9670
10337
  async function runSeed(config, options) {
9671
10338
  const manifest = await readSeedManifest(config.manifestPath);
9672
10339
  const ignorePatterns = await loadIgnorePatterns();
9673
10340
  const ov = await openVikingCliForMode(options.dryRun === true);
9674
- const statePath = (0, import_node_path9.join)(config.agentContextHome, SEED_STATE_FILE);
10341
+ const statePath = (0, import_node_path10.join)(config.agentContextHome, SEED_STATE_FILE);
9675
10342
  const state = options.force === true ? { files: {}, version: 1 } : await readSeedState(statePath);
9676
10343
  const projects = filterProjects(manifest.projects, options.only);
9677
10344
  let importedCount = 0;
@@ -9751,13 +10418,13 @@ async function readSeedState(path) {
9751
10418
  }
9752
10419
  }
9753
10420
  async function writeSeedState(path, state) {
9754
- await ensureDirectory((0, import_node_path9.dirname)(path), false);
10421
+ await ensureDirectory((0, import_node_path10.dirname)(path), false);
9755
10422
  await (0, import_promises8.writeFile)(path, `${JSON.stringify(state, void 0, 2)}
9756
10423
  `, { encoding: "utf8", mode: 384 });
9757
10424
  }
9758
10425
  async function runInitManifest(config, options) {
9759
10426
  const manifestPath = expandPath(
9760
- options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path9.join)(config.agentContextHome, USER_MANIFEST_NAME)
10427
+ options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path10.join)(config.agentContextHome, USER_MANIFEST_NAME)
9761
10428
  );
9762
10429
  const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
9763
10430
  const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
@@ -9800,7 +10467,7 @@ async function runInitManifest(config, options) {
9800
10467
  console.log(output2.trimEnd());
9801
10468
  return;
9802
10469
  }
9803
- await ensureDirectory((0, import_node_path9.dirname)(manifestPath), false);
10470
+ await ensureDirectory((0, import_node_path10.dirname)(manifestPath), false);
9804
10471
  await (0, import_promises8.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
9805
10472
  await (0, import_promises8.chmod)(manifestPath, 384);
9806
10473
  console.log(`Wrote manifest: ${manifestPath}`);
@@ -9838,7 +10505,7 @@ async function projectIdentity(path) {
9838
10505
  }
9839
10506
  }
9840
10507
  function projectManifestForRepo(repoRoot, existingProjects) {
9841
- const baseName = uriSegment((0, import_node_path9.basename)(repoRoot));
10508
+ const baseName = uriSegment((0, import_node_path10.basename)(repoRoot));
9842
10509
  const usedNames = new Set(existingProjects.map((project) => project.name));
9843
10510
  const usedUris = new Set(existingProjects.map((project) => project.uri));
9844
10511
  let name = baseName;
@@ -9860,7 +10527,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
9860
10527
  for (const pattern of project.seed) {
9861
10528
  const files = await resolveProjectPattern(projectRoot, pattern);
9862
10529
  for (const filePath of files) {
9863
- const relativePath = toPosixPath((0, import_node_path9.relative)(projectRoot, filePath));
10530
+ const relativePath = toPosixPath((0, import_node_path10.relative)(projectRoot, filePath));
9864
10531
  if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
9865
10532
  continue;
9866
10533
  }
@@ -9878,17 +10545,17 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
9878
10545
  async function resolveProjectPattern(projectRoot, pattern) {
9879
10546
  const normalizedPattern = toPosixPath(pattern);
9880
10547
  if (!hasGlob(normalizedPattern)) {
9881
- const filePath = (0, import_node_path9.join)(projectRoot, normalizedPattern);
10548
+ const filePath = (0, import_node_path10.join)(projectRoot, normalizedPattern);
9882
10549
  return await isFile(filePath) ? [filePath] : [];
9883
10550
  }
9884
10551
  const globBase = getGlobBase(normalizedPattern);
9885
- const basePath = (0, import_node_path9.join)(projectRoot, globBase);
10552
+ const basePath = (0, import_node_path10.join)(projectRoot, globBase);
9886
10553
  if (!await exists(basePath)) {
9887
10554
  return [];
9888
10555
  }
9889
10556
  const regex = globToRegExp(normalizedPattern);
9890
10557
  const files = await walkFiles(basePath);
9891
- return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path9.relative)(projectRoot, filePath))));
10558
+ return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path10.relative)(projectRoot, filePath))));
9892
10559
  }
9893
10560
  async function prepareSeedFile(config, candidate, dryRun) {
9894
10561
  const content = await (0, import_promises8.readFile)(candidate.filePath, "utf8");
@@ -9903,12 +10570,12 @@ async function prepareSeedFile(config, candidate, dryRun) {
9903
10570
  if (redactedContent === content) {
9904
10571
  return candidate.filePath;
9905
10572
  }
9906
- const redactedPath = (0, import_node_path9.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
10573
+ const redactedPath = (0, import_node_path10.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
9907
10574
  if (dryRun) {
9908
10575
  console.log(`Would write redacted copy: ${redactedPath}`);
9909
10576
  return redactedPath;
9910
10577
  }
9911
- await ensureDirectory((0, import_node_path9.dirname)(redactedPath), false);
10578
+ await ensureDirectory((0, import_node_path10.dirname)(redactedPath), false);
9912
10579
  await (0, import_promises8.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
9913
10580
  await (0, import_promises8.chmod)(redactedPath, 384);
9914
10581
  return redactedPath;
@@ -9966,10 +10633,10 @@ async function resolveAbsolutePattern(pattern) {
9966
10633
  return files.filter((filePath) => regex.test(toPosixPath(filePath)));
9967
10634
  }
9968
10635
  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`;
10636
+ 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
10637
  }
9971
10638
  async function loadIgnorePatterns() {
9972
- const raw = await (0, import_promises8.readFile)((0, import_node_path9.join)(toolRoot(), ".threadnoteignore"), "utf8");
10639
+ const raw = await (0, import_promises8.readFile)((0, import_node_path10.join)(toolRoot(), ".threadnoteignore"), "utf8");
9973
10640
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
9974
10641
  }
9975
10642
  function matchesIgnore(relativePath, patterns) {
@@ -10044,7 +10711,7 @@ var import_node_child_process3 = require("node:child_process");
10044
10711
  var import_node_fs6 = require("node:fs");
10045
10712
  var import_promises11 = require("node:fs/promises");
10046
10713
  var import_node_os6 = require("node:os");
10047
- var import_node_path11 = require("node:path");
10714
+ var import_node_path12 = require("node:path");
10048
10715
  var import_node_process2 = require("node:process");
10049
10716
  var import_promises12 = require("node:readline/promises");
10050
10717
 
@@ -10052,7 +10719,7 @@ var import_promises12 = require("node:readline/promises");
10052
10719
  var import_node_fs5 = require("node:fs");
10053
10720
  var import_promises9 = require("node:fs/promises");
10054
10721
  var import_node_os5 = require("node:os");
10055
- var import_node_path10 = require("node:path");
10722
+ var import_node_path11 = require("node:path");
10056
10723
  var import_promises10 = require("node:readline/promises");
10057
10724
  var import_node_process = require("node:process");
10058
10725
  var NPM_PACKAGE_NAME = "threadnote";
@@ -10267,7 +10934,7 @@ async function waitForOpenVikingHealthy(config, timeoutMs) {
10267
10934
  return isOpenVikingHealthy(config);
10268
10935
  }
10269
10936
  function launchAgentPlistPath() {
10270
- return (0, import_node_path10.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
10937
+ return (0, import_node_path11.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
10271
10938
  }
10272
10939
  async function isLaunchAgentInstalled() {
10273
10940
  if (process.platform !== "darwin") {
@@ -10303,7 +10970,7 @@ async function getUpdateInfo(config, options) {
10303
10970
  };
10304
10971
  }
10305
10972
  async function currentPackageVersion() {
10306
- const rawPackage = await (0, import_promises9.readFile)((0, import_node_path10.join)(toolRoot(), "package.json"), "utf8");
10973
+ const rawPackage = await (0, import_promises9.readFile)((0, import_node_path11.join)(toolRoot(), "package.json"), "utf8");
10307
10974
  const parsed = JSON.parse(rawPackage);
10308
10975
  if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
10309
10976
  throw new Error("Could not read current threadnote package version.");
@@ -10357,7 +11024,7 @@ async function writeUpdateCache2(config, cache) {
10357
11024
  `, { encoding: "utf8", mode: 384 });
10358
11025
  }
10359
11026
  function updateCachePath(config) {
10360
- return (0, import_node_path10.join)(config.agentContextHome, "update-check.json");
11027
+ return (0, import_node_path11.join)(config.agentContextHome, "update-check.json");
10361
11028
  }
10362
11029
  async function runApplicablePostUpdateMigrations(config, options) {
10363
11030
  const state = await readPostUpdateState(config);
@@ -10421,7 +11088,7 @@ async function applicablePostUpdateMigrations(config, options) {
10421
11088
  return applicable;
10422
11089
  }
10423
11090
  async function readPostUpdateMigrations() {
10424
- const raw = await readFileIfExists((0, import_node_path10.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
11091
+ const raw = await readFileIfExists((0, import_node_path11.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
10425
11092
  if (!raw) {
10426
11093
  return [];
10427
11094
  }
@@ -10499,7 +11166,7 @@ async function writePostUpdateState(config, state) {
10499
11166
  `, { encoding: "utf8", mode: 384 });
10500
11167
  }
10501
11168
  function postUpdateStatePath(config) {
10502
- return (0, import_node_path10.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
11169
+ return (0, import_node_path11.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
10503
11170
  }
10504
11171
  async function resolveUpdateRuntime(runtime) {
10505
11172
  if (runtime !== "auto") {
@@ -10529,14 +11196,14 @@ async function runtimeThreadnoteBin(runtime) {
10529
11196
  if (runtime === "npm") {
10530
11197
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
10531
11198
  const prefix = result.stdout.trim();
10532
- return prefix ? (0, import_node_path10.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
11199
+ return prefix ? (0, import_node_path11.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
10533
11200
  }
10534
11201
  if (runtime === "bun") {
10535
11202
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
10536
11203
  const binDir = result.stdout.trim();
10537
- return binDir ? (0, import_node_path10.join)(binDir, NPM_PACKAGE_NAME) : void 0;
11204
+ return binDir ? (0, import_node_path11.join)(binDir, NPM_PACKAGE_NAME) : void 0;
10538
11205
  }
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);
11206
+ 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
11207
  }
10541
11208
  function updatePackageCommand(runtime, registry) {
10542
11209
  if (runtime === "npm") {
@@ -10591,9 +11258,9 @@ async function runDoctor(config, options) {
10591
11258
  checks.push(await commandShimCheck());
10592
11259
  checks.push(...await userAgentInstructionsChecks());
10593
11260
  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"));
11261
+ checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
11262
+ checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
11263
+ checks.push(await fileCheck((0, import_node_path12.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
10597
11264
  checks.push(await healthCheck(config));
10598
11265
  for (const check of checks) {
10599
11266
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -10610,9 +11277,9 @@ async function runInstall(config, options) {
10610
11277
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
10611
11278
  const dryRun = options.dryRun === true;
10612
11279
  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);
11280
+ await ensureDirectory((0, import_node_path12.join)(config.agentContextHome, "logs"), dryRun);
11281
+ await ensureDirectory((0, import_node_path12.join)(config.agentContextHome, "redacted"), dryRun);
11282
+ await ensureDirectory((0, import_node_path12.join)(config.agentContextHome, "mcp"), dryRun);
10616
11283
  await installCommandShim(dryRun);
10617
11284
  await installUserAgentInstructions(dryRun);
10618
11285
  const serverPath = await findOpenVikingServer();
@@ -10649,17 +11316,17 @@ async function runInstall(config, options) {
10649
11316
  }
10650
11317
  await writeTemplateIfMissing({
10651
11318
  config,
10652
- destinationPath: (0, import_node_path11.join)(config.agentContextHome, "ov.conf"),
11319
+ destinationPath: (0, import_node_path12.join)(config.agentContextHome, "ov.conf"),
10653
11320
  dryRun,
10654
11321
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
10655
- templatePath: (0, import_node_path11.join)(toolRoot(), "config", "ov.conf.template.json")
11322
+ templatePath: (0, import_node_path12.join)(toolRoot(), "config", "ov.conf.template.json")
10656
11323
  });
10657
11324
  await writeTemplateIfMissing({
10658
11325
  config,
10659
- destinationPath: (0, import_node_path11.join)(config.agentContextHome, "ovcli.conf"),
11326
+ destinationPath: (0, import_node_path12.join)(config.agentContextHome, "ovcli.conf"),
10660
11327
  dryRun,
10661
11328
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
10662
- templatePath: (0, import_node_path11.join)(toolRoot(), "config", "ovcli.conf.template.json")
11329
+ templatePath: (0, import_node_path12.join)(toolRoot(), "config", "ovcli.conf.template.json")
10663
11330
  });
10664
11331
  if (options.start !== false) {
10665
11332
  const healthy = await repairServerHealth(config, dryRun);
@@ -10713,7 +11380,7 @@ async function runUninstall(config, options) {
10713
11380
  }
10714
11381
  console.log("Uninstalling local Threadnote setup.");
10715
11382
  await runStop(config, { dryRun });
10716
- await removePathIfExists((0, import_node_path11.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
11383
+ await removePathIfExists((0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
10717
11384
  await removeLaunchAgent(dryRun);
10718
11385
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
10719
11386
  await removeMcpSnippets(config, dryRun);
@@ -10770,7 +11437,7 @@ async function repairManifest(config, dryRun) {
10770
11437
  console.log(output2.trimEnd());
10771
11438
  return;
10772
11439
  }
10773
- await ensureDirectory((0, import_node_path11.dirname)(config.manifestPath), false);
11440
+ await ensureDirectory((0, import_node_path12.dirname)(config.manifestPath), false);
10774
11441
  const currentContent = await readFileIfExists(config.manifestPath);
10775
11442
  if (currentContent !== void 0) {
10776
11443
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
@@ -10788,7 +11455,7 @@ async function findOpenVikingServer() {
10788
11455
  return onPath;
10789
11456
  }
10790
11457
  for (const candidateDir of await openVikingServerCandidateDirs()) {
10791
- const candidate = (0, import_node_path11.join)(candidateDir, OPENVIKING_SERVER_COMMAND);
11458
+ const candidate = (0, import_node_path12.join)(candidateDir, OPENVIKING_SERVER_COMMAND);
10792
11459
  if (await isExecutable(candidate)) {
10793
11460
  return candidate;
10794
11461
  }
@@ -10834,7 +11501,7 @@ async function maybePrintOpenVikingPathHint(serverPath) {
10834
11501
  if (onPath) {
10835
11502
  return;
10836
11503
  }
10837
- const binDir = (0, import_node_path11.dirname)(serverPath);
11504
+ const binDir = (0, import_node_path12.dirname)(serverPath);
10838
11505
  const rcHint = suggestedShellRc(process.env.SHELL, (0, import_node_os6.platform)());
10839
11506
  console.log(
10840
11507
  `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 +11545,7 @@ async function runStart(config, options) {
10878
11545
  );
10879
11546
  }
10880
11547
  const logPath = openVikingLogPath(config);
10881
- await ensureDirectory((0, import_node_path11.dirname)(logPath), false);
11548
+ await ensureDirectory((0, import_node_path12.dirname)(logPath), false);
10882
11549
  if (options.foreground === true) {
10883
11550
  const result = await runInteractive(server, args);
10884
11551
  process.exitCode = result;
@@ -10887,7 +11554,7 @@ async function runStart(config, options) {
10887
11554
  const logFd = (0, import_node_fs6.openSync)(logPath, "a");
10888
11555
  const child = spawnDetachedServerWithLog(server, args, logFd);
10889
11556
  child.unref();
10890
- await (0, import_promises11.writeFile)((0, import_node_path11.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
11557
+ await (0, import_promises11.writeFile)((0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
10891
11558
  `, "utf8");
10892
11559
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
10893
11560
  if (health) {
@@ -10920,7 +11587,7 @@ async function runStop(config, options) {
10920
11587
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
10921
11588
  }
10922
11589
  }
10923
- const pidPath = (0, import_node_path11.join)(config.agentContextHome, "openviking-server.pid");
11590
+ const pidPath = (0, import_node_path12.join)(config.agentContextHome, "openviking-server.pid");
10924
11591
  const pidText = await readFileIfExists(pidPath);
10925
11592
  if (!pidText) {
10926
11593
  console.log("No pid file found for detached OpenViking server.");
@@ -10962,7 +11629,7 @@ async function openVikingServerCheck() {
10962
11629
  }
10963
11630
  const result = await runCommand(executable, ["--help"], { allowFailure: true });
10964
11631
  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)`;
11632
+ const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path12.dirname)(executable)} to PATH)`;
10966
11633
  return {
10967
11634
  name,
10968
11635
  status: result.exitCode === 0 ? "ok" : "warn",
@@ -11031,7 +11698,7 @@ async function pythonSystemCertificatesCheck() {
11031
11698
  };
11032
11699
  }
11033
11700
  async function commandShimCheck() {
11034
- const shimPath = (0, import_node_path11.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
11701
+ const shimPath = (0, import_node_path12.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
11035
11702
  const content = await readFileIfExists(shimPath);
11036
11703
  if (content === void 0) {
11037
11704
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -11101,7 +11768,7 @@ async function siblingPythonForExecutable(executablePath) {
11101
11768
  } catch (_err) {
11102
11769
  return void 0;
11103
11770
  }
11104
- const pythonPath = (0, import_node_path11.join)((0, import_node_path11.dirname)(resolvedPath), "python");
11771
+ const pythonPath = (0, import_node_path12.join)((0, import_node_path12.dirname)(resolvedPath), "python");
11105
11772
  return await exists(pythonPath) ? pythonPath : void 0;
11106
11773
  }
11107
11774
  async function manifestCheck(path) {
@@ -11321,14 +11988,14 @@ async function writeTemplateIfMissing(options) {
11321
11988
  console.log(`Would write ${options.destinationPath}`);
11322
11989
  return;
11323
11990
  }
11324
- await ensureDirectory((0, import_node_path11.dirname)(options.destinationPath), false);
11991
+ await ensureDirectory((0, import_node_path12.dirname)(options.destinationPath), false);
11325
11992
  await (0, import_promises11.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
11326
11993
  await (0, import_promises11.chmod)(options.destinationPath, 384);
11327
11994
  console.log(`Wrote ${options.destinationPath}`);
11328
11995
  }
11329
11996
  async function installCommandShim(dryRun) {
11330
11997
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
11331
- const shimPath = (0, import_node_path11.join)(binDir, "threadnote");
11998
+ const shimPath = (0, import_node_path12.join)(binDir, "threadnote");
11332
11999
  const existingContent = await readFileIfExists(shimPath);
11333
12000
  if (existingContent && !isManagedCommandShim(existingContent)) {
11334
12001
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -11349,7 +12016,7 @@ async function installCommandShim(dryRun) {
11349
12016
  console.log(`Wrote command shim: ${shimPath}`);
11350
12017
  }
11351
12018
  async function removeCommandShim(dryRun) {
11352
- const shimPath = (0, import_node_path11.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
12019
+ const shimPath = (0, import_node_path12.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
11353
12020
  const content = await readFileIfExists(shimPath);
11354
12021
  if (content === void 0) {
11355
12022
  console.log(`Already absent: ${shimPath}`);
@@ -11383,7 +12050,7 @@ async function installUserAgentInstructions(dryRun) {
11383
12050
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
11384
12051
  continue;
11385
12052
  }
11386
- await ensureDirectory((0, import_node_path11.dirname)(targetPath), false);
12053
+ await ensureDirectory((0, import_node_path12.dirname)(targetPath), false);
11387
12054
  await (0, import_promises11.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
11388
12055
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
11389
12056
  }
@@ -11441,7 +12108,7 @@ async function renderUserAgentInstructions(target) {
11441
12108
  ].join("\n");
11442
12109
  }
11443
12110
  async function renderUserAgentInstructionsBlock() {
11444
- const instructions = (await (0, import_promises11.readFile)((0, import_node_path11.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
12111
+ const instructions = (await (0, import_promises11.readFile)((0, import_node_path12.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
11445
12112
  return `${USER_INSTRUCTIONS_START_MARKER}
11446
12113
  ${instructions}
11447
12114
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -11525,7 +12192,7 @@ async function installLaunchAgent(config, dryRun) {
11525
12192
  `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
12193
  );
11527
12194
  }
11528
- const source = (0, import_node_path11.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
12195
+ const source = (0, import_node_path12.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
11529
12196
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
11530
12197
  const rendered = renderTemplate(await (0, import_promises11.readFile)(source, "utf8"), config, {
11531
12198
  OPENVIKING_SERVER_PATH: resolvedServer ?? OPENVIKING_SERVER_COMMAND
@@ -11539,8 +12206,8 @@ async function installLaunchAgent(config, dryRun) {
11539
12206
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
11540
12207
  return;
11541
12208
  }
11542
- await ensureDirectory((0, import_node_path11.dirname)(destination), false);
11543
- await ensureDirectory((0, import_node_path11.dirname)(openVikingLogPath(config)), false);
12209
+ await ensureDirectory((0, import_node_path12.dirname)(destination), false);
12210
+ await ensureDirectory((0, import_node_path12.dirname)(openVikingLogPath(config)), false);
11544
12211
  await (0, import_promises11.writeFile)(destination, rendered, "utf8");
11545
12212
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
11546
12213
  await maybeRun(false, "launchctl", ["load", destination]);
@@ -11604,7 +12271,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
11604
12271
  if (typeof parsed.default_user !== "string") {
11605
12272
  return false;
11606
12273
  }
11607
- if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path11.join)(config.agentContextHome, "data")) {
12274
+ if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path12.join)(config.agentContextHome, "data")) {
11608
12275
  return false;
11609
12276
  }
11610
12277
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -11724,16 +12391,19 @@ async function main() {
11724
12391
  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
12392
  await runMigrateLifecycle(getRuntimeConfig(program2), options);
11726
12393
  });
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) => {
12394
+ 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
12395
  await runRecall(getRuntimeConfig(program2), options);
11729
12396
  });
12397
+ 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) => {
12398
+ await runCompact(getRuntimeConfig(program2), options);
12399
+ });
11730
12400
  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
12401
  await runRead(getRuntimeConfig(program2), uri, options);
11732
12402
  });
11733
12403
  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
12404
  await runList(getRuntimeConfig(program2), uri, options);
11735
12405
  });
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) => {
12406
+ 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
12407
  await runHandoff(getRuntimeConfig(program2), options);
11738
12408
  });
11739
12409
  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) => {