threadnote 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3498,6 +3498,12 @@ var USER_AGENT_INSTRUCTION_TARGETS = [
3498
3498
  path: "~/.copilot/instructions/threadnote.instructions.md"
3499
3499
  }
3500
3500
  ];
3501
+ var CLAUDE_SETTINGS_PATH = "~/.claude/settings.json";
3502
+ var THREADNOTE_HOOK_MARKER = "_threadnote";
3503
+ var THREADNOTE_HOOK_MARKER_VALUE = "managed";
3504
+ var HOOK_PRE_COMPACT_COMMAND = "threadnote pre-compact-hook";
3505
+ var HOOK_SESSION_START_COMMAND = "threadnote session-start-hook";
3506
+ var HOOK_AUTO_PRECOMPACT_TOPIC = "auto-precompact";
3501
3507
  var DEFAULT_SEED_PATTERNS = [
3502
3508
  "AGENTS.md",
3503
3509
  "CLAUDE.md",
@@ -3515,6 +3521,16 @@ var DEFAULT_SEED_PATTERNS = [
3515
3521
  "docs/**/*.md"
3516
3522
  ];
3517
3523
 
3524
+ // src/hooks.ts
3525
+ var import_promises5 = require("node:fs/promises");
3526
+ var import_node_path5 = require("node:path");
3527
+
3528
+ // src/mcp.ts
3529
+ var import_node_fs2 = require("node:fs");
3530
+ var import_promises2 = require("node:fs/promises");
3531
+ var import_node_path2 = require("node:path");
3532
+ var import_node_os2 = require("node:os");
3533
+
3518
3534
  // src/utils.ts
3519
3535
  var import_node_child_process = require("node:child_process");
3520
3536
  var import_node_crypto = require("node:crypto");
@@ -3986,10 +4002,6 @@ function errorMessage(err) {
3986
4002
  }
3987
4003
 
3988
4004
  // src/mcp.ts
3989
- var import_node_fs2 = require("node:fs");
3990
- var import_promises2 = require("node:fs/promises");
3991
- var import_node_path2 = require("node:path");
3992
- var import_node_os2 = require("node:os");
3993
4005
  async function runMcpInstall(config, agent, options) {
3994
4006
  const name = options.name ?? OPENVIKING_MCP_NAME;
3995
4007
  const url = options.url ?? `http://${config.host}:${config.port}/mcp`;
@@ -4470,50 +4482,6 @@ function existsSyncDirectory(path) {
4470
4482
  }
4471
4483
  }
4472
4484
 
4473
- // src/runtime.ts
4474
- var import_node_fs3 = require("node:fs");
4475
- var import_node_os3 = require("node:os");
4476
- var import_node_path3 = require("node:path");
4477
- function getRuntimeConfig(program2, manifestOverride) {
4478
- const options = program2.opts();
4479
- const threadnoteHome = expandPath(options.home ?? process.env.THREADNOTE_HOME ?? "~/.openviking");
4480
- const manifestPath = expandPath(
4481
- manifestOverride ?? options.manifest ?? process.env.THREADNOTE_MANIFEST ?? defaultManifestPath(threadnoteHome)
4482
- );
4483
- return {
4484
- account: process.env.THREADNOTE_ACCOUNT ?? DEFAULT_ACCOUNT,
4485
- agentContextHome: threadnoteHome,
4486
- agentId: process.env.THREADNOTE_AGENT_ID ?? DEFAULT_AGENT_ID,
4487
- host: options.host ?? process.env.THREADNOTE_HOST ?? DEFAULT_HOST,
4488
- manifestPath,
4489
- openVikingVersion: process.env.THREADNOTE_OPENVIKING_VERSION ?? DEFAULT_OPENVIKING_VERSION,
4490
- port: options.port ?? parsePort(process.env.THREADNOTE_PORT ?? String(DEFAULT_PORT)),
4491
- user: process.env.THREADNOTE_USER ?? (0, import_node_os3.userInfo)().username
4492
- };
4493
- }
4494
- function defaultManifestPath(agentContextHome) {
4495
- const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
4496
- return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
4497
- }
4498
- function builtInExampleManifestPath() {
4499
- return (0, import_node_path3.join)(toolRoot(), "config", "seed-manifest.example.yaml");
4500
- }
4501
- function openVikingHealthUrl(config) {
4502
- return `http://${config.host}:${config.port}/health`;
4503
- }
4504
- function openVikingLogPath(config) {
4505
- return (0, import_node_path3.join)(config.agentContextHome, "logs", "server.log");
4506
- }
4507
- function openVikingServerArgs(config) {
4508
- return ["--config", (0, import_node_path3.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
4509
- }
4510
- function withIdentity(config, args) {
4511
- return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
4512
- }
4513
- function renderTemplate(template, config) {
4514
- 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);
4515
- }
4516
-
4517
4485
  // src/memory.ts
4518
4486
  var import_promises4 = require("node:fs/promises");
4519
4487
  var import_node_path4 = require("node:path");
@@ -7205,6 +7173,50 @@ function readStringArray(object, key) {
7205
7173
  return value;
7206
7174
  }
7207
7175
 
7176
+ // src/runtime.ts
7177
+ var import_node_fs3 = require("node:fs");
7178
+ var import_node_os3 = require("node:os");
7179
+ var import_node_path3 = require("node:path");
7180
+ function getRuntimeConfig(program2, manifestOverride) {
7181
+ const options = program2.opts();
7182
+ const threadnoteHome = expandPath(options.home ?? process.env.THREADNOTE_HOME ?? "~/.openviking");
7183
+ const manifestPath = expandPath(
7184
+ manifestOverride ?? options.manifest ?? process.env.THREADNOTE_MANIFEST ?? defaultManifestPath(threadnoteHome)
7185
+ );
7186
+ return {
7187
+ account: process.env.THREADNOTE_ACCOUNT ?? DEFAULT_ACCOUNT,
7188
+ agentContextHome: threadnoteHome,
7189
+ agentId: process.env.THREADNOTE_AGENT_ID ?? DEFAULT_AGENT_ID,
7190
+ host: options.host ?? process.env.THREADNOTE_HOST ?? DEFAULT_HOST,
7191
+ manifestPath,
7192
+ openVikingVersion: process.env.THREADNOTE_OPENVIKING_VERSION ?? DEFAULT_OPENVIKING_VERSION,
7193
+ port: options.port ?? parsePort(process.env.THREADNOTE_PORT ?? String(DEFAULT_PORT)),
7194
+ user: process.env.THREADNOTE_USER ?? (0, import_node_os3.userInfo)().username
7195
+ };
7196
+ }
7197
+ function defaultManifestPath(agentContextHome) {
7198
+ const userManifest = (0, import_node_path3.join)(agentContextHome, USER_MANIFEST_NAME);
7199
+ return (0, import_node_fs3.existsSync)(userManifest) ? userManifest : builtInExampleManifestPath();
7200
+ }
7201
+ function builtInExampleManifestPath() {
7202
+ return (0, import_node_path3.join)(toolRoot(), "config", "seed-manifest.example.yaml");
7203
+ }
7204
+ function openVikingHealthUrl(config) {
7205
+ return `http://${config.host}:${config.port}/health`;
7206
+ }
7207
+ function openVikingLogPath(config) {
7208
+ return (0, import_node_path3.join)(config.agentContextHome, "logs", "server.log");
7209
+ }
7210
+ function openVikingServerArgs(config) {
7211
+ return ["--config", (0, import_node_path3.join)(config.agentContextHome, "ov.conf"), "--host", config.host, "--port", String(config.port)];
7212
+ }
7213
+ function withIdentity(config, args) {
7214
+ return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
7215
+ }
7216
+ function renderTemplate(template, config) {
7217
+ 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);
7218
+ }
7219
+
7208
7220
  // src/memory.ts
7209
7221
  function parseMemoryKind(value) {
7210
7222
  if (["durable", "handoff", "incident", "preference", "smoke"].includes(value)) {
@@ -7228,12 +7240,16 @@ async function runRemember(config, options) {
7228
7240
  project: normalizeOptionalMetadata(options.project),
7229
7241
  sourceAgentClient: options.sourceAgentClient ?? "codex",
7230
7242
  status: options.status ?? "active",
7231
- supersedes: options.replace,
7232
7243
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7233
7244
  topic: normalizeOptionalMetadata(options.topic)
7234
7245
  };
7235
- const memory = formatMemoryDocument("MEMORY", metadata, text.trim());
7236
- await storeMemory(config, memory, { dryRun: options.dryRun === true, metadata, replaceUri: options.replace });
7246
+ await storeMemory(config, {
7247
+ bodyText: text.trim(),
7248
+ dryRun: options.dryRun === true,
7249
+ metadata,
7250
+ replaceUri: options.replace,
7251
+ title: "MEMORY"
7252
+ });
7237
7253
  }
7238
7254
  async function runMigrateMemories(config, options) {
7239
7255
  const dryRun = options.dryRun === true;
@@ -7406,8 +7422,14 @@ async function runList(config, uri, options) {
7406
7422
  await maybeRun(options.dryRun === true, ov, withIdentity(config, args));
7407
7423
  }
7408
7424
  async function runHandoff(config, options) {
7409
- const { handoff, metadata } = await buildHandoff(options);
7410
- await storeMemory(config, handoff, { dryRun: options.dryRun === true, metadata, replaceUri: options.replace });
7425
+ const { bodyText, metadata } = await buildHandoff(options);
7426
+ await storeMemory(config, {
7427
+ bodyText,
7428
+ dryRun: options.dryRun === true,
7429
+ metadata,
7430
+ replaceUri: options.replace,
7431
+ title: "HANDOFF"
7432
+ });
7411
7433
  }
7412
7434
  async function runArchive(config, uri, options) {
7413
7435
  assertVikingUri(uri);
@@ -7424,12 +7446,12 @@ async function runArchive(config, uri, options) {
7424
7446
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7425
7447
  topic: normalizeOptionalMetadata(options.topic)
7426
7448
  };
7427
- const archiveMemory2 = formatMemoryDocument(
7428
- "MEMORY",
7429
- fallbackMetadata,
7430
- ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n")
7431
- );
7432
- await storeMemory(config, archiveMemory2, { dryRun: true, metadata: fallbackMetadata });
7449
+ await storeMemory(config, {
7450
+ bodyText: ["Archived original Threadnote memory.", "", "<original memory content would be read here>"].join("\n"),
7451
+ dryRun: true,
7452
+ metadata: fallbackMetadata,
7453
+ title: "MEMORY"
7454
+ });
7433
7455
  console.log(formatShellCommand(ov, withIdentity(config, ["rm", uri])));
7434
7456
  return;
7435
7457
  }
@@ -7446,12 +7468,12 @@ async function runArchive(config, uri, options) {
7446
7468
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7447
7469
  topic: normalizeOptionalMetadata(options.topic) ?? inferredMetadata.topic
7448
7470
  };
7449
- const archiveMemory = formatMemoryDocument(
7450
- "MEMORY",
7471
+ await storeMemory(config, {
7472
+ bodyText: ["Archived original Threadnote memory.", "", original].join("\n"),
7473
+ dryRun: false,
7451
7474
  metadata,
7452
- ["Archived original Threadnote memory.", "", original].join("\n")
7453
- );
7454
- await storeMemory(config, archiveMemory, { dryRun: false, metadata });
7475
+ title: "MEMORY"
7476
+ });
7455
7477
  const removedOriginal = await removeVikingResourceWithRetry(ov, config, uri);
7456
7478
  if (removedOriginal) {
7457
7479
  console.log(`Archived original memory: ${uri}`);
@@ -7532,14 +7554,19 @@ async function printExactMemoryMatches(config, ov, query, options) {
7532
7554
  console.log("\nExact durable memory matches:");
7533
7555
  console.log(outputs.join("\n\n"));
7534
7556
  }
7535
- async function storeMemory(config, memory, options) {
7557
+ async function storeMemory(config, options) {
7536
7558
  if (options.replaceUri) {
7537
7559
  assertVikingUri(options.replaceUri);
7538
7560
  }
7539
7561
  const ov = await openVikingCliForMode(options.dryRun);
7540
7562
  const memoryPath = (0, import_node_path4.join)(config.agentContextHome, "last-memory.txt");
7541
- const memoryUri = memoryUriFor(config, memory, options.metadata);
7542
- const writeMode = await memoryWriteMode(ov, config, memoryUri, options.metadata);
7563
+ const candidateMetadata = { ...options.metadata, supersedes: options.replaceUri };
7564
+ const candidateMemory = formatMemoryDocument(options.title, candidateMetadata, options.bodyText);
7565
+ const memoryUri = memoryUriFor(config, candidateMemory, candidateMetadata);
7566
+ const isInPlaceUpdate = options.replaceUri !== void 0 && options.replaceUri === memoryUri;
7567
+ const finalMetadata = isInPlaceUpdate ? { ...options.metadata, supersedes: void 0 } : candidateMetadata;
7568
+ const memory = isInPlaceUpdate ? formatMemoryDocument(options.title, finalMetadata, options.bodyText) : candidateMemory;
7569
+ const writeMode = await memoryWriteMode(ov, config, memoryUri, finalMetadata);
7543
7570
  if (options.dryRun) {
7544
7571
  console.log(memory);
7545
7572
  console.log("\nWould run:");
@@ -7559,17 +7586,17 @@ async function storeMemory(config, memory, options) {
7559
7586
  ])
7560
7587
  )
7561
7588
  );
7562
- if (options.replaceUri && options.replaceUri !== memoryUri) {
7589
+ if (options.replaceUri && !isInPlaceUpdate) {
7563
7590
  console.log(formatShellCommand(ov, withIdentity(config, ["rm", options.replaceUri])));
7564
7591
  }
7565
7592
  return;
7566
7593
  }
7567
7594
  await (0, import_promises4.writeFile)(memoryPath, memory, { encoding: "utf8", mode: 384 });
7568
7595
  await (0, import_promises4.chmod)(memoryPath, 384);
7569
- await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, options.metadata));
7596
+ await ensureMemoryDirectory(ov, config, memoryDirectoryUri(config, finalMetadata));
7570
7597
  await writeDurableMemoryFile(ov, config, memoryUri, memoryPath, writeMode);
7571
7598
  console.log(`Stored memory: ${memoryUri}`);
7572
- if (options.replaceUri && options.replaceUri !== memoryUri) {
7599
+ if (options.replaceUri && !isInPlaceUpdate) {
7573
7600
  const removedReplacedMemory = await removeVikingResourceWithRetry(ov, config, options.replaceUri);
7574
7601
  if (removedReplacedMemory) {
7575
7602
  console.log(`Forgot replaced memory: ${options.replaceUri}`);
@@ -7578,7 +7605,7 @@ async function storeMemory(config, memory, options) {
7578
7605
  `Replacement stored, but the superseded memory is still processing. Retry later: threadnote forget ${options.replaceUri}`
7579
7606
  );
7580
7607
  }
7581
- } else if (options.replaceUri === memoryUri) {
7608
+ } else if (isInPlaceUpdate) {
7582
7609
  console.log(`Updated existing memory in place: ${memoryUri}`);
7583
7610
  }
7584
7611
  }
@@ -8031,11 +8058,10 @@ async function buildHandoff(options) {
8031
8058
  project: normalizeOptionalMetadata(options.project) ?? repoName,
8032
8059
  sourceAgentClient: options.sourceAgentClient ?? "codex",
8033
8060
  status: "active",
8034
- supersedes: options.replace,
8035
8061
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8036
8062
  topic: normalizeOptionalMetadata(options.topic)
8037
8063
  };
8038
- const body = [
8064
+ const bodyText = [
8039
8065
  `repo: ${repoName}`,
8040
8066
  `repo_path: ${repoRoot}`,
8041
8067
  `branch: ${branch || "unknown"}`,
@@ -8059,7 +8085,7 @@ async function buildHandoff(options) {
8059
8085
  "next_step:",
8060
8086
  options.nextStep ?? "- inspect the current repo state and continue from this handoff"
8061
8087
  ].join("\n");
8062
- return { handoff: formatMemoryDocument("HANDOFF", metadata, body), metadata };
8088
+ return { bodyText, metadata };
8063
8089
  }
8064
8090
  async function gitTouchedFiles(cwd) {
8065
8091
  const changedFiles = await gitValue(["diff", "--name-only", "HEAD"], cwd);
@@ -8083,9 +8109,201 @@ function formatBlock(value, emptyValue) {
8083
8109
  return trimmed.split("\n").map((line) => `- ${line}`).join("\n");
8084
8110
  }
8085
8111
 
8112
+ // src/hooks.ts
8113
+ var MANAGED_HOOKS = [
8114
+ {
8115
+ event: "PreCompact",
8116
+ command: HOOK_PRE_COMPACT_COMMAND,
8117
+ description: "Auto-store a handoff snapshot before context compaction so the next turn can recall it."
8118
+ },
8119
+ {
8120
+ event: "SessionStart",
8121
+ command: HOOK_SESSION_START_COMMAND,
8122
+ description: "Pre-load the latest handoff for the current repo into the new session context."
8123
+ }
8124
+ ];
8125
+ async function runHooksInstall(config, agent, options) {
8126
+ const apply = options.apply === true && options.dryRun !== true;
8127
+ const remove = options.remove === true;
8128
+ switch (agent) {
8129
+ case "claude":
8130
+ await runClaudeHooksInstall({ apply, remove });
8131
+ return;
8132
+ case "codex":
8133
+ printCodexHooksNotice(remove);
8134
+ return;
8135
+ case "cursor":
8136
+ printNoHooksSupported("cursor", remove);
8137
+ return;
8138
+ case "copilot":
8139
+ printNoHooksSupported("copilot", remove);
8140
+ return;
8141
+ }
8142
+ }
8143
+ async function runClaudeHooksInstall(options) {
8144
+ const path = expandPath(CLAUDE_SETTINGS_PATH);
8145
+ const existingRaw = await exists(path) ? await (0, import_promises5.readFile)(path, "utf8") : "{}";
8146
+ const parsed = parseJsonConfigObject(existingRaw) ?? {};
8147
+ const next = options.remove ? withoutThreadnoteHooks(parsed) : withThreadnoteHooks(parsed);
8148
+ const before = JSON.stringify(parsed);
8149
+ const after = JSON.stringify(next);
8150
+ if (before === after) {
8151
+ console.log(`Claude hooks already ${options.remove ? "absent" : "managed"} in ${path}.`);
8152
+ return;
8153
+ }
8154
+ console.log(`${options.apply ? "Updating" : "Would update"} ${path}:`);
8155
+ for (const entry of MANAGED_HOOKS) {
8156
+ console.log(` ${options.remove ? "-" : "+"} ${entry.event}: ${entry.command}`);
8157
+ }
8158
+ if (!options.apply) {
8159
+ console.log("\nRe-run with --apply to actually modify the file.");
8160
+ return;
8161
+ }
8162
+ await (0, import_promises5.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
8163
+ const serialized = `${JSON.stringify(next, void 0, 2)}
8164
+ `;
8165
+ await (0, import_promises5.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
8166
+ await (0, import_promises5.chmod)(path, 384);
8167
+ console.log(`${options.remove ? "Removed" : "Installed"} threadnote-managed Claude hooks.`);
8168
+ }
8169
+ function withThreadnoteHooks(input2) {
8170
+ const hooks = ensureMutableObject(input2.hooks);
8171
+ for (const entry of MANAGED_HOOKS) {
8172
+ const list = ensureMutableArray(hooks[entry.event]).filter((item) => !isManagedThreadnoteEntry(item));
8173
+ list.push({
8174
+ [THREADNOTE_HOOK_MARKER]: THREADNOTE_HOOK_MARKER_VALUE,
8175
+ matcher: "",
8176
+ hooks: [{ type: "command", command: entry.command }]
8177
+ });
8178
+ hooks[entry.event] = list;
8179
+ }
8180
+ return { ...input2, hooks };
8181
+ }
8182
+ function withoutThreadnoteHooks(input2) {
8183
+ if (!isJsonObject(input2.hooks)) {
8184
+ return input2;
8185
+ }
8186
+ const hooks = {};
8187
+ for (const [event, value] of Object.entries(input2.hooks)) {
8188
+ if (!Array.isArray(value)) {
8189
+ hooks[event] = value;
8190
+ continue;
8191
+ }
8192
+ const filtered = value.filter((item) => !isManagedThreadnoteEntry(item));
8193
+ if (filtered.length > 0) {
8194
+ hooks[event] = filtered;
8195
+ }
8196
+ }
8197
+ if (Object.keys(hooks).length === 0) {
8198
+ const next = { ...input2 };
8199
+ delete next.hooks;
8200
+ return next;
8201
+ }
8202
+ return { ...input2, hooks };
8203
+ }
8204
+ function isManagedThreadnoteEntry(value) {
8205
+ return isJsonObject(value) && value[THREADNOTE_HOOK_MARKER] === THREADNOTE_HOOK_MARKER_VALUE;
8206
+ }
8207
+ function ensureMutableObject(value) {
8208
+ return isJsonObject(value) ? { ...value } : {};
8209
+ }
8210
+ function ensureMutableArray(value) {
8211
+ return Array.isArray(value) ? [...value] : [];
8212
+ }
8213
+ function printCodexHooksNotice(remove) {
8214
+ if (remove) {
8215
+ console.log("Codex CLI does not expose a managed hook surface today, so there is nothing to remove.");
8216
+ return;
8217
+ }
8218
+ console.log(
8219
+ [
8220
+ "Codex CLI does not currently expose lifecycle hooks (no PreCompact or SessionStart analog).",
8221
+ "Threadnote already installs Codex user instructions at ~/.codex/AGENTS.md; that remains the active guidance surface.",
8222
+ "If a future Codex release adds hook events, threadnote will pick them up via `install-hooks codex`."
8223
+ ].join("\n")
8224
+ );
8225
+ }
8226
+ function printNoHooksSupported(agent, remove) {
8227
+ if (remove) {
8228
+ console.log(`${agent} does not expose a hook surface; nothing to remove.`);
8229
+ return;
8230
+ }
8231
+ console.log(
8232
+ [
8233
+ `${agent} does not expose agent-mode hooks today.`,
8234
+ "Threadnote already installs user-level instructions for this agent; that remains the active guidance surface.",
8235
+ "Hooks support will be added if the agent gains a hook surface upstream."
8236
+ ].join("\n")
8237
+ );
8238
+ }
8239
+ async function hasManagedClaudeHooks() {
8240
+ const path = expandPath(CLAUDE_SETTINGS_PATH);
8241
+ if (!await exists(path)) {
8242
+ return false;
8243
+ }
8244
+ const raw = await (0, import_promises5.readFile)(path, "utf8");
8245
+ const parsed = parseJsonConfigObject(raw);
8246
+ if (!parsed || !isJsonObject(parsed.hooks)) {
8247
+ return false;
8248
+ }
8249
+ for (const value of Object.values(parsed.hooks)) {
8250
+ if (!Array.isArray(value)) {
8251
+ continue;
8252
+ }
8253
+ if (value.some((item) => isManagedThreadnoteEntry(item))) {
8254
+ return true;
8255
+ }
8256
+ }
8257
+ return false;
8258
+ }
8259
+ async function runPreCompactHook(config, options = {}) {
8260
+ try {
8261
+ const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
8262
+ const project = repoRoot ? (0, import_node_path5.basename)(repoRoot) : "general";
8263
+ await runHandoff(config, {
8264
+ blockers: "- none recorded",
8265
+ dryRun: options.dryRun === true,
8266
+ nextStep: "Continue from this auto-snapshot. A manual `threadnote handoff` will produce a richer write-up if you have more context.",
8267
+ project,
8268
+ sourceAgentClient: "claude",
8269
+ task: "Auto-snapshot captured at Claude PreCompact (deterministic safety net before context compaction).",
8270
+ tests: "- not recorded (auto-snapshot)",
8271
+ topic: HOOK_AUTO_PRECOMPACT_TOPIC
8272
+ });
8273
+ } catch (err) {
8274
+ process.stderr.write(
8275
+ `threadnote pre-compact-hook: snapshot skipped (${err instanceof Error ? err.message : String(err)})
8276
+ `
8277
+ );
8278
+ }
8279
+ }
8280
+ async function runSessionStartHook(config, options = {}) {
8281
+ try {
8282
+ const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
8283
+ if (!repoRoot) {
8284
+ return;
8285
+ }
8286
+ const project = (0, import_node_path5.basename)(repoRoot);
8287
+ process.stdout.write(`## Threadnote \u2014 latest context for ${project}
8288
+
8289
+ `);
8290
+ await runRecall(config, {
8291
+ dryRun: options.dryRun === true,
8292
+ inferScope: true,
8293
+ nodeLimit: "5",
8294
+ query: `${project} latest handoff durable feature memory`
8295
+ });
8296
+ } catch (err) {
8297
+ process.stderr.write(
8298
+ `threadnote session-start-hook: recall skipped (${err instanceof Error ? err.message : String(err)})
8299
+ `
8300
+ );
8301
+ }
8302
+ }
8303
+
8086
8304
  // src/seeding.ts
8087
- var import_promises5 = require("node:fs/promises");
8088
- var import_node_path5 = require("node:path");
8305
+ var import_promises6 = require("node:fs/promises");
8306
+ var import_node_path6 = require("node:path");
8089
8307
  async function runSeed(config, options) {
8090
8308
  const manifest = await readSeedManifest(config.manifestPath);
8091
8309
  const ignorePatterns = await loadIgnorePatterns();
@@ -8114,7 +8332,7 @@ async function runSeed(config, options) {
8114
8332
  }
8115
8333
  async function runInitManifest(config, options) {
8116
8334
  const manifestPath = expandPath(
8117
- options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path5.join)(config.agentContextHome, USER_MANIFEST_NAME)
8335
+ options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path6.join)(config.agentContextHome, USER_MANIFEST_NAME)
8118
8336
  );
8119
8337
  const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
8120
8338
  const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
@@ -8157,9 +8375,9 @@ async function runInitManifest(config, options) {
8157
8375
  console.log(output2.trimEnd());
8158
8376
  return;
8159
8377
  }
8160
- await ensureDirectory((0, import_node_path5.dirname)(manifestPath), false);
8161
- await (0, import_promises5.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
8162
- await (0, import_promises5.chmod)(manifestPath, 384);
8378
+ await ensureDirectory((0, import_node_path6.dirname)(manifestPath), false);
8379
+ await (0, import_promises6.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
8380
+ await (0, import_promises6.chmod)(manifestPath, 384);
8163
8381
  console.log(`Wrote manifest: ${manifestPath}`);
8164
8382
  console.log("Seed with:");
8165
8383
  console.log(" threadnote seed --dry-run");
@@ -8189,13 +8407,13 @@ async function resolveRepoRoot(repoInput) {
8189
8407
  async function projectIdentity(path) {
8190
8408
  const expanded = expandPath(path);
8191
8409
  try {
8192
- return await (0, import_promises5.realpath)(expanded);
8410
+ return await (0, import_promises6.realpath)(expanded);
8193
8411
  } catch (_err) {
8194
8412
  return expanded;
8195
8413
  }
8196
8414
  }
8197
8415
  function projectManifestForRepo(repoRoot, existingProjects) {
8198
- const baseName = uriSegment((0, import_node_path5.basename)(repoRoot));
8416
+ const baseName = uriSegment((0, import_node_path6.basename)(repoRoot));
8199
8417
  const usedNames = new Set(existingProjects.map((project) => project.name));
8200
8418
  const usedUris = new Set(existingProjects.map((project) => project.uri));
8201
8419
  let name = baseName;
@@ -8217,7 +8435,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
8217
8435
  for (const pattern of project.seed) {
8218
8436
  const files = await resolveProjectPattern(projectRoot, pattern);
8219
8437
  for (const filePath of files) {
8220
- const relativePath = toPosixPath((0, import_node_path5.relative)(projectRoot, filePath));
8438
+ const relativePath = toPosixPath((0, import_node_path6.relative)(projectRoot, filePath));
8221
8439
  if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
8222
8440
  continue;
8223
8441
  }
@@ -8235,20 +8453,20 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
8235
8453
  async function resolveProjectPattern(projectRoot, pattern) {
8236
8454
  const normalizedPattern = toPosixPath(pattern);
8237
8455
  if (!hasGlob(normalizedPattern)) {
8238
- const filePath = (0, import_node_path5.join)(projectRoot, normalizedPattern);
8456
+ const filePath = (0, import_node_path6.join)(projectRoot, normalizedPattern);
8239
8457
  return await isFile(filePath) ? [filePath] : [];
8240
8458
  }
8241
8459
  const globBase = getGlobBase(normalizedPattern);
8242
- const basePath = (0, import_node_path5.join)(projectRoot, globBase);
8460
+ const basePath = (0, import_node_path6.join)(projectRoot, globBase);
8243
8461
  if (!await exists(basePath)) {
8244
8462
  return [];
8245
8463
  }
8246
8464
  const regex = globToRegExp(normalizedPattern);
8247
8465
  const files = await walkFiles(basePath);
8248
- return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path5.relative)(projectRoot, filePath))));
8466
+ return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path6.relative)(projectRoot, filePath))));
8249
8467
  }
8250
8468
  async function prepareSeedFile(config, candidate, dryRun) {
8251
- const content = await (0, import_promises5.readFile)(candidate.filePath, "utf8");
8469
+ const content = await (0, import_promises6.readFile)(candidate.filePath, "utf8");
8252
8470
  const redactedContent = shouldRedactPath(candidate.relativePath) ? redactContent(candidate.relativePath, content) : content;
8253
8471
  const secretMatches = detectSecretMatches(redactedContent);
8254
8472
  if (secretMatches.length > 0) {
@@ -8260,14 +8478,14 @@ async function prepareSeedFile(config, candidate, dryRun) {
8260
8478
  if (redactedContent === content) {
8261
8479
  return candidate.filePath;
8262
8480
  }
8263
- const redactedPath = (0, import_node_path5.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
8481
+ const redactedPath = (0, import_node_path6.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
8264
8482
  if (dryRun) {
8265
8483
  console.log(`Would write redacted copy: ${redactedPath}`);
8266
8484
  return redactedPath;
8267
8485
  }
8268
- await ensureDirectory((0, import_node_path5.dirname)(redactedPath), false);
8269
- await (0, import_promises5.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
8270
- await (0, import_promises5.chmod)(redactedPath, 384);
8486
+ await ensureDirectory((0, import_node_path6.dirname)(redactedPath), false);
8487
+ await (0, import_promises6.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
8488
+ await (0, import_promises6.chmod)(redactedPath, 384);
8271
8489
  return redactedPath;
8272
8490
  }
8273
8491
  async function collectSkillCandidates(config) {
@@ -8292,7 +8510,7 @@ async function collectSkillCandidates(config) {
8292
8510
  for (const source of sources) {
8293
8511
  const files = await resolveAbsolutePattern(expandPath(source.pattern));
8294
8512
  for (const filePath of files) {
8295
- const content = await (0, import_promises5.readFile)(filePath, "utf8");
8513
+ const content = await (0, import_promises6.readFile)(filePath, "utf8");
8296
8514
  const matches = detectSecretMatches(content);
8297
8515
  if (matches.length > 0) {
8298
8516
  console.log(`SKIP skill with possible secret: ${filePath}`);
@@ -8323,10 +8541,10 @@ async function resolveAbsolutePattern(pattern) {
8323
8541
  return files.filter((filePath) => regex.test(toPosixPath(filePath)));
8324
8542
  }
8325
8543
  function skillResourceUri(skill) {
8326
- return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0, import_node_path5.basename)((0, import_node_path5.dirname)(skill.filePath)))}-${skill.hash.slice(0, 12)}.md`;
8544
+ return `viking://resources/agent-skills/${uriSegment(skill.source)}/${uriSegment((0, import_node_path6.basename)((0, import_node_path6.dirname)(skill.filePath)))}-${skill.hash.slice(0, 12)}.md`;
8327
8545
  }
8328
8546
  async function loadIgnorePatterns() {
8329
- const raw = await (0, import_promises5.readFile)((0, import_node_path5.join)(toolRoot(), ".threadnoteignore"), "utf8");
8547
+ const raw = await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), ".threadnoteignore"), "utf8");
8330
8548
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8331
8549
  }
8332
8550
  function matchesIgnore(relativePath, patterns) {
@@ -8397,14 +8615,16 @@ function detectSecretMatches(content) {
8397
8615
  }
8398
8616
 
8399
8617
  // src/share.ts
8400
- var import_promises6 = require("node:fs/promises");
8618
+ var import_promises7 = require("node:fs/promises");
8401
8619
  var import_node_os4 = require("node:os");
8402
- var import_node_path6 = require("node:path");
8620
+ var import_node_path7 = require("node:path");
8403
8621
  var TEAMS_FILE_VERSION = 1;
8404
8622
  var SHARED_SEGMENT = "shared";
8405
8623
  var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
8406
8624
  var DEFAULT_GIT_REMOTE_NAME = "origin";
8407
8625
  var SCRUBBER_PATTERNS = [
8626
+ // Credentials: never redactable. Blocking is the only safe response —
8627
+ // automated redaction risks false negatives that leave material in git.
8408
8628
  { name: "private key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
8409
8629
  { name: "API key (sk-...)", regex: /\bsk-[A-Za-z0-9_-]{16,}/ },
8410
8630
  { name: "GitHub token", regex: /\bgh[pousr]_[A-Za-z0-9_]{16,}/ },
@@ -8417,8 +8637,35 @@ var SCRUBBER_PATTERNS = [
8417
8637
  { name: "AWS access key", regex: /\bAKIA[0-9A-Z]{16}\b/ },
8418
8638
  // Slack tokens: xoxa/xoxb/xoxc (configuration)/xoxd (legacy user cookie)/
8419
8639
  // xoxe (refresh)/xoxp/xoxr/xoxs, with optional -N- segment for the workspace tier.
8420
- { name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i }
8640
+ { name: "Slack token", regex: /\bxox[abcdeprs](?:-\d-)?[A-Za-z0-9._-]{10,}/i },
8641
+ // Soft leaks: block by default (so the agent sees them and decides), but
8642
+ // allow opt-in redaction so curated memories with incidental matches can
8643
+ // ship without a manual rewrite. Local home paths are the recurring
8644
+ // real-world leak; the regexes greedily consume the whole path segment
8645
+ // (including subdirectories) up to whitespace or common closing punctuation
8646
+ // so redaction collapses an entire path to a single placeholder rather than
8647
+ // leaving the subpath visible.
8648
+ { name: "macOS home path", placeholder: "<local-path>", regex: /\/Users\/[^\s)>"'`,]+/ },
8649
+ { name: "linux home path", placeholder: "<local-path>", regex: /\b\/home\/[^\s)>"'`,]+/ }
8421
8650
  ];
8651
+ function applyScrubber(content, { redact }) {
8652
+ let cleaned = content;
8653
+ const redactions = [];
8654
+ for (const pattern of SCRUBBER_PATTERNS) {
8655
+ if (!pattern.regex.test(cleaned)) {
8656
+ continue;
8657
+ }
8658
+ if (!pattern.placeholder || !redact) {
8659
+ return { blocker: pattern.name, cleaned: content, redactions: [] };
8660
+ }
8661
+ const flags = pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`;
8662
+ const globalRegex = new RegExp(pattern.regex.source, flags);
8663
+ const matches = cleaned.match(globalRegex) ?? [];
8664
+ cleaned = cleaned.replace(globalRegex, pattern.placeholder);
8665
+ redactions.push({ count: matches.length, name: pattern.name });
8666
+ }
8667
+ return { cleaned, redactions };
8668
+ }
8422
8669
  async function runShareInit(config, remoteUrl, options) {
8423
8670
  if (!remoteUrl.trim()) {
8424
8671
  throw new Error("Provide a git remote URL for the shared memories repo.");
@@ -8437,8 +8684,8 @@ async function runShareInit(config, remoteUrl, options) {
8437
8684
  if (await exists(gitdir)) {
8438
8685
  throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
8439
8686
  }
8440
- await ensureDirectory((0, import_node_path6.dirname)(worktree), dryRun);
8441
- await ensureDirectory((0, import_node_path6.dirname)(gitdir), dryRun);
8687
+ await ensureDirectory((0, import_node_path7.dirname)(worktree), dryRun);
8688
+ await ensureDirectory((0, import_node_path7.dirname)(gitdir), dryRun);
8442
8689
  const git = await requiredExecutable("git");
8443
8690
  await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
8444
8691
  const newConfig = {
@@ -8469,7 +8716,7 @@ async function runShareInit(config, remoteUrl, options) {
8469
8716
  var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
8470
8717
  var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
8471
8718
  async function ensureSharedGitignore(worktree, git, push) {
8472
- const gitignorePath = (0, import_node_path6.join)(worktree, ".gitignore");
8719
+ const gitignorePath = (0, import_node_path7.join)(worktree, ".gitignore");
8473
8720
  const existing = await readFileIfExists(gitignorePath) ?? "";
8474
8721
  const lines = existing.split("\n").map((line) => line.trim());
8475
8722
  const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
@@ -8488,7 +8735,7 @@ async function ensureSharedGitignore(worktree, git, push) {
8488
8735
  segments.push(SHARED_GITIGNORE_HEADER, "\n");
8489
8736
  }
8490
8737
  segments.push(missingPatterns.join("\n"), "\n");
8491
- await (0, import_promises6.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
8738
+ await (0, import_promises7.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
8492
8739
  console.log(`Added ${missingPatterns.join(", ")} to ${portablePath(gitignorePath)}`);
8493
8740
  await maybeRun(false, git, ["-C", worktree, "add", ".gitignore"]);
8494
8741
  const commitResult = await runCommand(
@@ -8553,7 +8800,7 @@ async function runShareSync(config, options) {
8553
8800
  if (dryRun) {
8554
8801
  console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
8555
8802
  } else if (pullResult && pullResult.exitCode !== 0) {
8556
- if (await exists((0, import_node_path6.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path6.join)(team.config.gitdir, "rebase-apply"))) {
8803
+ if (await exists((0, import_node_path7.join)(team.config.gitdir, "rebase-merge")) || await exists((0, import_node_path7.join)(team.config.gitdir, "rebase-apply"))) {
8557
8804
  throw new Error(
8558
8805
  `git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
8559
8806
  Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
@@ -8583,16 +8830,41 @@ async function runSharePublish(config, sourceUri, options) {
8583
8830
  assertVikingUri(sourceUri);
8584
8831
  const team = await resolveTeam(config, options.team);
8585
8832
  const dryRun = options.dryRun === true;
8833
+ const preview = options.preview === true;
8586
8834
  if (isInSharedNamespace(config, sourceUri)) {
8587
8835
  throw new Error(`Memory ${sourceUri} is already in the shared namespace.`);
8588
8836
  }
8589
8837
  const ov = await openVikingCliForMode(dryRun);
8590
- const content = await readMemoryContent(config, ov, sourceUri, dryRun);
8591
- const blocker = scrubberBlocker(content);
8592
- if (blocker) {
8593
- throw new Error(`Refusing to publish ${sourceUri}: possible ${blocker}. Strip the sensitive value, then retry.`);
8594
- }
8838
+ const rawContent = await readMemoryContent(config, ov, sourceUri, dryRun);
8839
+ const stripped = stripPersonalProvenance(rawContent);
8840
+ const scrub = applyScrubber(stripped, { redact: options.redact === true });
8595
8841
  const targetUri = sharedUriFor(config, sourceUri, team.name);
8842
+ if (preview) {
8843
+ console.log(`PREVIEW source: ${sourceUri}`);
8844
+ console.log(`PREVIEW destination: ${targetUri}`);
8845
+ if (scrub.blocker) {
8846
+ console.log(
8847
+ `PREVIEW BLOCKED: ${scrub.blocker}. Strip the sensitive value or rerun with --redact for soft-leak patterns.`
8848
+ );
8849
+ return;
8850
+ }
8851
+ for (const redaction of scrub.redactions) {
8852
+ console.log(`PREVIEW redact: ${redaction.count}\xD7 ${redaction.name}`);
8853
+ }
8854
+ console.log("-----BEGIN PREVIEW-----");
8855
+ console.log(scrub.cleaned);
8856
+ console.log("-----END PREVIEW-----");
8857
+ return;
8858
+ }
8859
+ if (scrub.blocker) {
8860
+ throw new Error(
8861
+ `Refusing to publish ${sourceUri}: possible ${scrub.blocker}. Strip the sensitive value or pass --redact for soft-leak patterns.`
8862
+ );
8863
+ }
8864
+ for (const redaction of scrub.redactions) {
8865
+ console.log(`Redacted ${redaction.count}\xD7 ${redaction.name} before publish.`);
8866
+ }
8867
+ const content = scrub.cleaned;
8596
8868
  if (!dryRun && await vikingResourceExists2(ov, config, targetUri)) {
8597
8869
  throw new Error(
8598
8870
  `Refusing to publish: ${targetUri} already exists in the shared namespace. Inspect it via threadnote read; if it should be replaced, forget the existing shared copy first.`
@@ -8704,10 +8976,10 @@ function normalizeTeamName(input2) {
8704
8976
  return candidate;
8705
8977
  }
8706
8978
  function teamsFilePath(config) {
8707
- return (0, import_node_path6.join)(config.agentContextHome, "share", "teams.json");
8979
+ return (0, import_node_path7.join)(config.agentContextHome, "share", "teams.json");
8708
8980
  }
8709
8981
  function teamWorktreePath(config, team) {
8710
- return (0, import_node_path6.join)(
8982
+ return (0, import_node_path7.join)(
8711
8983
  config.agentContextHome,
8712
8984
  "data",
8713
8985
  "viking",
@@ -8720,7 +8992,7 @@ function teamWorktreePath(config, team) {
8720
8992
  );
8721
8993
  }
8722
8994
  function teamGitdirPath(config, team) {
8723
- return (0, import_node_path6.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
8995
+ return (0, import_node_path7.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
8724
8996
  }
8725
8997
  async function readTeamsFile(config) {
8726
8998
  const path = teamsFilePath(config);
@@ -8763,13 +9035,13 @@ async function readTeamsFile(config) {
8763
9035
  }
8764
9036
  async function writeTeamsFile(config, contents) {
8765
9037
  const path = teamsFilePath(config);
8766
- await (0, import_promises6.mkdir)((0, import_node_path6.dirname)(path), { recursive: true });
9038
+ await (0, import_promises7.mkdir)((0, import_node_path7.dirname)(path), { recursive: true });
8767
9039
  const serializable = {
8768
9040
  defaultTeam: contents.defaultTeam,
8769
9041
  teams: contents.teams,
8770
9042
  version: contents.version
8771
9043
  };
8772
- await (0, import_promises6.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
9044
+ await (0, import_promises7.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
8773
9045
  `, { encoding: "utf8", mode: 384 });
8774
9046
  }
8775
9047
  async function resolveTeam(config, requested) {
@@ -8799,7 +9071,7 @@ async function assertWorktreeUsable(worktree) {
8799
9071
  if (!await isDirectory(worktree)) {
8800
9072
  throw new Error(`Cannot use ${worktree} as a worktree: not a directory.`);
8801
9073
  }
8802
- const entries = await (0, import_promises6.readdir)(worktree);
9074
+ const entries = await (0, import_promises7.readdir)(worktree);
8803
9075
  if (entries.length > 0) {
8804
9076
  const preview = entries.slice(0, 5).join(", ");
8805
9077
  const suffix = entries.length > 5 ? `, +${entries.length - 5} more` : "";
@@ -8823,7 +9095,7 @@ async function walkMemoryFiles(root) {
8823
9095
  async function visit(path, depth) {
8824
9096
  let entries;
8825
9097
  try {
8826
- entries = await (0, import_promises6.readdir)(path, { withFileTypes: true });
9098
+ entries = await (0, import_promises7.readdir)(path, { withFileTypes: true });
8827
9099
  } catch (err) {
8828
9100
  console.warn(`Skipping ${path} during shared-tree walk: ${err instanceof Error ? err.message : String(err)}`);
8829
9101
  return;
@@ -8832,7 +9104,7 @@ async function walkMemoryFiles(root) {
8832
9104
  if (entry.name === ".git") {
8833
9105
  continue;
8834
9106
  }
8835
- const full = (0, import_node_path6.join)(path, entry.name);
9107
+ const full = (0, import_node_path7.join)(path, entry.name);
8836
9108
  if (entry.isDirectory()) {
8837
9109
  if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
8838
9110
  continue;
@@ -8856,7 +9128,7 @@ async function walkMemoryFiles(root) {
8856
9128
  return out;
8857
9129
  }
8858
9130
  function workfileToVikingUri(config, team, filePath) {
8859
- const rel = (0, import_node_path6.relative)(team.worktree, filePath).split(import_node_path6.sep).join("/");
9131
+ const rel = (0, import_node_path7.relative)(team.worktree, filePath).split(import_node_path7.sep).join("/");
8860
9132
  return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
8861
9133
  }
8862
9134
  function isInSharedNamespace(config, uri) {
@@ -8888,8 +9160,26 @@ function vikingUriToWorktreeRelative(config, uri, team) {
8888
9160
  }
8889
9161
  return uri.slice(prefix.length);
8890
9162
  }
8891
- function scrubberBlocker(content) {
8892
- return SCRUBBER_PATTERNS.find((pattern) => pattern.regex.test(content))?.name;
9163
+ function stripPersonalProvenance(content) {
9164
+ const lines = content.split("\n");
9165
+ let headerEnd = lines.length;
9166
+ for (let index = 0; index < lines.length; index += 1) {
9167
+ if (lines[index].trim() === "") {
9168
+ headerEnd = index;
9169
+ break;
9170
+ }
9171
+ }
9172
+ const cleaned = [];
9173
+ for (let index = 0; index < headerEnd; index += 1) {
9174
+ if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
9175
+ continue;
9176
+ }
9177
+ cleaned.push(lines[index]);
9178
+ }
9179
+ for (let index = headerEnd; index < lines.length; index += 1) {
9180
+ cleaned.push(lines[index]);
9181
+ }
9182
+ return cleaned.join("\n");
8893
9183
  }
8894
9184
  async function readMemoryContent(config, ov, uri, dryRun) {
8895
9185
  const args = withIdentity(config, ["read", uri]);
@@ -8950,13 +9240,13 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun) {
8950
9240
  console.log(`Would run: ${formatShellCommand(ov, args)}`);
8951
9241
  return;
8952
9242
  }
8953
- const stagingDir = await (0, import_promises6.mkdtemp)((0, import_node_path6.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
8954
- const tempPath = (0, import_node_path6.join)(stagingDir, "body.txt");
9243
+ const stagingDir = await (0, import_promises7.mkdtemp)((0, import_node_path7.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
9244
+ const tempPath = (0, import_node_path7.join)(stagingDir, "body.txt");
8955
9245
  try {
8956
- await (0, import_promises6.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
9246
+ await (0, import_promises7.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
8957
9247
  await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode);
8958
9248
  } finally {
8959
- await (0, import_promises6.rm)(stagingDir, { force: true, recursive: true });
9249
+ await (0, import_promises7.rm)(stagingDir, { force: true, recursive: true });
8960
9250
  }
8961
9251
  }
8962
9252
  async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode) {
@@ -9014,7 +9304,7 @@ ${stdout}`.toLowerCase();
9014
9304
  return output2.includes("resource is busy") || output2.includes("resource is being processed") || output2.includes("network error") || output2.includes("error sending request") || output2.includes("http request failed") || output2.includes("connection refused") || output2.includes("connection reset") || output2.includes("timed out");
9015
9305
  }
9016
9306
  async function ingestSingleFile(ov, config, uri, filePath, initialMode) {
9017
- const content = await (0, import_promises6.readFile)(filePath, "utf8");
9307
+ const content = await (0, import_promises7.readFile)(filePath, "utf8");
9018
9308
  await writeMemoryFile(config, ov, uri, content, initialMode, false);
9019
9309
  }
9020
9310
  async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
@@ -9056,7 +9346,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
9056
9346
  if (!relative3) {
9057
9347
  return;
9058
9348
  }
9059
- await (0, import_promises6.rm)((0, import_node_path6.join)(worktree, relative3), { force: true });
9349
+ await (0, import_promises7.rm)((0, import_node_path7.join)(worktree, relative3), { force: true });
9060
9350
  }
9061
9351
  async function removeMemoryUri(config, ov, uri, dryRun) {
9062
9352
  const args = withIdentity(config, ["rm", uri]);
@@ -9113,8 +9403,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
9113
9403
  const oldRel = entries[index + 1];
9114
9404
  const newRel = entries[index + 2];
9115
9405
  if (oldRel && newRel) {
9116
- changes.push({ path: (0, import_node_path6.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
9117
- changes.push({ path: (0, import_node_path6.join)(worktree, newRel), relativePath: newRel, status: "added" });
9406
+ changes.push({ path: (0, import_node_path7.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
9407
+ changes.push({ path: (0, import_node_path7.join)(worktree, newRel), relativePath: newRel, status: "added" });
9118
9408
  }
9119
9409
  index += 3;
9120
9410
  continue;
@@ -9122,7 +9412,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
9122
9412
  const rel = entries[index + 1];
9123
9413
  if (rel) {
9124
9414
  const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
9125
- changes.push({ path: (0, import_node_path6.join)(worktree, rel), relativePath: rel, status });
9415
+ changes.push({ path: (0, import_node_path7.join)(worktree, rel), relativePath: rel, status });
9126
9416
  }
9127
9417
  index += 2;
9128
9418
  }
@@ -9160,18 +9450,18 @@ async function applyChangesToOpenViking(config, team, changes) {
9160
9450
  // src/lifecycle.ts
9161
9451
  var import_node_child_process2 = require("node:child_process");
9162
9452
  var import_node_fs5 = require("node:fs");
9163
- var import_promises9 = require("node:fs/promises");
9453
+ var import_promises10 = require("node:fs/promises");
9164
9454
  var import_node_os6 = require("node:os");
9165
- var import_node_path8 = require("node:path");
9455
+ var import_node_path9 = require("node:path");
9166
9456
  var import_node_process2 = require("node:process");
9167
- var import_promises10 = require("node:readline/promises");
9457
+ var import_promises11 = require("node:readline/promises");
9168
9458
 
9169
9459
  // src/update.ts
9170
9460
  var import_node_fs4 = require("node:fs");
9171
- var import_promises7 = require("node:fs/promises");
9461
+ var import_promises8 = require("node:fs/promises");
9172
9462
  var import_node_os5 = require("node:os");
9173
- var import_node_path7 = require("node:path");
9174
- var import_promises8 = require("node:readline/promises");
9463
+ var import_node_path8 = require("node:path");
9464
+ var import_promises9 = require("node:readline/promises");
9175
9465
  var import_node_process = require("node:process");
9176
9466
  var NPM_PACKAGE_NAME = "threadnote";
9177
9467
  var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
@@ -9319,7 +9609,7 @@ async function getUpdateInfo(config, options) {
9319
9609
  };
9320
9610
  }
9321
9611
  async function currentPackageVersion() {
9322
- const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
9612
+ const rawPackage = await (0, import_promises8.readFile)((0, import_node_path8.join)(toolRoot(), "package.json"), "utf8");
9323
9613
  const parsed = JSON.parse(rawPackage);
9324
9614
  if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
9325
9615
  throw new Error("Could not read current threadnote package version.");
@@ -9369,11 +9659,11 @@ async function readFreshCache(config, registry) {
9369
9659
  }
9370
9660
  async function writeUpdateCache(config, cache) {
9371
9661
  await ensureDirectory(config.agentContextHome, false);
9372
- await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
9662
+ await (0, import_promises8.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
9373
9663
  `, { encoding: "utf8", mode: 384 });
9374
9664
  }
9375
9665
  function updateCachePath(config) {
9376
- return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
9666
+ return (0, import_node_path8.join)(config.agentContextHome, "update-check.json");
9377
9667
  }
9378
9668
  async function runApplicablePostUpdateMigrations(config, options) {
9379
9669
  const state = await readPostUpdateState(config);
@@ -9437,7 +9727,7 @@ async function applicablePostUpdateMigrations(config, options) {
9437
9727
  return applicable;
9438
9728
  }
9439
9729
  async function readPostUpdateMigrations() {
9440
- const raw = await readFileIfExists((0, import_node_path7.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
9730
+ const raw = await readFileIfExists((0, import_node_path8.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
9441
9731
  if (!raw) {
9442
9732
  return [];
9443
9733
  }
@@ -9476,7 +9766,7 @@ function printPostUpdateMigration(migration) {
9476
9766
  }
9477
9767
  }
9478
9768
  async function confirmPostUpdateMigration(prompt) {
9479
- const readline = (0, import_promises8.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
9769
+ const readline = (0, import_promises9.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
9480
9770
  try {
9481
9771
  const answer = (await readline.question(prompt)).trim().toLowerCase();
9482
9772
  return answer === "y" || answer === "yes";
@@ -9508,11 +9798,11 @@ async function readPostUpdateState(config) {
9508
9798
  }
9509
9799
  async function writePostUpdateState(config, state) {
9510
9800
  await ensureDirectory(config.agentContextHome, false);
9511
- await (0, import_promises7.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
9801
+ await (0, import_promises8.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
9512
9802
  `, { encoding: "utf8", mode: 384 });
9513
9803
  }
9514
9804
  function postUpdateStatePath(config) {
9515
- return (0, import_node_path7.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
9805
+ return (0, import_node_path8.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
9516
9806
  }
9517
9807
  async function resolveUpdateRuntime(runtime) {
9518
9808
  if (runtime !== "auto") {
@@ -9542,18 +9832,18 @@ async function runtimeThreadnoteBin(runtime) {
9542
9832
  if (runtime === "npm") {
9543
9833
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
9544
9834
  const prefix = result.stdout.trim();
9545
- return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
9835
+ return prefix ? (0, import_node_path8.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
9546
9836
  }
9547
9837
  if (runtime === "bun") {
9548
9838
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
9549
9839
  const binDir = result.stdout.trim();
9550
- return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
9840
+ return binDir ? (0, import_node_path8.join)(binDir, NPM_PACKAGE_NAME) : void 0;
9551
9841
  }
9552
- return (0, import_node_path7.join)(process.env.DENO_INSTALL ?? (0, import_node_path7.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
9842
+ return (0, import_node_path8.join)(process.env.DENO_INSTALL ?? (0, import_node_path8.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
9553
9843
  }
9554
9844
  async function isExecutable(path) {
9555
9845
  try {
9556
- await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
9846
+ await (0, import_promises8.access)(path, import_node_fs4.constants.X_OK);
9557
9847
  return true;
9558
9848
  } catch (_err) {
9559
9849
  return false;
@@ -9644,9 +9934,9 @@ async function runDoctor(config, options) {
9644
9934
  checks.push(await commandShimCheck());
9645
9935
  checks.push(...await userAgentInstructionsChecks());
9646
9936
  checks.push(await manifestCheck(config.manifestPath));
9647
- checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
9648
- checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
9649
- checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
9937
+ checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
9938
+ checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
9939
+ checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
9650
9940
  checks.push(await healthCheck(config));
9651
9941
  for (const check of checks) {
9652
9942
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -9663,9 +9953,9 @@ async function runInstall(config, options) {
9663
9953
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
9664
9954
  const dryRun = options.dryRun === true;
9665
9955
  await ensureDirectory(config.agentContextHome, dryRun);
9666
- await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "logs"), dryRun);
9667
- await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "redacted"), dryRun);
9668
- await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "mcp"), dryRun);
9956
+ await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "logs"), dryRun);
9957
+ await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "redacted"), dryRun);
9958
+ await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "mcp"), dryRun);
9669
9959
  await installCommandShim(dryRun);
9670
9960
  await installUserAgentInstructions(dryRun);
9671
9961
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
@@ -9691,17 +9981,17 @@ async function runInstall(config, options) {
9691
9981
  }
9692
9982
  await writeTemplateIfMissing({
9693
9983
  config,
9694
- destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ov.conf"),
9984
+ destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ov.conf"),
9695
9985
  dryRun,
9696
9986
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
9697
- templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json")
9987
+ templatePath: (0, import_node_path9.join)(toolRoot(), "config", "ov.conf.template.json")
9698
9988
  });
9699
9989
  await writeTemplateIfMissing({
9700
9990
  config,
9701
- destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ovcli.conf"),
9991
+ destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ovcli.conf"),
9702
9992
  dryRun,
9703
9993
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
9704
- templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json")
9994
+ templatePath: (0, import_node_path9.join)(toolRoot(), "config", "ovcli.conf.template.json")
9705
9995
  });
9706
9996
  if (options.start !== false) {
9707
9997
  const healthy = await repairServerHealth(config, dryRun);
@@ -9738,6 +10028,10 @@ async function runRepair(config, options) {
9738
10028
  await runMcpInstall(config, client, { apply: !dryRun, name: OPENVIKING_MCP_NAME });
9739
10029
  }
9740
10030
  }
10031
+ if (await hasManagedClaudeHooks()) {
10032
+ console.log("\nRepairing claude hooks (re-asserting threadnote-managed entries).");
10033
+ await runHooksInstall(config, "claude", { apply: !dryRun, dryRun });
10034
+ }
9741
10035
  console.log("\nPost-repair doctor:");
9742
10036
  await runDoctor(config, { dryRun, strict: false });
9743
10037
  if (options.postUpdate !== false) {
@@ -9751,10 +10045,13 @@ async function runUninstall(config, options) {
9751
10045
  }
9752
10046
  console.log("Uninstalling local Threadnote setup.");
9753
10047
  await runStop(config, { dryRun });
9754
- await removePathIfExists((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
10048
+ await removePathIfExists((0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
9755
10049
  await removeLaunchAgent(dryRun);
9756
10050
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
9757
10051
  await removeMcpSnippets(config, dryRun);
10052
+ if (await hasManagedClaudeHooks()) {
10053
+ await runHooksInstall(config, "claude", { apply: !dryRun, dryRun, remove: true });
10054
+ }
9758
10055
  await removeCommandShim(dryRun);
9759
10056
  await removeUserAgentInstructions(dryRun);
9760
10057
  if (options.eraseMemories === true) {
@@ -9805,16 +10102,16 @@ async function repairManifest(config, dryRun) {
9805
10102
  console.log(output2.trimEnd());
9806
10103
  return;
9807
10104
  }
9808
- await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
10105
+ await ensureDirectory((0, import_node_path9.dirname)(config.manifestPath), false);
9809
10106
  const currentContent = await readFileIfExists(config.manifestPath);
9810
10107
  if (currentContent !== void 0) {
9811
10108
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
9812
- await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
9813
- await (0, import_promises9.chmod)(backupPath, 384);
10109
+ await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10110
+ await (0, import_promises10.chmod)(backupPath, 384);
9814
10111
  console.log(`Backup: ${backupPath}`);
9815
10112
  }
9816
- await (0, import_promises9.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
9817
- await (0, import_promises9.chmod)(config.manifestPath, 384);
10113
+ await (0, import_promises10.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
10114
+ await (0, import_promises10.chmod)(config.manifestPath, 384);
9818
10115
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
9819
10116
  }
9820
10117
  async function repairServerHealth(config, dryRun) {
@@ -9855,7 +10152,7 @@ async function runStart(config, options) {
9855
10152
  );
9856
10153
  }
9857
10154
  const logPath = openVikingLogPath(config);
9858
- await ensureDirectory((0, import_node_path8.dirname)(logPath), false);
10155
+ await ensureDirectory((0, import_node_path9.dirname)(logPath), false);
9859
10156
  if (options.foreground === true) {
9860
10157
  const result = await runInteractive(server, args);
9861
10158
  process.exitCode = result;
@@ -9864,7 +10161,7 @@ async function runStart(config, options) {
9864
10161
  const logFd = (0, import_node_fs5.openSync)(logPath, "a");
9865
10162
  const child = spawnDetachedServerWithLog(server, args, logFd);
9866
10163
  child.unref();
9867
- await (0, import_promises9.writeFile)((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
10164
+ await (0, import_promises10.writeFile)((0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
9868
10165
  `, "utf8");
9869
10166
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
9870
10167
  if (health) {
@@ -9897,7 +10194,7 @@ async function runStop(config, options) {
9897
10194
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
9898
10195
  }
9899
10196
  }
9900
- const pidPath = (0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid");
10197
+ const pidPath = (0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid");
9901
10198
  const pidText = await readFileIfExists(pidPath);
9902
10199
  if (!pidText) {
9903
10200
  console.log("No pid file found for detached OpenViking server.");
@@ -9993,7 +10290,7 @@ async function pythonSystemCertificatesCheck() {
9993
10290
  };
9994
10291
  }
9995
10292
  async function commandShimCheck() {
9996
- const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10293
+ const shimPath = (0, import_node_path9.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9997
10294
  const content = await readFileIfExists(shimPath);
9998
10295
  if (content === void 0) {
9999
10296
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -10059,11 +10356,11 @@ async function hasPythonModule(serverPath, moduleName) {
10059
10356
  async function siblingPythonForExecutable(executablePath) {
10060
10357
  let resolvedPath;
10061
10358
  try {
10062
- resolvedPath = await (0, import_promises9.realpath)(executablePath);
10359
+ resolvedPath = await (0, import_promises10.realpath)(executablePath);
10063
10360
  } catch (_err) {
10064
10361
  return void 0;
10065
10362
  }
10066
- const pythonPath = (0, import_node_path8.join)((0, import_node_path8.dirname)(resolvedPath), "python");
10363
+ const pythonPath = (0, import_node_path9.join)((0, import_node_path9.dirname)(resolvedPath), "python");
10067
10364
  return await exists(pythonPath) ? pythonPath : void 0;
10068
10365
  }
10069
10366
  async function manifestCheck(path) {
@@ -10134,7 +10431,7 @@ async function offerToInstallUv() {
10134
10431
  );
10135
10432
  return false;
10136
10433
  }
10137
- const readline = (0, import_promises10.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
10434
+ const readline = (0, import_promises11.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
10138
10435
  let answer;
10139
10436
  try {
10140
10437
  answer = (await readline.question(
@@ -10257,38 +10554,38 @@ function printInstallNextSteps(options) {
10257
10554
  }
10258
10555
  async function writeTemplateIfMissing(options) {
10259
10556
  if (await exists(options.destinationPath)) {
10260
- const currentContent = await (0, import_promises9.readFile)(options.destinationPath, "utf8");
10557
+ const currentContent = await (0, import_promises10.readFile)(options.destinationPath, "utf8");
10261
10558
  if (options.shouldRepair?.(currentContent) !== true) {
10262
10559
  console.log(`Already exists: ${options.destinationPath}`);
10263
10560
  return;
10264
10561
  }
10265
- const rendered2 = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
10562
+ const rendered2 = renderTemplate(await (0, import_promises10.readFile)(options.templatePath, "utf8"), options.config);
10266
10563
  if (options.dryRun) {
10267
10564
  console.log(`Would repair generated config: ${options.destinationPath}`);
10268
10565
  return;
10269
10566
  }
10270
10567
  const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
10271
- await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10272
- await (0, import_promises9.chmod)(backupPath, 384);
10273
- await (0, import_promises9.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
10274
- await (0, import_promises9.chmod)(options.destinationPath, 384);
10568
+ await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10569
+ await (0, import_promises10.chmod)(backupPath, 384);
10570
+ await (0, import_promises10.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
10571
+ await (0, import_promises10.chmod)(options.destinationPath, 384);
10275
10572
  console.log(`Repaired generated config: ${options.destinationPath}`);
10276
10573
  console.log(`Backup: ${backupPath}`);
10277
10574
  return;
10278
10575
  }
10279
- const rendered = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
10576
+ const rendered = renderTemplate(await (0, import_promises10.readFile)(options.templatePath, "utf8"), options.config);
10280
10577
  if (options.dryRun) {
10281
10578
  console.log(`Would write ${options.destinationPath}`);
10282
10579
  return;
10283
10580
  }
10284
- await ensureDirectory((0, import_node_path8.dirname)(options.destinationPath), false);
10285
- await (0, import_promises9.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
10286
- await (0, import_promises9.chmod)(options.destinationPath, 384);
10581
+ await ensureDirectory((0, import_node_path9.dirname)(options.destinationPath), false);
10582
+ await (0, import_promises10.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
10583
+ await (0, import_promises10.chmod)(options.destinationPath, 384);
10287
10584
  console.log(`Wrote ${options.destinationPath}`);
10288
10585
  }
10289
10586
  async function installCommandShim(dryRun) {
10290
10587
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
10291
- const shimPath = (0, import_node_path8.join)(binDir, "threadnote");
10588
+ const shimPath = (0, import_node_path9.join)(binDir, "threadnote");
10292
10589
  const existingContent = await readFileIfExists(shimPath);
10293
10590
  if (existingContent && !isManagedCommandShim(existingContent)) {
10294
10591
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -10304,12 +10601,12 @@ async function installCommandShim(dryRun) {
10304
10601
  return;
10305
10602
  }
10306
10603
  await ensureDirectory(binDir, false);
10307
- await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
10308
- await (0, import_promises9.chmod)(shimPath, 493);
10604
+ await (0, import_promises10.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
10605
+ await (0, import_promises10.chmod)(shimPath, 493);
10309
10606
  console.log(`Wrote command shim: ${shimPath}`);
10310
10607
  }
10311
10608
  async function removeCommandShim(dryRun) {
10312
- const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10609
+ const shimPath = (0, import_node_path9.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10313
10610
  const content = await readFileIfExists(shimPath);
10314
10611
  if (content === void 0) {
10315
10612
  console.log(`Already absent: ${shimPath}`);
@@ -10343,8 +10640,8 @@ async function installUserAgentInstructions(dryRun) {
10343
10640
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
10344
10641
  continue;
10345
10642
  }
10346
- await ensureDirectory((0, import_node_path8.dirname)(targetPath), false);
10347
- await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10643
+ await ensureDirectory((0, import_node_path9.dirname)(targetPath), false);
10644
+ await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10348
10645
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
10349
10646
  }
10350
10647
  }
@@ -10381,7 +10678,7 @@ async function removeUserAgentInstructions(dryRun) {
10381
10678
  console.log(`Would update ${targetPath}`);
10382
10679
  continue;
10383
10680
  }
10384
- await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10681
+ await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10385
10682
  console.log(`Updated ${targetPath}`);
10386
10683
  }
10387
10684
  }
@@ -10401,7 +10698,7 @@ async function renderUserAgentInstructions(target) {
10401
10698
  ].join("\n");
10402
10699
  }
10403
10700
  async function renderUserAgentInstructionsBlock() {
10404
- const instructions = (await (0, import_promises9.readFile)((0, import_node_path8.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
10701
+ const instructions = (await (0, import_promises10.readFile)((0, import_node_path9.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
10405
10702
  return `${USER_INSTRUCTIONS_START_MARKER}
10406
10703
  ${instructions}
10407
10704
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -10479,9 +10776,9 @@ async function installLaunchAgent(config, dryRun) {
10479
10776
  if ((0, import_node_os6.platform)() !== "darwin") {
10480
10777
  throw new Error("launchd autostart is only supported on macOS.");
10481
10778
  }
10482
- const source = (0, import_node_path8.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
10779
+ const source = (0, import_node_path9.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
10483
10780
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
10484
- const rendered = renderTemplate(await (0, import_promises9.readFile)(source, "utf8"), config);
10781
+ const rendered = renderTemplate(await (0, import_promises10.readFile)(source, "utf8"), config);
10485
10782
  if (dryRun) {
10486
10783
  console.log(`Would write ${destination}`);
10487
10784
  console.log(`Would run: launchctl unload ${destination}`);
@@ -10489,9 +10786,9 @@ async function installLaunchAgent(config, dryRun) {
10489
10786
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
10490
10787
  return;
10491
10788
  }
10492
- await ensureDirectory((0, import_node_path8.dirname)(destination), false);
10493
- await ensureDirectory((0, import_node_path8.dirname)(openVikingLogPath(config)), false);
10494
- await (0, import_promises9.writeFile)(destination, rendered, "utf8");
10789
+ await ensureDirectory((0, import_node_path9.dirname)(destination), false);
10790
+ await ensureDirectory((0, import_node_path9.dirname)(openVikingLogPath(config)), false);
10791
+ await (0, import_promises10.writeFile)(destination, rendered, "utf8");
10495
10792
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
10496
10793
  await maybeRun(false, "launchctl", ["load", destination]);
10497
10794
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
@@ -10554,7 +10851,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
10554
10851
  if (typeof parsed.default_user !== "string") {
10555
10852
  return false;
10556
10853
  }
10557
- if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path8.join)(config.agentContextHome, "data")) {
10854
+ if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path9.join)(config.agentContextHome, "data")) {
10558
10855
  return false;
10559
10856
  }
10560
10857
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -10582,9 +10879,20 @@ async function main() {
10582
10879
  await runDoctor(config, options);
10583
10880
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
10584
10881
  });
10585
- program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).action(async (options) => {
10882
+ program2.command("install").description("Install OpenViking, local config files, command shim, and user-level agent instructions").option("--dry-run", "Print the actions without making changes").option("--no-start", "Do not start OpenViking or check server health after installing").option("--package-manager <manager>", "uv, pipx, or pip", parsePackageManager).option(
10883
+ "--with-hooks",
10884
+ "Also install agent-side hooks (Claude PreCompact + SessionStart) for deterministic handoff snapshots and context preload"
10885
+ ).action(async (options) => {
10586
10886
  const config = getRuntimeConfig(program2);
10587
10887
  await runInstall(config, options);
10888
+ if (options.withHooks === true) {
10889
+ const dryRun = options.dryRun === true;
10890
+ for (const agent of ["claude", "codex", "cursor", "copilot"]) {
10891
+ console.log(`
10892
+ --- ${agent} hooks ---`);
10893
+ await runHooksInstall(config, agent, { apply: !dryRun, dryRun });
10894
+ }
10895
+ }
10588
10896
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
10589
10897
  });
10590
10898
  program2.command("update").description("Update the published Threadnote package, then repair local shims and MCP config").option("--check", "Only check whether a newer version is available").option("--dry-run", "Print update and repair commands without running them").option("--force", "Run package-manager update even if this version is already current").option("--registry <url>", "npm registry URL", process.env.THREADNOTE_NPM_REGISTRY).option("--runtime <runtime>", "auto, npm, bun, or deno", parseUpdateRuntime, "auto").option("--no-repair", "Skip threadnote repair after updating the package").option("--no-post-update", "Skip post-update migration prompts").option("--yes", "Accept applicable post-update migrations without prompting").action(async (options) => {
@@ -10629,6 +10937,21 @@ async function main() {
10629
10937
  program2.command("mcp-install").description("Install OpenViking MCP config for a supported agent").argument("<agent>", "codex, claude, cursor, or copilot").option("--apply", "Actually modify the selected agent config").option("--name <name>", "MCP server name", OPENVIKING_MCP_NAME).option("--native-http", "Install OpenViking native HTTP MCP endpoint instead of the local stdio adapter").option("--scope <scope>", "Claude MCP config scope: user, local, or project", parseClaudeMcpScope, "user").option("--url <url>", "OpenViking native HTTP MCP URL").option("--bearer-token-env-var <name>", "Environment variable containing the local API key").action(async (agent, options) => {
10630
10938
  await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
10631
10939
  });
10940
+ program2.command("install-hooks").description(
10941
+ "Install deterministic agent hooks (Claude PreCompact + SessionStart). Soft instruction files remain the cross-agent guidance surface; hooks add a deterministic safety net where the agent supports it."
10942
+ ).argument("<agent>", "codex, claude, cursor, or copilot").option("--apply", "Actually modify the selected agent config").option("--dry-run", "Print the planned change without applying it").option("--remove", "Remove threadnote-managed hook entries instead of adding them").action(async (agent, options) => {
10943
+ await runHooksInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
10944
+ });
10945
+ program2.command("pre-compact-hook", { hidden: true }).description(
10946
+ "Hook entry point: store a handoff snapshot before context compaction. Used by `install-hooks claude`."
10947
+ ).option("--dry-run", "Print the handoff payload without writing it").action(async (options) => {
10948
+ await runPreCompactHook(getRuntimeConfig(program2), options);
10949
+ });
10950
+ program2.command("session-start-hook", { hidden: true }).description(
10951
+ "Hook entry point: print the latest threadnote handoff/feature memory for the current repo so Claude can preload it."
10952
+ ).option("--dry-run", "Print the planned ov command without running it").action(async (options) => {
10953
+ await runSessionStartHook(getRuntimeConfig(program2), options);
10954
+ });
10632
10955
  program2.command("remember").description("Store a durable engineering memory in OpenViking").option("--dry-run", "Print memory and ov command without storing").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind, "durable").option("--project <name>", "Project/repo/topic namespace for lifecycle-aware storage").option("--replace <uri>", "Supersede an existing viking:// memory after the new memory is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--status <status>", "active, archived, or superseded", parseMemoryStatus, "active").option("--stdin", "Read memory text from stdin").option("--text <text>", "Memory text to store").option("--topic <name>", "Stable topic name; active memories with the same project/topic update one file").action(async (options) => {
10633
10956
  await runRemember(getRuntimeConfig(program2), options);
10634
10957
  });
@@ -10674,7 +10997,13 @@ async function main() {
10674
10997
  share.command("sync").description("Pull, reindex, and push the shared memories repo for a team").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message when auto-committing local edits").option("--no-auto-commit", "Refuse to sync if there are uncommitted local changes").option("--no-push", "Skip the push step after pulling and reindexing").option("--dry-run", "Print actions without running them").action(async (options) => {
10675
10998
  await runShareSync(getRuntimeConfig(program2), options);
10676
10999
  });
10677
- share.command("publish").description("Move a personal memory into the shared team namespace, commit and push").argument("<viking-uri>", "viking:// memory URI to publish").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").action(async (uri, options) => {
11000
+ share.command("publish").description("Move a personal memory into the shared team namespace, commit and push").argument("<viking-uri>", "viking:// memory URI to publish").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").option(
11001
+ "--preview",
11002
+ "Print the exact bytes that would land in the shared git repo (after frontmatter strip and scrubber redaction) without writing, committing, or pushing"
11003
+ ).option(
11004
+ "--redact",
11005
+ "Replace soft-leak matches (local paths) with placeholders and continue; credentials still block"
11006
+ ).action(async (uri, options) => {
10678
11007
  await runSharePublish(getRuntimeConfig(program2), uri, options);
10679
11008
  });
10680
11009
  share.command("unpublish").description("Pull a shared memory back into the personal namespace, commit removal and push").argument("<viking-uri>", "viking:// memory URI inside a team shared subtree").option("--team <name>", "Team name; defaults to the configured default team").option("--message <text>", "Commit message override").option("--no-push", "Skip the push step").option("--dry-run", "Print actions without running them").action(async (uri, options) => {