heyhank 0.1.0
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/README.md +40 -0
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
- package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
- package/dist/assets/CronManager-DDbz-yiT.js +1 -0
- package/dist/assets/HelpPage-DMfkzERp.js +1 -0
- package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
- package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
- package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
- package/dist/assets/Playground-Fc5cdc5p.js +109 -0
- package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
- package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
- package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
- package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
- package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
- package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
- package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
- package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
- package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
- package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
- package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
- package/dist/assets/index-C8M_PUmX.css +32 -0
- package/dist/assets/index-CEqZnThB.js +204 -0
- package/dist/assets/sw-register-LSSpj6RU.js +1 -0
- package/dist/assets/time-ago-B6r_l9u1.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon-32-original.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/heyhank-mascot-poster.png +0 -0
- package/dist/heyhank-mascot.mp4 +0 -0
- package/dist/heyhank-mascot.webm +0 -0
- package/dist/icon-192-original.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512-original.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +21 -0
- package/dist/logo-192.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo-original.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/push-sw.js +34 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-d2a0910a.js +1 -0
- package/package.json +109 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.ts +357 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-timeout.ts +107 -0
- package/server/agent-types.ts +122 -0
- package/server/ai-validation-settings.ts +37 -0
- package/server/ai-validator.ts +181 -0
- package/server/anthropic-provider-migration.ts +48 -0
- package/server/assistant-store.ts +272 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-approve.ts +153 -0
- package/server/auto-namer.ts +36 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.ts +61 -0
- package/server/calendar-service.ts +434 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.ts +1303 -0
- package/server/codex-adapter.ts +3027 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.ts +27 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.ts +1053 -0
- package/server/cost-tracker.ts +222 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/email-service.ts +354 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +75 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.ts +170 -0
- package/server/federation/node-connection.ts +190 -0
- package/server/federation/node-manager.ts +366 -0
- package/server/federation/node-store.ts +86 -0
- package/server/federation/node-types.ts +121 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.ts +379 -0
- package/server/google-media.ts +342 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +491 -0
- package/server/internal-ai.ts +237 -0
- package/server/kill-switch.ts +99 -0
- package/server/llm-providers.ts +342 -0
- package/server/logger.ts +259 -0
- package/server/mcp-registry.ts +401 -0
- package/server/message-bus.ts +271 -0
- package/server/message-delivery.ts +128 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.ts +13 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/provider-manager.ts +111 -0
- package/server/provider-registry.ts +393 -0
- package/server/push-notifications.ts +221 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.ts +320 -0
- package/server/reminder-scheduler.ts +38 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.ts +264 -0
- package/server/routes/assistant-routes.ts +90 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/federation-routes.ts +76 -0
- package/server/routes/fs-routes.ts +622 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/llm-routes.ts +166 -0
- package/server/routes/media-routes.ts +135 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/platform-routes.ts +1379 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/provider-routes.ts +109 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +285 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/socialmedia-routes.ts +208 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes/telephony-routes.ts +259 -0
- package/server/routes.ts +1379 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.ts +457 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.ts +824 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +511 -0
- package/server/settings-manager.ts +149 -0
- package/server/shared-context.ts +157 -0
- package/server/socialmedia/adapter.ts +15 -0
- package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
- package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
- package/server/socialmedia/manager.ts +227 -0
- package/server/socialmedia/store.ts +98 -0
- package/server/socialmedia/types.ts +89 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/telephony/audio-bridge.ts +331 -0
- package/server/telephony/call-manager.ts +457 -0
- package/server/telephony/call-types.ts +108 -0
- package/server/telephony/telephony-store.ts +119 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.ts +192 -0
- package/server/usage-limits.ts +225 -0
- package/server/web-push.d.ts +51 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +121 -0
- package/server/ws-bridge.ts +1240 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Formal session state machine for the HeyHank server.
|
|
2
|
+
// Centralizes session phase definitions and validates transitions.
|
|
3
|
+
|
|
4
|
+
import { metricsCollector } from "./metrics-collector.js";
|
|
5
|
+
import { log } from "./logger.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The formal phases a session can be in.
|
|
9
|
+
*
|
|
10
|
+
* - starting: CLI process spawned, WS not yet connected
|
|
11
|
+
* - initializing: CLI WS connected, awaiting system.init
|
|
12
|
+
* - ready: Idle, awaiting user input
|
|
13
|
+
* - streaming: Claude generating output (stream_event / assistant)
|
|
14
|
+
* - awaiting_permission: Tool call pending user approval
|
|
15
|
+
* - compacting: Context window compaction in progress
|
|
16
|
+
* - reconnecting: CLI socket dropped, within grace period
|
|
17
|
+
* - terminated: Process exited or killed
|
|
18
|
+
*/
|
|
19
|
+
export type SessionPhase =
|
|
20
|
+
| "starting"
|
|
21
|
+
| "initializing"
|
|
22
|
+
| "ready"
|
|
23
|
+
| "streaming"
|
|
24
|
+
| "awaiting_permission"
|
|
25
|
+
| "compacting"
|
|
26
|
+
| "reconnecting"
|
|
27
|
+
| "terminated";
|
|
28
|
+
|
|
29
|
+
/** Payload emitted on every successful state transition. */
|
|
30
|
+
export interface SessionTransitionEvent {
|
|
31
|
+
sessionId: string;
|
|
32
|
+
from: SessionPhase;
|
|
33
|
+
to: SessionPhase;
|
|
34
|
+
trigger: string;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Defines which (from -> to) transitions are valid.
|
|
40
|
+
* Any transition not listed here will be blocked with a warning.
|
|
41
|
+
*/
|
|
42
|
+
export const VALID_TRANSITIONS: ReadonlyMap<
|
|
43
|
+
SessionPhase,
|
|
44
|
+
ReadonlySet<SessionPhase>
|
|
45
|
+
> = new Map([
|
|
46
|
+
[
|
|
47
|
+
"starting",
|
|
48
|
+
new Set<SessionPhase>(["initializing", "streaming", "reconnecting", "terminated"]),
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"initializing",
|
|
52
|
+
new Set<SessionPhase>(["ready", "streaming", "reconnecting", "terminated"]),
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"ready",
|
|
56
|
+
new Set<SessionPhase>([
|
|
57
|
+
"streaming",
|
|
58
|
+
"compacting",
|
|
59
|
+
"reconnecting",
|
|
60
|
+
"terminated",
|
|
61
|
+
]),
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
"streaming",
|
|
65
|
+
new Set<SessionPhase>([
|
|
66
|
+
"ready",
|
|
67
|
+
"initializing",
|
|
68
|
+
"awaiting_permission",
|
|
69
|
+
"compacting",
|
|
70
|
+
"reconnecting",
|
|
71
|
+
"terminated",
|
|
72
|
+
]),
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"awaiting_permission",
|
|
76
|
+
new Set<SessionPhase>(["streaming", "ready", "reconnecting", "terminated"]),
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
"compacting",
|
|
80
|
+
new Set<SessionPhase>([
|
|
81
|
+
"ready",
|
|
82
|
+
"streaming",
|
|
83
|
+
"reconnecting",
|
|
84
|
+
"terminated",
|
|
85
|
+
]),
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
"reconnecting",
|
|
89
|
+
new Set<SessionPhase>(["initializing", "starting", "ready", "streaming", "terminated"]),
|
|
90
|
+
],
|
|
91
|
+
["terminated", new Set<SessionPhase>(["starting"])],
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
type TransitionListener = (event: SessionTransitionEvent) => void;
|
|
95
|
+
|
|
96
|
+
export class SessionStateMachine {
|
|
97
|
+
private _phase: SessionPhase;
|
|
98
|
+
private readonly _sessionId: string;
|
|
99
|
+
private _listeners: TransitionListener[] = [];
|
|
100
|
+
|
|
101
|
+
constructor(sessionId: string, initialPhase: SessionPhase = "starting") {
|
|
102
|
+
this._sessionId = sessionId;
|
|
103
|
+
this._phase = initialPhase;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get phase(): SessionPhase {
|
|
107
|
+
return this._phase;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get sessionId(): string {
|
|
111
|
+
return this._sessionId;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Attempt a state transition.
|
|
116
|
+
* Returns true if successful (or same-state no-op), false if blocked.
|
|
117
|
+
* Invalid transitions are logged but never throw.
|
|
118
|
+
*/
|
|
119
|
+
transition(to: SessionPhase, trigger: string): boolean {
|
|
120
|
+
if (this._phase === to) return true;
|
|
121
|
+
|
|
122
|
+
const allowed = VALID_TRANSITIONS.get(this._phase);
|
|
123
|
+
if (!allowed || !allowed.has(to)) {
|
|
124
|
+
metricsCollector.recordError("invalid_state_transition");
|
|
125
|
+
log.warn("state-machine", "Blocked invalid transition", {
|
|
126
|
+
sessionId: this._sessionId,
|
|
127
|
+
from: this._phase,
|
|
128
|
+
to,
|
|
129
|
+
trigger,
|
|
130
|
+
});
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const event: SessionTransitionEvent = {
|
|
135
|
+
sessionId: this._sessionId,
|
|
136
|
+
from: this._phase,
|
|
137
|
+
to,
|
|
138
|
+
trigger,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
this._phase = to;
|
|
143
|
+
|
|
144
|
+
// Snapshot listeners so additions/removals during iteration are safe
|
|
145
|
+
const snapshot = this._listeners.slice();
|
|
146
|
+
for (const listener of snapshot) {
|
|
147
|
+
try {
|
|
148
|
+
listener(event);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error(
|
|
151
|
+
`[state-machine] Listener error for ${this._sessionId}:`,
|
|
152
|
+
err,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Subscribe to state transitions. Returns an unsubscribe function. */
|
|
161
|
+
onTransition(listener: TransitionListener): () => void {
|
|
162
|
+
this._listeners.push(listener);
|
|
163
|
+
return () => {
|
|
164
|
+
const idx = this._listeners.indexOf(listener);
|
|
165
|
+
if (idx !== -1) this._listeners.splice(idx, 1);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Force-set state without validation or listener notification.
|
|
171
|
+
* Used for restoring state from disk.
|
|
172
|
+
*/
|
|
173
|
+
forceState(phase: SessionPhase): void {
|
|
174
|
+
this._phase = phase;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// -- Guard methods --
|
|
178
|
+
|
|
179
|
+
/** True only when session is idle and ready for a new user message. */
|
|
180
|
+
canAcceptUserMessage(): boolean {
|
|
181
|
+
return this._phase === "ready";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** True only when a permission request is pending. */
|
|
185
|
+
canRespondToPermission(): boolean {
|
|
186
|
+
return this._phase === "awaiting_permission";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** True when the CLI socket is expected to be reachable. */
|
|
190
|
+
canSendToCLI(): boolean {
|
|
191
|
+
return (
|
|
192
|
+
this._phase !== "terminated" &&
|
|
193
|
+
this._phase !== "reconnecting" &&
|
|
194
|
+
this._phase !== "starting"
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** True when the session has not terminated. */
|
|
199
|
+
isActive(): boolean {
|
|
200
|
+
return this._phase !== "terminated";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** True only when the session is idle (ready). */
|
|
204
|
+
isIdle(): boolean {
|
|
205
|
+
return this._phase === "ready";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import type {
|
|
5
|
+
SessionState,
|
|
6
|
+
BrowserIncomingMessage,
|
|
7
|
+
PermissionRequest,
|
|
8
|
+
BufferedBrowserEvent,
|
|
9
|
+
} from "./session-types.js";
|
|
10
|
+
|
|
11
|
+
// ─── Serializable session shape ─────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface PersistedSession {
|
|
14
|
+
id: string;
|
|
15
|
+
state: SessionState;
|
|
16
|
+
messageHistory: BrowserIncomingMessage[];
|
|
17
|
+
pendingMessages: string[];
|
|
18
|
+
pendingPermissions: [string, PermissionRequest][];
|
|
19
|
+
eventBuffer?: BufferedBrowserEvent[];
|
|
20
|
+
nextEventSeq?: number;
|
|
21
|
+
lastAckSeq?: number;
|
|
22
|
+
processedClientMessageIds?: string[];
|
|
23
|
+
archived?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Store ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const DEFAULT_DIR = join(tmpdir(), "vibe-sessions");
|
|
29
|
+
|
|
30
|
+
export class SessionStore {
|
|
31
|
+
private dir: string;
|
|
32
|
+
private debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
33
|
+
|
|
34
|
+
constructor(dir?: string) {
|
|
35
|
+
this.dir = dir || DEFAULT_DIR;
|
|
36
|
+
mkdirSync(this.dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private filePath(sessionId: string): string {
|
|
40
|
+
return join(this.dir, `${sessionId}.json`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Debounced write — batches rapid changes (e.g. multiple stream events). */
|
|
44
|
+
save(session: PersistedSession): void {
|
|
45
|
+
const existing = this.debounceTimers.get(session.id);
|
|
46
|
+
if (existing) clearTimeout(existing);
|
|
47
|
+
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
this.debounceTimers.delete(session.id);
|
|
50
|
+
this.saveSync(session);
|
|
51
|
+
}, 150);
|
|
52
|
+
this.debounceTimers.set(session.id, timer);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Immediate write — use for critical state changes. */
|
|
56
|
+
saveSync(session: PersistedSession): void {
|
|
57
|
+
try {
|
|
58
|
+
writeFileSync(this.filePath(session.id), JSON.stringify(session), "utf-8");
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error(`[session-store] Failed to save session ${session.id}:`, err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Load a single session from disk. */
|
|
65
|
+
load(sessionId: string): PersistedSession | null {
|
|
66
|
+
try {
|
|
67
|
+
const raw = readFileSync(this.filePath(sessionId), "utf-8");
|
|
68
|
+
return JSON.parse(raw) as PersistedSession;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Load all sessions from disk. */
|
|
75
|
+
loadAll(): PersistedSession[] {
|
|
76
|
+
const sessions: PersistedSession[] = [];
|
|
77
|
+
try {
|
|
78
|
+
const files = readdirSync(this.dir).filter((f) => f.endsWith(".json") && f !== "launcher.json");
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
try {
|
|
81
|
+
const raw = readFileSync(join(this.dir, file), "utf-8");
|
|
82
|
+
sessions.push(JSON.parse(raw));
|
|
83
|
+
} catch {
|
|
84
|
+
// Skip corrupt files
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Dir doesn't exist yet
|
|
89
|
+
}
|
|
90
|
+
return sessions;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Set the archived flag on a persisted session. */
|
|
94
|
+
setArchived(sessionId: string, archived: boolean): boolean {
|
|
95
|
+
const session = this.load(sessionId);
|
|
96
|
+
if (!session) return false;
|
|
97
|
+
session.archived = archived;
|
|
98
|
+
this.saveSync(session);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Remove a session file from disk. */
|
|
103
|
+
remove(sessionId: string): void {
|
|
104
|
+
const timer = this.debounceTimers.get(sessionId);
|
|
105
|
+
if (timer) {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
this.debounceTimers.delete(sessionId);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
unlinkSync(this.filePath(sessionId));
|
|
111
|
+
} catch {
|
|
112
|
+
// File may not exist
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Persist launcher state (separate file). */
|
|
117
|
+
saveLauncher(data: unknown): void {
|
|
118
|
+
try {
|
|
119
|
+
writeFileSync(join(this.dir, "launcher.json"), JSON.stringify(data), "utf-8");
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error("[session-store] Failed to save launcher state:", err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Load launcher state. */
|
|
126
|
+
loadLauncher<T>(): T | null {
|
|
127
|
+
try {
|
|
128
|
+
const raw = readFileSync(join(this.dir, "launcher.json"), "utf-8");
|
|
129
|
+
return JSON.parse(raw) as T;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Cancel all pending debounce timers (for clean test teardown). */
|
|
136
|
+
dispose(): void {
|
|
137
|
+
for (const timer of this.debounceTimers.values()) {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
}
|
|
140
|
+
this.debounceTimers.clear();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
get directory(): string {
|
|
144
|
+
return this.dir;
|
|
145
|
+
}
|
|
146
|
+
}
|