@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.
- package/.claude-plugin/marketplace.json +68 -0
- package/LICENSE +95 -0
- package/mods/agents-panel/.claude-plugin/plugin.json +9 -0
- package/mods/agents-panel/hooks/hooks.json +1 -0
- package/mods/agents-panel/hooks/register.tsx +277 -0
- package/mods/agents-panel/types/index.d.ts +24 -0
- package/mods/context-bar/.claude-plugin/plugin.json +9 -0
- package/mods/context-bar/hooks/hooks.json +1 -0
- package/mods/context-bar/hooks/register.tsx +215 -0
- package/mods/context-bar/types/index.d.ts +25 -0
- package/mods/custom-crab-agents/.claude-plugin/plugin.json +9 -0
- package/mods/custom-crab-agents/hooks/hooks.json +1 -0
- package/mods/custom-crab-agents/hooks/register.tsx +508 -0
- package/mods/custom-crab-agents/types/index.d.ts +45 -0
- package/mods/delete-guard/.claude-plugin/plugin.json +9 -0
- package/mods/delete-guard/hooks/hooks.json +1 -0
- package/mods/delete-guard/hooks/register.ts +151 -0
- package/mods/delete-guard/types/index.d.ts +14 -0
- package/mods/focus-mode/.claude-plugin/plugin.json +17 -0
- package/mods/focus-mode/hooks/hooks.json +1 -0
- package/mods/focus-mode/hooks/register.tsx +184 -0
- package/mods/focus-mode/types/index.d.ts +14 -0
- package/mods/handoff/.claude-plugin/plugin.json +9 -0
- package/mods/handoff/hooks/hooks.json +1 -0
- package/mods/handoff/hooks/register.tsx +156 -0
- package/mods/handoff/types/index.d.ts +15 -0
- package/mods/hide-personal-data/.claude-plugin/plugin.json +17 -0
- package/mods/hide-personal-data/hooks/hooks.json +1 -0
- package/mods/hide-personal-data/hooks/register.tsx +196 -0
- package/mods/hide-personal-data/types/index.d.ts +17 -0
- package/mods/mini-game/.claude-plugin/plugin.json +9 -0
- package/mods/mini-game/hooks/hooks.json +1 -0
- package/mods/mini-game/hooks/register.tsx +273 -0
- package/mods/mini-game/types/index.d.ts +21 -0
- package/mods/tool-timeline/.claude-plugin/plugin.json +9 -0
- package/mods/tool-timeline/hooks/hooks.json +1 -0
- package/mods/tool-timeline/hooks/register.tsx +311 -0
- package/mods/tool-timeline/types/index.d.ts +25 -0
- package/mods/tsconfig.json +17 -0
- package/mods/you-should-know/.claude-plugin/plugin.json +23 -0
- package/mods/you-should-know/hooks/hooks.json +1 -0
- package/mods/you-should-know/hooks/register.tsx +116 -0
- package/mods/you-should-know/types/index.d.ts +15 -0
- package/package.json +25 -4
- 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 @@
|
|
|
1
|
+
{ "modules": ["./register.tsx"] }
|