@jyork0828/pi-pilot 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.
package/dist/index.js ADDED
@@ -0,0 +1,2395 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { existsSync } from "fs";
5
+ import { readFile as readFile4 } from "fs/promises";
6
+ import { dirname as dirname5, extname, join as join8, resolve as resolve5, sep as sep3 } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { serve } from "@hono/node-server";
9
+ import { Hono as Hono4 } from "hono";
10
+ import { cors } from "hono/cors";
11
+
12
+ // src/config.ts
13
+ import { homedir } from "os";
14
+ import { join } from "path";
15
+ var config = {
16
+ /** HTTP + WS port. */
17
+ port: Number(process.env.PI_PILOT_PORT ?? 5174),
18
+ /** Bind address. Hard-coded localhost; do NOT make this configurable
19
+ * without first reviewing the security implications of exposing bash. */
20
+ host: "127.0.0.1",
21
+ /** Where pi-pilot stores its own (non-pi) state. */
22
+ dataDir: process.env.PI_PILOT_DATA_DIR ?? join(homedir(), ".pi", "webui"),
23
+ /** Dev origin allowed by CORS for /api. The Vite dev server. */
24
+ corsOrigin: process.env.PI_PILOT_CORS_ORIGIN ?? "http://localhost:5173"
25
+ };
26
+
27
+ // src/api/workspaces.ts
28
+ import { stat as stat2 } from "fs/promises";
29
+ import { basename as basename2, isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
30
+ import { Hono } from "hono";
31
+
32
+ // src/storage/resource-writer.ts
33
+ import {
34
+ mkdir,
35
+ readFile,
36
+ rm,
37
+ stat,
38
+ unlink,
39
+ writeFile
40
+ } from "fs/promises";
41
+ import { dirname, isAbsolute, join as join2, resolve, sep } from "path";
42
+ var SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
43
+ var PROMPT_NAME_RE = /^[a-z0-9_](?:[a-z0-9_-]{0,62}[a-z0-9_])?$/;
44
+ function ensureSkillName(name) {
45
+ if (!SKILL_NAME_RE.test(name)) {
46
+ throw new HttpError(400, "skill name must be lowercase a-z/0-9/hyphens, 1-64 chars, no leading/trailing/double hyphens");
47
+ }
48
+ }
49
+ function ensurePromptName(name) {
50
+ if (!PROMPT_NAME_RE.test(name)) {
51
+ throw new HttpError(400, "prompt name must be lowercase a-z/0-9/hyphens/underscores, 1-64 chars");
52
+ }
53
+ }
54
+ function assertUnder(target, roots) {
55
+ const t = resolve(target);
56
+ const ok = roots.some((root) => {
57
+ const r = resolve(root);
58
+ return t === r || t.startsWith(r + sep);
59
+ });
60
+ if (!ok) {
61
+ throw new HttpError(500, `refusing to touch path outside managed roots: ${target}`);
62
+ }
63
+ }
64
+ function skillDirFor(scope, name, roots) {
65
+ ensureSkillName(name);
66
+ const base = scope === "user" ? roots.userSkills : roots.projectSkills;
67
+ return join2(base, name);
68
+ }
69
+ function promptFileFor(scope, name, roots) {
70
+ ensurePromptName(name);
71
+ const base = scope === "user" ? roots.userPrompts : roots.projectPrompts;
72
+ return join2(base, `${name}.md`);
73
+ }
74
+ function resolveResourceRoots(opts) {
75
+ return {
76
+ userSkills: join2(opts.agentDir, "skills"),
77
+ projectSkills: join2(opts.workspaceCwd, ".pi", "skills"),
78
+ userPrompts: join2(opts.agentDir, "prompts"),
79
+ projectPrompts: join2(opts.workspaceCwd, ".pi", "prompts")
80
+ };
81
+ }
82
+ function scopeFor(absPath, roots) {
83
+ const p = resolve(absPath);
84
+ for (const [root, scope] of [
85
+ [roots.userSkills, "user"],
86
+ [roots.projectSkills, "project"],
87
+ [roots.userPrompts, "user"],
88
+ [roots.projectPrompts, "project"]
89
+ ]) {
90
+ const r = resolve(root);
91
+ if (p === r || p.startsWith(r + sep)) return scope;
92
+ }
93
+ return void 0;
94
+ }
95
+ function parseFile(content) {
96
+ const rawLines = content.split(/\r?\n/);
97
+ if (rawLines[0] !== "---") {
98
+ return { frontmatter: {}, lines: [], body: content };
99
+ }
100
+ const fm = {};
101
+ const lines = [];
102
+ let i = 1;
103
+ while (i < rawLines.length && rawLines[i] !== "---") {
104
+ const line = rawLines[i] ?? "";
105
+ const m = line.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*?)\s*$/);
106
+ if (m) {
107
+ const key = m[1];
108
+ const rawVal = m[2] ?? "";
109
+ const value = parseScalar(rawVal);
110
+ fm[key] = value;
111
+ lines.push({ key, value, raw: line });
112
+ }
113
+ i++;
114
+ }
115
+ if (i >= rawLines.length) {
116
+ return { frontmatter: {}, lines: [], body: content };
117
+ }
118
+ const body = rawLines.slice(i + 1).join("\n");
119
+ return { frontmatter: fm, lines, body };
120
+ }
121
+ function extraLines(lines, knownKeys) {
122
+ const known = new Set(knownKeys);
123
+ return lines.filter((l) => !known.has(l.key)).map((l) => l.raw);
124
+ }
125
+ function parseScalar(raw) {
126
+ if (raw === "true") return true;
127
+ if (raw === "false") return false;
128
+ if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
129
+ return raw.slice(1, -1).replace(/\\(["\\])/g, "$1");
130
+ }
131
+ if (raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2) {
132
+ return raw.slice(1, -1).replace(/''/g, "'");
133
+ }
134
+ return raw;
135
+ }
136
+ function buildFile(fields, body, extras = []) {
137
+ const out = ["---"];
138
+ for (const [key, value] of fields) {
139
+ if (value === void 0 || value === "") continue;
140
+ if (typeof value === "boolean") {
141
+ out.push(`${key}: ${value ? "true" : "false"}`);
142
+ } else {
143
+ out.push(`${key}: ${formatString(value)}`);
144
+ }
145
+ }
146
+ for (const raw of extras) out.push(raw);
147
+ out.push("---");
148
+ const trimmedBody = body.replace(/\s+$/, "");
149
+ const text = `${out.join("\n")}
150
+ ${trimmedBody}
151
+ `;
152
+ return text;
153
+ }
154
+ var SKILL_KNOWN_KEYS = ["name", "description", "disable-model-invocation"];
155
+ var PROMPT_KNOWN_KEYS = ["description", "argument-hint"];
156
+ function formatString(value) {
157
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
158
+ return `"${escaped}"`;
159
+ }
160
+ async function createSkill(opts) {
161
+ const dir = skillDirFor(opts.scope, opts.name, opts.roots);
162
+ assertUnder(dir, [opts.roots.userSkills, opts.roots.projectSkills]);
163
+ if (await exists(dir)) {
164
+ throw new HttpError(409, `skill already exists at ${dir}`);
165
+ }
166
+ const file = join2(dir, "SKILL.md");
167
+ const text = buildFile(
168
+ [
169
+ ["name", opts.name],
170
+ ["description", opts.description],
171
+ ["disable-model-invocation", opts.disableModelInvocation]
172
+ ],
173
+ opts.body
174
+ );
175
+ await mkdir(dir, { recursive: true });
176
+ await writeFile(file, text, "utf8");
177
+ return file;
178
+ }
179
+ async function updateSkill(opts) {
180
+ if (!isAbsolute(opts.filePath)) {
181
+ throw new HttpError(400, "filePath must be absolute");
182
+ }
183
+ assertUnder(opts.filePath, [opts.roots.userSkills, opts.roots.projectSkills]);
184
+ if (!await exists(opts.filePath)) {
185
+ throw new HttpError(404, `skill file not found: ${opts.filePath}`);
186
+ }
187
+ const existing = await readFile(opts.filePath, "utf8");
188
+ const extras = extraLines(parseFile(existing).lines, SKILL_KNOWN_KEYS);
189
+ const text = buildFile(
190
+ [
191
+ ["name", opts.name],
192
+ ["description", opts.description],
193
+ ["disable-model-invocation", opts.disableModelInvocation]
194
+ ],
195
+ opts.body,
196
+ extras
197
+ );
198
+ await writeFile(opts.filePath, text, "utf8");
199
+ return opts.filePath;
200
+ }
201
+ async function deleteSkill(filePath, roots) {
202
+ if (!isAbsolute(filePath)) {
203
+ throw new HttpError(400, "filePath must be absolute");
204
+ }
205
+ assertUnder(filePath, [roots.userSkills, roots.projectSkills]);
206
+ if (!await exists(filePath)) {
207
+ throw new HttpError(404, `skill file not found: ${filePath}`);
208
+ }
209
+ const dir = dirname(filePath);
210
+ const parentIsRoot = resolve(dir) === resolve(roots.userSkills) || resolve(dir) === resolve(roots.projectSkills);
211
+ if (parentIsRoot) {
212
+ await unlink(filePath);
213
+ return;
214
+ }
215
+ assertUnder(dir, [roots.userSkills, roots.projectSkills]);
216
+ await rm(dir, { recursive: true, force: true });
217
+ }
218
+ async function createPrompt(opts) {
219
+ const file = promptFileFor(opts.scope, opts.name, opts.roots);
220
+ assertUnder(file, [opts.roots.userPrompts, opts.roots.projectPrompts]);
221
+ if (await exists(file)) {
222
+ throw new HttpError(409, `prompt already exists at ${file}`);
223
+ }
224
+ const text = buildFile(
225
+ [
226
+ ["description", opts.description],
227
+ ["argument-hint", opts.argumentHint]
228
+ ],
229
+ opts.body
230
+ );
231
+ await mkdir(dirname(file), { recursive: true });
232
+ await writeFile(file, text, "utf8");
233
+ return file;
234
+ }
235
+ async function updatePrompt(opts) {
236
+ if (!isAbsolute(opts.filePath)) {
237
+ throw new HttpError(400, "filePath must be absolute");
238
+ }
239
+ assertUnder(opts.filePath, [opts.roots.userPrompts, opts.roots.projectPrompts]);
240
+ if (!await exists(opts.filePath)) {
241
+ throw new HttpError(404, `prompt file not found: ${opts.filePath}`);
242
+ }
243
+ ensurePromptName(opts.name);
244
+ const dir = dirname(opts.filePath);
245
+ const newPath = join2(dir, `${opts.name}.md`);
246
+ assertUnder(newPath, [opts.roots.userPrompts, opts.roots.projectPrompts]);
247
+ const existing = await readFile(opts.filePath, "utf8");
248
+ const extras = extraLines(parseFile(existing).lines, PROMPT_KNOWN_KEYS);
249
+ const text = buildFile(
250
+ [
251
+ ["description", opts.description],
252
+ ["argument-hint", opts.argumentHint]
253
+ ],
254
+ opts.body,
255
+ extras
256
+ );
257
+ if (resolve(newPath) !== resolve(opts.filePath)) {
258
+ if (await exists(newPath)) {
259
+ throw new HttpError(409, `prompt already exists at ${newPath}`);
260
+ }
261
+ await writeFile(newPath, text, "utf8");
262
+ try {
263
+ await unlink(opts.filePath);
264
+ } catch (err) {
265
+ await unlink(newPath).catch(() => void 0);
266
+ throw err;
267
+ }
268
+ return newPath;
269
+ }
270
+ await writeFile(opts.filePath, text, "utf8");
271
+ return opts.filePath;
272
+ }
273
+ async function deletePrompt(filePath, roots) {
274
+ if (!isAbsolute(filePath)) {
275
+ throw new HttpError(400, "filePath must be absolute");
276
+ }
277
+ assertUnder(filePath, [roots.userPrompts, roots.projectPrompts]);
278
+ if (!await exists(filePath)) {
279
+ throw new HttpError(404, `prompt file not found: ${filePath}`);
280
+ }
281
+ await unlink(filePath);
282
+ }
283
+ async function readSkillFile(filePath, roots) {
284
+ assertUnder(filePath, [roots.userSkills, roots.projectSkills]);
285
+ const text = await readFile(filePath, "utf8");
286
+ const { frontmatter, body } = parseFile(text);
287
+ return {
288
+ body,
289
+ name: stringOr(frontmatter.name, ""),
290
+ description: stringOr(frontmatter.description, ""),
291
+ disableModelInvocation: booleanOr(frontmatter["disable-model-invocation"], void 0)
292
+ };
293
+ }
294
+ async function readPromptFile(filePath, roots) {
295
+ assertUnder(filePath, [roots.userPrompts, roots.projectPrompts]);
296
+ const text = await readFile(filePath, "utf8");
297
+ const { frontmatter, body } = parseFile(text);
298
+ const stem = basename(filePath).replace(/\.md$/, "");
299
+ return {
300
+ body,
301
+ name: stem,
302
+ description: stringOr(frontmatter.description, ""),
303
+ argumentHint: stringOr(frontmatter["argument-hint"], void 0)
304
+ };
305
+ }
306
+ function basename(p) {
307
+ const parts = p.split(sep);
308
+ return parts.at(-1) || p;
309
+ }
310
+ function stringOr(value, fallback) {
311
+ return typeof value === "string" ? value : fallback;
312
+ }
313
+ function booleanOr(value, fallback) {
314
+ return typeof value === "boolean" ? value : fallback;
315
+ }
316
+ async function exists(p) {
317
+ try {
318
+ await stat(p);
319
+ return true;
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+ var HttpError = class extends Error {
325
+ constructor(status, message) {
326
+ super(message);
327
+ this.status = status;
328
+ }
329
+ status;
330
+ };
331
+
332
+ // src/storage/workspace-registry.ts
333
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
334
+ import { dirname as dirname2, join as join3 } from "path";
335
+ import { randomUUID } from "crypto";
336
+ var REGISTRY_PATH = join3(config.dataDir, "workspaces.json");
337
+ var cache;
338
+ async function load() {
339
+ if (cache) return cache;
340
+ try {
341
+ const raw = await readFile2(REGISTRY_PATH, "utf8");
342
+ cache = JSON.parse(raw);
343
+ if (!Array.isArray(cache.workspaces)) cache = { workspaces: [] };
344
+ } catch (err) {
345
+ if (err.code === "ENOENT") {
346
+ cache = { workspaces: [] };
347
+ } else {
348
+ throw err;
349
+ }
350
+ }
351
+ return cache;
352
+ }
353
+ async function save() {
354
+ if (!cache) return;
355
+ await mkdir2(dirname2(REGISTRY_PATH), { recursive: true });
356
+ await writeFile2(REGISTRY_PATH, JSON.stringify(cache, null, 2), "utf8");
357
+ }
358
+ async function listWorkspaces() {
359
+ const r = await load();
360
+ return [...r.workspaces];
361
+ }
362
+ async function getWorkspace(id) {
363
+ const r = await load();
364
+ return r.workspaces.find((w) => w.id === id);
365
+ }
366
+ async function addWorkspace(input) {
367
+ const r = await load();
368
+ const existing = r.workspaces.find((w) => w.path === input.path);
369
+ if (existing) return existing;
370
+ const ws = {
371
+ id: randomUUID(),
372
+ name: input.name,
373
+ path: input.path,
374
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
375
+ };
376
+ r.workspaces.push(ws);
377
+ await save();
378
+ return ws;
379
+ }
380
+ async function removeWorkspace(id) {
381
+ const r = await load();
382
+ const before = r.workspaces.length;
383
+ r.workspaces = r.workspaces.filter((w) => w.id !== id);
384
+ if (r.workspaces.length === before) return false;
385
+ await save();
386
+ return true;
387
+ }
388
+
389
+ // src/storage/workspace-stats.ts
390
+ import { execFile } from "child_process";
391
+ import { promisify } from "util";
392
+ var exec = promisify(execFile);
393
+ var CACHE_TTL_MS = 3e4;
394
+ var cache2 = /* @__PURE__ */ new Map();
395
+ var inflight = /* @__PURE__ */ new Map();
396
+ async function enrichWorkspace(ws) {
397
+ const stats = await getStats(ws.path);
398
+ return {
399
+ id: ws.id,
400
+ name: ws.name,
401
+ path: ws.path,
402
+ addedAt: ws.addedAt,
403
+ gitBranch: stats.gitBranch,
404
+ fileCount: stats.fileCount
405
+ };
406
+ }
407
+ async function getStats(path) {
408
+ const now = Date.now();
409
+ const cached = cache2.get(path);
410
+ if (cached && cached.expiresAt > now) return cached;
411
+ const pending = inflight.get(path);
412
+ if (pending) return pending;
413
+ const probe = probeStats(path).then((stats) => {
414
+ const entry = {
415
+ ...stats,
416
+ expiresAt: Date.now() + CACHE_TTL_MS
417
+ };
418
+ cache2.set(path, entry);
419
+ return entry;
420
+ }).finally(() => {
421
+ inflight.delete(path);
422
+ });
423
+ inflight.set(path, probe);
424
+ return probe;
425
+ }
426
+ async function probeStats(path) {
427
+ const [branchResult, filesResult] = await Promise.allSettled([
428
+ runGit(path, ["rev-parse", "--abbrev-ref", "HEAD"]),
429
+ runGit(path, [
430
+ "ls-files",
431
+ "--cached",
432
+ "--others",
433
+ "--exclude-standard"
434
+ ])
435
+ ]);
436
+ let gitBranch = null;
437
+ if (branchResult.status === "fulfilled") {
438
+ const out = branchResult.value.trim();
439
+ if (out && out !== "HEAD") {
440
+ gitBranch = out;
441
+ } else if (out === "HEAD") {
442
+ try {
443
+ const sha = (await runGit(path, ["rev-parse", "--short", "HEAD"])).trim();
444
+ gitBranch = sha ? `@${sha}` : null;
445
+ } catch {
446
+ gitBranch = null;
447
+ }
448
+ }
449
+ }
450
+ let fileCount = null;
451
+ if (filesResult.status === "fulfilled") {
452
+ const out = filesResult.value;
453
+ if (out) {
454
+ let n = 0;
455
+ for (let i = 0; i < out.length; i++) {
456
+ if (out.charCodeAt(i) === 10) n++;
457
+ }
458
+ if (out.length > 0 && out.charCodeAt(out.length - 1) !== 10) n++;
459
+ fileCount = n;
460
+ } else {
461
+ fileCount = 0;
462
+ }
463
+ }
464
+ return { gitBranch, fileCount };
465
+ }
466
+ async function runGit(cwd, args) {
467
+ const { stdout } = await exec("git", args, {
468
+ cwd,
469
+ timeout: 2e3,
470
+ maxBuffer: 5 * 1024 * 1024,
471
+ // Don't inherit GIT_DIR / GIT_WORK_TREE from the server process —
472
+ // could leak the server's own repo into a workspace probe.
473
+ env: { ...process.env, GIT_DIR: void 0, GIT_WORK_TREE: void 0 }
474
+ });
475
+ return stdout;
476
+ }
477
+
478
+ // src/workspace-manager.ts
479
+ import { unlink as unlink2 } from "fs/promises";
480
+ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
481
+ import {
482
+ createAgentSessionFromServices,
483
+ createAgentSessionRuntime,
484
+ createAgentSessionServices,
485
+ getAgentDir,
486
+ SessionManager
487
+ } from "@earendil-works/pi-coding-agent";
488
+
489
+ // src/ws/extension-ui.ts
490
+ var ExtensionUIBridge = class {
491
+ /** Symmetric with the old bridge so workspace-manager's dispose path
492
+ * can call it uniformly. There is no state to release. */
493
+ dispose() {
494
+ }
495
+ // ============== dialog methods (resolve to default) ==============
496
+ select() {
497
+ return Promise.resolve(void 0);
498
+ }
499
+ confirm() {
500
+ return Promise.resolve(false);
501
+ }
502
+ input() {
503
+ return Promise.resolve(void 0);
504
+ }
505
+ editor() {
506
+ return Promise.resolve(void 0);
507
+ }
508
+ // ============== fire-and-forget methods (no-op) ==============
509
+ notify() {
510
+ }
511
+ setStatus() {
512
+ }
513
+ setWidget() {
514
+ }
515
+ setTitle() {
516
+ }
517
+ setEditorText() {
518
+ }
519
+ pasteToEditor() {
520
+ }
521
+ // ============== TUI-only methods (no-op / defaults) ==============
522
+ onTerminalInput() {
523
+ return () => {
524
+ };
525
+ }
526
+ setWorkingMessage() {
527
+ }
528
+ setWorkingVisible() {
529
+ }
530
+ setWorkingIndicator() {
531
+ }
532
+ setHiddenThinkingLabel() {
533
+ }
534
+ setFooter() {
535
+ }
536
+ setHeader() {
537
+ }
538
+ async custom() {
539
+ return void 0;
540
+ }
541
+ getEditorText() {
542
+ return "";
543
+ }
544
+ addAutocompleteProvider() {
545
+ }
546
+ setEditorComponent() {
547
+ }
548
+ getEditorComponent() {
549
+ return void 0;
550
+ }
551
+ get theme() {
552
+ return void 0;
553
+ }
554
+ getAllThemes() {
555
+ return [];
556
+ }
557
+ getTheme() {
558
+ return void 0;
559
+ }
560
+ setTheme() {
561
+ return { success: false, error: "Theme switching not supported in pi-pilot" };
562
+ }
563
+ getToolsExpanded() {
564
+ return false;
565
+ }
566
+ setToolsExpanded() {
567
+ }
568
+ };
569
+
570
+ // src/workspace-manager.ts
571
+ var EXTENSIONS_ENABLED = process.env.PI_PILOT_ENABLE_EXTENSIONS === "1";
572
+ var createRuntime = async ({
573
+ cwd,
574
+ sessionManager,
575
+ sessionStartEvent
576
+ }) => {
577
+ const services = await createAgentSessionServices({
578
+ cwd,
579
+ resourceLoaderOptions: EXTENSIONS_ENABLED ? void 0 : { noExtensions: true }
580
+ });
581
+ const sessionResult = await createAgentSessionFromServices({
582
+ services,
583
+ sessionManager,
584
+ sessionStartEvent
585
+ });
586
+ return {
587
+ ...sessionResult,
588
+ services,
589
+ diagnostics: services.diagnostics
590
+ };
591
+ };
592
+ var WorkspaceManager = class {
593
+ states = /* @__PURE__ */ new Map();
594
+ /**
595
+ * Subscribers live independently of `states` so the hub can register a
596
+ * WebSocket *before* `getOrCreate` triggers a runtime build (which may
597
+ * fire `session_start` synchronously, and any UI request from a
598
+ * session_start handler would otherwise broadcast to an empty set).
599
+ */
600
+ subscribers = /* @__PURE__ */ new Map();
601
+ /** Per-workspace lock to serialize concurrent creations. */
602
+ pending = /* @__PURE__ */ new Map();
603
+ rebindListeners = /* @__PURE__ */ new Map();
604
+ getOrCreateSubscriberSet(workspaceId) {
605
+ let set = this.subscribers.get(workspaceId);
606
+ if (!set) {
607
+ set = /* @__PURE__ */ new Set();
608
+ this.subscribers.set(workspaceId, set);
609
+ }
610
+ return set;
611
+ }
612
+ async getOrCreate(workspaceId) {
613
+ const existing = this.states.get(workspaceId);
614
+ if (existing) return existing.runtime;
615
+ const inflight3 = this.pending.get(workspaceId);
616
+ if (inflight3) return (await inflight3).runtime;
617
+ const p = this.build(workspaceId);
618
+ this.pending.set(workspaceId, p);
619
+ try {
620
+ const state = await p;
621
+ this.states.set(workspaceId, state);
622
+ return state.runtime;
623
+ } finally {
624
+ this.pending.delete(workspaceId);
625
+ }
626
+ }
627
+ async build(workspaceId) {
628
+ const ws = await getWorkspace(workspaceId);
629
+ if (!ws) throw new Error(`Workspace not found: ${workspaceId}`);
630
+ const sessionManager = SessionManager.continueRecent(ws.path);
631
+ const runtime = await createAgentSessionRuntime(createRuntime, {
632
+ cwd: ws.path,
633
+ agentDir: getAgentDir(),
634
+ sessionManager
635
+ });
636
+ const subscribers = this.getOrCreateSubscriberSet(workspaceId);
637
+ const bridge = new ExtensionUIBridge();
638
+ const onError = (err) => {
639
+ const msg = {
640
+ type: "extension_error",
641
+ workspaceId,
642
+ extensionPath: err.extensionPath,
643
+ event: err.event,
644
+ message: err.error
645
+ };
646
+ broadcastTo(subscribers, msg);
647
+ console.error(
648
+ `[ext-error] ${workspaceId} ${err.extensionPath}@${err.event}: ${err.error}` + (err.stack ? `
649
+ ${err.stack}` : "")
650
+ );
651
+ };
652
+ await runtime.session.bindExtensions({ uiContext: bridge, onError });
653
+ runtime.setRebindSession(async () => {
654
+ await runtime.session.bindExtensions({ uiContext: bridge, onError });
655
+ this.notifySessionReplaced(workspaceId);
656
+ });
657
+ return { runtime, bridge };
658
+ }
659
+ get(workspaceId) {
660
+ return this.states.get(workspaceId)?.runtime;
661
+ }
662
+ /**
663
+ * Register a WS connection as a subscriber for `workspaceId`. Safe to
664
+ * call before `getOrCreate`; the set is lazily created so the bridge,
665
+ * when later built, sees the same Set instance and any pre-existing
666
+ * subscribers.
667
+ */
668
+ addSubscriber(workspaceId, ws) {
669
+ this.getOrCreateSubscriberSet(workspaceId).add(ws);
670
+ }
671
+ removeSubscriber(workspaceId, ws) {
672
+ const set = this.subscribers.get(workspaceId);
673
+ if (!set) return;
674
+ set.delete(ws);
675
+ if (set.size === 0) this.subscribers.delete(workspaceId);
676
+ }
677
+ /**
678
+ * Fan a server-initiated message out to every WS subscribed to the
679
+ * workspace. Used by API handlers that mutate runtime state and need
680
+ * to refresh derived snapshots (e.g. `context_usage` after `setModel`,
681
+ * which pi's event stream doesn't surface unless thinking-level also
682
+ * clamps).
683
+ */
684
+ broadcast(workspaceId, msg) {
685
+ const set = this.subscribers.get(workspaceId);
686
+ if (!set || set.size === 0) return;
687
+ broadcastTo(set, msg);
688
+ }
689
+ onSessionReplaced(workspaceId, listener) {
690
+ let listeners = this.rebindListeners.get(workspaceId);
691
+ if (!listeners) {
692
+ listeners = /* @__PURE__ */ new Set();
693
+ this.rebindListeners.set(workspaceId, listeners);
694
+ }
695
+ listeners.add(listener);
696
+ return () => {
697
+ const current = this.rebindListeners.get(workspaceId);
698
+ if (!current) return;
699
+ current.delete(listener);
700
+ if (current.size === 0) {
701
+ this.rebindListeners.delete(workspaceId);
702
+ }
703
+ };
704
+ }
705
+ notifySessionReplaced(workspaceId) {
706
+ const listeners = this.rebindListeners.get(workspaceId);
707
+ if (!listeners) return;
708
+ for (const listener of [...listeners]) {
709
+ try {
710
+ listener();
711
+ } catch (e) {
712
+ console.error(`[wm] rebind listener for ${workspaceId} failed:`, e);
713
+ }
714
+ }
715
+ }
716
+ async listSessions(workspaceId) {
717
+ const ws = await getWorkspace(workspaceId);
718
+ if (!ws) throw new Error(`Workspace not found: ${workspaceId}`);
719
+ const sessions = await SessionManager.list(ws.path);
720
+ return sessions.slice().sort((a, b) => b.modified.getTime() - a.modified.getTime()).map(toSessionSummary);
721
+ }
722
+ getSessionHistory(workspaceId, sessionPath) {
723
+ const runtime = this.states.get(workspaceId)?.runtime;
724
+ if (!runtime) return { items: [], isStreaming: false };
725
+ if (sessionPath) {
726
+ const activeFile = runtime.session.sessionFile ? resolve2(runtime.session.sessionFile) : void 0;
727
+ if (activeFile !== resolve2(sessionPath)) {
728
+ return { items: [], isStreaming: false };
729
+ }
730
+ }
731
+ const isStreaming = runtime.session.isStreaming ?? false;
732
+ const branch = runtime.session.sessionManager.getBranch();
733
+ const items = [];
734
+ const argsByCallId = /* @__PURE__ */ new Map();
735
+ for (const entry of branch) {
736
+ if (entry.type !== "message") continue;
737
+ const msg = entry.message;
738
+ const role = msg.role;
739
+ if (role === "user") {
740
+ const text = extractUserText(msg);
741
+ if (text) items.push({ kind: "user", text });
742
+ } else if (role === "assistant") {
743
+ const { text, thinking, toolCalls } = extractAssistantContent(
744
+ msg
745
+ );
746
+ for (const tc of toolCalls) {
747
+ argsByCallId.set(tc.id, tc.args);
748
+ }
749
+ if (text || thinking) items.push({ kind: "assistant", text, thinking });
750
+ } else if (role === "toolResult") {
751
+ const tr = msg;
752
+ items.push({
753
+ kind: "tool",
754
+ toolCallId: tr.toolCallId,
755
+ toolName: tr.toolName,
756
+ args: argsByCallId.get(tr.toolCallId) ?? "",
757
+ text: extractContentText(tr.content),
758
+ isError: tr.isError
759
+ });
760
+ } else if (role === "bashExecution") {
761
+ const be = msg;
762
+ items.push({
763
+ kind: "bash",
764
+ command: be.command,
765
+ output: be.output,
766
+ exitCode: be.exitCode
767
+ });
768
+ }
769
+ }
770
+ return { items, isStreaming };
771
+ }
772
+ /**
773
+ * Delete a session JSONL file belonging to this workspace.
774
+ *
775
+ * Errors are tagged with HTTP semantics via HttpError so the route layer
776
+ * can map them to the right status code:
777
+ * - 400: sessionPath not absolute
778
+ * - 404: workspace gone, or session not in this workspace's list
779
+ * - 409: file is the currently-active session (caller must switch first)
780
+ *
781
+ * Idempotent on ENOENT: if the file is missing at unlink time (e.g. a
782
+ * concurrent external delete between list and unlink), we treat it as
783
+ * success — the goal state has been reached.
784
+ */
785
+ async deleteSession(workspaceId, sessionPath) {
786
+ const ws = await getWorkspace(workspaceId);
787
+ if (!ws) throw new HttpError(404, `Workspace not found: ${workspaceId}`);
788
+ if (!isAbsolute2(sessionPath)) {
789
+ throw new HttpError(400, "Session path must be absolute");
790
+ }
791
+ const sessions = await SessionManager.list(ws.path);
792
+ const resolved = resolve2(sessionPath);
793
+ const target = sessions.find((session) => resolve2(session.path) === resolved);
794
+ if (!target) {
795
+ throw new HttpError(404, `Session not found: ${sessionPath}`);
796
+ }
797
+ const runtime = this.states.get(workspaceId)?.runtime;
798
+ const activePath = runtime?.session.sessionFile ? resolve2(runtime.session.sessionFile) : void 0;
799
+ if (activePath === resolved) {
800
+ throw new HttpError(
801
+ 409,
802
+ "Cannot delete the currently active session \u2014 switch to another session first"
803
+ );
804
+ }
805
+ try {
806
+ await unlink2(resolved);
807
+ } catch (err) {
808
+ if (err?.code === "ENOENT") {
809
+ console.warn(
810
+ `[wm] deleteSession: ${resolved} was already gone at unlink time`
811
+ );
812
+ return;
813
+ }
814
+ throw err;
815
+ }
816
+ }
817
+ async switchSession(workspaceId, sessionPath) {
818
+ const ws = await getWorkspace(workspaceId);
819
+ if (!ws) throw new Error(`Workspace not found: ${workspaceId}`);
820
+ if (!isAbsolute2(sessionPath)) {
821
+ throw new Error("Session path must be absolute");
822
+ }
823
+ const sessions = await SessionManager.list(ws.path);
824
+ const resolved = resolve2(sessionPath);
825
+ const target = sessions.find((session) => resolve2(session.path) === resolved);
826
+ if (!target) {
827
+ throw new Error(`Session not found: ${sessionPath}`);
828
+ }
829
+ const runtime = await this.getOrCreate(workspaceId);
830
+ const currentPath = runtime.session.sessionFile ? resolve2(runtime.session.sessionFile) : void 0;
831
+ if (currentPath === resolved) return false;
832
+ if (runtime.session.isStreaming) {
833
+ throw new Error("Cannot switch sessions while the agent is streaming");
834
+ }
835
+ const result = await runtime.switchSession(resolved, { cwdOverride: ws.path });
836
+ return !result.cancelled;
837
+ }
838
+ async dispose(workspaceId) {
839
+ const state = this.states.get(workspaceId);
840
+ if (!state) return;
841
+ this.states.delete(workspaceId);
842
+ this.rebindListeners.delete(workspaceId);
843
+ this.subscribers.delete(workspaceId);
844
+ try {
845
+ state.bridge.dispose();
846
+ } catch (e) {
847
+ console.error(`[wm] dispose bridge ${workspaceId} failed:`, e);
848
+ }
849
+ try {
850
+ state.runtime.session.dispose();
851
+ } catch (e) {
852
+ console.error(`[wm] dispose ${workspaceId} failed:`, e);
853
+ }
854
+ }
855
+ async disposeAll() {
856
+ const ids = [...this.states.keys()];
857
+ await Promise.all(ids.map((id) => this.dispose(id)));
858
+ }
859
+ };
860
+ function toSessionSummary(info) {
861
+ const preview = info.firstMessage.replace(/\s+/g, " ").trim();
862
+ return {
863
+ path: info.path,
864
+ name: info.name,
865
+ updatedAt: info.modified.toISOString(),
866
+ preview: preview ? preview.slice(0, 160) : void 0
867
+ };
868
+ }
869
+ function extractUserText(msg) {
870
+ if (typeof msg.content === "string") return msg.content;
871
+ return extractContentText(msg.content);
872
+ }
873
+ function extractAssistantContent(msg) {
874
+ const textParts = [];
875
+ const thinkingParts = [];
876
+ const toolCalls = [];
877
+ for (const block of msg.content ?? []) {
878
+ if (!block || typeof block !== "object") continue;
879
+ const b = block;
880
+ if (b.type === "text" && typeof b.text === "string") textParts.push(b.text);
881
+ else if (b.type === "thinking" && typeof b.thinking === "string") thinkingParts.push(b.thinking);
882
+ else if (b.type === "toolCall" && typeof b.id === "string") {
883
+ toolCalls.push({
884
+ id: b.id,
885
+ args: b.arguments != null ? JSON.stringify(b.arguments) : ""
886
+ });
887
+ }
888
+ }
889
+ return { text: textParts.join(""), thinking: thinkingParts.join(""), toolCalls };
890
+ }
891
+ function extractContentText(content) {
892
+ if (!Array.isArray(content)) return "";
893
+ const parts = [];
894
+ for (const block of content) {
895
+ if (block && typeof block === "object" && block.type === "text") {
896
+ const text = block.text;
897
+ if (typeof text === "string") parts.push(text);
898
+ }
899
+ }
900
+ return parts.join("");
901
+ }
902
+ var workspaceManager = new WorkspaceManager();
903
+ function broadcastTo(subscribers, msg) {
904
+ const wire = JSON.stringify(msg);
905
+ for (const ws of subscribers) {
906
+ if (ws.readyState !== ws.OPEN) continue;
907
+ try {
908
+ ws.send(wire);
909
+ } catch {
910
+ }
911
+ }
912
+ }
913
+
914
+ // src/api/config.ts
915
+ function buildConfigResponse(workspaceId) {
916
+ const runtime = workspaceManager.get(workspaceId);
917
+ if (!runtime) throw new Error("runtime not initialized");
918
+ const session = runtime.session;
919
+ const model = session.model;
920
+ const currentModel = model ? {
921
+ provider: model.provider,
922
+ modelId: model.id,
923
+ name: model.name,
924
+ reasoning: model.reasoning
925
+ } : null;
926
+ const availableModels = session.modelRegistry.getAvailable().map((m) => ({
927
+ provider: m.provider,
928
+ modelId: m.id,
929
+ name: m.name,
930
+ reasoning: m.reasoning
931
+ }));
932
+ const allTools = session.getAllTools().map((t) => ({
933
+ name: t.name,
934
+ description: t.description
935
+ }));
936
+ return {
937
+ currentModel,
938
+ thinkingLevel: session.thinkingLevel,
939
+ availableThinkingLevels: session.getAvailableThinkingLevels(),
940
+ activeTools: session.getActiveToolNames(),
941
+ availableModels,
942
+ allTools
943
+ };
944
+ }
945
+ async function requireWorkspace(c, id) {
946
+ const ws = await getWorkspace(id);
947
+ if (!ws) {
948
+ c.status(404);
949
+ c.header("Content-Type", "application/json");
950
+ return false;
951
+ }
952
+ return true;
953
+ }
954
+ function rejectIfStreaming(c, workspaceId) {
955
+ const runtime = workspaceManager.get(workspaceId);
956
+ if (runtime?.session.isStreaming) {
957
+ return true;
958
+ }
959
+ return false;
960
+ }
961
+ function broadcastContextUsage(workspaceId, runtime) {
962
+ const usage = runtime.session.getContextUsage();
963
+ if (!usage) return;
964
+ const payload = {
965
+ kind: "context_usage",
966
+ tokens: usage.tokens,
967
+ contextWindow: usage.contextWindow,
968
+ percent: usage.percent
969
+ };
970
+ workspaceManager.broadcast(workspaceId, {
971
+ type: "event",
972
+ workspaceId,
973
+ sessionPath: runtime.session.sessionFile ?? null,
974
+ payload
975
+ });
976
+ }
977
+ function mountConfigRoutes(app2) {
978
+ app2.get("/:id/config", async (c) => {
979
+ const id = c.req.param("id");
980
+ const exists2 = await requireWorkspace(c, id);
981
+ if (!exists2) return c.json({ ok: false, error: "not found" }, 404);
982
+ try {
983
+ await workspaceManager.getOrCreate(id);
984
+ return c.json(buildConfigResponse(id));
985
+ } catch (err) {
986
+ const message = err instanceof Error ? err.message : String(err);
987
+ return c.json({ ok: false, error: message }, 500);
988
+ }
989
+ });
990
+ app2.put("/:id/config/model", async (c) => {
991
+ const id = c.req.param("id");
992
+ const exists2 = await requireWorkspace(c, id);
993
+ if (!exists2) return c.json({ ok: false, error: "not found" }, 404);
994
+ const body = await c.req.json();
995
+ if (!body?.provider || !body?.modelId) {
996
+ return c.json({ ok: false, error: "provider and modelId are required" }, 400);
997
+ }
998
+ try {
999
+ const runtime = await workspaceManager.getOrCreate(id);
1000
+ if (rejectIfStreaming(c, id)) {
1001
+ return c.json({ ok: false, error: "cannot change model while the agent is streaming" }, 409);
1002
+ }
1003
+ const model = runtime.session.modelRegistry.getAvailable().find(
1004
+ (m) => m.provider === body.provider && m.id === body.modelId
1005
+ );
1006
+ if (!model) {
1007
+ return c.json({ ok: false, error: `model not found or no auth: ${body.provider}/${body.modelId}` }, 404);
1008
+ }
1009
+ await runtime.session.setModel(model);
1010
+ broadcastContextUsage(id, runtime);
1011
+ return c.json(buildConfigResponse(id));
1012
+ } catch (err) {
1013
+ const message = err instanceof Error ? err.message : String(err);
1014
+ return c.json({ ok: false, error: message }, 500);
1015
+ }
1016
+ });
1017
+ app2.put("/:id/config/thinking-level", async (c) => {
1018
+ const id = c.req.param("id");
1019
+ const exists2 = await requireWorkspace(c, id);
1020
+ if (!exists2) return c.json({ ok: false, error: "not found" }, 404);
1021
+ const body = await c.req.json();
1022
+ const validLevels = ["off", "minimal", "low", "medium", "high", "xhigh"];
1023
+ if (!body?.level || !validLevels.includes(body.level)) {
1024
+ return c.json({ ok: false, error: `level must be one of: ${validLevels.join(", ")}` }, 400);
1025
+ }
1026
+ try {
1027
+ await workspaceManager.getOrCreate(id);
1028
+ if (rejectIfStreaming(c, id)) {
1029
+ return c.json({ ok: false, error: "cannot change thinking level while the agent is streaming" }, 409);
1030
+ }
1031
+ const runtime = workspaceManager.get(id);
1032
+ if (!runtime) {
1033
+ return c.json({ ok: false, error: "runtime not initialized" }, 500);
1034
+ }
1035
+ runtime.session.setThinkingLevel(body.level);
1036
+ return c.json(buildConfigResponse(id));
1037
+ } catch (err) {
1038
+ const message = err instanceof Error ? err.message : String(err);
1039
+ return c.json({ ok: false, error: message }, 500);
1040
+ }
1041
+ });
1042
+ app2.put("/:id/config/tools", async (c) => {
1043
+ const id = c.req.param("id");
1044
+ const exists2 = await requireWorkspace(c, id);
1045
+ if (!exists2) return c.json({ ok: false, error: "not found" }, 404);
1046
+ const body = await c.req.json();
1047
+ if (!body?.tools || !Array.isArray(body.tools)) {
1048
+ return c.json({ ok: false, error: "tools must be an array of tool names" }, 400);
1049
+ }
1050
+ try {
1051
+ await workspaceManager.getOrCreate(id);
1052
+ if (rejectIfStreaming(c, id)) {
1053
+ return c.json({ ok: false, error: "cannot change tools while the agent is streaming" }, 409);
1054
+ }
1055
+ const runtime = workspaceManager.get(id);
1056
+ if (!runtime) {
1057
+ return c.json({ ok: false, error: "runtime not initialized" }, 500);
1058
+ }
1059
+ runtime.session.setActiveToolsByName(body.tools);
1060
+ return c.json(buildConfigResponse(id));
1061
+ } catch (err) {
1062
+ const message = err instanceof Error ? err.message : String(err);
1063
+ return c.json({ ok: false, error: message }, 500);
1064
+ }
1065
+ });
1066
+ }
1067
+
1068
+ // src/api/files.ts
1069
+ import { execFile as execFile2 } from "child_process";
1070
+ import { readdir } from "fs/promises";
1071
+ import { join as join4, relative, sep as sep2 } from "path";
1072
+ import { promisify as promisify2 } from "util";
1073
+ var exec2 = promisify2(execFile2);
1074
+ var LIST_TTL_MS = 1e4;
1075
+ var MAX_CACHED_WORKSPACES = 16;
1076
+ var MAX_FILES_TRACKED = 2e4;
1077
+ var DEFAULT_LIMIT = 30;
1078
+ var MAX_LIMIT = 100;
1079
+ var WALK_MAX_DEPTH = 8;
1080
+ var WALK_IGNORES = /* @__PURE__ */ new Set([
1081
+ ".git",
1082
+ "node_modules",
1083
+ "dist",
1084
+ "build",
1085
+ ".next",
1086
+ ".turbo",
1087
+ ".cache",
1088
+ ".vite",
1089
+ "out",
1090
+ "coverage",
1091
+ "target",
1092
+ "venv",
1093
+ ".venv",
1094
+ "__pycache__",
1095
+ ".DS_Store"
1096
+ ]);
1097
+ var listCache = /* @__PURE__ */ new Map();
1098
+ var inflight2 = /* @__PURE__ */ new Map();
1099
+ async function getFileList(workspacePath) {
1100
+ const now = Date.now();
1101
+ const cached = listCache.get(workspacePath);
1102
+ if (cached && cached.expiresAt > now) return cached.files;
1103
+ const pending = inflight2.get(workspacePath);
1104
+ if (pending) return (await pending).files;
1105
+ const probe = probeFileList(workspacePath).then((files) => {
1106
+ const entry = {
1107
+ files,
1108
+ expiresAt: Date.now() + LIST_TTL_MS
1109
+ };
1110
+ listCache.delete(workspacePath);
1111
+ listCache.set(workspacePath, entry);
1112
+ while (listCache.size > MAX_CACHED_WORKSPACES) {
1113
+ const oldest = listCache.keys().next().value;
1114
+ if (!oldest) break;
1115
+ listCache.delete(oldest);
1116
+ }
1117
+ return entry;
1118
+ }).finally(() => inflight2.delete(workspacePath));
1119
+ inflight2.set(workspacePath, probe);
1120
+ return (await probe).files;
1121
+ }
1122
+ async function probeFileList(workspacePath) {
1123
+ try {
1124
+ const { stdout } = await exec2(
1125
+ "git",
1126
+ ["ls-files", "--cached", "--others", "--exclude-standard"],
1127
+ {
1128
+ cwd: workspacePath,
1129
+ timeout: 3e3,
1130
+ maxBuffer: 16 * 1024 * 1024,
1131
+ env: { ...process.env, GIT_DIR: void 0, GIT_WORK_TREE: void 0 }
1132
+ }
1133
+ );
1134
+ const lines = stdout.split("\n");
1135
+ const out2 = [];
1136
+ for (const line of lines) {
1137
+ if (!line) continue;
1138
+ out2.push(line);
1139
+ if (out2.length >= MAX_FILES_TRACKED) break;
1140
+ }
1141
+ return out2;
1142
+ } catch {
1143
+ }
1144
+ const out = [];
1145
+ await walkDir(workspacePath, workspacePath, 0, out);
1146
+ return out;
1147
+ }
1148
+ async function walkDir(root, dir, depth, out) {
1149
+ if (out.length >= MAX_FILES_TRACKED) return;
1150
+ if (depth > WALK_MAX_DEPTH) return;
1151
+ let dirents;
1152
+ try {
1153
+ dirents = await readdir(dir, { withFileTypes: true });
1154
+ } catch {
1155
+ return;
1156
+ }
1157
+ for (const d of dirents) {
1158
+ if (out.length >= MAX_FILES_TRACKED) return;
1159
+ if (WALK_IGNORES.has(d.name)) continue;
1160
+ const abs = join4(dir, d.name);
1161
+ if (d.isDirectory()) {
1162
+ await walkDir(root, abs, depth + 1, out);
1163
+ } else if (d.isFile()) {
1164
+ out.push(relative(root, abs).split(sep2).join("/"));
1165
+ }
1166
+ }
1167
+ }
1168
+ function scoreMatch(relPath, q) {
1169
+ const lower = relPath.toLowerCase();
1170
+ const slash = lower.lastIndexOf("/");
1171
+ const base = slash >= 0 ? lower.slice(slash + 1) : lower;
1172
+ if (base.startsWith(q)) return 1e3 - relPath.length;
1173
+ const baseIdx = base.indexOf(q);
1174
+ if (baseIdx >= 0) return 800 - baseIdx - relPath.length * 0.01;
1175
+ const pathIdx = lower.indexOf(q);
1176
+ if (pathIdx >= 0) return 500 - pathIdx - relPath.length * 0.01;
1177
+ return null;
1178
+ }
1179
+ async function ensureWorkspaceExists(id) {
1180
+ const ws = await getWorkspace(id);
1181
+ return ws ? ws.path : null;
1182
+ }
1183
+ function mountFilesRoute(app2) {
1184
+ app2.get("/:id/files/search", async (c) => {
1185
+ const id = c.req.param("id");
1186
+ const workspacePath = await ensureWorkspaceExists(id);
1187
+ if (!workspacePath) return c.json({ ok: false, error: "not found" }, 404);
1188
+ const qRaw = (c.req.query("q") ?? "").trim();
1189
+ const limitRaw = c.req.query("limit");
1190
+ let limit = limitRaw ? Number(limitRaw) : DEFAULT_LIMIT;
1191
+ if (!Number.isFinite(limit) || limit <= 0) limit = DEFAULT_LIMIT;
1192
+ if (limit > MAX_LIMIT) limit = MAX_LIMIT;
1193
+ try {
1194
+ const all = await getFileList(workspacePath);
1195
+ let entries;
1196
+ let truncated = false;
1197
+ if (!qRaw) {
1198
+ const slice = all.slice(0, limit);
1199
+ entries = slice.map((relPath) => ({
1200
+ path: join4(workspacePath, relPath),
1201
+ relPath
1202
+ }));
1203
+ truncated = all.length > limit;
1204
+ } else {
1205
+ const q = qRaw.toLowerCase();
1206
+ const scored = [];
1207
+ let matchCount = 0;
1208
+ for (const relPath of all) {
1209
+ const score = scoreMatch(relPath, q);
1210
+ if (score === null) continue;
1211
+ matchCount++;
1212
+ scored.push({ relPath, score });
1213
+ }
1214
+ scored.sort((a, b) => b.score - a.score);
1215
+ const top = scored.slice(0, limit);
1216
+ entries = top.map((e) => ({
1217
+ path: join4(workspacePath, e.relPath),
1218
+ relPath: e.relPath
1219
+ }));
1220
+ truncated = matchCount > limit;
1221
+ }
1222
+ const body = { workspacePath, entries, truncated };
1223
+ return c.json(body);
1224
+ } catch (err) {
1225
+ const message = err instanceof Error ? err.message : String(err);
1226
+ console.error(`[api/files] search for ${id} failed:`, err);
1227
+ return c.json({ ok: false, error: message }, 500);
1228
+ }
1229
+ });
1230
+ }
1231
+
1232
+ // src/api/resources.ts
1233
+ import { readdir as readdir2 } from "fs/promises";
1234
+ import { join as join5 } from "path";
1235
+ import { getAgentDir as getAgentDir2 } from "@earendil-works/pi-coding-agent";
1236
+ function toResourceSource(info) {
1237
+ return {
1238
+ scope: info.scope,
1239
+ label: info.source,
1240
+ path: info.path
1241
+ };
1242
+ }
1243
+ async function scanExtensionDirs(workspaceCwd) {
1244
+ const dirs = [join5(getAgentDir2(), "extensions"), join5(workspaceCwd, ".pi", "extensions")];
1245
+ const found = [];
1246
+ for (const dir of dirs) {
1247
+ try {
1248
+ const entries = await readdir2(dir, { withFileTypes: true });
1249
+ for (const entry of entries) {
1250
+ if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
1251
+ found.push(join5(dir, entry.name));
1252
+ } else if (entry.isDirectory()) {
1253
+ found.push(join5(dir, entry.name));
1254
+ }
1255
+ }
1256
+ } catch {
1257
+ }
1258
+ }
1259
+ return found;
1260
+ }
1261
+ async function snapshot(workspaceId, roots, workspaceCwd) {
1262
+ const runtime = workspaceManager.get(workspaceId);
1263
+ if (!runtime) throw new HttpError(500, "runtime not initialized");
1264
+ const loader = runtime.services.resourceLoader;
1265
+ const { skills } = loader.getSkills();
1266
+ const { prompts } = loader.getPrompts();
1267
+ const extResult = loader.getExtensions();
1268
+ const skillsOut = skills.map((s) => ({
1269
+ name: s.name,
1270
+ description: s.description,
1271
+ filePath: s.filePath,
1272
+ disableModelInvocation: s.disableModelInvocation,
1273
+ source: toResourceSource(s.sourceInfo),
1274
+ managed: scopeFor(s.filePath, roots) !== void 0
1275
+ }));
1276
+ const promptsOut = prompts.map((p) => ({
1277
+ name: p.name,
1278
+ description: p.description,
1279
+ argumentHint: p.argumentHint,
1280
+ filePath: p.filePath,
1281
+ content: p.content,
1282
+ source: toResourceSource(p.sourceInfo),
1283
+ managed: scopeFor(p.filePath, roots) !== void 0
1284
+ }));
1285
+ const extensionsOut = extResult.extensions.map((e) => ({
1286
+ path: e.path,
1287
+ resolvedPath: e.resolvedPath,
1288
+ source: toResourceSource(e.sourceInfo),
1289
+ tools: [...e.tools.keys()],
1290
+ commands: [...e.commands.keys()],
1291
+ flags: [...e.flags.keys()],
1292
+ shortcuts: [...e.shortcuts.keys()]
1293
+ }));
1294
+ const extensionErrors = extResult.errors.map((err) => ({
1295
+ path: err.path,
1296
+ error: err.error
1297
+ }));
1298
+ const disabledExtensions = EXTENSIONS_ENABLED ? [] : await scanExtensionDirs(workspaceCwd);
1299
+ return {
1300
+ skills: skillsOut,
1301
+ prompts: promptsOut,
1302
+ extensionsEnabled: EXTENSIONS_ENABLED,
1303
+ extensions: extensionsOut,
1304
+ extensionErrors,
1305
+ disabledExtensions
1306
+ };
1307
+ }
1308
+ async function rootsFor(workspaceId) {
1309
+ const ws = await getWorkspace(workspaceId);
1310
+ if (!ws) throw new HttpError(404, "workspace not found");
1311
+ const roots = resolveResourceRoots({ agentDir: getAgentDir2(), workspaceCwd: ws.path });
1312
+ return { roots, workspaceCwd: ws.path };
1313
+ }
1314
+ function respondError(c, err) {
1315
+ if (err instanceof HttpError) {
1316
+ return c.json({ ok: false, error: err.message }, err.status);
1317
+ }
1318
+ const message = err instanceof Error ? err.message : String(err);
1319
+ console.error(`[api/resources] unexpected error:`, err);
1320
+ return c.json({ ok: false, error: message }, 500);
1321
+ }
1322
+ async function reload(workspaceId) {
1323
+ const runtime = workspaceManager.get(workspaceId);
1324
+ if (!runtime) throw new HttpError(500, "runtime not initialized");
1325
+ await runtime.services.resourceLoader.reload();
1326
+ }
1327
+ function mountResourcesRoute(app2) {
1328
+ app2.get("/:id/resources", async (c) => {
1329
+ const id = c.req.param("id");
1330
+ const ws = await getWorkspace(id);
1331
+ if (!ws) return c.json({ ok: false, error: "not found" }, 404);
1332
+ try {
1333
+ await workspaceManager.getOrCreate(id);
1334
+ const { roots, workspaceCwd } = await rootsFor(id);
1335
+ return c.json(await snapshot(id, roots, workspaceCwd));
1336
+ } catch (err) {
1337
+ return respondError(c, err);
1338
+ }
1339
+ });
1340
+ app2.post("/:id/resources/reload", async (c) => {
1341
+ const id = c.req.param("id");
1342
+ const ws = await getWorkspace(id);
1343
+ if (!ws) return c.json({ ok: false, error: "not found" }, 404);
1344
+ try {
1345
+ await workspaceManager.getOrCreate(id);
1346
+ await reload(id);
1347
+ const { roots, workspaceCwd } = await rootsFor(id);
1348
+ return c.json(await snapshot(id, roots, workspaceCwd));
1349
+ } catch (err) {
1350
+ return respondError(c, err);
1351
+ }
1352
+ });
1353
+ app2.get("/:id/resources/skill", async (c) => {
1354
+ const id = c.req.param("id");
1355
+ const ws = await getWorkspace(id);
1356
+ if (!ws) return c.json({ ok: false, error: "not found" }, 404);
1357
+ const filePath = c.req.query("path");
1358
+ if (!filePath) return c.json({ ok: false, error: "path query is required" }, 400);
1359
+ try {
1360
+ const { roots } = await rootsFor(id);
1361
+ const scope = scopeFor(filePath, roots);
1362
+ if (scope === void 0) {
1363
+ return c.json({ ok: false, error: "skill file is not under a managed root" }, 400);
1364
+ }
1365
+ const data = await readSkillFile(filePath, roots);
1366
+ const body = {
1367
+ filePath,
1368
+ scope,
1369
+ name: data.name,
1370
+ description: data.description,
1371
+ disableModelInvocation: data.disableModelInvocation,
1372
+ body: data.body
1373
+ };
1374
+ return c.json(body);
1375
+ } catch (err) {
1376
+ return respondError(c, err);
1377
+ }
1378
+ });
1379
+ app2.post("/:id/resources/skills", async (c) => {
1380
+ const id = c.req.param("id");
1381
+ const ws = await getWorkspace(id);
1382
+ if (!ws) return c.json({ ok: false, error: "not found" }, 404);
1383
+ const body = await c.req.json().catch(() => null);
1384
+ if (!body || !isScope(body.scope) || typeof body.name !== "string" || typeof body.description !== "string" || typeof body.body !== "string") {
1385
+ return c.json({ ok: false, error: "scope, name, description, body are required" }, 400);
1386
+ }
1387
+ try {
1388
+ await workspaceManager.getOrCreate(id);
1389
+ const { roots, workspaceCwd } = await rootsFor(id);
1390
+ await createSkill({
1391
+ roots,
1392
+ scope: body.scope,
1393
+ name: body.name,
1394
+ description: body.description,
1395
+ body: body.body,
1396
+ disableModelInvocation: body.disableModelInvocation
1397
+ });
1398
+ await reload(id);
1399
+ return c.json(await snapshot(id, roots, workspaceCwd));
1400
+ } catch (err) {
1401
+ return respondError(c, err);
1402
+ }
1403
+ });
1404
+ app2.put("/:id/resources/skills", async (c) => {
1405
+ const id = c.req.param("id");
1406
+ const ws = await getWorkspace(id);
1407
+ if (!ws) return c.json({ ok: false, error: "not found" }, 404);
1408
+ const body = await c.req.json().catch(() => null);
1409
+ if (!body || typeof body.filePath !== "string" || typeof body.name !== "string" || typeof body.description !== "string" || typeof body.body !== "string") {
1410
+ return c.json({ ok: false, error: "filePath, name, description, body are required" }, 400);
1411
+ }
1412
+ try {
1413
+ await workspaceManager.getOrCreate(id);
1414
+ const { roots, workspaceCwd } = await rootsFor(id);
1415
+ await updateSkill({
1416
+ roots,
1417
+ filePath: body.filePath,
1418
+ name: body.name,
1419
+ description: body.description,
1420
+ body: body.body,
1421
+ disableModelInvocation: body.disableModelInvocation
1422
+ });
1423
+ await reload(id);
1424
+ return c.json(await snapshot(id, roots, workspaceCwd));
1425
+ } catch (err) {
1426
+ return respondError(c, err);
1427
+ }
1428
+ });
1429
+ app2.delete("/:id/resources/skills", async (c) => {
1430
+ const id = c.req.param("id");
1431
+ const ws = await getWorkspace(id);
1432
+ if (!ws) return c.json({ ok: false, error: "not found" }, 404);
1433
+ const filePath = c.req.query("path");
1434
+ if (!filePath) return c.json({ ok: false, error: "path query is required" }, 400);
1435
+ try {
1436
+ await workspaceManager.getOrCreate(id);
1437
+ const { roots, workspaceCwd } = await rootsFor(id);
1438
+ await deleteSkill(filePath, roots);
1439
+ await reload(id);
1440
+ return c.json(await snapshot(id, roots, workspaceCwd));
1441
+ } catch (err) {
1442
+ return respondError(c, err);
1443
+ }
1444
+ });
1445
+ app2.get("/:id/resources/prompt", async (c) => {
1446
+ const id = c.req.param("id");
1447
+ const ws = await getWorkspace(id);
1448
+ if (!ws) return c.json({ ok: false, error: "not found" }, 404);
1449
+ const filePath = c.req.query("path");
1450
+ if (!filePath) return c.json({ ok: false, error: "path query is required" }, 400);
1451
+ try {
1452
+ const { roots } = await rootsFor(id);
1453
+ const scope = scopeFor(filePath, roots);
1454
+ if (scope === void 0) {
1455
+ return c.json({ ok: false, error: "prompt file is not under a managed root" }, 400);
1456
+ }
1457
+ const data = await readPromptFile(filePath, roots);
1458
+ const body = {
1459
+ filePath,
1460
+ scope,
1461
+ name: data.name,
1462
+ description: data.description,
1463
+ argumentHint: data.argumentHint,
1464
+ body: data.body
1465
+ };
1466
+ return c.json(body);
1467
+ } catch (err) {
1468
+ return respondError(c, err);
1469
+ }
1470
+ });
1471
+ app2.post("/:id/resources/prompts", async (c) => {
1472
+ const id = c.req.param("id");
1473
+ const ws = await getWorkspace(id);
1474
+ if (!ws) return c.json({ ok: false, error: "not found" }, 404);
1475
+ const body = await c.req.json().catch(() => null);
1476
+ if (!body || !isScope(body.scope) || typeof body.name !== "string" || typeof body.description !== "string" || typeof body.body !== "string") {
1477
+ return c.json({ ok: false, error: "scope, name, description, body are required" }, 400);
1478
+ }
1479
+ try {
1480
+ await workspaceManager.getOrCreate(id);
1481
+ const { roots, workspaceCwd } = await rootsFor(id);
1482
+ await createPrompt({
1483
+ roots,
1484
+ scope: body.scope,
1485
+ name: body.name,
1486
+ description: body.description,
1487
+ argumentHint: body.argumentHint,
1488
+ body: body.body
1489
+ });
1490
+ await reload(id);
1491
+ return c.json(await snapshot(id, roots, workspaceCwd));
1492
+ } catch (err) {
1493
+ return respondError(c, err);
1494
+ }
1495
+ });
1496
+ app2.put("/:id/resources/prompts", async (c) => {
1497
+ const id = c.req.param("id");
1498
+ const ws = await getWorkspace(id);
1499
+ if (!ws) return c.json({ ok: false, error: "not found" }, 404);
1500
+ const body = await c.req.json().catch(() => null);
1501
+ if (!body || typeof body.filePath !== "string" || typeof body.name !== "string" || typeof body.description !== "string" || typeof body.body !== "string") {
1502
+ return c.json({ ok: false, error: "filePath, name, description, body are required" }, 400);
1503
+ }
1504
+ try {
1505
+ await workspaceManager.getOrCreate(id);
1506
+ const { roots, workspaceCwd } = await rootsFor(id);
1507
+ await updatePrompt({
1508
+ roots,
1509
+ filePath: body.filePath,
1510
+ name: body.name,
1511
+ description: body.description,
1512
+ argumentHint: body.argumentHint,
1513
+ body: body.body
1514
+ });
1515
+ await reload(id);
1516
+ return c.json(await snapshot(id, roots, workspaceCwd));
1517
+ } catch (err) {
1518
+ return respondError(c, err);
1519
+ }
1520
+ });
1521
+ app2.delete("/:id/resources/prompts", async (c) => {
1522
+ const id = c.req.param("id");
1523
+ const ws = await getWorkspace(id);
1524
+ if (!ws) return c.json({ ok: false, error: "not found" }, 404);
1525
+ const filePath = c.req.query("path");
1526
+ if (!filePath) return c.json({ ok: false, error: "path query is required" }, 400);
1527
+ try {
1528
+ await workspaceManager.getOrCreate(id);
1529
+ const { roots, workspaceCwd } = await rootsFor(id);
1530
+ await deletePrompt(filePath, roots);
1531
+ await reload(id);
1532
+ return c.json(await snapshot(id, roots, workspaceCwd));
1533
+ } catch (err) {
1534
+ return respondError(c, err);
1535
+ }
1536
+ });
1537
+ }
1538
+ function isScope(value) {
1539
+ return value === "user" || value === "project";
1540
+ }
1541
+
1542
+ // src/api/workspaces.ts
1543
+ var workspacesRoute = new Hono();
1544
+ workspacesRoute.get("/", async (c) => {
1545
+ const raw = await listWorkspaces();
1546
+ const workspaces = await Promise.all(raw.map(enrichWorkspace));
1547
+ const body = { workspaces };
1548
+ return c.json(body);
1549
+ });
1550
+ workspacesRoute.get("/:id/sessions", async (c) => {
1551
+ const id = c.req.param("id");
1552
+ const existed = await getWorkspace(id);
1553
+ if (!existed) return c.json({ ok: false, error: "not found" }, 404);
1554
+ try {
1555
+ const sessions = await workspaceManager.listSessions(id);
1556
+ const body = { sessions };
1557
+ return c.json(body);
1558
+ } catch (err) {
1559
+ const message = err instanceof Error ? err.message : String(err);
1560
+ console.error(`[api] list sessions for ${id} failed:`, err);
1561
+ return c.json({ ok: false, error: message }, 500);
1562
+ }
1563
+ });
1564
+ workspacesRoute.delete("/:id/sessions", async (c) => {
1565
+ const id = c.req.param("id");
1566
+ const existed = await getWorkspace(id);
1567
+ if (!existed) return c.json({ ok: false, error: "not found" }, 404);
1568
+ const sessionPath = c.req.query("path");
1569
+ if (!sessionPath) {
1570
+ return c.json({ ok: false, error: "path query is required" }, 400);
1571
+ }
1572
+ try {
1573
+ await workspaceManager.deleteSession(id, sessionPath);
1574
+ const body = { ok: true };
1575
+ return c.json(body);
1576
+ } catch (err) {
1577
+ if (err instanceof HttpError) {
1578
+ return c.json(
1579
+ { ok: false, error: err.message },
1580
+ err.status
1581
+ );
1582
+ }
1583
+ const message = err instanceof Error ? err.message : String(err);
1584
+ console.error(`[api] delete session for ${id} failed:`, err);
1585
+ return c.json({ ok: false, error: message }, 500);
1586
+ }
1587
+ });
1588
+ workspacesRoute.get("/:id/fork-points", async (c) => {
1589
+ const id = c.req.param("id");
1590
+ const existed = await getWorkspace(id);
1591
+ if (!existed) return c.json({ ok: false, error: "not found" }, 404);
1592
+ try {
1593
+ const runtime = await workspaceManager.getOrCreate(id);
1594
+ const raw = runtime.session.getUserMessagesForForking();
1595
+ const points = raw.map((p) => ({
1596
+ entryId: p.entryId,
1597
+ text: p.text.length > 120 ? p.text.slice(0, 120) + "\u2026" : p.text
1598
+ }));
1599
+ const body = { points };
1600
+ return c.json(body);
1601
+ } catch (err) {
1602
+ const message = err instanceof Error ? err.message : String(err);
1603
+ console.error(`[api] fork-points for ${id} failed:`, err);
1604
+ return c.json({ ok: false, error: message }, 500);
1605
+ }
1606
+ });
1607
+ workspacesRoute.get("/:id/history", async (c) => {
1608
+ const id = c.req.param("id");
1609
+ const existed = await getWorkspace(id);
1610
+ if (!existed) return c.json({ ok: false, error: "not found" }, 404);
1611
+ try {
1612
+ await workspaceManager.getOrCreate(id);
1613
+ const sessionPath = c.req.query("sessionPath");
1614
+ const body = workspaceManager.getSessionHistory(id, sessionPath);
1615
+ return c.json(body);
1616
+ } catch (err) {
1617
+ const message = err instanceof Error ? err.message : String(err);
1618
+ console.error(`[api] history for ${id} failed:`, err);
1619
+ return c.json({ ok: false, error: message }, 500);
1620
+ }
1621
+ });
1622
+ workspacesRoute.post("/", async (c) => {
1623
+ const body = await c.req.json();
1624
+ if (!body?.path || typeof body.path !== "string") {
1625
+ return c.json({ ok: false, error: "path is required" }, 400);
1626
+ }
1627
+ if (!isAbsolute3(body.path)) {
1628
+ return c.json({ ok: false, error: "path must be absolute" }, 400);
1629
+ }
1630
+ const resolved = resolve3(body.path);
1631
+ try {
1632
+ const st = await stat2(resolved);
1633
+ if (!st.isDirectory()) {
1634
+ return c.json({ ok: false, error: "path is not a directory" }, 400);
1635
+ }
1636
+ } catch {
1637
+ return c.json({ ok: false, error: "path does not exist" }, 400);
1638
+ }
1639
+ const stored = await addWorkspace({
1640
+ path: resolved,
1641
+ name: body.name?.trim() || basename2(resolved) || resolved
1642
+ });
1643
+ const ws = await enrichWorkspace(stored);
1644
+ const res = { workspace: ws };
1645
+ return c.json(res);
1646
+ });
1647
+ workspacesRoute.delete("/:id", async (c) => {
1648
+ const id = c.req.param("id");
1649
+ const existed = await getWorkspace(id);
1650
+ if (!existed) return c.json({ ok: false, error: "not found" }, 404);
1651
+ await workspaceManager.dispose(id);
1652
+ await removeWorkspace(id);
1653
+ const body = { ok: true };
1654
+ return c.json(body);
1655
+ });
1656
+ mountConfigRoutes(workspacesRoute);
1657
+ mountResourcesRoute(workspacesRoute);
1658
+ mountFilesRoute(workspacesRoute);
1659
+
1660
+ // src/api/fs.ts
1661
+ import { readdir as readdir3 } from "fs/promises";
1662
+ import { homedir as homedir2 } from "os";
1663
+ import { dirname as dirname3, isAbsolute as isAbsolute4, join as join6, resolve as resolve4 } from "path";
1664
+ import { Hono as Hono2 } from "hono";
1665
+ var fsRoute = new Hono2();
1666
+ fsRoute.get("/browse", async (c) => {
1667
+ const rawPath = c.req.query("path");
1668
+ const showHidden = c.req.query("showHidden") === "1";
1669
+ const target = rawPath && isAbsolute4(rawPath) ? resolve4(rawPath) : homedir2();
1670
+ let dirents;
1671
+ try {
1672
+ dirents = await readdir3(target, { withFileTypes: true });
1673
+ } catch (err) {
1674
+ const code = err.code;
1675
+ const msg = code === "EACCES" ? "permission denied" : code === "ENOENT" ? "not found" : "read failed";
1676
+ return c.json({ ok: false, error: msg, path: target }, 400);
1677
+ }
1678
+ const entries = dirents.filter((d) => d.isDirectory()).filter((d) => showHidden || !d.name.startsWith(".")).map((d) => ({
1679
+ name: d.name,
1680
+ path: join6(target, d.name),
1681
+ type: "dir"
1682
+ })).sort((a, b) => a.name.localeCompare(b.name));
1683
+ const parent = (() => {
1684
+ const p = dirname3(target);
1685
+ return p === target ? null : p;
1686
+ })();
1687
+ const body = { path: target, parent, entries };
1688
+ return c.json(body);
1689
+ });
1690
+
1691
+ // src/api/model-configs.ts
1692
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1693
+ import { dirname as dirname4, join as join7 } from "path";
1694
+ import { Hono as Hono3 } from "hono";
1695
+ import {
1696
+ getAgentDir as getAgentDir3
1697
+ } from "@earendil-works/pi-coding-agent";
1698
+ var modelConfigsRoute = new Hono3();
1699
+ var writeLock = Promise.resolve();
1700
+ function withWriteLock(fn) {
1701
+ const next = writeLock.then(fn, fn);
1702
+ writeLock = next.then(() => {
1703
+ }, () => {
1704
+ });
1705
+ return next;
1706
+ }
1707
+ function modelsPath() {
1708
+ return join7(getAgentDir3(), "models.json");
1709
+ }
1710
+ async function readModelsJson() {
1711
+ try {
1712
+ const raw = await readFile3(modelsPath(), "utf-8");
1713
+ return JSON.parse(raw);
1714
+ } catch (err) {
1715
+ if (err?.code === "ENOENT") {
1716
+ return { providers: {} };
1717
+ }
1718
+ throw err;
1719
+ }
1720
+ }
1721
+ async function writeModelsJson(config2) {
1722
+ const p = modelsPath();
1723
+ await mkdir3(dirname4(p), { recursive: true });
1724
+ await writeFile3(p, JSON.stringify(config2, null, 2), "utf-8");
1725
+ }
1726
+ var ValidationError = class extends Error {
1727
+ constructor(message, status) {
1728
+ super(message);
1729
+ this.status = status;
1730
+ }
1731
+ status;
1732
+ };
1733
+ function refreshRegistry(workspaceId) {
1734
+ if (!workspaceId) return;
1735
+ const runtime = workspaceManager.get(workspaceId);
1736
+ if (runtime) {
1737
+ try {
1738
+ runtime.session.modelRegistry.refresh();
1739
+ } catch (e) {
1740
+ console.error(`[model-configs] refresh registry for ${workspaceId} failed:`, e);
1741
+ }
1742
+ }
1743
+ }
1744
+ modelConfigsRoute.get("/", async (c) => {
1745
+ try {
1746
+ const config2 = await readModelsJson();
1747
+ const body = { config: config2 };
1748
+ return c.json(body);
1749
+ } catch (err) {
1750
+ const message = err instanceof Error ? err.message : String(err);
1751
+ return c.json({ ok: false, error: message }, 500);
1752
+ }
1753
+ });
1754
+ modelConfigsRoute.put("/", async (c) => {
1755
+ const body = await c.req.json();
1756
+ if (!body?.config?.providers) {
1757
+ return c.json({ ok: false, error: "config.providers is required" }, 400);
1758
+ }
1759
+ try {
1760
+ await withWriteLock(async () => {
1761
+ await writeModelsJson(body.config);
1762
+ });
1763
+ const workspaceId = c.req.query("workspaceId");
1764
+ refreshRegistry(workspaceId ?? void 0);
1765
+ const resp = { config: body.config };
1766
+ return c.json(resp);
1767
+ } catch (err) {
1768
+ const message = err instanceof Error ? err.message : String(err);
1769
+ return c.json({ ok: false, error: message }, 500);
1770
+ }
1771
+ });
1772
+ modelConfigsRoute.post("/providers", async (c) => {
1773
+ const body = await c.req.json();
1774
+ if (!body?.name || !body?.provider) {
1775
+ return c.json({ ok: false, error: "name and provider are required" }, 400);
1776
+ }
1777
+ if (!body.provider.baseUrl || !body.provider.api || !body.provider.apiKey) {
1778
+ return c.json({ ok: false, error: "provider must have baseUrl, api, and apiKey" }, 400);
1779
+ }
1780
+ if (!Array.isArray(body.provider.models)) {
1781
+ return c.json({ ok: false, error: "provider.models must be an array" }, 400);
1782
+ }
1783
+ try {
1784
+ const config2 = await withWriteLock(async () => {
1785
+ const cfg = await readModelsJson();
1786
+ cfg.providers[body.name] = body.provider;
1787
+ await writeModelsJson(cfg);
1788
+ return cfg;
1789
+ });
1790
+ const workspaceId = c.req.query("workspaceId");
1791
+ refreshRegistry(workspaceId ?? void 0);
1792
+ const resp = { config: config2 };
1793
+ return c.json(resp);
1794
+ } catch (err) {
1795
+ const message = err instanceof Error ? err.message : String(err);
1796
+ return c.json({ ok: false, error: message }, 500);
1797
+ }
1798
+ });
1799
+ modelConfigsRoute.delete("/providers", async (c) => {
1800
+ const name = c.req.query("name");
1801
+ if (!name) {
1802
+ return c.json({ ok: false, error: "name query param is required" }, 400);
1803
+ }
1804
+ try {
1805
+ const config2 = await withWriteLock(async () => {
1806
+ const cfg = await readModelsJson();
1807
+ if (!cfg.providers[name]) {
1808
+ throw new ValidationError(`provider "${name}" not found`, 404);
1809
+ }
1810
+ delete cfg.providers[name];
1811
+ await writeModelsJson(cfg);
1812
+ return cfg;
1813
+ });
1814
+ const workspaceId = c.req.query("workspaceId");
1815
+ refreshRegistry(workspaceId ?? void 0);
1816
+ const resp = { config: config2 };
1817
+ return c.json(resp);
1818
+ } catch (err) {
1819
+ if (err instanceof ValidationError) {
1820
+ return c.json({ ok: false, error: err.message }, err.status);
1821
+ }
1822
+ const message = err instanceof Error ? err.message : String(err);
1823
+ return c.json({ ok: false, error: message }, 500);
1824
+ }
1825
+ });
1826
+ modelConfigsRoute.post("/providers/:provider/models", async (c) => {
1827
+ const provider = c.req.param("provider");
1828
+ const body = await c.req.json();
1829
+ if (!body?.model?.id || !body?.model?.name) {
1830
+ return c.json({ ok: false, error: "model.id and model.name are required" }, 400);
1831
+ }
1832
+ try {
1833
+ const config2 = await withWriteLock(async () => {
1834
+ const cfg = await readModelsJson();
1835
+ if (!cfg.providers[provider]) {
1836
+ throw new ValidationError(`provider "${provider}" not found`, 404);
1837
+ }
1838
+ const existing = cfg.providers[provider].models.find((m) => m.id === body.model.id);
1839
+ if (existing) {
1840
+ throw new ValidationError(`model "${body.model.id}" already exists in provider "${provider}"`, 409);
1841
+ }
1842
+ cfg.providers[provider].models.push(body.model);
1843
+ await writeModelsJson(cfg);
1844
+ return cfg;
1845
+ });
1846
+ const workspaceId = c.req.query("workspaceId");
1847
+ refreshRegistry(workspaceId ?? void 0);
1848
+ const resp = { config: config2 };
1849
+ return c.json(resp);
1850
+ } catch (err) {
1851
+ if (err instanceof ValidationError) {
1852
+ return c.json({ ok: false, error: err.message }, err.status);
1853
+ }
1854
+ const message = err instanceof Error ? err.message : String(err);
1855
+ return c.json({ ok: false, error: message }, 500);
1856
+ }
1857
+ });
1858
+ modelConfigsRoute.put("/providers/:provider/models/:modelId", async (c) => {
1859
+ const provider = c.req.param("provider");
1860
+ const modelId = c.req.param("modelId");
1861
+ const body = await c.req.json();
1862
+ if (!body?.model) {
1863
+ return c.json({ ok: false, error: "model is required" }, 400);
1864
+ }
1865
+ if (body.model.id !== modelId) {
1866
+ return c.json({ ok: false, error: "model.id does not match URL parameter" }, 400);
1867
+ }
1868
+ try {
1869
+ const config2 = await withWriteLock(async () => {
1870
+ const cfg = await readModelsJson();
1871
+ if (!cfg.providers[provider]) {
1872
+ throw new ValidationError(`provider "${provider}" not found`, 404);
1873
+ }
1874
+ const idx = cfg.providers[provider].models.findIndex((m) => m.id === modelId);
1875
+ if (idx === -1) {
1876
+ throw new ValidationError(`model "${modelId}" not found in provider "${provider}"`, 404);
1877
+ }
1878
+ cfg.providers[provider].models[idx] = body.model;
1879
+ await writeModelsJson(cfg);
1880
+ return cfg;
1881
+ });
1882
+ const workspaceId = c.req.query("workspaceId");
1883
+ refreshRegistry(workspaceId ?? void 0);
1884
+ const resp = { config: config2 };
1885
+ return c.json(resp);
1886
+ } catch (err) {
1887
+ if (err instanceof ValidationError) {
1888
+ return c.json({ ok: false, error: err.message }, err.status);
1889
+ }
1890
+ const message = err instanceof Error ? err.message : String(err);
1891
+ return c.json({ ok: false, error: message }, 500);
1892
+ }
1893
+ });
1894
+ modelConfigsRoute.delete("/providers/:provider/models/:modelId", async (c) => {
1895
+ const provider = c.req.param("provider");
1896
+ const modelId = c.req.param("modelId");
1897
+ try {
1898
+ const config2 = await withWriteLock(async () => {
1899
+ const cfg = await readModelsJson();
1900
+ if (!cfg.providers[provider]) {
1901
+ throw new ValidationError(`provider "${provider}" not found`, 404);
1902
+ }
1903
+ const idx = cfg.providers[provider].models.findIndex((m) => m.id === modelId);
1904
+ if (idx === -1) {
1905
+ throw new ValidationError(`model "${modelId}" not found in provider "${provider}"`, 404);
1906
+ }
1907
+ cfg.providers[provider].models.splice(idx, 1);
1908
+ await writeModelsJson(cfg);
1909
+ return cfg;
1910
+ });
1911
+ const workspaceId = c.req.query("workspaceId");
1912
+ refreshRegistry(workspaceId ?? void 0);
1913
+ const resp = { config: config2 };
1914
+ return c.json(resp);
1915
+ } catch (err) {
1916
+ if (err instanceof ValidationError) {
1917
+ return c.json({ ok: false, error: err.message }, err.status);
1918
+ }
1919
+ const message = err instanceof Error ? err.message : String(err);
1920
+ return c.json({ ok: false, error: message }, 500);
1921
+ }
1922
+ });
1923
+
1924
+ // src/ws/hub.ts
1925
+ import { WebSocketServer } from "ws";
1926
+
1927
+ // src/ws/bridge.ts
1928
+ function translatePiEvent(ev) {
1929
+ switch (ev.type) {
1930
+ case "agent_start":
1931
+ return { kind: "agent_start" };
1932
+ case "agent_end":
1933
+ return { kind: "agent_end", willRetry: ev.willRetry };
1934
+ case "turn_start":
1935
+ return { kind: "turn_start" };
1936
+ case "turn_end":
1937
+ return { kind: "turn_end" };
1938
+ case "message_start":
1939
+ return { kind: "message_start", role: roleOf(ev.message) };
1940
+ case "message_end":
1941
+ return { kind: "message_end", role: roleOf(ev.message) };
1942
+ case "message_update": {
1943
+ const ame = ev.assistantMessageEvent;
1944
+ if (ame.type === "text_delta") {
1945
+ return {
1946
+ kind: "message_update",
1947
+ delta: { kind: "text", contentIndex: ame.contentIndex, text: ame.delta }
1948
+ };
1949
+ }
1950
+ if (ame.type === "thinking_delta") {
1951
+ return {
1952
+ kind: "message_update",
1953
+ delta: { kind: "thinking", contentIndex: ame.contentIndex, text: ame.delta }
1954
+ };
1955
+ }
1956
+ return { kind: "message_update", delta: { kind: "other" } };
1957
+ }
1958
+ case "tool_execution_start":
1959
+ return {
1960
+ kind: "tool_execution_start",
1961
+ toolCallId: ev.toolCallId,
1962
+ toolName: ev.toolName,
1963
+ args: ev.args
1964
+ };
1965
+ case "tool_execution_update":
1966
+ return {
1967
+ kind: "tool_execution_update",
1968
+ toolCallId: ev.toolCallId,
1969
+ toolName: ev.toolName,
1970
+ partialText: extractText(ev.partialResult)
1971
+ };
1972
+ case "tool_execution_end":
1973
+ return {
1974
+ kind: "tool_execution_end",
1975
+ toolCallId: ev.toolCallId,
1976
+ toolName: ev.toolName,
1977
+ isError: ev.isError,
1978
+ text: extractText(ev.result)
1979
+ };
1980
+ case "queue_update":
1981
+ return {
1982
+ kind: "queue_update",
1983
+ steering: [...ev.steering],
1984
+ followUp: [...ev.followUp]
1985
+ };
1986
+ case "auto_retry_start":
1987
+ return {
1988
+ kind: "auto_retry_start",
1989
+ attempt: ev.attempt,
1990
+ maxAttempts: ev.maxAttempts,
1991
+ delayMs: ev.delayMs,
1992
+ errorMessage: ev.errorMessage
1993
+ };
1994
+ case "auto_retry_end":
1995
+ return {
1996
+ kind: "auto_retry_end",
1997
+ success: ev.success,
1998
+ attempt: ev.attempt,
1999
+ finalError: ev.finalError
2000
+ };
2001
+ case "compaction_start":
2002
+ return { kind: "compaction_start", reason: ev.reason };
2003
+ case "compaction_end":
2004
+ return {
2005
+ kind: "compaction_end",
2006
+ reason: ev.reason,
2007
+ aborted: ev.aborted,
2008
+ willRetry: ev.willRetry,
2009
+ errorMessage: ev.errorMessage
2010
+ };
2011
+ case "session_info_changed":
2012
+ return { kind: "session_info_changed", name: ev.name };
2013
+ case "thinking_level_changed":
2014
+ return { kind: "thinking_level_changed", level: ev.level };
2015
+ default:
2016
+ return void 0;
2017
+ }
2018
+ }
2019
+ function roleOf(message) {
2020
+ const role = message?.role;
2021
+ if (role === "user" || role === "assistant" || role === "toolResult" || role === "bashExecution") {
2022
+ return role;
2023
+ }
2024
+ return "assistant";
2025
+ }
2026
+ function extractText(result) {
2027
+ if (!result || typeof result !== "object") return void 0;
2028
+ const content = result.content;
2029
+ if (!Array.isArray(content)) return void 0;
2030
+ const parts = [];
2031
+ for (const c of content) {
2032
+ if (c && typeof c === "object" && c.type === "text") {
2033
+ const text = c.text;
2034
+ if (typeof text === "string") parts.push(text);
2035
+ }
2036
+ }
2037
+ return parts.length === 0 ? void 0 : parts.join("");
2038
+ }
2039
+
2040
+ // src/ws/hub.ts
2041
+ var replacementLocks = /* @__PURE__ */ new Map();
2042
+ function withReplacementLock(workspaceId, fn) {
2043
+ const prev = replacementLocks.get(workspaceId) ?? Promise.resolve();
2044
+ const next = prev.then(fn, fn);
2045
+ replacementLocks.set(workspaceId, next);
2046
+ const cleanup = () => {
2047
+ if (replacementLocks.get(workspaceId) === next) {
2048
+ replacementLocks.delete(workspaceId);
2049
+ }
2050
+ };
2051
+ next.then(cleanup, cleanup);
2052
+ return next;
2053
+ }
2054
+ function attachWsHub(httpServer) {
2055
+ const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
2056
+ wss.on("connection", (ws) => {
2057
+ const state = {};
2058
+ ws.on("message", async (raw) => {
2059
+ let msg;
2060
+ try {
2061
+ msg = JSON.parse(raw.toString());
2062
+ } catch {
2063
+ send(ws, { type: "error", message: "invalid JSON" });
2064
+ return;
2065
+ }
2066
+ try {
2067
+ await handle(ws, state, msg);
2068
+ } catch (err) {
2069
+ const message = err instanceof Error ? err.message : String(err);
2070
+ send(ws, { type: "error", message, command: msg.type });
2071
+ }
2072
+ });
2073
+ ws.on("close", () => {
2074
+ detach(state, ws);
2075
+ });
2076
+ });
2077
+ return wss;
2078
+ }
2079
+ async function handle(ws, state, msg) {
2080
+ switch (msg.type) {
2081
+ case "subscribe": {
2082
+ const hadCurrentSubscription = state.workspaceId === msg.workspaceId && !!state.unsubscribeSession;
2083
+ ensureRebindListener(ws, state, msg.workspaceId);
2084
+ await workspaceManager.getOrCreate(msg.workspaceId);
2085
+ let switched = false;
2086
+ let switchError;
2087
+ if (msg.sessionPath) {
2088
+ await withReplacementLock(msg.workspaceId, async () => {
2089
+ try {
2090
+ switched = await workspaceManager.switchSession(msg.workspaceId, msg.sessionPath);
2091
+ } catch (err) {
2092
+ switchError = err instanceof Error ? err.message : String(err);
2093
+ }
2094
+ });
2095
+ }
2096
+ if (!switched && !hadCurrentSubscription) {
2097
+ bindCurrentSession(ws, state, msg.workspaceId);
2098
+ }
2099
+ if (switchError) {
2100
+ send(ws, { type: "error", message: switchError, command: "subscribe" });
2101
+ }
2102
+ send(ws, { type: "ack", command: "subscribe" });
2103
+ return;
2104
+ }
2105
+ case "prompt": {
2106
+ const wsId = state.workspaceId;
2107
+ if (!wsId) {
2108
+ send(ws, { type: "error", message: "not subscribed", command: "prompt" });
2109
+ return;
2110
+ }
2111
+ if (replacementLocks.has(wsId)) {
2112
+ send(ws, { type: "error", message: "session switching in progress", command: "prompt" });
2113
+ return;
2114
+ }
2115
+ const runtime = workspaceManager.get(wsId);
2116
+ if (!runtime) {
2117
+ send(ws, { type: "error", message: "runtime gone", command: "prompt" });
2118
+ return;
2119
+ }
2120
+ void runtime.session.prompt(msg.message, {
2121
+ streamingBehavior: msg.streamingBehavior
2122
+ }).catch((err) => {
2123
+ const message = err instanceof Error ? err.message : String(err);
2124
+ send(ws, { type: "error", message, command: "prompt" });
2125
+ });
2126
+ return;
2127
+ }
2128
+ case "abort": {
2129
+ const wsId = state.workspaceId;
2130
+ if (!wsId) {
2131
+ send(ws, { type: "error", message: "not subscribed", command: "abort" });
2132
+ return;
2133
+ }
2134
+ if (replacementLocks.has(wsId)) {
2135
+ send(ws, { type: "error", message: "session switching in progress", command: "abort" });
2136
+ return;
2137
+ }
2138
+ const runtime = workspaceManager.get(wsId);
2139
+ if (!runtime) return;
2140
+ await runtime.session.abort();
2141
+ return;
2142
+ }
2143
+ case "new_session": {
2144
+ const wsId = state.workspaceId;
2145
+ if (!wsId) {
2146
+ send(ws, { type: "error", message: "not subscribed", command: "new_session" });
2147
+ return;
2148
+ }
2149
+ await withReplacementLock(msg.workspaceId, async () => {
2150
+ const runtime = workspaceManager.get(wsId);
2151
+ if (!runtime) {
2152
+ send(ws, { type: "error", message: "runtime gone", command: "new_session" });
2153
+ return;
2154
+ }
2155
+ if (runtime.session.isStreaming) {
2156
+ send(ws, { type: "error", message: "cannot create session while streaming", command: "new_session" });
2157
+ return;
2158
+ }
2159
+ const result = await runtime.newSession();
2160
+ if (result.cancelled) {
2161
+ send(ws, { type: "error", message: "new session cancelled", command: "new_session" });
2162
+ }
2163
+ });
2164
+ return;
2165
+ }
2166
+ case "fork": {
2167
+ const wsId = state.workspaceId;
2168
+ if (!wsId) {
2169
+ send(ws, { type: "error", message: "not subscribed", command: "fork" });
2170
+ return;
2171
+ }
2172
+ await withReplacementLock(msg.workspaceId, async () => {
2173
+ const runtime = workspaceManager.get(wsId);
2174
+ if (!runtime) {
2175
+ send(ws, { type: "error", message: "runtime gone", command: "fork" });
2176
+ return;
2177
+ }
2178
+ if (runtime.session.isStreaming) {
2179
+ send(ws, { type: "error", message: "cannot fork while streaming", command: "fork" });
2180
+ return;
2181
+ }
2182
+ const result = await runtime.fork(msg.entryId);
2183
+ if (result.cancelled) {
2184
+ send(ws, { type: "error", message: "fork cancelled", command: "fork" });
2185
+ }
2186
+ });
2187
+ return;
2188
+ }
2189
+ default: {
2190
+ const _ = msg;
2191
+ void _;
2192
+ send(ws, { type: "error", message: "unknown command" });
2193
+ }
2194
+ }
2195
+ }
2196
+ function ensureRebindListener(ws, state, workspaceId) {
2197
+ if (state.workspaceId === workspaceId && state.unsubscribeRebind) return;
2198
+ detach(state, ws);
2199
+ state.workspaceId = workspaceId;
2200
+ workspaceManager.addSubscriber(workspaceId, ws);
2201
+ state.unsubscribeRebind = workspaceManager.onSessionReplaced(workspaceId, () => {
2202
+ if (state.workspaceId !== workspaceId) return;
2203
+ bindCurrentSession(ws, state, workspaceId);
2204
+ });
2205
+ }
2206
+ function bindCurrentSession(ws, state, workspaceId) {
2207
+ const runtime = workspaceManager.get(workspaceId);
2208
+ if (!runtime) {
2209
+ send(ws, { type: "error", message: "runtime gone", command: "subscribe" });
2210
+ return;
2211
+ }
2212
+ state.unsubscribeSession?.();
2213
+ const session = runtime.session;
2214
+ const sessionPath = session.sessionFile ?? null;
2215
+ let assistantStartAt;
2216
+ let assistantFirstTokenAt;
2217
+ state.unsubscribeSession = session.subscribe((ev) => {
2218
+ const payload = translatePiEvent(ev);
2219
+ if (!payload) return;
2220
+ if (payload.kind === "message_start" && payload.role === "assistant") {
2221
+ assistantStartAt = performance.now();
2222
+ assistantFirstTokenAt = void 0;
2223
+ } else if (payload.kind === "message_update" && payload.delta.kind === "text" && assistantStartAt !== void 0 && assistantFirstTokenAt === void 0) {
2224
+ assistantFirstTokenAt = performance.now();
2225
+ }
2226
+ send(ws, {
2227
+ type: "event",
2228
+ workspaceId,
2229
+ sessionPath,
2230
+ payload
2231
+ });
2232
+ if (payload.kind === "message_end" && payload.role === "assistant" && assistantStartAt !== void 0) {
2233
+ const now = performance.now();
2234
+ const timing = {
2235
+ kind: "assistant_timing",
2236
+ firstTokenMs: assistantFirstTokenAt !== void 0 ? Math.round(assistantFirstTokenAt - assistantStartAt) : null,
2237
+ totalMs: Math.round(now - assistantStartAt)
2238
+ };
2239
+ send(ws, {
2240
+ type: "event",
2241
+ workspaceId,
2242
+ sessionPath,
2243
+ payload: timing
2244
+ });
2245
+ assistantStartAt = void 0;
2246
+ assistantFirstTokenAt = void 0;
2247
+ }
2248
+ if (payload.kind === "agent_end" || payload.kind === "compaction_end" || payload.kind === "session_info_changed" || payload.kind === "thinking_level_changed") {
2249
+ sendContextUsage(ws, runtime, workspaceId, sessionPath);
2250
+ }
2251
+ });
2252
+ send(ws, {
2253
+ type: "subscribed",
2254
+ workspaceId,
2255
+ sessionPath,
2256
+ sessionId: session.sessionId
2257
+ });
2258
+ sendContextUsage(ws, runtime, workspaceId, sessionPath);
2259
+ }
2260
+ function sendContextUsage(ws, runtime, workspaceId, sessionPath) {
2261
+ const usage = runtime.session.getContextUsage();
2262
+ if (!usage) return;
2263
+ const payload = {
2264
+ kind: "context_usage",
2265
+ tokens: usage.tokens,
2266
+ contextWindow: usage.contextWindow,
2267
+ percent: usage.percent
2268
+ };
2269
+ send(ws, {
2270
+ type: "event",
2271
+ workspaceId,
2272
+ sessionPath,
2273
+ payload
2274
+ });
2275
+ }
2276
+ function detach(state, ws) {
2277
+ state.unsubscribeSession?.();
2278
+ state.unsubscribeSession = void 0;
2279
+ state.unsubscribeRebind?.();
2280
+ state.unsubscribeRebind = void 0;
2281
+ if (state.workspaceId && ws) {
2282
+ workspaceManager.removeSubscriber(state.workspaceId, ws);
2283
+ }
2284
+ state.workspaceId = void 0;
2285
+ }
2286
+ function send(ws, msg) {
2287
+ if (ws.readyState !== ws.OPEN) return;
2288
+ ws.send(JSON.stringify(msg));
2289
+ }
2290
+
2291
+ // src/index.ts
2292
+ var app = new Hono4();
2293
+ var distDir = dirname5(fileURLToPath(import.meta.url));
2294
+ var webRoot = resolve5(process.env.PI_PILOT_WEB_ROOT ?? join8(distDir, "..", "public"));
2295
+ var webIndexPath = join8(webRoot, "index.html");
2296
+ var mimeTypes = {
2297
+ ".css": "text/css; charset=utf-8",
2298
+ ".html": "text/html; charset=utf-8",
2299
+ ".ico": "image/x-icon",
2300
+ ".js": "text/javascript; charset=utf-8",
2301
+ ".json": "application/json; charset=utf-8",
2302
+ ".map": "application/json; charset=utf-8",
2303
+ ".png": "image/png",
2304
+ ".svg": "image/svg+xml",
2305
+ ".txt": "text/plain; charset=utf-8",
2306
+ ".webp": "image/webp",
2307
+ ".woff": "font/woff",
2308
+ ".woff2": "font/woff2"
2309
+ };
2310
+ function isApiOrWsPath(pathname) {
2311
+ return pathname === "/api" || pathname.startsWith("/api/") || pathname === "/ws";
2312
+ }
2313
+ function safeResolveWebPath(pathname) {
2314
+ let decoded;
2315
+ try {
2316
+ decoded = decodeURIComponent(pathname);
2317
+ } catch {
2318
+ return void 0;
2319
+ }
2320
+ const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
2321
+ const candidate = resolve5(webRoot, relativePath);
2322
+ if (candidate !== webRoot && !candidate.startsWith(`${webRoot}${sep3}`)) {
2323
+ return void 0;
2324
+ }
2325
+ return candidate;
2326
+ }
2327
+ async function readWebFile(path) {
2328
+ try {
2329
+ return await readFile4(path);
2330
+ } catch (err) {
2331
+ const code = err.code;
2332
+ if (code === "ENOENT" || code === "EISDIR") return void 0;
2333
+ throw err;
2334
+ }
2335
+ }
2336
+ async function serveWeb(c) {
2337
+ const pathname = new URL(c.req.url).pathname;
2338
+ if (isApiOrWsPath(pathname)) return c.notFound();
2339
+ const assetPath = safeResolveWebPath(pathname);
2340
+ if (!assetPath) return c.text("invalid asset path", 400);
2341
+ const asset = await readWebFile(assetPath);
2342
+ const body = asset ?? await readFile4(webIndexPath);
2343
+ const filePath = asset ? assetPath : webIndexPath;
2344
+ const headers = {
2345
+ "Content-Type": mimeTypes[extname(filePath)] ?? "application/octet-stream",
2346
+ "Cache-Control": asset && pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache"
2347
+ };
2348
+ return new Response(body, { headers });
2349
+ }
2350
+ app.use(
2351
+ "/api/*",
2352
+ cors({
2353
+ origin: config.corsOrigin,
2354
+ allowMethods: ["GET", "POST", "PUT", "DELETE"]
2355
+ })
2356
+ );
2357
+ app.get("/api/health", (c) => c.json({ ok: true }));
2358
+ app.route("/api/workspaces", workspacesRoute);
2359
+ app.route("/api/fs", fsRoute);
2360
+ app.route("/api/model-configs", modelConfigsRoute);
2361
+ if (existsSync(webIndexPath)) {
2362
+ app.get("*", serveWeb);
2363
+ } else {
2364
+ app.get(
2365
+ "/",
2366
+ (c) => c.text(
2367
+ "pi-pilot server is running, but the web UI assets were not found. Run `pnpm build` from the repository root, or set PI_PILOT_WEB_ROOT to a built web dist directory.",
2368
+ 500
2369
+ )
2370
+ );
2371
+ }
2372
+ var server = serve(
2373
+ {
2374
+ fetch: app.fetch,
2375
+ hostname: config.host,
2376
+ port: config.port
2377
+ },
2378
+ (info) => {
2379
+ console.log(`[pi-pilot] http://${info.address}:${info.port}`);
2380
+ }
2381
+ );
2382
+ attachWsHub(server);
2383
+ async function shutdown(reason) {
2384
+ console.log(`[pi-pilot] shutting down (${reason})`);
2385
+ try {
2386
+ await workspaceManager.disposeAll();
2387
+ } catch (e) {
2388
+ console.error("[pi-pilot] disposeAll error:", e);
2389
+ }
2390
+ server.close(() => process.exit(0));
2391
+ setTimeout(() => process.exit(1), 3e3).unref();
2392
+ }
2393
+ process.on("SIGINT", () => void shutdown("SIGINT"));
2394
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
2395
+ //# sourceMappingURL=index.js.map