@pragma-sh/claude-code-plugin 0.1.0-alpha.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.
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "pragma",
3
+ "description": "Pragma's bundled Claude Code plugin that reports agent status to the Pragma desktop app.",
4
+ "owner": {
5
+ "name": "Pragma"
6
+ },
7
+ "plugins": [
8
+ {
9
+ "name": "pragma-claude-code",
10
+ "source": "./",
11
+ "description": "Reports Claude Code agent status (running / done / attention) to Pragma automatically."
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "pragma-claude-code",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Reports Claude Code agent status (running / done / attention) to Pragma automatically.",
5
+ "author": {
6
+ "name": "Pragma"
7
+ }
8
+ }
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <title>Claude Code</title>
3
+ <path clip-rule="evenodd"
4
+ d="M20.998 10.949H24v3.102h-3v3.028h-1.487V20H18v-2.921h-1.487V20H15v-2.921H9V20H7.488v-2.921H6V20H4.487v-2.921H3V14.05H0V10.95h3V5h17.998v5.949zM6 10.949h1.488V8.102H6v2.847zm10.51 0H18V8.102h-1.49v2.847z"
5
+ fill="#D97757"
6
+ fill-rule="evenodd" />
7
+ </svg>
@@ -0,0 +1,476 @@
1
+ // ../plugin/dist/shared/chunk-k5kqv14t.js
2
+ var PLUGIN_API_VERSION = "0.0.0";
3
+
4
+ // ../plugin/dist/shared/chunk-0rpz2gg0.js
5
+ function defineAgent(input) {
6
+ return input;
7
+ }
8
+ function definePlugin(input) {
9
+ return { ...input, __apiVersion: PLUGIN_API_VERSION };
10
+ }
11
+ function defineUsageLimitProvider(input) {
12
+ return input;
13
+ }
14
+
15
+ // ../watcher-kit/src/index.ts
16
+ var DEFAULT_APPROVE_KEYS = "\r";
17
+ var RIGHT_ARROW = "\x1B[C";
18
+ var DOWN_ARROW = "\x1B[B";
19
+ var DEFAULT_DENY_KEYS = `${RIGHT_ARROW}${RIGHT_ARROW}\r`;
20
+ var DEFAULT_SUBMIT_KEYS = "\r";
21
+ var BRACKETED_PASTE_START = "\x1B[200~";
22
+ var BRACKETED_PASTE_END = "\x1B[201~";
23
+ var QUESTION_REJECT_KEYS = "\x1B";
24
+ var QUESTION_DIGIT_MAX = 9;
25
+ var QUESTION_PROMPT_MOUNT_DELAY_MS = 150;
26
+ var QUESTION_OTHER_INPUT_DELAY_MS = 150;
27
+ var QUESTION_NEXT_PROMPT_DELAY_MS = 500;
28
+ var ANSWER_SEPARATOR = "\x1F";
29
+ var RESUBSCRIBE_DELAY_MS = 500;
30
+ var DEFAULT_INTERJECT_SUBMIT_DELAY_MS = 200;
31
+ function createTuiWatcher(options) {
32
+ const {
33
+ agent,
34
+ handleDecisions,
35
+ handleQuestionAnswers = handleDecisions,
36
+ interjectSubmitDelayMs = DEFAULT_INTERJECT_SUBMIT_DELAY_MS,
37
+ interjectMode = "bracketed",
38
+ interjectSubmitKeys,
39
+ questionSelectMode = "digit",
40
+ questionOtherMode = "navigate-enter",
41
+ questionDismissKeys = QUESTION_REJECT_KEYS,
42
+ questionFinalizeKeys = ""
43
+ } = options;
44
+ return {
45
+ agent,
46
+ async watch(ctx) {
47
+ const watcherContext = ctx;
48
+ const runtime = {
49
+ keys: resolveKeys(watcherContext.config, interjectSubmitKeys),
50
+ handleDecisions,
51
+ handleQuestionAnswers,
52
+ interjectSubmitDelayMs,
53
+ interjectMode,
54
+ questionSelectMode,
55
+ questionOtherMode,
56
+ questionDismissKeys,
57
+ questionFinalizeKeys,
58
+ seenRequestIds: new Set,
59
+ questionsByRequestId: new Map,
60
+ outstandingCommandRequestId: null
61
+ };
62
+ let failures = 0;
63
+ while (!ctx.signal.aborted) {
64
+ try {
65
+ await consumeControlEvents(watcherContext, runtime);
66
+ failures = 0;
67
+ } catch (error) {
68
+ failures += 1;
69
+ reportStreamFailure(agent, failures, error);
70
+ }
71
+ if (ctx.signal.aborted) {
72
+ return;
73
+ }
74
+ await delay(RESUBSCRIBE_DELAY_MS, ctx.signal);
75
+ }
76
+ }
77
+ };
78
+ }
79
+ function reportStreamFailure(agent, consecutive, error) {
80
+ if (consecutive !== 1 && consecutive % STREAM_FAILURE_LOG_EVERY !== 0) {
81
+ return;
82
+ }
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ console.warn(JSON.stringify({ type: "watcher.streamError", agent, consecutive, error: message }));
85
+ }
86
+ var STREAM_FAILURE_LOG_EVERY = 240;
87
+ function resolveKeys(config, interjectSubmitKeys) {
88
+ const c = config ?? {};
89
+ return {
90
+ approveKeys: c.approveKeys ?? DEFAULT_APPROVE_KEYS,
91
+ denyKeys: c.denyKeys ?? DEFAULT_DENY_KEYS,
92
+ submitKeys: c.submitKeys ?? interjectSubmitKeys ?? DEFAULT_SUBMIT_KEYS
93
+ };
94
+ }
95
+ async function consumeControlEvents(ctx, runtime) {
96
+ const connection = await ctx.sdk.agents.connect({
97
+ agent: ctx.agentId,
98
+ tabId: ctx.session.tabId,
99
+ worktreeId: ctx.session.worktreeId,
100
+ signal: ctx.signal
101
+ });
102
+ for await (const event of connection) {
103
+ if (ctx.signal.aborted) {
104
+ return;
105
+ }
106
+ await handleControlEvent(ctx, runtime, event);
107
+ }
108
+ }
109
+ async function handleControlEvent(ctx, runtime, event) {
110
+ if (event.type === "agent") {
111
+ if (runtime.handleQuestionAnswers)
112
+ rememberQuestion(runtime.questionsByRequestId, event);
113
+ if (runtime.handleDecisions)
114
+ rememberCommandAttention(runtime, event);
115
+ return;
116
+ }
117
+ if (runtime.handleDecisions && await handleDecision(ctx, runtime, event)) {
118
+ return;
119
+ }
120
+ if (runtime.handleQuestionAnswers && await handleAnswer(ctx, runtime, event)) {
121
+ return;
122
+ }
123
+ if (event.type === "agentInput") {
124
+ await handleInterjection(ctx, runtime.keys.submitKeys, runtime.interjectSubmitDelayMs, runtime.interjectMode, event.input.text);
125
+ }
126
+ }
127
+ async function handleDecision(ctx, runtime, event) {
128
+ if (event.type !== "agentDecision")
129
+ return false;
130
+ if (runtime.seenRequestIds.has(event.decision.requestId))
131
+ return true;
132
+ runtime.seenRequestIds.add(event.decision.requestId);
133
+ if (runtime.outstandingCommandRequestId !== null && runtime.outstandingCommandRequestId !== event.decision.requestId) {
134
+ return true;
135
+ }
136
+ runtime.outstandingCommandRequestId = null;
137
+ await writeKeys(ctx, event.decision.approved ? runtime.keys.approveKeys : runtime.keys.denyKeys);
138
+ return true;
139
+ }
140
+ async function handleAnswer(ctx, runtime, event) {
141
+ if (event.type !== "agentAnswer")
142
+ return false;
143
+ const { answer } = event;
144
+ const cached = runtime.questionsByRequestId.get(answer.requestId);
145
+ if (!cached)
146
+ return true;
147
+ if (runtime.seenRequestIds.has(answer.requestId))
148
+ return true;
149
+ runtime.seenRequestIds.add(answer.requestId);
150
+ runtime.questionsByRequestId.delete(answer.requestId);
151
+ await delay(QUESTION_PROMPT_MOUNT_DELAY_MS, ctx.signal);
152
+ if (ctx.signal.aborted)
153
+ return true;
154
+ const reply = answer.answer?.trim() ?? null;
155
+ if (cached.questions.length > 1) {
156
+ if (!answer.dismissed && reply) {
157
+ await writeMultipleQuestionAnswers(ctx, runtime, cached.questions, reply);
158
+ } else if (answer.dismissed || !reply) {
159
+ await writeKeys(ctx, runtime.questionDismissKeys);
160
+ }
161
+ return true;
162
+ }
163
+ const single = cached.questions[0] ?? { question: "", options: [] };
164
+ await writeQuestionAnswer(ctx, runtime, single, reply, answer.dismissed);
165
+ if (!answer.dismissed && reply)
166
+ await finalizeQuestionAnswers(ctx, runtime);
167
+ return true;
168
+ }
169
+ async function writeQuestionAnswer(ctx, runtime, question, reply, dismissed) {
170
+ if (dismissed || !reply) {
171
+ await writeKeys(ctx, runtime.questionDismissKeys);
172
+ return;
173
+ }
174
+ if (!dismissed && reply && !question.options.includes(reply)) {
175
+ await writeFreeTextAnswer(ctx, question.options.length, reply, runtime.questionSelectMode, runtime.questionOtherMode);
176
+ return;
177
+ }
178
+ const strokes = questionAnswerKeys({
179
+ dismissed,
180
+ reply,
181
+ options: question.options,
182
+ selectMode: runtime.questionSelectMode
183
+ });
184
+ if (strokes)
185
+ await writeKeys(ctx, strokes);
186
+ }
187
+ async function writeFreeTextAnswer(ctx, optionCount, reply, selectMode, otherMode) {
188
+ await writeKeys(ctx, openOtherEditorKeys(optionCount, selectMode, otherMode));
189
+ await delay(QUESTION_OTHER_INPUT_DELAY_MS, ctx.signal);
190
+ if (!ctx.signal.aborted)
191
+ await writeKeys(ctx, `${reply}\r`);
192
+ }
193
+ async function writeMultipleQuestionAnswers(ctx, runtime, questions, combined) {
194
+ const replies = combined.split(ANSWER_SEPARATOR).map((reply) => reply.trim());
195
+ for (const [index, question] of questions.entries()) {
196
+ const reply = replies[index];
197
+ if (!reply || ctx.signal.aborted)
198
+ return;
199
+ await writeQuestionAnswer(ctx, runtime, question, reply, false);
200
+ if (index < questions.length - 1) {
201
+ await delay(QUESTION_NEXT_PROMPT_DELAY_MS, ctx.signal);
202
+ }
203
+ }
204
+ await finalizeQuestionAnswers(ctx, runtime);
205
+ }
206
+ async function finalizeQuestionAnswers(ctx, runtime) {
207
+ if (!runtime.questionFinalizeKeys || ctx.signal.aborted)
208
+ return;
209
+ await delay(QUESTION_NEXT_PROMPT_DELAY_MS, ctx.signal);
210
+ if (!ctx.signal.aborted)
211
+ await writeKeys(ctx, runtime.questionFinalizeKeys);
212
+ }
213
+ async function handleInterjection(ctx, submitKeys, submitDelayMs, mode, text) {
214
+ const input = mode === "plain" ? text : `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`;
215
+ await writeKeys(ctx, input);
216
+ if (!submitKeys) {
217
+ return;
218
+ }
219
+ await delay(submitDelayMs, ctx.signal);
220
+ if (!ctx.signal.aborted)
221
+ await writeKeys(ctx, submitKeys);
222
+ }
223
+ function rememberCommandAttention(runtime, event) {
224
+ if (event.status === "attention" && event.attentionKind === "command" && typeof event.requestId === "string" && event.requestId.length > 0) {
225
+ runtime.outstandingCommandRequestId = event.requestId;
226
+ return;
227
+ }
228
+ if (event.status !== "attention") {
229
+ runtime.outstandingCommandRequestId = null;
230
+ }
231
+ }
232
+ function rememberQuestion(cache, event) {
233
+ if (event.status === "attention" && event.attentionKind === "question" && typeof event.requestId === "string" && event.requestId.length > 0) {
234
+ const entries = (event.questions ?? []).flatMap(questionEntry);
235
+ if (entries.length > 0) {
236
+ cache.set(event.requestId, { questions: entries });
237
+ return;
238
+ }
239
+ const options = (event.options ?? []).map((option) => option.label).filter((option) => option.trim() !== "");
240
+ cache.set(event.requestId, {
241
+ questions: [{ question: event.question ?? "", options }]
242
+ });
243
+ }
244
+ }
245
+ function questionEntry(value) {
246
+ if (typeof value !== "object" || value === null || !("question" in value)) {
247
+ return [];
248
+ }
249
+ const record = value;
250
+ if (typeof record.question !== "string" || !record.question.trim()) {
251
+ return [];
252
+ }
253
+ const options = Array.isArray(record.options) ? record.options.filter((option) => Boolean(option) && typeof option === "object").map((option) => option.label).filter((label) => typeof label === "string" && label.trim() !== "") : [];
254
+ return [{ question: record.question, options }];
255
+ }
256
+ function questionAnswerKeys(input) {
257
+ if (input.dismissed || input.reply === null) {
258
+ return QUESTION_REJECT_KEYS;
259
+ }
260
+ const reply = input.reply.trim();
261
+ if (!reply) {
262
+ return QUESTION_REJECT_KEYS;
263
+ }
264
+ const options = input.options;
265
+ const selectMode = input.selectMode ?? "digit";
266
+ const matchIndex = options.findIndex((option) => option === reply);
267
+ if (matchIndex >= 0) {
268
+ return selectOptionKeys(matchIndex, options.length, selectMode);
269
+ }
270
+ return `${openOtherEditorKeys(options.length, selectMode, "navigate-enter")}${reply}\r`;
271
+ }
272
+ function openOtherEditorKeys(optionCount, selectMode, otherMode) {
273
+ if (otherMode === "shortcut-z")
274
+ return "z";
275
+ const navigate = DOWN_ARROW.repeat(optionCount);
276
+ return selectMode === "arrow-space" || otherMode === "navigate" ? navigate : `${navigate}\r`;
277
+ }
278
+ function selectOptionKeys(index, optionCount, selectMode) {
279
+ if (selectMode === "arrow-space") {
280
+ return `${DOWN_ARROW.repeat(index)} \r`;
281
+ }
282
+ const total = optionCount + 1;
283
+ if (index < QUESTION_DIGIT_MAX && index < total) {
284
+ return String(index + 1);
285
+ }
286
+ return `${DOWN_ARROW.repeat(index)}\r`;
287
+ }
288
+ async function writeKeys(ctx, data) {
289
+ try {
290
+ await ctx.sendKeys(data);
291
+ } catch {}
292
+ }
293
+ function delay(ms, signal) {
294
+ if (signal.aborted) {
295
+ return Promise.resolve();
296
+ }
297
+ return new Promise((resolve) => {
298
+ const finish = () => resolve();
299
+ const timer = setTimeout(finish, ms);
300
+ signal.addEventListener("abort", () => {
301
+ clearTimeout(timer);
302
+ finish();
303
+ }, { once: true });
304
+ });
305
+ }
306
+
307
+ // src/pragma-plugin.ts
308
+ var INTERJECT_SUBMIT_DELAY_MS = 200;
309
+ var USAGE_REQUEST = JSON.stringify({
310
+ type: "control_request",
311
+ request_id: "pragma-usage",
312
+ request: { subtype: "get_usage" }
313
+ });
314
+ var USAGE_COMMAND = `printf '%s\\n' '${USAGE_REQUEST}' | claude -p --safe-mode --input-format stream-json --output-format stream-json --verbose`;
315
+ var reasoningFull = [
316
+ { id: "low", name: "Low" },
317
+ { id: "medium", name: "Medium" },
318
+ { id: "high", name: "High" },
319
+ { id: "xhigh", name: "Extra High" },
320
+ { id: "max", name: "Max" }
321
+ ];
322
+ var reasoningStandard = reasoningFull.slice(0, 3);
323
+ var claudeCodeAgentPlugin = definePlugin({
324
+ name: "Claude Code",
325
+ description: "Launch Claude Code from Pragma.",
326
+ usageLimits: [
327
+ defineUsageLimitProvider({
328
+ id: "claude-code",
329
+ title: "Claude Code",
330
+ dashboardUrl: "https://claude.ai/new#settings/usage",
331
+ iconPath: "assets/claude-code.svg",
332
+ primaryLimitId: "five-hour",
333
+ refreshIntervalMs: 300000,
334
+ load: loadClaudeUsageLimits
335
+ })
336
+ ],
337
+ watchers: [
338
+ createTuiWatcher({
339
+ agent: "claude-code",
340
+ handleDecisions: false,
341
+ interjectSubmitDelayMs: INTERJECT_SUBMIT_DELAY_MS
342
+ })
343
+ ],
344
+ agents: [
345
+ defineAgent({
346
+ id: "claude-code",
347
+ name: "Claude Code",
348
+ icon: () => null,
349
+ iconPath: "assets/claude-code.svg",
350
+ launch: { command: ["claude", "--permission-mode", "auto"] },
351
+ models: [
352
+ { id: "sonnet", name: "Sonnet", reasoning: reasoningFull },
353
+ { id: "opus", name: "Opus", reasoning: reasoningStandard },
354
+ { id: "fable", name: "Fable", reasoning: reasoningFull },
355
+ { id: "haiku", name: "Haiku", reasoning: reasoningStandard }
356
+ ],
357
+ permissionModes: [],
358
+ excludeFeatures: ["commandApproval"],
359
+ args: {
360
+ model: (modelId) => ["--model", modelId],
361
+ reasoning: (reasoningId) => ["--effort", reasoningId],
362
+ permissionMode: () => []
363
+ }
364
+ })
365
+ ]
366
+ });
367
+ var pragma_plugin_default = claudeCodeAgentPlugin;
368
+ async function loadClaudeUsageLimits(ctx) {
369
+ const cwd = ctx.project?.path ?? "/tmp";
370
+ const [result] = await ctx.sdk.exec.run({ cwd, commands: [USAGE_COMMAND] });
371
+ if (!result || result.status !== 0) {
372
+ throw new Error(result?.stderr.trim() || "Claude Code usage request failed");
373
+ }
374
+ return extractUsageFromOutput(result.stdout);
375
+ }
376
+ function extractUsageFromOutput(stdout) {
377
+ for (const line of stdout.split(`
378
+ `)) {
379
+ const response = tryParseUsageControlResponse(line);
380
+ if (response === undefined) {
381
+ continue;
382
+ }
383
+ if (response.subtype === "error") {
384
+ throw new Error(response.error || "Claude Code usage request failed");
385
+ }
386
+ return parseClaudeUsage(response.response, Date.now());
387
+ }
388
+ throw new Error("Claude Code did not return usage data");
389
+ }
390
+ function tryParseUsageControlResponse(line) {
391
+ try {
392
+ const value = JSON.parse(line);
393
+ return isUsageControlResponse(value) ? value.response : undefined;
394
+ } catch (cause) {
395
+ if (cause instanceof SyntaxError) {
396
+ return;
397
+ }
398
+ throw cause;
399
+ }
400
+ }
401
+ function parseClaudeUsage(value, observedAt) {
402
+ if (!isRecord(value)) {
403
+ throw new Error("Claude Code usage response was not an object");
404
+ }
405
+ if (value.rate_limits_available !== true) {
406
+ return {
407
+ status: "unavailable",
408
+ reason: "authentication-required",
409
+ message: "Sign in to Claude Code with a subscription to load usage limits."
410
+ };
411
+ }
412
+ if (!isRecord(value.rate_limits)) {
413
+ throw new Error("Claude Code usage data is temporarily unavailable (rate limited)");
414
+ }
415
+ const rateLimits = value.rate_limits;
416
+ const limits = [];
417
+ addWindow(limits, "five-hour", "5-hour limit", rateLimits.five_hour, observedAt);
418
+ addWindow(limits, "seven-day", "Weekly limit", rateLimits.seven_day, observedAt);
419
+ addWindow(limits, "seven-day-oauth-apps", "Weekly OAuth apps", rateLimits.seven_day_oauth_apps, observedAt);
420
+ addWindow(limits, "seven-day-opus", "Weekly Opus", rateLimits.seven_day_opus, observedAt);
421
+ addWindow(limits, "seven-day-sonnet", "Weekly Sonnet", rateLimits.seven_day_sonnet, observedAt);
422
+ if (Array.isArray(rateLimits.model_scoped)) {
423
+ for (const [index, scoped] of rateLimits.model_scoped.entries()) {
424
+ if (!isRecord(scoped) || typeof scoped.display_name !== "string") {
425
+ continue;
426
+ }
427
+ addWindow(limits, `model-${index}`, scoped.display_name, { utilization: scoped.utilization, resets_at: scoped.resets_at }, observedAt);
428
+ }
429
+ }
430
+ if (!limits.some((limit) => limit.id === "five-hour")) {
431
+ return {
432
+ status: "unavailable",
433
+ reason: "unsupported",
434
+ message: "Claude Code did not return its 5-hour usage limit."
435
+ };
436
+ }
437
+ return { status: "ready", observedAt, limits };
438
+ }
439
+ function addWindow(limits, id, title, value, observedAt) {
440
+ if (!isRecord(value) || !isFiniteNumber(value.utilization)) {
441
+ return;
442
+ }
443
+ const resetAt = parseDate(value.resets_at);
444
+ limits.push({
445
+ id,
446
+ title,
447
+ used: Math.min(100, Math.max(0, value.utilization)),
448
+ limit: 100,
449
+ ...resetAt === null ? {} : { resetsInMs: Math.max(0, resetAt - observedAt) }
450
+ });
451
+ }
452
+ function parseDate(value) {
453
+ if (typeof value !== "string") {
454
+ return null;
455
+ }
456
+ const parsed = Date.parse(value);
457
+ return Number.isFinite(parsed) ? parsed : null;
458
+ }
459
+ function isUsageControlResponse(value) {
460
+ if (!isRecord(value) || value.type !== "control_response" || !isRecord(value.response)) {
461
+ return false;
462
+ }
463
+ return value.response.subtype === "success" && "response" in value.response || value.response.subtype === "error";
464
+ }
465
+ function isFiniteNumber(value) {
466
+ return typeof value === "number" && Number.isFinite(value);
467
+ }
468
+ function isRecord(value) {
469
+ return typeof value === "object" && value !== null && !Array.isArray(value);
470
+ }
471
+ export {
472
+ parseClaudeUsage,
473
+ loadClaudeUsageLimits,
474
+ pragma_plugin_default as default,
475
+ claudeCodeAgentPlugin
476
+ };
@@ -0,0 +1,105 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/report.sh\" cleared"
9
+ }
10
+ ]
11
+ }
12
+ ],
13
+ "UserPromptSubmit": [
14
+ {
15
+ "hooks": [
16
+ {
17
+ "type": "command",
18
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/report.sh\" started"
19
+ }
20
+ ]
21
+ }
22
+ ],
23
+ "Stop": [
24
+ {
25
+ "hooks": [
26
+ {
27
+ "type": "command",
28
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/report.sh\" stopped"
29
+ }
30
+ ]
31
+ }
32
+ ],
33
+ "SubagentStart": [
34
+ {
35
+ "hooks": [
36
+ {
37
+ "type": "command",
38
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/report.sh\" subagent-start"
39
+ }
40
+ ]
41
+ }
42
+ ],
43
+ "SubagentStop": [
44
+ {
45
+ "hooks": [
46
+ {
47
+ "type": "command",
48
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/report.sh\" subagent-stop"
49
+ }
50
+ ]
51
+ }
52
+ ],
53
+ "PostToolUse": [
54
+ {
55
+ "hooks": [
56
+ {
57
+ "type": "command",
58
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/report.sh\" running"
59
+ }
60
+ ]
61
+ }
62
+ ],
63
+ "PermissionRequest": [
64
+ {
65
+ "hooks": [
66
+ {
67
+ "type": "command",
68
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/report.sh\" permission"
69
+ }
70
+ ]
71
+ }
72
+ ],
73
+ "Elicitation": [
74
+ {
75
+ "hooks": [
76
+ {
77
+ "type": "command",
78
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/report.sh\" attention"
79
+ }
80
+ ]
81
+ }
82
+ ],
83
+ "SessionEnd": [
84
+ {
85
+ "hooks": [
86
+ {
87
+ "type": "command",
88
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/report.sh\" cleared"
89
+ }
90
+ ]
91
+ }
92
+ ],
93
+ "Notification": [
94
+ {
95
+ "matcher": "idle_prompt",
96
+ "hooks": [
97
+ {
98
+ "type": "command",
99
+ "command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/report.sh\" idle"
100
+ }
101
+ ]
102
+ }
103
+ ]
104
+ }
105
+ }