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