@larose/pi-web 0.3.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.
Files changed (43) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +50 -0
  3. package/THIRD_PARTY_LICENSES.md +40 -0
  4. package/dist/client/home.js +1619 -0
  5. package/dist/client/session.js +3703 -0
  6. package/dist/server/api.js +485 -0
  7. package/dist/server/cli.js +51 -0
  8. package/dist/server/directory-browser.js +104 -0
  9. package/dist/server/errors.js +10 -0
  10. package/dist/server/event-buffer.js +40 -0
  11. package/dist/server/extension-ui.js +245 -0
  12. package/dist/server/git-workspaces.js +559 -0
  13. package/dist/server/runtime-registry.js +703 -0
  14. package/dist/server/server.js +190 -0
  15. package/dist/server/session-repository.js +374 -0
  16. package/package.json +46 -0
  17. package/public/home.html +139 -0
  18. package/public/session.html +144 -0
  19. package/public/styles.css +2463 -0
  20. package/screenshots/home.png +0 -0
  21. package/screenshots/session.png +0 -0
  22. package/src/client/display-title.ts +36 -0
  23. package/src/client/event-stream.ts +194 -0
  24. package/src/client/home.ts +1575 -0
  25. package/src/client/markdown.ts +98 -0
  26. package/src/client/message-queue.ts +67 -0
  27. package/src/client/path-combobox.ts +271 -0
  28. package/src/client/session.ts +2174 -0
  29. package/src/client/shared.ts +99 -0
  30. package/src/client/slash-completion.ts +184 -0
  31. package/src/client/transcript-activity.ts +188 -0
  32. package/src/client/usage-format.ts +156 -0
  33. package/src/client/workspace-browser.ts +36 -0
  34. package/src/server/api.ts +652 -0
  35. package/src/server/cli.ts +63 -0
  36. package/src/server/directory-browser.ts +137 -0
  37. package/src/server/errors.ts +11 -0
  38. package/src/server/event-buffer.ts +59 -0
  39. package/src/server/extension-ui.ts +359 -0
  40. package/src/server/git-workspaces.ts +750 -0
  41. package/src/server/runtime-registry.ts +943 -0
  42. package/src/server/server.ts +248 -0
  43. package/src/server/session-repository.ts +488 -0
