@moxt-ai/mobius 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/acp/activity.d.ts +29 -0
  2. package/dist/acp/activity.d.ts.map +1 -0
  3. package/dist/acp/activity.js +253 -0
  4. package/dist/acp/activity.js.map +1 -0
  5. package/dist/acp/create-client.d.ts +3 -0
  6. package/dist/acp/create-client.d.ts.map +1 -0
  7. package/dist/acp/create-client.js +3 -0
  8. package/dist/acp/create-client.js.map +1 -0
  9. package/dist/acp/run-prompt.d.ts +15 -0
  10. package/dist/acp/run-prompt.d.ts.map +1 -0
  11. package/dist/acp/run-prompt.js +21 -0
  12. package/dist/acp/run-prompt.js.map +1 -0
  13. package/dist/acp/session-manager.d.ts +25 -0
  14. package/dist/acp/session-manager.d.ts.map +1 -0
  15. package/dist/acp/session-manager.js +301 -0
  16. package/dist/acp/session-manager.js.map +1 -0
  17. package/dist/audit/audit-store.d.ts +12 -0
  18. package/dist/audit/audit-store.d.ts.map +1 -0
  19. package/dist/audit/audit-store.js +35 -0
  20. package/dist/audit/audit-store.js.map +1 -0
  21. package/dist/authorization/authorize-binding.d.ts +6 -0
  22. package/dist/authorization/authorize-binding.d.ts.map +1 -0
  23. package/dist/authorization/authorize-binding.js +30 -0
  24. package/dist/authorization/authorize-binding.js.map +1 -0
  25. package/dist/cli.d.ts +3 -0
  26. package/dist/cli.d.ts.map +1 -0
  27. package/dist/cli.js +1792 -0
  28. package/dist/cli.js.map +7 -0
  29. package/dist/config.d.ts +26 -0
  30. package/dist/config.d.ts.map +1 -0
  31. package/dist/config.js +85 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/index.d.ts +8 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +1784 -0
  36. package/dist/index.js.map +7 -0
  37. package/dist/pairing/pair-local-machine.d.ts +13 -0
  38. package/dist/pairing/pair-local-machine.d.ts.map +1 -0
  39. package/dist/pairing/pair-local-machine.js +302 -0
  40. package/dist/pairing/pair-local-machine.js.map +1 -0
  41. package/dist/relay-connection/connect.d.ts +2 -0
  42. package/dist/relay-connection/connect.d.ts.map +1 -0
  43. package/dist/relay-connection/connect.js +61 -0
  44. package/dist/relay-connection/connect.js.map +1 -0
  45. package/dist/run-daemon.d.ts +4 -0
  46. package/dist/run-daemon.d.ts.map +1 -0
  47. package/dist/run-daemon.js +274 -0
  48. package/dist/run-daemon.js.map +1 -0
  49. package/package.json +46 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1792 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/audit/audit-store.ts
