agendex-cli 0.18.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +515 -25
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1059,8 +1059,8 @@ var init_cleanup = __esm(() => {
1059
1059
 
1060
1060
  // src/cli.ts
1061
1061
  import { spawn as spawn4 } from "node:child_process";
1062
- import { existsSync as existsSync11, statSync as statSync2, writeSync } from "node:fs";
1063
- import { resolve as resolve8 } from "node:path";
1062
+ import { existsSync as existsSync12, statSync as statSync2, writeSync } from "node:fs";
1063
+ import { resolve as resolve9 } from "node:path";
1064
1064
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1065
1065
 
1066
1066
  // ../shared/src/adapters/catalog.ts
@@ -1924,6 +1924,19 @@ function parseDate2(value) {
1924
1924
  return;
1925
1925
  return date;
1926
1926
  }
1927
+ function liveSessionIdentity(input) {
1928
+ if (input.sourcePlanPath) {
1929
+ return hashPath(`plannotator-live:${resolve2(input.sourcePlanPath)}`);
1930
+ }
1931
+ if (input.session.reviewId) {
1932
+ return hashPath(`plannotator-live-review:${input.session.reviewId}`);
1933
+ }
1934
+ const project = input.session.project ?? input.planResponse.versionInfo?.project ?? input.planResponse.projectRoot;
1935
+ if (project && input.session.label) {
1936
+ return hashPath(`plannotator-live:${project}:${input.session.label}:${input.mode}`);
1937
+ }
1938
+ return hashPath(input.sessionPath);
1939
+ }
1927
1940
  function metadataRecord(metadata) {
1928
1941
  return {
1929
1942
  source: "plannotator",
@@ -2085,11 +2098,18 @@ async function parseLiveSession(filePath) {
2085
2098
  sessionPath: filePath,
2086
2099
  sourcePlanPath,
2087
2100
  startedAt: session.startedAt,
2088
- writebackCapable: true
2101
+ writebackCapable: true,
2102
+ liveness: "live"
2089
2103
  };
2090
2104
  return [
2091
2105
  {
2092
- id: hashPath(filePath),
2106
+ id: liveSessionIdentity({
2107
+ sessionPath: filePath,
2108
+ session,
2109
+ planResponse,
2110
+ mode: responseMode,
2111
+ sourcePlanPath
2112
+ }),
2093
2113
  agent: origin ?? "plannotator",
2094
2114
  title: extractTitle5(planResponse.plan, session.label ?? filePath),
2095
2115
  content: planResponse.plan,
@@ -2181,6 +2201,12 @@ var plannotatorAdapter = {
2181
2201
  return false;
2182
2202
  if (!isSafePlannotatorUrl(metadata.url))
2183
2203
  return false;
2204
+ if (payload.action === "approve") {
2205
+ return await postJson(apiUrl(metadata.url, "/api/approve"), {
2206
+ ...payload.feedback.trim() && { feedback: payload.feedback.trim() },
2207
+ planSave: { enabled: true }
2208
+ });
2209
+ }
2184
2210
  const feedback = formatWritebackFeedback(plan, payload);
2185
2211
  if (metadata.mode === "review" || metadata.mode === "annotate") {
2186
2212
  return await postJson(apiUrl(metadata.url, "/api/feedback"), {
@@ -2910,6 +2936,8 @@ async function loadOrInitConfig(options = {}) {
2910
2936
  // ../shared/src/daemon-status.ts
2911
2937
  var CLI_DAEMON_HEARTBEAT_INTERVAL_MS = 30000;
2912
2938
  var CLI_DAEMON_STALE_AFTER_MS = CLI_DAEMON_HEARTBEAT_INTERVAL_MS * 5;
2939
+ // ../shared/src/services/annotation-store.ts
2940
+ var mutationQueue = Promise.resolve();
2913
2941
  // ../shared/src/services/plan-service.ts
2914
2942
  import { existsSync as existsSync5, readdirSync as readdirSync3, realpathSync, statSync } from "node:fs";
2915
2943
  import { lstat, mkdir, readdir as readdir2, readFile as readFile7, stat as stat7, writeFile as writeFile3 } from "node:fs/promises";
@@ -3578,7 +3606,13 @@ async function requestChanges(id, payload) {
3578
3606
  return false;
3579
3607
  const ok = await adapter.requestChanges(plan, payload);
3580
3608
  if (ok) {
3581
- const plannotator = typeof plan.metadata.plannotator === "object" && plan.metadata.plannotator !== null ? { ...plan.metadata.plannotator, lastWritebackStatus: "sent", lastWritebackAt: Date.now() } : undefined;
3609
+ const writebackAt = Date.now();
3610
+ const plannotator = typeof plan.metadata.plannotator === "object" && plan.metadata.plannotator !== null ? {
3611
+ ...plan.metadata.plannotator,
3612
+ ...payload.action === "approve" ? { status: "approved" } : {},
3613
+ lastWritebackStatus: "sent",
3614
+ lastWritebackAt: writebackAt
3615
+ } : undefined;
3582
3616
  if (plannotator)
3583
3617
  plan.metadata = { ...plan.metadata, plannotator };
3584
3618
  plan.updatedAt = new Date;
@@ -4632,7 +4666,33 @@ var RESTART_WINDOW_MS = 60000;
4632
4666
  var RESTART_DELAY_MS = 5000;
4633
4667
  var PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS = 15000;
4634
4668
  var PLANNOTATOR_WRITEBACK_EXPIRED_ERROR = "Write-back expired before delivery.";
4635
- var PLANNOTATOR_WRITEBACK_FAILED_ERROR = "No live Plannotator session accepted the request-changes payload.";
4669
+ var PLANNOTATOR_WRITEBACK_FAILED_ERROR = "No live Plannotator session accepted the write-back payload.";
4670
+ var PLANNOTATOR_LIVENESS_SWEEP_INTERVAL_MS = 20000;
4671
+ function isRecord5(value) {
4672
+ return typeof value === "object" && value !== null;
4673
+ }
4674
+ function isLivePlannotatorPayload(payload) {
4675
+ const plannotator = isRecord5(payload.metadata) ? payload.metadata.plannotator : undefined;
4676
+ if (!isRecord5(plannotator))
4677
+ return false;
4678
+ return plannotator.kind === "live-session" && plannotator.writebackCapable === true;
4679
+ }
4680
+ function buildEndedPlannotatorPayload(payload) {
4681
+ const metadata = isRecord5(payload.metadata) ? payload.metadata : {};
4682
+ const plannotator = isRecord5(metadata.plannotator) ? metadata.plannotator : {};
4683
+ return {
4684
+ ...payload,
4685
+ metadata: {
4686
+ ...metadata,
4687
+ plannotator: {
4688
+ ...plannotator,
4689
+ writebackCapable: false,
4690
+ liveness: "ended",
4691
+ endedAt: Date.now()
4692
+ }
4693
+ }
4694
+ };
4695
+ }
4636
4696
  async function runWorker() {
4637
4697
  const config = await loadOrInitConfig();
4638
4698
  const hostname2 = osHostname2();
@@ -4642,6 +4702,7 @@ async function runWorker() {
4642
4702
  const syncCache = loadSyncCache();
4643
4703
  const syncQueue = [];
4644
4704
  const pendingWritebackReports = loadPendingWritebackReports();
4705
+ const liveSessions = new Map;
4645
4706
  let syncing = false;
4646
4707
  let cachedIpAddress;
4647
4708
  async function getSyncIpAddress() {
@@ -4717,6 +4778,31 @@ async function runWorker() {
4717
4778
  if (syncQueue.length > 0)
4718
4779
  processSyncQueue();
4719
4780
  }
4781
+ async function reconcileLivePlannotatorSessions(plans2) {
4782
+ const ipAddress = await getSyncIpAddress();
4783
+ const livePayloads = new Map;
4784
+ for (const plan of plans2) {
4785
+ const payload = planToSyncPayload(plan, config.deviceId, hostname2, ipAddress);
4786
+ if (isLivePlannotatorPayload(payload))
4787
+ livePayloads.set(payload.localPlanId, payload);
4788
+ }
4789
+ let queued = false;
4790
+ for (const [planId, lastPayload] of liveSessions) {
4791
+ if (livePayloads.has(planId))
4792
+ continue;
4793
+ const endedPayload = buildEndedPlannotatorPayload(lastPayload);
4794
+ syncQueue.push(endedPayload);
4795
+ liveSessions.delete(planId);
4796
+ queued = true;
4797
+ console.log(`[agendex] Plannotator session ended: ${endedPayload.title}`);
4798
+ }
4799
+ for (const [planId, payload] of livePayloads) {
4800
+ liveSessions.set(planId, payload);
4801
+ }
4802
+ if (queued)
4803
+ processSyncQueue();
4804
+ return queued;
4805
+ }
4720
4806
  function persistPendingWritebackReports() {
4721
4807
  if (!savePendingWritebackReports(pendingWritebackReports)) {
4722
4808
  console.error("[agendex] failed to persist Plannotator write-back delivery cache");
@@ -4759,6 +4845,7 @@ async function runWorker() {
4759
4845
  return;
4760
4846
  }
4761
4847
  const ok = await requestChanges(job.localPlanId, {
4848
+ action: job.action,
4762
4849
  feedback: job.feedback,
4763
4850
  revisedContent: job.revisedContent,
4764
4851
  annotations: job.annotations,
@@ -4769,7 +4856,11 @@ async function runWorker() {
4769
4856
  if (ok) {
4770
4857
  const updatedPlan = getById(job.localPlanId);
4771
4858
  if (updatedPlan) {
4772
- syncQueue.push(planToSyncPayload(updatedPlan, config.deviceId, hostname2, await getSyncIpAddress()));
4859
+ const updatedPayload = planToSyncPayload(updatedPlan, config.deviceId, hostname2, await getSyncIpAddress());
4860
+ syncQueue.push(updatedPayload);
4861
+ if (isLivePlannotatorPayload(updatedPayload)) {
4862
+ liveSessions.set(updatedPayload.localPlanId, updatedPayload);
4863
+ }
4773
4864
  }
4774
4865
  pendingWritebackReports.set(job._id, "sent");
4775
4866
  persistPendingWritebackReports();
@@ -4834,6 +4925,7 @@ async function runWorker() {
4834
4925
  const lowValueSuffix = lowValuePlanCount > 0 ? `, ${lowValuePlanCount} low-value hidden/pruned` : "";
4835
4926
  console.log(`[agendex] syncing ${initialQueuedSyncable} plans${lowValueSuffix} (${initialQueuedLowValue} low-value queued, ${initialSkipped} unchanged)...`);
4836
4927
  await processSyncQueue();
4928
+ await reconcileLivePlannotatorSessions(getAll());
4837
4929
  setInterval(() => {
4838
4930
  (async () => {
4839
4931
  await sendHeartbeat(await getSyncIpAddress());
@@ -4842,6 +4934,14 @@ async function runWorker() {
4842
4934
  if (shouldEnablePlannotatorSync(config)) {
4843
4935
  setInterval(() => void pollPlannotatorWritebacks(), PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS);
4844
4936
  pollPlannotatorWritebacks();
4937
+ setInterval(() => {
4938
+ (async () => {
4939
+ await scan();
4940
+ await reconcileLivePlannotatorSessions(getAll());
4941
+ })().catch((err) => {
4942
+ console.error("[agendex] Plannotator liveness sweep failed:", err);
4943
+ });
4944
+ }, PLANNOTATOR_LIVENESS_SWEEP_INTERVAL_MS);
4845
4945
  }
4846
4946
  startWatching((changedPlans) => {
4847
4947
  (async () => {
@@ -4850,6 +4950,7 @@ async function runWorker() {
4850
4950
  syncQueue.push(planToSyncPayload(plan, config.deviceId, hostname2, ipAddress));
4851
4951
  }
4852
4952
  processSyncQueue();
4953
+ await reconcileLivePlannotatorSessions(getAll());
4853
4954
  })().catch((err) => {
4854
4955
  console.error("[agendex] failed to queue changed plans:", err);
4855
4956
  });
@@ -4883,8 +4984,12 @@ async function startSupervisor() {
4883
4984
  const scriptPath = resolve6(process.argv[1] ?? fileURLToPath(new URL("./cli.ts", import.meta.url)));
4884
4985
  const restartTimes = [];
4885
4986
  while (!stopping) {
4886
- workerProc = spawn2(process.execPath, [scriptPath, "start", "--worker"], {
4887
- stdio: ["ignore", "inherit", "inherit"]
4987
+ const workerArgs = [scriptPath, "start", "--worker"];
4988
+ if (isDevMode())
4989
+ workerArgs.push("--dev");
4990
+ workerProc = spawn2(process.execPath, workerArgs, {
4991
+ stdio: ["ignore", "inherit", "inherit"],
4992
+ env: { ...process.env, ...isDevMode() ? { AGENDEX_DEV: "1" } : {} }
4888
4993
  });
4889
4994
  const exitCode = await new Promise((resolve7) => {
4890
4995
  workerProc?.once("exit", (code) => resolve7(code));
@@ -4912,6 +5017,379 @@ async function startSupervisor() {
4912
5017
  removePid();
4913
5018
  }
4914
5019
 
5020
+ // src/hooks.ts
5021
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
5022
+ import { mkdir as mkdir2, rm, writeFile as writeFile4 } from "node:fs/promises";
5023
+ import { homedir as homedir10 } from "node:os";
5024
+ import { dirname as dirname4, join as join14, resolve as resolve7 } from "node:path";
5025
+ var SUPPORTED_AGENTS = ["claude-code", "codex", "pi"];
5026
+ var MANAGED_MARKER = "agendex-plan-review";
5027
+ var HOOK_TIMEOUT_SECONDS = 345600;
5028
+ var CLAUDE_PREVIEW_FLAG = "--preview";
5029
+ function shellQuote(value) {
5030
+ return `'${value.replace(/'/g, `'\\''`)}'`;
5031
+ }
5032
+ function commandFor(cliEntry, agent) {
5033
+ return `${shellQuote(process.execPath)} ${shellQuote(cliEntry)} review-plan --hook --agent ${agent}`;
5034
+ }
5035
+ function scopeRoot(scope) {
5036
+ return scope === "repo" ? resolve7(process.env.PWD || process.cwd()) : homedir10();
5037
+ }
5038
+ function hooksJsonPath(agent, scope) {
5039
+ const root = scopeRoot(scope);
5040
+ if (agent === "claude-code")
5041
+ return join14(root, ".claude", "hooks.json");
5042
+ if (agent === "codex")
5043
+ return join14(root, ".codex", "hooks.json");
5044
+ return join14(root, scope === "repo" ? ".pi/extensions/agendex/index.ts" : ".pi/agent/extensions/agendex/index.ts");
5045
+ }
5046
+ function codexConfigPath(scope) {
5047
+ return join14(scopeRoot(scope), ".codex", "config.toml");
5048
+ }
5049
+ function readJsonFile(path) {
5050
+ if (!existsSync10(path))
5051
+ return {};
5052
+ try {
5053
+ return JSON.parse(readFileSync7(path, "utf-8"));
5054
+ } catch (err) {
5055
+ throw new Error(`Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
5056
+ }
5057
+ }
5058
+ function isRecord6(value) {
5059
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5060
+ }
5061
+ function backupPathFor(path) {
5062
+ return `${path}.bak-${new Date().toISOString().replace(/[:.]/g, "-")}`;
5063
+ }
5064
+ async function writeWithBackup(path, content, dryRun) {
5065
+ if (dryRun)
5066
+ return;
5067
+ await mkdir2(dirname4(path), { recursive: true });
5068
+ if (existsSync10(path)) {
5069
+ await writeFile4(backupPathFor(path), readFileSync7(path, "utf-8"), "utf-8");
5070
+ }
5071
+ await writeFile4(path, content, "utf-8");
5072
+ }
5073
+ async function removeWithBackup(path, dryRun) {
5074
+ if (dryRun || !existsSync10(path))
5075
+ return;
5076
+ await writeFile4(backupPathFor(path), readFileSync7(path, "utf-8"), "utf-8");
5077
+ await rm(path);
5078
+ }
5079
+ function mergeCommandHook({
5080
+ config,
5081
+ event,
5082
+ matcher,
5083
+ command
5084
+ }) {
5085
+ const hooks = isRecord6(config.hooks) ? { ...config.hooks } : {};
5086
+ const existingEvent = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
5087
+ const entry = {
5088
+ ...matcher ? { matcher } : {},
5089
+ id: MANAGED_MARKER,
5090
+ hooks: [
5091
+ {
5092
+ type: "command",
5093
+ command,
5094
+ timeout: HOOK_TIMEOUT_SECONDS
5095
+ }
5096
+ ]
5097
+ };
5098
+ const index = existingEvent.findIndex((item) => {
5099
+ if (!isRecord6(item))
5100
+ return false;
5101
+ if (item.id === MANAGED_MARKER)
5102
+ return true;
5103
+ return JSON.stringify(item).includes("agendex") && JSON.stringify(item).includes("review-plan");
5104
+ });
5105
+ if (index >= 0)
5106
+ existingEvent[index] = entry;
5107
+ else
5108
+ existingEvent.push(entry);
5109
+ hooks[event] = existingEvent;
5110
+ return { ...config, hooks };
5111
+ }
5112
+ function hasManagedHook(path) {
5113
+ if (!existsSync10(path))
5114
+ return false;
5115
+ const raw = readFileSync7(path, "utf-8");
5116
+ return raw.includes(MANAGED_MARKER) || raw.includes("agendex") && raw.includes("review-plan");
5117
+ }
5118
+ function isManagedHookEntry(item) {
5119
+ if (!isRecord6(item))
5120
+ return false;
5121
+ if (item.id === MANAGED_MARKER)
5122
+ return true;
5123
+ const serialized = JSON.stringify(item);
5124
+ return serialized.includes("agendex") && serialized.includes("review-plan");
5125
+ }
5126
+ function removeManagedHooks(config) {
5127
+ if (!isRecord6(config.hooks))
5128
+ return config;
5129
+ const hooks = {};
5130
+ for (const [event, value] of Object.entries(config.hooks)) {
5131
+ if (!Array.isArray(value)) {
5132
+ hooks[event] = value;
5133
+ continue;
5134
+ }
5135
+ const kept = value.filter((item) => !isManagedHookEntry(item));
5136
+ if (kept.length > 0)
5137
+ hooks[event] = kept;
5138
+ }
5139
+ return Object.keys(hooks).length > 0 ? { ...config, hooks } : { ...config, hooks: {} };
5140
+ }
5141
+ function ensureCodexHooksEnabled(raw) {
5142
+ const lines = raw ? raw.split(`
5143
+ `) : [];
5144
+ const featuresIndex = lines.findIndex((line) => /^\s*\[features\]\s*$/.test(line));
5145
+ if (featuresIndex === -1) {
5146
+ return `${raw.trimEnd()}
5147
+
5148
+ [features]
5149
+ hooks = true
5150
+ `;
5151
+ }
5152
+ let insertAt = lines.length;
5153
+ for (let i = featuresIndex + 1;i < lines.length; i++) {
5154
+ if (/^\s*\[.+\]\s*$/.test(lines[i] ?? "")) {
5155
+ insertAt = i;
5156
+ break;
5157
+ }
5158
+ if (/^\s*hooks\s*=/.test(lines[i] ?? "")) {
5159
+ lines[i] = "hooks = true";
5160
+ return `${lines.join(`
5161
+ `).trimEnd()}
5162
+ `;
5163
+ }
5164
+ }
5165
+ lines.splice(insertAt, 0, "hooks = true");
5166
+ return `${lines.join(`
5167
+ `).trimEnd()}
5168
+ `;
5169
+ }
5170
+ function printClaudePreviewBlock() {
5171
+ console.error("[agendex] refusing to install claude-code hook: hook-native plan review is not implemented yet.");
5172
+ console.error("[agendex] Installing it now would cause Claude Code to deny ExitPlanMode permission requests.");
5173
+ console.error(`[agendex] Re-run with ${CLAUDE_PREVIEW_FLAG} to opt in deliberately, or install codex/pi separately.`);
5174
+ }
5175
+ function printClaudePreviewWarning(dryRun) {
5176
+ console.error("[agendex] WARNING: claude-code hook support is preview-only.");
5177
+ console.error(dryRun ? "[agendex] This dry run describes a PermissionRequest hook that would deny ExitPlanMode until hook-native plan review ships." : "[agendex] The installed PermissionRequest hook will deny ExitPlanMode until hook-native plan review ships.");
5178
+ console.error("[agendex] Remove it with: agendex hooks uninstall claude-code");
5179
+ }
5180
+ async function installClaude(scope, cliEntry, dryRun) {
5181
+ const path = hooksJsonPath("claude-code", scope);
5182
+ const config = mergeCommandHook({
5183
+ config: readJsonFile(path),
5184
+ event: "PermissionRequest",
5185
+ matcher: "ExitPlanMode",
5186
+ command: commandFor(cliEntry, "claude-code")
5187
+ });
5188
+ await writeWithBackup(path, `${JSON.stringify(config, null, 2)}
5189
+ `, dryRun);
5190
+ return path;
5191
+ }
5192
+ async function installCodex(scope, cliEntry, dryRun) {
5193
+ const configPath = codexConfigPath(scope);
5194
+ const config = existsSync10(configPath) ? readFileSync7(configPath, "utf-8") : "";
5195
+ await writeWithBackup(configPath, ensureCodexHooksEnabled(config), dryRun);
5196
+ const hooksPath = hooksJsonPath("codex", scope);
5197
+ const hookConfig = mergeCommandHook({
5198
+ config: readJsonFile(hooksPath),
5199
+ event: "Stop",
5200
+ command: commandFor(cliEntry, "codex")
5201
+ });
5202
+ await writeWithBackup(hooksPath, `${JSON.stringify(hookConfig, null, 2)}
5203
+ `, dryRun);
5204
+ return hooksPath;
5205
+ }
5206
+ function piExtensionSource(cliEntry) {
5207
+ const command = commandFor(cliEntry, "pi");
5208
+ return `import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
5209
+
5210
+ const REVIEW_COMMAND = ${JSON.stringify(command)};
5211
+
5212
+ export default function agendexPiExtension(pi: ExtensionAPI): void {
5213
+ pi.registerCommand('agendex-review-plan', {
5214
+ description: 'Open an Agendex plan review gate for the current Pi session',
5215
+ handler: async (_args, ctx) => {
5216
+ ctx.ui.notify('Agendex review command registered. Native interactive review is handled by the Agendex CLI hook path.', 'info');
5217
+ pi.sendMessage(
5218
+ {
5219
+ customType: 'agendex-review-command',
5220
+ content: '[Agendex] Run this hook command from a shell-integrated Pi workflow when you need hook-native review JSON:
5221
+ ' + REVIEW_COMMAND,
5222
+ display: true,
5223
+ },
5224
+ { triggerTurn: false },
5225
+ );
5226
+ },
5227
+ });
5228
+
5229
+ pi.registerCommand('agendex-annotate', {
5230
+ description: 'Record Agendex annotation feedback in the active Pi session',
5231
+ handler: async (args, ctx) => {
5232
+ const feedback = args?.trim();
5233
+ if (!feedback) {
5234
+ ctx.ui.notify('Usage: /agendex-annotate <feedback>', 'warning');
5235
+ return;
5236
+ }
5237
+ pi.sendMessage(
5238
+ {
5239
+ customType: 'agendex-annotation-feedback',
5240
+ content: '[Agendex annotation feedback]
5241
+ ' + feedback,
5242
+ display: true,
5243
+ },
5244
+ { triggerTurn: true },
5245
+ );
5246
+ },
5247
+ });
5248
+
5249
+ pi.on('session_start', async (_event, ctx) => {
5250
+ ctx.ui.setStatus('agendex', 'agendex hooks');
5251
+ });
5252
+ }
5253
+ `;
5254
+ }
5255
+ async function installPi(scope, cliEntry, dryRun) {
5256
+ const path = hooksJsonPath("pi", scope);
5257
+ await writeWithBackup(path, piExtensionSource(cliEntry), dryRun);
5258
+ return path;
5259
+ }
5260
+ async function installAgent(agent, scope, cliEntry, dryRun) {
5261
+ if (agent === "claude-code")
5262
+ return await installClaude(scope, cliEntry, dryRun);
5263
+ if (agent === "codex")
5264
+ return await installCodex(scope, cliEntry, dryRun);
5265
+ return await installPi(scope, cliEntry, dryRun);
5266
+ }
5267
+ async function uninstallJsonAgent(agent, scope, dryRun) {
5268
+ const path = hooksJsonPath(agent, scope);
5269
+ if (!existsSync10(path))
5270
+ return path;
5271
+ const updated = removeManagedHooks(readJsonFile(path));
5272
+ await writeWithBackup(path, `${JSON.stringify(updated, null, 2)}
5273
+ `, dryRun);
5274
+ return path;
5275
+ }
5276
+ async function uninstallPi(scope, dryRun) {
5277
+ const path = hooksJsonPath("pi", scope);
5278
+ if (hasManagedHook(path))
5279
+ await removeWithBackup(path, dryRun);
5280
+ return path;
5281
+ }
5282
+ async function uninstallAgent(agent, scope, dryRun) {
5283
+ if (agent === "claude-code" || agent === "codex")
5284
+ return await uninstallJsonAgent(agent, scope, dryRun);
5285
+ return await uninstallPi(scope, dryRun);
5286
+ }
5287
+ function statusFor(agent, scope) {
5288
+ const path = hooksJsonPath(agent, scope);
5289
+ if (agent === "codex") {
5290
+ const configPath = codexConfigPath(scope);
5291
+ const configEnabled = existsSync10(configPath) && /hooks\s*=\s*true/.test(readFileSync7(configPath, "utf-8"));
5292
+ const installed2 = configEnabled && hasManagedHook(path);
5293
+ return {
5294
+ agent,
5295
+ installed: installed2,
5296
+ path,
5297
+ detail: installed2 ? "Stop hook installed and hooks feature enabled" : "Missing Stop hook or [features].hooks = true"
5298
+ };
5299
+ }
5300
+ const installed = hasManagedHook(path);
5301
+ return {
5302
+ agent,
5303
+ installed,
5304
+ path,
5305
+ detail: installed ? "Agendex hook installed" : "Agendex hook not installed"
5306
+ };
5307
+ }
5308
+ function parseScope(args) {
5309
+ const scopeIndex = args.indexOf("--scope");
5310
+ const value = scopeIndex >= 0 ? args[scopeIndex + 1] : undefined;
5311
+ if (value === "user" || value === "repo")
5312
+ return value;
5313
+ return args.includes("--user") ? "user" : "repo";
5314
+ }
5315
+ function parseAgent(value) {
5316
+ if (!value)
5317
+ return;
5318
+ if (value === "all")
5319
+ return "all";
5320
+ if (value === "claude" || value === "claude-code")
5321
+ return "claude-code";
5322
+ if (value === "codex")
5323
+ return "codex";
5324
+ if (value === "pi")
5325
+ return "pi";
5326
+ return;
5327
+ }
5328
+ async function runHooksCommand(args, cliEntry) {
5329
+ const subcommand = args.find((arg) => arg !== "hooks" && arg !== "--dev" && !arg.startsWith("--"));
5330
+ const scope = parseScope(args);
5331
+ if (!subcommand || subcommand === "status") {
5332
+ const rows = SUPPORTED_AGENTS.map((agent) => statusFor(agent, scope));
5333
+ for (const row of rows) {
5334
+ console.log(`[agendex] ${row.agent}: ${row.installed ? "installed" : "not installed"} (${row.detail})`);
5335
+ console.log(` ${row.path}`);
5336
+ }
5337
+ return 0;
5338
+ }
5339
+ if (subcommand === "doctor") {
5340
+ await runHooksCommand(["hooks", "status", "--scope", scope], cliEntry);
5341
+ console.log("[agendex] restart the target agent after installing or changing hooks.");
5342
+ return 0;
5343
+ }
5344
+ if (subcommand === "install") {
5345
+ const agentArg = args.find((arg, index) => index > args.indexOf("install") && !arg.startsWith("--") && arg !== scope);
5346
+ const parsed = parseAgent(agentArg);
5347
+ if (!parsed) {
5348
+ console.error(`[agendex] usage: agendex hooks install <claude-code|codex|pi|all> [--scope repo|user] [--dry-run] [${CLAUDE_PREVIEW_FLAG}]`);
5349
+ return 1;
5350
+ }
5351
+ const dryRun = args.includes("--dry-run");
5352
+ const preview = args.includes(CLAUDE_PREVIEW_FLAG);
5353
+ const agents = parsed === "all" ? SUPPORTED_AGENTS : [parsed];
5354
+ if (!preview && agents.includes("claude-code")) {
5355
+ printClaudePreviewBlock();
5356
+ return 1;
5357
+ }
5358
+ for (const agent of agents) {
5359
+ if (agent === "claude-code")
5360
+ printClaudePreviewWarning(dryRun);
5361
+ const path = await installAgent(agent, scope, resolve7(cliEntry), dryRun);
5362
+ console.log(`[agendex] ${dryRun ? "would install" : "installed"} ${agent} hook: ${path}`);
5363
+ }
5364
+ return 0;
5365
+ }
5366
+ if (subcommand === "uninstall") {
5367
+ const agentArg = args.find((arg, index) => index > args.indexOf("uninstall") && !arg.startsWith("--") && arg !== scope);
5368
+ const parsed = parseAgent(agentArg);
5369
+ if (!parsed) {
5370
+ console.error("[agendex] usage: agendex hooks uninstall <claude-code|codex|pi|all> [--scope repo|user] [--dry-run]");
5371
+ return 1;
5372
+ }
5373
+ const dryRun = args.includes("--dry-run");
5374
+ const agents = parsed === "all" ? SUPPORTED_AGENTS : [parsed];
5375
+ for (const agent of agents) {
5376
+ const path = await uninstallAgent(agent, scope, dryRun);
5377
+ console.log(`[agendex] ${dryRun ? "would uninstall" : "uninstalled"} ${agent} hook: ${path}`);
5378
+ }
5379
+ return 0;
5380
+ }
5381
+ console.error(`[agendex] unknown hooks command: ${subcommand}`);
5382
+ return 1;
5383
+ }
5384
+ async function runHookReviewCommand(args) {
5385
+ if (!args.includes("--hook")) {
5386
+ console.error("[agendex] review-plan currently supports hook mode only: agendex review-plan --hook --agent <agent>");
5387
+ return 1;
5388
+ }
5389
+ console.error("[agendex] hook-native plan review is not implemented yet. Uninstall this hook or wait for the interactive review-session server before enabling it.");
5390
+ return 1;
5391
+ }
5392
+
4915
5393
  // src/sync.ts
4916
5394
  import { hostname as osHostname3 } from "node:os";
4917
5395
  async function syncAll(force = false) {
@@ -4970,17 +5448,17 @@ async function syncAll(force = false) {
4970
5448
  // src/upgrade.ts
4971
5449
  import { spawn as spawn3, spawnSync } from "node:child_process";
4972
5450
  import { realpathSync as realpathSync2 } from "node:fs";
4973
- import { dirname as dirname4, resolve as resolve7, sep as sep4 } from "node:path";
5451
+ import { dirname as dirname5, resolve as resolve8, sep as sep4 } from "node:path";
4974
5452
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4975
5453
 
4976
5454
  // src/version.ts
4977
- import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
5455
+ import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
4978
5456
  import { tmpdir } from "node:os";
4979
- import { join as join14 } from "node:path";
5457
+ import { join as join15 } from "node:path";
4980
5458
  // package.json
4981
5459
  var package_default = {
4982
5460
  name: "agendex-cli",
4983
- version: "0.18.0",
5461
+ version: "1.1.0",
4984
5462
  description: "Agendex CLI for login, sync, and daemon workflows",
4985
5463
  homepage: "https://github.com/Tyru5/Agendex#readme",
4986
5464
  bugs: {
@@ -5024,14 +5502,14 @@ var package_default = {
5024
5502
 
5025
5503
  // src/version.ts
5026
5504
  var CLI_VERSION = package_default.version;
5027
- var CACHE_FILE = process.env.AGENDEX_UPDATE_CACHE_FILE ?? join14(tmpdir(), ".agendex-update-cache.json");
5505
+ var CACHE_FILE = process.env.AGENDEX_UPDATE_CACHE_FILE ?? join15(tmpdir(), ".agendex-update-cache.json");
5028
5506
  var CACHE_TTL_MS = 24 * 60 * 60 * 1000;
5029
5507
  var UPDATE_URL = process.env.AGENDEX_UPDATE_URL ?? "https://registry.npmjs.org/agendex-cli/latest";
5030
5508
  function readCache(current) {
5031
5509
  try {
5032
- if (!existsSync10(CACHE_FILE))
5510
+ if (!existsSync11(CACHE_FILE))
5033
5511
  return null;
5034
- const { result, ts } = JSON.parse(readFileSync7(CACHE_FILE, "utf8"));
5512
+ const { result, ts } = JSON.parse(readFileSync8(CACHE_FILE, "utf8"));
5035
5513
  if (Date.now() - ts > CACHE_TTL_MS)
5036
5514
  return null;
5037
5515
  return normalizeResult(result, current);
@@ -5100,12 +5578,12 @@ function isNewer(latest, current) {
5100
5578
 
5101
5579
  // src/upgrade.ts
5102
5580
  var PACKAGE_NAME = "agendex-cli";
5103
- var moduleDir = dirname4(fileURLToPath2(import.meta.url));
5581
+ var moduleDir = dirname5(fileURLToPath2(import.meta.url));
5104
5582
  function getPackageRoot() {
5105
5583
  try {
5106
- return realpathSync2(resolve7(moduleDir, ".."));
5584
+ return realpathSync2(resolve8(moduleDir, ".."));
5107
5585
  } catch {
5108
- return resolve7(moduleDir, "..");
5586
+ return resolve8(moduleDir, "..");
5109
5587
  }
5110
5588
  }
5111
5589
  function detectPackageManager(packageRoot) {
@@ -5324,7 +5802,7 @@ function firstCommandToken(argv) {
5324
5802
  return;
5325
5803
  }
5326
5804
  var command = firstCommandToken(args) ?? "start";
5327
- var cliEntry = resolve8(process.argv[1] ?? fileURLToPath3(import.meta.url));
5805
+ var cliEntry = resolve9(process.argv[1] ?? fileURLToPath3(import.meta.url));
5328
5806
  async function main() {
5329
5807
  const isInternal = args.includes("--daemon") || args.includes("--worker");
5330
5808
  if (command === "--version" || command === "-v") {
@@ -5339,6 +5817,8 @@ async function main() {
5339
5817
  "open",
5340
5818
  "view",
5341
5819
  "cleanup",
5820
+ "hooks",
5821
+ "review-plan",
5342
5822
  "add-dir",
5343
5823
  "remove-dir",
5344
5824
  "list-dirs",
@@ -5442,6 +5922,12 @@ async function main() {
5442
5922
  await syncAll(force);
5443
5923
  return 0;
5444
5924
  }
5925
+ case "hooks": {
5926
+ return await runHooksCommand(args, cliEntry);
5927
+ }
5928
+ case "review-plan": {
5929
+ return await runHookReviewCommand(args);
5930
+ }
5445
5931
  case "cleanup": {
5446
5932
  const config = loadConfig();
5447
5933
  if (!config?.cloudToken || !config?.convexUrl) {
@@ -5524,7 +6010,7 @@ async function main() {
5524
6010
  return 1;
5525
6011
  }
5526
6012
  const resolved = resolveCustomPlanDirPath(dirPath);
5527
- if (!existsSync11(resolved)) {
6013
+ if (!existsSync12(resolved)) {
5528
6014
  writeStderr(`[agendex] path does not exist: ${resolved}`);
5529
6015
  return 1;
5530
6016
  }
@@ -5543,7 +6029,7 @@ async function main() {
5543
6029
  const { request } = await import("node:http");
5544
6030
  const body = JSON.stringify({ path: resolved });
5545
6031
  try {
5546
- const res = await new Promise((resolve9, reject) => {
6032
+ const res = await new Promise((resolve10, reject) => {
5547
6033
  const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
5548
6034
  method: "POST",
5549
6035
  headers: {
@@ -5557,7 +6043,7 @@ async function main() {
5557
6043
  res2.on("data", (chunk) => {
5558
6044
  data += chunk;
5559
6045
  });
5560
- res2.on("end", () => resolve9({ status: res2.statusCode ?? 0, body: data }));
6046
+ res2.on("end", () => resolve10({ status: res2.statusCode ?? 0, body: data }));
5561
6047
  res2.on("error", reject);
5562
6048
  });
5563
6049
  req.on("error", reject);
@@ -5707,6 +6193,10 @@ Usage:
5707
6193
  agendex view <url> Open a shared plan URL in your browser
5708
6194
  agendex logout Clear stored cloud token
5709
6195
  agendex configure Select which agents/adapters to index
6196
+ agendex hooks status Show Claude Code, Codex, and Pi hook status
6197
+ agendex hooks install <agent|all> Install hook integration (--preview required for claude-code)
6198
+ agendex hooks uninstall <agent|all> Remove managed Agendex hook entries
6199
+ agendex review-plan --hook --agent <agent> Hook-native plan review command
5710
6200
  agendex add-dir <path> Add a custom directory to scan for plans
5711
6201
  agendex add-dir <path> --live Add dir and notify running server immediately
5712
6202
  agendex remove-dir <path> Remove a custom directory
@@ -5746,13 +6236,13 @@ function flushStream(stream) {
5746
6236
  if (stream.destroyed || !stream.writable) {
5747
6237
  return Promise.resolve();
5748
6238
  }
5749
- return new Promise((resolve9, reject) => {
6239
+ return new Promise((resolve10, reject) => {
5750
6240
  stream.write("", (error) => {
5751
6241
  if (error) {
5752
6242
  reject(error);
5753
6243
  return;
5754
6244
  }
5755
- resolve9();
6245
+ resolve10();
5756
6246
  });
5757
6247
  });
5758
6248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.18.0",
3
+ "version": "1.1.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {