@adhdev/session-host-core 0.7.12

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/index.mjs ADDED
@@ -0,0 +1,475 @@
1
+ // src/buffer.ts
2
+ var SessionRingBuffer = class {
3
+ maxBytes;
4
+ chunks = [];
5
+ nextSeq = 1;
6
+ totalBytes = 0;
7
+ constructor(options = {}) {
8
+ this.maxBytes = options.maxBytes ?? 512 * 1024;
9
+ }
10
+ append(data) {
11
+ const normalized = typeof data === "string" ? data : String(data ?? "");
12
+ const bytes = Buffer.byteLength(normalized, "utf8");
13
+ const seq = this.nextSeq++;
14
+ this.chunks.push({ seq, data: normalized, bytes });
15
+ this.totalBytes += bytes;
16
+ this.trim();
17
+ return seq;
18
+ }
19
+ snapshot(sinceSeq) {
20
+ const relevant = typeof sinceSeq === "number" ? this.chunks.filter((chunk) => chunk.seq > sinceSeq) : this.chunks;
21
+ const text = relevant.map((chunk) => chunk.data).join("");
22
+ const truncated = !!this.chunks[0] && typeof sinceSeq === "number" && sinceSeq < this.chunks[0].seq - 1;
23
+ return {
24
+ seq: this.nextSeq - 1,
25
+ text,
26
+ truncated
27
+ };
28
+ }
29
+ getState() {
30
+ return {
31
+ scrollbackBytes: this.totalBytes,
32
+ snapshotSeq: this.nextSeq - 1
33
+ };
34
+ }
35
+ clear() {
36
+ this.chunks = [];
37
+ this.totalBytes = 0;
38
+ this.nextSeq = 1;
39
+ }
40
+ restore(snapshot) {
41
+ this.clear();
42
+ const text = String(snapshot.text || "");
43
+ if (!text) {
44
+ this.nextSeq = Math.max(1, Number(snapshot.seq || 0) + 1);
45
+ return;
46
+ }
47
+ const bytes = Buffer.byteLength(text, "utf8");
48
+ const seq = Math.max(1, Number(snapshot.seq || 1));
49
+ this.chunks = [{ seq, data: text, bytes }];
50
+ this.totalBytes = bytes;
51
+ this.nextSeq = seq + 1;
52
+ this.trim();
53
+ }
54
+ trim() {
55
+ while (this.totalBytes > this.maxBytes && this.chunks.length > 1) {
56
+ const removed = this.chunks.shift();
57
+ if (!removed) break;
58
+ this.totalBytes -= removed.bytes;
59
+ }
60
+ }
61
+ };
62
+
63
+ // src/registry.ts
64
+ import { randomUUID } from "crypto";
65
+
66
+ // src/runtime-labels.ts
67
+ import * as path from "path";
68
+ function normalizeSlug(input) {
69
+ return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
70
+ }
71
+ function normalizeValue(input) {
72
+ return input.trim().toLowerCase();
73
+ }
74
+ function getWorkspaceLabel(workspace) {
75
+ const trimmed = workspace.trim();
76
+ if (!trimmed) return "workspace";
77
+ const normalized = trimmed.replace(/[\\/]+$/, "");
78
+ const base = path.basename(normalized);
79
+ return base || normalized;
80
+ }
81
+ function buildRuntimeDisplayName(payload) {
82
+ const explicit = payload.displayName?.trim();
83
+ if (explicit) return explicit;
84
+ const workspaceLabel = getWorkspaceLabel(payload.workspace);
85
+ const providerLabel = payload.providerType.trim() || "runtime";
86
+ return `${providerLabel} @ ${workspaceLabel}`;
87
+ }
88
+ function buildRuntimeKey(payload, existingKeys) {
89
+ const requested = payload.runtimeKey?.trim();
90
+ const existing = new Set(Array.from(existingKeys, (key) => key.toLowerCase()));
91
+ const displayName = buildRuntimeDisplayName(payload);
92
+ const baseKey = normalizeSlug(requested || displayName || getWorkspaceLabel(payload.workspace) || payload.providerType || "runtime") || "runtime";
93
+ if (!existing.has(baseKey)) return baseKey;
94
+ let suffix = 2;
95
+ let candidate = `${baseKey}-${suffix}`;
96
+ while (existing.has(candidate)) {
97
+ suffix += 1;
98
+ candidate = `${baseKey}-${suffix}`;
99
+ }
100
+ return candidate;
101
+ }
102
+ function uniqueMatch(records, predicate) {
103
+ const matches = records.filter(predicate);
104
+ if (matches.length === 1) return matches[0] || null;
105
+ if (matches.length === 0) return null;
106
+ const labels = matches.map((record) => `${record.runtimeKey} (${record.sessionId})`).join(", ");
107
+ throw new Error(`Ambiguous runtime target. Matches: ${labels}`);
108
+ }
109
+ function resolveRuntimeRecord(records, identifier) {
110
+ const target = identifier.trim();
111
+ if (!target) {
112
+ throw new Error("Runtime target is required");
113
+ }
114
+ const exact = uniqueMatch(
115
+ records,
116
+ (record) => record.sessionId === target || normalizeValue(record.runtimeKey) === normalizeValue(target) || normalizeValue(record.displayName) === normalizeValue(target)
117
+ );
118
+ if (exact) return exact;
119
+ const prefix = uniqueMatch(
120
+ records,
121
+ (record) => record.sessionId.startsWith(target) || normalizeValue(record.runtimeKey).startsWith(normalizeValue(target))
122
+ );
123
+ if (prefix) return prefix;
124
+ throw new Error(`Unknown runtime target: ${target}`);
125
+ }
126
+ function formatRuntimeOwner(record) {
127
+ if (!record.writeOwner) return "none";
128
+ return `${record.writeOwner.ownerType}:${record.writeOwner.clientId}`;
129
+ }
130
+
131
+ // src/registry.ts
132
+ var SessionHostRegistry = class {
133
+ sessions = /* @__PURE__ */ new Map();
134
+ createSession(payload) {
135
+ const sessionId = payload.sessionId || randomUUID();
136
+ if (this.sessions.has(sessionId)) {
137
+ throw new Error(`Session already exists: ${sessionId}`);
138
+ }
139
+ const now = Date.now();
140
+ const initialClient = payload.clientId ? [{
141
+ clientId: payload.clientId,
142
+ type: payload.clientType || "daemon",
143
+ readOnly: false,
144
+ attachedAt: now,
145
+ lastSeenAt: now
146
+ }] : [];
147
+ const record = {
148
+ sessionId,
149
+ runtimeKey: buildRuntimeKey(
150
+ payload,
151
+ Array.from(this.sessions.values(), (state) => state.record.runtimeKey)
152
+ ),
153
+ displayName: buildRuntimeDisplayName(payload),
154
+ workspaceLabel: getWorkspaceLabel(payload.workspace),
155
+ transport: "pty",
156
+ providerType: payload.providerType,
157
+ category: payload.category,
158
+ workspace: payload.workspace,
159
+ launchCommand: payload.launchCommand,
160
+ createdAt: now,
161
+ lastActivityAt: now,
162
+ lifecycle: "starting",
163
+ writeOwner: null,
164
+ attachedClients: initialClient,
165
+ buffer: {
166
+ scrollbackBytes: 0,
167
+ snapshotSeq: 0
168
+ },
169
+ meta: payload.meta || {}
170
+ };
171
+ record.meta = {
172
+ sessionHostCols: payload.cols || 120,
173
+ sessionHostRows: payload.rows || 40,
174
+ ...record.meta
175
+ };
176
+ this.sessions.set(sessionId, {
177
+ record,
178
+ buffer: new SessionRingBuffer()
179
+ });
180
+ return this.cloneRecord(record);
181
+ }
182
+ restoreSession(record, snapshot) {
183
+ const cloned = this.cloneRecord(record);
184
+ this.sessions.set(cloned.sessionId, {
185
+ record: cloned,
186
+ buffer: (() => {
187
+ const buffer = new SessionRingBuffer();
188
+ if (snapshot) buffer.restore(snapshot);
189
+ return buffer;
190
+ })()
191
+ });
192
+ return this.cloneRecord(cloned);
193
+ }
194
+ listSessions() {
195
+ return Array.from(this.sessions.values()).map((state) => this.cloneRecord(state.record)).sort((a, b) => b.lastActivityAt - a.lastActivityAt);
196
+ }
197
+ getSession(sessionId) {
198
+ const state = this.sessions.get(sessionId);
199
+ return state ? this.cloneRecord(state.record) : null;
200
+ }
201
+ attachClient(payload) {
202
+ const state = this.requireSession(payload.sessionId);
203
+ const now = Date.now();
204
+ let removedDaemonOwner = false;
205
+ if (payload.clientType === "daemon") {
206
+ const staleDaemonClientIds = state.record.attachedClients.filter((client) => client.type === "daemon" && client.clientId !== payload.clientId).map((client) => client.clientId);
207
+ if (staleDaemonClientIds.length > 0) {
208
+ state.record.attachedClients = state.record.attachedClients.filter(
209
+ (client) => !(client.type === "daemon" && client.clientId !== payload.clientId)
210
+ );
211
+ if (state.record.writeOwner && staleDaemonClientIds.includes(state.record.writeOwner.clientId)) {
212
+ removedDaemonOwner = true;
213
+ }
214
+ }
215
+ }
216
+ const existing = state.record.attachedClients.find((client) => client.clientId === payload.clientId);
217
+ if (existing) {
218
+ existing.type = payload.clientType;
219
+ existing.readOnly = !!payload.readOnly;
220
+ existing.lastSeenAt = now;
221
+ } else {
222
+ state.record.attachedClients.push({
223
+ clientId: payload.clientId,
224
+ type: payload.clientType,
225
+ readOnly: !!payload.readOnly,
226
+ attachedAt: now,
227
+ lastSeenAt: now
228
+ });
229
+ }
230
+ if (removedDaemonOwner) {
231
+ state.record.writeOwner = null;
232
+ }
233
+ state.record.lastActivityAt = now;
234
+ return this.cloneRecord(state.record);
235
+ }
236
+ detachClient(payload) {
237
+ const state = this.requireSession(payload.sessionId);
238
+ state.record.attachedClients = state.record.attachedClients.filter((client) => client.clientId !== payload.clientId);
239
+ if (state.record.writeOwner?.clientId === payload.clientId) {
240
+ state.record.writeOwner = null;
241
+ }
242
+ state.record.lastActivityAt = Date.now();
243
+ return this.cloneRecord(state.record);
244
+ }
245
+ acquireWrite(payload) {
246
+ const state = this.requireSession(payload.sessionId);
247
+ if (state.record.writeOwner && state.record.writeOwner.clientId !== payload.clientId && !payload.force) {
248
+ throw new Error(`Write owned by ${state.record.writeOwner.clientId}`);
249
+ }
250
+ const attachedClient = state.record.attachedClients.find((client) => client.clientId === payload.clientId);
251
+ if (attachedClient) {
252
+ attachedClient.readOnly = false;
253
+ attachedClient.lastSeenAt = Date.now();
254
+ }
255
+ state.record.writeOwner = {
256
+ clientId: payload.clientId,
257
+ ownerType: payload.ownerType,
258
+ acquiredAt: Date.now()
259
+ };
260
+ state.record.lastActivityAt = Date.now();
261
+ return this.cloneRecord(state.record);
262
+ }
263
+ releaseWrite(payload) {
264
+ const state = this.requireSession(payload.sessionId);
265
+ const attachedClient = state.record.attachedClients.find((client) => client.clientId === payload.clientId);
266
+ if (attachedClient) {
267
+ attachedClient.readOnly = false;
268
+ attachedClient.lastSeenAt = Date.now();
269
+ }
270
+ if (state.record.writeOwner?.clientId === payload.clientId) {
271
+ state.record.writeOwner = null;
272
+ }
273
+ state.record.lastActivityAt = Date.now();
274
+ return this.cloneRecord(state.record);
275
+ }
276
+ appendOutput(sessionId, data) {
277
+ const state = this.requireSession(sessionId);
278
+ const seq = state.buffer.append(data);
279
+ state.record.buffer = state.buffer.getState();
280
+ state.record.lastActivityAt = Date.now();
281
+ return { record: this.cloneRecord(state.record), seq };
282
+ }
283
+ getSnapshot(sessionId, sinceSeq) {
284
+ const state = this.requireSession(sessionId);
285
+ state.record.buffer = state.buffer.getState();
286
+ return state.buffer.snapshot(sinceSeq);
287
+ }
288
+ clearBuffer(sessionId) {
289
+ const state = this.requireSession(sessionId);
290
+ state.buffer.clear();
291
+ state.record.buffer = state.buffer.getState();
292
+ state.record.lastActivityAt = Date.now();
293
+ return this.cloneRecord(state.record);
294
+ }
295
+ markStarted(sessionId, pid) {
296
+ const state = this.requireSession(sessionId);
297
+ state.record.lifecycle = "running";
298
+ state.record.startedAt = state.record.startedAt || Date.now();
299
+ if (typeof pid === "number") state.record.osPid = pid;
300
+ state.record.lastActivityAt = Date.now();
301
+ return this.cloneRecord(state.record);
302
+ }
303
+ markStopped(sessionId, lifecycle = "stopped") {
304
+ const state = this.requireSession(sessionId);
305
+ state.record.lifecycle = lifecycle;
306
+ state.record.lastActivityAt = Date.now();
307
+ return this.cloneRecord(state.record);
308
+ }
309
+ setLifecycle(sessionId, lifecycle) {
310
+ const state = this.requireSession(sessionId);
311
+ state.record.lifecycle = lifecycle;
312
+ state.record.lastActivityAt = Date.now();
313
+ return this.cloneRecord(state.record);
314
+ }
315
+ requireSession(sessionId) {
316
+ const state = this.sessions.get(sessionId);
317
+ if (!state) throw new Error(`Unknown session: ${sessionId}`);
318
+ return state;
319
+ }
320
+ cloneRecord(record) {
321
+ return {
322
+ ...record,
323
+ launchCommand: {
324
+ ...record.launchCommand,
325
+ args: [...record.launchCommand.args],
326
+ env: record.launchCommand.env ? { ...record.launchCommand.env } : void 0
327
+ },
328
+ writeOwner: record.writeOwner ? { ...record.writeOwner } : null,
329
+ attachedClients: record.attachedClients.map((client) => ({ ...client })),
330
+ buffer: { ...record.buffer },
331
+ meta: { ...record.meta }
332
+ };
333
+ }
334
+ };
335
+
336
+ // src/ipc.ts
337
+ import * as os from "os";
338
+ import * as path2 from "path";
339
+ import * as net from "net";
340
+ import { randomUUID as randomUUID2 } from "crypto";
341
+ function getDefaultSessionHostEndpoint(appName = "adhdev") {
342
+ if (process.platform === "win32") {
343
+ return {
344
+ kind: "pipe",
345
+ path: `\\\\.\\pipe\\${appName}-session-host`
346
+ };
347
+ }
348
+ return {
349
+ kind: "unix",
350
+ path: path2.join(os.tmpdir(), `${appName}-session-host.sock`)
351
+ };
352
+ }
353
+ function serializeEnvelope(envelope) {
354
+ return `${JSON.stringify(envelope)}
355
+ `;
356
+ }
357
+ function createLineParser(onEnvelope) {
358
+ let buffer = "";
359
+ return (chunk) => {
360
+ buffer += chunk.toString();
361
+ let newlineIndex = buffer.indexOf("\n");
362
+ while (newlineIndex >= 0) {
363
+ const rawLine = buffer.slice(0, newlineIndex).trim();
364
+ buffer = buffer.slice(newlineIndex + 1);
365
+ if (rawLine) {
366
+ onEnvelope(JSON.parse(rawLine));
367
+ }
368
+ newlineIndex = buffer.indexOf("\n");
369
+ }
370
+ };
371
+ }
372
+ var SessionHostClient = class {
373
+ endpoint;
374
+ socket = null;
375
+ requestWaiters = /* @__PURE__ */ new Map();
376
+ eventListeners = /* @__PURE__ */ new Set();
377
+ constructor(options = {}) {
378
+ this.endpoint = options.endpoint || getDefaultSessionHostEndpoint(options.appName || "adhdev");
379
+ }
380
+ async connect() {
381
+ if (this.socket && !this.socket.destroyed) return;
382
+ const socket = net.createConnection(this.endpoint.path);
383
+ this.socket = socket;
384
+ socket.on("data", createLineParser((envelope) => {
385
+ if (envelope.kind === "response") {
386
+ const waiter = this.requestWaiters.get(envelope.requestId);
387
+ if (waiter) {
388
+ this.requestWaiters.delete(envelope.requestId);
389
+ waiter.resolve(envelope.response);
390
+ }
391
+ return;
392
+ }
393
+ if (envelope.kind === "event") {
394
+ for (const listener of this.eventListeners) listener(envelope.event);
395
+ }
396
+ }));
397
+ socket.on("error", (error) => {
398
+ for (const waiter of this.requestWaiters.values()) {
399
+ waiter.reject(error);
400
+ }
401
+ this.requestWaiters.clear();
402
+ });
403
+ await new Promise((resolve, reject) => {
404
+ socket.once("connect", () => resolve());
405
+ socket.once("error", reject);
406
+ });
407
+ }
408
+ onEvent(listener) {
409
+ this.eventListeners.add(listener);
410
+ return () => {
411
+ this.eventListeners.delete(listener);
412
+ };
413
+ }
414
+ async request(request) {
415
+ await this.connect();
416
+ if (!this.socket) throw new Error("Session host socket unavailable");
417
+ const requestId = randomUUID2();
418
+ const envelope = {
419
+ kind: "request",
420
+ requestId,
421
+ request
422
+ };
423
+ const response = await new Promise((resolve, reject) => {
424
+ this.requestWaiters.set(requestId, { resolve, reject });
425
+ this.socket?.write(serializeEnvelope(envelope));
426
+ });
427
+ return response;
428
+ }
429
+ async close() {
430
+ if (!this.socket) return;
431
+ const socket = this.socket;
432
+ this.socket = null;
433
+ for (const waiter of this.requestWaiters.values()) {
434
+ waiter.reject(new Error("Session host client closed"));
435
+ }
436
+ this.requestWaiters.clear();
437
+ await new Promise((resolve) => {
438
+ let settled = false;
439
+ const done = () => {
440
+ if (settled) return;
441
+ settled = true;
442
+ resolve();
443
+ };
444
+ socket.once("close", done);
445
+ socket.end();
446
+ socket.destroy();
447
+ setTimeout(done, 50);
448
+ });
449
+ }
450
+ };
451
+ function createResponseEnvelope(requestId, response) {
452
+ return {
453
+ kind: "response",
454
+ requestId,
455
+ response
456
+ };
457
+ }
458
+ function writeEnvelope(socket, envelope) {
459
+ socket.write(serializeEnvelope(envelope));
460
+ }
461
+ export {
462
+ SessionHostClient,
463
+ SessionHostRegistry,
464
+ SessionRingBuffer,
465
+ buildRuntimeDisplayName,
466
+ buildRuntimeKey,
467
+ createLineParser,
468
+ createResponseEnvelope,
469
+ formatRuntimeOwner,
470
+ getDefaultSessionHostEndpoint,
471
+ getWorkspaceLabel,
472
+ resolveRuntimeRecord,
473
+ writeEnvelope
474
+ };
475
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/buffer.ts","../src/registry.ts","../src/runtime-labels.ts","../src/ipc.ts"],"sourcesContent":["import type { SessionBufferSnapshot } from './types.js';\n\nexport interface SessionRingBufferOptions {\n maxBytes?: number;\n}\n\nexport class SessionRingBuffer {\n private maxBytes: number;\n private chunks: { seq: number; data: string; bytes: number }[] = [];\n private nextSeq = 1;\n private totalBytes = 0;\n\n constructor(options: SessionRingBufferOptions = {}) {\n this.maxBytes = options.maxBytes ?? 512 * 1024;\n }\n\n append(data: string): number {\n const normalized = typeof data === 'string' ? data : String(data ?? '');\n const bytes = Buffer.byteLength(normalized, 'utf8');\n const seq = this.nextSeq++;\n\n this.chunks.push({ seq, data: normalized, bytes });\n this.totalBytes += bytes;\n this.trim();\n return seq;\n }\n\n snapshot(sinceSeq?: number): SessionBufferSnapshot {\n const relevant = typeof sinceSeq === 'number'\n ? this.chunks.filter(chunk => chunk.seq > sinceSeq)\n : this.chunks;\n\n const text = relevant.map(chunk => chunk.data).join('');\n const truncated = !!this.chunks[0] && typeof sinceSeq === 'number' && sinceSeq < this.chunks[0].seq - 1;\n\n return {\n seq: this.nextSeq - 1,\n text,\n truncated,\n };\n }\n\n getState(): { scrollbackBytes: number; snapshotSeq: number } {\n return {\n scrollbackBytes: this.totalBytes,\n snapshotSeq: this.nextSeq - 1,\n };\n }\n\n clear(): void {\n this.chunks = [];\n this.totalBytes = 0;\n this.nextSeq = 1;\n }\n\n restore(snapshot: { seq: number; text: string }): void {\n this.clear();\n const text = String(snapshot.text || '');\n if (!text) {\n this.nextSeq = Math.max(1, Number(snapshot.seq || 0) + 1);\n return;\n }\n const bytes = Buffer.byteLength(text, 'utf8');\n const seq = Math.max(1, Number(snapshot.seq || 1));\n this.chunks = [{ seq, data: text, bytes }];\n this.totalBytes = bytes;\n this.nextSeq = seq + 1;\n this.trim();\n }\n\n private trim(): void {\n while (this.totalBytes > this.maxBytes && this.chunks.length > 1) {\n const removed = this.chunks.shift();\n if (!removed) break;\n this.totalBytes -= removed.bytes;\n }\n }\n}\n","import { randomUUID } from 'crypto';\nimport type {\n AcquireWritePayload,\n AttachSessionPayload,\n CreateSessionPayload,\n DetachSessionPayload,\n ReleaseWritePayload,\n SessionAttachedClient,\n SessionHostRecord,\n} from './types.js';\nimport { SessionRingBuffer } from './buffer.js';\nimport { buildRuntimeDisplayName, buildRuntimeKey, getWorkspaceLabel } from './runtime-labels.js';\n\ninterface SessionRuntimeState {\n record: SessionHostRecord;\n buffer: SessionRingBuffer;\n}\n\nexport class SessionHostRegistry {\n private sessions = new Map<string, SessionRuntimeState>();\n\n createSession(payload: CreateSessionPayload): SessionHostRecord {\n const sessionId = payload.sessionId || randomUUID();\n if (this.sessions.has(sessionId)) {\n throw new Error(`Session already exists: ${sessionId}`);\n }\n const now = Date.now();\n const initialClient = payload.clientId\n ? [{\n clientId: payload.clientId,\n type: payload.clientType || 'daemon',\n readOnly: false,\n attachedAt: now,\n lastSeenAt: now,\n } satisfies SessionAttachedClient]\n : [];\n\n const record: SessionHostRecord = {\n sessionId,\n runtimeKey: buildRuntimeKey(\n payload,\n Array.from(this.sessions.values(), (state) => state.record.runtimeKey),\n ),\n displayName: buildRuntimeDisplayName(payload),\n workspaceLabel: getWorkspaceLabel(payload.workspace),\n transport: 'pty',\n providerType: payload.providerType,\n category: payload.category,\n workspace: payload.workspace,\n launchCommand: payload.launchCommand,\n createdAt: now,\n lastActivityAt: now,\n lifecycle: 'starting',\n writeOwner: null,\n attachedClients: initialClient,\n buffer: {\n scrollbackBytes: 0,\n snapshotSeq: 0,\n },\n meta: payload.meta || {},\n };\n\n record.meta = {\n sessionHostCols: payload.cols || 120,\n sessionHostRows: payload.rows || 40,\n ...record.meta,\n };\n\n this.sessions.set(sessionId, {\n record,\n buffer: new SessionRingBuffer(),\n });\n\n return this.cloneRecord(record);\n }\n\n restoreSession(record: SessionHostRecord, snapshot?: { seq: number; text: string } | null): SessionHostRecord {\n const cloned = this.cloneRecord(record);\n this.sessions.set(cloned.sessionId, {\n record: cloned,\n buffer: (() => {\n const buffer = new SessionRingBuffer();\n if (snapshot) buffer.restore(snapshot);\n return buffer;\n })(),\n });\n return this.cloneRecord(cloned);\n }\n\n listSessions(): SessionHostRecord[] {\n return Array.from(this.sessions.values())\n .map(state => this.cloneRecord(state.record))\n .sort((a, b) => b.lastActivityAt - a.lastActivityAt);\n }\n\n getSession(sessionId: string): SessionHostRecord | null {\n const state = this.sessions.get(sessionId);\n return state ? this.cloneRecord(state.record) : null;\n }\n\n attachClient(payload: AttachSessionPayload): SessionHostRecord {\n const state = this.requireSession(payload.sessionId);\n const now = Date.now();\n let removedDaemonOwner = false;\n\n if (payload.clientType === 'daemon') {\n const staleDaemonClientIds = state.record.attachedClients\n .filter(client => client.type === 'daemon' && client.clientId !== payload.clientId)\n .map(client => client.clientId);\n if (staleDaemonClientIds.length > 0) {\n state.record.attachedClients = state.record.attachedClients.filter(\n client => !(client.type === 'daemon' && client.clientId !== payload.clientId),\n );\n if (state.record.writeOwner && staleDaemonClientIds.includes(state.record.writeOwner.clientId)) {\n removedDaemonOwner = true;\n }\n }\n }\n\n const existing = state.record.attachedClients.find(client => client.clientId === payload.clientId);\n\n if (existing) {\n existing.type = payload.clientType;\n existing.readOnly = !!payload.readOnly;\n existing.lastSeenAt = now;\n } else {\n state.record.attachedClients.push({\n clientId: payload.clientId,\n type: payload.clientType,\n readOnly: !!payload.readOnly,\n attachedAt: now,\n lastSeenAt: now,\n });\n }\n\n if (removedDaemonOwner) {\n state.record.writeOwner = null;\n }\n\n state.record.lastActivityAt = now;\n return this.cloneRecord(state.record);\n }\n\n detachClient(payload: DetachSessionPayload): SessionHostRecord {\n const state = this.requireSession(payload.sessionId);\n state.record.attachedClients = state.record.attachedClients.filter(client => client.clientId !== payload.clientId);\n if (state.record.writeOwner?.clientId === payload.clientId) {\n state.record.writeOwner = null;\n }\n state.record.lastActivityAt = Date.now();\n return this.cloneRecord(state.record);\n }\n\n acquireWrite(payload: AcquireWritePayload): SessionHostRecord {\n const state = this.requireSession(payload.sessionId);\n if (state.record.writeOwner && state.record.writeOwner.clientId !== payload.clientId && !payload.force) {\n throw new Error(`Write owned by ${state.record.writeOwner.clientId}`);\n }\n const attachedClient = state.record.attachedClients.find(client => client.clientId === payload.clientId);\n if (attachedClient) {\n attachedClient.readOnly = false;\n attachedClient.lastSeenAt = Date.now();\n }\n state.record.writeOwner = {\n clientId: payload.clientId,\n ownerType: payload.ownerType,\n acquiredAt: Date.now(),\n };\n state.record.lastActivityAt = Date.now();\n return this.cloneRecord(state.record);\n }\n\n releaseWrite(payload: ReleaseWritePayload): SessionHostRecord {\n const state = this.requireSession(payload.sessionId);\n const attachedClient = state.record.attachedClients.find(client => client.clientId === payload.clientId);\n if (attachedClient) {\n attachedClient.readOnly = false;\n attachedClient.lastSeenAt = Date.now();\n }\n if (state.record.writeOwner?.clientId === payload.clientId) {\n state.record.writeOwner = null;\n }\n state.record.lastActivityAt = Date.now();\n return this.cloneRecord(state.record);\n }\n\n appendOutput(sessionId: string, data: string): { record: SessionHostRecord; seq: number } {\n const state = this.requireSession(sessionId);\n const seq = state.buffer.append(data);\n state.record.buffer = state.buffer.getState();\n state.record.lastActivityAt = Date.now();\n return { record: this.cloneRecord(state.record), seq };\n }\n\n getSnapshot(sessionId: string, sinceSeq?: number) {\n const state = this.requireSession(sessionId);\n state.record.buffer = state.buffer.getState();\n return state.buffer.snapshot(sinceSeq);\n }\n\n clearBuffer(sessionId: string): SessionHostRecord {\n const state = this.requireSession(sessionId);\n state.buffer.clear();\n state.record.buffer = state.buffer.getState();\n state.record.lastActivityAt = Date.now();\n return this.cloneRecord(state.record);\n }\n\n markStarted(sessionId: string, pid?: number): SessionHostRecord {\n const state = this.requireSession(sessionId);\n state.record.lifecycle = 'running';\n state.record.startedAt = state.record.startedAt || Date.now();\n if (typeof pid === 'number') state.record.osPid = pid;\n state.record.lastActivityAt = Date.now();\n return this.cloneRecord(state.record);\n }\n\n markStopped(sessionId: string, lifecycle: 'stopped' | 'failed' = 'stopped'): SessionHostRecord {\n const state = this.requireSession(sessionId);\n state.record.lifecycle = lifecycle;\n state.record.lastActivityAt = Date.now();\n return this.cloneRecord(state.record);\n }\n\n setLifecycle(sessionId: string, lifecycle: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'interrupted'): SessionHostRecord {\n const state = this.requireSession(sessionId);\n state.record.lifecycle = lifecycle;\n state.record.lastActivityAt = Date.now();\n return this.cloneRecord(state.record);\n }\n\n private requireSession(sessionId: string): SessionRuntimeState {\n const state = this.sessions.get(sessionId);\n if (!state) throw new Error(`Unknown session: ${sessionId}`);\n return state;\n }\n\n private cloneRecord(record: SessionHostRecord): SessionHostRecord {\n return {\n ...record,\n launchCommand: {\n ...record.launchCommand,\n args: [...record.launchCommand.args],\n env: record.launchCommand.env ? { ...record.launchCommand.env } : undefined,\n },\n writeOwner: record.writeOwner ? { ...record.writeOwner } : null,\n attachedClients: record.attachedClients.map(client => ({ ...client })),\n buffer: { ...record.buffer },\n meta: { ...record.meta },\n };\n }\n}\n","import * as path from 'path';\nimport type { CreateSessionPayload, SessionHostRecord } from './types.js';\n\nfunction normalizeSlug(input: string): string {\n return input\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 48);\n}\n\nfunction normalizeValue(input: string): string {\n return input.trim().toLowerCase();\n}\n\nexport function getWorkspaceLabel(workspace: string): string {\n const trimmed = workspace.trim();\n if (!trimmed) return 'workspace';\n const normalized = trimmed.replace(/[\\\\/]+$/, '');\n const base = path.basename(normalized);\n return base || normalized;\n}\n\nexport function buildRuntimeDisplayName(payload: Pick<CreateSessionPayload, 'displayName' | 'providerType' | 'workspace'>): string {\n const explicit = payload.displayName?.trim();\n if (explicit) return explicit;\n const workspaceLabel = getWorkspaceLabel(payload.workspace);\n const providerLabel = payload.providerType.trim() || 'runtime';\n return `${providerLabel} @ ${workspaceLabel}`;\n}\n\nexport function buildRuntimeKey(\n payload: Pick<CreateSessionPayload, 'runtimeKey' | 'displayName' | 'providerType' | 'workspace'>,\n existingKeys: Iterable<string>,\n): string {\n const requested = payload.runtimeKey?.trim();\n const existing = new Set(Array.from(existingKeys, (key) => key.toLowerCase()));\n const displayName = buildRuntimeDisplayName(payload);\n const baseKey = normalizeSlug(requested || displayName || getWorkspaceLabel(payload.workspace) || payload.providerType || 'runtime') || 'runtime';\n if (!existing.has(baseKey)) return baseKey;\n\n let suffix = 2;\n let candidate = `${baseKey}-${suffix}`;\n while (existing.has(candidate)) {\n suffix += 1;\n candidate = `${baseKey}-${suffix}`;\n }\n return candidate;\n}\n\nfunction uniqueMatch(records: SessionHostRecord[], predicate: (record: SessionHostRecord) => boolean): SessionHostRecord | null {\n const matches = records.filter(predicate);\n if (matches.length === 1) return matches[0] || null;\n if (matches.length === 0) return null;\n const labels = matches.map((record) => `${record.runtimeKey} (${record.sessionId})`).join(', ');\n throw new Error(`Ambiguous runtime target. Matches: ${labels}`);\n}\n\nexport function resolveRuntimeRecord(records: SessionHostRecord[], identifier: string): SessionHostRecord {\n const target = identifier.trim();\n if (!target) {\n throw new Error('Runtime target is required');\n }\n\n const exact = uniqueMatch(records, (record) =>\n record.sessionId === target ||\n normalizeValue(record.runtimeKey) === normalizeValue(target) ||\n normalizeValue(record.displayName) === normalizeValue(target),\n );\n if (exact) return exact;\n\n const prefix = uniqueMatch(records, (record) =>\n record.sessionId.startsWith(target) ||\n normalizeValue(record.runtimeKey).startsWith(normalizeValue(target)),\n );\n if (prefix) return prefix;\n\n throw new Error(`Unknown runtime target: ${target}`);\n}\n\nexport function formatRuntimeOwner(record: Pick<SessionHostRecord, 'writeOwner'>): string {\n if (!record.writeOwner) return 'none';\n return `${record.writeOwner.ownerType}:${record.writeOwner.clientId}`;\n}\n","import * as os from 'os';\nimport * as path from 'path';\nimport * as net from 'net';\nimport { randomUUID } from 'crypto';\nimport type {\n SessionHostEvent,\n SessionHostRequest,\n SessionHostRequestEnvelope,\n SessionHostResponse,\n SessionHostResponseEnvelope,\n SessionHostWireEnvelope,\n} from './types.js';\n\nexport interface SessionHostEndpoint {\n kind: 'unix' | 'pipe';\n path: string;\n}\n\nexport function getDefaultSessionHostEndpoint(appName = 'adhdev'): SessionHostEndpoint {\n if (process.platform === 'win32') {\n return {\n kind: 'pipe',\n path: `\\\\\\\\.\\\\pipe\\\\${appName}-session-host`,\n };\n }\n\n return {\n kind: 'unix',\n path: path.join(os.tmpdir(), `${appName}-session-host.sock`),\n };\n}\n\nfunction serializeEnvelope(envelope: SessionHostWireEnvelope): string {\n return `${JSON.stringify(envelope)}\\n`;\n}\n\nfunction createLineParser(onEnvelope: (envelope: SessionHostWireEnvelope) => void) {\n let buffer = '';\n return (chunk: Buffer | string) => {\n buffer += chunk.toString();\n let newlineIndex = buffer.indexOf('\\n');\n while (newlineIndex >= 0) {\n const rawLine = buffer.slice(0, newlineIndex).trim();\n buffer = buffer.slice(newlineIndex + 1);\n if (rawLine) {\n onEnvelope(JSON.parse(rawLine) as SessionHostWireEnvelope);\n }\n newlineIndex = buffer.indexOf('\\n');\n }\n };\n}\n\nexport interface SessionHostClientOptions {\n endpoint?: SessionHostEndpoint;\n appName?: string;\n}\n\nexport class SessionHostClient {\n readonly endpoint: SessionHostEndpoint;\n\n private socket: net.Socket | null = null;\n private requestWaiters = new Map<string, { resolve: (value: SessionHostResponse) => void; reject: (error: Error) => void }>();\n private eventListeners = new Set<(event: SessionHostEvent) => void>();\n\n constructor(options: SessionHostClientOptions = {}) {\n this.endpoint = options.endpoint || getDefaultSessionHostEndpoint(options.appName || 'adhdev');\n }\n\n async connect(): Promise<void> {\n if (this.socket && !this.socket.destroyed) return;\n\n const socket = net.createConnection(this.endpoint.path);\n this.socket = socket;\n\n socket.on('data', createLineParser((envelope) => {\n if (envelope.kind === 'response') {\n const waiter = this.requestWaiters.get(envelope.requestId);\n if (waiter) {\n this.requestWaiters.delete(envelope.requestId);\n waiter.resolve(envelope.response);\n }\n return;\n }\n\n if (envelope.kind === 'event') {\n for (const listener of this.eventListeners) listener(envelope.event);\n }\n }));\n\n socket.on('error', (error) => {\n for (const waiter of this.requestWaiters.values()) {\n waiter.reject(error);\n }\n this.requestWaiters.clear();\n });\n\n await new Promise<void>((resolve, reject) => {\n socket.once('connect', () => resolve());\n socket.once('error', reject);\n });\n }\n\n onEvent(listener: (event: SessionHostEvent) => void): () => void {\n this.eventListeners.add(listener);\n return () => {\n this.eventListeners.delete(listener);\n };\n }\n\n async request<T = unknown>(request: SessionHostRequest): Promise<SessionHostResponse<T>> {\n await this.connect();\n if (!this.socket) throw new Error('Session host socket unavailable');\n\n const requestId = randomUUID();\n const envelope: SessionHostRequestEnvelope = {\n kind: 'request',\n requestId,\n request,\n };\n\n const response = await new Promise<SessionHostResponse>((resolve, reject) => {\n this.requestWaiters.set(requestId, { resolve, reject });\n this.socket?.write(serializeEnvelope(envelope));\n });\n\n return response as SessionHostResponse<T>;\n }\n\n async close(): Promise<void> {\n if (!this.socket) return;\n const socket = this.socket;\n this.socket = null;\n for (const waiter of this.requestWaiters.values()) {\n waiter.reject(new Error('Session host client closed'));\n }\n this.requestWaiters.clear();\n await new Promise<void>((resolve) => {\n let settled = false;\n const done = () => {\n if (settled) return;\n settled = true;\n resolve();\n };\n socket.once('close', done);\n socket.end();\n socket.destroy();\n setTimeout(done, 50);\n });\n }\n}\n\nexport function createResponseEnvelope(requestId: string, response: SessionHostResponse): SessionHostResponseEnvelope {\n return {\n kind: 'response',\n requestId,\n response,\n };\n}\n\nexport function writeEnvelope(socket: Pick<net.Socket, 'write'>, envelope: SessionHostWireEnvelope): void {\n socket.write(serializeEnvelope(envelope));\n}\n\nexport { createLineParser };\n"],"mappings":"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","names":["path","randomUUID"]}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@adhdev/session-host-core",
3
+ "version": "0.7.12",
4
+ "description": "ADHDev local session host core — session registry, protocol, buffers",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "typecheck": "tsc --noEmit -p tsconfig.json"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "keywords": [
25
+ "adhdev",
26
+ "session-host",
27
+ "pty",
28
+ "runtime"
29
+ ],
30
+ "author": "vilmire",
31
+ "license": "AGPL-3.0-or-later",
32
+ "devDependencies": {
33
+ "@types/node": "^22.0.0",
34
+ "tsup": "^8.2.0",
35
+ "typescript": "^5.5.0"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/vilmire/adhdev.git",
40
+ "directory": "packages/session-host-core"
41
+ },
42
+ "homepage": "https://github.com/vilmire/adhdev#readme"
43
+ }
package/src/buffer.ts ADDED
@@ -0,0 +1,78 @@
1
+ import type { SessionBufferSnapshot } from './types.js';
2
+
3
+ export interface SessionRingBufferOptions {
4
+ maxBytes?: number;
5
+ }
6
+
7
+ export class SessionRingBuffer {
8
+ private maxBytes: number;
9
+ private chunks: { seq: number; data: string; bytes: number }[] = [];
10
+ private nextSeq = 1;
11
+ private totalBytes = 0;
12
+
13
+ constructor(options: SessionRingBufferOptions = {}) {
14
+ this.maxBytes = options.maxBytes ?? 512 * 1024;
15
+ }
16
+
17
+ append(data: string): number {
18
+ const normalized = typeof data === 'string' ? data : String(data ?? '');
19
+ const bytes = Buffer.byteLength(normalized, 'utf8');
20
+ const seq = this.nextSeq++;
21
+
22
+ this.chunks.push({ seq, data: normalized, bytes });
23
+ this.totalBytes += bytes;
24
+ this.trim();
25
+ return seq;
26
+ }
27
+
28
+ snapshot(sinceSeq?: number): SessionBufferSnapshot {
29
+ const relevant = typeof sinceSeq === 'number'
30
+ ? this.chunks.filter(chunk => chunk.seq > sinceSeq)
31
+ : this.chunks;
32
+
33
+ const text = relevant.map(chunk => chunk.data).join('');
34
+ const truncated = !!this.chunks[0] && typeof sinceSeq === 'number' && sinceSeq < this.chunks[0].seq - 1;
35
+
36
+ return {
37
+ seq: this.nextSeq - 1,
38
+ text,
39
+ truncated,
40
+ };
41
+ }
42
+
43
+ getState(): { scrollbackBytes: number; snapshotSeq: number } {
44
+ return {
45
+ scrollbackBytes: this.totalBytes,
46
+ snapshotSeq: this.nextSeq - 1,
47
+ };
48
+ }
49
+
50
+ clear(): void {
51
+ this.chunks = [];
52
+ this.totalBytes = 0;
53
+ this.nextSeq = 1;
54
+ }
55
+
56
+ restore(snapshot: { seq: number; text: string }): void {
57
+ this.clear();
58
+ const text = String(snapshot.text || '');
59
+ if (!text) {
60
+ this.nextSeq = Math.max(1, Number(snapshot.seq || 0) + 1);
61
+ return;
62
+ }
63
+ const bytes = Buffer.byteLength(text, 'utf8');
64
+ const seq = Math.max(1, Number(snapshot.seq || 1));
65
+ this.chunks = [{ seq, data: text, bytes }];
66
+ this.totalBytes = bytes;
67
+ this.nextSeq = seq + 1;
68
+ this.trim();
69
+ }
70
+
71
+ private trim(): void {
72
+ while (this.totalBytes > this.maxBytes && this.chunks.length > 1) {
73
+ const removed = this.chunks.shift();
74
+ if (!removed) break;
75
+ this.totalBytes -= removed.bytes;
76
+ }
77
+ }
78
+ }