@adhdev/daemon-core 0.5.8 → 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,7 @@ import { DaemonCommandHandler } from './handler.js';
16
16
  import { DaemonCliManager } from './cli-manager.js';
17
17
  import type { ProviderLoader } from '../providers/provider-loader.js';
18
18
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
19
- import { launchWithCdp } from '../launch.js';
19
+ import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
20
20
  import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
21
21
  import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
22
22
  import { appendWorkspaceActivity } from '../config/workspace-activity.js';
@@ -159,9 +159,9 @@ export class DaemonCommandRouter {
159
159
  this.deps.providerLoader.getMeta(targetType)?.category === 'ide';
160
160
 
161
161
  if (isIde) {
162
- // IDE restart: stop → launch
163
- await this.stopIde(targetType);
164
- const launchResult = await this.executeDaemonCommand('launch_ide', { ideType: targetType, enableCdp: true });
162
+ // IDE restart: stop (with process kill) → launch
163
+ await this.stopIde(targetType, true);
164
+ const launchResult = await this.executeDaemonCommand('launch_ide', { ideType: targetType, enableCdp: true, workspace: args?.workspace });
165
165
  return { success: true, restarted: true, ideType: targetType, launch: launchResult };
166
166
  }
167
167
 
@@ -173,16 +173,17 @@ export class DaemonCommandRouter {
173
173
  case 'stop_ide': {
174
174
  const ideType = args?.ideType;
175
175
  if (!ideType) throw new Error('ideType required');
176
- await this.stopIde(ideType);
177
- return { success: true, ideType, stopped: true };
176
+ const killProcess = args?.killProcess !== false; // default true
177
+ await this.stopIde(ideType, killProcess);
178
+ return { success: true, ideType, stopped: true, processKilled: killProcess };
178
179
  }
179
180
 
180
181
  // ─── IDE restart ───
181
182
  case 'restart_ide': {
182
183
  const ideType = args?.ideType;
183
184
  if (!ideType) throw new Error('ideType required');
184
- await this.stopIde(ideType);
185
- const launchResult = await this.executeDaemonCommand('launch_ide', { ideType, enableCdp: true });
185
+ await this.stopIde(ideType, true); // always kill process on restart
186
+ const launchResult = await this.executeDaemonCommand('launch_ide', { ideType, enableCdp: true, workspace: args?.workspace });
186
187
  return { success: true, ideType, restarted: true, launch: launchResult };
187
188
  }
188
189
 
@@ -235,10 +236,57 @@ export class DaemonCommandRouter {
235
236
  return { success: result.success, ...result as any };
236
237
  }
237
238
 
238
- // ─── IDE detection ───
239
+ // ─── Detect IDEs ───
239
240
  case 'detect_ides': {
240
- this.deps.detectedIdes.value = await detectIDEs();
241
- return { success: true, ides: this.deps.detectedIdes.value };
241
+ const results = await detectIDEs();
242
+ this.deps.detectedIdes.value = results;
243
+ return { success: true, detectedInfo: results };
244
+ }
245
+
246
+ // ─── Set User Name ───
247
+ case 'set_user_name': {
248
+ const name = args?.userName;
249
+ if (!name || typeof name !== 'string') throw new Error('userName required');
250
+ updateConfig({ userName: name });
251
+ return { success: true, userName: name };
252
+ }
253
+
254
+ // ─── Daemon Self-Upgrade ───
255
+ case 'daemon_upgrade': {
256
+ LOG.info('Upgrade', 'Remote upgrade requested from dashboard');
257
+ try {
258
+ const { execSync } = await import('child_process');
259
+
260
+ // Check latest version
261
+ const latest = execSync('npm view adhdev version', { encoding: 'utf-8', timeout: 10000 }).trim();
262
+ LOG.info('Upgrade', `Latest available: v${latest}`);
263
+
264
+ // Install latest
265
+ execSync('npm install -g adhdev@latest', {
266
+ encoding: 'utf-8',
267
+ timeout: 60000,
268
+ stdio: ['pipe', 'pipe', 'pipe'],
269
+ });
270
+ LOG.info('Upgrade', `✅ Upgraded to v${latest}`);
271
+
272
+ // Schedule restart after response is sent
273
+ setTimeout(() => {
274
+ LOG.info('Upgrade', 'Restarting daemon with new version...');
275
+ const { spawn } = require('child_process');
276
+ const child = spawn(process.execPath, [process.argv[1], 'daemon', '-p', '19222'], {
277
+ detached: true,
278
+ stdio: 'ignore',
279
+ env: { ...process.env },
280
+ });
281
+ child.unref();
282
+ process.exit(0);
283
+ }, 3000);
284
+
285
+ return { success: true, upgraded: true, version: latest };
286
+ } catch (e: any) {
287
+ LOG.error('Upgrade', `Failed: ${e.message}`);
288
+ return { success: false, error: e.message };
289
+ }
242
290
  }
243
291
 
244
292
  // ─── Machine Settings ───
@@ -247,42 +295,93 @@ export class DaemonCommandRouter {
247
295
  updateConfig({ machineNickname: nickname || null });
248
296
  return { success: true };
249
297
  }
298
+
299
+ default:
300
+ break;
250
301
  }
251
302
 
252
303
  return null; // Not handled at this level → delegate to CommandHandler
253
304
  }
