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,480 @@
1
+ // packages/pi-bridge/src/rpc/client.ts
2
+ import { execFile, spawn as nodeSpawn } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { accessSync, constants, statSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { promisify } from "node:util";
8
+ import { PiBridgeError } from "../errors.js";
9
+ import { JsonlDecoder } from "../jsonl.js";
10
+ import {
11
+ classifyRpcRecord
12
+ } from "./wire.js";
13
+ var execFileAsync = promisify(execFile);
14
+ var SUPERVISOR_PATH = fileURLToPath(new URL("./supervisor.mjs", import.meta.url));
15
+ var defaultSpawn = (executable, args, options) => {
16
+ const supervisorEnv = {
17
+ ...options.env,
18
+ PITA_SUPERVISOR_EXECUTABLE: executable,
19
+ PITA_SUPERVISOR_ARGS: JSON.stringify(args)
20
+ };
21
+ delete supervisorEnv.PITA_SUPERVISOR_IGNORE_EOF;
22
+ if (options.supervisorIgnoreEofForTests === true) {
23
+ supervisorEnv.PITA_SUPERVISOR_IGNORE_EOF = "1";
24
+ }
25
+ return nodeSpawn(process.execPath, [SUPERVISOR_PATH, "--launch-id", randomUUID()], {
26
+ cwd: options.cwd,
27
+ env: supervisorEnv,
28
+ shell: false,
29
+ // A dedicated POSIX process group lets Webdesk terminate Pi and any tool
30
+ // descendants together during normal shutdown and crash recovery.
31
+ detached: process.platform !== "win32",
32
+ stdio: ["pipe", "pipe", "pipe"]
33
+ });
34
+ };
35
+ var SUPERVISOR_ACTIVATION_LINE = "PITA_ACTIVATE\n";
36
+ function executableFile(candidate) {
37
+ try {
38
+ if (!statSync(candidate).isFile()) return false;
39
+ accessSync(candidate, constants.X_OK);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ function resolveExecutable(executable, cwd, env) {
46
+ if (executable.includes("/")) {
47
+ const candidate = path.isAbsolute(executable) ? executable : path.resolve(cwd, executable);
48
+ return executableFile(candidate) ? candidate : null;
49
+ }
50
+ for (const directory of (env.PATH ?? "").split(path.delimiter)) {
51
+ const base = directory === "" ? cwd : path.isAbsolute(directory) ? directory : path.resolve(cwd, directory);
52
+ const candidate = path.join(base, executable);
53
+ if (executableFile(candidate)) return candidate;
54
+ }
55
+ return null;
56
+ }
57
+ var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
58
+ var STDERR_TAIL_LIMIT = 8 * 1024;
59
+ var PiRpcClient = class {
60
+ #options;
61
+ #handlers;
62
+ #pending = /* @__PURE__ */ new Map();
63
+ #decoder;
64
+ #child = null;
65
+ #observedExitInfo = null;
66
+ #exitInfo = null;
67
+ #processExitWaiters = [];
68
+ #exitWaiters = [];
69
+ #streamFailed = false;
70
+ #fatalError = null;
71
+ #stderrTail = "";
72
+ #nextRequestId = 0;
73
+ #ownsProcessGroup;
74
+ constructor(options) {
75
+ this.#options = options;
76
+ this.#handlers = options.handlers ?? {};
77
+ this.#ownsProcessGroup = options.spawnProcess === void 0 && process.platform !== "win32";
78
+ this.#decoder = new JsonlDecoder({
79
+ ...options.maxRecordBytes !== void 0 ? { maxRecordBytes: options.maxRecordBytes } : {}
80
+ });
81
+ }
82
+ get exitInfo() {
83
+ return this.#exitInfo;
84
+ }
85
+ get processId() {
86
+ const pid = this.#child?.pid;
87
+ return pid !== void 0 && Number.isSafeInteger(pid) && pid > 0 ? pid : null;
88
+ }
89
+ /** Spawns Pi. Resolves once the OS process exists; rejects with an
90
+ * actionable typed error when the executable is missing. */
91
+ async start() {
92
+ if (this.#child !== null) {
93
+ throw new PiBridgeError("PI_PROTOCOL_VIOLATION", "PiRpcClient.start() called twice");
94
+ }
95
+ const spawnProcess = this.#options.spawnProcess ?? defaultSpawn;
96
+ const spawnEnv = this.#options.env ?? process.env;
97
+ const executable = spawnProcess === defaultSpawn ? resolveExecutable(this.#options.executable, this.#options.cwd, spawnEnv) : this.#options.executable;
98
+ if (executable === null) {
99
+ throw new PiBridgeError(
100
+ "PI_EXECUTABLE_NOT_FOUND",
101
+ `Pi executable not found: ${this.#options.executable}. Set PITA_PI_EXECUTABLE to an existing pi binary.`,
102
+ { details: { executable: this.#options.executable } }
103
+ );
104
+ }
105
+ const child = spawnProcess(executable, this.#options.args, {
106
+ cwd: this.#options.cwd,
107
+ env: spawnEnv,
108
+ ...this.#options.supervisorIgnoreEofForTests === true ? { supervisorIgnoreEofForTests: true } : {}
109
+ });
110
+ this.#child = child;
111
+ child.stdout?.on("data", (chunk) => this.#handleStdout(chunk));
112
+ child.stdout?.on("end", () => this.#handleStdoutEnd());
113
+ child.stderr?.on("data", (chunk) => {
114
+ this.#stderrTail = (this.#stderrTail + String(chunk)).slice(-STDERR_TAIL_LIMIT);
115
+ });
116
+ child.stdin?.on("error", (error) => this.#failTransport(error));
117
+ child.on("exit", (code, signal) => {
118
+ this.#handleProcessExit({ code, signal });
119
+ });
120
+ child.on("close", (code, signal) => {
121
+ const info = this.#observedExitInfo ?? { code, signal };
122
+ this.#handleProcessExit(info);
123
+ this.#handleClose(info);
124
+ });
125
+ await new Promise((resolve, reject) => {
126
+ let settled = false;
127
+ child.on("spawn", () => {
128
+ if (settled) return;
129
+ settled = true;
130
+ resolve();
131
+ });
132
+ child.on("error", (error) => {
133
+ if (!settled) {
134
+ settled = true;
135
+ reject(this.#mapSpawnError(error));
136
+ return;
137
+ }
138
+ this.#failTransport(error);
139
+ });
140
+ });
141
+ }
142
+ get stderrTail() {
143
+ return this.#stderrTail;
144
+ }
145
+ /** Releases the gated production supervisor after durable ownership fsync. */
146
+ activateSupervisor() {
147
+ if (!this.#ownsProcessGroup) return;
148
+ const child = this.#requireChild();
149
+ if (child.stdin === null) {
150
+ throw new PiBridgeError("PI_SPAWN_FAILED", "Pi supervisor stdin is unavailable");
151
+ }
152
+ child.stdin.write(SUPERVISOR_ACTIVATION_LINE);
153
+ }
154
+ /**
155
+ * Sends one command and resolves with its validated response record.
156
+ * Rejects on failure response, timeout, process exit, or protocol failure.
157
+ */
158
+ request(command, options = {}) {
159
+ const child = this.#requireChild();
160
+ if (this.#fatalError !== null) {
161
+ return Promise.reject(this.#fatalError);
162
+ }
163
+ if (this.#exitInfo !== null) {
164
+ return Promise.reject(this.#exitedError(command.type));
165
+ }
166
+ const id = `pita-${++this.#nextRequestId}`;
167
+ const timeoutMs = options.timeoutMs ?? this.#options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
168
+ return new Promise((resolve, reject) => {
169
+ const timer = setTimeout(() => {
170
+ this.#pending.delete(id);
171
+ reject(
172
+ new PiBridgeError(
173
+ "PI_REQUEST_TIMEOUT",
174
+ `Pi did not respond to ${command.type} within ${timeoutMs}ms`,
175
+ { details: { commandType: command.type, timeoutMs } }
176
+ )
177
+ );
178
+ }, timeoutMs);
179
+ timer.unref?.();
180
+ this.#pending.set(id, { commandType: command.type, resolve, reject, timer });
181
+ try {
182
+ this.#writeLine({ id, ...command });
183
+ } catch (error) {
184
+ clearTimeout(timer);
185
+ this.#pending.delete(id);
186
+ reject(
187
+ error instanceof PiBridgeError ? error : new PiBridgeError("PI_SPAWN_FAILED", "Failed to write to Pi stdin", {
188
+ cause: error
189
+ })
190
+ );
191
+ }
192
+ });
193
+ }
194
+ /** Writes an extension UI reply. No response record is expected. */
195
+ sendExtensionUiResponse(response) {
196
+ this.#requireChild();
197
+ if (this.#fatalError !== null) {
198
+ throw this.#fatalError;
199
+ }
200
+ if (this.#exitInfo !== null) {
201
+ throw this.#exitedError("extension_ui_response");
202
+ }
203
+ this.#writeLine(response);
204
+ }
205
+ /** Closes stdin so Pi can exit gracefully. */
206
+ endInput() {
207
+ this.#child?.stdin?.end();
208
+ }
209
+ kill(signal = "SIGTERM") {
210
+ const pid = this.processId;
211
+ if (this.#ownsProcessGroup && pid !== null) {
212
+ if (this.#observedExitInfo !== null) return;
213
+ try {
214
+ process.kill(-pid, signal);
215
+ return;
216
+ } catch (error) {
217
+ if (error.code === "ESRCH") return;
218
+ throw new PiBridgeError(
219
+ "PI_RUNTIME_EXITED",
220
+ `Could not signal Pi process group ${pid} with ${signal}`,
221
+ { cause: error, details: { pid, signal } }
222
+ );
223
+ }
224
+ }
225
+ this.#child?.kill(signal);
226
+ }
227
+ /** Resolves when the process has exited. */
228
+ waitForExit() {
229
+ if (this.#exitInfo !== null) return Promise.resolve(this.#exitInfo);
230
+ return new Promise((resolve) => this.#exitWaiters.push(resolve));
231
+ }
232
+ /** Resolves on OS process exit even if inherited stdio delays `close`. */
233
+ waitForProcessExit() {
234
+ if (this.#observedExitInfo !== null) {
235
+ return Promise.resolve(this.#observedExitInfo);
236
+ }
237
+ return new Promise((resolve) => this.#processExitWaiters.push(resolve));
238
+ }
239
+ /**
240
+ * Graceful shutdown: close stdin, wait up to `graceMs` for exit, then
241
+ * escalate to SIGTERM and finally SIGKILL.
242
+ */
243
+ async stop(graceMs = 3e3) {
244
+ if (this.#child === null) return { code: null, signal: null };
245
+ if (this.#observedExitInfo !== null) {
246
+ await this.#stopRemainingProcessGroup(graceMs);
247
+ return this.#observedExitInfo;
248
+ }
249
+ this.endInput();
250
+ for (const signal of [null, "SIGTERM", "SIGKILL"]) {
251
+ if (signal !== null) this.kill(signal);
252
+ const info2 = await Promise.race([
253
+ this.waitForProcessExit(),
254
+ new Promise((resolve) => {
255
+ const timer = setTimeout(() => resolve(null), graceMs);
256
+ timer.unref?.();
257
+ })
258
+ ]);
259
+ if (info2 !== null) {
260
+ await this.#stopRemainingProcessGroup(graceMs);
261
+ return info2;
262
+ }
263
+ }
264
+ const info = await this.waitForProcessExit();
265
+ await this.#stopRemainingProcessGroup(graceMs);
266
+ return info;
267
+ }
268
+ async #stopRemainingProcessGroup(graceMs) {
269
+ const pid = this.processId;
270
+ if (!this.#ownsProcessGroup || pid === null) return;
271
+ const deadline = Date.now() + Math.max(0, graceMs);
272
+ while (await this.#processGroupExists(pid) && Date.now() < deadline) {
273
+ await new Promise((resolve) => setTimeout(resolve, 25));
274
+ }
275
+ if (!await this.#processGroupExists(pid)) return;
276
+ throw new PiBridgeError(
277
+ "PI_RUNTIME_EXITED",
278
+ `Pi supervisor ${pid} exited while its process group remained alive; Webdesk retained the durable owner rather than signalling an unverified group.`,
279
+ { details: { pid, graceMs } }
280
+ );
281
+ }
282
+ async #processGroupExists(pid) {
283
+ try {
284
+ const { stdout } = await execFileAsync("ps", ["-axo", "pgid=,stat="], {
285
+ encoding: "utf8",
286
+ env: { ...process.env, LC_ALL: "C", LANG: "C" },
287
+ timeout: 1e3,
288
+ maxBuffer: 1024 * 1024
289
+ });
290
+ return stdout.split("\n").some((line) => {
291
+ const [group, state] = line.trim().split(/\s+/);
292
+ return Number(group) === pid && state !== void 0 && !state.startsWith("Z");
293
+ });
294
+ } catch {
295
+ }
296
+ try {
297
+ process.kill(-pid, 0);
298
+ return true;
299
+ } catch (error) {
300
+ return error.code === "EPERM";
301
+ }
302
+ }
303
+ #requireChild() {
304
+ if (this.#child === null) {
305
+ throw new PiBridgeError("PI_PROTOCOL_VIOLATION", "PiRpcClient used before start()");
306
+ }
307
+ return this.#child;
308
+ }
309
+ #mapSpawnError(error) {
310
+ if (error.code === "ENOENT") {
311
+ return new PiBridgeError(
312
+ "PI_EXECUTABLE_NOT_FOUND",
313
+ `Pi executable not found: ${this.#options.executable}. Set PITA_PI_EXECUTABLE to a pi binary, or install @earendil-works/pi-coding-agent so the workspace-local binary can be used.`,
314
+ { cause: error, details: { executable: this.#options.executable } }
315
+ );
316
+ }
317
+ return new PiBridgeError(
318
+ "PI_SPAWN_FAILED",
319
+ `Failed to spawn Pi (${this.#options.executable}): ${error.message}`,
320
+ { cause: error, details: { executable: this.#options.executable } }
321
+ );
322
+ }
323
+ #writeLine(value) {
324
+ const child = this.#requireChild();
325
+ if (this.#fatalError !== null) {
326
+ throw this.#fatalError;
327
+ }
328
+ if (child.stdin === null) {
329
+ throw new PiBridgeError("PI_PROTOCOL_VIOLATION", "Pi stdin is not available");
330
+ }
331
+ try {
332
+ child.stdin.write(`${JSON.stringify(value)}
333
+ `);
334
+ } catch (error) {
335
+ const bridgeError = this.#failTransport(error);
336
+ throw bridgeError;
337
+ }
338
+ }
339
+ #handleStdout(chunk) {
340
+ if (this.#streamFailed || this.#exitInfo !== null) return;
341
+ let records;
342
+ try {
343
+ records = this.#decoder.push(chunk);
344
+ } catch (error) {
345
+ this.#failStream(error);
346
+ return;
347
+ }
348
+ for (const raw of records) {
349
+ let record;
350
+ try {
351
+ record = classifyRpcRecord(raw);
352
+ } catch (error) {
353
+ this.#failStream(error);
354
+ return;
355
+ }
356
+ this.#dispatch(record);
357
+ }
358
+ }
359
+ #dispatch(record) {
360
+ switch (record.kind) {
361
+ case "response": {
362
+ const { id } = record.record;
363
+ const pending = id === void 0 ? void 0 : this.#pending.get(id);
364
+ if (pending === void 0) {
365
+ this.#handlers.onProtocolIssue?.({
366
+ message: `Pi sent an uncorrelated response for command "${record.record.command}"` + (record.record.error !== void 0 ? `: ${record.record.error}` : ""),
367
+ fatal: false
368
+ });
369
+ return;
370
+ }
371
+ if (record.record.command !== pending.commandType) {
372
+ this.#failStream(
373
+ new PiBridgeError(
374
+ "PI_PROTOCOL_VIOLATION",
375
+ `Pi response ${id} reported command "${record.record.command}" but the pending request is "${pending.commandType}"`,
376
+ {
377
+ details: {
378
+ id,
379
+ reportedCommand: record.record.command,
380
+ expectedCommand: pending.commandType
381
+ }
382
+ }
383
+ )
384
+ );
385
+ return;
386
+ }
387
+ this.#pending.delete(id);
388
+ clearTimeout(pending.timer);
389
+ if (record.record.success) {
390
+ pending.resolve(record.record);
391
+ } else {
392
+ pending.reject(
393
+ new PiBridgeError(
394
+ "PI_REQUEST_FAILED",
395
+ `Pi rejected ${pending.commandType}: ${record.record.error ?? "unknown error"}`,
396
+ { details: { commandType: pending.commandType, error: record.record.error } }
397
+ )
398
+ );
399
+ }
400
+ return;
401
+ }
402
+ case "extension_ui_request":
403
+ this.#handlers.onExtensionUiRequest?.(record.record);
404
+ return;
405
+ case "event":
406
+ this.#handlers.onEvent?.(record.record);
407
+ return;
408
+ }
409
+ }
410
+ #handleStdoutEnd() {
411
+ if (this.#streamFailed) return;
412
+ try {
413
+ this.#decoder.end();
414
+ } catch (error) {
415
+ this.#failStream(error);
416
+ }
417
+ }
418
+ #failStream(error) {
419
+ this.#streamFailed = true;
420
+ const bridgeError = error instanceof PiBridgeError ? error : new PiBridgeError("PI_PROTOCOL_VIOLATION", String(error), { cause: error });
421
+ if (this.#fatalError !== null) return;
422
+ this.#fatalError = bridgeError;
423
+ this.#handlers.onProtocolIssue?.({ message: bridgeError.message, fatal: true });
424
+ this.#rejectAllPending(bridgeError);
425
+ this.kill("SIGKILL");
426
+ }
427
+ #failTransport(error) {
428
+ if (this.#fatalError !== null) {
429
+ return this.#fatalError;
430
+ }
431
+ const message = error instanceof Error ? error.message : String(error);
432
+ const bridgeError = new PiBridgeError(
433
+ "PI_TRANSPORT_FAILED",
434
+ `Pi RPC transport failed while writing to stdin: ${message}`,
435
+ { cause: error }
436
+ );
437
+ this.#fatalError = bridgeError;
438
+ this.#handlers.onProtocolIssue?.({ message: bridgeError.message, fatal: true });
439
+ this.#rejectAllPending(bridgeError);
440
+ this.kill("SIGKILL");
441
+ return bridgeError;
442
+ }
443
+ #handleClose(info) {
444
+ if (this.#exitInfo !== null) return;
445
+ this.#exitInfo = info;
446
+ this.#rejectAllPending(this.#exitedError());
447
+ const waiters = this.#exitWaiters;
448
+ this.#exitWaiters = [];
449
+ for (const waiter of waiters) waiter(info);
450
+ this.#handlers.onExit?.(info);
451
+ }
452
+ #handleProcessExit(info) {
453
+ if (this.#observedExitInfo !== null) return;
454
+ this.#observedExitInfo = info;
455
+ const waiters = this.#processExitWaiters;
456
+ this.#processExitWaiters = [];
457
+ for (const waiter of waiters) waiter(info);
458
+ }
459
+ #rejectAllPending(error) {
460
+ const pending = [...this.#pending.values()];
461
+ this.#pending.clear();
462
+ for (const request of pending) {
463
+ clearTimeout(request.timer);
464
+ request.reject(error);
465
+ }
466
+ }
467
+ #exitedError(commandType) {
468
+ const exit = this.#exitInfo;
469
+ const suffix = exit === null ? "" : ` (code ${exit.code}, signal ${exit.signal})`;
470
+ const target = commandType === void 0 ? "a pending request" : `"${commandType}"`;
471
+ return new PiBridgeError(
472
+ "PI_RUNTIME_EXITED",
473
+ `Pi exited${suffix} before responding to ${target}`,
474
+ { details: { code: exit?.code ?? null, signal: exit?.signal ?? null } }
475
+ );
476
+ }
477
+ };
478
+ export {
479
+ PiRpcClient
480
+ };