antechamber 1.0.40 → 1.0.41

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/routes-v2.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { execFile } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { tmpdir } from "node:os";
5
+ import { resolve } from "node:path";
5
6
  import { promisify } from "node:util";
6
7
  import { errorBody, json, RequestBodyError } from "./http.js";
7
8
  import { nativePromptInput, parsePromptParts, v2PromptAttachments, v2PromptParts } from "./prompt-parts.js";
@@ -12,10 +13,11 @@ import { toV2Permission } from "./approvals.js";
12
13
  import { formQuestionAnswer, questionFormID, toQuestionForm } from "./question-forms.js";
13
14
  import { getAgentName } from "./agent-name.js";
14
15
  import { vcsRead, VcsError } from "./vcs.js";
15
- import { announceSessionError, compactSession, getCommands, unsupportedShare } from "./session-commands.js";
16
+ import { announceSessionError, compactSession, unsupportedShare } from "./session-commands.js";
16
17
  import { defaultV2Model, flattenModels, toV2Message, toV2Session } from "./v2.js";
17
18
  import { SHELL_CANCELLATION_KEY } from "./omo-shells.js";
18
19
  import { WorkStatusError } from "./work-status.js";
20
+ import { FileWatchError } from "./file-watch.js";
19
21
  import { agentDescriptors, modelRefFromBody, numberParam, OPENCODE_COMPAT_VERSION, promptAsync, runSessionShell, runSessionSlash, sessionDirectory, sessionMetadata, } from "./route-services.js";
