@jhizzard/termdeck 0.12.0 → 0.14.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 +3 -2
- package/packages/client/public/app.js +60 -16
- package/packages/server/src/agent-adapters/claude.js +158 -0
- package/packages/server/src/agent-adapters/codex.js +199 -0
- package/packages/server/src/agent-adapters/gemini.js +158 -0
- package/packages/server/src/agent-adapters/grok-models.js +115 -0
- package/packages/server/src/agent-adapters/grok.js +253 -0
- package/packages/server/src/agent-adapters/index.js +61 -0
- package/packages/server/src/index.js +45 -6
- package/packages/server/src/rumen-pool-resilience.js +111 -0
- package/packages/server/src/session.js +72 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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"
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"start": "NODE_ENV=production node packages/cli/src/index.js",
|
|
31
31
|
"test": "node --test packages/server/tests/**/*.test.js",
|
|
32
32
|
"install:app": "bash install.sh",
|
|
33
|
-
"sync-rumen-functions": "bash scripts/sync-rumen-functions.sh"
|
|
33
|
+
"sync-rumen-functions": "bash scripts/sync-rumen-functions.sh",
|
|
34
|
+
"sync:agents": "node scripts/sync-agent-instructions.js"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
@@ -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,29 +2479,54 @@
|
|
|
2460
2479
|
return;
|
|
2461
2480
|
}
|
|
2462
2481
|
|
|
2463
|
-
//
|
|
2482
|
+
// Sprint 45 T4: registry-driven shorthand resolution. Pre-Sprint-45
|
|
2483
|
+
// had hardcoded claude/cc/gemini/python branches here; now the type
|
|
2484
|
+
// detection consults state.agentAdapters (loaded from
|
|
2485
|
+
// /api/agent-adapters at init), and only the Claude `cc` alias and
|
|
2486
|
+
// the python-server detection (no adapter exists) stay as
|
|
2487
|
+
// special-cases below. Adapter matching uses an anchored prefix on
|
|
2488
|
+
// the adapter's binary name (`^binary\b`, case-insensitive) which
|
|
2489
|
+
// fits all four Sprint-45 adapters (claude / codex / gemini / grok)
|
|
2490
|
+
// since each binary is uniquely named.
|
|
2464
2491
|
let resolvedCommand = command;
|
|
2465
2492
|
let resolvedType = 'shell';
|
|
2466
2493
|
let resolvedCwd = undefined;
|
|
2467
|
-
|
|
2468
2494
|
let resolvedProject = project || undefined;
|
|
2469
2495
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2496
|
+
// Claude `cc` alias normalization. Documented Claude shorthand —
|
|
2497
|
+
// does not generalize to other adapters, so it stays in client UX,
|
|
2498
|
+
// not in the server-side adapter contract.
|
|
2499
|
+
let canonical = command;
|
|
2500
|
+
if (/^cc\b/i.test(canonical)) {
|
|
2501
|
+
canonical = canonical.replace(/^cc\b/i, 'claude');
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
const adapter = (state.agentAdapters || []).find((a) =>
|
|
2505
|
+
a && a.binary && new RegExp(`^${a.binary}\\b`, 'i').test(canonical)
|
|
2506
|
+
);
|
|
2507
|
+
|
|
2508
|
+
if (adapter) {
|
|
2509
|
+
resolvedType = adapter.sessionType;
|
|
2510
|
+
// Claude shorthand: `claude <project-or-cwd>` rewrites to `claude`
|
|
2511
|
+
// and routes the trailing arg into either the project dropdown
|
|
2512
|
+
// (if it's a known project name) or the cwd parameter. Other
|
|
2513
|
+
// adapters' arg-parsing — codex sub-commands, gemini -p flag,
|
|
2514
|
+
// grok --model — pass through unchanged via resolvedCommand.
|
|
2515
|
+
if (adapter.name === 'claude') {
|
|
2516
|
+
const argMatch = canonical.match(/^claude\s+(?:code\s+)?(.+)/i);
|
|
2517
|
+
if (argMatch) {
|
|
2518
|
+
const arg = argMatch[1].trim();
|
|
2519
|
+
if (state.config.projects && state.config.projects[arg]) {
|
|
2520
|
+
resolvedProject = arg;
|
|
2521
|
+
} else {
|
|
2522
|
+
resolvedCwd = arg;
|
|
2523
|
+
}
|
|
2480
2524
|
}
|
|
2525
|
+
resolvedCommand = adapter.binary;
|
|
2481
2526
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
} else if (/^python3?\b.*(?:runserver|uvicorn|flask|gunicorn)/i.test(command)) {
|
|
2527
|
+
} else if (/^python3?\b.*(?:runserver|uvicorn|flask|gunicorn)/i.test(canonical)) {
|
|
2528
|
+
// python-server is a server SUBTYPE for status badges, not an
|
|
2529
|
+
// agent adapter. No registry entry for it; detection stays here.
|
|
2486
2530
|
resolvedType = 'python-server';
|
|
2487
2531
|
}
|
|
2488
2532
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Claude Code adapter — Sprint 44 T3
|
|
2
|
+
//
|
|
3
|
+
// First adapter in the AGENT_ADAPTERS registry (see ./index.js). Lifts the
|
|
4
|
+
// claude-code logic that previously lived as hardcoded branches in
|
|
5
|
+
// packages/server/src/session.js. Behavior is bit-for-bit identical to the
|
|
6
|
+
// pre-Sprint-44 inline path: same regexes, same status strings, same
|
|
7
|
+
// transcript-parser cut-offs. Sprint 45 adds Codex / Gemini / Grok adapters
|
|
8
|
+
// alongside this one; Sprint 46 wires per-lane agent assignment in 4+1.
|
|
9
|
+
//
|
|
10
|
+
// Contract (memorialization doc § 4 + lane brief T3):
|
|
11
|
+
// {
|
|
12
|
+
// name: string, // adapter id used in registry
|
|
13
|
+
// sessionType: string, // session.meta.type produced
|
|
14
|
+
// matches: (cmd) => boolean, // command-string detection
|
|
15
|
+
// spawn: { binary, defaultArgs, env },
|
|
16
|
+
// patterns: { prompt, thinking, editing, tool, idle, error },
|
|
17
|
+
// patternNames: { error: string }, // diag-event label preservation
|
|
18
|
+
// statusFor: (data) => { status, statusDetail } | null,
|
|
19
|
+
// parseTranscript:(raw) => Memory[], // for memory-session-end hook
|
|
20
|
+
// bootPromptTemplate: (lane, sprint) => string,
|
|
21
|
+
// costBand: 'free' | 'pay-per-token' | 'subscription',
|
|
22
|
+
// }
|
|
23
|
+
//
|
|
24
|
+
// `statusFor` returns null when no pattern matches — preserves the original
|
|
25
|
+
// "no change" semantics for the claude-code switch case. Caller leaves
|
|
26
|
+
// `meta.status` and `meta.statusDetail` untouched on null.
|
|
27
|
+
|
|
28
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
29
|
+
// Patterns — verbatim regexes lifted from session.js so the adapter and the
|
|
30
|
+
// shim remain reference-equal. Don't redeclare these elsewhere; import from
|
|
31
|
+
// the adapter so future tweaks land in one place.
|
|
32
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const PROMPT = /^[>❯]\s/m;
|
|
35
|
+
const THINKING = /\b(thinking|Thinking)\b/;
|
|
36
|
+
const EDITING = /^(Edit|Create|Update|Delete)\s/m;
|
|
37
|
+
const EDITING_DETAIL = /^(Edit|Create|Update|Delete)\s+(.+)$/m;
|
|
38
|
+
const TOOL = /^⏺\s/m;
|
|
39
|
+
const IDLE = /^>\s*$/m;
|
|
40
|
+
|
|
41
|
+
// errorLineStart from session.js — line-anchored variant for claude-code
|
|
42
|
+
// sessions whose tool output (grep results, test logs, file dumps) routinely
|
|
43
|
+
// mentions "Error" mid-line without representing an actual failure.
|
|
44
|
+
// Sprint 40 T2 added mixed-case `Fatal` + the special-cased `npm ERR!` shape.
|
|
45
|
+
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)\b|npm ERR!)/m;
|
|
46
|
+
|
|
47
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
48
|
+
// statusFor — replaces the `case 'claude-code':` block of _updateStatus.
|
|
49
|
+
// Order matters: thinking → editing → tool → idle. First match wins, exactly
|
|
50
|
+
// as the original switch did with cascading `else if`s.
|
|
51
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function statusFor(data) {
|
|
54
|
+
if (THINKING.test(data)) {
|
|
55
|
+
return { status: 'thinking', statusDetail: 'Claude is reasoning...' };
|
|
56
|
+
}
|
|
57
|
+
if (EDITING.test(data)) {
|
|
58
|
+
const match = data.match(EDITING_DETAIL);
|
|
59
|
+
return {
|
|
60
|
+
status: 'editing',
|
|
61
|
+
statusDetail: match ? `${match[1]} ${match[2]}` : 'Editing files',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (TOOL.test(data)) {
|
|
65
|
+
return { status: 'active', statusDetail: 'Using tools' };
|
|
66
|
+
}
|
|
67
|
+
if (IDLE.test(data)) {
|
|
68
|
+
return { status: 'idle', statusDetail: 'Waiting for input' };
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
74
|
+
// parseTranscript — Claude Code JSONL format, lifted from
|
|
75
|
+
// packages/stack-installer/assets/hooks/memory-session-end.js:88-102.
|
|
76
|
+
// Emits records of shape { role: 'user'|'assistant', content: string }
|
|
77
|
+
// truncated to 400 chars per message. The hook itself remains the consumer
|
|
78
|
+
// in Sprint 44; Sprint 45 T4 wires it to read from this adapter so other
|
|
79
|
+
// agents can plug in their own format parsers.
|
|
80
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function parseTranscript(raw) {
|
|
83
|
+
if (typeof raw !== 'string' || raw.length === 0) return [];
|
|
84
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
85
|
+
const messages = [];
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
let msg;
|
|
88
|
+
try { msg = JSON.parse(line); } catch (_) { continue; }
|
|
89
|
+
const role = msg && msg.message && msg.message.role;
|
|
90
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
91
|
+
const content = msg.message.content;
|
|
92
|
+
let text = '';
|
|
93
|
+
if (typeof content === 'string') {
|
|
94
|
+
text = content;
|
|
95
|
+
} else if (Array.isArray(content)) {
|
|
96
|
+
text = content
|
|
97
|
+
.filter((c) => c && c.type === 'text')
|
|
98
|
+
.map((c) => c.text)
|
|
99
|
+
.join(' ');
|
|
100
|
+
}
|
|
101
|
+
if (text) messages.push({ role, content: text.slice(0, 400) });
|
|
102
|
+
}
|
|
103
|
+
return messages;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
107
|
+
// bootPromptTemplate — minimal scaffold matching the global-CLAUDE.md 4+1
|
|
108
|
+
// boot block. Sprint 46 T2 will refine per-agent prompts; this is the
|
|
109
|
+
// placeholder so the adapter contract is complete in Sprint 44.
|
|
110
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function bootPromptTemplate(lane = {}, sprint = {}) {
|
|
113
|
+
const tn = lane.id || 'T?';
|
|
114
|
+
const sprintNum = sprint.number || '?';
|
|
115
|
+
const sprintName = sprint.name || 'unnamed';
|
|
116
|
+
const project = (lane.project || sprint.project || 'termdeck');
|
|
117
|
+
const briefing = lane.briefingPath || `docs/sprint-${sprintNum}-${sprintName}/${tn}-<lane>.md`;
|
|
118
|
+
return [
|
|
119
|
+
`You are ${tn} in Sprint ${sprintNum} (${sprintName}). Boot sequence:`,
|
|
120
|
+
`1. memory_recall(project="${project}", query="<topic>")`,
|
|
121
|
+
`2. memory_recall(query="<broader topic>")`,
|
|
122
|
+
`3. Read ~/.claude/CLAUDE.md and ./CLAUDE.md`,
|
|
123
|
+
`4. Read docs/sprint-${sprintNum}-${sprintName}/PLANNING.md`,
|
|
124
|
+
`5. Read docs/sprint-${sprintNum}-${sprintName}/STATUS.md`,
|
|
125
|
+
`6. Read ${briefing}`,
|
|
126
|
+
'',
|
|
127
|
+
'Then begin. Stay in your lane. Post FINDING / FIX-PROPOSED / DONE in STATUS.md.',
|
|
128
|
+
"Don't bump versions, don't touch CHANGELOG, don't commit.",
|
|
129
|
+
].join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const claudeAdapter = {
|
|
133
|
+
name: 'claude',
|
|
134
|
+
sessionType: 'claude-code',
|
|
135
|
+
matches: (cmd) => typeof cmd === 'string' && /claude/i.test(cmd),
|
|
136
|
+
spawn: {
|
|
137
|
+
binary: 'claude',
|
|
138
|
+
defaultArgs: [],
|
|
139
|
+
env: {},
|
|
140
|
+
},
|
|
141
|
+
patterns: {
|
|
142
|
+
prompt: PROMPT,
|
|
143
|
+
thinking: THINKING,
|
|
144
|
+
editing: EDITING,
|
|
145
|
+
tool: TOOL,
|
|
146
|
+
idle: IDLE,
|
|
147
|
+
error: ERROR,
|
|
148
|
+
},
|
|
149
|
+
patternNames: {
|
|
150
|
+
error: 'errorLineStart',
|
|
151
|
+
},
|
|
152
|
+
statusFor,
|
|
153
|
+
parseTranscript,
|
|
154
|
+
bootPromptTemplate,
|
|
155
|
+
costBand: 'pay-per-token',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
module.exports = claudeAdapter;
|
|
@@ -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;
|