threadnote 0.4.1 → 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,18 +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");
9166
9370
  var import_node_process2 = require("node:process");
9167
- var import_promises10 = require("node:readline/promises");
9371
+ var import_promises11 = require("node:readline/promises");
9168
9372
 
9169
9373
  // src/update.ts
9170
9374
  var import_node_fs4 = require("node:fs");
9171
- var import_promises7 = require("node:fs/promises");
9375
+ var import_promises8 = require("node:fs/promises");
9172
9376
  var import_node_os5 = require("node:os");
9173
- var import_node_path7 = require("node:path");
9174
- var import_promises8 = require("node:readline/promises");
9377
+ var import_node_path8 = require("node:path");
9378
+ var import_promises9 = require("node:readline/promises");
9175
9379
  var import_node_process = require("node:process");
9176
9380
  var NPM_PACKAGE_NAME = "threadnote";
9177
9381
  var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
@@ -9319,7 +9523,7 @@ async function getUpdateInfo(config, options) {
9319
9523
  };
9320
9524
  }
9321
9525
  async function currentPackageVersion() {
9322
- 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");
9323
9527
  const parsed = JSON.parse(rawPackage);
9324
9528
  if (!isJsonObject(parsed) || typeof parsed.version !== "string") {
9325
9529
  throw new Error("Could not read current threadnote package version.");
@@ -9369,11 +9573,11 @@ async function readFreshCache(config, registry) {
9369
9573
  }
9370
9574
  async function writeUpdateCache(config, cache) {
9371
9575
  await ensureDirectory(config.agentContextHome, false);
9372
- 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)}
9373
9577
  `, { encoding: "utf8", mode: 384 });
9374
9578
  }
9375
9579
  function updateCachePath(config) {
9376
- return (0, import_node_path7.join)(config.agentContextHome, "update-check.json");
9580
+ return (0, import_node_path8.join)(config.agentContextHome, "update-check.json");
9377
9581
  }
9378
9582
  async function runApplicablePostUpdateMigrations(config, options) {
9379
9583
  const state = await readPostUpdateState(config);
@@ -9437,7 +9641,7 @@ async function applicablePostUpdateMigrations(config, options) {
9437
9641
  return applicable;
9438
9642
  }
9439
9643
  async function readPostUpdateMigrations() {
9440
- 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));
9441
9645
  if (!raw) {
9442
9646
  return [];
9443
9647
  }
@@ -9476,7 +9680,7 @@ function printPostUpdateMigration(migration) {
9476
9680
  }
9477
9681
  }
9478
9682
  async function confirmPostUpdateMigration(prompt) {
9479
- 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 });
9480
9684
  try {
9481
9685
  const answer = (await readline.question(prompt)).trim().toLowerCase();
9482
9686
  return answer === "y" || answer === "yes";
@@ -9508,11 +9712,11 @@ async function readPostUpdateState(config) {
9508
9712
  }
9509
9713
  async function writePostUpdateState(config, state) {
9510
9714
  await ensureDirectory(config.agentContextHome, false);
9511
- 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)}
9512
9716
  `, { encoding: "utf8", mode: 384 });
9513
9717
  }
9514
9718
  function postUpdateStatePath(config) {
9515
- 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);
9516
9720
  }
9517
9721
  async function resolveUpdateRuntime(runtime) {
9518
9722
  if (runtime !== "auto") {
@@ -9542,18 +9746,18 @@ async function runtimeThreadnoteBin(runtime) {
9542
9746
  if (runtime === "npm") {
9543
9747
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
9544
9748
  const prefix = result.stdout.trim();
9545
- 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;
9546
9750
  }
9547
9751
  if (runtime === "bun") {
9548
9752
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
9549
9753
  const binDir = result.stdout.trim();
9550
- 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;
9551
9755
  }
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);
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);
9553
9757
  }
