@pstdio/pocketcoder-remote 0.2.0 → 0.2.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/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type:app"
7
7
  ]
8
8
  },
9
- "version": "0.2.0",
9
+ "version": "0.2.1",
10
10
  "private": false,
11
11
  "description": "Pi-based terminal UI for PocketCoder workspaces.",
12
12
  "type": "module",
@@ -18,8 +18,6 @@
18
18
  },
19
19
  "files": [
20
20
  "dist",
21
- "src",
22
- "!src/**/*.test.ts",
23
21
  "README.md",
24
22
  "LICENSE"
25
23
  ],
@@ -33,18 +31,18 @@
33
31
  "node": ">=22.19.0"
34
32
  },
35
33
  "scripts": {
36
- "build": "bun build ./src/bin.ts --outdir ./dist --target node",
34
+ "build": "bun build ./src/bin.ts ./src/extension.ts --outdir ./dist --target node --external '@earendil-works/*'",
37
35
  "start": "bun ./src/bin.ts",
38
36
  "test": "bun test",
39
37
  "typecheck": "tsc --project ./tsconfig.json --noEmit"
40
38
  },
41
39
  "dependencies": {
42
- "@pstdio/pocketcoder-client": "workspace:*",
43
40
  "@earendil-works/pi-ai": "0.83.0",
44
41
  "@earendil-works/pi-coding-agent": "0.83.0",
45
42
  "@earendil-works/pi-tui": "0.83.0"
46
43
  },
47
44
  "devDependencies": {
45
+ "@pstdio/pocketcoder-client": "workspace:*",
48
46
  "@types/bun": "1.3.14",
49
47
  "typescript": "5.9.3"
50
48
  }