4
+ import { mkdirSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import { DatabaseSync } from "node:sqlite";
7
+ var AuditStore = class {
8
+ database;
9
+ constructor(path) {
10
+ mkdirSync(dirname(path), { recursive: true });
11
+ this.database = new DatabaseSync(path);
12
+ this.database.exec(`
13
+ CREATE TABLE IF NOT EXISTS audit_events (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ recorded_at TEXT NOT NULL,
16
+ event TEXT NOT NULL,
17
+ command_id TEXT NOT NULL,
18
+ session_id TEXT NOT NULL
19
+ ) STRICT
20
+ `);
21
+ }
22
+ record(record) {
23
+ this.database.prepare(
24
+ `
25
+ INSERT INTO audit_events (
26
+ recorded_at,
27
+ event,
28
+ command_id,
29
+ session_id
30
+ ) VALUES (?, ?, ?, ?)
31
+ `
32
+ ).run(
33
+ (/* @__PURE__ */ new Date()).toISOString(),
34
+ record.event,
35
+ record.commandId,
36
+ record.sessionId
37
+ );
38
+ }
39
+ close() {
40
+ this.database.close();
41
+ }
42
+ };
43
+
44
+ // src/pairing/pair-local-machine.ts
45
+ import { randomUUID } from "node:crypto";
46
+ import {
47
+ mkdir,
48
+ open,
49
+ realpath,
50
+ rename,
51
+ stat,
52
+ writeFile
53
+ } from "node:fs/promises";
54
+ import { homedir, hostname } from "node:os";
55
+ import { basename, join, resolve } from "node:path";
56
+
57
+ // src/config.ts
58
+ import { fileURLToPath } from "node:url";
59
+ var repositoryRoot = fileURLToPath(new URL("../../../", import.meta.url));
60
+ function adapterEntrypoint(packageName) {
61
+ return fileURLToPath(new URL("./index.js", import.meta.resolve(packageName)));
62
+ }
63
+ var LocalAgentDefinition = class {
64
+ arguments;
65
+ command;
66
+ id;
67
+ label;
68
+ constructor(id, label, command, arguments_) {
69
+ this.id = id;
70
+ this.label = label;
71
+ this.command = command;
72
+ this.arguments = arguments_;
73
+ }
74
+ };
75
+ function loadLocalAgents() {
76
+ const overridePath = process.env["MOBIUS_ACP_AGENT_PATH"]?.trim();
77
+ if (overridePath !== void 0 && overridePath.length > 0) {
78
+ return [
79
+ new LocalAgentDefinition("codex", "Codex", process.execPath, [
80
+ overridePath,
81
+ "--agent-name",
82
+ "Codex"
83
+ ]),
84
+ new LocalAgentDefinition("claude", "Claude Code", process.execPath, [
85
+ overridePath,
86
+ "--agent-name",
87
+ "Claude Code"
88
+ ])
89
+ ];
90
+ }
91
+ return [
92
+ new LocalAgentDefinition("codex", "Codex", process.execPath, [
93
+ adapterEntrypoint("@agentclientprotocol/codex-acp")
94
+ ]),
95
+ new LocalAgentDefinition("claude", "Claude Code", process.execPath, [
96
+ adapterEntrypoint("@agentclientprotocol/claude-agent-acp")
97
+ ])
98
+ ];
99
+ }
100
+ function createDaemonConfig(connection) {
101
+ return {
102
+ agents: loadLocalAgents(),
103
+ approvedRoot: connection.approvedRoot,
104
+ auditDatabasePath: connection.auditDatabasePath,
105
+ bindingId: "workspace",
106
+ channelId: connection.channelId,
107
+ relayUrl: connection.relayUrl
108
+ };
109
+ }
110
+ function findLocalAgent(agents, agentId) {
111
+ for (const agent of agents) {
112
+ if (agent.id === agentId) {
113
+ return agent;
114
+ }
115
+ }
116
+ throw new Error("The requested local agent is not approved");
117
+ }
118
+
119
+ // src/pairing/pair-local-machine.ts
120
+ var PAIRING_TIMEOUT_MILLISECONDS = 2e4;
121
+ var PAIRING_CODE_PATTERN = /^[A-HJ-NP-Z2-9]{4}(?:-[A-HJ-NP-Z2-9]{4}){4}$/;
122
+ var IDENTIFIER_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
123
+ var TOKEN_PATTERN = /^[a-zA-Z0-9_-]{32,256}$/;
124
+ var MAX_CONNECTION_FILE_LENGTH = 16 * 1024;
125
+ var CLAIM_RESPONSE_KEYS = ["channelId", "daemonToken"];
126
+ var SAVED_CONNECTION_KEYS = [
127
+ "approvedRoot",
128
+ "channelId",
129
+ "daemonToken",
130
+ "relayOrigin"
131
+ ];
132
+ var PairingClientError = class extends Error {
133
+ code;
134
+ constructor(code, message, options = {}) {
135
+ super(message, options);
136
+ this.code = code;
137
+ this.name = "PairingClientError";
138
+ }
139
+ };
140
+ function isRecord(value) {
141
+ return typeof value === "object" && value !== null && !Array.isArray(value);
142
+ }
143
+ function hasExactKeys(value, expectedKeys) {
144
+ const keys = Object.keys(value);
145
+ return keys.length === expectedKeys.length && expectedKeys.every((key) => Object.hasOwn(value, key));
146
+ }
147
+ function readSingleOption(arguments_, option) {
148
+ const values = [];
149
+ for (let index = 0; index < arguments_.length; index += 1) {
150
+ if (arguments_[index] !== option) {
151
+ continue;
152
+ }
153
+ const value = arguments_[index + 1];
154
+ if (value === void 0 || value.startsWith("--")) {
155
+ throw new PairingClientError(
156
+ "invalid_arguments",
157
+ `${option} requires a value`
158
+ );
159
+ }
160
+ values.push(value);
161
+ index += 1;
162
+ }
163
+ if (values.length !== 1) {
164
+ throw new PairingClientError(
165
+ "invalid_arguments",
166
+ `${option} must be provided exactly once`
167
+ );
168
+ }
169
+ return values[0] ?? "";
170
+ }
171
+ function assertKnownArguments(arguments_) {
172
+ for (let index = 0; index < arguments_.length; index += 2) {
173
+ const option = arguments_[index];
174
+ if (option !== "--url" && option !== "--code" && option !== "--root") {
175
+ throw new PairingClientError(
176
+ "invalid_arguments",
177
+ `Unknown pairing option: ${option ?? "missing option"}`
178
+ );
179
+ }
180
+ }
181
+ }
182
+ function parseRelayOrigin(value) {
183
+ let url;
184
+ try {
185
+ url = new URL(value);
186
+ } catch (error) {
187
+ throw new PairingClientError(
188
+ "invalid_relay_url",
189
+ "The relay URL is invalid",
190
+ { cause: error }
191
+ );
192
+ }
193
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
194
+ throw new PairingClientError(
195
+ "invalid_relay_url",
196
+ "The relay URL must use http or https"
197
+ );
198
+ }
199
+ if (url.username.length > 0 || url.password.length > 0) {
200
+ throw new PairingClientError(
201
+ "invalid_relay_url",
202
+ "The relay URL cannot contain credentials"
203
+ );
204
+ }
205
+ if (url.protocol === "http:" && url.hostname !== "127.0.0.1" && url.hostname !== "localhost") {
206
+ throw new PairingClientError(
207
+ "invalid_relay_url",
208
+ "A remote relay must use https"
209
+ );
210
+ }
211
+ url.hash = "";
212
+ url.pathname = "/";
213
+ url.search = "";
214
+ return url;
215
+ }
216
+ function parsePairingCode(value) {
217
+ const code = value.trim().toUpperCase();
218
+ if (!PAIRING_CODE_PATTERN.test(code)) {
219
+ throw new PairingClientError(
220
+ "invalid_pairing_code",
221
+ "The pairing code must match the code shown in the browser"
222
+ );
223
+ }
224
+ return code;
225
+ }
226
+ function parsePairCommand(arguments_) {
227
+ assertKnownArguments(arguments_);
228
+ return {
229
+ code: parsePairingCode(readSingleOption(arguments_, "--code")),
230
+ relayOrigin: parseRelayOrigin(readSingleOption(arguments_, "--url")),
231
+ root: readSingleOption(arguments_, "--root")
232
+ };
233
+ }
234
+ function parseChannelArgument(arguments_) {
235
+ if (arguments_.length !== 2 || arguments_[0] !== "--channel") {
236
+ throw new PairingClientError(
237
+ "invalid_arguments",
238
+ "Usage: mobius start --channel <channel-id>"
239
+ );
240
+ }
241
+ const channelId = arguments_[1] ?? "";
242
+ if (!IDENTIFIER_PATTERN.test(channelId)) {
243
+ throw new PairingClientError(
244
+ "invalid_channel_id",
245
+ "The saved channel ID is invalid"
246
+ );
247
+ }
248
+ return channelId;
249
+ }
250
+ async function resolveApprovedRoot(path) {
251
+ const requestedRoot = resolve(path);
252
+ try {
253
+ const approvedRoot = await realpath(requestedRoot);
254
+ const metadata = await stat(approvedRoot);
255
+ if (!metadata.isDirectory()) {
256
+ throw new PairingClientError(
257
+ "invalid_workspace",
258
+ "The approved workspace root is not a directory"
259
+ );
260
+ }
261
+ return approvedRoot;
262
+ } catch (error) {
263
+ if (error instanceof PairingClientError) {
264
+ throw error;
265
+ }
266
+ throw new PairingClientError(
267
+ "invalid_workspace",
268
+ "The approved workspace root cannot be resolved",
269
+ { cause: error }
270
+ );
271
+ }
272
+ }
273
+ async function readJsonResponse(response) {
274
+ if (response.headers.get("cf-mitigated") === "challenge") {
275
+ throw new PairingClientError(
276
+ "relay_challenge",
277
+ "Cloudflare is protecting this temporary preview. Claim the deployment or use an authenticated deployment, then pair again"
278
+ );
279
+ }
280
+ const encoded = await response.text();
281
+ try {
282
+ return JSON.parse(encoded);
283
+ } catch (error) {
284
+ throw new PairingClientError(
285
+ "invalid_relay_response",
286
+ "The relay returned an invalid response",
287
+ { cause: error }
288
+ );
289
+ }
290
+ }
291
+ function readRelayError(value) {
292
+ if (isRecord(value)) {
293
+ const error = value["error"];
294
+ if (isRecord(error)) {
295
+ const code = error["code"];
296
+ const message = error["message"];
297
+ if (typeof code === "string" && typeof message === "string") {
298
+ return new PairingClientError(code, message);
299
+ }
300
+ }
301
+ }
302
+ return new PairingClientError(
303
+ "pairing_failed",
304
+ "The relay could not pair this machine"
305
+ );
306
+ }
307
+ function decodeClaimResponse(value) {
308
+ if (!isRecord(value) || !hasExactKeys(value, CLAIM_RESPONSE_KEYS)) {
309
+ throw new PairingClientError(
310
+ "invalid_relay_response",
311
+ "The relay returned an invalid pairing response"
312
+ );
313
+ }
314
+ const channelId = value["channelId"];
315
+ const daemonToken = value["daemonToken"];
316
+ if (typeof channelId !== "string" || !IDENTIFIER_PATTERN.test(channelId) || typeof daemonToken !== "string" || !TOKEN_PATTERN.test(daemonToken)) {
317
+ throw new PairingClientError(
318
+ "invalid_relay_response",
319
+ "The relay returned an invalid pairing response"
320
+ );
321
+ }
322
+ return { channelId, daemonToken };
323
+ }
324
+ async function claimPairing(options, approvedRoot) {
325
+ const agents = loadLocalAgents().map((agent) => ({
326
+ id: agent.id,
327
+ label: agent.label
328
+ }));
329
+ const endpoint = new URL("/v1/pairings/claim", options.relayOrigin);
330
+ let response;
331
+ try {
332
+ response = await fetch(endpoint, {
333
+ body: JSON.stringify({
334
+ agents,
335
+ machineName: hostname(),
336
+ pairingCode: options.code,
337
+ workspaceName: basename(approvedRoot) || "Local workspace"
338
+ }),
339
+ headers: { "content-type": "application/json" },
340
+ method: "POST",
341
+ signal: AbortSignal.timeout(PAIRING_TIMEOUT_MILLISECONDS)
342
+ });
343
+ } catch (error) {
344
+ throw new PairingClientError(
345
+ "relay_unavailable",
346
+ "The pairing relay could not be reached",
347
+ { cause: error }
348
+ );
349
+ }
350
+ const value = await readJsonResponse(response);
351
+ if (!response.ok) {
352
+ throw readRelayError(value);
353
+ }
354
+ return decodeClaimResponse(value);
355
+ }
356
+ function readStateDirectory() {
357
+ const configured = process.env["MOBIUS_STATE_DIRECTORY"]?.trim();
358
+ if (configured !== void 0 && configured.length > 0) {
359
+ return resolve(configured);
360
+ }
361
+ return join(homedir(), ".mobius");
362
+ }
363
+ async function persistConnection(channelId, relayOrigin, daemonToken, approvedRoot) {
364
+ const connectionsDirectory = join(readStateDirectory(), "connections");
365
+ await mkdir(connectionsDirectory, { mode: 448, recursive: true });
366
+ const connectionPath = join(connectionsDirectory, `${channelId}.json`);
367
+ const temporaryPath = join(
368
+ connectionsDirectory,
369
+ `.${channelId}.${randomUUID()}.tmp`
370
+ );
371
+ await writeFile(
372
+ temporaryPath,
373
+ `${JSON.stringify(
374
+ {
375
+ approvedRoot,
376
+ channelId,
377
+ daemonToken,
378
+ relayOrigin: relayOrigin.toString()
379
+ },
380
+ null,
381
+ 2
382
+ )}
383
+ `,
384
+ { flag: "wx", mode: 384 }
385
+ );
386
+ await rename(temporaryPath, connectionPath);
387
+ return connectionPath;
388
+ }
389
+ function decodeSavedConnection(value, expectedChannelId) {
390
+ if (!isRecord(value) || !hasExactKeys(value, SAVED_CONNECTION_KEYS)) {
391
+ throw new PairingClientError(
392
+ "invalid_connection",
393
+ "The saved connection is invalid"
394
+ );
395
+ }
396
+ const approvedRoot = value["approvedRoot"];
397
+ const channelId = value["channelId"];
398
+ const daemonToken = value["daemonToken"];
399
+ const relayOrigin = value["relayOrigin"];
400
+ if (typeof approvedRoot !== "string" || typeof channelId !== "string" || channelId !== expectedChannelId || !IDENTIFIER_PATTERN.test(channelId) || typeof daemonToken !== "string" || !TOKEN_PATTERN.test(daemonToken) || typeof relayOrigin !== "string") {
401
+ throw new PairingClientError(
402
+ "invalid_connection",
403
+ "The saved connection is invalid"
404
+ );
405
+ }
406
+ return {
407
+ approvedRoot,
408
+ channelId,
409
+ daemonToken,
410
+ relayOrigin: parseRelayOrigin(relayOrigin)
411
+ };
412
+ }
413
+ async function readSavedConnection(channelId) {
414
+ const connectionPath = join(
415
+ readStateDirectory(),
416
+ "connections",
417
+ `${channelId}.json`
418
+ );
419
+ let encoded = "";
420
+ const handle = await open(connectionPath, "r");
421
+ try {
422
+ const metadata = await handle.stat();
423
+ if (!metadata.isFile() || metadata.size > MAX_CONNECTION_FILE_LENGTH) {
424
+ throw new PairingClientError(
425
+ "invalid_connection",
426
+ "The saved connection is invalid"
427
+ );
428
+ }
429
+ encoded = await handle.readFile({ encoding: "utf8" });
430
+ } finally {
431
+ await handle.close();
432
+ }
433
+ let value;
434
+ try {
435
+ value = JSON.parse(encoded);
436
+ } catch (error) {
437
+ throw new PairingClientError(
438
+ "invalid_connection",
439
+ "The saved connection is invalid",
440
+ { cause: error }
441
+ );
442
+ }
443
+ const connection = decodeSavedConnection(value, channelId);
444
+ return {
445
+ approvedRoot: await resolveApprovedRoot(connection.approvedRoot),
446
+ channelId: connection.channelId,
447
+ daemonToken: connection.daemonToken,
448
+ relayOrigin: connection.relayOrigin
449
+ };
450
+ }
451
+ function buildPairedDaemon(connection, connectionPath) {
452
+ const relayUrl = new URL(
453
+ `/v1/channels/${encodeURIComponent(connection.channelId)}/daemon`,
454
+ connection.relayOrigin
455
+ );
456
+ relayUrl.protocol = relayUrl.protocol === "https:" ? "wss:" : "ws:";
457
+ relayUrl.searchParams.set("token", connection.daemonToken);
458
+ const auditDatabasePath = join(
459
+ readStateDirectory(),
460
+ "audit",
461
+ `${connection.channelId}.sqlite`
462
+ );
463
+ return {
464
+ channelUrl: new URL(
465
+ `/channels/${encodeURIComponent(connection.channelId)}`,
466
+ connection.relayOrigin
467
+ ).toString(),
468
+ config: createDaemonConfig({
469
+ approvedRoot: connection.approvedRoot,
470
+ auditDatabasePath,
471
+ channelId: connection.channelId,
472
+ relayUrl
473
+ }),
474
+ connectionPath
475
+ };
476
+ }
477
+ async function pairLocalMachine(arguments_) {
478
+ const options = parsePairCommand(arguments_);
479
+ const approvedRoot = await resolveApprovedRoot(options.root);
480
+ const claim = await claimPairing(options, approvedRoot);
481
+ const connectionPath = await persistConnection(
482
+ claim.channelId,
483
+ options.relayOrigin,
484
+ claim.daemonToken,
485
+ approvedRoot
486
+ );
487
+ return buildPairedDaemon(
488
+ {
489
+ approvedRoot,
490
+ channelId: claim.channelId,
491
+ daemonToken: claim.daemonToken,
492
+ relayOrigin: options.relayOrigin
493
+ },
494
+ connectionPath
495
+ );
496
+ }
497
+ async function resumeLocalMachine(arguments_) {
498
+ const channelId = parseChannelArgument(arguments_);
499
+ const connectionPath = join(
500
+ readStateDirectory(),
501
+ "connections",
502
+ `${channelId}.json`
503
+ );
504
+ return buildPairedDaemon(
505
+ await readSavedConnection(channelId),
506
+ connectionPath
507
+ );
508
+ }
509
+
510
+ // src/run-daemon.ts
511
+ import { randomUUID as randomUUID2 } from "node:crypto";
512
+
513
+ // ../protocol/src/messages.ts
514
+ var SESSION_PROMPT_KIND = "session.prompt";
515
+ var SESSION_CANCEL_KIND = "session.cancel";
516
+ var SESSION_STARTED_KIND = "session.started";
517
+ var AGENT_MESSAGE_DELTA_KIND = "agent.message.delta";
518
+ var AGENT_ACTIVITY_KIND = "agent.activity";
519
+ var AGENT_ACTIVITY_DETAIL_LIMIT = 24e3;
520
+ var AGENT_ACTIVITY_LOCATION_LIMIT = 12;
521
+ var AGENT_ACTIVITY_LOCATION_LENGTH_LIMIT = 1024;
522
+ var SESSION_COMPLETED_KIND = "session.completed";
523
+ var SESSION_FAILED_KIND = "session.failed";
524
+
525
+ // ../protocol/src/version.ts
526
+ var MOBIUS_PROTOCOL_VERSION = 1;
527
+
528
+ // ../protocol/src/codecs/messages.ts
529
+ var IDENTIFIER_PATTERN2 = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
530
+ var MAX_PROMPT_LENGTH = 1e4;
531
+ var ProtocolMessageError = class extends Error {
532
+ code = "invalid_protocol_message";
533
+ constructor(message, options = {}) {
534
+ super(message, options);
535
+ this.name = "ProtocolMessageError";
536
+ }
537
+ };
538
+ function isRecord2(value) {
539
+ return typeof value === "object" && value !== null && !Array.isArray(value);
540
+ }
541
+ function parseJson(encoded) {
542
+ let value;
543
+ try {
544
+ value = JSON.parse(encoded);
545
+ } catch (error) {
546
+ throw new ProtocolMessageError("Message is not valid JSON", {
547
+ cause: error
548
+ });
549
+ }
550
+ if (!isRecord2(value)) {
551
+ throw new ProtocolMessageError("Message must be a JSON object");
552
+ }
553
+ return value;
554
+ }
555
+ function assertExactKeys(value, allowedKeys) {
556
+ for (const key of Object.keys(value)) {
557
+ if (!allowedKeys.includes(key)) {
558
+ throw new ProtocolMessageError(`Unexpected message field: ${key}`);
559
+ }
560
+ }
561
+ }
562
+ function readString(value, key, maximumLength) {
563
+ const field = value[key];
564
+ if (typeof field !== "string" || field.length === 0 || field.length > maximumLength) {
565
+ throw new ProtocolMessageError(`Invalid string field: ${key}`);
566
+ }
567
+ return field;
568
+ }
569
+ function readIdentifier(value, key) {
570
+ const field = readString(value, key, 128);
571
+ if (!IDENTIFIER_PATTERN2.test(field)) {
572
+ throw new ProtocolMessageError(`Invalid identifier field: ${key}`);
573
+ }
574
+ return field;
575
+ }
576
+ function assertProtocolVersion(value) {
577
+ if (value["protocolVersion"] !== MOBIUS_PROTOCOL_VERSION) {
578
+ throw new ProtocolMessageError("Unsupported protocol version");
579
+ }
580
+ }
581
+ function assertKind(value, expectedKind) {
582
+ if (value["kind"] !== expectedKind) {
583
+ throw new ProtocolMessageError(`Expected message kind: ${expectedKind}`);
584
+ }
585
+ }
586
+ function decodeMessageKind(encoded) {
587
+ return readString(parseJson(encoded), "kind", 64);
588
+ }
589
+ function decodeSessionPromptCommand(encoded) {
590
+ const value = parseJson(encoded);
591
+ assertExactKeys(value, [
592
+ "agentId",
593
+ "bindingId",
594
+ "commandId",
595
+ "kind",
596
+ "prompt",
597
+ "protocolVersion",
598
+ "sessionId"
599
+ ]);
600
+ assertProtocolVersion(value);
601
+ assertKind(value, SESSION_PROMPT_KIND);
602
+ const prompt = readString(value, "prompt", MAX_PROMPT_LENGTH);
603
+ if (prompt.trim().length === 0) {
604
+ throw new ProtocolMessageError("Prompt must contain visible text");
605
+ }
606
+ return {
607
+ agentId: readIdentifier(value, "agentId"),
608
+ bindingId: readIdentifier(value, "bindingId"),
609
+ commandId: readIdentifier(value, "commandId"),
610
+ kind: SESSION_PROMPT_KIND,
611
+ prompt,
612
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
613
+ sessionId: readIdentifier(value, "sessionId")
614
+ };
615
+ }
616
+ function decodeSessionCancelCommand(encoded) {
617
+ const value = parseJson(encoded);
618
+ assertExactKeys(value, ["commandId", "kind", "protocolVersion", "sessionId"]);
619
+ assertProtocolVersion(value);
620
+ assertKind(value, SESSION_CANCEL_KIND);
621
+ return {
622
+ commandId: readIdentifier(value, "commandId"),
623
+ kind: SESSION_CANCEL_KIND,
624
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
625
+ sessionId: readIdentifier(value, "sessionId")
626
+ };
627
+ }
628
+ function encodeSessionStartedEvent(message) {
629
+ return JSON.stringify(message);
630
+ }
631
+ function encodeAgentMessageDeltaEvent(message) {
632
+ return JSON.stringify(message);
633
+ }
634
+ function encodeAgentActivityEvent(message) {
635
+ return JSON.stringify(message);
636
+ }
637
+ function encodeSessionCompletedEvent(message) {
638
+ return JSON.stringify(message);
639
+ }
640
+ function encodeSessionFailedEvent(message) {
641
+ return JSON.stringify(message);
642
+ }
643
+
644
+ // src/acp/session-manager.ts
645
+ import { spawn } from "node:child_process";
646
+ import { once } from "node:events";
647
+ import {
648
+ methods,
649
+ ndJsonStream,
650
+ PROTOCOL_VERSION
651
+ } from "@agentclientprotocol/sdk";
652
+
653
+ // src/acp/activity.ts
654
+ var TRUNCATION_MARKER = "\n\u2026 output truncated by Mobius";
655
+ function isRecord3(value) {
656
+ return typeof value === "object" && value !== null && !Array.isArray(value);
657
+ }
658
+ var AcpActivity = class _AcpActivity {
659
+ activityId;
660
+ activityType;
661
+ input;
662
+ locations;
663
+ name;
664
+ output;
665
+ sequence;
666
+ status;
667
+ title;
668
+ toolKind;
669
+ constructor(activityId, activityType, input, locations, name, output, sequence, status, title, toolKind) {
670
+ this.activityId = activityId;
671
+ this.activityType = activityType;
672
+ this.input = input;
673
+ this.locations = locations;
674
+ this.name = name;
675
+ this.output = output;
676
+ this.sequence = sequence;
677
+ this.status = status;
678
+ this.title = title;
679
+ this.toolKind = toolKind;
680
+ }
681
+ complete = () => (this.activityType === "progress" || this.activityType === "reasoning") && (this.status === "pending" || this.status === "running") ? new _AcpActivity(
682
+ this.activityId,
683
+ this.activityType,
684
+ this.input,
685
+ this.locations,
686
+ this.name,
687
+ this.output,
688
+ this.sequence,
689
+ "completed",
690
+ this.title,
691
+ this.toolKind
692
+ ) : this;
693
+ };
694
+ var ActivityIdentity = class {
695
+ id;
696
+ sequence;
697
+ constructor(id, sequence) {
698
+ this.id = id;
699
+ this.sequence = sequence;
700
+ }
701
+ };
702
+ function truncate(value, maximumLength) {
703
+ if (value.length <= maximumLength) {
704
+ return value;
705
+ }
706
+ return `${value.slice(0, maximumLength - TRUNCATION_MARKER.length)}${TRUNCATION_MARKER}`;
707
+ }
708
+ function formatUnknown(value) {
709
+ if (typeof value === "string") {
710
+ return truncate(value, AGENT_ACTIVITY_DETAIL_LIMIT);
711
+ }
712
+ try {
713
+ const encoded = JSON.stringify(value, null, 2);
714
+ return truncate(encoded ?? "", AGENT_ACTIVITY_DETAIL_LIMIT);
715
+ } catch {
716
+ return truncate(String(value), AGENT_ACTIVITY_DETAIL_LIMIT);
717
+ }
718
+ }
719
+ function readContentOutput(content) {
720
+ if (!Array.isArray(content) || content.length === 0) {
721
+ return "";
722
+ }
723
+ return formatUnknown(content);
724
+ }
725
+ function normalizeLocations(locations) {
726
+ const normalized = [];
727
+ const unique = /* @__PURE__ */ new Set();
728
+ for (const location of locations) {
729
+ if (normalized.length === AGENT_ACTIVITY_LOCATION_LIMIT) {
730
+ break;
731
+ }
732
+ const suffix = typeof location.line === "number" ? `:${location.line.toString()}` : "";
733
+ const value = truncate(
734
+ `${location.path}${suffix}`,
735
+ AGENT_ACTIVITY_LOCATION_LENGTH_LIMIT
736
+ );
737
+ if (value.length > 0 && !unique.has(value)) {
738
+ unique.add(value);
739
+ normalized.push(value);
740
+ }
741
+ }
742
+ return normalized;
743
+ }
744
+ function normalizeToolStatus(status, fallback) {
745
+ if (status === "pending") {
746
+ return "pending";
747
+ }
748
+ if (status === "in_progress") {
749
+ return "running";
750
+ }
751
+ if (status === "completed" || status === "failed") {
752
+ return status;
753
+ }
754
+ return fallback;
755
+ }
756
+ function normalizeTitle(title, fallback) {
757
+ const normalized = title.trim();
758
+ return truncate(normalized.length === 0 ? fallback : normalized, 500);
759
+ }
760
+ function formatPlanEntries(entries) {
761
+ const lines = [];
762
+ for (const entry of entries) {
763
+ lines.push(`[${entry.status}] ${entry.content}`);
764
+ }
765
+ return truncate(lines.join("\n"), AGENT_ACTIVITY_DETAIL_LIMIT);
766
+ }
767
+ function planStatus(entries) {
768
+ if (entries.length === 0) {
769
+ return "completed";
770
+ }
771
+ if (entries.every((entry) => entry.status === "completed")) {
772
+ return "completed";
773
+ }
774
+ if (entries.some((entry) => entry.status === "in_progress")) {
775
+ return "running";
776
+ }
777
+ return "pending";
778
+ }
779
+ function contentPhase(content) {
780
+ const metadata = content._meta;
781
+ if (metadata === void 0 || metadata === null) {
782
+ return "";
783
+ }
784
+ const codex = metadata["codex"];
785
+ if (!isRecord3(codex)) {
786
+ return "";
787
+ }
788
+ const phase = codex["phase"];
789
+ return typeof phase === "string" ? phase : "";
790
+ }
791
+ function isProgressMessage(content) {
792
+ return contentPhase(content) === "commentary";
793
+ }
794
+ var AcpActivityTracker = class {
795
+ #activities = /* @__PURE__ */ new Map();
796
+ #identities = /* @__PURE__ */ new Map();
797
+ #onActivity;
798
+ #activeNarrativeKey = "";
799
+ #nextSequence = 0;
800
+ constructor(onActivity) {
801
+ this.#onActivity = onActivity;
802
+ }
803
+ recordNarrative = async (activityType, messageId, text) => {
804
+ const key = `${activityType}:${messageId}`;
805
+ if (this.#activeNarrativeKey !== key) {
806
+ await this.finishNarrative();
807
+ }
808
+ const previous = this.#activities.get(key);
809
+ const identity = this.#identity(key);
810
+ const activity = new AcpActivity(
811
+ identity.id,
812
+ activityType,
813
+ "",
814
+ [],
815
+ "",
816
+ truncate(`${previous?.output ?? ""}${text}`, AGENT_ACTIVITY_DETAIL_LIMIT),
817
+ identity.sequence,
818
+ "running",
819
+ activityType === "reasoning" ? "Reasoning" : "Progress update",
820
+ activityType === "reasoning" ? "think" : ""
821
+ );
822
+ await this.#record(key, activity);
823
+ this.#activeNarrativeKey = key;
824
+ };
825
+ recordPlan = async (plan) => {
826
+ await this.finishNarrative();
827
+ await this.#recordPlanEntries("plan:default", plan.entries);
828
+ };
829
+ recordPlanUpdate = async (update) => {
830
+ await this.finishNarrative();
831
+ const plan = update.plan;
832
+ const key = `plan:${plan.planId}`;
833
+ if (plan.type === "items") {
834
+ await this.#recordPlanEntries(key, plan.entries);
835
+ return;
836
+ }
837
+ const identity = this.#identity(key);
838
+ const output = plan.type === "markdown" ? plan.content : `Plan file: ${plan.uri}`;
839
+ await this.#record(
840
+ key,
841
+ new AcpActivity(
842
+ identity.id,
843
+ "plan",
844
+ "",
845
+ [],
846
+ "",
847
+ truncate(output, AGENT_ACTIVITY_DETAIL_LIMIT),
848
+ identity.sequence,
849
+ "running",
850
+ "Plan",
851
+ ""
852
+ )
853
+ );
854
+ };
855
+ removePlan = async (update) => {
856
+ await this.finishNarrative();
857
+ const key = `plan:${update.planId}`;
858
+ const previous = this.#activities.get(key);
859
+ if (previous !== void 0) {
860
+ await this.#record(
861
+ key,
862
+ new AcpActivity(
863
+ previous.activityId,
864
+ previous.activityType,
865
+ previous.input,
866
+ previous.locations,
867
+ previous.name,
868
+ previous.output,
869
+ previous.sequence,
870
+ "completed",
871
+ previous.title,
872
+ previous.toolKind
873
+ )
874
+ );
875
+ }
876
+ };
877
+ recordToolCall = async (update) => {
878
+ await this.finishNarrative();
879
+ const key = `tool:${update.toolCallId}`;
880
+ const identity = this.#identity(key);
881
+ const output = update.rawOutput === void 0 ? readContentOutput(update.content) : formatUnknown(update.rawOutput);
882
+ await this.#record(
883
+ key,
884
+ new AcpActivity(
885
+ identity.id,
886
+ "tool",
887
+ update.rawInput === void 0 ? "" : formatUnknown(update.rawInput),
888
+ normalizeLocations(update.locations ?? []),
889
+ truncate(update.name ?? "", 128),
890
+ output,
891
+ identity.sequence,
892
+ normalizeToolStatus(update.status ?? "pending", "pending"),
893
+ normalizeTitle(update.title, "Tool call"),
894
+ update.kind ?? "other"
895
+ )
896
+ );
897
+ };
898
+ recordToolCallUpdate = async (update) => {
899
+ await this.finishNarrative();
900
+ const key = `tool:${update.toolCallId}`;
901
+ const previous = this.#activities.get(key);
902
+ const identity = this.#identity(key);
903
+ const output = update.rawOutput !== void 0 ? formatUnknown(update.rawOutput) : update.content !== void 0 && update.content !== null ? readContentOutput(update.content) : previous?.output ?? "";
904
+ await this.#record(
905
+ key,
906
+ new AcpActivity(
907
+ identity.id,
908
+ "tool",
909
+ update.rawInput === void 0 ? previous?.input ?? "" : formatUnknown(update.rawInput),
910
+ update.locations === void 0 || update.locations === null ? previous?.locations ?? [] : normalizeLocations(update.locations),
911
+ update.name === void 0 || update.name === null ? previous?.name ?? "" : truncate(update.name, 128),
912
+ output,
913
+ identity.sequence,
914
+ update.status === void 0 || update.status === null ? previous?.status ?? "pending" : normalizeToolStatus(update.status, previous?.status ?? "pending"),
915
+ update.title === void 0 || update.title === null ? previous?.title ?? "Tool call" : normalizeTitle(update.title, "Tool call"),
916
+ update.kind === void 0 || update.kind === null ? previous?.toolKind ?? "other" : update.kind
917
+ )
918
+ );
919
+ };
920
+ finishNarrative = async () => {
921
+ const key = this.#activeNarrativeKey;
922
+ if (key.length === 0) {
923
+ return;
924
+ }
925
+ this.#activeNarrativeKey = "";
926
+ const activity = this.#activities.get(key);
927
+ if (activity === void 0) {
928
+ return;
929
+ }
930
+ const completed = activity.complete();
931
+ if (completed !== activity) {
932
+ await this.#record(key, completed);
933
+ }
934
+ };
935
+ finish = async () => {
936
+ await this.finishNarrative();
937
+ const entries = [...this.#activities.entries()].sort(
938
+ (left, right) => left[1].sequence - right[1].sequence
939
+ );
940
+ for (const [key, activity] of entries) {
941
+ const completed = activity.complete();
942
+ if (completed !== activity) {
943
+ await this.#record(key, completed);
944
+ }
945
+ }
946
+ };
947
+ #identity = (key) => {
948
+ const existing = this.#identities.get(key);
949
+ if (existing !== void 0) {
950
+ return existing;
951
+ }
952
+ const identity = new ActivityIdentity(
953
+ `activity-${crypto.randomUUID()}`,
954
+ this.#nextSequence
955
+ );
956
+ this.#nextSequence += 1;
957
+ this.#identities.set(key, identity);
958
+ return identity;
959
+ };
960
+ #record = async (key, activity) => {
961
+ this.#activities.set(key, activity);
962
+ await this.#onActivity(activity);
963
+ };
964
+ #recordPlanEntries = async (key, entries) => {
965
+ const identity = this.#identity(key);
966
+ await this.#record(
967
+ key,
968
+ new AcpActivity(
969
+ identity.id,
970
+ "plan",
971
+ "",
972
+ [],
973
+ "",
974
+ formatPlanEntries(entries),
975
+ identity.sequence,
976
+ planStatus(entries),
977
+ "Plan",
978
+ ""
979
+ )
980
+ );
981
+ };
982
+ };
983
+
984
+ // src/acp/create-client.ts
985
+ import { client } from "@agentclientprotocol/sdk";
986
+ var createAcpClient = () => client({ name: "mobius" });
987
+
988
+ // src/acp/session-manager.ts
989
+ var PROCESS_STOP_TIMEOUT_MILLISECONDS = 1e3;
990
+ var AcpExecutionError = class extends Error {
991
+ code = "acp_execution_failed";
992
+ constructor(message, options = {}) {
993
+ super(message, options);
994
+ this.name = "AcpExecutionError";
995
+ }
996
+ };
997
+ function readableWebStream(input) {
998
+ let streamOpen = true;
999
+ return new ReadableStream({
1000
+ start(controller) {
1001
+ input.on("data", (chunk) => {
1002
+ if (!streamOpen) {
1003
+ return;
1004
+ }
1005
+ if (chunk instanceof Uint8Array) {
1006
+ controller.enqueue(new Uint8Array(chunk));
1007
+ return;
1008
+ }
1009
+ streamOpen = false;
1010
+ controller.error(new Error("ACP process produced a non-binary chunk"));
1011
+ });
1012
+ input.once("end", () => {
1013
+ if (!streamOpen) {
1014
+ return;
1015
+ }
1016
+ streamOpen = false;
1017
+ controller.close();
1018
+ });
1019
+ input.once("error", (error) => {
1020
+ if (!streamOpen) {
1021
+ return;
1022
+ }
1023
+ streamOpen = false;
1024
+ controller.error(error);
1025
+ });
1026
+ },
1027
+ cancel() {
1028
+ streamOpen = false;
1029
+ }
1030
+ });
1031
+ }
1032
+ function writableWebStream(output) {
1033
+ return new WritableStream({
1034
+ write(chunk) {
1035
+ return new Promise((resolve2, reject) => {
1036
+ output.write(chunk, (error) => {
1037
+ if (error) {
1038
+ reject(error);
1039
+ return;
1040
+ }
1041
+ resolve2();
1042
+ });
1043
+ });
1044
+ }
1045
+ });
1046
+ }
1047
+ async function stopAgentProcess(child) {
1048
+ child.stdin?.end();
1049
+ if (child.exitCode !== null) {
1050
+ return;
1051
+ }
1052
+ child.kill("SIGTERM");
1053
+ try {
1054
+ await once(child, "exit", {
1055
+ signal: AbortSignal.timeout(PROCESS_STOP_TIMEOUT_MILLISECONDS)
1056
+ });
1057
+ } catch {
1058
+ if (child.exitCode === null) {
1059
+ child.kill("SIGKILL");
1060
+ await once(child, "exit");
1061
+ }
1062
+ }
1063
+ }
1064
+ function rejectWhenAborted(signal) {
1065
+ return new Promise((_resolve, reject) => {
1066
+ const rejectAbort = () => {
1067
+ reject(signal.reason ?? new Error("ACP request was cancelled"));
1068
+ };
1069
+ signal.addEventListener("abort", rejectAbort, { once: true });
1070
+ if (signal.aborted) {
1071
+ rejectAbort();
1072
+ }
1073
+ });
1074
+ }
1075
+ var AcpProcessSession = class _AcpProcessSession {
1076
+ #agentId;
1077
+ #child;
1078
+ #connection;
1079
+ #processExit;
1080
+ #session;
1081
+ #turnRunning = false;
1082
+ constructor(agentId, child, connection, session, processExit) {
1083
+ this.#agentId = agentId;
1084
+ this.#child = child;
1085
+ this.#connection = connection;
1086
+ this.#session = session;
1087
+ this.#processExit = processExit;
1088
+ }
1089
+ static async start(agent, cwd, signal) {
1090
+ signal.throwIfAborted();
1091
+ const child = spawn(agent.command, [...agent.arguments], {
1092
+ cwd,
1093
+ stdio: ["pipe", "pipe", "pipe"]
1094
+ });
1095
+ child.stderr.setEncoding("utf8");
1096
+ child.stderr.on("data", (chunk) => {
1097
+ process.stderr.write(`[${agent.id}-acp] ${chunk}`);
1098
+ });
1099
+ const processExit = new Promise((resolve2) => {
1100
+ child.once("error", resolve2);
1101
+ child.once("exit", resolve2);
1102
+ });
1103
+ const stream = ndJsonStream(
1104
+ writableWebStream(child.stdin),
1105
+ readableWebStream(child.stdout)
1106
+ );
1107
+ const connection = createAcpClient().connect(stream);
1108
+ try {
1109
+ await Promise.race([
1110
+ connection.agent.request(
1111
+ methods.agent.initialize,
1112
+ {
1113
+ clientCapabilities: {},
1114
+ protocolVersion: PROTOCOL_VERSION
1115
+ },
1116
+ { cancellationSignal: signal }
1117
+ ),
1118
+ processExit.then(() => {
1119
+ throw new AcpExecutionError("ACP agent exited during initialization");
1120
+ }),
1121
+ rejectWhenAborted(signal)
1122
+ ]);
1123
+ const session = await Promise.race([
1124
+ connection.agent.buildSession(cwd).start({ cancellationSignal: signal }),
1125
+ processExit.then(() => {
1126
+ throw new AcpExecutionError(
1127
+ "ACP agent exited while starting a session"
1128
+ );
1129
+ }),
1130
+ rejectWhenAborted(signal)
1131
+ ]);
1132
+ return new _AcpProcessSession(
1133
+ agent.id,
1134
+ child,
1135
+ connection,
1136
+ session,
1137
+ processExit
1138
+ );
1139
+ } catch (error) {
1140
+ connection.close(error);
1141
+ await stopAgentProcess(child);
1142
+ throw new AcpExecutionError("ACP agent initialization failed", {
1143
+ cause: error
1144
+ });
1145
+ }
1146
+ }
1147
+ belongsTo = (agentId) => this.#agentId === agentId;
1148
+ cancel = async () => {
1149
+ if (!this.#turnRunning) {
1150
+ return;
1151
+ }
1152
+ await this.#connection.agent.notify(methods.agent.session.cancel, {
1153
+ sessionId: this.#session.sessionId
1154
+ });
1155
+ };
1156
+ prompt = async (prompt, onActivity, onText, signal) => {
1157
+ if (this.#turnRunning) {
1158
+ throw new AcpExecutionError(
1159
+ "Another prompt is already running in this conversation"
1160
+ );
1161
+ }
1162
+ this.#turnRunning = true;
1163
+ const activities = new AcpActivityTracker(onActivity);
1164
+ const runTurn = async () => {
1165
+ const completion = this.#session.prompt(prompt, {
1166
+ cancellationSignal: signal
1167
+ });
1168
+ for (; ; ) {
1169
+ const message = await this.#session.nextUpdate();
1170
+ if (message.kind === "stop") {
1171
+ await completion;
1172
+ await activities.finish();
1173
+ return { stopReason: message.stopReason };
1174
+ }
1175
+ const update = message.update;
1176
+ if (update.sessionUpdate === "agent_message_chunk") {
1177
+ if (update.content.type === "text") {
1178
+ if (isProgressMessage(update)) {
1179
+ await activities.recordNarrative(
1180
+ "progress",
1181
+ update.messageId ?? "current",
1182
+ update.content.text
1183
+ );
1184
+ } else {
1185
+ await activities.finishNarrative();
1186
+ await onText(update.content.text);
1187
+ }
1188
+ }
1189
+ continue;
1190
+ }
1191
+ if (update.sessionUpdate === "agent_thought_chunk") {
1192
+ if (update.content.type === "text") {
1193
+ await activities.recordNarrative(
1194
+ "reasoning",
1195
+ update.messageId ?? "current",
1196
+ update.content.text
1197
+ );
1198
+ }
1199
+ continue;
1200
+ }
1201
+ if (update.sessionUpdate === "tool_call") {
1202
+ await activities.recordToolCall(update);
1203
+ continue;
1204
+ }
1205
+ if (update.sessionUpdate === "tool_call_update") {
1206
+ await activities.recordToolCallUpdate(update);
1207
+ continue;
1208
+ }
1209
+ if (update.sessionUpdate === "plan") {
1210
+ await activities.recordPlan(update);
1211
+ continue;
1212
+ }
1213
+ if (update.sessionUpdate === "plan_update") {
1214
+ await activities.recordPlanUpdate(update);
1215
+ continue;
1216
+ }
1217
+ if (update.sessionUpdate === "plan_removed") {
1218
+ await activities.removePlan(update);
1219
+ }
1220
+ }
1221
+ };
1222
+ try {
1223
+ return await Promise.race([
1224
+ runTurn(),
1225
+ this.#processExit.then(() => {
1226
+ throw new AcpExecutionError("ACP agent exited during a prompt");
1227
+ }),
1228
+ rejectWhenAborted(signal)
1229
+ ]);
1230
+ } catch (error) {
1231
+ await activities.finish();
1232
+ throw new AcpExecutionError("ACP agent execution failed", {
1233
+ cause: error
1234
+ });
1235
+ } finally {
1236
+ this.#turnRunning = false;
1237
+ }
1238
+ };
1239
+ close = async () => {
1240
+ this.#session.dispose();
1241
+ this.#connection.close();
1242
+ await stopAgentProcess(this.#child);
1243
+ };
1244
+ };
1245
+ var AcpSessionManager = class {
1246
+ #activeSessionIds = /* @__PURE__ */ new Set();
1247
+ #cancellationRequests = /* @__PURE__ */ new Set();
1248
+ #sessions = /* @__PURE__ */ new Map();
1249
+ cancelPrompt = async (sessionId) => {
1250
+ this.#cancellationRequests.add(sessionId);
1251
+ const runtime = this.#sessions.get(sessionId);
1252
+ if (runtime !== void 0) {
1253
+ await runtime.cancel();
1254
+ }
1255
+ };
1256
+ runPrompt = async (options) => {
1257
+ if (this.#activeSessionIds.has(options.sessionId)) {
1258
+ throw new AcpExecutionError(
1259
+ "Another prompt is already running in this conversation"
1260
+ );
1261
+ }
1262
+ this.#activeSessionIds.add(options.sessionId);
1263
+ let runtime = this.#sessions.get(options.sessionId);
1264
+ if (runtime !== void 0 && !runtime.belongsTo(options.agent.id)) {
1265
+ this.#activeSessionIds.delete(options.sessionId);
1266
+ throw new AcpExecutionError(
1267
+ "The agent cannot change after a conversation has started"
1268
+ );
1269
+ }
1270
+ try {
1271
+ if (this.#cancellationRequests.has(options.sessionId)) {
1272
+ return { stopReason: "cancelled" };
1273
+ }
1274
+ if (runtime === void 0) {
1275
+ runtime = await AcpProcessSession.start(
1276
+ options.agent,
1277
+ options.cwd,
1278
+ options.signal
1279
+ );
1280
+ this.#sessions.set(options.sessionId, runtime);
1281
+ }
1282
+ if (this.#cancellationRequests.has(options.sessionId)) {
1283
+ return { stopReason: "cancelled" };
1284
+ }
1285
+ return await runtime.prompt(
1286
+ options.prompt,
1287
+ options.onActivity,
1288
+ options.onText,
1289
+ options.signal
1290
+ );
1291
+ } catch (error) {
1292
+ if (runtime !== void 0) {
1293
+ this.#sessions.delete(options.sessionId);
1294
+ await runtime.close();
1295
+ }
1296
+ if (error instanceof AcpExecutionError) {
1297
+ throw error;
1298
+ }
1299
+ throw new AcpExecutionError("ACP agent execution failed", {
1300
+ cause: error
1301
+ });
1302
+ } finally {
1303
+ this.#activeSessionIds.delete(options.sessionId);
1304
+ this.#cancellationRequests.delete(options.sessionId);
1305
+ }
1306
+ };
1307
+ close = async () => {
1308
+ const sessions = [...this.#sessions.values()];
1309
+ this.#sessions.clear();
1310
+ await Promise.all(sessions.map(async (session) => await session.close()));
1311
+ };
1312
+ };
1313
+
1314
+ // src/authorization/authorize-binding.ts
1315
+ import { realpath as realpath2, stat as stat2 } from "node:fs/promises";
1316
+ var LocalAuthorizationError = class extends Error {
1317
+ code = "local_authorization_failed";
1318
+ constructor(message, options = {}) {
1319
+ super(message, options);
1320
+ this.name = "LocalAuthorizationError";
1321
+ }
1322
+ };
1323
+ async function authorizeBinding(requestedBindingId, approvedBindingId, approvedRoot) {
1324
+ if (requestedBindingId !== approvedBindingId) {
1325
+ throw new LocalAuthorizationError("The runtime binding is not authorized");
1326
+ }
1327
+ try {
1328
+ const resolvedRoot = await realpath2(approvedRoot);
1329
+ const metadata = await stat2(resolvedRoot);
1330
+ if (!metadata.isDirectory()) {
1331
+ throw new LocalAuthorizationError("The approved root is not a directory");
1332
+ }
1333
+ return resolvedRoot;
1334
+ } catch (error) {
1335
+ if (error instanceof LocalAuthorizationError) {
1336
+ throw error;
1337
+ }
1338
+ throw new LocalAuthorizationError("The approved root cannot be resolved", {
1339
+ cause: error
1340
+ });
1341
+ }
1342
+ }
1343
+
1344
+ // src/relay-connection/connect.ts
1345
+ var CONNECTION_TIMEOUT_MILLISECONDS = 15e3;
1346
+ var RelayConnectionError = class extends Error {
1347
+ code;
1348
+ constructor(code, message, options = {}) {
1349
+ super(message, options);
1350
+ this.code = code;
1351
+ this.name = "RelayConnectionError";
1352
+ }
1353
+ };
1354
+ function connectRelay(url, signal) {
1355
+ signal.throwIfAborted();
1356
+ return new Promise((resolve2, reject) => {
1357
+ const socket = new WebSocket(url);
1358
+ let finished = false;
1359
+ const timeout = setTimeout(onTimeout, CONNECTION_TIMEOUT_MILLISECONDS);
1360
+ const cleanup = () => {
1361
+ clearTimeout(timeout);
1362
+ signal.removeEventListener("abort", onAbort);
1363
+ socket.removeEventListener("close", onClose);
1364
+ socket.removeEventListener("error", onError);
1365
+ socket.removeEventListener("open", onOpen);
1366
+ };
1367
+ const fail = (error) => {
1368
+ if (finished) {
1369
+ return;
1370
+ }
1371
+ finished = true;
1372
+ cleanup();
1373
+ socket.close();
1374
+ reject(error);
1375
+ };
1376
+ const onAbort = () => {
1377
+ fail(
1378
+ new RelayConnectionError(
1379
+ "relay_connection_cancelled",
1380
+ "Relay connection was cancelled",
1381
+ { cause: signal.reason }
1382
+ )
1383
+ );
1384
+ };
1385
+ const onClose = () => {
1386
+ fail(
1387
+ new RelayConnectionError(
1388
+ "relay_connection_closed",
1389
+ "Relay connection closed before it was ready"
1390
+ )
1391
+ );
1392
+ };
1393
+ const onError = () => {
1394
+ fail(
1395
+ new RelayConnectionError(
1396
+ "relay_connection_failed",
1397
+ "Relay connection failed"
1398
+ )
1399
+ );
1400
+ };
1401
+ const onOpen = () => {
1402
+ if (finished) {
1403
+ return;
1404
+ }
1405
+ finished = true;
1406
+ cleanup();
1407
+ resolve2(socket);
1408
+ };
1409
+ function onTimeout() {
1410
+ fail(
1411
+ new RelayConnectionError(
1412
+ "relay_connection_timeout",
1413
+ "Relay connection timed out"
1414
+ )
1415
+ );
1416
+ }
1417
+ signal.addEventListener("abort", onAbort, { once: true });
1418
+ socket.addEventListener("close", onClose, { once: true });
1419
+ socket.addEventListener("error", onError, { once: true });
1420
+ socket.addEventListener("open", onOpen, { once: true });
1421
+ if (signal.aborted) {
1422
+ onAbort();
1423
+ }
1424
+ });
1425
+ }
1426
+
1427
+ // src/run-daemon.ts
1428
+ var RELAY_RECONNECT_DELAY_MILLISECONDS = 500;
1429
+ var ActiveDaemonTurn = class {
1430
+ command;
1431
+ #connectionController;
1432
+ #cancellationRequested = false;
1433
+ constructor(command, connectionController) {
1434
+ this.command = command;
1435
+ this.#connectionController = connectionController;
1436
+ }
1437
+ abortConnection = () => {
1438
+ this.#connectionController.abort();
1439
+ };
1440
+ matches = (cancellation) => cancellation.commandId === this.command.commandId && cancellation.sessionId === this.command.sessionId;
1441
+ requestCancellation = () => {
1442
+ if (this.#cancellationRequested) {
1443
+ return false;
1444
+ }
1445
+ this.#cancellationRequested = true;
1446
+ return true;
1447
+ };
1448
+ get signal() {
1449
+ return this.#connectionController.signal;
1450
+ }
1451
+ };
1452
+ function send(socket, message) {
1453
+ if (socket.readyState !== WebSocket.OPEN) {
1454
+ throw new Error("Relay connection is not open");
1455
+ }
1456
+ socket.send(message);
1457
+ }
1458
+ function reportFailure(socket, command, code, message) {
1459
+ if (socket.readyState !== WebSocket.OPEN) {
1460
+ return;
1461
+ }
1462
+ send(
1463
+ socket,
1464
+ encodeSessionFailedEvent({
1465
+ code,
1466
+ commandId: command.commandId,
1467
+ kind: SESSION_FAILED_KIND,
1468
+ message,
1469
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
1470
+ sessionId: command.sessionId
1471
+ })
1472
+ );
1473
+ }
1474
+ async function executePrompt(socket, command, config, audit, sessions, signal) {
1475
+ try {
1476
+ const approvedRoot = await authorizeBinding(
1477
+ command.bindingId,
1478
+ config.bindingId,
1479
+ config.approvedRoot
1480
+ );
1481
+ const agent = findLocalAgent(config.agents, command.agentId);
1482
+ const processAttemptId = randomUUID2();
1483
+ audit.record({
1484
+ commandId: command.commandId,
1485
+ event: "session_authorized",
1486
+ sessionId: command.sessionId
1487
+ });
1488
+ send(
1489
+ socket,
1490
+ encodeSessionStartedEvent({
1491
+ commandId: command.commandId,
1492
+ kind: SESSION_STARTED_KIND,
1493
+ processAttemptId,
1494
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
1495
+ sessionId: command.sessionId
1496
+ })
1497
+ );
1498
+ const result = await sessions.runPrompt({
1499
+ agent,
1500
+ cwd: approvedRoot,
1501
+ onActivity: (activity) => {
1502
+ send(
1503
+ socket,
1504
+ encodeAgentActivityEvent({
1505
+ activityId: activity.activityId,
1506
+ activityType: activity.activityType,
1507
+ commandId: command.commandId,
1508
+ input: activity.input,
1509
+ kind: AGENT_ACTIVITY_KIND,
1510
+ locations: activity.locations,
1511
+ name: activity.name,
1512
+ output: activity.output,
1513
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
1514
+ sequence: activity.sequence,
1515
+ sessionId: command.sessionId,
1516
+ status: activity.status,
1517
+ title: activity.title,
1518
+ toolKind: activity.toolKind
1519
+ })
1520
+ );
1521
+ return Promise.resolve();
1522
+ },
1523
+ onText: (text) => {
1524
+ send(
1525
+ socket,
1526
+ encodeAgentMessageDeltaEvent({
1527
+ commandId: command.commandId,
1528
+ kind: AGENT_MESSAGE_DELTA_KIND,
1529
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
1530
+ sessionId: command.sessionId,
1531
+ text
1532
+ })
1533
+ );
1534
+ return Promise.resolve();
1535
+ },
1536
+ prompt: command.prompt,
1537
+ sessionId: command.sessionId,
1538
+ signal
1539
+ });
1540
+ send(
1541
+ socket,
1542
+ encodeSessionCompletedEvent({
1543
+ commandId: command.commandId,
1544
+ kind: SESSION_COMPLETED_KIND,
1545
+ protocolVersion: MOBIUS_PROTOCOL_VERSION,
1546
+ sessionId: command.sessionId,
1547
+ stopReason: result.stopReason
1548
+ })
1549
+ );
1550
+ audit.record({
1551
+ commandId: command.commandId,
1552
+ event: result.stopReason === "cancelled" ? "session_cancelled" : "session_completed",
1553
+ sessionId: command.sessionId
1554
+ });
1555
+ } catch (error) {
1556
+ audit.record({
1557
+ commandId: command.commandId,
1558
+ event: "session_failed",
1559
+ sessionId: command.sessionId
1560
+ });
1561
+ reportFailure(
1562
+ socket,
1563
+ command,
1564
+ "agent_execution_failed",
1565
+ "The local agent could not complete the request"
1566
+ );
1567
+ console.error("Mobius daemon command failed", error);
1568
+ }
1569
+ }
1570
+ function waitForRelayRetry(signal) {
1571
+ if (signal.aborted) {
1572
+ return Promise.resolve();
1573
+ }
1574
+ return new Promise((resolve2) => {
1575
+ const timeout = setTimeout(finish, RELAY_RECONNECT_DELAY_MILLISECONDS);
1576
+ function finish() {
1577
+ clearTimeout(timeout);
1578
+ signal.removeEventListener("abort", finish);
1579
+ resolve2();
1580
+ }
1581
+ signal.addEventListener("abort", finish, { once: true });
1582
+ });
1583
+ }
1584
+ async function runRelayConnection(config, audit, sessions, signal) {
1585
+ const socket = await connectRelay(config.relayUrl, signal);
1586
+ const activeTurns = /* @__PURE__ */ new Map();
1587
+ await new Promise((resolve2, reject) => {
1588
+ let finished = false;
1589
+ const cleanup = () => {
1590
+ signal.removeEventListener("abort", stop);
1591
+ socket.removeEventListener("close", handleClose);
1592
+ socket.removeEventListener("error", handleError);
1593
+ socket.removeEventListener("message", handleMessage);
1594
+ for (const turn of activeTurns.values()) {
1595
+ turn.abortConnection();
1596
+ }
1597
+ activeTurns.clear();
1598
+ };
1599
+ const resolveConnection = () => {
1600
+ if (finished) {
1601
+ return;
1602
+ }
1603
+ finished = true;
1604
+ cleanup();
1605
+ resolve2();
1606
+ };
1607
+ const rejectConnection = () => {
1608
+ if (finished) {
1609
+ return;
1610
+ }
1611
+ finished = true;
1612
+ cleanup();
1613
+ reject(new Error("Relay WebSocket failed"));
1614
+ };
1615
+ const stop = () => {
1616
+ socket.close(1e3, "Daemon stopped");
1617
+ resolveConnection();
1618
+ };
1619
+ signal.addEventListener("abort", stop, { once: true });
1620
+ const handleClose = () => {
1621
+ resolveConnection();
1622
+ };
1623
+ const handleError = () => {
1624
+ if (signal.aborted) {
1625
+ resolveConnection();
1626
+ return;
1627
+ }
1628
+ rejectConnection();
1629
+ };
1630
+ const handleMessage = (event) => {
1631
+ const encoded = event.data;
1632
+ if (typeof encoded !== "string") {
1633
+ socket.close(1003, "Text message required");
1634
+ return;
1635
+ }
1636
+ try {
1637
+ const kind = decodeMessageKind(encoded);
1638
+ if (kind === SESSION_CANCEL_KIND) {
1639
+ const cancellation = decodeSessionCancelCommand(encoded);
1640
+ const turn2 = activeTurns.get(cancellation.commandId);
1641
+ if (turn2 === void 0 || !turn2.matches(cancellation)) {
1642
+ console.warn(
1643
+ "Mobius daemon ignored cancellation for an inactive turn"
1644
+ );
1645
+ return;
1646
+ }
1647
+ if (!turn2.requestCancellation()) {
1648
+ return;
1649
+ }
1650
+ audit.record({
1651
+ commandId: cancellation.commandId,
1652
+ event: "session_cancellation_requested",
1653
+ sessionId: cancellation.sessionId
1654
+ });
1655
+ void sessions.cancelPrompt(cancellation.sessionId).catch((error) => {
1656
+ console.error(
1657
+ "Mobius daemon could not cancel the agent turn",
1658
+ error
1659
+ );
1660
+ });
1661
+ return;
1662
+ }
1663
+ if (kind !== SESSION_PROMPT_KIND) {
1664
+ socket.close(1008, "Unsupported protocol message");
1665
+ return;
1666
+ }
1667
+ const command = decodeSessionPromptCommand(encoded);
1668
+ if (activeTurns.has(command.commandId)) {
1669
+ reportFailure(
1670
+ socket,
1671
+ command,
1672
+ "duplicate_command",
1673
+ "The command is already running"
1674
+ );
1675
+ return;
1676
+ }
1677
+ const turn = new ActiveDaemonTurn(command, new AbortController());
1678
+ activeTurns.set(command.commandId, turn);
1679
+ void executePrompt(
1680
+ socket,
1681
+ command,
1682
+ config,
1683
+ audit,
1684
+ sessions,
1685
+ turn.signal
1686
+ ).catch((error) => {
1687
+ console.error("Mobius daemon turn failed", error);
1688
+ }).finally(() => {
1689
+ activeTurns.delete(command.commandId);
1690
+ });
1691
+ } catch (error) {
1692
+ console.error("Mobius daemon rejected a message", error);
1693
+ socket.close(1008, "Invalid protocol message");
1694
+ }
1695
+ };
1696
+ socket.addEventListener("close", handleClose);
1697
+ socket.addEventListener("error", handleError);
1698
+ socket.addEventListener("message", handleMessage);
1699
+ if (signal.aborted) {
1700
+ stop();
1701
+ }
1702
+ });
1703
+ }
1704
+ async function runDaemon(config, audit, signal) {
1705
+ const sessions = new AcpSessionManager();
1706
+ let retryAnnounced = false;
1707
+ try {
1708
+ while (!signal.aborted) {
1709
+ try {
1710
+ await runRelayConnection(config, audit, sessions, signal);
1711
+ retryAnnounced = false;
1712
+ } catch {
1713
+ if (signal.aborted) {
1714
+ return;
1715
+ }
1716
+ if (!retryAnnounced) {
1717
+ console.warn("Mobius daemon is waiting for the relay");
1718
+ retryAnnounced = true;
1719
+ }
1720
+ }
1721
+ await waitForRelayRetry(signal);
1722
+ }
1723
+ } finally {
1724
+ await sessions.close();
1725
+ }
1726
+ }
1727
+
1728
+ // src/cli.ts
1729
+ var shutdown = new AbortController();
1730
+ process.once("SIGINT", () => shutdown.abort());
1731
+ process.once("SIGTERM", () => shutdown.abort());
1732
+ var USAGE = `Mobius CLI
1733
+
1734
+ Usage:
1735
+ mobius pair --url <relay-url> --code <pairing-code> --root <workspace>
1736
+ mobius start --channel <channel-id>
1737
+
1738
+ Commands:
1739
+ pair Pair this machine and start its local daemon
1740
+ start Start the local daemon for a saved channel
1741
+ `;
1742
+ async function main() {
1743
+ const command = process.argv[2];
1744
+ if (command === void 0 || command === "--help" || command === "-h") {
1745
+ process.stdout.write(USAGE);
1746
+ return;
1747
+ }
1748
+ let config;
1749
+ if (command === "pair") {
1750
+ const paired = await pairLocalMachine(process.argv.slice(3));
1751
+ config = paired.config;
1752
+ process.stdout.write("\nLocal machine paired.\n");
1753
+ process.stdout.write(`Channel: ${paired.channelUrl}
1754
+ `);
1755
+ process.stdout.write(
1756
+ "Open this channel in any browser while the daemon is running.\n"
1757
+ );
1758
+ process.stdout.write(`Connection saved to ${paired.connectionPath}
1759
+ `);
1760
+ } else if (command === "start") {
1761
+ const paired = await resumeLocalMachine(process.argv.slice(3));
1762
+ config = paired.config;
1763
+ process.stdout.write("\nSaved local machine channel loaded.\n");
1764
+ process.stdout.write(`Channel: ${paired.channelUrl}
1765
+ `);
1766
+ process.stdout.write(
1767
+ "Open this channel in any browser while the daemon is running.\n"
1768
+ );
1769
+ } else {
1770
+ throw new Error(USAGE.trimEnd());
1771
+ }
1772
+ process.stdout.write(
1773
+ "Starting the local daemon. Keep this terminal open.\n\n"
1774
+ );
1775
+ const audit = new AuditStore(config.auditDatabasePath);
1776
+ try {
1777
+ await runDaemon(config, audit, shutdown.signal);
1778
+ } finally {
1779
+ audit.close();
1780
+ }
1781
+ }
1782
+ try {
1783
+ await main();
1784
+ } catch (error) {
1785
+ if (!shutdown.signal.aborted) {
1786
+ const message = error instanceof Error ? error.message : "Unknown failure";
1787
+ process.stderr.write(`Mobius daemon failed: ${message}
1788
+ `);
1789
+ process.exitCode = 1;
1790
+ }
1791
+ }
1792
+ //# sourceMappingURL=cli.js.map