agent-blocked 0.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/README.md +21 -0
- package/bin/agent-blocked.mjs +409 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# agent-blocked
|
|
2
|
+
|
|
3
|
+
CLI installer and reporter for Agent Blocked.
|
|
4
|
+
|
|
5
|
+
Install adapters in any agent project:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx agent-blocked install --tool=all
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Report a blocker manually:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx agent-blocked report --event=needs_direction --severity=medium --reason="Need human direction"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
After `install`, the project gets local helper files under `.agent-blocked/`, so agents can report without depending on the Agent Blocked app repository:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node .agent-blocked/report.mjs --event=needs_credentials --severity=critical --reason="Missing deploy credentials"
|
|
21
|
+
```
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const root = process.cwd();
|
|
8
|
+
const markerStart = "<!-- agent-blocked:start -->";
|
|
9
|
+
const markerEnd = "<!-- agent-blocked:end -->";
|
|
10
|
+
|
|
11
|
+
function arg(name, fallback = "") {
|
|
12
|
+
const prefix = `--${name}=`;
|
|
13
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
14
|
+
const found = process.argv.find((value) => value.startsWith(prefix));
|
|
15
|
+
if (found) return found.slice(prefix.length);
|
|
16
|
+
if (index !== -1 && process.argv[index + 1] && !process.argv[index + 1].startsWith("--")) {
|
|
17
|
+
return process.argv[index + 1];
|
|
18
|
+
}
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hasFlag(name) {
|
|
23
|
+
return process.argv.includes(`--${name}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function commandName() {
|
|
27
|
+
return process.argv[2] && !process.argv[2].startsWith("--") ? process.argv[2] : "help";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function shellQuoted(value) {
|
|
31
|
+
return `'${String(value).replace(/'/g, "'\"'\"'")}'`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeFileIfMissing(path, content) {
|
|
35
|
+
if (existsSync(path) && !hasFlag("force")) {
|
|
36
|
+
console.log(`Kept existing ${path}. Use --force to overwrite.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
40
|
+
writeFileSync(path, content);
|
|
41
|
+
console.log(`Wrote ${path}.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const reportScript = `#!/usr/bin/env node
|
|
45
|
+
|
|
46
|
+
function arg(name, fallback = "") {
|
|
47
|
+
const prefix = \`--\${name}=\`;
|
|
48
|
+
const index = process.argv.indexOf(\`--\${name}\`);
|
|
49
|
+
const found = process.argv.find((value) => value.startsWith(prefix));
|
|
50
|
+
if (found) return found.slice(prefix.length);
|
|
51
|
+
if (index !== -1 && process.argv[index + 1] && !process.argv[index + 1].startsWith("--")) {
|
|
52
|
+
return process.argv[index + 1];
|
|
53
|
+
}
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const webhookUrl = arg("url", process.env.AGENT_BLOCKED_WEBHOOK_URL || "https://agentblocked.com/api/agent-blocked");
|
|
58
|
+
const token = arg("token", process.env.AGENT_BLOCKED_AGENT_TOKEN || "");
|
|
59
|
+
const agentId = arg("agent", process.env.AGENT_NAME || "coding-agent");
|
|
60
|
+
const eventType = arg("event", "needs_direction");
|
|
61
|
+
const severity = arg("severity", "medium");
|
|
62
|
+
const reason = arg("reason", "The agent needs human help to continue.");
|
|
63
|
+
const details = arg("details", "");
|
|
64
|
+
const runId = arg("run-id", "");
|
|
65
|
+
const provider = arg("provider", "");
|
|
66
|
+
const model = arg("model", "");
|
|
67
|
+
const confidence = arg("confidence", "");
|
|
68
|
+
|
|
69
|
+
if (!token) {
|
|
70
|
+
console.error("Missing AGENT_BLOCKED_AGENT_TOKEN or --token=...");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const payload = {
|
|
75
|
+
agentId,
|
|
76
|
+
eventType,
|
|
77
|
+
severity,
|
|
78
|
+
reason,
|
|
79
|
+
...(details ? { details } : {}),
|
|
80
|
+
...(runId ? { runId } : {}),
|
|
81
|
+
...(provider ? { provider } : {}),
|
|
82
|
+
...(model ? { model } : {}),
|
|
83
|
+
...(confidence ? { confidence: Number(confidence) } : {})
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const response = await fetch(webhookUrl, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: {
|
|
89
|
+
authorization: \`Bearer \${token}\`,
|
|
90
|
+
"content-type": "application/json"
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify(payload)
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const body = await response.text();
|
|
96
|
+
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
console.error(\`Agent Blocked report failed: \${response.status} \${body}\`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(body);
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
const claudeHookScript = `#!/usr/bin/env node
|
|
106
|
+
|
|
107
|
+
let input = "";
|
|
108
|
+
process.stdin.setEncoding("utf8");
|
|
109
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
110
|
+
|
|
111
|
+
const event = input ? JSON.parse(input) : {};
|
|
112
|
+
const webhookUrl = process.env.AGENT_BLOCKED_WEBHOOK_URL || "https://agentblocked.com/api/agent-blocked";
|
|
113
|
+
const token = process.env.AGENT_BLOCKED_AGENT_TOKEN;
|
|
114
|
+
const agentId = process.env.AGENT_NAME || "claude-code";
|
|
115
|
+
|
|
116
|
+
if (!token) process.exit(0);
|
|
117
|
+
|
|
118
|
+
function mapEventType(eventName, notificationType) {
|
|
119
|
+
if (eventName === "Notification" && notificationType === "permission_prompt") return "approval_required";
|
|
120
|
+
if (eventName === "Notification" && notificationType === "idle_prompt") return "needs_direction";
|
|
121
|
+
if (eventName === "PermissionDenied") return "needs_credentials";
|
|
122
|
+
if (eventName === "StopFailure") return "tool_failure";
|
|
123
|
+
return "needs_direction";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const eventType = mapEventType(event.hook_event_name, event.notification_type);
|
|
127
|
+
const payload = {
|
|
128
|
+
agentId,
|
|
129
|
+
eventType,
|
|
130
|
+
severity: eventType === "tool_failure" || eventType === "needs_credentials" ? "high" : "medium",
|
|
131
|
+
reason: event.message || event.title || \`Claude Code hook fired: \${event.hook_event_name || "unknown"}\`,
|
|
132
|
+
details: JSON.stringify({
|
|
133
|
+
hookEventName: event.hook_event_name,
|
|
134
|
+
notificationType: event.notification_type,
|
|
135
|
+
cwd: event.cwd,
|
|
136
|
+
sessionId: event.session_id
|
|
137
|
+
}),
|
|
138
|
+
provider: "anthropic",
|
|
139
|
+
model: "claude-code",
|
|
140
|
+
runId: event.session_id
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await fetch(webhookUrl, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: {
|
|
147
|
+
authorization: \`Bearer \${token}\`,
|
|
148
|
+
"content-type": "application/json"
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify(payload)
|
|
151
|
+
});
|
|
152
|
+
} catch {
|
|
153
|
+
// Hooks should not break Claude Code when notification delivery fails.
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
const instructions = `# Agent Blocked instructions for coding agents
|
|
158
|
+
|
|
159
|
+
When you are blocked by missing credentials, unclear direction, approval requirements, repeated tool failures, low confidence, or external context, stop burning tokens and report the blockage.
|
|
160
|
+
|
|
161
|
+
Preferred report command from the repository root:
|
|
162
|
+
|
|
163
|
+
\`\`\`bash
|
|
164
|
+
node .agent-blocked/report.mjs --event=needs_direction --severity=medium --reason="I need human direction to continue" --details="Explain what decision or context is missing."
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
Environment expected by the report command:
|
|
168
|
+
|
|
169
|
+
\`\`\`bash
|
|
170
|
+
export AGENT_BLOCKED_WEBHOOK_URL="https://agentblocked.com/api/agent-blocked"
|
|
171
|
+
export AGENT_BLOCKED_AGENT_TOKEN="your-scoped-token"
|
|
172
|
+
export AGENT_NAME="your-agent-name"
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
Event types:
|
|
176
|
+
|
|
177
|
+
- hard_blocked
|
|
178
|
+
- needs_credentials
|
|
179
|
+
- needs_direction
|
|
180
|
+
- tool_failure
|
|
181
|
+
- approval_required
|
|
182
|
+
- low_confidence
|
|
183
|
+
- lonely_agent
|
|
184
|
+
- other
|
|
185
|
+
|
|
186
|
+
Never include secrets, API keys, passwords, OAuth tokens, or private customer data in the report payload.
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
function ensureHelperFiles() {
|
|
190
|
+
writeFileIfMissing(join(root, ".agent-blocked", "report.mjs"), reportScript);
|
|
191
|
+
writeFileIfMissing(join(root, ".agent-blocked", "instructions.md"), instructions);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function mergeHookEntries(existing = [], incoming = []) {
|
|
195
|
+
const seen = new Set(existing.map((value) => JSON.stringify(value)));
|
|
196
|
+
const merged = [...existing];
|
|
197
|
+
|
|
198
|
+
for (const entry of incoming) {
|
|
199
|
+
const sig = JSON.stringify(entry);
|
|
200
|
+
if (!seen.has(sig)) {
|
|
201
|
+
merged.push(entry);
|
|
202
|
+
seen.add(sig);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return merged;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function mergeClaudeSettings(path, settings) {
|
|
210
|
+
let currentSettings = {};
|
|
211
|
+
let currentHooks = {};
|
|
212
|
+
|
|
213
|
+
if (existsSync(path)) {
|
|
214
|
+
const existingRaw = readFileSync(path, "utf8");
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(existingRaw);
|
|
217
|
+
currentSettings = parsed && typeof parsed === "object" ? parsed : {};
|
|
218
|
+
currentHooks = currentSettings.hooks && typeof currentSettings.hooks === "object" ? currentSettings.hooks : {};
|
|
219
|
+
} catch {
|
|
220
|
+
console.log(`Could not parse ${path}. Wrote example file for manual merge instead.`);
|
|
221
|
+
writeFileIfMissing(join(root, ".claude", "settings.agent-blocked.example.json"), `${JSON.stringify(settings, null, 2)}\n`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const mergedHooks = { ...currentHooks };
|
|
227
|
+
for (const [eventName, hooks] of Object.entries(settings.hooks || {})) {
|
|
228
|
+
mergedHooks[eventName] = mergeHookEntries(
|
|
229
|
+
Array.isArray(mergedHooks[eventName]) ? mergedHooks[eventName] : [],
|
|
230
|
+
hooks || []
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
currentSettings.hooks = mergedHooks;
|
|
235
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
236
|
+
writeFileSync(path, `${JSON.stringify(currentSettings, null, 2)}\n`);
|
|
237
|
+
console.log(`Updated ${path} with Agent Blocked hooks.`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function upsertMarkedSection(path, title) {
|
|
241
|
+
const section = `${markerStart}
|
|
242
|
+
## ${title}
|
|
243
|
+
|
|
244
|
+
${instructions}
|
|
245
|
+
${markerEnd}`;
|
|
246
|
+
|
|
247
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
248
|
+
const next = current.includes(markerStart) && current.includes(markerEnd)
|
|
249
|
+
? current.replace(new RegExp(`${markerStart}[\\s\\S]*?${markerEnd}`), section)
|
|
250
|
+
: `${current.trim() ? `${current.trim()}\n\n` : ""}${section}\n`;
|
|
251
|
+
|
|
252
|
+
writeFileSync(path, next);
|
|
253
|
+
console.log(`Installed Agent Blocked instructions in ${path}.`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function installClaude() {
|
|
257
|
+
writeFileIfMissing(join(root, ".agent-blocked", "claude-hook.mjs"), claudeHookScript);
|
|
258
|
+
|
|
259
|
+
const settings = {
|
|
260
|
+
hooks: {
|
|
261
|
+
Notification: [
|
|
262
|
+
{
|
|
263
|
+
matcher: "permission_prompt|idle_prompt",
|
|
264
|
+
hooks: [{ type: "command", command: "node .agent-blocked/claude-hook.mjs" }]
|
|
265
|
+
}
|
|
266
|
+
],
|
|
267
|
+
PermissionDenied: [
|
|
268
|
+
{
|
|
269
|
+
hooks: [{ type: "command", command: "node .agent-blocked/claude-hook.mjs" }]
|
|
270
|
+
}
|
|
271
|
+
],
|
|
272
|
+
StopFailure: [
|
|
273
|
+
{
|
|
274
|
+
hooks: [{ type: "command", command: "node .agent-blocked/claude-hook.mjs" }]
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
writeFileIfMissing(join(root, ".claude", "settings.agent-blocked.example.json"), `${JSON.stringify(settings, null, 2)}\n`);
|
|
281
|
+
mergeClaudeSettings(join(root, ".claude", "settings.local.json"), settings);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function installCodex() {
|
|
285
|
+
upsertMarkedSection(join(root, "AGENTS.md"), "Codex Agent Blocked setup");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function installGemini() {
|
|
289
|
+
upsertMarkedSection(join(root, "GEMINI.md"), "Gemini CLI Agent Blocked setup");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function installAider() {
|
|
293
|
+
upsertMarkedSection(join(root, "CONVENTIONS.md"), "Aider Agent Blocked setup");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const installers = {
|
|
297
|
+
claude: installClaude,
|
|
298
|
+
codex: installCodex,
|
|
299
|
+
gemini: installGemini,
|
|
300
|
+
aider: installAider
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
function install() {
|
|
304
|
+
const tool = arg("tool", "all").toLowerCase();
|
|
305
|
+
const selected = tool === "all" ? Object.keys(installers) : tool.split(",").map((value) => value.trim()).filter(Boolean);
|
|
306
|
+
|
|
307
|
+
ensureHelperFiles();
|
|
308
|
+
|
|
309
|
+
for (const name of selected) {
|
|
310
|
+
if (!installers[name]) {
|
|
311
|
+
console.error(`Unknown tool "${name}". Use --tool=claude,codex,gemini,aider or --tool=all.`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
installers[name]();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log("\nNext: set AGENT_BLOCKED_WEBHOOK_URL, AGENT_BLOCKED_AGENT_TOKEN, and AGENT_NAME in the shell where your agent runs.");
|
|
318
|
+
console.log("Manual report command: node .agent-blocked/report.mjs --event=needs_direction --severity=medium --reason=\"Need human direction\"");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function report() {
|
|
322
|
+
const webhookUrl = arg("url", process.env.AGENT_BLOCKED_WEBHOOK_URL || "https://agentblocked.com/api/agent-blocked");
|
|
323
|
+
const token = arg("token", process.env.AGENT_BLOCKED_AGENT_TOKEN || "");
|
|
324
|
+
const agentId = arg("agent", process.env.AGENT_NAME || "coding-agent");
|
|
325
|
+
const eventType = arg("event", "needs_direction");
|
|
326
|
+
const severity = arg("severity", "medium");
|
|
327
|
+
const reason = arg("reason", "The agent needs human help to continue.");
|
|
328
|
+
const details = arg("details", "");
|
|
329
|
+
|
|
330
|
+
if (!token) {
|
|
331
|
+
console.error("Missing AGENT_BLOCKED_AGENT_TOKEN or --token=...");
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const response = await fetch(webhookUrl, {
|
|
336
|
+
method: "POST",
|
|
337
|
+
headers: {
|
|
338
|
+
authorization: `Bearer ${token}`,
|
|
339
|
+
"content-type": "application/json"
|
|
340
|
+
},
|
|
341
|
+
body: JSON.stringify({
|
|
342
|
+
agentId,
|
|
343
|
+
eventType,
|
|
344
|
+
severity,
|
|
345
|
+
reason,
|
|
346
|
+
...(details ? { details } : {})
|
|
347
|
+
})
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const body = await response.text();
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
console.error(`Agent Blocked report failed: ${response.status} ${body}`);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
console.log(body);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function codex() {
|
|
359
|
+
const separator = process.argv.indexOf("--");
|
|
360
|
+
const args = separator === -1 ? process.argv.slice(3) : process.argv.slice(separator + 1);
|
|
361
|
+
const command = args[0] || "codex";
|
|
362
|
+
const commandArgs = args[0] ? args.slice(1) : [];
|
|
363
|
+
|
|
364
|
+
const child = spawn(command, commandArgs, { stdio: "inherit", shell: false });
|
|
365
|
+
const code = await new Promise((resolve) => child.on("close", resolve));
|
|
366
|
+
|
|
367
|
+
if (code && process.env.AGENT_BLOCKED_AGENT_TOKEN) {
|
|
368
|
+
process.argv = [
|
|
369
|
+
process.argv[0],
|
|
370
|
+
process.argv[1],
|
|
371
|
+
"report",
|
|
372
|
+
"--event=tool_failure",
|
|
373
|
+
"--severity=high",
|
|
374
|
+
`--reason=Codex exited with code ${code}`,
|
|
375
|
+
`--details=${command} ${commandArgs.join(" ")}`
|
|
376
|
+
];
|
|
377
|
+
await report();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
process.exit(code || 0);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function help() {
|
|
384
|
+
console.log(`Agent Blocked CLI
|
|
385
|
+
|
|
386
|
+
Usage:
|
|
387
|
+
agent-blocked install --tool=claude|codex|gemini|aider|all
|
|
388
|
+
agent-blocked report --event=needs_direction --severity=medium --reason="Need human help"
|
|
389
|
+
agent-blocked codex -- codex "your task"
|
|
390
|
+
|
|
391
|
+
Examples:
|
|
392
|
+
npx agent-blocked install --tool=all
|
|
393
|
+
npx agent-blocked report --event=needs_credentials --severity=critical --reason="Missing AWS credentials"
|
|
394
|
+
`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
switch (commandName()) {
|
|
398
|
+
case "install":
|
|
399
|
+
install();
|
|
400
|
+
break;
|
|
401
|
+
case "report":
|
|
402
|
+
await report();
|
|
403
|
+
break;
|
|
404
|
+
case "codex":
|
|
405
|
+
await codex();
|
|
406
|
+
break;
|
|
407
|
+
default:
|
|
408
|
+
help();
|
|
409
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-blocked",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI installer and reporter for Agent Blocked AI agent escalation alerts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-blocked": "bin/agent-blocked.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md",
|
|
12
|
+
"package.json"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai",
|
|
19
|
+
"agents",
|
|
20
|
+
"alerts",
|
|
21
|
+
"claude",
|
|
22
|
+
"codex",
|
|
23
|
+
"gemini",
|
|
24
|
+
"aider"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT"
|
|
27
|
+
}
|