agendex-cli 0.17.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 +490 -49
  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";
@@ -4176,12 +4179,12 @@ async function login(siteUrlOverride) {
4176
4179
  const existing = loadConfig();
4177
4180
  const config = {
4178
4181
  configVersion: 3,
4179
- token: existing?.token,
4180
4182
  cloudToken: callback.token,
4181
4183
  convexUrl: callback.convexUrl,
4182
- deviceId: existing?.deviceId,
4183
4184
  enabledAdapters: existing?.enabledAdapters ?? [],
4184
- customPlanDirs: existing?.customPlanDirs ?? []
4185
+ customPlanDirs: existing?.customPlanDirs ?? [],
4186
+ ...existing?.token ? { token: existing.token } : {},
4187
+ ...existing?.deviceId ? { deviceId: existing.deviceId } : {}
4185
4188
  };
4186
4189
  saveConfig(config);
4187
4190
  console.log(`[agendex] Logged in successfully!`);
@@ -4195,12 +4198,10 @@ function logout() {
4195
4198
  }
4196
4199
  const config = {
4197
4200
  configVersion: 3,
4198
- token: existing.token,
4199
- cloudToken: undefined,
4200
- convexUrl: undefined,
4201
- deviceId: existing.deviceId,
4202
4201
  enabledAdapters: existing.enabledAdapters,
4203
- customPlanDirs: existing.customPlanDirs
4202
+ customPlanDirs: existing.customPlanDirs,
4203
+ ...existing.token ? { token: existing.token } : {},
4204
+ ...existing.deviceId ? { deviceId: existing.deviceId } : {}
4204
4205
  };
4205
4206
  saveConfig(config);
4206
4207
  console.log("[agendex] Logged out. Cloud token removed.");
@@ -4290,38 +4291,31 @@ async function startCallbackServer() {
4290
4291
  };
4291
4292
  }
4292
4293
  function callbackPage(success) {
4293
- const title = success ? "Login successful" : "Login failed";
4294
- const message = success ? "You can close this tab and return to your terminal." : "Missing token. Please try again.";
4295
- const icon = success ? '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:32px;height:32px;color:#22c55e"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:32px;height:32px;color:#ef4444"><path stroke-linecap="round" stroke-linejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>';
4294
+ const title = success ? "Signed in" : "Sign in failed";
4295
+ const message = success ? "Return to your terminal." : "Run agendex login again.";
4296
4296
  return `<!DOCTYPE html>
4297
4297
  <html lang="en">
4298
4298
  <head>
4299
4299
  <meta charset="utf-8"/>
4300
4300
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
4301
- <title>${title} Agendex</title>
4301
+ <title>${title} | Agendex</title>
4302
4302
  <style>
4303
- *{margin:0;padding:0;box-sizing:border-box}
4304
- @media(prefers-color-scheme:dark){
4305
- :root{--bg:#111;--surface:#161616;--text:#e8e8e8;--secondary:#888;--tertiary:#555;--border:rgba(255,255,255,0.06)}
4306
- }
4307
- @media(prefers-color-scheme:light){
4308
- :root{--bg:#fafafa;--surface:#fff;--text:#111;--secondary:#666;--tertiary:#999;--border:rgba(0,0,0,0.06)}
4309
- }
4310
- body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);display:flex;align-items:center;justify-content:center;min-height:100vh;-webkit-font-smoothing:antialiased}
4311
- .card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:40px 48px;text-align:center;max-width:400px;width:100%;box-shadow:0 2px 16px rgba(0,0,0,0.04)}
4312
- .icon{margin-bottom:16px;display:flex;justify-content:center}
4313
- h1{font-size:18px;font-weight:600;letter-spacing:-0.02em;margin-bottom:8px}
4314
- p{font-size:13px;color:var(--secondary);line-height:1.5}
4315
- .brand{margin-top:24px;font-size:11px;color:var(--tertiary);letter-spacing:0.04em;font-weight:500}
4303
+ *{box-sizing:border-box}
4304
+ :root{color-scheme:dark light;--bg:oklch(13% 0.018 180);--text:oklch(91% 0.012 125);--muted:oklch(58% 0.018 160);--accent:oklch(90% 0.23 125);--err:oklch(64% 0.2 25)}
4305
+ @media(prefers-color-scheme:light){:root{--bg:oklch(97% 0.014 125);--text:oklch(18% 0.016 135);--muted:oklch(48% 0.018 155)}}
4306
+ body{margin:0;min-height:100vh;background:var(--bg);color:var(--text);font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;display:grid;place-items:center;padding:32px;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
4307
+ main{width:min(100%,340px)}
4308
+ h1{font-size:21px;font-weight:560;line-height:1.25;letter-spacing:-.02em;margin:0}
4309
+ p{font-size:15px;line-height:1.5;color:var(--muted);margin:9px 0 0}
4310
+ .brand{font-family:'SF Mono','JetBrains Mono','Fira Code',ui-monospace,monospace;font-size:12px;line-height:1;color:var(--accent);margin-top:42px;letter-spacing:.02em}
4316
4311
  </style>
4317
4312
  </head>
4318
4313
  <body>
4319
- <div class="card">
4320
- <div class="icon">${icon}</div>
4321
- <h1>${title}</h1>
4314
+ <main aria-labelledby="callback-title">
4315
+ <h1 id="callback-title">${title}</h1>
4322
4316
  <p>${message}</p>
4323
- <div class="brand">AGENDEX</div>
4324
- </div>
4317
+ <div class="brand">agendex</div>
4318
+ </main>
4325
4319
  </body>
4326
4320
  </html>`;
4327
4321
  }
