@adhdev/daemon-core 0.6.68 → 0.6.70

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.6.68",
3
+ "version": "0.6.70",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -34,6 +34,7 @@
34
34
  "@xterm/xterm": "^6.0.0",
35
35
  "chalk": "^5.3.0",
36
36
  "conf": "^13.0.0",
37
+ "node-pty": "^1.1.0",
37
38
  "ws": "^8.19.0"
38
39
  },
39
40
  "devDependencies": {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared helper for forwarding agent stream snapshots into the IDE instance.
3
+ *
4
+ * Both cloud and standalone daemons use the same InstanceManager wiring.
5
+ */
6
+
7
+ export function forwardAgentStreamsToIdeInstance(
8
+ instanceManager: { getInstance: (key: string) => any },
9
+ ideType: string,
10
+ streams: any[],
11
+ ): void {
12
+ const ideInstance = instanceManager.getInstance(`ide:${ideType}`) as
13
+ | { onEvent?: (event: string, payload: Record<string, unknown>) => void }
14
+ | undefined;
15
+
16
+ if (!ideInstance?.onEvent) return;
17
+
18
+ for (const stream of streams) {
19
+ ideInstance.onEvent('stream_update', {
20
+ extensionType: stream.agentType,
21
+ streams: [stream],
22
+ messages: stream.messages || [],
23
+ status: stream.status || 'idle',
24
+ activeModal: stream.activeModal || null,
25
+ model: stream.model || undefined,
26
+ mode: stream.mode || undefined,
27
+ });
28
+ }
29
+ }
@@ -654,6 +654,8 @@ export class ProviderCliAdapter implements CliAdapter {
654
654
  /Quick safety check/i,
655
655
  /Is this a project/i,
656
656
  /Enter to confirm/i,
657
+ /be able to read, edit, and execute/i,
658
+ /Security guide/i,
657
659
  ];
658
660
  if (dialogPatterns.some(p => p.test(this.startupBuffer))) {
659
661
  setTimeout(() => this.ptyProcess?.write('\r'), this.timeouts.dialogAccept);
@@ -721,17 +723,46 @@ export class ProviderCliAdapter implements CliAdapter {
721
723
  }
722
724
 
723
725
  private evaluateSettled(): void {
724
- if (this.submitPendingUntil > Date.now()) return;
725
- if (this.responseSettleIgnoreUntil > Date.now()) return;
726
+ const now = Date.now();
727
+ if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
728
+ const delayTime = Math.max(this.submitPendingUntil - now, this.responseSettleIgnoreUntil - now) + 50;
729
+ if (this.settleTimer) clearTimeout(this.settleTimer);
730
+ this.settleTimer = setTimeout(() => {
731
+ this.settleTimer = null;
732
+ this.settledBuffer = this.recentOutputBuffer;
733
+ this.evaluateSettled();
734
+ }, delayTime);
735
+ return;
736
+ }
726
737
  const tail = this.settledBuffer;
727
738
  const modal = this.runParseApproval(tail);
728
739
  const rawScriptStatus = this.runDetectStatus(tail);
729
- const scriptStatus = rawScriptStatus === 'waiting_approval' || modal ? 'waiting_approval' : rawScriptStatus;
740
+ // detectStatus is the sole authority for status. parseApproval only enriches modal info.
741
+ const scriptStatus = rawScriptStatus;
730
742
  if (!scriptStatus) return;
731
743
 
732
744
  const prevStatus = this.currentStatus;
733
745
 
734
746
  if (scriptStatus === 'waiting_approval') {
747
+ // Auto-accept startup safety dialogs (e.g., "Claude Code'll be able to read, edit, and execute")
748
+ const modalMessage = modal?.message || '';
749
+ const screenText = this.terminalScreen.getText() || this.accumulatedBuffer;
750
+ const autoAcceptPatterns = [
751
+ /be able to read, edit, and execute/i,
752
+ /Security guide/i,
753
+ /Enter to confirm/i,
754
+ /Quick safety check/i,
755
+ /Do you trust the files/i,
756
+ /Is this a project/i,
757
+ ];
758
+ if (autoAcceptPatterns.some(p => p.test(modalMessage) || p.test(screenText))) {
759
+ LOG.info('CLI', `[${this.cliType}] Auto-accepting startup dialog: ${modalMessage.slice(0, 80)}`);
760
+ setTimeout(() => this.ptyProcess?.write('\r'), 200);
761
+ this.lastApprovalResolvedAt = Date.now();
762
+ this.activeModal = null;
763
+ return;
764
+ }
765
+
735
766
  const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
736
767
  if (!inCooldown) {
737
768
  this.isWaitingForResponse = true;
@@ -7,6 +7,7 @@
7
7
  import { homedir } from 'os';
8
8
  import { join } from 'path';
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
10
+ import { randomUUID } from 'crypto';
10
11
  import { migrateWorkspacesFromRecent } from './workspaces.js';
11
12
  import type { WorkspaceEntry } from './workspaces.js';
12
13
  import type { WorkspaceActivityEntry } from './workspace-activity.js';
@@ -64,12 +65,15 @@ export interface ADHDevConfig {
64
65
  // Machine nickname (user-customizable label for this machine)
65
66
  machineNickname: string | null;
66
67
 
67
- // Stable machine ID (prevents duplicate daemon entries when OS hostname changes dynamically)
68
+ // Stable local machine ID shared by standalone and cloud daemon modes
68
69
  machineId?: string;
69
70
 
70
71
  // Machine secret for server auth (replaces connectionToken)
71
72
  machineSecret?: string | null;
72
73
 
74
+ // Account-scoped registered machine row ID (cloud-side)
75
+ registeredMachineId?: string;
76
+
73
77
  // CLI launch history
74
78
  cliHistory: CliHistoryEntry[];
75
79
 
@@ -123,12 +127,44 @@ const DEFAULT_CONFIG: ADHDevConfig = {
123
127
  machineNickname: null,
124
128
  machineId: undefined,
125
129
  machineSecret: null,
130
+ registeredMachineId: undefined,
126
131
  cliHistory: [],
127
132
  providerSettings: {},
128
133
  ideSettings: {},
129
134
  disableUpstream: false,
130
135
  };
131
136
 
137
+ const MACHINE_ID_PREFIX = 'mach_';
138
+
139
+ export function generateMachineId(): string {
140
+ return `${MACHINE_ID_PREFIX}${randomUUID().replace(/-/g, '')}`;
141
+ }
142
+
143
+ export function isStableMachineId(machineId?: string | null): boolean {
144
+ return typeof machineId === 'string' && machineId.startsWith(MACHINE_ID_PREFIX);
145
+ }
146
+
147
+ function ensureMachineId(config: ADHDevConfig): { config: ADHDevConfig; changed: boolean } {
148
+ if (isStableMachineId(config.machineId)) {
149
+ return { config, changed: false };
150
+ }
151
+
152
+ // TODO(2026-04-06): Remove this legacy bridge after cloud clients have had
153
+ // time to persist registeredMachineId from the upgraded setup/login flow.
154
+ const legacyRegisteredMachineId = (!config.registeredMachineId && config.machineSecret && config.machineId)
155
+ ? config.machineId
156
+ : config.registeredMachineId;
157
+
158
+ return {
159
+ config: {
160
+ ...config,
161
+ machineId: generateMachineId(),
162
+ registeredMachineId: legacyRegisteredMachineId,
163
+ },
164
+ changed: true,
165
+ };
166
+ }
167
+
132
168
  /**
133
169
  * Get the config directory path
134
170
  */
@@ -154,7 +190,11 @@ export function loadConfig(): ADHDevConfig {
154
190
  const configPath = getConfigPath();
155
191
 
156
192
  if (!existsSync(configPath)) {
157
- return { ...DEFAULT_CONFIG };
193
+ const initialized = ensureMachineId({ ...DEFAULT_CONFIG });
194
+ try {
195
+ saveConfig(initialized.config);
196
+ } catch { /* ignore */ }
197
+ return initialized.config;
158
198
  }
159
199
 
160
200
  try {
@@ -166,30 +206,25 @@ export function loadConfig(): ADHDevConfig {
166
206
  }
167
207
  delete (merged as any).activeWorkspaceId;
168
208
  const hadStoredWorkspaces = Array.isArray(parsed.workspaces) && parsed.workspaces.length > 0;
169
- migrateWorkspacesFromRecent(merged);
209
+ const ensured = ensureMachineId(merged);
210
+ const normalized = ensured.config as ADHDevConfig & { activeWorkspaceId?: string | null };
211
+ migrateWorkspacesFromRecent(normalized);
170
212
 
171
- let configChanged = false;
172
- if (!merged.machineId) {
173
- const os = require('os');
174
- const crypto = require('crypto');
175
- const safeHostname = os.hostname().replace(/[^a-zA-Z0-9]/g, '_');
176
- const machineHash = crypto.createHash('md5').update(os.hostname() + os.homedir()).digest('hex').slice(0, 8);
177
- merged.machineId = `${safeHostname}_${machineHash}`;
178
- configChanged = true;
179
- }
213
+ let configChanged = ensured.changed;
180
214
 
181
- if (!hadStoredWorkspaces && (merged.workspaces?.length || 0) > 0) {
215
+ if (!hadStoredWorkspaces && (normalized.workspaces?.length || 0) > 0) {
182
216
  configChanged = true;
183
217
  }
184
218
 
185
219
  if (configChanged) {
186
220
  try {
187
- saveConfig(merged);
221
+ saveConfig(normalized);
188
222
  } catch { /* ignore */ }
189
223
  }
190
- return merged;
224
+ return normalized;
191
225
  } catch {
192
- return { ...DEFAULT_CONFIG };
226
+ const initialized = ensureMachineId({ ...DEFAULT_CONFIG });
227
+ return initialized.config;
193
228
  }
194
229
  }
195
230
 
package/src/index.ts CHANGED
@@ -75,6 +75,8 @@ export type { CommandRouterDeps, CommandRouterResult } from './commands/router.j
75
75
  // ── Status ──
76
76
  export { DaemonStatusReporter } from './status/reporter.js';
77
77
  export { buildManagedIdes, buildManagedClis, buildManagedAcps, buildAllManagedEntries, findCdpManager, hasCdpManager, isCdpConnected } from './status/builders.js';
78
+ export { buildStatusSnapshot } from './status/snapshot.js';
79
+ export type { StatusSnapshotOptions, StatusSnapshot } from './status/snapshot.js';
78
80
 
79
81
  // ── Logger ──
80
82
  export { LOG, installGlobalInterceptor, setLogLevel, getLogLevel, getRecentLogs } from './logging/logger.js';
@@ -97,6 +99,7 @@ export { readChatHistory } from './config/chat-history.js';
97
99
  export { DaemonAgentStreamManager } from './agent-stream/index.js';
98
100
  export { AgentStreamPoller } from './agent-stream/index.js';
99
101
  export type { AgentStreamPollerDeps } from './agent-stream/index.js';
102
+ export { forwardAgentStreamsToIdeInstance } from './agent-stream/forward.js';
100
103
 
101
104
  // ── Providers ──
102
105
  export { ProviderLoader } from './providers/provider-loader.js';
@@ -122,4 +125,3 @@ export type { ExtensionInfo as InstallerExtensionInfo } from './installer.js';
122
125
  // ── Boot / Lifecycle ──
123
126
  export { initDaemonComponents, shutdownDaemonComponents } from './boot/daemon-lifecycle.js';
124
127
  export type { DaemonInitConfig, DaemonComponents } from './boot/daemon-lifecycle.js';
125
-
@@ -5,14 +5,9 @@
5
5
  * Each Instance manages its own status/transition. This module only assembles + transmits.
6
6
  */
7
7
 
8
- import * as os from 'os';
9
- import * as path from 'path';
10
- import { loadConfig } from '../config/config.js';
11
- import { getWorkspaceState } from '../config/workspaces.js';
12
- import { getHostMemorySnapshot } from '../system/host-memory.js';
13
- import { getWorkspaceActivity } from '../config/workspace-activity.js';
14
8
  import { LOG } from '../logging/logger.js';
15
9
  import { buildAllManagedEntries } from './builders.js';
10
+ import { buildStatusSnapshot } from './snapshot.js';
16
11
  import type {
17
12
  ProviderState,
18
13
  IdeProviderState,
@@ -27,9 +22,8 @@ export interface StatusReporterDeps {
27
22
  cdpManagers: Map<string, { isConnected: boolean }>;
28
23
  p2p: { isConnected: boolean; isAvailable: boolean; connectionState: string; connectedPeerCount: number; screenshotActive: boolean; sendStatus(data: any): void } | null;
29
24
  providerLoader: { resolve(type: string): any; getAll(): any[] };
30
- adapters: Map<string, { cliType: string; cliName: string; workingDir: string; getStatus(): any; getPartialResponse(): string }>;
31
25
  detectedIdes: any[];
32
- ideType: string;
26
+ instanceId: string;
33
27
  daemonVersion?: string;
34
28
  instanceManager: { collectAllStates(): ProviderState[]; collectStatesByCategory(cat: string): ProviderState[] };
35
29
  getScreenshotUsage?: () => { dailyUsedMinutes: number; dailyBudgetMinutes: number; budgetExhausted: boolean } | null;
@@ -163,50 +157,26 @@ export class DaemonStatusReporter {
163
157
  this.deps.cdpManagers as Map<string, any>,
164
158
  );
165
159
 
166
-
167
-
168
-
169
-
170
- const cfg = loadConfig();
171
- const wsState = getWorkspaceState(cfg);
172
- const memSnap = getHostMemorySnapshot();
173
-
174
160
  // ═══ Assemble payload (P2P — required data only) ═══
175
161
  const payload: Record<string, any> = {
176
- daemonMode: true,
177
- version: this.deps.daemonVersion || 'unknown',
178
- workspaces: wsState.workspaces,
179
- defaultWorkspaceId: wsState.defaultWorkspaceId,
180
- defaultWorkspacePath: wsState.defaultWorkspacePath,
181
- workspaceActivity: getWorkspaceActivity(cfg, 15),
182
- machine: {
183
- hostname: os.hostname(),
184
- platform: os.platform(),
185
- arch: os.arch(),
186
- cpus: os.cpus().length,
187
- totalMem: memSnap.totalMem,
188
- freeMem: memSnap.freeMem,
189
- availableMem: memSnap.availableMem,
190
- loadavg: os.loadavg(),
191
- uptime: os.uptime(),
192
- },
193
- managedIdes,
194
- managedClis,
195
- managedAcps,
196
- p2p: {
197
- available: p2p?.isAvailable || false,
198
- state: p2p?.connectionState || 'unavailable',
199
- peers: p2p?.connectedPeerCount || 0,
200
- screenshotActive: p2p?.screenshotActive || false,
201
- },
162
+ ...buildStatusSnapshot({
163
+ allStates,
164
+ cdpManagers: this.deps.cdpManagers as Map<string, unknown>,
165
+ providerLoader: this.deps.providerLoader,
166
+ detectedIdes: this.deps.detectedIdes || [],
167
+ instanceId: this.deps.instanceId,
168
+ version: this.deps.daemonVersion || 'unknown',
169
+ daemonMode: true,
170
+ timestamp: now,
171
+ p2p: {
172
+ available: p2p?.isAvailable || false,
173
+ state: p2p?.connectionState || 'unavailable',
174
+ peers: p2p?.connectedPeerCount || 0,
175
+ screenshotActive: p2p?.screenshotActive || false,
176
+ },
177
+ }),
202
178
  screenshotUsage: this.deps.getScreenshotUsage?.() || null,
203
179
  connectedExtensions: [],
204
- detectedIdes: this.deps.detectedIdes || [],
205
- availableProviders: this.deps.providerLoader.getAll().map((p: any) => ({
206
- type: p.type, icon: p.icon || '💻', displayName: p.displayName || p.type,
207
- category: p.category,
208
- })),
209
- timestamp: now,
210
180
  };
211
181
 
212
182
  // ═══ P2P transmit ═══
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Shared status snapshot builders.
3
+ *
4
+ * Used by:
5
+ * - DaemonStatusReporter (cloud)
6
+ * - daemon-standalone HTTP/WS status responses
7
+ */
8
+
9
+ import * as os from 'os';
10
+ import { loadConfig } from '../config/config.js';
11
+ import { getWorkspaceState } from '../config/workspaces.js';
12
+ import { getWorkspaceActivity } from '../config/workspace-activity.js';
13
+ import { getHostMemorySnapshot } from '../system/host-memory.js';
14
+ import { buildAllManagedEntries, isCdpConnected } from './builders.js';
15
+ import type { ProviderState } from '../providers/provider-instance.js';
16
+ import type {
17
+ AvailableProviderInfo,
18
+ DetectedIdeInfo,
19
+ StatusReportPayload,
20
+ } from '../shared-types.js';
21
+
22
+ export interface StatusSnapshotOptions {
23
+ allStates: ProviderState[];
24
+ cdpManagers: Map<string, unknown>;
25
+ providerLoader: {
26
+ getAll(): Array<{
27
+ type: string;
28
+ icon?: string;
29
+ displayName?: string;
30
+ category: 'ide' | 'extension' | 'cli' | 'acp';
31
+ }>;
32
+ };
33
+ detectedIdes: Array<{
34
+ id: string;
35
+ name?: string;
36
+ displayName?: string;
37
+ installed?: boolean;
38
+ path?: string;
39
+ }>;
40
+ instanceId: string;
41
+ version: string;
42
+ daemonMode: boolean;
43
+ timestamp?: number;
44
+ p2p?: StatusReportPayload['p2p'];
45
+ machineNickname?: string | null;
46
+ }
47
+
48
+ export interface StatusSnapshot extends StatusReportPayload {
49
+ availableProviders: AvailableProviderInfo[];
50
+ }
51
+
52
+ function buildDetectedIdeInfos(
53
+ detectedIdes: StatusSnapshotOptions['detectedIdes'],
54
+ cdpManagers: StatusSnapshotOptions['cdpManagers'],
55
+ ): DetectedIdeInfo[] {
56
+ return detectedIdes
57
+ .filter((ide) => ide.installed !== false)
58
+ .map((ide) => ({
59
+ id: ide.id,
60
+ type: ide.id,
61
+ name: ide.displayName || ide.name || ide.id,
62
+ running: isCdpConnected(cdpManagers as Map<string, any>, ide.id),
63
+ ...(ide.path ? { path: ide.path } : {}),
64
+ }));
65
+ }
66
+
67
+ function buildAvailableProviders(
68
+ providerLoader: StatusSnapshotOptions['providerLoader'],
69
+ ): AvailableProviderInfo[] {
70
+ return providerLoader.getAll().map((provider) => ({
71
+ type: provider.type,
72
+ name: provider.displayName || provider.type,
73
+ displayName: provider.displayName || provider.type,
74
+ icon: provider.icon || '💻',
75
+ category: provider.category,
76
+ }));
77
+ }
78
+
79
+ export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnapshot {
80
+ const cfg = loadConfig();
81
+ const wsState = getWorkspaceState(cfg);
82
+ const memSnap = getHostMemorySnapshot();
83
+ const { managedIdes, managedClis, managedAcps } = buildAllManagedEntries(
84
+ options.allStates,
85
+ options.cdpManagers as Map<string, any>,
86
+ {
87
+ detectedIdes: options.detectedIdes.map((ide) => ({
88
+ id: ide.id,
89
+ installed: ide.installed !== false,
90
+ })),
91
+ },
92
+ );
93
+
94
+ return {
95
+ instanceId: options.instanceId,
96
+ version: options.version,
97
+ daemonMode: options.daemonMode,
98
+ machine: {
99
+ hostname: os.hostname(),
100
+ platform: os.platform(),
101
+ arch: os.arch(),
102
+ cpus: os.cpus().length,
103
+ totalMem: memSnap.totalMem,
104
+ freeMem: memSnap.freeMem,
105
+ availableMem: memSnap.availableMem,
106
+ loadavg: os.loadavg(),
107
+ uptime: os.uptime(),
108
+ release: os.release(),
109
+ },
110
+ machineNickname: options.machineNickname ?? cfg.machineNickname ?? null,
111
+ timestamp: options.timestamp ?? Date.now(),
112
+ detectedIdes: buildDetectedIdeInfos(options.detectedIdes, options.cdpManagers),
113
+ ...(options.p2p ? { p2p: options.p2p } : {}),
114
+ managedIdes,
115
+ managedClis,
116
+ managedAcps,
117
+ workspaces: wsState.workspaces,
118
+ defaultWorkspaceId: wsState.defaultWorkspaceId,
119
+ defaultWorkspacePath: wsState.defaultWorkspacePath,
120
+ workspaceActivity: getWorkspaceActivity(cfg, 15),
121
+ availableProviders: buildAvailableProviders(options.providerLoader),
122
+ };
123
+ }