@jhizzard/termdeck 1.6.1 → 1.8.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.
@@ -16,6 +16,15 @@ const claude = require('./claude');
16
16
  const codex = require('./codex');
17
17
  const gemini = require('./gemini');
18
18
  const grok = require('./grok');
19
+ // Sprint 70 T1 — Antigravity CLI (`agy`). Registered under its canonical
20
+ // adapter name `antigravity` (= source_agent); the binary it matches is `agy`.
21
+ const antigravity = require('./agy');
22
+ // Sprint 72 T2 — Grok web-chat panel (`type:'web-chat'`). NOT a node-pty agent:
23
+ // driven by the CDP render-bridge (packages/web-chat-driver) against a real
24
+ // grok.com tab. Registered for `getAdapterForSessionType('web-chat')` +
25
+ // onPanelClose/periodic capture only — its `matches:()=>false` + absent
26
+ // `patterns.prompt` mean it never participates in output/command detection.
27
+ const webChatGrok = require('./web-chat-grok');
19
28
 
20
29
  // Keyed by adapter name (NOT session.meta.type — adapters expose their own
21
30
  // `sessionType` field for that mapping). Order is iteration order for the
@@ -25,6 +34,16 @@ const AGENT_ADAPTERS = {
25
34
  codex,
26
35
  gemini,
27
36
  grok,
37
+ // Listed last: its idle `> ` prompt overlaps claude's, and claude (first)
38
+ // claims that string in the detect loop. agy panels are normally resolved by
39
+ // exact-binary direct-spawn, not output sniffing, so order is not load-bearing.
40
+ antigravity,
41
+ // Sprint 72 T2 — web-chat-grok. Order is irrelevant: it carries no
42
+ // `patterns.prompt` and `matches()` returns false, so detectAdapter() + the
43
+ // direct-spawn loop skip it entirely. It is reachable ONLY via
44
+ // getAdapterForSessionType('web-chat') (the `.find(a=>a.sessionType===type)`
45
+ // fallback, since the registry key 'web-chat-grok' ≠ sessionType 'web-chat').
46
+ 'web-chat-grok': webChatGrok,
28
47
  };
29
48
 
30
49
  // Convenience accessor — returns the adapter whose `sessionType` matches the
