@adhdev/daemon-core 0.7.41 → 0.7.43

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.
Files changed (55) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +3 -4
  2. package/dist/cli-adapters/pty-transport.d.ts +1 -0
  3. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +4 -0
  4. package/dist/cli-adapters/terminal-backends/types.d.ts +4 -0
  5. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +4 -0
  6. package/dist/cli-adapters/terminal-screen.d.ts +4 -0
  7. package/dist/commands/cli-manager.d.ts +4 -2
  8. package/dist/config/chat-history.d.ts +0 -3
  9. package/dist/config/config.d.ts +2 -22
  10. package/dist/index.js +377 -195
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +377 -195
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/providers/cli-provider-instance.d.ts +4 -10
  15. package/dist/providers/contracts.d.ts +0 -79
  16. package/dist/providers/extension-provider-instance.d.ts +1 -0
  17. package/dist/providers/provider-instance.d.ts +0 -3
  18. package/dist/shared-types.d.ts +1 -3
  19. package/dist/status/normalize.js +60 -1
  20. package/dist/status/normalize.js.map +1 -1
  21. package/dist/status/normalize.mjs +60 -1
  22. package/dist/status/normalize.mjs.map +1 -1
  23. package/dist/status/reporter.d.ts +1 -0
  24. package/node_modules/@adhdev/session-host-core/dist/index.js +2 -2
  25. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  26. package/node_modules/@adhdev/session-host-core/dist/index.mjs +2 -2
  27. package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
  28. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  29. package/package.json +1 -1
  30. package/src/agent-stream/forward.ts +21 -1
  31. package/src/agent-stream/poller.ts +6 -1
  32. package/src/cli-adapters/provider-cli-adapter.ts +115 -71
  33. package/src/cli-adapters/pty-transport.ts +2 -0
  34. package/src/cli-adapters/session-host-transport.ts +1 -0
  35. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +5 -0
  36. package/src/cli-adapters/terminal-backends/types.ts +1 -0
  37. package/src/cli-adapters/terminal-backends/xterm-backend.ts +10 -0
  38. package/src/cli-adapters/terminal-screen.ts +4 -0
  39. package/src/commands/cli-manager.ts +44 -39
  40. package/src/commands/router.ts +1 -0
  41. package/src/commands/stream-commands.ts +14 -0
  42. package/src/config/chat-history.ts +3 -55
  43. package/src/config/config.d.ts +5 -50
  44. package/src/config/config.ts +71 -49
  45. package/src/config/workspaces.d.ts +1 -4
  46. package/src/providers/cli-provider-instance.ts +18 -42
  47. package/src/providers/contracts.ts +0 -81
  48. package/src/providers/extension-provider-instance.ts +27 -0
  49. package/src/providers/ide-provider-instance.ts +12 -0
  50. package/src/providers/provider-instance.d.ts +0 -1
  51. package/src/providers/provider-instance.ts +0 -3
  52. package/src/shared-types.ts +1 -3
  53. package/src/status/builders.ts +7 -2
  54. package/src/status/normalize.ts +81 -0
  55. package/src/status/reporter.ts +31 -2
@@ -106,6 +106,10 @@ export class TerminalScreen {
106
106
  return this.terminal.getText();
107
107
  }
108
108
 
109
+ getCursorPosition(): { col: number; row: number } {
110
+ return this.terminal.getCursorPosition();
111
+ }
112
+
109
113
  dispose(): void {
110
114
  this.terminal.dispose();
111
115
  }
@@ -60,7 +60,6 @@ export interface HostedCliRuntimeDescriptor {
60
60
  cliType: string;
61
61
  workspace: string;
62
62
  cliArgs?: string[];
63
- launchMode?: string;
64
63
  }
65
64
 
66
65
  const chalkApi: any = (chalk as any)?.yellow
@@ -91,6 +90,19 @@ export class DaemonCliManager {
91
90
  return `${cliType}_${hash}`;
92
91
  }
93
92
 
