@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
@@ -12,6 +12,20 @@ const PANEL = 'agents-panel'
12
12
  /** Key prefix of the agents panel's own elements, and the one this section carries so the panel shows it. */
13
13
  const PANEL_KEY = 'agents:'
14
14
  const ADDON_KEY = 'addon:'
15
+
16
+ /** Prop the mods viewer adds to every pane it draws; the Claude Code extension draws on the `vscode` surface too, without it. */
17
+ const VIEWER_PROP = 'g3nViewer'
18
+
19
+ /**
20
+ * Tells whether the mods viewer asked for this drawing, rather than the Claude Code extension on the same surface.
21
+ *
22
+ * @param props - The `Pane` props of the `ui.render` argument.
23
+ * @returns `true` when the viewer's prop is set.
24
+ */
25
+ function isViewer(props: object): boolean {
26
+ return (props as Readonly<Record<string, unknown>>)[VIEWER_PROP] === true
27
+ }
28
+
15
29
  const crabs = atom({ plugin: 'custom-crab-agents', key: 'crabs' } as const, [])
16
30
  const planned = atom({ plugin: 'custom-crab-agents', key: 'planned' } as const, [])
17
31
  const contextWindow = atom({ plugin: 'custom-crab-agents', key: 'window' } as const, 1_000_000)
