@overmux/pi 0.0.1

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 (70) hide show
  1. package/README.md +118 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +40 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/config-2jNv4tol.d.ts +34 -0
  6. package/dist/config-2jNv4tol.d.ts.map +1 -0
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.js +47 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/extension.d.ts +19 -0
  11. package/dist/extension.d.ts.map +1 -0
  12. package/dist/extension.js +265 -0
  13. package/dist/extension.js.map +1 -0
  14. package/dist/index.d.ts +10 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +16 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/jsonl-tail.d.ts +13 -0
  19. package/dist/jsonl-tail.d.ts.map +1 -0
  20. package/dist/jsonl-tail.js +83 -0
  21. package/dist/jsonl-tail.js.map +1 -0
  22. package/dist/live-events-ClhFOMGW.js +497 -0
  23. package/dist/live-events-ClhFOMGW.js.map +1 -0
  24. package/dist/live-events-DAmf6RRx.d.ts +107 -0
  25. package/dist/live-events-DAmf6RRx.d.ts.map +1 -0
  26. package/dist/live-events.d.ts +2 -0
  27. package/dist/live-events.js +2 -0
  28. package/dist/notification-DzOd9cRc.d.ts +20 -0
  29. package/dist/notification-DzOd9cRc.d.ts.map +1 -0
  30. package/dist/notification.d.ts +2 -0
  31. package/dist/notification.js +54 -0
  32. package/dist/notification.js.map +1 -0
  33. package/dist/plugin.d.ts +90 -0
  34. package/dist/plugin.d.ts.map +1 -0
  35. package/dist/plugin.js +213 -0
  36. package/dist/plugin.js.map +1 -0
  37. package/dist/projection.d.ts +101 -0
  38. package/dist/projection.d.ts.map +1 -0
  39. package/dist/projection.js +550 -0
  40. package/dist/projection.js.map +1 -0
  41. package/dist/protocol-CsrnSPOv.d.ts +115 -0
  42. package/dist/protocol-CsrnSPOv.d.ts.map +1 -0
  43. package/dist/protocol.d.ts +2 -0
  44. package/dist/protocol.js +365 -0
  45. package/dist/protocol.js.map +1 -0
  46. package/dist/react.d.ts +139 -0
  47. package/dist/react.d.ts.map +1 -0
  48. package/dist/react.js +796 -0
  49. package/dist/react.js.map +1 -0
  50. package/dist/server.d.ts +5 -0
  51. package/dist/server.js +4 -0
  52. package/dist/session-status-CZLTo8Km.d.ts +71 -0
  53. package/dist/session-status-CZLTo8Km.d.ts.map +1 -0
  54. package/dist/styles.css +619 -0
  55. package/docs/index.md +10 -0
  56. package/package.json +115 -0
  57. package/src/cli.ts +59 -0
  58. package/src/config.ts +87 -0
  59. package/src/extension.ts +486 -0
  60. package/src/index.ts +14 -0
  61. package/src/jsonl-tail.ts +110 -0
  62. package/src/live-events.ts +578 -0
  63. package/src/notification.ts +109 -0
  64. package/src/plugin.ts +369 -0
  65. package/src/projection.ts +995 -0
  66. package/src/protocol.ts +805 -0
  67. package/src/react.tsx +1293 -0
  68. package/src/server.ts +15 -0
  69. package/src/session-status.ts +379 -0
  70. package/src/styles.css +619 -0
