@jhizzard/termdeck 0.13.0 → 0.15.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
5
5
  "bin": {
6
6
  "termdeck": "./packages/cli/src/index.js"
@@ -10,6 +10,14 @@
10
10
  layout: '2x1',
11
11
  themes: {},
12
12
  config: {},
13
+ // Sprint 45 T4: serializable projection of the multi-agent registry
14
+ // (server's AGENT_ADAPTERS). Populated from GET /api/agent-adapters
15
+ // during init(). The launcher's command-shorthand parser reads this
16
+ // to detect which adapter (if any) a typed command should map to.
17
+ // Fallback list is the pre-Sprint-45 default so the launcher still
18
+ // works if the endpoint 404s on an older server during a rolling
19
+ // upgrade — Claude only, anchored binary match.
20
+ agentAdapters: [{ name: 'claude', sessionType: 'claude-code', binary: 'claude', costBand: 'pay-per-token' }],
13
21
  focusedId: null
14
22
  };
15
23
 
@@ -27,6 +35,17 @@
27
35
  state.config = await api('GET', '/api/config');
28
36
  updateRagIndicator();
29
37
 
38
+ // Sprint 45 T4: fetch the multi-agent adapter registry projection.
39
+ // Drives the launcher's command-shorthand → sessionType resolution
40
+ // below in launchTerminal(). Falls back to the bootstrap default
41
+ // (Claude only) if the endpoint isn't available on this server.
42
+ try {
43
+ const adapters = await api('GET', '/api/agent-adapters');
44
+ if (Array.isArray(adapters) && adapters.length > 0) {
45
+ state.agentAdapters = adapters;
46
+ }
47
+ } catch (_) { /* keep bootstrap fallback */ }
48
+
30
49
  // Populate project dropdown
31
50
  const sel = document.getElementById('promptProject');
32
51
  for (const name of Object.keys(state.config.projects || {})) {
@@ -2460,31 +2479,20 @@
2460
2479
  return;
2461
2480
  }
2462
2481
 
2463
- // Parse shorthand commands
2464
- let resolvedCommand = command;
2465
- let resolvedType = 'shell';
2466
- let resolvedCwd = undefined;
2467
-
2468
- let resolvedProject = project || undefined;
2469
-
2470
- if (/^claude\b/i.test(command) || /^cc\b/i.test(command)) {
2471
- resolvedType = 'claude-code';
2472
- const argMatch = command.match(/(?:claude|cc)\s+(?:code\s+)?(.+)/i);
2473
- if (argMatch) {
2474
- const arg = argMatch[1].trim();
2475
- // Check if arg is a known project name
2476
- if (state.config.projects && state.config.projects[arg]) {
2477
- resolvedProject = arg;
2478
- } else {
2479
- resolvedCwd = arg;
2480
- }
2481
- }
2482
- resolvedCommand = 'claude';
2483
- } else if (/^gemini\b/i.test(command)) {
2484
- resolvedType = 'gemini';
2485
- } else if (/^python3?\b.*(?:runserver|uvicorn|flask|gunicorn)/i.test(command)) {
2486
- resolvedType = 'python-server';
2487
- }
2482
+ // Sprint 45 T4 + Sprint 46 T4: resolver extracted to
2483
+ // packages/client/public/launcher-resolver.js so the same routing
2484
+ // logic runs in the browser AND under `node --test` (see
2485
+ // tests/launcher-resolver.test.js for the contract pin). Sprint 46
2486
+ // T4 also extended the python-server preemptive regex to recognize
2487
+ // `http.server` so the python topbar quick-launch button is typed
2488
+ // correctly from the first frame.
2489
+ const { resolvedCommand, resolvedType, resolvedCwd, resolvedProject } =
2490
+ LauncherResolver.resolve(
2491
+ command,
2492
+ project,
2493
+ state.agentAdapters,
2494
+ state.config.projects
2495
+ );
2488
2496
 
