threadnote 0.4.0 → 0.5.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)) {
@@ -8083,9 +8095,201 @@ function formatBlock(value, emptyValue) {
8083
8095
  return trimmed.split("\n").map((line) => `- ${line}`).join("\n");
8084
8096
  }
8085
8097
 
8098
+ // src/hooks.ts
8099
+ var MANAGED_HOOKS = [
8100
+ {
8101
+ event: "PreCompact",
8102
+ command: HOOK_PRE_COMPACT_COMMAND,
8103
+ description: "Auto-store a handoff snapshot before context compaction so the next turn can recall it."
8104
+ },
8105
+ {
8106
+ event: "SessionStart",
8107
+ command: HOOK_SESSION_START_COMMAND,
8108
+ description: "Pre-load the latest handoff for the current repo into the new session context."
8109
+ }
8110
+ ];
8111
+ async function runHooksInstall(config, agent, options) {
8112
+ const apply = options.apply === true && options.dryRun !== true;
8113
+ const remove = options.remove === true;
8114
+ switch (agent) {
8115
+ case "claude":
8116
+ await runClaudeHooksInstall({ apply, remove });
8117
+ return;
8118
+ case "codex":
8119
+ printCodexHooksNotice(remove);
8120
+ return;
8121
+ case "cursor":
8122
+ printNoHooksSupported("cursor", remove);
8123
+ return;
8124
+ case "copilot":
8125
+ printNoHooksSupported("copilot", remove);
8126
+ return;
8127
+ }
8128
+ }
8129
+ async function runClaudeHooksInstall(options) {
8130
+ const path = expandPath(CLAUDE_SETTINGS_PATH);
8131
+ const existingRaw = await exists(path) ? await (0, import_promises5.readFile)(path, "utf8") : "{}";
8132
+ const parsed = parseJsonConfigObject(existingRaw) ?? {};
8133
+ const next = options.remove ? withoutThreadnoteHooks(parsed) : withThreadnoteHooks(parsed);
8134
+ const before = JSON.stringify(parsed);
8135
+ const after = JSON.stringify(next);
8136
+ if (before === after) {
8137
+ console.log(`Claude hooks already ${options.remove ? "absent" : "managed"} in ${path}.`);
8138
+ return;
8139
+ }
8140
+ console.log(`${options.apply ? "Updating" : "Would update"} ${path}:`);
8141
+ for (const entry of MANAGED_HOOKS) {
8142
+ console.log(` ${options.remove ? "-" : "+"} ${entry.event}: ${entry.command}`);
8143
+ }
8144
+ if (!options.apply) {
8145
+ console.log("\nRe-run with --apply to actually modify the file.");
8146
+ return;
8147
+ }
8148
+ await (0, import_promises5.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
8149
+ const serialized = `${JSON.stringify(next, void 0, 2)}
8150
+ `;
8151
+ await (0, import_promises5.writeFile)(path, serialized, { encoding: "utf8", mode: 384 });
8152
+ await (0, import_promises5.chmod)(path, 384);
8153
+ console.log(`${options.remove ? "Removed" : "Installed"} threadnote-managed Claude hooks.`);
8154
+ }
8155
+ function withThreadnoteHooks(input2) {
8156
+ const hooks = ensureMutableObject(input2.hooks);
8157
+ for (const entry of MANAGED_HOOKS) {
8158
+ const list = ensureMutableArray(hooks[entry.event]).filter((item) => !isManagedThreadnoteEntry(item));
8159
+ list.push({
8160
+ [THREADNOTE_HOOK_MARKER]: THREADNOTE_HOOK_MARKER_VALUE,
8161
+ matcher: "",
8162
+ hooks: [{ type: "command", command: entry.command }]
8163
+ });
8164
+ hooks[entry.event] = list;
8165
+ }
8166
+ return { ...input2, hooks };
8167
+ }
8168
+ function withoutThreadnoteHooks(input2) {
8169
+ if (!isJsonObject(input2.hooks)) {
8170
+ return input2;
8171
+ }
8172
+ const hooks = {};
8173
+ for (const [event, value] of Object.entries(input2.hooks)) {
8174
+ if (!Array.isArray(value)) {
8175
+ hooks[event] = value;
8176
+ continue;
8177
+ }
8178
+ const filtered = value.filter((item) => !isManagedThreadnoteEntry(item));
8179
+ if (filtered.length > 0) {
8180
+ hooks[event] = filtered;
8181
+ }
8182
+ }
8183
+ if (Object.keys(hooks).length === 0) {
8184
+ const next = { ...input2 };
8185
+ delete next.hooks;
8186
+ return next;
8187
+ }
8188
+ return { ...input2, hooks };
8189
+ }
8190
+ function isManagedThreadnoteEntry(value) {
8191
+ return isJsonObject(value) && value[THREADNOTE_HOOK_MARKER] === THREADNOTE_HOOK_MARKER_VALUE;
8192
+ }
8193
+ function ensureMutableObject(value) {
8194
+ return isJsonObject(value) ? { ...value } : {};
8195
+ }
8196
+ function ensureMutableArray(value) {
8197
+ return Array.isArray(value) ? [...value] : [];
8198
+ }
8199
+ function printCodexHooksNotice(remove) {
8200
+ if (remove) {
8201
+ console.log("Codex CLI does not expose a managed hook surface today, so there is nothing to remove.");
8202
+ return;
8203
+ }
8204
+ console.log(
8205
+ [
8206
+ "Codex CLI does not currently expose lifecycle hooks (no PreCompact or SessionStart analog).",
8207
+ "Threadnote already installs Codex user instructions at ~/.codex/AGENTS.md; that remains the active guidance surface.",
8208
+ "If a future Codex release adds hook events, threadnote will pick them up via `install-hooks codex`."
8209
+ ].join("\n")
8210
+ );
8211
+ }
8212
+ function printNoHooksSupported(agent, remove) {
8213
+ if (remove) {
8214
+ console.log(`${agent} does not expose a hook surface; nothing to remove.`);
8215
+ return;
8216
+ }
8217
+ console.log(
8218
+ [
8219
+ `${agent} does not expose agent-mode hooks today.`,
8220
+ "Threadnote already installs user-level instructions for this agent; that remains the active guidance surface.",
8221
+ "Hooks support will be added if the agent gains a hook surface upstream."
8222
+ ].join("\n")
8223
+ );
8224
+ }
8225
+ async function hasManagedClaudeHooks() {
8226
+ const path = expandPath(CLAUDE_SETTINGS_PATH);
8227
+ if (!await exists(path)) {
8228
+ return false;
8229
+ }
8230
+ const raw = await (0, import_promises5.readFile)(path, "utf8");
8231
+ const parsed = parseJsonConfigObject(raw);
8232
+ if (!parsed || !isJsonObject(parsed.hooks)) {
8233
+ return false;
8234
+ }
8235
+ for (const value of Object.values(parsed.hooks)) {
8236
+ if (!Array.isArray(value)) {
8237
+ continue;
8238
+ }
8239
+ if (value.some((item) => isManagedThreadnoteEntry(item))) {
8240
+ return true;
8241
+ }
8242
+ }
8243
+ return false;
8244
+ }
8245
+ async function runPreCompactHook(config, options = {}) {
8246
+ try {
8247
+ const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
8248
+ const project = repoRoot ? (0, import_node_path5.basename)(repoRoot) : "general";
8249
+ await runHandoff(config, {
8250
+ blockers: "- none recorded",
8251
+ dryRun: options.dryRun === true,
8252
+ nextStep: "Continue from this auto-snapshot. A manual `threadnote handoff` will produce a richer write-up if you have more context.",
8253
+ project,
8254
+ sourceAgentClient: "claude",
8255
+ task: "Auto-snapshot captured at Claude PreCompact (deterministic safety net before context compaction).",
8256
+ tests: "- not recorded (auto-snapshot)",
8257
+ topic: HOOK_AUTO_PRECOMPACT_TOPIC
8258
+ });
8259
+ } catch (err) {
8260
+ process.stderr.write(
8261
+ `threadnote pre-compact-hook: snapshot skipped (${err instanceof Error ? err.message : String(err)})
8262
+ `
8263
+ );
8264
+ }
8265
+ }
8266
+ async function runSessionStartHook(config, options = {}) {
8267
+ try {
8268
+ const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]);
8269
+ if (!repoRoot) {
8270
+ return;
8271
+ }
8272
+ const project = (0, import_node_path5.basename)(repoRoot);
8273
+ process.stdout.write(`## Threadnote \u2014 latest context for ${project}
8274
+
8275
+ `);
8276
+ await runRecall(config, {
8277
+ dryRun: options.dryRun === true,
8278
+ inferScope: true,
8279
+ nodeLimit: "5",
8280
+ query: `${project} latest handoff durable feature memory`
8281
+ });
8282
+ } catch (err) {
8283
+ process.stderr.write(
8284
+ `threadnote session-start-hook: recall skipped (${err instanceof Error ? err.message : String(err)})
8285
+ `
8286
+ );
8287
+ }
8288
+ }
8289
+
8086
8290
  // src/seeding.ts
