@g3nomaly/g3n-mods 0.1.2 → 0.1.4
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/.claude-plugin/marketplace.json +3 -3
- package/README.md +4 -4
- package/mods/agents-panel/hooks/register.tsx +35 -8
- package/mods/custom-crab-agents/.claude-plugin/plugin.json +5 -2
- package/mods/custom-crab-agents/hooks/register.tsx +39 -21
- package/mods/delete-guard/.claude-plugin/plugin.json +1 -1
- package/mods/delete-guard/hooks/hooks.json +1 -1
- package/mods/delete-guard/hooks/register.tsx +585 -0
- package/mods/delete-guard/types/index.d.ts +26 -1
- package/mods/focus-mode/.claude-plugin/plugin.json +1 -1
- package/mods/focus-mode/hooks/register.tsx +36 -3
- package/mods/mini-game/hooks/register.tsx +15 -6
- package/mods/tool-timeline/hooks/register.tsx +102 -38
- package/mods/tool-timeline/types/index.d.ts +2 -0
- package/package.json +2 -2
- package/mods/delete-guard/hooks/register.ts +0 -151
|
@@ -19,19 +19,19 @@
|
|
|
19
19
|
{
|
|
20
20
|
"name": "custom-crab-agents",
|
|
21
21
|
"source": "./mods/custom-crab-agents",
|
|
22
|
-
"description": "
|
|
22
|
+
"description": "Draws each subagent in the agents panel as a crab with a hat: effort tier, model, steps, context, tokens, cost and time.",
|
|
23
23
|
"version": "0.1.0"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"name": "delete-guard",
|
|
27
27
|
"source": "./mods/delete-guard",
|
|
28
|
-
"description": "
|
|
28
|
+
"description": "Holds rm -rf and other permanent deletes on a card that lists what they remove, until you press Delete or Cancel.",
|
|
29
29
|
"version": "0.1.0"
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"name": "focus-mode",
|
|
33
33
|
"source": "./mods/focus-mode",
|
|
34
|
-
"description": "A timed focus block
|
|
34
|
+
"description": "A timed focus block in which you're away: Claude asks no questions and gets no permission prompts. Countdown, progress bar and end button.",
|
|
35
35
|
"version": "0.1.0"
|
|
36
36
|
},
|
|
37
37
|
{
|
package/README.md
CHANGED
|
@@ -22,16 +22,16 @@ Then open the project in VS Code and trust the workspace. New sessions load the
|
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `agents-panel` | The agents in `.claude/agents` with their model and a run button. A running agent shows a spinner, and a toast says when it starts and finishes | `/agents-panel` |
|
|
24
24
|
| `context-bar` | One bar of context-window use split by source, with the total and where compaction starts | `/context-bar` |
|
|
25
|
-
| `custom-crab-agents` |
|
|
26
|
-
| `delete-guard` |
|
|
27
|
-
| `focus-mode` | A timed focus block
|
|
25
|
+
| `custom-crab-agents` | Adds each subagent to the agents panel as a crab with a hat: effort tier, model, steps, context, tokens, cost and time. Needs `agents-panel`, which installing it adds | `/agents-panel` |
|
|
26
|
+
| `delete-guard` | Off by default: deletes run unasked. Turn it on with the switch in its pane, or `/delete-guard on`; it stays on in later sessions. While on, `rm -rf` and other permanent deletes wait on a card listing the files, their sizes and whether git can restore them, until you press **Delete** or **Cancel**. Cancel tells Claude which trash command to use instead. With nothing to draw the card on, the delete is refused | `/delete-guard on \| off` |
|
|
27
|
+
| `focus-mode` | A timed focus block in which you're away: Claude's questions are refused, so it picks for itself and says what it assumed, and permission prompts are refused, so it works around the step and lists it for later. Countdown, progress bar and end button. 25 minutes unless you pass a number | `/focus-mode [minutes] \| end` |
|
|
28
28
|
| `handoff` | Writes a handoff with the `handoff` skill, clears the session, and attaches the handoff to your next prompt | `/handoff-new [what the next session is for]` |
|
|
29
29
|
| `hide-personal-data` | Masks names, emails, phone numbers, IBANs and card numbers in replies. Hover a mask to read it | `/hide-personal-data on \| off` |
|
|
30
30
|
| `mini-game` | Minesweeper, to play while Claude works | `/mini-game` |
|
|
31
31
|
| `tool-timeline` | Each tool call with its duration. A denied call is struck through, next to the call that replaced it | `/tool-timeline` |
|
|
32
32
|
| `you-should-know` | A card under long replies with their key point, a headline and one supporting line. It costs one small Haiku call per reply of 1500 characters or more | none |
|
|
33
33
|
|
|
34
|
-
For `agents-panel`, `context-bar
|
|
34
|
+
For `agents-panel`, `context-bar` and `tool-timeline`, the command opens the pane and running it again closes it.
|
|
35
35
|
|
|
36
36
|
Three mods take settings, declared under `userConfig` in their `.claude-plugin/plugin.json`:
|
|
37
37
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/g3n-mods/mods/agents-panel/hooks/register.tsx
|
|
2
2
|
import { atom, read, update } from 'claude-code'
|
|
3
|
-
import type { EngineInterface, Register } from 'claude-code'
|
|
3
|
+
import type { EngineInterface, Register, RenderElement } from 'claude-code'
|
|
4
4
|
|
|
5
5
|
import type { AgentDef, RunningAgent } from '../types'
|
|
6
6
|
|
|
@@ -20,6 +20,27 @@ const NAMED_COLORS: Readonly<Record<string, string>> = {
|
|
|
20
20
|
const SPINNER = ['◐', '◓', '◑', '◒']
|
|
21
21
|
const DONE_STATUSES = new Set(['completed', 'failed', 'killed', 'idle'])
|
|
22
22
|
|
|
23
|
+
/** Key prefix of the panel's own elements; an add-on looks for it to find the panel's tree. */
|
|
24
|
+
export const OWN_KEY = 'agents:'
|
|
25
|
+
|
|
26
|
+
/** Key prefix an add-on's section carries, such as custom-crab-agents' crabs, so the panel knows to show it. */
|
|
27
|
+
export const ADDON_KEY = 'addon:'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tells whether a drawn tree holds an element whose key starts with `prefix`.
|
|
31
|
+
*
|
|
32
|
+
* @param tree - Tree as a `ui.render` chain returned it, or one of its children.
|
|
33
|
+
* @param prefix - Key prefix to look for, e.g. `addon:`.
|
|
34
|
+
* @returns True when some element's key starts with `prefix`.
|
|
35
|
+
*/
|
|
36
|
+
export function holdsKey(tree: unknown, prefix: string): boolean {
|
|
37
|
+
if (Array.isArray(tree)) return tree.some(child => holdsKey(child, prefix))
|
|
38
|
+
if (typeof tree !== 'object' || tree === null) return false
|
|
39
|
+
const { props, children } = tree as { props?: { key?: unknown }; children?: unknown }
|
|
40
|
+
if (typeof props?.key === 'string' && props.key.startsWith(prefix)) return true
|
|
41
|
+
return holdsKey(children, prefix)
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
/**
|
|
24
45
|
* Parses the frontmatter of a Markdown file into flat `key: value` pairs.
|
|
25
46
|
*
|
|
@@ -157,6 +178,7 @@ export const register: Register = on => {
|
|
|
157
178
|
on('session.start', async ($, e, next) => {
|
|
158
179
|
await $.command.register({ name: 'agents-panel', description: 'Show or hide the agents panel' })
|
|
159
180
|
await loadAgents($)
|
|
181
|
+
void $.ui.open({ id: PANE, title: TITLE })
|
|
160
182
|
$.clock.every(1000, () => void pollRunning($))
|
|
161
183
|
return next(e)
|
|
162
184
|
})
|
|
@@ -183,8 +205,13 @@ export const register: Register = on => {
|
|
|
183
205
|
return result
|
|
184
206
|
}).catch(($, e, next) => next(e))
|
|
185
207
|
|
|
186
|
-
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
208
|
+
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e, next) => {
|
|
187
209
|
const { Box, Text, Button } = $.ui.resolve(e)
|
|
210
|
+
// An add-on that depends on this panel, such as custom-crab-agents, may draw beneath this hook; its section joins the panel.
|
|
211
|
+
const below: RenderElement | undefined = await next(e).catch(() => undefined)
|
|
212
|
+
const addOn = below !== undefined && holdsKey(below, ADDON_KEY) ? below : undefined
|
|
213
|
+
/** Puts an add-on's section under the panel's own tree, when one was drawn beneath. */
|
|
214
|
+
const withAddOn = (own: RenderElement) => (addOn === undefined ? own : <Box flexDirection="column" gap={1}>{own}{addOn}</Box>)
|
|
188
215
|
const list = await read($, agents)
|
|
189
216
|
const runs = await read($, running)
|
|
190
217
|
const now = await $.clock.now()
|
|
@@ -202,11 +229,11 @@ export const register: Register = on => {
|
|
|
202
229
|
)
|
|
203
230
|
|
|
204
231
|
if (list.length === 0) {
|
|
205
|
-
return (
|
|
206
|
-
<Box flexDirection="column" gap={1}>
|
|
232
|
+
return withAddOn(
|
|
233
|
+
<Box key={`${OWN_KEY}body`} flexDirection="column" gap={1}>
|
|
207
234
|
{header}
|
|
208
235
|
<Text dimColor>No agents in {AGENTS_DIR} or ~/{AGENTS_DIR}. Add one with /agents.</Text>
|
|
209
|
-
</Box
|
|
236
|
+
</Box>,
|
|
210
237
|
)
|
|
211
238
|
}
|
|
212
239
|
|
|
@@ -264,14 +291,14 @@ export const register: Register = on => {
|
|
|
264
291
|
)
|
|
265
292
|
})()
|
|
266
293
|
|
|
267
|
-
return (
|
|
268
|
-
<Box flexDirection="column" gap={1}>
|
|
294
|
+
return withAddOn(
|
|
295
|
+
<Box key={`${OWN_KEY}body`} flexDirection="column" gap={1}>
|
|
269
296
|
{header}
|
|
270
297
|
{input}
|
|
271
298
|
{section('project')}
|
|
272
299
|
{section('user')}
|
|
273
300
|
<Text dimColor>click ▶ run to start one · /agents-panel to hide</Text>
|
|
274
|
-
</Box
|
|
301
|
+
</Box>,
|
|
275
302
|
)
|
|
276
303
|
})
|
|
277
304
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "custom-crab-agents",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Draws each subagent in the agents panel as a crab with a hat: effort tier, model, steps, context, tokens, cost and time.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "g3ntrovert"
|
|
7
7
|
},
|
|
8
|
-
"types": "./types/index.d.ts"
|
|
8
|
+
"types": "./types/index.d.ts",
|
|
9
|
+
"dependencies": [
|
|
10
|
+
"agents-panel"
|
|
11
|
+
]
|
|
9
12
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// packages/g3n-mods/mods/custom-crab-agents/hooks/register.tsx
|
|
2
2
|
import { atom, read, update } from 'claude-code'
|
|
3
|
-
import type { EngineInterface, Register } from 'claude-code'
|
|
3
|
+
import type { EngineInterface, Register, RenderElement } from 'claude-code'
|
|
4
4
|
|
|
5
5
|
import type { Crab, CrabSteps, PlannedItem } from '../types'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
const
|
|
7
|
+
/** The agents-panel mod's pane, which this mod depends on and draws its crabs into. */
|
|
8
|
+
const PANEL = 'agents-panel'
|
|
9
|
+
|
|
10
|
+
/** Key prefix of the agents panel's own elements, and the one this section carries so the panel shows it. */
|
|
11
|
+
const PANEL_KEY = 'agents:'
|
|
12
|
+
const ADDON_KEY = 'addon:'
|
|
9
13
|
const crabs = atom({ plugin: 'custom-crab-agents', key: 'crabs' } as const, [])
|
|
10
14
|
const planned = atom({ plugin: 'custom-crab-agents', key: 'planned' } as const, [])
|
|
11
15
|
const contextWindow = atom({ plugin: 'custom-crab-agents', key: 'window' } as const, 1_000_000)
|
|
@@ -178,6 +182,21 @@ const isNumber = (v: unknown): v is number => typeof v === 'number'
|
|
|
178
182
|
/** Tells whether `v` is a non-null object. */
|
|
179
183
|
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null
|
|
180
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Tells whether a drawn tree holds an element whose key starts with `prefix`.
|
|
187
|
+
*
|
|
188
|
+
* @param tree - Tree as a `ui.render` chain returned it, or one of its children.
|
|
189
|
+
* @param prefix - Key prefix to look for, e.g. `agents:`.
|
|
190
|
+
* @returns True when some element's key starts with `prefix`.
|
|
191
|
+
*/
|
|
192
|
+
export function holdsKey(tree: unknown, prefix: string): boolean {
|
|
193
|
+
if (Array.isArray(tree)) return tree.some(child => holdsKey(child, prefix))
|
|
194
|
+
if (typeof tree !== 'object' || tree === null) return false
|
|
195
|
+
const { props, children } = tree as { props?: { key?: unknown }; children?: unknown }
|
|
196
|
+
if (typeof props?.key === 'string' && props.key.startsWith(prefix)) return true
|
|
197
|
+
return holdsKey(children, prefix)
|
|
198
|
+
}
|
|
199
|
+
|
|
181
200
|
/**
|
|
182
201
|
* Applies `patch` to every crab that `match` selects.
|
|
183
202
|
*
|
|
@@ -190,14 +209,13 @@ async function patchCrab($: EngineInterface, match: (crab: Crab) => boolean, pat
|
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
/**
|
|
193
|
-
* Records a subagent the main thread just started
|
|
212
|
+
* Records a subagent the main thread just started.
|
|
194
213
|
*
|
|
195
214
|
* @param $ - Engine handle the hook received, see `EngineInterface`.
|
|
196
215
|
* @param crab - New running crab to add, see `Crab`.
|
|
197
216
|
*/
|
|
198
217
|
async function startCrab($: EngineInterface, crab: Crab): Promise<void> {
|
|
199
218
|
await update($, crabs, list => [...list.filter(one => one.id !== crab.id), crab].slice(-KEEP))
|
|
200
|
-
void $.ui.open({ id: PANE, title: TITLE })
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
/**
|
|
@@ -332,27 +350,17 @@ async function readWindow($: EngineInterface): Promise<void> {
|
|
|
332
350
|
}
|
|
333
351
|
|
|
334
352
|
/**
|
|
335
|
-
* Registers this mod's
|
|
353
|
+
* Registers this mod's session, tool, subagent and turn hooks, and its section of the agents panel.
|
|
336
354
|
*
|
|
337
355
|
* @param on - Hook registration function the engine passes in.
|
|
338
356
|
*/
|
|
339
357
|
export const register: Register = on => {
|
|
340
358
|
on('session.start', async ($, e, next) => {
|
|
341
|
-
await $.command.register({ name: 'crabs', description: 'Show or hide the subagent crabs' })
|
|
342
359
|
await readWindow($)
|
|
343
360
|
$.clock.every(1000, () => void tick($))
|
|
344
361
|
return next(e)
|
|
345
362
|
})
|
|
346
363
|
|
|
347
|
-
on('command.run', { command: 'crabs' }, async $ => {
|
|
348
|
-
if ((await $.ui.panes()).some(pane => pane.id === PANE)) {
|
|
349
|
-
await $.ui.close({ id: PANE })
|
|
350
|
-
return { text: 'Crabs hidden.' }
|
|
351
|
-
}
|
|
352
|
-
await $.ui.open({ id: PANE, title: TITLE })
|
|
353
|
-
return { text: 'Crabs shown.' }
|
|
354
|
-
})
|
|
355
|
-
|
|
356
364
|
on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
|
|
357
365
|
if (e.agentId !== undefined) return next(e)
|
|
358
366
|
const startedAt = await $.clock.now()
|
|
@@ -398,13 +406,22 @@ export const register: Register = on => {
|
|
|
398
406
|
return result
|
|
399
407
|
}).catch(($, e, next) => next(e))
|
|
400
408
|
|
|
401
|
-
|
|
409
|
+
// Draws into the agents panel: under the panel's tree when the panel's hook lies beneath this one, or alone with an
|
|
410
|
+
// `addon:` key that the panel's hook picks up when it lies above. Either way the crabs end up under the agents.
|
|
411
|
+
on('ui.render', { component: 'Pane', requestId: PANEL }, async ($, e, next) => {
|
|
402
412
|
const { Box, Text } = $.ui.resolve(e)
|
|
413
|
+
const below: RenderElement | undefined = await next(e).catch(() => undefined)
|
|
414
|
+
const panel = below !== undefined && holdsKey(below, PANEL_KEY) ? below : undefined
|
|
415
|
+
/** Puts the crabs' section under the panel's tree, when it was drawn beneath. */
|
|
416
|
+
const joined = (section: RenderElement) => (panel === undefined ? section : <Box flexDirection="column" gap={1}>{panel}{section}</Box>)
|
|
417
|
+
|
|
403
418
|
const list = await read($, crabs)
|
|
404
419
|
const plan = await read($, planned)
|
|
405
420
|
const window = await read($, contextWindow)
|
|
406
421
|
const now = await $.clock.now()
|
|
407
|
-
if (list.length === 0 && plan.length === 0)
|
|
422
|
+
if (list.length === 0 && plan.length === 0) {
|
|
423
|
+
return joined(<Box key={`${ADDON_KEY}crabs`}><Text dimColor>No subagents yet. Each one shows up here as a crab.</Text></Box>)
|
|
424
|
+
}
|
|
408
425
|
|
|
409
426
|
const barWidth = Math.max(120, (e.props.bodyColumns - 10) * 7)
|
|
410
427
|
/**
|
|
@@ -484,8 +501,9 @@ export const register: Register = on => {
|
|
|
484
501
|
const running = list.filter(crab => crab.status === 'running')
|
|
485
502
|
const done = list.filter(crab => crab.status !== 'running')
|
|
486
503
|
|
|
487
|
-
return (
|
|
488
|
-
<Box flexDirection="column">
|
|
504
|
+
return joined(
|
|
505
|
+
<Box key={`${ADDON_KEY}crabs`} flexDirection="column">
|
|
506
|
+
<Text bold>Subagents</Text>
|
|
489
507
|
{running.map(card)}
|
|
490
508
|
{done.length > 0 && <Text dimColor>▾ Finished · {done.length}</Text>}
|
|
491
509
|
{done.slice().reverse().map(card)}
|
|
@@ -502,7 +520,7 @@ export const register: Register = on => {
|
|
|
502
520
|
</Box>
|
|
503
521
|
</Box>
|
|
504
522
|
))}
|
|
505
|
-
</Box
|
|
523
|
+
</Box>,
|
|
506
524
|
)
|
|
507
525
|
})
|
|
508
526
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delete-guard",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Holds rm -rf and other permanent deletes on a card that lists what they remove, until you press Delete or Cancel.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "g3ntrovert"
|
|
7
7
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "modules": ["./register.
|
|
1
|
+
{ "modules": ["./register.tsx"] }
|