@adhdev/daemon-core 0.8.19 → 0.8.20
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/dist/config/config.d.ts +1 -10
- package/dist/config/recent-activity.d.ts +7 -7
- package/dist/config/saved-sessions.d.ts +4 -4
- package/dist/config/state-store.d.ts +32 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +129 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +126 -61
- package/dist/index.mjs.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/commands/cli-manager.ts +5 -4
- package/src/commands/router.ts +11 -10
- package/src/config/config.d.ts +1 -10
- package/src/config/config.ts +47 -29
- package/src/config/recent-activity.ts +16 -16
- package/src/config/saved-sessions.ts +9 -9
- package/src/config/state-store.ts +94 -0
- package/src/index.ts +4 -0
- package/src/status/snapshot.ts +5 -3
package/package.json
CHANGED
|
@@ -11,7 +11,8 @@ import * as crypto from 'crypto';
|
|
|
11
11
|
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
|
-
import { loadConfig
|
|
14
|
+
import { loadConfig } from '../config/config.js';
|
|
15
|
+
import { loadState, saveState } from '../config/state-store.js';
|
|
15
16
|
import { getWorkspaceState, resolveLaunchDirectory } from '../config/workspaces.js';
|
|
16
17
|
import { appendRecentActivity } from '../config/recent-activity.js';
|
|
17
18
|
import { upsertSavedProviderSession } from '../config/saved-sessions.js';
|
|
@@ -260,9 +261,9 @@ export class DaemonCliManager {
|
|
|
260
261
|
title?: string;
|
|
261
262
|
}): void {
|
|
262
263
|
try {
|
|
263
|
-
let
|
|
264
|
+
let nextState = appendRecentActivity(loadState(), entry);
|
|
264
265
|
if (entry.providerSessionId && (entry.kind === 'cli' || entry.kind === 'acp')) {
|
|
265
|
-
|
|
266
|
+
nextState = upsertSavedProviderSession(nextState, {
|
|
266
267
|
kind: entry.kind,
|
|
267
268
|
providerType: entry.providerType,
|
|
268
269
|
providerName: entry.providerName,
|
|
@@ -272,7 +273,7 @@ export class DaemonCliManager {
|
|
|
272
273
|
title: entry.title,
|
|
273
274
|
});
|
|
274
275
|
}
|
|
275
|
-
|
|
276
|
+
saveState(nextState);
|
|
276
277
|
} catch (e) {
|
|
277
278
|
console.error(colorize('red', ` ✗ Failed to save recent activity: ${e}`));
|
|
278
279
|
}
|
package/src/commands/router.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
|
18
18
|
import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
|
|
19
19
|
import { launchWithCdp, killIdeProcess, isIdeRunning } from '../launch.js';
|
|
20
20
|
import { loadConfig, saveConfig, updateConfig } from '../config/config.js';
|
|
21
|
+
import { loadState, saveState } from '../config/state-store.js';
|
|
21
22
|
import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
|
|
22
23
|
import { appendRecentActivity, getRecentActivity, markSessionSeen } from '../config/recent-activity.js';
|
|
23
24
|
import { getSavedProviderSessions } from '../config/saved-sessions.js';
|
|
@@ -172,9 +173,9 @@ export class DaemonCommandRouter {
|
|
|
172
173
|
const offset = Math.max(0, Number(args?.offset) || 0);
|
|
173
174
|
const limit = Math.max(1, Math.min(100, Number(args?.limit) || 30));
|
|
174
175
|
const { sessions: historySessions, hasMore } = listSavedHistorySessions(providerType, { offset, limit });
|
|
175
|
-
const
|
|
176
|
-
const savedSessions = getSavedProviderSessions(
|
|
177
|
-
const recentSessions = getRecentActivity(
|
|
176
|
+
const state = loadState();
|
|
177
|
+
const savedSessions = getSavedProviderSessions(state, { providerType, kind });
|
|
178
|
+
const recentSessions = getRecentActivity(state, 200)
|
|
178
179
|
.filter(entry => entry.providerType === providerType && entry.kind === kind && entry.providerSessionId);
|
|
179
180
|
const savedSessionById = new Map(savedSessions.map(entry => [entry.providerSessionId, entry]));
|
|
180
181
|
const recentSessionById = new Map(recentSessions.map(entry => [entry.providerSessionId!, entry]));
|
|
@@ -284,18 +285,18 @@ export class DaemonCommandRouter {
|
|
|
284
285
|
this.deps.onIdeConnected?.();
|
|
285
286
|
if (result.success && resolvedWorkspace) {
|
|
286
287
|
try {
|
|
287
|
-
const next = appendRecentActivity(
|
|
288
|
+
const next = appendRecentActivity(loadState(), {
|
|
288
289
|
kind: 'ide',
|
|
289
290
|
providerType: result.ideId || ideKey,
|
|
290
291
|
providerName: result.ideId || ideKey,
|
|
291
292
|
workspace: resolvedWorkspace,
|
|
292
293
|
title: result.ideId || ideKey,
|
|
293
294
|
});
|
|
294
|
-
|
|
295
|
+
saveState(next);
|
|
295
296
|
} catch { /* ignore activity persist errors */ }
|
|
296
297
|
} else if (result.success && (result.ideId || ideKey)) {
|
|
297
298
|
try {
|
|
298
|
-
|
|
299
|
+
saveState(appendRecentActivity(loadState(), {
|
|
299
300
|
kind: 'ide',
|
|
300
301
|
providerType: result.ideId || ideKey,
|
|
301
302
|
providerName: result.ideId || ideKey,
|
|
@@ -326,8 +327,8 @@ export class DaemonCommandRouter {
|
|
|
326
327
|
if (!sessionId || typeof sessionId !== 'string') {
|
|
327
328
|
return { success: false, error: 'sessionId is required' };
|
|
328
329
|
}
|
|
329
|
-
const
|
|
330
|
-
const prevSeenAt =
|
|
330
|
+
const currentState = loadState();
|
|
331
|
+
const prevSeenAt = currentState.sessionReads?.[sessionId] || 0;
|
|
331
332
|
const sessionEntries = buildSessionEntries(
|
|
332
333
|
this.deps.instanceManager.collectAllStates(),
|
|
333
334
|
this.deps.cdpManagers as Map<string, any>,
|
|
@@ -335,7 +336,7 @@ export class DaemonCommandRouter {
|
|
|
335
336
|
const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
|
|
336
337
|
const completionMarker = targetSession ? getSessionCompletionMarker(targetSession) : '';
|
|
337
338
|
const next = markSessionSeen(
|
|
338
|
-
|
|
339
|
+
currentState,
|
|
339
340
|
sessionId,
|
|
340
341
|
typeof args?.seenAt === 'number' ? args.seenAt : Date.now(),
|
|
341
342
|
completionMarker,
|
|
@@ -343,7 +344,7 @@ export class DaemonCommandRouter {
|
|
|
343
344
|
if (READ_DEBUG_ENABLED) {
|
|
344
345
|
LOG.info('RecentRead', `mark_session_seen sessionId=${sessionId} seenAt=${String(args?.seenAt || '')} prevSeenAt=${String(prevSeenAt)} nextSeenAt=${String(next.sessionReads?.[sessionId] || 0)} marker=${completionMarker || '-'}`);
|
|
345
346
|
}
|
|
346
|
-
|
|
347
|
+
saveState(next);
|
|
347
348
|
this.deps.onStatusChange?.();
|
|
348
349
|
return {
|
|
349
350
|
success: true,
|
package/src/config/config.d.ts
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
* Manages launcher config, machine auth, and user preferences.
|
|
5
5
|
*/
|
|
6
6
|
import type { WorkspaceEntry } from './workspaces.js';
|
|
7
|
-
import type { RecentActivityEntry } from './recent-activity.js';
|
|
8
|
-
import type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
9
7
|
export type { WorkspaceEntry } from './workspaces.js';
|
|
10
8
|
export type { RecentActivityEntry } from './recent-activity.js';
|
|
11
9
|
export type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
10
|
+
export type { DaemonState } from './state-store.js';
|
|
12
11
|
export interface ADHDevConfig {
|
|
13
12
|
serverUrl: string;
|
|
14
13
|
selectedIde: string | null;
|
|
@@ -23,14 +22,6 @@ export interface ADHDevConfig {
|
|
|
23
22
|
workspaces?: WorkspaceEntry[];
|
|
24
23
|
/** Default workspace id (from workspaces[]) — never used implicitly for launch */
|
|
25
24
|
defaultWorkspaceId?: string | null;
|
|
26
|
-
/** Unified recent activity across IDE / CLI / ACP launch flows */
|
|
27
|
-
recentActivity?: RecentActivityEntry[];
|
|
28
|
-
/** Persistent resume-capable provider sessions keyed by providerSessionId */
|
|
29
|
-
savedProviderSessions?: SavedProviderSessionEntry[];
|
|
30
|
-
/** Last seen timestamps for live sessions, keyed by sessionId */
|
|
31
|
-
sessionReads?: Record<string, number>;
|
|
32
|
-
/** Last seen completion marker for live sessions, keyed by sessionId */
|
|
33
|
-
sessionReadMarkers?: Record<string, string>;
|
|
34
25
|
machineNickname: string | null;
|
|
35
26
|
/**
|
|
36
27
|
* Stable local machine ID (prefix: `mach_`) — generated locally on first run.
|
package/src/config/config.ts
CHANGED
|
@@ -9,11 +9,10 @@ import { join } from 'path';
|
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
|
|
10
10
|
import { randomUUID } from 'crypto';
|
|
11
11
|
import type { WorkspaceEntry } from './workspaces.js';
|
|
12
|
-
import type { RecentActivityEntry } from './recent-activity.js';
|
|
13
|
-
import type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
14
12
|
export type { WorkspaceEntry } from './workspaces.js';
|
|
15
13
|
export type { RecentActivityEntry } from './recent-activity.js';
|
|
16
14
|
export type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
15
|
+
export type { DaemonState } from './state-store.js';
|
|
17
16
|
|
|
18
17
|
export interface ADHDevConfig {
|
|
19
18
|
// Server connection
|
|
@@ -44,15 +43,6 @@ export interface ADHDevConfig {
|
|
|
44
43
|
/** Default workspace id (from workspaces[]) — never used implicitly for launch */
|
|
45
44
|
defaultWorkspaceId?: string | null;
|
|
46
45
|
|
|
47
|
-
/** Unified recent activity across IDE / CLI / ACP launch flows */
|
|
48
|
-
recentActivity?: RecentActivityEntry[];
|
|
49
|
-
/** Persistent resume-capable provider sessions keyed by providerSessionId */
|
|
50
|
-
savedProviderSessions?: SavedProviderSessionEntry[];
|
|
51
|
-
/** Last seen timestamps for live sessions, keyed by sessionId */
|
|
52
|
-
sessionReads?: Record<string, number>;
|
|
53
|
-
/** Last seen completion marker for live sessions, keyed by sessionId */
|
|
54
|
-
sessionReadMarkers?: Record<string, string>;
|
|
55
|
-
|
|
56
46
|
// Machine nickname (user-customizable label for this machine)
|
|
57
47
|
machineNickname: string | null;
|
|
58
48
|
|
|
@@ -102,10 +92,6 @@ const DEFAULT_CONFIG: ADHDevConfig = {
|
|
|
102
92
|
enabledIdes: [],
|
|
103
93
|
workspaces: [],
|
|
104
94
|
defaultWorkspaceId: null,
|
|
105
|
-
recentActivity: [],
|
|
106
|
-
savedProviderSessions: [],
|
|
107
|
-
sessionReads: {},
|
|
108
|
-
sessionReadMarkers: {},
|
|
109
95
|
machineNickname: null,
|
|
110
96
|
machineId: undefined,
|
|
111
97
|
machineSecret: null,
|
|
@@ -140,16 +126,6 @@ function asBoolean(value: unknown, fallback: boolean): boolean {
|
|
|
140
126
|
|
|
141
127
|
function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: string | null } {
|
|
142
128
|
const parsed = isPlainObject(raw) ? raw : {};
|
|
143
|
-
const legacySessionReads = isPlainObject(parsed.recentSessionReads) ? parsed.recentSessionReads : {};
|
|
144
|
-
const sessionReads = isPlainObject(parsed.sessionReads) ? parsed.sessionReads : {};
|
|
145
|
-
const mergedSessionReads = Object.fromEntries(
|
|
146
|
-
Object.entries({ ...legacySessionReads, ...sessionReads })
|
|
147
|
-
.filter(([, value]) => typeof value === 'number' && Number.isFinite(value))
|
|
148
|
-
);
|
|
149
|
-
const sessionReadMarkers = Object.fromEntries(
|
|
150
|
-
Object.entries(isPlainObject(parsed.sessionReadMarkers) ? parsed.sessionReadMarkers : {})
|
|
151
|
-
.filter(([, value]) => typeof value === 'string')
|
|
152
|
-
);
|
|
153
129
|
|
|
154
130
|
return {
|
|
155
131
|
serverUrl: typeof parsed.serverUrl === 'string' && parsed.serverUrl.trim()
|
|
@@ -165,10 +141,6 @@ function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: str
|
|
|
165
141
|
enabledIdes: asStringArray(parsed.enabledIdes),
|
|
166
142
|
workspaces: Array.isArray(parsed.workspaces) ? parsed.workspaces as WorkspaceEntry[] : [],
|
|
167
143
|
defaultWorkspaceId: asNullableString(parsed.defaultWorkspaceId) ?? asNullableString(parsed.activeWorkspaceId),
|
|
168
|
-
recentActivity: Array.isArray(parsed.recentActivity) ? parsed.recentActivity as RecentActivityEntry[] : [],
|
|
169
|
-
savedProviderSessions: Array.isArray(parsed.savedProviderSessions) ? parsed.savedProviderSessions as SavedProviderSessionEntry[] : [],
|
|
170
|
-
sessionReads: mergedSessionReads,
|
|
171
|
-
sessionReadMarkers,
|
|
172
144
|
machineNickname: asNullableString(parsed.machineNickname),
|
|
173
145
|
machineId: asOptionalString(parsed.machineId),
|
|
174
146
|
machineSecret: parsed.machineSecret === null ? null : asOptionalString(parsed.machineSecret),
|
|
@@ -227,6 +199,48 @@ function getConfigPath(): string {
|
|
|
227
199
|
return join(getConfigDir(), 'config.json');
|
|
228
200
|
}
|
|
229
201
|
|
|
202
|
+
/**
|
|
203
|
+
* One-time migration: move runtime state fields from config.json to state.json.
|
|
204
|
+
* Called eagerly during loadConfig so state is extracted before the config
|
|
205
|
+
* normalizer strips the unknown fields.
|
|
206
|
+
*/
|
|
207
|
+
function migrateStateToStateFile(raw: Record<string, any>): void {
|
|
208
|
+
const statePath = join(getConfigDir(), 'state.json');
|
|
209
|
+
if (existsSync(statePath)) return;
|
|
210
|
+
|
|
211
|
+
const recentActivity = Array.isArray(raw.recentActivity) ? raw.recentActivity : [];
|
|
212
|
+
const savedProviderSessions = Array.isArray(raw.savedProviderSessions) ? raw.savedProviderSessions : [];
|
|
213
|
+
const legacySessionReads = isPlainObject(raw.recentSessionReads) ? raw.recentSessionReads : {};
|
|
214
|
+
const sessionReads = isPlainObject(raw.sessionReads) ? raw.sessionReads : {};
|
|
215
|
+
const sessionReadMarkers = isPlainObject(raw.sessionReadMarkers) ? raw.sessionReadMarkers : {};
|
|
216
|
+
|
|
217
|
+
const hasData = recentActivity.length > 0
|
|
218
|
+
|| savedProviderSessions.length > 0
|
|
219
|
+
|| Object.keys(sessionReads).length > 0
|
|
220
|
+
|| Object.keys(legacySessionReads as object).length > 0
|
|
221
|
+
|| Object.keys(sessionReadMarkers as object).length > 0;
|
|
222
|
+
|
|
223
|
+
if (!hasData) return;
|
|
224
|
+
|
|
225
|
+
const mergedReads = Object.fromEntries(
|
|
226
|
+
Object.entries({ ...legacySessionReads, ...sessionReads })
|
|
227
|
+
.filter(([, v]) => typeof v === 'number' && Number.isFinite(v as number))
|
|
228
|
+
);
|
|
229
|
+
const cleanedMarkers = Object.fromEntries(
|
|
230
|
+
Object.entries(sessionReadMarkers as Record<string, unknown>)
|
|
231
|
+
.filter(([, v]) => typeof v === 'string')
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const state = {
|
|
235
|
+
recentActivity,
|
|
236
|
+
savedProviderSessions,
|
|
237
|
+
sessionReads: mergedReads,
|
|
238
|
+
sessionReadMarkers: cleanedMarkers,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
242
|
+
}
|
|
243
|
+
|
|
230
244
|
/**
|
|
231
245
|
* Load configuration from disk
|
|
232
246
|
*/
|
|
@@ -244,6 +258,10 @@ export function loadConfig(): ADHDevConfig {
|
|
|
244
258
|
try {
|
|
245
259
|
const raw = readFileSync(configPath, 'utf-8');
|
|
246
260
|
const parsed = JSON.parse(raw);
|
|
261
|
+
|
|
262
|
+
// One-time migration: move runtime state to ~/.adhdev/state.json
|
|
263
|
+
migrateStateToStateFile(parsed);
|
|
264
|
+
|
|
247
265
|
const normalizedInput = normalizeConfig(parsed);
|
|
248
266
|
const ensured = ensureMachineId(normalizedInput);
|
|
249
267
|
const normalized = ensured.config as ADHDevConfig & { activeWorkspaceId?: string | null };
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import type {
|
|
11
|
+
import type { DaemonState } from './state-store.js';
|
|
12
12
|
import { expandPath } from './workspaces.js';
|
|
13
13
|
|
|
14
14
|
export interface RecentActivityEntry {
|
|
@@ -49,9 +49,9 @@ export function buildRecentActivityKeyForEntry(
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export function appendRecentActivity(
|
|
52
|
-
|
|
52
|
+
state: DaemonState,
|
|
53
53
|
entry: Omit<RecentActivityEntry, 'id' | 'lastUsedAt'> & { lastUsedAt?: number },
|
|
54
|
-
):
|
|
54
|
+
): DaemonState {
|
|
55
55
|
const nextEntry: RecentActivityEntry = {
|
|
56
56
|
...entry,
|
|
57
57
|
workspace: entry.workspace ? normalizeWorkspace(entry.workspace) : undefined,
|
|
@@ -59,39 +59,39 @@ export function appendRecentActivity(
|
|
|
59
59
|
lastUsedAt: entry.lastUsedAt || Date.now(),
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
const filtered = (
|
|
62
|
+
const filtered = (state.recentActivity || []).filter((item) => item.id !== nextEntry.id);
|
|
63
63
|
return {
|
|
64
|
-
...
|
|
64
|
+
...state,
|
|
65
65
|
recentActivity: [nextEntry, ...filtered].slice(0, MAX_ACTIVITY),
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export function getRecentActivity(
|
|
70
|
-
return [...(
|
|
69
|
+
export function getRecentActivity(state: DaemonState, limit = 20): RecentActivityEntry[] {
|
|
70
|
+
return [...(state.recentActivity || [])]
|
|
71
71
|
.sort((a, b) => b.lastUsedAt - a.lastUsedAt)
|
|
72
72
|
.slice(0, limit);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export function getSessionSeenAt(
|
|
76
|
-
return
|
|
75
|
+
export function getSessionSeenAt(state: DaemonState, sessionId: string): number {
|
|
76
|
+
return state.sessionReads?.[sessionId] || 0;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export function getSessionSeenMarker(
|
|
80
|
-
return
|
|
79
|
+
export function getSessionSeenMarker(state: DaemonState, sessionId: string): string {
|
|
80
|
+
return state.sessionReadMarkers?.[sessionId] || '';
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export function markSessionSeen(
|
|
84
|
-
|
|
84
|
+
state: DaemonState,
|
|
85
85
|
sessionId: string,
|
|
86
86
|
seenAt = Date.now(),
|
|
87
87
|
completionMarker?: string | null,
|
|
88
|
-
):
|
|
89
|
-
const prev =
|
|
88
|
+
): DaemonState {
|
|
89
|
+
const prev = state.sessionReads || {};
|
|
90
90
|
const nextSeenAt = Math.max(prev[sessionId] || 0, seenAt);
|
|
91
|
-
const prevMarkers =
|
|
91
|
+
const prevMarkers = state.sessionReadMarkers || {};
|
|
92
92
|
const nextMarker = typeof completionMarker === 'string' ? completionMarker : '';
|
|
93
93
|
return {
|
|
94
|
-
...
|
|
94
|
+
...state,
|
|
95
95
|
sessionReads: {
|
|
96
96
|
...prev,
|
|
97
97
|
[sessionId]: nextSeenAt,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
-
import type {
|
|
2
|
+
import type { DaemonState } from './state-store.js';
|
|
3
3
|
import { expandPath } from './workspaces.js';
|
|
4
4
|
|
|
5
5
|
export interface SavedProviderSessionEntry {
|
|
@@ -31,14 +31,14 @@ export function buildSavedProviderSessionKey(providerSessionId: string) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export function upsertSavedProviderSession(
|
|
34
|
-
|
|
34
|
+
state: DaemonState,
|
|
35
35
|
entry: Omit<SavedProviderSessionEntry, 'id' | 'createdAt' | 'lastUsedAt'> & { createdAt?: number; lastUsedAt?: number },
|
|
36
|
-
):
|
|
36
|
+
): DaemonState {
|
|
37
37
|
const providerSessionId = typeof entry.providerSessionId === 'string' ? entry.providerSessionId.trim() : '';
|
|
38
|
-
if (!providerSessionId) return
|
|
38
|
+
if (!providerSessionId) return state;
|
|
39
39
|
|
|
40
40
|
const id = buildSavedProviderSessionKey(providerSessionId);
|
|
41
|
-
const existing = (
|
|
41
|
+
const existing = (state.savedProviderSessions || []).find(item => item.id === id);
|
|
42
42
|
const nextEntry: SavedProviderSessionEntry = {
|
|
43
43
|
id,
|
|
44
44
|
kind: entry.kind,
|
|
@@ -52,18 +52,18 @@ export function upsertSavedProviderSession(
|
|
|
52
52
|
lastUsedAt: entry.lastUsedAt || Date.now(),
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
const filtered = (
|
|
55
|
+
const filtered = (state.savedProviderSessions || []).filter(item => item.id !== id);
|
|
56
56
|
return {
|
|
57
|
-
...
|
|
57
|
+
...state,
|
|
58
58
|
savedProviderSessions: [nextEntry, ...filtered].slice(0, MAX_SAVED_SESSIONS),
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function getSavedProviderSessions(
|
|
63
|
-
|
|
63
|
+
state: DaemonState,
|
|
64
64
|
filters?: { providerType?: string; kind?: SavedProviderSessionEntry['kind'] },
|
|
65
65
|
): SavedProviderSessionEntry[] {
|
|
66
|
-
return [...(
|
|
66
|
+
return [...(state.savedProviderSessions || [])]
|
|
67
67
|
.filter(entry => {
|
|
68
68
|
if (filters?.providerType && entry.providerType !== filters.providerType) return false;
|
|
69
69
|
if (filters?.kind && entry.kind !== filters.kind) return false;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADHDev State Store — Runtime state persistence
|
|
3
|
+
*
|
|
4
|
+
* Separates volatile runtime state (sessions, activity, read markers)
|
|
5
|
+
* from static configuration (config.json).
|
|
6
|
+
*
|
|
7
|
+
* State is stored in ~/.adhdev/state.json
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { getConfigDir } from './config.js';
|
|
13
|
+
import type { RecentActivityEntry } from './recent-activity.js';
|
|
14
|
+
import type { SavedProviderSessionEntry } from './saved-sessions.js';
|
|
15
|
+
|
|
16
|
+
export interface DaemonState {
|
|
17
|
+
/** Unified recent activity across IDE / CLI / ACP launch flows */
|
|
18
|
+
recentActivity: RecentActivityEntry[];
|
|
19
|
+
/** Persistent resume-capable provider sessions keyed by providerSessionId */
|
|
20
|
+
savedProviderSessions: SavedProviderSessionEntry[];
|
|
21
|
+
/** Last seen timestamps for live sessions, keyed by sessionId */
|
|
22
|
+
sessionReads: Record<string, number>;
|
|
23
|
+
/** Last seen completion marker for live sessions, keyed by sessionId */
|
|
24
|
+
sessionReadMarkers: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_STATE: DaemonState = {
|
|
28
|
+
recentActivity: [],
|
|
29
|
+
savedProviderSessions: [],
|
|
30
|
+
sessionReads: {},
|
|
31
|
+
sessionReadMarkers: {},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function isPlainObject(value: unknown): value is Record<string, any> {
|
|
35
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getStatePath(): string {
|
|
39
|
+
return join(getConfigDir(), 'state.json');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeState(raw: unknown): DaemonState {
|
|
43
|
+
const parsed = isPlainObject(raw) ? raw : {};
|
|
44
|
+
|
|
45
|
+
const sessionReads = Object.fromEntries(
|
|
46
|
+
Object.entries(isPlainObject(parsed.sessionReads) ? parsed.sessionReads : {})
|
|
47
|
+
.filter(([, value]) => typeof value === 'number' && Number.isFinite(value as number))
|
|
48
|
+
);
|
|
49
|
+
const sessionReadMarkers = Object.fromEntries(
|
|
50
|
+
Object.entries(isPlainObject(parsed.sessionReadMarkers) ? parsed.sessionReadMarkers : {})
|
|
51
|
+
.filter(([, value]) => typeof value === 'string')
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
recentActivity: Array.isArray(parsed.recentActivity) ? parsed.recentActivity as RecentActivityEntry[] : [],
|
|
56
|
+
savedProviderSessions: Array.isArray(parsed.savedProviderSessions) ? parsed.savedProviderSessions as SavedProviderSessionEntry[] : [],
|
|
57
|
+
sessionReads,
|
|
58
|
+
sessionReadMarkers,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load runtime state from disk
|
|
64
|
+
*/
|
|
65
|
+
export function loadState(): DaemonState {
|
|
66
|
+
const statePath = getStatePath();
|
|
67
|
+
|
|
68
|
+
if (!existsSync(statePath)) {
|
|
69
|
+
return { ...DEFAULT_STATE };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const raw = readFileSync(statePath, 'utf-8');
|
|
74
|
+
return normalizeState(JSON.parse(raw));
|
|
75
|
+
} catch {
|
|
76
|
+
return { ...DEFAULT_STATE };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Save runtime state to disk
|
|
82
|
+
*/
|
|
83
|
+
export function saveState(state: DaemonState): void {
|
|
84
|
+
const statePath = getStatePath();
|
|
85
|
+
const normalized = normalizeState(state);
|
|
86
|
+
writeFileSync(statePath, JSON.stringify(normalized, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Reset runtime state
|
|
91
|
+
*/
|
|
92
|
+
export function resetState(): void {
|
|
93
|
+
saveState({ ...DEFAULT_STATE });
|
|
94
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -69,6 +69,10 @@ export type { RecentActivityEntry } from './config/recent-activity.js';
|
|
|
69
69
|
export { getSavedProviderSessions, upsertSavedProviderSession } from './config/saved-sessions.js';
|
|
70
70
|
export type { SavedProviderSessionEntry } from './config/saved-sessions.js';
|
|
71
71
|
|
|
72
|
+
// ── State Store ──
|
|
73
|
+
export { loadState, saveState, resetState } from './config/state-store.js';
|
|
74
|
+
export type { DaemonState } from './config/state-store.js';
|
|
75
|
+
|
|
72
76
|
// ── Detection ──
|
|
73
77
|
export { detectIDEs } from './detection/ide-detector.js';
|
|
74
78
|
export type { IDEInfo } from './detection/ide-detector.js';
|
package/src/status/snapshot.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import { loadConfig } from '../config/config.js';
|
|
11
|
+
import { loadState } from '../config/state-store.js';
|
|
11
12
|
import { getRecentActivity, getSessionSeenAt, getSessionSeenMarker } from '../config/recent-activity.js';
|
|
12
13
|
import { getWorkspaceState } from '../config/workspaces.js';
|
|
13
14
|
import { getHostMemorySnapshot } from '../system/host-memory.js';
|
|
@@ -195,16 +196,17 @@ function buildRecentLaunches(
|
|
|
195
196
|
|
|
196
197
|
export function buildStatusSnapshot(options: StatusSnapshotOptions): StatusSnapshot {
|
|
197
198
|
const cfg = loadConfig();
|
|
199
|
+
const state = loadState();
|
|
198
200
|
const wsState = getWorkspaceState(cfg);
|
|
199
201
|
const memSnap = getHostMemorySnapshot();
|
|
200
|
-
const recentActivity = getRecentActivity(
|
|
202
|
+
const recentActivity = getRecentActivity(state, 20);
|
|
201
203
|
const sessions = buildSessionEntries(
|
|
202
204
|
options.allStates,
|
|
203
205
|
options.cdpManagers as Map<string, any>,
|
|
204
206
|
);
|
|
205
207
|
for (const session of sessions) {
|
|
206
|
-
const lastSeenAt = getSessionSeenAt(
|
|
207
|
-
const seenCompletionMarker = getSessionSeenMarker(
|
|
208
|
+
const lastSeenAt = getSessionSeenAt(state, session.id);
|
|
209
|
+
const seenCompletionMarker = getSessionSeenMarker(state, session.id);
|
|
208
210
|
const lastUsedAt = getSessionLastUsedAt(session);
|
|
209
211
|
const completionMarker = getSessionCompletionMarker(session);
|
|
210
212
|
const { unread, inboxBucket } = session.surfaceHidden
|