@adhdev/daemon-core 0.8.27 → 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/commands/handler.d.ts +1 -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 +603 -355
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +601 -353
- package/dist/index.mjs.map +1 -1
- package/dist/providers/acp-provider-instance.d.ts +1 -1
- package/dist/providers/provider-loader.d.ts +26 -0
- package/dist/shared-types.d.ts +2 -0
- package/dist/status/snapshot.d.ts +8 -0
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/agent-stream/provider-adapter.ts +15 -2
- package/src/boot/daemon-lifecycle.ts +28 -1
- package/src/cli-adapters/provider-cli-adapter.ts +14 -3
- package/src/commands/chat-commands.ts +36 -7
- package/src/commands/cli-manager.ts +2 -2
- package/src/commands/handler.ts +1 -0
- package/src/commands/router.ts +12 -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/provider-loader.ts +144 -11
- package/src/shared-types.ts +2 -0
- package/src/status/snapshot.ts +19 -1
|
@@ -77,7 +77,7 @@ export declare class AcpProviderInstance implements ProviderInstance {
|
|
|
77
77
|
sendPrompt(text: string, contentBlocks?: ContentBlock[]): Promise<void>;
|
|
78
78
|
private cancelSession;
|
|
79
79
|
private permissionResolvers;
|
|
80
|
-
|
|
80
|
+
resolvePermission(approved: boolean): Promise<void>;
|
|
81
81
|
private handleSessionUpdate;
|
|
82
82
|
/** Handle legacy session/update formats (pre-standardization compat) */
|
|
83
83
|
private handleLegacyUpdate;
|
|
@@ -16,6 +16,7 @@ import { VersionArchive } from './version-archive.js';
|
|
|
16
16
|
import type { ProviderModule, ProviderCategory, ProviderSettingSchema, ResolvedProvider } from './contracts.js';
|
|
17
17
|
export declare class ProviderLoader {
|
|
18
18
|
private providers;
|
|
19
|
+
private providerAvailability;
|
|
19
20
|
private userDir;
|
|
20
21
|
private upstreamDir;
|
|
21
22
|
private disableUpstream;
|
|
@@ -163,6 +164,28 @@ export declare class ProviderLoader {
|
|
|
163
164
|
* Available IDE types (only those with cdpPorts)
|
|
164
165
|
*/
|
|
165
166
|
getAvailableIdeTypes(): string[];
|
|
167
|
+
getSpawnCommand(type: string, fallback?: string): string;
|
|
168
|
+
getIdeCliCommand(type: string, fallback?: string | null): string | null;
|
|
169
|
+
getIdePathCandidates(type: string, fallback?: string[]): string[];
|
|
170
|
+
setProviderAvailability(type: string, state: {
|
|
171
|
+
installed: boolean;
|
|
172
|
+
detectedPath?: string | null;
|
|
173
|
+
}): void;
|
|
174
|
+
setCliDetectionResults(results: Array<{
|
|
175
|
+
id: string;
|
|
176
|
+
installed: boolean;
|
|
177
|
+
path?: string;
|
|
178
|
+
}>, replace?: boolean): void;
|
|
179
|
+
setIdeDetectionResults(results: Array<{
|
|
180
|
+
id: string;
|
|
181
|
+
installed: boolean;
|
|
182
|
+
path?: string | null;
|
|
183
|
+
cliCommand?: string | null;
|
|
184
|
+
}>, replace?: boolean): void;
|
|
185
|
+
getAvailableProviderInfos(): Array<ProviderModule & {
|
|
186
|
+
installed?: boolean;
|
|
187
|
+
detectedPath?: string | null;
|
|
188
|
+
}>;
|
|
166
189
|
/**
|
|
167
190
|
* Register IDE providers to core/detector registry
|
|
168
191
|
* → Enables detectIDEs() to detect provider.js-based IDEs
|
|
@@ -242,6 +265,9 @@ export declare class ProviderLoader {
|
|
|
242
265
|
* Save provider setting value (writes to config.json)
|
|
243
266
|
*/
|
|
244
267
|
setSetting(type: string, key: string, value: any): boolean;
|
|
268
|
+
private getOptionalStringSetting;
|
|
269
|
+
private getSettingsSchema;
|
|
270
|
+
private getSyntheticSettings;
|
|
245
271
|
/**
|
|
246
272
|
* Find the on-disk directory for a provider by type.
|
|
247
273
|
* Canonical shape: root/category/type.
|
package/dist/shared-types.d.ts
CHANGED
|
@@ -84,6 +84,8 @@ export interface AvailableProviderInfo {
|
|
|
84
84
|
category: 'ide' | 'extension' | 'cli' | 'acp';
|
|
85
85
|
displayName: string;
|
|
86
86
|
icon: string;
|
|
87
|
+
installed?: boolean;
|
|
88
|
+
detectedPath?: string | null;
|
|
87
89
|
}
|
|
88
90
|
/** ACP config option (model/mode/thought_level selection) */
|
|
89
91
|
export interface AcpConfigOption {
|
|
@@ -17,6 +17,14 @@ export interface StatusSnapshotOptions {
|
|
|
17
17
|
displayName?: string;
|
|
18
18
|
category: 'ide' | 'extension' | 'cli' | 'acp';
|
|
19
19
|
}>;
|
|
20
|
+
getAvailableProviderInfos?: () => Array<{
|
|
21
|
+
type: string;
|
|
22
|
+
icon?: string;
|
|
23
|
+
displayName?: string;
|
|
24
|
+
category: 'ide' | 'extension' | 'cli' | 'acp';
|
|
25
|
+
installed?: boolean;
|
|
26
|
+
detectedPath?: string | null;
|
|
27
|
+
}>;
|
|
20
28
|
};
|
|
21
29
|
detectedIdes: Array<{
|
|
22
30
|
id: string;
|
package/package.json
CHANGED
|
@@ -171,14 +171,27 @@ export class ProviderStreamAdapter implements IAgentStreamAdapter {
|
|
|
171
171
|
const raw = await evaluate(script, 10000) as string;
|
|
172
172
|
const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
173
173
|
if (data?.error) return [];
|
|
174
|
-
|
|
174
|
+
if (Array.isArray(data)) return data;
|
|
175
|
+
if (Array.isArray(data?.sessions)) return data.sessions;
|
|
176
|
+
if (Array.isArray(data?.chats)) return data.chats;
|
|
177
|
+
return [];
|
|
175
178
|
} catch { return []; }
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
async switchSession(evaluate: AgentEvaluateFn, sessionId: string): Promise<boolean> {
|
|
179
182
|
const script = this.callScript('switchSession', sessionId);
|
|
180
183
|
if (!script) return false;
|
|
181
|
-
|
|
184
|
+
const raw = await evaluate(script, 10000);
|
|
185
|
+
const data = this.parseMaybeJson(raw);
|
|
186
|
+
if (data === true) return true;
|
|
187
|
+
if (typeof data === 'string') {
|
|
188
|
+
const normalized = data.trim().toLowerCase();
|
|
189
|
+
return normalized === 'true' || normalized === 'ok' || normalized === 'switched' || normalized === 'success';
|
|
190
|
+
}
|
|
191
|
+
if (data && typeof data === 'object') {
|
|
192
|
+
return data.switched === true || data.success === true || data.ok === true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
async focusEditor(evaluate: AgentEvaluateFn): Promise<void> {
|
|
@@ -25,6 +25,7 @@ import { VersionArchive, detectAllVersions } from '../providers/version-archive.
|
|
|
25
25
|
import { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
|
|
26
26
|
import { DevServer } from '../daemon/dev-server.js';
|
|
27
27
|
import { detectIDEs } from '../detection/ide-detector.js';
|
|
28
|
+
import { detectCLI, detectCLIs } from '../detection/cli-detector.js';
|
|
28
29
|
import { SessionRegistry } from '../sessions/registry.js';
|
|
29
30
|
import { installGlobalInterceptor, LOG } from '../logging/logger.js';
|
|
30
31
|
import { loadConfig } from '../config/config.js';
|
|
@@ -160,6 +161,28 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
|
|
|
160
161
|
let agentStreamManager: DaemonAgentStreamManager | null = null;
|
|
161
162
|
let poller: AgentStreamPoller | null = null;
|
|
162
163
|
|
|
164
|
+
const refreshProviderAvailability = async (providerType?: string) => {
|
|
165
|
+
const targetProvider = providerType ? providerLoader.getMeta(providerLoader.resolveAlias(providerType)) : null;
|
|
166
|
+
const targetCategory = targetProvider?.category;
|
|
167
|
+
|
|
168
|
+
if (!providerType || targetCategory === 'cli' || targetCategory === 'acp') {
|
|
169
|
+
if (providerType && targetProvider) {
|
|
170
|
+
const detected = await detectCLI(targetProvider.type, providerLoader, { includeVersion: false });
|
|
171
|
+
providerLoader.setProviderAvailability(targetProvider.type, {
|
|
172
|
+
installed: !!detected,
|
|
173
|
+
detectedPath: detected?.path || null,
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
providerLoader.setCliDetectionResults(await detectCLIs(providerLoader, { includeVersion: false }), true);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!providerType || targetCategory === 'ide') {
|
|
181
|
+
detectedIdesRef.value = await detectIDEs(providerLoader);
|
|
182
|
+
providerLoader.setIdeDetectionResults(detectedIdesRef.value, true);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
163
186
|
// 4. CLI Manager
|
|
164
187
|
const cliManager = new DaemonCliManager({
|
|
165
188
|
...config.cliManagerDeps,
|
|
@@ -169,7 +192,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
|
|
|
169
192
|
|
|
170
193
|
// 5. Detect IDEs
|
|
171
194
|
LOG.info('Init', 'Detecting IDEs...');
|
|
172
|
-
|
|
195
|
+
await refreshProviderAvailability();
|
|
173
196
|
const installed = detectedIdesRef.value.filter((i: any) => i.installed);
|
|
174
197
|
LOG.info('Init', `Found ${installed.length} IDE(s): ${installed.map((i: any) => i.id).join(', ') || 'none'}`);
|
|
175
198
|
|
|
@@ -219,6 +242,10 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
|
|
|
219
242
|
providerLoader,
|
|
220
243
|
instanceManager,
|
|
221
244
|
sessionRegistry,
|
|
245
|
+
onProviderSettingChanged: async (providerType) => {
|
|
246
|
+
await refreshProviderAvailability(providerType);
|
|
247
|
+
config.onStatusChange?.();
|
|
248
|
+
},
|
|
222
249
|
});
|
|
223
250
|
|
|
224
251
|
// 8. AgentStreamManager
|
|
@@ -304,12 +304,20 @@ function computeTerminalQueryTail(buffer: string): string {
|
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
function findBinary(name: string): string {
|
|
307
|
+
const trimmed = String(name || '').trim();
|
|
308
|
+
if (!trimmed) return trimmed;
|
|
309
|
+
const expanded = trimmed.startsWith('~')
|
|
310
|
+
? path.join(os.homedir(), trimmed.slice(1))
|
|
311
|
+
: trimmed;
|
|
312
|
+
if (path.isAbsolute(expanded) || expanded.includes('/') || expanded.includes('\\')) {
|
|
313
|
+
return path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
|
|
314
|
+
}
|
|
307
315
|
const isWin = os.platform() === 'win32';
|
|
308
316
|
try {
|
|
309
|
-
const cmd = isWin ? `where ${
|
|
317
|
+
const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
|
|
310
318
|
return execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0].trim();
|
|
311
319
|
} catch {
|
|
312
|
-
return isWin ? `${
|
|
320
|
+
return isWin ? `${trimmed}.cmd` : trimmed;
|
|
313
321
|
}
|
|
314
322
|
}
|
|
315
323
|
|
|
@@ -928,7 +936,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
928
936
|
if (this.ptyProcess) return;
|
|
929
937
|
|
|
930
938
|
const { spawn: spawnConfig } = this.provider;
|
|
931
|
-
const
|
|
939
|
+
const configuredCommand = typeof this.runtimeSettings.executablePath === 'string' && this.runtimeSettings.executablePath.trim()
|
|
940
|
+
? this.runtimeSettings.executablePath.trim()
|
|
941
|
+
: spawnConfig.command;
|
|
942
|
+
const binaryPath = findBinary(configuredCommand);
|
|
932
943
|
const isWin = os.platform() === 'win32';
|
|
933
944
|
const allArgs = [...spawnConfig.args, ...this.extraArgs];
|
|
934
945
|
|
|
@@ -478,6 +478,14 @@ export async function handleListChats(h: CommandHelpers, args: any): Promise<Com
|
|
|
478
478
|
if (evalResult) {
|
|
479
479
|
let parsed = evalResult.result;
|
|
480
480
|
if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { } }
|
|
481
|
+
if (parsed?.sessions && Array.isArray(parsed.sessions)) {
|
|
482
|
+
LOG.info('Command', `[list_chats] OK: ${parsed.sessions.length} chats`);
|
|
483
|
+
return { success: true, chats: parsed.sessions };
|
|
484
|
+
}
|
|
485
|
+
if (parsed?.chats && Array.isArray(parsed.chats)) {
|
|
486
|
+
LOG.info('Command', `[list_chats] OK: ${parsed.chats.length} chats`);
|
|
487
|
+
return { success: true, chats: parsed.chats };
|
|
488
|
+
}
|
|
481
489
|
if (Array.isArray(parsed)) {
|
|
482
490
|
LOG.info('Command', `[list_chats] OK: ${parsed.length} chats`);
|
|
483
491
|
return { success: true, chats: parsed };
|
|
@@ -561,8 +569,14 @@ export async function handleSwitchChat(h: CommandHelpers, args: any): Promise<Co
|
|
|
561
569
|
return { success: false, error: `webviewSwitchSession failed: ${e.message}` };
|
|
562
570
|
}
|
|
563
571
|
|
|
564
|
-
const
|
|
565
|
-
|
|
572
|
+
const switchParams = {
|
|
573
|
+
sessionId,
|
|
574
|
+
title: sessionId,
|
|
575
|
+
id: sessionId,
|
|
576
|
+
SESSION_ID: JSON.stringify(sessionId),
|
|
577
|
+
};
|
|
578
|
+
const script = h.getProviderScript('switchSession', switchParams)
|
|
579
|
+
|| h.getProviderScript('switch_session', switchParams);
|
|
566
580
|
if (!script) return { success: false, error: 'switch_session script not available' };
|
|
567
581
|
|
|
568
582
|
try {
|
|
@@ -630,8 +644,8 @@ export async function handleSetMode(h: CommandHelpers, args: any): Promise<Comma
|
|
|
630
644
|
const adapter = getTargetedCliAdapter(h, args, provider?.type);
|
|
631
645
|
if (adapter) {
|
|
632
646
|
const acpInstance = (adapter as any)._acpInstance;
|
|
633
|
-
if (acpInstance && typeof acpInstance.
|
|
634
|
-
acpInstance.
|
|
647
|
+
if (acpInstance && typeof acpInstance.setMode === 'function') {
|
|
648
|
+
await acpInstance.setMode(mode);
|
|
635
649
|
return { success: true, mode };
|
|
636
650
|
}
|
|
637
651
|
}
|
|
@@ -687,9 +701,9 @@ export async function handleChangeModel(h: CommandHelpers, args: any): Promise<C
|
|
|
687
701
|
LOG.info('Command', `[change_model] ACP adapter found: ${!!adapter}, type=${(adapter as any)?.cliType}, hasAcpInstance=${!!(adapter as any)?._acpInstance}`);
|
|
688
702
|
if (adapter) {
|
|
689
703
|
const acpInstance = (adapter as any)._acpInstance;
|
|
690
|
-
if (acpInstance && typeof acpInstance.
|
|
691
|
-
acpInstance.
|
|
692
|
-
LOG.info('Command', `[change_model]
|
|
704
|
+
if (acpInstance && typeof acpInstance.setConfigOption === 'function') {
|
|
705
|
+
await acpInstance.setConfigOption('model', model);
|
|
706
|
+
LOG.info('Command', `[change_model] Updated ACP model to ${model}`);
|
|
693
707
|
return { success: true, model };
|
|
694
708
|
}
|
|
695
709
|
}
|
|
@@ -819,6 +833,21 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
|
|
|
819
833
|
return { success: ok };
|
|
820
834
|
}
|
|
821
835
|
|
|
836
|
+
// 1.5 ACP transport: resolve protocol permission request directly
|
|
837
|
+
if (transport === 'acp') {
|
|
838
|
+
const adapter = getTargetedCliAdapter(h, args, provider?.type);
|
|
839
|
+
const acpInstance = adapter?._acpInstance;
|
|
840
|
+
if (!acpInstance) return { success: false, error: 'ACP instance not found' };
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
await acpInstance.resolvePermission(action === 'approve' || action === 'accept' || action === 'always');
|
|
844
|
+
LOG.info('Command', `[resolveAction] ACP → ${action}`);
|
|
845
|
+
return { success: true, action };
|
|
846
|
+
} catch (e: any) {
|
|
847
|
+
return { success: false, error: e?.message || 'ACP resolve action failed' };
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
822
851
|
// 2. Webview Provider script
|
|
823
852
|
if (provider?.scripts?.webviewResolveAction || provider?.scripts?.webview_resolve_action) {
|
|
824
853
|
const script = h.getProviderScript('webviewResolveAction', { action, button, buttonText: button })
|
|
@@ -522,10 +522,10 @@ export class DaemonCliManager {
|
|
|
522
522
|
if (!cliInfo) {
|
|
523
523
|
const installHint = provider?.install || '';
|
|
524
524
|
const displayName = provider?.displayName || provider?.name || cliType;
|
|
525
|
-
const spawnCmd = provider?.spawn?.command || cliType;
|
|
525
|
+
const spawnCmd = this.providerLoader.getSpawnCommand(normalizedType, provider?.spawn?.command || cliType);
|
|
526
526
|
throw new Error(
|
|
527
527
|
`${displayName} is not installed.\n` +
|
|
528
|
-
`Command '${spawnCmd}' not
|
|
528
|
+
`Command '${spawnCmd}' is not available.\n` +
|
|
529
529
|
(installHint ? `\n${installHint}\n` : '') +
|
|
530
530
|
`\nRun 'adhdev doctor' for detailed diagnostics.`
|
|
531
531
|
);
|
package/src/commands/handler.ts
CHANGED
|
@@ -44,6 +44,7 @@ export interface CommandContext {
|
|
|
44
44
|
/** ProviderInstanceManager — for runtime settings propagation */
|
|
45
45
|
instanceManager?: ProviderInstanceManager;
|
|
46
46
|
sessionRegistry?: SessionRegistry;
|
|
47
|
+
onProviderSettingChanged?: (providerType: string, key: string, value: any) => Promise<void> | void;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
package/src/commands/router.ts
CHANGED
|
@@ -365,6 +365,11 @@ export class DaemonCommandRouter {
|
|
|
365
365
|
if (!ideType) throw new Error('ideType required');
|
|
366
366
|
const killProcess = args?.killProcess !== false; // default true
|
|
367
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 */ }
|
|
368
373
|
return { success: true, ideType, stopped: true, processKilled: killProcess };
|
|
369
374
|
}
|
|
370
375
|
|
|
@@ -415,6 +420,11 @@ export class DaemonCommandRouter {
|
|
|
415
420
|
}
|
|
416
421
|
}
|
|
417
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 */ }
|
|
418
428
|
if (result.success && resolvedWorkspace) {
|
|
419
429
|
try {
|
|
420
430
|
const next = appendRecentActivity(loadState(), {
|
|
@@ -441,8 +451,9 @@ export class DaemonCommandRouter {
|
|
|
441
451
|
|
|
442
452
|
// ─── Detect IDEs ───
|
|
443
453
|
case 'detect_ides': {
|
|
444
|
-
const results = await detectIDEs();
|
|
454
|
+
const results = await detectIDEs(this.deps.providerLoader);
|
|
445
455
|
this.deps.detectedIdes.value = results;
|
|
456
|
+
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
446
457
|
return { success: true, detectedInfo: results };
|
|
447
458
|
}
|
|
448
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);
|