@g3nomaly/g3n-mods 0.0.0-stage → 0.1.1

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.
Files changed (45) hide show
  1. package/.claude-plugin/marketplace.json +68 -0
  2. package/LICENSE +95 -0
  3. package/mods/agents-panel/.claude-plugin/plugin.json +9 -0
  4. package/mods/agents-panel/hooks/hooks.json +1 -0
  5. package/mods/agents-panel/hooks/register.tsx +277 -0
  6. package/mods/agents-panel/types/index.d.ts +24 -0
  7. package/mods/context-bar/.claude-plugin/plugin.json +9 -0
  8. package/mods/context-bar/hooks/hooks.json +1 -0
  9. package/mods/context-bar/hooks/register.tsx +215 -0
  10. package/mods/context-bar/types/index.d.ts +25 -0
  11. package/mods/custom-crab-agents/.claude-plugin/plugin.json +9 -0
  12. package/mods/custom-crab-agents/hooks/hooks.json +1 -0
  13. package/mods/custom-crab-agents/hooks/register.tsx +508 -0
  14. package/mods/custom-crab-agents/types/index.d.ts +45 -0
  15. package/mods/delete-guard/.claude-plugin/plugin.json +9 -0
  16. package/mods/delete-guard/hooks/hooks.json +1 -0
  17. package/mods/delete-guard/hooks/register.ts +151 -0
  18. package/mods/delete-guard/types/index.d.ts +14 -0
  19. package/mods/focus-mode/.claude-plugin/plugin.json +17 -0
  20. package/mods/focus-mode/hooks/hooks.json +1 -0
  21. package/mods/focus-mode/hooks/register.tsx +184 -0
  22. package/mods/focus-mode/types/index.d.ts +14 -0
  23. package/mods/handoff/.claude-plugin/plugin.json +9 -0
  24. package/mods/handoff/hooks/hooks.json +1 -0
  25. package/mods/handoff/hooks/register.tsx +156 -0
  26. package/mods/handoff/types/index.d.ts +15 -0
  27. package/mods/hide-personal-data/.claude-plugin/plugin.json +17 -0
  28. package/mods/hide-personal-data/hooks/hooks.json +1 -0
  29. package/mods/hide-personal-data/hooks/register.tsx +196 -0
  30. package/mods/hide-personal-data/types/index.d.ts +17 -0
  31. package/mods/mini-game/.claude-plugin/plugin.json +9 -0
  32. package/mods/mini-game/hooks/hooks.json +1 -0
  33. package/mods/mini-game/hooks/register.tsx +273 -0
  34. package/mods/mini-game/types/index.d.ts +21 -0
  35. package/mods/tool-timeline/.claude-plugin/plugin.json +9 -0
  36. package/mods/tool-timeline/hooks/hooks.json +1 -0
  37. package/mods/tool-timeline/hooks/register.tsx +311 -0
  38. package/mods/tool-timeline/types/index.d.ts +25 -0
  39. package/mods/tsconfig.json +17 -0
  40. package/mods/you-should-know/.claude-plugin/plugin.json +23 -0
  41. package/mods/you-should-know/hooks/hooks.json +1 -0
  42. package/mods/you-should-know/hooks/register.tsx +116 -0
  43. package/mods/you-should-know/types/index.d.ts +15 -0
  44. package/package.json +25 -4
  45. package/README.md +0 -3