254
305
 
255
306
  /**
256
- * IDE stop: CDP disconnect + remove from InstanceManager + clean instanceIdMap
307
+ * IDE stop: CDP disconnect + InstanceManager cleanup + optionally kill OS process
257
308
  */
258
- private async stopIde(ideType: string): Promise<void> {
259
- // 1. Release CDP manager
260
- const cdp = this.deps.cdpManagers.get(ideType);
261
- if (cdp) {
262
- try { cdp.disconnect(); } catch { /* noop */ }
263
- this.deps.cdpManagers.delete(ideType);
264
- LOG.info('StopIDE', `CDP disconnected: ${ideType}`);
309
+ private async stopIde(ideType: string, killProcess: boolean = false): Promise<void> {
310
+ // 1. Release CDP manager(s) — handle multi-instance (e.g. "cursor" and "cursor_workspace")
311
+ const cdpKeysToRemove: string[] = [];
312
+ for (const key of this.deps.cdpManagers.keys()) {
313
+ if (key === ideType || key.startsWith(`${ideType}_`)) {
314
+ cdpKeysToRemove.push(key);
315
+ }
316
+ }
317
+ for (const key of cdpKeysToRemove) {
318
+ const cdp = this.deps.cdpManagers.get(key);
319
+ if (cdp) {
320
+ try { cdp.disconnect(); } catch { /* noop */ }
321
+ this.deps.cdpManagers.delete(key);
322
+ LOG.info('StopIDE', `CDP disconnected: ${key}`);
323
+ }
265
324
  }
266
325
 
267
- // 2. Remove IDE instance from InstanceManager
268
- const instanceKey = `ide:${ideType}`;
269
- const ideInstance = this.deps.instanceManager.getInstance(instanceKey) as any;
270
- if (ideInstance) {
271
- // Remove IDE and child Extension UUIDs from instanceIdMap
272
- if (ideInstance.getInstanceId) {
273
- this.deps.instanceIdMap.delete(ideInstance.getInstanceId());
326
+ // 2. Remove IDE instance(s) from InstanceManager
327
+ const keysToRemove: string[] = [];
328
+ for (const key of (this.deps.instanceManager as any).instances?.keys?.() || []) {
329
+ if (key === `ide:${ideType}` || (typeof key === 'string' && key.startsWith(`ide:${ideType}_`))) {
330
+ keysToRemove.push(key);
331
+ }
332
+ }
333
+ for (const instanceKey of keysToRemove) {
334
+ const ideInstance = this.deps.instanceManager.getInstance(instanceKey) as any;
335
+ if (ideInstance) {
336
+ // Remove IDE and child Extension UUIDs from instanceIdMap
337
+ if (ideInstance.getInstanceId) {
338
+ this.deps.instanceIdMap.delete(ideInstance.getInstanceId());
339
+ }
340
+ if (ideInstance.getExtensionInstances) {
341
+ for (const ext of ideInstance.getExtensionInstances()) {
342
+ if (ext.getInstanceId) this.deps.instanceIdMap.delete(ext.getInstanceId());
343
+ }
344
+ }
345
+ this.deps.instanceManager.removeInstance(instanceKey);
346
+ LOG.info('StopIDE', `Instance removed: ${instanceKey}`);
274
347
  }
275
- if (ideInstance.getExtensionInstances) {
276
- for (const ext of ideInstance.getExtensionInstances()) {
277
- if (ext.getInstanceId) this.deps.instanceIdMap.delete(ext.getInstanceId());
348
+ }
349
+ // Fallback: single instance key
350
+ if (keysToRemove.length === 0) {
351
+ const instanceKey = `ide:${ideType}`;
352
+ const ideInstance = this.deps.instanceManager.getInstance(instanceKey) as any;
353
+ if (ideInstance) {
354
+ if (ideInstance.getInstanceId) {
355
+ this.deps.instanceIdMap.delete(ideInstance.getInstanceId());
356
+ }
357
+ if (ideInstance.getExtensionInstances) {
358
+ for (const ext of ideInstance.getExtensionInstances()) {
359
+ if (ext.getInstanceId) this.deps.instanceIdMap.delete(ext.getInstanceId());
360
+ }
361
+ }
362
+ this.deps.instanceManager.removeInstance(instanceKey);
363
+ LOG.info('StopIDE', `Instance removed: ${instanceKey}`);
364
+ }
365
+ }
366
+
367
+ // 3. Kill OS process if requested
368
+ if (killProcess) {
369
+ const running = isIdeRunning(ideType);
370
+ if (running) {
371
+ LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
372
+ const killed = await killIdeProcess(ideType);
373
+ if (killed) {
374
+ LOG.info('StopIDE', `✅ Process killed: ${ideType}`);
375
+ } else {
376
+ LOG.warn('StopIDE', `⚠ Could not kill process: ${ideType} (may need manual intervention)`);
278
377
  }
378
+ } else {
379
+ LOG.info('StopIDE', `Process not running: ${ideType}`);
279
380
  }
280
- this.deps.instanceManager.removeInstance(instanceKey);
281
- LOG.info('StopIDE', `Instance removed: ${instanceKey}`);
282
381
  }
283
382
 
284
- // 3. Notify consumer for status update
383
+ // 4. Notify consumer for status update
285
384
  this.deps.onStatusChange?.();
286
- LOG.info('StopIDE', `IDE stopped: ${ideType}`);
385
+ LOG.info('StopIDE', `IDE stopped: ${ideType} (processKill=${killProcess})`);
287
386
  }
288
387
  }
@@ -21,6 +21,8 @@ import * as os from 'os';
21
21
  import type { ProviderLoader } from '../providers/provider-loader.js';
22
22
  import type { ChildProcess } from 'child_process';
23
23
  import type { DaemonCdpManager } from '../cdp/manager.js';
24
+ import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
25
+ import type { DaemonCliManager } from '../commands/cli-manager.js';
24
26
  import { generateTemplate as genScaffoldTemplate, generateFiles as genScaffoldFiles } from './scaffold-template.js';
25
27
  import { VersionArchive, detectAllVersions } from '../providers/version-archive.js';
26
28
  import { LOG } from '../logging/logger.js';
@@ -31,6 +33,8 @@ export class DevServer {
31
33
  private server: http.Server | null = null;
32
34
  private providerLoader: ProviderLoader;
33
35
  private cdpManagers: Map<string, DaemonCdpManager>;
36
+ private instanceManager: ProviderInstanceManager | null;
37
+ private cliManager: DaemonCliManager | null;
34
38
  private logFn: (msg: string) => void;
35
39
  private sseClients: http.ServerResponse[] = [];
36
40
  private watchScriptPath: string | null = null;
@@ -42,13 +46,20 @@ export class DevServer {
42
46
  private autoImplSSEClients: http.ServerResponse[] = [];
43
47
  private autoImplStatus: { running: boolean; type: string | null; progress: any[] } = { running: false, type: null, progress: [] };
44
48
 
49
+ // CLI debug SSE
50
+ private cliSSEClients: http.ServerResponse[] = [];
51
+
45
52
  constructor(options: {
46
53
  providerLoader: ProviderLoader;
47
54
  cdpManagers: Map<string, DaemonCdpManager>;
55
+ instanceManager?: ProviderInstanceManager;
56
+ cliManager?: DaemonCliManager;
48
57
  logFn?: (msg: string) => void;
49
58
  }) {
50
59
  this.providerLoader = options.providerLoader;
51
60
  this.cdpManagers = options.cdpManagers;
61
+ this.instanceManager = options.instanceManager || null;
62
+ this.cliManager = options.cliManager || null;
52
63
  this.logFn = options.logFn || LOG.forComponent('DevServer').asLogFn();
53
64
  }
54
65
 
@@ -81,6 +92,15 @@ export class DevServer {
81
92
  { method: 'POST', pattern: '/api/watch/stop', handler: (q, s) => this.handleWatchStop(q, s) },
82
93
  { method: 'GET', pattern: '/api/watch/events', handler: (q, s) => this.handleSSE(q, s) },
83
94
  { method: 'POST', pattern: '/api/scaffold', handler: (q, s) => this.handleScaffold(q, s) },
95
+ // CLI Debug routes
96
+ { method: 'GET', pattern: '/api/cli/status', handler: (q, s) => this.handleCliStatus(q, s) },
97
+ { method: 'POST', pattern: '/api/cli/launch', handler: (q, s) => this.handleCliLaunch(q, s) },
98
+ { method: 'POST', pattern: '/api/cli/send', handler: (q, s) => this.handleCliSend(q, s) },
99
+ { method: 'POST', pattern: '/api/cli/resolve', handler: (q, s) => this.handleCliResolve(q, s) },
100
+ { method: 'POST', pattern: '/api/cli/raw', handler: (q, s) => this.handleCliRaw(q, s) },
101
+ { method: 'POST', pattern: '/api/cli/stop', handler: (q, s) => this.handleCliStop(q, s) },
102
+ { method: 'GET', pattern: '/api/cli/events', handler: (q, s) => this.handleCliSSE(q, s) },
103
+ { method: 'GET', pattern: /^\/api\/cli\/debug\/([^/]+)$/, handler: (q, s, p) => this.handleCliDebug(p![0], q, s) },
84
104
  // Dynamic routes (provider :type param)
85
105
  { method: 'POST', pattern: /^\/api\/providers\/([^/]+)\/script$/, handler: (q, s, p) => this.handleRunScript(p![0], q, s) },
86
106
  { method: 'GET', pattern: /^\/api\/providers\/([^/]+)\/files$/, handler: (q, s, p) => this.handleListFiles(p![0], q, s) },
@@ -2375,4 +2395,269 @@ export class DevServer {
2375
2395
  });
2376
2396
  });
2377
2397
  }
2398
+
2399
+ // ─── CLI Debug Handlers ──────────────────────────────
2400
+
2401
+ /** GET /api/cli/status — list all running CLI/ACP instances with state */
2402
+ private async handleCliStatus(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
2403
+ if (!this.instanceManager) {
2404
+ this.json(res, 503, { error: 'InstanceManager not available (daemon not fully initialized)' });
2405
+ return;
2406
+ }
2407
+ const allStates = this.instanceManager.collectAllStates();
2408
+ const cliStates = allStates.filter(s => s.category === 'cli' || s.category === 'acp');
2409
+ const result = cliStates.map(s => ({
2410
+ instanceId: s.instanceId,
2411
+ type: s.type,
2412
+ name: s.name,
2413
+ category: s.category,
2414
+ status: s.status,
2415
+ mode: s.mode,
2416
+ workspace: s.workspace,
2417
+ messageCount: s.activeChat?.messages?.length || 0,
2418
+ lastMessage: s.activeChat?.messages?.slice(-1)[0] || null,
2419
+ activeModal: s.activeChat?.activeModal || null,
2420
+ pendingEvents: s.pendingEvents || [],
2421
+ currentModel: s.currentModel,
2422
+ settings: s.settings,
2423
+ }));
2424
+ this.json(res, 200, { instances: result, count: result.length });
2425
+ }
2426
+
2427
+ /** POST /api/cli/launch — launch a CLI agent { type, workingDir?, args? } */
2428
+ private async handleCliLaunch(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
2429
+ if (!this.cliManager) {
2430
+ this.json(res, 503, { error: 'CliManager not available' });
2431
+ return;
2432
+ }
2433
+ const body = await this.readBody(req);
2434
+ const { type, workingDir, args } = body;
2435
+ if (!type) {
2436
+ this.json(res, 400, { error: 'type required (e.g. claude-cli, gemini-cli)' });
2437
+ return;
2438
+ }
2439
+ try {
2440
+ await this.cliManager.startSession(type, workingDir || process.cwd(), args || []);
2441
+ this.json(res, 200, { launched: true, type, workspace: workingDir || process.cwd() });
2442
+ } catch (e: any) {
2443
+ this.json(res, 500, { error: `Launch failed: ${e.message}` });
2444
+ }
2445
+ }
2446
+
2447
+ /** POST /api/cli/send — send message to a running CLI { type, text } */
2448
+ private async handleCliSend(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
2449
+ if (!this.instanceManager) {
2450
+ this.json(res, 503, { error: 'InstanceManager not available' });
2451
+ return;
2452
+ }
2453
+ const body = await this.readBody(req);
2454
+ const { type, text, instanceId } = body;
2455
+ if (!text) {
2456
+ this.json(res, 400, { error: 'text required' });
2457
+ return;
2458
+ }
2459
+
2460
+ // Find the CLI instance by type or instanceId
2461
+ const allStates = this.instanceManager.collectAllStates();
2462
+ const target = allStates.find(s =>
2463
+ (s.category === 'cli' || s.category === 'acp') &&
2464
+ (instanceId ? s.instanceId === instanceId : s.type === type)
2465
+ );
2466
+ if (!target) {
2467
+ this.json(res, 404, { error: `No running instance found for: ${type || instanceId}` });
2468
+ return;
2469
+ }
2470
+
2471
+ try {
2472
+ this.instanceManager.sendEvent(target.instanceId, 'send_message', { text });
2473
+ this.json(res, 200, { sent: true, type: target.type, instanceId: target.instanceId });
2474
+ } catch (e: any) {
2475
+ this.json(res, 500, { error: `Send failed: ${e.message}` });
2476
+ }
2477
+ }
2478
+
2479
+ /** POST /api/cli/stop — stop a running CLI { type } */
2480
+ private async handleCliStop(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
2481
+ if (!this.instanceManager) {
2482
+ this.json(res, 503, { error: 'InstanceManager not available' });
2483
+ return;
2484
+ }
2485
+ const body = await this.readBody(req);
2486
+ const { type, instanceId } = body;
2487
+
2488
+ const allStates = this.instanceManager.collectAllStates();
2489
+ const target = allStates.find(s =>
2490
+ (s.category === 'cli' || s.category === 'acp') &&
2491
+ (instanceId ? s.instanceId === instanceId : s.type === type)
2492
+ );
2493
+ if (!target) {
2494
+ this.json(res, 404, { error: `No running instance found for: ${type || instanceId}` });
2495
+ return;
2496
+ }
2497
+
2498
+ try {
2499
+ this.instanceManager.removeInstance(target.instanceId);
2500
+ this.json(res, 200, { stopped: true, type: target.type, instanceId: target.instanceId });
2501
+ } catch (e: any) {
2502
+ this.json(res, 500, { error: `Stop failed: ${e.message}` });
2503
+ }
2504
+ }
2505
+
2506
+ /** GET /api/cli/events — SSE stream of CLI status events */
2507
+ private handleCliSSE(_req: http.IncomingMessage, res: http.ServerResponse): void {
2508
+ res.writeHead(200, {
2509
+ 'Content-Type': 'text/event-stream',
2510
+ 'Cache-Control': 'no-cache',
2511
+ 'Connection': 'keep-alive',
2512
+ 'Access-Control-Allow-Origin': '*',
2513
+ });
2514
+ res.write('data: {"type":"connected"}\n\n');
2515
+ this.cliSSEClients.push(res);
2516
+
2517
+ // Register event listener if first client + instanceManager available
2518
+ if (this.cliSSEClients.length === 1 && this.instanceManager) {
2519
+ this.instanceManager.onEvent((event) => {
2520
+ this.sendCliSSE(event);
2521
+ });
2522
+ }
2523
+
2524
+ // Send current state snapshot immediately
2525
+ if (this.instanceManager) {
2526
+ const allStates = this.instanceManager.collectAllStates();
2527
+ const cliStates = allStates.filter(s => s.category === 'cli' || s.category === 'acp');
2528
+ for (const s of cliStates) {
2529
+ this.sendCliSSE({ event: 'snapshot', providerType: s.type, status: s.status, instanceId: s.instanceId });
2530
+ }
2531
+ }
2532
+
2533
+ _req.on('close', () => {
2534
+ this.cliSSEClients = this.cliSSEClients.filter(c => c !== res);
2535
+ });
2536
+ }
2537
+
2538
+ private sendCliSSE(data: any): void {
2539
+ const msg = `data: ${JSON.stringify({ ...data, timestamp: Date.now() })}\n\n`;
2540
+ for (const client of this.cliSSEClients) {
2541
+ try { client.write(msg); } catch { /* ignore */ }
2542
+ }
2543
+ }
2544
+
2545
+ /** GET /api/cli/debug/:type — full internal debug state of a CLI adapter */
2546
+ private async handleCliDebug(type: string, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
2547
+ if (!this.instanceManager) {
2548
+ this.json(res, 503, { error: 'InstanceManager not available' });
2549
+ return;
2550
+ }
2551
+
2552
+ // Find the matching CLI instance
2553
+ const allStates = this.instanceManager.collectAllStates();
2554
+ const target = allStates.find(s =>
2555
+ (s.category === 'cli' || s.category === 'acp') && s.type === type
2556
+ );
2557
+ if (!target) {
2558
+ this.json(res, 404, { error: `No running instance for: ${type}`, available: allStates.filter(s => s.category === 'cli' || s.category === 'acp').map(s => s.type) });
2559
+ return;
2560
+ }
2561
+
2562
+ // Get the ProviderInstance and access adapter debug state
2563
+ const instance = this.instanceManager.getInstance(target.instanceId) as any;
2564
+ if (!instance) {
2565
+ this.json(res, 404, { error: `Instance not found: ${target.instanceId}` });
2566
+ return;
2567
+ }
2568
+
2569
+ try {
2570
+ const adapter = instance.getAdapter?.() || instance.adapter;
2571
+ if (adapter && typeof adapter.getDebugState === 'function') {
2572
+ const debugState = adapter.getDebugState();
2573
+ this.json(res, 200, {
2574
+ instanceId: target.instanceId,
2575
+ providerState: {
2576
+ type: target.type,
2577
+ name: target.name,
2578
+ status: target.status,
2579
+ mode: 'mode' in target ? target.mode : undefined,
2580
+ },
2581
+ debug: debugState,
2582
+ });
2583
+ } else {
2584
+ // Fallback: return what we can from the state
2585
+ this.json(res, 200, {
2586
+ instanceId: target.instanceId,
2587
+ providerState: target,
2588
+ debug: null,
2589
+ message: 'No debug state available (adapter.getDebugState not found)',
2590
+ });
2591
+ }
2592
+ } catch (e: any) {
2593
+ this.json(res, 500, { error: `Debug state failed: ${e.message}` });
2594
+ }
2595
+ }
2596
+
2597
+ /** POST /api/cli/resolve — resolve an approval modal { type, buttonIndex } */
2598
+ private async handleCliResolve(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
2599
+ const body = await this.readBody(req);
2600
+ const { type, buttonIndex, instanceId } = body;
2601
+ if (buttonIndex === undefined || buttonIndex === null) {
2602
+ this.json(res, 400, { error: 'buttonIndex required (0=Yes, 1=Always, 2=Deny)' });
2603
+ return;
2604
+ }
2605
+
2606
+ if (!this.cliManager) {
2607
+ this.json(res, 503, { error: 'CliManager not available' });
2608
+ return;
2609
+ }
2610
+ // Find adapter from cliManager.adapters map
2611
+ let adapter: any = null;
2612
+ for (const [, a] of this.cliManager.adapters) {
2613
+ if (type && (a as any).cliType === type) { adapter = a; break; }
2614
+ }
2615
+ if (!adapter) {
2616
+ this.json(res, 404, { error: `No running adapter for: ${type || instanceId}` });
2617
+ return;
2618
+ }
2619
+ try {
2620
+ if (typeof adapter.resolveModal === 'function') {
2621
+ adapter.resolveModal(buttonIndex);
2622
+ this.json(res, 200, { resolved: true, type, buttonIndex });
2623
+ } else {
2624
+ this.json(res, 400, { error: 'resolveModal not available on this adapter' });
2625
+ }
2626
+ } catch (e: any) {
2627
+ this.json(res, 500, { error: `Resolve failed: ${e.message}` });
2628
+ }
2629
+ }
2630
+
2631
+ /** POST /api/cli/raw — send raw keystrokes to PTY { type, keys } */
2632
+ private async handleCliRaw(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
2633
+ const body = await this.readBody(req);
2634
+ const { type, keys, instanceId } = body;
2635
+ if (!keys) {
2636
+ this.json(res, 400, { error: 'keys required (raw string to send to PTY)' });
2637
+ return;
2638
+ }
2639
+
2640
+ if (!this.cliManager) {
2641
+ this.json(res, 503, { error: 'CliManager not available' });
2642
+ return;
2643
+ }
2644
+ let adapter: any = null;
2645
+ for (const [, a] of this.cliManager.adapters) {
2646
+ if (type && (a as any).cliType === type) { adapter = a; break; }
2647
+ }
2648
+ if (!adapter) {
2649
+ this.json(res, 404, { error: `No running adapter for: ${type || instanceId}` });
2650
+ return;
2651
+ }
2652
+ try {
2653
+ if (typeof adapter.writeRaw === 'function') {
2654
+ adapter.writeRaw(keys);
2655
+ this.json(res, 200, { sent: true, type, keysLength: keys.length });
2656
+ } else {
2657
+ this.json(res, 400, { error: 'writeRaw not available on this adapter' });
2658
+ }
2659
+ } catch (e: any) {
2660
+ this.json(res, 500, { error: `Raw send failed: ${e.message}` });
2661
+ }
2662
+ }
2378
2663
  }
@@ -5,7 +5,8 @@
5
5
  * Actual implementation extracted from launcher and placed in this package.
6
6
  */
7
7
 
8
- import type { DaemonStatus, CommandResult, DaemonEvent, IdeEntry, CliEntry, AcpEntry } from './types.js';
8
+ import type { StatusResponse, CommandResult, DaemonEvent } from './types.js';
9
+ import type { ManagedIdeEntry, ManagedCliEntry, ManagedAcpEntry } from './shared-types.js';
9
10
 
10
11
  export interface DaemonCoreOptions {
11
12
  /** Data directory for config, logs */
@@ -28,10 +29,10 @@ export interface IDaemonCore {
28
29
  stop(): Promise<void>;
29
30
 
30
31
  /** Get current daemon status snapshot */
31
- getStatus(): DaemonStatus;
32
+ getStatus(): StatusResponse;
32
33
 
33
34
  /** Subscribe to status changes. Returns unsubscribe function. */
34
- onStatusChange(callback: (status: DaemonStatus) => void): () => void;
35
+ onStatusChange(callback: (status: StatusResponse) => void): () => void;
35
36
 
36
37
  /** Subscribe to all daemon events. Returns unsubscribe function. */
37
38
  onEvent(callback: (event: DaemonEvent) => void): () => void;
@@ -40,11 +41,11 @@ export interface IDaemonCore {
40
41
  executeCommand(type: string, payload: any, target?: string): Promise<CommandResult>;
41
42
 
42
43
  /** Get currently detected/managed IDEs */
43
- getManagedIdes(): IdeEntry[];
44
+ getManagedIdes(): ManagedIdeEntry[];
44
45
 
45
46
  /** Get currently detected/managed CLIs */
46
- getManagedClis(): CliEntry[];
47
+ getManagedClis(): ManagedCliEntry[];
47
48
 
48
49
  /** Get currently detected/managed ACP agents */
49
- getManagedAcps(): AcpEntry[];
50
+ getManagedAcps(): ManagedAcpEntry[];
50
51
  }
@@ -89,13 +89,13 @@ function getIdeVersion(cliCommand: string): string | null {
89
89
  }
90
90
 
91
91
  function checkPathExists(paths: string[]): string | null {
92
+ const home = homedir();
92
93
  for (const p of paths) {
93
94
  if (p.includes('*')) {
94
- const home = homedir();
95
- const starIdx = p.indexOf('*');
96
- const suffix = p.substring(starIdx + 1);
97
- const homeNormalized = home.replace(/\//g, '\\\\');
98
- const resolved = homeNormalized + suffix;
95
+ // Wildcard expansion: replace `*` with the current user's home folder name
96
+ // e.g. "C:\Users\*\AppData\..." → "C:\Users\vilmi\AppData\..."
97
+ const username = home.split(/[\\/]/).pop() || '';
98
+ const resolved = p.replace('*', username);
99
99
  if (existsSync(resolved)) return resolved;
100
100
  } else {
101
101
  if (existsSync(p)) return p;
package/src/index.ts CHANGED
@@ -7,10 +7,6 @@
7
7
 
8
8
  // ── Types ──
9
9
  export type {
10
- DaemonStatus,
11
- IdeEntry,
12
- CliEntry,
13
- AcpEntry,
14
10
  ChatMessage,
15
11
  ExtensionInfo,
16
12
  CommandResult as CoreCommandResult,
@@ -20,10 +16,32 @@ export type {
20
16
  SystemInfo,
21
17
  DetectedIde,
22
18
  ProviderInfo,
23
- AgentStreamEntry,
24
19
  AgentEntry,
25
20
  } from './types.js';
26
21
 
22
+ // ── Shared Types (cross-package) ──
23
+ export type {
24
+ ManagedIdeEntry,
25
+ ManagedCliEntry,
26
+ ManagedAcpEntry,
27
+ ManagedAgentStream,
28
+ AvailableProviderInfo,
29
+ AcpConfigOption,
30
+ AcpMode,
31
+ StatusReportPayload,
32
+ MachineInfo,
33
+ DetectedIdeInfo,
34
+ WorkspaceEntry,
35
+ WorkspaceActivity,
36
+ ProviderStatus,
37
+ ProviderErrorReason,
38
+ ActiveChatData,
39
+ IdeProviderState,
40
+ CliProviderState,
41
+ AcpProviderState,
42
+ ExtensionProviderState,
43
+ } from './shared-types.js';
44
+
27
45
  // ── Core Interface ──
28
46
  export type { IDaemonCore, DaemonCoreOptions } from './daemon-core.js';
29
47
 
@@ -31,7 +49,6 @@ export type { IDaemonCore, DaemonCoreOptions } from './daemon-core.js';
31
49
  export { loadConfig, saveConfig, resetConfig, isSetupComplete, addCliHistory, markSetupComplete, updateConfig } from './config/config.js';
32
50
  export { getWorkspaceState } from './config/workspaces.js';
33
51
  export { getWorkspaceActivity } from './config/workspace-activity.js';
34
- export type { WorkspaceEntry } from './config/workspaces.js';
35
52
 
36
53
  // ── Detection ──
37
54
  export { detectIDEs } from './detection/ide-detector.js';
@@ -68,7 +85,7 @@ export { logCommand, getRecentCommands } from './logging/command-log.js';
68
85
  export { DaemonCliManager } from './commands/cli-manager.js';
69
86
 
70
87
  // ── Launch ──
71
- export { launchWithCdp, getAvailableIdeIds } from './launch.js';
88
+ export { launchWithCdp, getAvailableIdeIds, killIdeProcess, isIdeRunning } from './launch.js';
72
89
 
73
90
  // ── IPC ──
74
91
  export { DEFAULT_DAEMON_PORT, DAEMON_WS_PATH } from './ipc-protocol.js';