@@ -592,7 +606,7 @@ export const register: Register = on => {
592
606
  const now = await $.clock.now()
593
607
 
594
608
  // The viewer draws the crabs with the mod's own view: hand it the cards as data, inside the key the panel looks for.
595
- if (e.surface === 'vscode') {
609
+ if (e.surface === 'vscode' && isViewer(e.props)) {
596
610
  const { Input } = $.ui.resolve(e)
597
611
  const data: CrabsViewData = {
598
612
  crabs: list.map(crab => {
@@ -5,5 +5,13 @@
5
5
  "author": {
6
6
  "name": "g3ntrovert"
7
7
  },
8
- "types": "./types/index.d.ts"
8
+ "types": "./types/index.d.ts",
9
+ "userConfig": {
10
+ "showInClaudeCode": {
11
+ "type": "boolean",
12
+ "title": "Show in Claude Code",
13
+ "default": false,
14
+ "description": "Open this mod's pane in the Claude Code chat when a session starts. The mods viewer shows it either way."
15
+ }
16
+ }
9
17
  }
@@ -424,6 +424,9 @@ const held: Held[] = []
424
424
  /** The card lists every file instead of the first `SHOWN_FILES`. */
425
425
  let isExpanded = false
426
426
 
427
+ /** The switch stays in the Claude Code chat; else only a waiting delete's card shows there. Set from the plugin options. */
428
+ let isShownInChat = false
429
+
427
430
  /**
428
431
  * Turns the guard on or off, for this session and every later one.
429
432
  *
@@ -436,12 +439,16 @@ async function setGuard($: EngineInterface, isGuarding: boolean): Promise<void>
436
439
  }
437
440
 
438
441
  /**
439
- * Shows the switch again once no delete waits: the pane's title goes back from the card's.
442
+ * Shows the switch again once no delete waits: the pane's title goes back from the card's, or the pane leaves the
443
+ * Claude Code chat when the switch is not shown there.
440
444
  *
441
445
  * @param $ - Engine handle the hook received.
442
446
  */
443
447
  async function showSwitch($: EngineInterface): Promise<void> {
444
- if (held.length === 0) await $.ui.open({ id: PANE, title: TITLE }).catch(() => undefined)
448
+ if (held.length === 0) {
449
+ if (isShownInChat) await $.ui.open({ id: PANE, title: TITLE }).catch(() => undefined)
450
+ else await $.ui.close({ id: PANE }).catch(() => undefined)
451
+ }
445
452
  $.ui.invalidate('ui.render')
446
453
  }
447
454
 
@@ -483,7 +490,8 @@ async function hold($: EngineInterface, card: Omit<Held, 'settle'>, signal: Abor
483
490
  const onAbort = (): void => drop('cancel')
484
491
  signal?.addEventListener('abort', onAbort, { once: true })
485
492
 
486
- // The switch's pane is usually open already: retitle and focus it, then redraw it as the card.
493
+ // A delete waits on an answer, so its card opens in the Claude Code chat whatever the option says: retitle and focus
494
+ // the switch's pane when it is open already, then redraw it as the card.
487
495
  if (held.length === 1) await $.ui.open({ id: PANE, title: HOLD_TITLE, focus: true })
488
496
  $.ui.invalidate('ui.render')
489
497
 
@@ -508,8 +516,10 @@ async function hold($: EngineInterface, card: Omit<Held, 'settle'>, signal: Abor
508
516
  * Registers this mod's `delete-guard` command, its session hook, its Bash tool hook, and its pane: the switch, or the card while a delete waits.
509
517
  *
510
518
  * @param on - Hook registration function the engine passes in.
519
+ * @param options - Plugin options; `showInClaudeCode` keeps the switch in the Claude Code chat.
511
520
  */
512
- export const register: Register = on => {
521
+ export const register: Register = (on, options) => {
522
+ isShownInChat = options.showInClaudeCode === true
513
523
  let trash: string | undefined
514
524
  let home = ''
515
525
 
@@ -524,7 +534,7 @@ export const register: Register = on => {
524
534
  description: 'Turn the permanent-delete guard on or off; it stays that way in later sessions',
525
535
  argumentHint: 'on | off',
526
536
  })
527
- void $.ui.open({ id: PANE, title: TITLE })
537
+ if (isShownInChat) void $.ui.open({ id: PANE, title: TITLE })
528
538
  return next(e)
529
539
  })
530
540
 
@@ -0,0 +1,8 @@
1
+ {
2
+ "panes": [
3
+ {
4
+ "id": "delete-guard",
5
+ "title": "Delete guard"
6
+ }
7
+ ]
8
+ }
@@ -12,6 +12,12 @@
12
12
  "title": "Minutes",
13
13
  "default": 25,
14
14
  "description": "Length of a focus block in minutes"
15
+ },
16
+ "showInClaudeCode": {
17
+ "type": "boolean",
18
+ "title": "Show in Claude Code",
19
+ "default": false,
20
+ "description": "Open this mod's pane in the Claude Code chat when a session starts. The mods viewer shows it either way."
15
21
  }
16
22
  }
17
23
  }
@@ -81,7 +81,7 @@ async function setLength($: EngineInterface, minutes: number): Promise<void> {
81
81
  }
82
82
 
83
83
  /**
84
- * Starts a block of `minutes`, replacing one already running, and shows the pane.
84
+ * Starts a block of `minutes`, replacing one already running.
85
85
  *
86
86
  * @param $ - Engine handle the hook received, see `EngineInterface`.
87
87
  * @param minutes - Length of the block in minutes.
@@ -89,7 +89,7 @@ async function setLength($: EngineInterface, minutes: number): Promise<void> {
89
89
  async function startFocus($: EngineInterface, minutes: number): Promise<void> {
90
90
  const startedAt = await $.clock.now()
91
91
  await update($, block, () => ({ startedAt, endsAt: startedAt + minutes * 60_000, minutes }))
92
- await $.ui.open({ id: PANE, title: TITLE })
92
+ $.ui.invalidate('ui.render')
93
93
  }
94
94
 
95
95
  /**
@@ -144,7 +144,8 @@ async function awayBlock($: EngineInterface): Promise<FocusBlock | null> {
144
144
  * from asking anything during a block: questions are refused, and so are permission prompts.
145
145
  *
146
146
  * @param on - Hook registration function the engine passes in.
147
- * @param options - Plugin options; `minutes` sets the default block length.
147
+ * @param options - Plugin options; `minutes` sets the default block length, `showInClaudeCode` opens the pane in the
148
+ * Claude Code chat at session start.
148
149
  */
149
150
  export const register: Register = (on, options) => {
150
151
  const configured = Number(options.minutes ?? DEFAULT_MINUTES)
@@ -158,7 +159,7 @@ export const register: Register = (on, options) => {
158
159
  description: 'Start a focus block in which Claude asks you nothing (the length you set, or the minutes given), or end the running one',
159
160
  argumentHint: '[minutes] | end',
160
161
  })
161
- void $.ui.open({ id: PANE, title: TITLE })
162
+ if (options.showInClaudeCode === true) void $.ui.open({ id: PANE, title: TITLE })
162
163
  $.clock.every(1000, () => void tick($))
163
164
  return next(e)
164
165
  })
@@ -177,6 +178,8 @@ export const register: Register = (on, options) => {
177
178
  // A length typed with the command becomes the one the pane starts with next time.
178
179
  if (arg !== '') await setLength($, minutes)
179
180
  await startFocus($, minutes)
181
+ // Typed in the chat, so the countdown shows there too.
182
+ await $.ui.open({ id: PANE, title: TITLE })
180
183
  return { text: `Focus block started: ${minutes} min.` }
181
184
  })
