@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
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Saved workspaces — shared by IDE launch, CLI, ACP (daemon-local).
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as os from 'os';
7
+ import * as path from 'path';
8
+ import { randomUUID } from 'crypto';
9
+ import type { ADHDevConfig } from './config.js';
10
+
11
+ export interface WorkspaceEntry {
12
+ id: string;
13
+ path: string;
14
+ label?: string;
15
+ addedAt: number;
16
+ }
17
+
18
+ const MAX_WORKSPACES = 50;
19
+
20
+ export function expandPath(p: string): string {
21
+ const t = (p || '').trim();
22
+ if (!t) return '';
23
+ if (t.startsWith('~')) return path.join(os.homedir(), t.slice(1).replace(/^\//, ''));
24
+ return path.resolve(t);
25
+ }
26
+
27
+ export function validateWorkspacePath(absPath: string): { ok: true } | { ok: false; error: string } {
28
+ try {
29
+ if (!absPath) return { ok: false, error: 'Path required' };
30
+ if (!fs.existsSync(absPath)) return { ok: false, error: 'Path does not exist' };
31
+ const st = fs.statSync(absPath);
32
+ if (!st.isDirectory()) return { ok: false, error: 'Not a directory' };
33
+ return { ok: true };
34
+ } catch (e: any) {
35
+ return { ok: false, error: e?.message || 'Invalid path' };
36
+ }
37
+ }
38
+
39
+ /** Default workspace label from path */
40
+ export function defaultWorkspaceLabel(absPath: string): string {
41
+ const base = path.basename(absPath) || absPath;
42
+ return base;
43
+ }
44
+
45
+ /**
46
+ * Ensure config.workspaces exists; seed from recentCliWorkspaces once (same paths).
47
+ */
48
+ export function migrateWorkspacesFromRecent(config: ADHDevConfig): ADHDevConfig {
49
+ if (!config.workspaces) config.workspaces = [];
50
+ if (config.workspaces.length > 0) return config;
51
+
52
+ const recent = config.recentCliWorkspaces || [];
53
+ const now = Date.now();
54
+ for (const raw of recent) {
55
+ const abs = expandPath(raw);
56
+ if (!abs || validateWorkspacePath(abs).ok !== true) continue;
57
+ if (config.workspaces.some(w => path.resolve(w.path) === abs)) continue;
58
+ config.workspaces.push({
59
+ id: randomUUID(),
60
+ path: abs,
61
+ label: defaultWorkspaceLabel(abs),
62
+ addedAt: now,
63
+ });
64
+ if (config.workspaces.length >= MAX_WORKSPACES) break;
65
+ }
66
+ return config;
67
+ }
68
+
69
+ export function getDefaultWorkspacePath(config: ADHDevConfig): string | null {
70
+ const id = config.defaultWorkspaceId;
71
+ if (!id) return null;
72
+ const w = (config.workspaces || []).find(x => x.id === id);
73
+ if (!w) return null;
74
+ const abs = expandPath(w.path);
75
+ if (validateWorkspacePath(abs).ok !== true) return null;
76
+ return abs;
77
+ }
78
+
79
+ export function getWorkspaceState(config: ADHDevConfig): {
80
+ workspaces: WorkspaceEntry[];
81
+ defaultWorkspaceId: string | null;
82
+ defaultWorkspacePath: string | null;
83
+ } {
84
+ const workspaces = [...(config.workspaces || [])].sort((a, b) => b.addedAt - a.addedAt);
85
+ const defaultWorkspacePath = getDefaultWorkspacePath(config);
86
+ return {
87
+ workspaces,
88
+ defaultWorkspaceId: config.defaultWorkspaceId ?? null,
89
+ defaultWorkspacePath,
90
+ };
91
+ }
92
+
93
+ export type LaunchDirectorySource = 'dir' | 'workspaceId' | 'defaultWorkspace' | 'home';
94
+
95
+ export type ResolveLaunchDirectoryResult =
96
+ | { ok: true; path: string; source: LaunchDirectorySource }
97
+ | { ok: false; code: 'WORKSPACE_LAUNCH_CONTEXT_REQUIRED'; message: string };
98
+
99
+ /**
100
+ * Resolve cwd for CLI/ACP. No implicit default workspace or home — caller must pass
101
+ * useDefaultWorkspace or useHome (or an explicit dir / workspaceId).
102
+ */
103
+ export function resolveLaunchDirectory(
104
+ args: {
105
+ dir?: string;
106
+ workspaceId?: string;
107
+ useDefaultWorkspace?: boolean;
108
+ useHome?: boolean;
109
+ } | undefined,
110
+ config: ADHDevConfig,
111
+ ): ResolveLaunchDirectoryResult {
112
+ const a = args || {};
113
+ if (a.dir != null && String(a.dir).trim()) {
114
+ const abs = expandPath(String(a.dir).trim());
115
+ if (abs && validateWorkspacePath(abs).ok === true) {
116
+ return { ok: true, path: abs, source: 'dir' };
117
+ }
118
+ return {
119
+ ok: false,
120
+ code: 'WORKSPACE_LAUNCH_CONTEXT_REQUIRED',
121
+ message: abs ? 'Directory path is not valid or does not exist' : 'Invalid directory path',
122
+ };
123
+ }
124
+ if (a.workspaceId) {
125
+ const w = (config.workspaces || []).find(x => x.id === a.workspaceId);
126
+ if (w) {
127
+ const abs = expandPath(w.path);
128
+ if (validateWorkspacePath(abs).ok === true) {
129
+ return { ok: true, path: abs, source: 'workspaceId' };
130
+ }
131
+ }
132
+ return {
133
+ ok: false,
134
+ code: 'WORKSPACE_LAUNCH_CONTEXT_REQUIRED',
135
+ message: 'Saved workspace not found or path is no longer valid',
136
+ };
137
+ }
138
+ if (a.useDefaultWorkspace === true) {
139
+ const d = getDefaultWorkspacePath(config);
140
+ if (d) return { ok: true, path: d, source: 'defaultWorkspace' };
141
+ return {
142
+ ok: false,
143
+ code: 'WORKSPACE_LAUNCH_CONTEXT_REQUIRED',
144
+ message: 'No default workspace is set',
145
+ };
146
+ }
147
+ if (a.useHome === true) {
148
+ return { ok: true, path: os.homedir(), source: 'home' };
149
+ }
150
+ return {
151
+ ok: false,
152
+ code: 'WORKSPACE_LAUNCH_CONTEXT_REQUIRED',
153
+ message: 'Choose a directory, saved workspace, default workspace, or home before launching.',
154
+ };
155
+ }
156
+
157
+ /**
158
+ * IDE folder from explicit args only (`workspace`, `workspaceId`, or `useDefaultWorkspace: true`).
159
+ */
160
+ export function resolveIdeWorkspaceFromArgs(
161
+ args: {
162
+ workspace?: string;
163
+ workspaceId?: string;
164
+ useDefaultWorkspace?: boolean;
165
+ } | undefined,
166
+ config: ADHDevConfig,
167
+ ): string | undefined {
168
+ const ar = args || {};
169
+ if (ar.workspace) {
170
+ const abs = expandPath(ar.workspace);
171
+ if (abs && validateWorkspacePath(abs).ok === true) return abs;
172
+ }
173
+ if (ar.workspaceId) {
174
+ const w = (config.workspaces || []).find(x => x.id === ar.workspaceId);
175
+ if (w) {
176
+ const abs = expandPath(w.path);
177
+ if (validateWorkspacePath(abs).ok === true) return abs;
178
+ }
179
+ }
180
+ if (ar.useDefaultWorkspace === true) {
181
+ return getDefaultWorkspacePath(config) || undefined;
182
+ }
183
+ return undefined;
184
+ }
185
+
186
+ /**
187
+ * IDE launch folder — same saved workspaces + default as CLI/ACP.
188
+ * After explicit `workspace` / `workspaceId` / `useDefaultWorkspace: true`, falls back to
189
+ * config default workspace when set. Pass `useDefaultWorkspace: false` to open IDE without that folder.
190
+ */
191
+ export function resolveIdeLaunchWorkspace(
192
+ args: {
193
+ workspace?: string;
194
+ workspaceId?: string;
195
+ useDefaultWorkspace?: boolean;
196
+ } | undefined,
197
+ config: ADHDevConfig,
198
+ ): string | undefined {
199
+ const direct = resolveIdeWorkspaceFromArgs(args, config);
200
+ if (direct) return direct;
201
+ if (args?.useDefaultWorkspace === false) return undefined;
202
+ return getDefaultWorkspacePath(config) || undefined;
203
+ }
204
+
205
+ export function findWorkspaceByPath(config: ADHDevConfig, rawPath: string): WorkspaceEntry | undefined {
206
+ const abs = path.resolve(expandPath(rawPath));
207
+ if (!abs) return undefined;
208
+ return (config.workspaces || []).find(w => path.resolve(expandPath(w.path)) === abs);
209
+ }
210
+
211
+ export function addWorkspaceEntry(config: ADHDevConfig, rawPath: string, label?: string): { config: ADHDevConfig; entry: WorkspaceEntry } | { error: string } {
212
+ const abs = expandPath(rawPath);
213
+ const v = validateWorkspacePath(abs);
214
+ if (!v.ok) return { error: v.error };
215
+
216
+ const list = [...(config.workspaces || [])];
217
+ if (list.some(w => path.resolve(w.path) === abs)) {
218
+ return { error: 'Workspace already in list' };
219
+ }
220
+ if (list.length >= MAX_WORKSPACES) {
221
+ return { error: `Maximum ${MAX_WORKSPACES} workspaces` };
222
+ }
223
+ const entry: WorkspaceEntry = {
224
+ id: randomUUID(),
225
+ path: abs,
226
+ label: (label || '').trim() || defaultWorkspaceLabel(abs),
227
+ addedAt: Date.now(),
228
+ };
229
+ list.push(entry);
230
+ return { config: { ...config, workspaces: list }, entry };
231
+ }
232
+
233
+ export function removeWorkspaceEntry(config: ADHDevConfig, id: string): { config: ADHDevConfig } | { error: string } {
234
+ const list = (config.workspaces || []).filter(w => w.id !== id);
235
+ if (list.length === (config.workspaces || []).length) return { error: 'Workspace not found' };
236
+ let defaultWorkspaceId = config.defaultWorkspaceId;
237
+ if (defaultWorkspaceId === id) defaultWorkspaceId = null;
238
+ return { config: { ...config, workspaces: list, defaultWorkspaceId } };
239
+ }
240
+
241
+ export function setDefaultWorkspaceId(config: ADHDevConfig, id: string | null): { config: ADHDevConfig } | { error: string } {
242
+ if (id === null) {
243
+ return { config: { ...config, defaultWorkspaceId: null } };
244
+ }
245
+ const w = (config.workspaces || []).find(x => x.id === id);
246
+ if (!w) return { error: 'Workspace not found' };
247
+ const abs = expandPath(w.path);
248
+ if (validateWorkspacePath(abs).ok !== true) return { error: 'Workspace path is no longer valid' };
249
+ return { config: { ...config, defaultWorkspaceId: id } };
250
+ }
@@ -2238,7 +2238,7 @@ export class DevServer {
2238
2238
  lines.push('## Required Return Format');
2239
2239
  lines.push('| Function | Return JSON |');
2240
2240
  lines.push('|---|---|');
2241
- lines.push('| readChat | `{ id, status, title, messages: [{role, content, index}], inputContent, activeModal }` |');
2241
+ lines.push('| readChat | `{ id, status, title, messages: [{role, content, index, kind?, meta?}], inputContent, activeModal }` — optional `kind`: standard, thought, tool, terminal; optional `meta`: e.g. `{ label, isRunning }` for dashboard |');
2242
2242
  lines.push('| sendMessage | `{ sent: false, needsTypeAndSend: true, selector }` |');
2243
2243
  lines.push('| resolveAction | `{ resolved: true/false, clicked? }` |');
2244
2244
  lines.push('| listSessions | `{ sessions: [{ id, title, active, index }] }` |');
package/src/index.ts CHANGED
@@ -29,11 +29,16 @@ export type { IDaemonCore, DaemonCoreOptions } from './daemon-core.js';
29
29
 
30
30
  // ── Config ──
31
31
  export { loadConfig, saveConfig, resetConfig, isSetupComplete, addCliHistory, markSetupComplete, updateConfig } from './config/config.js';
32
+ export { getWorkspaceState } from './config/workspaces.js';
33
+ export { getWorkspaceActivity } from './config/workspace-activity.js';
34
+ export type { WorkspaceEntry } from './config/workspaces.js';
32
35
 
33
36
  // ── Detection ──
34
37
  export { detectIDEs } from './detection/ide-detector.js';
35
38
  export type { IDEInfo } from './detection/ide-detector.js';
36
39
  export { detectCLIs } from './detection/cli-detector.js';
40
+ export { getHostMemorySnapshot } from './system/host-memory.js';
41
+ export type { HostMemorySnapshot } from './system/host-memory.js';
37
42
 
38
43
  // ── CDP ──
39
44
  export { DaemonCdpManager } from './cdp/manager.js';
package/src/launch.ts CHANGED
@@ -224,7 +224,7 @@ function detectCurrentWorkspace(ideId: string): string | undefined {
224
224
  );
225
225
  if (fs.existsSync(storagePath)) {
226
226
  const data = JSON.parse(fs.readFileSync(storagePath, 'utf-8'));
227
- // openedPathsList.workspaces3 has recent folders
227
+ // openedPathsList.workspaces3 has recent workspace paths
228
228
  const workspaces = data?.openedPathsList?.workspaces3 || data?.openedPathsList?.entries || [];
229
229
  if (workspaces.length > 0) {
230
230
  const recent = workspaces[0];
@@ -13,6 +13,7 @@ import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
13
13
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
14
14
  import { StatusMonitor } from './status-monitor.js';
15
15
  import { ChatHistoryWriter } from '../config/chat-history.js';
16
+ import { LOG } from '../logging/logger.js';
16
17
 
17
18
  export class CliProviderInstance implements ProviderInstance {
18
19
  readonly type: string;
@@ -156,18 +157,22 @@ export class CliProviderInstance implements ProviderInstance {
156
157
  const chatTitle = `${this.provider.name} · ${dirName}`;
157
158
 
158
159
  if (newStatus !== this.lastStatus) {
160
+ LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
159
161
  if (this.lastStatus === 'idle' && newStatus === 'generating') {
160
162
  this.generatingStartedAt = now;
161
163
  this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now });
162
164
  } else if (newStatus === 'waiting_approval') {
163
165
  if (!this.generatingStartedAt) this.generatingStartedAt = now;
166
+ const modal = adapterStatus.activeModal;
167
+ LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
164
168
  this.pushEvent({
165
169
  event: 'agent:waiting_approval', chatTitle, timestamp: now,
166
- modalMessage: adapterStatus.activeModal?.message,
167
- modalButtons: adapterStatus.activeModal?.buttons,
170
+ modalMessage: modal?.message,
171
+ modalButtons: modal?.buttons,
168
172
  });
169
173
  } else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
170
174
  const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
175
+ LOG.info('CLI', `[${this.type}] completed in ${duration}s`);
171
176
  this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now });
172
177
  this.generatingStartedAt = 0;
173
178
  } else if (newStatus === 'stopped') {
@@ -274,6 +274,17 @@ export class IdeProviderInstance implements ProviderInstance {
274
274
  msg.receivedAt = prevByHash.get(h) || now;
275
275
  }
276
276
 
277
+ // Filter messages by provider settings (showThinking, showToolCalls, showTerminal)
278
+ if (raw.messages?.length > 0) {
279
+ const hiddenKinds = new Set<string>();
280
+ if (this.settings.showThinking === false) hiddenKinds.add('thought');
281
+ if (this.settings.showToolCalls === false) hiddenKinds.add('tool');
282
+ if (this.settings.showTerminal === false) hiddenKinds.add('terminal');
283
+ if (hiddenKinds.size > 0) {
284
+ raw.messages = raw.messages.filter((m: any) => !hiddenKinds.has(m.kind));
285
+ }
286
+ }
287
+
277
288
  this.cachedChat = { ...raw, activeModal };
278
289
  this.detectAgentTransitions(raw, now);
279
290
 
@@ -8,6 +8,9 @@
8
8
  import * as os from 'os';
9
9
  import * as path from 'path';
10
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';
11
14
  import { LOG } from '../logging/logger.js';
12
15
 
13
16
  // ─── Daemon dependency interface ──────────────────────
@@ -95,6 +98,11 @@ export class DaemonStatusReporter {
95
98
  // (agent-stream polling backward compat)
96
99
  updateAgentStreams(_ideType: string, _streams: any[]): void { /* Managed by Instance itself */ }
97
100
 
101
+ /** Reset P2P dedup hash — forces next send to transmit even if content unchanged */
102
+ resetP2PHash(): void {
103
+ this.lastP2PStatusHash = '';
104
+ }
105
+
98
106
  // ─── Core ────────────────────────────────────────
99
107
 
100
108
  private ts(): string {
@@ -197,17 +205,26 @@ export class DaemonStatusReporter {
197
205
 
198
206
 
199
207
 
208
+ const cfg = loadConfig();
209
+ const wsState = getWorkspaceState(cfg);
210
+ const memSnap = getHostMemorySnapshot();
211
+
200
212
  // ═══ Assemble payload (P2P — required data only) ═══
201
213
  const payload: Record<string, any> = {
202
214
  daemonMode: true,
203
- machineNickname: loadConfig().machineNickname || null,
215
+ machineNickname: cfg.machineNickname || null,
216
+ workspaces: wsState.workspaces,
217
+ defaultWorkspaceId: wsState.defaultWorkspaceId,
218
+ defaultWorkspacePath: wsState.defaultWorkspacePath,
219
+ workspaceActivity: getWorkspaceActivity(cfg, 15),
204
220
  machine: {
205
221
  hostname: os.hostname(),
206
222
  platform: os.platform(),
207
223
  arch: os.arch(),
208
224
  cpus: os.cpus().length,
209
- totalMem: os.totalmem(),
210
- freeMem: os.freemem(),
225
+ totalMem: memSnap.totalMem,
226
+ freeMem: memSnap.freeMem,
227
+ availableMem: memSnap.availableMem,
211
228
  loadavg: os.loadavg(),
212
229
  uptime: os.uptime(),
213
230
  },
@@ -243,6 +260,8 @@ export class DaemonStatusReporter {
243
260
  const wsPayload = {
244
261
  daemonMode: true,
245
262
  machineNickname: payload.machineNickname,
263
+ defaultWorkspaceId: wsState.defaultWorkspaceId,
264
+ workspaceCount: (wsState.workspaces || []).length,
246
265
  // managedIdes: server only saves id, type, cdpConnected
247
266
  managedIdes: managedIdes.map(ide => ({
248
267
  ideType: ide.ideType,
@@ -271,7 +290,7 @@ export class DaemonStatusReporter {
271
290
  private sendP2PPayload(payload: Record<string, any>): boolean {
272
291
  const { timestamp: _ts, system: _sys, ...hashTarget } = payload;
273
292
  if (hashTarget.machine) {
274
- const { freeMem: _f, loadavg: _l, uptime: _u, ...stableMachine } = hashTarget.machine as any;
293
+ const { freeMem: _f, availableMem: _a, loadavg: _l, uptime: _u, ...stableMachine } = hashTarget.machine as any;
275
294
  hashTarget.machine = stableMachine;
276
295
  }
277
296
  const h = this.simpleHash(JSON.stringify(hashTarget));
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Host memory metrics — macOS-aware "available" memory.
3
+ *
4
+ * Node's os.freemem() on darwin reports only the tiny truly-free pool; most RAM
5
+ * sits in inactive/file-backed cache that the OS can reclaim. Dashboard "used %"
6
+ * based on (total - freemem) looks ~99% almost always — misleading.
7
+ *
8
+ * On macOS we parse `vm_stat` and approximate available bytes as:
9
+ * (free + inactive + speculative + purgeable [+ file_backed]) × page size
10
+ * (aligned with common Activity Monitor–style interpretations.)
11
+ */
12
+
13
+ import * as os from 'os';
14
+ import { execSync } from 'child_process';
15
+
16
+ export interface HostMemorySnapshot {
17
+ totalMem: number;
18
+ /** Raw kernel "free" — small on macOS; kept for debugging / API compat */
19
+ freeMem: number;
20
+ /** Use this for UI "used %" — on darwin from vm_stat; else equals freeMem */
21
+ availableMem: number;
22
+ }
23
+
24
+ function parseDarwinAvailableBytes(totalMem: number): number | null {
25
+ if (os.platform() !== 'darwin') return null;
26
+ try {
27
+ const out = execSync('vm_stat', {
28
+ encoding: 'utf-8',
29
+ timeout: 4000,
30
+ maxBuffer: 256 * 1024,
31
+ });
32
+ const pageSizeMatch = out.match(/page size of (\d+)\s*bytes/i);
33
+ const pageSize = pageSizeMatch ? parseInt(pageSizeMatch[1], 10) : 4096;
34
+
35
+ const counts: Record<string, number> = {};
36
+ for (const line of out.split('\n')) {
37
+ const m = line.match(/^\s*Pages\s+([^:]+):\s+([\d,]+)\s*\.?/);
38
+ if (!m) continue;
39
+ const key = m[1].trim().toLowerCase().replace(/\s+/g, '_');
40
+ const n = parseInt(m[2].replace(/,/g, ''), 10);
41
+ if (!Number.isNaN(n)) counts[key] = n;
42
+ }
43
+
44
+ const free = counts['free'] ?? 0;
45
+ const inactive = counts['inactive'] ?? 0;
46
+ const speculative = counts['speculative'] ?? 0;
47
+ const purgeable = counts['purgeable'] ?? 0;
48
+ const fileBacked = counts['file_backed'] ?? 0;
49
+
50
+ const availPages = free + inactive + speculative + purgeable + fileBacked;
51
+ const bytes = availPages * pageSize;
52
+ if (!Number.isFinite(bytes) || bytes < 0) return null;
53
+ return Math.min(bytes, totalMem);
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export function getHostMemorySnapshot(): HostMemorySnapshot {
60
+ const totalMem = os.totalmem();
61
+ const freeMem = os.freemem();
62
+ const darwinAvail = parseDarwinAvailableBytes(totalMem);
63
+ const availableMem = darwinAvail != null ? darwinAvail : freeMem;
64
+ return { totalMem, freeMem, availableMem };
65
+ }
package/src/types.ts CHANGED
@@ -43,7 +43,7 @@ export interface IdeEntry {
43
43
  agentStreams?: AgentStreamEntry[];
44
44
  /** Extension agents monitored via CDP */
45
45
  extensions?: ExtensionInfo[];
46
- /** Workspace folders */
46
+ /** IDE-reported workspace roots (name + path) */
47
47
  workspaceFolders?: { name: string; path: string }[];
48
48
  /** Active file path */
49
49
  activeFile?: string | null;
@@ -150,6 +150,8 @@ export interface SystemInfo {
150
150
  cpus: number;
151
151
  totalMem: number;
152
152
  freeMem: number;
153
+ /** macOS: reclaimable-inclusive; prefer for UI used% (see host-memory.ts) */
154
+ availableMem?: number;
153
155
  loadavg: number[];
154
156
  uptime: number;
155
157
  arch: string;
@@ -183,6 +185,11 @@ export interface StatusResponse {
183
185
  detectedIdes: DetectedIde[];
184
186
  availableProviders: ProviderInfo[];
185
187
  system: SystemInfo;
188
+ /** Saved workspaces (standalone WS / HTTP status) */
189
+ workspaces?: { id: string; path: string; label?: string; addedAt: number }[];
190
+ defaultWorkspaceId?: string | null;
191
+ defaultWorkspacePath?: string | null;
192
+ workspaceActivity?: { path: string; lastUsedAt: number; kind?: string; agentType?: string }[];
186
193
  }
187
194
 
188
195
  /** Agent stream entry within an IDE */