93
+ getSessionPresentationMode(sessionId: string): 'terminal' | 'chat' | null {
94
+ if (!sessionId) return null;
95
+ const instance = this.deps.getInstanceManager()?.getInstance(sessionId) as any;
96
+ const mode = instance?.category === 'cli'
97
+ ? instance.getPresentationMode?.()
98
+ : null;
99
+ return mode === 'chat' || mode === 'terminal' ? mode : null;
100
+ }
101
+
102
+ isTerminalSession(sessionId: string): boolean {
103
+ return this.getSessionPresentationMode(sessionId) === 'terminal';
104
+ }
105
+
94
106
  private persistRecentActivity(entry: {
95
107
  kind: 'ide' | 'cli' | 'acp';
96
108
  providerType: string;
@@ -179,13 +191,12 @@ export class DaemonCliManager {
179
191
  provider: any,
180
192
  settings: Record<string, any>,
181
193
  attachExisting = false,
182
- launchModeId?: string,
183
194
  ): Promise<void> {
184
195
  const instanceManager = this.deps.getInstanceManager();
185
196
  const sessionRegistry = this.deps.getSessionRegistry?.() || null;
186
197
  if (!instanceManager) throw new Error('InstanceManager not available');
187
198
  const transportFactory = this.getTransportFactory(key, normalizedType, resolvedDir, cliArgs, attachExisting);
188
- const cliInstance = new CliProviderInstance(provider, resolvedDir, cliArgs, key, transportFactory, launchModeId);
199
+ const cliInstance = new CliProviderInstance(provider, resolvedDir, cliArgs, key, transportFactory);
189
200
  try {
190
201
  await instanceManager.addInstance(key, cliInstance, {
191
202
  serverConn: this.deps.getServerConn(),
@@ -214,7 +225,7 @@ export class DaemonCliManager {
214
225
 
215
226
  // ─── Session start/management ──────────────────────────────
216
227
 
217
- async startSession(cliType: string, workingDir: string, cliArgs?: string[], initialModel?: string, launchMode?: string, launchOptionValues?: Record<string, string | boolean | number>): Promise<void> {
228
+ async startSession(cliType: string, workingDir: string, cliArgs?: string[], initialModel?: string): Promise<void> {
218
229
  const trimmed = (workingDir || '').trim();
219
230
  if (!trimmed) throw new Error('working directory required');
220
231
  const resolvedDir = trimmed.startsWith('~')
@@ -321,38 +332,7 @@ export class DaemonCliManager {
321
332
  }
322
333
 
323
334
  // ─── Resolve launch options → extra args ───
324
- let resolvedCliArgs = cliArgs;
325
- let resolvedLaunchMode = launchMode;
326
-
327
- const activeMode = provider?.launchModes?.length
328
- ? (launchMode
329
- ? provider.launchModes.find((m: any) => m.id === launchMode)
330
- : provider.launchModes.find((m: any) => m.default))
331
- : undefined;
332
-
333
- if (activeMode) {
334
- resolvedLaunchMode = activeMode.id;
335
- }
336
-
337
- if (provider?.launchArgBuilder) {
338
- // Build option values: schema defaults → mode preset → user args?.launchOptionValues
339
- const defaults: Record<string, string | boolean | number> = {};
340
- for (const opt of (provider.launchOptions || [])) {
341
- if (opt.default !== undefined) defaults[opt.id] = opt.default;
342
- }
343
- const modeOptions: Record<string, string | boolean | number> = activeMode?.options || {};
344
- const userOptions: Record<string, string | boolean | number> = launchOptionValues || {};
345
- const merged = { ...defaults, ...modeOptions, ...userOptions };
346
- const extraArgs = provider.launchArgBuilder(merged);
347
- if (extraArgs.length) {
348
- resolvedCliArgs = [...(cliArgs || []), ...extraArgs];
349
- console.log(colorize('cyan', ` 🚀 Launch options applied: ${extraArgs.join(' ')}`));
350
- }
351
- } else if (activeMode?.extraArgs?.length) {
352
- // Fallback: simple extraArgs from mode (no launchArgBuilder)
353
- resolvedCliArgs = [...(cliArgs || []), ...activeMode.extraArgs];
354
- console.log(colorize('cyan', ` 🚀 Launch mode '${activeMode.name}': appending args ${activeMode.extraArgs.join(' ')}`));
355
- }
335
+ const resolvedCliArgs = cliArgs;
356
336
 
357
337
  // If InstanceManager exists, manage as CliProviderInstance unified
358
338
  const instanceManager = this.deps.getInstanceManager();
@@ -367,7 +347,6 @@ export class DaemonCliManager {
367
347
  resolvedProvider,
368
348
  {},
369
349
  false,
370
- resolvedLaunchMode,
371
350
  );
372
351
  console.log(colorize('green', ` ✓ CLI started: ${cliInfo.displayName} v${cliInfo.version || 'unknown'} in ${resolvedDir}`));
373
352
  } else {
@@ -495,7 +474,6 @@ export class DaemonCliManager {
495
474
  resolvedProvider,
496
475
  {},
497
476
  true,
498
- record.launchMode,
499
477
  );
500
478
  restored += 1;
501
479
  LOG.info('CLI', `♻ Restored hosted runtime: ${record.runtimeKey || record.runtimeId} (${record.displayName || record.workspace})`);
@@ -545,6 +523,15 @@ export class DaemonCliManager {
545
523
  return null;
546
524
  }
547
525
 
526
+ private findAdapterBySessionId(instanceKey?: string): { adapter: CliAdapter; key: string } | null {
527
+ if (!instanceKey) return null;
528
+ let ik = instanceKey;
529
+ const colonIdx = ik.lastIndexOf(':');
530
+ if (colonIdx >= 0) ik = ik.substring(colonIdx + 1);
531
+ const adapter = this.adapters.get(ik);
532
+ return adapter ? { adapter, key: ik } : null;
533
+ }
534
+
548
535
  // ─── CLI command handling ────────────────────────────
549
536
 
550
537
  async handleCliCommand(cmd: string, args: any): Promise<CommandResult | null> {
@@ -575,7 +562,7 @@ export class DaemonCliManager {
575
562
  const launchSource = resolved.source;
576
563
  if (!cliType) throw new Error('cliType required');
577
564
 
578
- await this.startSession(cliType, dir, args?.cliArgs, args?.initialModel, args?.launchMode, args?.launchOptionValues);
565
+ await this.startSession(cliType, dir, args?.cliArgs, args?.initialModel);
579
566
 
580
567
  // On startSession success, new UUID key exists in adapters (last added item)
581
568
  let newKey: string | null = null;
@@ -601,6 +588,24 @@ export class DaemonCliManager {
601
588
  }
602
589
  return { success: true, cliType, dir, stopped: true, mode };
603
590
  }
591
+ case 'set_cli_view_mode': {
592
+ const mode = args?.mode === 'chat' ? 'chat' : 'terminal';
593
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId : '';
594
+ const cliType = args?.cliType || args?.agentType || '';
595
+ const dir = args?.dir || '';
596
+ const found = this.findAdapterBySessionId(targetSessionId)
597
+ || (cliType ? this.findAdapter(cliType, { instanceKey: targetSessionId, dir }) : null);
598
+ if (!found) {
599
+ return { success: false, error: 'CLI session not found', code: 'CLI_SESSION_NOT_FOUND' };
600
+ }
601
+ const instance = this.deps.getInstanceManager()?.getInstance(found.key);
602
+ if (!(instance instanceof CliProviderInstance)) {
603
+ return { success: false, error: 'CLI instance not found', code: 'CLI_INSTANCE_NOT_FOUND' };
604
+ }
605
+ instance.setPresentationMode(mode);
606
+ this.deps.onStatusChange();
607
+ return { success: true, id: found.key, mode };
608
+ }
604
609
  case 'restart_session': {
605
610
  const cliType = args?.cliType || args?.agentType || args?.ideType;
606
611
  const cfg = loadConfig();
@@ -121,6 +121,7 @@ export class DaemonCommandRouter {
121
121
  // ─── CLI / ACP commands ───
122
122
  case 'launch_cli':
123
123
  case 'stop_cli':
124
+ case 'set_cli_view_mode':
124
125
  case 'agent_command': {
125
126
  return this.deps.cliManager.handleCliCommand(cmd, args);
126
127
  }
@@ -7,6 +7,14 @@ import type { CommandResult, CommandHelpers } from './handler.js';
7
7
  import type { ProviderLoader } from '../providers/provider-loader.js';
8
8
  import { LOG } from '../logging/logger.js';
9
9
 
10
+ function getCliPresentationMode(h: CommandHelpers, targetSessionId?: string): 'terminal' | 'chat' | null {
11
+ if (!targetSessionId) return null;
12
+ const instance = h.ctx.instanceManager?.getInstance(targetSessionId) as any;
13
+ if (instance?.category !== 'cli') return null;
14
+ const mode = instance.getPresentationMode?.();
15
+ return mode === 'chat' || mode === 'terminal' ? mode : null;
16
+ }
17
+
10
18
  export async function handleFocusSession(h: CommandHelpers, args: any): Promise<CommandResult> {
11
19
  if (!h.agentStream || !h.getCdp()) return { success: false, error: 'AgentStream or CDP not available' };
12
20
  const sessionId = args?.targetSessionId || h.currentSession?.sessionId;
@@ -20,6 +28,9 @@ export async function handleFocusSession(h: CommandHelpers, args: any): Promise<
20
28
  export function handlePtyInput(h: CommandHelpers, args: any): CommandResult {
21
29
  const { cliType, data, targetSessionId } = args || {};
22
30
  if (!data) return { success: false, error: 'data required' };
31
+ if (getCliPresentationMode(h, targetSessionId) === 'chat') {
32
+ return { success: false, error: 'CLI session is in chat mode', code: 'CLI_VIEW_MODE_NOT_TERMINAL' };
33
+ }
23
34
  const adapter = h.getCliAdapter(targetSessionId || cliType);
24
35
  if (!adapter || typeof adapter.writeRaw !== 'function') {
25
36
  return { success: false, error: `CLI adapter not found: ${targetSessionId || cliType || 'unknown'}` };
@@ -31,6 +42,9 @@ export function handlePtyInput(h: CommandHelpers, args: any): CommandResult {
31
42
  export function handlePtyResize(h: CommandHelpers, args: any): CommandResult {
32
43
  const { cliType, cols, rows, force, targetSessionId } = args || {};
33
44
  if (!cols || !rows) return { success: false, error: 'cols and rows required' };
45
+ if (getCliPresentationMode(h, targetSessionId) === 'chat') {
46
+ return { success: false, error: 'CLI session is in chat mode', code: 'CLI_VIEW_MODE_NOT_TERMINAL' };
47
+ }
34
48
  const adapter = h.getCliAdapter(targetSessionId || cliType);
35
49
  if (!adapter || typeof adapter.resize !== 'function') {
36
50
  return { success: false, error: `CLI adapter not found: ${targetSessionId || cliType || 'unknown'}` };
@@ -27,12 +27,10 @@ interface HistoryMessage {
27
27
  }
28
28
 
29
29
  export class ChatHistoryWriter {
30
- /** Last seen message count per agent (deduplication) */
30
+ /** Last seen message count per agent (deduplication) */
31
31
  private lastSeenCounts = new Map<string, number>();
32
- /** Last seen message hash per agent (deduplication) */
32
+ /** Last seen message hash per agent (deduplication) */
33
33
  private lastSeenHashes = new Map<string, Set<string>>();
34
- /** Last seen append-only terminal transcript per agent */
35
- private lastSeenTerminal = new Map<string, string>();
36
34
  private rotated = false;
37
35
 
38
36
  /**
@@ -109,60 +107,10 @@ export class ChatHistoryWriter {
109
107
  }
110
108
  }
111
109
 
112
- appendTerminalHistory(
113
- agentType: string,
114
- terminalHistory: string,
115
- sessionTitle?: string,
116
- instanceId?: string,
117
- ): void {
118
- const next = String(terminalHistory || '');
119
- if (!next.trim()) return;
120
-
121
- try {
122
- const dedupKey = instanceId ? `${agentType}:${instanceId}:terminal` : `${agentType}:terminal`;
123
- const prev = this.lastSeenTerminal.get(dedupKey) || '';
124
- if (prev === next) return;
125
-
126
- let delta = '';
127
- if (!prev) {
128
- delta = next;
129
- } else if (next.startsWith(prev)) {
130
- delta = next.slice(prev.length);
131
- } else if (prev.includes(next)) {
132
- this.lastSeenTerminal.set(dedupKey, next);
133
- return;
134
- } else {
135
- delta = `\n\n[terminal snapshot reset ${new Date().toISOString()} | ${sessionTitle || agentType}]\n${next}`;
136
- }
137
-
138
- if (!delta) {
139
- this.lastSeenTerminal.set(dedupKey, next);
140
- return;
141
- }
142
-
143
- const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
144
- fs.mkdirSync(dir, { recursive: true });
145
-
146
- const date = new Date().toISOString().slice(0, 10);
147
- const filePrefix = instanceId ? `${this.sanitize(instanceId)}_` : '';
148
- const filePath = path.join(dir, `${filePrefix}${date}.terminal.log`);
149
- fs.appendFileSync(filePath, delta, 'utf-8');
150
- this.lastSeenTerminal.set(dedupKey, next);
151
-
152
- if (!this.rotated) {
153
- this.rotated = true;
154
- this.rotateOldFiles().catch(() => {});
155
- }
156
- } catch {
157
- // Ignore terminal history save failures
158
- }
159
- }
160
-
161
- /** Called when agent session is explicitly changed */
110
+ /** Called when agent session is explicitly changed */
162
111
  onSessionChange(agentType: string): void {
163
112
  this.lastSeenHashes.delete(agentType);
164
113
  this.lastSeenCounts.delete(agentType);
165
- this.lastSeenTerminal.delete(`${agentType}:terminal`);
166
114
  }
167
115
 
168
116
  /** Delete history files older than 30 days */
@@ -1,65 +1,39 @@
1
1
  /**
2
2
  * ADHDev Launcher — Configuration
3
3
  *
4
- * Manages launcher config, server connection tokens, and user preferences.
4
+ * Manages launcher config, machine auth, and user preferences.
5
5
  */
6
6
  import type { WorkspaceEntry } from './workspaces.js';
7
- import type { WorkspaceActivityEntry } from './workspace-activity.js';
8
7
  import type { RecentActivityEntry } from './recent-activity.js';
9
8
  export type { WorkspaceEntry } from './workspaces.js';
10
- export type { WorkspaceActivityEntry } from './workspace-activity.js';
11
9
  export type { RecentActivityEntry } from './recent-activity.js';
12
10
  export interface ADHDevConfig {
13
11
  serverUrl: string;
14
- apiToken: string | null;
15
- connectionToken: string | null;
16
12
  selectedIde: string | null;
17
13
  configuredIdes: string[];
18
14
  installedExtensions: string[];
19
- autoConnect: boolean;
20
- /**
21
- * @deprecated Not read at runtime. Notification preferences are now managed by:
22
- * - Web UI layer: useNotificationPrefs (localStorage)
23
- * - Daemon layer: per-provider settings (approvalAlert, longGeneratingAlert)
24
- * Kept for backward config compat — will be removed in v0.7+.
25
- */
26
- notifications: boolean;
27
15
  userEmail: string | null;
28
16
  userName: string | null;
29
17
  setupCompleted: boolean;
30
18
  setupDate: string | null;
31
- configuredCLIs: string[];
32
19
  enabledIdes: string[];
33
- recentCliWorkspaces: string[];
34
20
  /** Saved workspaces for IDE/CLI/ACP launch (daemon-local) */
35
21
  workspaces?: WorkspaceEntry[];
36
22
  /** Default workspace id (from workspaces[]) — never used implicitly for launch */
37
23
  defaultWorkspaceId?: string | null;
38
- /** Recently used workspaces (IDE / CLI / ACP / default) for quick resume */
39
- recentWorkspaceActivity?: WorkspaceActivityEntry[];
40
24
  /** Unified recent activity across IDE / CLI / ACP launch flows */
41
25
  recentActivity?: RecentActivityEntry[];
26
+ /** Last seen timestamps for live sessions, keyed by sessionId */
27
+ sessionReads?: Record<string, number>;
28
+ /** Last seen completion marker for live sessions, keyed by sessionId */
29
+ sessionReadMarkers?: Record<string, string>;
42
30
  machineNickname: string | null;
43
31
  /**
44
32
  * Stable local machine ID (prefix: `mach_`) — generated locally on first run.
45
33
  * Used as daemon instance key (`daemon_<machineId>`) and in status reports.
46
- * NOT the same as the server-side D1 `machines.id` — see `registeredMachineId`.
47
34
  */
48
35
  machineId?: string;
49
36
  machineSecret?: string | null;
50
- /**
51
- * Server-side D1 `machines.id` — the row ID assigned when daemon registers via
52
- * `POST /cli/complete`. Corresponds to `machineId` in server DO context
53
- * (`DaemonConnection.machineId`, `StatusContext.machineId`).
54
- *
55
- * Naming differs from server-side `machineId` to avoid confusion with the local
56
- * `config.machineId` (mach_ prefix) which is a different value.
57
- *
58
- * @deprecated Legacy bridge field — will be removed after 2026-04-06.
59
- * Modern auth flow uses `machineSecret` (adm_) to identify machines.
60
- */
61
- registeredMachineId?: string;
62
- cliHistory: CliHistoryEntry[];
63
37
  providerSettings: Record<string, Record<string, any>>;
64
38
  ideSettings: Record<string, {
65
39
  extensions?: Record<string, {
@@ -69,17 +43,6 @@ export interface ADHDevConfig {
69
43
  disableUpstream?: boolean;
70
44
  providerDir?: string;
71
45
  }
72
- export interface CliHistoryEntry {
73
- category?: 'ide' | 'cli' | 'acp';
74
- cliType: string;
75
- dir: string;
76
- cliArgs?: string[];
77
- workspace?: string;
78
- newWindow?: boolean;
79
- model?: string;
80
- timestamp: number;
81
- label?: string;
82
- }
83
46
  export declare function generateMachineId(): string;
84
47
  export declare function isStableMachineId(machineId?: string | null): boolean;
85
48
  /**
@@ -110,11 +73,3 @@ export declare function isSetupComplete(): boolean;
110
73
  * Reset configuration
111
74
  */
112
75
  export declare function resetConfig(): void;
113
- /**
114
- * Generate a connection token for server authentication
115
- */
116
- export declare function generateConnectionToken(): string;
117
- /**
118
- * Add launch to history (max 20, dedup by category+type+dir+args+workspace+model)
119
- */
120
- export declare function addCliHistory(entry: Omit<CliHistoryEntry, 'timestamp'>): void;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ADHDev Launcher — Configuration
3
- *
4
- * Manages launcher config, server connection tokens, and user preferences.
3
+ *
4
+ * Manages launcher config, machine auth, and user preferences.
5
5
  */
6
6
 
7
7
  import { homedir } from 'os';
@@ -16,8 +16,6 @@ export type { RecentActivityEntry } from './recent-activity.js';
16
16
  export interface ADHDevConfig {
17
17
  // Server connection
18
18
  serverUrl: string;
19
- apiToken: string | null;
20
- connectionToken: string | null;
21
19
 
22
20
  // Selected IDE (primary)
23
21
  selectedIde: string | null;
@@ -28,16 +26,6 @@ export interface ADHDevConfig {
28
26
  // Installed extensions
29
27
  installedExtensions: string[];
30
28
 
31
- // User preferences
32
- autoConnect: boolean;
33
- /**
34
- * @deprecated Not read at runtime. Notification preferences are now managed by:
35
- * - Web UI layer: useNotificationPrefs (localStorage)
36
- * - Daemon layer: per-provider settings (approvalAlert, longGeneratingAlert)
37
- * Kept for backward config compat — will be removed in v0.7+.
38
- */
39
- notifications: boolean;
40
-
41
29
  // Auth
42
30
  userEmail: string | null;
43
31
  userName: string | null;
@@ -46,9 +34,6 @@ export interface ADHDevConfig {
46
34
  setupCompleted: boolean;
47
35
  setupDate: string | null;
48
36
 
49
- // Configured CLI agents
50
- configuredCLIs: string[];
51
-
52
37
  // Daemon: which IDEs to connect (empty = all)
53
38
  enabledIdes: string[];
54
39
 
@@ -70,20 +55,15 @@ export interface ADHDevConfig {
70
55
  /**
71
56
  * Stable local machine ID (prefix: `mach_`) — generated locally on first run.
72
57
  * Used as daemon instance key (`daemon_<machineId>`) and in status reports.
73
- * NOT the same as the server-side D1 `machines.id` — see `registeredMachineId`.
74
58
  */
75
59
  machineId?: string;
76
60
 
77
- // Machine secret for server auth (replaces connectionToken)
61
+ // Machine secret for server auth
78
62
  machineSecret?: string | null;
79
63
 
80
64
  /**
81
65
  * Server-side D1 `machines.id` — the row ID assigned when daemon registers via
82
- * `POST /cli/complete`. Corresponds to `machineId` in server DO context
83
- * (`DaemonConnection.machineId`, `StatusContext.machineId`).
84
- *
85
- * Naming differs from server-side `machineId` to avoid confusion with the local
86
- * `config.machineId` (mach_ prefix) which is a different value.
66
+ * `POST /cli/complete`. Used as fallback for machine lookup on re-auth.
87
67
  *
88
68
  * @deprecated Legacy bridge field — will be removed after 2026-04-06.
89
69
  * Modern auth flow uses `machineSecret` (adm_) to identify machines.
@@ -108,18 +88,13 @@ export interface ADHDevConfig {
108
88
 
109
89
  const DEFAULT_CONFIG: ADHDevConfig = {
110
90
  serverUrl: 'https://api.adhf.dev',
111
- apiToken: null,
112
- connectionToken: null,
113
91
  selectedIde: null,
114
92
  configuredIdes: [],
115
93
  installedExtensions: [],
116
- autoConnect: true,
117
- notifications: true,
118
94
  userEmail: null,
119
95
  userName: null,
120
96
  setupCompleted: false,
121
97
  setupDate: null,
122
- configuredCLIs: [],
123
98
  enabledIdes: [],
124
99
  workspaces: [],
125
100
  defaultWorkspaceId: null,
@@ -137,6 +112,68 @@ const DEFAULT_CONFIG: ADHDevConfig = {
137
112
 
138
113
  const MACHINE_ID_PREFIX = 'mach_';
139
114
 
115
+ function isPlainObject(value: unknown): value is Record<string, any> {
116
+ return !!value && typeof value === 'object' && !Array.isArray(value);
117
+ }
118
+
119
+ function asStringArray(value: unknown): string[] {
120
+ if (!Array.isArray(value)) return [];
121
+ return value.filter((item): item is string => typeof item === 'string');
122
+ }
123
+
124
+ function asNullableString(value: unknown): string | null {
125
+ return typeof value === 'string' ? value : null;
126
+ }
127
+
128
+ function asOptionalString(value: unknown): string | undefined {
129
+ return typeof value === 'string' && value.trim() ? value : undefined;
130
+ }
131
+
132
+ function asBoolean(value: unknown, fallback: boolean): boolean {
133
+ return typeof value === 'boolean' ? value : fallback;
134
+ }
135
+
136
+ function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: string | null } {
137
+ const parsed = isPlainObject(raw) ? raw : {};
138
+ const legacySessionReads = isPlainObject(parsed.recentSessionReads) ? parsed.recentSessionReads : {};
139
+ const sessionReads = isPlainObject(parsed.sessionReads) ? parsed.sessionReads : {};
140
+ const mergedSessionReads = Object.fromEntries(
141
+ Object.entries({ ...legacySessionReads, ...sessionReads })
142
+ .filter(([, value]) => typeof value === 'number' && Number.isFinite(value))
143
+ );
144
+ const sessionReadMarkers = Object.fromEntries(
145
+ Object.entries(isPlainObject(parsed.sessionReadMarkers) ? parsed.sessionReadMarkers : {})
146
+ .filter(([, value]) => typeof value === 'string')
147
+ );
148
+
149
+ return {
150
+ serverUrl: typeof parsed.serverUrl === 'string' && parsed.serverUrl.trim()
151
+ ? parsed.serverUrl
152
+ : DEFAULT_CONFIG.serverUrl,
153
+ selectedIde: asNullableString(parsed.selectedIde),
154
+ configuredIdes: asStringArray(parsed.configuredIdes),
155
+ installedExtensions: asStringArray(parsed.installedExtensions),
156
+ userEmail: asNullableString(parsed.userEmail),
157
+ userName: asNullableString(parsed.userName),
158
+ setupCompleted: asBoolean(parsed.setupCompleted, DEFAULT_CONFIG.setupCompleted),
159
+ setupDate: asNullableString(parsed.setupDate),
160
+ enabledIdes: asStringArray(parsed.enabledIdes),
161
+ workspaces: Array.isArray(parsed.workspaces) ? parsed.workspaces as WorkspaceEntry[] : [],
162
+ defaultWorkspaceId: asNullableString(parsed.defaultWorkspaceId) ?? asNullableString(parsed.activeWorkspaceId),
163
+ recentActivity: Array.isArray(parsed.recentActivity) ? parsed.recentActivity as RecentActivityEntry[] : [],
164
+ sessionReads: mergedSessionReads,
165
+ sessionReadMarkers,
166
+ machineNickname: asNullableString(parsed.machineNickname),
167
+ machineId: asOptionalString(parsed.machineId),
168
+ machineSecret: parsed.machineSecret === null ? null : asOptionalString(parsed.machineSecret),
169
+ registeredMachineId: asOptionalString(parsed.registeredMachineId),
170
+ providerSettings: isPlainObject(parsed.providerSettings) ? parsed.providerSettings : {},
171
+ ideSettings: isPlainObject(parsed.ideSettings) ? parsed.ideSettings : {},
172
+ disableUpstream: asBoolean(parsed.disableUpstream, DEFAULT_CONFIG.disableUpstream ?? false),
173
+ providerDir: asOptionalString(parsed.providerDir),
174
+ };
175
+ }
176
+
140
177
  export function generateMachineId(): string {
141
178
  return `${MACHINE_ID_PREFIX}${randomUUID().replace(/-/g, '')}`;
142
179
  }
@@ -201,14 +238,10 @@ export function loadConfig(): ADHDevConfig {
201
238
  try {
202
239
  const raw = readFileSync(configPath, 'utf-8');
203
240
  const parsed = JSON.parse(raw);
204
- const merged = { ...DEFAULT_CONFIG, ...parsed } as ADHDevConfig & { activeWorkspaceId?: string | null };
205
- if (merged.defaultWorkspaceId == null && merged.activeWorkspaceId != null) {
206
- (merged as ADHDevConfig).defaultWorkspaceId = merged.activeWorkspaceId;
207
- }
208
- delete (merged as any).activeWorkspaceId;
209
- const ensured = ensureMachineId(merged);
241
+ const normalizedInput = normalizeConfig(parsed);
242
+ const ensured = ensureMachineId(normalizedInput);
210
243
  const normalized = ensured.config as ADHDevConfig & { activeWorkspaceId?: string | null };
211
- if (ensured.changed) {
244
+ if (ensured.changed || JSON.stringify(parsed) !== JSON.stringify(normalized)) {
212
245
  try {
213
246
  saveConfig(normalized);
214
247
  } catch { /* ignore */ }
@@ -226,12 +259,13 @@ export function loadConfig(): ADHDevConfig {
226
259
  export function saveConfig(config: ADHDevConfig): void {
227
260
  const configPath = getConfigPath();
228
261
  const dir = getConfigDir();
262
+ const normalized = normalizeConfig(config);
229
263
 
230
264
  if (!existsSync(dir)) {
231
265
  mkdirSync(dir, { recursive: true, mode: 0o700 });
232
266
  }
233
267
 
234
- writeFileSync(configPath, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
268
+ writeFileSync(configPath, JSON.stringify(normalized, null, 2), { encoding: 'utf-8', mode: 0o600 });
235
269
  try { chmodSync(configPath, 0o600); } catch { /* Windows etc. not supported */ }
236
270
  }
237
271
 
@@ -276,15 +310,3 @@ export function isSetupComplete(): boolean {
276
310
  export function resetConfig(): void {
277
311
  saveConfig({ ...DEFAULT_CONFIG });
278
312
  }
279
-
280
- /**
281
- * Generate a connection token for server authentication
282
- */
283
- export function generateConnectionToken(): string {
284
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
285
- let token = 'db_';
286
- for (let i = 0; i < 32; i++) {
287
- token += chars.charAt(Math.floor(Math.random() * chars.length));
288
- }
289
- return token;
290
- }
@@ -17,10 +17,7 @@ export declare function validateWorkspacePath(absPath: string): {
17
17
  };
18
18
  /** Default workspace label from path */
19
19
  export declare function defaultWorkspaceLabel(absPath: string): string;
20
- /**
21
- * Ensure config.workspaces exists; seed from recentCliWorkspaces once (same paths).
22
- */
23
- export declare function migrateWorkspacesFromRecent(config: ADHDevConfig): ADHDevConfig;
20
+
24
21
  export declare function getDefaultWorkspacePath(config: ADHDevConfig): string | null;
25
22
  export declare function getWorkspaceState(config: ADHDevConfig): {
26
23
  workspaces: WorkspaceEntry[];