@jhizzard/termdeck 0.12.0 → 0.13.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.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -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,55 @@
|
|
|
1
|
+
// Agent adapter registry — Sprint 44 T3
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for per-agent terminal behavior. Each adapter
|
|
4
|
+
// implements the contract documented in ./claude.js (and the memorialization
|
|
5
|
+
// doc § 4) — type detection, status patterns, transcript parsing, boot prompt
|
|
6
|
+
// templating, cost band. session.js consults this registry when analyzing
|
|
7
|
+
// PTY output so adding a new agent (Codex / Gemini / Grok in Sprint 45) is a
|
|
8
|
+
// new file in this directory + one entry in `AGENT_ADAPTERS` below — no
|
|
9
|
+
// switch statements to extend.
|
|
10
|
+
//
|
|
11
|
+
// Sprint 44 lands the Claude adapter only. The other agents stay on the
|
|
12
|
+
// in-file shim path in session.js until Sprint 45 T1-T3 ship their adapters
|
|
13
|
+
// and Sprint 45 T4 wires the launcher UI through the same registry.
|
|
14
|
+
|
|
15
|
+
const claude = require('./claude');
|
|
16
|
+
|
|
17
|
+
// Keyed by adapter name (NOT session.meta.type — adapters expose their own
|
|
18
|
+
// `sessionType` field for that mapping). Order is iteration order for the
|
|
19
|
+
// detect loop in session.js, so list more-specific adapters before less.
|
|
20
|
+
const AGENT_ADAPTERS = {
|
|
21
|
+
claude,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Convenience accessor — returns the adapter whose `sessionType` matches the
|
|
25
|
+
// session.meta.type value, or undefined if no adapter has claimed that type.
|
|
26
|
+
// session.js calls this in the hot path; keep it cheap.
|
|
27
|
+
function getAdapterForSessionType(type) {
|
|
28
|
+
if (!type) return undefined;
|
|
29
|
+
for (const adapter of Object.values(AGENT_ADAPTERS)) {
|
|
30
|
+
if (adapter.sessionType === type) return adapter;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Convenience accessor — first adapter whose prompt regex matches `data` or
|
|
36
|
+
// whose `matches(command)` returns true. Returns undefined if no adapter
|
|
37
|
+
// claims the session, leaving the caller to fall back to legacy detection
|
|
38
|
+
// (gemini / python-server / shell). session.js calls this from `_detectType`.
|
|
39
|
+
function detectAdapter(data, command) {
|
|
40
|
+
for (const adapter of Object.values(AGENT_ADAPTERS)) {
|
|
41
|
+
const promptHit = adapter.patterns
|
|
42
|
+
&& adapter.patterns.prompt
|
|
43
|
+
&& typeof data === 'string'
|
|
44
|
+
&& adapter.patterns.prompt.test(data);
|
|
45
|
+
const cmdHit = typeof adapter.matches === 'function' && adapter.matches(command);
|
|
46
|
+
if (promptHit || cmdHit) return adapter;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
AGENT_ADAPTERS,
|
|
53
|
+
getAdapterForSessionType,
|
|
54
|
+
detectAdapter,
|
|
55
|
+
};
|
|
@@ -15,6 +15,8 @@ const os = require('os');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { resolveTheme } = require('./theme-resolver');
|
|
17
17
|
const flashbackDiag = require('./flashback-diag');
|
|
18
|
+
const claudeAdapter = require('./agent-adapters/claude');
|
|
19
|
+
const { detectAdapter, getAdapterForSessionType } = require('./agent-adapters');
|
|
18
20
|
|
|
19
21
|
// Strip ANSI escape codes for pattern matching
|
|
20
22
|
function stripAnsi(str) {
|
|
@@ -25,14 +27,22 @@ function stripAnsi(str) {
|
|
|
25
27
|
.replace(/\x1b[>=<]/g, ''); // Keypad/cursor modes
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
// Pattern matchers for detecting terminal type and status
|
|
30
|
+
// Pattern matchers for detecting terminal type and status.
|
|
31
|
+
//
|
|
32
|
+
// Sprint 44 T3: claudeCode patterns are owned by the Claude adapter at
|
|
33
|
+
// ./agent-adapters/claude.js. This object continues to expose them under
|
|
34
|
+
// the legacy `PATTERNS.claudeCode.*` shape so external callers
|
|
35
|
+
// (tests/rcfile-noise.test.js, tests/analyzer-error-fixtures.test.js, the
|
|
36
|
+
// rcfile-noise analyze.js fixture script) keep working without import
|
|
37
|
+
// changes. Sprint 45 T4 removes this shim — new code should consume the
|
|
38
|
+
// adapter directly via require('./agent-adapters/claude').
|
|
29
39
|
const PATTERNS = {
|
|
30
40
|
claudeCode: {
|
|
31
|
-
prompt:
|
|
32
|
-
thinking:
|
|
33
|
-
editing:
|
|
34
|
-
tool:
|
|
35
|
-
idle:
|
|
41
|
+
prompt: claudeAdapter.patterns.prompt,
|
|
42
|
+
thinking: claudeAdapter.patterns.thinking,
|
|
43
|
+
editing: claudeAdapter.patterns.editing,
|
|
44
|
+
tool: claudeAdapter.patterns.tool,
|
|
45
|
+
idle: claudeAdapter.patterns.idle
|
|
36
46
|
},
|
|
37
47
|
geminiCli: {
|
|
38
48
|
prompt: /^gemini>\s/m,
|
|
@@ -86,7 +96,11 @@ const PATTERNS = {
|
|
|
86
96
|
// Sprint 40 T2: added mixed-case `Fatal` (mirrors `fatal` / `FATAL`) and
|
|
87
97
|
// the `npm ERR!` shape (special-cased outside the alternation because
|
|
88
98
|
// `!` is not a word character so `\b` after `npm ERR!` doesn't match).
|
|
89
|
-
|
|
99
|
+
// Sprint 44 T3: this regex is now owned by the Claude adapter
|
|
100
|
+
// (./agent-adapters/claude.js patterns.error). The shim below preserves
|
|
101
|
+
// the legacy PATTERNS.errorLineStart export — same regex object, so any
|
|
102
|
+
// existing reference equality (e.g. `=== PATTERNS.errorLineStart`) holds.
|
|
103
|
+
errorLineStart: claudeAdapter.patterns.error,
|
|
90
104
|
// Sprint 33: PATTERNS.error misses the most common Unix shell errors —
|
|
91
105
|
// `cat: /foo: No such file or directory`, `bash: foo: command not found`,
|
|
92
106
|
// `rm: cannot remove ...: Permission denied`. These have a colon-prefix
|
|
@@ -241,9 +255,18 @@ class Session {
|
|
|
241
255
|
}
|
|
242
256
|
|
|
243
257
|
_detectType(data) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
258
|
+
// Sprint 44 T3: registry-aware detection. detectAdapter() iterates
|
|
259
|
+
// AGENT_ADAPTERS in declaration order and returns the first hit by
|
|
260
|
+
// prompt regex OR command-string match. Sprint 44 lands Claude only
|
|
261
|
+
// (so this returns the Claude adapter or undefined); Sprint 45 adds
|
|
262
|
+
// Codex / Gemini / Grok adapters and the gemini fall-through below
|
|
263
|
+
// moves into gemini.js.
|
|
264
|
+
const adapter = detectAdapter(data, this.meta.command);
|
|
265
|
+
if (adapter) {
|
|
266
|
+
this.meta.type = adapter.sessionType;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (PATTERNS.geminiCli.prompt.test(data) || /gemini/i.test(this.meta.command)) {
|
|
247
270
|
this.meta.type = 'gemini';
|
|
248
271
|
} else if (
|
|
249
272
|
PATTERNS.pythonServer.uvicorn.test(data) ||
|
|
@@ -259,24 +282,21 @@ class Session {
|
|
|
259
282
|
const p = PATTERNS;
|
|
260
283
|
const oldStatus = this.meta.status;
|
|
261
284
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
break;
|
|
279
|
-
|
|
285
|
+
// Sprint 44 T3: claude-code status detection now lives in the Claude
|
|
286
|
+
// adapter's `statusFor(data)` method. Returns { status, statusDetail }
|
|
287
|
+
// on a match, null on no-change — preserves the original switch's
|
|
288
|
+
// "leave status untouched if no claude pattern fires" semantics.
|
|
289
|
+
// Other types (gemini, python-server, default shell) stay in-file
|
|
290
|
+
// until Sprint 45 migrates them.
|
|
291
|
+
const adapter = getAdapterForSessionType(this.meta.type);
|
|
292
|
+
if (adapter && typeof adapter.statusFor === 'function') {
|
|
293
|
+
const result = adapter.statusFor(data);
|
|
294
|
+
if (result && result.status) {
|
|
295
|
+
this.meta.status = result.status;
|
|
296
|
+
this.meta.statusDetail = result.statusDetail || '';
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
switch (this.meta.type) {
|
|
280
300
|
case 'gemini':
|
|
281
301
|
if (p.geminiCli.thinking.test(data)) {
|
|
282
302
|
this.meta.status = 'thinking';
|
|
@@ -309,6 +329,7 @@ class Session {
|
|
|
309
329
|
} else {
|
|
310
330
|
this.meta.status = 'active';
|
|
311
331
|
}
|
|
332
|
+
}
|
|
312
333
|
}
|
|
313
334
|
|
|
314
335
|
// Debounce status change events (3s) to avoid flooding RAG with active↔idle flaps
|
|
@@ -387,10 +408,20 @@ class Session {
|
|
|
387
408
|
// Claude Code's tool output frequently contains "error"/"Error" mid-line
|
|
388
409
|
// (grep matches, test results, log dumps). Use a line-anchored pattern
|
|
389
410
|
// for that session type so we don't flag content as failure.
|
|
390
|
-
|
|
391
|
-
|
|
411
|
+
//
|
|
412
|
+
// Sprint 44 T3: per-agent primary error pattern is now read off the
|
|
413
|
+
// adapter (`patterns.error` + `patternNames.error`). Falls back to the
|
|
414
|
+
// generic prose-shape PATTERNS.error when no adapter has claimed the
|
|
415
|
+
// session type. The Claude adapter's `patterns.error` IS the same regex
|
|
416
|
+
// object as PATTERNS.errorLineStart (the shim wires them together), so
|
|
417
|
+
// existing `=== PATTERNS.errorLineStart` reference checks still hold.
|
|
418
|
+
const adapter = getAdapterForSessionType(this.meta.type);
|
|
419
|
+
const primaryPattern = adapter && adapter.patterns && adapter.patterns.error
|
|
420
|
+
? adapter.patterns.error
|
|
392
421
|
: PATTERNS.error;
|
|
393
|
-
const primaryName =
|
|
422
|
+
const primaryName = adapter && adapter.patternNames && adapter.patternNames.error
|
|
423
|
+
? adapter.patternNames.error
|
|
424
|
+
: 'error';
|
|
394
425
|
// Sprint 33 fix: the structured patterns above miss `cat: /foo: No such
|
|
395
426
|
// file or directory` and friends — the most common Unix shell error
|
|
396
427
|
// shapes Josh hits day-to-day. Fall through to PATTERNS.shellError so
|