8087
- var import_promises5 = require("node:fs/promises");
8088
- var import_node_path5 = require("node:path");
8291
+ var import_promises6 = require("node:fs/promises");
8292
+ var import_node_path6 = require("node:path");
8089
8293
  async function runSeed(config, options) {
8090
8294
  const manifest = await readSeedManifest(config.manifestPath);
8091
8295
  const ignorePatterns = await loadIgnorePatterns();
@@ -8114,7 +8318,7 @@ async function runSeed(config, options) {
8114
8318
  }
8115
8319
  async function runInitManifest(config, options) {
8116
8320
  const manifestPath = expandPath(
8117
- options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path5.join)(config.agentContextHome, USER_MANIFEST_NAME)
8321
+ options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path6.join)(config.agentContextHome, USER_MANIFEST_NAME)
8118
8322
  );
8119
8323
  const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
8120
8324
  const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
@@ -8157,9 +8361,9 @@ async function runInitManifest(config, options) {
8157
8361
  console.log(output2.trimEnd());
8158
8362
  return;
8159
8363
  }
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);
8364
+ await ensureDirectory((0, import_node_path6.dirname)(manifestPath), false);
8365
+ await (0, import_promises6.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
8366
+ await (0, import_promises6.chmod)(manifestPath, 384);
8163
8367
  console.log(`Wrote manifest: ${manifestPath}`);
8164
8368
  console.log("Seed with:");
8165
8369
  console.log(" threadnote seed --dry-run");
@@ -8189,13 +8393,13 @@ async function resolveRepoRoot(repoInput) {
8189
8393
  async function projectIdentity(path) {
8190
8394
  const expanded = expandPath(path);
8191
8395
  try {
8192
- return await (0, import_promises5.realpath)(expanded);
8396
+ return await (0, import_promises6.realpath)(expanded);
8193
8397
  } catch (_err) {
8194
8398
  return expanded;
8195
8399
  }
8196
8400
  }
8197
8401
  function projectManifestForRepo(repoRoot, existingProjects) {
8198
- const baseName = uriSegment((0, import_node_path5.basename)(repoRoot));
8402
+ const baseName = uriSegment((0, import_node_path6.basename)(repoRoot));
8199
8403
  const usedNames = new Set(existingProjects.map((project) => project.name));
8200
8404
  const usedUris = new Set(existingProjects.map((project) => project.uri));
8201
8405
  let name = baseName;
@@ -8217,7 +8421,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
8217
8421
  for (const pattern of project.seed) {
8218
8422
  const files = await resolveProjectPattern(projectRoot, pattern);
8219
8423
  for (const filePath of files) {
8220
- const relativePath = toPosixPath((0, import_node_path5.relative)(projectRoot, filePath));
8424
+ const relativePath = toPosixPath((0, import_node_path6.relative)(projectRoot, filePath));
8221
8425
  if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
8222
8426
  continue;
8223
8427
  }
@@ -8235,20 +8439,20 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
8235
8439
  async function resolveProjectPattern(projectRoot, pattern) {
8236
8440
  const normalizedPattern = toPosixPath(pattern);
8237
8441
  if (!hasGlob(normalizedPattern)) {
8238
- const filePath = (0, import_node_path5.join)(projectRoot, normalizedPattern);
8442
+ const filePath = (0, import_node_path6.join)(projectRoot, normalizedPattern);
8239
8443
  return await isFile(filePath) ? [filePath] : [];
8240
8444
  }
8241
8445
  const globBase = getGlobBase(normalizedPattern);
8242
- const basePath = (0, import_node_path5.join)(projectRoot, globBase);
8446
+ const basePath = (0, import_node_path6.join)(projectRoot, globBase);
8243
8447
  if (!await exists(basePath)) {
8244
8448
  return [];
8245
8449
  }
8246
8450
  const regex = globToRegExp(normalizedPattern);
8247
8451
  const files = await walkFiles(basePath);
8248
- return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path5.relative)(projectRoot, filePath))));
8452
+ return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path6.relative)(projectRoot, filePath))));
8249
8453
  }