@@ -0,0 +1,215 @@
1
+ // packages/g3n-mods/mods/context-bar/hooks/register.tsx
2
+ import { atom, read, update } from 'claude-code'
3
+ import type { EngineInterface, Register } from 'claude-code'
4
+
5
+ import type { ContextRow, ContextSnapshot } from '../types'
6
+
7
+ const PANE = 'context-bar'
8
+ const TITLE = 'Context'
9
+ const snapshot = atom({ plugin: 'context-bar', key: 'snapshot' } as const, null)
10
+
11
+ /** Fewest milliseconds between two measurements; measure events in between fold into one. */
12
+ const MEASURE_GAP_MS = 2000
13
+
14
+ /** Approximate pixel width of one cell of the editor's code font. */
15
+ const CELL_PX = 7
16
+
17
+ const FREE_COLOR = '#2b3240'
18
+ const MARKER_COLOR = '#e6c167'
19
+
20
+ /** Bar label and color per source, matched against the engine's category names in order. */
21
+ const SOURCES: ReadonlyArray<{ match: RegExp; label: string; color: string }> = [
22
+ { match: /system prompt/i, label: 'system prompt', color: '#6b8fd6' },
23
+ { match: /mcp/i, label: 'mcp tools', color: '#9b87f5' },
24
+ { match: /tool/i, label: 'tools', color: '#5fb3c4' },
25
+ { match: /agent/i, label: 'agents', color: '#7cc47f' },
26
+ { match: /memory/i, label: 'memory files', color: '#e6c167' },
27
+ { match: /skill/i, label: 'skills', color: '#f09bb5' },
28
+ { match: /message/i, label: 'messages', color: '#e0775a' },
29
+ ]
30
+
31
+ /**
32
+ * Formats a token count the way the bar labels it, with `k` and `M` suffixes.
33
+ *
34
+ * @param tokens - Token count, e.g. `52000`.
35
+ * @returns Short label, e.g. `892`, `4.2k`, `52k` or `1M`.
36
+ */
37
+ export function formatTokens(tokens: number): string {
38
+ if (tokens >= 1_000_000) return `${+(tokens / 1_000_000).toFixed(1)}M`
39
+ if (tokens >= 10_000) return `${Math.round(tokens / 1000)}k`
40
+ if (tokens >= 1000) return `${+(tokens / 1000).toFixed(1)}k`
41
+ return String(Math.round(tokens))
42
+ }
43
+
44
+ /**
45
+ * Rounds `part` as a percent of `whole`.
46
+ *
47
+ * @returns The rounded percent, or `0` when `whole` is not positive.
48
+ */
49
+ function percentOf(part: number, whole: number): number {
50
+ return whole > 0 ? Math.round((part / whole) * 100) : 0
51
+ }
52
+
53
+ /**
54
+ * @brief Measures the context window by source.
55
+ *
56
+ * Categories of the same source add up into one row; deferred tools and the
57
+ * compaction buffer are left out.
58
+ *
59
+ * @param $ - Engine handle the hook received, see `EngineInterface`.
60
+ * @returns The snapshot, or `null` before the session has measured anything.
61
+ */
62
+ async function measure($: EngineInterface): Promise<ContextSnapshot | null> {
63
+ const usage = await $.session.usage({ breakdown: 'summary' })
64
+ const breakdown = usage.context.breakdown
65
+ if (breakdown === undefined) return null
66
+
67
+ const rows: ContextRow[] = SOURCES.map(source => ({ label: source.label, tokens: 0, color: source.color }))
68
+ let free = 0
69
+ for (const category of breakdown.categories) {
70
+ if (category.kind === 'free') free += category.tokens
71
+ if (category.kind !== 'used') continue
72
+ const index = SOURCES.findIndex(source => source.match.test(category.name))
73
+ const row = index === -1 ? undefined : rows[index]
74
+ if (row !== undefined) row.tokens += category.tokens
75
+ }
76
+
77
+ return {
78
+ rows: [...rows, { label: 'free', tokens: free, color: FREE_COLOR }],
79
+ used: breakdown.totalTokens,
80
+ window: breakdown.rawMaxTokens,
81
+ compactAt: breakdown.isAutoCompactEnabled ? breakdown.autoCompactThreshold : undefined,
82
+ measuredAt: await $.clock.now(),
83
+ }
84
+ }
85
+
86
+ /**
87
+ * The bar as one SVG: a rect per source, the free remainder, and a mark at the compaction point.
88
+ *
89
+ * @param snap - Snapshot to draw, see `ContextSnapshot`.
90
+ * @param width - Width of the SVG in pixels.
91
+ * @returns The SVG markup as a string.
92
+ */
93
+ function barSvg(snap: ContextSnapshot, width: number): string {
94
+ const height = 14
95
+ let x = 0
96
+ const rects = snap.rows.map(row => {
97
+ const w = snap.window > 0 ? (row.tokens / snap.window) * width : 0
98
+ const rect = `<rect x="${x.toFixed(2)}" y="0" width="${w.toFixed(2)}" height="${height}" fill="${row.color}"/>`
99
+ x += w
100
+ return rect
101
+ })
102
+ const marker = snap.compactAt === undefined || snap.window === 0
103
+ ? ''
104
+ : `<rect x="${Math.min(width - 2, (snap.compactAt / snap.window) * width).toFixed(2)}" y="0" width="2" height="${height}" fill="${MARKER_COLOR}"/>`
105
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="${width}" height="${height}" rx="3" fill="${FREE_COLOR}"/>${rects.join('')}${marker}</svg>`
106
+ }
107
+
108
+ let lastMeasured = 0
109
+ let isScheduled = false
110
+
111
+ /**
112
+ * Measures now, or once the gap since the last measurement has passed; calls in between fold into one.
113
+ *
114
+ * @param $ - Engine handle the hook received, see `EngineInterface`.
115
+ */
116
+ async function refresh($: EngineInterface): Promise<void> {
117
+ const now = await $.clock.now()
118
+ const wait = lastMeasured + MEASURE_GAP_MS - now
119
+ if (wait > 0) {
120
+ if (isScheduled) return
121
+ isScheduled = true
122
+ $.clock.after(wait, () => {
123
+ isScheduled = false
124
+ void refresh($)
125
+ })
126
+ return
127
+ }
128
+ lastMeasured = now
129
+ const snap = await measure($).catch(() => null)
130
+ if (snap !== null) await update($, snapshot, () => snap)
131
+ }
132
+
133
+ /**
134
+ * Registers this mod's `context-bar` command, its session, measure and turn hooks, and its pane.
135
+ *
136
+ * @param on - Hook registration function the engine passes in.
137
+ */
138
+ export const register: Register = on => {
139
+ on('session.start', async ($, e, next) => {
140
+ await $.command.register({ name: 'context-bar', description: 'Show or hide the context window bar' })
141
+ void $.ui.open({ id: PANE, title: TITLE })
142
+ void refresh($)
143
+ return next(e)
144
+ })
145
+
146
+ on('command.run', { command: 'context-bar' }, async $ => {
147
+ const isOpen = (await $.ui.panes()).some(pane => pane.id === PANE)
148
+ if (isOpen) {
149
+ await $.ui.close({ id: PANE })
150
+ return { text: 'Context bar hidden.' }
151
+ }
152
+ await $.ui.open({ id: PANE, title: TITLE })
153
+ await refresh($)
154
+ return { text: 'Context bar shown.' }
155
+ })
156
+
157
+ on('session.measure', async ($, e, next) => {
158
+ const result = await next(e)
159
+ void refresh($)
160
+ return result
161
+ })
162
+
163
+ on('turn.complete', async ($, e, next) => {
164
+ const result = await next(e)
165
+ if (e.agentId === undefined) void refresh($)
166
+ return result
167
+ })
168
+
169
+ on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
170
+ const snap = await read($, snapshot)
171
+ const { Box, Text } = $.ui.resolve(e)
172
+ if (snap === null) return <Text dimColor>No context measured yet. It fills in after the first reply.</Text>
173
+
174
+ const columns = Math.max(24, e.props.bodyColumns)
175
+ const used = percentOf(snap.used, snap.window)
176
+ const compacts = snap.compactAt === undefined ? '' : ` · compacts at ${formatTokens(snap.compactAt)}`
177
+
178
+ let bar
179
+ if (e.surface === 'terminal') {
180
+ const cells = snap.rows.map(row => ({ row, n: Math.round((row.tokens / Math.max(1, snap.window)) * columns) }))
181
+ bar = <Text>{cells.map(({ row, n }) => <Text color={row.color}>{'█'.repeat(n)}</Text>)}</Text>
182
+ } else {
183
+ const { Svg } = $.ui.resolve(e)
184
+ const width = columns * CELL_PX
185
+ bar = <Svg source={barSvg(snap, width)} alt={`context ${used}% used`} width={width} height={14} />
186
+ }
187
+
188
+ return (
189
+ <Box flexDirection="column" gap={1}>
190
+ <Box flexDirection="row" justifyContent="space-between">
191
+ <Text>
192
+ <Text color="#e0775a">◆ </Text>
193
+ <Text bold>context window</Text>
194
+ </Text>
195
+ <Text>
196
+ <Text bold>{formatTokens(snap.used)}</Text>
197
+ <Text dimColor> of {formatTokens(snap.window)}{compacts} </Text>
198
+ <Text bold color="#9be39f" backgroundColor="#2f4a33"> {used}% </Text>
199
+ </Text>
200
+ </Box>
201
+ {bar}
202
+ <Box flexDirection="row" flexWrap="wrap" columnGap={3}>
203
+ {snap.rows.map(row => (
204
+ <Text>
205
+ <Text color={row.color}>■ </Text>
206
+ <Text dimColor={row.label === 'free'}>{row.label} </Text>
207
+ <Text bold={row.label !== 'free'} dimColor={row.label === 'free'}>{formatTokens(row.tokens)}</Text>
208
+ {row.label === 'free' ? '' : <Text dimColor> {percentOf(row.tokens, snap.window)}%</Text>}
209
+ </Text>
210
+ ))}
211
+ </Box>
212
+ </Box>
213
+ )
214
+ })
215
+ }
@@ -0,0 +1,25 @@
1
+ // packages/g3n-mods/mods/context-bar/types/index.d.ts
2
+
3
+ /** One source of context use as the bar draws it, e.g. `{ label: "mcp tools", tokens: 52000, color: "#9b87f5" }`. */
4
+ export type ContextRow = {
5
+ label: string
6
+ tokens: number
7
+ color: string
8
+ }
9
+
10
+ /** The context window as last measured: what each source uses, the window and where compaction runs. */
11
+ export type ContextSnapshot = {
12
+ rows: ContextRow[]
13
+ used: number
14
+ window: number
15
+ /** Token count at which auto-compaction runs; absent when it is off */
16
+ compactAt?: number
17
+ /** Milliseconds since the epoch */
18
+ measuredAt: number
19
+ }
20
+
21
+ declare module 'claude-code' {
22
+ interface PluginState {
23
+ 'context-bar': { snapshot: ContextSnapshot | null }
24
+ }
25
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "custom-crab-agents",
3
+ "version": "0.1.0",
4
+ "description": "Each subagent as a crab with a hat: effort tier, model, steps, context, tokens, cost and time.",
5
+ "author": {
6
+ "name": "g3ntrovert"
7
+ },
8
+ "types": "./types/index.d.ts"
9
+ }
@@ -0,0 +1 @@
1
+ { "modules": ["./register.tsx"] }