@@ -0,0 +1,259 @@
1
+ // web-chat-grok adapter — Sprint 72 T2 (Workstream B)
2
+ //
3
+ // Sixth adapter in the AGENT_ADAPTERS registry (see ./index.js). Unlike every
4
+ // other adapter it is NOT backed by a node-pty child process — a `web-chat`
5
+ // panel is driven by T1's CDP render-bridge against a real, logged-in headful
6
+ // grok.com tab (`packages/web-chat-driver`). The adapter is the seam the
7
+ // TermDeck server (index.js) consumes; the driver is the seam the adapter
8
+ // consumes. See docs/sprint-72-grok-panel/PLANNING.md § "The 8 TermDeck seams".
9
+ //
10
+ // ── Why a distinct adapter from grok.js (the CLI) ────────────────────────────
11
+ // grok.js drives the `grok-dev` CLI (PTY, ~/.grok/grok.db, GROK_MODEL env). This
12
+ // adapter drives grok.com in a browser — the FLAT-RATE (subscription) path to
13
+ // Grok's reasoning model, which the CLI rejects (`reasoningEffort` → HTTP 400;
14
+ // see grok-models.js). Same provider, different runtime + different cost
15
+ // realization, so it is a separate `sessionType:'web-chat'`. Provenance is
16
+ // tagged `sourceAgent:'grok'` this sprint (ORCH zero-touch decision — see the
17
+ // "source_agent attribution" section); a distinct 'grok-web' tag is deferred.
18
+ //
19
+ // ── The one hard constraint: NO node-pty, NO on-disk transcript ──────────────
20
+ // There is no PTY stream and no conversation file on disk. The server seam
21
+ // accumulates each turn (injected prompt + Grok's completed response) into an
22
+ // in-memory buffer `session._webChatTranscript.turns` (`[{role,content}]`).
23
+ // `resolveTranscriptPath` materializes that buffer into a Gemini-shaped JSON
24
+ // envelope tempfile — EXACTLY the agy.js (Sprint 70 T1) pattern — so the
25
+ // bundled hook's `parseAutoDetect`/`parseGeminiJson` ingest it with NO
26
+ // dedicated `TRANSCRIPT_PARSERS['web-chat']` entry. onPanelClose's close→hook
27
+ // path is reused with no second write path. (Mirrors agy's "live source →
28
+ // tempfile envelope → existing hook" decoupling from the hook layer.)
29
+ //
30
+ // ── statusFor is a contract backstop, not the primary status signal ──────────
31
+ // PTY adapters derive status by pattern-matching escape-laden output. A
32
+ // web-chat panel has no escapes; its status is EVENT-driven by the server seam
33
+ // (a prompt was injected ⇒ 'thinking'; T3's completion detector fired ⇒
34
+ // 'idle'). `statusFor(text)` is implemented for contract uniformity + as a
35
+ // text-shape backstop (and is what index.js routes a completed response
36
+ // through), but the load-bearing transitions are wired in index.js off the
37
+ // driver's inject/onComplete events. We deliberately do NOT carry a
38
+ // `patterns.error` (a Grok answer that DISCUSSES an error is not a panel
39
+ // error) — index.js does not route web-chat text through `_detectErrors`.
40
+ //
41
+ // ── source_agent attribution ─────────────────────────────────────────────────
42
+ // `sourceAgent:'grok'` (ORCH decision 2026-06-08, Blocker 3): we reuse the
43
+ // already-allow-listed 'grok' tag so this sprint touches ZERO release-sensitive
44
+ // surface — the bundled hooks in packages/stack-installer/assets/hooks/* (Brad
45
+ // runs the installed copy) stay pristine. The provenance is still accurate
46
+ // (it IS Grok producing the content); it just doesn't yet distinguish web from
47
+ // CLI. A distinct 'grok-web' tag — which WOULD require adding 'grok-web' to the
48
+ // hook's ALLOWED_SOURCE_AGENTS (else normalizeSourceAgent coerces the row to
49
+ // 'claude') + a hook-version-stamp bump + an install refresh — is deferred to a
50
+ // follow-up. onPanelClose emits `adapter.sourceAgent || adapter.name`.
51
+ //
52
+ // Byte-floor note (also deferred with grok-web): the bundled hook skips
53
+ // transcripts < 5 KB unless sessionType is specifically exempted (only
54
+ // 'antigravity' is today). Our materialized envelope is compact, so a SHORT
55
+ // (<5 KB) web-chat session is currently dropped — substantive auditor/worker
56
+ // sessions (the real use case) run well past 5 KB and capture normally. The
57
+ // exemption is a hook edit, so it rides the same deferred follow-up.
58
+ //
59
+ // Contract — see ./claude.js header for the full annotated adapter shape.
60
+
61
+ 'use strict';
62
+
63
+ // ──────────────────────────────────────────────────────────────────────────
64
+ // Patterns. A web-chat panel produces no PTY output, so there is intentionally
65
+ // NO `prompt` pattern (would let detectAdapter steal a real PTY panel's output)
66
+ // and NO `error` pattern (chat prose mentioning "Error:" is not a panel error;
67
+ // index.js never runs `_detectErrors` on web-chat text). `thinking` is the one
68
+ // useful text shape: if a completed response somehow still carries Grok's
69
+ // shimmer label, treat it as still-working. Reused conceptually from grok.js.
70
+ // ──────────────────────────────────────────────────────────────────────────
71
+
72
+ const THINKING = /Planning next moves|Generating plan[….]|Answering[….]|\bThinking\b/;
73
+
74
+ // ──────────────────────────────────────────────────────────────────────────
75
+ // statusFor — text → { status, statusDetail } | null. By the time index.js
76
+ // routes a string here it is a COMPLETED Grok response (the onComplete event
77
+ // already fired), so the dominant outcome is 'idle' (Grok is done, awaiting the
78
+ // next prompt). The thinking branch is a defensive backstop for the unlikely
79
+ // case a streaming/partial chunk is routed through. null on empty/non-string
80
+ // preserves the contract's "leave status untouched" semantics.
81
+ // ──────────────────────────────────────────────────────────────────────────
82
+
83
+ function statusFor(data) {
84
+ if (typeof data !== 'string' || data.length === 0) return null;
85
+ if (THINKING.test(data)) {
86
+ return { status: 'thinking', statusDetail: 'Grok is responding…' };
87
+ }
88
+ return { status: 'idle', statusDetail: 'Ready' };
89
+ }
90
+
91
+ // ──────────────────────────────────────────────────────────────────────────
92
+ // parseTranscript — web-chat capture is ALWAYS structured (the server seam
93
+ // builds `[{role,content}]` turns; there is no raw-ANSI path like agy's TUI
94
+ // scrape). Dual-mode for round-trip safety: accept this adapter's own
95
+ // Gemini-shaped envelope `{messages:[{type,content}]}` AND a bare
96
+ // `[{role,content}]` array. Returns [] on empty/garbage (fail-soft parity).
97
+ // Content truncated to 400 chars to match the other adapters' parsers and the
98
+ // hook's summary builder.
99
+ // ──────────────────────────────────────────────────────────────────────────
100
+
101
+ function parseTranscript(raw) {
102
+ if (typeof raw !== 'string' || raw.length === 0) return [];
103
+ let obj;
104
+ try { obj = JSON.parse(raw); }
105
+ catch (_) { return []; }
106
+ const rows = Array.isArray(obj)
107
+ ? obj
108
+ : (obj && Array.isArray(obj.messages) ? obj.messages : null);
109
+ if (!rows) return [];
110
+ const out = [];
111
+ for (const m of rows) {
112
+ if (!m || typeof m !== 'object') continue;
113
+ // Accept both shapes: {role} (array form) and {type} (envelope form). Only
114
+ // 'user'/'assistant' pass in EITHER field; any other value is dropped — same
115
+ // as the bundled hook's parseGeminiJson treats the envelope (we only ever
116
+ // materialize 'user'/'assistant', so this is strictness, not behavior loss).
117
+ let role = null;
118
+ if (m.role === 'user' || m.role === 'assistant') role = m.role;
119
+ else if (m.type === 'user' || m.type === 'assistant') role = m.type;
120
+ if (role !== 'user' && role !== 'assistant') continue;
121
+ const content = m.content;
122
+ let text = '';
123
+ if (typeof content === 'string') text = content;
124
+ else if (Array.isArray(content)) {
125
+ text = content
126
+ .filter((c) => c && typeof c.text === 'string')
127
+ .map((c) => c.text)
128
+ .join(' ');
129
+ }
130
+ if (text) out.push({ role, content: text.slice(0, 400) });
131
+ }
132
+ return out;
133
+ }
134
+
135
+ // ──────────────────────────────────────────────────────────────────────────
136
+ // resolveTranscriptPath — Sprint 72 T2. No on-disk transcript exists; the
137
+ // server seam accumulates turns into `session._webChatTranscript.turns`. We
138
+ // materialize that into a Gemini-shaped `{messages:[{type,content}]}` tempfile
139
+ // the bundled hook's parseAutoDetect ingests. Returns null when the panel
140
+ // produced no turn so onPanelClose + the periodic-capture timer no-op cleanly.
141
+ //
142
+ // Called by BOTH onPanelClose (once, at close) and onPanelPeriodicCapture
143
+ // (every interval) — each call re-materializes the current buffer so the
144
+ // periodic timer's size-delta throttle sees the transcript grow. Mirrors agy's
145
+ // resolveTranscriptPath exactly (same envelope shape, same tmpfile discipline).
146
+ // ──────────────────────────────────────────────────────────────────────────
147
+
148
+ // Per-turn content cap. The hook truncates to 400 for the summary, but storing
149
+ // a bit more keeps the envelope useful for any future richer consumer while
150
+ // bounding tempfile size on a long auditor session.
151
+ const MAX_TURN_CHARS = 4000;
152
+
153
+ async function resolveTranscriptPath(session) {
154
+ const fs = require('fs');
155
+ const path = require('path');
156
+ const os = require('os');
157
+ if (!session || !session.meta) return null;
158
+ const buf = session._webChatTranscript;
159
+ if (!buf || !Array.isArray(buf.turns) || buf.turns.length === 0) return null;
160
+
161
+ const messages = [];
162
+ for (const t of buf.turns) {
163
+ if (!t || (t.role !== 'user' && t.role !== 'assistant')) continue;
164
+ const content = typeof t.content === 'string' ? t.content : '';
165
+ if (!content) continue;
166
+ messages.push({ type: t.role, content: content.slice(0, MAX_TURN_CHARS) });
167
+ }
168
+ if (messages.length === 0) return null;
169
+
170
+ const envelope = { messages };
171
+ const safeId = String(session.id || `unknown-${session.pid || ''}`)
172
+ .replace(/[^a-zA-Z0-9._-]/g, '_');
173
+ const tmpfile = path.join(os.tmpdir(), `termdeck-webchat-${safeId}.json`);
174
+ try {
175
+ fs.writeFileSync(tmpfile, JSON.stringify(envelope), 'utf8');
176
+ } catch (_) {
177
+ return null; // fail-soft — a tmpfile write failure must not block teardown
178
+ }
179
+ return tmpfile;
180
+ }
181
+
182
+ // ──────────────────────────────────────────────────────────────────────────
183
+ // bootPromptTemplate — a web-chat Grok panel used as a 4+1 lane gets its boot
184
+ // prompt INJECTED into the composer (via the server's two-stage inject seam),
185
+ // not typed into a CLI. Same memory_recall + read-instructional-file + read-
186
+ // sprint-docs scaffold as the Grok CLI adapter; points at AGENTS.md (Grok's
187
+ // project-prompt convention). Contract-complete placeholder.
188
+ // ──────────────────────────────────────────────────────────────────────────
189
+
190
+ function bootPromptTemplate(lane = {}, sprint = {}) {
191
+ const tn = lane.id || 'T?';
192
+ const sprintNum = sprint.number || '?';
193
+ const sprintName = sprint.name || 'unnamed';
194
+ const project = (lane.project || sprint.project || 'termdeck');
195
+ const briefing = lane.briefingPath || `docs/sprint-${sprintNum}-${sprintName}/${tn}-<lane>.md`;
196
+ const topic = lane.topic || lane.briefingPath || sprintName;
197
+ return [
198
+ `You are ${tn} in Sprint ${sprintNum} (${sprintName}). Boot sequence:`,
199
+ `1. memory_recall(project="${project}", query="${topic}")`,
200
+ `2. memory_recall(query="recent decisions and bugs")`,
201
+ `3. Read ~/.claude/CLAUDE.md and ./AGENTS.md`,
202
+ `4. Read docs/sprint-${sprintNum}-${sprintName}/PLANNING.md`,
203
+ `5. Read docs/sprint-${sprintNum}-${sprintName}/STATUS.md`,
204
+ `6. Read ${briefing}`,
205
+ '',
206
+ 'Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in STATUS.md.',
207
+ "Don't bump versions, don't touch CHANGELOG, don't commit.",
208
+ ].join('\n');
209
+ }
210
+
211
+ const webChatGrokAdapter = {
212
+ name: 'web-chat-grok',
213
+ sessionType: 'web-chat',
214
+ // ORCH decision 2026-06-08 (Blocker 3): reuse the already-allow-listed 'grok'
215
+ // tag so this sprint touches zero release-sensitive hook surface. A distinct
216
+ // 'grok-web' tag is deferred (needs a hook allowlist edit + version bump).
217
+ // See the "source_agent attribution" header for the full rationale.
218
+ sourceAgent: 'grok',
219
+ // Sprint 50 T3 — human-readable label for launcher buttons + panel headers.
220
+ displayName: 'Grok (Web)',
221
+ // Provider URL the CDP driver navigates the dedicated-profile tab to on
222
+ // attach (T1's `cdp.attach({startUrl})` defaults to about:blank). Provider-
223
+ // owned so a future web-chat-<provider> adapter sets its own. The server seam
224
+ // reads this and passes it through as `startUrl`.
225
+ webChatUrl: 'https://grok.com',
226
+ // CRITICAL: never claim a command-spawned session. web-chat panels are created
227
+ // ONLY via an explicit `type:'web-chat'` on POST /api/sessions — never by
228
+ // output sniffing or command-string match. Returning false here (and carrying
229
+ // no `patterns.prompt` below) guarantees this adapter can never hijack a real
230
+ // PTY panel's detection in detectAdapter()/the direct-spawn loop.
231
+ matches: () => false,
232
+ // No `spawn` block — there is no binary. The direct-spawn loop in index.js is
233
+ // gated on `matches()` (always false here) so it is never reached.
234
+ patterns: {
235
+ // Intentionally NO `prompt` (see matches) and NO `error` (chat prose is not
236
+ // a panel error). Only the thinking shimmer, used by statusFor.
237
+ thinking: THINKING,
238
+ },
239
+ patternNames: {},
240
+ statusFor,
241
+ parseTranscript,
242
+ // 10th adapter field — materializes the in-flight turn buffer into a tempfile
243
+ // envelope (see header). Its PRESENCE is what makes onPanelClose +
244
+ // onPanelPeriodicCapture fire for web-chat panels.
245
+ resolveTranscriptPath,
246
+ bootPromptTemplate,
247
+ // The whole point of the web path: flat-rate subscription, not per-token.
248
+ costBand: 'subscription',
249
+ // N/A for a browser composer — the server seam assembles the 4+1 two-stage
250
+ // paste/submit into a single `grok.inject(handle, fullText)` call, so there is
251
+ // no PTY bracketed-paste handler to be capable-or-not. Declared for contract
252
+ // completeness.
253
+ acceptsPaste: true,
254
+ // No MCP config to auto-wire — grok.com is a browser session, not a CLI with
255
+ // an MCP-server registry file. null = user-managed/none (same as Claude).
256
+ mcpConfig: null,
257
+ };
258
+
259
+ module.exports = webChatGrokAdapter;