pi-webdesk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +111 -0
  2. package/dist/apps/daemon/src/appearance-preferences.js +218 -0
  3. package/dist/apps/daemon/src/auth.js +88 -0
  4. package/dist/apps/daemon/src/bin.js +123 -0
  5. package/dist/apps/daemon/src/cli.js +48 -0
  6. package/dist/apps/daemon/src/event-hub.js +155 -0
  7. package/dist/apps/daemon/src/index.js +102 -0
  8. package/dist/apps/daemon/src/launcher-control.js +114 -0
  9. package/dist/apps/daemon/src/launcher.js +73 -0
  10. package/dist/apps/daemon/src/pi-auth.js +290 -0
  11. package/dist/apps/daemon/src/pi-resources.js +182 -0
  12. package/dist/apps/daemon/src/pi-runtime-factory.js +19 -0
  13. package/dist/apps/daemon/src/pi-sessions.js +265 -0
  14. package/dist/apps/daemon/src/runtime-process.js +241 -0
  15. package/dist/apps/daemon/src/secret.js +71 -0
  16. package/dist/apps/daemon/src/server.js +1662 -0
  17. package/dist/apps/daemon/src/session-projection.js +117 -0
  18. package/dist/apps/daemon/src/state-lock.js +31 -0
  19. package/dist/apps/daemon/src/static-web.js +53 -0
  20. package/dist/apps/daemon/src/task-archive.js +152 -0
  21. package/dist/apps/daemon/src/task-commit.js +503 -0
  22. package/dist/apps/daemon/src/task-merge.js +912 -0
  23. package/dist/apps/daemon/src/task-review.js +204 -0
  24. package/dist/apps/daemon/src/task-runtime.js +1124 -0
  25. package/dist/apps/daemon/src/task-validation.js +352 -0
  26. package/dist/apps/daemon/src/workspace-store.js +140 -0
  27. package/dist/apps/daemon/src/workspace.js +795 -0
  28. package/dist/extensions/webdesk.js +34 -0
  29. package/dist/packages/git/src/commit.js +675 -0
  30. package/dist/packages/git/src/errors.js +55 -0
  31. package/dist/packages/git/src/fingerprint.js +286 -0
  32. package/dist/packages/git/src/index.js +123 -0
  33. package/dist/packages/git/src/merge.js +1008 -0
  34. package/dist/packages/git/src/paths.js +58 -0
  35. package/dist/packages/git/src/repository.js +77 -0
  36. package/dist/packages/git/src/review.js +396 -0
  37. package/dist/packages/git/src/runner.js +110 -0
  38. package/dist/packages/git/src/validation.js +263 -0
  39. package/dist/packages/git/src/worktree.js +233 -0
  40. package/dist/packages/pi-bridge/extensions/pita-policy.js +117 -0
  41. package/dist/packages/pi-bridge/src/auth.js +80 -0
  42. package/dist/packages/pi-bridge/src/errors.js +19 -0
  43. package/dist/packages/pi-bridge/src/handshake.js +43 -0
  44. package/dist/packages/pi-bridge/src/index.js +76 -0
  45. package/dist/packages/pi-bridge/src/jsonl.js +105 -0
  46. package/dist/packages/pi-bridge/src/policy-approval.js +62 -0
  47. package/dist/packages/pi-bridge/src/resolve.js +59 -0
  48. package/dist/packages/pi-bridge/src/resources-child.mjs +23 -0
  49. package/dist/packages/pi-bridge/src/resources.js +481 -0
  50. package/dist/packages/pi-bridge/src/rpc/client.js +480 -0
  51. package/dist/packages/pi-bridge/src/rpc/runtime.js +496 -0
  52. package/dist/packages/pi-bridge/src/rpc/supervisor.mjs +129 -0
  53. package/dist/packages/pi-bridge/src/rpc/tool-events.js +78 -0
  54. package/dist/packages/pi-bridge/src/rpc/wire.js +263 -0
  55. package/dist/packages/pi-bridge/src/runtime.js +0 -0
  56. package/dist/packages/pi-bridge/src/sessions-child.mjs +38 -0
  57. package/dist/packages/pi-bridge/src/sessions.js +314 -0
  58. package/dist/packages/pi-bridge/src/tool-activity.js +56 -0
  59. package/dist/packages/protocol/src/index.js +1863 -0
  60. package/dist/web/assets/index-BOw_fhvO.css +2 -0
  61. package/dist/web/assets/index-oXs7yAAo.js +119 -0
  62. package/dist/web/index.html +14 -0
  63. package/package.json +69 -0
  64. package/scripts/prepare.mjs +7 -0
