@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.
- package/dist/acp/activity.d.ts +29 -0
- package/dist/acp/activity.d.ts.map +1 -0
- package/dist/acp/activity.js +253 -0
- package/dist/acp/activity.js.map +1 -0
- package/dist/acp/create-client.d.ts +3 -0
- package/dist/acp/create-client.d.ts.map +1 -0
- package/dist/acp/create-client.js +3 -0
- package/dist/acp/create-client.js.map +1 -0
- package/dist/acp/run-prompt.d.ts +15 -0
- package/dist/acp/run-prompt.d.ts.map +1 -0
- package/dist/acp/run-prompt.js +21 -0
- package/dist/acp/run-prompt.js.map +1 -0
- package/dist/acp/session-manager.d.ts +25 -0
- package/dist/acp/session-manager.d.ts.map +1 -0
- package/dist/acp/session-manager.js +301 -0
- package/dist/acp/session-manager.js.map +1 -0
- package/dist/audit/audit-store.d.ts +12 -0
- package/dist/audit/audit-store.d.ts.map +1 -0
- package/dist/audit/audit-store.js +35 -0
- package/dist/audit/audit-store.js.map +1 -0
- package/dist/authorization/authorize-binding.d.ts +6 -0
- package/dist/authorization/authorize-binding.d.ts.map +1 -0
- package/dist/authorization/authorize-binding.js +30 -0
- package/dist/authorization/authorize-binding.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1792 -0
- package/dist/cli.js.map +7 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +85 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1784 -0
- package/dist/index.js.map +7 -0
- package/dist/pairing/pair-local-machine.d.ts +13 -0
- package/dist/pairing/pair-local-machine.d.ts.map +1 -0
- package/dist/pairing/pair-local-machine.js +302 -0
- package/dist/pairing/pair-local-machine.js.map +1 -0
- package/dist/relay-connection/connect.d.ts +2 -0
- package/dist/relay-connection/connect.d.ts.map +1 -0
- package/dist/relay-connection/connect.js +61 -0
- package/dist/relay-connection/connect.js.map +1 -0
- package/dist/run-daemon.d.ts +4 -0
- package/dist/run-daemon.d.ts.map +1 -0
- package/dist/run-daemon.js +274 -0
- package/dist/run-daemon.js.map +1 -0
- 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
|