threadnote 0.7.4 → 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");
@@ -3850,6 +3850,27 @@ async function exists(path) {
3850
3850
  return false;
3851
3851
  }
3852
3852
  }
3853
+ async function isExecutable(path) {
3854
+ try {
3855
+ await (0, import_promises.access)(path, import_node_fs.constants.X_OK);
3856
+ return true;
3857
+ } catch (_err) {
3858
+ return false;
3859
+ }
3860
+ }
3861
+ function suggestedShellRc(shellPath, currentPlatform) {
3862
+ const shell = shellPath ?? "";
3863
+ if (shell.endsWith("/zsh")) {
3864
+ return "~/.zshrc";
3865
+ }
3866
+ if (shell.endsWith("/bash")) {
3867
+ return currentPlatform === "darwin" ? "~/.bash_profile" : "~/.bashrc";
3868
+ }
3869
+ if (shell.endsWith("/fish")) {
3870
+ return "~/.config/fish/config.fish";
3871
+ }
3872
+ return "your shell rc";
3873
+ }
3853
3874
  async function isFile(path) {
3854
3875
  try {
3855
3876
  return (await (0, import_promises.stat)(path)).isFile();
@@ -3937,6 +3958,57 @@ function portablePath(path) {
3937
3958
  function getInvocationCwd() {
3938
3959
  return process.env.THREADNOTE_CALLER_CWD ?? process.cwd();
3939
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
+ }
3940
4012
  function toPosixPath(path) {
3941
4013
  return path.split(import_node_path.sep).join("/");
3942
4014
  }
@@ -3960,19 +4032,29 @@ function exactRecallTerms(query) {
3960
4032
  "branch",
3961
4033
  "case",
3962
4034
  "current",
4035
+ "durable",
3963
4036
  "find",
4037
+ "feature",
4038
+ "features",
3964
4039
  "handoff",
3965
4040
  "issue",
3966
4041
  "issues",
4042
+ "knowledge",
3967
4043
  "latest",
3968
4044
  "memory",
3969
4045
  "memories",
4046
+ "project",
3970
4047
  "recall",
4048
+ "repo",
4049
+ "repository",
3971
4050
  "related",
3972
4051
  "search",
3973
- "the",
4052
+ "stored",
3974
4053
  "this",
3975
- "with"
4054
+ "the",
4055
+ "with",
4056
+ "workspace",
4057
+ "worktree"
3976
4058
  ]);
3977
4059
  const seen = /* @__PURE__ */ new Set();
3978
4060
  const terms = [];
@@ -4520,7 +4602,7 @@ function existsSyncDirectory(path) {
4520
4602
 
4521
4603
  // src/memory.ts
4522
4604
  var import_promises5 = require("node:fs/promises");
4523
- var import_node_path5 = require("node:path");
4605
+ var import_node_path6 = require("node:path");
4524
4606
 
4525
4607
  // src/manifest.ts
4526
4608
  var import_promises3 = require("node:fs/promises");
@@ -7218,10 +7300,445 @@ function readStringArray(object, key) {
7218
7300
  return value;
7219
7301
  }
7220
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
+
7221
7738
  // src/runtime.ts
7222
7739
  var import_node_fs3 = require("node:fs");
7223
7740
  var import_node_os3 = require("node:os");
7224
- var import_node_path3 = require("node:path");
7741
+ var import_node_path4 = require("node:path");
7225
7742
  function getRuntimeConfig(program2, manifestOverride) {
7226
7743
  const options = program2.opts();
7227
7744
  const threadnoteHome = expandPath(options.home ?? process.env.THREADNOTE_HOME ?? "~/.openviking");
@@ -7240,32 +7757,36 @@ function getRuntimeConfig(program2, manifestOverride) {
7240
7757
  };
7241
7758
  }
7242
7759
  function defaultManifestPath(agentContextHome) {
7243
- const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
7760
+ const userManifest = (0, import_node_path4.join)(agentContextHome, USER_MANIFEST_NAME);
7244
7761
  return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
7245
7762
  }
7246
7763
  function builtInExampleManifestPath() {
7247
- 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");
7248
7765
  }
7249
7766
  function openVikingHealthUrl(config) {
7250
7767
  return `http://${config.host}:${config.port}/health`;
7251
7768
  }
7252
7769
  function openVikingLogPath(config) {
7253
- return (0, import_node_path3.join)(config.agentContextHome, "logs", "server.log");
7770
+ return (0, import_node_path4.join)(config.agentContextHome, "logs", "server.log");
7254
7771
  }
7255
7772
  function openVikingServerArgs(config) {
7256
- 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)];
7257
7774
  }
7258
7775
  function withIdentity(config, args) {
7259
7776
  return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
7260
7777
  }
7261
- function renderTemplate(template, config) {
7262
- return template.replaceAll("{{THREADNOTE_HOME}}", config.agentContextHome).replaceAll("{{OPENVIKING_ACCOUNT}}", config.account).replaceAll("{{OPENVIKING_AGENT_ID}}", config.agentId).replaceAll("{{OPENVIKING_HOST}}", config.host).replaceAll("{{OPENVIKING_PORT}}", String(config.port)).replaceAll("{{OPENVIKING_USER}}", config.user);
7778
+ function renderTemplate(template, config, extras = {}) {
7779
+ let rendered = template.replaceAll("{{THREADNOTE_HOME}}", config.agentContextHome).replaceAll("{{OPENVIKING_ACCOUNT}}", config.account).replaceAll("{{OPENVIKING_AGENT_ID}}", config.agentId).replaceAll("{{OPENVIKING_HOST}}", config.host).replaceAll("{{OPENVIKING_PORT}}", String(config.port)).replaceAll("{{OPENVIKING_USER}}", config.user);
7780
+ for (const [key, value] of Object.entries(extras)) {
7781
+ rendered = rendered.replaceAll(`{{${key}}}`, () => value);
7782
+ }
7783
+ return rendered;
7263
7784
  }
7264
7785
 
7265
7786
  // src/share.ts
7266
7787
  var import_promises4 = require("node:fs/promises");
7267
7788
  var import_node_os4 = require("node:os");
7268
- var import_node_path4 = require("node:path");
7789
+ var import_node_path5 = require("node:path");
7269
7790
  var TEAMS_FILE_VERSION = 1;
7270
7791
  var SHARED_SEGMENT = "shared";
7271
7792
  var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
@@ -7334,8 +7855,8 @@ async function runShareInit(config, remoteUrl, options) {
7334
7855
  if (await exists(gitdir)) {
7335
7856
  throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
7336
7857
  }
7337
- await ensureDirectory((0, import_node_path4.dirname)(worktree), dryRun);
7338
- 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);
7339
7860
  const git = await requiredExecutable("git");
7340
7861
  await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
7341
7862
  const newConfig = {
@@ -7366,7 +7887,7 @@ async function runShareInit(config, remoteUrl, options) {
7366
7887
  var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
7367
7888
  var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
7368
7889
  async function ensureSharedGitignore(worktree, git, push) {
7369
- const gitignorePath = (0, import_node_path4.join)(worktree, ".gitignore");
7890
+ const gitignorePath = (0, import_node_path5.join)(worktree, ".gitignore");
7370
7891
  const existing = await readFileIfExists(gitignorePath) ?? "";
7371
7892
  const lines = existing.split("\n").map((line) => line.trim());
7372
7893
  const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
@@ -7467,7 +7988,7 @@ function autoShareState(config) {
7467
7988
  return state;
7468
7989
  }
7469
7990
  function pendingReindexesPath(config) {
7470
- 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");
7471
7992
  }
7472
7993
  async function loadPendingReindexes(config, state) {
7473
7994
  const raw = await readFileIfExists(pendingReindexesPath(config));
@@ -7511,7 +8032,7 @@ async function writePendingReindexes(config, state) {
7511
8032
  teams: Object.fromEntries(state.pendingReindexes),
7512
8033
  version: 1
7513
8034
  };
7514
- 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 });
7515
8036
  const tempPath = `${path}.${process.pid}.tmp`;
7516
8037
  await (0, import_promises4.writeFile)(tempPath, `${JSON.stringify(contents, void 0, 2)}
7517
8038
  `, { encoding: "utf8", mode: 384 });
@@ -7604,7 +8125,7 @@ async function runShareSync(config, options) {
7604
8125
  if (dryRun) {
7605
8126
  console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
7606
8127
  } else if (pullResult && pullResult.exitCode !== 0) {
7607
- 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"))) {
7608
8129
  throw new Error(
7609
8130
  `git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
7610
8131
  Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
@@ -7659,7 +8180,7 @@ async function runShareSyncQuiet(config, state, options) {
7659
8180
  const beforeRev = await gitOutput(worktree, ["rev-parse", "HEAD"], false);
7660
8181
  const pullResult = await runCommand(git, ["-C", worktree, "rebase", "@{u}"], { allowFailure: true });
7661
8182
  if (pullResult.exitCode !== 0) {
7662
- 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"))) {
7663
8184
  throw new Error(
7664
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.`
7665
8186
  );
@@ -7833,10 +8354,10 @@ function normalizeTeamName(input2) {
7833
8354
  return candidate;
7834
8355
  }
7835
8356
  function teamsFilePath(config) {
7836
- return (0, import_node_path4.join)(config.agentContextHome, "share", "teams.json");
8357
+ return (0, import_node_path5.join)(config.agentContextHome, "share", "teams.json");
7837
8358
  }
7838
8359
  function teamWorktreePath(config, team) {
7839
- return (0, import_node_path4.join)(
8360
+ return (0, import_node_path5.join)(
7840
8361
  config.agentContextHome,
7841
8362
  "data",
7842
8363
  "viking",
@@ -7849,7 +8370,7 @@ function teamWorktreePath(config, team) {
7849
8370
  );
7850
8371
  }
7851
8372
  function teamGitdirPath(config, team) {
7852
- 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`);
7853
8374
  }
7854
8375
  async function readTeamsFile(config) {
7855
8376
  const path = teamsFilePath(config);
@@ -7892,7 +8413,7 @@ async function readTeamsFile(config) {
7892
8413
  }
7893
8414
  async function writeTeamsFile(config, contents) {
7894
8415
  const path = teamsFilePath(config);
7895
- 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 });
7896
8417
  const serializable = {
7897
8418
  defaultTeam: contents.defaultTeam,
7898
8419
  teams: contents.teams,
@@ -7961,7 +8482,7 @@ async function walkMemoryFiles(root) {
7961
8482
  if (entry.name === ".git") {
7962
8483
  continue;
7963
8484
  }
7964
- const full = (0, import_node_path4.join)(path, entry.name);
8485
+ const full = (0, import_node_path5.join)(path, entry.name);
7965
8486
  if (entry.isDirectory()) {
7966
8487
  if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
7967
8488
  continue;
@@ -7985,7 +8506,7 @@ async function walkMemoryFiles(root) {
7985
8506
  return out;
7986
8507
  }
7987
8508
  function workfileToVikingUri(config, team, filePath) {
7988
- 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("/");
7989
8510
  return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
7990
8511
  }
7991
8512
  function isInSharedNamespace(config, uri) {
@@ -8105,8 +8626,8 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun, op
8105
8626
  }
8106
8627
  return;
8107
8628
  }
8108
- const stagingDir = await (0, import_promises4.mkdtemp)((0, import_node_path4.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
8109
- 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");
8110
8631
  try {
8111
8632
  await (0, import_promises4.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
8112
8633
  await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode, options);
@@ -8228,7 +8749,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
8228
8749
  if (!relative3) {
8229
8750
  return;
8230
8751
  }
8231
- 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 });
8232
8753
  }
8233
8754
  async function removeMemoryUri(config, ov, uri, dryRun, options = {}) {
8234
8755
  const args = withIdentity(config, ["rm", uri]);
@@ -8292,8 +8813,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
8292
8813
  const oldRel = entries[index + 1];
8293
8814
  const newRel = entries[index + 2];
8294
8815
  if (oldRel && newRel) {
8295
- changes.push({ path: (0, import_node_path4.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
8296
- 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" });
8297
8818
  }
8298
8819
  index += 3;
8299
8820
  continue;
@@ -8301,7 +8822,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
8301
8822
  const rel = entries[index + 1];
8302
8823
  if (rel) {
8303
8824
  const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
8304
- 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 });
8305
8826
  }
8306
8827
  index += 2;
8307
8828
  }
@@ -8385,6 +8906,12 @@ function parseMemoryStatus(value) {
8385
8906
  }
8386
8907
  throw new Error(`Unsupported memory status "${value}". Expected active, archived, or superseded.`);
8387
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
+ }
8388
8915
  async function runRemember(config, options) {
8389
8916
  const text = await getInputText(options.text, options.stdin === true);
8390
8917
  if (!text.trim()) {
@@ -8392,11 +8919,11 @@ async function runRemember(config, options) {
8392
8919
  }
8393
8920
  const metadata = {
8394
8921
  kind: options.kind ?? "durable",
8395
- project: normalizeOptionalMetadata(options.project),
8922
+ project: normalizeOptionalMetadata2(options.project),
8396
8923
  sourceAgentClient: options.sourceAgentClient ?? "codex",
8397
8924
  status: options.status ?? "active",
8398
8925
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8399
- topic: normalizeOptionalMetadata(options.topic)
8926
+ topic: normalizeOptionalMetadata2(options.topic)
8400
8927
  };
8401
8928
  await storeMemory(config, {
8402
8929
  bodyText: text.trim(),
@@ -8417,7 +8944,7 @@ async function runMigrateMemories(config, options) {
8417
8944
  const candidates = await legacyMemoryCandidates(config, sourceAccounts);
8418
8945
  const existingHashes = await existingDurableMemoryHashes(config);
8419
8946
  const ov = await openVikingCliForMode(dryRun);
8420
- 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");
8421
8948
  let duplicateCount = 0;
8422
8949
  let migratedCount = 0;
8423
8950
  let sensitiveCount = 0;
@@ -8480,7 +9007,7 @@ async function runMigrateLifecycle(config, options) {
8480
9007
  const limit = options.limit ? parsePositiveInteger(options.limit, "lifecycle migration limit") : void 0;
8481
9008
  const ov = await openVikingCliForMode(dryRun);
8482
9009
  const candidates = await legacyLifecycleHandoffCandidates(config);
8483
- 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");
8484
9011
  let existingCount = 0;
8485
9012
  let migratedCount = 0;
8486
9013
  let skippedCount = 0;
@@ -8490,7 +9017,7 @@ async function runMigrateLifecycle(config, options) {
8490
9017
  break;
8491
9018
  }
8492
9019
  const destinationUri = lifecycleMigrationUri(config, candidate.metadata, sha256(candidate.original.trim()));
8493
- const migratedMemory = formatMemoryDocument(
9020
+ const migratedMemory = formatMemoryDocument2(
8494
9021
  "HANDOFF",
8495
9022
  candidate.metadata,
8496
9023
  ["Migrated legacy handoff from the historical events trail.", "", candidate.original.trim()].join("\n")
@@ -8536,8 +9063,10 @@ async function runRecall(config, options) {
8536
9063
  await syncSharedReposAndLog(config);
8537
9064
  }
8538
9065
  const ov = await openVikingCliForMode(options.dryRun === true);
8539
- const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, options.query));
8540
- 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];
8541
9070
  if (inferredUri) {
8542
9071
  args.push("--uri", inferredUri);
8543
9072
  console.log(`Recall scope: ${inferredUri}`);
@@ -8545,24 +9074,41 @@ async function runRecall(config, options) {
8545
9074
  if (options.nodeLimit) {
8546
9075
  args.push("--node-limit", String(parsePositiveInteger(options.nodeLimit, "node limit")));
8547
9076
  }
8548
- await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
8549
- await augmentRecallWithSeededResources(config, ov, options, inferredUri);
8550
- 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, {
8551
9093
  dryRun: options.dryRun === true,
8552
9094
  includeArchived: options.includeArchived === true
8553
9095
  });
9096
+ if (exactOutput) {
9097
+ recallOutputs.push(exactOutput);
9098
+ }
9099
+ await printRecallHygieneNudges(config, recallOutputs.join("\n"));
8554
9100
  }
8555
- async function augmentRecallWithSeededResources(config, ov, options, inferredUri) {
9101
+ async function augmentRecallWithSeededResources(config, ov, options, inferredUri, projectQuery = options.query) {
8556
9102
  if (options.uri || options.inferScope === false) {
8557
- return;
9103
+ return void 0;
8558
9104
  }
8559
- const project = await inferProjectFromQuery(config.manifestPath, options.query);
9105
+ const project = await inferProjectFromQuery(config.manifestPath, projectQuery);
8560
9106
  if (!project) {
8561
- return;
9107
+ return void 0;
8562
9108
  }
8563
9109
  const projectResourceUri = trimTrailingSlash(project.uri);
8564
9110
  if (!projectResourceUri.startsWith("viking://") || projectResourceUri === inferredUri) {
8565
- return;
9111
+ return void 0;
8566
9112
  }
8567
9113
  const args = ["search", options.query, "--uri", projectResourceUri];
8568
9114
  if (options.nodeLimit) {
@@ -8570,7 +9116,8 @@ async function augmentRecallWithSeededResources(config, ov, options, inferredUri
8570
9116
  }
8571
9117
  console.log(`
8572
9118
  Also searching seeded resources: ${projectResourceUri}`);
8573
- 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;
8574
9121
  }
8575
9122
  async function runRead(config, uri, options) {
8576
9123
  assertVikingUri(uri);
@@ -8598,6 +9145,147 @@ async function syncSharedReposAndLog(config) {
8598
9145
  console.error(`Auto-sync warning: ${err instanceof Error ? err.message : String(err)}`);
8599
9146
  }
8600
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
+ }
8601
9289
  async function runList(config, uri, options) {
8602
9290
  assertVikingUri(uri);
8603
9291
  const ov = await openVikingCliForMode(options.dryRun === true);
@@ -8635,11 +9323,11 @@ async function runArchive(config, uri, options) {
8635
9323
  const fallbackMetadata = {
8636
9324
  archivedFrom: uri,
8637
9325
  kind: options.kind ?? "handoff",
8638
- project: normalizeOptionalMetadata(options.project),
9326
+ project: normalizeOptionalMetadata2(options.project),
8639
9327
  sourceAgentClient: "threadnote",
8640
9328
  status: "archived",
8641
9329
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8642
- topic: normalizeOptionalMetadata(options.topic)
9330
+ topic: normalizeOptionalMetadata2(options.topic)
8643
9331
  };
8644
9332
  await storeMemory(config, {
8645
9333
  bodyText: ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n"),
@@ -8657,11 +9345,11 @@ async function runArchive(config, uri, options) {
8657
9345
  const metadata = {
8658
9346
  archivedFrom: uri,
8659
9347
  kind: options.kind ?? inferredMetadata.kind ?? "handoff",
8660
- project: normalizeOptionalMetadata(options.project) ?? inferredMetadata.project,
9348
+ project: normalizeOptionalMetadata2(options.project) ?? inferredMetadata.project,
8661
9349
  sourceAgentClient: "threadnote",
8662
9350
  status: "archived",
8663
9351
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8664
- topic: normalizeOptionalMetadata(options.topic) ?? inferredMetadata.topic
9352
+ topic: normalizeOptionalMetadata2(options.topic) ?? inferredMetadata.topic
8665
9353
  };
8666
9354
  await storeMemory(config, {
8667
9355
  bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
@@ -8690,7 +9378,7 @@ async function runForget(config, uri, options) {
8690
9378
  }
8691
9379
  async function runExportPack(config, options) {
8692
9380
  const ov = await openVikingCliForMode(options.dryRun === true);
8693
- 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`);
8694
9382
  const outputPath = expandPath(options.path ?? defaultPath);
8695
9383
  await maybeRun(options.dryRun === true, ov, withIdentity(config, ["export", options.uri ?? "viking://", outputPath]));
8696
9384
  }
@@ -8715,7 +9403,7 @@ async function inferRecallUri(config, query) {
8715
9403
  async function printExactMemoryMatches(config, ov, query, options) {
8716
9404
  const terms = exactRecallTerms(query);
8717
9405
  if (terms.length === 0) {
8718
- return;
9406
+ return void 0;
8719
9407
  }
8720
9408
  const scopes = exactMemoryScopes(config, options.includeArchived);
8721
9409
  const outputs = [];
@@ -8727,30 +9415,32 @@ async function printExactMemoryMatches(config, ov, query, options) {
8727
9415
  continue;
8728
9416
  }
8729
9417
  const result = await runCommand(ov, args, { allowFailure: true });
8730
- const output2 = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
8731
- if (result.exitCode === 0 && grepOutputHasMatches(output2)) {
8732
- 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);
8733
9421
  }
8734
9422
  }
8735
9423
  }
8736
9424
  if (outputs.length === 0) {
8737
- return;
9425
+ return void 0;
8738
9426
  }
8739
- console.log("\nExact durable memory matches:");
8740
- 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;
8741
9431
  }
8742
9432
  async function storeMemory(config, options) {
8743
9433
  if (options.replaceUri) {
8744
9434
  assertVikingUri(options.replaceUri);
8745
9435
  }
8746
9436
  const ov = await openVikingCliForMode(options.dryRun);
8747
- 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");
8748
9438
  const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
8749
- const candidateMemory = formatMemoryDocument(options.title, candidateMetadata, options.bodyText);
9439
+ const candidateMemory = formatMemoryDocument2(options.title, candidateMetadata, options.bodyText);
8750
9440
  const memoryUri = memoryUriFor(config, candidateMemory, candidateMetadata);
8751
9441
  const isInPlaceUpdate = options.replaceUri !== void 0 && options.replaceUri === memoryUri;
8752
9442
  const finalMetadata = isInPlaceUpdate ? { ...options.metadata, supersedes: void 0 } : candidateMetadata;
8753
- const memory = isInPlaceUpdate ? formatMemoryDocument(options.title, finalMetadata, options.bodyText) : candidateMemory;
9443
+ const memory = isInPlaceUpdate ? formatMemoryDocument2(options.title, finalMetadata, options.bodyText) : candidateMemory;
8754
9444
  const writeMode = await memoryWriteMode(ov, config, memoryUri, finalMetadata);
8755
9445
  if (options.dryRun) {
8756
9446
  console.log(memory);
@@ -8892,7 +9582,7 @@ async function hasLegacyLifecycleHandoffCandidates(config) {
8892
9582
  return (await legacyLifecycleHandoffCandidates(config, 1)).length > 0;
8893
9583
  }
8894
9584
  async function legacyLifecycleHandoffCandidates(config, limit) {
8895
- const eventsRoot = (0, import_node_path5.join)(localUserMemoriesRoot(config), "events");
9585
+ const eventsRoot = (0, import_node_path6.join)(localUserMemoriesRoot(config), "events");
8896
9586
  let entries;
8897
9587
  try {
8898
9588
  entries = await (0, import_promises5.readdir)(eventsRoot, { withFileTypes: true });
@@ -8904,7 +9594,7 @@ async function legacyLifecycleHandoffCandidates(config, limit) {
8904
9594
  if (!entry.isFile() || entry.name.startsWith(".") || !entry.name.endsWith(".md")) {
8905
9595
  continue;
8906
9596
  }
8907
- const sourcePath = (0, import_node_path5.join)(eventsRoot, entry.name);
9597
+ const sourcePath = (0, import_node_path6.join)(eventsRoot, entry.name);
8908
9598
  const original = await readTextIfExists(sourcePath);
8909
9599
  if (!original || !isClearLegacyHandoffMemory(original) || sensitiveMemoryReason(original)) {
8910
9600
  continue;
@@ -8990,7 +9680,7 @@ function vikingDirectoryChain(directoryUri) {
8990
9680
  }
8991
9681
  return chain;
8992
9682
  }
8993
- function formatMemoryDocument(title, metadata, body) {
9683
+ function formatMemoryDocument2(title, metadata, body) {
8994
9684
  const header = [
8995
9685
  title,
8996
9686
  `kind: ${metadata.kind}`,
@@ -9007,10 +9697,10 @@ function formatMemoryDocument(title, metadata, body) {
9007
9697
  function inferMemoryMetadata(memory) {
9008
9698
  const header = memory.slice(0, Math.max(0, memory.indexOf("\n\n")) || memory.length);
9009
9699
  const firstLine2 = header.split("\n")[0]?.trim();
9010
- const kind = parseOptionalMemoryKind(parseHeaderValue(header, "kind")) ?? (firstLine2 === "HANDOFF" ? "handoff" : void 0);
9011
- const status = parseOptionalMemoryStatus(parseHeaderValue(header, "status"));
9012
- const project = normalizeOptionalMetadata(parseHeaderValue(header, "project")) ?? normalizeOptionalMetadata(parseHeaderValue(header, "repo"));
9013
- 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"));
9014
9704
  return {
9015
9705
  kind,
9016
9706
  project,
@@ -9046,9 +9736,9 @@ function inferLegacyProject(memory) {
9046
9736
  return "general";
9047
9737
  }
9048
9738
  const trimmed = explicit.trim().replace(/[`.,;]+$/g, "");
9049
- return trimmed.includes("/") ? (0, import_node_path5.basename)(trimmed) : trimmed;
9739
+ return trimmed.includes("/") ? (0, import_node_path6.basename)(trimmed) : trimmed;
9050
9740
  }
9051
- function parseOptionalMemoryKind(value) {
9741
+ function parseOptionalMemoryKind2(value) {
9052
9742
  if (!value) {
9053
9743
  return void 0;
9054
9744
  }
@@ -9058,7 +9748,7 @@ function parseOptionalMemoryKind(value) {
9058
9748
  return void 0;
9059
9749
  }
9060
9750
  }
9061
- function parseOptionalMemoryStatus(value) {
9751
+ function parseOptionalMemoryStatus2(value) {
9062
9752
  if (!value) {
9063
9753
  return void 0;
9064
9754
  }
@@ -9068,7 +9758,7 @@ function parseOptionalMemoryStatus(value) {
9068
9758
  return void 0;
9069
9759
  }
9070
9760
  }
9071
- function normalizeOptionalMetadata(value) {
9761
+ function normalizeOptionalMetadata2(value) {
9072
9762
  const trimmed = value?.trim();
9073
9763
  return trimmed ? trimmed : void 0;
9074
9764
  }
@@ -9086,14 +9776,14 @@ async function legacySourceAccounts(config, options) {
9086
9776
  async function legacyMemoryCandidates(config, sourceAccounts) {
9087
9777
  const candidates = [];
9088
9778
  for (const sourceAccount of sourceAccounts) {
9089
- const sessionRoot = (0, import_node_path5.join)(localVikingDataRoot(config), sourceAccount, "session");
9779
+ const sessionRoot = (0, import_node_path6.join)(localVikingDataRoot(config), sourceAccount, "session");
9090
9780
  for (const sourceSession of await childDirectoryNames(sessionRoot)) {
9091
- const historyRoot = (0, import_node_path5.join)(sessionRoot, sourceSession, "history");
9781
+ const historyRoot = (0, import_node_path6.join)(sessionRoot, sourceSession, "history");
9092
9782
  for (const sourceArchive of await childDirectoryNames(historyRoot)) {
9093
9783
  if (!sourceArchive.startsWith("archive_")) {
9094
9784
  continue;
9095
9785
  }
9096
- const sourcePath = (0, import_node_path5.join)(historyRoot, sourceArchive, "messages.jsonl");
9786
+ const sourcePath = (0, import_node_path6.join)(historyRoot, sourceArchive, "messages.jsonl");
9097
9787
  for (const text of await legacyMemoryTexts(sourcePath)) {
9098
9788
  candidates.push({
9099
9789
  comparableHash: sha256(comparableMemoryText(text)),
@@ -9161,7 +9851,7 @@ async function collectDurableMemoryHashes(root, hashes) {
9161
9851
  return;
9162
9852
  }
9163
9853
  for (const entry of entries) {
9164
- const path = (0, import_node_path5.join)(root, entry.name);
9854
+ const path = (0, import_node_path6.join)(root, entry.name);
9165
9855
  if (entry.isDirectory()) {
9166
9856
  await collectDurableMemoryHashes(path, hashes);
9167
9857
  continue;
@@ -9178,7 +9868,7 @@ async function collectDurableMemoryHashes(root, hashes) {
9178
9868
  }
9179
9869
  }
9180
9870
  function isDurableMemoryPath(path) {
9181
- return path.split(import_node_path5.sep).includes("memories");
9871
+ return path.split(import_node_path6.sep).includes("memories");
9182
9872
  }
9183
9873
  async function childDirectoryNames(path) {
9184
9874
  let entries;
@@ -9222,10 +9912,10 @@ function legacySourceLabel(candidate) {
9222
9912
  return `${candidate.sourceAccount}/${candidate.sourceSession}/${candidate.sourceArchive}`;
9223
9913
  }
9224
9914
  function localVikingDataRoot(config) {
9225
- return (0, import_node_path5.join)(config.agentContextHome, "data", "viking");
9915
+ return (0, import_node_path6.join)(config.agentContextHome, "data", "viking");
9226
9916
  }
9227
9917
  function localUserMemoriesRoot(config) {
9228
- 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");
9229
9919
  }
9230
9920
  function uniqueStrings(values) {
9231
9921
  return [...new Set(values)].sort();
@@ -9241,14 +9931,15 @@ async function buildHandoff(options) {
9241
9931
  const status = await gitValue(["status", "--short"], repoRoot) ?? "";
9242
9932
  const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
9243
9933
  const touchedFiles = await gitTouchedFiles(repoRoot);
9244
- 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";
9245
9936
  const metadata = {
9246
9937
  kind: "handoff",
9247
- project: normalizeOptionalMetadata(options.project) ?? repoName,
9938
+ project: normalizeOptionalMetadata2(options.project) ?? repoName,
9248
9939
  sourceAgentClient: options.sourceAgentClient ?? "codex",
9249
9940
  status: "active",
9250
9941
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
9251
- topic: normalizeOptionalMetadata(options.topic)
9942
+ topic: handoffTopicForBranch(topicBranch, { timestamped: options.timestamped, topic: options.topic })
9252
9943
  };
9253
9944
  const bodyText = [
9254
9945
  `repo: ${repoName}`,
@@ -9301,7 +9992,7 @@ function formatBlock(value, emptyValue) {
9301
9992
  // src/update-check.ts
9302
9993
  var import_node_child_process2 = require("node:child_process");
9303
9994
  var import_promises6 = require("node:fs/promises");
9304
- var import_node_path6 = require("node:path");
9995
+ var import_node_path7 = require("node:path");
9305
9996
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
9306
9997
  var NPM_LATEST_URL = "https://registry.npmjs.org/threadnote/latest";
9307
9998
  var FETCH_TIMEOUT_MS = 3e3;
@@ -9380,7 +10071,7 @@ async function readUpdateCache(cachePath) {
9380
10071
  }
9381
10072
  async function writeUpdateCache(cachePath, contents) {
9382
10073
  try {
9383
- 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 });
9384
10075
  await (0, import_promises6.writeFile)(cachePath, `${JSON.stringify(contents)}
9385
10076
  `, { encoding: "utf8", mode: 384 });
9386
10077
  } catch {
@@ -9404,14 +10095,14 @@ async function fetchLatestVersion() {
9404
10095
 
9405
10096
  // src/version.ts
9406
10097
  var import_node_fs4 = require("node:fs");
9407
- var import_node_path7 = require("node:path");
10098
+ var import_node_path8 = require("node:path");
9408
10099
  var cachedVersion;
9409
10100
  function getThreadnoteVersion() {
9410
10101
  if (cachedVersion !== void 0) {
9411
10102
  return cachedVersion;
9412
10103
  }
9413
10104
  try {
9414
- const packageJsonPath = (0, import_node_path7.join)(__dirname, "..", "package.json");
10105
+ const packageJsonPath = (0, import_node_path8.join)(__dirname, "..", "package.json");
9415
10106
  const parsed = JSON.parse((0, import_node_fs4.readFileSync)(packageJsonPath, "utf8"));
9416
10107
  cachedVersion = typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : "unknown";
9417
10108
  } catch {
@@ -9470,7 +10161,7 @@ async function runClaudeHooksInstall(options) {
9470
10161
  console.log("\nRe-run with --apply to actually modify the file.");
9471
10162
  return;
9472
10163
  }
9473
- 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 });
9474
10165
  const serialized = `${JSON.stringify(next, void 0, 2)}
9475
10166
  `;
9476
10167
  await (0, import_promises7.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
@@ -9570,7 +10261,7 @@ async function hasManagedClaudeHooks() {
9570
10261
  async function runPreCompactHook(config, options = {}) {
9571
10262
  try {
9572
10263
  const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
9573
- const project = repoRoot ? (0, import_node_path8.basename)(repoRoot) : "general";
10264
+ const project = repoRoot ? (0, import_node_path9.basename)(repoRoot) : "general";
9574
10265
  await runHandoff(config, {
9575
10266
  blockers: "- none recorded",
9576
10267
  dryRun: options.dryRun === true,
@@ -9594,7 +10285,7 @@ async function runSessionStartHook(config, options = {}) {
9594
10285
  if (!repoRoot) {
9595
10286
  return;
9596
10287
  }
9597
- const project = (0, import_node_path8.basename)(repoRoot);
10288
+ const project = (0, import_node_path9.basename)(repoRoot);
9598
10289
  await emitUpdateBannerIfOutdated(config);
9599
10290
  process.stdout.write(`## Threadnote \u2014 latest context for ${project}
9600
10291
 
@@ -9603,7 +10294,8 @@ async function runSessionStartHook(config, options = {}) {
9603
10294
  dryRun: options.dryRun === true,
9604
10295
  inferScope: true,
9605
10296
  nodeLimit: "5",
9606
- 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`
9607
10299
  });
9608
10300
  } catch (err) {
9609
10301
  process.stderr.write(
@@ -9615,7 +10307,7 @@ async function runSessionStartHook(config, options = {}) {
9615
10307
  async function emitUpdateBannerIfOutdated(config) {
9616
10308
  try {
9617
10309
  const result = await checkForThreadnoteUpdate({
9618
- cachePath: (0, import_node_path8.join)(config.agentContextHome, ".update-state.json"),
10310
+ cachePath: (0, import_node_path9.join)(config.agentContextHome, ".update-state.json"),
9619
10311
  currentVersion: getThreadnoteVersion()
9620
10312
  });
9621
10313
  if (!result || !result.outdated) {
@@ -9641,12 +10333,12 @@ async function emitUpdateBannerIfOutdated(config) {
9641
10333
 
9642
10334
  // src/seeding.ts
9643
10335
  var import_promises8 = require("node:fs/promises");
9644
- var import_node_path9 = require("node:path");
10336
+ var import_node_path10 = require("node:path");
9645
10337
  async function runSeed(config, options) {
9646
10338
  const manifest = await readSeedManifest(config.manifestPath);
9647
10339
  const ignorePatterns = await loadIgnorePatterns();
9648
10340
  const ov = await openVikingCliForMode(options.dryRun === true);
9649
- 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);
9650
10342
  const state = options.force === true ? { files: {}, version: 1 } : await readSeedState(statePath);
9651
10343
  const projects = filterProjects(manifest.projects, options.only);
9652
10344
  let importedCount = 0;
@@ -9726,13 +10418,13 @@ async function readSeedState(path) {
9726
10418
  }
9727
10419
  }
9728
10420
  async function writeSeedState(path, state) {
9729
- await ensureDirectory((0, import_node_path9.dirname)(path), false);
10421
+ await ensureDirectory((0, import_node_path10.dirname)(path), false);
9730
10422
  await (0, import_promises8.writeFile)(path, `${JSON.stringify(state, void 0, 2)}
9731
10423
  `, { encoding: "utf8", mode: 384 });
9732
10424
  }
9733
10425
  async function runInitManifest(config, options) {
9734
10426
  const manifestPath = expandPath(
9735
- 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)
9736
10428
  );
9737
10429
  const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
9738
10430
  const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
@@ -9775,7 +10467,7 @@ async function runInitManifest(config, options) {
9775
10467
  console.log(output2.trimEnd());
9776
10468
  return;
9777
10469
  }
9778
- await ensureDirectory((0, import_node_path9.dirname)(manifestPath), false);
10470
+ await ensureDirectory((0, import_node_path10.dirname)(manifestPath), false);
9779
10471
  await (0, import_promises8.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
9780
10472
  await (0, import_promises8.chmod)(manifestPath, 384);
9781
10473
  console.log(`Wrote manifest: ${manifestPath}`);
@@ -9813,7 +10505,7 @@ async function projectIdentity(path) {
9813
10505
  }
9814
10506
  }
9815
10507
  function projectManifestForRepo(repoRoot, existingProjects) {
9816
- const baseName = uriSegment((0, import_node_path9.basename)(repoRoot));
10508
+ const baseName = uriSegment((0, import_node_path10.basename)(repoRoot));
9817
10509
  const usedNames = new Set(existingProjects.map((project) => project.name));
9818
10510
  const usedUris = new Set(existingProjects.map((project) => project.uri));
9819
10511
  let name = baseName;
@@ -9835,7 +10527,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
9835
10527
  for (const pattern of project.seed) {
9836
10528
  const files = await resolveProjectPattern(projectRoot, pattern);
9837
10529
  for (const filePath of files) {
9838
- const relativePath = toPosixPath((0, import_node_path9.relative)(projectRoot, filePath));
10530
+ const relativePath = toPosixPath((0, import_node_path10.relative)(projectRoot, filePath));
9839
10531
  if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
9840
10532
  continue;
9841
10533
  }
@@ -9853,17 +10545,17 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
9853
10545
  async function resolveProjectPattern(projectRoot, pattern) {
9854
10546
  const normalizedPattern = toPosixPath(pattern);
9855
10547
  if (!hasGlob(normalizedPattern)) {
9856
- const filePath = (0, import_node_path9.join)(projectRoot, normalizedPattern);
10548
+ const filePath = (0, import_node_path10.join)(projectRoot, normalizedPattern);
9857
10549
  return await isFile(filePath) ? [filePath] : [];
9858
10550
  }
9859
10551
  const globBase = getGlobBase(normalizedPattern);
9860
- const basePath = (0, import_node_path9.join)(projectRoot, globBase);
10552
+ const basePath = (0, import_node_path10.join)(projectRoot, globBase);
9861
10553
  if (!await exists(basePath)) {
9862
10554
  return [];
9863
10555
  }
9864
10556
  const regex = globToRegExp(normalizedPattern);
9865
10557
  const files = await walkFiles(basePath);
9866
- 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))));
9867
10559
  }
9868
10560
  async function prepareSeedFile(config, candidate, dryRun) {
9869
10561
  const content = await (0, import_promises8.readFile)(candidate.filePath, "utf8");
@@ -9878,12 +10570,12 @@ async function prepareSeedFile(config, candidate, dryRun) {
9878
10570
  if (redactedContent === content) {
9879
10571
  return candidate.filePath;
9880
10572
  }
9881
- 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);
9882
10574
  if (dryRun) {
9883
10575
  console.log(`Would write redacted copy: ${redactedPath}`);
9884
10576
  return redactedPath;
9885
10577
  }
9886
- await ensureDirectory((0, import_node_path9.dirname)(redactedPath), false);
10578
+ await ensureDirectory((0, import_node_path10.dirname)(redactedPath), false);
9887
10579
  await (0, import_promises8.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
9888
10580
  await (0, import_promises8.chmod)(redactedPath, 384);
9889
10581
  return redactedPath;
@@ -9941,10 +10633,10 @@ async function resolveAbsolutePattern(pattern) {
9941
10633
  return files.filter((filePath) => regex.test(toPosixPath(filePath)));
9942
10634
  }
9943
10635
  function skillResourceUri(skill) {
9944
- 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`;
9945
10637
  }
9946
10638
  async function loadIgnorePatterns() {
9947
- 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");
9948
10640
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
9949
10641
  }
9950
10642
  function matchesIgnore(relativePath, patterns) {
@@ -10019,7 +10711,7 @@ var import_node_child_process3 = require("node:child_process");
10019
10711
  var import_node_fs6 = require("node:fs");
10020
10712
  var import_promises11 = require("node:fs/promises");
10021
10713
  var import_node_os6 = require("node:os");
10022
- var import_node_path11 = require("node:path");
10714
+ var import_node_path12 = require("node:path");
10023
10715
  var import_node_process2 = require("node:process");
10024
10716
  var import_promises12 = require("node:readline/promises");
10025
10717
 
@@ -10027,7 +10719,7 @@ var import_promises12 = require("node:readline/promises");
10027
10719
  var import_node_fs5 = require("node:fs");
10028
10720
  var import_promises9 = require("node:fs/promises");
10029
10721
  var import_node_os5 = require("node:os");
10030
- var import_node_path10 = require("node:path");
10722
+ var import_node_path11 = require("node:path");
10031
10723
  var import_promises10 = require("node:readline/promises");
10032
10724
  var import_node_process = require("node:process");
10033
10725
  var NPM_PACKAGE_NAME = "threadnote";
@@ -10242,7 +10934,7 @@ async function waitForOpenVikingHealthy(config, timeoutMs) {
10242
10934
  return isOpenVikingHealthy(config);
10243
10935
  }
10244
10936
  function launchAgentPlistPath() {
10245
- 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");
10246
10938
  }
10247
10939
  async function isLaunchAgentInstalled() {
10248
10940
  if (process.platform !== "darwin") {
@@ -10278,7 +10970,7 @@ async function getUpdateInfo(config, options) {
10278
10970
  };
10279
10971
  }
10280
10972
  async function currentPackageVersion() {
10281
- 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");
10282
10974
  const parsed = JSON.parse(rawPackage);
10283
10975
  if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
10284
10976
  throw new Error("Could not read current threadnote package version.");
@@ -10332,7 +11024,7 @@ async function writeUpdateCache2(config, cache) {
10332
11024
  `, { encoding: "utf8", mode: 384 });
10333
11025
  }
10334
11026
  function updateCachePath(config) {
10335
- return (0, import_node_path10.join)(config.agentContextHome, "update-check.json");
11027
+ return (0, import_node_path11.join)(config.agentContextHome, "update-check.json");
10336
11028
  }
10337
11029
  async function runApplicablePostUpdateMigrations(config, options) {
10338
11030
  const state = await readPostUpdateState(config);
@@ -10396,7 +11088,7 @@ async function applicablePostUpdateMigrations(config, options) {
10396
11088
  return applicable;
10397
11089
  }
10398
11090
  async function readPostUpdateMigrations() {
10399
- 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));
10400
11092
  if (!raw) {
10401
11093
  return [];
10402
11094
  }
@@ -10474,7 +11166,7 @@ async function writePostUpdateState(config, state) {
10474
11166
  `, { encoding: "utf8", mode: 384 });
10475
11167
  }
10476
11168
  function postUpdateStatePath(config) {
10477
- 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);
10478
11170
  }
10479
11171
  async function resolveUpdateRuntime(runtime) {
10480
11172
  if (runtime !== "auto") {
@@ -10504,22 +11196,14 @@ async function runtimeThreadnoteBin(runtime) {
10504
11196
  if (runtime === "npm") {
10505
11197
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
10506
11198
  const prefix = result.stdout.trim();
10507
- 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;
10508
11200
  }
10509
11201
  if (runtime === "bun") {
10510
11202
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
10511
11203
  const binDir = result.stdout.trim();
10512
- return binDir ? (0, import_node_path10.join)(binDir, NPM_PACKAGE_NAME) : void 0;
10513
- }
10514
- 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);
10515
- }
10516
- async function isExecutable(path) {
10517
- try {
10518
- await (0, import_promises9.access)(path, import_node_fs5.constants.X_OK);
10519
- return true;
10520
- } catch (_err) {
10521
- return false;
11204
+ return binDir ? (0, import_node_path11.join)(binDir, NPM_PACKAGE_NAME) : void 0;
10522
11205
  }
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);
10523
11207
  }
10524
11208
  function updatePackageCommand(runtime, registry) {
10525
11209
  if (runtime === "npm") {
@@ -10564,7 +11248,7 @@ async function runDoctor(config, options) {
10564
11248
  checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
10565
11249
  checks.push(await commandCheck("node", ["--version"]));
10566
11250
  checks.push(await commandCheck("python3", ["--version"]));
10567
- checks.push(await commandCheck("openviking-server", ["--help"]));
11251
+ checks.push(await openVikingServerCheck());
10568
11252
  checks.push(await firstCommandCheck("openviking cli", ["ov", "openviking"], ["--help"]));
10569
11253
  checks.push(await localEmbeddingCheck());
10570
11254
  checks.push(await pythonSystemCertificatesCheck());
@@ -10574,9 +11258,9 @@ async function runDoctor(config, options) {
10574
11258
  checks.push(await commandShimCheck());
10575
11259
  checks.push(...await userAgentInstructionsChecks());
10576
11260
  checks.push(await manifestCheck(config.manifestPath));
10577
- checks.push(await fileCheck((0, import_node_path11.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
10578
- checks.push(await fileCheck((0, import_node_path11.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
10579
- 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"));
10580
11264
  checks.push(await healthCheck(config));
10581
11265
  for (const check of checks) {
10582
11266
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -10593,12 +11277,13 @@ async function runInstall(config, options) {
10593
11277
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
10594
11278
  const dryRun = options.dryRun === true;
10595
11279
  await ensureDirectory(config.agentContextHome, dryRun);
10596
- await ensureDirectory((0, import_node_path11.join)(config.agentContextHome, "logs"), dryRun);
10597
- await ensureDirectory((0, import_node_path11.join)(config.agentContextHome, "redacted"), dryRun);
10598
- 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);
10599
11283
  await installCommandShim(dryRun);
10600
11284
  await installUserAgentInstructions(dryRun);
10601
- const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
11285
+ const serverPath = await findOpenVikingServer();
11286
+ let serverInstallRan = false;
10602
11287
  if (serverPath) {
10603
11288
  console.log(`OpenViking server already installed: ${serverPath}`);
10604
11289
  const localEmbeddingMissing = await hasLocalEmbeddingDependency(serverPath) === false;
@@ -10606,6 +11291,7 @@ async function runInstall(config, options) {
10606
11291
  if (options.force === true) {
10607
11292
  console.log(`Reinstalling OpenViking at pinned version ${config.openVikingVersion} (--force).`);
10608
11293
  await runInstallCommands(config, options.packageManager, true, dryRun);
11294
+ serverInstallRan = true;
10609
11295
  } else if (localEmbeddingMissing) {
10610
11296
  const repairReasons = [];
10611
11297
  repairReasons.push("local embedding extra is missing");
@@ -10614,6 +11300,7 @@ async function runInstall(config, options) {
10614
11300
  }
10615
11301
  console.log(`OpenViking install needs repair: ${repairReasons.join("; ")}.`);
10616
11302
  await runInstallCommands(config, options.packageManager, true, dryRun);
11303
+ serverInstallRan = true;
10617
11304
  } else if (pythonSystemCertificatesMissing) {
10618
11305
  console.log("OpenViking install needs repair: Python system certificate bridge is missing.");
10619
11306
  const installCommand = await getPythonSystemCertificatesInstallCommand(serverPath);
@@ -10621,20 +11308,25 @@ async function runInstall(config, options) {
10621
11308
  }
10622
11309
  } else {
10623
11310
  await runInstallCommands(config, options.packageManager, false, dryRun);
11311
+ serverInstallRan = true;
11312
+ }
11313
+ const resolvedServerPath = serverInstallRan ? await findOpenVikingServer() : serverPath;
11314
+ if (resolvedServerPath && !dryRun) {
11315
+ await maybePrintOpenVikingPathHint(resolvedServerPath);
10624
11316
  }
10625
11317
  await writeTemplateIfMissing({
10626
11318
  config,
10627
- destinationPath: (0, import_node_path11.join)(config.agentContextHome, "ov.conf"),
11319
+ destinationPath: (0, import_node_path12.join)(config.agentContextHome, "ov.conf"),
10628
11320
  dryRun,
10629
11321
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
10630
- 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")
10631
11323
  });
10632
11324
  await writeTemplateIfMissing({
10633
11325
  config,
10634
- destinationPath: (0, import_node_path11.join)(config.agentContextHome, "ovcli.conf"),
11326
+ destinationPath: (0, import_node_path12.join)(config.agentContextHome, "ovcli.conf"),
10635
11327
  dryRun,
10636
11328
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
10637
- 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")
10638
11330
  });
10639
11331
  if (options.start !== false) {
10640
11332
  const healthy = await repairServerHealth(config, dryRun);
@@ -10688,7 +11380,7 @@ async function runUninstall(config, options) {
10688
11380
  }
10689
11381
  console.log("Uninstalling local Threadnote setup.");
10690
11382
  await runStop(config, { dryRun });
10691
- 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);
10692
11384
  await removeLaunchAgent(dryRun);
10693
11385
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
10694
11386
  await removeMcpSnippets(config, dryRun);
@@ -10745,7 +11437,7 @@ async function repairManifest(config, dryRun) {
10745
11437
  console.log(output2.trimEnd());
10746
11438
  return;
10747
11439
  }
10748
- await ensureDirectory((0, import_node_path11.dirname)(config.manifestPath), false);
11440
+ await ensureDirectory((0, import_node_path12.dirname)(config.manifestPath), false);
10749
11441
  const currentContent = await readFileIfExists(config.manifestPath);
10750
11442
  if (currentContent !== void 0) {
10751
11443
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
@@ -10757,6 +11449,64 @@ async function repairManifest(config, dryRun) {
10757
11449
  await (0, import_promises11.chmod)(config.manifestPath, 384);
10758
11450
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
10759
11451
  }
11452
+ async function findOpenVikingServer() {
11453
+ const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
11454
+ if (onPath) {
11455
+ return onPath;
11456
+ }
11457
+ for (const candidateDir of await openVikingServerCandidateDirs()) {
11458
+ const candidate = (0, import_node_path12.join)(candidateDir, OPENVIKING_SERVER_COMMAND);
11459
+ if (await isExecutable(candidate)) {
11460
+ return candidate;
11461
+ }
11462
+ }
11463
+ return void 0;
11464
+ }
11465
+ var candidateDirsPromise;
11466
+ async function openVikingServerCandidateDirs() {
11467
+ if (!candidateDirsPromise) {
11468
+ candidateDirsPromise = computeOpenVikingServerCandidateDirs();
11469
+ }
11470
+ return candidateDirsPromise;
11471
+ }
11472
+ async function computeOpenVikingServerCandidateDirs() {
11473
+ const dirs = [];
11474
+ const uv = await findExecutable(["uv"]);
11475
+ if (uv) {
11476
+ const result = await runCommand(uv, ["tool", "dir", "--bin"], { allowFailure: true });
11477
+ if (result.exitCode === 0) {
11478
+ const dir = result.stdout.trim();
11479
+ if (dir) {
11480
+ dirs.push(dir);
11481
+ }
11482
+ }
11483
+ }
11484
+ if (process.env.UV_TOOL_BIN_DIR) {
11485
+ dirs.push(process.env.UV_TOOL_BIN_DIR);
11486
+ }
11487
+ dirs.push(expandPath("~/.local/bin"));
11488
+ return Array.from(new Set(dirs));
11489
+ }
11490
+ async function requireOpenVikingServer() {
11491
+ const resolved = await findOpenVikingServer();
11492
+ if (!resolved) {
11493
+ throw new Error(
11494
+ `${OPENVIKING_SERVER_COMMAND} was not found in PATH, uv tool bin dir, $UV_TOOL_BIN_DIR, or ~/.local/bin. Run \`threadnote install\` first.`
11495
+ );
11496
+ }
11497
+ return resolved;
11498
+ }
11499
+ async function maybePrintOpenVikingPathHint(serverPath) {
11500
+ const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
11501
+ if (onPath) {
11502
+ return;
11503
+ }
11504
+ const binDir = (0, import_node_path12.dirname)(serverPath);
11505
+ const rcHint = suggestedShellRc(process.env.SHELL, (0, import_node_os6.platform)());
11506
+ console.log(
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.`
11508
+ );
11509
+ }
10760
11510
  async function repairServerHealth(config, dryRun) {
10761
11511
  const existingHealth = await readOpenVikingHealthIfAvailable(config, 800);
10762
11512
  if (existingHealth) {
@@ -10777,7 +11527,7 @@ async function runStart(config, options) {
10777
11527
  await installLaunchAgent(config, options.dryRun === true);
10778
11528
  return;
10779
11529
  }
10780
- const server = options.dryRun === true ? await findExecutable([OPENVIKING_SERVER_COMMAND]) ?? OPENVIKING_SERVER_COMMAND : await requiredExecutable(OPENVIKING_SERVER_COMMAND);
11530
+ const server = options.dryRun === true ? await findOpenVikingServer() ?? OPENVIKING_SERVER_COMMAND : await requireOpenVikingServer();
10781
11531
  const args = openVikingServerArgs(config);
10782
11532
  if (options.dryRun === true) {
10783
11533
  console.log(formatShellCommand(server, args));
@@ -10795,7 +11545,7 @@ async function runStart(config, options) {
10795
11545
  );
10796
11546
  }
10797
11547
  const logPath = openVikingLogPath(config);
10798
- await ensureDirectory((0, import_node_path11.dirname)(logPath), false);
11548
+ await ensureDirectory((0, import_node_path12.dirname)(logPath), false);
10799
11549
  if (options.foreground === true) {
10800
11550
  const result = await runInteractive(server, args);
10801
11551
  process.exitCode = result;
@@ -10804,7 +11554,7 @@ async function runStart(config, options) {
10804
11554
  const logFd = (0, import_node_fs6.openSync)(logPath, "a");
10805
11555
  const child = spawnDetachedServerWithLog(server, args, logFd);
10806
11556
  child.unref();
10807
- 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}
10808
11558
  `, "utf8");
10809
11559
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
10810
11560
  if (health) {
@@ -10837,7 +11587,7 @@ async function runStop(config, options) {
10837
11587
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
10838
11588
  }
10839
11589
  }
10840
- 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");
10841
11591
  const pidText = await readFileIfExists(pidPath);
10842
11592
  if (!pidText) {
10843
11593
  console.log("No pid file found for detached OpenViking server.");
@@ -10871,6 +11621,21 @@ async function commandCheck(name, args) {
10871
11621
  detail: firstLine(result.stdout || result.stderr) || executable
10872
11622
  };
10873
11623
  }
11624
+ async function openVikingServerCheck() {
11625
+ const name = OPENVIKING_SERVER_COMMAND;
11626
+ const executable = await findOpenVikingServer();
11627
+ if (!executable) {
11628
+ return { name, status: "fail", detail: "missing; install will fetch it via uv or pipx" };
11629
+ }
11630
+ const result = await runCommand(executable, ["--help"], { allowFailure: true });
11631
+ const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
11632
+ const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path12.dirname)(executable)} to PATH)`;
11633
+ return {
11634
+ name,
11635
+ status: result.exitCode === 0 ? "ok" : "warn",
11636
+ detail: result.exitCode === 0 ? detail : firstLine(result.stderr || result.stdout) || detail
11637
+ };
11638
+ }
10874
11639
  async function commandPresenceCheck(name, args) {
10875
11640
  const executable = await findExecutable([name]);
10876
11641
  if (!executable) {
@@ -10899,7 +11664,7 @@ async function firstCommandCheck(name, commands, args) {
10899
11664
  return { name, status: "fail", detail: `none found: ${commands.join(", ")}` };
10900
11665
  }
10901
11666
  async function localEmbeddingCheck() {
10902
- const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
11667
+ const serverPath = await findOpenVikingServer();
10903
11668
  if (!serverPath) {
10904
11669
  return { name: "local embedding extra", status: "warn", detail: "openviking-server missing" };
10905
11670
  }
@@ -10918,7 +11683,7 @@ async function localEmbeddingCheck() {
10918
11683
  };
10919
11684
  }
10920
11685
  async function pythonSystemCertificatesCheck() {
10921
- const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
11686
+ const serverPath = await findOpenVikingServer();
10922
11687
  if (!serverPath) {
10923
11688
  return { name: "python system certs", status: "warn", detail: "openviking-server missing" };
10924
11689
  }
@@ -10933,7 +11698,7 @@ async function pythonSystemCertificatesCheck() {
10933
11698
  };
10934
11699
  }
10935
11700
  async function commandShimCheck() {
10936
- 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");
10937
11702
  const content = await readFileIfExists(shimPath);
10938
11703
  if (content === void 0) {
10939
11704
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -11003,7 +11768,7 @@ async function siblingPythonForExecutable(executablePath) {
11003
11768
  } catch (_err) {
11004
11769
  return void 0;
11005
11770
  }
11006
- 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");
11007
11772
  return await exists(pythonPath) ? pythonPath : void 0;
11008
11773
  }
11009
11774
  async function manifestCheck(path) {
@@ -11099,6 +11864,7 @@ async function installUv() {
11099
11864
  console.log(result.stdout.trim());
11100
11865
  }
11101
11866
  if (await findExecutable(["uv"])) {
11867
+ candidateDirsPromise = void 0;
11102
11868
  return true;
11103
11869
  }
11104
11870
  } else {
@@ -11115,6 +11881,7 @@ async function installUv() {
11115
11881
  console.log(result.stdout.trim());
11116
11882
  }
11117
11883
  if (await findExecutable(["uv"])) {
11884
+ candidateDirsPromise = void 0;
11118
11885
  return true;
11119
11886
  }
11120
11887
  console.warn(
@@ -11221,14 +11988,14 @@ async function writeTemplateIfMissing(options) {
11221
11988
  console.log(`Would write ${options.destinationPath}`);
11222
11989
  return;
11223
11990
  }
11224
- await ensureDirectory((0, import_node_path11.dirname)(options.destinationPath), false);
11991
+ await ensureDirectory((0, import_node_path12.dirname)(options.destinationPath), false);
11225
11992
  await (0, import_promises11.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
11226
11993
  await (0, import_promises11.chmod)(options.destinationPath, 384);
11227
11994
  console.log(`Wrote ${options.destinationPath}`);
11228
11995
  }
11229
11996
  async function installCommandShim(dryRun) {
11230
11997
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
11231
- const shimPath = (0, import_node_path11.join)(binDir, "threadnote");
11998
+ const shimPath = (0, import_node_path12.join)(binDir, "threadnote");
11232
11999
  const existingContent = await readFileIfExists(shimPath);
11233
12000
  if (existingContent && !isManagedCommandShim(existingContent)) {
11234
12001
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -11249,7 +12016,7 @@ async function installCommandShim(dryRun) {
11249
12016
  console.log(`Wrote command shim: ${shimPath}`);
11250
12017
  }
11251
12018
  async function removeCommandShim(dryRun) {
11252
- 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");
11253
12020
  const content = await readFileIfExists(shimPath);
11254
12021
  if (content === void 0) {
11255
12022
  console.log(`Already absent: ${shimPath}`);
@@ -11283,7 +12050,7 @@ async function installUserAgentInstructions(dryRun) {
11283
12050
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
11284
12051
  continue;
11285
12052
  }
11286
- await ensureDirectory((0, import_node_path11.dirname)(targetPath), false);
12053
+ await ensureDirectory((0, import_node_path12.dirname)(targetPath), false);
11287
12054
  await (0, import_promises11.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
11288
12055
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
11289
12056
  }
@@ -11341,7 +12108,7 @@ async function renderUserAgentInstructions(target) {
11341
12108
  ].join("\n");
11342
12109
  }
11343
12110
  async function renderUserAgentInstructionsBlock() {
11344
- 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();
11345
12112
  return `${USER_INSTRUCTIONS_START_MARKER}
11346
12113
  ${instructions}
11347
12114
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -11419,18 +12186,28 @@ async function installLaunchAgent(config, dryRun) {
11419
12186
  if ((0, import_node_os6.platform)() !== "darwin") {
11420
12187
  throw new Error("launchd autostart is only supported on macOS.");
11421
12188
  }
11422
- const source = (0, import_node_path11.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
12189
+ const resolvedServer = await findOpenVikingServer();
12190
+ if (!resolvedServer && !dryRun) {
12191
+ throw new Error(
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.`
12193
+ );
12194
+ }
12195
+ const source = (0, import_node_path12.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
11423
12196
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
11424
- const rendered = renderTemplate(await (0, import_promises11.readFile)(source, "utf8"), config);
12197
+ const rendered = renderTemplate(await (0, import_promises11.readFile)(source, "utf8"), config, {
12198
+ OPENVIKING_SERVER_PATH: resolvedServer ?? OPENVIKING_SERVER_COMMAND
12199
+ });
11425
12200
  if (dryRun) {
12201
+ const resolutionDetail = resolvedServer ?? `<not found; would use bare \`${OPENVIKING_SERVER_COMMAND}\`>`;
12202
+ console.log(`Resolved openviking-server: ${resolutionDetail}`);
11426
12203
  console.log(`Would write ${destination}`);
11427
12204
  console.log(`Would run: launchctl unload ${destination}`);
11428
12205
  console.log(`Would run: launchctl load ${destination}`);
11429
12206
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
11430
12207
  return;
11431
12208
  }
11432
- await ensureDirectory((0, import_node_path11.dirname)(destination), false);
11433
- 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);
11434
12211
  await (0, import_promises11.writeFile)(destination, rendered, "utf8");
11435
12212
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
11436
12213
  await maybeRun(false, "launchctl", ["load", destination]);
@@ -11494,7 +12271,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
11494
12271
  if (typeof parsed.default_user !== "string") {
11495
12272
  return false;
11496
12273
  }
11497
- 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")) {
11498
12275
  return false;
11499
12276
  }
11500
12277
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -11614,16 +12391,19 @@ async function main() {
11614
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) => {
11615
12392
  await runMigrateLifecycle(getRuntimeConfig(program2), options);
11616
12393
  });
11617
- 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) => {
11618
12395
  await runRecall(getRuntimeConfig(program2), options);
11619
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
+ });
11620
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) => {
11621
12401
  await runRead(getRuntimeConfig(program2), uri, options);
11622
12402
  });
11623
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) => {
11624
12404
  await runList(getRuntimeConfig(program2), uri, options);
11625
12405
  });
11626
- 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) => {
11627
12407
  await runHandoff(getRuntimeConfig(program2), options);
11628
12408
  });
11629
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) => {