chat-recall 0.6.5 → 0.6.7

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.
package/dist/cli.js CHANGED
@@ -2034,7 +2034,7 @@ var require_thread_stream = __commonJS({
2034
2034
  var { version: version2 } = require_package();
2035
2035
  var { EventEmitter } = __require("events");
2036
2036
  var { Worker: Worker2 } = __require("worker_threads");
2037
- var { join: join59 } = __require("path");
2037
+ var { join: join62 } = __require("path");
2038
2038
  var { pathToFileURL } = __require("url");
2039
2039
  var { wait } = require_wait();
2040
2040
  var {
@@ -2077,7 +2077,7 @@ var require_thread_stream = __commonJS({
2077
2077
  function createWorker(stream, opts) {
2078
2078
  const { filename, workerData } = opts;
2079
2079
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
2080
- const toExecute = bundlerOverrides["thread-stream-worker"] || join59(__dirname, "lib", "worker.js");
2080
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join62(__dirname, "lib", "worker.js");
2081
2081
  const worker = new Worker2(toExecute, {
2082
2082
  ...opts.workerOpts,
2083
2083
  trackUnmanagedFds: false,
@@ -2480,7 +2480,7 @@ var require_transport = __commonJS({
2480
2480
  "use strict";
2481
2481
  var { createRequire: createRequire3 } = __require("module");
2482
2482
  var getCallers = require_caller();
2483
- var { join: join59, isAbsolute: isAbsolute4, sep: sep4 } = __require("node:path");
2483
+ var { join: join62, isAbsolute: isAbsolute4, sep: sep4 } = __require("node:path");
2484
2484
  var sleep2 = require_atomic_sleep();
2485
2485
  var onExit = require_on_exit_leak_free();
2486
2486
  var ThreadStream = require_thread_stream();
@@ -2543,7 +2543,7 @@ var require_transport = __commonJS({
2543
2543
  throw new Error("only one of target or targets can be specified");
2544
2544
  }
2545
2545
  if (targets) {
2546
- target = bundlerOverrides["pino-worker"] || join59(__dirname, "worker.js");
2546
+ target = bundlerOverrides["pino-worker"] || join62(__dirname, "worker.js");
2547
2547
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
2548
2548
  return {
2549
2549
  ...dest,
@@ -2561,7 +2561,7 @@ var require_transport = __commonJS({
2561
2561
  });
2562
2562
  });
2563
2563
  } else if (pipeline) {
2564
- target = bundlerOverrides["pino-worker"] || join59(__dirname, "worker.js");
2564
+ target = bundlerOverrides["pino-worker"] || join62(__dirname, "worker.js");
2565
2565
  options.pipelines = [pipeline.map((dest) => {
2566
2566
  return {
2567
2567
  ...dest,
@@ -2583,7 +2583,7 @@ var require_transport = __commonJS({
2583
2583
  return origin;
2584
2584
  }
2585
2585
  if (origin === "pino/file") {
2586
- return join59(__dirname, "..", "file.js");
2586
+ return join62(__dirname, "..", "file.js");
2587
2587
  }
2588
2588
  let fixTarget2;
2589
2589
  for (const filePath of callers) {
@@ -3572,7 +3572,7 @@ var require_safe_stable_stringify = __commonJS({
3572
3572
  return circularValue;
3573
3573
  }
3574
3574
  let res = "";
3575
- let join59 = ",";
3575
+ let join62 = ",";
3576
3576
  const originalIndentation = indentation;
3577
3577
  if (Array.isArray(value)) {
3578
3578
  if (value.length === 0) {
@@ -3586,7 +3586,7 @@ var require_safe_stable_stringify = __commonJS({
3586
3586
  indentation += spacer;
3587
3587
  res += `
3588
3588
  ${indentation}`;
3589
- join59 = `,
3589
+ join62 = `,
3590
3590
  ${indentation}`;
3591
3591
  }
3592
3592
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -3594,13 +3594,13 @@ ${indentation}`;
3594
3594
  for (; i < maximumValuesToStringify - 1; i++) {
3595
3595
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
3596
3596
  res += tmp2 !== void 0 ? tmp2 : "null";
3597
- res += join59;
3597
+ res += join62;
3598
3598
  }
3599
3599
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
3600
3600
  res += tmp !== void 0 ? tmp : "null";
3601
3601
  if (value.length - 1 > maximumBreadth) {
3602
3602
  const removedKeys = value.length - maximumBreadth - 1;
3603
- res += `${join59}"... ${getItemCount(removedKeys)} not stringified"`;
3603
+ res += `${join62}"... ${getItemCount(removedKeys)} not stringified"`;
3604
3604
  }
3605
3605
  if (spacer !== "") {
3606
3606
  res += `
@@ -3621,7 +3621,7 @@ ${originalIndentation}`;
3621
3621
  let separator = "";
3622
3622
  if (spacer !== "") {
3623
3623
  indentation += spacer;
3624
- join59 = `,
3624
+ join62 = `,
3625
3625
  ${indentation}`;
3626
3626
  whitespace = " ";
3627
3627
  }
@@ -3635,13 +3635,13 @@ ${indentation}`;
3635
3635
  const tmp = stringifyFnReplacer(key3, value, stack, replacer, spacer, indentation);
3636
3636
  if (tmp !== void 0) {
3637
3637
  res += `${separator}${strEscape(key3)}:${whitespace}${tmp}`;
3638
- separator = join59;
3638
+ separator = join62;
3639
3639
  }
3640
3640
  }
3641
3641
  if (keyLength > maximumBreadth) {
3642
3642
  const removedKeys = keyLength - maximumBreadth;
3643
3643
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
3644
- separator = join59;
3644
+ separator = join62;
3645
3645
  }
3646
3646
  if (spacer !== "" && separator.length > 1) {
3647
3647
  res = `
@@ -3682,7 +3682,7 @@ ${originalIndentation}`;
3682
3682
  }
3683
3683
  const originalIndentation = indentation;
3684
3684
  let res = "";
3685
- let join59 = ",";
3685
+ let join62 = ",";
3686
3686
  if (Array.isArray(value)) {
3687
3687
  if (value.length === 0) {
3688
3688
  return "[]";
@@ -3695,7 +3695,7 @@ ${originalIndentation}`;
3695
3695
  indentation += spacer;
3696
3696
  res += `
3697
3697
  ${indentation}`;
3698
- join59 = `,
3698
+ join62 = `,
3699
3699
  ${indentation}`;
3700
3700
  }
3701
3701
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -3703,13 +3703,13 @@ ${indentation}`;
3703
3703
  for (; i < maximumValuesToStringify - 1; i++) {
3704
3704
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
3705
3705
  res += tmp2 !== void 0 ? tmp2 : "null";
3706
- res += join59;
3706
+ res += join62;
3707
3707
  }
3708
3708
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
3709
3709
  res += tmp !== void 0 ? tmp : "null";
3710
3710
  if (value.length - 1 > maximumBreadth) {
3711
3711
  const removedKeys = value.length - maximumBreadth - 1;
3712
- res += `${join59}"... ${getItemCount(removedKeys)} not stringified"`;
3712
+ res += `${join62}"... ${getItemCount(removedKeys)} not stringified"`;
3713
3713
  }
3714
3714
  if (spacer !== "") {
3715
3715
  res += `
@@ -3722,7 +3722,7 @@ ${originalIndentation}`;
3722
3722
  let whitespace = "";
3723
3723
  if (spacer !== "") {
3724
3724
  indentation += spacer;
3725
- join59 = `,
3725
+ join62 = `,
3726
3726
  ${indentation}`;
3727
3727
  whitespace = " ";
3728
3728
  }
@@ -3731,7 +3731,7 @@ ${indentation}`;
3731
3731
  const tmp = stringifyArrayReplacer(key3, value[key3], stack, replacer, spacer, indentation);
3732
3732
  if (tmp !== void 0) {
3733
3733
  res += `${separator}${strEscape(key3)}:${whitespace}${tmp}`;
3734
- separator = join59;
3734
+ separator = join62;
3735
3735
  }
3736
3736
  }
3737
3737
  if (spacer !== "" && separator.length > 1) {
@@ -3789,20 +3789,20 @@ ${originalIndentation}`;
3789
3789
  indentation += spacer;
3790
3790
  let res2 = `
3791
3791
  ${indentation}`;
3792
- const join60 = `,
3792
+ const join63 = `,
3793
3793
  ${indentation}`;
3794
3794
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
3795
3795
  let i = 0;
3796
3796
  for (; i < maximumValuesToStringify - 1; i++) {
3797
3797
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
3798
3798
  res2 += tmp2 !== void 0 ? tmp2 : "null";
3799
- res2 += join60;
3799
+ res2 += join63;
3800
3800
  }
3801
3801
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
3802
3802
  res2 += tmp !== void 0 ? tmp : "null";
3803
3803
  if (value.length - 1 > maximumBreadth) {
3804
3804
  const removedKeys = value.length - maximumBreadth - 1;
3805
- res2 += `${join60}"... ${getItemCount(removedKeys)} not stringified"`;
3805
+ res2 += `${join63}"... ${getItemCount(removedKeys)} not stringified"`;
3806
3806
  }
3807
3807
  res2 += `
3808
3808
  ${originalIndentation}`;
@@ -3818,16 +3818,16 @@ ${originalIndentation}`;
3818
3818
  return '"[Object]"';
3819
3819
  }
3820
3820
  indentation += spacer;
3821
- const join59 = `,
3821
+ const join62 = `,
3822
3822
  ${indentation}`;
3823
3823
  let res = "";
3824
3824
  let separator = "";
3825
3825
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
3826
3826
  if (isTypedArrayWithEntries(value)) {
3827
- res += stringifyTypedArray(value, join59, maximumBreadth);
3827
+ res += stringifyTypedArray(value, join62, maximumBreadth);
3828
3828
  keys = keys.slice(value.length);
3829
3829
  maximumPropertiesToStringify -= value.length;
3830
- separator = join59;
3830
+ separator = join62;
3831
3831
  }
3832
3832
  if (deterministic) {
3833
3833
  keys = sort(keys, comparator);
@@ -3838,13 +3838,13 @@ ${indentation}`;
3838
3838
  const tmp = stringifyIndent(key3, value[key3], stack, spacer, indentation);
3839
3839
  if (tmp !== void 0) {
3840
3840
  res += `${separator}${strEscape(key3)}: ${tmp}`;
3841
- separator = join59;
3841
+ separator = join62;
3842
3842
  }
3843
3843
  }
3844
3844
  if (keyLength > maximumBreadth) {
3845
3845
  const removedKeys = keyLength - maximumBreadth;
3846
3846
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
3847
- separator = join59;
3847
+ separator = join62;
3848
3848
  }
3849
3849
  if (separator !== "") {
3850
3850
  res = `
@@ -7443,11 +7443,11 @@ async function archiveRawSession(store, sessionId) {
7443
7443
  return await store.putRawSession(sessionId, exp.tool, Math.floor(exp.mtime), gz, size);
7444
7444
  }
7445
7445
  function detectForkPredecessor(sessionPath) {
7446
- const { readFileSync: readFileSync49, readdirSync: readdirSync35 } = requireFs();
7447
- const { dirname: dirname28, join: join59, basename: basename28 } = requirePath();
7446
+ const { readFileSync: readFileSync50, readdirSync: readdirSync35 } = requireFs();
7447
+ const { dirname: dirname30, join: join62, basename: basename28 } = requirePath();
7448
7448
  let text;
7449
7449
  try {
7450
- text = readFileSync49(sessionPath, "utf-8");
7450
+ text = readFileSync50(sessionPath, "utf-8");
7451
7451
  } catch {
7452
7452
  return null;
7453
7453
  }
@@ -7469,7 +7469,7 @@ function detectForkPredecessor(sessionPath) {
7469
7469
  return null;
7470
7470
  if (text.includes(`"uuid":"${headParent}"`))
7471
7471
  return null;
7472
- const dir = dirname28(sessionPath);
7472
+ const dir = dirname30(sessionPath);
7473
7473
  const self = basename28(sessionPath);
7474
7474
  let entries;
7475
7475
  try {
@@ -7481,7 +7481,7 @@ function detectForkPredecessor(sessionPath) {
7481
7481
  if (!f.endsWith(".jsonl") || f === self || f === "sessions-index.json")
7482
7482
  continue;
7483
7483
  try {
7484
- if (readFileSync49(join59(dir, f), "utf-8").includes(`"uuid":"${headParent}"`)) {
7484
+ if (readFileSync50(join62(dir, f), "utf-8").includes(`"uuid":"${headParent}"`)) {
7485
7485
  return basename28(f, ".jsonl");
7486
7486
  }
7487
7487
  } catch {
@@ -13995,10 +13995,10 @@ var init_types = __esm({
13995
13995
 
13996
13996
  // ../engine/dist/transcript/opencode.js
13997
13997
  async function parseOpenCodeSubagents(parentSessionId) {
13998
- const { existsSync: existsSync55 } = await import("fs");
13998
+ const { existsSync: existsSync57 } = await import("fs");
13999
13999
  const { opencodeBackend: opencodeBackend2 } = await Promise.resolve().then(() => (init_opencode(), opencode_exports));
14000
14000
  const dbPath = opencodeBackend2.dbPath();
14001
- if (!existsSync55(dbPath))
14001
+ if (!existsSync57(dbPath))
14002
14002
  return [];
14003
14003
  const realParent = parentSessionId.replace("opencode_", "");
14004
14004
  const db = openSqliteReadonlyOrThrow(dbPath);
@@ -14039,10 +14039,10 @@ async function parseOpenCodeSubagents(parentSessionId) {
14039
14039
  }
14040
14040
  }
14041
14041
  async function parseOpenCodeTranscript(sessionId) {
14042
- const { existsSync: existsSync55 } = await import("fs");
14042
+ const { existsSync: existsSync57 } = await import("fs");
14043
14043
  const { opencodeBackend: opencodeBackend2 } = await Promise.resolve().then(() => (init_opencode(), opencode_exports));
14044
14044
  const dbPath = opencodeBackend2.dbPath();
14045
- if (!existsSync55(dbPath))
14045
+ if (!existsSync57(dbPath))
14046
14046
  return [];
14047
14047
  const realId = sessionId.replace("opencode_", "");
14048
14048
  const db = openSqliteReadonlyOrThrow(dbPath);
@@ -18817,7 +18817,7 @@ var init_http = __esm({
18817
18817
  CLI_VERSION = JSON.parse(readFileSync31(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
18818
18818
  } catch {
18819
18819
  try {
18820
- if (true) CLI_VERSION = "0.6.5";
18820
+ if (true) CLI_VERSION = "0.6.7";
18821
18821
  } catch {
18822
18822
  }
18823
18823
  }
@@ -19337,7 +19337,7 @@ var init_telemetry = __esm({
19337
19337
  init_sync_client();
19338
19338
  cliVersion2 = () => {
19339
19339
  try {
19340
- return true ? "0.6.5" : "dev";
19340
+ return true ? "0.6.7" : "dev";
19341
19341
  } catch {
19342
19342
  return "dev";
19343
19343
  }
@@ -19666,12 +19666,16 @@ var init_client_events = __esm({
19666
19666
  // src/auto-update.ts
19667
19667
  var auto_update_exports = {};
19668
19668
  __export(auto_update_exports, {
19669
+ clearUpdateFailures: () => clearUpdateFailures,
19669
19670
  compareVersions: () => compareVersions,
19670
19671
  executeAutoUpdate: () => executeAutoUpdate,
19672
+ hasGivenUpOn: () => hasGivenUpOn,
19671
19673
  installedVersion: () => installedVersion,
19672
19674
  isAutoUpdateEnabled: () => isAutoUpdateEnabled,
19673
19675
  newerOf: () => newerOf,
19676
+ noteUpdateFailure: () => noteUpdateFailure,
19674
19677
  planAutoUpdate: () => planAutoUpdate,
19678
+ readUpdateState: () => readUpdateState,
19675
19679
  runAutoUpdate: () => runAutoUpdate,
19676
19680
  runningPrefix: () => runningPrefix,
19677
19681
  sweepStaleStaging: () => sweepStaleStaging,
@@ -19680,9 +19684,9 @@ __export(auto_update_exports, {
19680
19684
  import { execSync as execSync2 } from "node:child_process";
19681
19685
  import { readFileSync as readFileSync34 } from "node:fs";
19682
19686
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19683
- import { writeFileSync as writeFileSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync5, readdirSync as readdirSync31, statSync as statSync36 } from "node:fs";
19687
+ import { writeFileSync as writeFileSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync5, readdirSync as readdirSync31, statSync as statSync36, mkdirSync as mkdirSync9 } from "node:fs";
19684
19688
  import { join as join41, dirname as dirname18, sep as sep2 } from "node:path";
19685
- import { tmpdir as tmpdir2 } from "node:os";
19689
+ import { tmpdir as tmpdir2, homedir as homedir15 } from "node:os";
19686
19690
  import { createHash as createHash8 } from "node:crypto";
19687
19691
  function compareVersions(a, b) {
19688
19692
  const pa = String(a).split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
@@ -19796,16 +19800,25 @@ function runningPrefix(moduleUrl = import.meta.url) {
19796
19800
  const beforeNodeModules = here.slice(0, at);
19797
19801
  return beforeNodeModules.endsWith(`${sep2}lib`) ? beforeNodeModules.slice(0, -4) : beforeNodeModules;
19798
19802
  }
19803
+ function npmInstall(cmd) {
19804
+ try {
19805
+ execSync2(cmd, { stdio: ["ignore", "ignore", "pipe"] });
19806
+ } catch (e) {
19807
+ const err = e;
19808
+ const detail = String(err.stderr ?? "").trim().split("\n").filter((l) => l.trim() && !/^npm (notice|warn)\b/i.test(l)).slice(-4).join(" | ").slice(0, 400);
19809
+ throw new Error(detail ? `${err.message ?? "install failed"} \u2014 ${detail}` : err.message ?? "install failed");
19810
+ }
19811
+ }
19799
19812
  function realInstall(tgz) {
19800
19813
  const prefix = runningPrefix();
19801
19814
  if (prefix) {
19802
- execSync2(`npm install -g --prefix "${prefix}" "${tgz}"`, { stdio: "ignore" });
19815
+ npmInstall(`npm install -g --prefix "${prefix}" "${tgz}"`);
19803
19816
  return;
19804
19817
  }
19805
19818
  try {
19806
- execSync2(`npm install -g "${tgz}"`, { stdio: "ignore" });
19819
+ npmInstall(`npm install -g "${tgz}"`);
19807
19820
  } catch {
19808
- execSync2(`npm install -g --prefix "${process.env.HOME}/.local" "${tgz}"`, { stdio: "ignore" });
19821
+ npmInstall(`npm install -g --prefix "${process.env.HOME}/.local" "${tgz}"`);
19809
19822
  }
19810
19823
  }
19811
19824
  function realRestart(platform2) {
@@ -19813,6 +19826,37 @@ function realRestart(platform2) {
19813
19826
  else if (platform2 === "darwin") execSync2("launchctl kickstart -k gui/$(id -u)/com.chat-recall.watch", { stdio: "ignore", shell: "/bin/bash" });
19814
19827
  else if (platform2 === "win32") execSync2("schtasks /End /TN chat-recall-watch & schtasks /Run /TN chat-recall-watch", { stdio: "ignore" });
19815
19828
  }
19829
+ function updateStatePath() {
19830
+ const dir = process.env.CHAT_RECALL_DATA_DIR || join41(homedir15(), ".chat-recall");
19831
+ return join41(dir, "auto-update-state.json");
19832
+ }
19833
+ function readUpdateState() {
19834
+ try {
19835
+ return JSON.parse(readFileSync34(updateStatePath(), "utf8"));
19836
+ } catch {
19837
+ return {};
19838
+ }
19839
+ }
19840
+ function writeUpdateState(st) {
19841
+ try {
19842
+ const p = updateStatePath();
19843
+ mkdirSync9(dirname18(p), { recursive: true });
19844
+ writeFileSync8(p, JSON.stringify(st), "utf8");
19845
+ } catch {
19846
+ }
19847
+ }
19848
+ function hasGivenUpOn(target, st = readUpdateState()) {
19849
+ return st.target === target && (st.failures ?? 0) >= GIVE_UP_AFTER;
19850
+ }
19851
+ function noteUpdateFailure(target) {
19852
+ const st = readUpdateState();
19853
+ const failures = st.target === target ? (st.failures ?? 0) + 1 : 1;
19854
+ writeUpdateState({ target, failures });
19855
+ return failures;
19856
+ }
19857
+ function clearUpdateFailures() {
19858
+ writeUpdateState({});
19859
+ }
19816
19860
  async function runAutoUpdate(base3, authHeaders, ownVersion3, deps) {
19817
19861
  let caps;
19818
19862
  try {
@@ -19835,6 +19879,10 @@ async function runAutoUpdate(base3, authHeaders, ownVersion3, deps) {
19835
19879
  }
19836
19880
  return { updated: false, reason: plan.reason };
19837
19881
  }
19882
+ const target = plan.to ?? "";
19883
+ if (target && hasGivenUpOn(target)) {
19884
+ return { updated: false, reason: `gave up installing ${plan.to} after ${GIVE_UP_AFTER} failed attempts \u2014 run \`npm install -g chat-recall\` by hand` };
19885
+ }
19838
19886
  const result = await executeAutoUpdate(plan, {
19839
19887
  download: deps?.download ?? realDownload,
19840
19888
  install: deps?.install ?? realInstall,
@@ -19842,20 +19890,27 @@ async function runAutoUpdate(base3, authHeaders, ownVersion3, deps) {
19842
19890
  platform: deps?.platform
19843
19891
  });
19844
19892
  if (!result.updated) {
19845
- try {
19846
- const { reportClientEvent: reportClientEvent2 } = await Promise.resolve().then(() => (init_client_events(), client_events_exports));
19847
- reportClientEvent2("auto_update_failed", { message: `${plan.from} \u2192 ${plan.to}: ${result.reason}` });
19848
- } catch {
19893
+ const failures = target ? noteUpdateFailure(target) : 1;
19894
+ if (failures <= GIVE_UP_AFTER) {
19895
+ try {
19896
+ const { reportClientEvent: reportClientEvent2 } = await Promise.resolve().then(() => (init_client_events(), client_events_exports));
19897
+ const suffix = failures >= GIVE_UP_AFTER ? ` \u2014 giving up after ${failures} attempts, needs a manual \`npm install -g chat-recall\`` : ` (attempt ${failures} of ${GIVE_UP_AFTER})`;
19898
+ reportClientEvent2("auto_update_failed", { message: `${plan.from} \u2192 ${plan.to}: ${result.reason}${suffix}` });
19899
+ } catch {
19900
+ }
19849
19901
  }
19902
+ } else {
19903
+ clearUpdateFailures();
19850
19904
  }
19851
19905
  return result;
19852
19906
  }
19853
- var tarballUrl, reportedStandingSkip;
19907
+ var tarballUrl, GIVE_UP_AFTER, reportedStandingSkip;
19854
19908
  var init_auto_update = __esm({
19855
19909
  "src/auto-update.ts"() {
19856
19910
  "use strict";
19857
19911
  init_http();
19858
19912
  tarballUrl = (base3) => `${base3.replace(/\/+$/, "")}/install/chat-recall.tgz`;
19913
+ GIVE_UP_AFTER = 3;
19859
19914
  reportedStandingSkip = /* @__PURE__ */ new Set();
19860
19915
  }
19861
19916
  });
@@ -19890,7 +19945,7 @@ __export(sync_client_exports, {
19890
19945
  syncSessions: () => syncSessions
19891
19946
  });
19892
19947
  import { join as join42, dirname as dirname19, basename as basename20 } from "node:path";
19893
- import { readFileSync as readFileSync35, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync4, rmSync as rmSync6, chmodSync as chmodSync3, unlinkSync as unlinkSync2, readdirSync as readdirSync32, statSync as statSync37 } from "node:fs";
19948
+ import { readFileSync as readFileSync35, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync4, rmSync as rmSync6, chmodSync as chmodSync3, unlinkSync as unlinkSync2, readdirSync as readdirSync32, statSync as statSync37 } from "node:fs";
19894
19949
  import { tmpdir as tmpdir3, hostname as hostname2 } from "node:os";
19895
19950
  import { createHash as createHash9 } from "node:crypto";
19896
19951
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -19925,7 +19980,7 @@ function classifyError(msg, err) {
19925
19980
  }
19926
19981
  function saveCredentials(c) {
19927
19982
  assertTransportSafe(c.serverUrl);
19928
- mkdirSync9(dirname19(credPath()), { recursive: true });
19983
+ mkdirSync10(dirname19(credPath()), { recursive: true });
19929
19984
  const targets = loadAllCredentials().filter((t) => t.serverUrl !== c.serverUrl);
19930
19985
  targets.unshift(c);
19931
19986
  writeFileSync9(credPath(), JSON.stringify({ targets }, null, 2));
@@ -20020,7 +20075,7 @@ function persistSyncConfig(base3, config2) {
20020
20075
  try {
20021
20076
  const all = readPersistedSyncConfigs();
20022
20077
  all[base3] = { fetchedAt: Date.now(), config: config2 };
20023
- mkdirSync9(dirname19(syncConfigCachePath()), { recursive: true });
20078
+ mkdirSync10(dirname19(syncConfigCachePath()), { recursive: true });
20024
20079
  writeFileSync9(syncConfigCachePath(), JSON.stringify(all, null, 2));
20025
20080
  } catch {
20026
20081
  }
@@ -21426,7 +21481,7 @@ var init_sync_client = __esm({
21426
21481
  ownVersion = JSON.parse(readFileSync35(join42(dir, "../package.json"), "utf-8")).version || ownVersion;
21427
21482
  } catch {
21428
21483
  try {
21429
- if (true) ownVersion = "0.6.5";
21484
+ if (true) ownVersion = "0.6.7";
21430
21485
  } catch {
21431
21486
  }
21432
21487
  }
@@ -21678,16 +21733,16 @@ __export(mcp_clients_exports, {
21678
21733
  inspectMcpClients: () => inspectMcpClients,
21679
21734
  registerMcpEverywhere: () => registerMcpEverywhere
21680
21735
  });
21681
- import { existsSync as existsSync42, mkdirSync as mkdirSync10, readFileSync as readFileSync37, writeFileSync as writeFileSync11 } from "fs";
21682
- import { homedir as homedir15 } from "os";
21683
- import { dirname as dirname20, join as join45 } from "path";
21736
+ import { existsSync as existsSync43, mkdirSync as mkdirSync12, readFileSync as readFileSync37, writeFileSync as writeFileSync12 } from "fs";
21737
+ import { homedir as homedir16 } from "os";
21738
+ import { dirname as dirname21, join as join46 } from "path";
21684
21739
  function envHome(varName, fallback) {
21685
21740
  const v = process.env[varName];
21686
21741
  return v && v.trim() ? v.trim() : fallback;
21687
21742
  }
21688
21743
  function writeFileMkdir(path, body) {
21689
- mkdirSync10(dirname20(path), { recursive: true });
21690
- writeFileSync11(path, body);
21744
+ mkdirSync12(dirname21(path), { recursive: true });
21745
+ writeFileSync12(path, body);
21691
21746
  }
21692
21747
  function readJsonObject(path) {
21693
21748
  const raw = readFileSync37(path, "utf-8");
@@ -21697,7 +21752,7 @@ function readJsonObject(path) {
21697
21752
  }
21698
21753
  function registerMcpServersJson(target, path, spec) {
21699
21754
  let cfg = {};
21700
- if (existsSync42(path)) {
21755
+ if (existsSync43(path)) {
21701
21756
  try {
21702
21757
  cfg = readJsonObject(path);
21703
21758
  } catch (err) {
@@ -21726,7 +21781,7 @@ function registerOpencodeJson(path, spec) {
21726
21781
  const id = "opencode";
21727
21782
  const label = "OpenCode";
21728
21783
  let cfg = {};
21729
- if (existsSync42(path)) {
21784
+ if (existsSync43(path)) {
21730
21785
  try {
21731
21786
  cfg = readJsonObject(path);
21732
21787
  } catch (err) {
@@ -21765,7 +21820,7 @@ function registerCodexToml(path, spec) {
21765
21820
  const id = "codex";
21766
21821
  const label = "Codex";
21767
21822
  const block = codexBlock(spec);
21768
- const prev = existsSync42(path) ? readFileSync37(path, "utf-8") : "";
21823
+ const prev = existsSync43(path) ? readFileSync37(path, "utf-8") : "";
21769
21824
  const lines = prev.length ? prev.split("\n") : [];
21770
21825
  const start = lines.findIndex((l) => l.trim() === CODEX_HEADER);
21771
21826
  if (start === -1) {
@@ -21797,16 +21852,16 @@ function getMcpClient(id) {
21797
21852
  return t;
21798
21853
  }
21799
21854
  function registerMcpEverywhere(spec, opts = {}) {
21800
- const home = opts.home ?? homedir15();
21855
+ const home = opts.home ?? homedir16();
21801
21856
  const forced = new Set(opts.extraIds ?? []);
21802
21857
  return MCP_CLIENTS.filter((c) => c.isPresent(home) || forced.has(c.id)).map((c) => c.register(spec, home));
21803
21858
  }
21804
21859
  function inspectMcpClients(opts = {}) {
21805
- const home = opts.home ?? homedir15();
21860
+ const home = opts.home ?? homedir16();
21806
21861
  return MCP_CLIENTS.map((c) => {
21807
21862
  const path = c.configPath(home);
21808
21863
  let registered = false;
21809
- if (existsSync42(path)) {
21864
+ if (existsSync43(path)) {
21810
21865
  try {
21811
21866
  const raw = readFileSync37(path, "utf-8");
21812
21867
  registered = c.id === "codex" ? raw.includes(CODEX_HEADER) : !!(JSON.parse(raw).mcpServers?.[MCP_SERVER_NAME] ?? JSON.parse(raw).mcp?.[MCP_SERVER_NAME]);
@@ -21828,7 +21883,7 @@ var init_mcp_clients = __esm({
21828
21883
  id: "claude",
21829
21884
  label: "Claude Code",
21830
21885
  bin: "claude",
21831
- configPath: (home) => join45(home, ".mcp.json"),
21886
+ configPath: (home) => join46(home, ".mcp.json"),
21832
21887
  // Claude Code is the reference client and `init`'s historical target, so we
21833
21888
  // always write it — a fresh machine has no ~/.claude yet on first install.
21834
21889
  isPresent: () => true,
@@ -21840,10 +21895,10 @@ var init_mcp_clients = __esm({
21840
21895
  id: "codex",
21841
21896
  label: "Codex",
21842
21897
  bin: "codex",
21843
- configPath: (home) => join45(envHome("CHAT_RECALL_CODEX_HOME", join45(home, ".codex")), "config.toml"),
21898
+ configPath: (home) => join46(envHome("CHAT_RECALL_CODEX_HOME", join46(home, ".codex")), "config.toml"),
21844
21899
  isPresent(home) {
21845
21900
  const p = this.configPath(home);
21846
- return existsSync42(p) || existsSync42(dirname20(p));
21901
+ return existsSync43(p) || existsSync43(dirname21(p));
21847
21902
  },
21848
21903
  register(spec, home) {
21849
21904
  return registerCodexToml(this.configPath(home), spec);
@@ -21853,10 +21908,10 @@ var init_mcp_clients = __esm({
21853
21908
  id: "opencode",
21854
21909
  label: "OpenCode",
21855
21910
  bin: "opencode",
21856
- configPath: (home) => join45(envHome("XDG_CONFIG_HOME", join45(home, ".config")), "opencode", "opencode.json"),
21911
+ configPath: (home) => join46(envHome("XDG_CONFIG_HOME", join46(home, ".config")), "opencode", "opencode.json"),
21857
21912
  isPresent(home) {
21858
21913
  const p = this.configPath(home);
21859
- return existsSync42(p) || existsSync42(dirname20(p));
21914
+ return existsSync43(p) || existsSync43(dirname21(p));
21860
21915
  },
21861
21916
  register(spec, home) {
21862
21917
  return registerOpencodeJson(this.configPath(home), spec);
@@ -21868,9 +21923,9 @@ var init_mcp_clients = __esm({
21868
21923
  id: "agy",
21869
21924
  label: "Antigravity",
21870
21925
  bin: "agy",
21871
- configPath: (home) => join45(home, ".gemini", "config", "mcp_config.json"),
21926
+ configPath: (home) => join46(home, ".gemini", "config", "mcp_config.json"),
21872
21927
  isPresent(home) {
21873
- return existsSync42(join45(home, ".gemini", "antigravity-cli")) || existsSync42(this.configPath(home));
21928
+ return existsSync43(join46(home, ".gemini", "antigravity-cli")) || existsSync43(this.configPath(home));
21874
21929
  },
21875
21930
  register(spec, home) {
21876
21931
  return registerMcpServersJson(this, this.configPath(home), spec);
@@ -21880,10 +21935,10 @@ var init_mcp_clients = __esm({
21880
21935
  id: "cursor",
21881
21936
  label: "Cursor",
21882
21937
  bin: "cursor",
21883
- configPath: (home) => join45(home, ".cursor", "mcp.json"),
21938
+ configPath: (home) => join46(home, ".cursor", "mcp.json"),
21884
21939
  isPresent(home) {
21885
21940
  const p = this.configPath(home);
21886
- return existsSync42(p) || existsSync42(dirname20(p));
21941
+ return existsSync43(p) || existsSync43(dirname21(p));
21887
21942
  },
21888
21943
  register(spec, home) {
21889
21944
  return registerMcpServersJson(this, this.configPath(home), spec);
@@ -21907,9 +21962,9 @@ __export(companions_exports, {
21907
21962
  uninstallCodeindex: () => uninstallCodeindex,
21908
21963
  unregisterCodeindexMcp: () => unregisterCodeindexMcp
21909
21964
  });
21910
- import { existsSync as existsSync43, mkdirSync as mkdirSync11, statSync as statSync39, chmodSync as chmodSync4, unlinkSync as unlinkSync3, renameSync as renameSync5, createWriteStream, readFileSync as readFileSync38, writeFileSync as writeFileSync12 } from "node:fs";
21911
- import { homedir as homedir16 } from "node:os";
21912
- import { join as join46 } from "node:path";
21965
+ import { existsSync as existsSync44, mkdirSync as mkdirSync13, statSync as statSync39, chmodSync as chmodSync4, unlinkSync as unlinkSync3, renameSync as renameSync5, createWriteStream, readFileSync as readFileSync38, writeFileSync as writeFileSync13 } from "node:fs";
21966
+ import { homedir as homedir17 } from "node:os";
21967
+ import { join as join47 } from "node:path";
21913
21968
  import { execSync as execSync3 } from "node:child_process";
21914
21969
  import { createHash as createHash10 } from "node:crypto";
21915
21970
  function expectedCodeindexSha256(artifact) {
@@ -22011,7 +22066,7 @@ async function installCodeindex(opts = {}) {
22011
22066
  return status;
22012
22067
  if (!status.prebuiltAvailable)
22013
22068
  return status;
22014
- mkdirSync11(CODEINDEX_INSTALL_DIR, { recursive: true });
22069
+ mkdirSync13(CODEINDEX_INSTALL_DIR, { recursive: true });
22015
22070
  const tmpPath = `${CODEINDEX_BIN_PATH}.partial`;
22016
22071
  try {
22017
22072
  await downloadArtifact(status.artifactName, tmpPath);
@@ -22030,7 +22085,7 @@ Refusing to install. The release may have been re-uploaded, or something is serv
22030
22085
  renameSync5(tmpPath, CODEINDEX_BIN_PATH);
22031
22086
  } catch (err) {
22032
22087
  try {
22033
- if (existsSync43(tmpPath))
22088
+ if (existsSync44(tmpPath))
22034
22089
  unlinkSync3(tmpPath);
22035
22090
  } catch {
22036
22091
  }
@@ -22039,7 +22094,7 @@ Refusing to install. The release may have been re-uploaded, or something is serv
22039
22094
  return checkCodeindexStatus();
22040
22095
  }
22041
22096
  function uninstallCodeindex() {
22042
- if (existsSync43(CODEINDEX_BIN_PATH)) {
22097
+ if (existsSync44(CODEINDEX_BIN_PATH)) {
22043
22098
  unlinkSync3(CODEINDEX_BIN_PATH);
22044
22099
  return { removed: true, path: CODEINDEX_BIN_PATH };
22045
22100
  }
@@ -22047,7 +22102,7 @@ function uninstallCodeindex() {
22047
22102
  }
22048
22103
  function registerCodeindexMcp(mcpJsonPath, binaryPath) {
22049
22104
  let cfg = {};
22050
- if (existsSync43(mcpJsonPath)) {
22105
+ if (existsSync44(mcpJsonPath)) {
22051
22106
  try {
22052
22107
  cfg = JSON.parse(readFileSync38(mcpJsonPath, "utf-8"));
22053
22108
  } catch {
@@ -22058,11 +22113,11 @@ function registerCodeindexMcp(mcpJsonPath, binaryPath) {
22058
22113
  if (cfg.mcpServers["codeindex"])
22059
22114
  return { added: false };
22060
22115
  cfg.mcpServers["codeindex"] = { command: binaryPath, args: ["--mcp"] };
22061
- writeFileSync12(mcpJsonPath, JSON.stringify(cfg, null, 2));
22116
+ writeFileSync13(mcpJsonPath, JSON.stringify(cfg, null, 2));
22062
22117
  return { added: true };
22063
22118
  }
22064
22119
  function unregisterCodeindexMcp(mcpJsonPath) {
22065
- if (!existsSync43(mcpJsonPath))
22120
+ if (!existsSync44(mcpJsonPath))
22066
22121
  return { removed: false };
22067
22122
  let cfg;
22068
22123
  try {
@@ -22073,7 +22128,7 @@ function unregisterCodeindexMcp(mcpJsonPath) {
22073
22128
  if (!cfg.mcpServers?.["codeindex"])
22074
22129
  return { removed: false };
22075
22130
  delete cfg.mcpServers["codeindex"];
22076
- writeFileSync12(mcpJsonPath, JSON.stringify(cfg, null, 2));
22131
+ writeFileSync13(mcpJsonPath, JSON.stringify(cfg, null, 2));
22077
22132
  return { removed: true };
22078
22133
  }
22079
22134
  var CODEINDEX_REPO, CODEINDEX_VERSION, CODEINDEX_SHA256, CODEINDEX_INSTALL_DIR, CODEINDEX_BIN_NAME, CODEINDEX_BIN_PATH;
@@ -22091,9 +22146,9 @@ var init_companions = __esm({
22091
22146
  "x86_64-windows": "a96fc8c8949bb399a44cc0615b12e458460a529c69f01e8829cc39a20f45ab29",
22092
22147
  "aarch64-windows": "42620d56272cd9ba9d785569d8d754d6b4a3e80f36f28db3e0c483fd600ed117"
22093
22148
  };
22094
- CODEINDEX_INSTALL_DIR = join46(homedir16(), ".local", "bin");
22149
+ CODEINDEX_INSTALL_DIR = join47(homedir17(), ".local", "bin");
22095
22150
  CODEINDEX_BIN_NAME = process.platform === "win32" ? "codeindex.exe" : "codeindex";
22096
- CODEINDEX_BIN_PATH = join46(CODEINDEX_INSTALL_DIR, CODEINDEX_BIN_NAME);
22151
+ CODEINDEX_BIN_PATH = join47(CODEINDEX_INSTALL_DIR, CODEINDEX_BIN_NAME);
22097
22152
  }
22098
22153
  });
22099
22154
 
@@ -22108,11 +22163,11 @@ __export(install_skills_exports, {
22108
22163
  skillsSourceDir: () => skillsSourceDir,
22109
22164
  uninstallSkills: () => uninstallSkills
22110
22165
  });
22111
- import { readdirSync as readdirSync33, existsSync as existsSync44, mkdirSync as mkdirSync12, rmSync as rmSync7, cpSync, writeFileSync as writeFileSync13, readFileSync as readFileSync39, statSync as statSync40, realpathSync as realpathSync4 } from "fs";
22166
+ import { readdirSync as readdirSync33, existsSync as existsSync45, mkdirSync as mkdirSync14, rmSync as rmSync7, cpSync, writeFileSync as writeFileSync14, readFileSync as readFileSync39, statSync as statSync40, realpathSync as realpathSync4 } from "fs";
22112
22167
  import { createHash as createHash11 } from "crypto";
22113
- import { join as join47, basename as basename21 } from "path";
22168
+ import { join as join48, basename as basename21 } from "path";
22114
22169
  function skillsSourceDir() {
22115
- return join47(import.meta.dirname, "..", "skills");
22170
+ return join48(import.meta.dirname, "..", "skills");
22116
22171
  }
22117
22172
  function version() {
22118
22173
  try {
@@ -22125,9 +22180,9 @@ function skillsFingerprint() {
22125
22180
  const src = skillsSourceDir();
22126
22181
  const h = createHash11("sha256");
22127
22182
  for (const name of bundledSkillNames()) {
22128
- for (const rel of filesUnder(join47(src, name)).sort()) {
22183
+ for (const rel of filesUnder(join48(src, name)).sort()) {
22129
22184
  h.update(name + "/" + rel);
22130
- h.update(readFileSync39(join47(src, name, rel)));
22185
+ h.update(readFileSync39(join48(src, name, rel)));
22131
22186
  }
22132
22187
  }
22133
22188
  return h.digest("hex").slice(0, 12);
@@ -22137,7 +22192,7 @@ function filesUnder(dir, prefix = "") {
22137
22192
  for (const e of readdirSync33(dir, { withFileTypes: true })) {
22138
22193
  if (e.name === MARKER) continue;
22139
22194
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
22140
- if (e.isDirectory()) out.push(...filesUnder(join47(dir, e.name), rel));
22195
+ if (e.isDirectory()) out.push(...filesUnder(join48(dir, e.name), rel));
22141
22196
  else out.push(rel);
22142
22197
  }
22143
22198
  return out;
@@ -22147,8 +22202,8 @@ function skillStamp() {
22147
22202
  }
22148
22203
  function bundledSkillNames() {
22149
22204
  const src = skillsSourceDir();
22150
- if (!existsSync44(src)) return [];
22151
- return readdirSync33(src, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync44(join47(src, e.name, "SKILL.md"))).map((e) => e.name).sort();
22205
+ if (!existsSync45(src)) return [];
22206
+ return readdirSync33(src, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync45(join48(src, e.name, "SKILL.md"))).map((e) => e.name).sort();
22152
22207
  }
22153
22208
  function realPathOrSelf(p) {
22154
22209
  try {
@@ -22161,7 +22216,7 @@ function claudeSkillTargets() {
22161
22216
  const targets = [];
22162
22217
  const seen = /* @__PURE__ */ new Set();
22163
22218
  for (const home of claudeHomeDirs()) {
22164
- const dir = join47(home, "skills");
22219
+ const dir = join48(home, "skills");
22165
22220
  const key2 = realPathOrSelf(dir);
22166
22221
  if (seen.has(key2)) continue;
22167
22222
  seen.add(key2);
@@ -22172,7 +22227,7 @@ function claudeSkillTargets() {
22172
22227
  dir,
22173
22228
  // Judge each profile on ITS OWN transcripts, not the primary's: a home
22174
22229
  // with no projects/ is a directory someone made, not a Claude install.
22175
- available: existsSync44(join47(home, "projects"))
22230
+ available: existsSync45(join48(home, "projects"))
22176
22231
  });
22177
22232
  }
22178
22233
  if (targets.length === 0) {
@@ -22187,16 +22242,16 @@ function skillTargets() {
22187
22242
  // both produces "skill conflict" duplicate warnings. Every tool we support
22188
22243
  // has its own dir, so per-tool coverage is complete.
22189
22244
  ...claudeSkillTargets(),
22190
- { id: "agy", label: "Antigravity", dir: join47(agyBackend.homeDir(), "skills"), available: agyBackend.isAvailable() },
22245
+ { id: "agy", label: "Antigravity", dir: join48(agyBackend.homeDir(), "skills"), available: agyBackend.isAvailable() },
22191
22246
  { id: "codex", label: "Codex", dir: codexBackend.skillsDir(), available: codexBackend.isAvailable() },
22192
22247
  { id: "opencode", label: "OpenCode", dir: opencodeBackend.skillsDir(), available: opencodeBackend.isAvailable() },
22193
22248
  // Cursor DOES have its own dir (~/.cursor/skills). `skills-cursor` next to
22194
22249
  // it is Cursor's bundled set — read-only, never an install target.
22195
- { id: "cursor", label: "Cursor", dir: join47(cursorHomeDir(), "skills"), available: cursorBackend.isAvailable() }
22250
+ { id: "cursor", label: "Cursor", dir: join48(cursorHomeDir(), "skills"), available: cursorBackend.isAvailable() }
22196
22251
  ];
22197
22252
  }
22198
22253
  function isManaged(dir) {
22199
- return existsSync44(join47(dir, MARKER));
22254
+ return existsSync45(join48(dir, MARKER));
22200
22255
  }
22201
22256
  function installSkills(opts = {}) {
22202
22257
  const onlyAvailable = opts.onlyAvailable ?? true;
@@ -22213,15 +22268,15 @@ function installSkills(opts = {}) {
22213
22268
  continue;
22214
22269
  }
22215
22270
  for (const name of names) {
22216
- const dest = join47(t.dir, name);
22217
- if (existsSync44(dest) && !isManaged(dest)) {
22271
+ const dest = join48(t.dir, name);
22272
+ if (existsSync45(dest) && !isManaged(dest)) {
22218
22273
  skippedUserOwned.push(name);
22219
22274
  continue;
22220
22275
  }
22221
- mkdirSync12(t.dir, { recursive: true });
22276
+ mkdirSync14(t.dir, { recursive: true });
22222
22277
  rmSync7(dest, { recursive: true, force: true });
22223
- cpSync(join47(src, name), dest, { recursive: true });
22224
- writeFileSync13(join47(dest, MARKER), `${stamp}
22278
+ cpSync(join48(src, name), dest, { recursive: true });
22279
+ writeFileSync14(join48(dest, MARKER), `${stamp}
22225
22280
  `);
22226
22281
  installed.push(name);
22227
22282
  }
@@ -22234,13 +22289,13 @@ function uninstallSkills() {
22234
22289
  const perTarget = [];
22235
22290
  for (const t of skillTargets()) {
22236
22291
  const removed = [];
22237
- if (!existsSync44(t.dir)) {
22292
+ if (!existsSync45(t.dir)) {
22238
22293
  perTarget.push({ id: t.id, label: t.label, removed });
22239
22294
  continue;
22240
22295
  }
22241
22296
  for (const entry of readdirSync33(t.dir, { withFileTypes: true })) {
22242
22297
  if (!entry.isDirectory()) continue;
22243
- const dir = join47(t.dir, entry.name);
22298
+ const dir = join48(t.dir, entry.name);
22244
22299
  if (isManaged(dir) && (names.has(entry.name) || entry.name.startsWith("chat-recall"))) {
22245
22300
  rmSync7(dir, { recursive: true, force: true });
22246
22301
  removed.push(entry.name);
@@ -22257,8 +22312,8 @@ function skillsNeedRefresh() {
22257
22312
  for (const t of skillTargets()) {
22258
22313
  if (!t.available) continue;
22259
22314
  for (const name of names) {
22260
- const marker = join47(t.dir, name, MARKER);
22261
- if (!existsSync44(marker)) return true;
22315
+ const marker = join48(t.dir, name, MARKER);
22316
+ if (!existsSync45(marker)) return true;
22262
22317
  try {
22263
22318
  if (readFileSync39(marker, "utf-8").trim() !== cur) return true;
22264
22319
  } catch {
@@ -22275,8 +22330,8 @@ function skillStatus() {
22275
22330
  let installed = 0;
22276
22331
  let ver = null;
22277
22332
  for (const name of names) {
22278
- const marker = join47(t.dir, name, MARKER);
22279
- if (existsSync44(marker)) {
22333
+ const marker = join48(t.dir, name, MARKER);
22334
+ if (existsSync45(marker)) {
22280
22335
  installed++;
22281
22336
  try {
22282
22337
  ver = readFileSync39(marker, "utf-8").trim();
@@ -22304,8 +22359,8 @@ var init_install_skills = __esm({
22304
22359
 
22305
22360
  // ../engine/dist/core/artifact-codec.js
22306
22361
  import { readFileSync as readFileSync40 } from "fs";
22307
- import { basename as basename22, join as join48 } from "path";
22308
- import { homedir as homedir17 } from "os";
22362
+ import { basename as basename22, join as join49 } from "path";
22363
+ import { homedir as homedir18 } from "os";
22309
22364
  function parseCommandText(content, format, fallbackName) {
22310
22365
  if (format === "toml") {
22311
22366
  const t = parseScalarToml(content);
@@ -22339,29 +22394,29 @@ function readInstructions(filePath, name) {
22339
22394
  function commandPath(tool, name) {
22340
22395
  switch (tool) {
22341
22396
  case "claude":
22342
- return join48(claudeBackend.commandsDir(), `${name}.md`);
22397
+ return join49(claudeBackend.commandsDir(), `${name}.md`);
22343
22398
  case "agy":
22344
- return join48(join48(agyBackend.homeDir(), "commands"), `${name}.toml`);
22399
+ return join49(join49(agyBackend.homeDir(), "commands"), `${name}.toml`);
22345
22400
  case "opencode":
22346
- return join48(opencodeBackend.commandsDir(), `${name}.md`);
22401
+ return join49(opencodeBackend.commandsDir(), `${name}.md`);
22347
22402
  case "codex":
22348
- return join48(codexBackend.promptsDir(), `${name}.md`);
22403
+ return join49(codexBackend.promptsDir(), `${name}.md`);
22349
22404
  case "cursor":
22350
- return join48(cursorHomeDir(), "commands", `${name}.md`);
22405
+ return join49(cursorHomeDir(), "commands", `${name}.md`);
22351
22406
  }
22352
22407
  }
22353
22408
  function agentPath(tool, name) {
22354
22409
  switch (tool) {
22355
22410
  case "claude":
22356
- return join48(claudeBackend.agentsDir(), `${name}.md`);
22411
+ return join49(claudeBackend.agentsDir(), `${name}.md`);
22357
22412
  case "agy":
22358
- return join48(join48(agyBackend.homeDir(), "agents"), `${name}.md`);
22413
+ return join49(join49(agyBackend.homeDir(), "agents"), `${name}.md`);
22359
22414
  case "opencode":
22360
- return join48(opencodeBackend.agentsDir(), `${name}.md`);
22415
+ return join49(opencodeBackend.agentsDir(), `${name}.md`);
22361
22416
  case "codex":
22362
- return join48(codexBackend.agentsDir(), `${name}.toml`);
22417
+ return join49(codexBackend.agentsDir(), `${name}.toml`);
22363
22418
  case "cursor":
22364
- return join48(cursorHomeDir(), "agents", `${name}.md`);
22419
+ return join49(cursorHomeDir(), "agents", `${name}.md`);
22365
22420
  }
22366
22421
  }
22367
22422
  function instructionsFilename(tool) {
@@ -22386,21 +22441,21 @@ function instructionsFilename(tool) {
22386
22441
  function instructionsPath(tool, projectPath) {
22387
22442
  const file = instructionsFilename(tool);
22388
22443
  if (projectPath)
22389
- return join48(projectPath, file);
22444
+ return join49(projectPath, file);
22390
22445
  switch (tool) {
22391
22446
  case "claude":
22392
- return join48(claudeBackend.homeDir(), file);
22447
+ return join49(claudeBackend.homeDir(), file);
22393
22448
  case "agy":
22394
- return join48(agyBackend.homeDir(), file);
22449
+ return join49(agyBackend.homeDir(), file);
22395
22450
  case "codex":
22396
- return join48(codexBackend.homeDir(), file);
22451
+ return join49(codexBackend.homeDir(), file);
22397
22452
  case "opencode":
22398
- return join48(homedir17(), ".config", "opencode", file);
22453
+ return join49(homedir18(), ".config", "opencode", file);
22399
22454
  // Cursor has no single user-scope instructions FILE — user-level guidance
22400
22455
  // is User Rules plus ~/.cursor/rules/*.mdc. AGENTS.md at the home root is
22401
22456
  // the closest equivalent and is what a mirror can actually write.
22402
22457
  case "cursor":
22403
- return join48(cursorHomeDir(), file);
22458
+ return join49(cursorHomeDir(), file);
22404
22459
  }
22405
22460
  }
22406
22461
  function emit(type, art, toTool, projectPath) {
@@ -22464,9 +22519,9 @@ __export(toolkit_sync_exports, {
22464
22519
  supportedTargetsFor: () => supportedTargetsFor,
22465
22520
  writeMcpEntry: () => writeMcpEntry
22466
22521
  });
22467
- import { existsSync as existsSync45, mkdirSync as mkdirSync13, readFileSync as readFileSync41, writeFileSync as writeFileSync14, cpSync as cpSync2 } from "fs";
22468
- import { dirname as dirname21, join as join49, basename as basename23 } from "path";
22469
- import { homedir as homedir18 } from "os";
22522
+ import { existsSync as existsSync46, mkdirSync as mkdirSync15, readFileSync as readFileSync41, writeFileSync as writeFileSync15, cpSync as cpSync2 } from "fs";
22523
+ import { dirname as dirname22, join as join50, basename as basename23 } from "path";
22524
+ import { homedir as homedir19 } from "os";
22470
22525
  function supportedTargetsFor(type) {
22471
22526
  return SUPPORTED_TARGETS[type];
22472
22527
  }
@@ -22507,26 +22562,26 @@ function skillsDirFor(tool) {
22507
22562
  case "claude":
22508
22563
  return claudeBackend.skillsDir();
22509
22564
  case "agy":
22510
- return join49(agyBackend.homeDir(), "skills");
22565
+ return join50(agyBackend.homeDir(), "skills");
22511
22566
  case "opencode":
22512
22567
  return opencodeBackend.skillsDir();
22513
22568
  case "codex":
22514
22569
  return codexBackend.skillsDir();
22515
22570
  // user skills, NOT .system
22516
22571
  case "cursor":
22517
- return join49(cursorHomeDir(), "skills");
22572
+ return join50(cursorHomeDir(), "skills");
22518
22573
  }
22519
22574
  }
22520
22575
  function copySkill(row, toTool) {
22521
- const skillDir = row.extra.skillDir || dirname21(row.filePath);
22522
- if (!existsSync45(skillDir))
22576
+ const skillDir = row.extra.skillDir || dirname22(row.filePath);
22577
+ if (!existsSync46(skillDir))
22523
22578
  return { ok: false, status: 404, error: `Source dir missing: ${skillDir}` };
22524
22579
  const name = row.extra.skillName || basename23(skillDir);
22525
- const targetDir = join49(skillsDirFor(toTool), name);
22526
- if (existsSync45(targetDir))
22580
+ const targetDir = join50(skillsDirFor(toTool), name);
22581
+ if (existsSync46(targetDir))
22527
22582
  return { ok: false, status: 409, error: `Already exists: ${targetDir}` };
22528
22583
  try {
22529
- mkdirSync13(dirname21(targetDir), { recursive: true });
22584
+ mkdirSync15(dirname22(targetDir), { recursive: true });
22530
22585
  cpSync2(skillDir, targetDir, { recursive: true });
22531
22586
  return { ok: true, targetPath: targetDir };
22532
22587
  } catch (e) {
@@ -22534,28 +22589,28 @@ function copySkill(row, toTool) {
22534
22589
  }
22535
22590
  }
22536
22591
  function readMcpEntry(tool, name) {
22537
- const home = homedir18();
22592
+ const home = homedir19();
22538
22593
  const tries = [];
22539
22594
  if (tool === "claude") {
22540
- tries.push({ path: join49(home, ".mcp.json"), key: "mcpServers" });
22541
- tries.push({ path: join49(home, ".claude.json"), key: "mcpServers" });
22595
+ tries.push({ path: join50(home, ".mcp.json"), key: "mcpServers" });
22596
+ tries.push({ path: join50(home, ".claude.json"), key: "mcpServers" });
22542
22597
  } else if (tool === "gemini") {
22543
- tries.push({ path: join49(home, ".gemini", "settings.json"), key: "mcpServers" });
22598
+ tries.push({ path: join50(home, ".gemini", "settings.json"), key: "mcpServers" });
22544
22599
  } else if (tool === "agy") {
22545
- tries.push({ path: join49(home, ".gemini", "config", "mcp_config.json"), key: "mcpServers" });
22600
+ tries.push({ path: join50(home, ".gemini", "config", "mcp_config.json"), key: "mcpServers" });
22546
22601
  } else if (tool === "cursor") {
22547
- tries.push({ path: join49(cursorHomeDir(), "mcp.json"), key: "mcpServers" });
22602
+ tries.push({ path: join50(cursorHomeDir(), "mcp.json"), key: "mcpServers" });
22548
22603
  } else if (tool === "opencode") {
22549
- tries.push({ path: join49(home, ".config", "opencode", "opencode.json"), key: "mcp" });
22550
- tries.push({ path: join49(home, ".config", "opencode", "config.json"), key: "mcp" });
22551
- tries.push({ path: join49(home, ".opencode", "config.json"), key: "mcp" });
22604
+ tries.push({ path: join50(home, ".config", "opencode", "opencode.json"), key: "mcp" });
22605
+ tries.push({ path: join50(home, ".config", "opencode", "config.json"), key: "mcp" });
22606
+ tries.push({ path: join50(home, ".opencode", "config.json"), key: "mcp" });
22552
22607
  } else if (tool === "codex") {
22553
- const entry = readCodexMcpEntry(join49(home, ".codex", "config.toml"), name);
22608
+ const entry = readCodexMcpEntry(join50(home, ".codex", "config.toml"), name);
22554
22609
  if (entry)
22555
22610
  return entry;
22556
22611
  }
22557
22612
  for (const t of tries) {
22558
- if (!existsSync45(t.path))
22613
+ if (!existsSync46(t.path))
22559
22614
  continue;
22560
22615
  try {
22561
22616
  const cfg = JSON.parse(readFileSync41(t.path, "utf-8"));
@@ -22568,7 +22623,7 @@ function readMcpEntry(tool, name) {
22568
22623
  return null;
22569
22624
  }
22570
22625
  function readCodexMcpEntry(path, target) {
22571
- if (!existsSync45(path))
22626
+ if (!existsSync46(path))
22572
22627
  return null;
22573
22628
  let content;
22574
22629
  try {
@@ -22622,7 +22677,7 @@ function readCodexMcpEntry(path, target) {
22622
22677
  }
22623
22678
  function writeCodexMcpEntry(path, name, entry) {
22624
22679
  let content = "";
22625
- if (existsSync45(path))
22680
+ if (existsSync46(path))
22626
22681
  content = readFileSync41(path, "utf-8");
22627
22682
  const header = `[mcp_servers.${name}]`;
22628
22683
  if (content.includes(header))
@@ -22652,14 +22707,14 @@ function writeCodexMcpEntry(path, name, entry) {
22652
22707
  for (const [k, v] of Object.entries(entry.env))
22653
22708
  lines.push(`${k} = ${escape(String(v))}`);
22654
22709
  }
22655
- mkdirSync13(dirname21(path), { recursive: true });
22656
- writeFileSync14(path, content + lines.join("\n") + "\n");
22710
+ mkdirSync15(dirname22(path), { recursive: true });
22711
+ writeFileSync15(path, content + lines.join("\n") + "\n");
22657
22712
  return true;
22658
22713
  }
22659
22714
  function writeMcpEntry(toTool, name, entry) {
22660
- const home = homedir18();
22715
+ const home = homedir19();
22661
22716
  if (toTool === "codex") {
22662
- const path2 = join49(home, ".codex", "config.toml");
22717
+ const path2 = join50(home, ".codex", "config.toml");
22663
22718
  if (!writeCodexMcpEntry(path2, name, entry))
22664
22719
  return { ok: false, status: 409, error: `MCP "${name}" already exists in ${path2}.` };
22665
22720
  return { ok: true, targetPath: path2 };
@@ -22667,27 +22722,27 @@ function writeMcpEntry(toTool, name, entry) {
22667
22722
  let path;
22668
22723
  let key2;
22669
22724
  if (toTool === "claude") {
22670
- path = join49(home, ".mcp.json");
22725
+ path = join50(home, ".mcp.json");
22671
22726
  key2 = "mcpServers";
22672
22727
  } else if (toTool === "agy") {
22673
- path = join49(home, ".gemini", "config", "mcp_config.json");
22728
+ path = join50(home, ".gemini", "config", "mcp_config.json");
22674
22729
  key2 = "mcpServers";
22675
22730
  } else if (toTool === "cursor") {
22676
- path = join49(cursorHomeDir(), "mcp.json");
22731
+ path = join50(cursorHomeDir(), "mcp.json");
22677
22732
  key2 = "mcpServers";
22678
22733
  } else {
22679
- path = join49(home, ".config", "opencode", "opencode.json");
22734
+ path = join50(home, ".config", "opencode", "opencode.json");
22680
22735
  key2 = "mcp";
22681
22736
  }
22682
22737
  let cfg = {};
22683
- if (existsSync45(path)) {
22738
+ if (existsSync46(path)) {
22684
22739
  try {
22685
22740
  cfg = JSON.parse(readFileSync41(path, "utf-8"));
22686
22741
  } catch {
22687
22742
  return { ok: false, status: 500, error: `Target config is malformed: ${path}` };
22688
22743
  }
22689
22744
  } else {
22690
- mkdirSync13(dirname21(path), { recursive: true });
22745
+ mkdirSync15(dirname22(path), { recursive: true });
22691
22746
  }
22692
22747
  cfg[key2] = cfg[key2] || {};
22693
22748
  if (cfg[key2][name])
@@ -22714,7 +22769,7 @@ function writeMcpEntry(toTool, name, entry) {
22714
22769
  }
22715
22770
  cfg[key2][name] = normalized;
22716
22771
  try {
22717
- writeFileSync14(path, JSON.stringify(cfg, null, 2));
22772
+ writeFileSync15(path, JSON.stringify(cfg, null, 2));
22718
22773
  return { ok: true, targetPath: path };
22719
22774
  } catch (e) {
22720
22775
  return { ok: false, status: 500, error: e instanceof Error ? e.message : "write failed" };
@@ -22744,11 +22799,11 @@ function copyCommandOrAgent(type, row, toTool) {
22744
22799
  return { ok: false, status: 404, error: `Source unreadable: ${e instanceof Error ? e.message : "failed"}` };
22745
22800
  }
22746
22801
  const out = emit(type, art, toTool);
22747
- if (existsSync45(out.path))
22802
+ if (existsSync46(out.path))
22748
22803
  return { ok: false, status: 409, error: `Already exists: ${out.path}` };
22749
22804
  try {
22750
- mkdirSync13(dirname21(out.path), { recursive: true });
22751
- writeFileSync14(out.path, out.content);
22805
+ mkdirSync15(dirname22(out.path), { recursive: true });
22806
+ writeFileSync15(out.path, out.content);
22752
22807
  return { ok: true, targetPath: out.path };
22753
22808
  } catch (e) {
22754
22809
  return { ok: false, status: 500, error: e instanceof Error ? e.message : "write failed" };
@@ -22764,11 +22819,11 @@ function copyInstructions(row, toTool) {
22764
22819
  return { ok: false, status: 404, error: `Source unreadable: ${e instanceof Error ? e.message : "failed"}` };
22765
22820
  }
22766
22821
  const out = emit("instructions", art, toTool, row.projectPath || void 0);
22767
- if (existsSync45(out.path))
22822
+ if (existsSync46(out.path))
22768
22823
  return { ok: false, status: 409, error: `Already exists: ${out.path}` };
22769
22824
  try {
22770
- mkdirSync13(dirname21(out.path), { recursive: true });
22771
- writeFileSync14(out.path, out.content);
22825
+ mkdirSync15(dirname22(out.path), { recursive: true });
22826
+ writeFileSync15(out.path, out.content);
22772
22827
  return { ok: true, targetPath: out.path };
22773
22828
  } catch (e) {
22774
22829
  return { ok: false, status: 500, error: e instanceof Error ? e.message : "write failed" };
@@ -22914,8 +22969,8 @@ __export(toolkit_pull_exports, {
22914
22969
  planPull: () => planPull,
22915
22970
  portableCommand: () => portableCommand
22916
22971
  });
22917
- import { existsSync as existsSync46, mkdirSync as mkdirSync14, statSync as statSync41, writeFileSync as writeFileSync15 } from "node:fs";
22918
- import { dirname as dirname22, join as join50, basename as basename24, isAbsolute as isAbsolute3 } from "node:path";
22972
+ import { existsSync as existsSync47, mkdirSync as mkdirSync16, statSync as statSync41, writeFileSync as writeFileSync16 } from "node:fs";
22973
+ import { dirname as dirname23, join as join51, basename as basename24, isAbsolute as isAbsolute3 } from "node:path";
22919
22974
  function isExecutableFile(p) {
22920
22975
  try {
22921
22976
  return statSync41(p).isFile();
@@ -23065,9 +23120,9 @@ function executePull(rows, opts = {}) {
23065
23120
  const outcomes = [];
23066
23121
  for (const { name, body, truncated, redacted, tools } of skills) {
23067
23122
  for (const tool of tools) {
23068
- const dir = join50(skillsDirFor(tool), name);
23069
- const file = join50(dir, "SKILL.md");
23070
- if (existsSync46(file)) {
23123
+ const dir = join51(skillsDirFor(tool), name);
23124
+ const file = join51(dir, "SKILL.md");
23125
+ if (existsSync47(file)) {
23071
23126
  outcomes.push({ type: "skill", name, tool, status: "present" });
23072
23127
  continue;
23073
23128
  }
@@ -23080,8 +23135,8 @@ function executePull(rows, opts = {}) {
23080
23135
  continue;
23081
23136
  }
23082
23137
  try {
23083
- mkdirSync14(dir, { recursive: true });
23084
- writeFileSync15(file, body);
23138
+ mkdirSync16(dir, { recursive: true });
23139
+ writeFileSync16(file, body);
23085
23140
  outcomes.push({ type: "skill", name, tool, status: "written", path: file, redactedContent: redacted });
23086
23141
  } catch (e) {
23087
23142
  outcomes.push({ type: "skill", name, tool, status: "failed", reason: e instanceof Error ? e.message : "write failed" });
@@ -23112,7 +23167,7 @@ function executePull(rows, opts = {}) {
23112
23167
  outcomes.push({ type, name, tool, status: "failed", reason: e instanceof Error ? e.message : "emit failed" });
23113
23168
  continue;
23114
23169
  }
23115
- if (existsSync46(out.path)) {
23170
+ if (existsSync47(out.path)) {
23116
23171
  outcomes.push({ type, name, tool, status: "present" });
23117
23172
  continue;
23118
23173
  }
@@ -23121,8 +23176,8 @@ function executePull(rows, opts = {}) {
23121
23176
  continue;
23122
23177
  }
23123
23178
  try {
23124
- mkdirSync14(dirname22(out.path), { recursive: true });
23125
- writeFileSync15(out.path, out.content);
23179
+ mkdirSync16(dirname23(out.path), { recursive: true });
23180
+ writeFileSync16(out.path, out.content);
23126
23181
  outcomes.push({ type, name, tool, status: "written", path: out.path, redactedContent: redacted });
23127
23182
  } catch (e) {
23128
23183
  outcomes.push({ type, name, tool, status: "failed", reason: e instanceof Error ? e.message : "write failed" });
@@ -23258,6 +23313,103 @@ var init_init_progress = __esm({
23258
23313
  }
23259
23314
  });
23260
23315
 
23316
+ // src/board-hooks.ts
23317
+ var board_hooks_exports = {};
23318
+ __export(board_hooks_exports, {
23319
+ BOARD_HOOKS: () => BOARD_HOOKS,
23320
+ boardHookCommand: () => boardHookCommand,
23321
+ boardHookEntries: () => boardHookEntries,
23322
+ isBoardHookEntry: () => isBoardHookEntry,
23323
+ mergeBoardHooks: () => mergeBoardHooks,
23324
+ registerBoardHooks: () => registerBoardHooks,
23325
+ removeBoardHooks: () => removeBoardHooks,
23326
+ resolveCliEntry: () => resolveCliEntry
23327
+ });
23328
+ import { existsSync as existsSync48, mkdirSync as mkdirSync17, readFileSync as readFileSync42, writeFileSync as writeFileSync17 } from "node:fs";
23329
+ import { dirname as dirname24, join as join52 } from "node:path";
23330
+ function boardHookCommand(execPath, cliEntry, mode) {
23331
+ return `"${execPath}" "${cliEntry}" task-hook --${mode}`;
23332
+ }
23333
+ function isBoardHookEntry(entry) {
23334
+ const cmd = entry?.hooks?.[0]?.command;
23335
+ if (typeof cmd !== "string") return false;
23336
+ if (cmd.includes("chat_recall_task_hook.sh")) return true;
23337
+ return cmd.includes("task-hook") && (cmd.includes("--claim") || cmd.includes("--close"));
23338
+ }
23339
+ function boardHookEntries(execPath, cliEntry) {
23340
+ return BOARD_HOOKS.map(({ event, mode }) => [
23341
+ event,
23342
+ { matcher: "", hooks: [{ type: "command", command: boardHookCommand(execPath, cliEntry, mode) }] }
23343
+ ]);
23344
+ }
23345
+ function mergeBoardHooks(config2, execPath, cliEntry) {
23346
+ if (!config2.hooks || typeof config2.hooks !== "object") config2.hooks = {};
23347
+ const entries = boardHookEntries(execPath, cliEntry);
23348
+ for (const { event } of BOARD_HOOKS) {
23349
+ const current = Array.isArray(config2.hooks[event]) ? config2.hooks[event] : [];
23350
+ const kept = current.filter((h) => !isBoardHookEntry(h));
23351
+ for (const [ev, entry] of entries) if (ev === event) kept.push(entry);
23352
+ config2.hooks[event] = kept;
23353
+ }
23354
+ }
23355
+ function removeBoardHooks(config2) {
23356
+ if (!config2.hooks) return 0;
23357
+ let removed = 0;
23358
+ for (const { event } of BOARD_HOOKS) {
23359
+ const current = Array.isArray(config2.hooks[event]) ? config2.hooks[event] : [];
23360
+ const kept = current.filter((h) => !isBoardHookEntry(h));
23361
+ removed += current.length - kept.length;
23362
+ if (kept.length) config2.hooks[event] = kept;
23363
+ else delete config2.hooks[event];
23364
+ }
23365
+ return removed;
23366
+ }
23367
+ function readConfig(file) {
23368
+ if (!existsSync48(file)) return { hooks: {} };
23369
+ try {
23370
+ const parsed = JSON.parse(readFileSync42(file, "utf-8"));
23371
+ if (!parsed.hooks || typeof parsed.hooks !== "object") parsed.hooks = {};
23372
+ return parsed;
23373
+ } catch {
23374
+ return { hooks: {} };
23375
+ }
23376
+ }
23377
+ function registerBoardHooks(files, execPath, cliEntry) {
23378
+ const written = [];
23379
+ for (const file of files) {
23380
+ if (existsSync48(file)) {
23381
+ try {
23382
+ JSON.parse(readFileSync42(file, "utf-8"));
23383
+ } catch {
23384
+ continue;
23385
+ }
23386
+ }
23387
+ const config2 = readConfig(file);
23388
+ mergeBoardHooks(config2, execPath, cliEntry);
23389
+ try {
23390
+ mkdirSync17(dirname24(file), { recursive: true });
23391
+ writeFileSync17(file, JSON.stringify(config2, null, 2) + "\n");
23392
+ written.push(file);
23393
+ } catch {
23394
+ }
23395
+ }
23396
+ return written;
23397
+ }
23398
+ function resolveCliEntry(distDir) {
23399
+ const bundled = join52(distDir, "cli.js");
23400
+ return existsSync48(bundled) ? bundled : join52(distDir, "cli.ts");
23401
+ }
23402
+ var BOARD_HOOKS;
23403
+ var init_board_hooks = __esm({
23404
+ "src/board-hooks.ts"() {
23405
+ "use strict";
23406
+ BOARD_HOOKS = [
23407
+ { event: "UserPromptSubmit", mode: "claim" },
23408
+ { event: "SessionEnd", mode: "close" }
23409
+ ];
23410
+ }
23411
+ });
23412
+
23261
23413
  // src/repair.ts
23262
23414
  var repair_exports = {};
23263
23415
  __export(repair_exports, {
@@ -23613,6 +23765,222 @@ var init_escalate = __esm({
23613
23765
  }
23614
23766
  });
23615
23767
 
23768
+ // ../engine/dist/core/guard-registration.js
23769
+ var guard_registration_exports = {};
23770
+ __export(guard_registration_exports, {
23771
+ GUARD_ID: () => GUARD_ID,
23772
+ guardCommand: () => guardCommand,
23773
+ guardTargets: () => guardTargets,
23774
+ guardWrapperName: () => guardWrapperName,
23775
+ guardWrapperSource: () => guardWrapperSource,
23776
+ opencodeConfigDir: () => opencodeConfigDir,
23777
+ opencodePluginDir: () => opencodePluginDir,
23778
+ opencodePluginSource: () => opencodePluginSource,
23779
+ registerGuard: () => registerGuard,
23780
+ unregisterGuard: () => unregisterGuard
23781
+ });
23782
+ import { homedir as homedir20 } from "os";
23783
+ import { join as join53 } from "path";
23784
+ function opencodeConfigDir() {
23785
+ const xdg = process.env.XDG_CONFIG_HOME;
23786
+ const base3 = xdg && xdg.trim() ? xdg : join53(homedir20(), ".config");
23787
+ return join53(base3, "opencode");
23788
+ }
23789
+ function opencodePluginDir() {
23790
+ return join53(opencodeConfigDir(), "plugins");
23791
+ }
23792
+ function guardTargets() {
23793
+ return [
23794
+ {
23795
+ tool: "codex",
23796
+ label: "Codex",
23797
+ configPath: join53(codexBackend.homeDir(), "hooks.json"),
23798
+ homeDir: codexBackend.homeDir(),
23799
+ // Codex will not run a hook until a person has reviewed it, and the
23800
+ // approval is bound to the hook's hash. There is no way to grant it from
23801
+ // outside: /etc/codex/requirements.toml is the root-owned admin policy,
23802
+ // and the only other route is the per-invocation
23803
+ // --dangerously-bypass-hook-trust flag. An installer that could grant its
23804
+ // own approval is the thing the gate exists to stop.
23805
+ note: "Codex asks you to approve a hook before it runs one. Run /hooks in Codex once to approve this."
23806
+ },
23807
+ {
23808
+ tool: "agy",
23809
+ label: "Antigravity",
23810
+ configPath: join53(agyBackend.homeDir(), "hooks.json"),
23811
+ homeDir: agyBackend.homeDir()
23812
+ },
23813
+ {
23814
+ tool: "cursor",
23815
+ label: "Cursor",
23816
+ configPath: join53(cursorBackend.homeDir(), "hooks.json"),
23817
+ homeDir: cursorBackend.homeDir(),
23818
+ note: "Cursor checks a shell command before it runs and a file edit after it is written."
23819
+ },
23820
+ {
23821
+ tool: "opencode",
23822
+ label: "OpenCode",
23823
+ configPath: join53(opencodePluginDir(), "chat-recall-guard.js"),
23824
+ homeDir: opencodeConfigDir(),
23825
+ note: "OpenCode plugin hooks do not fire for subagent tool calls, so a delegating agent bypasses this."
23826
+ }
23827
+ ];
23828
+ }
23829
+ function guardWrapperName(tool) {
23830
+ return `chat_recall_guard_${tool}.sh`;
23831
+ }
23832
+ function guardWrapperSource(tool, hookPath) {
23833
+ return `#!/bin/sh
23834
+ # Written by \`chat-recall install-hooks\`. Re-running replaces it.
23835
+ CHAT_RECALL_GUARD_HARNESS=${tool}
23836
+ export CHAT_RECALL_GUARD_HARNESS
23837
+ exec ${JSON.stringify(hookPath)} "$@"
23838
+ `;
23839
+ }
23840
+ function guardCommand(wrapperPath) {
23841
+ return wrapperPath.includes(" ") ? `"${wrapperPath}"` : wrapperPath;
23842
+ }
23843
+ function mergeCodex(config2, command) {
23844
+ const c = config2 && typeof config2 === "object" ? { ...config2 } : {};
23845
+ const hooks = c.hooks && typeof c.hooks === "object" ? { ...c.hooks } : {};
23846
+ const kept = asArray2(hooks.PreToolUse).filter((e) => !asArray2(e?.hooks).some((h) => isOurs(h?.command)));
23847
+ kept.push({
23848
+ matcher: CODEX_MATCHER,
23849
+ hooks: [{ type: "command", command, statusMessage: "chat-recall: checking decisions", timeout: TIMEOUT_SECS }]
23850
+ });
23851
+ hooks.PreToolUse = kept;
23852
+ c.hooks = hooks;
23853
+ return c;
23854
+ }
23855
+ function stripCodex(config2) {
23856
+ const c = config2 && typeof config2 === "object" ? { ...config2 } : {};
23857
+ const hooks = c.hooks && typeof c.hooks === "object" ? { ...c.hooks } : {};
23858
+ const before = asArray2(hooks.PreToolUse);
23859
+ const kept = before.filter((e) => !asArray2(e?.hooks).some((h) => isOurs(h?.command)));
23860
+ if (kept.length)
23861
+ hooks.PreToolUse = kept;
23862
+ else
23863
+ delete hooks.PreToolUse;
23864
+ c.hooks = hooks;
23865
+ return { config: c, removed: before.length - kept.length };
23866
+ }
23867
+ function mergeAgy(config2, command) {
23868
+ const c = config2 && typeof config2 === "object" ? { ...config2 } : {};
23869
+ c[GUARD_ID] = {
23870
+ enabled: true,
23871
+ PreToolUse: [{
23872
+ matcher: AGY_MATCHER,
23873
+ hooks: [{ type: "command", command, timeout: TIMEOUT_SECS }]
23874
+ }]
23875
+ };
23876
+ return c;
23877
+ }
23878
+ function stripAgy(config2) {
23879
+ const c = config2 && typeof config2 === "object" ? { ...config2 } : {};
23880
+ const had = Object.prototype.hasOwnProperty.call(c, GUARD_ID) ? 1 : 0;
23881
+ delete c[GUARD_ID];
23882
+ return { config: c, removed: had };
23883
+ }
23884
+ function mergeCursor(config2, command) {
23885
+ const c = config2 && typeof config2 === "object" ? { ...config2 } : {};
23886
+ c.version = typeof c.version === "number" ? c.version : 1;
23887
+ const hooks = c.hooks && typeof c.hooks === "object" ? { ...c.hooks } : {};
23888
+ for (const event of ["beforeShellExecution", "afterFileEdit"]) {
23889
+ const kept = asArray2(hooks[event]).filter((e) => !isOurs(e?.command));
23890
+ kept.push({ command });
23891
+ hooks[event] = kept;
23892
+ }
23893
+ c.hooks = hooks;
23894
+ return c;
23895
+ }
23896
+ function stripCursor(config2) {
23897
+ const c = config2 && typeof config2 === "object" ? { ...config2 } : {};
23898
+ const hooks = c.hooks && typeof c.hooks === "object" ? { ...c.hooks } : {};
23899
+ let removed = 0;
23900
+ for (const event of ["beforeShellExecution", "afterFileEdit"]) {
23901
+ const before = asArray2(hooks[event]);
23902
+ const kept = before.filter((e) => !isOurs(e?.command));
23903
+ removed += before.length - kept.length;
23904
+ if (kept.length)
23905
+ hooks[event] = kept;
23906
+ else
23907
+ delete hooks[event];
23908
+ }
23909
+ c.hooks = hooks;
23910
+ return { config: c, removed };
23911
+ }
23912
+ function registerGuard(tool, config2, command) {
23913
+ switch (tool) {
23914
+ case "codex":
23915
+ return mergeCodex(config2, command);
23916
+ case "agy":
23917
+ return mergeAgy(config2, command);
23918
+ case "cursor":
23919
+ return mergeCursor(config2, command);
23920
+ }
23921
+ }
23922
+ function unregisterGuard(tool, config2) {
23923
+ switch (tool) {
23924
+ case "codex":
23925
+ return stripCodex(config2);
23926
+ case "agy":
23927
+ return stripAgy(config2);
23928
+ case "cursor":
23929
+ return stripCursor(config2);
23930
+ }
23931
+ }
23932
+ function opencodePluginSource(hookPath) {
23933
+ return `// Written by \`chat-recall install-hooks\`. Re-running replaces it.
23934
+ //
23935
+ // Sends each tool call to the same guard the other harnesses run, and prints
23936
+ // what it says. Blocking is opt-in: set CHAT_RECALL_GUARD_ENFORCE=1 and a
23937
+ // finding throws, which is how an OpenCode plugin stops a call.
23938
+ import { spawn } from "child_process";
23939
+
23940
+ const HOOK = ${JSON.stringify(hookPath)};
23941
+
23942
+ function ask(payload) {
23943
+ return new Promise((resolve) => {
23944
+ let out = "";
23945
+ const child = spawn(HOOK, {
23946
+ env: { ...process.env, CHAT_RECALL_GUARD_HARNESS: "opencode" },
23947
+ stdio: ["pipe", "pipe", "ignore"],
23948
+ });
23949
+ const done = setTimeout(() => { child.kill(); resolve(null); }, ${TIMEOUT_SECS * 1e3});
23950
+ child.stdout.on("data", (c) => { out += c; });
23951
+ child.on("error", () => { clearTimeout(done); resolve(null); });
23952
+ child.on("close", () => {
23953
+ clearTimeout(done);
23954
+ try { resolve(out.trim() ? JSON.parse(out) : null); } catch { resolve(null); }
23955
+ });
23956
+ child.stdin.end(JSON.stringify(payload));
23957
+ });
23958
+ }
23959
+
23960
+ export const ChatRecallGuard = async () => ({
23961
+ "tool.execute.before": async (input, output) => {
23962
+ const verdict = await ask({ tool: input?.tool, args: output?.args });
23963
+ if (!verdict || !verdict.message) return;
23964
+ if (verdict.block) throw new Error(verdict.message);
23965
+ console.error(verdict.message);
23966
+ },
23967
+ });
23968
+ `;
23969
+ }
23970
+ var GUARD_ID, AGY_MATCHER, CODEX_MATCHER, TIMEOUT_SECS, isOurs, asArray2;
23971
+ var init_guard_registration = __esm({
23972
+ "../engine/dist/core/guard-registration.js"() {
23973
+ "use strict";
23974
+ init_backends();
23975
+ GUARD_ID = "chat-recall-guard";
23976
+ AGY_MATCHER = "run_command|write_to_file|replace_file_content|multi_replace_file_content";
23977
+ CODEX_MATCHER = "Bash|apply_patch|Edit|Write";
23978
+ TIMEOUT_SECS = 8;
23979
+ isOurs = (cmd) => typeof cmd === "string" && cmd.includes("chat_recall_guard");
23980
+ asArray2 = (v) => Array.isArray(v) ? v : [];
23981
+ }
23982
+ });
23983
+
23616
23984
  // src/verify-deep.ts
23617
23985
  var verify_deep_exports = {};
23618
23986
  __export(verify_deep_exports, {
@@ -23681,11 +24049,11 @@ var verify_repair_exports = {};
23681
24049
  __export(verify_repair_exports, {
23682
24050
  forceFullResync: () => forceFullResync
23683
24051
  });
23684
- import { copyFileSync as copyFileSync3, existsSync as existsSync47 } from "fs";
24052
+ import { copyFileSync as copyFileSync3, existsSync as existsSync49 } from "fs";
23685
24053
  function forceFullResync(server, sessionIds) {
23686
24054
  if (sessionIds.length === 0) return 0;
23687
24055
  const path = ledgerFilePath();
23688
- if (existsSync47(path)) {
24056
+ if (existsSync49(path)) {
23689
24057
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace(/-/g, "");
23690
24058
  try {
23691
24059
  copyFileSync3(path, `${path}.bak-${stamp}`);
@@ -23875,8 +24243,8 @@ __export(collector_exports, {
23875
24243
  });
23876
24244
  import { spawn as spawn3 } from "node:child_process";
23877
24245
  import { execSync as execSync4 } from "node:child_process";
23878
- import { readFileSync as readFileSync42 } from "node:fs";
23879
- import { join as join51, basename as basename26 } from "node:path";
24246
+ import { readFileSync as readFileSync43 } from "node:fs";
24247
+ import { join as join54, basename as basename26 } from "node:path";
23880
24248
  async function resolveCodeindexBin(autoInstall = true) {
23881
24249
  let st = checkCodeindexStatus();
23882
24250
  if (!st.installed && autoInstall)
@@ -24055,7 +24423,7 @@ async function collectCode(opts) {
24055
24423
  }
24056
24424
  const complexity = (r) => {
24057
24425
  try {
24058
- const txt = readFileSync42(join51(ws, r), "utf8");
24426
+ const txt = readFileSync43(join54(ws, r), "utf8");
24059
24427
  if (txt.length > 2e6)
24060
24428
  return 1;
24061
24429
  const m = txt.match(CF_RE);
@@ -24839,9 +25207,9 @@ __export(project_tasks_exports, {
24839
25207
  pushProjectTaskStatuses: () => pushProjectTaskStatuses,
24840
25208
  renderTeamTasksMd: () => renderTeamTasksMd
24841
25209
  });
24842
- import { existsSync as existsSync48, readFileSync as readFileSync43, writeFileSync as writeFileSync16, mkdirSync as mkdirSync15, statSync as statSync42 } from "node:fs";
24843
- import { join as join52, dirname as dirname23 } from "node:path";
24844
- import { homedir as homedir19 } from "node:os";
25210
+ import { existsSync as existsSync50, readFileSync as readFileSync44, writeFileSync as writeFileSync18, mkdirSync as mkdirSync18, statSync as statSync42 } from "node:fs";
25211
+ import { join as join55, dirname as dirname25 } from "node:path";
25212
+ import { homedir as homedir21 } from "node:os";
24845
25213
  function isTeamTaskStatus(v) {
24846
25214
  return !!v && TEAM_STATUSES.has(v);
24847
25215
  }
@@ -24891,22 +25259,22 @@ function parseTaskFile(content) {
24891
25259
  return out;
24892
25260
  }
24893
25261
  function ledgerPath2() {
24894
- return join52(homedir19(), ".chat-recall", "project-tasks-ledger.json");
25262
+ return join55(homedir21(), ".chat-recall", "project-tasks-ledger.json");
24895
25263
  }
24896
25264
  function readLedger() {
24897
25265
  try {
24898
- return JSON.parse(readFileSync43(ledgerPath2(), "utf8"));
25266
+ return JSON.parse(readFileSync44(ledgerPath2(), "utf8"));
24899
25267
  } catch {
24900
25268
  return {};
24901
25269
  }
24902
25270
  }
24903
25271
  function writeLedger(l) {
24904
25272
  try {
24905
- mkdirSync15(dirname23(ledgerPath2()), { recursive: true });
25273
+ mkdirSync18(dirname25(ledgerPath2()), { recursive: true });
24906
25274
  } catch {
24907
25275
  }
24908
25276
  try {
24909
- writeFileSync16(ledgerPath2(), JSON.stringify(l));
25277
+ writeFileSync18(ledgerPath2(), JSON.stringify(l));
24910
25278
  } catch {
24911
25279
  }
24912
25280
  }
@@ -24924,8 +25292,8 @@ async function pushProjectTaskStatuses(base3, authHeaders, opts = {}) {
24924
25292
  continue;
24925
25293
  }
24926
25294
  for (const project of projects) {
24927
- const file = join52(project, fk.filename);
24928
- if (!existsSync48(file)) continue;
25295
+ const file = join55(project, fk.filename);
25296
+ if (!existsSync50(file)) continue;
24929
25297
  let mtime;
24930
25298
  try {
24931
25299
  mtime = statSync42(file).mtimeMs;
@@ -24935,7 +25303,7 @@ async function pushProjectTaskStatuses(base3, authHeaders, opts = {}) {
24935
25303
  if (ledger[file] === mtime) continue;
24936
25304
  let changed;
24937
25305
  try {
24938
- changed = parseTaskFile(readFileSync43(file, "utf8")).filter((t) => t.kind === fk.kind);
25306
+ changed = parseTaskFile(readFileSync44(file, "utf8")).filter((t) => t.kind === fk.kind);
24939
25307
  } catch {
24940
25308
  continue;
24941
25309
  }
@@ -25007,10 +25375,10 @@ __export(intent_drain_exports, {
25007
25375
  drainSyncIntents: () => drainSyncIntents,
25008
25376
  instructionTargets: () => instructionTargets
25009
25377
  });
25010
- import { existsSync as existsSync49, readFileSync as readFileSync44, writeFileSync as writeFileSync17, appendFileSync, mkdirSync as mkdirSync16 } from "node:fs";
25011
- import { join as join53 } from "node:path";
25378
+ import { existsSync as existsSync51, readFileSync as readFileSync45, writeFileSync as writeFileSync19, appendFileSync, mkdirSync as mkdirSync19 } from "node:fs";
25379
+ import { join as join56 } from "node:path";
25012
25380
  import { fileURLToPath as fileURLToPath4 } from "node:url";
25013
- import { dirname as dirname24 } from "node:path";
25381
+ import { dirname as dirname26 } from "node:path";
25014
25382
  function instructionTargets(rootPath, isGlobal) {
25015
25383
  const installed = INSTRUCTION_TOOLS.filter((t) => {
25016
25384
  try {
@@ -25022,17 +25390,17 @@ function instructionTargets(rootPath, isGlobal) {
25022
25390
  const tools = installed.length ? installed : [INSTRUCTION_TOOLS[0]];
25023
25391
  if (isGlobal) return [...new Set(tools.map((t) => instructionsPath(t.id)))];
25024
25392
  if (!rootPath) return [];
25025
- const present = INSTRUCTION_FILENAMES.map((f) => join53(rootPath, f)).filter((p) => existsSync49(p));
25393
+ const present = INSTRUCTION_FILENAMES.map((f) => join56(rootPath, f)).filter((p) => existsSync51(p));
25026
25394
  if (present.length) return present;
25027
- return [...new Set(tools.map((t) => join53(rootPath, instructionsFilename(t.id))))];
25395
+ return [...new Set(tools.map((t) => join56(rootPath, instructionsFilename(t.id))))];
25028
25396
  }
25029
25397
  function applyCodeRecommendation(intent) {
25030
25398
  try {
25031
25399
  const meta = JSON.parse(intent.name || "{}");
25032
25400
  if (intent.artifact_type === "write_tasks_file") {
25033
25401
  if (!meta.rootPath || !meta.content) return { status: "error", result: JSON.stringify({ error: "missing rootPath or content" }) };
25034
- const file = join53(meta.rootPath, meta.filename || "CODE_TASKS.md");
25035
- writeFileSync17(file, meta.content);
25402
+ const file = join56(meta.rootPath, meta.filename || "CODE_TASKS.md");
25403
+ writeFileSync19(file, meta.content);
25036
25404
  return { status: "done", result: JSON.stringify({ wrote: file }) };
25037
25405
  }
25038
25406
  if (intent.artifact_type === "append_claude_md") {
@@ -25047,10 +25415,10 @@ function applyCodeRecommendation(intent) {
25047
25415
  for (const file of targets) {
25048
25416
  const dir = file.slice(0, file.lastIndexOf("/"));
25049
25417
  try {
25050
- mkdirSync16(dir, { recursive: true });
25418
+ mkdirSync19(dir, { recursive: true });
25051
25419
  } catch {
25052
25420
  }
25053
- const existing = existsSync49(file) ? readFileSync44(file, "utf8") : "";
25421
+ const existing = existsSync51(file) ? readFileSync45(file, "utf8") : "";
25054
25422
  if (existing.includes(text)) {
25055
25423
  skipped.push(file);
25056
25424
  continue;
@@ -25060,7 +25428,7 @@ function applyCodeRecommendation(intent) {
25060
25428
  ${text}
25061
25429
  `;
25062
25430
  if (existing) appendFileSync(file, block);
25063
- else writeFileSync17(file, `# ${meta.rootPath?.split("/").pop() || "Global"} instructions
25431
+ else writeFileSync19(file, `# ${meta.rootPath?.split("/").pop() || "Global"} instructions
25064
25432
  ${block}`);
25065
25433
  appended.push(file);
25066
25434
  }
@@ -25224,11 +25592,11 @@ var init_intent_drain = __esm({
25224
25592
  ];
25225
25593
  ownVersion2 = "0.0.0";
25226
25594
  try {
25227
- const dir = dirname24(fileURLToPath4(import.meta.url));
25228
- ownVersion2 = JSON.parse(readFileSync44(join53(dir, "../package.json"), "utf-8")).version || ownVersion2;
25595
+ const dir = dirname26(fileURLToPath4(import.meta.url));
25596
+ ownVersion2 = JSON.parse(readFileSync45(join56(dir, "../package.json"), "utf-8")).version || ownVersion2;
25229
25597
  } catch {
25230
25598
  try {
25231
- if (true) ownVersion2 = "0.6.5";
25599
+ if (true) ownVersion2 = "0.6.7";
25232
25600
  } catch {
25233
25601
  }
25234
25602
  }
@@ -25429,48 +25797,48 @@ var team_merge_exports = {};
25429
25797
  __export(team_merge_exports, {
25430
25798
  mergePullResult: () => mergePullResult
25431
25799
  });
25432
- import { existsSync as existsSync50, mkdirSync as mkdirSync17, readFileSync as readFileSync45, rmSync as rmSync8, writeFileSync as writeFileSync18, statSync as statSync43 } from "fs";
25433
- import { dirname as dirname25, join as join54 } from "path";
25800
+ import { existsSync as existsSync52, mkdirSync as mkdirSync20, readFileSync as readFileSync46, rmSync as rmSync8, writeFileSync as writeFileSync20, statSync as statSync43 } from "fs";
25801
+ import { dirname as dirname27, join as join57 } from "path";
25434
25802
  import { createHash as createHash14 } from "crypto";
25435
25803
  function installPathFor(art, tool) {
25436
25804
  switch (art.type) {
25437
25805
  case "skill":
25438
25806
  switch (tool) {
25439
25807
  case "claude":
25440
- return join54(claudeBackend.skillsDir(), art.name, "SKILL.md");
25808
+ return join57(claudeBackend.skillsDir(), art.name, "SKILL.md");
25441
25809
  // Antigravity's OWN skills dir. It used to resolve to the shared
25442
25810
  // ~/.gemini/skills while toolkit-sync installed to
25443
25811
  // ~/.gemini/antigravity-cli/skills, so a team skill landed in one place
25444
25812
  // and a synced skill in the other.
25445
25813
  case "agy":
25446
- return join54(agyBackend.homeDir(), "skills", art.name, "SKILL.md");
25814
+ return join57(agyBackend.homeDir(), "skills", art.name, "SKILL.md");
25447
25815
  case "opencode":
25448
- return join54(opencodeBackend.skillsDir(), art.name, "SKILL.md");
25816
+ return join57(opencodeBackend.skillsDir(), art.name, "SKILL.md");
25449
25817
  // User skills go to ~/.codex/skills (NOT .system, which is OpenAI's bundle).
25450
25818
  case "codex":
25451
- return join54(codexBackend.skillsDir(), art.name, "SKILL.md");
25819
+ return join57(codexBackend.skillsDir(), art.name, "SKILL.md");
25452
25820
  case "cursor":
25453
- return join54(cursorHomeDir(), "skills", art.name, "SKILL.md");
25821
+ return join57(cursorHomeDir(), "skills", art.name, "SKILL.md");
25454
25822
  }
25455
25823
  break;
25456
25824
  case "agent":
25457
25825
  if (tool === "claude")
25458
- return join54(claudeBackend.agentsDir(), `${art.name}.md`);
25826
+ return join57(claudeBackend.agentsDir(), `${art.name}.md`);
25459
25827
  return null;
25460
25828
  case "command":
25461
25829
  if (tool === "claude")
25462
- return join54(claudeBackend.commandsDir(), `${art.name}.md`);
25830
+ return join57(claudeBackend.commandsDir(), `${art.name}.md`);
25463
25831
  return null;
25464
25832
  case "hook":
25465
25833
  if (tool === "claude")
25466
- return join54(claudeBackend.homeDir(), "chat-recall-hooks", `${art.name}.json`);
25834
+ return join57(claudeBackend.homeDir(), "chat-recall-hooks", `${art.name}.json`);
25467
25835
  return null;
25468
25836
  case "plan":
25469
25837
  switch (tool) {
25470
25838
  case "claude":
25471
- return join54(claudeBackend.plansDir(), `${art.name}.md`);
25839
+ return join57(claudeBackend.plansDir(), `${art.name}.md`);
25472
25840
  case "opencode":
25473
- return join54(opencodeBackend.plansDir(), `${art.name}.md`);
25841
+ return join57(opencodeBackend.plansDir(), `${art.name}.md`);
25474
25842
  case "agy":
25475
25843
  return null;
25476
25844
  // Antigravity plans live per-session
@@ -25481,11 +25849,11 @@ function installPathFor(art, tool) {
25481
25849
  }
25482
25850
  break;
25483
25851
  case "instructions":
25484
- return join54(getDataDir(), "team-instructions", tool, `${art.name}.md`);
25852
+ return join57(getDataDir(), "team-instructions", tool, `${art.name}.md`);
25485
25853
  case "mcp":
25486
- return join54(getDataDir(), "team-mcps", `${art.name}.json`);
25854
+ return join57(getDataDir(), "team-mcps", `${art.name}.json`);
25487
25855
  case "plugin":
25488
- return join54(getDataDir(), "team-plugins", tool, art.name, "manifest.json");
25856
+ return join57(getDataDir(), "team-plugins", tool, art.name, "manifest.json");
25489
25857
  }
25490
25858
  return null;
25491
25859
  }
@@ -25524,8 +25892,8 @@ async function mergePullResult(opts) {
25524
25892
  pending.push({ artifactId: a.id, tool, artifactType: a.type, artifactName: a.name, sha256: sha, installedAt: Date.now() });
25525
25893
  continue;
25526
25894
  }
25527
- mkdirSync17(dirname25(path), { recursive: true });
25528
- writeFileSync18(path, body);
25895
+ mkdirSync20(dirname27(path), { recursive: true });
25896
+ writeFileSync20(path, body);
25529
25897
  pending.push({ artifactId: a.id, tool, artifactType: a.type, artifactName: a.name, sha256: sha, installedAt: Date.now() });
25530
25898
  report.written.push({ artifactId: a.id, path });
25531
25899
  } catch (err) {
@@ -25546,7 +25914,7 @@ async function mergePullResult(opts) {
25546
25914
  if (!path)
25547
25915
  continue;
25548
25916
  try {
25549
- if (existsSync50(path))
25917
+ if (existsSync52(path))
25550
25918
  rmSync8(path, { force: true });
25551
25919
  await teamInstallsForget(id, row.tool);
25552
25920
  report.removed.push({ artifactId: id, path });
@@ -25564,12 +25932,12 @@ async function mergePullResult(opts) {
25564
25932
  }
25565
25933
  function readFileIfExists(path) {
25566
25934
  try {
25567
- if (!existsSync50(path))
25935
+ if (!existsSync52(path))
25568
25936
  return null;
25569
25937
  const s = statSync43(path);
25570
25938
  if (!s.isFile())
25571
25939
  return null;
25572
- return readFileSync45(path);
25940
+ return readFileSync46(path);
25573
25941
  } catch {
25574
25942
  return null;
25575
25943
  }
@@ -25650,8 +26018,8 @@ var init_vault_crypto = __esm({
25650
26018
  });
25651
26019
 
25652
26020
  // ../engine/dist/core/vault-source.js
25653
- import { existsSync as existsSync51, readdirSync as readdirSync34, statSync as statSync44 } from "fs";
25654
- import { extname, join as join55 } from "path";
26021
+ import { existsSync as existsSync53, readdirSync as readdirSync34, statSync as statSync44 } from "fs";
26022
+ import { extname, join as join58 } from "path";
25655
26023
  function* walkVaultSources() {
25656
26024
  const t = loadSettings().team.vault;
25657
26025
  const include2 = new Set(t.syncTools);
@@ -25676,7 +26044,7 @@ function safeStat(path) {
25676
26044
  }
25677
26045
  function* walkClaude() {
25678
26046
  const root = claudeBackend.projectsDir();
25679
- if (!existsSync51(root))
26047
+ if (!existsSync53(root))
25680
26048
  return;
25681
26049
  let projectDirs = [];
25682
26050
  try {
@@ -25688,7 +26056,7 @@ function* walkClaude() {
25688
26056
  if (proj.startsWith("."))
25689
26057
  continue;
25690
26058
  const projectPath = resolveProjectDirName(proj);
25691
- const projDir = join55(root, proj);
26059
+ const projDir = join58(root, proj);
25692
26060
  let entries = [];
25693
26061
  try {
25694
26062
  entries = readdirSync34(projDir);
@@ -25698,7 +26066,7 @@ function* walkClaude() {
25698
26066
  for (const f of entries) {
25699
26067
  if (extname(f) !== ".jsonl")
25700
26068
  continue;
25701
- const path = join55(projDir, f);
26069
+ const path = join58(projDir, f);
25702
26070
  const s = safeStat(path);
25703
26071
  if (!s)
25704
26072
  continue;
@@ -25728,8 +26096,8 @@ function* walkCursor() {
25728
26096
  }
25729
26097
  }
25730
26098
  function* walkCodex() {
25731
- const sessionsRoot = join55(codexBackend.homeDir(), "sessions");
25732
- if (!existsSync51(sessionsRoot))
26099
+ const sessionsRoot = join58(codexBackend.homeDir(), "sessions");
26100
+ if (!existsSync53(sessionsRoot))
25733
26101
  return;
25734
26102
  const stack = [sessionsRoot];
25735
26103
  while (stack.length) {
@@ -25741,7 +26109,7 @@ function* walkCodex() {
25741
26109
  continue;
25742
26110
  }
25743
26111
  for (const e of entries) {
25744
- const path = join55(dir, e.name);
26112
+ const path = join58(dir, e.name);
25745
26113
  if (e.isDirectory()) {
25746
26114
  stack.push(path);
25747
26115
  continue;
@@ -25757,8 +26125,8 @@ function* walkCodex() {
25757
26125
  }
25758
26126
  }
25759
26127
  function* walkAgy() {
25760
- const brainDir = join55(agyBackend.homeDir(), "brain");
25761
- if (!existsSync51(brainDir))
26128
+ const brainDir = join58(agyBackend.homeDir(), "brain");
26129
+ if (!existsSync53(brainDir))
25762
26130
  return;
25763
26131
  let sessionDirs = [];
25764
26132
  try {
@@ -25767,18 +26135,18 @@ function* walkAgy() {
25767
26135
  return;
25768
26136
  }
25769
26137
  for (const rawId of sessionDirs) {
25770
- const sessionPath = join55(brainDir, rawId);
26138
+ const sessionPath = join58(brainDir, rawId);
25771
26139
  try {
25772
26140
  if (!statSync44(sessionPath).isDirectory())
25773
26141
  continue;
25774
26142
  } catch {
25775
26143
  continue;
25776
26144
  }
25777
- let filePath = join55(sessionPath, ".system_generated", "logs", "transcript.jsonl");
25778
- if (!existsSync51(filePath)) {
25779
- filePath = join55(sessionPath, ".system_generated", "logs", "transcript_full.jsonl");
26145
+ let filePath = join58(sessionPath, ".system_generated", "logs", "transcript.jsonl");
26146
+ if (!existsSync53(filePath)) {
26147
+ filePath = join58(sessionPath, ".system_generated", "logs", "transcript_full.jsonl");
25780
26148
  }
25781
- if (!existsSync51(filePath))
26149
+ if (!existsSync53(filePath))
25782
26150
  continue;
25783
26151
  const s = safeStat(filePath);
25784
26152
  if (!s)
@@ -25814,8 +26182,8 @@ __export(vault_client_exports, {
25814
26182
  vaultStatus: () => vaultStatus,
25815
26183
  vaultSync: () => vaultSync
25816
26184
  });
25817
- import { mkdirSync as mkdirSync18, readFileSync as readFileSync46, writeFileSync as writeFileSync19, existsSync as existsSync52 } from "fs";
25818
- import { dirname as dirname26, join as join56 } from "path";
26185
+ import { mkdirSync as mkdirSync21, readFileSync as readFileSync47, writeFileSync as writeFileSync21, existsSync as existsSync54 } from "fs";
26186
+ import { dirname as dirname28, join as join59 } from "path";
25819
26187
  function context2() {
25820
26188
  const s = loadSettings().team;
25821
26189
  if (!s.enabled)
@@ -25954,7 +26322,7 @@ async function vaultSync(passphrase) {
25954
26322
  continue;
25955
26323
  }
25956
26324
  try {
25957
- const plaintext = readFileSync46(src.path);
26325
+ const plaintext = readFileSync47(src.path);
25958
26326
  const sourceSha = sha256Hex(plaintext);
25959
26327
  const priorSha = uploaded.get(uploadKey(src.sessionId, src.tool));
25960
26328
  if (priorSha === sourceSha) {
@@ -26006,7 +26374,7 @@ async function vaultSync(passphrase) {
26006
26374
  }
26007
26375
  function importedPathFor(tool, sessionId) {
26008
26376
  const base3 = sessionId.replace(/^gemini_/, "").replace(/^codex_/, "").replace(/^agy_/, "").replace(/^cursor_/, "").replace(/^opencode_/, "");
26009
- return join56(IMPORTED_ROOT(), tool, `${base3}.jsonl`);
26377
+ return join59(IMPORTED_ROOT(), tool, `${base3}.jsonl`);
26010
26378
  }
26011
26379
  async function vaultRestore(passphrase, opts = {}) {
26012
26380
  const ctx = context2();
@@ -26027,7 +26395,7 @@ async function vaultRestore(passphrase, opts = {}) {
26027
26395
  }
26028
26396
  try {
26029
26397
  const path = importedPathFor(blob.tool, blob.sessionId);
26030
- if (existsSync52(path)) {
26398
+ if (existsSync54(path)) {
26031
26399
  const localStat = await import("fs").then((m) => m.statSync(path));
26032
26400
  if (Math.abs(localStat.mtimeMs - blob.mtimeSourceMs) < 1) {
26033
26401
  report.skipped.push({ sessionId: blob.sessionId, tool: blob.tool, reason: "unchanged" });
@@ -26043,8 +26411,8 @@ async function vaultRestore(passphrase, opts = {}) {
26043
26411
  throw new Error("cipher integrity check failed");
26044
26412
  }
26045
26413
  const plaintext = decryptBlob(key2, cipher);
26046
- mkdirSync18(dirname26(path), { recursive: true });
26047
- writeFileSync19(path, Buffer.from(plaintext));
26414
+ mkdirSync21(dirname28(path), { recursive: true });
26415
+ writeFileSync21(path, Buffer.from(plaintext));
26048
26416
  const { utimesSync } = await import("fs");
26049
26417
  const t = blob.mtimeSourceMs / 1e3;
26050
26418
  utimesSync(path, t, t);
@@ -26095,7 +26463,7 @@ var init_vault_client = __esm({
26095
26463
  uploadKey = (sessionId, tool) => `${sessionId}\0${tool}`;
26096
26464
  VaultPassphraseMismatchError = class extends Error {
26097
26465
  };
26098
- IMPORTED_ROOT = () => join56(getDataDir(), "imported");
26466
+ IMPORTED_ROOT = () => join59(getDataDir(), "imported");
26099
26467
  }
26100
26468
  });
26101
26469
 
@@ -26236,6 +26604,413 @@ var init_keyring = __esm({
26236
26604
  }
26237
26605
  });
26238
26606
 
26607
+ // ../engine/dist/core/decision-guard.js
26608
+ function normaliseName(raw) {
26609
+ let t = raw.trim().replace(/^["'`]|["'`,;]+$/g, "").trim();
26610
+ if (!t || NOT_A_PACKAGE.has(t))
26611
+ return null;
26612
+ if (t.startsWith("-"))
26613
+ return null;
26614
+ if (/^(?:\.{1,2}\/|\/|~\/|[a-z]+:\/\/)/i.test(t))
26615
+ return null;
26616
+ if (/\.(?:tgz|tar\.gz|whl|zip)$/i.test(t))
26617
+ return null;
26618
+ if (t.startsWith("@")) {
26619
+ const slash = t.indexOf("/");
26620
+ t = slash > 0 ? t.slice(slash + 1) : t.slice(1);
26621
+ }
26622
+ t = t.split("@")[0];
26623
+ t = t.split(/[=<>~!^]/)[0];
26624
+ t = t.replace(/\[[^\]]*\]$/, "");
26625
+ t = t.trim().toLowerCase();
26626
+ if (!t || t.length < 2)
26627
+ return null;
26628
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(t))
26629
+ return null;
26630
+ return t;
26631
+ }
26632
+ function fromCommand(command) {
26633
+ const out = [];
26634
+ for (const { re } of INSTALL_PATTERNS) {
26635
+ re.lastIndex = 0;
26636
+ let m;
26637
+ while ((m = re.exec(command)) !== null) {
26638
+ for (const tok of (m[1] ?? "").split(/\s+/)) {
26639
+ const n = normaliseName(tok);
26640
+ if (n)
26641
+ out.push(n);
26642
+ }
26643
+ }
26644
+ }
26645
+ return out;
26646
+ }
26647
+ function fromContent(content) {
26648
+ const out = [];
26649
+ for (const re of IMPORT_PATTERNS) {
26650
+ re.lastIndex = 0;
26651
+ let m;
26652
+ while ((m = re.exec(content)) !== null) {
26653
+ const spec = m[1] ?? m[2];
26654
+ if (!spec)
26655
+ continue;
26656
+ const root = spec.split(/[./]/)[0];
26657
+ const n = normaliseName(spec.startsWith("@") ? spec : root);
26658
+ if (n)
26659
+ out.push(n);
26660
+ }
26661
+ }
26662
+ return out;
26663
+ }
26664
+ function fromManifest(content) {
26665
+ const out = [];
26666
+ for (const m of content.matchAll(/"([^"]+)"\s*:\s*"[^"]*"/g)) {
26667
+ const n = normaliseName(m[1]);
26668
+ if (n)
26669
+ out.push(n);
26670
+ }
26671
+ for (const m of content.matchAll(/^\s*([A-Za-z][\w.-]*)\s*(?:=|==|>=|$)/gm)) {
26672
+ const n = normaliseName(m[1]);
26673
+ if (n)
26674
+ out.push(n);
26675
+ }
26676
+ return out;
26677
+ }
26678
+ function introducedBy(input) {
26679
+ const names = /* @__PURE__ */ new Set();
26680
+ let via = null;
26681
+ if (input.command) {
26682
+ for (const n of fromCommand(input.command)) {
26683
+ names.add(n);
26684
+ via = "install";
26685
+ }
26686
+ }
26687
+ if (input.content) {
26688
+ if (isManifest(input.path)) {
26689
+ for (const n of fromManifest(input.content)) {
26690
+ names.add(n);
26691
+ via = via ?? "manifest";
26692
+ }
26693
+ }
26694
+ for (const n of fromContent(input.content)) {
26695
+ names.add(n);
26696
+ via = via ?? "import";
26697
+ }
26698
+ }
26699
+ return { names: [...names], via };
26700
+ }
26701
+ var INSTALL_PATTERNS, NOT_A_PACKAGE, MANIFESTS, IMPORT_PATTERNS, isManifest;
26702
+ var init_decision_guard = __esm({
26703
+ "../engine/dist/core/decision-guard.js"() {
26704
+ "use strict";
26705
+ INSTALL_PATTERNS = [
26706
+ // npm/pnpm/yarn/bun. `add` and `install`, global or not.
26707
+ { re: /\b(?:npm|pnpm|yarn|bun)\s+(?:install|add|i)\b([^\n;&|]*)/gi, via: "install" },
26708
+ // pip / pipx / uv
26709
+ { re: /\b(?:pip3?|pipx|uv\s+pip)\s+install\b([^\n;&|]*)/gi, via: "install" },
26710
+ // cargo
26711
+ { re: /\bcargo\s+add\b([^\n;&|]*)/gi, via: "install" },
26712
+ // go
26713
+ { re: /\bgo\s+get\b([^\n;&|]*)/gi, via: "install" },
26714
+ // ruby
26715
+ { re: /\b(?:gem\s+install|bundle\s+add)\b([^\n;&|]*)/gi, via: "install" },
26716
+ // php
26717
+ { re: /\bcomposer\s+require\b([^\n;&|]*)/gi, via: "install" }
26718
+ ];
26719
+ NOT_A_PACKAGE = /* @__PURE__ */ new Set([
26720
+ "-g",
26721
+ "--global",
26722
+ "-d",
26723
+ "-D",
26724
+ "--save",
26725
+ "--save-dev",
26726
+ "--save-exact",
26727
+ "--dev",
26728
+ "-e",
26729
+ "--editable",
26730
+ "--user",
26731
+ "--upgrade",
26732
+ "-U",
26733
+ "--force",
26734
+ "-f",
26735
+ "--yes",
26736
+ "-y",
26737
+ "--prefix",
26738
+ "--no-save",
26739
+ "--legacy-peer-deps",
26740
+ "--frozen-lockfile",
26741
+ "-r",
26742
+ "--requirement",
26743
+ "--quiet",
26744
+ "-q",
26745
+ "--silent",
26746
+ ".",
26747
+ "..",
26748
+ "*"
26749
+ ]);
26750
+ MANIFESTS = [
26751
+ "package.json",
26752
+ "cargo.toml",
26753
+ "requirements.txt",
26754
+ "pyproject.toml",
26755
+ "go.mod",
26756
+ "gemfile",
26757
+ "composer.json",
26758
+ "build.gradle",
26759
+ "pom.xml"
26760
+ ];
26761
+ IMPORT_PATTERNS = [
26762
+ /\bimport\s+(?:[\w*{}\s,]+\s+from\s+)?["']([^"']+)["']/g,
26763
+ // ES
26764
+ /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g,
26765
+ // CJS
26766
+ // Python. The bare `import X` form must END there: without the anchor it also
26767
+ // matched the `import Keycloak` half of `import Keycloak from "keycloak-js"`,
26768
+ // so one JS line yielded both `keycloak` and `keycloak-js` and the guard
26769
+ // would have reported a library nobody named.
26770
+ /^\s*from\s+([\w.]+)\s+import\b/gm,
26771
+ /^\s*import\s+([\w.]+)(?:\s+as\s+\w+)?\s*$/gm,
26772
+ /^\s*use\s+([a-z0-9_]+)\s*(?:::|;)/gmi
26773
+ // Rust
26774
+ ];
26775
+ isManifest = (p) => {
26776
+ if (!p)
26777
+ return false;
26778
+ const base3 = p.split(/[\\/]/).pop()?.toLowerCase() ?? "";
26779
+ return MANIFESTS.includes(base3);
26780
+ };
26781
+ }
26782
+ });
26783
+
26784
+ // ../engine/dist/core/guard-adapters.js
26785
+ var guard_adapters_exports = {};
26786
+ __export(guard_adapters_exports, {
26787
+ COVERAGE: () => COVERAGE,
26788
+ explain: () => explain,
26789
+ namesFrom: () => namesFrom,
26790
+ readPayload: () => readPayload,
26791
+ writeVerdict: () => writeVerdict
26792
+ });
26793
+ function readPayload(harness, payload) {
26794
+ const p = payload ?? {};
26795
+ switch (harness) {
26796
+ case "claude":
26797
+ case "codex": {
26798
+ const input = p.tool_input ?? p.toolInput ?? {};
26799
+ return {
26800
+ tool: p.tool_name ?? p.toolName ?? null,
26801
+ command: input.command ?? null,
26802
+ path: input.file_path ?? input.path ?? null,
26803
+ content: input.new_string ?? input.content ?? input.new_str ?? null
26804
+ };
26805
+ }
26806
+ case "agy": {
26807
+ const call2 = p.toolCall ?? p.tool_call ?? {};
26808
+ const input = call2.args ?? p.tool_input ?? p.args ?? {};
26809
+ return {
26810
+ tool: call2.name ?? p.tool_name ?? p.tool ?? null,
26811
+ command: input.CommandLine ?? input.command ?? input.cmd ?? null,
26812
+ path: input.TargetFile ?? input.AbsolutePath ?? input.file_path ?? input.path ?? null,
26813
+ content: input.CodeContent ?? input.ReplacementContent ?? input.content ?? input.new_string ?? null
26814
+ };
26815
+ }
26816
+ case "cursor": {
26817
+ const edits = Array.isArray(p.edits) ? p.edits : [];
26818
+ return {
26819
+ tool: p.hook_event_name ?? null,
26820
+ command: p.command ?? null,
26821
+ path: p.file_path ?? null,
26822
+ content: edits.map((e) => e?.new_string ?? e?.newText ?? "").join("\n") || p.content || null
26823
+ };
26824
+ }
26825
+ case "opencode": {
26826
+ const args = p.args ?? p.output?.args ?? {};
26827
+ return {
26828
+ tool: p.tool ?? p.input?.tool ?? null,
26829
+ command: args.command ?? null,
26830
+ path: args.filePath ?? args.path ?? null,
26831
+ content: args.content ?? args.newString ?? null
26832
+ };
26833
+ }
26834
+ }
26835
+ }
26836
+ function namesFrom(harness, payload) {
26837
+ return introducedBy(readPayload(harness, payload));
26838
+ }
26839
+ function explain(findings) {
26840
+ if (!findings.length)
26841
+ return "";
26842
+ const lines = findings.map((f) => {
26843
+ const head = f.instead ? `${f.name} was ruled out \u2014 ${f.instead} is the decision` : `${f.name} was ruled out`;
26844
+ const when = f.since ? ` (${f.since})` : "";
26845
+ const where = f.source_session ? ` \xB7 session ${f.source_session}` : "";
26846
+ return `- ${head}${when}${where}`;
26847
+ });
26848
+ return [
26849
+ "chat-recall: this reaches for something already decided against.",
26850
+ ...lines,
26851
+ "Proceed only if this is a deliberate reversal. If it is, record it:",
26852
+ "recall_decision_record with the area, so the old decision is closed rather than contradicted."
26853
+ ].join("\n");
26854
+ }
26855
+ function writeVerdict(harness, verdict, message) {
26856
+ if (verdict === "allow" || !message)
26857
+ return { exitCode: 0 };
26858
+ switch (harness) {
26859
+ case "claude":
26860
+ case "codex": {
26861
+ if (verdict === "deny") {
26862
+ return {
26863
+ json: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: message } },
26864
+ exitCode: 0
26865
+ };
26866
+ }
26867
+ return {
26868
+ json: { hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: message } },
26869
+ exitCode: 0
26870
+ };
26871
+ }
26872
+ case "agy": {
26873
+ const decision = verdict === "deny" ? "deny" : verdict === "ask" ? "ask" : "allow";
26874
+ return { json: { decision, reason: message }, exitCode: 0 };
26875
+ }
26876
+ case "cursor": {
26877
+ if (verdict === "deny") {
26878
+ return { json: { permission: "deny", user_message: message, userMessage: message }, exitCode: 0 };
26879
+ }
26880
+ return {
26881
+ json: { permission: "allow", agent_message: message, agentMessage: message },
26882
+ exitCode: 0
26883
+ };
26884
+ }
26885
+ case "opencode": {
26886
+ return { json: { block: verdict === "deny", message }, exitCode: 0 };
26887
+ }
26888
+ }
26889
+ }
26890
+ var COVERAGE;
26891
+ var init_guard_adapters = __esm({
26892
+ "../engine/dist/core/guard-adapters.js"() {
26893
+ "use strict";
26894
+ init_decision_guard();
26895
+ COVERAGE = {
26896
+ claude: { commands: true, edits: "before" },
26897
+ codex: { commands: true, edits: "before" },
26898
+ agy: { commands: true, edits: "before" },
26899
+ cursor: {
26900
+ commands: true,
26901
+ edits: "after",
26902
+ note: "Cursor exposes beforeShellExecution but only afterFileEdit, so an import added in an edit is reported after the write, not prevented."
26903
+ },
26904
+ opencode: {
26905
+ commands: true,
26906
+ edits: "before",
26907
+ note: "OpenCode plugin hooks do not fire for subagent tool calls (anomalyco/opencode#5894), so an agent that delegates bypasses this."
26908
+ }
26909
+ };
26910
+ }
26911
+ });
26912
+
26913
+ // ../engine/dist/core/task-hook.js
26914
+ var task_hook_exports = {};
26915
+ __export(task_hook_exports, {
26916
+ CLAIMABLE_STATUSES: () => CLAIMABLE_STATUSES,
26917
+ CLOSEABLE_STATUSES: () => CLOSEABLE_STATUSES,
26918
+ COMMIT_LOG_FORMAT: () => COMMIT_LOG_FORMAT,
26919
+ MAX_EVIDENCE_COMMITS: () => MAX_EVIDENCE_COMMITS,
26920
+ MAX_EVIDENCE_FILES: () => MAX_EVIDENCE_FILES,
26921
+ TASK_ID_PATTERN: () => TASK_ID_PATTERN,
26922
+ extractTaskIds: () => extractTaskIds,
26923
+ mayClaim: () => mayClaim,
26924
+ mayClose: () => mayClose,
26925
+ parseChangedFiles: () => parseChangedFiles,
26926
+ parseCommitLog: () => parseCommitLog,
26927
+ planCloses: () => planCloses
26928
+ });
26929
+ function extractTaskIds(text) {
26930
+ if (!text)
26931
+ return [];
26932
+ const seen = /* @__PURE__ */ new Set();
26933
+ const out = [];
26934
+ for (const match of text.toLowerCase().matchAll(TASK_ID_PATTERN)) {
26935
+ if (seen.has(match[0]))
26936
+ continue;
26937
+ seen.add(match[0]);
26938
+ out.push(match[0]);
26939
+ }
26940
+ return out;
26941
+ }
26942
+ function parseCommitLog(raw) {
26943
+ if (!raw)
26944
+ return [];
26945
+ const out = [];
26946
+ for (const record3 of raw.split("")) {
26947
+ const sep4 = record3.indexOf("");
26948
+ if (sep4 < 0)
26949
+ continue;
26950
+ const sha = record3.slice(0, sep4).trim();
26951
+ if (!/^[0-9a-f]{7,40}$/i.test(sha))
26952
+ continue;
26953
+ out.push({ sha, message: record3.slice(sep4 + 1) });
26954
+ }
26955
+ return out;
26956
+ }
26957
+ function planCloses(commits) {
26958
+ const byId = /* @__PURE__ */ new Map();
26959
+ for (const commit of commits) {
26960
+ for (const id of extractTaskIds(commit.message)) {
26961
+ const shas = byId.get(id) ?? [];
26962
+ if (!shas.includes(commit.sha) && shas.length < MAX_EVIDENCE_COMMITS)
26963
+ shas.push(commit.sha);
26964
+ byId.set(id, shas);
26965
+ }
26966
+ }
26967
+ return [...byId].map(([id, shas]) => ({ id, commits: shas }));
26968
+ }
26969
+ function parseChangedFiles(raw) {
26970
+ if (!raw)
26971
+ return [];
26972
+ const seen = /* @__PURE__ */ new Set();
26973
+ const out = [];
26974
+ for (const line of raw.split("\n")) {
26975
+ const file = line.trim();
26976
+ if (!file)
26977
+ continue;
26978
+ if (file.startsWith("/") || file.startsWith("~"))
26979
+ continue;
26980
+ if (/(^|\/)\.\.(\/|$)/.test(file))
26981
+ continue;
26982
+ if (seen.has(file))
26983
+ continue;
26984
+ seen.add(file);
26985
+ out.push(file);
26986
+ if (out.length >= MAX_EVIDENCE_FILES)
26987
+ break;
26988
+ }
26989
+ return out;
26990
+ }
26991
+ function mayClaim(status, linkedSessionId, sessionId) {
26992
+ if (!sessionId)
26993
+ return false;
26994
+ if (linkedSessionId === sessionId && status === "in_progress")
26995
+ return false;
26996
+ return CLAIMABLE_STATUSES.has(String(status ?? ""));
26997
+ }
26998
+ function mayClose(status) {
26999
+ return CLOSEABLE_STATUSES.has(String(status ?? ""));
27000
+ }
27001
+ var TASK_ID_PATTERN, CLAIMABLE_STATUSES, CLOSEABLE_STATUSES, MAX_EVIDENCE_COMMITS, MAX_EVIDENCE_FILES, COMMIT_LOG_FORMAT;
27002
+ var init_task_hook = __esm({
27003
+ "../engine/dist/core/task-hook.js"() {
27004
+ "use strict";
27005
+ TASK_ID_PATTERN = /\bt_[0-9a-f]{18}\b/g;
27006
+ CLAIMABLE_STATUSES = /* @__PURE__ */ new Set(["todo"]);
27007
+ CLOSEABLE_STATUSES = /* @__PURE__ */ new Set(["todo", "in_progress"]);
27008
+ MAX_EVIDENCE_COMMITS = 20;
27009
+ MAX_EVIDENCE_FILES = 50;
27010
+ COMMIT_LOG_FORMAT = "%H%x1f%B%x1e";
27011
+ }
27012
+ });
27013
+
26239
27014
  // src/service-installer.ts
26240
27015
  var service_installer_exports = {};
26241
27016
  __export(service_installer_exports, {
@@ -26250,15 +27025,15 @@ __export(service_installer_exports, {
26250
27025
  uninstallService: () => uninstallService
26251
27026
  });
26252
27027
  import { execSync as execSync5 } from "child_process";
26253
- import { existsSync as existsSync53, mkdirSync as mkdirSync19, readFileSync as readFileSync47, rmSync as rmSync9, writeFileSync as writeFileSync20 } from "fs";
26254
- import { homedir as homedir20 } from "os";
26255
- import { join as join57 } from "path";
27028
+ import { existsSync as existsSync55, mkdirSync as mkdirSync22, readFileSync as readFileSync48, rmSync as rmSync9, writeFileSync as writeFileSync22 } from "fs";
27029
+ import { homedir as homedir22 } from "os";
27030
+ import { join as join60 } from "path";
26256
27031
  import { fileURLToPath as fileURLToPath5 } from "url";
26257
27032
  function resolveDaemonPaths() {
26258
27033
  const watchJs = fileURLToPath5(new URL("./watch.js", import.meta.url));
26259
27034
  const node = process.execPath;
26260
- const dataDir = join57(homedir20(), ".chat-recall");
26261
- const logFile = join57(dataDir, "watch.log");
27035
+ const dataDir = join60(homedir22(), ".chat-recall");
27036
+ const logFile = join60(dataDir, "watch.log");
26262
27037
  return { watchJs, node, logFile, dataDir };
26263
27038
  }
26264
27039
  function tryCmd(cmd) {
@@ -26285,13 +27060,13 @@ function refreshServiceDefinition() {
26285
27060
  if (process.platform !== "linux") return false;
26286
27061
  try {
26287
27062
  const { watchJs, node, logFile } = resolveDaemonPaths();
26288
- const unitPath = join57(homedir20(), ".config", "systemd", "user", "chat-recall-watch.service");
26289
- if (!existsSync53(unitPath)) return false;
26290
- const current = readFileSync47(unitPath, "utf8");
27063
+ const unitPath = join60(homedir22(), ".config", "systemd", "user", "chat-recall-watch.service");
27064
+ if (!existsSync55(unitPath)) return false;
27065
+ const current = readFileSync48(unitPath, "utf8");
26291
27066
  if (!current.includes(watchJs)) return false;
26292
27067
  const wanted = renderSystemdUnit(watchJs, node, logFile);
26293
- if (readFileSync47(unitPath, "utf8") === wanted) return false;
26294
- writeFileSync20(unitPath, wanted);
27068
+ if (readFileSync48(unitPath, "utf8") === wanted) return false;
27069
+ writeFileSync22(unitPath, wanted);
26295
27070
  execSync5("systemctl --user daemon-reload", { stdio: "ignore" });
26296
27071
  return true;
26297
27072
  } catch {
@@ -26300,7 +27075,7 @@ function refreshServiceDefinition() {
26300
27075
  }
26301
27076
  function installService() {
26302
27077
  const { watchJs, node, logFile, dataDir } = resolveDaemonPaths();
26303
- mkdirSync19(dataDir, { recursive: true });
27078
+ mkdirSync22(dataDir, { recursive: true });
26304
27079
  if (process.platform === "linux") {
26305
27080
  return installSystemd(watchJs, node, logFile);
26306
27081
  }
@@ -26316,11 +27091,11 @@ function installService() {
26316
27091
  );
26317
27092
  }
26318
27093
  function installSystemd(watchJs, node, logFile) {
26319
- const unitDir = join57(homedir20(), ".config", "systemd", "user");
26320
- const unitPath = join57(unitDir, "chat-recall-watch.service");
27094
+ const unitDir = join60(homedir22(), ".config", "systemd", "user");
27095
+ const unitPath = join60(unitDir, "chat-recall-watch.service");
26321
27096
  const unit = renderSystemdUnit(watchJs, node, logFile);
26322
- mkdirSync19(unitDir, { recursive: true });
26323
- writeFileSync20(unitPath, unit);
27097
+ mkdirSync22(unitDir, { recursive: true });
27098
+ writeFileSync22(unitPath, unit);
26324
27099
  try {
26325
27100
  execSync5("systemctl --user daemon-reload && systemctl --user enable chat-recall-watch.service && systemctl --user restart chat-recall-watch.service", { stdio: "inherit" });
26326
27101
  try {
@@ -26391,11 +27166,11 @@ function renderSystemdUnit(watchJs, node, logFile) {
26391
27166
  }
26392
27167
  function installLaunchd(watchJs, node, logFile) {
26393
27168
  const label = "com.chat-recall.watch";
26394
- const plistDir = join57(homedir20(), "Library", "LaunchAgents");
26395
- const plistPath = join57(plistDir, `${label}.plist`);
27169
+ const plistDir = join60(homedir22(), "Library", "LaunchAgents");
27170
+ const plistPath = join60(plistDir, `${label}.plist`);
26396
27171
  const plist = renderLaunchdPlist(watchJs, node, logFile, label);
26397
- mkdirSync19(plistDir, { recursive: true });
26398
- writeFileSync20(plistPath, plist);
27172
+ mkdirSync22(plistDir, { recursive: true });
27173
+ writeFileSync22(plistPath, plist);
26399
27174
  try {
26400
27175
  try {
26401
27176
  execSync5(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
@@ -26448,8 +27223,8 @@ function uninstallService() {
26448
27223
  removed = true;
26449
27224
  } catch {
26450
27225
  }
26451
- const unitPath = join57(homedir20(), ".config", "systemd", "user", "chat-recall-watch.service");
26452
- if (existsSync53(unitPath)) {
27226
+ const unitPath = join60(homedir22(), ".config", "systemd", "user", "chat-recall-watch.service");
27227
+ if (existsSync55(unitPath)) {
26453
27228
  rmSync9(unitPath, { force: true });
26454
27229
  removed = true;
26455
27230
  }
@@ -26460,13 +27235,13 @@ function uninstallService() {
26460
27235
  return removed;
26461
27236
  }
26462
27237
  if (process.platform === "darwin") {
26463
- const plistPath = join57(homedir20(), "Library", "LaunchAgents", "com.chat-recall.watch.plist");
27238
+ const plistPath = join60(homedir22(), "Library", "LaunchAgents", "com.chat-recall.watch.plist");
26464
27239
  try {
26465
27240
  execSync5(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore" });
26466
27241
  } catch {
26467
27242
  }
26468
27243
  let removed = false;
26469
- if (existsSync53(plistPath)) {
27244
+ if (existsSync55(plistPath)) {
26470
27245
  rmSync9(plistPath, { force: true });
26471
27246
  removed = true;
26472
27247
  }
@@ -26879,9 +27654,9 @@ init_project_resolver();
26879
27654
  import { config } from "dotenv";
26880
27655
  import { Command } from "commander";
26881
27656
  import chalk2 from "chalk";
26882
- import { existsSync as existsSync54, readFileSync as readFileSync48, writeFileSync as writeFileSync21, realpathSync as realpathSync5 } from "fs";
26883
- import { homedir as homedir21 } from "os";
26884
- import { join as join58, resolve as resolve2, dirname as dirname27, basename as basename27 } from "path";
27657
+ import { existsSync as existsSync56, readFileSync as readFileSync49, writeFileSync as writeFileSync23, realpathSync as realpathSync5 } from "fs";
27658
+ import { homedir as homedir23 } from "os";
27659
+ import { join as join61, resolve as resolve2, dirname as dirname29, basename as basename27 } from "path";
26885
27660
 
26886
27661
  // ../engine/dist/core/score-tier.js
26887
27662
  var STRONG_RATIO = 0.85;
@@ -26986,6 +27761,47 @@ function renderEnvNeeds(needs, installedCount) {
26986
27761
  return lines;
26987
27762
  }
26988
27763
 
27764
+ // src/star-nudge.ts
27765
+ init_paths();
27766
+ import { existsSync as existsSync42, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "node:fs";
27767
+ import { dirname as dirname20, join as join45 } from "node:path";
27768
+ var REPO = "https://github.com/munhq/chat-recall";
27769
+ function markerPath() {
27770
+ return join45(getDataDir(), ".star-asked");
27771
+ }
27772
+ function suppressed() {
27773
+ const env = process.env;
27774
+ if (env.CHAT_RECALL_NO_STAR_PROMPT) return true;
27775
+ return Boolean(env.CI || env.GITHUB_ACTIONS || env.GITLAB_CI || env.BUILDKITE || env.TEAMCITY_VERSION);
27776
+ }
27777
+ function shouldAskForStar(resultCount) {
27778
+ if (resultCount <= 0) return false;
27779
+ if (suppressed()) return false;
27780
+ if (!process.stderr.isTTY) return false;
27781
+ try {
27782
+ return !existsSync42(markerPath());
27783
+ } catch {
27784
+ return false;
27785
+ }
27786
+ }
27787
+ function markStarAsked() {
27788
+ try {
27789
+ const p = markerPath();
27790
+ mkdirSync11(dirname20(p), { recursive: true });
27791
+ writeFileSync11(p, `${(/* @__PURE__ */ new Date()).toISOString()}
27792
+ `);
27793
+ } catch {
27794
+ }
27795
+ }
27796
+ function askForStar(resultCount, dim) {
27797
+ if (!shouldAskForStar(resultCount)) return;
27798
+ markStarAsked();
27799
+ process.stderr.write(`${dim(`If this saved you time, a star helps other people find it: ${REPO}`)}
27800
+ `);
27801
+ process.stderr.write(`${dim("You will not be asked again. CHAT_RECALL_NO_STAR_PROMPT=1 disables it everywhere.")}
27802
+ `);
27803
+ }
27804
+
26989
27805
  // src/cli.ts
26990
27806
  function matchLabel(tier) {
26991
27807
  switch (tier) {
@@ -27004,8 +27820,8 @@ function claudeHookConfigFiles() {
27004
27820
  const out = [];
27005
27821
  const seen = /* @__PURE__ */ new Set();
27006
27822
  for (const home of claudeHomeDirs()) {
27007
- if (!existsSync54(join58(home, "projects"))) continue;
27008
- const file = join58(home, "hooks.json");
27823
+ if (!existsSync56(join61(home, "projects"))) continue;
27824
+ const file = join61(home, "hooks.json");
27009
27825
  let key2;
27010
27826
  try {
27011
27827
  key2 = realpathSync5(file);
@@ -27100,13 +27916,13 @@ async function serverDelete(path, body) {
27100
27916
  }
27101
27917
  var pkgEngines = (() => {
27102
27918
  try {
27103
- return JSON.parse(readFileSync48(new URL("../package.json", import.meta.url), "utf-8")).engines?.node ?? "";
27919
+ return JSON.parse(readFileSync49(new URL("../package.json", import.meta.url), "utf-8")).engines?.node ?? "";
27104
27920
  } catch {
27105
27921
  return "";
27106
27922
  }
27107
27923
  })();
27108
27924
  var pkgVersion = JSON.parse(
27109
- readFileSync48(new URL("../package.json", import.meta.url), "utf-8")
27925
+ readFileSync49(new URL("../package.json", import.meta.url), "utf-8")
27110
27926
  ).version;
27111
27927
  var DEFAULT_SERVER = (process.env.CHAT_RECALL_DEFAULT_SERVER || "https://chatrecall.dev").replace(/\/+$/, "");
27112
27928
  var DEFAULT_ALLOW = [
@@ -27194,7 +28010,7 @@ process.env.CHAT_RECALL_CLI_VERSION = pkgVersion;
27194
28010
  enforceNodeVersion();
27195
28011
  var program = new Command();
27196
28012
  program.name("chat-recall").description("Semantic search for Claude Code sessions - recall and resume past conversations").version(pkgVersion);
27197
- program.command("init").description("Set up chat-recall: connect to your server, configure MCP, ship your local sessions").option("--server <url>", "chat-recall server URL to connect this machine to").option("--token <token>", "Self-host device token (skips the interactive OIDC login)").option("--skip-mcp", "Skip MCP server configuration").option("--skip-sync", "Skip the first session sync").option("--with-codeindex", "Force-download the codeindex binary during init. Default behavior is to detect an already-installed codeindex on PATH and register it as an MCP server.").option("--skip-codeindex", "Skip the codeindex companion entirely (no detection, no registration).").option("--skip-service", "Skip installing the per-user background sync service (Linux/macOS/Windows). By default init installs it so new conversations ship automatically.").option("--yes", "Do not pause before the first upload. The scope summary still prints.", false).option("--prompt-json", "Print the device prompt as one JSON line on stdout, then continue", false).action(async (options) => {
28013
+ program.command("init").description("Set up chat-recall: connect to your server, configure MCP, ship your local sessions").option("--server <url>", "chat-recall server URL to connect this machine to").option("--token <token>", "Self-host device token (skips the interactive OIDC login)").option("--skip-mcp", "Skip MCP server configuration").option("--skip-sync", "Skip the first session sync").option("--with-codeindex", "Force-download the codeindex binary during init. Default behavior is to detect an already-installed codeindex on PATH and register it as an MCP server.").option("--skip-codeindex", "Skip the codeindex companion entirely (no detection, no registration).").option("--skip-service", "Skip installing the per-user background sync service (Linux/macOS/Windows). By default init installs it so new conversations ship automatically.").option("--skip-hooks", "Skip registering the task-board hooks").option("--yes", "Do not pause before the first upload. The scope summary still prints.", false).option("--prompt-json", "Print the device prompt as one JSON line on stdout, then continue", false).action(async (options) => {
27198
28014
  try {
27199
28015
  console.log(chalk2.bold("chat-recall init") + chalk2.dim(` v${pkgVersion}`));
27200
28016
  const { createInitReporter: createInitReporter2 } = await Promise.resolve().then(() => (init_init_report(), init_report_exports));
@@ -27220,6 +28036,18 @@ program.command("init").description("Set up chat-recall: connect to your server,
27220
28036
  }
27221
28037
  const foundTools = clis.filter((c) => c.available).map((c) => c.name.replace(/ (CLI|Code)$/, ""));
27222
28038
  if (!foundTools.length) rep.warn(chalk2.yellow("No AI tools detected \u2014 chat-recall indexes their transcripts, so there is nothing to ship yet."));
28039
+ else {
28040
+ try {
28041
+ const { listAvailableBackends: listAvailableBackends2 } = await Promise.resolve().then(() => (init_tool_backend(), tool_backend_exports));
28042
+ const transcripts = listAvailableBackends2().reduce((n, b) => n + (b.listSessions?.({ limit: 1 })?.length ?? 0), 0);
28043
+ if (transcripts === 0) {
28044
+ rep.warn(chalk2.yellow(
28045
+ `Found ${foundTools.join(", ")} but no conversations yet \u2014 chat-recall indexes the transcripts your AI tools write as you work. Use one for a while, then run ${chalk2.bold("chat-recall sync")}.`
28046
+ ));
28047
+ }
28048
+ } catch {
28049
+ }
28050
+ }
27223
28051
  const serverBad = (text) => rep.warn(`${chalk2.yellow("Not connected")} \u2014 ${text}`);
27224
28052
  let target = firstTarget();
27225
28053
  let deadTarget = false;
@@ -27267,10 +28095,10 @@ program.command("init").description("Set up chat-recall: connect to your server,
27267
28095
  } else if (target) {
27268
28096
  }
27269
28097
  if (!options.skipMcp) {
27270
- const projectRoot = join58(import.meta.dirname, "..");
28098
+ const projectRoot = join61(import.meta.dirname, "..");
27271
28099
  let mcpBinOnPath = false;
27272
28100
  mcpBinOnPath = isOnPath("chat-recall-mcp");
27273
- const launch = mcpBinOnPath ? { command: "chat-recall-mcp" } : { command: "node", args: [join58(projectRoot, "dist", "mcp-relay.js")] };
28101
+ const launch = mcpBinOnPath ? { command: "chat-recall-mcp" } : { command: "node", args: [join61(projectRoot, "dist", "mcp-relay.js")] };
27274
28102
  const MCP_ENV = { NODE_OPTIONS: "--max-old-space-size=1024" };
27275
28103
  const { registerMcpEverywhere: registerMcpEverywhere2 } = await Promise.resolve().then(() => (init_mcp_clients(), mcp_clients_exports));
27276
28104
  const MCP_CLIENT_FOR_BIN = {
@@ -27310,7 +28138,7 @@ program.command("init").description("Set up chat-recall: connect to your server,
27310
28138
  const detected = checkCodeindexStatus2();
27311
28139
  if (detected.installed && !forceInstall) {
27312
28140
  if (!options.skipMcp) {
27313
- const mcpJsonPath = join58(homedir21(), ".mcp.json");
28141
+ const mcpJsonPath = join61(homedir23(), ".mcp.json");
27314
28142
  const reg = registerCodeindexMcp2(mcpJsonPath, detected.path);
27315
28143
  }
27316
28144
  } else if (forceInstall) {
@@ -27319,7 +28147,7 @@ program.command("init").description("Set up chat-recall: connect to your server,
27319
28147
  if (result.installed) {
27320
28148
  const sizeMb = result.size ? `${(result.size / 1024 / 1024).toFixed(1)} MB` : "?";
27321
28149
  if (!options.skipMcp) {
27322
- const mcpJsonPath = join58(homedir21(), ".mcp.json");
28150
+ const mcpJsonPath = join61(homedir23(), ".mcp.json");
27323
28151
  const reg = registerCodeindexMcp2(mcpJsonPath, CODEINDEX_BIN_PATH2);
27324
28152
  }
27325
28153
  } else if (!result.prebuiltAvailable) {
@@ -27438,6 +28266,15 @@ program.command("init").description("Set up chat-recall: connect to your server,
27438
28266
  } else {
27439
28267
  }
27440
28268
  void options.skipService;
28269
+ if (!options.skipHooks) {
28270
+ try {
28271
+ const { registerBoardHooks: registerBoardHooks2, resolveCliEntry: resolveCliEntry2 } = await Promise.resolve().then(() => (init_board_hooks(), board_hooks_exports));
28272
+ const { fileURLToPath: toPath } = await import("url");
28273
+ const distDir = toPath(new URL(".", import.meta.url));
28274
+ registerBoardHooks2(claudeHookConfigFiles(), process.execPath, resolveCliEntry2(distDir));
28275
+ } catch {
28276
+ }
28277
+ }
27441
28278
  rep.done(chalk2.green.bold("\u2713 Done"));
27442
28279
  } catch (err) {
27443
28280
  console.error(chalk2.red("Error:"), err);
@@ -27574,6 +28411,7 @@ program.command("search <query>").description("Search your server for relevant s
27574
28411
  }
27575
28412
  console.log();
27576
28413
  }
28414
+ askForStar(results.length, chalk2.dim);
27577
28415
  } catch (err) {
27578
28416
  console.error(chalk2.red("Error:"), err instanceof Error ? err.message : err);
27579
28417
  process.exit(1);
@@ -27767,8 +28605,8 @@ memory.command("wake-up").description("Generate wake-up context (high-importance
27767
28605
  try {
27768
28606
  const identityFile = getIdentityFilePath();
27769
28607
  let identity = "AI coding assistant";
27770
- if (existsSync54(identityFile)) {
27771
- identity = readFileSync48(identityFile, "utf-8").trim();
28608
+ if (existsSync56(identityFile)) {
28609
+ identity = readFileSync49(identityFile, "utf-8").trim();
27772
28610
  }
27773
28611
  const lines = [
27774
28612
  chalk2.bold("# Wake-Up Context"),
@@ -27868,7 +28706,7 @@ program.command("feedback").argument("<message...>", "what you want to say \u201
27868
28706
  }
27869
28707
  });
27870
28708
  program.command("doctor").description("Quick health check across login, server, credentials, hooks, MCP server, and codeindex").option("--purge-local", "Remove the legacy local index (cache.db + vector/KG files) left by pre-thin-collector versions \u2014 storage is server-side now. Requires login; keeps credentials, sync ledger, identity, and the audit log.").action(async (opts) => {
27871
- const { existsSync: existsSync55, readFileSync: readFileSync49, statSync: statSync45 } = await import("fs");
28709
+ const { existsSync: existsSync57, readFileSync: readFileSync50, statSync: statSync45 } = await import("fs");
27872
28710
  const { execSync: execSync6 } = await import("child_process");
27873
28711
  const rows = [];
27874
28712
  const note = (ok, label, detail) => rows.push({ ok, label, detail });
@@ -27931,8 +28769,8 @@ program.command("doctor").description("Quick health check across login, server,
27931
28769
  }
27932
28770
  }
27933
28771
  }
27934
- const credFile = join58(getDataDir(), "credentials.json");
27935
- if (!existsSync55(credFile)) {
28772
+ const credFile = join61(getDataDir(), "credentials.json");
28773
+ if (!existsSync57(credFile)) {
27936
28774
  note(false, "Credentials file", `missing \u2014 run \`chat-recall login <server-url>\``);
27937
28775
  } else {
27938
28776
  try {
@@ -27945,23 +28783,23 @@ program.command("doctor").description("Quick health check across login, server,
27945
28783
  }
27946
28784
  }
27947
28785
  for (const hooksJson of claudeHookConfigFiles()) {
27948
- const profile = basename27(dirname27(hooksJson));
28786
+ const profile = basename27(dirname29(hooksJson));
27949
28787
  const label = `Claude hooks (${profile})`;
27950
- if (!existsSync55(hooksJson)) {
28788
+ if (!existsSync57(hooksJson)) {
27951
28789
  note(false, label, `no ${hooksJson} \u2014 run \`chat-recall install-hooks\``);
27952
28790
  continue;
27953
28791
  }
27954
28792
  try {
27955
28793
  const readHooks = (file) => {
27956
- if (!existsSync55(file)) return {};
28794
+ if (!existsSync57(file)) return {};
27957
28795
  try {
27958
- return JSON.parse(readFileSync49(file, "utf-8")).hooks || {};
28796
+ return JSON.parse(readFileSync50(file, "utf-8")).hooks || {};
27959
28797
  } catch {
27960
28798
  return {};
27961
28799
  }
27962
28800
  };
27963
28801
  const cfg = readHooks(hooksJson);
27964
- const settings2 = readHooks(join58(dirname27(hooksJson), "settings.json"));
28802
+ const settings2 = readHooks(join61(dirname29(hooksJson), "settings.json"));
27965
28803
  const all = ["SessionStart", "UserPromptSubmit", "Stop", "PreCompact", "SessionEnd"];
27966
28804
  const hits = [];
27967
28805
  for (const ev of all) {
@@ -27976,16 +28814,54 @@ program.command("doctor").description("Quick health check across login, server,
27976
28814
  label,
27977
28815
  hits.length === 0 ? "none \u2014 run `chat-recall install-hooks`" : `${hits.join(", ")}${hits.length < all.length ? ` (missing ${all.filter((e) => !hits.includes(e)).join(", ")} \u2014 run install-hooks)` : ""}`
27978
28816
  );
28817
+ const boardEvents = ["UserPromptSubmit", "SessionEnd"];
28818
+ const boardHits = boardEvents.filter((ev) => [
28819
+ ...Array.isArray(cfg[ev]) ? cfg[ev] : [],
28820
+ ...Array.isArray(settings2[ev]) ? settings2[ev] : []
28821
+ ].some((h) => {
28822
+ const cmd = h.hooks?.[0]?.command || "";
28823
+ return cmd.includes("task-hook") && (cmd.includes("--claim") || cmd.includes("--close"));
28824
+ }));
28825
+ note(
28826
+ boardHits.length === boardEvents.length,
28827
+ `Task board hooks (${profile})`,
28828
+ boardHits.length === 0 ? "not registered \u2014 run `chat-recall install-hooks`" : boardHits.length === boardEvents.length ? "claim on a prompt, close on a commit" : `only ${boardHits.join(", ")} \u2014 run \`chat-recall install-hooks\``
28829
+ );
27979
28830
  } catch {
27980
28831
  note(false, label, `${hooksJson} unparseable`);
27981
28832
  }
27982
28833
  }
28834
+ try {
28835
+ const reg = await Promise.resolve().then(() => (init_guard_registration(), guard_registration_exports));
28836
+ for (const target2 of reg.guardTargets()) {
28837
+ if (!existsSync57(target2.homeDir)) continue;
28838
+ const label = `Decision guard \u2014 ${target2.label}`;
28839
+ if (!existsSync57(target2.configPath)) {
28840
+ note(false, label, `not registered \u2014 run \`chat-recall install-hooks\``);
28841
+ continue;
28842
+ }
28843
+ if (target2.tool === "opencode") {
28844
+ note(true, label, target2.configPath);
28845
+ continue;
28846
+ }
28847
+ let registered = false;
28848
+ try {
28849
+ const cfg = JSON.parse(readFileSync50(target2.configPath, "utf-8"));
28850
+ registered = JSON.stringify(cfg).includes("chat_recall_guard");
28851
+ } catch {
28852
+ }
28853
+ const detail = registered && target2.tool === "codex" ? `${target2.configPath} \u2014 approve it in Codex with /hooks` : registered ? target2.configPath : `${target2.configPath} has no chat-recall entry \u2014 run \`chat-recall install-hooks\``;
28854
+ note(registered, label, detail);
28855
+ }
28856
+ } catch (err) {
28857
+ note(false, "Decision guard", `check failed: ${err}`);
28858
+ }
27983
28859
  try {
27984
28860
  const { skillTargets: skillTargets2, bundledSkillNames: bundledSkillNames2 } = await Promise.resolve().then(() => (init_install_skills(), install_skills_exports));
27985
28861
  const want = bundledSkillNames2().length;
27986
28862
  for (const t of skillTargets2()) {
27987
28863
  if (!t.available) continue;
27988
- const have = want === 0 ? 0 : bundledSkillNames2().filter((n) => existsSync55(join58(t.dir, n, ".chat-recall-managed"))).length;
28864
+ const have = want === 0 ? 0 : bundledSkillNames2().filter((n) => existsSync57(join61(t.dir, n, ".chat-recall-managed"))).length;
27989
28865
  note(
27990
28866
  have === want,
27991
28867
  `Skills \u2014 ${t.label}`,
@@ -28060,14 +28936,14 @@ program.command("doctor").description("Quick health check across login, server,
28060
28936
  note(watchRunning, "Watch daemon", watchRunning ? "running" : "not running (run `chat-recall watch --install-service` to keep collection running)");
28061
28937
  const dataDir = getDataDir();
28062
28938
  const legacyPaths = [
28063
- join58(dataDir, "cache.db"),
28064
- join58(dataDir, "cache.db-wal"),
28065
- join58(dataDir, "cache.db-shm"),
28066
- join58(dataDir, "index", "lancedb"),
28067
- join58(dataDir, "index", "knowledge_graph.db"),
28068
- join58(dataDir, "index", "metadata.db")
28939
+ join61(dataDir, "cache.db"),
28940
+ join61(dataDir, "cache.db-wal"),
28941
+ join61(dataDir, "cache.db-shm"),
28942
+ join61(dataDir, "index", "lancedb"),
28943
+ join61(dataDir, "index", "knowledge_graph.db"),
28944
+ join61(dataDir, "index", "metadata.db")
28069
28945
  ];
28070
- const legacyPresent = legacyPaths.filter((p) => existsSync55(p));
28946
+ const legacyPresent = legacyPaths.filter((p) => existsSync57(p));
28071
28947
  if (opts.purgeLocal) {
28072
28948
  if (!target) {
28073
28949
  note(false, "Purge legacy index", "refused \u2014 log in and `chat-recall sync` first, so your history is on the server before removing local copies");
@@ -28138,7 +29014,7 @@ companions.command("install").description("Install codeindex (downloads from mun
28138
29014
  if (result.installed) {
28139
29015
  const sizeMb = result.size ? `${(result.size / 1024 / 1024).toFixed(1)} MB` : "?";
28140
29016
  console.log(chalk2.green(`\u2713 codeindex installed (${sizeMb}) \u2192 ${result.path}`));
28141
- const mcpJsonPath = join58(homedir21(), ".mcp.json");
29017
+ const mcpJsonPath = join61(homedir23(), ".mcp.json");
28142
29018
  const reg = registerCodeindexMcp2(mcpJsonPath, CODEINDEX_BIN_PATH2);
28143
29019
  if (reg.added) console.log(chalk2.dim(` Registered as MCP server in ${mcpJsonPath}`));
28144
29020
  else console.log(chalk2.dim(` Already registered as MCP server`));
@@ -28160,7 +29036,7 @@ companions.command("uninstall").description("Remove the codeindex binary chat-re
28160
29036
  const r = uninstallCodeindex2();
28161
29037
  if (r.removed) console.log(chalk2.green(`\u2713 Removed ${r.path}`));
28162
29038
  else console.log(chalk2.dim(` Nothing to remove at ${r.path}`));
28163
- const mcpJsonPath = join58(homedir21(), ".mcp.json");
29039
+ const mcpJsonPath = join61(homedir23(), ".mcp.json");
28164
29040
  const u = unregisterCodeindexMcp2(mcpJsonPath);
28165
29041
  if (u.removed) console.log(chalk2.dim(` Unregistered MCP server from ${mcpJsonPath}`));
28166
29042
  });
@@ -28711,10 +29587,10 @@ tasks.command("pull [dir]").description("Write TEAM_TASKS.md for your tasks in t
28711
29587
  try {
28712
29588
  const { resolveProjectId: resolveProjectId2 } = await Promise.resolve().then(() => (init_project_resolver(), project_resolver_exports));
28713
29589
  const { renderTeamTasksMd: renderTeamTasksMd2, parseTeamTasksFile: parseTeamTasksFile2 } = await Promise.resolve().then(() => (init_project_tasks(), project_tasks_exports));
28714
- const { writeFileSync: writeFileSync22, readFileSync: readFileSync49, existsSync: existsSync55 } = await import("node:fs");
28715
- const { join: join59 } = await import("node:path");
28716
- const file = join59(cwd, "TEAM_TASKS.md");
28717
- if (!opts.force && existsSync55(file) && parseTeamTasksFile2(readFileSync49(file, "utf8")).length > 0) {
29590
+ const { writeFileSync: writeFileSync24, readFileSync: readFileSync50, existsSync: existsSync57 } = await import("node:fs");
29591
+ const { join: join62 } = await import("node:path");
29592
+ const file = join62(cwd, "TEAM_TASKS.md");
29593
+ if (!opts.force && existsSync57(file) && parseTeamTasksFile2(readFileSync50(file, "utf8")).length > 0) {
28718
29594
  console.error(chalk2.red("TEAM_TASKS.md has unpushed edits."), "Run", chalk2.bold("chat-recall tasks push"), "first, or", chalk2.bold("tasks pull --force"), "to discard.");
28719
29595
  process.exit(1);
28720
29596
  }
@@ -28722,7 +29598,7 @@ tasks.command("pull [dir]").description("Write TEAM_TASKS.md for your tasks in t
28722
29598
  const pid = resolved && resolved.source !== "ignored" ? resolved.id : "";
28723
29599
  const { tasks: tasks2 } = await serverGet("/api/tasks?assignee=@me");
28724
29600
  const mine = tasks2.filter((t) => !pid || !t.projectId || t.projectId === pid);
28725
- writeFileSync22(file, renderTeamTasksMd2(mine));
29601
+ writeFileSync24(file, renderTeamTasksMd2(mine));
28726
29602
  console.log(chalk2.green("\u2713 wrote"), "TEAM_TASKS.md", chalk2.gray(`(${mine.length} task(s))`), "\u2014 edit, then", chalk2.bold("chat-recall tasks push"));
28727
29603
  } catch (e) {
28728
29604
  console.error(chalk2.red(e instanceof Error ? e.message : String(e)));
@@ -28733,14 +29609,14 @@ tasks.command("push [dir]").description("Push TEAM_TASKS.md status edits back to
28733
29609
  const cwd = dir || process.cwd();
28734
29610
  try {
28735
29611
  const { parseTeamTasksFile: parseTeamTasksFile2 } = await Promise.resolve().then(() => (init_project_tasks(), project_tasks_exports));
28736
- const { readFileSync: readFileSync49, existsSync: existsSync55 } = await import("node:fs");
28737
- const { join: join59 } = await import("node:path");
28738
- const file = join59(cwd, "TEAM_TASKS.md");
28739
- if (!existsSync55(file)) {
29612
+ const { readFileSync: readFileSync50, existsSync: existsSync57 } = await import("node:fs");
29613
+ const { join: join62 } = await import("node:path");
29614
+ const file = join62(cwd, "TEAM_TASKS.md");
29615
+ if (!existsSync57(file)) {
28740
29616
  console.error(chalk2.red("no TEAM_TASKS.md here \u2014"), "run", chalk2.bold("chat-recall tasks pull"), "first");
28741
29617
  process.exit(1);
28742
29618
  }
28743
- const edits = parseTeamTasksFile2(readFileSync49(file, "utf8"));
29619
+ const edits = parseTeamTasksFile2(readFileSync50(file, "utf8"));
28744
29620
  let n = 0;
28745
29621
  for (const e of edits) {
28746
29622
  await serverPatch(`/api/tasks/${encodeURIComponent(e.id)}`, { status: e.intent });
@@ -28846,8 +29722,8 @@ team.command("publish <type> <name> <file>").description("Publish a local file a
28846
29722
  console.error(chalk2.red(`Invalid tool: ${opts.tool}. Must be one of: ${validTools.join(", ")}`));
28847
29723
  process.exit(1);
28848
29724
  }
28849
- const { readFileSync: readFileSync49 } = await import("fs");
28850
- const body = readFileSync49(file);
29725
+ const { readFileSync: readFileSync50 } = await import("fs");
29726
+ const body = readFileSync50(file);
28851
29727
  const { teamPublish: teamPublish2 } = await Promise.resolve().then(() => (init_team_client(), team_client_exports));
28852
29728
  const a = await teamPublish2({
28853
29729
  type,
@@ -29033,18 +29909,201 @@ vault.command("forget").description("Remove the stored passphrase from this mach
29033
29909
  const { keyringDelete: keyringDelete2 } = await Promise.resolve().then(() => (init_keyring(), keyring_exports));
29034
29910
  console.log(keyringDelete2() ? chalk2.green("\u2713 Passphrase removed from the OS keyring.") : chalk2.dim("Nothing stored in the OS keyring."));
29035
29911
  }));
29036
- program.command("install-hooks").description("Install Claude Code hooks (Stop + PreCompact + UserPromptSubmit + SessionEnd + SessionStart) into every Claude profile").option("--uninstall", "Remove hooks instead of installing them").option("--no-resume-hint", "Don't install the UserPromptSubmit resume-hint hook").option("--no-escalate", "Don't install the SessionEnd learnings-escalation hook").option("--no-wakeup", "Don't install the SessionStart wake-up hook").action(async (opts) => {
29037
- const { mkdirSync: mkdirSync20, copyFileSync: copyFileSync4, chmodSync: chmodSync5, existsSync: existsSync55, readFileSync: readFileSync49, writeFileSync: writeFileSync22, statSync: statSync45 } = await import("fs");
29912
+ program.command("guard").description("Check a pending tool call against the decision register (used by the pre-execution hooks)").option("--harness <name>", "claude | codex | agy | cursor | opencode", "claude").option("--project <id>", "Resolve decisions for this project").option("--enforce", "Return a blocking verdict instead of a warning").action(async (opts) => {
29913
+ try {
29914
+ const { namesFrom: namesFrom2, writeVerdict: writeVerdict2, explain: explain2 } = await Promise.resolve().then(() => (init_guard_adapters(), guard_adapters_exports));
29915
+ const harness = opts.harness ?? "claude";
29916
+ const raw = await new Promise((resolve3) => {
29917
+ let buf = "";
29918
+ const t = setTimeout(() => resolve3(buf), 2e3);
29919
+ process.stdin.setEncoding("utf8");
29920
+ process.stdin.on("data", (c) => {
29921
+ buf += c;
29922
+ });
29923
+ process.stdin.on("end", () => {
29924
+ clearTimeout(t);
29925
+ resolve3(buf);
29926
+ });
29927
+ process.stdin.on("error", () => {
29928
+ clearTimeout(t);
29929
+ resolve3(buf);
29930
+ });
29931
+ });
29932
+ if (!raw.trim()) return;
29933
+ let payload;
29934
+ try {
29935
+ payload = JSON.parse(raw);
29936
+ } catch {
29937
+ return;
29938
+ }
29939
+ const { names, via } = namesFrom2(harness, payload);
29940
+ if (!names.length) return;
29941
+ const target = firstTarget();
29942
+ if (!target) return;
29943
+ const { fetchWithTimeout: fetchWithTimeout2 } = await Promise.resolve().then(() => (init_http(), http_exports));
29944
+ const headers = { "content-type": "application/json" };
29945
+ if (target.token) headers.authorization = `Bearer ${target.token}`;
29946
+ const res = await fetchWithTimeout2(
29947
+ `${target.base}/api/decisions/check`,
29948
+ { method: "POST", headers, body: JSON.stringify({ names, via, project: opts.project }) },
29949
+ 4e3
29950
+ );
29951
+ if (!res.ok) return;
29952
+ const out = await res.json();
29953
+ if (!out.findings?.length) return;
29954
+ const message = explain2(out.findings);
29955
+ const verdict = opts.enforce ? "deny" : out.verdict === "ask" ? "ask" : "warn";
29956
+ const written = writeVerdict2(harness, verdict, message);
29957
+ if (written.json) process.stdout.write(JSON.stringify(written.json));
29958
+ } catch {
29959
+ }
29960
+ });
29961
+ program.command("task-hook").description("Move a board card when a prompt or a commit names its id (used by the task hooks)").option("--claim", "UserPromptSubmit: claim every open card the prompt names").option("--close", "SessionEnd: close every open card a recent commit names").option("--since <hours>", "How far back --close reads commits", "24").action(async (opts) => {
29962
+ if (process.env.CHAT_RECALL_TASK_HOOK === "0") return;
29963
+ const deadline = setTimeout(() => process.exit(0), opts.close ? 2e4 : 1e4);
29964
+ deadline.unref();
29965
+ try {
29966
+ const {
29967
+ extractTaskIds: extractTaskIds2,
29968
+ parseCommitLog: parseCommitLog2,
29969
+ planCloses: planCloses2,
29970
+ parseChangedFiles: parseChangedFiles2,
29971
+ mayClaim: mayClaim2,
29972
+ mayClose: mayClose2,
29973
+ COMMIT_LOG_FORMAT: COMMIT_LOG_FORMAT2
29974
+ } = await Promise.resolve().then(() => (init_task_hook(), task_hook_exports));
29975
+ const raw = await new Promise((resolve3) => {
29976
+ let buf = "";
29977
+ const t = setTimeout(() => resolve3(buf), 2e3);
29978
+ process.stdin.setEncoding("utf8");
29979
+ process.stdin.on("data", (c) => {
29980
+ buf += c;
29981
+ });
29982
+ process.stdin.on("end", () => {
29983
+ clearTimeout(t);
29984
+ resolve3(buf);
29985
+ });
29986
+ process.stdin.on("error", () => {
29987
+ clearTimeout(t);
29988
+ resolve3(buf);
29989
+ });
29990
+ });
29991
+ if (!raw.trim()) return;
29992
+ let payload;
29993
+ try {
29994
+ payload = JSON.parse(raw);
29995
+ } catch {
29996
+ return;
29997
+ }
29998
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : "";
29999
+ if (!sessionId) return;
30000
+ const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
30001
+ const target = firstTarget();
30002
+ if (!target) return;
30003
+ const { fetchWithTimeout: fetchWithTimeout2 } = await Promise.resolve().then(() => (init_http(), http_exports));
30004
+ const headers = { "content-type": "application/json" };
30005
+ if (target.token) headers.authorization = `Bearer ${target.token}`;
30006
+ const readCard = async (id) => {
30007
+ const res = await fetchWithTimeout2(
30008
+ `${target.base}/api/tasks/${encodeURIComponent(id)}`,
30009
+ { headers },
30010
+ 4e3
30011
+ );
30012
+ if (!res.ok) return null;
30013
+ const body = await res.json().catch(() => ({}));
30014
+ return body.task ?? null;
30015
+ };
30016
+ const patchCard = async (id, body) => {
30017
+ const res = await fetchWithTimeout2(
30018
+ `${target.base}/api/tasks/${encodeURIComponent(id)}`,
30019
+ { method: "PATCH", headers, body: JSON.stringify(body) },
30020
+ 6e3
30021
+ );
30022
+ return res.ok;
30023
+ };
30024
+ if (opts.claim) {
30025
+ const ids = extractTaskIds2(payload.prompt);
30026
+ if (!ids.length) return;
30027
+ const claimed = [];
30028
+ for (const id of ids) {
30029
+ const card = await readCard(id);
30030
+ if (!card) continue;
30031
+ if (!mayClaim2(card.status, card.linkedSessionId, sessionId)) continue;
30032
+ if (await patchCard(id, { status: "in_progress", linkedSessionId: sessionId })) {
30033
+ claimed.push(`${id} \u2014 ${card.title}`);
30034
+ }
30035
+ }
30036
+ if (claimed.length) {
30037
+ process.stdout.write("<!-- chat-recall-task-claim -->\n");
30038
+ process.stdout.write(
30039
+ "**Claimed on the chat-recall board**, linked to this session:\n" + claimed.map((c) => `- \u{1F7E1} ${c}`).join("\n") + "\n\nWrite the card id in the commit message and the card closes itself at session end.\n"
30040
+ );
30041
+ }
30042
+ return;
30043
+ }
30044
+ if (!opts.close) return;
30045
+ const { execFileSync: execFileSync4 } = await import("node:child_process");
30046
+ const git2 = (args) => {
30047
+ try {
30048
+ return execFileSync4("git", args, {
30049
+ cwd,
30050
+ encoding: "utf8",
30051
+ timeout: 5e3,
30052
+ stdio: ["ignore", "pipe", "ignore"]
30053
+ });
30054
+ } catch {
30055
+ return "";
30056
+ }
30057
+ };
30058
+ if (!git2(["rev-parse", "--is-inside-work-tree"]).trim().startsWith("true")) return;
30059
+ const hours = Math.max(1, Math.min(720, Number(opts.since) || 24));
30060
+ const intents = planCloses2(parseCommitLog2(git2([
30061
+ "log",
30062
+ `--since=${hours}.hours.ago`,
30063
+ "--max-count=200",
30064
+ "--no-merges",
30065
+ `--pretty=format:${COMMIT_LOG_FORMAT2}`
30066
+ ])));
30067
+ if (!intents.length) return;
30068
+ for (const intent of intents) {
30069
+ const card = await readCard(intent.id);
30070
+ if (!card) continue;
30071
+ if (!mayClose2(card.status)) continue;
30072
+ const files = parseChangedFiles2(
30073
+ intent.commits.map((sha) => git2(["show", "--name-only", "--pretty=format:", sha])).join("\n")
30074
+ );
30075
+ const shortShas = intent.commits.map((s) => s.slice(0, 8)).join(", ");
30076
+ await patchCard(intent.id, {
30077
+ status: "done",
30078
+ linkedSessionId: sessionId,
30079
+ doneEvidence: {
30080
+ commits: intent.commits,
30081
+ files,
30082
+ summary: `Closed by ${intent.commits.length === 1 ? "commit" : "commits"} ${shortShas}, which named this card.`
30083
+ }
30084
+ });
30085
+ }
30086
+ } catch {
30087
+ }
30088
+ });
30089
+ program.command("install-hooks").description("Install Claude Code hooks (Stop + PreCompact + UserPromptSubmit + SessionEnd + SessionStart) into every Claude profile").option("--uninstall", "Remove hooks instead of installing them").option("--no-resume-hint", "Don't install the UserPromptSubmit resume-hint hook").option("--no-escalate", "Don't install the SessionEnd learnings-escalation hook").option("--no-wakeup", "Don't install the SessionStart wake-up hook").option("--no-guard", "Don't install the PreToolUse decision guard").option("--no-task-board", "Don't install the hooks that claim and close board cards").action(async (opts) => {
30090
+ const { mkdirSync: mkdirSync23, copyFileSync: copyFileSync4, chmodSync: chmodSync5, existsSync: existsSync57, readFileSync: readFileSync50, writeFileSync: writeFileSync24, statSync: statSync45, unlinkSync: unlinkSync4 } = await import("fs");
29038
30091
  const { fileURLToPath: fileURLToPath6 } = await import("url");
30092
+ const { boardHookEntries: boardHookEntries2, isBoardHookEntry: isBoardHookEntry2, registerBoardHooks: registerBoardHooks2, resolveCliEntry: resolveCliEntry2 } = await Promise.resolve().then(() => (init_board_hooks(), board_hooks_exports));
29039
30093
  const here = fileURLToPath6(new URL(".", import.meta.url));
29040
30094
  const findHook = (name) => {
29041
30095
  const candidates2 = [
29042
- join58(here, "..", "hooks", name),
29043
- join58(here, "..", "..", "hooks", name)
30096
+ join61(here, "..", "hooks", name),
30097
+ join61(here, "..", "..", "hooks", name),
30098
+ // The monorepo root, for a run straight from the checkout. The copy
30099
+ // under packages/cli/hooks is written by the package build, so a
30100
+ // freshly added script is only at the root until that build runs.
30101
+ join61(here, "..", "..", "..", "hooks", name)
29044
30102
  ];
29045
- return candidates2.find((p) => existsSync55(p));
30103
+ return candidates2.find((p) => existsSync57(p));
29046
30104
  };
29047
30105
  const sourceSaveHook = findHook("chat_recall_save_hook.sh");
30106
+ const sourceGuardHook = findHook("chat_recall_guard_hook.sh");
29048
30107
  const sourceResumeHook = findHook("chat_recall_resume_hook.sh");
29049
30108
  const sourceEscalateHook = findHook("chat_recall_escalate_hook.sh");
29050
30109
  const sourceWakeupHook = findHook("chat_recall_wakeup_hook.sh");
@@ -29053,15 +30112,16 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
29053
30112
  process.exit(1);
29054
30113
  }
29055
30114
  const hooksDir = getHooksDir();
29056
- const installedSaveHook = join58(hooksDir, "chat_recall_save_hook.sh");
29057
- const installedResumeHook = join58(hooksDir, "chat_recall_resume_hook.sh");
29058
- const installedEscalateHook = join58(hooksDir, "chat_recall_escalate_hook.sh");
29059
- const installedWakeupHook = join58(hooksDir, "chat_recall_wakeup_hook.sh");
30115
+ const installedSaveHook = join61(hooksDir, "chat_recall_save_hook.sh");
30116
+ const installedGuardHook = join61(hooksDir, "chat_recall_guard_hook.sh");
30117
+ const installedResumeHook = join61(hooksDir, "chat_recall_resume_hook.sh");
30118
+ const installedEscalateHook = join61(hooksDir, "chat_recall_escalate_hook.sh");
30119
+ const installedWakeupHook = join61(hooksDir, "chat_recall_wakeup_hook.sh");
29060
30120
  const wakeupWiredInSettings = (home) => {
29061
- const settings2 = join58(home, "settings.json");
29062
- if (!existsSync55(settings2)) return false;
30121
+ const settings2 = join61(home, "settings.json");
30122
+ if (!existsSync57(settings2)) return false;
29063
30123
  try {
29064
- const cfg = JSON.parse(readFileSync49(settings2, "utf-8"));
30124
+ const cfg = JSON.parse(readFileSync50(settings2, "utf-8"));
29065
30125
  const arr = Array.isArray(cfg.hooks?.SessionStart) ? cfg.hooks.SessionStart : [];
29066
30126
  return arr.some((h) => (h?.hooks?.[0]?.command || "").includes("chat_recall_wakeup_hook"));
29067
30127
  } catch {
@@ -29070,9 +30130,9 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
29070
30130
  };
29071
30131
  const hookConfigFiles = claudeHookConfigFiles();
29072
30132
  const readHookConfig = (file) => {
29073
- if (!existsSync55(file)) return { hooks: {} };
30133
+ if (!existsSync57(file)) return { hooks: {} };
29074
30134
  try {
29075
- const c = JSON.parse(readFileSync49(file, "utf-8"));
30135
+ const c = JSON.parse(readFileSync50(file, "utf-8"));
29076
30136
  if (!c.hooks || typeof c.hooks !== "object") c.hooks = {};
29077
30137
  return c;
29078
30138
  } catch (err) {
@@ -29083,9 +30143,58 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
29083
30143
  };
29084
30144
  const matchesOurs = (h) => {
29085
30145
  const cmd = h?.hooks?.[0]?.command;
29086
- return typeof cmd === "string" && (cmd.includes("chat_recall_save_hook.sh") || cmd.includes("chat_recall_resume_hook.sh") || cmd.includes("chat_recall_escalate_hook.sh") || cmd.includes("chat_recall_wakeup_hook.sh"));
30146
+ return typeof cmd === "string" && (cmd.includes("chat_recall_save_hook.sh") || cmd.includes("chat_recall_resume_hook.sh") || cmd.includes("chat_recall_escalate_hook.sh") || cmd.includes("chat_recall_wakeup_hook.sh") || cmd.includes("chat_recall_guard_hook.sh")) || isBoardHookEntry2(h);
30147
+ };
30148
+ const HOOK_EVENTS = ["Stop", "PreCompact", "UserPromptSubmit", "SessionEnd", "SessionStart", "PreToolUse"];
30149
+ const readJson4 = (file) => {
30150
+ if (!existsSync57(file)) return {};
30151
+ try {
30152
+ return JSON.parse(readFileSync50(file, "utf-8"));
30153
+ } catch {
30154
+ return {};
30155
+ }
30156
+ };
30157
+ const addGuardToOtherTools = async (hookPath) => {
30158
+ const reg = await Promise.resolve().then(() => (init_guard_registration(), guard_registration_exports));
30159
+ const notes = [];
30160
+ for (const target of reg.guardTargets()) {
30161
+ if (!existsSync57(target.homeDir)) continue;
30162
+ const wrapper = join61(hooksDir, reg.guardWrapperName(target.tool));
30163
+ writeFileSync24(wrapper, reg.guardWrapperSource(target.tool, hookPath));
30164
+ chmodSync5(wrapper, 493);
30165
+ mkdirSync23(dirname29(target.configPath), { recursive: true });
30166
+ if (target.tool === "opencode") {
30167
+ writeFileSync24(target.configPath, reg.opencodePluginSource(wrapper));
30168
+ } else {
30169
+ const merged = reg.registerGuard(target.tool, readJson4(target.configPath), reg.guardCommand(wrapper));
30170
+ writeFileSync24(target.configPath, JSON.stringify(merged, null, 2) + "\n");
30171
+ }
30172
+ console.log(chalk2.green(`\u2713 Registered the guard for ${target.label}`));
30173
+ console.log(chalk2.dim(` ${target.configPath}`));
30174
+ if (target.note) notes.push(`${target.label}: ${target.note}`);
30175
+ }
30176
+ for (const n of notes) console.log(chalk2.yellow(` ! ${n}`));
30177
+ };
30178
+ const removeGuardFromOtherTools = async () => {
30179
+ const reg = await Promise.resolve().then(() => (init_guard_registration(), guard_registration_exports));
30180
+ let removed = 0;
30181
+ for (const target of reg.guardTargets()) {
30182
+ if (!existsSync57(target.configPath)) continue;
30183
+ if (target.tool === "opencode") {
30184
+ const body = readFileSync50(target.configPath, "utf-8");
30185
+ if (!body.includes("chat-recall install-hooks")) continue;
30186
+ unlinkSync4(target.configPath);
30187
+ removed += 1;
30188
+ } else {
30189
+ const r = reg.unregisterGuard(target.tool, readJson4(target.configPath));
30190
+ if (r.removed === 0) continue;
30191
+ writeFileSync24(target.configPath, JSON.stringify(r.config, null, 2) + "\n");
30192
+ removed += r.removed;
30193
+ }
30194
+ console.log(chalk2.green(`\u2713 Removed the guard from ${target.label} (${target.configPath})`));
30195
+ }
30196
+ return removed;
29087
30197
  };
29088
- const HOOK_EVENTS = ["Stop", "PreCompact", "UserPromptSubmit", "SessionEnd", "SessionStart"];
29089
30198
  if (opts.uninstall) {
29090
30199
  let total = 0;
29091
30200
  for (const hooksJson of hookConfigFiles) {
@@ -29098,29 +30207,39 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
29098
30207
  if (filtered.length) config2.hooks[event] = filtered;
29099
30208
  else delete config2.hooks[event];
29100
30209
  }
29101
- if (removed === 0 && !existsSync55(hooksJson)) continue;
29102
- writeFileSync22(hooksJson, JSON.stringify(config2, null, 2) + "\n");
30210
+ if (removed === 0 && !existsSync57(hooksJson)) continue;
30211
+ writeFileSync24(hooksJson, JSON.stringify(config2, null, 2) + "\n");
29103
30212
  total += removed;
29104
30213
  console.log(chalk2.green(`\u2713 Removed ${removed} chat-recall hook entr${removed === 1 ? "y" : "ies"} from ${hooksJson}`));
29105
30214
  }
30215
+ total += await removeGuardFromOtherTools();
29106
30216
  if (total === 0) console.log(chalk2.dim("No chat-recall hook entries were registered."));
29107
30217
  console.log(chalk2.dim(` (Hook scripts left in ${hooksDir} \u2014 delete manually if you want.)`));
29108
30218
  return;
29109
30219
  }
29110
30220
  if (process.platform === "win32") {
29111
- console.log(chalk2.yellow("Hooks are not installed on Windows."));
30221
+ if (opts.taskBoard !== false && !opts.uninstall) {
30222
+ const written = registerBoardHooks2(hookConfigFiles, process.execPath, resolveCliEntry2(here));
30223
+ if (written.length) {
30224
+ console.log(chalk2.green("\u2713 Registered the chat-recall task-board hooks"));
30225
+ console.log(chalk2.dim(" A prompt naming a card id claims it; a commit message naming one closes it."));
30226
+ for (const f of written) console.log(chalk2.dim(` ${f}`));
30227
+ console.log();
30228
+ }
30229
+ }
30230
+ console.log(chalk2.yellow("The five shell hooks are not installed on Windows."));
29112
30231
  console.log(chalk2.dim(" They are POSIX shell scripts, and Claude Code runs hook commands through"));
29113
30232
  console.log(chalk2.dim(" cmd.exe, which cannot execute them. Registering them would put an error on"));
29114
30233
  console.log(chalk2.dim(" every turn instead of saving anything."));
29115
30234
  console.log(chalk2.dim(""));
29116
- console.log(chalk2.dim(" Everything else works: indexing, sync, search and the MCP server are"));
29117
- console.log(chalk2.dim(" unaffected. Hooks only add automatic fact-saving and the wake-up bundle,"));
29118
- console.log(chalk2.dim(" and you can get the same context by calling recall_wake_up from the agent."));
30235
+ console.log(chalk2.dim(" Everything else works: indexing, sync, search, the MCP server and the"));
30236
+ console.log(chalk2.dim(" board hooks above. The five scripts add automatic fact-saving and the"));
30237
+ console.log(chalk2.dim(" wake-up bundle; recall_wake_up gives an agent the same context on demand."));
29119
30238
  console.log(chalk2.dim(""));
29120
30239
  console.log(chalk2.dim(" Under WSL, run this from inside the WSL shell and it installs normally."));
29121
30240
  return;
29122
30241
  }
29123
- mkdirSync20(hooksDir, { recursive: true });
30242
+ mkdirSync23(hooksDir, { recursive: true });
29124
30243
  copyFileSync4(sourceSaveHook, installedSaveHook);
29125
30244
  chmodSync5(installedSaveHook, 493);
29126
30245
  const installResume = opts.resumeHint !== false && !!sourceResumeHook;
@@ -29133,40 +30252,56 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
29133
30252
  copyFileSync4(sourceEscalateHook, installedEscalateHook);
29134
30253
  chmodSync5(installedEscalateHook, 493);
29135
30254
  }
30255
+ const installGuard = opts.guard !== false && !!sourceGuardHook;
30256
+ if (opts.guard !== false && !sourceGuardHook) {
30257
+ console.error(chalk2.yellow("! chat_recall_guard_hook.sh is not in this package \u2014 the decision guard is not installed."));
30258
+ }
30259
+ if (installGuard) {
30260
+ copyFileSync4(sourceGuardHook, installedGuardHook);
30261
+ chmodSync5(installedGuardHook, 493);
30262
+ }
29136
30263
  const installWakeup = opts.wakeup !== false && !!sourceWakeupHook;
29137
30264
  if (installWakeup) {
29138
30265
  copyFileSync4(sourceWakeupHook, installedWakeupHook);
29139
30266
  chmodSync5(installedWakeupHook, 493);
29140
30267
  }
30268
+ const installTaskBoard = opts.taskBoard !== false;
29141
30269
  const stopEntry = { matcher: "", hooks: [{ type: "command", command: installedSaveHook }] };
29142
30270
  const precompactEntry = { matcher: "", hooks: [{ type: "command", command: `"${installedSaveHook}" --precompact` }] };
29143
30271
  const resumeEntry = { matcher: "", hooks: [{ type: "command", command: installedResumeHook }] };
29144
30272
  const escalateEntry = { matcher: "", hooks: [{ type: "command", command: installedEscalateHook }] };
29145
30273
  const wakeupEntry = { matcher: "startup|clear", hooks: [{ type: "command", command: installedWakeupHook }] };
30274
+ const guardEntry = {
30275
+ matcher: "Bash|Edit|Write|MultiEdit|NotebookEdit",
30276
+ hooks: [{ type: "command", command: installedGuardHook }]
30277
+ };
30278
+ const boardEntries = boardHookEntries2(process.execPath, resolveCliEntry2(here));
29146
30279
  const events = [
29147
30280
  ["Stop", stopEntry],
29148
30281
  ["PreCompact", precompactEntry]
29149
30282
  ];
29150
30283
  if (installResume) events.push(["UserPromptSubmit", resumeEntry]);
29151
30284
  if (installEscalate) events.push(["SessionEnd", escalateEntry]);
30285
+ if (installGuard) events.push(["PreToolUse", guardEntry]);
30286
+ if (installTaskBoard) for (const pair of boardEntries) events.push(pair);
29152
30287
  const wakeupSkipped = [];
29153
30288
  for (const hooksJson of hookConfigFiles) {
29154
30289
  const config2 = readHookConfig(hooksJson);
29155
- const home = dirname27(hooksJson);
30290
+ const home = dirname29(hooksJson);
29156
30291
  const alreadyWired = wakeupWiredInSettings(home);
29157
- if (alreadyWired) wakeupSkipped.push(join58(home, "settings.json"));
30292
+ if (alreadyWired) wakeupSkipped.push(join61(home, "settings.json"));
29158
30293
  const perProfile = installWakeup && !alreadyWired ? [...events, ["SessionStart", wakeupEntry]] : events;
29159
30294
  for (const event of HOOK_EVENTS) {
29160
30295
  const arr = Array.isArray(config2.hooks[event]) ? config2.hooks[event] : [];
29161
30296
  const without = arr.filter((h) => !matchesOurs(h));
29162
- const wanted = perProfile.find(([e]) => e === event);
29163
- if (wanted) without.push(wanted[1]);
30297
+ for (const [, entry] of perProfile.filter(([e]) => e === event)) without.push(entry);
29164
30298
  if (without.length) config2.hooks[event] = without;
29165
30299
  else delete config2.hooks[event];
29166
30300
  }
29167
- mkdirSync20(dirname27(hooksJson), { recursive: true });
29168
- writeFileSync22(hooksJson, JSON.stringify(config2, null, 2) + "\n");
30301
+ mkdirSync23(dirname29(hooksJson), { recursive: true });
30302
+ writeFileSync24(hooksJson, JSON.stringify(config2, null, 2) + "\n");
29169
30303
  }
30304
+ if (installGuard) await addGuardToOtherTools(installedGuardHook);
29170
30305
  const saveSz = statSync45(installedSaveHook).size;
29171
30306
  console.log(chalk2.green(`\u2713 Installed chat-recall save hook (${saveSz} bytes)`));
29172
30307
  if (installResume) {
@@ -29177,6 +30312,14 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
29177
30312
  const escalateSz = statSync45(installedEscalateHook).size;
29178
30313
  console.log(chalk2.green(`\u2713 Installed chat-recall learnings-escalation hook (${escalateSz} bytes)`));
29179
30314
  }
30315
+ if (installGuard) {
30316
+ const guardSz = statSync45(installedGuardHook).size;
30317
+ console.log(chalk2.green(`\u2713 Installed chat-recall decision guard (${guardSz} bytes)`));
30318
+ }
30319
+ if (installTaskBoard) {
30320
+ console.log(chalk2.green("\u2713 Registered the chat-recall task-board hooks"));
30321
+ console.log(chalk2.dim(" A prompt naming a card id claims it; a commit message naming one closes it."));
30322
+ }
29180
30323
  if (installWakeup) {
29181
30324
  const wakeupSz = statSync45(installedWakeupHook).size;
29182
30325
  console.log(chalk2.green(`\u2713 Installed chat-recall wake-up hook (${wakeupSz} bytes)`));
@@ -29188,7 +30331,12 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
29188
30331
  for (const [i, f] of hookConfigFiles.entries()) {
29189
30332
  console.log(chalk2.dim(` ${i === 0 ? "config: " : " "} ${f}`));
29190
30333
  }
29191
- console.log(chalk2.dim(` events: Stop, PreCompact${installResume ? ", UserPromptSubmit" : ""}${installEscalate ? ", SessionEnd" : ""}${installWakeup ? ", SessionStart" : ""}`));
30334
+ const registeredEvents = ["Stop", "PreCompact"];
30335
+ if (installResume || installTaskBoard) registeredEvents.push("UserPromptSubmit");
30336
+ if (installEscalate || installTaskBoard) registeredEvents.push("SessionEnd");
30337
+ if (installWakeup) registeredEvents.push("SessionStart");
30338
+ if (installGuard) registeredEvents.push("PreToolUse");
30339
+ console.log(chalk2.dim(` events: ${registeredEvents.join(", ")}`));
29192
30340
  console.log();
29193
30341
  console.log(chalk2.dim("Run `chat-recall install-hooks --uninstall` to remove later."));
29194
30342
  if (!installResume) {
@@ -29210,7 +30358,7 @@ program.command("dossier <project>").description("Generate a project dossier (ov
29210
30358
  );
29211
30359
  const md = dossier.markdown;
29212
30360
  if (options.out) {
29213
- writeFileSync21(options.out, md);
30361
+ writeFileSync23(options.out, md);
29214
30362
  console.log(chalk2.green(`Wrote dossier to ${options.out} (${md.length} chars)`));
29215
30363
  } else {
29216
30364
  console.log(md);
@@ -29468,8 +30616,8 @@ include.command("list", { isDefault: true }).description("Show paths opted back
29468
30616
  include.command("project <path>").description("Re-permit a path under a personal folder (substring match; e.g. ~/Documents/code)").action(async (path) => {
29469
30617
  const { loadSettings: loadSettings2, saveSettings: saveSettings2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
29470
30618
  const { resolve: resolve3 } = await import("node:path");
29471
- const { homedir: homedir22 } = await import("node:os");
29472
- const abs = resolve3(path.replace(/^~(?=\/|$)/, homedir22()));
30619
+ const { homedir: homedir24 } = await import("node:os");
30620
+ const abs = resolve3(path.replace(/^~(?=\/|$)/, homedir24()));
29473
30621
  const s = loadSettings2();
29474
30622
  s.sync.includeProjects = s.sync.includeProjects || [];
29475
30623
  if (s.sync.includeProjects.includes(abs)) {
@@ -29483,8 +30631,8 @@ include.command("project <path>").description("Re-permit a path under a personal
29483
30631
  include.command("remove <path>").description("Undo an opt-in (personal folder goes back to skipped)").action(async (path) => {
29484
30632
  const { loadSettings: loadSettings2, saveSettings: saveSettings2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
29485
30633
  const { resolve: resolve3 } = await import("node:path");
29486
- const { homedir: homedir22 } = await import("node:os");
29487
- const abs = resolve3(path.replace(/^~(?=\/|$)/, homedir22()));
30634
+ const { homedir: homedir24 } = await import("node:os");
30635
+ const abs = resolve3(path.replace(/^~(?=\/|$)/, homedir24()));
29488
30636
  const s = loadSettings2();
29489
30637
  const before = (s.sync.includeProjects || []).length;
29490
30638
  s.sync.includeProjects = (s.sync.includeProjects || []).filter((p) => p !== path && p !== abs);
@@ -29623,8 +30771,8 @@ program.command("projects").description("List distinct project ids discovered on
29623
30771
  exclude.command("project <path>").description("Exclude a project path from syncing (substring match on the project path; e.g. ~/.claude-pr-bot)").action(async (path) => {
29624
30772
  const { loadSettings: loadSettings2, saveSettings: saveSettings2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
29625
30773
  const { resolve: resolve3 } = await import("node:path");
29626
- const { homedir: homedir22 } = await import("node:os");
29627
- const abs = resolve3(path.replace(/^~(?=\/|$)/, homedir22()));
30774
+ const { homedir: homedir24 } = await import("node:os");
30775
+ const abs = resolve3(path.replace(/^~(?=\/|$)/, homedir24()));
29628
30776
  const s = loadSettings2();
29629
30777
  if (s.sync.excludeProjects.includes(abs)) {
29630
30778
  console.log(chalk2.dim(`Already excluded: ${abs}`));
@@ -29653,8 +30801,8 @@ exclude.command("tool <tool>").description("Exclude an AI tool entirely (claude
29653
30801
  exclude.command("remove <value>").description("Remove an exclusion (tool name or project path)").action(async (value) => {
29654
30802
  const { loadSettings: loadSettings2, saveSettings: saveSettings2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
29655
30803
  const { resolve: resolve3 } = await import("node:path");
29656
- const { homedir: homedir22 } = await import("node:os");
29657
- const abs = resolve3(value.replace(/^~(?=\/|$)/, homedir22()));
30804
+ const { homedir: homedir24 } = await import("node:os");
30805
+ const abs = resolve3(value.replace(/^~(?=\/|$)/, homedir24()));
29658
30806
  const s = loadSettings2();
29659
30807
  const beforeTools = s.sync.excludeTools.length;
29660
30808
  const beforeProjects = s.sync.excludeProjects.length;