@adhdev/daemon-core 0.8.25 → 0.8.28
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/agent-stream/provider-adapter.d.ts +1 -0
- package/dist/boot/daemon-lifecycle.d.ts +2 -0
- package/dist/cli-adapters/pty-transport.d.ts +3 -0
- package/dist/commands/handler.d.ts +1 -0
- package/dist/commands/router.d.ts +24 -0
- package/dist/commands/stream-commands.d.ts +1 -1
- package/dist/detection/cli-detector.d.ts +6 -2
- package/dist/detection/ide-detector.d.ts +2 -1
- package/dist/index.js +824 -369
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +822 -367
- package/dist/index.mjs.map +1 -1
- package/dist/providers/acp-provider-instance.d.ts +1 -1
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/dist/providers/extension-provider-instance.d.ts +7 -0
- package/dist/providers/provider-loader.d.ts +26 -0
- package/dist/shared-types.d.ts +2 -0
- package/dist/status/normalize.js +14 -2
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +14 -2
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/snapshot.d.ts +8 -2
- package/node_modules/@adhdev/session-host-core/dist/index.d.mts +72 -1
- package/node_modules/@adhdev/session-host-core/dist/index.d.ts +72 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/agent-stream/provider-adapter.ts +45 -3
- package/src/boot/daemon-lifecycle.ts +31 -1
- package/src/cli-adapters/provider-cli-adapter.ts +14 -3
- package/src/cli-adapters/pty-transport.ts +3 -0
- package/src/cli-adapters/session-host-transport.ts +8 -0
- package/src/commands/chat-commands.ts +38 -9
- package/src/commands/cli-manager.ts +2 -2
- package/src/commands/handler.ts +26 -3
- package/src/commands/router.ts +144 -1
- package/src/commands/stream-commands.ts +6 -3
- package/src/detection/cli-detector.ts +72 -29
- package/src/detection/ide-detector.ts +24 -8
- package/src/launch.ts +1 -1
- package/src/providers/acp-provider-instance.ts +19 -10
- package/src/providers/cli-provider-instance.ts +29 -1
- package/src/providers/extension-provider-instance.ts +24 -1
- package/src/providers/provider-loader.ts +144 -11
- package/src/shared-types.ts +2 -0
- package/src/status/normalize.ts +19 -2
- package/src/status/snapshot.ts +25 -14
package/src/commands/router.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { registerExtensionProviders } from '../cdp/setup.js';
|
|
|
14
14
|
import { DaemonCommandHandler } from './handler.js';
|
|
15
15
|
import { DaemonCliManager } from './cli-manager.js';
|
|
16
16
|
import { supportsExplicitSessionResume } from './cli-manager.js';
|
|
17
|
+
import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
|
|
17
18
|
import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
18
19
|
import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
|
|
19
20
|
import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
|
|
@@ -35,6 +36,18 @@ import * as fs from 'fs';
|
|
|
35
36
|
|
|
36
37
|
// ─── Types ───
|
|
37
38
|
|
|
39
|
+
export interface SessionHostControlPlane {
|
|
40
|
+
getDiagnostics(payload?: { includeSessions?: boolean; limit?: number }): Promise<any>;
|
|
41
|
+
listSessions(): Promise<any[]>;
|
|
42
|
+
stopSession(sessionId: string): Promise<any>;
|
|
43
|
+
resumeSession(sessionId: string): Promise<any>;
|
|
44
|
+
restartSession(sessionId: string): Promise<any>;
|
|
45
|
+
sendSignal(sessionId: string, signal: string): Promise<any>;
|
|
46
|
+
forceDetachClient(sessionId: string, clientId: string): Promise<any>;
|
|
47
|
+
acquireWrite(payload: { sessionId: string; clientId: string; ownerType: 'agent' | 'user'; force?: boolean }): Promise<any>;
|
|
48
|
+
releaseWrite(payload: { sessionId: string; clientId: string }): Promise<any>;
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
export interface CommandRouterDeps {
|
|
39
52
|
commandHandler: DaemonCommandHandler;
|
|
40
53
|
cliManager: DaemonCliManager;
|
|
@@ -56,6 +69,8 @@ export interface CommandRouterDeps {
|
|
|
56
69
|
getCdpLogFn?: (ideType: string) => (msg: string) => void;
|
|
57
70
|
/** Package name for upgrade detection ('adhdev' or '@adhdev/daemon-standalone') */
|
|
58
71
|
packageName?: string;
|
|
72
|
+
/** Session host control plane */
|
|
73
|
+
sessionHostControl?: SessionHostControlPlane | null;
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
export interface CommandRouterResult {
|
|
@@ -70,6 +85,30 @@ const CHAT_COMMANDS = [
|
|
|
70
85
|
];
|
|
71
86
|
const READ_DEBUG_ENABLED = process.argv.includes('--dev') || process.env.ADHDEV_READ_DEBUG === '1';
|
|
72
87
|
|
|
88
|
+
function toHostedCliRuntimeDescriptor(record: any): HostedCliRuntimeDescriptor | null {
|
|
89
|
+
if (!record || typeof record !== 'object') return null;
|
|
90
|
+
const runtimeId = typeof record.sessionId === 'string' ? record.sessionId : '';
|
|
91
|
+
const cliType = typeof record.providerType === 'string' ? record.providerType : '';
|
|
92
|
+
const workspace = typeof record.workspace === 'string' ? record.workspace : '';
|
|
93
|
+
if (!runtimeId || !cliType || !workspace) return null;
|
|
94
|
+
return {
|
|
95
|
+
runtimeId,
|
|
96
|
+
runtimeKey: typeof record.runtimeKey === 'string' ? record.runtimeKey : undefined,
|
|
97
|
+
displayName: typeof record.displayName === 'string' ? record.displayName : undefined,
|
|
98
|
+
workspaceLabel: typeof record.workspaceLabel === 'string' ? record.workspaceLabel : undefined,
|
|
99
|
+
lifecycle: typeof record.lifecycle === 'string' ? record.lifecycle as HostedCliRuntimeDescriptor['lifecycle'] : undefined,
|
|
100
|
+
recoveryState: typeof record.meta?.runtimeRecoveryState === 'string'
|
|
101
|
+
? String(record.meta.runtimeRecoveryState)
|
|
102
|
+
: null,
|
|
103
|
+
cliType,
|
|
104
|
+
workspace,
|
|
105
|
+
cliArgs: Array.isArray(record.meta?.cliArgs) ? record.meta.cliArgs as string[] : [],
|
|
106
|
+
providerSessionId: typeof record.meta?.providerSessionId === 'string'
|
|
107
|
+
? String(record.meta.providerSessionId)
|
|
108
|
+
: undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
73
112
|
export class DaemonCommandRouter {
|
|
74
113
|
private deps: CommandRouterDeps;
|
|
75
114
|
|
|
@@ -159,6 +198,99 @@ export class DaemonCommandRouter {
|
|
|
159
198
|
}
|
|
160
199
|
}
|
|
161
200
|
|
|
201
|
+
case 'session_host_get_diagnostics': {
|
|
202
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
203
|
+
const diagnostics = await this.deps.sessionHostControl.getDiagnostics({
|
|
204
|
+
includeSessions: args?.includeSessions !== false,
|
|
205
|
+
limit: Number(args?.limit) || undefined,
|
|
206
|
+
});
|
|
207
|
+
return { success: true, diagnostics };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case 'session_host_list_sessions': {
|
|
211
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
212
|
+
const sessions = await this.deps.sessionHostControl.listSessions();
|
|
213
|
+
return { success: true, sessions };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'session_host_stop_session': {
|
|
217
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
218
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
219
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
220
|
+
const record = await this.deps.sessionHostControl.stopSession(sessionId);
|
|
221
|
+
return { success: true, record };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case 'session_host_resume_session': {
|
|
225
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
226
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
227
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
228
|
+
const record = await this.deps.sessionHostControl.resumeSession(sessionId);
|
|
229
|
+
const hosted = toHostedCliRuntimeDescriptor(record);
|
|
230
|
+
if (hosted) {
|
|
231
|
+
await this.deps.cliManager.restoreHostedSessions([hosted]);
|
|
232
|
+
}
|
|
233
|
+
return { success: true, record };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case 'session_host_restart_session': {
|
|
237
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
238
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
239
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
240
|
+
const record = await this.deps.sessionHostControl.restartSession(sessionId);
|
|
241
|
+
const hosted = toHostedCliRuntimeDescriptor(record);
|
|
242
|
+
if (hosted) {
|
|
243
|
+
await this.deps.cliManager.restoreHostedSessions([hosted]);
|
|
244
|
+
}
|
|
245
|
+
return { success: true, record };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case 'session_host_send_signal': {
|
|
249
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
250
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
251
|
+
const signal = typeof args?.signal === 'string' ? args.signal : '';
|
|
252
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
253
|
+
if (!signal) return { success: false, error: 'signal required' };
|
|
254
|
+
const record = await this.deps.sessionHostControl.sendSignal(sessionId, signal);
|
|
255
|
+
return { success: true, record };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'session_host_force_detach_client': {
|
|
259
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
260
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
261
|
+
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
262
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
263
|
+
if (!clientId) return { success: false, error: 'clientId required' };
|
|
264
|
+
const record = await this.deps.sessionHostControl.forceDetachClient(sessionId, clientId);
|
|
265
|
+
return { success: true, record };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
case 'session_host_acquire_write': {
|
|
269
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
270
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
271
|
+
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
272
|
+
const ownerType = args?.ownerType === 'agent' ? 'agent' : 'user';
|
|
273
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
274
|
+
if (!clientId) return { success: false, error: 'clientId required' };
|
|
275
|
+
const record = await this.deps.sessionHostControl.acquireWrite({
|
|
276
|
+
sessionId,
|
|
277
|
+
clientId,
|
|
278
|
+
ownerType,
|
|
279
|
+
force: args?.force !== false,
|
|
280
|
+
});
|
|
281
|
+
return { success: true, record };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case 'session_host_release_write': {
|
|
285
|
+
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
286
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
287
|
+
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
288
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
289
|
+
if (!clientId) return { success: false, error: 'clientId required' };
|
|
290
|
+
const record = await this.deps.sessionHostControl.releaseWrite({ sessionId, clientId });
|
|
291
|
+
return { success: true, record };
|
|
292
|
+
}
|
|
293
|
+
|
|
162
294
|
case 'list_saved_sessions': {
|
|
163
295
|
const providerType = typeof args?.providerType === 'string'
|
|
164
296
|
? args.providerType.trim()
|
|
@@ -233,6 +365,11 @@ export class DaemonCommandRouter {
|
|
|
233
365
|
if (!ideType) throw new Error('ideType required');
|
|
234
366
|
const killProcess = args?.killProcess !== false; // default true
|
|
235
367
|
await this.stopIde(ideType, killProcess);
|
|
368
|
+
try {
|
|
369
|
+
const results = await detectIDEs(this.deps.providerLoader);
|
|
370
|
+
this.deps.detectedIdes.value = results;
|
|
371
|
+
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
372
|
+
} catch { /* ignore detection refresh errors */ }
|
|
236
373
|
return { success: true, ideType, stopped: true, processKilled: killProcess };
|
|
237
374
|
}
|
|
238
375
|
|
|
@@ -283,6 +420,11 @@ export class DaemonCommandRouter {
|
|
|
283
420
|
}
|
|
284
421
|
}
|
|
285
422
|
this.deps.onIdeConnected?.();
|
|
423
|
+
try {
|
|
424
|
+
const results = await detectIDEs(this.deps.providerLoader);
|
|
425
|
+
this.deps.detectedIdes.value = results;
|
|
426
|
+
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
427
|
+
} catch { /* ignore detection refresh errors */ }
|
|
286
428
|
if (result.success && resolvedWorkspace) {
|
|
287
429
|
try {
|
|
288
430
|
const next = appendRecentActivity(loadState(), {
|
|
@@ -309,8 +451,9 @@ export class DaemonCommandRouter {
|
|
|
309
451
|
|
|
310
452
|
// ─── Detect IDEs ───
|
|
311
453
|
case 'detect_ides': {
|
|
312
|
-
const results = await detectIDEs();
|
|
454
|
+
const results = await detectIDEs(this.deps.providerLoader);
|
|
313
455
|
this.deps.detectedIdes.value = results;
|
|
456
|
+
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
314
457
|
return { success: true, detectedInfo: results };
|
|
315
458
|
}
|
|
316
459
|
|
|
@@ -76,7 +76,7 @@ export function handleGetProviderSettings(h: CommandHelpers, args: any): Command
|
|
|
76
76
|
return { success: true, settings: allSettings, values: allValues };
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export function handleSetProviderSetting(h: CommandHelpers, args: any): CommandResult {
|
|
79
|
+
export async function handleSetProviderSetting(h: CommandHelpers, args: any): Promise<CommandResult> {
|
|
80
80
|
const loader = h.ctx.providerLoader as ProviderLoader | undefined;
|
|
81
81
|
const { providerType, key, value } = args || {};
|
|
82
82
|
if (!providerType || !key || value === undefined) {
|
|
@@ -89,6 +89,7 @@ export function handleSetProviderSetting(h: CommandHelpers, args: any): CommandR
|
|
|
89
89
|
const updated = h.ctx.instanceManager.updateInstanceSettings(providerType, allSettings);
|
|
90
90
|
LOG.info('Command', `[set_provider_setting] ${providerType}.${key}=${JSON.stringify(value)} → ${updated} instance(s) updated`);
|
|
91
91
|
}
|
|
92
|
+
await h.ctx.onProviderSettingChanged?.(providerType, key, value);
|
|
92
93
|
return { success: true, providerType, key, value };
|
|
93
94
|
}
|
|
94
95
|
return { success: false, error: `Failed to set ${providerType}.${key} — invalid key, value, or not a public setting` };
|
|
@@ -133,7 +134,7 @@ function getCliScriptCommand(payload: any): { type: string; text?: string } | nu
|
|
|
133
134
|
|
|
134
135
|
const command = payload.command;
|
|
135
136
|
if (!command || typeof command !== 'object') return null;
|
|
136
|
-
if (command.type !== 'send_message') return null;
|
|
137
|
+
if (command.type !== 'send_message' && command.type !== 'pty_write') return null;
|
|
137
138
|
|
|
138
139
|
const text = typeof command.text === 'string'
|
|
139
140
|
? command.text.trim()
|
|
@@ -141,7 +142,7 @@ function getCliScriptCommand(payload: any): { type: string; text?: string } | nu
|
|
|
141
142
|
? command.message.trim()
|
|
142
143
|
: '';
|
|
143
144
|
if (!text) return null;
|
|
144
|
-
return { type:
|
|
145
|
+
return { type: command.type, text };
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
function applyProviderPatch(h: CommandHelpers, args: any, payload: any): void {
|
|
@@ -191,6 +192,8 @@ async function executeProviderScript(h: CommandHelpers, args: any, scriptName: s
|
|
|
191
192
|
const cliCommand = getCliScriptCommand(parsed.payload);
|
|
192
193
|
if (cliCommand?.type === 'send_message' && cliCommand.text) {
|
|
193
194
|
await adapter.sendMessage(cliCommand.text);
|
|
195
|
+
} else if (cliCommand?.type === 'pty_write' && cliCommand.text && adapter.writeRaw) {
|
|
196
|
+
adapter.writeRaw(cliCommand.text + '\r');
|
|
194
197
|
}
|
|
195
198
|
applyProviderPatch(h, args, parsed.payload);
|
|
196
199
|
return { success: true, ...(parsed.payload && typeof parsed.payload === 'object' ? parsed.payload : { result: parsed.payload }) };
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { exec } from 'child_process';
|
|
11
11
|
import * as os from 'os';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
12
14
|
import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
13
15
|
|
|
14
16
|
export interface CLIInfo {
|
|
@@ -28,6 +30,33 @@ function parseVersion(raw: string): string {
|
|
|
28
30
|
return match ? match[1] : raw.split('\n')[0].slice(0, 100);
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function shellQuote(value: string): string {
|
|
34
|
+
if (/^[a-zA-Z0-9_./:@%+=,-]+$/.test(value)) return value;
|
|
35
|
+
return `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function expandHome(value: string): string {
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
if (!trimmed.startsWith('~')) return trimmed;
|
|
41
|
+
return path.join(os.homedir(), trimmed.slice(1));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isExplicitCommandPath(command: string): boolean {
|
|
45
|
+
const trimmed = command.trim();
|
|
46
|
+
return path.isAbsolute(trimmed) || trimmed.includes('/') || trimmed.includes('\\') || trimmed.startsWith('~');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveCommandPath(command: string): string | null {
|
|
50
|
+
const trimmed = command.trim();
|
|
51
|
+
if (!trimmed) return null;
|
|
52
|
+
if (isExplicitCommandPath(trimmed)) {
|
|
53
|
+
const expanded = expandHome(trimmed);
|
|
54
|
+
const candidate = path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
|
|
55
|
+
return existsSync(candidate) ? candidate : null;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
/** Run a shell command with timeout, returning stdout or null on failure */
|
|
32
61
|
function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
|
|
33
62
|
return new Promise((resolve) => {
|
|
@@ -47,9 +76,13 @@ function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
|
|
|
47
76
|
* Detect all CLI/ACP agents (parallel)
|
|
48
77
|
* @param providerLoader ProviderLoader instance (dynamic list creation)
|
|
49
78
|
*/
|
|
50
|
-
export async function detectCLIs(
|
|
79
|
+
export async function detectCLIs(
|
|
80
|
+
providerLoader?: ProviderLoader,
|
|
81
|
+
options?: { includeVersion?: boolean },
|
|
82
|
+
): Promise<CLIInfo[]> {
|
|
51
83
|
const platform = os.platform();
|
|
52
84
|
const whichCmd = platform === 'win32' ? 'where' : 'which';
|
|
85
|
+
const includeVersion = options?.includeVersion !== false;
|
|
53
86
|
|
|
54
87
|
// Provider-based dynamic list creation, fallback is empty array
|
|
55
88
|
const cliList = providerLoader
|
|
@@ -60,28 +93,31 @@ export async function detectCLIs(providerLoader?: ProviderLoader): Promise<CLIIn
|
|
|
60
93
|
const results = await Promise.all(
|
|
61
94
|
cliList.map(async (cli): Promise<CLIInfo> => {
|
|
62
95
|
try {
|
|
63
|
-
const
|
|
96
|
+
const explicitPath = resolveCommandPath(cli.command);
|
|
97
|
+
const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(cli.command)}`);
|
|
64
98
|
if (!pathResult) return { ...cli, installed: false };
|
|
65
99
|
|
|
66
|
-
const firstPath = pathResult.split('\n')[0];
|
|
100
|
+
const firstPath = explicitPath || pathResult.split('\n')[0];
|
|
67
101
|
|
|
68
102
|
// Get version (parallel with other checks)
|
|
69
103
|
let version: string | undefined;
|
|
70
|
-
|
|
104
|
+
if (includeVersion) {
|
|
71
105
|
const versionCommands = [
|
|
106
|
+
`"${firstPath}" --version`,
|
|
107
|
+
`"${firstPath}" -V`,
|
|
108
|
+
`"${firstPath}" -v`,
|
|
72
109
|
cli.versionCommand,
|
|
73
|
-
`${cli.command} --version`,
|
|
74
|
-
`${cli.command} -V`,
|
|
75
|
-
`${cli.command} -v`,
|
|
76
110
|
].filter((v): v is string => !!v);
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
111
|
+
try {
|
|
112
|
+
for (const versionCommand of versionCommands) {
|
|
113
|
+
const versionResult = await execAsync(versionCommand, 3000);
|
|
114
|
+
if (versionResult) {
|
|
115
|
+
version = parseVersion(versionResult);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
82
118
|
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
119
|
+
} catch { }
|
|
120
|
+
}
|
|
85
121
|
|
|
86
122
|
return { ...cli, installed: true, version, path: firstPath };
|
|
87
123
|
} catch {
|
|
@@ -94,7 +130,11 @@ export async function detectCLIs(providerLoader?: ProviderLoader): Promise<CLIIn
|
|
|
94
130
|
}
|
|
95
131
|
|
|
96
132
|
/** Detect specific CLI — only probes the one requested provider */
|
|
97
|
-
export async function detectCLI(
|
|
133
|
+
export async function detectCLI(
|
|
134
|
+
cliId: string,
|
|
135
|
+
providerLoader?: ProviderLoader,
|
|
136
|
+
options?: { includeVersion?: boolean },
|
|
137
|
+
): Promise<CLIInfo | null> {
|
|
98
138
|
const resolvedId = providerLoader ? providerLoader.resolveAlias(cliId) : cliId;
|
|
99
139
|
|
|
100
140
|
if (providerLoader) {
|
|
@@ -104,25 +144,28 @@ export async function detectCLI(cliId: string, providerLoader?: ProviderLoader):
|
|
|
104
144
|
const platform = os.platform();
|
|
105
145
|
const whichCmd = platform === 'win32' ? 'where' : 'which';
|
|
106
146
|
try {
|
|
107
|
-
const
|
|
147
|
+
const explicitPath = resolveCommandPath(target.command);
|
|
148
|
+
const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(target.command)}`);
|
|
108
149
|
if (!pathResult) return null;
|
|
109
|
-
const firstPath = pathResult.split('\n')[0];
|
|
150
|
+
const firstPath = explicitPath || pathResult.split('\n')[0];
|
|
110
151
|
let version: string | undefined;
|
|
111
|
-
|
|
152
|
+
if (options?.includeVersion !== false) {
|
|
112
153
|
const versionCommands = [
|
|
154
|
+
`"${firstPath}" --version`,
|
|
155
|
+
`"${firstPath}" -V`,
|
|
156
|
+
`"${firstPath}" -v`,
|
|
113
157
|
target.versionCommand,
|
|
114
|
-
`${target.command} --version`,
|
|
115
|
-
`${target.command} -V`,
|
|
116
|
-
`${target.command} -v`,
|
|
117
158
|
].filter((v): v is string => !!v);
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
159
|
+
try {
|
|
160
|
+
for (const versionCommand of versionCommands) {
|
|
161
|
+
const versionResult = await execAsync(versionCommand, 3000);
|
|
162
|
+
if (versionResult) {
|
|
163
|
+
version = parseVersion(versionResult);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
123
166
|
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
167
|
+
} catch { }
|
|
168
|
+
}
|
|
126
169
|
return { ...target, installed: true, version, path: firstPath };
|
|
127
170
|
} catch {
|
|
128
171
|
return null;
|
|
@@ -131,6 +174,6 @@ export async function detectCLI(cliId: string, providerLoader?: ProviderLoader):
|
|
|
131
174
|
}
|
|
132
175
|
|
|
133
176
|
// Fallback: full scan for unknown provider IDs
|
|
134
|
-
const all = await detectCLIs(providerLoader);
|
|
177
|
+
const all = await detectCLIs(providerLoader, options);
|
|
135
178
|
return all.find((c) => c.id === resolvedId && c.installed) || null;
|
|
136
179
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { platform, homedir } from 'os';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
13
15
|
|
|
14
16
|
// ─── Types ──────────────────────────────────────
|
|
15
17
|
|
|
@@ -62,9 +64,18 @@ function getMergedDefinitions(): IDEDefinition[] {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
function findCliCommand(command: string): string | null {
|
|
67
|
+
const trimmed = String(command || '').trim();
|
|
68
|
+
if (!trimmed) return null;
|
|
69
|
+
if (path.isAbsolute(trimmed) || trimmed.includes('/') || trimmed.includes('\\') || trimmed.startsWith('~')) {
|
|
70
|
+
const candidate = trimmed.startsWith('~')
|
|
71
|
+
? path.join(homedir(), trimmed.slice(1))
|
|
72
|
+
: trimmed;
|
|
73
|
+
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
|
|
74
|
+
return existsSync(resolved) ? resolved : null;
|
|
75
|
+
}
|
|
65
76
|
try {
|
|
66
77
|
const result = execSync(
|
|
67
|
-
platform() === 'win32' ? `where ${
|
|
78
|
+
platform() === 'win32' ? `where ${trimmed}` : `which ${trimmed}`,
|
|
68
79
|
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
69
80
|
).trim();
|
|
70
81
|
return result.split('\n')[0] || null;
|
|
@@ -89,27 +100,29 @@ function getIdeVersion(cliCommand: string): string | null {
|
|
|
89
100
|
function checkPathExists(paths: string[]): string | null {
|
|
90
101
|
const home = homedir();
|
|
91
102
|
for (const p of paths) {
|
|
92
|
-
|
|
103
|
+
const normalized = p.startsWith('~')
|
|
104
|
+
? path.join(home, p.slice(1))
|
|
105
|
+
: p;
|
|
106
|
+
if (normalized.includes('*')) {
|
|
93
107
|
// Wildcard expansion: replace `*` with the current user's home folder name
|
|
94
108
|
// e.g. "C:\Users\*\AppData\..." → "C:\Users\vilmi\AppData\..."
|
|
95
109
|
const username = home.split(/[\\/]/).pop() || '';
|
|
96
|
-
const resolved =
|
|
110
|
+
const resolved = normalized.replace('*', username);
|
|
97
111
|
if (existsSync(resolved)) return resolved;
|
|
98
112
|
} else {
|
|
99
|
-
if (existsSync(
|
|
113
|
+
if (existsSync(normalized)) return normalized;
|
|
100
114
|
}
|
|
101
115
|
}
|
|
102
116
|
return null;
|
|
103
117
|
}
|
|
104
118
|
|
|
105
|
-
export async function detectIDEs(): Promise<IDEInfo[]> {
|
|
119
|
+
export async function detectIDEs(providerLoader?: ProviderLoader): Promise<IDEInfo[]> {
|
|
106
120
|
const os = platform() as 'darwin' | 'win32' | 'linux';
|
|
107
121
|
const results: IDEInfo[] = [];
|
|
108
122
|
|
|
109
123
|
for (const def of getMergedDefinitions()) {
|
|
110
|
-
const cliPath = findCliCommand(def.cli);
|
|
111
|
-
const appPath = checkPathExists(def.paths[os] || []);
|
|
112
|
-
const installed = !!(cliPath || appPath);
|
|
124
|
+
const cliPath = findCliCommand(providerLoader?.getIdeCliCommand(def.id, def.cli) || def.cli);
|
|
125
|
+
const appPath = checkPathExists(providerLoader?.getIdePathCandidates(def.id, def.paths[os] || []) || []);
|
|
113
126
|
|
|
114
127
|
let resolvedCli = cliPath;
|
|
115
128
|
|
|
@@ -136,6 +149,9 @@ export async function detectIDEs(): Promise<IDEInfo[]> {
|
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
const installed = os === 'darwin'
|
|
153
|
+
? !!(resolvedCli || appPath)
|
|
154
|
+
: !!resolvedCli;
|
|
139
155
|
const version = resolvedCli ? getIdeVersion(resolvedCli) : null;
|
|
140
156
|
|
|
141
157
|
results.push({
|
package/src/launch.ts
CHANGED
|
@@ -310,7 +310,7 @@ export async function launchWithCdp(options: LaunchOptions = {}): Promise<Launch
|
|
|
310
310
|
|
|
311
311
|
// 1. IDE determine
|
|
312
312
|
let targetIde: IDEInfo | undefined;
|
|
313
|
-
const ides = await detectIDEs();
|
|
313
|
+
const ides = await detectIDEs(getProviderLoader());
|
|
314
314
|
|
|
315
315
|
if (options.ideId) {
|
|
316
316
|
targetIde = ides.find(i => i.id === options.ideId && i.installed);
|
|
@@ -327,8 +327,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
327
327
|
// Find configId for this category
|
|
328
328
|
const opt = this.configOptions.find(c => c.category === category);
|
|
329
329
|
if (!opt) {
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
const message = `[${this.type}] No config option for category: ${category}`;
|
|
331
|
+
this.log.warn(message);
|
|
332
|
+
throw new Error(message);
|
|
332
333
|
}
|
|
333
334
|
|
|
334
335
|
// Static config mode: update selection and restart process
|
|
@@ -343,8 +344,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
343
344
|
}
|
|
344
345
|
|
|
345
346
|
if (!this.connection || !this.sessionId) {
|
|
346
|
-
|
|
347
|
-
|
|
347
|
+
const message = `[${this.type}] Cannot set config: no active connection/session`;
|
|
348
|
+
this.log.warn(message);
|
|
349
|
+
throw new Error(message);
|
|
348
350
|
}
|
|
349
351
|
|
|
350
352
|
try {
|
|
@@ -361,7 +363,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
361
363
|
if (result?.configOptions) this.parseConfigOptions(result.configOptions);
|
|
362
364
|
this.log.info(`[${this.type}] Config ${category} set to: ${value} | response: ${JSON.stringify(result)?.slice(0, 300)}`);
|
|
363
365
|
} catch (e: any) {
|
|
364
|
-
|
|
366
|
+
const message = e?.message || 'Unknown ACP config error';
|
|
367
|
+
this.log.warn(`[${this.type}] set_config_option failed: ${message}`);
|
|
368
|
+
throw new Error(message);
|
|
365
369
|
}
|
|
366
370
|
}
|
|
367
371
|
|
|
@@ -380,8 +384,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
380
384
|
}
|
|
381
385
|
|
|
382
386
|
if (!this.connection || !this.sessionId) {
|
|
383
|
-
|
|
384
|
-
|
|
387
|
+
const message = `[${this.type}] Cannot set mode: no active connection/session`;
|
|
388
|
+
this.log.warn(message);
|
|
389
|
+
throw new Error(message);
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
try {
|
|
@@ -392,7 +397,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
392
397
|
this.currentMode = modeId;
|
|
393
398
|
this.log.info(`[${this.type}] Mode set to: ${modeId}`);
|
|
394
399
|
} catch (e: any) {
|
|
395
|
-
|
|
400
|
+
const message = e?.message || 'Unknown ACP mode error';
|
|
401
|
+
this.log.warn(`[${this.type}] set_mode failed: ${message}`);
|
|
402
|
+
throw new Error(message);
|
|
396
403
|
}
|
|
397
404
|
}
|
|
398
405
|
|
|
@@ -447,7 +454,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
447
454
|
throw new Error(`[ACP:${this.type}] No spawn config defined`);
|
|
448
455
|
}
|
|
449
456
|
|
|
450
|
-
const command =
|
|
457
|
+
const command = typeof this.settings.executablePath === 'string' && this.settings.executablePath.trim()
|
|
458
|
+
? this.settings.executablePath.trim()
|
|
459
|
+
: spawnConfig.command;
|
|
451
460
|
// Static config: create args via spawnArgBuilder (when provider defines it)
|
|
452
461
|
let baseArgs = spawnConfig.args || [];
|
|
453
462
|
if (this.provider.spawnArgBuilder && Object.keys(this.selectedConfig).length > 0) {
|
|
@@ -822,7 +831,7 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
822
831
|
|
|
823
832
|
private permissionResolvers: ((approved: boolean) => void)[] = [];
|
|
824
833
|
|
|
825
|
-
|
|
834
|
+
async resolvePermission(approved: boolean): Promise<void> {
|
|
826
835
|
const resolver = this.permissionResolvers.shift();
|
|
827
836
|
if (resolver) {
|
|
828
837
|
resolver(approved);
|
|
@@ -14,7 +14,7 @@ import type { ProviderModule } from './contracts.js';
|
|
|
14
14
|
import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext } from './provider-instance.js';
|
|
15
15
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
16
16
|
import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
|
|
17
|
-
import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
17
|
+
import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
18
18
|
import { StatusMonitor } from './status-monitor.js';
|
|
19
19
|
import { ChatHistoryWriter, readChatHistory } from '../config/chat-history.js';
|
|
20
20
|
import { LOG } from '../logging/logger.js';
|
|
@@ -133,6 +133,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
133
133
|
|
|
134
134
|
// PTY spawn
|
|
135
135
|
await this.adapter.spawn();
|
|
136
|
+
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
136
137
|
if (this.providerSessionId) {
|
|
137
138
|
const restoredHistory = readChatHistory(this.type, 0, 200, this.providerSessionId);
|
|
138
139
|
if (restoredHistory.messages.length > 0) {
|
|
@@ -246,6 +247,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
246
247
|
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
247
248
|
}
|
|
248
249
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
250
|
+
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
249
251
|
const parsedMessages = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [];
|
|
250
252
|
const controlValues = extractProviderControlValues(this.provider.controls, parsedStatus);
|
|
251
253
|
if (controlValues) {
|
|
@@ -603,6 +605,32 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
603
605
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
604
606
|
}
|
|
605
607
|
|
|
608
|
+
private maybeAppendRuntimeRecoveryMessage(runtime: PtyRuntimeMetadata | null): void {
|
|
609
|
+
if (!runtime?.restoredFromStorage || !runtime.runtimeId) return;
|
|
610
|
+
|
|
611
|
+
const recoveryState = String(runtime.recoveryState || '').trim();
|
|
612
|
+
if (!recoveryState) return;
|
|
613
|
+
|
|
614
|
+
let content = '';
|
|
615
|
+
if (recoveryState === 'auto_resumed') {
|
|
616
|
+
content = 'Session host restored this CLI after restart and reattached it from a saved snapshot.';
|
|
617
|
+
} else if (recoveryState === 'resume_failed') {
|
|
618
|
+
const errorSuffix = runtime.recoveryError ? ` Resume failed: ${runtime.recoveryError}` : '';
|
|
619
|
+
content = `Session host found this CLI after restart, but automatic resume failed.${errorSuffix}`;
|
|
620
|
+
} else if (recoveryState === 'host_restart_interrupted') {
|
|
621
|
+
content = 'Session host found this CLI in interrupted state after restart and is attempting to resume it.';
|
|
622
|
+
} else if (recoveryState === 'orphan_snapshot') {
|
|
623
|
+
content = 'Session host restored the last snapshot for this CLI, but the original runtime was not resumed automatically.';
|
|
624
|
+
} else {
|
|
625
|
+
content = `Session host restored this CLI after restart (${recoveryState}).`;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
this.appendRuntimeSystemMessage(
|
|
629
|
+
content,
|
|
630
|
+
`runtime_recovery:${runtime.runtimeId}:${recoveryState}`,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
606
634
|
private appendRuntimeSystemMessage(content: string, dedupKey: string, receivedAt = Date.now()): void {
|
|
607
635
|
const normalizedContent = String(content || '').trim();
|
|
608
636
|
if (!normalizedContent) return;
|