@adhdev/daemon-core 0.5.3 → 0.5.6

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 (45) hide show
  1. package/dist/index.d.ts +88 -2
  2. package/dist/index.js +1230 -439
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/providers/_builtin/extension/cline/scripts/read_chat.js +14 -1
  6. package/providers/_builtin/ide/antigravity/scripts/1.106/read_chat.js +24 -1
  7. package/providers/_builtin/ide/antigravity/scripts/1.107/read_chat.js +24 -1
  8. package/providers/_builtin/ide/cursor/scripts/0.49/focus_editor.js +3 -3
  9. package/providers/_builtin/ide/cursor/scripts/0.49/list_models.js +1 -1
  10. package/providers/_builtin/ide/cursor/scripts/0.49/list_modes.js +1 -1
  11. package/providers/_builtin/ide/cursor/scripts/0.49/open_panel.js +4 -4
  12. package/providers/_builtin/ide/cursor/scripts/0.49/read_chat.js +5 -1
  13. package/providers/_builtin/ide/cursor/scripts/0.49.bak/dismiss_notification.js +30 -0
  14. package/providers/_builtin/ide/cursor/scripts/0.49.bak/focus_editor.js +13 -0
  15. package/providers/_builtin/ide/cursor/scripts/0.49.bak/list_models.js +78 -0
  16. package/providers/_builtin/ide/cursor/scripts/0.49.bak/list_modes.js +40 -0
  17. package/providers/_builtin/ide/cursor/scripts/0.49.bak/list_notifications.js +23 -0
  18. package/providers/_builtin/ide/cursor/scripts/0.49.bak/list_sessions.js +42 -0
  19. package/providers/_builtin/ide/cursor/scripts/0.49.bak/new_session.js +20 -0
  20. package/providers/_builtin/ide/cursor/scripts/0.49.bak/open_panel.js +23 -0
  21. package/providers/_builtin/ide/cursor/scripts/0.49.bak/read_chat.js +79 -0
  22. package/providers/_builtin/ide/cursor/scripts/0.49.bak/resolve_action.js +19 -0
  23. package/providers/_builtin/ide/cursor/scripts/0.49.bak/scripts.js +78 -0
  24. package/providers/_builtin/ide/cursor/scripts/0.49.bak/send_message.js +23 -0
  25. package/providers/_builtin/ide/cursor/scripts/0.49.bak/set_mode.js +38 -0
  26. package/providers/_builtin/ide/cursor/scripts/0.49.bak/set_model.js +81 -0
  27. package/providers/_builtin/ide/cursor/scripts/0.49.bak/switch_session.js +28 -0
  28. package/providers/_builtin/ide/windsurf/scripts/read_chat.js +18 -1
  29. package/src/cli-adapters/provider-cli-adapter.ts +231 -12
  30. package/src/commands/chat-commands.ts +36 -0
  31. package/src/commands/cli-manager.ts +128 -30
  32. package/src/commands/handler.ts +47 -3
  33. package/src/commands/router.ts +32 -2
  34. package/src/commands/workspace-commands.ts +108 -0
  35. package/src/config/config.ts +29 -1
  36. package/src/config/workspace-activity.ts +65 -0
  37. package/src/config/workspaces.ts +250 -0
  38. package/src/daemon/dev-server.ts +1 -1
  39. package/src/index.ts +5 -0
  40. package/src/launch.ts +1 -1
  41. package/src/providers/cli-provider-instance.ts +7 -2
  42. package/src/providers/ide-provider-instance.ts +11 -0
  43. package/src/status/reporter.ts +23 -4
  44. package/src/system/host-memory.ts +65 -0
  45. package/src/types.ts +8 -1
@@ -12,6 +12,8 @@ import chalk from 'chalk';
12
12
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
13
13
  import { detectCLI } from '../detection/cli-detector.js';
14
14
  import { loadConfig, saveConfig, addCliHistory } from '../config/config.js';
15
+ import { getWorkspaceState, resolveLaunchDirectory } from '../config/workspaces.js';
16
+ import { appendWorkspaceActivity } from '../config/workspace-activity.js';
15
17
  import { CliProviderInstance } from '../providers/cli-provider-instance.js';
