@g3nomaly/g3n-mods 0.1.6 → 0.1.8

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 (39) hide show
  1. package/.claude-plugin/marketplace.json +8 -2
  2. package/README.md +13 -6
  3. package/mods/agent-adventure/.claude-plugin/plugin.json +23 -0
  4. package/mods/agent-adventure/hooks/content.ts +263 -0
  5. package/mods/agent-adventure/hooks/engine.ts +1125 -0
  6. package/mods/agent-adventure/hooks/hooks.json +1 -0
  7. package/mods/agent-adventure/hooks/register.tsx +465 -0
  8. package/mods/agent-adventure/hooks/sprite.ts +424 -0
  9. package/mods/agent-adventure/types/index.d.ts +248 -0
  10. package/mods/agent-adventure/view/panes.json +8 -0
  11. package/mods/agent-adventure/view/view.css +230 -0
  12. package/mods/agent-adventure/view/view.ts +308 -0
  13. package/mods/agents-panel/.claude-plugin/plugin.json +9 -1
  14. package/mods/agents-panel/hooks/register.tsx +18 -4
  15. package/mods/agents-panel/view/panes.json +8 -0
  16. package/mods/context-bar/.claude-plugin/plugin.json +9 -1
  17. package/mods/context-bar/hooks/register.tsx +4 -3
  18. package/mods/context-bar/view/panes.json +8 -0
  19. package/mods/custom-crab-agents/hooks/register.tsx +15 -1
  20. package/mods/delete-guard/.claude-plugin/plugin.json +9 -1
  21. package/mods/delete-guard/hooks/register.tsx +15 -5
  22. package/mods/delete-guard/view/panes.json +8 -0
  23. package/mods/focus-mode/.claude-plugin/plugin.json +6 -0
  24. package/mods/focus-mode/hooks/register.tsx +7 -4
  25. package/mods/focus-mode/view/panes.json +8 -0
  26. package/mods/mini-game/.claude-plugin/plugin.json +10 -2
  27. package/mods/mini-game/hooks/bubbles.ts +27 -0
  28. package/mods/mini-game/hooks/minesweeper.ts +108 -0
  29. package/mods/mini-game/hooks/mole.ts +91 -0
  30. package/mods/mini-game/hooks/register.tsx +206 -148
  31. package/mods/mini-game/types/index.d.ts +18 -1
  32. package/mods/mini-game/view/panes.json +8 -0
  33. package/mods/tool-timeline/.claude-plugin/plugin.json +9 -1
  34. package/mods/tool-timeline/hooks/register.tsx +18 -4
  35. package/mods/tool-timeline/view/panes.json +8 -0
  36. package/mods/you-should-know/.claude-plugin/plugin.json +3 -3
  37. package/mods/you-should-know/hooks/register.tsx +173 -45
  38. package/mods/you-should-know/types/index.d.ts +13 -4
  39. package/package.json +2 -1
@@ -2,84 +2,204 @@
2
2
  import { atom, read, update } from 'claude-code'
3
3
  import type { EngineInterface, Register } from 'claude-code'
4
4
 
5
- import type { Card } from '../types'
5
+ import type { Card, Point, PointKind } from '../types'
6
6
 
7
- const cards = atom({ plugin: 'you-should-know', key: 'cards' } as const, [])
7
+ const notes = atom({ plugin: 'you-should-know', key: 'notes' } as const, [])
8
8
 
9
- const DEFAULT_MIN_CHARS = 1500
9
+ const DEFAULT_MIN_CHARS = 800
10
10
  const DEFAULT_MODEL = 'haiku'
11
11
  const TAIL = 200
12
12
  const KEEP = 20
13
13
  /** Most of a reply sent to the model; the end of a reply carries its conclusion. */
14
14
  const MAX_PROMPT_CHARS = 40_000