@@ -0,0 +1,190 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createServer } from "node:http";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { handleApi, writeApiError } from "./api.js";
6
+ import { DirectoryBrowser } from "./directory-browser.js";
7
+ import { AppError } from "./errors.js";
8
+ import { GitWorkspaceService } from "./git-workspaces.js";
9
+ import { createRuntimeRegistry } from "./runtime-registry.js";
10
+ import { SessionRepository } from "./session-repository.js";
11
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
12
+ const homeDocument = {
13
+ path: join(packageRoot, "public/home.html"),
14
+ contentType: "text/html; charset=utf-8",
15
+ };
16
+ const sessionDocument = {
17
+ path: join(packageRoot, "public/session.html"),
18
+ contentType: "text/html; charset=utf-8",
19
+ };
20
+ const staticFiles = new Map([
21
+ ["/", homeDocument],
22
+ ["/styles.css", { path: join(packageRoot, "public/styles.css"), contentType: "text/css; charset=utf-8" }],
23
+ ["/home.js", { path: join(packageRoot, "dist/client/home.js"), contentType: "text/javascript; charset=utf-8" }],
24
+ ["/session.js", { path: join(packageRoot, "dist/client/session.js"), contentType: "text/javascript; charset=utf-8" }],
25
+ ]);
26
+ function errorDetails(error) {
27
+ if (!(error instanceof Error)) {
28
+ return String(error);
29
+ }
30
+ const details = error.stack ?? error.message;
31
+ return error.cause === undefined ? details : `${details}\nCaused by: ${errorDetails(error.cause)}`;
32
+ }
33
+ function shouldReportError(error) {
34
+ return !(error instanceof AppError) || error.status >= 500 || error.cause !== undefined;
35
+ }
36
+ function applySecurityHeaders(response) {
37
+ response.setHeader("content-security-policy", "default-src 'self'; base-uri 'none'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; script-src 'self'; style-src 'self'");
38
+ response.setHeader("referrer-policy", "no-referrer");
39
+ response.setHeader("x-content-type-options", "nosniff");
40
+ response.setHeader("x-frame-options", "DENY");
41
+ }
42
+ function boundPort(server) {
43
+ const address = server.address();
44
+ if (!address || typeof address === "string") {
45
+ throw new Error("Server is not bound to a TCP address");
46
+ }
47
+ return address.port;
48
+ }
49
+ function validateLocalRequest(request, server) {
50
+ const host = request.headers.host;
51
+ if (!host) {
52
+ throw new AppError("invalid_host", "A Host header is required", 403);
53
+ }
54
+ let hostUrl;
55
+ try {
56
+ hostUrl = new URL(`http://${host}`);
57
+ }
58
+ catch {
59
+ throw new AppError("invalid_host", "The Host header is invalid", 403);
60
+ }
61
+ const expectedPort = String(boundPort(server));
62
+ if (!new Set(["127.0.0.1", "localhost"]).has(hostUrl.hostname) || hostUrl.port !== expectedPort) {
63
+ throw new AppError("invalid_host", "The Host header is not allowed", 403);
64
+ }
65
+ const origin = request.headers.origin;
66
+ if (!origin) {
67
+ return;
68
+ }
69
+ try {
70
+ const originUrl = new URL(origin);
71
+ if (originUrl.protocol !== "http:" ||
72
+ !new Set(["127.0.0.1", "localhost"]).has(originUrl.hostname) ||
73
+ originUrl.port !== expectedPort) {
74
+ throw new Error("Origin does not match the local server");
75
+ }
76
+ }
77
+ catch {
78
+ throw new AppError("invalid_origin", "The Origin header is not allowed", 403);
79
+ }
80
+ }
81
+ async function serveAsset(asset, response) {
82
+ const contents = await readFile(asset.path);
83
+ response.writeHead(200, {
84
+ "cache-control": "no-store",
85
+ "content-length": contents.byteLength,
86
+ "content-type": asset.contentType,
87
+ });
88
+ response.end(contents);
89
+ }
90
+ async function serveStatic(pathname, response) {
91
+ const asset = staticFiles.get(pathname);
92
+ if (!asset) {
93
+ return false;
94
+ }
95
+ await serveAsset(asset, response);
96
+ return true;
97
+ }
98
+ function newSessionSource(pathname) {
99
+ const match = /^\/sessions\/([^/]+)\/new\/?$/.exec(pathname);
100
+ if (!match?.[1]) {
101
+ return undefined;
102
+ }
103
+ try {
104
+ return decodeURIComponent(match[1]);
105
+ }
106
+ catch {
107
+ throw new AppError("invalid_path", "The source session id is not valid", 400);
108
+ }
109
+ }
110
+ export async function startServer(options = {}) {
111
+ const hostname = options.hostname ?? "127.0.0.1";
112
+ const port = options.port ?? 31_415;
113
+ if (hostname !== "127.0.0.1") {
114
+ throw new Error("Pi Web only supports the loopback host 127.0.0.1");
115
+ }
116
+ const repository = options.context?.repository ?? (await SessionRepository.create(options.startupCwd ?? process.cwd()));
117
+ const runtimes = options.context?.runtimes ?? (await createRuntimeRegistry(repository));
118
+ const directories = options.context?.directories ?? new DirectoryBrowser();
119
+ const gitWorkspaces = options.context?.gitWorkspaces ?? new GitWorkspaceService();
120
+ const context = { repository, runtimes, directories, gitWorkspaces };
121
+ let closing;
122
+ const server = createServer(async (request, response) => {
123
+ applySecurityHeaders(response);
124
+ try {
125
+ validateLocalRequest(request, server);
126
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
127
+ if (await handleApi(request, response, url.pathname, context)) {
128
+ return;
129
+ }
130
+ if (request.method === "GET") {
131
+ const sourceId = newSessionSource(url.pathname);
132
+ if (sourceId) {
133
+ await repository.get(sourceId);
134
+ await serveAsset(homeDocument, response);
135
+ return;
136
+ }
137
+ if (/^\/sessions\/[^/]+\/?$/.test(url.pathname)) {
138
+ await serveAsset(sessionDocument, response);
139
+ return;
140
+ }
141
+ if (await serveStatic(url.pathname, response)) {
142
+ return;
143
+ }
144
+ }
145
+ throw new AppError("not_found", "Not found", 404);
146
+ }
147
+ catch (error) {
148
+ if (shouldReportError(error)) {
149
+ process.stderr.write(`[pi-web] ${request.method ?? "UNKNOWN"} ${request.url ?? "/"} failed\n${errorDetails(error)}\n`);
150
+ }
151
+ writeApiError(response, error);
152
+ }
153
+ });
154
+ try {
155
+ await new Promise((resolveListening, reject) => {
156
+ server.once("error", reject);
157
+ server.listen(port, hostname, () => {
158
+ server.off("error", reject);
159
+ resolveListening();
160
+ });
161
+ });
162
+ }
163
+ catch (error) {
164
+ if (!options.context) {
165
+ await runtimes.close();
166
+ }
167
+ throw error;
168
+ }
169
+ return {
170
+ hostname,
171
+ port: boundPort(server),
172
+ server,
173
+ repository,
174
+ runtimes,
175
+ directories,
176
+ gitWorkspaces,
177
+ close: () => {
178
+ if (closing) {
179
+ return closing;
180
+ }
181
+ closing = (async () => {
182
+ await runtimes.close();
183
+ await new Promise((resolveClose, reject) => {
184
+ server.close((error) => (error ? reject(error) : resolveClose()));
185
+ });
186
+ })();
187
+ return closing;
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,374 @@
1
+ import { open, realpath, stat, unlink } from "node:fs/promises";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { CURRENT_SESSION_VERSION, SessionManager, sessionEntryToContextMessages, } from "@earendil-works/pi-coding-agent";
4
+ import { AppError } from "./errors.js";
5
+ const MAX_SESSION_HEADER_BYTES = 1024 * 1024;
6
+ function isCurrentSessionHeader(value) {
7
+ if (!value || typeof value !== "object") {
8
+ return false;
9
+ }
10
+ const header = value;
11
+ return (header.type === "session" &&
12
+ header.version === CURRENT_SESSION_VERSION &&
13
+ typeof header.id === "string" &&
14
+ header.id.trim() !== "" &&
15
+ typeof header.timestamp === "string" &&
16
+ header.timestamp.trim() !== "" &&
17
+ !Number.isNaN(new Date(header.timestamp).getTime()) &&
18
+ typeof header.cwd === "string" &&
19
+ header.cwd.trim() !== "" &&
20
+ (header.parentSession === undefined || typeof header.parentSession === "string"));
21
+ }
22
+ async function readCurrentSessionHeader(path) {
23
+ let file;
24
+ try {
25
+ file = await open(path, "r");
26
+ const buffer = Buffer.allocUnsafe(MAX_SESSION_HEADER_BYTES + 1);
27
+ let bytesRead = 0;
28
+ let newline = -1;
29
+ while (bytesRead < buffer.length && newline === -1) {
30
+ const result = await file.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
31
+ if (result.bytesRead === 0) {
32
+ break;
33
+ }
34
+ newline = buffer.indexOf(0x0a, bytesRead);
35
+ bytesRead += result.bytesRead;
36
+ if (newline >= bytesRead) {
37
+ newline = -1;
38
+ }
39
+ }
40
+ const bounded = buffer.subarray(0, Math.min(bytesRead, MAX_SESSION_HEADER_BYTES));
41
+ if (newline === -1 && bytesRead > MAX_SESSION_HEADER_BYTES) {
42
+ return null;
43
+ }
44
+ const line = bounded.subarray(0, newline === -1 ? bounded.length : newline).toString("utf8");
45
+ const parsed = JSON.parse(line);
46
+ return isCurrentSessionHeader(parsed) ? parsed : null;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ finally {
52
+ if (file) {
53
+ await file.close().catch(() => undefined);
54
+ }
55
+ }
56
+ }
57
+ function toSummary(info, header) {
58
+ return {
59
+ id: header.id,
60
+ cwd: header.cwd,
61
+ ...(info.name ? { name: info.name } : {}),
62
+ created: header.timestamp,
63
+ modified: info.modified.toISOString(),
64
+ messageCount: info.messageCount,
65
+ firstMessage: info.firstMessage,
66
+ };
67
+ }
68
+ async function canonicalFile(path) {
69
+ try {
70
+ const file = await stat(path);
71
+ if (!file.isFile()) {
72
+ throw new AppError("stale_session", "The session path is no longer a file", 410);
73
+ }
74
+ return await realpath(path);
75
+ }
76
+ catch (error) {
77
+ if (error instanceof AppError) {
78
+ throw error;
79
+ }
80
+ throw new AppError("stale_session", "The session file no longer exists or is inaccessible", 410, {
81
+ cause: error,
82
+ });
83
+ }
84
+ }
85
+ export async function validateCwd(input) {
86
+ if (typeof input !== "string" || input.trim() === "") {
87
+ throw new AppError("invalid_cwd", "A working directory is required");
88
+ }
89
+ const candidate = input.trim();
90
+ if (!isAbsolute(candidate)) {
91
+ throw new AppError("invalid_cwd", "The working directory must be an absolute path");
92
+ }
93
+ try {
94
+ const canonical = await realpath(resolve(candidate));
95
+ const directory = await stat(canonical);
96
+ if (!directory.isDirectory()) {
97
+ throw new AppError("invalid_cwd", "The working directory is not a directory");
98
+ }
99
+ return canonical;
100
+ }
101
+ catch (error) {
102
+ if (error instanceof AppError) {
103
+ throw error;
104
+ }
105
+ throw new AppError("invalid_cwd", "The working directory does not exist or is inaccessible", 400, {
106
+ cause: error,
107
+ });
108
+ }
109
+ }
110
+ export class SessionRepository {
111
+ startupCwd;
112
+ sessionDir;
113
+ pathsById = new Map();
114
+ pendingManagers = new Map();
115
+ constructor(startupCwd, sessionDir) {
116
+ this.startupCwd = startupCwd;
117
+ this.sessionDir = sessionDir;
118
+ }
119
+ static async create(startupCwd, options = {}) {
120
+ const configuredSessionDir = options.sessionDir ?? process.env.PI_CODING_AGENT_SESSION_DIR;
121
+ const sessionDir = configuredSessionDir ? resolve(configuredSessionDir) : undefined;
122
+ return new SessionRepository(await validateCwd(startupCwd), sessionDir);
123
+ }
124
+ async list() {
125
+ const infos = this.sessionDir ? await SessionManager.listAll(this.sessionDir) : await SessionManager.listAll();
126
+ const nextPaths = new Map();
127
+ const groups = new Map();
128
+ for (const info of infos) {
129
+ const header = await readCurrentSessionHeader(info.path);
130
+ if (!header || header.id !== info.id || nextPaths.has(header.id)) {
131
+ continue;
132
+ }
133
+ nextPaths.set(header.id, info.path);
134
+ const sessions = groups.get(header.cwd) ?? [];
135
+ sessions.push(toSummary(info, header));
136
+ groups.set(header.cwd, sessions);
137
+ }
138
+ for (const [id, pending] of [...this.pendingManagers]) {
139
+ if (nextPaths.has(id)) {
140
+ continue;
141
+ }
142
+ let session;
143
+ try {
144
+ session = await this.get(id);
145
+ }
146
+ catch (error) {
147
+ if (!(error instanceof AppError) || error.code !== "invalid_session") {
148
+ throw error;
149
+ }
150
+ this.pendingManagers.delete(id);
151
+ this.pathsById.delete(id);
152
+ continue;
153
+ }
154
+ const sessions = groups.get(session.cwd) ?? [];
155
+ sessions.unshift({
156
+ id: session.id,
157
+ cwd: session.cwd,
158
+ ...(session.name ? { name: session.name } : {}),
159
+ created: session.created,
160
+ modified: session.modified,
161
+ messageCount: session.messageCount,
162
+ firstMessage: session.firstMessage,
163
+ });
164
+ groups.set(session.cwd, sessions);
165
+ const file = pending.getSessionFile();
166
+ if (file) {
167
+ nextPaths.set(id, file);
168
+ }
169
+ }
170
+ this.pathsById = nextPaths;
171
+ const knownCwds = [this.startupCwd];
172
+ for (const sessions of groups.values()) {
173
+ const cwd = sessions[0]?.cwd;
174
+ if (cwd && !knownCwds.includes(cwd)) {
175
+ knownCwds.push(cwd);
176
+ }
177
+ }
178
+ return {
179
+ groups: [...groups].map(([cwd, sessions]) => ({ cwd, sessions })),
180
+ knownCwds,
181
+ };
182
+ }
183
+ async resolvePath(id) {
184
+ if (!id) {
185
+ throw new AppError("unknown_session", "A session id is required", 404);
186
+ }
187
+ let path = this.pathsById.get(id);
188
+ if (!path) {
189
+ await this.list();
190
+ path = this.pathsById.get(id);
191
+ }
192
+ if (!path) {
193
+ throw new AppError("unknown_session", `Unknown session: ${id}`, 404);
194
+ }
195
+ return canonicalFile(path);
196
+ }
197
+ async delete(id) {
198
+ if (!id) {
199
+ throw new AppError("unknown_session", "A session id is required", 404);
200
+ }
201
+ const pending = this.pendingManagers.get(id);
202
+ let path;
203
+ if (pending) {
204
+ const pendingFile = pending.getSessionFile();
205
+ if (!pendingFile) {
206
+ throw new AppError("invalid_session", "The session is not persisted", 422);
207
+ }
208
+ try {
209
+ await stat(pendingFile);
210
+ }
211
+ catch (error) {
212
+ if (error.code !== "ENOENT") {
213
+ throw new AppError("session_delete_failed", "Could not inspect the session file", 500, {
214
+ cause: error,
215
+ });
216
+ }
217
+ this.pendingManagers.delete(id);
218
+ this.pathsById.delete(id);
219
+ return;
220
+ }
221
+ path = await canonicalFile(pendingFile);
222
+ }
223
+ else {
224
+ path = await this.resolvePath(id);
225
+ }
226
+ const header = await readCurrentSessionHeader(path);
227
+ if (!header || header.id !== id) {
228
+ throw new AppError("invalid_session", "The session file is not in the current format", 422);
229
+ }
230
+ try {
231
+ await unlink(path);
232
+ }
233
+ catch (error) {
234
+ if (error.code === "ENOENT") {
235
+ throw new AppError("stale_session", "The session file no longer exists", 410, { cause: error });
236
+ }
237
+ throw new AppError("session_delete_failed", "Could not delete the session file", 500, {
238
+ cause: error,
239
+ });
240
+ }
241
+ this.pendingManagers.delete(id);
242
+ this.pathsById.delete(id);
243
+ }
244
+ adoptManager(manager) {
245
+ const id = manager.getSessionId();
246
+ this.pendingManagers.set(id, manager);
247
+ const file = manager.getSessionFile();
248
+ if (file) {
249
+ this.pathsById.set(id, file);
250
+ }
251
+ }
252
+ async openManager(id) {
253
+ const pending = this.pendingManagers.get(id);
254
+ if (pending) {
255
+ const pendingFile = pending.getSessionFile();
256
+ if (!pendingFile) {
257
+ return pending;
258
+ }
259
+ try {
260
+ await stat(pendingFile);
261
+ }
262
+ catch {
263
+ return pending;
264
+ }
265
+ const header = await readCurrentSessionHeader(pendingFile);
266
+ if (!header || header.id !== id) {
267
+ throw new AppError("invalid_session", "The session file is not in the current format", 422);
268
+ }
269
+ this.pendingManagers.delete(id);
270
+ this.pathsById.set(id, pendingFile);
271
+ }
272
+ const path = await this.resolvePath(id);
273
+ const header = await readCurrentSessionHeader(path);
274
+ if (!header || header.id !== id) {
275
+ throw new AppError("invalid_session", "The session file is not in the current format", 422);
276
+ }
277
+ try {
278
+ const manager = SessionManager.open(path);
279
+ if (manager.getSessionId() !== id) {
280
+ throw new AppError("stale_session", "The session file id no longer matches", 410);
281
+ }
282
+ await validateCwd(manager.getCwd());
283
+ return manager;
284
+ }
285
+ catch (error) {
286
+ if (error instanceof AppError) {
287
+ throw error;
288
+ }
289
+ throw new AppError("invalid_session", "The session file could not be opened", 422, { cause: error });
290
+ }
291
+ }
292
+ async get(id) {
293
+ const manager = await this.openManager(id);
294
+ const context = manager.buildSessionContext();
295
+ const branch = manager.getBranch();
296
+ const transcriptEntries = [];
297
+ for (const entry of branch) {
298
+ if (entry.type === "message") {
299
+ const persisted = { entryId: entry.id, message: entry.message };
300
+ transcriptEntries.push({ kind: "message", ...persisted });
301
+ continue;
302
+ }
303
+ if (entry.type === "custom_message") {
304
+ for (const message of sessionEntryToContextMessages(entry)) {
305
+ const persisted = { entryId: entry.id, message };
306
+ transcriptEntries.push({ kind: "message", ...persisted });
307
+ }
308
+ continue;
309
+ }
310
+ if (entry.type === "custom") {
311
+ const persisted = { entryId: entry.id, customType: entry.customType, data: entry.data };
312
+ transcriptEntries.push({ kind: "custom", ...persisted });
313
+ }
314
+ }
315
+ const file = manager.getSessionFile();
316
+ if (!file) {
317
+ throw new AppError("invalid_session", "The session is not persisted", 422);
318
+ }
319
+ const details = await stat(file).catch(() => undefined);
320
+ const header = manager.getHeader();
321
+ if (!isCurrentSessionHeader(header)) {
322
+ throw new AppError("invalid_session", "The session file is not in the current format", 422);
323
+ }
324
+ const name = manager.getSessionName();
325
+ const created = header.timestamp;
326
+ return {
327
+ id: manager.getSessionId(),
328
+ cwd: manager.getCwd(),
329
+ ...(name ? { name } : {}),
330
+ created,
331
+ modified: details?.mtime.toISOString() ?? created,
332
+ messageCount: transcriptEntries.reduce((count, entry) => count + (entry.kind === "message" ? 1 : 0), 0),
333
+ firstMessage: this.firstUserText(transcriptEntries),
334
+ transcriptEntries,
335
+ model: context.model,
336
+ thinkingLevel: context.thinkingLevel,
337
+ };
338
+ }
339
+ async createSession(cwdInput) {
340
+ const cwd = await validateCwd(cwdInput);
341
+ let manager;
342
+ try {
343
+ manager = SessionManager.create(cwd, this.sessionDir);
344
+ }
345
+ catch (error) {
346
+ throw new AppError("session_create_failed", "Could not create the session file", 500, { cause: error });
347
+ }
348
+ const file = manager.getSessionFile();
349
+ if (!file) {
350
+ throw new AppError("session_create_failed", "Pi did not create a persistent session", 500);
351
+ }
352
+ this.adoptManager(manager);
353
+ return {
354
+ manager,
355
+ session: await this.get(manager.getSessionId()),
356
+ };
357
+ }
358
+ firstUserText(entries) {
359
+ for (const entry of entries) {
360
+ if (entry.kind !== "message" || entry.message.role !== "user") {
361
+ continue;
362
+ }
363
+ const { message } = entry;
364
+ if (typeof message.content === "string") {
365
+ return message.content;
366
+ }
367
+ return message.content
368
+ .filter((part) => part.type === "text")
369
+ .map((part) => part.text)
370
+ .join("");
371
+ }
372
+ return "";
373
+ }
374
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@larose/pi-web",
3
+ "version": "0.3.0",
4
+ "description": "Pi, in your browser",
5
+ "author": {
6
+ "name": "Mathieu Larose",
7
+ "email": "mathieu@mathieularose.com",
8
+ "url": "https://mathieularose.com"
9
+ },
10
+ "license": "AGPL-3.0-or-later",
11
+ "type": "module",
12
+ "keywords": [
13
+ "pi-package"
14
+ ],
15
+ "pi": {
16
+ "image": "https://unpkg.com/@larose/pi-web/screenshots/session.png"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "files": [
22
+ "dist/",
23
+ "public/",
24
+ "screenshots/",
25
+ "THIRD_PARTY_LICENSES.md",
26
+ "src/**/*.ts"
27
+ ],
28
+ "bin": {
29
+ "pi-web": "dist/server/cli.js"
30
+ },
31
+ "dependencies": {
32
+ "@earendil-works/pi-coding-agent": "^0.85.0",
33
+ "@earendil-works/pi-server": "^0.85.0"
34
+ },
35
+ "devDependencies": {
36
+ "@biomejs/biome": "2.5.12",
37
+ "@earendil-works/pi-agent-core": "0.85.0",
38
+ "@types/node": "22.20.1",
39
+ "esbuild": "0.28.1",
40
+ "marked": "^18.0.12",
41
+ "typescript": "5.9.3"
42
+ },
43
+ "engines": {
44
+ "node": ">=22.19.0"
45
+ }
46
+ }