@adhdev/daemon-core 0.5.3 → 0.5.5
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 +79 -2
- package/dist/index.js +1131 -433
- 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 +167 -7
- 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/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,124 @@ 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*$/,
|
|
189
|
+
/[›➤]\s*$/,
|
|
190
|
+
/for shortcuts/i,
|
|
191
|
+
/\?\s*for help/i,
|
|
192
|
+
/Press enter/i,
|
|
193
|
+
/^[\s\u2500-\u257f]*>\s*$/m,
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const FALLBACK_GENERATING: RegExp[] = [
|
|
197
|
+
/thinking/i,
|
|
198
|
+
/writing/i,
|
|
199
|
+
/Claude is/i,
|
|
200
|
+
/Opus|Sonnet|Haiku/i,
|
|
201
|
+
/[\u2800-\u28ff]/, // Braille spinner blocks
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const FALLBACK_APPROVAL: RegExp[] = [
|
|
205
|
+
/approve/i,
|
|
206
|
+
/Allow( once)?/i,
|
|
207
|
+
/\(y\/n\)/i,
|
|
208
|
+
/\[Y\/n\]/i,
|
|
209
|
+
/continue\?/i,
|
|
210
|
+
/Run this command/i,
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
function defaultCleanOutput(raw: string, _lastUserInput?: string): string {
|
|
214
|
+
return stripAnsi(raw).trim();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function normalizeCliProviderForRuntime(raw: any): CliProviderModule {
|
|
218
|
+
const patterns = raw?.patterns || {};
|
|
219
|
+
return {
|
|
220
|
+
...raw,
|
|
221
|
+
patterns: {
|
|
222
|
+
prompt: coercePatternArray(patterns.prompt, FALLBACK_PROMPT),
|
|
223
|
+
generating: coercePatternArray(patterns.generating, FALLBACK_GENERATING),
|
|
224
|
+
approval: coercePatternArray(patterns.approval, FALLBACK_APPROVAL),
|
|
225
|
+
ready: coercePatternArray(patterns.ready, []),
|
|
226
|
+
},
|
|
227
|
+
cleanOutput: typeof raw?.cleanOutput === 'function' ? raw.cleanOutput : defaultCleanOutput,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
96
231
|
export class ProviderCliAdapter implements CliAdapter {
|
|
97
232
|
readonly cliType: string;
|
|
98
233
|
readonly cliName: string;
|
|
@@ -129,7 +264,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
129
264
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
130
265
|
|
|
131
266
|
constructor(provider: CliProviderModule, workingDir: string, private extraArgs: string[] = []) {
|
|
132
|
-
this.provider = provider;
|
|
267
|
+
this.provider = normalizeCliProviderForRuntime(provider);
|
|
133
268
|
this.cliType = provider.type;
|
|
134
269
|
this.cliName = provider.name;
|
|
135
270
|
this.workingDir = workingDir.startsWith('~')
|
|
@@ -180,18 +315,28 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
180
315
|
|
|
181
316
|
let shellCmd: string;
|
|
182
317
|
let shellArgs: string[];
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
318
|
+
const useShellUnix = !isWin && (
|
|
319
|
+
!!spawnConfig.shell
|
|
320
|
+
|| !path.isAbsolute(binaryPath)
|
|
321
|
+
|| isScriptBinary(binaryPath)
|
|
322
|
+
|| !looksLikeMachOOrElf(binaryPath)
|
|
323
|
+
);
|
|
324
|
+
const useShell = isWin ? !!spawnConfig.shell : useShellUnix;
|
|
325
|
+
|
|
326
|
+
if (useShell) {
|
|
327
|
+
// Execute via shell (npm shims, shebang scripts, anything that is not a bare Mach-O/ELF)
|
|
328
|
+
if (!spawnConfig.shell && !isWin) {
|
|
329
|
+
LOG.info('CLI', `[${this.cliType}] Using login shell (script shim or non-native binary)`);
|
|
330
|
+
}
|
|
186
331
|
shellCmd = isWin ? 'cmd.exe' : (process.env.SHELL || '/bin/zsh');
|
|
187
|
-
const fullCmd = [binaryPath, ...allArgs].join(' ');
|
|
332
|
+
const fullCmd = [binaryPath, ...allArgs].map(shSingleQuote).join(' ');
|
|
188
333
|
shellArgs = isWin ? ['/c', fullCmd] : ['-l', '-c', fullCmd];
|
|
189
334
|
} else {
|
|
190
335
|
shellCmd = binaryPath;
|
|
191
336
|
shellArgs = allArgs;
|
|
192
337
|
}
|
|
193
338
|
|
|
194
|
-
|
|
339
|
+
const ptyOpts = {
|
|
195
340
|
name: 'xterm-256color',
|
|
196
341
|
cols: 120,
|
|
197
342
|
rows: 40,
|
|
@@ -200,7 +345,22 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
200
345
|
...process.env,
|
|
201
346
|
...spawnConfig.env,
|
|
202
347
|
} as Record<string, string>,
|
|
203
|
-
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
this.ptyProcess = pty.spawn(shellCmd, shellArgs, ptyOpts);
|
|
352
|
+
} catch (err: any) {
|
|
353
|
+
const msg = err?.message || String(err);
|
|
354
|
+
if (!isWin && !useShell && /posix_spawn|spawn/i.test(msg)) {
|
|
355
|
+
LOG.warn('CLI', `[${this.cliType}] Direct spawn failed (${msg}), retrying via login shell`);
|
|
356
|
+
shellCmd = process.env.SHELL || '/bin/zsh';
|
|
357
|
+
const fullCmd = [binaryPath, ...allArgs].map(shSingleQuote).join(' ');
|
|
358
|
+
shellArgs = ['-l', '-c', fullCmd];
|
|
359
|
+
this.ptyProcess = pty.spawn(shellCmd, shellArgs, ptyOpts);
|
|
360
|
+
} else {
|
|
361
|
+
throw err;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
204
364
|
|
|
205
365
|
this.ptyProcess.onData((data: string) => {
|
|
206
366
|
this.handleOutput(data);
|
|
@@ -12,6 +12,8 @@ import chalk from 'chalk';
|
|
|
12
12
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
13
13
|
import { detectCLI } from '../detection/cli-detector.js';
|
|
14
14
|
import { loadConfig, saveConfig, addCliHistory } from '../config/config.js';
|
|
15
|
+
import { getWorkspaceState, resolveLaunchDirectory } from '../config/workspaces.js';
|
|
16
|
+
import { appendWorkspaceActivity } from '../config/workspace-activity.js';
|
|
15
17
|
import { CliProviderInstance } from '../providers/cli-provider-instance.js';
|
|
16
18
|
import { AcpProviderInstance } from '../providers/acp-provider-instance.js';
|
|
17
19
|
import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
|
|
@@ -54,6 +56,25 @@ export class DaemonCliManager {
|
|
|
54
56
|
return `${cliType}_${hash}`;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
private persistRecentDir(cliType: string, dir: string): void {
|
|
60
|
+
try {
|
|
61
|
+
const normalizedType = this.providerLoader.resolveAlias(cliType);
|
|
62
|
+
const provider = this.providerLoader.getByAlias(cliType);
|
|
63
|
+
const actKind = provider?.category === 'acp' ? 'acp' : 'cli';
|
|
64
|
+
let next = loadConfig();
|
|
65
|
+
console.log(chalk.cyan(` 📂 Saving recent workspace: ${dir}`));
|
|
66
|
+
const recent = next.recentCliWorkspaces || [];
|
|
67
|
+
if (!recent.includes(dir)) {
|
|
68
|
+
next = { ...next, recentCliWorkspaces: [dir, ...recent].slice(0, 10) };
|
|
69
|
+
}
|
|
70
|
+
next = appendWorkspaceActivity(next, dir, { kind: actKind, agentType: normalizedType });
|
|
71
|
+
saveConfig(next);
|
|
72
|
+
console.log(chalk.green(` ✓ Recent workspace saved: ${dir}`));
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error(chalk.red(` ✗ Failed to save recent workspace: ${e}`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
private createAdapter(cliType: string, workingDir: string, cliArgs?: string[]): CliAdapter {
|
|
58
79
|
// cliType normalize (Resolve alias)
|
|
59
80
|
const normalizedType = this.providerLoader.resolveAlias(cliType);
|
|
@@ -71,7 +92,8 @@ export class DaemonCliManager {
|
|
|
71
92
|
// ─── Session start/management ──────────────────────────────
|
|
72
93
|
|
|
73
94
|
async startSession(cliType: string, workingDir: string, cliArgs?: string[], initialModel?: string): Promise<void> {
|
|
74
|
-
const trimmed = (workingDir ||
|
|
95
|
+
const trimmed = (workingDir || '').trim();
|
|
96
|
+
if (!trimmed) throw new Error('working directory required');
|
|
75
97
|
const resolvedDir = trimmed.startsWith('~')
|
|
76
98
|
? trimmed.replace(/^~/, os.homedir())
|
|
77
99
|
: path.resolve(trimmed);
|
|
@@ -161,21 +183,54 @@ export class DaemonCliManager {
|
|
|
161
183
|
const instanceManager = this.deps.getInstanceManager();
|
|
162
184
|
if (provider && instanceManager) {
|
|
163
185
|
const cliInstance = new CliProviderInstance(provider, resolvedDir, cliArgs, key);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
186
|
+
try {
|
|
187
|
+
await instanceManager.addInstance(key, cliInstance, {
|
|
188
|
+
serverConn: this.deps.getServerConn(),
|
|
189
|
+
settings: {},
|
|
190
|
+
onPtyData: (data: string) => {
|
|
191
|
+
this.deps.getP2p()?.broadcastPtyOutput(key, data);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
} catch (spawnErr: any) {
|
|
195
|
+
// Spawn failed — cleanup and propagate error
|
|
196
|
+
LOG.error('CLI', `[${cliType}] Spawn failed: ${spawnErr?.message}`);
|
|
197
|
+
instanceManager.removeInstance(key);
|
|
198
|
+
throw new Error(`Failed to start ${cliInfo.displayName}: ${spawnErr?.message}`);
|
|
199
|
+
}
|
|
171
200
|
|
|
172
201
|
// Keep adapter ref too (backward compat — write, resize etc)
|
|
173
202
|
this.adapters.set(key, cliInstance.getAdapter() as any);
|
|
174
203
|
console.log(chalk.green(` ✓ CLI started: ${cliInfo.displayName} v${cliInfo.version || 'unknown'} in ${resolvedDir}`));
|
|
204
|
+
|
|
205
|
+
// Monitor for stopped/error → auto-cleanup
|
|
206
|
+
const checkStopped = setInterval(() => {
|
|
207
|
+
try {
|
|
208
|
+
const adapter = this.adapters.get(key);
|
|
209
|
+
if (!adapter) { clearInterval(checkStopped); return; }
|
|
210
|
+
const status = adapter.getStatus?.();
|
|
211
|
+
if (status?.status === 'stopped' || status?.status === 'error') {
|
|
212
|
+
clearInterval(checkStopped);
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
if (this.adapters.has(key)) {
|
|
215
|
+
this.adapters.delete(key);
|
|
216
|
+
this.deps.removeAgentTracking(key);
|
|
217
|
+
instanceManager.removeInstance(key);
|
|
218
|
+
LOG.info('CLI', `🧹 Auto-cleaned ${status.status} CLI: ${cliType}`);
|
|
219
|
+
this.deps.onStatusChange();
|
|
220
|
+
}
|
|
221
|
+
}, 5000);
|
|
222
|
+
}
|
|
223
|
+
} catch { /* ignore */ }
|
|
224
|
+
}, 3000);
|
|
175
225
|
} else {
|
|
176
226
|
// Fallback: InstanceManager without directly adapter manage
|
|
177
227
|
const adapter = this.createAdapter(cliType, resolvedDir, cliArgs);
|
|
178
|
-
|
|
228
|
+
try {
|
|
229
|
+
await adapter.spawn();
|
|
230
|
+
} catch (spawnErr: any) {
|
|
231
|
+
LOG.error('CLI', `[${cliType}] Spawn failed: ${spawnErr?.message}`);
|
|
232
|
+
throw new Error(`Failed to start ${cliInfo.displayName}: ${spawnErr?.message}`);
|
|
233
|
+
}
|
|
179
234
|
|
|
180
235
|
const serverConn = this.deps.getServerConn();
|
|
181
236
|
if (serverConn && typeof adapter.setServerConn === 'function') {
|
|
@@ -184,12 +239,12 @@ export class DaemonCliManager {
|
|
|
184
239
|
adapter.setOnStatusChange(() => {
|
|
185
240
|
this.deps.onStatusChange();
|
|
186
241
|
const status = adapter.getStatus?.();
|
|
187
|
-
if (status?.status === 'stopped') {
|
|
242
|
+
if (status?.status === 'stopped' || status?.status === 'error') {
|
|
188
243
|
setTimeout(() => {
|
|
189
244
|
if (this.adapters.get(key) === adapter) {
|
|
190
245
|
this.adapters.delete(key);
|
|
191
246
|
this.deps.removeAgentTracking(key);
|
|
192
|
-
|
|
247
|
+
LOG.info('CLI', `🧹 Auto-cleaned ${status.status} CLI: ${adapter.cliType}`);
|
|
193
248
|
this.deps.onStatusChange();
|
|
194
249
|
}
|
|
195
250
|
}, 3000);
|
|
@@ -214,13 +269,26 @@ export class DaemonCliManager {
|
|
|
214
269
|
async stopSession(key: string): Promise<void> {
|
|
215
270
|
const adapter = this.adapters.get(key);
|
|
216
271
|
if (adapter) {
|
|
217
|
-
|
|
272
|
+
try {
|
|
273
|
+
adapter.shutdown();
|
|
274
|
+
} catch (e: any) {
|
|
275
|
+
LOG.warn('CLI', `Shutdown error for ${adapter.cliType}: ${e?.message} (force-cleaning)`);
|
|
276
|
+
}
|
|
277
|
+
// Always cleanup regardless of shutdown success
|
|
218
278
|
this.adapters.delete(key);
|
|
219
279
|
this.deps.removeAgentTracking(key);
|
|
220
|
-
// Also remove from InstanceManager
|
|
221
280
|
this.deps.getInstanceManager()?.removeInstance(key);
|
|
222
|
-
|
|
281
|
+
LOG.info('CLI', `🛑 Agent stopped: ${adapter.cliType} in ${adapter.workingDir}`);
|
|
223
282
|
this.deps.onStatusChange();
|
|
283
|
+
} else {
|
|
284
|
+
// Adapter not found — try InstanceManager direct removal
|
|
285
|
+
const im = this.deps.getInstanceManager();
|
|
286
|
+
if (im) {
|
|
287
|
+
im.removeInstance(key);
|
|
288
|
+
this.deps.removeAgentTracking(key);
|
|
289
|
+
LOG.warn('CLI', `🧹 Force-removed orphan entry: ${key}`);
|
|
290
|
+
this.deps.onStatusChange();
|
|
291
|
+
}
|
|
224
292
|
}
|
|
225
293
|
}
|
|
226
294
|
|
|
@@ -270,8 +338,28 @@ export class DaemonCliManager {
|
|
|
270
338
|
switch (cmd) {
|
|
271
339
|
case 'launch_cli': {
|
|
272
340
|
const cliType = args?.cliType;
|
|
273
|
-
const
|
|
274
|
-
const
|
|
341
|
+
const config = loadConfig();
|
|
342
|
+
const resolved = resolveLaunchDirectory(
|
|
343
|
+
{
|
|
344
|
+
dir: args?.dir,
|
|
345
|
+
workspaceId: args?.workspaceId,
|
|
346
|
+
useDefaultWorkspace: args?.useDefaultWorkspace === true,
|
|
347
|
+
useHome: args?.useHome === true,
|
|
348
|
+
},
|
|
349
|
+
config,
|
|
350
|
+
);
|
|
351
|
+
if (!resolved.ok) {
|
|
352
|
+
const ws = getWorkspaceState(config);
|
|
353
|
+
return {
|
|
354
|
+
success: false,
|
|
355
|
+
error: resolved.message,
|
|
356
|
+
code: resolved.code,
|
|
357
|
+
workspaces: ws.workspaces,
|
|
358
|
+
defaultWorkspacePath: ws.defaultWorkspacePath,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const dir = resolved.path;
|
|
362
|
+
const launchSource = resolved.source;
|
|
275
363
|
if (!cliType) throw new Error('cliType required');
|
|
276
364
|
|
|
277
365
|
await this.startSession(cliType, dir, args?.cliArgs, args?.initialModel);
|
|
@@ -284,20 +372,9 @@ export class DaemonCliManager {
|
|
|
284
372
|
}
|
|
285
373
|
}
|
|
286
374
|
|
|
287
|
-
|
|
288
|
-
const config = loadConfig();
|
|
289
|
-
console.log(chalk.cyan(` 📂 Saving recent workspace: ${dir}`));
|
|
290
|
-
const recent = config.recentCliWorkspaces || [];
|
|
291
|
-
if (!recent.includes(dir)) {
|
|
292
|
-
const updated = [dir, ...recent].slice(0, 10);
|
|
293
|
-
saveConfig({ ...config, recentCliWorkspaces: updated });
|
|
294
|
-
console.log(chalk.green(` ✓ Recent workspace saved: ${dir}`));
|
|
295
|
-
}
|
|
296
|
-
} catch (e) {
|
|
297
|
-
console.error(chalk.red(` ✗ Failed to save recent workspace: ${e}`));
|
|
298
|
-
}
|
|
375
|
+
this.persistRecentDir(cliType, dir);
|
|
299
376
|
|
|
300
|
-
return { success: true, cliType, dir, id: newKey,
|
|
377
|
+
return { success: true, cliType, dir, id: newKey, launchSource };
|
|
301
378
|
}
|
|
302
379
|
case 'stop_cli': {
|
|
303
380
|
const cliType = args?.cliType;
|
|
@@ -314,11 +391,32 @@ export class DaemonCliManager {
|
|
|
314
391
|
}
|
|
315
392
|
case 'restart_session': {
|
|
316
393
|
const cliType = args?.cliType || args?.agentType || args?.ideType;
|
|
317
|
-
const
|
|
394
|
+
const cfg = loadConfig();
|
|
395
|
+
const rdir = resolveLaunchDirectory(
|
|
396
|
+
{
|
|
397
|
+
dir: args?.dir,
|
|
398
|
+
workspaceId: args?.workspaceId,
|
|
399
|
+
useDefaultWorkspace: args?.useDefaultWorkspace === true,
|
|
400
|
+
useHome: args?.useHome === true,
|
|
401
|
+
},
|
|
402
|
+
cfg,
|
|
403
|
+
);
|
|
404
|
+
if (!rdir.ok) {
|
|
405
|
+
const ws = getWorkspaceState(cfg);
|
|
406
|
+
return {
|
|
407
|
+
success: false,
|
|
408
|
+
error: rdir.message,
|
|
409
|
+
code: rdir.code,
|
|
410
|
+
workspaces: ws.workspaces,
|
|
411
|
+
defaultWorkspacePath: ws.defaultWorkspacePath,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const dir = rdir.path;
|
|
318
415
|
if (!cliType) throw new Error('cliType required');
|
|
319
416
|
const found = this.findAdapter(cliType, { instanceKey: args?._targetInstance, dir });
|
|
320
417
|
if (found) await this.stopSession(found.key);
|
|
321
418
|
await this.startSession(cliType, dir);
|
|
419
|
+
this.persistRecentDir(cliType, dir);
|
|
322
420
|
return { success: true, restarted: true };
|
|
323
421
|
}
|
|
324
422
|
case 'agent_command': {
|