16
18
  import { AcpProviderInstance } from '../providers/acp-provider-instance.js';
17
19
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
@@ -54,6 +56,25 @@ export class DaemonCliManager {
54
56
  return `${cliType}_${hash}`;
55
57
  }
56
58
 
59
+ private persistRecentDir(cliType: string, dir: string): void {
60
+ try {
61
+ const normalizedType = this.providerLoader.resolveAlias(cliType);
62
+ const provider = this.providerLoader.getByAlias(cliType);
63
+ const actKind = provider?.category === 'acp' ? 'acp' : 'cli';
64
+ let next = loadConfig();
65
+ console.log(chalk.cyan(` 📂 Saving recent workspace: ${dir}`));
66
+ const recent = next.recentCliWorkspaces || [];
67
+ if (!recent.includes(dir)) {
68
+ next = { ...next, recentCliWorkspaces: [dir, ...recent].slice(0, 10) };
69
+ }
70
+ next = appendWorkspaceActivity(next, dir, { kind: actKind, agentType: normalizedType });
71
+ saveConfig(next);
72
+ console.log(chalk.green(` ✓ Recent workspace saved: ${dir}`));
73
+ } catch (e) {
74
+ console.error(chalk.red(` ✗ Failed to save recent workspace: ${e}`));
75
+ }
76
+ }
77
+
57
78
  private createAdapter(cliType: string, workingDir: string, cliArgs?: string[]): CliAdapter {
58
79
  // cliType normalize (Resolve alias)
59
80
  const normalizedType = this.providerLoader.resolveAlias(cliType);
@@ -71,7 +92,8 @@ export class DaemonCliManager {
71
92
  // ─── Session start/management ──────────────────────────────
72
93
 
73
94
  async startSession(cliType: string, workingDir: string, cliArgs?: string[], initialModel?: string): Promise<void> {
74
- const trimmed = (workingDir || os.homedir()).trim();
95
+ const trimmed = (workingDir || '').trim();
96
+ if (!trimmed) throw new Error('working directory required');
75
97
  const resolvedDir = trimmed.startsWith('~')
76
98
  ? trimmed.replace(/^~/, os.homedir())
77
99
  : path.resolve(trimmed);
@@ -161,21 +183,54 @@ export class DaemonCliManager {
161
183
  const instanceManager = this.deps.getInstanceManager();
162
184
  if (provider && instanceManager) {
163
185
  const cliInstance = new CliProviderInstance(provider, resolvedDir, cliArgs, key);
164
- await instanceManager.addInstance(key, cliInstance, {
165
- serverConn: this.deps.getServerConn(),
166
- settings: {},
167
- onPtyData: (data: string) => {
168
- this.deps.getP2p()?.broadcastPtyOutput(key, data);
169
- },
170
- });
186
+ try {
187
+ await instanceManager.addInstance(key, cliInstance, {
188
+ serverConn: this.deps.getServerConn(),
189
+ settings: {},
190
+ onPtyData: (data: string) => {
191
+ this.deps.getP2p()?.broadcastPtyOutput(key, data);
192
+ },
193
+ });
194
+ } catch (spawnErr: any) {
195
+ // Spawn failed — cleanup and propagate error
196
+ LOG.error('CLI', `[${cliType}] Spawn failed: ${spawnErr?.message}`);
197
+ instanceManager.removeInstance(key);
198
+ throw new Error(`Failed to start ${cliInfo.displayName}: ${spawnErr?.message}`);
199
+ }
171
200
 
172
201
  // Keep adapter ref too (backward compat — write, resize etc)
173
202
  this.adapters.set(key, cliInstance.getAdapter() as any);
174
203
  console.log(chalk.green(` ✓ CLI started: ${cliInfo.displayName} v${cliInfo.version || 'unknown'} in ${resolvedDir}`));
204
+
205
+ // Monitor for stopped/error → auto-cleanup
206
+ const checkStopped = setInterval(() => {
207
+ try {
208
+ const adapter = this.adapters.get(key);
209
+ if (!adapter) { clearInterval(checkStopped); return; }
210
+ const status = adapter.getStatus?.();
211
+ if (status?.status === 'stopped' || status?.status === 'error') {
212
+ clearInterval(checkStopped);
213
+ setTimeout(() => {
214
+ if (this.adapters.has(key)) {
215
+ this.adapters.delete(key);
216
+ this.deps.removeAgentTracking(key);
217
+ instanceManager.removeInstance(key);
218
+ LOG.info('CLI', `🧹 Auto-cleaned ${status.status} CLI: ${cliType}`);
219
+ this.deps.onStatusChange();
220
+ }
221
+ }, 5000);
222
+ }
223
+ } catch { /* ignore */ }
224
+ }, 3000);
175
225
  } else {
176
226
  // Fallback: InstanceManager without directly adapter manage
177
227
  const adapter = this.createAdapter(cliType, resolvedDir, cliArgs);
178
- await adapter.spawn();
228
+ try {
229
+ await adapter.spawn();
230
+ } catch (spawnErr: any) {
231
+ LOG.error('CLI', `[${cliType}] Spawn failed: ${spawnErr?.message}`);
232
+ throw new Error(`Failed to start ${cliInfo.displayName}: ${spawnErr?.message}`);
233
+ }
179
234
 
180
235
  const serverConn = this.deps.getServerConn();
181
236
  if (serverConn && typeof adapter.setServerConn === 'function') {
@@ -184,12 +239,12 @@ export class DaemonCliManager {
184
239
  adapter.setOnStatusChange(() => {
185
240
  this.deps.onStatusChange();
186
241
  const status = adapter.getStatus?.();
187
- if (status?.status === 'stopped') {
242
+ if (status?.status === 'stopped' || status?.status === 'error') {
188
243
  setTimeout(() => {
189
244
  if (this.adapters.get(key) === adapter) {
190
245
  this.adapters.delete(key);
191
246
  this.deps.removeAgentTracking(key);
192
- console.log(chalk.yellow(` 🧹 Auto-cleaned stopped CLI: ${adapter.cliType}`));
247
+ LOG.info('CLI', `🧹 Auto-cleaned ${status.status} CLI: ${adapter.cliType}`);
193
248
  this.deps.onStatusChange();
194
249
  }
195
250
  }, 3000);
@@ -214,13 +269,26 @@ export class DaemonCliManager {
214
269
  async stopSession(key: string): Promise<void> {
215
270
  const adapter = this.adapters.get(key);
216
271
  if (adapter) {
217
- adapter.shutdown();
272
+ try {
273
+ adapter.shutdown();
274
+ } catch (e: any) {
275
+ LOG.warn('CLI', `Shutdown error for ${adapter.cliType}: ${e?.message} (force-cleaning)`);
276
+ }
277
+ // Always cleanup regardless of shutdown success
218
278
  this.adapters.delete(key);
219
279
  this.deps.removeAgentTracking(key);
220
- // Also remove from InstanceManager
221
280
  this.deps.getInstanceManager()?.removeInstance(key);
222
- console.log(chalk.yellow(` 🛑 CLI Agent stopped: ${adapter.cliType} in ${adapter.workingDir}`));
281
+ LOG.info('CLI', `🛑 Agent stopped: ${adapter.cliType} in ${adapter.workingDir}`);
223
282
  this.deps.onStatusChange();
283
+ } else {
284
+ // Adapter not found — try InstanceManager direct removal
285
+ const im = this.deps.getInstanceManager();
286
+ if (im) {
287
+ im.removeInstance(key);
288
+ this.deps.removeAgentTracking(key);
289
+ LOG.warn('CLI', `🧹 Force-removed orphan entry: ${key}`);
290
+ this.deps.onStatusChange();
291
+ }
224
292
  }
225
293
  }
226
294
 
@@ -270,8 +338,28 @@ export class DaemonCliManager {
270
338
  switch (cmd) {
271
339
  case 'launch_cli': {
272
340
  const cliType = args?.cliType;
273
- const defaultedToHome = !args?.dir;
274
- const dir = args?.dir || os.homedir();
341
+ const config = loadConfig();
342
+ const resolved = resolveLaunchDirectory(
343
+ {
344
+ dir: args?.dir,
345
+ workspaceId: args?.workspaceId,
346
+ useDefaultWorkspace: args?.useDefaultWorkspace === true,
347
+ useHome: args?.useHome === true,
348
+ },
349
+ config,
350
+ );
351
+ if (!resolved.ok) {
352
+ const ws = getWorkspaceState(config);
353
+ return {
354
+ success: false,
355
+ error: resolved.message,
356
+ code: resolved.code,
357
+ workspaces: ws.workspaces,
358
+ defaultWorkspacePath: ws.defaultWorkspacePath,
359
+ };
360
+ }
361
+ const dir = resolved.path;
362
+ const launchSource = resolved.source;
275
363
  if (!cliType) throw new Error('cliType required');
276
364
 
277
365
  await this.startSession(cliType, dir, args?.cliArgs, args?.initialModel);
@@ -284,20 +372,9 @@ export class DaemonCliManager {
284
372
  }
285
373
  }
286
374
 
287
- try {
288
- const config = loadConfig();
289
- console.log(chalk.cyan(` 📂 Saving recent workspace: ${dir}`));
290
- const recent = config.recentCliWorkspaces || [];
291
- if (!recent.includes(dir)) {
292
- const updated = [dir, ...recent].slice(0, 10);
293
- saveConfig({ ...config, recentCliWorkspaces: updated });
294
- console.log(chalk.green(` ✓ Recent workspace saved: ${dir}`));
295
- }
296
- } catch (e) {
297
- console.error(chalk.red(` ✗ Failed to save recent workspace: ${e}`));
298
- }
375
+ this.persistRecentDir(cliType, dir);
299
376
 
300
- return { success: true, cliType, dir, id: newKey, defaultedToHome };
377
+ return { success: true, cliType, dir, id: newKey, launchSource };
301
378
  }
302
379
  case 'stop_cli': {
303
380
  const cliType = args?.cliType;
@@ -314,11 +391,32 @@ export class DaemonCliManager {
314
391
  }
315
392
  case 'restart_session': {
316
393
  const cliType = args?.cliType || args?.agentType || args?.ideType;
317
- const dir = args?.dir || process.cwd();
394
+ const cfg = loadConfig();
395
+ const rdir = resolveLaunchDirectory(
396
+ {
397
+ dir: args?.dir,
398
+ workspaceId: args?.workspaceId,
399
+ useDefaultWorkspace: args?.useDefaultWorkspace === true,
400
+ useHome: args?.useHome === true,
401
+ },
402
+ cfg,
403
+ );
404
+ if (!rdir.ok) {
405
+ const ws = getWorkspaceState(cfg);
406
+ return {
407
+ success: false,
408
+ error: rdir.message,
409
+ code: rdir.code,
410
+ workspaces: ws.workspaces,
411
+ defaultWorkspacePath: ws.defaultWorkspacePath,
412
+ };
413
+ }
414
+ const dir = rdir.path;
318
415
  if (!cliType) throw new Error('cliType required');
319
416
  const found = this.findAdapter(cliType, { instanceKey: args?._targetInstance, dir });
320
417
  if (found) await this.stopSession(found.key);
321
418
  await this.startSession(cliType, dir);
419
+ this.persistRecentDir(cliType, dir);
322
420
  return { success: true, restarted: true };
323
421
  }
324
422
  case 'agent_command': {
@@ -23,6 +23,9 @@ import { LOG } from '../logging/logger.js';
23
23
  import * as Chat from './chat-commands.js';
24
24
  import * as Cdp from './cdp-commands.js';
25
25
  import * as Stream from './stream-commands.js';
26
+ import * as WorkspaceCmd from './workspace-commands.js';
27
+ import { getWorkspaceState } from '../config/workspaces.js';
28
+ import { getWorkspaceActivity } from '../config/workspace-activity.js';
26
29
 
27
30
  export interface CommandResult {
28
31
  success: boolean;
@@ -176,6 +179,11 @@ export class DaemonCommandHandler implements CommandHelpers {
176
179
 
177
180
  /** Extract ideType from _targetInstance */
178
181
  private extractIdeType(args: any): string | undefined {
182
+ // Also accept explicit ideType from args (agentType for extensions)
183
+ if (args?.ideType && this._ctx.cdpManagers.has(args.ideType)) {
184
+ return args.ideType;
185
+ }
186
+
179
187
  if (args?._targetInstance) {
180
188
  let raw = args._targetInstance as string;
181
189
  const ideMatch = raw.match(/:ide:(.+)$/);
@@ -189,8 +197,25 @@ export class DaemonCommandHandler implements CommandHelpers {
189
197
  return this._ctx.instanceIdMap.get(raw)!;
190
198
  }
191
199
 
200
+ // Direct CDP manager key match (e.g. "cursor", "antigravity")
201
+ if (this._ctx.cdpManagers.has(raw)) {
202
+ return raw;
203
+ }
204
+
205
+ // Fallback: if no structured format matched and raw looks like a machine ID
206
+ // (e.g. "standalone_hostname"), find first available connected CDP
207
+ if (!ideMatch && !cliMatch && !acpMatch) {
208
+ for (const [key, mgr] of this._ctx.cdpManagers.entries()) {
209
+ if (mgr.isConnected) return key;
210
+ }
211
+ }
212
+
213
+ // Legacy: strip trailing _N suffix (e.g. "cursor_1" → "cursor")
192
214
  const lastUnderscore = raw.lastIndexOf('_');
193
- if (lastUnderscore > 0) return raw.substring(0, lastUnderscore);
215
+ if (lastUnderscore > 0) {
216
+ const stripped = raw.substring(0, lastUnderscore);
217
+ if (this._ctx.cdpManagers.has(stripped)) return stripped;
218
+ }
194
219
  return raw;
195
220
  }
196
221
  return undefined;
@@ -273,6 +298,17 @@ export class DaemonCommandHandler implements CommandHelpers {
273
298
  case 'get_commands':
274
299
  return { success: false, error: `${cmd} requires bridge-extension (removed)` };
275
300
  case 'get_recent_workspaces': return this.handleGetRecentWorkspaces(args);
301
+ case 'get_cli_history': {
302
+ const config = loadConfig();
303
+ return { success: true, history: config.cliHistory || [] };
304
+ }
305
+
306
+ case 'workspace_list': return WorkspaceCmd.handleWorkspaceList();
307
+ case 'workspace_add': return WorkspaceCmd.handleWorkspaceAdd(args);
308
+ case 'workspace_remove': return WorkspaceCmd.handleWorkspaceRemove(args);
309
+ case 'workspace_set_default':
310
+ case 'workspace_set_active':
311
+ return WorkspaceCmd.handleWorkspaceSetDefault(args);
276
312
 
277
313
  // ─── Script manage ───────────────────
278
314
  case 'refresh_scripts': return this.handleRefreshScripts(args);
@@ -312,10 +348,18 @@ export class DaemonCommandHandler implements CommandHelpers {
312
348
 
313
349
  // ─── Misc (kept in handler — too small to extract) ───────
314
350
 
315
- private async handleGetRecentWorkspaces(args: any): Promise<CommandResult> {
351
+ private async handleGetRecentWorkspaces(_args: any): Promise<CommandResult> {
316
352
  const config = loadConfig();
317
353
  const cliRecent = config.recentCliWorkspaces || [];
318
- return { success: true, result: cliRecent };
354
+ const ws = getWorkspaceState(config);
355
+ return {
356
+ success: true,
357
+ result: cliRecent,
358
+ workspaces: ws.workspaces,
359
+ defaultWorkspaceId: ws.defaultWorkspaceId,
360
+ defaultWorkspacePath: ws.defaultWorkspacePath,
361
+ activity: getWorkspaceActivity(config, 25),
362
+ };
319
363
  }
320
364
 
321
365
  private async handleRefreshScripts(_args: any): Promise<CommandResult> {
@@ -17,6 +17,9 @@ 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
19
  import { launchWithCdp } from '../launch.js';
20
+ import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
21
+ import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
22
+ import { appendWorkspaceActivity } from '../config/workspace-activity.js';
20
23
  import { detectIDEs } from '../detection/ide-detector.js';
21
24
  import { LOG } from '../logging/logger.js';
22
25
  import { logCommand } from '../logging/command-log.js';
@@ -185,8 +188,20 @@ export class DaemonCommandRouter {
185
188
 
186
189
  // ─── IDE launch + CDP connect ───
187
190
  case 'launch_ide': {
188
- const launchArgs = { ...args, ideId: args?.ideId || args?.ideType };
189
- const ideKey = launchArgs.ideId;
191
+ const ideKey = args?.ideId || args?.ideType;
192
+ const resolvedWorkspace = resolveIdeLaunchWorkspace(
193
+ {
194
+ workspace: args?.workspace,
195
+ workspaceId: args?.workspaceId,
196
+ useDefaultWorkspace: args?.useDefaultWorkspace,
197
+ },
198
+ loadConfig(),
199
+ );
200
+ const launchArgs = {
201
+ ideId: ideKey,
202
+ workspace: resolvedWorkspace,
203
+ newWindow: args?.newWindow,
204
+ };
190
205
  LOG.info('LaunchIDE', `target=${ideKey || 'auto'}`);
191
206
  const result = await launchWithCdp(launchArgs);
192
207
 
@@ -209,6 +224,14 @@ export class DaemonCommandRouter {
209
224
  }
210
225
  }
211
226
  this.deps.onIdeConnected?.();
227
+ if (result.success && resolvedWorkspace) {
228
+ try {
229
+ saveConfig(appendWorkspaceActivity(loadConfig(), resolvedWorkspace, {
230
+ kind: 'ide',
231
+ agentType: result.ideId,
232
+ }));
233
+ } catch { /* ignore activity persist errors */ }
234
+ }
212
235
  return { success: result.success, ...result as any };
213
236
  }
214
237
 
@@ -217,6 +240,13 @@ export class DaemonCommandRouter {
217
240
  this.deps.detectedIdes.value = await detectIDEs();
218
241
  return { success: true, ides: this.deps.detectedIdes.value };
219
242
  }
243
+
244
+ // ─── Machine Settings ───
245
+ case 'set_machine_nickname': {
246
+ const nickname = args?.nickname;
247
+ updateConfig({ machineNickname: nickname || null });
248
+ return { success: true };
249
+ }
220
250
  }
221
251
 
222
252
  return null; // Not handled at this level → delegate to CommandHandler
@@ -0,0 +1,108 @@
1
+ /**
2
+ * workspace_* commands — list/add/remove/default (config.json)
3
+ */
4
+
5
+ import { loadConfig, saveConfig } from '../config/config.js';
6
+ import * as W from '../config/workspaces.js';
7
+ import { appendWorkspaceActivity, getWorkspaceActivity, removeActivityForPath } from '../config/workspace-activity.js';
8
+
9
+ export type WorkspaceCommandResult = { success: boolean;[key: string]: unknown };
10
+
11
+ export function handleWorkspaceList(): WorkspaceCommandResult {
12
+ const config = loadConfig();
13
+ const state = W.getWorkspaceState(config);
14
+ return {
15
+ success: true,
16
+ workspaces: state.workspaces,
17
+ defaultWorkspaceId: state.defaultWorkspaceId,
18
+ defaultWorkspacePath: state.defaultWorkspacePath,
19
+ legacyRecentPaths: config.recentCliWorkspaces || [],
20
+ activity: getWorkspaceActivity(config, 25),
21
+ };
22
+ }
23
+
24
+ export function handleWorkspaceAdd(args: any): WorkspaceCommandResult {
25
+ const rawPath = (args?.path || args?.dir || '').trim();
26
+ const label = (args?.label || '').trim() || undefined;
27
+ if (!rawPath) return { success: false, error: 'path required' };
28
+
29
+ const config = loadConfig();
30
+ const result = W.addWorkspaceEntry(config, rawPath, label);
31
+ if ('error' in result) return { success: false, error: result.error };
32
+
33
+ let cfg = appendWorkspaceActivity(result.config, result.entry.path, {});
34
+ saveConfig(cfg);
35
+ const state = W.getWorkspaceState(cfg);
36
+ return { success: true, entry: result.entry, ...state, activity: getWorkspaceActivity(cfg, 25) };
37
+ }
38
+
39
+ export function handleWorkspaceRemove(args: any): WorkspaceCommandResult {
40
+ const id = (args?.id || '').trim();
41
+ if (!id) return { success: false, error: 'id required' };
42
+
43
+ const config = loadConfig();
44
+ const removed = (config.workspaces || []).find(w => w.id === id);
45
+ const result = W.removeWorkspaceEntry(config, id);
46
+ if ('error' in result) return { success: false, error: result.error };
47
+
48
+ let cfg = result.config;
49
+ if (removed) {
50
+ cfg = removeActivityForPath(cfg, removed.path);
51
+ }
52
+ saveConfig(cfg);
53
+ const state = W.getWorkspaceState(cfg);
54
+ return { success: true, removedId: id, ...state, activity: getWorkspaceActivity(cfg, 25) };
55
+ }
56
+
57
+ export function handleWorkspaceSetDefault(args: any): WorkspaceCommandResult {
58
+ const clear = args?.clear === true || args?.id === null || args?.id === '';
59
+ if (clear) {
60
+ const config = loadConfig();
61
+ const result = W.setDefaultWorkspaceId(config, null);
62
+ if ('error' in result) return { success: false, error: result.error };
63
+ saveConfig(result.config);
64
+ const state = W.getWorkspaceState(result.config);
65
+ return {
66
+ success: true,
67
+ ...state,
68
+ activity: getWorkspaceActivity(result.config, 25),
69
+ };
70
+ }
71
+
72
+ const pathArg = (args?.path != null && String(args.path).trim()) ? String(args.path).trim() : '';
73
+ const idArg = args?.id !== undefined && args?.id !== null && String(args.id).trim()
74
+ ? String(args.id).trim()
75
+ : '';
76
+
77
+ if (!pathArg && !idArg) {
78
+ return { success: false, error: 'id or path required (or clear: true)' };
79
+ }
80
+
81
+ let config = loadConfig();
82
+ let nextId: string;
83
+
84
+ if (pathArg) {
85
+ let w = W.findWorkspaceByPath(config, pathArg);
86
+ if (!w) {
87
+ const add = W.addWorkspaceEntry(config, pathArg);
88
+ if ('error' in add) return { success: false, error: add.error };
89
+ config = add.config;
90
+ w = add.entry;
91
+ }
92
+ nextId = w.id;
93
+ } else {
94
+ nextId = idArg;
95
+ }
96
+
97
+ const result = W.setDefaultWorkspaceId(config, nextId);
98
+ if ('error' in result) return { success: false, error: result.error };
99
+
100
+ let out = result.config;
101
+ const ap = W.getDefaultWorkspacePath(out);
102
+ if (ap) {
103
+ out = appendWorkspaceActivity(out, ap, { kind: 'default' });
104
+ }
105
+ saveConfig(out);
106
+ const state = W.getWorkspaceState(out);
107
+ return { success: true, ...state, activity: getWorkspaceActivity(out, 25) };
108
+ }
@@ -7,6 +7,11 @@
7
7
  import { homedir } from 'os';
8
8
  import { join } from 'path';
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
10
+ import { migrateWorkspacesFromRecent } from './workspaces.js';
11
+ import type { WorkspaceEntry } from './workspaces.js';
12
+ import type { WorkspaceActivityEntry } from './workspace-activity.js';
13
+ export type { WorkspaceEntry } from './workspaces.js';
14
+ export type { WorkspaceActivityEntry } from './workspace-activity.js';
10
15
 
11
16
  export interface ADHDevConfig {
12
17
  // Server connection
@@ -42,6 +47,14 @@ export interface ADHDevConfig {
42
47
  enabledIdes: string[];
43
48
  recentCliWorkspaces: string[];
44
49
 
50
+ /** Saved workspaces for IDE/CLI/ACP launch (daemon-local) */
51
+ workspaces?: WorkspaceEntry[];
52
+ /** Default workspace id (from workspaces[]) — never used implicitly for launch */
53
+ defaultWorkspaceId?: string | null;
54
+
55
+ /** Recently used workspaces (IDE / CLI / ACP / default) for quick resume */
56
+ recentWorkspaceActivity?: WorkspaceActivityEntry[];
57
+
45
58
  // Machine nickname (user-customizable label for this machine)
46
59
  machineNickname: string | null;
47
60
 
@@ -81,6 +94,9 @@ const DEFAULT_CONFIG: ADHDevConfig = {
81
94
  configuredCLIs: [],
82
95
  enabledIdes: [],
83
96
  recentCliWorkspaces: [],
97
+ workspaces: [],
98
+ defaultWorkspaceId: null,
99
+ recentWorkspaceActivity: [],
84
100
  machineNickname: null,
85
101
  cliHistory: [],
86
102
  providerSettings: {},
@@ -118,7 +134,19 @@ export function loadConfig(): ADHDevConfig {
118
134
  try {
119
135
  const raw = readFileSync(configPath, 'utf-8');
120
136
  const parsed = JSON.parse(raw);
121
- return { ...DEFAULT_CONFIG, ...parsed };
137
+ const merged = { ...DEFAULT_CONFIG, ...parsed } as ADHDevConfig & { activeWorkspaceId?: string | null };
138
+ if (merged.defaultWorkspaceId == null && merged.activeWorkspaceId != null) {
139
+ (merged as ADHDevConfig).defaultWorkspaceId = merged.activeWorkspaceId;
140
+ }
141
+ delete (merged as any).activeWorkspaceId;
142
+ const hadStoredWorkspaces = Array.isArray(parsed.workspaces) && parsed.workspaces.length > 0;
143
+ migrateWorkspacesFromRecent(merged);
144
+ if (!hadStoredWorkspaces && (merged.workspaces?.length || 0) > 0) {
145
+ try {
146
+ saveConfig(merged);
147
+ } catch { /* ignore */ }
148
+ }
149
+ return merged;
122
150
  } catch {
123
151
  return { ...DEFAULT_CONFIG };
124
152
  }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Recent workspace activity — quick "pick up where you left off" (daemon-local).
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import type { ADHDevConfig } from './config.js';
7
+ import { expandPath } from './workspaces.js';
8
+
9
+ export interface WorkspaceActivityEntry {
10
+ path: string;
11
+ lastUsedAt: number;
12
+ /** `active` legacy — same meaning as default */
13
+ kind?: 'ide' | 'cli' | 'acp' | 'default' | 'active';
14
+ /** IDE id or CLI/ACP provider type */
15
+ agentType?: string;
16
+ }
17
+
18
+ const MAX_ACTIVITY = 30;
19
+
20
+ export function normWorkspacePath(p: string): string {
21
+ try {
22
+ return path.resolve(expandPath(p));
23
+ } catch {
24
+ return path.resolve(p);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Append or bump a path to the front of recent activity (returns new config object).
30
+ */
31
+ export function appendWorkspaceActivity(
32
+ config: ADHDevConfig,
33
+ rawPath: string,
34
+ meta?: { kind?: WorkspaceActivityEntry['kind']; agentType?: string },
35
+ ): ADHDevConfig {
36
+ const abs = normWorkspacePath(rawPath);
37
+ if (!abs) return config;
38
+
39
+ const prev = config.recentWorkspaceActivity || [];
40
+ const filtered = prev.filter(e => normWorkspacePath(e.path) !== abs);
41
+ const entry: WorkspaceActivityEntry = {
42
+ path: abs,
43
+ lastUsedAt: Date.now(),
44
+ kind: meta?.kind,
45
+ agentType: meta?.agentType,
46
+ };
47
+ const recentWorkspaceActivity = [entry, ...filtered].slice(0, MAX_ACTIVITY);
48
+ return { ...config, recentWorkspaceActivity };
49
+ }
50
+
51
+ export function getWorkspaceActivity(config: ADHDevConfig, limit = 20): WorkspaceActivityEntry[] {
52
+ const list = [...(config.recentWorkspaceActivity || [])];
53
+ list.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
54
+ return list.slice(0, limit);
55
+ }
56
+
57
+ export function removeActivityForPath(config: ADHDevConfig, rawPath: string): ADHDevConfig {
58
+ const n = normWorkspacePath(rawPath);
59
+ return {
60
+ ...config,
61
+ recentWorkspaceActivity: (config.recentWorkspaceActivity || []).filter(
62
+ e => normWorkspacePath(e.path) !== n,
63
+ ),
64
+ };
65
+ }