20
22
  async function v2Agents(services) {
21
23
  return (await agentDescriptors(services)).map(({ model, variant, ...agent }) => ({
@@ -71,18 +73,6 @@ function followsCursor(session, cursor, order) {
71
73
  const comparison = session.time.updated - cursor.updated || session.id.localeCompare(cursor.id);
72
74
  return order === "asc" ? comparison > 0 : comparison < 0;
73
75
  }
74
- async function currentGitBranch(directory) {
75
- try {
76
- const { stdout } = await execFileAsync("git", ["symbolic-ref", "--short", "HEAD"], {
77
- cwd: directory,
78
- timeout: 3000,
79
- });
80
- return stdout.trim() || undefined;
81
- }
82
- catch {
83
- return undefined;
84
- }
85
- }
86
76
  async function initializeGit(directory) {
87
77
  try {
88
78
  await execFileAsync("git", ["init", "-q"], { cwd: directory, timeout: 3000 });
@@ -107,7 +97,7 @@ function v2Projects(services) {
107
97
  vcs: "git",
108
98
  }));
109
99
  }
110
- async function handleV2Bootstrap(services, res, path, method, directory) {
100
+ async function handleV2Bootstrap(services, res, url, path, method, directory) {
111
101
  const withLocation = (data) => ({ location: { directory }, data });
112
102
  if (path === "/api/info" && method === "GET") {
113
103
  json(res, 200, {
@@ -128,17 +118,19 @@ async function handleV2Bootstrap(services, res, path, method, directory) {
128
118
  return true;
129
119
  }
130
120
  if (path === "/api/provider" && method === "GET") {
131
- const cat = await services.catalog();
121
+ const cat = await services.catalog(directory, true);
132
122
  json(res, 200, withLocation(cat.providers.map((p) => ({ ...p, activation: "enabled" }))));
133
123
  return true;
134
124
  }
135
125
  if (path === "/api/model" && method === "GET") {
136
- const cat = await services.catalog();
137
- json(res, 200, withLocation(flattenModels(cat)));
126
+ const cat = await services.catalog(directory, true);
127
+ const requested = url.searchParams.getAll("types");
128
+ const types = requested.length === 0 ? ["chat"] : requested;
129
+ json(res, 200, withLocation(flattenModels(cat).filter((model) => types.some((type) => model.capabilities.output.includes(type === "chat" ? "text" : type)))));
138
130
  return true;
139
131
  }
140
132
  if (path === "/api/model/default" && method === "GET") {
141
- const cat = await services.catalog();
133
+ const cat = await services.catalog(directory);
142
134
  json(res, 200, withLocation(defaultV2Model(cat) ?? null));
143
135
  return true;
144
136
  }
@@ -157,7 +149,7 @@ async function handleV2Bootstrap(services, res, path, method, directory) {
157
149
  return true;
158
150
  }
159
151
  if (path === "/api/command" && method === "GET") {
160
- json(res, 200, withLocation((await getCommands(directory)).map((command) => ({
152
+ json(res, 200, withLocation((await services.commands(directory)).map((command) => ({
161
153
  ...command,
162
154
  source: "command",
163
155
  hints: [],
@@ -210,8 +202,12 @@ export async function handleV2(services, req, res, url, path, method, directory)
210
202
  const location = { directory };
211
203
  const withLocation = (data) => ({ location, data });
212
204
  // --- bootstrap ---
213
- if (await handleV2Bootstrap(services, res, path, method, directory))
205
+ if (await handleV2Bootstrap(services, res, url, path, method, directory))
214
206
  return;
207
+ if (path === "/api/debug/location" && method === "GET") {
208
+ json(res, 200, []);
209
+ return;
210
+ }
215
211
  if (path === "/api/debug/location" && method === "DELETE") {
216
212
  // OpenChamber evicts a location after git worktree changes so OpenCode re-reads it. antechamber
217
213
  // keeps no per-location instance (project and VCS reads are fresh per request): nothing to evict.
@@ -272,6 +268,56 @@ export async function handleV2(services, req, res, url, path, method, directory)
272
268
  });
273
269
  return;
274
270
  }
271
+ if (path === "/api/experimental/generate/text" && method === "POST") {
272
+ const body = await services.body(req);
273
+ if (typeof body !== "object" ||
274
+ body === null ||
275
+ !("prompt" in body) ||
276
+ typeof body.prompt !== "string" ||
277
+ body.prompt.trim() === "") {
278
+ json(res, 400, errorBody("InvalidRequestError", "prompt must be a nonempty string"));
279
+ return;
280
+ }
281
+ const model = "model" in body ? body.model : undefined;
282
+ if (model !== undefined &&
283
+ (typeof model !== "object" ||
284
+ model === null ||
285
+ !("providerID" in model) ||
286
+ typeof model.providerID !== "string" ||
287
+ !("id" in model) ||
288
+ typeof model.id !== "string")) {
289
+ json(res, 400, errorBody("InvalidRequestError", "model must include providerID and id"));
290
+ return;
291
+ }
292
+ const selected = model !== undefined && typeof model === "object" && model !== null && "providerID" in model && "id" in model
293
+ ? { providerID: String(model.providerID), id: String(model.id) }
294
+ : undefined;
295
+ const cancellation = new AbortController();
296
+ const disconnected = () => cancellation.abort(new Error("Text generation caller disconnected"));
297
+ res.once("close", disconnected);
298
+ if (res.destroyed)
299
+ disconnected();
300
+ try {
301
+ json(res, 200, {
302
+ data: {
303
+ text: await services.textGeneration.generate(services.defaultDirectory, body.prompt, selected, cancellation.signal),
304
+ },
305
+ });
306
+ }
307
+ catch (error) {
308
+ if (res.destroyed)
309
+ return;
310
+ json(res, 503, {
311
+ _tag: "ServiceUnavailableError",
312
+ message: error instanceof Error ? error.message : "Text generation is unavailable",
313
+ service: "opencode",
314
+ });
315
+ }
316
+ finally {
317
+ res.off("close", disconnected);
318
+ }
319
+ return;
320
+ }
275
321
  // OmO's background work (monitors, background terminal sessions, detached eval cells) as OpenCode shells.
276
322
  if (path === "/api/shell" && method === "GET") {
277
323
  json(res, 200, withLocation(services.sessions.shells.list(directory)));
@@ -291,10 +337,35 @@ export async function handleV2(services, req, res, url, path, method, directory)
291
337
  json(res, 400, errorBody("UnsupportedError", "Session statistics are not available with OmO"));
292
338
  return;
293
339
  }
340
+ if (path === "/api/experimental/usage/limits" && method === "GET") {
341
+ json(res, 200, withLocation(null));
342
+ return;
343
+ }
294
344
  if (path === "/api/mcp" || path.startsWith("/api/mcp/") || path.startsWith("/api/experimental/mcp/")) {
295
345
  json(res, 400, errorBody("UnsupportedError", "MCP is not available with OmO"));
296
346
  return;
297
347
  }
348
+ if (path === "/api/experimental/fs/watch" && (method === "POST" || method === "DELETE")) {
349
+ const body = await services.body(req);
350
+ if (typeof body !== "object" || body === null || !("path" in body) || typeof body.path !== "string") {
351
+ json(res, 400, errorBody("InvalidRequestError", "path must be a string"));
352
+ return;
353
+ }
354
+ try {
355
+ const file = await services.fileWatches.file(directory, body.path, method === "POST");
356
+ if (method === "POST")
357
+ services.fileWatches.watch(directory, file, resolve(directory, body.path));
358
+ else
359
+ services.fileWatches.unwatch(directory, file);
360
+ res.writeHead(204).end();
361
+ }
362
+ catch (error) {
363
+ if (!(error instanceof FileWatchError))
364
+ throw error;
365
+ json(res, error.status, errorBody(error.kind, error.message));
366
+ }
367
+ return;
368
+ }
298
369
  if (path === "/api/fs/find" && method === "GET") {
299
370
  const result = await miscRoute("GET", "/find/file", url, directory);
300
371
  if (result === undefined) {
@@ -308,7 +379,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
308
379
  const files = Array.isArray(result.body)
309
380
  ? result.body
310
381
  .filter((entry) => typeof entry === "string")
311
- .map((path) => ({ path, type: "file" }))
382
+ .map((path) => ({ path, type: path.endsWith("/") ? "directory" : "file" }))
312
383
  : [];
313
384
  json(res, 200, withLocation(files));
314
385
  return;
@@ -327,7 +398,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
327
398
  json(res, 200, withLocation(services.sessions
328
399
  .permissions()
329
400
  .filter((permission) => services.sessions.get(permission.sessionID)?.info.directory === directory)
330
- .map((permission) => ({ ...toV2Permission(permission), ...permission }))));
401
+ .map(toV2Permission)));
331
402
  return;
332
403
  }
333
404
  if (path === "/api/question/request" && method === "GET") {
@@ -343,7 +414,10 @@ export async function handleV2(services, req, res, url, path, method, directory)
343
414
  return;
344
415
  }
345
416
  if (path === "/api/form" && method === "GET") {
346
- json(res, 200, withLocation(services.sessions.questions().map(toQuestionForm)));
417
+ json(res, 200, withLocation(services.sessions
418
+ .questions()
419
+ .filter((question) => services.sessions.get(question.sessionID)?.info.directory === directory)
420
+ .map(toQuestionForm)));
347
421
  return;
348
422
  }
349
423
  if (path === "/api/vcs/init" && method === "POST") {
@@ -355,7 +429,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
355
429
  return;
356
430
  }
357
431
  if (path === "/api/vcs" && method === "GET") {
358
- const current = await currentGitBranch(directory);
432
+ const current = await services.vcsBranch(directory);
359
433
  json(res, 200, withLocation({ provider: "git", branch: current === undefined ? {} : { current } }));
360
434
  return;
361
435
  }
@@ -375,6 +449,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
375
449
  const dir = url.searchParams.get("directory") ?? url.searchParams.get("location");
376
450
  const search = url.searchParams.get("search");
377
451
  const parentID = url.searchParams.get("parentID");
452
+ const archived = url.searchParams.get("archived");
378
453
  const order = url.searchParams.get("order") === "asc" ? "asc" : "desc";
379
454
  const limit = numberParam(url.searchParams.get("limit"));
380
455
  let infos = (dir !== null && dir !== "" ? services.sessions.list(dir) : services.sessions.listAll()).filter(
@@ -383,7 +458,9 @@ export async function handleV2(services, req, res, url, path, method, directory)
383
458
  if (search)
384
459
  infos = infos.filter((s) => s.title.toLowerCase().includes(search.toLowerCase()));
385
460
  if (parentID !== null)
386
- infos = infos.filter((session) => session.parentID === parentID);
461
+ infos = infos.filter((session) => session.parentID === (parentID === "null" ? undefined : parentID));
462
+ if (archived === "true" || archived === "false")
463
+ infos = infos.filter((session) => (session.time.archived !== undefined) === (archived === "true"));
387
464
  let sessions = infos.map((info) => v2Session(services, info));
388
465
  sessions.sort((left, right) => compareSessions(left, right, order));
389
466
  const cursor = decodeSessionCursor(url.searchParams.get("cursor"));
@@ -430,6 +507,36 @@ export async function handleV2(services, req, res, url, path, method, directory)
430
507
  json(res, 200, { data });
431
508
  return;
432
509
  }
510
+ const instructionEntry = path.match(/^\/api\/experimental\/session\/([^/]+)\/instructions\/entries\/([^/]+)$/);
511
+ if (instructionEntry && (method === "PUT" || method === "DELETE")) {
512
+ const sessionId = decodeURIComponent(instructionEntry[1]);
513
+ const key = decodeURIComponent(instructionEntry[2]);
514
+ if (services.sessions.get(sessionId) === undefined) {
515
+ json(res, 404, errorBody("NotFoundError", `session not found: ${sessionId}`));
516
+ return;
517
+ }
518
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(key)) {
519
+ json(res, 400, errorBody("InvalidRequestError", "instruction entry key is invalid"));
520
+ return;
521
+ }
522
+ if (method === "PUT") {
523
+ const body = await services.body(req);
524
+ if (typeof body !== "object" || body === null || Array.isArray(body) || !("value" in body)) {
525
+ json(res, 400, errorBody("InvalidRequestError", "instruction entry requires a JSON value"));
526
+ return;
527
+ }
528
+ if (Buffer.byteLength(JSON.stringify(body.value), "utf8") > 256 * 1024) {
529
+ json(res, 413, errorBody("InstructionEntryValueTooLargeError", "instruction entry exceeds 256 KiB"));
530
+ return;
531
+ }
532
+ services.sessions.putInstruction(sessionId, key, body.value);
533
+ }
534
+ else {
535
+ services.sessions.removeInstruction(sessionId, key);
536
+ }
537
+ res.writeHead(204).end();
538
+ return;
539
+ }
433
540
  const sessionMatch = path.match(/^\/api\/session\/([^/]+)(\/.+)?$/);
434
541
  if (sessionMatch) {
435
542
  const sessionId = decodeURIComponent(sessionMatch[1]);
@@ -553,6 +660,16 @@ export async function handleV2(services, req, res, url, path, method, directory)
553
660
  json(res, 200, { data: v2Session(services, managed.info) });
554
661
  return;
555
662
  }
663
+ case "POST /view": {
664
+ const body = (await services.body(req));
665
+ if (typeof body.idle !== "number" || !Number.isFinite(body.idle)) {
666
+ json(res, 400, errorBody("InvalidRequestError", "idle must be a finite epoch-millisecond number"));
667
+ return;
668
+ }
669
+ services.sessions.view(sessionId, body.idle);
670
+ res.writeHead(204).end();
671
+ return;
672
+ }
556
673
  case "PATCH ": {
557
674
  const body = (await services.body(req));
558
675
  // OpenCode 2.1 takes `archived: boolean` (repeating the current state is a no-op);
@@ -578,7 +695,35 @@ export async function handleV2(services, req, res, url, path, method, directory)
578
695
  case "GET /message": {
579
696
  const order = url.searchParams.get("order");
580
697
  const limit = numberParam(url.searchParams.get("limit"));
581
- let messages = (services.sessions.messages(sessionId) ?? []).map((m) => toV2Message(m));
698
+ const history = services.sessions.messages(sessionId) ?? [];
699
+ const messagesWithTurns = history.flatMap((message) => {
700
+ if (message.parts.some((part) => part.type === "compaction")) {
701
+ const item = services.sessions.compaction(sessionId, message.info.id);
702
+ return item === undefined ? [] : [item];
703
+ }
704
+ if (message.info.role === "assistant" && message.info.summary === true)
705
+ return [];
706
+ const item = toV2Message(message);
707
+ const boundary = managed.tree.idleBoundaries?.[message.info.id];
708
+ if (boundary === undefined)
709
+ return [item];
710
+ return [
711
+ item,
712
+ {
713
+ id: `msg_idle_${message.info.id.replace(/^msg_/, "")}`,
714
+ type: "idle",
715
+ time: { created: boundary.at },
716
+ outcome: boundary.outcome,
717
+ ...(boundary.error !== undefined ? { error: boundary.error } : {}),
718
+ },
719
+ ];
720
+ });
721
+ if (managed.compactionID && !messagesWithTurns.some((message) => message.id === managed.compactionID)) {
722
+ const item = services.sessions.compaction(sessionId, managed.compactionID);
723
+ if (item !== undefined)
724
+ messagesWithTurns.push(item);
725
+ }
726
+ let messages = messagesWithTurns;
582
727
  if (order !== "asc")
583
728
  messages = messages.reverse(); // v2 default is newest-first
584
729
  const cursor = url.searchParams.get("cursor");
@@ -597,14 +742,52 @@ export async function handleV2(services, req, res, url, path, method, directory)
597
742
  }
598
743
  case "POST /compact":
599
744
  case "POST /summarize": {
600
- await services.body(req);
601
- const result = await compactSession(services.sessions, sessionId);
602
- if (result.kind === "error") {
603
- announceSessionError({ sessions: services.sessions, session: managed, emit: services.broadcast }, "BadRequestError", result.message);
604
- json(res, 400, errorBody("BadRequestError", result.message));
745
+ const body = (await services.body(req));
746
+ if (body.delivery !== undefined && body.delivery !== "steer" && body.delivery !== "queue")
747
+ throw new RequestBodyError(400, 'delivery must be "steer" or "queue"');
748
+ if (body.id !== undefined && (typeof body.id !== "string" || !body.id.startsWith("msg_")))
749
+ throw new RequestBodyError(400, "id must be a message ID");
750
+ const inputID = body.id ?? `msg_${randomUUID().replaceAll("-", "")}`;
751
+ const admission = services.sessions.admitCompaction(sessionId, inputID);
752
+ if (admission.kind === "conflict") {
753
+ json(res, 409, errorBody("ConflictError", "Message ID already in use"));
605
754
  return;
606
755
  }
607
- json(res, 200, { data: true });
756
+ if (admission.kind === "existing") {
757
+ json(res, 200, { data: admission.item });
758
+ return;
759
+ }
760
+ // The agent can stop before compaction_end: if the compaction is still this request's, no event closed it,
761
+ // so the client's card would stay "running" unless it is failed here.
762
+ const fail = (message) => {
763
+ if (managed.compactionID !== inputID)
764
+ return;
765
+ delete managed.compactionID;
766
+ delete managed.compactionReason;
767
+ services.broadcast(managed.info.directory, {
768
+ type: "session.compaction.failed",
769
+ properties: {
770
+ sessionID: sessionId,
771
+ inputID,
772
+ reason: "manual",
773
+ error: { name: "CompactionError", data: { message } },
774
+ },
775
+ });
776
+ };
777
+ void compactSession(services.sessions, sessionId)
778
+ .then((result) => {
779
+ if (result.kind === "error") {
780
+ fail(result.message);
781
+ announceSessionError({ sessions: services.sessions, session: managed, emit: services.broadcast }, "BadRequestError", result.message);
782
+ }
783
+ if (managed.compactionID === inputID)
784
+ delete managed.compactionID;
785
+ })
786
+ .catch((error) => {
787
+ fail(error instanceof Error ? error.message : String(error));
788
+ announceSessionError({ sessions: services.sessions, session: managed, emit: services.broadcast }, error instanceof Error ? error.name : "UnknownError", error instanceof Error ? error.message : String(error));
789
+ });
790
+ json(res, 200, { data: admission.item });
608
791
  return;
609
792
  }
610
793
  case "POST /shell": {
@@ -626,10 +809,14 @@ export async function handleV2(services, req, res, url, path, method, directory)
626
809
  }
627
810
  case "POST /command": {
628
811
  const body = (await services.body(req));
812
+ if (body.delivery !== undefined && body.delivery !== "steer" && body.delivery !== "queue")
813
+ throw new RequestBodyError(400, 'delivery must be "steer" or "queue"');
629
814
  const result = await runSessionSlash(services, sessionId, {
630
815
  command: body.name,
631
816
  arguments: body.text ?? "",
632
817
  messageID: body.id,
818
+ parts: [...parsePromptParts(body.parts), ...v2PromptParts("", body.files, body.agents)],
819
+ delivery: body.delivery,
633
820
  });
634
821
  if (result.kind === "input") {
635
822
  json(res, 400, errorBody("BadRequestError", "command is required"));
@@ -720,6 +907,10 @@ export async function handleV2(services, req, res, url, path, method, directory)
720
907
  }
721
908
  const messageID = typeof body.id === "string" && body.id !== "" ? body.id : `msg_${randomUUID().replaceAll("-", "")}`;
722
909
  const text = body.text;
910
+ if (body.description !== undefined && typeof body.description !== "string")
911
+ throw new RequestBodyError(400, "description must be a string");
912
+ if (body.description !== undefined)
913
+ (managed.tree.promptDescriptions ??= {})[messageID] = body.description;
723
914
  promptAsync(services, managed, text, undefined, true, messageID, body.delivery ?? "steer", undefined, metadata);
724
915
  // OpenChamber 2.3.2 treats anything but 200 + the admitted record as a failed send (captured OpenCode shape).
725
916
  json(res, 200, {
@@ -729,6 +920,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
729
920
  time: { created: Date.now() },
730
921
  type: "synthetic",
731
922
  payload: { text },
923
+ ...(body.description !== undefined ? { description: body.description } : {}),
732
924
  },
733
925
  });
734
926
  return;
@@ -835,7 +1027,33 @@ export async function handleV2(services, req, res, url, path, method, directory)
835
1027
  return;
836
1028
  }
837
1029
  case "GET /inbox": {
838
- json(res, 200, { data: [] });
1030
+ const messages = services.sessions.messages(sessionId) ?? [];
1031
+ json(res, 200, {
1032
+ data: [...(managed.inputs ?? [])].flatMap(([id, input]) => {
1033
+ if (!input.accepted || input.delivered)
1034
+ return [];
1035
+ const message = messages.find((item) => item.info.id === id);
1036
+ if (message?.info.role !== "user")
1037
+ return [];
1038
+ return [
1039
+ {
1040
+ id,
1041
+ sessionID: sessionId,
1042
+ time: { created: message.info.time.created },
1043
+ type: "user",
1044
+ delivery: input.delivery,
1045
+ payload: {
1046
+ text: message.parts
1047
+ .filter((part) => part.type === "text")
1048
+ .map((part) => part.text)
1049
+ .join("\n"),
1050
+ ...v2PromptAttachments(message.parts.filter((part) => part.type === "file" || part.type === "agent")),
1051
+ ...(message.info.metadata !== undefined ? { metadata: message.info.metadata } : {}),
1052
+ },
1053
+ },
1054
+ ];
1055
+ }),
1056
+ });
839
1057
  return;
840
1058
  }
841
1059
  case "GET /diff":
package/dist/server.d.ts CHANGED
@@ -19,12 +19,28 @@ export declare class AdapterServer {
19
19
  private webSocketEvents;
20
20
  private nextWebSocketEventID;
21
21
  private catalogPromise;
22
+ private catalogExpiresAt;
23
+ private catalogColdExpiresAt;
24
+ private readonly commandSnapshots;
25
+ private readonly commandColdReads;
26
+ private readonly branchSnapshots;
27
+ private readonly branchWatchers;
28
+ private readonly branchChecks;
29
+ private readonly projectDirectories;
30
+ private readonly fileWatches;
31
+ private readonly pendingRefreshes;
32
+ private readonly textGeneration;
33
+ private stopping;
22
34
  private actualPort;
23
35
  /** Bearer token for the `/antechamber/work-status` routes, published in the run record for the OmO Work extension. */
24
36
  readonly extensionToken: string;
25
37
  private readonly http;
26
38
  constructor(options: AdapterServerOptions);
27
39
  private catalog;
40
+ private commands;
41
+ private watchBranch;
42
+ private vcsBranch;
43
+ private readBranch;
28
44
  start(): Promise<void>;
29
45
  get url(): string;
30
46
  stop(): Promise<void>;