2489
2497
  const session = await api('POST', '/api/sessions', {
2490
2498
  command: resolvedCommand,
@@ -4449,18 +4457,28 @@
4449
4457
  for (const sess of data.sessions) {
4450
4458
  const id = sess.sessionId || sess.session_id || 'unknown';
4451
4459
  const shortId = id.slice(0, 8);
4452
- const type = sess.type || 'shell';
4460
+ // Server (/api/transcripts/recent) returns { sessions: [{ session_id, chunks: [...] }] }
4461
+ // with chunks already grouped per session in DESC created_at order. Type/project
4462
+ // metadata isn't on the transcripts table — fall back to optional fields if any
4463
+ // future server enrichment ships them.
4464
+ const chunks = Array.isArray(sess.chunks) ? sess.chunks : [];
4465
+ const type = sess.type || (chunks.length ? 'session' : 'shell');
4453
4466
  const project = sess.project || '';
4454
- const lines = sess.lines || sess.preview || [];
4455
- const lineCount = sess.totalLines || lines.length;
4467
+ const totalChunks = sess.totalLines || chunks.length;
4468
+ // Build preview from the most-recent chunks. Server returns DESC order, so
4469
+ // the first 6 entries are the newest — reverse for natural top-down reading.
4470
+ const previewChunks = chunks.slice(0, 6).reverse();
4471
+ const previewText = sess.preview
4472
+ ? (Array.isArray(sess.preview) ? sess.preview.join('\n') : String(sess.preview))
4473
+ : previewChunks.map(c => (c && typeof c.content === 'string') ? c.content : '').join('');
4456
4474
  html += `<div class="transcript-session" data-session-id="${escapeHtml(id)}">
4457
4475
  <div class="ts-header">
4458
4476
  <span class="ts-id">${escapeHtml(shortId)}</span>
4459
4477
  <span class="ts-type">${escapeHtml(type)}</span>
4460
4478
  ${project ? `<span class="ts-project">${escapeHtml(project)}</span>` : ''}
4461
- <span class="ts-lines">${lineCount} lines</span>
4479
+ <span class="ts-lines">${totalChunks} chunks</span>
4462
4480
  </div>
4463
- <pre class="ts-preview">${escapeHtml(lines.slice(-6).join('\n'))}</pre>
4481
+ <pre class="ts-preview">${escapeHtml(previewText)}</pre>
4464
4482
  </div>`;
4465
4483
  }
4466
4484
  body.innerHTML = html;
@@ -4500,7 +4518,11 @@
4500
4518
  const id = result.sessionId || result.session_id || 'unknown';
4501
4519
  const shortId = id.slice(0, 8);
4502
4520
  const line = result.line || result.content || '';
4503
- const ts = result.timestamp ? new Date(result.timestamp).toLocaleTimeString() : '';
4521
+ // Server (/api/transcripts/search) sends `created_at`; legacy `timestamp` kept
4522
+ // as a fallback in case a future enrichment swaps the field name.
4523
+ const tsSource = result.timestamp || result.created_at || '';
4524
+ const tsDate = tsSource ? new Date(tsSource) : null;
4525
+ const ts = (tsDate && !isNaN(tsDate.getTime())) ? tsDate.toLocaleTimeString() : '';
4504
4526
  html += `<div class="transcript-result" data-session-id="${escapeHtml(id)}">
4505
4527
  <div class="tr-meta">
4506
4528
  <span class="tr-session">${escapeHtml(shortId)}</span>
@@ -552,7 +552,7 @@
552
552
  .attr('stroke-width', 1.2)
553
553
  .attr('filter', 'url(#nodeGlow)')
554
554
  .style('cursor', 'pointer')
555
- .on('mouseenter', (event, d) => onNodeHover(d.id))
555
+ .on('mouseenter', (event, d) => { onNodeHover(d.id); showNodeTooltip(event, d); })
556
556
  .on('mouseleave', () => onNodeHover(null))
557
557
  .on('click', (event, d) => onNodeClick(d))
558
558
  .call(window.d3.drag()
@@ -738,6 +738,28 @@
738
738
  tip.hidden = false;
739
739
  moveTooltip(event);
740
740
  }
741
+
742
+ // Sprint 46 T1 — node hover tooltip. Shows project (color-coded) + a short
743
+ // content snippet so the user can scan the graph without having to open the
744
+ // drawer for every node. Click still opens the full detail drawer.
745
+ function showNodeTooltip(event, node) {
746
+ const tip = $('graphTooltip');
747
+ if (!tip || !node) return;
748
+ const proj = node.project || 'global';
749
+ const text = escapeHtml(truncate(node.label || node.snippet || '(no content)', 80));
750
+ const meta = node.source_type ? `<span style="opacity:0.7">${escapeHtml(node.source_type)}</span>` : '';
751
+ tip.innerHTML = `<strong style="color:${hashHue(proj)}">${escapeHtml(proj)}</strong> ${meta} · ${text}`;
752
+ tip.hidden = false;
753
+ moveTooltip(event);
754
+ }
755
+
756
+ function escapeHtml(s) {
757
+ return String(s == null ? '' : s)
758
+ .replace(/&/g, '&amp;')
759
+ .replace(/</g, '&lt;')
760
+ .replace(/>/g, '&gt;')
761
+ .replace(/"/g, '&quot;');
762
+ }
741
763
  function moveTooltip(event) {
742
764
  const tip = $('graphTooltip');
743
765
  if (tip.hidden) return;
@@ -372,6 +372,7 @@
372
372
  <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
373
373
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
374
374
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
375
+ <script src="launcher-resolver.js" defer></script>
375
376
  <script src="app.js" defer></script>
376
377
  </body>
377
378
  </html>
@@ -0,0 +1,68 @@
1
+ // TermDeck launcher resolver — extracted Sprint 46 T4
2
+ //
3
+ // Pure function: given (command, project, agentAdapters, projects),
4
+ // returns the resolved spawn parameters the launcher POSTs to /api/sessions.
5
+ // Lives in its own file so the same code runs in the browser (via
6
+ // <script src="launcher-resolver.js">) AND under `node --test` (via
7
+ // `require('.../launcher-resolver')`). Sprint 46 T4 added this extraction
8
+ // to close a zero-coverage gap on the client-side routing logic — see
9
+ // tests/launcher-resolver.test.js for the contract pin.
10
+ //
11
+ // Sprint 45 T4 refactor lives here too: registry-driven shorthand
12
+ // resolution. Pre-Sprint-45 had hardcoded claude/cc/gemini/python branches;
13
+ // now the type detection consults `agentAdapters` (loaded from
14
+ // /api/agent-adapters at init), and only the Claude `cc` alias and the
15
+ // python-server detection (no adapter exists) stay as special-cases.
16
+ // Adapter matching uses an anchored prefix on the adapter's binary name
17
+ // (`^binary\b`, case-insensitive) which fits all four Sprint-45 adapters
18
+ // (claude / codex / gemini / grok) since each binary is uniquely named.
19
+
20
+ (function (root, factory) {
21
+ if (typeof module === 'object' && module.exports) {
22
+ module.exports = factory();
23
+ } else {
24
+ root.LauncherResolver = factory();
25
+ }
26
+ })(typeof self !== 'undefined' ? self : this, function () {
27
+ function resolve(command, project, agentAdapters, projects) {
28
+ let resolvedCommand = command;
29
+ let resolvedType = 'shell';
30
+ let resolvedCwd;
31
+ let resolvedProject = project || undefined;
32
+
33
+ let canonical = command;
34
+ if (/^cc\b/i.test(canonical)) {
35
+ canonical = canonical.replace(/^cc\b/i, 'claude');
36
+ }
37
+
38
+ const adapter = (agentAdapters || []).find((a) =>
39
+ a && a.binary && new RegExp(`^${a.binary}\\b`, 'i').test(canonical)
40
+ );
41
+
42
+ if (adapter) {
43
+ resolvedType = adapter.sessionType;
44
+ if (adapter.name === 'claude') {
45
+ const argMatch = canonical.match(/^claude\s+(?:code\s+)?(.+)/i);
46
+ if (argMatch) {
47
+ const arg = argMatch[1].trim();
48
+ if (projects && projects[arg]) {
49
+ resolvedProject = arg;
50
+ } else {
51
+ resolvedCwd = arg;
52
+ }
53
+ }
54
+ resolvedCommand = adapter.binary;
55
+ }
56
+ } else if (/^python3?\b.*(?:runserver|uvicorn|flask|gunicorn|http\.server)/i.test(canonical)) {
57
+ // Sprint 46 T4: extended `http\.server` so the python topbar
58
+ // quick-launch button is preemptively typed correctly. Without
59
+ // this, the badge flickers through `shell` for ~1s before
60
+ // session.js's runtime detection (`/Serving HTTP on/`) catches up.
61
+ resolvedType = 'python-server';
62
+ }
63
+
64
+ return { resolvedCommand, resolvedType, resolvedCwd, resolvedProject };
65
+ }
66
+
67
+ return { resolve };
68
+ });
@@ -0,0 +1,199 @@
1
+ // Codex CLI adapter — Sprint 45 T1
2
+ //
3
+ // Second adapter in the AGENT_ADAPTERS registry (see ./index.js). Sprint 44 T3
4
+ // shipped the Claude adapter as the reference implementation; this file is the
5
+ // recipe in `docs/AGENT-RUNTIMES.md` § 6 turned into running code for Codex
6
+ // CLI (`/usr/local/bin/codex`, v0.125.0 verified 2026-05-01).
7
+ //
8
+ // This is *Codex-as-its-own-panel* — distinct from the existing
9
+ // `codex@openai-codex` Claude Code plugin which is a delegate-from-Claude
10
+ // pathway. Sprint 46 wires per-lane agent assignment; this lane just makes
11
+ // `codex` work end-to-end inside a TermDeck panel: type detection, status
12
+ // badge, transcript ingestion into Mnestra.
13
+ //
14
+ // Contract — see ./claude.js header for the full annotated shape.
15
+ //
16
+ // Pattern provenance:
17
+ // • Codex CLI ships a Ratatui (Rust) TUI. The TUI redraws on each turn so the
18
+ // raw PTY stream is heavy in ANSI escapes; session.js stripAnsi() runs
19
+ // *before* these regexes, so the patterns assume cleaned text.
20
+ // • The headless `codex exec` mode emits a documented sequence: a `--------`
21
+ // header block, `user` / `codex` speaker lines on their own row, function
22
+ // `exec_command` blocks, and a `tokens used` footer. The TUI mirrors these
23
+ // speaker shapes inside its rendered chat surface.
24
+ // • Reasoning markers come from the JSONL `response_item.payload.type=reasoning`
25
+ // events that the TUI renders as a "Thinking…" status line.
26
+ // • Apply-patch / exec markers come from `response_item.payload.type=function_call`
27
+ // entries with names like `apply_patch` and `exec_command`.
28
+ //
29
+ // Patterns are conservative defaults — Sprint 45 T4 / Sprint 46 will tune
30
+ // against captured real-world TUI output. Snapshot tests in
31
+ // tests/agent-adapter-codex.test.js pin the current behavior so any tuning
32
+ // is an explicit, reviewed change.
33
+
34
+ // ──────────────────────────────────────────────────────────────────────────
35
+ // Patterns
36
+ // ──────────────────────────────────────────────────────────────────────────
37
+
38
+ // Codex prompt detection. Three shapes accepted:
39
+ // 1. `codex>` literal (mirrors gemini's `gemini>` and the codex CLI's REPL
40
+ // prompt convention — used by `codex resume` interactive sessions).
41
+ // 2. A bare `codex` line (the speaker label the TUI prints above an
42
+ // assistant turn AND that headless `codex exec` prints before the reply).
43
+ // 3. The `--------` divider that wraps the codex header block in headless
44
+ // mode and bookends turns in the TUI.
45
+ const PROMPT = /^(?:codex>\s|codex\s*$|--------\s*$)/m;
46
+
47
+ // Reasoning indicator. Codex's TUI status line shows "Thinking" while the
48
+ // model reasons; "Reasoning" appears in some headless transcripts; "Working"
49
+ // is what `codex exec` prints for tool-loop progress.
50
+ const THINKING = /\b(Thinking|Reasoning|Working)\b/;
51
+
52
+ // File edit / patch markers. Codex applies diffs through the `apply_patch`
53
+ // tool which the TUI renders as `Apply patch <file>` headers. Plain
54
+ // Edit/Create/Update/Delete shapes are also kept so simple file ops register
55
+ // (mirrors the Claude adapter's editing markers for cross-adapter parity).
56
+ const EDITING = /^(Apply patch|Edit|Create|Update|Delete|Modified)\s/m;
57
+ const EDITING_DETAIL = /^(Apply patch|Edit|Create|Update|Delete|Modified)\s+(.+)$/m;
58
+
59
+ // Tool / shell-exec markers. Codex's TUI prefixes shell commands with `$`
60
+ // (chat-shell convention), arrow `→` for read tool calls, and bare keywords
61
+ // `exec` / `Running` / `Calling` for the phase between dispatch and result.
62
+ // `exec_command` is Codex's function-call name (verified in rollout JSONL
63
+ // 2026-05-01); the alternation handles both bare `exec` and the underscored
64
+ // `exec_command` shape (the underscore is a word character so `exec\b`
65
+ // alone wouldn't match `exec_command`).
66
+ const TOOL = /^(?:\$\s|→\s|exec(?:_command\b|\b)|Running\b|Calling\b)/m;
67
+
68
+ // Idle / waiting-for-input. The TUI returns to the bare `codex` speaker
69
+ // label when it's done reasoning and waiting on the user.
70
+ const IDLE = /^codex\s*$/m;
71
+
72
+ // Error patterns — line-anchored to avoid mid-line "error" mentions in tool
73
+ // output (grep results, test logs, file dumps) flagging false positives.
74
+ // Same shape as Claude with codex-specific OpenAI-API failure modes added
75
+ // (rate-limit 429, model-not-found, invalid_api_key) which surface as visible
76
+ // strings in Codex's error reporting and would otherwise slip through.
77
+ const ERROR = /^\s*(?:(?:error|Error|ERROR|exception|Exception|Traceback|fatal|Fatal|FATAL|segmentation fault|panic|EACCES|ECONNREFUSED|ENOENT|command not found|undefined reference|cannot find module|failed with exit code|No such file or directory|Permission denied|429\s+Too Many Requests|rate.?limit|invalid_api_key|model_not_found|insufficient_quota)\b|npm ERR!)/m;
78
+
79
+ // ──────────────────────────────────────────────────────────────────────────
80
+ // statusFor — Codex panel status. Order mirrors Claude's cascade:
81
+ // thinking → editing → tool → idle. First match wins.
82
+ // ──────────────────────────────────────────────────────────────────────────
83
+
84
+ function statusFor(data) {
85
+ if (THINKING.test(data)) {
86
+ return { status: 'thinking', statusDetail: 'Codex is reasoning...' };
87
+ }
88
+ if (EDITING.test(data)) {
89
+ const match = data.match(EDITING_DETAIL);
90
+ return {
91
+ status: 'editing',
92
+ statusDetail: match ? `${match[1]} ${match[2]}` : 'Editing files',
93
+ };
94
+ }
95
+ if (TOOL.test(data)) {
96
+ return { status: 'active', statusDetail: 'Using tools' };
97
+ }
98
+ if (IDLE.test(data)) {
99
+ return { status: 'idle', statusDetail: 'Waiting for input' };
100
+ }
101
+ return null;
102
+ }
103
+
104
+ // ──────────────────────────────────────────────────────────────────────────
105
+ // parseTranscript — Codex JSONL format.
106
+ //
107
+ // Each line is `{ timestamp, type, payload }`. We want only:
108
+ // type === 'response_item' && payload.type === 'message'
109
+ // with payload.role in {user, assistant}. The 'developer' role carries the
110
+ // permissions/sandbox prelude — skip. `event_msg` lines duplicate the
111
+ // canonical message channel and additionally carry exec_command_end shell
112
+ // output blocks — skip too.
113
+ //
114
+ // content is an array of { type: 'input_text' | 'output_text', text: string }
115
+ // (sometimes plain `text`). Joined with spaces and truncated to 400 chars
116
+ // per message (same cut-off Claude uses).
117
+ // ──────────────────────────────────────────────────────────────────────────
118
+
119
+ function parseTranscript(raw) {
120
+ if (typeof raw !== 'string' || raw.length === 0) return [];
121
+ const lines = raw.split('\n').filter(Boolean);
122
+ const messages = [];
123
+ for (const line of lines) {
124
+ let entry;
125
+ try { entry = JSON.parse(line); } catch (_) { continue; }
126
+ if (!entry || entry.type !== 'response_item') continue;
127
+ const p = entry.payload;
128
+ if (!p || p.type !== 'message') continue;
129
+ const role = p.role;
130
+ if (role !== 'user' && role !== 'assistant') continue;
131
+ const content = p.content;
132
+ let text = '';
133
+ if (typeof content === 'string') {
134
+ text = content;
135
+ } else if (Array.isArray(content)) {
136
+ text = content
137
+ .filter((c) => c && (c.type === 'input_text' || c.type === 'output_text' || c.type === 'text'))
138
+ .map((c) => c.text || '')
139
+ .join(' ');
140
+ }
141
+ if (text) messages.push({ role, content: text.slice(0, 400) });
142
+ }
143
+ return messages;
144
+ }
145
+
146
+ // ──────────────────────────────────────────────────────────────────────────
147
+ // bootPromptTemplate — Codex variant of the Claude scaffold. Points at
148
+ // AGENTS.md (Codex's instructional file) instead of CLAUDE.md. Sprint 46 T2
149
+ // will refine per-agent prompts; this is the placeholder so the contract is
150
+ // uniform across all four adapters.
151
+ // ──────────────────────────────────────────────────────────────────────────
152
+
153
+ function bootPromptTemplate(lane = {}, sprint = {}) {
154
+ const tn = lane.id || 'T?';
155
+ const sprintNum = sprint.number || '?';
156
+ const sprintName = sprint.name || 'unnamed';
157
+ const project = (lane.project || sprint.project || 'termdeck');
158
+ const briefing = lane.briefingPath || `docs/sprint-${sprintNum}-${sprintName}/${tn}-<lane>.md`;
159
+ return [
160
+ `You are ${tn} in Sprint ${sprintNum} (${sprintName}). Boot sequence:`,
161
+ `1. memory_recall(project="${project}", query="<topic>")`,
162
+ `2. memory_recall(query="<broader topic>")`,
163
+ `3. Read ~/.claude/CLAUDE.md and ./AGENTS.md`,
164
+ `4. Read docs/sprint-${sprintNum}-${sprintName}/PLANNING.md`,
165
+ `5. Read docs/sprint-${sprintNum}-${sprintName}/STATUS.md`,
166
+ `6. Read ${briefing}`,
167
+ '',
168
+ 'Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in STATUS.md.',
169
+ "Don't bump versions, don't touch CHANGELOG, don't commit.",
170
+ ].join('\n');
171
+ }
172
+
173
+ const codexAdapter = {
174
+ name: 'codex',
175
+ sessionType: 'codex',
176
+ matches: (cmd) => typeof cmd === 'string' && /\bcodex\b/i.test(cmd),
177
+ spawn: {
178
+ binary: 'codex',
179
+ defaultArgs: [],
180
+ env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY },
181
+ },
182
+ patterns: {
183
+ prompt: PROMPT,
184
+ thinking: THINKING,
185
+ editing: EDITING,
186
+ tool: TOOL,
187
+ idle: IDLE,
188
+ error: ERROR,
189
+ },
190
+ patternNames: {
191
+ error: 'codexErrorLineStart',
192
+ },
193
+ statusFor,
194
+ parseTranscript,
195
+ bootPromptTemplate,
196
+ costBand: 'pay-per-token',
197
+ };
198
+
199
+ module.exports = codexAdapter;
@@ -0,0 +1,158 @@
1
+ // Gemini CLI adapter — Sprint 45 T2
2
+ //
3
+ // Lifts the previously-hardcoded gemini logic out of session.js into the
4
+ // AGENT_ADAPTERS registry alongside the Claude adapter shipped in Sprint 44
5
+ // T3. Behavior is bit-for-bit identical to the pre-Sprint-45 inline path:
6
+ // same `^gemini>` prompt regex, same `Generating|Working` thinking regex,
7
+ // same status strings ("Gemini is generating..." / "Waiting for input"),
8
+ // same loose `/gemini/i` command-string match. parseTranscript is the new
9
+ // capability — Gemini sessions previously didn't write to Mnestra because
10
+ // the memory hook assumed Claude JSONL.
11
+ //
12
+ // Contract — see ./claude.js header for the full 7-field shape.
13
+ //
14
+ // Patterns intentionally omit `error`. The fallback in session.js
15
+ // `_detectErrors` (`adapter.patterns.error || PATTERNS.error`) lets generic
16
+ // prose-shape error detection continue to apply to Gemini sessions, which
17
+ // matches the pre-Sprint-45 behavior. Sprint 46+ can layer in a Gemini-
18
+ // specific line-anchored error pattern once we've observed enough TUI
19
+ // output to know what false positives to dodge.
20
+
21
+ // ──────────────────────────────────────────────────────────────────────────
22
+ // Patterns — verbatim regexes lifted from session.js's PATTERNS.geminiCli
23
+ // (lines 47-50). Reference-equal preservation matters because session.js
24
+ // keeps a `PATTERNS.geminiCli` shim that points back at these regex
25
+ // objects, the same way `PATTERNS.claudeCode.*` shimmed Sprint 44 T3.
26
+ // ──────────────────────────────────────────────────────────────────────────
27
+
28
+ const PROMPT = /^gemini>\s/m;
29
+ const THINKING = /\b(Generating|Working)\b/;
30
+
31
+ // ──────────────────────────────────────────────────────────────────────────
32
+ // statusFor — replaces the `case 'gemini':` block of _updateStatus. Order
33
+ // matches the legacy switch's `if/else if` cascade exactly: thinking wins,
34
+ // then prompt → idle. No editing/tool/error branches in the legacy switch,
35
+ // so statusFor has none either; null returns leave the status untouched
36
+ // just like the legacy fall-through.
37
+ // ──────────────────────────────────────────────────────────────────────────
38
+
39
+ function statusFor(data) {
40
+ if (THINKING.test(data)) {
41
+ return { status: 'thinking', statusDetail: 'Gemini is generating...' };
42
+ }
43
+ if (PROMPT.test(data)) {
44
+ return { status: 'idle', statusDetail: 'Waiting for input' };
45
+ }
46
+ return null;
47
+ }
48
+
49
+ // ──────────────────────────────────────────────────────────────────────────
50
+ // parseTranscript — Gemini CLI session JSON format (NOT JSONL).
51
+ //
52
+ // Captured shape (from `gemini -p "say hi"` 2026-05-01):
53
+ // {
54
+ // sessionId, projectHash, startTime, lastUpdated, kind,
55
+ // messages: [
56
+ // { id, timestamp, type: 'user', content: [{ text: '...' }] },
57
+ // { id, timestamp, type: 'gemini', content: '...', thoughts, tokens, model },
58
+ // ...
59
+ // ]
60
+ // }
61
+ //
62
+ // The user role carries a content ARRAY of `{text}` parts; the gemini
63
+ // (assistant) role carries a STRING. We normalize both to the Claude
64
+ // adapter's output shape — `{ role: 'user'|'assistant', content: string }`
65
+ // truncated to 400 chars — so the memory-hook summary builder doesn't have
66
+ // to branch on adapter type.
67
+ //
68
+ // `type: 'gemini'` maps to `role: 'assistant'` for cross-adapter parity.
69
+ // ──────────────────────────────────────────────────────────────────────────
70
+
71
+ function parseTranscript(raw) {
72
+ if (typeof raw !== 'string' || raw.length === 0) return [];
73
+ let session;
74
+ try { session = JSON.parse(raw); } catch (_) { return []; }
75
+ if (!session || !Array.isArray(session.messages)) return [];
76
+
77
+ const messages = [];
78
+ for (const msg of session.messages) {
79
+ if (!msg || typeof msg !== 'object') continue;
80
+ let role;
81
+ if (msg.type === 'user') role = 'user';
82
+ else if (msg.type === 'gemini' || msg.type === 'assistant') role = 'assistant';
83
+ else continue;
84
+
85
+ const content = msg.content;
86
+ let text = '';
87
+ if (typeof content === 'string') {
88
+ text = content;
89
+ } else if (Array.isArray(content)) {
90
+ text = content
91
+ .filter((c) => c && typeof c.text === 'string')
92
+ .map((c) => c.text)
93
+ .join(' ');
94
+ }
95
+ if (text) messages.push({ role, content: text.slice(0, 400) });
96
+ }
97
+ return messages;
98
+ }
99
+
100
+ // ──────────────────────────────────────────────────────────────────────────
101
+ // bootPromptTemplate — placeholder mirroring the Claude adapter's shape.
102
+ // Points at GEMINI.md (the auto-generated mirror of CLAUDE.md per Sprint 44
103
+ // T2's sync-agent-instructions.js script). Sprint 46 T2 will refine the
104
+ // per-agent boot prompt — Gemini doesn't have Claude's `memory_recall` MCP
105
+ // tool out-of-the-box, so the lane brief shape may need agent-specific
106
+ // scaffolding. The placeholder here keeps the contract complete.
107
+ // ──────────────────────────────────────────────────────────────────────────
108
+
109
+ function bootPromptTemplate(lane = {}, sprint = {}) {
110
+ const tn = lane.id || 'T?';
111
+ const sprintNum = sprint.number || '?';
112
+ const sprintName = sprint.name || 'unnamed';
113
+ const project = (lane.project || sprint.project || 'termdeck');
114
+ const briefing = lane.briefingPath || `docs/sprint-${sprintNum}-${sprintName}/${tn}-<lane>.md`;
115
+ return [
116
+ `You are ${tn} in Sprint ${sprintNum} (${sprintName}). Boot sequence:`,
117
+ `1. memory_recall(project="${project}", query="<topic>")`,
118
+ `2. memory_recall(query="<broader topic>")`,
119
+ `3. Read ~/.claude/CLAUDE.md and ./GEMINI.md`,
120
+ `4. Read docs/sprint-${sprintNum}-${sprintName}/PLANNING.md`,
121
+ `5. Read docs/sprint-${sprintNum}-${sprintName}/STATUS.md`,
122
+ `6. Read ${briefing}`,
123
+ '',
124
+ 'Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in STATUS.md.',
125
+ "Don't bump versions, don't touch CHANGELOG, don't commit.",
126
+ ].join('\n');
127
+ }
128
+
129
+ const geminiAdapter = {
130
+ name: 'gemini',
131
+ sessionType: 'gemini',
132
+ matches: (cmd) => typeof cmd === 'string' && /gemini/i.test(cmd),
133
+ spawn: {
134
+ binary: 'gemini',
135
+ defaultArgs: [],
136
+ // GEMINI_API_KEY is read via `process.env` at spawn time by index.js'
137
+ // PTY env merge — declared here for documentation / discoverability,
138
+ // not for in-adapter overriding. OAuth-personal is the typical auth
139
+ // path (settings.json `security.auth.selectedType: 'oauth-personal'`).
140
+ env: {},
141
+ },
142
+ patterns: {
143
+ prompt: PROMPT,
144
+ thinking: THINKING,
145
+ // editing / tool / error intentionally omitted — see header comment.
146
+ },
147
+ patternNames: {
148
+ // No adapter-owned error pattern → session.js falls back to the
149
+ // generic `PATTERNS.error` and the `'error'` diag label, which is
150
+ // exactly what gemini-typed sessions saw pre-Sprint-45.
151
+ },
152
+ statusFor,
153
+ parseTranscript,
154
+ bootPromptTemplate,
155
+ costBand: 'pay-per-token',
156
+ };
157
+
158
+ module.exports = geminiAdapter;