@@ -4642,6 +4636,32 @@ var RESTART_DELAY_MS = 5000;
4642
4636
  var PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS = 15000;
4643
4637
  var PLANNOTATOR_WRITEBACK_EXPIRED_ERROR = "Write-back expired before delivery.";
4644
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
+ }
4645
4665
  async function runWorker() {
4646
4666
  const config = await loadOrInitConfig();
4647
4667
  const hostname2 = osHostname2();
@@ -4651,6 +4671,7 @@ async function runWorker() {
4651
4671
  const syncCache = loadSyncCache();
4652
4672
  const syncQueue = [];
4653
4673
  const pendingWritebackReports = loadPendingWritebackReports();
4674
+ const liveSessions = new Map;
4654
4675
  let syncing = false;
4655
4676
  let cachedIpAddress;
4656
4677
  async function getSyncIpAddress() {
@@ -4726,6 +4747,31 @@ async function runWorker() {
4726
4747
  if (syncQueue.length > 0)
4727
4748
  processSyncQueue();
4728
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
+ }
4729
4775
  function persistPendingWritebackReports() {
4730
4776
  if (!savePendingWritebackReports(pendingWritebackReports)) {
4731
4777
  console.error("[agendex] failed to persist Plannotator write-back delivery cache");
@@ -4843,6 +4889,7 @@ async function runWorker() {
4843
4889
  const lowValueSuffix = lowValuePlanCount > 0 ? `, ${lowValuePlanCount} low-value hidden/pruned` : "";
4844
4890
  console.log(`[agendex] syncing ${initialQueuedSyncable} plans${lowValueSuffix} (${initialQueuedLowValue} low-value queued, ${initialSkipped} unchanged)...`);
4845
4891
  await processSyncQueue();
4892
+ await reconcileLivePlannotatorSessions(getAll());
4846
4893
  setInterval(() => {
4847
4894
  (async () => {
4848
4895
  await sendHeartbeat(await getSyncIpAddress());
@@ -4851,6 +4898,14 @@ async function runWorker() {
4851
4898
  if (shouldEnablePlannotatorSync(config)) {
4852
4899
  setInterval(() => void pollPlannotatorWritebacks(), PLANNOTATOR_WRITEBACK_POLL_INTERVAL_MS);
4853
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);
4854
4909
  }
4855
4910
  startWatching((changedPlans) => {
4856
4911
  (async () => {
@@ -4859,6 +4914,7 @@ async function runWorker() {
4859
4914
  syncQueue.push(planToSyncPayload(plan, config.deviceId, hostname2, ipAddress));
4860
4915
  }
4861
4916
  processSyncQueue();
4917
+ await reconcileLivePlannotatorSessions(getAll());
4862
4918
  })().catch((err) => {
4863
4919
  console.error("[agendex] failed to queue changed plans:", err);
4864
4920
  });
@@ -4921,6 +4977,379 @@ async function startSupervisor() {
4921
4977
  removePid();
4922
4978
  }
4923
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
+
4924
5353
  // src/sync.ts
4925
5354
  import { hostname as osHostname3 } from "node:os";
4926
5355
  async function syncAll(force = false) {
@@ -4979,17 +5408,17 @@ async function syncAll(force = false) {
4979
5408
  // src/upgrade.ts
4980
5409
  import { spawn as spawn3, spawnSync } from "node:child_process";
4981
5410
  import { realpathSync as realpathSync2 } from "node:fs";
4982
- 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";
4983
5412
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4984
5413
 
4985
5414
  // src/version.ts
4986
- 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";
4987
5416
  import { tmpdir } from "node:os";
4988
- import { join as join14 } from "node:path";
5417
+ import { join as join15 } from "node:path";
4989
5418
  // package.json
4990
5419
  var package_default = {
4991
5420
  name: "agendex-cli",
4992
- version: "0.17.0",
5421
+ version: "1.0.0",
4993
5422
  description: "Agendex CLI for login, sync, and daemon workflows",
4994
5423
  homepage: "https://github.com/Tyru5/Agendex#readme",
4995
5424
  bugs: {
@@ -5033,14 +5462,14 @@ var package_default = {
5033
5462
 
5034
5463
  // src/version.ts
5035
5464
  var CLI_VERSION = package_default.version;
5036
- 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");
5037
5466
  var CACHE_TTL_MS = 24 * 60 * 60 * 1000;
5038
5467
  var UPDATE_URL = process.env.AGENDEX_UPDATE_URL ?? "https://registry.npmjs.org/agendex-cli/latest";
5039
5468
  function readCache(current) {
5040
5469
  try {
5041
- if (!existsSync10(CACHE_FILE))
5470
+ if (!existsSync11(CACHE_FILE))
5042
5471
  return null;
5043
- const { result, ts } = JSON.parse(readFileSync7(CACHE_FILE, "utf8"));
5472
+ const { result, ts } = JSON.parse(readFileSync8(CACHE_FILE, "utf8"));
5044
5473
  if (Date.now() - ts > CACHE_TTL_MS)
5045
5474
  return null;
5046
5475
  return normalizeResult(result, current);
@@ -5109,12 +5538,12 @@ function isNewer(latest, current) {
5109
5538
 
5110
5539
  // src/upgrade.ts
5111
5540
  var PACKAGE_NAME = "agendex-cli";
5112
- var moduleDir = dirname4(fileURLToPath2(import.meta.url));
5541
+ var moduleDir = dirname5(fileURLToPath2(import.meta.url));
5113
5542
  function getPackageRoot() {
5114
5543
  try {
5115
- return realpathSync2(resolve7(moduleDir, ".."));
5544
+ return realpathSync2(resolve8(moduleDir, ".."));
5116
5545
  } catch {
5117
- return resolve7(moduleDir, "..");
5546
+ return resolve8(moduleDir, "..");
5118
5547
  }
5119
5548
  }
5120
5549
  function detectPackageManager(packageRoot) {
@@ -5333,7 +5762,7 @@ function firstCommandToken(argv) {
5333
5762
  return;
5334
5763
  }
5335
5764
  var command = firstCommandToken(args) ?? "start";
5336
- var cliEntry = resolve8(process.argv[1] ?? fileURLToPath3(import.meta.url));
5765
+ var cliEntry = resolve9(process.argv[1] ?? fileURLToPath3(import.meta.url));
5337
5766
  async function main() {
5338
5767
  const isInternal = args.includes("--daemon") || args.includes("--worker");
5339
5768
  if (command === "--version" || command === "-v") {
@@ -5348,6 +5777,8 @@ async function main() {
5348
5777
  "open",
5349
5778
  "view",
5350
5779
  "cleanup",
5780
+ "hooks",
5781
+ "review-plan",
5351
5782
  "add-dir",
5352
5783
  "remove-dir",
5353
5784
  "list-dirs",
@@ -5451,6 +5882,12 @@ async function main() {
5451
5882
  await syncAll(force);
5452
5883
  return 0;
5453
5884
  }
5885
+ case "hooks": {
5886
+ return await runHooksCommand(args, cliEntry);
5887
+ }
5888
+ case "review-plan": {
5889
+ return await runHookReviewCommand(args);
5890
+ }
5454
5891
  case "cleanup": {
5455
5892
  const config = loadConfig();
5456
5893
  if (!config?.cloudToken || !config?.convexUrl) {
@@ -5533,7 +5970,7 @@ async function main() {
5533
5970
  return 1;
5534
5971
  }
5535
5972
  const resolved = resolveCustomPlanDirPath(dirPath);
5536
- if (!existsSync11(resolved)) {
5973
+ if (!existsSync12(resolved)) {
5537
5974
  writeStderr(`[agendex] path does not exist: ${resolved}`);
5538
5975
  return 1;
5539
5976
  }
@@ -5552,7 +5989,7 @@ async function main() {
5552
5989
  const { request } = await import("node:http");
5553
5990
  const body = JSON.stringify({ path: resolved });
5554
5991
  try {
5555
- const res = await new Promise((resolve9, reject) => {
5992
+ const res = await new Promise((resolve10, reject) => {
5556
5993
  const req = request(`http://localhost:${port}/api/v1/plan-sources`, {
5557
5994
  method: "POST",
5558
5995
  headers: {
@@ -5566,7 +6003,7 @@ async function main() {
5566
6003
  res2.on("data", (chunk) => {
5567
6004
  data += chunk;
5568
6005
  });
5569
- res2.on("end", () => resolve9({ status: res2.statusCode ?? 0, body: data }));
6006
+ res2.on("end", () => resolve10({ status: res2.statusCode ?? 0, body: data }));
5570
6007
  res2.on("error", reject);
5571
6008
  });
5572
6009
  req.on("error", reject);
@@ -5716,6 +6153,10 @@ Usage:
5716
6153
  agendex view <url> Open a shared plan URL in your browser
5717
6154
  agendex logout Clear stored cloud token
5718
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
5719
6160
  agendex add-dir <path> Add a custom directory to scan for plans
5720
6161
  agendex add-dir <path> --live Add dir and notify running server immediately
5721
6162
  agendex remove-dir <path> Remove a custom directory
@@ -5755,13 +6196,13 @@ function flushStream(stream) {
5755
6196
  if (stream.destroyed || !stream.writable) {
5756
6197
  return Promise.resolve();
5757
6198
  }
5758
- return new Promise((resolve9, reject) => {
6199
+ return new Promise((resolve10, reject) => {
5759
6200
  stream.write("", (error) => {
5760
6201
  if (error) {
5761
6202
  reject(error);
5762
6203
  return;
5763
6204
  }
5764
- resolve9();
6205
+ resolve10();
5765
6206
  });
5766
6207
  });
5767
6208
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agendex-cli",
3
- "version": "0.17.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": {