agendex-cli 0.18.0 → 1.0.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 +469 -19
  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
@@ -2085,7 +2085,8 @@ async function parseLiveSession(filePath) {
2085
2085
  sessionPath: filePath,
2086
2086
  sourcePlanPath,
2087
2087
  startedAt: session.startedAt,
2088
- writebackCapable: true
2088
+ writebackCapable: true,
2089
+ liveness: "live"
2089
2090
  };
2090
2091
  return [
2091
2092
  {
@@ -2910,6 +2911,8 @@ async function loadOrInitConfig(options = {}) {
2910
2911
  // ../shared/src/daemon-status.ts
2911
2912
  var CLI_DAEMON_HEARTBEAT_INTERVAL_MS = 30000;
2912
2913
  var CLI_DAEMON_STALE_AFTER_MS = CLI_DAEMON_HEARTBEAT_INTERVAL_MS * 5;
2914
+ // ../shared/src/services/annotation-store.ts
2915
+ var mutationQueue = Promise.resolve();
2913
2916
  // ../shared/src/services/plan-service.ts
2914
2917
  import { existsSync as existsSync5, readdirSync as readdirSync3, realpathSync, statSync } from "node:fs";
2915
2918
  import { lstat, mkdir, readdir as readdir2, readFile as readFile7, stat as stat7, writeFile as writeFile3 } from "node:fs/promises";
@@ -4633,6 +4636,32 @@ var RESTART_DELAY_MS = 5000;
4633
4636
  var PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS = 15000;
4634
4637
  var PLANNOTATOR_WRITEBACK_EXPIRED_ERROR = "Write-back expired before delivery.";
4635
4638
  var PLANNOTATOR_WRITEBACK_FAILED_ERROR = "No live Plannotator session accepted the request-changes payload.";
4639
+ var PLANNOTATOR_LIVENESS_SWEEP_INTERVAL_MS = 20000;
4640
+ function isRecord5(value) {
4641
+ return typeof value === "object" && value !== null;
4642
+ }
4643
+ function isLivePlannotatorPayload(payload) {
4644
+ const plannotator = isRecord5(payload.metadata) ? payload.metadata.plannotator : undefined;
4645
+ if (!isRecord5(plannotator))
4646
+ return false;
4647
+ return plannotator.kind === "live-session" && plannotator.writebackCapable === true;
4648
+ }
4649
+ function buildEndedPlannotatorPayload(payload) {
4650
+ const metadata = isRecord5(payload.metadata) ? payload.metadata : {};
4651
+ const plannotator = isRecord5(metadata.plannotator) ? metadata.plannotator : {};
4652
+ return {
4653
+ ...payload,
4654
+ metadata: {
4655
+ ...metadata,
4656
+ plannotator: {
4657
+ ...plannotator,
4658
+ writebackCapable: false,
4659
+ liveness: "ended",
4660
+ endedAt: Date.now()
4661
+ }
4662
+ }
4663
+ };
4664
+ }
4636
4665
  async function runWorker() {
4637
4666
  const config = await loadOrInitConfig();
4638
4667
  const hostname2 = osHostname2();
@@ -4642,6 +4671,7 @@ async function runWorker() {
4642
4671
  const syncCache = loadSyncCache();
4643
4672
  const syncQueue = [];
4644
4673
  const pendingWritebackReports = loadPendingWritebackReports();
4674
+ const liveSessions = new Map;
4645
4675
  let syncing = false;
4646
4676
  let cachedIpAddress;
4647
4677
  async function getSyncIpAddress() {
@@ -4717,6 +4747,31 @@ async function runWorker() {
4717
4747
  if (syncQueue.length > 0)
4718
4748
  processSyncQueue();
4719
4749
  }
4750
+ async function reconcileLivePlannotatorSessions(plans2) {
4751
+ const ipAddress = await getSyncIpAddress();
4752
+ const livePayloads = new Map;
4753
+ for (const plan of plans2) {
4754
+ const payload = planToSyncPayload(plan, config.deviceId, hostname2, ipAddress);
4755
+ if (isLivePlannotatorPayload(payload))
4756
+ livePayloads.set(payload.localPlanId, payload);
4757
+ }
4758
+ let queued = false;
4759
+ for (const [planId, lastPayload] of liveSessions) {
4760
+ if (livePayloads.has(planId))
4761
+ continue;
4762
+ const endedPayload = buildEndedPlannotatorPayload(lastPayload);
4763
+ syncQueue.push(endedPayload);
4764
+ liveSessions.delete(planId);
4765
+ queued = true;
4766
+ console.log(`[agendex] Plannotator session ended: ${endedPayload.title}`);
4767
+ }
4768
+ for (const [planId, payload] of livePayloads) {
4769
+ liveSessions.set(planId, payload);
4770
+ }
4771
+ if (queued)
4772
+ processSyncQueue();
4773
+ return queued;
4774
+ }
4720
4775
  function persistPendingWritebackReports() {
4721
4776
  if (!savePendingWritebackReports(pendingWritebackReports)) {
4722
4777
  console.error("[agendex] failed to persist Plannotator write-back delivery cache");
@@ -4834,6 +4889,7 @@ async function runWorker() {
4834
4889
  const lowValueSuffix = lowValuePlanCount > 0 ? `, ${lowValuePlanCount} low-value hidden/pruned` : "";
4835
4890
  console.log(`[agendex] syncing ${initialQueuedSyncable} plans${lowValueSuffix} (${initialQueuedLowValue} low-value queued, ${initialSkipped} unchanged)...`);
4836
4891
  await processSyncQueue();
4892
+ await reconcileLivePlannotatorSessions(getAll());
4837
4893
  setInterval(() => {
4838
4894
  (async () => {
4839
4895
  await sendHeartbeat(await getSyncIpAddress());
@@ -4842,6 +4898,14 @@ async function runWorker() {
4842
4898
  if (shouldEnablePlannotatorSync(config)) {
4843
4899
  setInterval(() => void pollPlannotatorWritebacks(), PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS);
4844
4900
  pollPlannotatorWritebacks();
4901
+ setInterval(() => {
4902
+ (async () => {
4903
+ await scan();
4904
+ await reconcileLivePlannotatorSessions(getAll());
4905
+ })().catch((err) => {
4906
+ console.error("[agendex] Plannotator liveness sweep failed:", err);
4907
+ });
4908
+ }, PLANNOTATOR_LIVENESS_SWEEP_INTERVAL_MS);
4845
4909
  }
4846
4910
  startWatching((changedPlans) => {
4847
4911
  (async () => {
@@ -4850,6 +4914,7 @@ async function runWorker() {
4850
4914
  syncQueue.push(planToSyncPayload(plan, config.deviceId, hostname2, ipAddress));
4851
4915
  }
4852
4916
  processSyncQueue();
4917
+ await reconcileLivePlannotatorSessions(getAll());
4853
4918
  })().catch((err) => {
4854
4919
  console.error("[agendex] failed to queue changed plans:", err);
4855
4920
  });
@@ -4912,6 +4977,379 @@ async function startSupervisor() {
4912
4977
  removePid();
4913
4978
  }
4914
4979
 
4980
+ // src/hooks.ts
4981
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
4982
+ import { mkdir as mkdir2, rm, writeFile as writeFile4 } from "node:fs/promises";
4983
+ import { homedir as homedir10 } from "node:os";
4984
+ import { dirname as dirname4, join as join14, resolve as resolve7 } from "node:path";
4985
+ var SUPPORTED_AGENTS = ["claude-code", "codex", "pi"];
4986
+ var MANAGED_MARKER = "agendex-plan-review";
4987
+ var HOOK_TIMEOUT_SECONDS = 345600;
4988
+ var CLAUDE_PREVIEW_FLAG = "--preview";
4989
+ function shellQuote(value) {
4990
+ return `'${value.replace(/'/g, `'\\''`)}'`;
4991
+ }
4992
+ function commandFor(cliEntry, agent) {
4993
+ return `${shellQuote(process.execPath)} ${shellQuote(cliEntry)} review-plan --hook --agent ${agent}`;
4994
+ }
4995
+ function scopeRoot(scope) {
4996
+ return scope === "repo" ? resolve7(process.env.PWD || process.cwd()) : homedir10();
4997
+ }
4998
+ function hooksJsonPath(agent, scope) {
4999
+ const root = scopeRoot(scope);
5000
+ if (agent === "claude-code")
5001
+ return join14(root, ".claude", "hooks.json");
5002
+ if (agent === "codex")
5003
+ return join14(root, ".codex", "hooks.json");
5004
+ return join14(root, scope === "repo" ? ".pi/extensions/agendex/index.ts" : ".pi/agent/extensions/agendex/index.ts");
5005
+ }
5006
+ function codexConfigPath(scope) {
5007
+ return join14(scopeRoot(scope), ".codex", "config.toml");
5008
+ }
5009
+ function readJsonFile(path) {
5010
+ if (!existsSync10(path))
5011
+ return {};
5012
+ try {
5013
+ return JSON.parse(readFileSync7(path, "utf-8"));
5014
+ } catch (err) {
5015
+ throw new Error(`Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`);
5016
+ }
5017
+ }
5018
+ function isRecord6(value) {
5019
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5020
+ }
5021
+ function backupPathFor(path) {
5022
+ return `${path}.bak-${new Date().toISOString().replace(/[:.]/g, "-")}`;
5023
+ }
5024
+ async function writeWithBackup(path, content, dryRun) {
5025
+ if (dryRun)
5026
+ return;
5027
+ await mkdir2(dirname4(path), { recursive: true });
5028
+ if (existsSync10(path)) {
5029
+ await writeFile4(backupPathFor(path), readFileSync7(path, "utf-8"), "utf-8");
5030
+ }
5031
+ await writeFile4(path, content, "utf-8");
5032
+ }
5033
+ async function removeWithBackup(path, dryRun) {
5034
+ if (dryRun || !existsSync10(path))
5035
+ return;
5036
+ await writeFile4(backupPathFor(path), readFileSync7(path, "utf-8"), "utf-8");
5037
+ await rm(path);
5038
+ }
5039
+ function mergeCommandHook({
5040
+ config,
5041
+ event,
5042
+ matcher,
5043
+ command
5044
+ }) {
5045
+ const hooks = isRecord6(config.hooks) ? { ...config.hooks } : {};
5046
+ const existingEvent = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
5047
+ const entry = {
5048
+ ...matcher ? { matcher } : {},
5049
+ id: MANAGED_MARKER,
5050
+ hooks: [
5051
+ {
5052
+ type: "command",
5053
+ command,
5054
+ timeout: HOOK_TIMEOUT_SECONDS
5055
+ }
5056
+ ]
5057
+ };
5058
+ const index = existingEvent.findIndex((item) => {
5059
+ if (!isRecord6(item))
5060
+ return false;
5061
+ if (item.id === MANAGED_MARKER)
5062
+ return true;
5063
+ return JSON.stringify(item).includes("agendex") && JSON.stringify(item).includes("review-plan");
5064
+ });
5065
+ if (index >= 0)
5066
+ existingEvent[index] = entry;
5067
+ else
5068
+ existingEvent.push(entry);
5069
+ hooks[event] = existingEvent;
5070
+ return { ...config, hooks };
5071
+ }
5072
+ function hasManagedHook(path) {
5073
+ if (!existsSync10(path))
5074
+ return false;
5075
+ const raw = readFileSync7(path, "utf-8");
5076
+ return raw.includes(MANAGED_MARKER) || raw.includes("agendex") && raw.includes("review-plan");
5077
+ }
5078
+ function isManagedHookEntry(item) {
5079
+ if (!isRecord6(item))
5080
+ return false;
5081
+ if (item.id === MANAGED_MARKER)
5082
+ return true;
5083
+ const serialized = JSON.stringify(item);
5084
+ return serialized.includes("agendex") && serialized.includes("review-plan");
5085
+ }
5086
+ function removeManagedHooks(config) {
5087
+ if (!isRecord6(config.hooks))
5088
+ return config;
5089
+ const hooks = {};
5090
+ for (const [event, value] of Object.entries(config.hooks)) {
5091
+ if (!Array.isArray(value)) {
5092
+ hooks[event] = value;
5093
+ continue;
5094
+ }
5095
+ const kept = value.filter((item) => !isManagedHookEntry(item));
5096
+ if (kept.length > 0)
5097
+ hooks[event] = kept;
5098
+ }
5099
+ return Object.keys(hooks).length > 0 ? { ...config, hooks } : { ...config, hooks: {} };
5100
+ }
5101
+ function ensureCodexHooksEnabled(raw) {
5102
+ const lines = raw ? raw.split(`
5103
+ `) : [];
5104
+ const featuresIndex = lines.findIndex((line) => /^\s*\[features\]\s*$/.test(line));
5105
+ if (featuresIndex === -1) {
5106
+ return `${raw.trimEnd()}
5107
+
5108
+ [features]
5109
+ hooks = true
5110
+ `;
5111
+ }
5112
+ let insertAt = lines.length;
5113
+ for (let i = featuresIndex + 1;i < lines.length; i++) {
5114
+ if (/^\s*\[.+\]\s*$/.test(lines[i] ?? "")) {
5115
+ insertAt = i;
5116
+ break;
5117
+ }
5118
+ if (/^\s*hooks\s*=/.test(lines[i] ?? "")) {
5119
+ lines[i] = "hooks = true";
5120
+ return `${lines.join(`
5121
+ `).trimEnd()}
5122
+ `;
5123
+ }
5124
+ }
5125
+ lines.splice(insertAt, 0, "hooks = true");
5126
+ return `${lines.join(`
5127
+ `).trimEnd()}
5128
+ `;
5129
+ }
5130
+ function printClaudePreviewBlock() {
5131
+ console.error("[agendex] refusing to install claude-code hook: hook-native plan review is not implemented yet.");
5132
+ console.error("[agendex] Installing it now would cause Claude Code to deny ExitPlanMode permission requests.");
5133
+ console.error(`[agendex] Re-run with ${CLAUDE_PREVIEW_FLAG} to opt in deliberately, or install codex/pi separately.`);
5134
+ }
5135
+ function printClaudePreviewWarning(dryRun) {
5136
+ console.error("[agendex] WARNING: claude-code hook support is preview-only.");
5137
+ 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.");
5138
+ console.error("[agendex] Remove it with: agendex hooks uninstall claude-code");
5139
+ }
5140
+ async function installClaude(scope, cliEntry, dryRun) {
5141
+ const path = hooksJsonPath("claude-code", scope);
5142
+ const config = mergeCommandHook({
5143
+ config: readJsonFile(path),
5144
+ event: "PermissionRequest",
5145
+ matcher: "ExitPlanMode",
5146
+ command: commandFor(cliEntry, "claude-code")
5147
+ });
5148
+ await writeWithBackup(path, `${JSON.stringify(config, null, 2)}
5149
+ `, dryRun);
5150
+ return path;
5151
+ }
5152
+ async function installCodex(scope, cliEntry, dryRun) {
5153
+ const configPath = codexConfigPath(scope);
5154
+ const config = existsSync10(configPath) ? readFileSync7(configPath, "utf-8") : "";
5155
+ await writeWithBackup(configPath, ensureCodexHooksEnabled(config), dryRun);
5156
+ const hooksPath = hooksJsonPath("codex", scope);
5157
+ const hookConfig = mergeCommandHook({
5158
+ config: readJsonFile(hooksPath),
5159
+ event: "Stop",
5160
+ command: commandFor(cliEntry, "codex")
5161
+ });
5162
+ await writeWithBackup(hooksPath, `${JSON.stringify(hookConfig, null, 2)}
5163
+ `, dryRun);
5164
+ return hooksPath;
5165
+ }
5166
+ function piExtensionSource(cliEntry) {
5167
+ const command = commandFor(cliEntry, "pi");
5168
+ return `import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
5169
+
5170
+ const REVIEW_COMMAND = ${JSON.stringify(command)};
5171
+
5172
+ export default function agendexPiExtension(pi: ExtensionAPI): void {
5173
+ pi.registerCommand('agendex-review-plan', {
5174
+ description: 'Open an Agendex plan review gate for the current Pi session',
5175
+ handler: async (_args, ctx) => {
5176
+ ctx.ui.notify('Agendex review command registered. Native interactive review is handled by the Agendex CLI hook path.', 'info');
5177
+ pi.sendMessage(
5178
+ {
5179
+ customType: 'agendex-review-command',
5180
+ content: '[Agendex] Run this hook command from a shell-integrated Pi workflow when you need hook-native review JSON:
5181
+ ' + REVIEW_COMMAND,
5182
+ display: true,
5183
+ },
5184
+ { triggerTurn: false },
5185
+ );
5186
+ },
5187
+ });
5188
+
5189
+ pi.registerCommand('agendex-annotate', {
5190
+ description: 'Record Agendex annotation feedback in the active Pi session',
5191
+ handler: async (args, ctx) => {
5192
+ const feedback = args?.trim();
5193
+ if (!feedback) {
5194
+ ctx.ui.notify('Usage: /agendex-annotate <feedback>', 'warning');
5195
+ return;
5196
+ }
5197
+ pi.sendMessage(
5198
+ {
5199
+ customType: 'agendex-annotation-feedback',
5200
+ content: '[Agendex annotation feedback]
5201
+ ' + feedback,
5202
+ display: true,
5203
+ },
5204
+ { triggerTurn: true },
5205
+ );
5206
+ },
5207
+ });
5208
+
5209
+ pi.on('session_start', async (_event, ctx) => {
5210
+ ctx.ui.setStatus('agendex', 'agendex hooks');
5211
+ });
5212
+ }
5213
+ `;
5214
+ }
5215
+ async function installPi(scope, cliEntry, dryRun) {
5216
+ const path = hooksJsonPath("pi", scope);
5217
+ await writeWithBackup(path, piExtensionSource(cliEntry), dryRun);
5218
+ return path;
5219
+ }
5220
+ async function installAgent(agent, scope, cliEntry, dryRun) {
5221
+ if (agent === "claude-code")
5222
+ return await installClaude(scope, cliEntry, dryRun);
5223
+ if (agent === "codex")
5224
+ return await installCodex(scope, cliEntry, dryRun);
5225
+ return await installPi(scope, cliEntry, dryRun);
5226
+ }
5227
+ async function uninstallJsonAgent(agent, scope, dryRun) {
5228
+ const path = hooksJsonPath(agent, scope);
5229
+ if (!existsSync10(path))
5230
+ return path;
5231
+ const updated = removeManagedHooks(readJsonFile(path));
5232
+ await writeWithBackup(path, `${JSON.stringify(updated, null, 2)}
5233
+ `, dryRun);
5234
+ return path;
5235
+ }
5236
+ async function uninstallPi(scope, dryRun) {
5237
+ const path = hooksJsonPath("pi", scope);
5238
+ if (hasManagedHook(path))
5239
+ await removeWithBackup(path, dryRun);
5240
+ return path;
5241
+ }
5242
+ async function uninstallAgent(agent, scope, dryRun) {
5243
+ if (agent === "claude-code" || agent === "codex")
5244
+ return await uninstallJsonAgent(agent, scope, dryRun);
5245
+ return await uninstallPi(scope, dryRun);
5246
+ }
5247
+ function statusFor(agent, scope) {
5248
+ const path = hooksJsonPath(agent, scope);
5249
+ if (agent === "codex") {
5250
+ const configPath = codexConfigPath(scope);
5251
+ const configEnabled = existsSync10(configPath) && /hooks\s*=\s*true/.test(readFileSync7(configPath, "utf-8"));
5252
+ const installed2 = configEnabled && hasManagedHook(path);
5253
+ return {
5254
+ agent,
5255
+ installed: installed2,
5256
+ path,
5257
+ detail: installed2 ? "Stop hook installed and hooks feature enabled" : "Missing Stop hook or [features].hooks = true"
5258
+ };
5259
+ }
5260
+ const installed = hasManagedHook(path);
5261
+ return {
5262
+ agent,
5263
+ installed,
5264
+ path,
5265
+ detail: installed ? "Agendex hook installed" : "Agendex hook not installed"
5266
+ };
5267
+ }
5268
+ function parseScope(args) {
5269
+ const scopeIndex = args.indexOf("--scope");
5270
+ const value = scopeIndex >= 0 ? args[scopeIndex + 1] : undefined;
5271
+ if (value === "user" || value === "repo")
5272
+ return value;
5273
+ return args.includes("--user") ? "user" : "repo";
5274
+ }
5275
+ function parseAgent(value) {
5276
+ if (!value)
5277
+ return;
5278
+ if (value === "all")
5279
+ return "all";
5280
+ if (value === "claude" || value === "claude-code")
5281
+ return "claude-code";
5282
+ if (value === "codex")
5283
+ return "codex";
5284
+ if (value === "pi")
5285
+ return "pi";
5286
+ return;
5287
+ }
5288
+ async function runHooksCommand(args, cliEntry) {
5289
+ const subcommand = args.find((arg) => arg !== "hooks" && arg !== "--dev" && !arg.startsWith("--"));
5290
+ const scope = parseScope(args);
5291
+ if (!subcommand || subcommand === "status") {
5292
+ const rows = SUPPORTED_AGENTS.map((agent) => statusFor(agent, scope));
5293
+ for (const row of rows) {
5294
+ console.log(`[agendex] ${row.agent}: ${row.installed ? "installed" : "not installed"} (${row.detail})`);
5295
+ console.log(` ${row.path}`);
5296
+ }
5297
+ return 0;
5298
+ }
5299
+ if (subcommand === "doctor") {
5300
+ await runHooksCommand(["hooks", "status", "--scope", scope], cliEntry);
5301
+ console.log("[agendex] restart the target agent after installing or changing hooks.");
5302
+ return 0;
5303
+ }
5304
+ if (subcommand === "install") {
5305
+ const agentArg = args.find((arg, index) => index > args.indexOf("install") && !arg.startsWith("--") && arg !== scope);
5306
+ const parsed = parseAgent(agentArg);
5307
+ if (!parsed) {
5308
+ console.error(`[agendex] usage: agendex hooks install <claude-code|codex|pi|all> [--scope repo|user] [--dry-run] [${CLAUDE_PREVIEW_FLAG}]`);
5309
+ return 1;
5310
+ }
5311
+ const dryRun = args.includes("--dry-run");
5312
+ const preview = args.includes(CLAUDE_PREVIEW_FLAG);
5313
+ const agents = parsed === "all" ? SUPPORTED_AGENTS : [parsed];
5314
+ if (!preview && agents.includes("claude-code")) {
5315
+ printClaudePreviewBlock();
5316
+ return 1;
5317
+ }
5318
+ for (const agent of agents) {
5319
+ if (agent === "claude-code")
5320
+ printClaudePreviewWarning(dryRun);
5321
+ const path = await installAgent(agent, scope, resolve7(cliEntry), dryRun);
5322
+ console.log(`[agendex] ${dryRun ? "would install" : "installed"} ${agent} hook: ${path}`);
5323
+ }
5324
+ return 0;
5325
+ }
5326
+ if (subcommand === "uninstall") {
5327
+ const agentArg = args.find((arg, index) => index > args.indexOf("uninstall") && !arg.startsWith("--") && arg !== scope);
5328
+ const parsed = parseAgent(agentArg);
5329
+ if (!parsed) {
5330
+ console.error("[agendex] usage: agendex hooks uninstall <claude-code|codex|pi|all> [--scope repo|user] [--dry-run]");
5331
+ return 1;
5332
+ }
5333
+ const dryRun = args.includes("--dry-run");
5334
+ const agents = parsed === "all" ? SUPPORTED_AGENTS : [parsed];
5335
+ for (const agent of agents) {
5336
+ const path = await uninstallAgent(agent, scope, dryRun);
5337
+ console.log(`[agendex] ${dryRun ? "would uninstall" : "uninstalled"} ${agent} hook: ${path}`);
5338
+ }
5339
+ return 0;
5340
+ }
5341
+ console.error(`[agendex] unknown hooks command: ${subcommand}`);
5342
+ return 1;
5343
+ }
5344
+ async function runHookReviewCommand(args) {
5345
+ if (!args.includes("--hook")) {
5346
+ console.error("[agendex] review-plan currently supports hook mode only: agendex review-plan --hook --agent <agent>");
5347
+ return 1;
5348
+ }
5349
+ 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.");
5350
+ return 1;
5351
+ }
5352
+
4915
5353
  // src/sync.ts
4916
5354
  import { hostname as osHostname3 } from "node:os";
4917
5355
  async function syncAll(force = false) {
@@ -4970,17 +5408,17 @@ async function syncAll(force = false) {
4970
5408
  // src/upgrade.ts
4971
5409
  import { spawn as spawn3, spawnSync } from "node:child_process";
4972
5410
  import { realpathSync as realpathSync2 } from "node:fs";
4973
- import { dirname as dirname4, resolve as resolve7, sep as sep4 } from "node:path";
5411
+ import { dirname as dirname5, resolve as resolve8, sep as sep4 } from "node:path";
4974
5412
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4975
5413
 
4976
5414
  // src/version.ts
4977
- import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
5415
+ import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
4978
5416
  import { tmpdir } from "node:os";
4979
- import { join as join14 } from "node:path";
5417
+ import { join as join15 } from "node:path";
4980
5418
  // package.json
4981
5419
  var package_default = {
4982
5420
  name: "agendex-cli",
4983
- version: "0.18.0",
5421
+ version: "1.0.0",
4984
5422
  description: "Agendex CLI for login, sync, and daemon workflows",
4985
5423
  homepage: "https://github.com/Tyru5/Agendex#readme",
4986
5424
  bugs: {
@@ -5024,14 +5462,14 @@ var package_default = {
5024
5462
 
5025
5463
  // src/version.ts
5026
5464
  var CLI_VERSION = package_default.version;
5027
- var CACHE_FILE = process.env.AGENDEX_UPDATE_CACHE_FILE ?? join14(tmpdir(), ".agendex-update-cache.json");
5465
+ var CACHE_FILE = process.env.AGENDEX_UPDATE_CACHE_FILE ?? join15(tmpdir(), ".agendex-update-cache.json");
5028
5466
  var CACHE_TTL_MS = 24 * 60 * 60 * 1000;
5029
5467
  var UPDATE_URL = process.env.AGENDEX_UPDATE_URL ?? "https://registry.npmjs.org/agendex-cli/latest";
5030
5468
  function readCache(current) {
5031
5469
  try {
5032
- if (!existsSync10(CACHE_FILE))
5470
+ if (!existsSync11(CACHE_FILE))
5033
5471
  return null;
5034
- const { result, ts } = JSON.parse(readFileSync7(CACHE_FILE, "utf8"));
5472
+ const { result, ts } = JSON.parse(readFileSync8(CACHE_FILE, "utf8"));
5035
5473
  if (Date.now() - ts > CACHE_TTL_MS)
5036
5474
  return null;
5037
5475
  return normalizeResult(result, current);
@@ -5100,12 +5538,12 @@ function isNewer(latest, current) {
5100
5538
 
5101
5539
  // src/upgrade.ts
5102
5540
  var PACKAGE_NAME = "agendex-cli";
5103
- var moduleDir = dirname4(fileURLToPath2(import.meta.url));
5541
+ var moduleDir = dirname5(fileURLToPath2(import.meta.url));
5104
5542
  function getPackageRoot() {
5105
5543
  try {
5106
- return realpathSync2(resolve7(moduleDir, ".."));
5544
+ return realpathSync2(resolve8(moduleDir, ".."));
5107
5545
  } catch {
5108
- return resolve7(moduleDir, "..");
5546
+ return resolve8(moduleDir, "..");
5109
5547
  }
5110
5548
  }
5111
5549
  function detectPackageManager(packageRoot) {
@@ -5324,7 +5762,7 @@ function firstCommandToken(argv) {
5324
5762
  return;
5325
5763
  }
5326
5764
  var command = firstCommandToken(args) ?? "start";
5327
- var cliEntry = resolve8(process.argv[1] ?? fileURLToPath3(import.meta.url));
5765
+ var cliEntry = resolve9(process.argv[1] ?? fileURLToPath3(import.meta.url));
5328
5766
  async function main() {
5329
5767
  const isInternal = args.includes("--daemon") || args.includes("--worker");
5330
5768
  if (command === "--version" || command === "-v") {
@@ -5339,6 +5777,8 @@ async function main() {
5339
5777
  "open",
5340
5778
  "view",
5341
5779
  "cleanup",
5780
+ "hooks",
5781
+ "review-plan",
5342
5782
  "add-dir",
5343
5783
  "remove-dir",
5344
5784
  "list-dirs",
@@ -5442,6 +5882,12 @@ async function main() {
5442
5882
  await syncAll(force);
5443
5883
  return 0;
5444
5884
  }
5885
+ case "hooks": {
5886
+ return await runHooksCommand(args, cliEntry);
5887
+ }
5888
+ case "review-plan": {
5889
+ return await runHookReviewCommand(args);
5890
+ }
5445
5891
  case "cleanup": {
5446
5892
  const config = loadConfig();
5447
5893
  if (!config?.cloudToken || !config?.convexUrl) {
@@ -5524,7 +5970,7 @@ async function main() {
5524
5970
  return 1;
5525
5971
  }
5526
5972
  const resolved = resolveCustomPlanDirPath(dirPath);
5527
- if (!existsSync11(resolved)) {
5973
+ if (!existsSync12(resolved)) {
5528
5974
  writeStderr(`[agendex] path does not exist: ${resolved}`);
5529
5975
  return 1;
5530
5976
  }
@@ -5543,7 +5989,7 @@ async function main() {
5543
5989
  const { request } = await import("node:http");
5544
5990
  const body = JSON.stringify({ path: resolved });
5545
5991
  try {
5546
- const res = await new Promise((resolve9, reject) => {
5992
+ const res = await new Promise((resolve10, reject) => {
5547
5993
  const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
5548
5994
  method: "POST",
5549
5995
  headers: {
@@ -5557,7 +6003,7 @@ async function main() {
5557
6003
  res2.on("data", (chunk) => {
5558
6004
  data += chunk;
5559
6005
  });
5560
- res2.on("end", () => resolve9({ status: res2.statusCode ?? 0, body: data }));
6006
+ res2.on("end", () => resolve10({ status: res2.statusCode ?? 0, body: data }));
5561
6007
  res2.on("error", reject);
5562
6008
  });
5563
6009
  req.on("error", reject);
@@ -5707,6 +6153,10 @@ Usage:
5707
6153
  agendex view <url> Open a shared plan URL in your browser
5708
6154
  agendex logout Clear stored cloud token
5709
6155
  agendex configure Select which agents/adapters to index
6156
+ agendex hooks status Show Claude Code, Codex, and Pi hook status
6157
+ agendex hooks install <agent|all> Install hook integration (--preview required for claude-code)
6158
+ agendex hooks uninstall <agent|all> Remove managed Agendex hook entries
6159
+ agendex review-plan --hook --agent <agent> Hook-native plan review command
5710
6160
  agendex add-dir <path> Add a custom directory to scan for plans
5711
6161
  agendex add-dir <path> --live Add dir and notify running server immediately
5712
6162
  agendex remove-dir <path> Remove a custom directory
@@ -5746,13 +6196,13 @@ function flushStream(stream) {
5746
6196
  if (stream.destroyed || !stream.writable) {
5747
6197
  return Promise.resolve();
5748
6198
  }
5749
- return new Promise((resolve9, reject) => {
6199
+ return new Promise((resolve10, reject) => {
5750
6200
  stream.write("", (error) => {
5751
6201
  if (error) {
5752
6202
  reject(error);
5753
6203
  return;
5754
6204
  }
5755
- resolve9();
6205
+ resolve10();
5756
6206
  });
5757
6207
  });
5758
6208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.18.0",
3
+ "version": "1.0.0",
4
4
  "description": "Agendex CLI for login, sync, and daemon workflows",
5
5
  "homepage": "https://github.com/Tyru5/Agendex#readme",
6
6
  "repository": {