@dadado/agent-kit-cli 5.15.1 → 5.17.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/dashboard/dashboard-data.mjs +69 -65
- package/dashboard/lib/guards.d.mts +5 -0
- package/dashboard/lib/guards.mjs +31 -0
- package/dashboard/lib/identity-probe.mjs +56 -0
- package/dashboard/lib/pipeline-cache.mjs +58 -0
- package/dashboard/lib/processes.mjs +71 -0
- package/dashboard/serve.mjs +43 -1
- package/dashboard/start-broadcast.mjs +2 -12
- package/dashboard/start.mjs +2 -13
- package/dist/add-HTYIFKMN.js +150 -0
- package/dist/chunk-2IPXQFJE.js +253 -0
- package/dist/chunk-2RDRMGD3.js +82 -0
- package/dist/chunk-2YU2H7I6.js +42 -0
- package/dist/chunk-4IWG5CVE.js +214 -0
- package/dist/chunk-5VCOWTTP.js +6 -0
- package/dist/chunk-77V75A7O.js +39 -0
- package/dist/chunk-7HDQEK6T.js +26 -0
- package/dist/chunk-AW74INUW.js +1456 -0
- package/dist/chunk-B73KQ5KX.js +972 -0
- package/dist/chunk-CBCW5R5J.js +484 -0
- package/dist/chunk-DE3LQFSJ.js +18 -0
- package/dist/chunk-DQA434Y4.js +106 -0
- package/dist/chunk-F4VMYR7U.js +318 -0
- package/dist/chunk-F6C3GFKV.js +57 -0
- package/dist/chunk-HFUH3HVS.js +282 -0
- package/dist/chunk-HS7LVVLO.js +74 -0
- package/dist/chunk-IHROBTNM.js +135 -0
- package/dist/chunk-KD2MYBB6.js +213 -0
- package/dist/chunk-MAPOQEA2.js +43 -0
- package/dist/chunk-N3HTJVBR.js +341 -0
- package/dist/chunk-NQ7IQ5XT.js +26 -0
- package/dist/chunk-OUQZC744.js +1315 -0
- package/dist/chunk-PCCQUWE5.js +817 -0
- package/dist/chunk-PNFF6OU3.js +273 -0
- package/dist/chunk-PNG7LO2F.js +53 -0
- package/dist/chunk-QTON2GQX.js +203 -0
- package/dist/chunk-S6GMFDKV.js +50 -0
- package/dist/chunk-SMGFN3Z4.js +503 -0
- package/dist/chunk-STGFBAYT.js +641 -0
- package/dist/chunk-TFZ2JUXH.js +1081 -0
- package/dist/chunk-VY7MTPJ2.js +205 -0
- package/dist/chunk-W24CMQ32.js +525 -0
- package/dist/chunk-WW3YZKHW.js +165 -0
- package/dist/chunk-ZB43YMHW.js +20 -0
- package/dist/chunk-ZCTSUQRR.js +874 -0
- package/dist/chunk-ZXKZBTXE.js +17 -0
- package/dist/contribute-C6AW2P6J.js +367 -0
- package/dist/cursor-awareness-KTL2RGKD.js +77 -0
- package/dist/dashboard-YSNUGBFY.js +21 -0
- package/dist/dashboard-broadcast-M4CMW7RX.js +119 -0
- package/dist/diff-VHNR62SI.js +74 -0
- package/dist/doctor-PET2HRRJ.js +236 -0
- package/dist/dogfood-KKFVRWT6.js +366 -0
- package/dist/guard-EHXF76ID.js +530 -0
- package/dist/handoff-3UI6VKVG.js +302 -0
- package/dist/hook-DYIMKJYL.js +1074 -0
- package/dist/index.js +62 -14068
- package/dist/init-Q4LBM6KT.js +127 -0
- package/dist/install-RHOS3OYZ.js +34 -0
- package/dist/mission-control-LJACEVVN.js +89 -0
- package/dist/monitors-QV2IS3MP.js +176 -0
- package/dist/plan-index-JMC7Z4DW.js +44 -0
- package/dist/public-inbound-radar-WP3XF7GS.js +68 -0
- package/dist/remote-issues-SZ4HDQL5.js +44 -0
- package/dist/run-I3BFGULT.js +243 -0
- package/dist/run-plan-FO7CL4M3.js +118 -0
- package/dist/scan-NI2SWJ2E.js +33 -0
- package/dist/setup-global-M42GEODI.js +340 -0
- package/dist/status-XPVQVQMX.js +150 -0
- package/dist/update-FJ2EBZVO.js +283 -0
- package/dist/validate-V2CW7J4B.js +201 -0
- package/package.json +3 -3
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INTERACTIVE_DENY_RULES
|
|
3
|
+
} from "./chunk-MAPOQEA2.js";
|
|
4
|
+
import {
|
|
5
|
+
assertRealContained,
|
|
6
|
+
assertRealParentContained,
|
|
7
|
+
contentHash,
|
|
8
|
+
isRealWriteTargetContained,
|
|
9
|
+
loadManagedHashLedger,
|
|
10
|
+
resolveContained,
|
|
11
|
+
saveManagedHashLedger,
|
|
12
|
+
shouldPreserveCustomizedOverlay
|
|
13
|
+
} from "./chunk-AW74INUW.js";
|
|
14
|
+
import {
|
|
15
|
+
ensureDir,
|
|
16
|
+
fileExists
|
|
17
|
+
} from "./chunk-77V75A7O.js";
|
|
18
|
+
|
|
19
|
+
// src/generator/claude-command-adapters.ts
|
|
20
|
+
import { readFile, readdir, writeFile } from "fs/promises";
|
|
21
|
+
import path from "path";
|
|
22
|
+
var CURSOR_COMMANDS_DIR_REL = ".cursor/commands";
|
|
23
|
+
var CLAUDE_COMMANDS_DIR_REL = ".claude/commands";
|
|
24
|
+
var RESERVED_ADAPTER_NAMES = /* @__PURE__ */ new Set(["agent-kit"]);
|
|
25
|
+
function parseCommandFrontmatter(name, raw) {
|
|
26
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
const body = match[1] ?? "";
|
|
29
|
+
const descMatch = /^description:\s*(.+)$/m.exec(body);
|
|
30
|
+
if (!descMatch) return null;
|
|
31
|
+
const description = (descMatch[1] ?? "").trim();
|
|
32
|
+
if (!description) return null;
|
|
33
|
+
return { name, description };
|
|
34
|
+
}
|
|
35
|
+
var CLAUDE_HITL_FALLBACK = `Cursor "Ask questions" is unavailable here: use AskUserQuestion when possible, else print one line \`HITL_GATE: <ask-id> | <label 1> | <label 2> | ...\` immediately followed by the same labels as one numbered list, one list per message, and WAIT for the answer (ask-id and labels: \`.cursor/skills/core/hitl-gates/SKILL.md\`). A headless reply arrives as \`HITL_REPLY: <ask-id> | operator reply <n> | <label>\`; cite that line as the Ask provenance.`;
|
|
36
|
+
var MODEL_INVOCATION_DISABLED = /* @__PURE__ */ new Set([
|
|
37
|
+
"git-prod",
|
|
38
|
+
"kit-prod",
|
|
39
|
+
"backlog-delete",
|
|
40
|
+
"backlog-cancel",
|
|
41
|
+
"archive-plan",
|
|
42
|
+
"hotfix",
|
|
43
|
+
"run-plan-all"
|
|
44
|
+
]);
|
|
45
|
+
function renderClaudeCommandAdapter(command) {
|
|
46
|
+
const invocation = MODEL_INVOCATION_DISABLED.has(command.name) ? "disable-model-invocation: true\n" : "";
|
|
47
|
+
return `---
|
|
48
|
+
description: ${command.description}
|
|
49
|
+
${invocation}---
|
|
50
|
+
|
|
51
|
+
Read \`.cursor/commands/${command.name}.md\` now and follow that contract exactly \u2014 it is the source of truth for /${command.name}; this file is only a thin adapter for Claude Code.
|
|
52
|
+
|
|
53
|
+
Adapter rules (Claude Code CLI):
|
|
54
|
+
- ${CLAUDE_HITL_FALLBACK}
|
|
55
|
+
- Skip or cancel means stop.
|
|
56
|
+
- Never \`/git-prod\` without an explicit operator yes.
|
|
57
|
+
- Do not clone Cursor hooks or invent behavior beyond the SoT file.
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
async function discoverInstalledCommands(rootDir) {
|
|
61
|
+
const dir = path.join(rootDir, CURSOR_COMMANDS_DIR_REL);
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = (await readdir(dir)).filter((f) => f.endsWith(".md"));
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const commands = [];
|
|
69
|
+
for (const file of entries.sort()) {
|
|
70
|
+
const name = file.slice(0, -3);
|
|
71
|
+
if (RESERVED_ADAPTER_NAMES.has(name)) continue;
|
|
72
|
+
try {
|
|
73
|
+
const raw = await readFile(path.join(dir, file), "utf8");
|
|
74
|
+
const parsed = parseCommandFrontmatter(name, raw);
|
|
75
|
+
if (parsed) commands.push(parsed);
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return commands;
|
|
80
|
+
}
|
|
81
|
+
async function generateClaudeCommandAdapters(rootDir) {
|
|
82
|
+
const commands = await discoverInstalledCommands(rootDir);
|
|
83
|
+
if (commands.length === 0) return [];
|
|
84
|
+
const ledger = await loadManagedHashLedger(rootDir);
|
|
85
|
+
const results = [];
|
|
86
|
+
let ledgerDirty = false;
|
|
87
|
+
for (const command of commands) {
|
|
88
|
+
const relPath = path.posix.join(CLAUDE_COMMANDS_DIR_REL, `${command.name}.md`);
|
|
89
|
+
const rendered = renderClaudeCommandAdapter(command);
|
|
90
|
+
const abs = resolveContained(rootDir, relPath);
|
|
91
|
+
if (!await isRealWriteTargetContained(rootDir, abs)) {
|
|
92
|
+
results.push({ relativePath: relPath, status: "skipped-symlink" });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!await fileExists(abs)) {
|
|
96
|
+
await ensureDir(path.dirname(abs));
|
|
97
|
+
await writeFile(abs, rendered, "utf8");
|
|
98
|
+
ledger.hashes[relPath] = contentHash(rendered);
|
|
99
|
+
ledgerDirty = true;
|
|
100
|
+
results.push({ relativePath: relPath, status: "applied" });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const localContent = await readFile(abs, "utf8");
|
|
104
|
+
if (localContent === rendered) {
|
|
105
|
+
if (ledger.hashes[relPath] !== contentHash(rendered)) {
|
|
106
|
+
ledger.hashes[relPath] = contentHash(rendered);
|
|
107
|
+
ledgerDirty = true;
|
|
108
|
+
}
|
|
109
|
+
results.push({ relativePath: relPath, status: "unchanged" });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (shouldPreserveCustomizedOverlay(localContent, ledger.hashes[relPath])) {
|
|
113
|
+
results.push({ relativePath: relPath, status: "preserved-customized" });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
await writeFile(abs, rendered, "utf8");
|
|
117
|
+
ledger.hashes[relPath] = contentHash(rendered);
|
|
118
|
+
ledgerDirty = true;
|
|
119
|
+
results.push({ relativePath: relPath, status: "refreshed" });
|
|
120
|
+
}
|
|
121
|
+
if (ledgerDirty) await saveManagedHashLedger(rootDir, ledger);
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/generator/claude-session-start-hook.ts
|
|
126
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
127
|
+
import path3 from "path";
|
|
128
|
+
|
|
129
|
+
// src/generator/claude-settings-ledger.ts
|
|
130
|
+
import { lstat, mkdir, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
131
|
+
import path2 from "path";
|
|
132
|
+
var CLAUDE_SETTINGS_LEDGER_REL = ".cursor/agent-kit.claude-settings.json";
|
|
133
|
+
var ClaudeSettingsLedgerSymlinkError = class extends Error {
|
|
134
|
+
constructor(path4, detail) {
|
|
135
|
+
super(`Refusing to use Claude settings ledger ${path4}: ${detail}`);
|
|
136
|
+
this.path = path4;
|
|
137
|
+
this.name = "ClaudeSettingsLedgerSymlinkError";
|
|
138
|
+
}
|
|
139
|
+
path;
|
|
140
|
+
};
|
|
141
|
+
var ClaudeSettingsLedgerInvalidError = class extends Error {
|
|
142
|
+
constructor(path4) {
|
|
143
|
+
super(
|
|
144
|
+
`Claude settings ledger ${path4} is malformed or has an unknown schemaVersion; not overwriting without authorization.`
|
|
145
|
+
);
|
|
146
|
+
this.path = path4;
|
|
147
|
+
this.name = "ClaudeSettingsLedgerInvalidError";
|
|
148
|
+
}
|
|
149
|
+
path;
|
|
150
|
+
};
|
|
151
|
+
function emptyLedger() {
|
|
152
|
+
return { schemaVersion: 1, denyRowsWritten: [], hookEntriesWritten: [] };
|
|
153
|
+
}
|
|
154
|
+
function hookKey(r) {
|
|
155
|
+
return `${r.event}\0${r.marker}`;
|
|
156
|
+
}
|
|
157
|
+
function normalize(denyRows, hooks) {
|
|
158
|
+
const seen = /* @__PURE__ */ new Map();
|
|
159
|
+
for (const h of hooks) seen.set(hookKey(h), { event: h.event, marker: h.marker });
|
|
160
|
+
return {
|
|
161
|
+
schemaVersion: 1,
|
|
162
|
+
denyRowsWritten: [...new Set(denyRows)].sort(),
|
|
163
|
+
hookEntriesWritten: [...seen.values()].sort(
|
|
164
|
+
(a, b) => a.event.localeCompare(b.event) || a.marker.localeCompare(b.marker)
|
|
165
|
+
)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function parseLedger(raw) {
|
|
169
|
+
let data;
|
|
170
|
+
try {
|
|
171
|
+
data = JSON.parse(raw);
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return null;
|
|
176
|
+
const o = data;
|
|
177
|
+
if (o.schemaVersion !== 1) return null;
|
|
178
|
+
if (!Array.isArray(o.denyRowsWritten) || !o.denyRowsWritten.every((r) => typeof r === "string")) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
if (!Array.isArray(o.hookEntriesWritten)) return null;
|
|
182
|
+
const hooks = [];
|
|
183
|
+
for (const h of o.hookEntriesWritten) {
|
|
184
|
+
if (typeof h !== "object" || h === null || typeof h.event !== "string" || typeof h.marker !== "string") {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
hooks.push({
|
|
188
|
+
event: h.event,
|
|
189
|
+
marker: h.marker
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return normalize(o.denyRowsWritten, hooks);
|
|
193
|
+
}
|
|
194
|
+
async function readState(root) {
|
|
195
|
+
const abs = resolveContained(root, CLAUDE_SETTINGS_LEDGER_REL);
|
|
196
|
+
let exists = true;
|
|
197
|
+
try {
|
|
198
|
+
await lstat(abs);
|
|
199
|
+
} catch {
|
|
200
|
+
exists = false;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
if (exists) await assertRealContained(root, abs);
|
|
204
|
+
else await assertRealParentContained(root, abs);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
throw new ClaudeSettingsLedgerSymlinkError(
|
|
207
|
+
abs,
|
|
208
|
+
err instanceof Error ? err.message : String(err)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (!exists) return { abs, state: { kind: "missing" } };
|
|
212
|
+
const ledger = parseLedger(await readFile2(abs, "utf8"));
|
|
213
|
+
return { abs, state: ledger ? { kind: "ok", ledger } : { kind: "invalid" } };
|
|
214
|
+
}
|
|
215
|
+
async function loadClaudeSettingsLedger(root) {
|
|
216
|
+
const { state } = await readState(root);
|
|
217
|
+
return state.kind === "ok" ? state.ledger : null;
|
|
218
|
+
}
|
|
219
|
+
async function saveClaudeSettingsLedger(root, additions, opts = {}) {
|
|
220
|
+
const { abs, state } = await readState(root);
|
|
221
|
+
if (state.kind === "invalid" && !opts.overwriteInvalid) {
|
|
222
|
+
throw new ClaudeSettingsLedgerInvalidError(abs);
|
|
223
|
+
}
|
|
224
|
+
const base = state.kind === "ok" ? state.ledger : emptyLedger();
|
|
225
|
+
const merged = normalize(
|
|
226
|
+
[...base.denyRowsWritten, ...additions.denyRowsWritten ?? []],
|
|
227
|
+
[...base.hookEntriesWritten, ...additions.hookEntriesWritten ?? []]
|
|
228
|
+
);
|
|
229
|
+
await mkdir(path2.dirname(abs), { recursive: true });
|
|
230
|
+
await writeFile2(abs, `${JSON.stringify(merged, null, 2)}
|
|
231
|
+
`, "utf8");
|
|
232
|
+
return merged;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/generator/claude-session-start-hook.ts
|
|
236
|
+
var CLAUDE_SETTINGS_REL = ".claude/settings.json";
|
|
237
|
+
var RESOLVE_AGENT_KIT_REL = ".cursor/hooks/agent/resolve-agent-kit.sh";
|
|
238
|
+
var SESSION_START_HOOK_MARKER = "hook session-start --format claude";
|
|
239
|
+
var SESSION_START_DEGRADED_TEXT = "Agent Kit hooks are running in degraded fail-open mode: the agent-kit CLI could not be resolved (checked AGENT_KIT_HOOK_BIN, node_modules/.bin/agent-kit, PATH), or it resolved to a node script and node is not on PATH. Slash command adapters still work; session-context injection is inactive. Fix: install the CLI (npm i -D @dadado/agent-kit-cli), put node on PATH, or set AGENT_KIT_HOOK_BIN.";
|
|
240
|
+
function buildSessionStartHookCommand() {
|
|
241
|
+
const run = `{ if command -v run_agent_kit >/dev/null 2>&1; then run_agent_kit ${SESSION_START_HOOK_MARKER}; else exec $AGENT_KIT_RESOLVED ${SESSION_START_HOOK_MARKER}; fi; }`;
|
|
242
|
+
return `_AGENT_KIT_HOOK_ROOT="\${CLAUDE_PROJECT_DIR}"; . "\${CLAUDE_PROJECT_DIR}/${RESOLVE_AGENT_KIT_REL}" 2>/dev/null && resolve_agent_kit && ${run}; printf '%s' ${shellSingleQuote(SESSION_START_DEGRADED_TEXT)}`;
|
|
243
|
+
}
|
|
244
|
+
var GUARD_SHELL_HOOK_MARKER = "guard shell --format claude";
|
|
245
|
+
function buildGuardShellHookCommand() {
|
|
246
|
+
const run = `{ if command -v run_agent_kit >/dev/null 2>&1; then run_agent_kit ${GUARD_SHELL_HOOK_MARKER}; else exec $AGENT_KIT_RESOLVED ${GUARD_SHELL_HOOK_MARKER}; fi; }`;
|
|
247
|
+
return `_AGENT_KIT_HOOK_ROOT="\${CLAUDE_PROJECT_DIR}"; . "\${CLAUDE_PROJECT_DIR}/${RESOLVE_AGENT_KIT_REL}" 2>/dev/null && resolve_agent_kit && ${run}; :`;
|
|
248
|
+
}
|
|
249
|
+
function buildGuardShellHookEntry() {
|
|
250
|
+
return { type: "command", command: buildGuardShellHookCommand(), timeout: 10 };
|
|
251
|
+
}
|
|
252
|
+
function shellSingleQuote(text) {
|
|
253
|
+
return `'${text.replace(/'/g, "'\\''")}'`;
|
|
254
|
+
}
|
|
255
|
+
function buildSessionStartHookEntry() {
|
|
256
|
+
return {
|
|
257
|
+
type: "command",
|
|
258
|
+
command: buildSessionStartHookCommand(),
|
|
259
|
+
timeout: 15,
|
|
260
|
+
statusMessage: "Loading Agent Kit session context"
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function isPlainObject(value) {
|
|
264
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
265
|
+
}
|
|
266
|
+
function groupHasMarker(group, marker) {
|
|
267
|
+
if (!isPlainObject(group) || !Array.isArray(group.hooks)) return false;
|
|
268
|
+
return group.hooks.some(
|
|
269
|
+
(h) => isPlainObject(h) && typeof h.command === "string" && h.command.includes(marker)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
function upsertMarkedGroup(arr, group, marker) {
|
|
273
|
+
const existingIndex = arr.findIndex((g) => groupHasMarker(g, marker));
|
|
274
|
+
if (existingIndex === -1) {
|
|
275
|
+
arr.push(group);
|
|
276
|
+
return "applied";
|
|
277
|
+
}
|
|
278
|
+
if (JSON.stringify(arr[existingIndex]) === JSON.stringify(group)) return "unchanged";
|
|
279
|
+
arr[existingIndex] = group;
|
|
280
|
+
return "refreshed";
|
|
281
|
+
}
|
|
282
|
+
var SESSION_START_EVENT = "SessionStart";
|
|
283
|
+
var PRE_TOOL_USE_EVENT = "PreToolUse";
|
|
284
|
+
function mergeSessionStartHookIntoSettings(existingRaw, opts = {}) {
|
|
285
|
+
const ledger = opts.ledger ?? null;
|
|
286
|
+
const authorized = opts.authorized ?? true;
|
|
287
|
+
const entry = buildSessionStartHookEntry();
|
|
288
|
+
const newGroup = { hooks: [entry] };
|
|
289
|
+
let root = {};
|
|
290
|
+
if (existingRaw !== null && existingRaw.trim() !== "") {
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(existingRaw);
|
|
293
|
+
if (!isPlainObject(parsed)) throw new Error("root is not an object");
|
|
294
|
+
root = parsed;
|
|
295
|
+
} catch {
|
|
296
|
+
return {
|
|
297
|
+
content: null,
|
|
298
|
+
status: "unavailable",
|
|
299
|
+
instructions: instructionsBlock(entry)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const pending = { denyRows: [], hookEntries: [] };
|
|
304
|
+
const additions = { denyRowsWritten: [], hookEntriesWritten: [] };
|
|
305
|
+
const hooks = isPlainObject(root.hooks) ? { ...root.hooks } : {};
|
|
306
|
+
const sessionStart = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
|
|
307
|
+
const preToolUse = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
|
|
308
|
+
const upserts = [
|
|
309
|
+
upsertLedgerAwareGroup({
|
|
310
|
+
arr: sessionStart,
|
|
311
|
+
group: newGroup,
|
|
312
|
+
event: SESSION_START_EVENT,
|
|
313
|
+
marker: SESSION_START_HOOK_MARKER
|
|
314
|
+
}),
|
|
315
|
+
upsertLedgerAwareGroup({
|
|
316
|
+
arr: preToolUse,
|
|
317
|
+
group: { matcher: "Bash", hooks: [buildGuardShellHookEntry()] },
|
|
318
|
+
event: PRE_TOOL_USE_EVENT,
|
|
319
|
+
marker: GUARD_SHELL_HOOK_MARKER
|
|
320
|
+
})
|
|
321
|
+
];
|
|
322
|
+
hooks.SessionStart = sessionStart;
|
|
323
|
+
hooks.PreToolUse = preToolUse;
|
|
324
|
+
root.hooks = hooks;
|
|
325
|
+
function upsertLedgerAwareGroup(g) {
|
|
326
|
+
const record = { event: g.event, marker: g.marker };
|
|
327
|
+
if (g.arr.some((x) => groupHasMarker(x, g.marker))) {
|
|
328
|
+
additions.hookEntriesWritten.push(record);
|
|
329
|
+
return upsertMarkedGroup(g.arr, g.group, g.marker);
|
|
330
|
+
}
|
|
331
|
+
if (ledger?.hookEntriesWritten.some((h) => h.event === g.event && h.marker === g.marker)) {
|
|
332
|
+
return "unchanged";
|
|
333
|
+
}
|
|
334
|
+
if (ledger === null && !authorized) {
|
|
335
|
+
pending.hookEntries.push(record);
|
|
336
|
+
return "unchanged";
|
|
337
|
+
}
|
|
338
|
+
additions.hookEntriesWritten.push(record);
|
|
339
|
+
return upsertMarkedGroup(g.arr, g.group, g.marker);
|
|
340
|
+
}
|
|
341
|
+
const status = upserts.reduce(
|
|
342
|
+
(acc, next, i) => i === 0 ? next : combineStatus(acc, next),
|
|
343
|
+
"unchanged"
|
|
344
|
+
);
|
|
345
|
+
const denyAdded = mergeKitDenyRules(root, { ledger, authorized }, pending, additions);
|
|
346
|
+
const finalStatus = denyAdded && status === "unchanged" ? "refreshed" : status;
|
|
347
|
+
return {
|
|
348
|
+
content: `${JSON.stringify(root, null, 2)}
|
|
349
|
+
`,
|
|
350
|
+
status: finalStatus,
|
|
351
|
+
pending,
|
|
352
|
+
ledgerAdditions: additions
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function combineStatus(a, b) {
|
|
356
|
+
return a === "unchanged" && b !== "unchanged" ? "refreshed" : a;
|
|
357
|
+
}
|
|
358
|
+
function mergeKitDenyRules(root, ctx, pending, additions) {
|
|
359
|
+
if (root.permissions !== void 0 && !isPlainObject(root.permissions)) return false;
|
|
360
|
+
const permissions = isPlainObject(root.permissions) ? { ...root.permissions } : {};
|
|
361
|
+
if (permissions.deny !== void 0 && !Array.isArray(permissions.deny)) return false;
|
|
362
|
+
const deny = Array.isArray(permissions.deny) ? [...permissions.deny] : [];
|
|
363
|
+
const written = new Set(ctx.ledger?.denyRowsWritten ?? []);
|
|
364
|
+
for (const rule of INTERACTIVE_DENY_RULES) {
|
|
365
|
+
if (deny.includes(rule)) additions.denyRowsWritten.push(rule);
|
|
366
|
+
}
|
|
367
|
+
const missing = INTERACTIVE_DENY_RULES.filter(
|
|
368
|
+
(rule) => !deny.includes(rule) && !written.has(rule)
|
|
369
|
+
);
|
|
370
|
+
const toAdd = ctx.ledger === null && !ctx.authorized ? [] : missing;
|
|
371
|
+
if (ctx.ledger === null && !ctx.authorized) pending.denyRows.push(...missing);
|
|
372
|
+
if (toAdd.length === 0) return false;
|
|
373
|
+
additions.denyRowsWritten.push(...toAdd);
|
|
374
|
+
permissions.deny = [...deny, ...toAdd];
|
|
375
|
+
root.permissions = permissions;
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
function instructionsBlock(entry) {
|
|
379
|
+
return [
|
|
380
|
+
`Could not parse the existing ${CLAUDE_SETTINGS_REL} as JSON, so Agent Kit did not touch it.`,
|
|
381
|
+
"Add these hooks by hand (create the arrays if they do not exist).",
|
|
382
|
+
"Under hooks.SessionStart:",
|
|
383
|
+
"",
|
|
384
|
+
JSON.stringify(entry, null, 2),
|
|
385
|
+
"",
|
|
386
|
+
'Under hooks.PreToolUse, in a group with "matcher": "Bash":',
|
|
387
|
+
"",
|
|
388
|
+
JSON.stringify(buildGuardShellHookEntry(), null, 2)
|
|
389
|
+
].join("\n");
|
|
390
|
+
}
|
|
391
|
+
async function writeClaudeSessionStartHook(rootDir, opts = {}) {
|
|
392
|
+
const authorized = opts.authorized ?? true;
|
|
393
|
+
const abs = resolveContained(rootDir, CLAUDE_SETTINGS_REL);
|
|
394
|
+
if (!await isRealWriteTargetContained(rootDir, abs)) {
|
|
395
|
+
return { relativePath: CLAUDE_SETTINGS_REL, status: "skipped-symlink" };
|
|
396
|
+
}
|
|
397
|
+
let ledger;
|
|
398
|
+
try {
|
|
399
|
+
ledger = await loadClaudeSettingsLedger(rootDir);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
if (err instanceof ClaudeSettingsLedgerSymlinkError) {
|
|
402
|
+
return { relativePath: CLAUDE_SETTINGS_REL, status: "skipped-symlink" };
|
|
403
|
+
}
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
let existing = null;
|
|
407
|
+
try {
|
|
408
|
+
existing = await readFile3(abs, "utf8");
|
|
409
|
+
} catch {
|
|
410
|
+
existing = null;
|
|
411
|
+
}
|
|
412
|
+
const merged = mergeSessionStartHookIntoSettings(existing, { ledger, authorized });
|
|
413
|
+
if (merged.status === "unavailable" || merged.content === null) {
|
|
414
|
+
return {
|
|
415
|
+
relativePath: CLAUDE_SETTINGS_REL,
|
|
416
|
+
status: "unavailable",
|
|
417
|
+
instructions: merged.instructions
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (merged.status !== "unchanged") {
|
|
421
|
+
await ensureDir(path3.dirname(abs));
|
|
422
|
+
await writeFile3(abs, merged.content, "utf8");
|
|
423
|
+
}
|
|
424
|
+
const result = {
|
|
425
|
+
relativePath: CLAUDE_SETTINGS_REL,
|
|
426
|
+
status: merged.status
|
|
427
|
+
};
|
|
428
|
+
if (merged.pending && (merged.pending.denyRows.length || merged.pending.hookEntries.length)) {
|
|
429
|
+
result.pending = merged.pending;
|
|
430
|
+
}
|
|
431
|
+
if (authorized && merged.ledgerAdditions && ledgerGrows(ledger, merged.ledgerAdditions)) {
|
|
432
|
+
await saveClaudeSettingsLedger(rootDir, merged.ledgerAdditions, { overwriteInvalid: true });
|
|
433
|
+
result.ledgerPath = CLAUDE_SETTINGS_LEDGER_REL;
|
|
434
|
+
}
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
function ledgerGrows(ledger, add) {
|
|
438
|
+
if (ledger === null) return true;
|
|
439
|
+
const rows = new Set(ledger.denyRowsWritten);
|
|
440
|
+
const hooks = new Set(ledger.hookEntriesWritten.map((h) => `${h.event}\0${h.marker}`));
|
|
441
|
+
return add.denyRowsWritten.some((r) => !rows.has(r)) || add.hookEntriesWritten.some((h) => !hooks.has(`${h.event}\0${h.marker}`));
|
|
442
|
+
}
|
|
443
|
+
async function planClaudeSettingsDrift(rootDir) {
|
|
444
|
+
const abs = resolveContained(rootDir, CLAUDE_SETTINGS_REL);
|
|
445
|
+
if (!await isRealWriteTargetContained(rootDir, abs)) return null;
|
|
446
|
+
let raw;
|
|
447
|
+
try {
|
|
448
|
+
raw = await readFile3(abs, "utf8");
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
let existing;
|
|
453
|
+
try {
|
|
454
|
+
const parsed = JSON.parse(raw);
|
|
455
|
+
if (!isPlainObject(parsed)) return null;
|
|
456
|
+
existing = parsed;
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const hooks = isPlainObject(existing.hooks) ? existing.hooks : {};
|
|
461
|
+
const groupsOf = (event) => Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
462
|
+
const kitEntries = [
|
|
463
|
+
{ event: SESSION_START_EVENT, marker: SESSION_START_HOOK_MARKER },
|
|
464
|
+
{ event: PRE_TOOL_USE_EVENT, marker: GUARD_SHELL_HOOK_MARKER }
|
|
465
|
+
];
|
|
466
|
+
const present = kitEntries.filter(
|
|
467
|
+
(e) => groupsOf(e.event).some((g) => groupHasMarker(g, e.marker))
|
|
468
|
+
);
|
|
469
|
+
if (present.length === 0) return null;
|
|
470
|
+
let ledger;
|
|
471
|
+
try {
|
|
472
|
+
ledger = await loadClaudeSettingsLedger(rootDir);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
if (err instanceof ClaudeSettingsLedgerSymlinkError) return null;
|
|
475
|
+
throw err;
|
|
476
|
+
}
|
|
477
|
+
const merged = mergeSessionStartHookIntoSettings(raw, { ledger, authorized: false });
|
|
478
|
+
if (merged.content === null) return null;
|
|
479
|
+
const next = JSON.parse(merged.content);
|
|
480
|
+
const deny = (r) => isPlainObject(r.permissions) && Array.isArray(r.permissions.deny) ? r.permissions.deny.filter((x) => typeof x === "string") : [];
|
|
481
|
+
const had = new Set(deny(existing));
|
|
482
|
+
const denyRows = deny(next).filter((r) => !had.has(r));
|
|
483
|
+
const ambiguousDenyRows = merged.pending?.denyRows ?? [];
|
|
484
|
+
const missingEntries = kitEntries.filter((e) => !present.includes(e));
|
|
485
|
+
const ledgerHas = (e) => Boolean(ledger?.hookEntriesWritten.some((h) => h.event === e.event && h.marker === e.marker));
|
|
486
|
+
const hookEntries = missingEntries.filter((e) => !ledgerHas(e));
|
|
487
|
+
if (denyRows.length === 0 && ambiguousDenyRows.length === 0 && hookEntries.length === 0) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
return { denyRows, ambiguousDenyRows, hookEntries };
|
|
491
|
+
}
|
|
492
|
+
function formatClaudeSettingsDrift(report, opts) {
|
|
493
|
+
const rows = [...report.denyRows, ...report.ambiguousDenyRows];
|
|
494
|
+
const lines = [
|
|
495
|
+
`Claude settings drift in ${CLAUDE_SETTINGS_REL}${opts.protectedPath ? " (protected (skipped))" : ""}: update did not write it.`
|
|
496
|
+
];
|
|
497
|
+
if (rows.length > 0) {
|
|
498
|
+
lines.push("Add to permissions.deny:", JSON.stringify(rows, null, 2));
|
|
499
|
+
if (report.ambiguousDenyRows.length > 0) {
|
|
500
|
+
lines.push(
|
|
501
|
+
`(${report.ambiguousDenyRows.length} row(s) ambiguous: no ${CLAUDE_SETTINGS_LEDGER_REL} ledger, so a deliberate deletion cannot be told apart.)`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (report.hookEntries.length > 0) {
|
|
506
|
+
lines.push("Missing hook entries (add by hand or use the command below):");
|
|
507
|
+
for (const e of report.hookEntries) lines.push(` ${e.event}: ${e.marker}`);
|
|
508
|
+
}
|
|
509
|
+
lines.push("Apply with: agent-kit update --claude");
|
|
510
|
+
return lines;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export {
|
|
514
|
+
CLAUDE_COMMANDS_DIR_REL,
|
|
515
|
+
CLAUDE_HITL_FALLBACK,
|
|
516
|
+
discoverInstalledCommands,
|
|
517
|
+
generateClaudeCommandAdapters,
|
|
518
|
+
CLAUDE_SETTINGS_REL,
|
|
519
|
+
RESOLVE_AGENT_KIT_REL,
|
|
520
|
+
SESSION_START_HOOK_MARKER,
|
|
521
|
+
GUARD_SHELL_HOOK_MARKER,
|
|
522
|
+
writeClaudeSessionStartHook,
|
|
523
|
+
planClaudeSettingsDrift,
|
|
524
|
+
formatClaudeSettingsDrift
|
|
525
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RESERVED_GATE_IDS,
|
|
3
|
+
checkClaudeVersion,
|
|
4
|
+
claudeChildEnv,
|
|
5
|
+
claudeHeadlessArgs,
|
|
6
|
+
claudeRedactions,
|
|
7
|
+
claudeRunCaps,
|
|
8
|
+
cursorAgentHeadlessArgs,
|
|
9
|
+
redactSecrets,
|
|
10
|
+
resolveHitlRunOptions,
|
|
11
|
+
spawnLogged
|
|
12
|
+
} from "./chunk-OUQZC744.js";
|
|
13
|
+
import {
|
|
14
|
+
fileExists
|
|
15
|
+
} from "./chunk-77V75A7O.js";
|
|
16
|
+
|
|
17
|
+
// src/plan-loop/dispatch.ts
|
|
18
|
+
import { mkdir, readFile } from "fs/promises";
|
|
19
|
+
import path from "path";
|
|
20
|
+
var RUN_CATALOG = [
|
|
21
|
+
"run-plan",
|
|
22
|
+
"run-plan-all",
|
|
23
|
+
"continue-plan",
|
|
24
|
+
"start-project",
|
|
25
|
+
"backlog-add",
|
|
26
|
+
"backlog-edit",
|
|
27
|
+
"backlog-delete",
|
|
28
|
+
"backlog-cancel",
|
|
29
|
+
"git-staging"
|
|
30
|
+
];
|
|
31
|
+
var FIRST_CLASS_SLASH_COMMANDS = ["run-plan", "run-plan-all"];
|
|
32
|
+
var RUN_PROMOTE_BLOCKED = RESERVED_GATE_IDS;
|
|
33
|
+
function normalizeSlashName(raw) {
|
|
34
|
+
return raw.replace(/^\//, "").replace(/\.md$/i, "").trim();
|
|
35
|
+
}
|
|
36
|
+
function classifySlash(name) {
|
|
37
|
+
const n = normalizeSlashName(name);
|
|
38
|
+
if (RUN_PROMOTE_BLOCKED.includes(n)) return "promote-blocked";
|
|
39
|
+
if (n === "run-plan") return "run-plan";
|
|
40
|
+
if (RUN_CATALOG.includes(n)) return "catalog";
|
|
41
|
+
return "unknown";
|
|
42
|
+
}
|
|
43
|
+
function rewriteRootArgvToRun(argv) {
|
|
44
|
+
const idx = argv.findIndex((arg) => arg.length > 0 && !arg.startsWith("-"));
|
|
45
|
+
if (idx < 0) return argv;
|
|
46
|
+
const token = argv[idx];
|
|
47
|
+
if (!token || token === "run") return argv;
|
|
48
|
+
const kind = classifySlash(token);
|
|
49
|
+
if (kind === "unknown") return argv;
|
|
50
|
+
const slash = normalizeSlashName(token);
|
|
51
|
+
if (FIRST_CLASS_SLASH_COMMANDS.includes(slash)) {
|
|
52
|
+
if (token === slash) return argv;
|
|
53
|
+
const next2 = [...argv];
|
|
54
|
+
next2[idx] = slash;
|
|
55
|
+
return next2;
|
|
56
|
+
}
|
|
57
|
+
const next = [...argv];
|
|
58
|
+
next.splice(idx, 1, "run", slash);
|
|
59
|
+
return next;
|
|
60
|
+
}
|
|
61
|
+
function commandMarkdownPath(root, name) {
|
|
62
|
+
return path.join(root, ".cursor", "commands", `${normalizeSlashName(name)}.md`);
|
|
63
|
+
}
|
|
64
|
+
function buildDispatchPrompt(commandBody) {
|
|
65
|
+
return [
|
|
66
|
+
"Follow the Agent Kit command below. This session is headless: use numbered-list HITL (Ask questions is Cursor-only). Before each numbered list print one line `HITL_GATE: <ask-id> | <label 1> | <label 2> | ...` (ask-id and labels from the hitl-gates skill), then wait; the operator's answer arrives as `HITL_REPLY: <ask-id> | operator reply <n> | <label>`. Never /git-prod. No CLI --force promote.",
|
|
67
|
+
"",
|
|
68
|
+
commandBody.trimEnd(),
|
|
69
|
+
""
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
function promoteBlockedMessage(name) {
|
|
73
|
+
const n = normalizeSlashName(name);
|
|
74
|
+
return `${n} is operator-gated and omitted from agent-kit run. Use the Cursor slash /${n} (explicit yes). Never auto /git-prod.`;
|
|
75
|
+
}
|
|
76
|
+
function unknownSlashMessage(name) {
|
|
77
|
+
return `Unknown or unsupported slash '${name}'. Catalog: ${RUN_CATALOG.join(", ")}. git-prod and kit-prod are omitted (operator-gated).`;
|
|
78
|
+
}
|
|
79
|
+
async function readCommandFile(root, name) {
|
|
80
|
+
const filePath = commandMarkdownPath(root, name);
|
|
81
|
+
if (!await fileExists(filePath)) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
message: `Command file not found: ${filePath}. Install or refresh Agent Kit L0 so .cursor/commands/${normalizeSlashName(name)}.md exists.`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const body = await readFile(filePath, "utf8");
|
|
88
|
+
return { ok: true, path: filePath, body };
|
|
89
|
+
}
|
|
90
|
+
function stamp() {
|
|
91
|
+
const d = /* @__PURE__ */ new Date();
|
|
92
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
93
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}${p(d.getMilliseconds(), 3)}`;
|
|
94
|
+
}
|
|
95
|
+
function cursorAgentDispatchArgs(opts) {
|
|
96
|
+
return cursorAgentHeadlessArgs(opts);
|
|
97
|
+
}
|
|
98
|
+
function claudeDispatchArgs(opts = {}) {
|
|
99
|
+
return claudeHeadlessArgs(opts);
|
|
100
|
+
}
|
|
101
|
+
async function runHeadlessDispatch(opts) {
|
|
102
|
+
if (opts.backendId === "claude") {
|
|
103
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
104
|
+
const env = claudeChildEnv();
|
|
105
|
+
const redact = claudeRedactions(env);
|
|
106
|
+
const version = checkClaudeVersion(opts.bin, opts.versionFn);
|
|
107
|
+
if (!version.ok) throw new Error(`claude dispatch: ${version.message}`);
|
|
108
|
+
if (version.warning) log(version.warning);
|
|
109
|
+
const { caps, invalid } = claudeRunCaps(env);
|
|
110
|
+
for (const problem of invalid)
|
|
111
|
+
log(`claude dispatch: ignoring invalid ${problem}; running uncapped.`);
|
|
112
|
+
try {
|
|
113
|
+
return await spawnLogged(
|
|
114
|
+
opts.bin,
|
|
115
|
+
claudeDispatchArgs({ model: opts.model, ...caps }),
|
|
116
|
+
opts.logPath,
|
|
117
|
+
{
|
|
118
|
+
cwd: opts.workspace,
|
|
119
|
+
env,
|
|
120
|
+
redact,
|
|
121
|
+
spawnFn: opts.spawnFn,
|
|
122
|
+
hitl: resolveHitlRunOptions(opts.hitl, {
|
|
123
|
+
kind: "stdin-stream-json",
|
|
124
|
+
firstPrompt: opts.prompt
|
|
125
|
+
}),
|
|
126
|
+
render: opts.render,
|
|
127
|
+
onSpawn: opts.onSpawn,
|
|
128
|
+
backendId: "claude"
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw new Error(`claude dispatch failed to start: ${redactSecrets(String(err), redact)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const args = cursorAgentDispatchArgs({
|
|
136
|
+
workspace: opts.workspace,
|
|
137
|
+
prompt: opts.prompt,
|
|
138
|
+
model: opts.model
|
|
139
|
+
});
|
|
140
|
+
return spawnLogged(opts.bin, args, opts.logPath, {
|
|
141
|
+
spawnFn: opts.spawnFn,
|
|
142
|
+
redact: claudeRedactions(process.env),
|
|
143
|
+
hitl: resolveHitlRunOptions(opts.hitl, { kind: "none", backend: "cursor-agent" }),
|
|
144
|
+
render: opts.render,
|
|
145
|
+
onSpawn: opts.onSpawn,
|
|
146
|
+
backendId: "cursor-agent"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async function ensureDispatchLogPath(root) {
|
|
150
|
+
const logDir = path.join(root, ".cursor", "loop-logs");
|
|
151
|
+
await mkdir(logDir, { recursive: true });
|
|
152
|
+
return path.join(logDir, `run-${stamp()}.log`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
normalizeSlashName,
|
|
157
|
+
classifySlash,
|
|
158
|
+
rewriteRootArgvToRun,
|
|
159
|
+
buildDispatchPrompt,
|
|
160
|
+
promoteBlockedMessage,
|
|
161
|
+
unknownSlashMessage,
|
|
162
|
+
readCommandFile,
|
|
163
|
+
runHeadlessDispatch,
|
|
164
|
+
ensureDispatchLogPath
|
|
165
|
+
};
|