182
185
 
@@ -0,0 +1,8 @@
1
+ {
2
+ "panes": [
3
+ {
4
+ "id": "focus-mode",
5
+ "title": "Focus"
6
+ }
7
+ ]
8
+ }
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "mini-game",
3
3
  "version": "0.1.0",
4
- "description": "Minesweeper to play while Claude works.",
4
+ "description": "Minesweeper and whack-a-mole to play while Claude works.",
5
5
  "author": {
6
6
  "name": "g3ntrovert"
7
7
  },
8
- "types": "./types/index.d.ts"
8
+ "types": "./types/index.d.ts",
9
+ "userConfig": {
10
+ "showInClaudeCode": {
11
+ "type": "boolean",
12
+ "title": "Show in Claude Code",
13
+ "default": false,
14
+ "description": "Open this mod's pane in the Claude Code chat when a session starts. The mods viewer shows it either way."
15
+ }
16
+ }
9
17
  }
@@ -0,0 +1,27 @@
1
+ // packages/g3n-mods/mods/mini-game/hooks/bubbles.ts
2
+ import type { Sheet } from '../types'
3
+
4
+ const SIZE = 8
5
+
6
+ /**
7
+ * Creates a sheet with every bubble whole.
8
+ *
9
+ * @param cleared - Sheets popped clean before this one.
10
+ * @returns The new sheet, see `Sheet`.
11
+ */
12
+ export function newSheet(cleared: number): Sheet {
13
+ return { size: SIZE, popped: [], cleared }
14
+ }
15
+
16
+ /**
17
+ * Pops one bubble; popping the last one swaps in a fresh sheet.
18
+ *
19
+ * @param sheet - Sheet to pop on, see `Sheet`.
20
+ * @param bubble - Bubble number, counted row by row from `0`.
21
+ * @returns The sheet after the pop, or the same sheet when the bubble was already popped.
22
+ */
23
+ export function pop(sheet: Sheet, bubble: number): Sheet {
24
+ if (sheet.popped.includes(bubble)) return sheet
25
+ const popped = [...sheet.popped, bubble]
26
+ return popped.length === sheet.size * sheet.size ? newSheet(sheet.cleared + 1) : { ...sheet, popped }
27
+ }
@@ -0,0 +1,108 @@
1
+ // packages/g3n-mods/mods/mini-game/hooks/minesweeper.ts
2
+ import type { Game } from '../types'
3
+
4
+ const SIZE = 9
5
+ const MINES = 10
6
+
7
+ /**
8
+ * Lists the cells around `cell` on a `size` × `size` board.
9
+ *
10
+ * @param cell - Cell number, counted row by row from `0`.
11
+ * @param size - Board width and height in cells.
12
+ * @returns The neighboring cell numbers, from one to eight of them.
13
+ */
14
+ export function neighbors(cell: number, size: number): number[] {
15
+ const row = Math.floor(cell / size)
16
+ const col = cell % size
17
+ const found: number[] = []
18
+ for (let dr = -1; dr <= 1; dr++) {
19
+ for (let dc = -1; dc <= 1; dc++) {
20
+ const r = row + dr
21
+ const c = col + dc
22
+ if ((dr !== 0 || dc !== 0) && r >= 0 && r < size && c >= 0 && c < size) found.push(r * size + c)
23
+ }
24
+ }
25
+ return found
26
+ }
27
+
28
+ /**
29
+ * Counts the mines in the cells around `cell`.
30
+ *
31
+ * @param board - Board to read, see `Game`.
32
+ * @param cell - Cell number to count around.
33
+ * @returns Number of neighboring mines, from `0` to `8`.
34
+ */
35
+ export function adjacentMines(board: Game, cell: number): number {
36
+ return neighbors(cell, board.size).filter(n => board.mines.includes(n)).length
37
+ }
38
+
39
+ /**
40
+ * Picks random cells for the mines, leaving `safe` and the cells around it free.
41
+ *
42
+ * @param board - Board to place mines on, see `Game`.
43
+ * @param safe - Cell the first reveal hit, which must not be a mine.
44
+ * @returns Mine cell numbers, `board.mineCount` of them.
45
+ */
46
+ function placeMines(board: Game, safe: number): number[] {
47
+ const banned = new Set([safe, ...neighbors(safe, board.size)])
48
+ const free = Array.from({ length: board.size * board.size }, (_, i) => i).filter(i => !banned.has(i))
49
+ for (let i = free.length - 1; i > 0; i--) {
50
+ const j = Math.floor(Math.random() * (i + 1))
51
+ ;[free[i], free[j]] = [free[j] ?? 0, free[i] ?? 0]
52
+ }
53
+ return free.slice(0, board.mineCount)
54
+ }
55
+
56
+ /**
57
+ * @brief Reveals one cell: the first reveal places the mines, a zero opens its
58
+ * neighbours, a mine ends the game, and the last safe cell wins it.
59
+ *
60
+ * @param board - Board to reveal on, see `Game`.
61
+ * @param cell - Cell number to reveal.
62
+ * @param now - Milliseconds since the epoch, stamped as the start or end time.
63
+ * @returns The board after the reveal, or the same board when the cell cannot be revealed.
64
+ */
65
+ export function reveal(board: Game, cell: number, now: number): Game {
66
+ if (board.status === 'won' || board.status === 'lost') return board
67
+ if (board.revealed.includes(cell) || board.flagged.includes(cell)) return board
68
+
69
+ const mines = board.status === 'ready' ? placeMines(board, cell) : board.mines
70
+ const started: Game = board.status === 'ready' ? { ...board, mines, status: 'playing', startedAt: now } : board
71
+ if (mines.includes(cell)) return { ...started, revealed: [...started.revealed, cell], status: 'lost', endedAt: now }
72
+
73
+ const open = new Set(started.revealed)
74
+ const queue = [cell]
75
+ while (queue.length > 0) {
76
+ const next = queue.pop() ?? cell
77
+ if (open.has(next) || started.flagged.includes(next)) continue
78
+ open.add(next)
79
+ if (adjacentMines(started, next) === 0) queue.push(...neighbors(next, started.size).filter(n => !open.has(n)))
80
+ }
81
+
82
+ const revealed = [...open]
83
+ const isWon = revealed.length === started.size * started.size - started.mineCount
84
+ return isWon ? { ...started, revealed, status: 'won', endedAt: now } : { ...started, revealed }
85
+ }
86
+
87
+ /**
88
+ * Flags or unflags a hidden cell while the game is on.
89
+ *
90
+ * @param board - Board to flag on, see `Game`.
91
+ * @param cell - Cell number to flag.
92
+ * @returns The board after the flag, or the same board when the cell cannot be flagged.
93
+ */
94
+ export function toggleFlag(board: Game, cell: number): Game {
95
+ if (board.revealed.includes(cell) || board.status === 'won' || board.status === 'lost') return board
96
+ const flagged = board.flagged.includes(cell) ? board.flagged.filter(c => c !== cell) : [...board.flagged, cell]
97
+ return { ...board, flagged }
98
+ }
99
+
100
+ /**
101
+ * Creates a board in the `ready` state, with no mines placed yet.
102
+ *
103
+ * @param now - Milliseconds since the epoch, stamped as the start time.
104
+ * @returns The new board, see `Game`.
105
+ */
106
+ export function newGame(now: number): Game {
107
+ return { size: SIZE, mineCount: MINES, mines: [], revealed: [], flagged: [], status: 'ready', isFlagMode: false, startedAt: now }
108
+ }
@@ -0,0 +1,91 @@
1
+ // packages/g3n-mods/mods/mini-game/hooks/mole.ts
2
+ import type { Whack } from '../types'
3
+
4
+ const SIZE = 4
5
+ export const LIVES = 3
6
+ /** How long the first mole stays up. */
7
+ const START_WINDOW_MS = 1400
8
+ /** How long a mole stays up at the fastest. */
9
+ const MIN_WINDOW_MS = 450
10
+ /** What each hit multiplies the window by, so the game speeds up. */
11
+ const SPEEDUP = 0.94
12
+
13
+ /**
14
+ * Creates a game in the `ready` state, with no mole up.
15
+ *
16
+ * @returns The new game, see `Whack`.
17
+ */
18
+ export function newWhack(): Whack {
19
+ return { size: SIZE, mole: null, moleId: 0, score: 0, lives: LIVES, windowMs: START_WINDOW_MS, status: 'ready' }
20
+ }
21
+
22
+ /**
23
+ * Puts the next mole up, in a hole other than the current one.
24
+ *
25
+ * @param game - Game to put the mole up in, see `Whack`.
26
+ * @returns The game with the new mole and its id.
27
+ */
28
+ function nextMole(game: Whack): Whack {
29
+ const holes = game.size * game.size
30
+ const offset = 1 + Math.floor(Math.random() * (holes - 1))
31
+ const mole = game.mole === null ? Math.floor(Math.random() * holes) : (game.mole + offset) % holes
32
+ return { ...game, mole, moleId: game.moleId + 1 }
33
+ }
34
+
35
+ /**
36
+ * Takes a life; the last one ends the game.
37
+ *
38
+ * @param game - Game to take the life from, see `Whack`.
39
+ * @returns The game after the loss.
40
+ */
41
+ function loseLife(game: Whack): Whack {
42
+ const lives = game.lives - 1
43
+ return lives === 0 ? { ...game, lives, mole: null, status: 'over' } : { ...game, lives }
44
+ }
45
+
46
+ /**
47
+ * Starts the game with its first mole up.
48
+ *
49
+ * @param game - Game to start, see `Whack`.
50
+ * @returns The game in the `playing` state, or the same game when it is already on.
51
+ */
52
+ export function startWhack(game: Whack): Whack {
53
+ return game.status === 'playing' ? game : nextMole({ ...newWhack(), status: 'playing' })
54
+ }
55
+
56
+ /**
57
+ * Whacks one hole: the mole scores and speeds the game up, an empty hole costs a life.
58
+ *
59
+ * @param game - Game to whack in, see `Whack`.
60
+ * @param hole - Hole number, counted row by row from `0`.
61
+ * @returns The game after the whack, or the same game when it is not on.
62
+ */
63
+ export function whack(game: Whack, hole: number): Whack {
64
+ if (game.status !== 'playing') return game
65
+ if (hole !== game.mole) return loseLife(game)
66
+ const windowMs = Math.max(MIN_WINDOW_MS, Math.round(game.windowMs * SPEEDUP))
67
+ return nextMole({ ...game, score: game.score + 1, windowMs })
68
+ }
69
+
70
+ /**
71
+ * Lets a mole escape when its time runs out, which costs a life.
72
+ *
73
+ * @param game - Game the mole was up in, see `Whack`.
74
+ * @param moleId - Mole whose time ran out; a mole already whacked or gone is ignored.
75
+ * @returns The game after the escape, or the same game when that mole is no longer up.
76
+ */
77
+ export function escape(game: Whack, moleId: number): Whack {
78
+ if (game.status !== 'playing' || game.moleId !== moleId) return game
79
+ const after = loseLife(game)
80
+ return after.status === 'over' ? after : nextMole(after)
81
+ }
82
+
83
+ /**
84
+ * Ends a round early, keeping its score.
85
+ *
86
+ * @param game - Game to end, see `Whack`.
87
+ * @returns The game in the `over` state, or the same game when it is not on.
88
+ */
89
+ export function quit(game: Whack): Whack {
90
+ return game.status === 'playing' ? { ...game, mole: null, status: 'over' } : game
91
+ }