@adhdev/daemon-core 0.9.48 → 0.9.50

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.9.48",
3
+ "version": "0.9.50",
4
4
  "description": "ADHDev local session host core \u2014 session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.48",
3
+ "version": "0.9.50",
4
4
  "description": "ADHDev daemon core \u2014 CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -41,6 +41,7 @@ export interface CliAdapter {
41
41
  sendMessage(text: string): Promise<void>;
42
42
  getStatus(): CliAdapterStatus;
43
43
  getScriptParsedStatus?(): unknown;
44
+ getDebugSnapshot?(): unknown;
44
45
  invokeScript?(scriptName: string, args?: Record<string, unknown>): Promise<unknown>;
45
46
  getPartialResponse(): string;
46
47
  saveAndStop?(): Promise<void>;
@@ -2545,6 +2545,88 @@ export class ProviderCliAdapter implements CliAdapter {
2545
2545
  return this.responseBuffer;
2546
2546
  }
2547
2547
 
2548
+ getDebugSnapshot(): Record<string, unknown> {
2549
+ const screenText = this.readTerminalScreenText();
2550
+ const parsedResult = this.parsedStatusCache?.result && typeof this.parsedStatusCache.result === 'object'
2551
+ ? this.parsedStatusCache.result as Record<string, any>
2552
+ : null;
2553
+ return {
2554
+ cliType: this.cliType,
2555
+ cliName: this.cliName,
2556
+ workingDir: this.workingDir,
2557
+ currentStatus: this.currentStatus,
2558
+ ready: this.ready,
2559
+ isWaitingForResponse: this.isWaitingForResponse,
2560
+ activeModal: this.activeModal,
2561
+ parseErrorMessage: this.parseErrorMessage,
2562
+ messageCounts: {
2563
+ committed: this.committedMessages.length,
2564
+ structured: this.structuredMessages.length,
2565
+ visible: this.messages.length,
2566
+ parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
2567
+ },
2568
+ buffers: {
2569
+ accumulatedLength: this.accumulatedBuffer.length,
2570
+ accumulatedRawLength: this.accumulatedRawBuffer.length,
2571
+ recentOutputLength: this.recentOutputBuffer.length,
2572
+ responseLength: this.responseBuffer.length,
2573
+ startupLength: this.startupBuffer.length,
2574
+ accumulatedTail: this.accumulatedBuffer.slice(-24_000),
2575
+ accumulatedRawTail: this.accumulatedRawBuffer.slice(-24_000),
2576
+ recentOutputTail: this.recentOutputBuffer.slice(-12_000),
2577
+ responseTail: this.responseBuffer.slice(-12_000),
2578
+ },
2579
+ terminal: {
2580
+ screenText,
2581
+ lastScreenSnapshot: this.lastScreenSnapshot,
2582
+ lastScreenText: this.lastScreenText,
2583
+ lastOutputAt: this.lastOutputAt,
2584
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
2585
+ lastScreenChangeAt: this.lastScreenChangeAt,
2586
+ lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
2587
+ },
2588
+ parser: {
2589
+ scriptNames: listCliScriptNames(this.cliScripts),
2590
+ traceSessionId: this.traceSessionId,
2591
+ traceSeq: this.traceSeq,
2592
+ currentTurnScope: this.currentTurnScope,
2593
+ parsedStatusCache: parsedResult
2594
+ ? {
2595
+ id: parsedResult.id,
2596
+ status: parsedResult.status,
2597
+ title: parsedResult.title,
2598
+ providerSessionId: parsedResult.providerSessionId,
2599
+ transcriptAuthority: parsedResult.transcriptAuthority,
2600
+ coverage: parsedResult.coverage,
2601
+ messageCount: Array.isArray(parsedResult.messages) ? parsedResult.messages.length : undefined,
2602
+ activeModal: parsedResult.activeModal,
2603
+ }
2604
+ : null,
2605
+ pendingScriptStatus: this.pendingScriptStatus,
2606
+ pendingScriptStatusSince: this.pendingScriptStatusSince,
2607
+ },
2608
+ runtimeMetadata: this.getRuntimeMetadata(),
2609
+ statusHistory: this.statusHistory.slice(-80),
2610
+ traceEntries: this.traceEntries.slice(-120),
2611
+ timing: {
2612
+ spawnAt: this.spawnAt,
2613
+ startupFirstOutputAt: this.startupFirstOutputAt,
2614
+ submitPendingUntil: this.submitPendingUntil,
2615
+ responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
2616
+ responseEpoch: this.responseEpoch,
2617
+ resizeSuppressUntil: this.resizeSuppressUntil,
2618
+ lastApprovalResolvedAt: this.lastApprovalResolvedAt,
2619
+ committedMessagesChangedAt: this.committedMessagesChangedAt,
2620
+ },
2621
+ finish: {
2622
+ idleFinishCandidate: this.idleFinishCandidate,
2623
+ finishRetryCount: this.finishRetryCount,
2624
+ submitRetryUsed: this.submitRetryUsed,
2625
+ submitRetryPromptSnippet: this.submitRetryPromptSnippet,
2626
+ },
2627
+ };
2628
+ }
2629
+
2548
2630
  getRuntimeMetadata(): PtyRuntimeMetadata | null {
2549
2631
  if (!this.ptyProcess || typeof this.ptyProcess.getMetadata !== 'function') return null;
2550
2632
  return this.ptyProcess.getMetadata();
@@ -277,7 +277,12 @@ export function findBinary(name: string): string {
277
277
  const isWin = os.platform() === 'win32';
278
278
  try {
279
279
  const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
280
- return execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0].trim();
280
+ return execSync(cmd, {
281
+ encoding: 'utf-8',
282
+ timeout: 5000,
283
+ stdio: ['pipe', 'pipe', 'pipe'],
284
+ ...(isWin ? { windowsHide: true } : {}),
285
+ }).trim().split('\n')[0].trim();
281
286
  } catch {
282
287
  return isWin ? `${trimmed}.cmd` : trimmed;
283
288
  }
@@ -10,8 +10,8 @@ import { assertProviderSupportsDeclaredInput, assertTextOnlyInput } from '../pro
10
10
  import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
11
11
  import type { ProviderInstance } from '../providers/provider-instance.js';
12
12
  import { readProviderChatHistory } from '../config/chat-history.js';
13
- import { LOG } from '../logging/logger.js';
14
- import { recordDebugTrace } from '../logging/debug-trace.js';
13
+ import { LOG, getRecentLogs } from '../logging/logger.js';
14
+ import { getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
15
15
  import { buildChatMessageSignature } from '../chat/chat-signatures.js';
16
16
  import type { ChatMessage } from '../types.js';
17
17
  import type { ReadChatCursor, ReadChatSyncMode, SessionTransport } from '../shared-types.js';
@@ -489,6 +489,275 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
489
489
  };
490
490
  }