15
+ /** Most questions a card lists, and the longest one kept whole. */
16
+ const MAX_QUESTIONS = 3
17
+ const MAX_QUESTION_CHARS = 220
18
+ /** Most changes, conclusions and facts the model adds to a card. */
19
+ const MAX_MODEL_POINTS = 3
20
+ /** Most of the user's request sent beside the reply. */
21
+ const MAX_REQUEST_CHARS = 4000
22
+ /** Kinds the model may tag a point with, in the order a card lists them; questions come from the reply itself. */
23
+ const MODEL_KINDS: readonly PointKind[] = ['change', 'conclusion', 'fact']
24
+
25
+ /**
26
+ * Each tag's look: its name, its color, the dark tint of its round icon, and the icon's mark, drawn in a 40 by 40 box
27
+ * in `currentColor`, which `iconSvg` fills with the tag's color.
28
+ * `badge` is the mark's character for the terminal, which draws no SVG.
29
+ */
30
+ const TAGS: Readonly<Record<PointKind, { badge: string; name: string; color: string; tint: string; mark: string }>> = {
31
+ question: {
32
+ badge: '?', name: 'Question', color: '#e5b567', tint: '#3d3424',
33
+ mark: '<path d="M15.5 15.5a4.5 4.5 0 1 1 6.3 4.1c-1.2.6-1.8 1.5-1.8 2.8v1" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/><circle cx="20" cy="28.5" r="1.6" fill="currentColor"/>',
34
+ },
35
+ change: {
36
+ badge: '±', name: 'Change', color: '#b48ead', tint: '#36293a',
37
+ mark: '<path d="M13 27l1.6-5.2 9.2-9.2 3.6 3.6-9.2 9.2z" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round"/>',
38
+ },
39
+ conclusion: {
40
+ badge: '✓', name: 'Conclusion', color: '#7cc47f', tint: '#26392a',
41
+ mark: '<path d="M13.5 20.5l4.5 4.5 8.5-9.5" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>',
42
+ },
43
+ fact: {
44
+ badge: 'i', name: 'Fact', color: '#5b8def', tint: '#232e45',
45
+ mark: '<circle cx="20" cy="13.5" r="1.8" fill="currentColor"/><path d="M20 18.5v9.5" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/>',
46
+ },
47
+ }
48
+
49
+ /** Text color on a badge, the card's own dark. */
50
+ const CARD = '#1b1f27'
51
+
52
+ /** Width of the longest tag name, so the points' text starts level. */
53
+ const NAME_CELLS = Math.max(...Object.values(TAGS).map(tag => tag.name.length))
54
+
55
+ /**
56
+ * Spacing of the card per surface. The terminal counts whole cells; the Claude Code extension turns a number into CSS
57
+ * (`ch` across, `lh` down) and takes fractions, so there the card gets room inside its border and between its lines.
58
+ */
59
+ const SPACING = {
60
+ terminal: { paddingX: 1, paddingY: 0, rowGap: 0 },
61
+ other: { paddingX: 1.5, paddingY: 0.6, rowGap: 0.35 },
62
+ } as const
63
+
64
+ /** Size of a tag's round icon, in pixels. */
65
+ const ICON_PX = 18
66
+
67
+ /**
68
+ * Draws a tag's round icon: its tint filling the circle and its mark in the tag's color.
69
+ *
70
+ * @param kind - The tag, see `TAGS`.
71
+ * @returns The SVG markup as a string.
72
+ */
73
+ function iconSvg(kind: PointKind): string {
74
+ const tag = TAGS[kind]
75
+ const mark = tag.mark.replaceAll('currentColor', tag.color)
76
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${ICON_PX}" height="${ICON_PX}" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="${tag.tint}"/>${mark}</svg>`
77
+ }
78
+
15
79
 
16
80
  const SYSTEM = [
17
- 'You read one reply that an AI coding assistant wrote to its user, and pick the single point the user must not miss:',
18
- 'a cost, a risk, a decision they have to make, a breaking change, or a surprising fact.',
19
- 'Answer with JSON only, no prose and no code fence: {"headline": "...", "detail": "..."}.',
20
- 'The headline is a plain statement of at most 70 characters. The detail is one supporting line of at most 120 characters.',
21
- 'Use the reply\'s own facts and numbers. If nothing in the reply stands out, answer {"skip": true}.',
81
+ 'You read the request a user gave an AI coding assistant and the reply it wrote, and pick at most three points that',
82
+ 'tell the user what happened about their request.',
83
+ 'Tag each one: "change" for something the reply did, such as a file, feature, element or setting it added, removed',
84
+ 'or changed; "conclusion" for its direct answer to what the user asked; "fact" only for a cost, a breaking change',
85
+ 'or a decision the user must make. Prefer changes when the reply did work, and list the biggest first.',
86
+ 'Never pick caveats, limitations, what was not checked or tested, how to try it out, or anything the user did not ask about.',
87
+ 'A line saying something was not verified, could not be checked or may need fixing later is a caveat: leave it out.',
88
+ 'Fewer points beat a weak one; an empty list is fine.',
89
+ 'Leave out the questions the reply asks; they are shown elsewhere.',
90
+ 'Write a change as what was done, starting with the verb: "Removed the bulb icon", "Added a round icon per tag".',
91
+ 'Answer with JSON only, no prose and no code fence: {"points": [{"kind": "change", "text": "..."}]}.',
92
+ 'Each text is one plain line of at most 100 characters, in the reply\'s own facts and numbers.',
93
+ 'If nothing in the reply answers the request, answer {"points": []}.',
22
94
  ].join(' ')
