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