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