@@ -0,0 +1,496 @@
1
+ // packages/pi-bridge/src/rpc/runtime.ts
2
+ import { fileURLToPath } from "node:url";
3
+ import { extractToolCalls } from "../tool-activity.js";
4
+ import { extractToolResult, ToolEventProjection } from "./tool-events.js";
5
+ import path from "node:path";
6
+ import { PiBridgeError } from "../errors.js";
7
+ import { decodePolicyApproval } from "../policy-approval.js";
8
+ import {
9
+ PITA_POLICY_STATUS_KEY,
10
+ PITA_POLICY_VERSION,
11
+ decodePolicyStatus,
12
+ isSupportedPolicyVersion
13
+ } from "../handshake.js";
14
+ import { PiRpcClient } from "./client.js";
15
+ import {
16
+ extractEntryText,
17
+ getAvailableModelsDataSchema,
18
+ getEntriesDataSchema,
19
+ getStateDataSchema,
20
+ getTreeDataSchema,
21
+ rpcExtensionErrorEventSchema,
22
+ wireModelSchema
23
+ } from "./wire.js";
24
+ var DEFAULT_POLICY_EXTENSION_PATH = fileURLToPath(
25
+ new URL(import.meta.url.endsWith(".ts") ? "../../extensions/pita-policy.ts" : "../../extensions/pita-policy.js", import.meta.url)
26
+ );
27
+ var DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
28
+ function createPiRpcRuntime(options) {
29
+ return new PiRpcRuntime(options);
30
+ }
31
+ var PiRpcRuntime = class {
32
+ #options;
33
+ #mode;
34
+ #client;
35
+ #listeners = /* @__PURE__ */ new Set();
36
+ #toolEvents = new ToolEventProjection();
37
+ #state = "created";
38
+ #startPromise = null;
39
+ #handshakeSettle = null;
40
+ constructor(options) {
41
+ if (options.sessionFile !== void 0) {
42
+ if ((options.sessionMode ?? "persistent") === "ephemeral") {
43
+ throw new PiBridgeError(
44
+ "PI_INVALID_CONFIGURATION",
45
+ "A Pi session file cannot be resumed in ephemeral mode."
46
+ );
47
+ }
48
+ if (!path.isAbsolute(options.sessionFile)) {
49
+ throw new PiBridgeError(
50
+ "PI_INVALID_CONFIGURATION",
51
+ "The Pi session file must be an absolute path."
52
+ );
53
+ }
54
+ }
55
+ this.#options = options;
56
+ this.#mode = options.mode ?? "supervised";
57
+ this.#client = new PiRpcClient({
58
+ executable: options.executable,
59
+ args: this.#buildArgs(),
60
+ cwd: options.cwd,
61
+ env: {
62
+ ...options.env ?? process.env,
63
+ PITA_RUNTIME_MODE: this.#mode
64
+ },
65
+ ...options.spawnProcess !== void 0 ? { spawnProcess: options.spawnProcess } : {},
66
+ ...options.requestTimeoutMs !== void 0 ? { requestTimeoutMs: options.requestTimeoutMs } : {},
67
+ ...options.maxRecordBytes !== void 0 ? { maxRecordBytes: options.maxRecordBytes } : {},
68
+ ...options.supervisorIgnoreEofForTests === true ? { supervisorIgnoreEofForTests: true } : {},
69
+ handlers: {
70
+ onEvent: (record) => this.#handleEvent(record),
71
+ onExtensionUiRequest: (record) => this.#handleExtensionUiRequest(record),
72
+ onProtocolIssue: (issue) => {
73
+ if (issue.fatal && this.#state !== "exited") this.#setState("failed");
74
+ this.#emit({ kind: "protocol-issue", ...issue });
75
+ },
76
+ onExit: (info) => this.#handleExit(info)
77
+ }
78
+ });
79
+ }
80
+ get lifecycleState() {
81
+ return this.#state;
82
+ }
83
+ start(options = {}) {
84
+ if (this.#startPromise !== null) return this.#startPromise;
85
+ this.#startPromise = this.#start(options);
86
+ return this.#startPromise;
87
+ }
88
+ async #start(options) {
89
+ this.#setState("starting");
90
+ const timeoutMs = this.#options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
91
+ const handshakePromise = new Promise((resolve, reject) => {
92
+ this.#handshakeSettle = { resolve, reject };
93
+ });
94
+ void handshakePromise.catch(() => void 0);
95
+ const timer = setTimeout(() => {
96
+ this.#failStartup(
97
+ new PiBridgeError(
98
+ "PI_STARTUP_TIMEOUT",
99
+ `Did not receive the pita-policy handshake within ${timeoutMs}ms. Ensure the Webdesk policy extension is loaded with -e and that this Pi version emits extension_ui_request setStatus records in RPC mode.`,
100
+ { details: { timeoutMs } }
101
+ )
102
+ );
103
+ }, timeoutMs);
104
+ timer.unref?.();
105
+ try {
106
+ await this.#client.start();
107
+ const pid = this.#client.processId;
108
+ if (pid === null) {
109
+ throw new PiBridgeError(
110
+ "PI_SPAWN_FAILED",
111
+ "Pi spawned without a valid operating-system process id"
112
+ );
113
+ }
114
+ await options.onProcessStarted?.(pid);
115
+ this.#client.activateSupervisor();
116
+ const handshake = await handshakePromise;
117
+ this.#setState("ready");
118
+ return handshake;
119
+ } catch (error) {
120
+ if (this.#state !== "exited") this.#setState("failed");
121
+ try {
122
+ this.#client.kill("SIGKILL");
123
+ } catch {
124
+ }
125
+ throw error;
126
+ } finally {
127
+ clearTimeout(timer);
128
+ this.#handshakeSettle = null;
129
+ }
130
+ }
131
+ async prompt(message, options) {
132
+ this.#requireReady("prompt");
133
+ await this.#client.request({
134
+ type: "prompt",
135
+ message,
136
+ ...options?.streamingBehavior !== void 0 ? { streamingBehavior: options.streamingBehavior } : {}
137
+ });
138
+ }
139
+ async steer(message) {
140
+ this.#requireReady("steer");
141
+ await this.#client.request({ type: "steer", message });
142
+ }
143
+ async followUp(message) {
144
+ this.#requireReady("follow_up");
145
+ await this.#client.request({ type: "follow_up", message });
146
+ }
147
+ async abort() {
148
+ this.#requireReady("abort");
149
+ await this.#client.request({ type: "abort" });
150
+ }
151
+ async getState() {
152
+ this.#requireReady("get_state");
153
+ const response = await this.#client.request({ type: "get_state" });
154
+ const data = this.#parseData(getStateDataSchema, response.data, "get_state");
155
+ return {
156
+ model: data.model === null || data.model === void 0 ? null : { id: data.model.id, provider: data.model.provider, name: data.model.name },
157
+ isStreaming: data.isStreaming,
158
+ messageCount: data.messageCount,
159
+ sessionFile: data.sessionFile ?? null,
160
+ sessionId: data.sessionId ?? null,
161
+ sessionName: data.sessionName ?? null
162
+ };
163
+ }
164
+ async getAvailableModels() {
165
+ this.#requireReady("get_available_models");
166
+ const response = await this.#client.request({ type: "get_available_models" });
167
+ const data = this.#parseData(
168
+ getAvailableModelsDataSchema,
169
+ response.data,
170
+ "get_available_models"
171
+ );
172
+ return data.models.map(mapModel);
173
+ }
174
+ async setModel(provider, modelId) {
175
+ this.#requireReady("set_model");
176
+ const response = await this.#client.request({
177
+ type: "set_model",
178
+ provider,
179
+ modelId
180
+ });
181
+ const model = this.#parseData(wireModelSchema, response.data, "set_model");
182
+ return mapModel(model);
183
+ }
184
+ async getEntries(since) {
185
+ this.#requireReady("get_entries");
186
+ const response = await this.#client.request({
187
+ type: "get_entries",
188
+ ...since !== void 0 ? { since } : {}
189
+ });
190
+ const data = this.#parseData(getEntriesDataSchema, response.data, "get_entries");
191
+ return {
192
+ entries: data.entries.map((entry) => mapEntrySummary(entry)),
193
+ leafId: data.leafId
194
+ };
195
+ }
196
+ async getTree() {
197
+ this.#requireReady("get_tree");
198
+ const response = await this.#client.request({ type: "get_tree" });
199
+ const data = this.#parseData(getTreeDataSchema, response.data, "get_tree");
200
+ return {
201
+ roots: data.tree.map((node) => mapTreeNode(node)),
202
+ leafId: data.leafId,
203
+ truncated: data.truncated
204
+ };
205
+ }
206
+ subscribe(listener) {
207
+ this.#listeners.add(listener);
208
+ return () => this.#listeners.delete(listener);
209
+ }
210
+ respondToDialog(reply) {
211
+ switch (reply.kind) {
212
+ case "confirm":
213
+ this.#client.sendExtensionUiResponse({
214
+ type: "extension_ui_response",
215
+ id: reply.requestId,
216
+ confirmed: reply.confirmed
217
+ });
218
+ return;
219
+ case "value":
220
+ this.#client.sendExtensionUiResponse({
221
+ type: "extension_ui_response",
222
+ id: reply.requestId,
223
+ value: reply.value
224
+ });
225
+ return;
226
+ case "cancel":
227
+ this.#client.sendExtensionUiResponse({
228
+ type: "extension_ui_response",
229
+ id: reply.requestId,
230
+ cancelled: true
231
+ });
232
+ return;
233
+ }
234
+ }
235
+ async dispose() {
236
+ if (this.#state === "created") {
237
+ this.#setState("exited");
238
+ return { code: null, signal: null };
239
+ }
240
+ const info = await this.#client.stop();
241
+ return info;
242
+ }
243
+ #buildArgs() {
244
+ return [
245
+ ...this.#options.prependArgs ?? [],
246
+ "--mode",
247
+ "rpc",
248
+ ...(this.#options.sessionMode ?? "persistent") === "ephemeral" ? ["--no-session"] : this.#options.sessionFile === void 0 ? [] : ["--session", path.normalize(this.#options.sessionFile)],
249
+ "-e",
250
+ this.#options.policyExtensionPath ?? DEFAULT_POLICY_EXTENSION_PATH,
251
+ ...this.#options.extraArgs ?? []
252
+ ];
253
+ }
254
+ #requireReady(operation) {
255
+ if (this.#state !== "ready") {
256
+ throw new PiBridgeError(
257
+ "PI_RUNTIME_NOT_READY",
258
+ `Cannot ${operation}: runtime is ${this.#state} (requires ready)`,
259
+ { details: { state: this.#state } }
260
+ );
261
+ }
262
+ }
263
+ #parseData(schema, value, commandType) {
264
+ const result = schema.safeParse(value);
265
+ if (!result.success) {
266
+ throw new PiBridgeError(
267
+ "PI_PROTOCOL_VIOLATION",
268
+ `Pi returned malformed ${commandType} data: ${result.error.message}`,
269
+ { details: { commandType } }
270
+ );
271
+ }
272
+ return result.data;
273
+ }
274
+ #handleExtensionUiRequest(record) {
275
+ if (record.method === "setStatus" && record.statusKey === PITA_POLICY_STATUS_KEY) {
276
+ this.#handlePolicyStatus(record);
277
+ return;
278
+ }
279
+ const request = mapDialogRequest(record);
280
+ if (request !== null) {
281
+ this.#emit({ kind: "dialog-requested", request });
282
+ return;
283
+ }
284
+ if (record.method === "notify") {
285
+ this.#emit({
286
+ kind: "notification",
287
+ message: record.message ?? "",
288
+ level: record.notifyType ?? "info"
289
+ });
290
+ }
291
+ }
292
+ #handlePolicyStatus(record) {
293
+ const decoded = decodePolicyStatus(record.statusText);
294
+ if (!decoded.ok) {
295
+ this.#failStartup(
296
+ new PiBridgeError(
297
+ "PI_POLICY_HANDSHAKE_INCOMPATIBLE",
298
+ `Webdesk policy handshake is invalid: ${decoded.reason}`
299
+ )
300
+ );
301
+ return;
302
+ }
303
+ if (!isSupportedPolicyVersion(decoded.status.version)) {
304
+ this.#failStartup(
305
+ new PiBridgeError(
306
+ "PI_POLICY_HANDSHAKE_INCOMPATIBLE",
307
+ `Webdesk policy extension announced version ${decoded.status.version}, but this bridge supports version ${PITA_POLICY_VERSION}`,
308
+ { details: { announced: decoded.status.version, supported: PITA_POLICY_VERSION } }
309
+ )
310
+ );
311
+ return;
312
+ }
313
+ if (decoded.status.mode !== this.#mode) {
314
+ this.#failStartup(
315
+ new PiBridgeError(
316
+ "PI_POLICY_HANDSHAKE_INCOMPATIBLE",
317
+ `Webdesk policy extension announced ${decoded.status.mode} mode, but this runtime requires ${this.#mode} mode`,
318
+ { details: { announced: decoded.status.mode, required: this.#mode } }
319
+ )
320
+ );
321
+ return;
322
+ }
323
+ this.#emit({ kind: "policy-status", status: decoded.status });
324
+ this.#handshakeSettle?.resolve({
325
+ version: decoded.status.version,
326
+ mode: decoded.status.mode
327
+ });
328
+ }
329
+ #handleEvent(record) {
330
+ for (const tool of this.#toolEvents.accept(record)) this.#emit({ kind: "tool-activity", tool });
331
+ if (record.type === "extension_error") {
332
+ const parsed = rpcExtensionErrorEventSchema.safeParse(record);
333
+ const message = parsed.success ? parsed.data.error ?? "unknown extension error" : "unknown extension error";
334
+ const extensionPath = parsed.success ? parsed.data.extensionPath ?? null : null;
335
+ if (this.#handshakeSettle !== null) {
336
+ this.#failStartup(
337
+ new PiBridgeError(
338
+ "PI_STARTUP_FAILED",
339
+ `Extension error during startup${extensionPath === null ? "" : ` (${extensionPath})`}: ${message}`
340
+ )
341
+ );
342
+ return;
343
+ }
344
+ this.#emit({ kind: "extension-error", message, extensionPath });
345
+ return;
346
+ }
347
+ switch (record.type) {
348
+ case "agent_start":
349
+ this.#emit({ kind: "agent-activity", phase: "agent-start" });
350
+ return;
351
+ case "agent_end":
352
+ this.#emit({ kind: "agent-activity", phase: "agent-end" });
353
+ return;
354
+ case "agent_settled":
355
+ this.#emit({ kind: "agent-activity", phase: "agent-settled" });
356
+ return;
357
+ case "turn_start":
358
+ this.#emit({ kind: "agent-activity", phase: "turn-start" });
359
+ return;
360
+ case "turn_end":
361
+ this.#emit({ kind: "agent-activity", phase: "turn-end" });
362
+ return;
363
+ case "message_update": {
364
+ const delta = extractTextDelta(record);
365
+ if (delta !== null) this.#emit({ kind: "assistant-delta", text: delta });
366
+ return;
367
+ }
368
+ default:
369
+ return;
370
+ }
371
+ }
372
+ #handleExit(info) {
373
+ const exitedDuringStartup = this.#state === "starting";
374
+ if (this.#handshakeSettle !== null) {
375
+ const stderr = this.#client.stderrTail.trim();
376
+ this.#failStartup(
377
+ new PiBridgeError(
378
+ "PI_STARTUP_FAILED",
379
+ `Pi exited during startup (code ${info.code}, signal ${info.signal})` + (stderr === "" ? "" : `; stderr tail:
380
+ ${stderr}`),
381
+ { details: { code: info.code, signal: info.signal } }
382
+ )
383
+ );
384
+ }
385
+ if (exitedDuringStartup) {
386
+ this.#setState("failed");
387
+ } else if (this.#state !== "failed") {
388
+ this.#setState("exited");
389
+ }
390
+ this.#emit({ kind: "exited", code: info.code, signal: info.signal });
391
+ }
392
+ #failStartup(error) {
393
+ const settle = this.#handshakeSettle;
394
+ this.#handshakeSettle = null;
395
+ settle?.reject(error);
396
+ }
397
+ #setState(state) {
398
+ if (this.#state === state) return;
399
+ this.#state = state;
400
+ this.#emit({ kind: "lifecycle", state });
401
+ }
402
+ #emit(event) {
403
+ for (const listener of this.#listeners) listener(event);
404
+ }
405
+ };
406
+ function mapModel(model) {
407
+ return {
408
+ id: model.id,
409
+ ...model.provider === void 0 ? {} : { provider: model.provider },
410
+ ...model.name === void 0 ? {} : { name: model.name }
411
+ };
412
+ }
413
+ function mapDialogRequest(record) {
414
+ switch (record.method) {
415
+ case "confirm": {
416
+ const approval = decodePolicyApproval(record.title, record.message);
417
+ if (approval !== null) {
418
+ const transportTimeoutMs = Number.isSafeInteger(record.timeout) && (record.timeout ?? 0) > 0 ? record.timeout : approval.timeoutMs;
419
+ return {
420
+ requestId: record.id,
421
+ kind: "approval",
422
+ toolCallId: approval.toolCallId,
423
+ toolName: approval.toolName,
424
+ summary: approval.summary,
425
+ requestedAtMs: approval.requestedAtMs,
426
+ timeoutMs: Math.min(approval.timeoutMs, transportTimeoutMs)
427
+ };
428
+ }
429
+ return {
430
+ requestId: record.id,
431
+ kind: "confirm",
432
+ title: record.title ?? "",
433
+ message: record.message ?? "",
434
+ timeoutMs: record.timeout
435
+ };
436
+ }
437
+ case "select":
438
+ return {
439
+ requestId: record.id,
440
+ kind: "select",
441
+ title: record.title ?? "",
442
+ options: record.options ?? [],
443
+ timeoutMs: record.timeout
444
+ };
445
+ case "input":
446
+ return {
447
+ requestId: record.id,
448
+ kind: "input",
449
+ title: record.title ?? "",
450
+ placeholder: record.placeholder,
451
+ timeoutMs: record.timeout
452
+ };
453
+ case "editor":
454
+ return {
455
+ requestId: record.id,
456
+ kind: "editor",
457
+ title: record.title ?? "",
458
+ prefill: record.prefill
459
+ };
460
+ default:
461
+ return null;
462
+ }
463
+ }
464
+ function mapEntrySummary(entry) {
465
+ const text = entry.message === void 0 ? null : extractEntryText(entry.message.content);
466
+ const toolCalls = entry.message?.role === "assistant" ? extractToolCalls(entry.message.content) : [];
467
+ const toolResult = extractToolResult(entry.message);
468
+ return {
469
+ id: entry.id,
470
+ parentId: entry.parentId ?? null,
471
+ type: entry.type,
472
+ ...entry.message === void 0 ? {} : { role: entry.message.role },
473
+ ...text === null ? {} : { text },
474
+ ...toolCalls.length === 0 ? {} : { toolCalls },
475
+ ...toolResult === void 0 ? {} : { toolResult }
476
+ };
477
+ }
478
+ function mapTreeNode(node) {
479
+ return {
480
+ entry: mapEntrySummary(node.entry),
481
+ children: node.children.map((child) => mapTreeNode(child)),
482
+ label: node.label
483
+ };
484
+ }
485
+ function extractTextDelta(record) {
486
+ const event = record["assistantMessageEvent"];
487
+ if (typeof event !== "object" || event === null) return null;
488
+ const delta = event;
489
+ if (delta["type"] !== "text_delta") return null;
490
+ return typeof delta["delta"] === "string" ? delta["delta"] : null;
491
+ }
492
+ export {
493
+ DEFAULT_POLICY_EXTENSION_PATH,
494
+ PiRpcRuntime,
495
+ createPiRpcRuntime
496
+ };
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/pi-bridge/src/rpc/supervisor.mjs
4
+ import { execFileSync, spawn } from "node:child_process";
5
+ import { constants as osConstants } from "node:os";
6
+ var ACTIVATION_LINE = "PITA_ACTIVATE\n";
7
+ var MAX_GATE_BYTES = 128;
8
+ var SHUTDOWN_GRACE_MS = 3e3;
9
+ var executable = process.env.PITA_SUPERVISOR_EXECUTABLE;
10
+ var encodedArgs = process.env.PITA_SUPERVISOR_ARGS;
11
+ var launchId = process.argv[2] === "--launch-id" ? process.argv[3] : void 0;
12
+ if (executable === void 0 || encodedArgs === void 0 || launchId === void 0 || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
13
+ launchId
14
+ )) {
15
+ process.exit(70);
16
+ }
17
+ var args;
18
+ try {
19
+ const parsed = JSON.parse(encodedArgs);
20
+ if (!Array.isArray(parsed) || !parsed.every((value) => typeof value === "string")) {
21
+ throw new Error("invalid args");
22
+ }
23
+ args = parsed;
24
+ } catch {
25
+ process.exit(70);
26
+ }
27
+ var child = null;
28
+ var gate = Buffer.alloc(0);
29
+ var shuttingDown = false;
30
+ var gracefulTimer = null;
31
+ var exitCode = 0;
32
+ function groupMembers() {
33
+ try {
34
+ const output = execFileSync("ps", ["-axo", "pid=,pgid=,stat="], {
35
+ encoding: "utf8",
36
+ env: { ...process.env, LC_ALL: "C", LANG: "C" },
37
+ timeout: 1e3,
38
+ maxBuffer: 1024 * 1024,
39
+ // Otherwise the ps probe inherits this supervisor's group and observes
40
+ // itself as the final live member forever.
41
+ detached: true
42
+ });
43
+ return output.split("\n").map((line) => line.trim().split(/\s+/)).filter(
44
+ ([pid, pgid, stat]) => Number(pid) !== process.pid && Number(pgid) === process.pid && stat !== void 0 && !stat.startsWith("Z")
45
+ ).map(([pid]) => Number(pid)).filter((pid) => Number.isSafeInteger(pid) && pid > 0);
46
+ } catch {
47
+ return [1];
48
+ }
49
+ }
50
+ function finishWhenGroupIsEmpty(deadline) {
51
+ if (groupMembers().length === 0) process.exit(exitCode);
52
+ if (Date.now() >= deadline) {
53
+ try {
54
+ process.kill(-process.pid, "SIGKILL");
55
+ } catch {
56
+ process.exit(1);
57
+ }
58
+ return;
59
+ }
60
+ setTimeout(() => finishWhenGroupIsEmpty(deadline), 25);
61
+ }
62
+ function shutdown(code = exitCode) {
63
+ if (shuttingDown) return;
64
+ shuttingDown = true;
65
+ exitCode = code;
66
+ if (gracefulTimer !== null) clearTimeout(gracefulTimer);
67
+ gracefulTimer = null;
68
+ if (child !== null) process.stdin.unpipe(child.stdin);
69
+ try {
70
+ process.kill(-process.pid, "SIGTERM");
71
+ } catch (error) {
72
+ if (error?.code !== "ESRCH") exitCode = 1;
73
+ }
74
+ finishWhenGroupIsEmpty(Date.now() + SHUTDOWN_GRACE_MS);
75
+ }
76
+ function beginGracefulShutdown() {
77
+ if (shuttingDown || gracefulTimer !== null) return;
78
+ if (child === null) process.exit(0);
79
+ process.stdin.unpipe(child.stdin);
80
+ child.stdin.end();
81
+ gracefulTimer = setTimeout(() => shutdown(), SHUTDOWN_GRACE_MS);
82
+ }
83
+ function childResult(code, signal) {
84
+ if (code !== null) return code;
85
+ const signalNumber = signal === null ? void 0 : osConstants.signals[signal];
86
+ return signalNumber === void 0 ? 1 : 128 + signalNumber;
87
+ }
88
+ process.on("SIGTERM", () => shutdown());
89
+ process.on("SIGINT", () => shutdown());
90
+ process.stdout.on("error", () => shutdown(1));
91
+ process.stderr.on("error", () => shutdown(1));
92
+ process.on("uncaughtException", () => shutdown(1));
93
+ function activate(remainder) {
94
+ const childEnv = { ...process.env };
95
+ delete childEnv.PITA_SUPERVISOR_EXECUTABLE;
96
+ delete childEnv.PITA_SUPERVISOR_ARGS;
97
+ delete childEnv.PITA_SUPERVISOR_IGNORE_EOF;
98
+ child = spawn(executable, args, {
99
+ cwd: process.cwd(),
100
+ env: childEnv,
101
+ shell: false,
102
+ detached: false,
103
+ stdio: ["pipe", "pipe", "pipe"]
104
+ });
105
+ child.stdout?.pipe(process.stdout);
106
+ child.stderr?.pipe(process.stderr);
107
+ child.stdin?.on("error", () => shutdown(72));
108
+ if (remainder.length > 0) child.stdin?.write(remainder);
109
+ process.stdin.pipe(child.stdin);
110
+ child.on("error", () => shutdown(71));
111
+ child.on("exit", (code, signal) => shutdown(childResult(code, signal)));
112
+ }
113
+ function onGateData(chunk) {
114
+ gate = Buffer.concat([gate, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
115
+ if (gate.length > MAX_GATE_BYTES) process.exit(70);
116
+ const newline = gate.indexOf(10);
117
+ if (newline < 0) return;
118
+ process.stdin.off("data", onGateData);
119
+ const line = gate.subarray(0, newline + 1).toString("utf8");
120
+ if (line !== ACTIVATION_LINE) process.exit(70);
121
+ activate(gate.subarray(newline + 1));
122
+ gate = Buffer.alloc(0);
123
+ }
124
+ process.stdin.on("data", onGateData);
125
+ process.stdin.on("end", () => {
126
+ if (child === null) process.exit(0);
127
+ if (process.env.PITA_SUPERVISOR_IGNORE_EOF !== "1") beginGracefulShutdown();
128
+ });
129
+ process.stdin.resume();
@@ -0,0 +1,78 @@
1
+ // packages/pi-bridge/src/rpc/tool-events.ts
2
+ import { z } from "zod";
3
+ import { boundedToolText, extractToolCalls, toolCallProjection, toolResultText } from "../tool-activity.js";
4
+ var resultMessageSchema = z.object({
5
+ role: z.literal("toolResult"),
6
+ toolCallId: z.string().min(1).max(500),
7
+ toolName: z.string().min(1).max(200),
8
+ content: z.unknown(),
9
+ isError: z.boolean()
10
+ });
11
+ function extractToolResult(message) {
12
+ const parsed = resultMessageSchema.safeParse(message);
13
+ if (!parsed.success) return void 0;
14
+ const output = toolResultText(parsed.data.content);
15
+ return {
16
+ toolCallId: parsed.data.toolCallId,
17
+ name: boundedToolText(parsed.data.toolName, 200).text,
18
+ output: output.text,
19
+ omittedChars: output.omitted,
20
+ isError: parsed.data.isError
21
+ };
22
+ }
23
+ var executionSchema = z.object({
24
+ type: z.enum(["tool_execution_start", "tool_execution_update", "tool_execution_end"]),
25
+ toolCallId: z.string().min(1).max(500),
26
+ toolName: z.string().min(1).max(200),
27
+ args: z.unknown().optional(),
28
+ partialResult: z.object({ content: z.unknown() }).optional(),
29
+ result: z.object({ content: z.unknown() }).optional(),
30
+ isError: z.boolean().optional()
31
+ });
32
+ var ToolEventProjection = class {
33
+ #pending = /* @__PURE__ */ new Map();
34
+ accept(record) {
35
+ if (record.type === "message_end") {
36
+ const message = z.object({ role: z.string(), content: z.unknown() }).safeParse(record.message);
37
+ if (message.success && message.data.role === "assistant") {
38
+ const calls = extractToolCalls(message.data.content);
39
+ for (const call3 of calls) this.#remember(call3);
40
+ return calls;
41
+ }
42
+ const result = extractToolResult(record.message);
43
+ const call2 = result && this.#pending.get(result.toolCallId);
44
+ if (call2 && result) {
45
+ this.#pending.delete(call2.toolCallId);
46
+ return [{ ...call2, status: result.isError ? "failed" : "succeeded", output: result.output, outputOmittedChars: result.omittedChars }];
47
+ }
48
+ return [];
49
+ }
50
+ if (record.type === "agent_end" || record.type === "agent_settled") {
51
+ const interrupted = [...this.#pending.values()].map((call2) => ({ ...call2, status: "incomplete" }));
52
+ this.#pending.clear();
53
+ return interrupted;
54
+ }
55
+ const parsed = executionSchema.safeParse(record);
56
+ if (!parsed.success) return [];
57
+ const event = parsed.data;
58
+ if (event.type === "tool_execution_end" && (event.result === void 0 || event.isError === void 0)) return [];
59
+ const call = this.#pending.get(event.toolCallId) ?? toolCallProjection(event.toolCallId, event.toolName, event.args);
60
+ const output = toolResultText((event.result ?? event.partialResult)?.content);
61
+ const next = {
62
+ ...call,
63
+ status: event.type === "tool_execution_end" ? event.isError ? "failed" : "succeeded" : "running",
64
+ ...event.result || event.partialResult ? { output: output.text, outputOmittedChars: output.omitted } : {}
65
+ };
66
+ if (event.type === "tool_execution_end") this.#pending.delete(call.toolCallId);
67
+ else this.#remember(next);
68
+ return [next];
69
+ }
70
+ #remember(call) {
71
+ this.#pending.set(call.toolCallId, call);
72
+ if (this.#pending.size > 200) this.#pending.delete(this.#pending.keys().next().value);
73
+ }
74
+ };
75
+ export {
76
+ ToolEventProjection,
77
+ extractToolResult
78
+ };