9554
9758
  async function isExecutable(path) {
9555
9759
  try {
9556
- 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);
9557
9761
  return true;
9558
9762
  } catch (_err) {
9559
9763
  return false;
@@ -9644,9 +9848,9 @@ async function runDoctor(config, options) {
9644
9848
  checks.push(await commandShimCheck());
9645
9849
  checks.push(...await userAgentInstructionsChecks());
9646
9850
  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"));
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"));
9650
9854
  checks.push(await healthCheck(config));
9651
9855
  for (const check of checks) {
9652
9856
  console.log(`${formatStatus(check.status)} ${check.name}: ${check.detail}`);
@@ -9663,9 +9867,9 @@ async function runInstall(config, options) {
9663
9867
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
9664
9868
  const dryRun = options.dryRun === true;
9665
9869
  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);
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);
9669
9873
  await installCommandShim(dryRun);
9670
9874
  await installUserAgentInstructions(dryRun);
9671
9875
  const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
@@ -9691,17 +9895,17 @@ async function runInstall(config, options) {
9691
9895
  }
9692
9896
  await writeTemplateIfMissing({
9693
9897
  config,
9694
- destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ov.conf"),
9898
+ destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ov.conf"),
9695
9899
  dryRun,
9696
9900
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
9697
- 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")
9698
9902
  });
9699
9903
  await writeTemplateIfMissing({
9700
9904
  config,
9701
- destinationPath: (0, import_node_path8.join)(config.agentContextHome, "ovcli.conf"),
9905
+ destinationPath: (0, import_node_path9.join)(config.agentContextHome, "ovcli.conf"),
9702
9906
  dryRun,
9703
9907
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
9704
- 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")
9705
9909
  });
9706
9910
  if (options.start !== false) {
9707
9911
  const healthy = await repairServerHealth(config, dryRun);
@@ -9738,6 +9942,10 @@ async function runRepair(config, options) {
9738
9942
  await runMcpInstall(config, client, { apply: !dryRun, name: OPENVIKING_MCP_NAME });
9739
9943
  }
9740
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
+ }
9741
9949
  console.log("\nPost-repair doctor:");
9742
9950
  await runDoctor(config, { dryRun, strict: false });
9743
9951
  if (options.postUpdate !== false) {
@@ -9751,10 +9959,13 @@ async function runUninstall(config, options) {
9751
9959
  }
9752
9960
  console.log("Uninstalling local Threadnote setup.");
9753
9961
  await runStop(config, { dryRun });
9754
- 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);
9755
9963
  await removeLaunchAgent(dryRun);
9756
9964
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
9757
9965
  await removeMcpSnippets(config, dryRun);
9966
+ if (await hasManagedClaudeHooks()) {
9967
+ await runHooksInstall(config, "claude", { apply: !dryRun, dryRun, remove: true });
9968
+ }
9758
9969
  await removeCommandShim(dryRun);
9759
9970
  await removeUserAgentInstructions(dryRun);
9760
9971
  if (options.eraseMemories === true) {
@@ -9805,16 +10016,16 @@ async function repairManifest(config, dryRun) {
9805
10016
  console.log(output2.trimEnd());
9806
10017
  return;
9807
10018
  }
9808
- await ensureDirectory((0, import_node_path8.dirname)(config.manifestPath), false);
10019
+ await ensureDirectory((0, import_node_path9.dirname)(config.manifestPath), false);
9809
10020
  const currentContent = await readFileIfExists(config.manifestPath);
9810
10021
  if (currentContent !== void 0) {
9811
10022
  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);
10023
+ await (0, import_promises10.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
10024
+ await (0, import_promises10.chmod)(backupPath, 384);
9814
10025
  console.log(`Backup: ${backupPath}`);
9815
10026
  }
9816
- await (0, import_promises9.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
9817
- 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);
9818
10029
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
9819
10030
  }
9820
10031
  async function repairServerHealth(config, dryRun) {
@@ -9855,7 +10066,7 @@ async function runStart(config, options) {
9855
10066
  );
9856
10067
  }
9857
10068
  const logPath = openVikingLogPath(config);
9858
- await ensureDirectory((0, import_node_path8.dirname)(logPath), false);
10069
+ await ensureDirectory((0, import_node_path9.dirname)(logPath), false);
9859
10070
  if (options.foreground === true) {
9860
10071
  const result = await runInteractive(server, args);
9861
10072
  process.exitCode = result;
@@ -9864,7 +10075,7 @@ async function runStart(config, options) {
9864
10075
  const logFd = (0, import_node_fs5.openSync)(logPath, "a");
9865
10076
  const child = spawnDetachedServerWithLog(server, args, logFd);
9866
10077
  child.unref();
9867
- 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}
9868
10079
  `, "utf8");
9869
10080
  const health = await waitForOpenVikingHealth(config, START_HEALTH_TIMEOUT_MS);
9870
10081
  if (health) {
@@ -9897,7 +10108,7 @@ async function runStop(config, options) {
9897
10108
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
9898
10109
  }
9899
10110
  }
9900
- 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");
9901
10112
  const pidText = await readFileIfExists(pidPath);
9902
10113
  if (!pidText) {
9903
10114
  console.log("No pid file found for detached OpenViking server.");
@@ -9993,7 +10204,7 @@ async function pythonSystemCertificatesCheck() {
9993
10204
  };
9994
10205
  }
9995
10206
  async function commandShimCheck() {
9996
- 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");
9997
10208
  const content = await readFileIfExists(shimPath);
9998
10209
  if (content === void 0) {
9999
10210
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -10059,11 +10270,11 @@ async function hasPythonModule(serverPath, moduleName) {
10059
10270
  async function siblingPythonForExecutable(executablePath) {
10060
10271
  let resolvedPath;
10061
10272
  try {
10062
- resolvedPath = await (0, import_promises9.realpath)(executablePath);
10273
+ resolvedPath = await (0, import_promises10.realpath)(executablePath);
10063
10274
  } catch (_err) {
10064
10275
  return void 0;
10065
10276
  }
10066
- 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");
10067
10278
  return await exists(pythonPath) ? pythonPath : void 0;
10068
10279
  }
10069
10280
  async function manifestCheck(path) {
@@ -10134,7 +10345,7 @@ async function offerToInstallUv() {
10134
10345
  );
10135
10346
  return false;
10136
10347
  }
10137
- const readline = (0, import_promises10.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
10348
+ const readline = (0, import_promises11.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
10138
10349
  let answer;
10139
10350
  try {
10140
10351
  answer = (await readline.question(
@@ -10257,38 +10468,38 @@ function printInstallNextSteps(options) {
10257
10468
  }
10258
10469
  async function writeTemplateIfMissing(options) {
10259
10470
  if (await exists(options.destinationPath)) {
10260
- const currentContent = await (0, import_promises9.readFile)(options.destinationPath, "utf8");
10471
+ const currentContent = await (0, import_promises10.readFile)(options.destinationPath, "utf8");
10261
10472
  if (options.shouldRepair?.(currentContent) !== true) {
10262
10473
  console.log(`Already exists: ${options.destinationPath}`);
10263
10474
  return;
10264
10475
  }
10265
- 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);
10266
10477
  if (options.dryRun) {
10267
10478
  console.log(`Would repair generated config: ${options.destinationPath}`);
10268
10479
  return;
10269
10480
  }
10270
10481
  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);
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);
10275
10486
  console.log(`Repaired generated config: ${options.destinationPath}`);
10276
10487
  console.log(`Backup: ${backupPath}`);
10277
10488
  return;
10278
10489
  }
10279
- 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);
10280
10491
  if (options.dryRun) {
10281
10492
  console.log(`Would write ${options.destinationPath}`);
10282
10493
  return;
10283
10494
  }
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);
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);
10287
10498
  console.log(`Wrote ${options.destinationPath}`);
10288
10499
  }
10289
10500
  async function installCommandShim(dryRun) {
10290
10501
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
10291
- const shimPath = (0, import_node_path8.join)(binDir, "threadnote");
10502
+ const shimPath = (0, import_node_path9.join)(binDir, "threadnote");
10292
10503
  const existingContent = await readFileIfExists(shimPath);
10293
10504
  if (existingContent && !isManagedCommandShim(existingContent)) {
10294
10505
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -10304,12 +10515,12 @@ async function installCommandShim(dryRun) {
10304
10515
  return;
10305
10516
  }
10306
10517
  await ensureDirectory(binDir, false);
10307
- await (0, import_promises9.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
10308
- 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);
10309
10520
  console.log(`Wrote command shim: ${shimPath}`);
10310
10521
  }
10311
10522
  async function removeCommandShim(dryRun) {
10312
- 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");
10313
10524
  const content = await readFileIfExists(shimPath);
10314
10525
  if (content === void 0) {
10315
10526
  console.log(`Already absent: ${shimPath}`);
@@ -10343,8 +10554,8 @@ async function installUserAgentInstructions(dryRun) {
10343
10554
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
10344
10555
  continue;
10345
10556
  }
10346
- await ensureDirectory((0, import_node_path8.dirname)(targetPath), false);
10347
- 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 });
10348
10559
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
10349
10560
  }
10350
10561
  }
@@ -10381,7 +10592,7 @@ async function removeUserAgentInstructions(dryRun) {
10381
10592
  console.log(`Would update ${targetPath}`);
10382
10593
  continue;
10383
10594
  }
10384
- await (0, import_promises9.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10595
+ await (0, import_promises10.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
10385
10596
  console.log(`Updated ${targetPath}`);
10386
10597
  }
10387
10598
  }
@@ -10401,7 +10612,7 @@ async function renderUserAgentInstructions(target) {
10401
10612
  ].join("\n");
10402
10613
  }
10403
10614
  async function renderUserAgentInstructionsBlock() {
10404
- 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();
10405
10616
  return `${USER_INSTRUCTIONS_START_MARKER}
10406
10617
  ${instructions}
10407
10618
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -10479,9 +10690,9 @@ async function installLaunchAgent(config, dryRun) {
10479
10690
  if ((0, import_node_os6.platform)() !== "darwin") {
10480
10691
  throw new Error("launchd autostart is only supported on macOS.");
10481
10692
  }
10482
- 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`);
10483
10694
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
10484
- const rendered = renderTemplate(await (0, import_promises9.readFile)(source, "utf8"), config);
10695
+ const rendered = renderTemplate(await (0, import_promises10.readFile)(source, "utf8"), config);
10485
10696
  if (dryRun) {
10486
10697
  console.log(`Would write ${destination}`);
10487
10698
  console.log(`Would run: launchctl unload ${destination}`);
@@ -10489,9 +10700,9 @@ async function installLaunchAgent(config, dryRun) {
10489
10700
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
10490
10701
  return;
10491
10702
  }
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");
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");
10495
10706
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
10496
10707
  await maybeRun(false, "launchctl", ["load", destination]);
10497
10708
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
@@ -10554,7 +10765,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
10554
10765
  if (typeof parsed.default_user !== "string") {
10555
10766
  return false;
10556
10767
  }
10557
- 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")) {
10558
10769
  return false;
10559
10770
  }
10560
10771
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -10582,9 +10793,20 @@ async function main() {
10582
10793
  await runDoctor(config, options);
10583
10794
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
10584
10795
  });
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) => {
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) => {
10586
10800
  const config = getRuntimeConfig(program2);
10587
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
+ }
10588
10810
  await maybeNotifyUpdate(config, { dryRun: options.dryRun === true });
10589
10811
  });
10590
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) => {
@@ -10629,6 +10851,21 @@ async function main() {
10629
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) => {
10630
10852
  await runMcpInstall(getRuntimeConfig(program2), parseAgentClient(agent), options);
10631
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
+ });
10632
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) => {
10633
10870
  await runRemember(getRuntimeConfig(program2), options);
10634
10871
  });