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.
- package/dist/cli.js +490 -49
- 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
|
|
1063
|
-
import { resolve as
|
|
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 ? "
|
|
4294
|
-
const message = success ? "
|
|
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}
|
|
4301
|
+
<title>${title} | Agendex</title>
|
|
4302
4302
|
<style>
|
|
4303
|
-
*{
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
}
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
}
|
|
4310
|
-
|
|
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
|
-
<
|
|
4320
|
-
<
|
|
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">
|
|
4324
|
-
</
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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 ??
|
|
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 (!
|
|
5470
|
+
if (!existsSync11(CACHE_FILE))
|
|
5042
5471
|
return null;
|
|
5043
|
-
const { result, ts } = JSON.parse(
|
|
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 =
|
|
5541
|
+
var moduleDir = dirname5(fileURLToPath2(import.meta.url));
|
|
5113
5542
|
function getPackageRoot() {
|
|
5114
5543
|
try {
|
|
5115
|
-
return realpathSync2(
|
|
5544
|
+
return realpathSync2(resolve8(moduleDir, ".."));
|
|
5116
5545
|
} catch {
|
|
5117
|
-
return
|
|
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 =
|
|
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 (!
|
|
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((
|
|
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", () =>
|
|
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((
|
|
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
|
-
|
|
6205
|
+
resolve10();
|
|
5765
6206
|
});
|
|
5766
6207
|
});
|
|
5767
6208
|
}
|