8250
8454
  async function prepareSeedFile(config, candidate, dryRun) {
8251
- const content = await (0, import_promises5.readFile)(candidate.filePath, "utf8");
8455
+ const content = await (0, import_promises6.readFile)(candidate.filePath, "utf8");
8252
8456
  const redactedContent = shouldRedactPath(candidate.relativePath) ? redactContent(candidate.relativePath, content) : content;
8253
8457
  const secretMatches = detectSecretMatches(redactedContent);
8254
8458
  if (secretMatches.length > 0) {
@@ -8260,14 +8464,14 @@ async function prepareSeedFile(config, candidate, dryRun) {
8260
8464
  if (redactedContent === content) {
8261
8465
  return candidate.filePath;
8262
8466
  }
8263
- const redactedPath = (0, import_node_path5.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
8467
+ const redactedPath = (0, import_node_path6.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
8264
8468
  if (dryRun) {
8265
8469
  console.log(`Would write redacted copy: ${redactedPath}`);
8266
8470
  return redactedPath;
8267
8471
  }
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);
8472
+ await ensureDirectory((0, import_node_path6.dirname)(redactedPath), false);
8473
+ await (0, import_promises6.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
8474
+ await (0, import_promises6.chmod)(redactedPath, 384);
8271
8475
  return redactedPath;
8272
8476
  }
8273
8477
  async function collectSkillCandidates(config) {
@@ -8292,7 +8496,7 @@ async function collectSkillCandidates(config) {
8292
8496
  for (const source of sources) {
8293
8497
  const files = await resolveAbsolutePattern(expandPath(source.pattern));
8294
8498
  for (const filePath of files) {
8295
- const content = await (0, import_promises5.readFile)(filePath, "utf8");
8499
+ const content = await (0, import_promises6.readFile)(filePath, "utf8");
8296
8500
  const matches = detectSecretMatches(content);
8297
8501
  if (matches.length > 0) {
8298
8502
  console.log(`SKIP skill with possible secret: ${filePath}`);
@@ -8323,10 +8527,10 @@ async function resolveAbsolutePattern(pattern) {
8323
8527
  return files.filter((filePath) => regex.test(toPosixPath(filePath)));
8324
8528
  }
8325
8529
  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`;
8530
+ 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
8531
  }
8328
8532
  async function loadIgnorePatterns() {
8329
- const raw = await (0, import_promises5.readFile)((0, import_node_path5.join)(toolRoot(), ".threadnoteignore"), "utf8");
8533
+ const raw = await (0, import_promises6.readFile)((0, import_node_path6.join)(toolRoot(), ".threadnoteignore"), "utf8");
8330
8534
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8331
8535
  }
8332
8536
  function matchesIgnore(relativePath, patterns) {
@@ -8397,9 +8601,9 @@ function detectSecretMatches(content) {
8397
8601
  }
8398
8602
 
8399
8603
  // src/share.ts
8400
- var import_promises6 = require("node:fs/promises");
8604
+ var import_promises7 = require("node:fs/promises");
8401
8605
  var import_node_os4 = require("node:os");
8402
- var import_node_path6 = require("node:path");
8606
+ var import_node_path7 = require("node:path");
8403
8607
  var TEAMS_FILE_VERSION = 1;
8404
8608
  var SHARED_SEGMENT = "shared";
8405
8609
  var SHAREABLE_MEMORY_KIND_DIRS = ["durable"];
@@ -8437,8 +8641,8 @@ async function runShareInit(config, remoteUrl, options) {
8437
8641
  if (await exists(gitdir)) {
8438
8642
  throw new Error(`Gitdir already exists at ${gitdir}; remove it or pick a different team name.`);
8439
8643
  }
8440
- await ensureDirectory((0, import_node_path6.dirname)(worktree), dryRun);
8441
- await ensureDirectory((0, import_node_path6.dirname)(gitdir), dryRun);
8644
+ await ensureDirectory((0, import_node_path7.dirname)(worktree), dryRun);
8645
+ await ensureDirectory((0, import_node_path7.dirname)(gitdir), dryRun);
8442
8646
  const git = await requiredExecutable("git");
8443
8647
  await maybeRun(dryRun, git, ["clone", `--separate-git-dir=${gitdir}`, "--", remoteUrl, worktree]);
8444
8648
  const newConfig = {
@@ -8469,7 +8673,7 @@ async function runShareInit(config, remoteUrl, options) {
8469
8673
  var SHARED_GITIGNORE_PATTERNS = ["**/.abstract.md", "**/.overview.md"];
8470
8674
  var SHARED_GITIGNORE_HEADER = "# Threadnote: ignore OpenViking-generated directory summaries.";
8471
8675
  async function ensureSharedGitignore(worktree, git, push) {
8472
- const gitignorePath = (0, import_node_path6.join)(worktree, ".gitignore");
8676
+ const gitignorePath = (0, import_node_path7.join)(worktree, ".gitignore");
8473
8677
  const existing = await readFileIfExists(gitignorePath) ?? "";
8474
8678
  const lines = existing.split("\n").map((line) => line.trim());
8475
8679
  const missingPatterns = SHARED_GITIGNORE_PATTERNS.filter((pattern) => !lines.includes(pattern));
@@ -8488,7 +8692,7 @@ async function ensureSharedGitignore(worktree, git, push) {
8488
8692
  segments.push(SHARED_GITIGNORE_HEADER, "\n");
8489
8693
  }
8490
8694
  segments.push(missingPatterns.join("\n"), "\n");
8491
- await (0, import_promises6.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
8695
+ await (0, import_promises7.writeFile)(gitignorePath, `${existing}${segments.join("")}`, { encoding: "utf8" });
8492
8696
  console.log(`Added ${missingPatterns.join(", ")} to ${portablePath(gitignorePath)}`);
8493
8697
  await maybeRun(false, git, ["-C", worktree, "add", ".gitignore"]);
8494
8698
  const commitResult = await runCommand(
@@ -8553,7 +8757,7 @@ async function runShareSync(config, options) {
8553
8757
  if (dryRun) {
8554
8758
  console.log(`Would run: ${formatShellCommand(git, ["-C", worktree, "pull", "--rebase", DEFAULT_GIT_REMOTE_NAME])}`);
8555
8759
  } 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"))) {
8760
+ 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
8761
  throw new Error(
8558
8762
  `git pull --rebase reported conflicts in ${worktree}. The worktree is in a rebase-in-progress state.
8559
8763
  Resolve the conflicts in-place, run \`git -C ${worktree} rebase --continue\` (or --abort), then re-run \`threadnote share sync\`.`
@@ -8704,10 +8908,10 @@ function normalizeTeamName(input2) {
8704
8908
  return candidate;
8705
8909
  }
8706
8910
  function teamsFilePath(config) {
8707
- return (0, import_node_path6.join)(config.agentContextHome, "share", "teams.json");
8911
+ return (0, import_node_path7.join)(config.agentContextHome, "share", "teams.json");
8708
8912
  }
8709
8913
  function teamWorktreePath(config, team) {
8710
- return (0, import_node_path6.join)(
8914
+ return (0, import_node_path7.join)(
8711
8915
  config.agentContextHome,
8712
8916
  "data",
8713
8917
  "viking",
@@ -8720,7 +8924,7 @@ function teamWorktreePath(config, team) {
8720
8924
  );
8721
8925
  }
8722
8926
  function teamGitdirPath(config, team) {
8723
- return (0, import_node_path6.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
8927
+ return (0, import_node_path7.join)(config.agentContextHome, "share", "teams", `${team}.gitdir`);
8724
8928
  }
8725
8929
  async function readTeamsFile(config) {
8726
8930
  const path = teamsFilePath(config);
@@ -8763,13 +8967,13 @@ async function readTeamsFile(config) {
8763
8967
  }
8764
8968
  async function writeTeamsFile(config, contents) {
8765
8969
  const path = teamsFilePath(config);
8766
- await (0, import_promises6.mkdir)((0, import_node_path6.dirname)(path), { recursive: true });
8970
+ await (0, import_promises7.mkdir)((0, import_node_path7.dirname)(path), { recursive: true });
8767
8971
  const serializable = {
8768
8972
  defaultTeam: contents.defaultTeam,
8769
8973
  teams: contents.teams,
8770
8974
  version: contents.version
8771
8975
  };
8772
- await (0, import_promises6.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
8976
+ await (0, import_promises7.writeFile)(path, `${JSON.stringify(serializable, void 0, 2)}
8773
8977
  `, { encoding: "utf8", mode: 384 });
8774
8978
  }
8775
8979
  async function resolveTeam(config, requested) {
@@ -8799,7 +9003,7 @@ async function assertWorktreeUsable(worktree) {
8799
9003
  if (!await isDirectory(worktree)) {
8800
9004
  throw new Error(`Cannot use ${worktree} as a worktree: not a directory.`);
8801
9005
  }
8802
- const entries = await (0, import_promises6.readdir)(worktree);
9006
+ const entries = await (0, import_promises7.readdir)(worktree);
8803
9007
  if (entries.length > 0) {
8804
9008
  const preview = entries.slice(0, 5).join(", ");
8805
9009
  const suffix = entries.length > 5 ? `, +${entries.length - 5} more` : "";
@@ -8823,7 +9027,7 @@ async function walkMemoryFiles(root) {
8823
9027
  async function visit(path, depth) {
8824
9028
  let entries;
8825
9029
  try {
8826
- entries = await (0, import_promises6.readdir)(path, { withFileTypes: true });
9030
+ entries = await (0, import_promises7.readdir)(path, { withFileTypes: true });
8827
9031
  } catch (err) {
8828
9032
  console.warn(`Skipping ${path} during shared-tree walk: ${err instanceof Error ? err.message : String(err)}`);
8829
9033
  return;
@@ -8832,7 +9036,7 @@ async function walkMemoryFiles(root) {
8832
9036
  if (entry.name === ".git") {
8833
9037
  continue;
8834
9038
  }
8835
- const full = (0, import_node_path6.join)(path, entry.name);
9039
+ const full = (0, import_node_path7.join)(path, entry.name);
8836
9040
  if (entry.isDirectory()) {
8837
9041
  if (depth === 0 && !SHAREABLE_MEMORY_KIND_DIRS.includes(entry.name)) {
8838
9042
  continue;
@@ -8856,7 +9060,7 @@ async function walkMemoryFiles(root) {
8856
9060
  return out;
8857
9061
  }
8858
9062
  function workfileToVikingUri(config, team, filePath) {
8859
- const rel = (0, import_node_path6.relative)(team.worktree, filePath).split(import_node_path6.sep).join("/");
9063
+ const rel = (0, import_node_path7.relative)(team.worktree, filePath).split(import_node_path7.sep).join("/");
8860
9064
  return `viking://user/${uriSegment(config.user)}/memories/${SHARED_SEGMENT}/${team.name}/${rel}`;
8861
9065
  }
8862
9066
  function isInSharedNamespace(config, uri) {
@@ -8950,13 +9154,13 @@ async function writeMemoryFile(config, ov, uri, content, initialMode, dryRun) {
8950
9154
  console.log(`Would run: ${formatShellCommand(ov, args)}`);
8951
9155
  return;
8952
9156
  }
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");
9157
+ const stagingDir = await (0, import_promises7.mkdtemp)((0, import_node_path7.join)((0, import_node_os4.tmpdir)(), "threadnote-share-"));
9158
+ const tempPath = (0, import_node_path7.join)(stagingDir, "body.txt");
8955
9159
  try {
8956
- await (0, import_promises6.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
9160
+ await (0, import_promises7.writeFile)(tempPath, content, { encoding: "utf8", mode: 384 });
8957
9161
  await writeOvFileWithRetry(config, ov, uri, tempPath, initialMode);
8958
9162
  } finally {
8959
- await (0, import_promises6.rm)(stagingDir, { force: true, recursive: true });
9163
+ await (0, import_promises7.rm)(stagingDir, { force: true, recursive: true });
8960
9164
  }
8961
9165
  }
8962
9166
  async function writeOvFileWithRetry(config, ov, uri, fromFile, initialMode) {
@@ -9014,7 +9218,7 @@ ${stdout}`.toLowerCase();
9014
9218
  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
9219
  }
9016
9220
  async function ingestSingleFile(ov, config, uri, filePath, initialMode) {
9017
- const content = await (0, import_promises6.readFile)(filePath, "utf8");
9221
+ const content = await (0, import_promises7.readFile)(filePath, "utf8");
9018
9222
  await writeMemoryFile(config, ov, uri, content, initialMode, false);
9019
9223
  }
9020
9224
  async function removeWithRollback(config, ov, sourceUri, rollbackUri, worktree, dryRun, label) {
@@ -9056,7 +9260,7 @@ async function bestEffortRemoveWorktreeFile(rollbackUri, worktree, label) {
9056
9260
  if (!relative3) {
9057
9261
  return;
9058
9262
  }
9059
- await (0, import_promises6.rm)((0, import_node_path6.join)(worktree, relative3), { force: true });
9263
+ await (0, import_promises7.rm)((0, import_node_path7.join)(worktree, relative3), { force: true });
9060
9264
  }
9061
9265
  async function removeMemoryUri(config, ov, uri, dryRun) {
9062
9266
  const args = withIdentity(config, ["rm", uri]);
@@ -9113,8 +9317,8 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
9113
9317
  const oldRel = entries[index + 1];
9114
9318
  const newRel = entries[index + 2];
9115
9319
  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" });
9320
+ changes.push({ path: (0, import_node_path7.join)(worktree, oldRel), relativePath: oldRel, status: "removed" });
9321
+ changes.push({ path: (0, import_node_path7.join)(worktree, newRel), relativePath: newRel, status: "added" });
9118
9322
  }
9119
9323
  index += 3;
9120
9324
  continue;
@@ -9122,7 +9326,7 @@ async function listChangedFiles(worktree, beforeRev, afterRev) {
9122
9326
  const rel = entries[index + 1];
9123
9327
  if (rel) {
9124
9328
  const status = head === "A" ? "added" : head === "D" ? "removed" : "modified";
9125
- changes.push({ path: (0, import_node_path6.join)(worktree, rel), relativePath: rel, status });
9329
+ changes.push({ path: (0, import_node_path7.join)(worktree, rel), relativePath: rel, status });
9126
9330
  }
9127
9331
  index += 2;
9128
9332
  }
@@ -9160,16 +9364,18 @@ async function applyChangesToOpenViking(config, team, changes) {
9160
9364
  // src/lifecycle.ts
9161
9365
  var import_node_child_process2 = require("node:child_process");
9162
9366
  var import_node_fs5 = require("node:fs");
9163
- var import_promises9 = require("node:fs/promises");
9367
+ var import_promises10 = require("node:fs/promises");
9164
9368
  var import_node_os6 = require("node:os");
9165
- var import_node_path8 = require("node:path");
9369
+ var import_node_path9 = require("node:path");
9370
+ var import_node_process2 = require("node:process");
9371
+ var import_promises11 = require("node:readline/promises");
9166
9372
 
9167
9373
  // src/update.ts
9168
9374
  var import_node_fs4 = require("node:fs");
9169
- var import_promises7 = require("node:fs/promises");
9375
+ var import_promises8 = require("node:fs/promises");
9170
9376
  var import_node_os5 = require("node:os");
9171
- var import_node_path7 = require("node:path");
9172
- var import_promises8 = require("node:readline/promises");
9377
+ var import_node_path8 = require("node:path");
9378
+ var import_promises9 = require("node:readline/promises");
9173
9379
  var import_node_process = require("node:process");
9174
9380
  var NPM_PACKAGE_NAME = "threadnote";
9175
9381
  var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
@@ -9317,7 +9523,7 @@ async function getUpdateInfo(config, options) {
9317
9523
  };
9318
9524
  }
9319
9525
  async function currentPackageVersion() {
9320
- const rawPackage = await (0, import_promises7.readFile)((0, import_node_path7.join)(toolRoot(), "package.json"), "utf8");
9526
+ const rawPackage = await (0, import_promises8.readFile)((0, import_node_path8.join)(toolRoot(), "package.json"), "utf8");
9321
9527
  const parsed = JSON.parse(rawPackage);
9322
9528
  if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
9323
9529
  throw new Error("Could not read current threadnote package version.");
@@ -9367,11 +9573,11 @@ async function readFreshCache(config, registry) {
9367
9573
  }
9368
9574
  async function writeUpdateCache(config, cache) {
9369
9575
  await ensureDirectory(config.agentContextHome, false);
9370
- await (0, import_promises7.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
9576
+ await (0, import_promises8.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
9371
9577
  `, { encoding: "utf8", mode: 384 });
9372
9578
  }
9373
9579
  function updateCachePath(config) {
9374
- return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
9580
+ return (0, import_node_path8.join)(config.agentContextHome, "update-check.json");
9375
9581
  }
9376
9582
  async function runApplicablePostUpdateMigrations(config, options) {
9377
9583
  const state = await readPostUpdateState(config);
@@ -9435,7 +9641,7 @@ async function applicablePostUpdateMigrations(config, options) {
9435
9641
  return applicable;
9436
9642
  }
9437
9643
  async function readPostUpdateMigrations() {
9438
- const raw = await readFileIfExists((0, import_node_path7.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
9644
+ const raw = await readFileIfExists((0, import_node_path8.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
9439
9645
  if (!raw) {
9440
9646
  return [];
9441
9647
  }
@@ -9474,7 +9680,7 @@ function printPostUpdateMigration(migration) {
9474
9680
  }
9475
9681
  }
9476
9682
  async function confirmPostUpdateMigration(prompt) {
9477
- const readline = (0, import_promises8.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
9683
+ const readline = (0, import_promises9.createInterface)({ input: import_node_process.stdin, output: import_node_process.stdout });
9478
9684
  try {
9479
9685
  const answer = (await readline.question(prompt)).trim().toLowerCase();
9480
9686
  return answer === "y" || answer === "yes";
@@ -9506,11 +9712,11 @@ async function readPostUpdateState(config) {
9506
9712
  }
9507
9713
  async function writePostUpdateState(config, state) {
9508
9714
  await ensureDirectory(config.agentContextHome, false);
9509
- await (0, import_promises7.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
9715
+ await (0, import_promises8.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
9510
9716
  `, { encoding: "utf8", mode: 384 });
9511
9717
  }
9512
9718
  function postUpdateStatePath(config) {
9513
- return (0, import_node_path7.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
9719
+ return (0, import_node_path8.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
9514
9720
  }
9515
9721
  async function resolveUpdateRuntime(runtime) {
9516
9722
  if (runtime !== "auto") {
@@ -9540,18 +9746,18 @@ async function runtimeThreadnoteBin(runtime) {
9540
9746
  if (runtime === "npm") {
9541
9747
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
9542
9748
  const prefix = result.stdout.trim();
9543
- return prefix ? (0, import_node_path7.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
9749
+ return prefix ? (0, import_node_path8.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
9544
9750
  }
9545
9751
  if (runtime === "bun") {
9546
9752
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
9547
9753
  const binDir = result.stdout.trim();
9548
- return binDir ? (0, import_node_path7.join)(binDir, NPM_PACKAGE_NAME) : void 0;
9754
+ return binDir ? (0, import_node_path8.join)(binDir, NPM_PACKAGE_NAME) : void 0;
9549
9755
  }
9550
- 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);
9756
+ 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);
9551
9757
  }
9552
9758
  async function isExecutable(path) {
9553
9759
  try {
9554
- await (0, import_promises7.access)(path, import_node_fs4.constants.X_OK);
9760
+ await (0, import_promises8.access)(path, import_node_fs4.constants.X_OK);
9555
9761
  return true;
9556
9762
  } catch (_err) {
9557
9763
  return false;
@@ -9642,9 +9848,9 @@ async function runDoctor(config, options) {
9642
9848
  checks.push(await commandShimCheck());
9643
9849
  checks.push(...await userAgentInstructionsChecks());
9644
9850
  checks.push(await manifestCheck(config.manifestPath));
9645
- checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
9646
- checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
9647
- checks.push(await fileCheck((0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
9851
+ checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
9852
+ checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
9853
+ checks.push(await fileCheck((0, import_node_path9.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
9648
9854
  checks.push(await healthCheck(config));
9649
9855
  for (const check of checks) {
9650
9856
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -9661,9 +9867,9 @@ async function runInstall(config, options) {
9661
9867
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
9662
9868
  const dryRun = options.dryRun === true;
9663
9869
  await ensureDirectory(config.agentContextHome, dryRun);
9664
- await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "logs"), dryRun);
9665
- await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "redacted"), dryRun);
9666
- await ensureDirectory((0, import_node_path8.join)(config.agentContextHome, "mcp"), dryRun);
9870
+ await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "logs"), dryRun);
9871
+ await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "redacted"), dryRun);
9872
+ await ensureDirectory((0, import_node_path9.join)(config.agentContextHome, "mcp"), dryRun);
9667
9873
  await installCommandShim(dryRun);
9668
9874
  await installUserAgentInstructions(dryRun);
9669
9875
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
@@ -9689,17 +9895,17 @@ async function runInstall(config, options) {
9689
9895
  }
9690
9896
  await writeTemplateIfMissing({
9691
9897
  config,
9692
- destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ov.conf"),
9898
+ destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ov.conf"),
9693
9899
  dryRun,
9694
9900
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
9695
- templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ov.conf.template.json")
9901
+ templatePath: (0, import_node_path9.join)(toolRoot(), "config", "ov.conf.template.json")
9696
9902
  });
9697
9903
  await writeTemplateIfMissing({
9698
9904
  config,
9699
- destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ovcli.conf"),
9905
+ destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ovcli.conf"),
9700
9906
  dryRun,
9701
9907
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
9702
- templatePath: (0, import_node_path8.join)(toolRoot(), "config", "ovcli.conf.template.json")
9908
+ templatePath: (0, import_node_path9.join)(toolRoot(), "config", "ovcli.conf.template.json")
9703
9909
  });
9704
9910
  if (options.start !== false) {
9705
9911
  const healthy = await repairServerHealth(config, dryRun);
@@ -9736,6 +9942,10 @@ async function runRepair(config, options) {
9736
9942
  await runMcpInstall(config, client, { apply: !dryRun, name: OPENVIKING_MCP_NAME });
9737
9943
  }
9738
9944
  }
9945
+ if (await hasManagedClaudeHooks()) {
9946
+ console.log("\nRepairing claude hooks (re-asserting threadnote-managed entries).");
9947
+ await runHooksInstall(config, "claude", { apply: !dryRun, dryRun });
9948
+ }
9739
9949
  console.log("\nPost-repair doctor:");
9740
9950
  await runDoctor(config, { dryRun, strict: false });
9741
9951
  if (options.postUpdate !== false) {
@@ -9749,10 +9959,13 @@ async function runUninstall(config, options) {
9749
9959
  }
9750
9960
  console.log("Uninstalling local Threadnote setup.");
9751
9961
  await runStop(config, { dryRun });
9752
- await removePathIfExists((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
9962
+ await removePathIfExists((0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
9753
9963
  await removeLaunchAgent(dryRun);
9754
9964
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
9755
9965
  await removeMcpSnippets(config, dryRun);
9966
+ if (await hasManagedClaudeHooks()) {
9967
+ await runHooksInstall(config, "claude", { apply: !dryRun, dryRun, remove: true });
9968
+ }
9756
9969
  await removeCommandShim(dryRun);
9757
9970
  await removeUserAgentInstructions(dryRun);
9758
9971
  if (options.eraseMemories === true) {
@@ -9803,16 +10016,16 @@ async function repairManifest(config, dryRun) {
9803
10016
  console.log(output2.trimEnd());
9804
10017
  return;
9805
10018
  }
9806
- await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
10019
+ await ensureDirectory((0, import_node_path9.dirname)(config.manifestPath), false);
9807
10020
  const currentContent = await readFileIfExists(config.manifestPath);
9808
10021
  if (currentContent !== void 0) {
9809
10022
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
9810
- await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
9811
- await (0, import_promises9.chmod)(backupPath, 384);
10023
+ await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10024
+ await (0, import_promises10.chmod)(backupPath, 384);
9812
10025
  console.log(`Backup: ${backupPath}`);
9813
10026
  }
9814
- await (0, import_promises9.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
9815
- await (0, import_promises9.chmod)(config.manifestPath, 384);
10027
+ await (0, import_promises10.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
10028
+ await (0, import_promises10.chmod)(config.manifestPath, 384);
9816
10029
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
9817
10030
  }
9818
10031
  async function repairServerHealth(config, dryRun) {
@@ -9853,7 +10066,7 @@ async function runStart(config, options) {
9853
10066
  );
9854
10067
  }
9855
10068
  const logPath = openVikingLogPath(config);
9856
- await ensureDirectory((0, import_node_path8.dirname)(logPath), false);
10069
+ await ensureDirectory((0, import_node_path9.dirname)(logPath), false);
9857
10070
  if (options.foreground === true) {
9858
10071
  const result = await runInteractive(server, args);
9859
10072
  process.exitCode = result;
@@ -9862,7 +10075,7 @@ async function runStart(config, options) {
9862
10075
  const logFd = (0, import_node_fs5.openSync)(logPath, "a");
9863
10076
  const child = spawnDetachedServerWithLog(server, args, logFd);
9864
10077
  child.unref();
9865
- await (0, import_promises9.writeFile)((0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
10078
+ await (0, import_promises10.writeFile)((0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
9866
10079
  `, "utf8");
9867
10080
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
9868
10081
  if (health) {
@@ -9895,7 +10108,7 @@ async function runStop(config, options) {
9895
10108
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
9896
10109
  }
9897
10110
  }
9898
- const pidPath = (0, import_node_path8.join)(config.agentContextHome, "openviking-server.pid");
10111
+ const pidPath = (0, import_node_path9.join)(config.agentContextHome, "openviking-server.pid");
9899
10112
  const pidText = await readFileIfExists(pidPath);
9900
10113
  if (!pidText) {
9901
10114
  console.log("No pid file found for detached OpenViking server.");
@@ -9991,7 +10204,7 @@ async function pythonSystemCertificatesCheck() {
9991
10204
  };
9992
10205
  }
9993
10206
  async function commandShimCheck() {
9994
- const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10207
+ const shimPath = (0, import_node_path9.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
9995
10208
  const content = await readFileIfExists(shimPath);
9996
10209
  if (content === void 0) {
9997
10210
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -10057,11 +10270,11 @@ async function hasPythonModule(serverPath, moduleName) {
10057
10270
  async function siblingPythonForExecutable(executablePath) {
10058
10271
  let resolvedPath;
10059
10272
  try {
10060
- resolvedPath = await (0, import_promises9.realpath)(executablePath);
10273
+ resolvedPath = await (0, import_promises10.realpath)(executablePath);
10061
10274
  } catch (_err) {
10062
10275
  return void 0;
10063
10276
  }
10064
- const pythonPath = (0, import_node_path8.join)((0, import_node_path8.dirname)(resolvedPath), "python");
10277
+ const pythonPath = (0, import_node_path9.join)((0, import_node_path9.dirname)(resolvedPath), "python");
10065
10278
  return await exists(pythonPath) ? pythonPath : void 0;
10066
10279
  }
10067
10280
  async function manifestCheck(path) {
@@ -10110,11 +10323,83 @@ async function waitForOpenVikingHealth(config, timeoutMs) {
10110
10323
  return void 0;
10111
10324
  }
10112
10325
  async function runInstallCommands(config, preferred, force, dryRun) {
10113
- const installCommands = await getInstallCommands(config, preferred, force);
10326
+ let manager = preferred;
10327
+ if (manager === void 0 && !dryRun) {
10328
+ const detected = await detectPackageManager();
10329
+ if (detected === "pip" && await offerToInstallUv()) {
10330
+ const rediscovered = await detectPackageManager();
10331
+ if (rediscovered === "uv") {
10332
+ manager = "uv";
10333
+ }
10334
+ }
10335
+ }
10336
+ const installCommands = await getInstallCommands(config, manager, force);
10114
10337
  for (const installCommand of installCommands) {
10115
10338
  await maybeRun(dryRun, installCommand.executable, installCommand.args);
10116
10339
  }
10117
10340
  }
10341
+ async function offerToInstallUv() {
10342
+ if (import_node_process2.stdin.isTTY !== true || import_node_process2.stdout.isTTY !== true) {
10343
+ console.warn(
10344
+ "Neither uv nor pipx was found on PATH. Falling back to `python3 -m pip install --user`, which fails on PEP 668 (Homebrew/system) Python.\nRe-run with --package-manager uv after installing uv (brew install uv), or pass --package-manager pipx."
10345
+ );
10346
+ return false;
10347
+ }
10348
+ const readline = (0, import_promises11.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
10349
+ let answer;
10350
+ try {
10351
+ answer = (await readline.question(
10352
+ "OpenViking installs into Python; neither uv nor pipx is on PATH so threadnote would fall back to `pip install --user`, which fails on PEP 668 setups.\nInstall uv now? [Y/n] "
10353
+ )).trim().toLowerCase();
10354
+ } finally {
10355
+ readline.close();
10356
+ }
10357
+ if (answer === "n" || answer === "no") {
10358
+ console.log("Continuing with `python3 -m pip install --user`. You may hit PEP 668 errors on managed Pythons.");
10359
+ return false;
10360
+ }
10361
+ return await installUv();
10362
+ }
10363
+ async function installUv() {
10364
+ const brew = await findExecutable(["brew"]);
10365
+ if (brew) {
10366
+ console.log("Installing uv via Homebrew...");
10367
+ const result = await runCommand(brew, ["install", "uv"], { allowFailure: true });
10368
+ if (result.exitCode === 0) {
10369
+ if (result.stdout.trim()) {
10370
+ console.log(result.stdout.trim());
10371
+ }
10372
+ if (await findExecutable(["uv"])) {
10373
+ return true;
10374
+ }
10375
+ } else {
10376
+ console.warn(`brew install uv failed: ${(result.stderr || result.stdout).trim()}`);
10377
+ }
10378
+ }
10379
+ if (await findExecutable(["curl"]) && await findExecutable(["sh"])) {
10380
+ console.log("Installing uv via the official install script (curl -LsSf https://astral.sh/uv/install.sh | sh)...");
10381
+ const result = await runCommand("sh", ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"], {
10382
+ allowFailure: true
10383
+ });
10384
+ if (result.exitCode === 0) {
10385
+ if (result.stdout.trim()) {
10386
+ console.log(result.stdout.trim());
10387
+ }
10388
+ if (await findExecutable(["uv"])) {
10389
+ return true;
10390
+ }
10391
+ console.warn(
10392
+ "uv installed, but the new binary is not yet on this shell PATH. Open a new shell (or `source ~/.zshrc` / `source ~/.bashrc`) and re-run `threadnote install`."
10393
+ );
10394
+ return false;
10395
+ }
10396
+ console.warn(`uv install script failed: ${(result.stderr || result.stdout).trim()}`);
10397
+ }
10398
+ console.warn(
10399
+ "Could not install uv automatically. Install it manually (brew install uv) and re-run threadnote install."
10400
+ );
10401
+ return false;
10402
+ }
10118
10403
  async function getPythonSystemCertificatesInstallCommand(serverPath) {
10119
10404
  const pythonPath = await siblingPythonForExecutable(serverPath);
10120
10405
  if (!pythonPath) {
@@ -10183,38 +10468,38 @@ function printInstallNextSteps(options) {
10183
10468
  }
10184
10469
  async function writeTemplateIfMissing(options) {
10185
10470
  if (await exists(options.destinationPath)) {
10186
- const currentContent = await (0, import_promises9.readFile)(options.destinationPath, "utf8");
10471
+ const currentContent = await (0, import_promises10.readFile)(options.destinationPath, "utf8");
10187
10472
  if (options.shouldRepair?.(currentContent) !== true) {
10188
10473
  console.log(`Already exists: ${options.destinationPath}`);
10189
10474
  return;
10190
10475
  }
10191
- const rendered2 = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
10476
+ const rendered2 = renderTemplate(await (0, import_promises10.readFile)(options.templatePath, "utf8"), options.config);
10192
10477
  if (options.dryRun) {
10193
10478
  console.log(`Would repair generated config: ${options.destinationPath}`);
10194
10479
  return;
10195
10480
  }
10196
10481
  const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
10197
- await (0, import_promises9.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10198
- await (0, import_promises9.chmod)(backupPath, 384);
10199
- await (0, import_promises9.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
10200
- await (0, import_promises9.chmod)(options.destinationPath, 384);
10482
+ await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10483
+ await (0, import_promises10.chmod)(backupPath, 384);
10484
+ await (0, import_promises10.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
10485
+ await (0, import_promises10.chmod)(options.destinationPath, 384);
10201
10486
  console.log(`Repaired generated config: ${options.destinationPath}`);
10202
10487
  console.log(`Backup: ${backupPath}`);
10203
10488
  return;
10204
10489
  }
10205
- const rendered = renderTemplate(await (0, import_promises9.readFile)(options.templatePath, "utf8"), options.config);
10490
+ const rendered = renderTemplate(await (0, import_promises10.readFile)(options.templatePath, "utf8"), options.config);
10206
10491
  if (options.dryRun) {
10207
10492
  console.log(`Would write ${options.destinationPath}`);
10208
10493
  return;
10209
10494
  }
10210
- await ensureDirectory((0, import_node_path8.dirname)(options.destinationPath), false);
10211
- await (0, import_promises9.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
10212
- await (0, import_promises9.chmod)(options.destinationPath, 384);
10495
+ await ensureDirectory((0, import_node_path9.dirname)(options.destinationPath), false);
10496
+ await (0, import_promises10.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
10497
+ await (0, import_promises10.chmod)(options.destinationPath, 384);
10213
10498
  console.log(`Wrote ${options.destinationPath}`);
10214
10499
  }
10215
10500
  async function installCommandShim(dryRun) {
10216
10501
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
10217
- const shimPath = (0, import_node_path8.join)(binDir, "threadnote");
10502
+ const shimPath = (0, import_node_path9.join)(binDir, "threadnote");
10218
10503
  const existingContent = await readFileIfExists(shimPath);
10219
10504
  if (existingContent && !isManagedCommandShim(existingContent)) {
10220
10505
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -10230,12 +10515,12 @@ async function installCommandShim(dryRun) {
10230
10515
  return;
10231
10516
  }
10232
10517
  await ensureDirectory(binDir, false);
10233
- await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
10234
- await (0, import_promises9.chmod)(shimPath, 493);
10518
+ await (0, import_promises10.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
10519
+ await (0, import_promises10.chmod)(shimPath, 493);
10235
10520
  console.log(`Wrote command shim: ${shimPath}`);
10236
10521
  }
10237
10522
  async function removeCommandShim(dryRun) {
10238
- const shimPath = (0, import_node_path8.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10523
+ const shimPath = (0, import_node_path9.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
10239
10524
  const content = await readFileIfExists(shimPath);
10240
10525
  if (content === void 0) {
10241
10526
  console.log(`Already absent: ${shimPath}`);
@@ -10269,8 +10554,8 @@ async function installUserAgentInstructions(dryRun) {
10269
10554
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
10270
10555
  continue;
10271
10556
  }
10272
- await ensureDirectory((0, import_node_path8.dirname)(targetPath), false);
10273
- await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10557
+ await ensureDirectory((0, import_node_path9.dirname)(targetPath), false);
10558
+ await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10274
10559
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
10275
10560
  }
10276
10561
  }
@@ -10307,7 +10592,7 @@ async function removeUserAgentInstructions(dryRun) {
10307
10592
  console.log(`Would update ${targetPath}`);
10308
10593
  continue;
10309
10594
  }
10310
- await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10595
+ await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10311
10596
  console.log(`Updated ${targetPath}`);
10312
10597
  }
10313
10598
  }
@@ -10327,7 +10612,7 @@ async function renderUserAgentInstructions(target) {
10327
10612
  ].join("\n");
10328
10613
  }
10329
10614
  async function renderUserAgentInstructionsBlock() {
10330
- const instructions = (await (0, import_promises9.readFile)((0, import_node_path8.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
10615
+ const instructions = (await (0, import_promises10.readFile)((0, import_node_path9.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
10331
10616
  return `${USER_INSTRUCTIONS_START_MARKER}
10332
10617
  ${instructions}
10333
10618
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -10405,9 +10690,9 @@ async function installLaunchAgent(config, dryRun) {
10405
10690
  if ((0, import_node_os6.platform)() !== "darwin") {
10406
10691
  throw new Error("launchd autostart is only supported on macOS.");
10407
10692
  }
10408
- const source = (0, import_node_path8.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
10693
+ const source = (0, import_node_path9.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
10409
10694
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
10410
- const rendered = renderTemplate(await (0, import_promises9.readFile)(source, "utf8"), config);
10695
+ const rendered = renderTemplate(await (0, import_promises10.readFile)(source, "utf8"), config);
10411
10696
  if (dryRun) {
10412
10697
  console.log(`Would write ${destination}`);
10413
10698
  console.log(`Would run: launchctl unload ${destination}`);
@@ -10415,9 +10700,9 @@ async function installLaunchAgent(config, dryRun) {
10415
10700
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
10416
10701
  return;
10417
10702
  }
10418
- await ensureDirectory((0, import_node_path8.dirname)(destination), false);
10419
- await ensureDirectory((0, import_node_path8.dirname)(openVikingLogPath(config)), false);
10420
- await (0, import_promises9.writeFile)(destination, rendered, "utf8");
10703
+ await ensureDirectory((0, import_node_path9.dirname)(destination), false);
10704
+ await ensureDirectory((0, import_node_path9.dirname)(openVikingLogPath(config)), false);
10705
+ await (0, import_promises10.writeFile)(destination, rendered, "utf8");
10421
10706
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
10422
10707
  await maybeRun(false, "launchctl", ["load", destination]);
10423
10708
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
@@ -10480,7 +10765,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
10480
10765
  if (typeof parsed.default_user !== "string") {
10481
10766
  return false;
10482
10767
  }
10483
- if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path8.join)(config.agentContextHome, "data")) {
10768
+ if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path9.join)(config.agentContextHome, "data")) {
10484
10769
  return false;
10485
10770
  }
10486
10771
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -10508,9 +10793,20 @@ async function main() {
10508
10793
  await runDoctor(config, options);
10509
10794
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
10510
10795
  });
10511
- 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) => {
10796
+ 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(
10797
+ "--with-hooks",
10798
+ "Also install agent-side hooks (Claude PreCompact + SessionStart) for deterministic handoff snapshots and context preload"
10799
+ ).action(async (options) => {
10512
10800
  const config = getRuntimeConfig(program2);
10513
10801
  await runInstall(config, options);
10802
+ if (options.withHooks === true) {
10803
+ const dryRun = options.dryRun === true;
10804
+ for (const agent of ["claude", "codex", "cursor", "copilot"]) {
10805
+ console.log(`
10806
+ --- ${agent} hooks ---`);
10807
+ await runHooksInstall(config, agent, { apply: !dryRun, dryRun });
10808
+ }
10809
+ }
10514
10810
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
10515
10811
  });
10516
10812
  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) => {
@@ -10555,6 +10851,21 @@ async function main() {
10555
10851
  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) => {
10556
10852
  await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
10557
10853
  });
10854
+ program2.command("install-hooks").description(
10855
+ "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."
10856
+ ).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) => {
10857
+ await runHooksInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
10858
+ });
10859
+ program2.command("pre-compact-hook", { hidden: true }).description(
10860
+ "Hook entry point: store a handoff snapshot before context compaction. Used by `install-hooks claude`."
10861
+ ).option("--dry-run", "Print the handoff payload without writing it").action(async (options) => {
10862
+ await runPreCompactHook(getRuntimeConfig(program2), options);
10863
+ });
10864
+ program2.command("session-start-hook", { hidden: true }).description(
10865
+ "Hook entry point: print the latest threadnote handoff/feature memory for the current repo so Claude can preload it."
10866
+ ).option("--dry-run", "Print the planned ov command without running it").action(async (options) => {
10867
+ await runSessionStartHook(getRuntimeConfig(program2), options);
10868
+ });
10558
10869
  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) => {
10559
10870
  await runRemember(getRuntimeConfig(program2), options);
10560
10871
  });