clideck 1.22.7 → 1.23.2
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/.github/pull_request_template.md +27 -0
- package/CONTRIBUTING.md +53 -0
- package/README.md +5 -1
- package/activity.js +6 -1
- package/config.js +5 -3
- package/handlers.js +104 -20
- package/package.json +1 -1
- package/plugin-loader.js +40 -4
- package/public/img/clideck-logo-icon.png +0 -0
- package/public/img/clideck-logo-terminal-panel.png +0 -0
- package/public/index.html +103 -1
- package/public/js/app.js +346 -13
- package/public/js/creator.js +20 -10
- package/public/js/hotkeys.js +6 -0
- package/public/js/prompts.js +88 -30
- package/public/js/settings.js +1 -1
- package/public/js/terminals.js +29 -4
- package/public/tailwind.css +1 -1
- package/server.js +1 -1
- package/sessions.js +28 -12
- package/telemetry-receiver.js +0 -8
- package/transcript.js +168 -5
package/transcript.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { appendFile, mkdirSync, existsSync, readdirSync, readFileSync, unlinkSync } = require('fs');
|
|
1
|
+
const { appendFile, writeFileSync, mkdirSync, existsSync, readdirSync, readFileSync, unlinkSync } = require('fs');
|
|
2
2
|
const { join, basename } = require('path');
|
|
3
3
|
const { DATA_DIR } = require('./paths');
|
|
4
4
|
|
|
@@ -9,11 +9,19 @@ const MAX_CACHE = 50 * 1024;
|
|
|
9
9
|
const inputBuf = {};
|
|
10
10
|
const outputBuf = {};
|
|
11
11
|
const cache = {};
|
|
12
|
+
const prefixes = {};
|
|
12
13
|
let broadcast = null;
|
|
14
|
+
let notifyPlugin = null;
|
|
13
15
|
|
|
14
|
-
function init(bc, validIds) {
|
|
16
|
+
function init(bc, validIds, pluginNotify) {
|
|
15
17
|
broadcast = bc;
|
|
18
|
+
notifyPlugin = pluginNotify || null;
|
|
16
19
|
if (!existsSync(DIR)) mkdirSync(DIR, { recursive: true });
|
|
20
|
+
// Clean up orphaned .screen files (no matching .jsonl or session)
|
|
21
|
+
for (const file of readdirSync(DIR).filter(f => f.endsWith('.screen'))) {
|
|
22
|
+
const id = basename(file, '.screen');
|
|
23
|
+
if (!existsSync(fpath(id)) && (!validIds || !validIds.has(id))) { try { unlinkSync(join(DIR, file)); } catch {} }
|
|
24
|
+
}
|
|
17
25
|
for (const file of readdirSync(DIR).filter(f => f.endsWith('.jsonl'))) {
|
|
18
26
|
const id = basename(file, '.jsonl');
|
|
19
27
|
if (validIds && !validIds.has(id)) { try { unlinkSync(join(DIR, file)); } catch {} continue; }
|
|
@@ -26,13 +34,16 @@ function init(bc, validIds) {
|
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
function fpath(id) { return join(DIR, `${id}.jsonl`); }
|
|
37
|
+
function setPrefix(id, prefix) { prefixes[id] = prefix; }
|
|
29
38
|
|
|
30
39
|
function store(id, role, text) {
|
|
31
|
-
|
|
40
|
+
const prefix = prefixes[id] || '';
|
|
41
|
+
appendFile(fpath(id), JSON.stringify({ ts: Date.now(), role, text, ...(prefix && { prefix }) }) + '\n', () => {});
|
|
32
42
|
if (!cache[id]) cache[id] = '';
|
|
33
43
|
cache[id] += '\n' + text;
|
|
34
44
|
if (cache[id].length > MAX_CACHE) cache[id] = cache[id].slice(-MAX_CACHE);
|
|
35
|
-
if (broadcast) broadcast({ type: 'transcript.append', id, text });
|
|
45
|
+
if (broadcast) broadcast({ type: 'transcript.append', id, role, text });
|
|
46
|
+
if (notifyPlugin) notifyPlugin(id, role, text);
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
function trackInput(id, data) {
|
|
@@ -58,6 +69,7 @@ function trackInput(id, data) {
|
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
|
|
72
|
+
// Server-side fallback: captures raw PTY output (noisy but always available)
|
|
61
73
|
function trackOutput(id, data) {
|
|
62
74
|
if (!outputBuf[id]) outputBuf[id] = { text: '', timer: null };
|
|
63
75
|
const buf = outputBuf[id];
|
|
@@ -75,6 +87,155 @@ function flush(id) {
|
|
|
75
87
|
if (lines.length) store(id, 'agent', lines.join('\n'));
|
|
76
88
|
}
|
|
77
89
|
|
|
90
|
+
// Browser-side clean snapshot: overwrites a per-session file with the full
|
|
91
|
+
// xterm buffer as rendered by the browser. No diffing, no JSONL — just the
|
|
92
|
+
// clean screen content. Mobile reads this for "last agent message".
|
|
93
|
+
function storeBuffer(id, lines) {
|
|
94
|
+
const isChrome = t => !t || /^[─━═\u2500-\u257f]+$/.test(t) || /^[❯>$%#]\s*$/.test(t) || t === 'esc to interrupt' || t === '? for shortcuts';
|
|
95
|
+
const filtered = lines.filter(l => !isChrome(l.trim()));
|
|
96
|
+
while (filtered.length && !filtered[filtered.length - 1].trim()) filtered.pop();
|
|
97
|
+
const screenPath = join(DIR, `${id}.screen`);
|
|
98
|
+
if (filtered.length) writeFileSync(screenPath, filtered.join('\n'));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Read the clean screen snapshot for a session (if available).
|
|
102
|
+
function getScreen(id) {
|
|
103
|
+
const file = join(DIR, `${id}.screen`);
|
|
104
|
+
if (!existsSync(file)) return null;
|
|
105
|
+
try { return readFileSync(file, 'utf8'); } catch { return null; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Per-agent screen parsers. Each returns [{role, text}] from .screen content.
|
|
109
|
+
const agentParsers = {
|
|
110
|
+
'claude-code': (lines) => {
|
|
111
|
+
const turns = [];
|
|
112
|
+
let current = null;
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
const m = line.match(/^(?:[│ ]\s*)?([❯›]|[⏺•])\s(.*)$/);
|
|
115
|
+
if (m) {
|
|
116
|
+
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
117
|
+
current = { role: m[1] === '❯' || m[1] === '›' ? 'user' : 'agent', text: m[2] };
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!current) continue;
|
|
121
|
+
let cont = line;
|
|
122
|
+
if (cont.startsWith('│ ')) cont = cont.slice(2);
|
|
123
|
+
else if (cont.startsWith(' ')) cont = cont.slice(2);
|
|
124
|
+
current.text += '\n' + cont;
|
|
125
|
+
}
|
|
126
|
+
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
127
|
+
return turns.length >= 2 ? turns : null;
|
|
128
|
+
},
|
|
129
|
+
'codex': (lines) => {
|
|
130
|
+
const turns = [];
|
|
131
|
+
let current = null;
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
const m = line.match(/^(?:│\s*)?([›•])\s(.*)$/);
|
|
134
|
+
if (m) {
|
|
135
|
+
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
136
|
+
current = { role: m[1] === '›' ? 'user' : 'agent', text: m[2] };
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!current) continue;
|
|
140
|
+
let cont = line;
|
|
141
|
+
if (cont.startsWith('│ ')) cont = cont.slice(2);
|
|
142
|
+
else if (cont.startsWith(' ')) cont = cont.slice(2);
|
|
143
|
+
current.text += '\n' + cont;
|
|
144
|
+
}
|
|
145
|
+
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
146
|
+
return turns.length >= 2 ? turns : null;
|
|
147
|
+
},
|
|
148
|
+
'gemini-cli': (lines) => {
|
|
149
|
+
const turns = [];
|
|
150
|
+
let current = null;
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const isUser = line.startsWith(' > ');
|
|
153
|
+
const isAgent = line.startsWith('✦ ');
|
|
154
|
+
if (isUser || isAgent) {
|
|
155
|
+
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
156
|
+
current = { role: isUser ? 'user' : 'agent', text: isUser ? line.slice(3) : line.slice(2) };
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!current) continue;
|
|
160
|
+
current.text += '\n' + line;
|
|
161
|
+
}
|
|
162
|
+
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
163
|
+
return turns.length >= 2 ? turns : null;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Fallback anchor parser for agents without a dedicated parser.
|
|
168
|
+
// Uses JSONL user inputs to find prompt lines in the screen.
|
|
169
|
+
function anchorParse(id, lines) {
|
|
170
|
+
const file = fpath(id);
|
|
171
|
+
if (!existsSync(file)) return null;
|
|
172
|
+
const users = [];
|
|
173
|
+
try {
|
|
174
|
+
for (const l of readFileSync(file, 'utf8').trim().split('\n')) {
|
|
175
|
+
try { const e = JSON.parse(l); if (e.role === 'user') users.push(e.text); } catch {}
|
|
176
|
+
}
|
|
177
|
+
} catch { return null; }
|
|
178
|
+
if (!users.length) return null;
|
|
179
|
+
|
|
180
|
+
const isPrompt = (line, text) => { const t = line.trim(); return t.endsWith(text) && t.length - text.length <= 6; };
|
|
181
|
+
|
|
182
|
+
const lastUser = users[users.length - 1];
|
|
183
|
+
let lastIdx = -1;
|
|
184
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
185
|
+
if (isPrompt(lines[i], lastUser)) { lastIdx = i; break; }
|
|
186
|
+
}
|
|
187
|
+
if (lastIdx < 0) return null;
|
|
188
|
+
|
|
189
|
+
const anchors = [{ idx: lastIdx, text: lastUser }];
|
|
190
|
+
for (let u = users.length - 2; u >= 0 && anchors.length < 3; u--) {
|
|
191
|
+
for (let i = anchors[anchors.length - 1].idx - 1; i >= 0; i--) {
|
|
192
|
+
if (isPrompt(lines[i], users[u])) { anchors.push({ idx: i, text: users[u] }); break; }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
anchors.reverse();
|
|
196
|
+
|
|
197
|
+
const turns = [];
|
|
198
|
+
for (let p = 0; p < anchors.length; p++) {
|
|
199
|
+
turns.push({ role: 'user', text: anchors[p].text });
|
|
200
|
+
const start = anchors[p].idx + 1;
|
|
201
|
+
const end = p + 1 < anchors.length ? anchors[p + 1].idx : lines.length;
|
|
202
|
+
const agentLines = lines.slice(start, end).filter(l => l.trim());
|
|
203
|
+
if (agentLines.length) turns.push({ role: 'agent', text: agentLines.join('\n') });
|
|
204
|
+
}
|
|
205
|
+
if (turns.length < 2 || turns[turns.length - 1].role !== 'agent') return null;
|
|
206
|
+
return turns;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Parse .screen into structured turns — use agent-specific parser if available, else anchor fallback.
|
|
210
|
+
function getScreenTurns(id, agent) {
|
|
211
|
+
const screen = getScreen(id);
|
|
212
|
+
if (!screen) return null;
|
|
213
|
+
const lines = screen.split('\n');
|
|
214
|
+
const parser = agentParsers[agent];
|
|
215
|
+
return parser ? parser(lines) : anchorParse(id, lines);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getLastTurns(id, n) {
|
|
219
|
+
n = n || 4;
|
|
220
|
+
const file = fpath(id);
|
|
221
|
+
if (!existsSync(file)) return [];
|
|
222
|
+
try {
|
|
223
|
+
const lines = readFileSync(file, 'utf8').trim().split('\n');
|
|
224
|
+
const turns = [];
|
|
225
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
226
|
+
let entry;
|
|
227
|
+
try { entry = JSON.parse(lines[i]); } catch { continue; }
|
|
228
|
+
if (turns.length && turns[turns.length - 1].role === entry.role) {
|
|
229
|
+
turns[turns.length - 1].text = entry.text + '\n' + turns[turns.length - 1].text;
|
|
230
|
+
} else {
|
|
231
|
+
turns.push({ role: entry.role, text: entry.text });
|
|
232
|
+
if (turns.length >= n) break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return turns.reverse();
|
|
236
|
+
} catch { return []; }
|
|
237
|
+
}
|
|
238
|
+
|
|
78
239
|
function getCache() { return { ...cache }; }
|
|
79
240
|
|
|
80
241
|
function clear(id) {
|
|
@@ -85,6 +246,8 @@ function clear(id) {
|
|
|
85
246
|
delete outputBuf[id];
|
|
86
247
|
}
|
|
87
248
|
delete cache[id];
|
|
249
|
+
delete prefixes[id];
|
|
250
|
+
try { unlinkSync(join(DIR, `${id}.screen`)); } catch {}
|
|
88
251
|
}
|
|
89
252
|
|
|
90
|
-
module.exports = { init, trackInput, trackOutput, getCache, clear };
|
|
253
|
+
module.exports = { init, trackInput, trackOutput, storeBuffer, getScreen, getScreenTurns, getLastTurns, getCache, clear, setPrefix };
|