package/src/server.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * from "./plugin.js";
2
+ export * from "./projection.js";
3
+ export {
4
+ createPiSessionStatusSource,
5
+ createPiSessionStatusWriter,
6
+ isLocalPidAlive,
7
+ listPiSessionStatuses,
8
+ parsePiSessionStatus,
9
+ piSessionStatusPath,
10
+ piSessionStatusSchema,
11
+ type PiSessionStatus,
12
+ type PiSessionStatusSource,
13
+ type PiSessionStatusUpdate,
14
+ type PiSessionStatusWriter,
15
+ } from "./session-status.js";
@@ -0,0 +1,379 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { watch, type FSWatcher } from "node:fs";
3
+ import {
4
+ mkdir,
5
+ readFile,
6
+ readdir,
7
+ rename,
8
+ rm,
9
+ writeFile,
10
+ } from "node:fs/promises";
11
+ import { hostname as localHostname } from "node:os";
12
+ import { join, resolve } from "node:path";
13
+
14
+ import { z } from "zod";
15
+
16
+ export const piSessionStatusSchema = z
17
+ .object({
18
+ version: z.literal(1),
19
+ sessionId: z.string().min(1),
20
+ sessionFile: z.string().min(1),
21
+ streamId: z.string().min(1),
22
+ processInstanceId: z.string().min(1),
23
+ pid: z.number().int().positive(),
24
+ hostname: z.string().min(1),
25
+ tmuxPane: z.string().min(1).optional(),
26
+ startedAt: z.number().int().nonnegative(),
27
+ updatedAt: z.number().int().nonnegative(),
28
+ state: z.enum(["idle", "busy", "stopped"]),
29
+ model: z
30
+ .object({
31
+ provider: z.string().min(1),
32
+ id: z.string().min(1),
33
+ name: z.string().min(1),
34
+ })
35
+ .strict()
36
+ .optional(),
37
+ thinkingLevel: z.string().min(1),
38
+ modelOptions: z
39
+ .array(
40
+ z
41
+ .object({
42
+ provider: z.string().min(1),
43
+ id: z.string().min(1),
44
+ name: z.string().min(1),
45
+ })
46
+ .strict(),
47
+ )
48
+ .max(256)
49
+ .optional(),
50
+ contextUsage: z
51
+ .object({
52
+ tokens: z.number().nonnegative(),
53
+ contextWindow: z.number().positive(),
54
+ percent: z.number().nonnegative(),
55
+ })
56
+ .strict()
57
+ .optional(),
58
+ })
59
+ .strict();
60
+
61
+ export type PiSessionStatus = z.infer<typeof piSessionStatusSchema>;
62
+ export type PiSessionStatusUpdate = Partial<
63
+ Pick<
64
+ PiSessionStatus,
65
+ "contextUsage" | "model" | "modelOptions" | "state" | "thinkingLevel"
66
+ >
67
+ >;
68
+ export type PiSessionStatusWriter = {
69
+ filePath: string;
70
+ streamId: string;
71
+ update: (update: PiSessionStatusUpdate) => Promise<void>;
72
+ close: () => Promise<void>;
73
+ };
74
+ export type PiSessionStatusSource = {
75
+ list: () => Promise<readonly PiSessionStatus[]>;
76
+ subscribe: (
77
+ invalidate: () => void,
78
+ options: { signal: AbortSignal },
79
+ ) => () => void;
80
+ };
81
+
82
+ type StatusWriterState = {
83
+ accepting: boolean;
84
+ current: PiSessionStatus;
85
+ filePath: string;
86
+ onError?: (error: unknown) => void;
87
+ sessionDir: string;
88
+ writeChain: Promise<void>;
89
+ };
90
+
91
+ const SAFE_PATH_SEGMENT = /^[A-Za-z0-9._-]{1,128}$/;
92
+
93
+ const requirePathSegment = (value: string, label: string): string => {
94
+ if (!SAFE_PATH_SEGMENT.test(value)) {
95
+ throw new Error(`Invalid ${label} for Pi session status path`);
96
+ }
97
+ return value;
98
+ };
99
+
100
+ export const piSessionStatusPath = (
101
+ rootDir: string,
102
+ sessionId: string,
103
+ streamId: string,
104
+ ): string =>
105
+ join(
106
+ resolve(rootDir),
107
+ requirePathSegment(sessionId, "session ID"),
108
+ `${requirePathSegment(streamId, "stream ID")}.status.json`,
109
+ );
110
+
111
+ export const parsePiSessionStatus = (
112
+ value: unknown,
113
+ ): PiSessionStatus | undefined => {
114
+ const result = piSessionStatusSchema.safeParse(value);
115
+ return result.success ? result.data : undefined;
116
+ };
117
+
118
+ const reportError = (
119
+ onError: ((error: unknown) => void) | undefined,
120
+ error: unknown,
121
+ ): void => {
122
+ try {
123
+ onError?.(error);
124
+ } catch {}
125
+ };
126
+
127
+ const replaceStatusFile = async (
128
+ state: StatusWriterState,
129
+ status: PiSessionStatus,
130
+ ): Promise<void> => {
131
+ await mkdir(state.sessionDir, { recursive: true, mode: 0o700 });
132
+ const temporaryPath = `${state.filePath}.${randomUUID()}.tmp`;
133
+ try {
134
+ await writeFile(temporaryPath, `${JSON.stringify(status)}\n`, {
135
+ encoding: "utf8",
136
+ flag: "wx",
137
+ mode: 0o600,
138
+ });
139
+ await rename(temporaryPath, state.filePath);
140
+ } catch (error) {
141
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
142
+ throw error;
143
+ }
144
+ };
145
+
146
+ const updateStatus = (
147
+ state: StatusWriterState,
148
+ update: PiSessionStatusUpdate,
149
+ ): Promise<void> => {
150
+ if (!state.accepting) {
151
+ return state.writeChain;
152
+ }
153
+ const status = piSessionStatusSchema.parse({
154
+ ...state.current,
155
+ ...update,
156
+ updatedAt: Date.now(),
157
+ });
158
+ state.current = status;
159
+ const write = state.writeChain.then(() => replaceStatusFile(state, status));
160
+ state.writeChain = write.catch((error: unknown) =>
161
+ reportError(state.onError, error),
162
+ );
163
+ return state.writeChain;
164
+ };
165
+
166
+ export const createPiSessionStatusWriter = ({
167
+ rootDir,
168
+ status,
169
+ onError,
170
+ }: {
171
+ rootDir: string;
172
+ status: Omit<PiSessionStatus, "updatedAt" | "version">;
173
+ onError?: (error: unknown) => void;
174
+ }): PiSessionStatusWriter => {
175
+ const filePath = piSessionStatusPath(
176
+ rootDir,
177
+ status.sessionId,
178
+ status.streamId,
179
+ );
180
+ const state: StatusWriterState = {
181
+ accepting: true,
182
+ current: piSessionStatusSchema.parse({
183
+ ...status,
184
+ updatedAt: status.startedAt,
185
+ version: 1,
186
+ }),
187
+ filePath,
188
+ onError,
189
+ sessionDir: join(resolve(rootDir), status.sessionId),
190
+ writeChain: Promise.resolve(),
191
+ };
192
+
193
+ return {
194
+ filePath,
195
+ streamId: status.streamId,
196
+ update: (update) => updateStatus(state, update),
197
+ close: async () => {
198
+ if (!state.accepting) {
199
+ await state.writeChain;
200
+ return;
201
+ }
202
+ const closing = updateStatus(state, { state: "stopped" });
203
+ state.accepting = false;
204
+ await closing;
205
+ },
206
+ };
207
+ };
208
+
209
+ const readStatus = async (
210
+ filePath: string,
211
+ sessionId: string,
212
+ streamId: string,
213
+ ): Promise<PiSessionStatus | undefined> => {
214
+ try {
215
+ const status = parsePiSessionStatus(
216
+ JSON.parse(await readFile(filePath, "utf8")),
217
+ );
218
+ return status?.sessionId === sessionId && status.streamId === streamId
219
+ ? status
220
+ : undefined;
221
+ } catch {
222
+ return undefined;
223
+ }
224
+ };
225
+
226
+ const compareRecency = (
227
+ left: PiSessionStatus,
228
+ right: PiSessionStatus,
229
+ ): number =>
230
+ left.updatedAt - right.updatedAt ||
231
+ left.startedAt - right.startedAt ||
232
+ left.streamId.localeCompare(right.streamId);
233
+
234
+ const newestBy = (
235
+ statuses: readonly PiSessionStatus[],
236
+ keyOf: (status: PiSessionStatus) => string,
237
+ ): PiSessionStatus[] => {
238
+ const newest = new Map<string, PiSessionStatus>();
239
+ statuses.forEach((status) => {
240
+ const key = keyOf(status);
241
+ const previous = newest.get(key);
242
+ if (!previous || compareRecency(previous, status) < 0) {
243
+ newest.set(key, status);
244
+ }
245
+ });
246
+ return [...newest.values()];
247
+ };
248
+
249
+ export const isLocalPidAlive = (pid: number): boolean => {
250
+ try {
251
+ process.kill(pid, 0);
252
+ return true;
253
+ } catch (error) {
254
+ return (error as NodeJS.ErrnoException).code === "EPERM";
255
+ }
256
+ };
257
+
258
+ export const listPiSessionStatuses = async ({
259
+ rootDir,
260
+ hostname = localHostname(),
261
+ pidAlive = isLocalPidAlive,
262
+ }: {
263
+ rootDir: string;
264
+ hostname?: string;
265
+ pidAlive?: (pid: number) => boolean;
266
+ }): Promise<PiSessionStatus[]> => {
267
+ const sessionDirectories = (
268
+ await readdir(resolve(rootDir), {
269
+ withFileTypes: true,
270
+ }).catch(() => [])
271
+ )
272
+ .filter(
273
+ (entry) => entry.isDirectory() && SAFE_PATH_SEGMENT.test(entry.name),
274
+ )
275
+ .map((entry) => entry.name);
276
+ const statuses = (
277
+ await Promise.all(
278
+ sessionDirectories.map(async (sessionId) => {
279
+ const sessionDir = join(resolve(rootDir), sessionId);
280
+ const files = (
281
+ await readdir(sessionDir, { withFileTypes: true }).catch(() => [])
282
+ ).filter(
283
+ (entry) =>
284
+ entry.isFile() &&
285
+ entry.name.endsWith(".status.json") &&
286
+ SAFE_PATH_SEGMENT.test(entry.name.slice(0, -".status.json".length)),
287
+ );
288
+ return Promise.all(
289
+ files.map((entry) => {
290
+ const streamId = entry.name.slice(0, -".status.json".length);
291
+ return readStatus(
292
+ join(sessionDir, entry.name),
293
+ sessionId,
294
+ streamId,
295
+ );
296
+ }),
297
+ );
298
+ }),
299
+ )
300
+ )
301
+ .flatMap((values) => values)
302
+ .filter((status) => status !== undefined);
303
+ const processStatuses = newestBy(
304
+ statuses,
305
+ ({ hostname: statusHostname, processInstanceId }) =>
306
+ `${statusHostname}\0${processInstanceId}`,
307
+ ).filter(
308
+ (status) =>
309
+ status.state !== "stopped" &&
310
+ status.hostname === hostname &&
311
+ pidAlive(status.pid),
312
+ );
313
+ return newestBy(processStatuses, ({ sessionId }) => sessionId).sort(
314
+ (left, right) => left.sessionId.localeCompare(right.sessionId),
315
+ );
316
+ };
317
+
318
+ export const createPiSessionStatusSource = ({
319
+ rootDir,
320
+ pollIntervalMs = 250,
321
+ hostname,
322
+ pidAlive,
323
+ }: {
324
+ rootDir: string;
325
+ pollIntervalMs?: number;
326
+ hostname?: string;
327
+ pidAlive?: (pid: number) => boolean;
328
+ }): PiSessionStatusSource => ({
329
+ list: () => listPiSessionStatuses({ rootDir, hostname, pidAlive }),
330
+ subscribe: (invalidate, { signal }) => {
331
+ if (signal.aborted) {
332
+ return () => undefined;
333
+ }
334
+ let watcher: FSWatcher | undefined;
335
+ let scheduled: ReturnType<typeof setTimeout> | undefined;
336
+ let fingerprint: string | undefined;
337
+ let checkQueue = Promise.resolve();
338
+ const check = () => {
339
+ checkQueue = checkQueue
340
+ .then(async () => {
341
+ const next = JSON.stringify(
342
+ await listPiSessionStatuses({ rootDir, hostname, pidAlive }),
343
+ );
344
+ if (next === fingerprint || signal.aborted) {
345
+ return;
346
+ }
347
+ fingerprint = next;
348
+ invalidate();
349
+ })
350
+ .catch(() => undefined);
351
+ };
352
+ const schedule = () => {
353
+ if (scheduled || signal.aborted) {
354
+ return;
355
+ }
356
+ scheduled = setTimeout(() => {
357
+ scheduled = undefined;
358
+ check();
359
+ }, 10);
360
+ scheduled.unref();
361
+ };
362
+ try {
363
+ watcher = watch(resolve(rootDir), { recursive: true }, schedule);
364
+ } catch {}
365
+ check();
366
+ const timer = setInterval(schedule, pollIntervalMs);
367
+ timer.unref();
368
+ const dispose = () => {
369
+ signal.removeEventListener("abort", dispose);
370
+ if (scheduled) {
371
+ clearTimeout(scheduled);
372
+ }
373
+ clearInterval(timer);
374
+ watcher?.close();
375
+ };
376
+ signal.addEventListener("abort", dispose, { once: true });
377
+ return dispose;
378
+ },
379
+ });