@adhdev/daemon-core 0.5.7 → 0.5.16
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/dist/index.d.ts +460 -330
- package/dist/index.js +543 -101
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/providers/_builtin/cli/claude-cli/provider.json +34 -24
- package/providers/_builtin/cli/gemini-cli/provider.json +24 -17
- package/providers/_builtin/ide/antigravity/provider.json +21 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/resolve_action.js +19 -24
- package/providers/_builtin/ide/antigravity/scripts/1.107/read_chat.js +67 -5
- package/providers/_builtin/ide/antigravity/scripts/1.107/resolve_action.js +19 -24
- package/providers/_builtin/ide/cursor/scripts/0.49/read_chat.js +271 -32
- package/providers/_builtin/registry.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +96 -25
- package/src/commands/router.ts +132 -33
- package/src/daemon/dev-server.ts +285 -0
- package/src/daemon-core.ts +7 -6
- package/src/detection/ide-detector.ts +5 -5
- package/src/index.ts +24 -7
- package/src/launch.ts +2 -2
- package/src/providers/acp-provider-instance.ts +54 -56
- package/src/providers/cli-provider-instance.ts +32 -4
- package/src/providers/contracts.ts +5 -22
- package/src/providers/ide-provider-instance.ts +8 -10
- package/src/providers/provider-instance.ts +70 -33
- package/src/shared-types.ts +203 -0
- package/src/status/reporter.ts +31 -22
- package/src/types.ts +26 -110
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cursor — read_chat
|
|
2
|
+
* Cursor — read_chat (v2 — thoughts, tools, terminal, DOM-ordered)
|
|
3
3
|
*
|
|
4
4
|
* DOM 구조 (v0.49):
|
|
5
5
|
* 컴포저: [data-composer-id] + [data-composer-status]
|
|
6
6
|
* 메시지 쌍: .composer-human-ai-pair-container
|
|
7
7
|
* 사용자: .composer-human-message
|
|
8
|
-
* AI
|
|
9
|
-
*
|
|
8
|
+
* AI 텍스트: .composer-rendered-message (assistant content)
|
|
9
|
+
* 사고: .ui-step-group-collapsible → header에 "Thought briefly" 등
|
|
10
|
+
* 도구: .composer-tool-former-message (file edits, reads)
|
|
11
|
+
* 코드블록: .composer-code-block-container (terminal commands)
|
|
12
|
+
* Diff: .composer-diff-block (code changes)
|
|
10
13
|
* 승인: .run-command-review-active + button/cursor-pointer 요소
|
|
11
14
|
*
|
|
12
15
|
* → { id, status, title, messages[], inputContent, activeModal }
|
|
@@ -20,18 +23,12 @@
|
|
|
20
23
|
if (rawStatus === 'thinking' || rawStatus === 'streaming') status = 'generating';
|
|
21
24
|
else if (rawStatus === 'completed' || rawStatus === 'idle' || !rawStatus) status = 'idle';
|
|
22
25
|
|
|
23
|
-
//
|
|
26
|
+
// ─── Approval Detection ───
|
|
24
27
|
let activeModal = null;
|
|
25
|
-
|
|
26
|
-
// Primary signal: Cursor uses .run-command-review-active on conversations container
|
|
27
28
|
const reviewActive = !!document.querySelector('.run-command-review-active');
|
|
28
|
-
|
|
29
|
-
// Also check clickable elements (Cursor uses divs with cursor-pointer, not buttons)
|
|
30
|
-
// Note: Cursor concatenates button text with shortcut key labels (e.g. "SkipEsc", "Run⏎")
|
|
31
29
|
const clickableEls = [...document.querySelectorAll('button, [role="button"], .cursor-pointer')].filter(b =>
|
|
32
30
|
b.offsetWidth > 0 && /^(accept|reject|approve|deny|run|skip|allow|cancel)/i.test((b.textContent || b.getAttribute('aria-label') || '').trim())
|
|
33
31
|
);
|
|
34
|
-
|
|
35
32
|
if (reviewActive || clickableEls.length > 0) {
|
|
36
33
|
status = 'waiting_approval';
|
|
37
34
|
const reviewContainer = document.querySelector('.run-command-review-active');
|
|
@@ -44,36 +41,278 @@
|
|
|
44
41
|
};
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
// ─── Message Collection ───
|
|
45
|
+
const collected = [];
|
|
46
|
+
const seenHashes = new Set();
|
|
47
|
+
|
|
48
|
+
// Markdown Extractor to preserve tables/lists structure properly instead of flat innerText
|
|
49
|
+
function extractMarkdown(node) {
|
|
50
|
+
if (!node) return '';
|
|
51
|
+
if (node.nodeType === 3) return node.textContent;
|
|
52
|
+
if (node.nodeType !== 1) return '';
|
|
53
|
+
|
|
54
|
+
const tag = node.tagName.toLowerCase();
|
|
55
|
+
|
|
56
|
+
// Stop extracting into nested block containers that we already process as separate items
|
|
57
|
+
if (node.classList && (
|
|
58
|
+
node.classList.contains('composer-tool-former-message') ||
|
|
59
|
+
node.classList.contains('composer-code-block-container') ||
|
|
60
|
+
node.classList.contains('composer-diff-block')
|
|
61
|
+
)) {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let res = '';
|
|
66
|
+
if (tag === 'p' || tag === 'div') {
|
|
67
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
68
|
+
return res + '\n\n';
|
|
69
|
+
}
|
|
70
|
+
if (/^h[1-6]$/.test(tag)) {
|
|
71
|
+
const level = parseInt(tag.charAt(1), 10);
|
|
72
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
73
|
+
return '#'.repeat(level) + ' ' + res.trim() + '\n\n';
|
|
74
|
+
}
|
|
75
|
+
if (tag === 'ul' || tag === 'ol') {
|
|
76
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
77
|
+
return res + '\n';
|
|
78
|
+
}
|
|
79
|
+
if (tag === 'li') {
|
|
80
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
81
|
+
return '- ' + res.trim() + '\n';
|
|
82
|
+
}
|
|
83
|
+
if (tag === 'strong' || tag === 'b') {
|
|
84
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
85
|
+
return '**' + res + '**';
|
|
86
|
+
}
|
|
87
|
+
if (tag === 'em' || tag === 'i') {
|
|
88
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
89
|
+
return '*' + res + '*';
|
|
90
|
+
}
|
|
91
|
+
if (tag === 'code') {
|
|
92
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
93
|
+
return '`' + res + '`';
|
|
94
|
+
}
|
|
95
|
+
if (tag === 'pre') {
|
|
96
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
97
|
+
return '\n```\n' + res.trim() + '\n```\n\n';
|
|
98
|
+
}
|
|
99
|
+
if (tag === 'a') {
|
|
100
|
+
let text = '';
|
|
101
|
+
for (let i = 0; i < node.childNodes.length; i++) text += extractMarkdown(node.childNodes[i]);
|
|
102
|
+
return '[' + text + '](' + (node.href || '') + ')';
|
|
103
|
+
}
|
|
104
|
+
if (tag === 'table') {
|
|
105
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
106
|
+
return '\n' + res.trim() + '\n\n';
|
|
107
|
+
}
|
|
108
|
+
if (tag === 'thead') {
|
|
109
|
+
const tr = node.querySelector('tr');
|
|
110
|
+
if (tr) {
|
|
111
|
+
const ths = Array.from(tr.querySelectorAll('th')).map(th => extractMarkdown(th).trim());
|
|
112
|
+
return '| ' + ths.join(' | ') + ' |\n| ' + ths.map(() => '---').join(' | ') + ' |\n';
|
|
113
|
+
}
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
if (tag === 'tbody') {
|
|
117
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
118
|
+
return res;
|
|
119
|
+
}
|
|
120
|
+
if (tag === 'tr' && (!node.parentElement || node.parentElement.tagName.toLowerCase() !== 'thead')) {
|
|
121
|
+
const tds = Array.from(node.querySelectorAll('td')).map(td => extractMarkdown(td).trim().replace(/\n/g, '<br>'));
|
|
122
|
+
return '| ' + tds.join(' | ') + ' |\n';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Default
|
|
126
|
+
for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
|
|
127
|
+
return res;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
document.querySelectorAll('.composer-human-ai-pair-container').forEach((pair) => {
|
|
131
|
+
if (pair.children.length === 0) return; // virtual-scroll placeholder
|
|
132
|
+
|
|
133
|
+
// ─── User Message ───
|
|
134
|
+
const humanEl = pair.querySelector('.composer-human-message');
|
|
135
|
+
if (humanEl) {
|
|
136
|
+
const userText = (humanEl.innerText || '').trim().substring(0, 6000);
|
|
137
|
+
if (userText) {
|
|
138
|
+
const hash = 'user:' + userText.slice(0, 200);
|
|
139
|
+
if (!seenHashes.has(hash)) {
|
|
140
|
+
seenHashes.add(hash);
|
|
141
|
+
collected.push({ role: 'user', text: userText, el: humanEl, kind: 'standard' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── AI Message Blocks ───
|
|
147
|
+
const aiBlocks = Array.from(pair.querySelectorAll('.composer-assistant-message, [data-message-role="ai"], [data-message-role="assistant"]'));
|
|
148
|
+
for (const block of aiBlocks) {
|
|
149
|
+
|
|
150
|
+
// ─── Thinking ("Thought briefly", etc) ───
|
|
151
|
+
const uiStepGroup = block.querySelectorAll('.ui-step-group-collapsible, [class*="collapsible"]');
|
|
152
|
+
if (uiStepGroup.length > 0) {
|
|
153
|
+
uiStepGroup.forEach(group => {
|
|
154
|
+
const collapsibleHeader = group.querySelector('.ui-collapsible-header');
|
|
155
|
+
const spans = collapsibleHeader?.querySelectorAll('span');
|
|
156
|
+
const headerText = spans && spans.length > 0
|
|
157
|
+
? Array.from(spans).map(s => (s.textContent || '').trim()).filter(Boolean).join(' ')
|
|
158
|
+
: (collapsibleHeader?.innerText || collapsibleHeader?.textContent || '').trim();
|
|
159
|
+
|
|
160
|
+
// "Thought briefly", "Thought for Xs" — thinking content
|
|
161
|
+
if (headerText && /thought/i.test(headerText)) {
|
|
162
|
+
// Try to get expanded thought content
|
|
163
|
+
const contentEl = block.querySelector('.ui-collapsible-content, [class*="collapsible-content"]');
|
|
164
|
+
const thinkText = contentEl ? (contentEl.innerText || '').trim() : '';
|
|
165
|
+
const label = headerText;
|
|
166
|
+
const hash = 'think:' + (thinkText || headerText).slice(0, 200);
|
|
167
|
+
|
|
168
|
+
if (!seenHashes.has(hash)) {
|
|
169
|
+
seenHashes.add(hash);
|
|
170
|
+
// Prepend thinking into collected as a special block
|
|
171
|
+
let combined = `> **${label}**\n> \n`;
|
|
172
|
+
if (thinkText) {
|
|
173
|
+
combined += thinkText.split('\n').map(l => `> ${l}`).join('\n');
|
|
174
|
+
} else {
|
|
175
|
+
combined += '> ...\n'; // collapsed
|
|
176
|
+
}
|
|
177
|
+
collected.push({ role: 'assistant', text: combined, el: group, kind: 'thought' });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
// Note: If the entire block is just a thought, maybe continue, but usually there's rendered text after.
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Tools (Reads, Edits, etc) ───
|
|
185
|
+
const toolMsgs = block.querySelectorAll('.composer-tool-former-message');
|
|
186
|
+
if (toolMsgs.length > 0) {
|
|
187
|
+
toolMsgs.forEach(tm => {
|
|
188
|
+
let cleanText = '';
|
|
189
|
+
// For file modifications/reads, try to just grab the file path (first line)
|
|
190
|
+
// It usually looks like: "MachineDetail.tsx+3-1 Read file"
|
|
191
|
+
const rawText = (tm.innerText || '').trim();
|
|
192
|
+
if (rawText) {
|
|
193
|
+
cleanText = rawText.split('\n')[0].substring(0, 80)
|
|
194
|
+
.replace(/(?:Skip|Run|Accept|Reject)(?:Esc)?$/g, '').trim();
|
|
195
|
+
// If it looks like code/JSX, truncate further
|
|
196
|
+
if (/[<>{}()=]/.test(cleanText) && cleanText.length > 50) {
|
|
197
|
+
cleanText = cleanText.substring(0, 50) + '…';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!cleanText || cleanText.length < 2) return;
|
|
201
|
+
const hash = 'tool:' + cleanText;
|
|
202
|
+
if (!seenHashes.has(hash)) {
|
|
203
|
+
seenHashes.add(hash);
|
|
204
|
+
collected.push({ role: 'assistant', text: cleanText, el: tm, kind: 'tool' });
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── Code blocks / Terminal ───
|
|
210
|
+
const codeBlocks = block.querySelectorAll('.composer-code-block-container');
|
|
211
|
+
if (codeBlocks.length > 0) {
|
|
212
|
+
codeBlocks.forEach(cb => {
|
|
213
|
+
let codeText = (cb.textContent || '').trim().substring(0, 3000);
|
|
214
|
+
// Strip button text noise
|
|
215
|
+
codeText = codeText.replace(/(?:Skip|Run|Accept|Reject)(?:Esc|⏎|↵)?\s*$/g, '').trim();
|
|
216
|
+
codeText = codeText.replace(/\n(?:Skip|Run|Accept|Reject)(?:Esc|⏎|↵)?\s*/g, '\n').trim();
|
|
217
|
+
if (codeText.length < 2) return;
|
|
218
|
+
// Detect if it's a terminal command vs code block
|
|
219
|
+
const isTerminal = cb.querySelector('[class*="terminal"]') ||
|
|
220
|
+
cb.closest('.run-command-review-active') ||
|
|
221
|
+
/^\$\s/.test(codeText) ||
|
|
222
|
+
/^(?:cd |npm |node |npx |git |make |docker |curl |wget |ls |cat |mkdir |rm |mv |cp )/i.test(codeText);
|
|
223
|
+
const hash = (isTerminal ? 'term:' : 'code:') + codeText.slice(0, 200);
|
|
224
|
+
if (!seenHashes.has(hash)) {
|
|
225
|
+
seenHashes.add(hash);
|
|
226
|
+
// For terminal: extract first line as label
|
|
227
|
+
const label = isTerminal ? codeText.split('\n')[0].substring(0, 100) : undefined;
|
|
228
|
+
collected.push({
|
|
229
|
+
role: 'assistant',
|
|
230
|
+
text: codeText,
|
|
231
|
+
el: cb,
|
|
232
|
+
kind: isTerminal ? 'terminal' : 'code',
|
|
233
|
+
label
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Diff Blocks ───
|
|
240
|
+
const diffBlocks = block.querySelectorAll('.composer-diff-block');
|
|
241
|
+
if (diffBlocks.length > 0) {
|
|
242
|
+
diffBlocks.forEach(db => {
|
|
243
|
+
const diffText = (db.textContent || '').trim().substring(0, 2000);
|
|
244
|
+
if (diffText.length < 5) return;
|
|
245
|
+
const hash = 'diff:' + diffText.slice(0, 200);
|
|
246
|
+
if (!seenHashes.has(hash)) {
|
|
247
|
+
seenHashes.add(hash);
|
|
248
|
+
collected.push({ role: 'assistant', text: diffText, el: db, kind: 'tool' });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Rendered assistant message ───
|
|
254
|
+
const rendered = block.classList.contains('composer-rendered-message') ? block : block.querySelector('.composer-rendered-message');
|
|
255
|
+
if (rendered) {
|
|
256
|
+
const t = extractMarkdown(rendered).replace(/\n{3,}/g, '\n\n').trim();
|
|
257
|
+
if (t.length >= 2) {
|
|
258
|
+
// Strip button noise from end
|
|
259
|
+
const clean = t.replace(/(?:Skip|Run|Accept|Reject)(?:Esc)?\s*$/g, '').trim();
|
|
260
|
+
if (clean.length >= 2) {
|
|
261
|
+
const hash = 'assistant:' + clean.slice(0, 200);
|
|
262
|
+
if (!seenHashes.has(hash)) {
|
|
263
|
+
seenHashes.add(hash);
|
|
264
|
+
collected.push({ role: 'assistant', text: clean.substring(0, 6000), el: rendered, kind: 'standard' });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── Fallback: any remaining text block ───
|
|
272
|
+
const t = (block.innerText || '').trim();
|
|
60
273
|
if (t.length < 2) continue;
|
|
61
|
-
// Filter noise
|
|
274
|
+
// Filter noise
|
|
62
275
|
if (/^Thought\nfor \d+s?$/i.test(t) || /^Explored\n/i.test(t)) continue;
|
|
63
|
-
if (/^.{1,80}\n\d+s$/.test(t) && !t.includes('\n\n')) continue;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
276
|
+
if (/^.{1,80}\n\d+s$/.test(t) && !t.includes('\n\n')) continue;
|
|
277
|
+
|
|
278
|
+
const hash = 'other:' + t.slice(0, 200);
|
|
279
|
+
if (!seenHashes.has(hash)) {
|
|
280
|
+
seenHashes.add(hash);
|
|
281
|
+
collected.push({ role: 'assistant', text: t.substring(0, 6000), el: block, kind: 'standard' });
|
|
282
|
+
}
|
|
69
283
|
}
|
|
70
284
|
});
|
|
285
|
+
|
|
286
|
+
// ─── DOM Order Sort ───
|
|
287
|
+
collected.sort((a, b) => {
|
|
288
|
+
const pos = a.el.compareDocumentPosition(b.el);
|
|
289
|
+
if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
|
290
|
+
if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
|
291
|
+
return 0;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Keep last 50 messages
|
|
295
|
+
const trimmed = collected.length > 50 ? collected.slice(-50) : collected;
|
|
296
|
+
|
|
297
|
+
const messages = trimmed.map((m, i) => ({
|
|
298
|
+
id: 'msg_' + i,
|
|
299
|
+
role: m.role,
|
|
300
|
+
content: m.text.length > 6000 ? m.text.slice(0, 6000) + '\n[... truncated]' : m.text,
|
|
301
|
+
index: i,
|
|
302
|
+
kind: m.kind || 'standard',
|
|
303
|
+
meta: m.meta || undefined,
|
|
304
|
+
}));
|
|
305
|
+
|
|
306
|
+
// ─── Input Content ───
|
|
71
307
|
const inputEl = document.querySelector('.aislash-editor-input[contenteditable="true"]');
|
|
72
308
|
const inputContent = inputEl?.textContent?.trim() || '';
|
|
309
|
+
|
|
310
|
+
// ─── Title ───
|
|
73
311
|
const titleParts = document.title.split(' — ');
|
|
74
312
|
const projectTitle = (titleParts.length >= 2 ? titleParts[titleParts.length - 2] : titleParts[0] || '').trim();
|
|
75
|
-
|
|
313
|
+
|
|
314
|
+
return JSON.stringify({ id, status, title: projectTitle, messages, inputContent, activeModal });
|
|
76
315
|
} catch(e) {
|
|
77
|
-
return JSON.stringify({ id: '', status: 'error', messages: [] });
|
|
316
|
+
return JSON.stringify({ id: '', status: 'error', error: e.message, messages: [] });
|
|
78
317
|
}
|
|
79
318
|
})()
|
|
@@ -186,12 +186,11 @@ function coercePatternArray(raw: unknown, fallbacks: RegExp[]): RegExp[] {
|
|
|
186
186
|
const FALLBACK_PROMPT: RegExp[] = [
|
|
187
187
|
/Type your message/i,
|
|
188
188
|
/^>\s*$/m, // '>' alone on its own line
|
|
189
|
-
/[
|
|
190
|
-
/
|
|
191
|
-
|
|
189
|
+
/[›❯]\s*[\r\n]/, // prompt char followed by line ending (ANSI-stripped may not have $ at end)
|
|
190
|
+
/[›❯]\s*$/m, // prompt char at end of line (multiline)
|
|
191
|
+
/for\s*shortcuts/i, // Claude Code prompt (ANSI strip may remove spaces → 'forshortcuts')
|
|
192
|
+
/\?\s*for\s*help/i,
|
|
192
193
|
/Press enter/i,
|
|
193
|
-
// NOTE: removed /^[\s\u2500-\u257f]*>\s*$/m — the box-drawing char range is too wide and
|
|
194
|
-
// can match dialog-clearing ANSI output, causing false prompt detection in approval state.
|
|
195
194
|
];
|
|
196
195
|
|
|
197
196
|
const FALLBACK_GENERATING: RegExp[] = [
|
|
@@ -202,13 +201,14 @@ const FALLBACK_GENERATING: RegExp[] = [
|
|
|
202
201
|
];
|
|
203
202
|
|
|
204
203
|
const FALLBACK_APPROVAL: RegExp[] = [
|
|
205
|
-
/Allow\s
|
|
206
|
-
/Always\s
|
|
204
|
+
/Allow\s*once/i, // ANSI strip may remove spaces
|
|
205
|
+
/Always\s*allow/i,
|
|
207
206
|
/\(y\/n\)/i,
|
|
208
207
|
/\[Y\/n\]/i,
|
|
209
|
-
/Run\s
|
|
210
|
-
|
|
211
|
-
//
|
|
208
|
+
/Run\s*this\s*command/i,
|
|
209
|
+
/Allow\s*tool/i, // Claude Code v2 approval
|
|
210
|
+
/Yes,?\s*don'?t\s*ask/i, // "Yes, don't ask again" (Claude Code)
|
|
211
|
+
/Deny/i, // Deny button presence = approval dialog
|
|
212
212
|
];
|
|
213
213
|
|
|
214
214
|
function defaultCleanOutput(raw: string, _lastUserInput?: string): string {
|
|
@@ -268,9 +268,24 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
268
268
|
// Resize redraw suppression
|
|
269
269
|
private resizeSuppressUntil: number = 0;
|
|
270
270
|
|
|
271
|
+
// Debug: status transition history
|
|
272
|
+
private statusHistory: { status: string; at: number; trigger?: string }[] = [];
|
|
273
|
+
|
|
274
|
+
private setStatus(status: CliSessionStatus['status'], trigger?: string): void {
|
|
275
|
+
const prev = this.currentStatus;
|
|
276
|
+
if (prev === status) return;
|
|
277
|
+
this.currentStatus = status;
|
|
278
|
+
this.statusHistory.push({ status, at: Date.now(), trigger });
|
|
279
|
+
if (this.statusHistory.length > 50) this.statusHistory.shift();
|
|
280
|
+
LOG.info('CLI', `[${this.cliType}] status: ${prev} → ${status}${trigger ? ` (${trigger})` : ''}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
271
283
|
// Resolved timeouts (provider defaults + overrides)
|
|
272
284
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
273
285
|
|
|
286
|
+
// Provider approval key mapping (e.g. { 0: '1', 1: '2', 2: '3' }) — loaded from provider.json
|
|
287
|
+
private readonly approvalKeys: Record<number, string>;
|
|
288
|
+
|
|
274
289
|
constructor(provider: CliProviderModule, workingDir: string, private extraArgs: string[] = []) {
|
|
275
290
|
this.provider = normalizeCliProviderForRuntime(provider);
|
|
276
291
|
this.cliType = provider.type;
|
|
@@ -290,6 +305,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
290
305
|
maxResponse: t.maxResponse ?? 300000,
|
|
291
306
|
shutdownGrace: t.shutdownGrace ?? 1000,
|
|
292
307
|
};
|
|
308
|
+
|
|
309
|
+
// Load approval key mapping from provider (e.g. approvalKeys: {"0":"1","1":"2","2":"3"})
|
|
310
|
+
const rawKeys = (provider as any).approvalKeys;
|
|
311
|
+
this.approvalKeys = (rawKeys && typeof rawKeys === 'object') ? rawKeys : {};
|
|
293
312
|
}
|
|
294
313
|
|
|
295
314
|
// ─── Lifecycle ─────────────────────────────────
|
|
@@ -390,12 +409,12 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
390
409
|
this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
|
|
391
410
|
LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
|
|
392
411
|
this.ptyProcess = null;
|
|
393
|
-
this.
|
|
412
|
+
this.setStatus('stopped', 'pty_exit');
|
|
394
413
|
this.ready = false;
|
|
395
414
|
this.onStatusChange?.();
|
|
396
415
|
});
|
|
397
416
|
|
|
398
|
-
this.
|
|
417
|
+
this.setStatus('starting', 'spawn');
|
|
399
418
|
this.onStatusChange?.();
|
|
400
419
|
}
|
|
401
420
|
|
|
@@ -423,11 +442,15 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
423
442
|
// ─── Phase 1: Startup — ready status wait
|
|
424
443
|
if (!this.ready) {
|
|
425
444
|
this.startupBuffer += cleanData;
|
|
445
|
+
LOG.info('CLI', `[${this.cliType}] startup chunk (${cleanData.length} chars): ${cleanData.slice(0, 200).replace(/\n/g, '\\n')}`);
|
|
426
446
|
|
|
427
447
|
// Startup dialog auto-proceed (Enter)
|
|
428
448
|
const dialogPatterns = [
|
|
429
449
|
/Do you want to connect/i,
|
|
430
450
|
/Do you trust the files/i,
|
|
451
|
+
/Quick safety check/i,
|
|
452
|
+
/Is this a project/i,
|
|
453
|
+
/Enter to confirm/i,
|
|
431
454
|
];
|
|
432
455
|
if (dialogPatterns.some(p => p.test(this.startupBuffer))) {
|
|
433
456
|
setTimeout(() => this.ptyProcess?.write('\r'), this.timeouts.dialogAccept);
|
|
@@ -438,7 +461,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
438
461
|
// Prompt → ready
|
|
439
462
|
if (patterns.prompt.some(p => p.test(this.startupBuffer))) {
|
|
440
463
|
this.ready = true;
|
|
441
|
-
this.
|
|
464
|
+
this.setStatus('idle', 'prompt_matched');
|
|
442
465
|
LOG.info('CLI', `[${this.cliType}] ✓ Ready`);
|
|
443
466
|
this.onStatusChange?.();
|
|
444
467
|
}
|
|
@@ -455,12 +478,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
455
478
|
.map(l => l.trim())
|
|
456
479
|
.filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
|
|
457
480
|
this.isWaitingForResponse = true;
|
|
458
|
-
this.
|
|
481
|
+
this.setStatus('waiting_approval', 'approval_pattern');
|
|
459
482
|
this.recentOutputBuffer = '';
|
|
460
483
|
this.approvalTransitionBuffer = '';
|
|
461
484
|
this.activeModal = {
|
|
462
485
|
message: ctxLines.slice(-5).join(' ').slice(0, 200) || 'Approval required',
|
|
463
|
-
buttons:
|
|
486
|
+
buttons: this.cliType === 'claude-cli'
|
|
487
|
+
? ['Yes (y)', 'Always allow (a)', 'Deny (Esc)']
|
|
488
|
+
: ['Allow once', 'Always allow', 'Deny'],
|
|
464
489
|
};
|
|
465
490
|
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
466
491
|
// Safety timeout — if stuck in waiting_approval, auto-exit after 60s
|
|
@@ -473,7 +498,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
473
498
|
this.recentOutputBuffer = '';
|
|
474
499
|
this.approvalTransitionBuffer = '';
|
|
475
500
|
this.approvalExitTimeout = null;
|
|
476
|
-
this.
|
|
501
|
+
this.setStatus(this.isWaitingForResponse ? 'generating' : 'idle', 'approval_cleared');
|
|
477
502
|
this.onStatusChange?.();
|
|
478
503
|
}
|
|
479
504
|
}, 60000);
|
|
@@ -490,7 +515,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
490
515
|
const promptResume = patterns.prompt.some(p => p.test(this.approvalTransitionBuffer));
|
|
491
516
|
if (genResume) {
|
|
492
517
|
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
493
|
-
this.
|
|
518
|
+
this.setStatus('generating', 'approval_gen_resume');
|
|
494
519
|
this.activeModal = null;
|
|
495
520
|
this.recentOutputBuffer = '';
|
|
496
521
|
this.approvalTransitionBuffer = '';
|
|
@@ -512,7 +537,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
512
537
|
if (patterns.generating.some(p => p.test(cleanData))) {
|
|
513
538
|
this.isWaitingForResponse = true;
|
|
514
539
|
this.responseBuffer = '';
|
|
515
|
-
this.
|
|
540
|
+
this.setStatus('generating', 'autonomous_gen');
|
|
516
541
|
this.onStatusChange?.();
|
|
517
542
|
}
|
|
518
543
|
}
|
|
@@ -524,7 +549,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
524
549
|
|
|
525
550
|
const stillGenerating = patterns.generating.some(p => p.test(cleanData));
|
|
526
551
|
if (stillGenerating) {
|
|
527
|
-
this.
|
|
552
|
+
this.setStatus('generating', 'still_generating');
|
|
528
553
|
this.idleTimeout = setTimeout(() => {
|
|
529
554
|
if (this.isWaitingForResponse) this.finishResponse();
|
|
530
555
|
}, this.timeouts.generatingIdle);
|
|
@@ -575,7 +600,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
575
600
|
this.responseBuffer = '';
|
|
576
601
|
this.isWaitingForResponse = false;
|
|
577
602
|
this.activeModal = null;
|
|
578
|
-
this.
|
|
603
|
+
this.setStatus('idle', 'response_finished');
|
|
579
604
|
this.onStatusChange?.();
|
|
580
605
|
}
|
|
581
606
|
|
|
@@ -598,7 +623,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
598
623
|
this.messages.push({ role: 'user', content: text, timestamp: Date.now() });
|
|
599
624
|
this.isWaitingForResponse = true;
|
|
600
625
|
this.responseBuffer = '';
|
|
601
|
-
this.
|
|
626
|
+
this.setStatus('generating', 'sendMessage');
|
|
602
627
|
this.onStatusChange?.();
|
|
603
628
|
|
|
604
629
|
this.ptyProcess.write(text + '\r');
|
|
@@ -623,7 +648,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
623
648
|
setTimeout(() => {
|
|
624
649
|
try { this.ptyProcess?.kill(); } catch { }
|
|
625
650
|
this.ptyProcess = null;
|
|
626
|
-
this.
|
|
651
|
+
this.setStatus('stopped', 'stop_cmd');
|
|
627
652
|
this.ready = false;
|
|
628
653
|
this.onStatusChange?.();
|
|
629
654
|
}, this.timeouts.shutdownGrace);
|
|
@@ -649,9 +674,16 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
649
674
|
*/
|
|
650
675
|
resolveModal(buttonIndex: number): void {
|
|
651
676
|
if (!this.ptyProcess || this.currentStatus !== 'waiting_approval') return;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
this.
|
|
677
|
+
// Use provider-defined approval keys if available
|
|
678
|
+
// Each provider.json can define: { "approvalKeys": { "0": "1", "1": "2", "2": "3" } }
|
|
679
|
+
if (buttonIndex in this.approvalKeys) {
|
|
680
|
+
this.ptyProcess.write(this.approvalKeys[buttonIndex]);
|
|
681
|
+
} else {
|
|
682
|
+
// Generic fallback: Arrow Down to navigate + Enter to select
|
|
683
|
+
const DOWN = '\x1B[B';
|
|
684
|
+
const keys = DOWN.repeat(Math.max(0, buttonIndex)) + '\r';
|
|
685
|
+
this.ptyProcess.write(keys);
|
|
686
|
+
}
|
|
655
687
|
}
|
|
656
688
|
|
|
657
689
|
resize(cols: number, rows: number): void {
|
|
@@ -664,4 +696,43 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
664
696
|
} catch { }
|
|
665
697
|
}
|
|
666
698
|
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Full debug state — exposes all internal buffers, status, and patterns for debugging.
|
|
702
|
+
* Used by DevServer /api/cli/debug endpoint.
|
|
703
|
+
*/
|
|
704
|
+
getDebugState(): Record<string, any> {
|
|
705
|
+
return {
|
|
706
|
+
type: this.cliType,
|
|
707
|
+
name: this.cliName,
|
|
708
|
+
status: this.currentStatus,
|
|
709
|
+
ready: this.ready,
|
|
710
|
+
workingDir: this.workingDir,
|
|
711
|
+
messages: this.messages.slice(-20),
|
|
712
|
+
messageCount: this.messages.length,
|
|
713
|
+
// Buffers
|
|
714
|
+
startupBuffer: this.startupBuffer.slice(-500),
|
|
715
|
+
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
716
|
+
responseBuffer: this.responseBuffer.slice(-500),
|
|
717
|
+
approvalTransitionBuffer: this.approvalTransitionBuffer.slice(-500),
|
|
718
|
+
// State
|
|
719
|
+
isWaitingForResponse: this.isWaitingForResponse,
|
|
720
|
+
activeModal: this.activeModal,
|
|
721
|
+
lastApprovalResolvedAt: this.lastApprovalResolvedAt,
|
|
722
|
+
resizeSuppressUntil: this.resizeSuppressUntil,
|
|
723
|
+
// Provider patterns (serialized)
|
|
724
|
+
patterns: {
|
|
725
|
+
prompt: this.provider.patterns.prompt.map(p => p.toString()),
|
|
726
|
+
generating: this.provider.patterns.generating.map(p => p.toString()),
|
|
727
|
+
approval: this.provider.patterns.approval.map(p => p.toString()),
|
|
728
|
+
ready: this.provider.patterns.ready.map(p => p.toString()),
|
|
729
|
+
},
|
|
730
|
+
// Status history
|
|
731
|
+
statusHistory: this.statusHistory.slice(-30),
|
|
732
|
+
// Timeouts config
|
|
733
|
+
timeouts: this.timeouts,
|
|
734
|
+
// PTY alive
|
|
735
|
+
ptyAlive: !!this.ptyProcess,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
667
738
|
}
|