@@ -1,104 +0,0 @@
1
- interface MessageUpdate {
2
- event: "message_update";
3
- data: {
4
- id: number;
5
- message: string;
6
- role: string;
7
- time?: string;
8
- };
9
- }
10
-
11
- interface StatusChange {
12
- event: "status_change";
13
- data: {
14
- agent_type?: string;
15
- status: "running" | "stable";
16
- };
17
- }
18
-
19
- interface AgentError {
20
- event: "agent_error";
21
- data: {
22
- level?: string;
23
- message: string;
24
- time?: string;
25
- };
26
- }
27
-
28
- export type AgentApiEvent = MessageUpdate | StatusChange | AgentError;
29
-
30
- function isRecord(value: unknown): value is Record<string, unknown> {
31
- return typeof value === "object" && value !== null;
32
- }
33
-
34
- function eventPayload(block: string): { event: string; parsed: Record<string, unknown> } | null {
35
- let event = "message";
36
- const data: string[] = [];
37
- for (const line of block.split("\n")) {
38
- if (line.startsWith(":")) continue;
39
- const separator = line.indexOf(":");
40
- const field = separator === -1 ? line : line.slice(0, separator);
41
- const value = separator === -1 ? "" : line.slice(separator + 1).replace(/^ /, "");
42
- if (field === "event") event = value;
43
- if (field === "data") data.push(value);
44
- }
45
- if (data.length === 0) return null;
46
- try {
47
- const parsed = JSON.parse(data.join("\n")) as unknown;
48
- return isRecord(parsed) ? { event, parsed } : null;
49
- } catch {
50
- return null;
51
- }
52
- }
53
-
54
- function parseEvent(block: string): AgentApiEvent | null {
55
- const payload = eventPayload(block);
56
- if (!payload) return null;
57
- const { event, parsed } = payload;
58
- if (
59
- event === "message_update" &&
60
- typeof parsed.id === "number" &&
61
- typeof parsed.message === "string" &&
62
- typeof parsed.role === "string"
63
- ) {
64
- return { event, data: parsed as MessageUpdate["data"] };
65
- }
66
- if (event === "status_change" && (parsed.status === "running" || parsed.status === "stable")) {
67
- return { event, data: parsed as StatusChange["data"] };
68
- }
69
- if (event === "agent_error" && typeof parsed.message === "string") {
70
- return { event, data: parsed as AgentError["data"] };
71
- }
72
- return null;
73
- }
74
-
75
- export async function* readAgentApiEvents(
76
- body: ReadableStream<Uint8Array>,
77
- signal: AbortSignal,
78
- ): AsyncGenerator<AgentApiEvent> {
79
- const reader = body.getReader();
80
- const decoder = new TextDecoder();
81
- let buffer = "";
82
- const abort = () => {
83
- void reader.cancel(signal.reason).catch(() => {});
84
- };
85
- if (signal.aborted) abort();
86
- else signal.addEventListener("abort", abort, { once: true });
87
- try {
88
- while (!signal.aborted) {
89
- const next = await reader.read();
90
- if (next.done) break;
91
- buffer = `${buffer}${decoder.decode(next.value, { stream: true })}`.replaceAll("\r\n", "\n");
92
- let boundary = buffer.indexOf("\n\n");
93
- while (boundary >= 0) {
94
- const event = parseEvent(buffer.slice(0, boundary));
95
- buffer = buffer.slice(boundary + 2);
96
- if (event) yield event;
97
- boundary = buffer.indexOf("\n\n");
98
- }
99
- }
100
- } finally {
101
- signal.removeEventListener("abort", abort);
102
- reader.releaseLock();
103
- }
104
- }
@@ -1,158 +0,0 @@
1
- import { readFileSync, statSync } from "node:fs";
2
- import { basename, extname, isAbsolute, resolve } from "node:path";
3
- import type { Context } from "@earendil-works/pi-ai";
4
- import type { CommandContext, CommandRegistrar } from "./commands";
5
- import type { ControlPlaneClient } from "./control-plane";
6
- import type { TargetRef } from "./session-target";
7
-
8
- // Turn-level attachment capture for the Pi remote: pasted images, explicit
9
- // @path tokens, and the /attach queue all upload through the PocketCoder
10
- // attachment API before the message referencing them is sent.
11
-
12
- export const DIRECT_MODE_ATTACHMENT_ERROR =
13
- "file attachments need the PocketCoder workspace API; a direct AgentAPI URL (POCKETCODER_AGENTAPI_URL) cannot accept managed uploads";
14
-
15
- export interface TurnFile {
16
- name: string;
17
- mediaType: string;
18
- bytes: Uint8Array;
19
- localPath?: string;
20
- }
21
-
22
- const IMAGE_EXTENSIONS: Record<string, string> = {
23
- "image/gif": "gif",
24
- "image/jpeg": "jpg",
25
- "image/png": "png",
26
- "image/svg+xml": "svg",
27
- "image/webp": "webp",
28
- };
29
-
30
- const MEDIA_TYPES: Record<string, string> = {
31
- ".csv": "text/csv",
32
- ".gif": "image/gif",
33
- ".html": "text/html",
34
- ".jpeg": "image/jpeg",
35
- ".jpg": "image/jpeg",
36
- ".json": "application/json",
37
- ".md": "text/markdown",
38
- ".pdf": "application/pdf",
39
- ".png": "image/png",
40
- ".svg": "image/svg+xml",
41
- ".txt": "text/plain",
42
- ".webp": "image/webp",
43
- };
44
-
45
- function lastUserContent(context: Context): Context["messages"][number]["content"] | undefined {
46
- const message = context.messages.findLast((candidate) => candidate.role === "user");
47
- return message?.role === "user" ? message.content : undefined;
48
- }
49
-
50
- export function userTextOf(context: Context): string {
51
- const content = lastUserContent(context);
52
- if (content === undefined) throw new Error("local Pi did not provide a user message");
53
- if (typeof content === "string") return content;
54
- return content
55
- .filter((part) => part.type === "text")
56
- .map((part) => part.text)
57
- .join("\n");
58
- }
59
-
60
- function imageParts(context: Context): TurnFile[] {
61
- const content = lastUserContent(context);
62
- if (content === undefined || typeof content === "string") return [];
63
- return content
64
- .filter((part) => part.type === "image")
65
- .map((part) => ({
66
- name: `pasted-image.${IMAGE_EXTENSIONS[part.mimeType] ?? "bin"}`,
67
- mediaType: part.mimeType,
68
- bytes: Uint8Array.from(Buffer.from(part.data, "base64")),
69
- }));
70
- }
71
-
72
- // Explicit `@path` tokens that resolve to a local regular file; quoted forms
73
- // (`@"my file.pdf"`) support spaces.
74
- export function pathTokens(text: string, cwd = process.cwd()): string[] {
75
- const paths: string[] = [];
76
- for (const match of text.matchAll(/@(?:"([^"]+)"|(\S+))/g)) {
77
- const token = (match[1] ?? match[2]) as string;
78
- const path = isAbsolute(token) ? token : resolve(cwd, token);
79
- if (isFile(path) && !paths.includes(path)) paths.push(path);
80
- }
81
- return paths;
82
- }
83
-
84
- function isFile(path: string): boolean {
85
- try {
86
- return statSync(path).isFile();
87
- } catch {
88
- return false;
89
- }
90
- }
91
-
92
- function fileFromPath(path: string): TurnFile {
93
- return {
94
- name: basename(path),
95
- mediaType: MEDIA_TYPES[extname(path).toLowerCase()] ?? "application/octet-stream",
96
- bytes: new Uint8Array(readFileSync(path)),
97
- localPath: path,
98
- };
99
- }
100
-
101
- export function collectTurnFiles(context: Context, queue: string[], cwd?: string): TurnFile[] {
102
- const text = userTextOf(context);
103
- const paths = [...queue];
104
- for (const path of pathTokens(text, cwd)) {
105
- if (!paths.includes(path)) paths.push(path);
106
- }
107
- return [...imageParts(context), ...paths.map(fileFromPath)];
108
- }
109
-
110
- export async function uploadTurnFiles(
111
- controlPlane: ControlPlaneClient,
112
- workspaceId: string,
113
- files: TurnFile[],
114
- ): Promise<string[]> {
115
- const ids: string[] = [];
116
- for (const file of files) {
117
- const uploaded = await controlPlane.attachments.upload(workspaceId, {
118
- name: file.name,
119
- mediaType: file.mediaType,
120
- body: file.bytes,
121
- sizeBytes: file.bytes.byteLength,
122
- });
123
- ids.push(uploaded.id);
124
- }
125
- return ids;
126
- }
127
-
128
- export interface AttachCommandDeps {
129
- targets: TargetRef;
130
- queue: string[];
131
- }
132
-
133
- export function registerAttachCommand(pi: CommandRegistrar, deps: AttachCommandDeps): void {
134
- pi.registerCommand("attach", {
135
- description: "Queue a local file to upload with the next message",
136
- handler: async (args, ctx: CommandContext) => {
137
- if (deps.targets.current.mode === "direct") {
138
- ctx.ui.notify(DIRECT_MODE_ATTACHMENT_ERROR, "warning");
139
- return;
140
- }
141
- const token = args.trim();
142
- if (!token) {
143
- ctx.ui.notify("usage: /attach <path>", "info");
144
- return;
145
- }
146
- const path = isAbsolute(token) ? token : resolve(process.cwd(), token);
147
- if (!isFile(path)) {
148
- ctx.ui.notify(`no such file: ${token}`, "warning");
149
- return;
150
- }
151
- deps.queue.push(path);
152
- ctx.ui.notify(
153
- `queued ${basename(path)} (${deps.queue.length} attachment${deps.queue.length === 1 ? "" : "s"})`,
154
- "info",
155
- );
156
- },
157
- });
158
- }
package/src/bin.ts DELETED
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
3
- import { resolvePiInvocation } from "./launch";
4
-
5
- function main(): void {
6
- let invocation: ReturnType<typeof resolvePiInvocation>;
7
- try {
8
- invocation = resolvePiInvocation({ argv: process.argv.slice(2) });
9
- } catch (error) {
10
- console.error(`pocketcoder-remote: ${error instanceof Error ? error.message : String(error)}`);
11
- process.exit(1);
12
- }
13
- const child = spawn(invocation.command, invocation.args, {
14
- stdio: "inherit",
15
- env: invocation.env,
16
- });
17
- child.on("error", (error) => {
18
- console.error(`pocketcoder-remote: failed to launch pi: ${error.message}`);
19
- process.exit(1);
20
- });
21
- child.on("exit", (code, signal) => {
22
- process.exit(code ?? (signal ? 1 : 0));
23
- });
24
- }
25
-
26
- main();
package/src/client.ts DELETED
@@ -1,344 +0,0 @@
1
- import { type AgentApiEvent, readAgentApiEvents } from "./agentapi-events";
2
-
3
- export interface AgentApiMessage {
4
- id: number;
5
- content: string;
6
- role: string;
7
- }
8
-
9
- export interface RemoteAgentClientConfig {
10
- serviceUrl: string;
11
- key: string;
12
- pollIntervalMs?: number;
13
- timeoutMs?: number;
14
- }
15
-
16
- interface AgentApiStatus {
17
- status?: string;
18
- }
19
-
20
- interface AgentApiMessages {
21
- messages?: unknown;
22
- }
23
-
24
- interface WorkspaceChange {
25
- cursor?: unknown;
26
- workspace?: {
27
- agent_state?: unknown;
28
- };
29
- }
30
-
31
- type FetchLike = typeof fetch;
32
- type SnapshotCallback = (snapshot: string) => void;
33
-
34
- function delay(ms: number, signal?: AbortSignal): Promise<void> {
35
- return new Promise((resolve, reject) => {
36
- const onAbort = () => {
37
- clearTimeout(timeout);
38
- reject(signal?.reason ?? new Error("remote request aborted"));
39
- };
40
- const timeout = setTimeout(() => {
41
- signal?.removeEventListener("abort", onAbort);
42
- resolve();
43
- }, ms);
44
- if (signal?.aborted) {
45
- onAbort();
46
- return;
47
- }
48
- signal?.addEventListener("abort", onAbort, { once: true });
49
- });
50
- }
51
-
52
- function isRecord(value: unknown): value is Record<string, unknown> {
53
- return typeof value === "object" && value !== null;
54
- }
55
-
56
- function parseMessages(value: unknown): AgentApiMessage[] {
57
- const items = isRecord(value) && Array.isArray(value.messages) ? value.messages : [];
58
- return items.flatMap((item) => {
59
- if (
60
- !isRecord(item) ||
61
- typeof item.id !== "number" ||
62
- typeof item.content !== "string" ||
63
- typeof item.role !== "string"
64
- ) {
65
- return [];
66
- }
67
- return [{ id: item.id, content: item.content, role: item.role }];
68
- });
69
- }
70
-
71
- function isAgentMessage(message: AgentApiMessage): boolean {
72
- return message.role === "agent" || message.role === "assistant";
73
- }
74
-
75
- function changesUrlFor(serviceUrl: string): string | undefined {
76
- const url = new URL(serviceUrl);
77
- const match = url.pathname.match(/^(.*\/v1\/workspaces\/[^/]+)\/(?:agent|services\/agent)$/);
78
- if (!match) return undefined;
79
- url.pathname = `${match[1]}/changes`;
80
- url.search = "";
81
- url.hash = "";
82
- return url.toString();
83
- }
84
-
85
- async function responseError(response: Response): Promise<string> {
86
- const body = (await response.text()).trim();
87
- return body ? `${response.status} ${body.slice(0, 1000)}` : String(response.status);
88
- }
89
-
90
- export class RemoteAgentClient {
91
- readonly serviceUrl: string;
92
- readonly key: string;
93
- readonly pollIntervalMs: number;
94
- readonly timeoutMs: number;
95
- readonly fetchImpl: FetchLike;
96
- private changesUrl: string | undefined;
97
-
98
- constructor(config: RemoteAgentClientConfig, fetchImpl: FetchLike = fetch) {
99
- this.serviceUrl = config.serviceUrl.replace(/\/$/, "");
100
- this.key = config.key;
101
- this.pollIntervalMs = config.pollIntervalMs ?? 250;
102
- this.timeoutMs = config.timeoutMs ?? 600_000;
103
- this.fetchImpl = fetchImpl;
104
- this.changesUrl = changesUrlFor(this.serviceUrl);
105
- }
106
-
107
- private async request(path: string, init: RequestInit = {}): Promise<Response> {
108
- return await this.fetchImpl(`${this.serviceUrl}${path}`, {
109
- ...init,
110
- headers: {
111
- authorization: `Bearer ${this.key}`,
112
- ...(init.body ? { "content-type": "application/json" } : {}),
113
- ...(init.headers ?? {}),
114
- },
115
- });
116
- }
117
-
118
- private async messages(signal?: AbortSignal): Promise<AgentApiMessage[]> {
119
- const response = await this.request("/messages", { signal });
120
- if (!response.ok) {
121
- throw new Error(`AgentAPI messages request failed: ${await responseError(response)}`);
122
- }
123
- return parseMessages((await response.json()) as AgentApiMessages);
124
- }
125
-
126
- private async status(signal?: AbortSignal): Promise<string> {
127
- const response = await this.request("/status", { signal });
128
- if (!response.ok) {
129
- throw new Error(`AgentAPI status request failed: ${await responseError(response)}`);
130
- }
131
- const body = (await response.json()) as AgentApiStatus;
132
- if (body.status !== "running" && body.status !== "stable") {
133
- throw new Error(`AgentAPI returned an unknown status: ${JSON.stringify(body.status)}`);
134
- }
135
- return body.status;
136
- }
137
-
138
- private async eventStream(signal: AbortSignal): Promise<Response | null> {
139
- const response = await this.request("/events", {
140
- headers: { accept: "text/event-stream" },
141
- signal,
142
- });
143
- if ([404, 409, 422].includes(response.status)) return null;
144
- if (!response.ok) {
145
- throw new Error(`AgentAPI events request failed: ${await responseError(response)}`);
146
- }
147
- if (!response.body) throw new Error("AgentAPI events response had no body");
148
- return response;
149
- }
150
-
151
- private async workspaceChange(
152
- after: number,
153
- waitSeconds: number,
154
- signal?: AbortSignal,
155
- ): Promise<{ cursor: number; agentState: string } | undefined> {
156
- if (!this.changesUrl) return undefined;
157
- const url = new URL(this.changesUrl);
158
- url.searchParams.set("after", String(after));
159
- url.searchParams.set("wait", String(waitSeconds));
160
- const response = await this.fetchImpl(url, {
161
- headers: { authorization: `Bearer ${this.key}` },
162
- signal,
163
- });
164
- if (response.status === 404) {
165
- this.changesUrl = undefined;
166
- return undefined;
167
- }
168
- if (!response.ok) {
169
- throw new Error(`PocketCoder changes request failed: ${await responseError(response)}`);
170
- }
171
- const body = (await response.json()) as WorkspaceChange;
172
- if (
173
- typeof body.cursor !== "number" ||
174
- !isRecord(body.workspace) ||
175
- (body.workspace.agent_state !== "unknown" &&
176
- body.workspace.agent_state !== "running" &&
177
- body.workspace.agent_state !== "stable")
178
- ) {
179
- throw new Error("PocketCoder changes response was malformed");
180
- }
181
- return { cursor: body.cursor, agentState: body.workspace.agent_state };
182
- }
183
-
184
- private async submit(
185
- prompt: string,
186
- attachmentIds: string[],
187
- signal: AbortSignal,
188
- ): Promise<void> {
189
- const response = await this.request("/message", {
190
- method: "POST",
191
- body: JSON.stringify({
192
- content: prompt,
193
- type: "user",
194
- ...(attachmentIds.length > 0 ? { attachment_ids: attachmentIds } : {}),
195
- }),
196
- signal,
197
- });
198
- if (!response.ok) {
199
- throw new Error(`AgentAPI message request failed: ${await responseError(response)}`);
200
- }
201
- }
202
-
203
- private async pollForReply(
204
- baselineId: number,
205
- changeCursor: number,
206
- deadline: number,
207
- signal: AbortSignal,
208
- ): Promise<string> {
209
- while (Date.now() < deadline) {
210
- const remainingMs = deadline - Date.now();
211
- const change = await this.workspaceChange(
212
- changeCursor,
213
- Math.max(1, Math.min(30, Math.ceil(remainingMs / 1000))),
214
- signal,
215
- );
216
- if (change) changeCursor = change.cursor;
217
- const [status, messages] = change
218
- ? [change.agentState, await this.messages(signal)]
219
- : await Promise.all([this.status(signal), this.messages(signal)]);
220
- const reply = messages
221
- .filter((message) => message.id > baselineId && isAgentMessage(message))
222
- .at(-1);
223
- if (status === "stable" && reply?.content.trim()) return reply.content;
224
- if (!change) await delay(this.pollIntervalMs, signal);
225
- }
226
- throw new Error(`remote agent did not finish within ${this.timeoutMs}ms`);
227
- }
228
-
229
- private async consumeEvents(
230
- initial: Response,
231
- onEvent: (event: AgentApiEvent) => Promise<void>,
232
- signal: AbortSignal,
233
- ): Promise<"fallback" | "aborted"> {
234
- let response = initial;
235
- while (!signal.aborted) {
236
- try {
237
- if (!response.body) throw new Error("AgentAPI events response had no body");
238
- for await (const event of readAgentApiEvents(response.body, signal)) {
239
- await onEvent(event);
240
- }
241
- } catch {
242
- if (signal.aborted) return "aborted";
243
- }
244
- if (signal.aborted) return "aborted";
245
- await delay(this.pollIntervalMs, signal);
246
- try {
247
- const reconnected = await this.eventStream(signal);
248
- if (!reconnected) return "fallback";
249
- response = reconnected;
250
- } catch {
251
- if (signal.aborted) return "aborted";
252
- }
253
- }
254
- return "aborted";
255
- }
256
-
257
- async send(
258
- prompt: string,
259
- signal?: AbortSignal,
260
- attachmentIds: string[] = [],
261
- onSnapshot?: SnapshotCallback,
262
- ): Promise<string> {
263
- const timeout = new AbortController();
264
- const timer = setTimeout(
265
- () => timeout.abort(new Error(`remote agent did not finish within ${this.timeoutMs}ms`)),
266
- this.timeoutMs,
267
- );
268
- const turnSignal = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
269
- const stopEvents = new AbortController();
270
- const eventSignal = AbortSignal.any([turnSignal, stopEvents.signal]);
271
- try {
272
- const before = await this.messages(turnSignal);
273
- const baselineId = before.reduce((maximum, message) => Math.max(maximum, message.id), -1);
274
- const baselineChange = await this.workspaceChange(0, 0, turnSignal);
275
- const deadline = Date.now() + this.timeoutMs;
276
- const initialEvents = await this.eventStream(eventSignal);
277
- if (!initialEvents) {
278
- await this.submit(prompt, attachmentIds, turnSignal);
279
- return await this.pollForReply(
280
- baselineId,
281
- baselineChange?.cursor ?? 0,
282
- deadline,
283
- turnSignal,
284
- );
285
- }
286
-
287
- let submitted = false;
288
- let lastSnapshot = "";
289
- let complete!: (value: string) => void;
290
- const completed = new Promise<string>((resolve) => {
291
- complete = resolve;
292
- });
293
- const consume = this.consumeEvents(
294
- initialEvents,
295
- async (event) => {
296
- if (
297
- event.event === "message_update" &&
298
- event.data.id > baselineId &&
299
- (event.data.role === "agent" || event.data.role === "assistant") &&
300
- event.data.message.trim() &&
301
- event.data.message !== lastSnapshot
302
- ) {
303
- lastSnapshot = event.data.message;
304
- onSnapshot?.(lastSnapshot);
305
- }
306
- if (event.event !== "status_change" || event.data.status !== "stable" || !submitted) {
307
- return;
308
- }
309
- const final = (await this.messages(turnSignal))
310
- .filter((message) => message.id > baselineId && isAgentMessage(message))
311
- .at(-1);
312
- if (!final?.content.trim()) return;
313
- if (final.content !== lastSnapshot) onSnapshot?.(final.content);
314
- complete(final.content);
315
- },
316
- eventSignal,
317
- );
318
- submitted = true;
319
- await this.submit(prompt, attachmentIds, turnSignal);
320
- const outcome = await Promise.race([
321
- completed.then((value) => ({ kind: "complete" as const, value })),
322
- consume.then((result) => ({ kind: result })),
323
- ]);
324
- if (outcome.kind === "complete") return outcome.value;
325
- if (outcome.kind === "fallback") {
326
- return await this.pollForReply(
327
- baselineId,
328
- baselineChange?.cursor ?? 0,
329
- deadline,
330
- turnSignal,
331
- );
332
- }
333
- throw turnSignal.reason ?? new Error("remote request aborted");
334
- } catch (error) {
335
- if (timeout.signal.aborted) throw timeout.signal.reason;
336
- throw error;
337
- } finally {
338
- clearTimeout(timer);
339
- stopEvents.abort("turn ended");
340
- }
341
- }
342
- }
343
-
344
- export { serviceUrlFromEnvironment } from "./environment";