491
491
 
492
+
493
+ interface DebugSanitizeOptions {
494
+ maxDepth?: number;
495
+ maxArrayLength?: number;
496
+ maxObjectKeys?: number;
497
+ maxStringLength?: number;
498
+ }
499
+
500
+ const DEFAULT_DEBUG_SANITIZE_OPTIONS: Required<DebugSanitizeOptions> = {
501
+ maxDepth: 8,
502
+ maxArrayLength: 80,
503
+ maxObjectKeys: 120,
504
+ maxStringLength: 16_000,
505
+ };
506
+
507
+ const SECRET_KEY_PATTERN = /(?:token|secret|password|passwd|authorization|cookie|api[_-]?key|access[_-]?key|refresh[_-]?token|client[_-]?secret|private[_-]?key)/i;
508
+
509
+ function truncateDebugString(value: string, maxLength: number): string {
510
+ if (value.length <= maxLength) return value;
511
+ return `${value.slice(0, maxLength)}…[truncated ${value.length - maxLength} chars]`;
512
+ }
513
+
514
+ function redactDebugSecrets(value: string): string {
515
+ return value
516
+ .replace(/(Authorization\s*:\s*Bearer\s+)[^\s'"`]+/gi, '$1[REDACTED:bearer]')
517
+ .replace(/(Bearer\s+)[A-Za-z0-9._~+\/-]{16,}=*/gi, '$1[REDACTED:bearer]')
518
+ .replace(/\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{20,}\b/g, '[REDACTED:github-token]')
519
+ .replace(/\bsk-[A-Za-z0-9_-]{16,}\b/g, '[REDACTED:api-key]')
520
+ .replace(/\bxox[baprs]-[A-Za-z0-9-]{12,}\b/g, '[REDACTED:slack-token]')
521
+ .replace(/\b(?:adk|adm)_[A-Za-z0-9_-]{16,}\b/g, '[REDACTED:adhdev-token]')
522
+ .replace(/((?:api[_-]?key|token|secret|password|passwd|client[_-]?secret)\s*[:=]\s*)[^\s,'"`}&]+/gi, '$1[REDACTED:secret]')
523
+ .replace(/([?&](?:api[_-]?key|token|secret|password|client_secret)=)[^&#\s]+/gi, '$1[REDACTED:secret]');
524
+ }
525
+
526
+ export function sanitizeDebugBundleValue(
527
+ value: unknown,
528
+ options: DebugSanitizeOptions = {},
529
+ depth = 0,
530
+ keyHint = '',
531
+ ): unknown {
532
+ const normalizedOptions = { ...DEFAULT_DEBUG_SANITIZE_OPTIONS, ...options };
533
+ if (value === null || value === undefined) return value;
534
+ if (typeof value === 'number' || typeof value === 'boolean') return value;
535
+ if (typeof value === 'bigint') return String(value);
536
+ if (typeof value === 'string') {
537
+ if (SECRET_KEY_PATTERN.test(keyHint) && value.trim()) return '[REDACTED:secret-field]';
538
+ return truncateDebugString(redactDebugSecrets(value), normalizedOptions.maxStringLength);
539
+ }
540
+ if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`;
541
+ if (typeof value !== 'object') return String(value);
542
+ if (depth >= normalizedOptions.maxDepth) return '[MaxDepth]';
543
+
544
+ if (Array.isArray(value)) {
545
+ const items = value
546
+ .slice(0, normalizedOptions.maxArrayLength)
547
+ .map((item) => sanitizeDebugBundleValue(item, normalizedOptions, depth + 1, keyHint));
548
+ if (value.length > normalizedOptions.maxArrayLength) {
549
+ items.push(`[truncated ${value.length - normalizedOptions.maxArrayLength} items]`);
550
+ }
551
+ return items;
552
+ }
553
+
554
+ const record = value as Record<string, unknown>;
555
+ const result: Record<string, unknown> = {};
556
+ const entries = Object.entries(record).slice(0, normalizedOptions.maxObjectKeys);
557
+ for (const [key, item] of entries) {
558
+ result[key] = sanitizeDebugBundleValue(item, normalizedOptions, depth + 1, key);
559
+ }
560
+ const remaining = Object.keys(record).length - entries.length;
561
+ if (remaining > 0) result.__truncatedKeys = remaining;
562
+ return result;
563
+ }
564
+
565
+ function summarizeProviderForDebug(provider: ProviderModule | undefined): Record<string, unknown> | null {
566
+ if (!provider) return null;
567
+ const scripts = provider.scripts && typeof provider.scripts === 'object'
568
+ ? Object.keys(provider.scripts)
569
+ : [];
570
+ const controls = Array.isArray((provider as any).controls)
571
+ ? (provider as any).controls.map((control: any) => ({
572
+ id: control?.id,
573
+ label: control?.label,
574
+ type: control?.type,
575
+ settingKey: control?.settingKey,
576
+ invokeScript: control?.invokeScript,
577
+ listScript: control?.listScript,
578
+ location: control?.location,
579
+ }))
580
+ : [];
581
+ return {
582
+ type: provider.type,
583
+ name: provider.name,
584
+ category: provider.category,
585
+ version: (provider as any).version,
586
+ canonicalHistory: provider.canonicalHistory,
587
+ historyBehavior: provider.historyBehavior,
588
+ webviewMatchText: provider.webviewMatchText,
589
+ scriptNames: scripts,
590
+ controls,
591
+ resume: provider.resume,
592
+ };
593
+ }
594
+
595
+ function summarizeSessionForDebug(session: any): Record<string, unknown> | null {
596
+ if (!session || typeof session !== 'object') return null;
597
+ return {
598
+ sessionId: session.sessionId,
599
+ instanceKey: session.instanceKey,
600
+ adapterKey: session.adapterKey,
601
+ providerType: session.providerType,
602
+ providerName: session.providerName,
603
+ transport: session.transport,
604
+ kind: session.kind,
605
+ cdpManagerKey: session.cdpManagerKey,
606
+ parentSessionId: session.parentSessionId,
607
+ providerSessionId: session.providerSessionId,
608
+ workspace: session.workspace,
609
+ title: session.title,
610
+ status: session.status,
611
+ mode: session.mode,
612
+ capabilities: session.capabilities,
613
+ };
614
+ }
615
+
616
+ function summarizeStateForDebug(state: any): Record<string, unknown> | null {
617
+ if (!state || typeof state !== 'object') return null;
618
+ const activeChat = state.activeChat && typeof state.activeChat === 'object' ? state.activeChat : null;
619
+ return {
620
+ type: state.type,
621
+ name: state.name,
622
+ category: state.category,
623
+ status: state.status,
624
+ instanceId: state.instanceId,
625
+ providerSessionId: state.providerSessionId,
626
+ title: state.title,
627
+ transport: state.transport,
628
+ mode: state.mode,
629
+ workspace: state.workspace,
630
+ runtime: state.runtime,
631
+ errorMessage: state.errorMessage,
632
+ errorReason: state.errorReason,
633
+ activeChat: activeChat ? {
634
+ status: activeChat.status,
635
+ title: activeChat.title,
636
+ messageCount: Array.isArray(activeChat.messages) ? activeChat.messages.length : undefined,
637
+ activeModal: activeChat.activeModal,
638
+ messagesTail: Array.isArray(activeChat.messages) ? activeChat.messages.slice(-10) : undefined,
639
+ } : null,
640
+ controlValues: state.controlValues,
641
+ summaryMetadata: state.summaryMetadata,
642
+ };
643
+ }
644
+
645
+ function buildDebugBundleText(bundle: Record<string, unknown>): string {
646
+ return [
647
+ '# ADHDev Chat Debug Bundle',
648
+ '',
649
+ '```json',
650
+ JSON.stringify(bundle, null, 2),
651
+ '```',
652
+ ].join('\n');
653
+ }
654
+
655
+ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Promise<CommandResult> {
656
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
657
+ if (!targetSessionId && !h.currentSession) {
658
+ return { success: false, error: 'No targetSessionId specified — cannot route command' };
659
+ }
660
+
661
+ const provider = h.getProvider(args?.agentType);
662
+ const transport = getTargetTransport(h, provider);
663
+ const providerType = provider?.type || getCurrentProviderType(h, args?.agentType || '');
664
+ const adapter = isCliLikeTransport(transport) ? getTargetedCliAdapter(h, args, provider?.type) : null;
665
+ const targetInstance = getTargetInstance(h, args);
666
+
667
+ let adapterStatus: unknown = null;
668
+ let parsedStatus: unknown = null;
669
+ let adapterDebugSnapshot: unknown = null;
670
+ let partialResponse = '';
671
+ if (adapter) {
672
+ try { adapterStatus = adapter.getStatus?.(); } catch (error: any) { adapterStatus = { error: error?.message || String(error) }; }
673
+ try { parsedStatus = typeof adapter.getScriptParsedStatus === 'function' ? parseMaybeJson(adapter.getScriptParsedStatus()) : null; } catch (error: any) { parsedStatus = { error: error?.message || String(error) }; }
674
+ try { adapterDebugSnapshot = typeof adapter.getDebugSnapshot === 'function' ? adapter.getDebugSnapshot() : null; } catch (error: any) { adapterDebugSnapshot = { error: error?.message || String(error) }; }
675
+ try { partialResponse = adapter.getPartialResponse?.() || ''; } catch { partialResponse = ''; }
676
+ }
677
+
678
+ let instanceState: unknown = null;
679
+ if (targetInstance?.getState) {
680
+ try { instanceState = summarizeStateForDebug(targetInstance.getState()); } catch (error: any) { instanceState = { error: error?.message || String(error) }; }
681
+ }
682
+
683
+ let readChat: unknown = null;
684
+ try {
685
+ const readResult = await handleReadChat(h, { ...args, tailLimit: Math.max(1, Math.min(40, Number(args?.tailLimit || 40))) });
686
+ readChat = readResult.success
687
+ ? {
688
+ success: true,
689
+ status: readResult.status,
690
+ title: readResult.title,
691
+ totalMessages: readResult.totalMessages,
692
+ returnedMessages: Array.isArray(readResult.messages) ? readResult.messages.length : undefined,
693
+ syncMode: readResult.syncMode,
694
+ replaceFrom: readResult.replaceFrom,
695
+ lastMessageSignature: readResult.lastMessageSignature,
696
+ providerSessionId: readResult.providerSessionId,
697
+ transcriptAuthority: readResult.transcriptAuthority,
698
+ coverage: readResult.coverage,
699
+ activeModal: readResult.activeModal,
700
+ messagesTail: Array.isArray(readResult.messages) ? readResult.messages.slice(-20) : [],
701
+ debugReadChat: readResult.debugReadChat,
702
+ }
703
+ : { success: false, error: readResult.error };
704
+ } catch (error: any) {
705
+ readChat = { success: false, error: error?.message || String(error) };
706
+ }
707
+
708
+ const cdp = h.getCdp();
709
+ const rawBundle: Record<string, unknown> = {
710
+ version: 1,
711
+ createdAt: new Date().toISOString(),
712
+ target: {
713
+ targetSessionId,
714
+ providerType,
715
+ transport,
716
+ routeManagerKey: h.currentManagerKey,
717
+ currentIdeType: h.currentIdeType,
718
+ },
719
+ session: summarizeSessionForDebug(h.currentSession),
720
+ provider: summarizeProviderForDebug(provider),
721
+ daemon: {
722
+ pid: process.pid,
723
+ platform: process.platform,
724
+ nodeVersion: process.version,
725
+ cwd: process.cwd(),
726
+ },
727
+ cdp: {
728
+ requested: !!cdp,
729
+ connected: !!cdp?.isConnected,
730
+ managerKey: getCurrentManagerKey(h),
731
+ },
732
+ instanceState,
733
+ cli: adapter ? {
734
+ cliType: adapter.cliType,
735
+ cliName: adapter.cliName,
736
+ workingDir: adapter.workingDir,
737
+ status: (adapterStatus as any)?.status,
738
+ activeModal: (adapterStatus as any)?.activeModal,
739
+ messageCount: Array.isArray((adapterStatus as any)?.messages) ? (adapterStatus as any).messages.length : undefined,
740
+ messagesTail: Array.isArray((adapterStatus as any)?.messages) ? (adapterStatus as any).messages.slice(-20) : undefined,
741
+ parsedStatus,
742
+ partialResponse,
743
+ ready: typeof adapter.isReady === 'function' ? adapter.isReady() : undefined,
744
+ processing: typeof adapter.isProcessing === 'function' ? adapter.isProcessing() : undefined,
745
+ debugSnapshot: adapterDebugSnapshot,
746
+ } : null,
747
+ readChat,
748
+ frontend: args?.frontendSnapshot && typeof args.frontendSnapshot === 'object' ? args.frontendSnapshot : null,
749
+ recentLogs: getRecentLogs(80, 'debug'),
750
+ recentDebugTrace: getRecentDebugTrace({ limit: 120 }),
751
+ };
752
+
753
+ const bundle = sanitizeDebugBundleValue(rawBundle) as Record<string, unknown>;
754
+ return {
755
+ success: true,
756
+ bundle,
757
+ text: buildDebugBundleText(bundle),
758
+ };
759
+ }
760
+
492
761
  function didProviderConfirmSend(result: any): boolean {
493
762
  const parsed = parseMaybeJson(result);
494
763
  if (parsed === true) return true;
@@ -52,7 +52,10 @@ function commandExists(command: string): boolean {
52
52
  return existsSync(expandExecutable(trimmed));
53
53
  }
54
54
  try {
55
- execFileSync(process.platform === 'win32' ? 'where' : 'which', [trimmed], { stdio: 'ignore' });
55
+ execFileSync(process.platform === 'win32' ? 'where' : 'which', [trimmed], {
56
+ stdio: 'ignore',
57
+ ...(process.platform === 'win32' ? { windowsHide: true } : {}),
58
+ });
56
59
  return true;
57
60
  } catch {
58
61
  return false;
@@ -380,6 +380,7 @@ export class DaemonCommandHandler implements CommandHelpers {
380
380
 
381
381
  const sessionScopedCommands = new Set([
382
382
  'read_chat',
383
+ 'get_chat_debug_bundle',
383
384
  'send_chat',
384
385
  'list_chats',
385
386
  'new_chat',
@@ -431,6 +432,7 @@ export class DaemonCommandHandler implements CommandHelpers {
431
432
  switch (cmd) {
432
433
  // ─── Chat commands (chat-commands.ts) ───────────────
433
434
  case 'read_chat': return Chat.handleReadChat(this, args);
435
+ case 'get_chat_debug_bundle': return Chat.handleGetChatDebugBundle(this, args);
434
436
  case 'chat_history': return Chat.handleChatHistory(this, args);
435
437
  case 'send_chat': return Chat.handleSendChat(this, args);
436
438
  case 'list_chats': return Chat.handleListChats(this, args);
@@ -36,7 +36,7 @@ import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../sessi
36
36
  import { buildSessionEntries } from '../status/builders.js';
37
37
  import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
38
38
  import { getSessionCompletionMarker } from '../status/snapshot.js';
39
- import { spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
39
+ import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
40
40
  import * as fs from 'fs';
41
41
 
42
42
  // ─── Types ───
@@ -525,6 +525,12 @@ export class DaemonCommandRouter {
525
525
  const wantsAll = args?.all === true;
526
526
  const offset = wantsAll ? 0 : Math.max(0, Number(args?.offset) || 0);
527
527
  const limit = wantsAll ? Number.MAX_SAFE_INTEGER : Math.max(1, Math.min(100, Number(args?.limit) || 30));
528
+ const requestedWorkspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
529
+ const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
530
+ ? args.providerSessionId.trim()
531
+ : typeof args?.activeProviderSessionId === 'string'
532
+ ? args.activeProviderSessionId.trim()
533
+ : '';
528
534
  const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
529
535
  const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
530
536
  canonicalHistory: providerMeta?.canonicalHistory,
@@ -546,6 +552,10 @@ export class DaemonCommandRouter {
546
552
  sessions: historySessions.map(session => {
547
553
  const saved = savedSessionById.get(session.historySessionId);
548
554
  const recent = recentSessionById.get(session.historySessionId);
555
+ const workspace = saved?.workspace
556
+ || recent?.workspace
557
+ || session.workspace
558
+ || (requestedWorkspace && requestedProviderSessionId === session.historySessionId ? requestedWorkspace : undefined);
549
559
  return {
550
560
  id: session.historySessionId,
551
561
  providerSessionId: session.historySessionId,
@@ -553,13 +563,13 @@ export class DaemonCommandRouter {
553
563
  providerName: saved?.providerName || recent?.providerName || providerType,
554
564
  kind: saved?.kind || recent?.kind || kind,
555
565
  title: saved?.title || recent?.title || session.sessionTitle || session.preview || providerType,
556
- workspace: saved?.workspace || recent?.workspace || session.workspace,
566
+ workspace,
557
567
  summaryMetadata: saved?.summaryMetadata || recent?.summaryMetadata,
558
568
  preview: session.preview,
559
569
  messageCount: session.messageCount,
560
570
  firstMessageAt: session.firstMessageAt,
561
571
  lastMessageAt: session.lastMessageAt,
562
- canResume: !!(saved?.workspace || recent?.workspace || session.workspace) && canResumeById,
572
+ canResume: !!workspace && canResumeById,
563
573
  historySource: session.source,
564
574
  sourcePath: session.sourcePath,
565
575
  sourceMtimeMs: session.sourceMtimeMs,
@@ -847,23 +857,22 @@ export class DaemonCommandRouter {
847
857
  case 'daemon_upgrade': {
848
858
  LOG.info('Upgrade', 'Remote upgrade requested from dashboard');
849
859
  try {
850
- const { execSync } = await import('child_process');
851
-
852
860
  // Detect package name for upgrade
853
861
  const isStandalone = this.deps.packageName === '@adhdev/daemon-standalone'
854
862
  || process.argv[1]?.includes('daemon-standalone');
855
863
  const pkgName = isStandalone ? '@adhdev/daemon-standalone' : 'adhdev';
864
+ const npmSurface = resolveCurrentGlobalInstallSurface({ packageName: pkgName });
856
865
 
857
866
  // Check latest version
858
- const latest = execSync(`npm view ${pkgName} version`, { encoding: 'utf-8', timeout: 10000 }).trim();
867
+ const latest = String(execNpmCommandSync(['view', pkgName, 'version'], { encoding: 'utf-8', timeout: 10000 }, npmSurface)).trim();
859
868
  LOG.info('Upgrade', `Latest ${pkgName}: v${latest}`);
860
869
  let currentInstalled: string | null = null;
861
870
  try {
862
- const currentJson = execSync(`npm ls -g ${pkgName} --depth=0 --json`, {
871
+ const currentJson = String(execNpmCommandSync(['ls', '-g', pkgName, '--depth=0', '--json'], {
863
872
  encoding: 'utf-8',
864
873
  timeout: 10000,
865
874
  stdio: ['pipe', 'pipe', 'pipe'],
866
- }).trim();
875
+ }, npmSurface)).trim();
867
876
  const parsed = JSON.parse(currentJson);
868
877
  currentInstalled = parsed?.dependencies?.[pkgName]?.version || null;
869
878
  } catch {
@@ -1,4 +1,4 @@
1
- import { execFileSync } from 'child_process';
1
+ import { execFileSync, type ExecFileSyncOptions } from 'child_process';
2
2
  import { spawn } from 'child_process';
3
3
  import * as fs from 'fs';
4
4
  import * as os from 'os';
@@ -20,16 +20,18 @@ export interface CurrentGlobalInstallSurface {
20
20
  npmArgsPrefix?: string[];
21
21
  packageRoot: string | null;
22
22
  installPrefix: string | null;
23
- execOptions?: { shell: boolean };
23
+ execOptions?: NpmExecOptions;
24
24
  }
25
25
 
26
26
  export interface PinnedGlobalInstallCommand {
27
27
  command: string;
28
28
  args: string[];
29
29
  surface: CurrentGlobalInstallSurface;
30
- execOptions: { shell: boolean };
30
+ execOptions: NpmExecOptions;
31
31
  }
32
32
 
33
+ export type NpmExecOptions = { shell: boolean; windowsHide?: boolean };
34
+
33
35
  function getUpgradeLogPath(): string {
34
36
  const home = os.homedir();
35
37
  const dir = path.join(home, '.adhdev');
@@ -49,29 +51,29 @@ function appendUpgradeLog(message: string): void {
49
51
  function resolveSiblingNpmInvocation(nodeExecutable: string, platform: NodeJS.Platform = process.platform): {
50
52
  executable: string;
51
53
  argsPrefix: string[];
52
- execOptions: { shell: boolean };
54
+ execOptions: NpmExecOptions;
53
55
  } {
54
56
  const binDir = path.dirname(nodeExecutable);
55
57
  if (platform === 'win32') {
56
58
  const npmCliPath = path.join(binDir, 'node_modules', 'npm', 'bin', 'npm-cli.js');
57
59
  if (fs.existsSync(npmCliPath)) {
58
- return { executable: nodeExecutable, argsPrefix: [npmCliPath], execOptions: { shell: false } };
60
+ return { executable: nodeExecutable, argsPrefix: [npmCliPath], execOptions: getNpmExecOptions(platform) };
59
61
  }
60
62
  for (const candidate of ['npm.exe', 'npm']) {
61
63
  const candidatePath = path.join(binDir, candidate);
62
64
  if (fs.existsSync(candidatePath)) {
63
- return { executable: candidatePath, argsPrefix: [], execOptions: { shell: false } };
65
+ return { executable: candidatePath, argsPrefix: [], execOptions: getNpmExecOptions(platform) };
64
66
  }
65
67
  }
66
- return { executable: nodeExecutable, argsPrefix: [npmCliPath], execOptions: { shell: false } };
68
+ return { executable: nodeExecutable, argsPrefix: [npmCliPath], execOptions: getNpmExecOptions(platform) };
67
69
  }
68
70
  for (const candidate of ['npm']) {
69
71
  const candidatePath = path.join(binDir, candidate);
70
72
  if (fs.existsSync(candidatePath)) {
71
- return { executable: candidatePath, argsPrefix: [], execOptions: { shell: false } };
73
+ return { executable: candidatePath, argsPrefix: [], execOptions: getNpmExecOptions(platform) };
72
74
  }
73
75
  }
74
- return { executable: 'npm', argsPrefix: [], execOptions: { shell: false } };
76
+ return { executable: 'npm', argsPrefix: [], execOptions: getNpmExecOptions(platform) };
75
77
  }
76
78
 
77
79
  function findCurrentPackageRoot(currentCliPath: string | undefined, packageName: string): string | null {
@@ -167,14 +169,34 @@ export function buildPinnedGlobalInstallCommand(options: {
167
169
  };
168
170
  }
169
171
 
170
- function getNpmExecOptions(_platform: NodeJS.Platform = process.platform): { shell: boolean } {
172
+ export function getNpmExecOptions(platform: NodeJS.Platform = process.platform): NpmExecOptions {
173
+ if (platform === 'win32') {
174
+ return { shell: false, windowsHide: true };
175
+ }
171
176
  return { shell: false };
172
177
  }
173
178
 
179
+ export function execNpmCommandSync(
180
+ args: string[],
181
+ options: ExecFileSyncOptions = {},
182
+ surface?: Pick<CurrentGlobalInstallSurface, 'npmExecutable' | 'npmArgsPrefix' | 'execOptions'>,
183
+ ): Buffer | string {
184
+ const execOptions = surface?.execOptions || getNpmExecOptions();
185
+ return execFileSync(
186
+ surface?.npmExecutable || 'npm',
187
+ [...(surface?.npmArgsPrefix || []), ...args],
188
+ {
189
+ ...options,
190
+ ...execOptions,
191
+ ...(process.platform === 'win32' ? { windowsHide: true } : {}),
192
+ },
193
+ );
194
+ }
195
+
174
196
  function killPid(pid: number): boolean {
175
197
  try {
176
198
  if (process.platform === 'win32') {
177
- execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });
199
+ execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
178
200
  } else {
179
201
  process.kill(pid, 'SIGTERM');
180
202
  }
@@ -193,7 +215,7 @@ function getWindowsProcessCommandLine(pid: number): string | null {
193
215
  '-ExecutionPolicy', 'Bypass',
194
216
  '-Command',
195
217
  `(Get-CimInstance Win32_Process -Filter "${pidFilter}").CommandLine`,
196
- ], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
218
+ ], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
197
219
  if (psOut) return psOut;
198
220
  } catch {
199
221
  // fall through to wmic fallback
@@ -202,7 +224,7 @@ function getWindowsProcessCommandLine(pid: number): string | null {
202
224
  try {
203
225
  const wmicOut = execFileSync('wmic', [
204
226
  'process', 'where', pidFilter, 'get', 'CommandLine',
205
- ], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
227
+ ], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }).trim();
206
228
  if (wmicOut) return wmicOut;
207
229
  } catch {
208
230
  // noop
@@ -273,10 +295,10 @@ function removeDaemonPidFile(): void {
273
295
 
274
296
  function cleanupStaleGlobalInstallDirs(pkgName: string, surface: CurrentGlobalInstallSurface): void {
275
297
  const prefixArgs = surface.installPrefix ? ['--prefix', surface.installPrefix] : [];
276
- const npmRoot = execFileSync(surface.npmExecutable, [...(surface.npmArgsPrefix || []), 'root', '-g', ...prefixArgs], { encoding: 'utf8', ...surface.execOptions }).trim();
298
+ const npmRoot = String(execNpmCommandSync(['root', '-g', ...prefixArgs], { encoding: 'utf8' }, surface)).trim();
277
299
  if (!npmRoot) return;
278
300
  const npmPrefix = surface.installPrefix
279
- || execFileSync(surface.npmExecutable, [...(surface.npmArgsPrefix || []), 'prefix', '-g', ...prefixArgs], { encoding: 'utf8', ...surface.execOptions }).trim();
301
+ || String(execNpmCommandSync(['prefix', '-g', ...prefixArgs], { encoding: 'utf8' }, surface)).trim();
280
302
  const binDir = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
281
303
  const packageBaseName = pkgName.startsWith('@') ? pkgName.split('/')[1] : pkgName;
282
304
  const binNames = new Set<string>([packageBaseName]);