@adhdev/daemon-core 0.5.3 → 0.5.6
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 +88 -2
- package/dist/index.js +1230 -439
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/providers/_builtin/extension/cline/scripts/read_chat.js +14 -1
- package/providers/_builtin/ide/antigravity/scripts/1.106/read_chat.js +24 -1
- package/providers/_builtin/ide/antigravity/scripts/1.107/read_chat.js +24 -1
- package/providers/_builtin/ide/cursor/scripts/0.49/focus_editor.js +3 -3
- package/providers/_builtin/ide/cursor/scripts/0.49/list_models.js +1 -1
- package/providers/_builtin/ide/cursor/scripts/0.49/list_modes.js +1 -1
- package/providers/_builtin/ide/cursor/scripts/0.49/open_panel.js +4 -4
- package/providers/_builtin/ide/cursor/scripts/0.49/read_chat.js +5 -1
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/dismiss_notification.js +30 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/focus_editor.js +13 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/list_models.js +78 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/list_modes.js +40 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/list_notifications.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/list_sessions.js +42 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/new_session.js +20 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/open_panel.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/read_chat.js +79 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/resolve_action.js +19 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/scripts.js +78 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/send_message.js +23 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/set_mode.js +38 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/set_model.js +81 -0
- package/providers/_builtin/ide/cursor/scripts/0.49.bak/switch_session.js +28 -0
- package/providers/_builtin/ide/windsurf/scripts/read_chat.js +18 -1
- package/src/cli-adapters/provider-cli-adapter.ts +231 -12
- package/src/commands/chat-commands.ts +36 -0
- package/src/commands/cli-manager.ts +128 -30
- package/src/commands/handler.ts +47 -3
- package/src/commands/router.ts +32 -2
- package/src/commands/workspace-commands.ts +108 -0
- package/src/config/config.ts +29 -1
- package/src/config/workspace-activity.ts +65 -0
- package/src/config/workspaces.ts +250 -0
- package/src/daemon/dev-server.ts +1 -1
- package/src/index.ts +5 -0
- package/src/launch.ts +1 -1
- package/src/providers/cli-provider-instance.ts +7 -2
- package/src/providers/ide-provider-instance.ts +11 -0
- package/src/status/reporter.ts +23 -4
- package/src/system/host-memory.ts +65 -0
- package/src/types.ts +8 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor — send_message
|
|
3
|
+
*
|
|
4
|
+
* Cursor는 cdp-type-and-send 방식:
|
|
5
|
+
* 1. 입력 필드 존재 확인
|
|
6
|
+
* 2. needsTypeAndSend: true 반환 → daemon이 CDP로 타이핑 + Enter 처리
|
|
7
|
+
*
|
|
8
|
+
* 입력 셀렉터: .aislash-editor-input[contenteditable="true"]
|
|
9
|
+
* 파라미터: ${ MESSAGE } (사용되지 않음 — daemon이 직접 타이핑)
|
|
10
|
+
*/
|
|
11
|
+
(() => {
|
|
12
|
+
try {
|
|
13
|
+
const input = document.querySelector('.aislash-editor-input[contenteditable="true"]');
|
|
14
|
+
if (!input) return JSON.stringify({ sent: false, error: 'Input box not found' });
|
|
15
|
+
return JSON.stringify({
|
|
16
|
+
sent: false,
|
|
17
|
+
needsTypeAndSend: true,
|
|
18
|
+
selector: '.aislash-editor-input[contenteditable="true"]',
|
|
19
|
+
});
|
|
20
|
+
} catch(e) {
|
|
21
|
+
return JSON.stringify({ sent: false, error: e.message });
|
|
22
|
+
}
|
|
23
|
+
})()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor — set_mode
|
|
3
|
+
*
|
|
4
|
+
* 모드 드롭다운에서 대상 모드 선택:
|
|
5
|
+
* 1. 드롭다운 열기
|
|
6
|
+
* 2. 매칭 아이템 클릭
|
|
7
|
+
*
|
|
8
|
+
* params.mode: string — 모드 이름
|
|
9
|
+
* → { success: true/false, mode? }
|
|
10
|
+
*/
|
|
11
|
+
async (params) => {
|
|
12
|
+
try {
|
|
13
|
+
const target = params.mode;
|
|
14
|
+
|
|
15
|
+
const modeBtn = document.querySelector('.composer-unified-dropdown:not(.composer-unified-dropdown-model)');
|
|
16
|
+
if (!modeBtn) return JSON.stringify({ success: false, error: 'Mode button not found' });
|
|
17
|
+
|
|
18
|
+
modeBtn.click();
|
|
19
|
+
await new Promise(r => setTimeout(r, 500));
|
|
20
|
+
|
|
21
|
+
const menu = document.querySelector('[data-testid="model-picker-menu"]') || document.querySelector('.typeahead-popover');
|
|
22
|
+
if (!menu) return JSON.stringify({ success: false, error: 'Mode menu not found' });
|
|
23
|
+
|
|
24
|
+
const items = menu.querySelectorAll('.composer-unified-context-menu-item');
|
|
25
|
+
for (const item of items) {
|
|
26
|
+
const nameEl = item.querySelector('.monaco-highlighted-label');
|
|
27
|
+
const name = nameEl?.textContent?.trim() || '';
|
|
28
|
+
if (name && (name === target || name.toLowerCase() === target.toLowerCase())) {
|
|
29
|
+
item.click();
|
|
30
|
+
await new Promise(r => setTimeout(r, 200));
|
|
31
|
+
return JSON.stringify({ success: true, mode: name });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
36
|
+
return JSON.stringify({ success: false, error: 'mode not found: ' + target });
|
|
37
|
+
} catch(e) { return JSON.stringify({ success: false, error: e.message }); }
|
|
38
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor — set_model
|
|
3
|
+
*
|
|
4
|
+
* 모델 드롭다운에서 대상 모델 선택:
|
|
5
|
+
* 1. 드롭다운 열기
|
|
6
|
+
* 2. Auto 토글 끄기 (필요 시)
|
|
7
|
+
* 3. 검색 입력으로 필터
|
|
8
|
+
* 4. 매칭 아이템 클릭
|
|
9
|
+
* 5. 원래 Auto 상태 복원
|
|
10
|
+
*
|
|
11
|
+
* params.model: string — 모델 이름 (🧠 접미사 가능)
|
|
12
|
+
* → { success: true/false, model? }
|
|
13
|
+
*/
|
|
14
|
+
async (params) => {
|
|
15
|
+
try {
|
|
16
|
+
const target = params.model;
|
|
17
|
+
|
|
18
|
+
const modelBtn = document.querySelector('.composer-unified-dropdown-model');
|
|
19
|
+
if (!modelBtn) return JSON.stringify({ success: false, error: 'Model button not found' });
|
|
20
|
+
|
|
21
|
+
modelBtn.click();
|
|
22
|
+
await new Promise(r => setTimeout(r, 500));
|
|
23
|
+
|
|
24
|
+
const menu = document.querySelector('[data-testid="model-picker-menu"]');
|
|
25
|
+
if (!menu) return JSON.stringify({ success: false, error: 'Model picker menu not found' });
|
|
26
|
+
|
|
27
|
+
// 🧠 접미사 처리
|
|
28
|
+
const wantBrain = target.includes('🧠');
|
|
29
|
+
const searchName = target.replace(/\s*🧠\s*$/, '').trim();
|
|
30
|
+
|
|
31
|
+
// Auto 토글 끄기
|
|
32
|
+
const autoItem = menu.querySelector('.composer-unified-context-menu-item[data-is-selected="true"]');
|
|
33
|
+
const autoToggle = autoItem ? [...autoItem.querySelectorAll('[class*="rounded-full"]')].find(el => el.offsetWidth === 24 && el.offsetHeight === 14) : null;
|
|
34
|
+
let wasAutoOn = false;
|
|
35
|
+
if (autoToggle) {
|
|
36
|
+
const bgStyle = autoToggle.getAttribute('style') || '';
|
|
37
|
+
wasAutoOn = bgStyle.includes('green');
|
|
38
|
+
if (wasAutoOn) {
|
|
39
|
+
autoToggle.click();
|
|
40
|
+
await new Promise(r => setTimeout(r, 500));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 검색 입력으로 필터링
|
|
45
|
+
const refreshedMenu = document.querySelector('[data-testid="model-picker-menu"]');
|
|
46
|
+
const searchInput = refreshedMenu?.querySelector('input[placeholder="Search models"]');
|
|
47
|
+
if (searchInput) {
|
|
48
|
+
searchInput.focus();
|
|
49
|
+
searchInput.value = searchName;
|
|
50
|
+
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
51
|
+
await new Promise(r => setTimeout(r, 300));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 아이템에서 찾기
|
|
55
|
+
const items = (refreshedMenu || menu).querySelectorAll('.composer-unified-context-menu-item');
|
|
56
|
+
for (const item of items) {
|
|
57
|
+
const nameEl = item.querySelector('.monaco-highlighted-label');
|
|
58
|
+
const name = nameEl?.textContent?.trim() || '';
|
|
59
|
+
if (!name || name === 'Add Models') continue;
|
|
60
|
+
const hasBrain = !!item.querySelector('[class*="codicon-br"]');
|
|
61
|
+
|
|
62
|
+
if (name.toLowerCase().includes(searchName.toLowerCase()) && hasBrain === wantBrain) {
|
|
63
|
+
item.click();
|
|
64
|
+
await new Promise(r => setTimeout(r, 200));
|
|
65
|
+
const displayName = hasBrain ? name + ' 🧠' : name;
|
|
66
|
+
return JSON.stringify({ success: true, model: displayName });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Auto 복원 + 닫기
|
|
71
|
+
if (wasAutoOn) {
|
|
72
|
+
const nm = document.querySelector('[data-testid="model-picker-menu"]');
|
|
73
|
+
const nai = nm?.querySelector('.composer-unified-context-menu-item');
|
|
74
|
+
const nt = nai ? [...nai.querySelectorAll('[class*="rounded-full"]')].find(el => el.offsetWidth === 24) : null;
|
|
75
|
+
if (nt) nt.click();
|
|
76
|
+
await new Promise(r => setTimeout(r, 200));
|
|
77
|
+
}
|
|
78
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
79
|
+
return JSON.stringify({ success: false, error: 'model not found: ' + target });
|
|
80
|
+
} catch(e) { return JSON.stringify({ success: false, error: e.message }); }
|
|
81
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor — switch_session
|
|
3
|
+
*
|
|
4
|
+
* 사이드바 셀 클릭으로 세션 전환:
|
|
5
|
+
* 제목 매칭 (params.title) 또는 인덱스 (params.index)
|
|
6
|
+
*
|
|
7
|
+
* params.index: number, params.title: string|null
|
|
8
|
+
* → { switched: true/false, title?, error? }
|
|
9
|
+
*/
|
|
10
|
+
(params) => {
|
|
11
|
+
try {
|
|
12
|
+
const cells = [...document.querySelectorAll('.agent-sidebar-cell')];
|
|
13
|
+
let target;
|
|
14
|
+
if (params.title) {
|
|
15
|
+
target = cells.find(c => {
|
|
16
|
+
const t = c.querySelector('.agent-sidebar-cell-text')?.textContent?.trim() || '';
|
|
17
|
+
return t.toLowerCase().includes(params.title.toLowerCase());
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
target = cells[params.index || 0];
|
|
21
|
+
}
|
|
22
|
+
if (!target) return JSON.stringify({ switched: false, error: 'Session not found', available: cells.length });
|
|
23
|
+
target.click();
|
|
24
|
+
return JSON.stringify({ switched: true, title: target.querySelector('.agent-sidebar-cell-text')?.textContent?.trim() });
|
|
25
|
+
} catch(e) {
|
|
26
|
+
return JSON.stringify({ switched: false, error: e.message });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -86,6 +86,23 @@
|
|
|
86
86
|
const title = document.title.split(' \u2014 ')[0].trim() || 'Cascade';
|
|
87
87
|
|
|
88
88
|
// ─── 4. HTML → Markdown 변환기 ───
|
|
89
|
+
const BLOCK_TAGS = new Set(['DIV', 'P', 'BR', 'LI', 'TR', 'SECTION', 'ARTICLE', 'HEADER', 'FOOTER']);
|
|
90
|
+
function extractCodeText(node) {
|
|
91
|
+
if (node.nodeType === 3) return node.textContent || '';
|
|
92
|
+
if (node.nodeType !== 1) return '';
|
|
93
|
+
if (node.tagName === 'BR') return '\n';
|
|
94
|
+
const parts = [];
|
|
95
|
+
for (const child of node.childNodes) {
|
|
96
|
+
const isBlock = child.nodeType === 1 && BLOCK_TAGS.has(child.tagName);
|
|
97
|
+
const text = extractCodeText(child);
|
|
98
|
+
if (text) {
|
|
99
|
+
if (isBlock && parts.length > 0) parts.push('\n');
|
|
100
|
+
parts.push(text);
|
|
101
|
+
if (isBlock) parts.push('\n');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return parts.join('').replace(/\n{2,}/g, '\n');
|
|
105
|
+
}
|
|
89
106
|
function htmlToMd(node) {
|
|
90
107
|
if (node.nodeType === 3) return node.textContent || '';
|
|
91
108
|
if (node.nodeType !== 1) return '';
|
|
@@ -121,7 +138,7 @@
|
|
|
121
138
|
if (tag === 'PRE') {
|
|
122
139
|
const codeEl = node.querySelector('code');
|
|
123
140
|
const lang = codeEl ? (codeEl.className.match(/language-(\w+)/)?.[1] || '') : '';
|
|
124
|
-
const code = (codeEl || node)
|
|
141
|
+
const code = extractCodeText(codeEl || node);
|
|
125
142
|
return '\n```' + lang + '\n' + code.trim() + '\n```\n';
|
|
126
143
|
}
|
|
127
144
|
if (tag === 'CODE') {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import * as os from 'os';
|
|
19
|
+
import * as path from 'path';
|
|
19
20
|
import { execSync } from 'child_process';
|
|
20
21
|
import type { CliAdapter } from '../cli-adapter-types.js';
|
|
21
22
|
import { LOG } from '../logging/logger.js';
|
|
@@ -23,6 +24,22 @@ import { LOG } from '../logging/logger.js';
|
|
|
23
24
|
let pty: any;
|
|
24
25
|
try {
|
|
25
26
|
pty = require('node-pty');
|
|
27
|
+
// node-pty@1.1.0 ships spawn-helper without +x on macOS — fix it
|
|
28
|
+
if (os.platform() !== 'win32') {
|
|
29
|
+
try {
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const ptyDir = path.dirname(require.resolve('node-pty'));
|
|
32
|
+
const arch = os.arch() === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
|
|
33
|
+
const helper = path.join(ptyDir, 'prebuilds', arch, 'spawn-helper');
|
|
34
|
+
if (fs.existsSync(helper)) {
|
|
35
|
+
const stat = fs.statSync(helper);
|
|
36
|
+
if (!(stat.mode & 0o111)) {
|
|
37
|
+
fs.chmodSync(helper, stat.mode | 0o755);
|
|
38
|
+
LOG.info('CLI', '[node-pty] Fixed spawn-helper permissions');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch { /* best-effort */ }
|
|
42
|
+
}
|
|
26
43
|
} catch {
|
|
27
44
|
LOG.error('CLI', '[ProviderCliAdapter] node-pty not found. Install: npm install node-pty@1.0.0');
|
|
28
45
|
}
|
|
@@ -93,6 +110,125 @@ function findBinary(name: string): string {
|
|
|
93
110
|
}
|
|
94
111
|
}
|
|
95
112
|
|
|
113
|
+
/** True if file starts with a UTF-8 BOM then #!, or plain #!. */
|
|
114
|
+
function isScriptBinary(binaryPath: string): boolean {
|
|
115
|
+
if (!path.isAbsolute(binaryPath)) return false;
|
|
116
|
+
try {
|
|
117
|
+
const fs = require('fs');
|
|
118
|
+
const resolved = fs.realpathSync(binaryPath);
|
|
119
|
+
const head = Buffer.alloc(8);
|
|
120
|
+
const fd = fs.openSync(resolved, 'r');
|
|
121
|
+
fs.readSync(fd, head, 0, 8, 0);
|
|
122
|
+
fs.closeSync(fd);
|
|
123
|
+
let i = 0;
|
|
124
|
+
if (head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf) i = 3;
|
|
125
|
+
return head[i] === 0x23 && head[i + 1] === 0x21; // '#!'
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** True only for Mach-O / ELF — npm shims and shell scripts return false. */
|
|
132
|
+
function looksLikeMachOOrElf(filePath: string): boolean {
|
|
133
|
+
if (!path.isAbsolute(filePath)) return false;
|
|
134
|
+
try {
|
|
135
|
+
const fs = require('fs');
|
|
136
|
+
const resolved = fs.realpathSync(filePath);
|
|
137
|
+
const buf = Buffer.alloc(8);
|
|
138
|
+
const fd = fs.openSync(resolved, 'r');
|
|
139
|
+
fs.readSync(fd, buf, 0, 8, 0);
|
|
140
|
+
fs.closeSync(fd);
|
|
141
|
+
let i = 0;
|
|
142
|
+
if (buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) i = 3;
|
|
143
|
+
const b = buf.subarray(i);
|
|
144
|
+
if (b.length < 4) return false;
|
|
145
|
+
// ELF
|
|
146
|
+
if (b[0] === 0x7f && b[1] === 0x45 && b[2] === 0x4c && b[3] === 0x46) return true;
|
|
147
|
+
const le = b.readUInt32LE(0);
|
|
148
|
+
const be = b.readUInt32BE(0);
|
|
149
|
+
const magics = [0xfeedface, 0xfeedfacf, 0xcafebabe, 0xbebafeca];
|
|
150
|
+
return magics.some(m => m === le || m === be);
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function shSingleQuote(arg: string): string {
|
|
157
|
+
if (/^[a-zA-Z0-9@%_+=:,./-]+$/.test(arg)) return arg;
|
|
158
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* provider.json stores regex placeholders as `{}` — JSON.parse leaves plain objects,
|
|
163
|
+
* so `.test` is missing and the PTY handler throws on first output. Coerce to RegExp
|
|
164
|
+
* (including `{ "source": "...", "flags": "i" }`) or fall back to generic TUIs (Claude Code, etc.).
|
|
165
|
+
*/
|
|
166
|
+
function parsePatternEntry(x: unknown): RegExp | null {
|
|
167
|
+
if (x instanceof RegExp) return x;
|
|
168
|
+
if (x && typeof x === 'object' && typeof (x as { source?: string }).source === 'string') {
|
|
169
|
+
try {
|
|
170
|
+
const s = x as { source: string; flags?: string };
|
|
171
|
+
return new RegExp(s.source, s.flags || '');
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function coercePatternArray(raw: unknown, fallbacks: RegExp[]): RegExp[] {
|
|
180
|
+
if (!Array.isArray(raw)) return [...fallbacks];
|
|
181
|
+
const parsed = raw.map(parsePatternEntry).filter((r): r is RegExp => r != null);
|
|
182
|
+
return parsed.length > 0 ? parsed : [...fallbacks];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Defaults tuned for Claude Code / similar agent CLIs when provider.json patterns are empty. */
|
|
186
|
+
const FALLBACK_PROMPT: RegExp[] = [
|
|
187
|
+
/Type your message/i,
|
|
188
|
+
/^>\s*$/m, // '>' alone on its own line
|
|
189
|
+
/[›➤]\s*$/,
|
|
190
|
+
/for shortcuts/i,
|
|
191
|
+
/\?\s*for help/i,
|
|
192
|
+
/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
|
+
];
|
|
196
|
+
|
|
197
|
+
const FALLBACK_GENERATING: RegExp[] = [
|
|
198
|
+
/[\u2800-\u28ff]/, // Braille spinner blocks (universal TUI)
|
|
199
|
+
/esc to (cancel|interrupt|stop)/i, // Common TUI generation status line
|
|
200
|
+
/generating\.\.\./i,
|
|
201
|
+
/Claude is (?:thinking|processing|working)/i, // Specific Claude Code status
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const FALLBACK_APPROVAL: RegExp[] = [
|
|
205
|
+
/Allow\s+once/i,
|
|
206
|
+
/Always\s+allow/i,
|
|
207
|
+
/\(y\/n\)/i,
|
|
208
|
+
/\[Y\/n\]/i,
|
|
209
|
+
/Run\s+this\s+command/i,
|
|
210
|
+
// NOTE: removed /Do you want to (?:run|execute|allow)/i — too broad, matches AI explanation
|
|
211
|
+
// text like "Do you want to allow this feature?" causing false approval notifications.
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
function defaultCleanOutput(raw: string, _lastUserInput?: string): string {
|
|
215
|
+
return stripAnsi(raw).trim();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function normalizeCliProviderForRuntime(raw: any): CliProviderModule {
|
|
219
|
+
const patterns = raw?.patterns || {};
|
|
220
|
+
return {
|
|
221
|
+
...raw,
|
|
222
|
+
patterns: {
|
|
223
|
+
prompt: coercePatternArray(patterns.prompt, FALLBACK_PROMPT),
|
|
224
|
+
generating: coercePatternArray(patterns.generating, FALLBACK_GENERATING),
|
|
225
|
+
approval: coercePatternArray(patterns.approval, FALLBACK_APPROVAL),
|
|
226
|
+
ready: coercePatternArray(patterns.ready, []),
|
|
227
|
+
},
|
|
228
|
+
cleanOutput: typeof raw?.cleanOutput === 'function' ? raw.cleanOutput : defaultCleanOutput,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
96
232
|
export class ProviderCliAdapter implements CliAdapter {
|
|
97
233
|
readonly cliType: string;
|
|
98
234
|
readonly cliName: string;
|
|
@@ -125,11 +261,18 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
125
261
|
// Approval cooldown
|
|
126
262
|
private lastApprovalResolvedAt: number = 0;
|
|
127
263
|
|
|
264
|
+
// Approval state machine
|
|
265
|
+
private approvalTransitionBuffer: string = '';
|
|
266
|
+
private approvalExitTimeout: NodeJS.Timeout | null = null;
|
|
267
|
+
|
|
268
|
+
// Resize redraw suppression
|
|
269
|
+
private resizeSuppressUntil: number = 0;
|
|
270
|
+
|
|
128
271
|
// Resolved timeouts (provider defaults + overrides)
|
|
129
272
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
130
273
|
|
|
131
274
|
constructor(provider: CliProviderModule, workingDir: string, private extraArgs: string[] = []) {
|
|
132
|
-
this.provider = provider;
|
|
275
|
+
this.provider = normalizeCliProviderForRuntime(provider);
|
|
133
276
|
this.cliType = provider.type;
|
|
134
277
|
this.cliName = provider.name;
|
|
135
278
|
this.workingDir = workingDir.startsWith('~')
|
|
@@ -180,18 +323,28 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
180
323
|
|
|
181
324
|
let shellCmd: string;
|
|
182
325
|
let shellArgs: string[];
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
326
|
+
const useShellUnix = !isWin && (
|
|
327
|
+
!!spawnConfig.shell
|
|
328
|
+
|| !path.isAbsolute(binaryPath)
|
|
329
|
+
|| isScriptBinary(binaryPath)
|
|
330
|
+
|| !looksLikeMachOOrElf(binaryPath)
|
|
331
|
+
);
|
|
332
|
+
const useShell = isWin ? !!spawnConfig.shell : useShellUnix;
|
|
333
|
+
|
|
334
|
+
if (useShell) {
|
|
335
|
+
// Execute via shell (npm shims, shebang scripts, anything that is not a bare Mach-O/ELF)
|
|
336
|
+
if (!spawnConfig.shell && !isWin) {
|
|
337
|
+
LOG.info('CLI', `[${this.cliType}] Using login shell (script shim or non-native binary)`);
|
|
338
|
+
}
|
|
186
339
|
shellCmd = isWin ? 'cmd.exe' : (process.env.SHELL || '/bin/zsh');
|
|
187
|
-
const fullCmd = [binaryPath, ...allArgs].join(' ');
|
|
340
|
+
const fullCmd = [binaryPath, ...allArgs].map(shSingleQuote).join(' ');
|
|
188
341
|
shellArgs = isWin ? ['/c', fullCmd] : ['-l', '-c', fullCmd];
|
|
189
342
|
} else {
|
|
190
343
|
shellCmd = binaryPath;
|
|
191
344
|
shellArgs = allArgs;
|
|
192
345
|
}
|
|
193
346
|
|
|
194
|
-
|
|
347
|
+
const ptyOpts = {
|
|
195
348
|
name: 'xterm-256color',
|
|
196
349
|
cols: 120,
|
|
197
350
|
rows: 40,
|
|
@@ -200,7 +353,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
200
353
|
...process.env,
|
|
201
354
|
...spawnConfig.env,
|
|
202
355
|
} as Record<string, string>,
|
|
203
|
-
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
this.ptyProcess = pty.spawn(shellCmd, shellArgs, ptyOpts);
|
|
360
|
+
} catch (err: any) {
|
|
361
|
+
const msg = err?.message || String(err);
|
|
362
|
+
if (!isWin && !useShell && /posix_spawn|spawn/i.test(msg)) {
|
|
363
|
+
LOG.warn('CLI', `[${this.cliType}] Direct spawn failed (${msg}), retrying via login shell`);
|
|
364
|
+
shellCmd = process.env.SHELL || '/bin/zsh';
|
|
365
|
+
const fullCmd = [binaryPath, ...allArgs].map(shSingleQuote).join(' ');
|
|
366
|
+
shellArgs = ['-l', '-c', fullCmd];
|
|
367
|
+
this.ptyProcess = pty.spawn(shellCmd, shellArgs, ptyOpts);
|
|
368
|
+
} else {
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
204
372
|
|
|
205
373
|
this.ptyProcess.onData((data: string) => {
|
|
206
374
|
this.handleOutput(data);
|
|
@@ -234,6 +402,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
234
402
|
// ─── Output state machine ────────────────────────────
|
|
235
403
|
|
|
236
404
|
private handleOutput(rawData: string): void {
|
|
405
|
+
// Suppress output processing briefly after resize to avoid false triggers from screen redraws
|
|
406
|
+
if (Date.now() < this.resizeSuppressUntil) return;
|
|
407
|
+
|
|
237
408
|
const cleanData = stripAnsi(rawData);
|
|
238
409
|
const { patterns } = this.provider;
|
|
239
410
|
|
|
@@ -279,32 +450,57 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
279
450
|
if (hasApproval && this.currentStatus !== 'waiting_approval') {
|
|
280
451
|
if (this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown) return;
|
|
281
452
|
|
|
453
|
+
// Capture context before clearing (recentOutputBuffer still has content here)
|
|
454
|
+
const ctxLines = this.recentOutputBuffer.split('\n')
|
|
455
|
+
.map(l => l.trim())
|
|
456
|
+
.filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
|
|
282
457
|
this.isWaitingForResponse = true;
|
|
283
458
|
this.currentStatus = 'waiting_approval';
|
|
284
459
|
this.recentOutputBuffer = '';
|
|
285
|
-
|
|
460
|
+
this.approvalTransitionBuffer = '';
|
|
286
461
|
this.activeModal = {
|
|
287
462
|
message: ctxLines.slice(-5).join(' ').slice(0, 200) || 'Approval required',
|
|
288
463
|
buttons: ['Allow once', 'Always allow', 'Deny'],
|
|
289
464
|
};
|
|
290
465
|
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
466
|
+
// Safety timeout — if stuck in waiting_approval, auto-exit after 60s
|
|
467
|
+
if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
|
|
468
|
+
this.approvalExitTimeout = setTimeout(() => {
|
|
469
|
+
if (this.currentStatus === 'waiting_approval') {
|
|
470
|
+
LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-exiting waiting_approval`);
|
|
471
|
+
this.activeModal = null;
|
|
472
|
+
this.lastApprovalResolvedAt = Date.now();
|
|
473
|
+
this.recentOutputBuffer = '';
|
|
474
|
+
this.approvalTransitionBuffer = '';
|
|
475
|
+
this.approvalExitTimeout = null;
|
|
476
|
+
this.currentStatus = this.isWaitingForResponse ? 'generating' : 'idle';
|
|
477
|
+
this.onStatusChange?.();
|
|
478
|
+
}
|
|
479
|
+
}, 60000);
|
|
291
480
|
this.onStatusChange?.();
|
|
292
481
|
return;
|
|
293
482
|
}
|
|
294
483
|
|
|
295
484
|
// ─── Phase 3: Approval release
|
|
485
|
+
// Accumulate chunks into approvalTransitionBuffer — the approval dialog clears via ANSI
|
|
486
|
+
// sequences that strip to nothing, so we can't rely on a single cleanData chunk matching.
|
|
296
487
|
if (this.currentStatus === 'waiting_approval') {
|
|
297
|
-
|
|
298
|
-
const
|
|
488
|
+
this.approvalTransitionBuffer = (this.approvalTransitionBuffer + cleanData).slice(-500);
|
|
489
|
+
const genResume = patterns.generating.some(p => p.test(this.approvalTransitionBuffer));
|
|
490
|
+
const promptResume = patterns.prompt.some(p => p.test(this.approvalTransitionBuffer));
|
|
299
491
|
if (genResume) {
|
|
492
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
300
493
|
this.currentStatus = 'generating';
|
|
301
494
|
this.activeModal = null;
|
|
302
495
|
this.recentOutputBuffer = '';
|
|
496
|
+
this.approvalTransitionBuffer = '';
|
|
303
497
|
this.lastApprovalResolvedAt = Date.now();
|
|
304
498
|
this.onStatusChange?.();
|
|
305
499
|
} else if (promptResume) {
|
|
500
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
306
501
|
this.activeModal = null;
|
|
307
502
|
this.recentOutputBuffer = '';
|
|
503
|
+
this.approvalTransitionBuffer = '';
|
|
308
504
|
this.lastApprovalResolvedAt = Date.now();
|
|
309
505
|
this.finishResponse();
|
|
310
506
|
}
|
|
@@ -337,7 +533,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
337
533
|
}
|
|
338
534
|
|
|
339
535
|
// Prompt → response complete
|
|
340
|
-
|
|
536
|
+
// Only check the LAST 2 lines of cleanData — the prompt appears at the very end of the
|
|
537
|
+
// output stream. Checking the full chunk causes false positives when response content
|
|
538
|
+
// has '>' in code blocks, shell examples, or mid-response lines.
|
|
539
|
+
const trailingLines = cleanData.split('\n').slice(-2).join('\n');
|
|
540
|
+
if (patterns.prompt.some(p => p.test(trailingLines))) {
|
|
341
541
|
this.finishResponse();
|
|
342
542
|
} else {
|
|
343
543
|
this.idleTimeout = setTimeout(() => {
|
|
@@ -353,6 +553,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
353
553
|
private finishResponse(): void {
|
|
354
554
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
355
555
|
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
556
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
356
557
|
|
|
357
558
|
const lastUserText = this.messages.filter(m => m.role === 'user').pop()?.content;
|
|
358
559
|
let response = this.provider.cleanOutput(this.responseBuffer, lastUserText);
|
|
@@ -416,6 +617,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
416
617
|
cancel(): void { this.shutdown(); }
|
|
417
618
|
|
|
418
619
|
shutdown(): void {
|
|
620
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
419
621
|
if (this.ptyProcess) {
|
|
420
622
|
this.ptyProcess.write('\x03');
|
|
421
623
|
setTimeout(() => {
|
|
@@ -440,9 +642,26 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
440
642
|
this.ptyProcess?.write(data);
|
|
441
643
|
}
|
|
442
644
|
|
|
645
|
+
/**
|
|
646
|
+
* Resolve an approval modal by navigating to the button at `buttonIndex` and pressing Enter.
|
|
647
|
+
* Index 0 = first option (already selected by default — just Enter).
|
|
648
|
+
* Index N = press Arrow Down N times, then Enter.
|
|
649
|
+
*/
|
|
650
|
+
resolveModal(buttonIndex: number): void {
|
|
651
|
+
if (!this.ptyProcess || this.currentStatus !== 'waiting_approval') return;
|
|
652
|
+
const DOWN = '\x1B[B'; // Arrow Down
|
|
653
|
+
const keys = DOWN.repeat(Math.max(0, buttonIndex)) + '\r';
|
|
654
|
+
this.ptyProcess.write(keys);
|
|
655
|
+
}
|
|
656
|
+
|
|
443
657
|
resize(cols: number, rows: number): void {
|
|
444
658
|
if (this.ptyProcess) {
|
|
445
|
-
try {
|
|
659
|
+
try {
|
|
660
|
+
this.ptyProcess.resize(cols, rows);
|
|
661
|
+
// Suppress output for 300ms after resize — PTY redraws the screen and
|
|
662
|
+
// the redrawn content (spinners, status text) would falsely trigger generating detection
|
|
663
|
+
this.resizeSuppressUntil = Date.now() + 300;
|
|
664
|
+
} catch { }
|
|
446
665
|
}
|
|
447
666
|
}
|
|
448
667
|
}
|
|
@@ -601,6 +601,42 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
|
|
|
601
601
|
|
|
602
602
|
LOG.info('Command', `[resolveAction] action=${action} button="${button}" provider=${provider?.type}`);
|
|
603
603
|
|
|
604
|
+
// 0. CLI / ACP category: navigate approval dialog via PTY arrow keys + Enter
|
|
605
|
+
if (provider?.category === 'cli') {
|
|
606
|
+
const adapter = h.getCliAdapter(provider.type);
|
|
607
|
+
if (!adapter) return { success: false, error: 'CLI adapter not running' };
|
|
608
|
+
const status = (adapter as any).getStatus?.();
|
|
609
|
+
if (status?.status !== 'waiting_approval') {
|
|
610
|
+
return { success: false, error: 'Not in approval state' };
|
|
611
|
+
}
|
|
612
|
+
const buttons: string[] = status.activeModal?.buttons || ['Allow once', 'Always allow', 'Deny'];
|
|
613
|
+
// Resolve button index: explicit buttonIndex arg → button text match → action fallback
|
|
614
|
+
let buttonIndex = typeof args?.buttonIndex === 'number' ? args.buttonIndex : -1;
|
|
615
|
+
if (buttonIndex < 0) {
|
|
616
|
+
const btnLower = button.toLowerCase();
|
|
617
|
+
buttonIndex = buttons.findIndex(b => b.toLowerCase().includes(btnLower));
|
|
618
|
+
}
|
|
619
|
+
if (buttonIndex < 0) {
|
|
620
|
+
if (action === 'reject' || action === 'deny') {
|
|
621
|
+
buttonIndex = buttons.findIndex(b => /deny|reject|no/i.test(b));
|
|
622
|
+
if (buttonIndex < 0) buttonIndex = buttons.length - 1;
|
|
623
|
+
} else if (action === 'always' || /always/i.test(button)) {
|
|
624
|
+
buttonIndex = buttons.findIndex(b => /always/i.test(b));
|
|
625
|
+
if (buttonIndex < 0) buttonIndex = 1;
|
|
626
|
+
} else {
|
|
627
|
+
buttonIndex = 0; // approve → first option (default selected)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (typeof (adapter as any).resolveModal === 'function') {
|
|
631
|
+
(adapter as any).resolveModal(buttonIndex);
|
|
632
|
+
} else {
|
|
633
|
+
const keys = '\x1B[B'.repeat(Math.max(0, buttonIndex)) + '\r';
|
|
634
|
+
(adapter as any).writeRaw?.(keys);
|
|
635
|
+
}
|
|
636
|
+
LOG.info('Command', `[resolveAction] CLI PTY → buttonIndex=${buttonIndex} "${buttons[buttonIndex] ?? '?'}"`);
|
|
637
|
+
return { success: true, buttonIndex, button: buttons[buttonIndex] ?? button };
|
|
638
|
+
}
|
|
639
|
+
|
|
604
640
|
// 1. Extension: via AgentStreamManager
|
|
605
641
|
if (provider?.category === 'extension' && h.agentStream && h.getCdp()) {
|
|
606
642
|
const ok = await h.agentStream.resolveAgentAction(
|