23
95
 
24
- const BULB_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><circle cx="20" cy="20" r="20" fill="#3d3424"/><path d="M20 11a6 6 0 0 0-3.6 10.8c.8.6 1.1 1.3 1.1 2.2v1h5v-1c0-.9.3-1.6 1.1-2.2A6 6 0 0 0 20 11z" fill="none" stroke="#e5b567" stroke-width="1.6"/><path d="M17.5 28h5M18.3 30.5h3.4" stroke="#e5b567" stroke-width="1.6" stroke-linecap="round"/></svg>'
25
-
26
96
  /** Collapses every run of whitespace to one space and trims both ends. */
27
97
  function normalize(text: string): string {
28
98
  return text.replace(/\s+/g, ' ').trim()
29
99
  }
30
100
 
31
101
  /**
32
- * @brief Reads the model's answer into a card's headline and detail.
102
+ * @brief Finds the questions a reply asks, in its own words: the sentences of its prose that end in a question mark.
103
+ *
104
+ * Code blocks and inline code holding a `?` are left out, and so is a `?` inside a word or URL, such as `?page=2`.
105
+ * List markers, heading marks and bold or italic marks are taken off.
106
+ *
107
+ * @param text - Full text of the reply.
108
+ * @returns Up to `MAX_QUESTIONS` questions, in order, each cut to `MAX_QUESTION_CHARS`.
109
+ */
110
+ export function extractQuestions(text: string): string[] {
111
+ const prose = text
112
+ .replace(/```[\s\S]*?```/g, '\n')
113
+ .replace(/`([^`\n]*)`/g, (_, code: string) => (code.includes('?') ? ' ' : code))
114
+ const questions: string[] = []
115
+ for (const rawLine of prose.split('\n')) {
116
+ const line = rawLine.replace(/^\s*(?:[-*+]|\d+\.|#+|>)\s+/, '').replace(/\*\*|__/g, '')
117
+ for (const match of line.matchAll(/[^.!?]*\?(?=[\s)"'*_]|$)/g)) {
118
+ const question = normalize(match[0])
119
+ if (question.length >= 8 && !questions.includes(question)) questions.push(question.slice(0, MAX_QUESTION_CHARS))
120
+ }
121
+ }
122
+ return questions.slice(0, MAX_QUESTIONS)
123
+ }
124
+
125
+ /**
126
+ * @brief Reads the model's answer into tagged points.
33
127
  *
34
128
  * @param text - Raw answer from the model.
35
- * @returns The headline and detail, or `undefined` for a skip or anything malformed.
129
+ * @returns The changes, conclusions and facts, at most `MAX_MODEL_POINTS`; empty for a skip or anything malformed.
36
130
  */
37
- export function parseCard(text: string): Omit<Card, 'tail'> | undefined {
131
+ export function parseModelPoints(text: string): Point[] {
38
132
  const json = /\{[\s\S]*\}/.exec(text)?.[0]
39
- if (json === undefined) return undefined
133
+ if (json === undefined) return []
40
134
  let value: unknown
41
135
  try {
42
136
  value = JSON.parse(json)
43
137
  } catch {
44
- return undefined
138
+ return []
139
+ }
140
+ if (typeof value !== 'object' || value === null || !('points' in value) || !Array.isArray(value.points)) return []
141
+ const points: Point[] = []
142
+ for (const item of value.points as unknown[]) {
143
+ if (typeof item !== 'object' || item === null || !('kind' in item) || !('text' in item)) continue
144
+ const { kind, text: line } = item
145
+ const tagged = MODEL_KINDS.find(one => one === kind)
146
+ if (tagged === undefined || typeof line !== 'string' || line.trim() === '') continue
147
+ points.push({ kind: tagged, text: line.trim().slice(0, 140) })
45
148
  }
46
- if (typeof value !== 'object' || value === null || !('headline' in value) || !('detail' in value)) return undefined
47
- const { headline, detail } = value
48
- if (typeof headline !== 'string' || typeof detail !== 'string' || headline.trim().length === 0) return undefined
49
- return { headline: headline.trim().slice(0, 90), detail: detail.trim().slice(0, 160) }
149
+ return points.slice(0, MAX_MODEL_POINTS)
50
150
  }
51
151
 
52
152
  /**
53
- * @brief Asks the model for the reply's key point and keeps the card when there is one.
153
+ * @brief Builds a reply's card: its questions, then the model's changes, conclusions and facts when the reply is long
154
+ * enough.
54
155
  *
55
- * @param $ - Engine handle the model call and the cards state go through.
156
+ * @param $ - Engine handle the model call and the notes state go through.
157
+ * @param request - The user's text the reply answers; empty for a turn started without one.
56
158
  * @param answer - Full text of the assistant's reply.
57
- * @param model - Model that reads the reply.
159
+ * @param model - Model that reads the reply, or `undefined` to skip the call for a short reply.
58
160
  */
59
- async function extract($: EngineInterface, answer: string, model: string): Promise<void> {
60
- const result = await $.model.complete({ model, system: SYSTEM, prompt: answer.slice(-MAX_PROMPT_CHARS), maxTokens: 300 })
61
- if (!result.isAnswered) return
62
- const card = parseCard(result.text)
63
- if (card === undefined) return
161
+ async function buildCard($: EngineInterface, request: string, answer: string, model: string | undefined): Promise<void> {
162
+ const questions: Point[] = extractQuestions(answer).map(text => ({ kind: 'question', text }))
163
+ let found: Point[] = []
164
+ if (model !== undefined) {
165
+ const asked = request.trim() === '' ? '(no typed request; the reply continues earlier work)' : request.slice(0, MAX_REQUEST_CHARS)
166
+ const prompt = `The user asked:\n${asked}\n\nThe reply:\n${answer.slice(-MAX_PROMPT_CHARS)}`
167
+ const result = await $.model.complete({ model, system: SYSTEM, prompt, maxTokens: 400 })
168
+ if (result.isAnswered) found = parseModelPoints(result.text)
169
+ }
170
+ const points = [...questions, ...MODEL_KINDS.flatMap(kind => found.filter(point => point.kind === kind))]
171
+ if (points.length === 0) return
64
172
  const tail = normalize(answer).slice(-TAIL)
65
- await update($, cards, list => [...list.filter(one => one.tail !== tail), { tail, ...card }].slice(-KEEP))
173
+ await update($, notes, list => [...list.filter(one => one.tail !== tail), { tail, points }].slice(-KEEP))
66
174
  }
67
175
 
68
176
  /**
69
177
  * @brief Subscribes the card handlers to the engine's events and renders the card.
70
178
  *
71
179
  * @param on - Subscribes a handler to one engine event.
72
- * @param options - Plugin options: `minChars` sets the shortest reply read, `model` the model that reads it.
180
+ * @param options - Plugin options: `minChars` sets the shortest reply the model reads for conclusions and facts,
181
+ * `model` the model that reads it. A reply that asks a question gets a card at any length.
73
182
  */
74
183
  export const register: Register = (on, options) => {
75
184
  const minChars = Number(options.minChars ?? DEFAULT_MIN_CHARS)
76
185
  const model = typeof options.model === 'string' && options.model.length > 0 ? options.model : DEFAULT_MODEL
77
186
 
187
+ /** The user's text of each running turn, by turn id, so the model reads a reply beside the request it answers. */
188
+ const requests = new Map<string, string>()
189
+
190
+ on('turn.start', async ($, e, next) => {
191
+ requests.set(e.turnId, e.text)
192
+ return next(e)
193
+ })
194
+
78
195
  on('turn.complete', async ($, e, next) => {
79
196
  const result = await next(e)
80
- if (e.agentId === undefined && e.reason === 'answer' && e.answer.length >= minChars) {
197
+ const request = requests.get(e.turnId) ?? ''
198
+ requests.delete(e.turnId)
199
+ if (e.agentId === undefined && e.reason === 'answer') {
81
200
  const answer = e.answer
82
- $.clock.after(0, () => void extract($, answer, model))
201
+ const isLong = answer.length >= minChars
202
+ if (isLong || extractQuestions(answer).length > 0) $.clock.after(0, () => void buildCard($, request, answer, isLong ? model : undefined))
83
203
  }
84
204
  return result
85
205
  }).catch(($, e, next) => next(e))
@@ -88,27 +208,35 @@ export const register: Register = (on, options) => {
88
208
  if (e.props.isSummary === true) return next(e)
89
209
  const text = normalize(e.props.text)
90
210
  if (text.length < 20) return next(e)
91
- const card = (await read($, cards)).find(one => one.tail.endsWith(text.slice(-TAIL)))
211
+ const card: Card | undefined = (await read($, notes)).find(one => one.tail.endsWith(text.slice(-TAIL)))
92
212
  if (card === undefined) return next(e)
93
213
 
94
214
  const { Box, Text } = $.ui.resolve(e)
95
- const icon = e.surface === 'terminal'
96
- ? <Text color="#e5b567">💡</Text>
97
- : (() => {
98
- const { Svg } = $.ui.resolve(e)
99
- return <Svg source={BULB_SVG} alt="key point" width={40} height={40} />
100
- })()
215
+ const space = e.surface === 'terminal' ? SPACING.terminal : SPACING.other
216
+ /** A tag's round icon; the terminal, which draws no SVG, gets its mark's character on its color. */
217
+ const icon = (kind: PointKind) => {
218
+ if (e.surface === 'terminal') return <Text bold color={CARD} backgroundColor={TAGS[kind].color}>{` ${TAGS[kind].badge} `}</Text>
219
+ const { Svg } = $.ui.resolve(e)
220
+ return <Svg source={iconSvg(kind)} alt={TAGS[kind].name} width={ICON_PX} height={ICON_PX} />
221
+ }
101
222
 
223
+ // One line per point, each led by its tag's icon and name; only the tags the reply has show. Rows say `nowrap`: the
224
+ // Claude Code extension wraps a row by default, which would drop a long point under its icon. The point's text
225
+ // wraps inside its own column instead.
102
226
  return (
103
227
  <Box flexDirection="column" gap={1}>
104
228
  {await next(e)}
105
- <Box borderStyle="round" borderColor="#3a3f4b" paddingX={2} paddingY={1} flexDirection="row" columnGap={2} alignItems="center">
106
- {icon}
107
- <Box flexDirection="column">
108
- <Text bold color="#e5b567">YOU SHOULD KNOW</Text>
109
- <Text bold>{card.headline}</Text>
110
- <Text dimColor>{card.detail}</Text>
111
- </Box>
229
+ <Box borderStyle="round" borderColor="#3a3f4b" paddingX={space.paddingX} paddingY={space.paddingY} flexDirection="column" rowGap={space.rowGap}>
230
+ {card.points.map(point => {
231
+ const tag = TAGS[point.kind]
232
+ return (
233
+ <Box flexDirection="row" flexWrap="nowrap" columnGap={1} alignItems="flex-start">
234
+ <Box flexShrink={0}>{icon(point.kind)}</Box>
235
+ <Box width={NAME_CELLS} flexShrink={0}><Text color={tag.color}>{tag.name}</Text></Box>
236
+ <Box flexGrow={1} flexShrink={1} minWidth={0}><Text>{point.text}</Text></Box>
237
+ </Box>
238
+ )
239
+ })}
112
240
  </Box>
113
241
  </Box>
114
242
  )
@@ -1,15 +1,24 @@
1
1
  // packages/g3n-mods/mods/you-should-know/types/index.d.ts
2
2
 
3
- /** The key point of one long reply, keyed by the reply's last characters so its message can find it. */
3
+ /** What a point is: a question the reply asks the user, a change it made, its answer to the request, or a fact they must act on. */
4
+ export type PointKind = 'question' | 'change' | 'conclusion' | 'fact'
5
+
6
+ /** One tagged line of a card. */
7
+ export type Point = {
8
+ kind: PointKind
9
+ /** For a question, the reply's own words; else one plain line */
10
+ text: string
11
+ }
12
+
13
+ /** The points of one reply, keyed by the reply's last characters so its message can find them. */
4
14
  export type Card = {
5
15
  /** The reply's last 200 characters, whitespace collapsed */
6
16
  tail: string
7
- headline: string
8
- detail: string
17
+ points: Point[]
9
18
  }
10
19
 
11
20
  declare module 'claude-code' {
12
21
  interface PluginState {
13
- 'you-should-know': { cards: Card[] }
22
+ 'you-should-know': { notes: Card[] }
14
23
  }
15
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@g3nomaly/g3n-mods",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "The g3n Claude Code mods: panes, status lines, commands and hooks, installed with @g3nomaly/mods.",
5
5
  "type": "module",
6
6
  "files": [
@@ -11,6 +11,7 @@
11
11
  "build": "bun run build:marketplace",
12
12
  "typecheck": "tsc --noEmit && tsc --noEmit --project mods/tsconfig.json && tsc --noEmit --project mods/tsconfig.view.json",
13
13
  "validate": "bun scripts/validate.ts",
14
+ "test": "bun test tests",
14
15
  "build:marketplace": "bun scripts/build-marketplace.ts",
15
16
  "types": "bun scripts/sync-types.ts"
16
17
  },