@opencomputer/cli 0.3.3 → 0.3.5

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/local.js CHANGED
@@ -1,130 +1,667 @@
1
+ import { createOpencode, } from "@opencode-ai/sdk/v2";
1
2
  import { spawn } from "node:child_process";
2
3
  import { randomBytes, timingSafeEqual } from "node:crypto";
3
- import { access } from "node:fs/promises";
4
- import { createServer } from "node:http";
5
- import { resolve } from "node:path";
6
- import { findAgentRoot, prepareAgent } from "./project.js";
4
+ import { mkdir, readFile, rm, writeFile, } from "node:fs/promises";
5
+ import { createServer, } from "node:http";
6
+ import { delimiter, dirname, resolve } from "node:path";
7
+ import { createInterface } from "node:readline/promises";
8
+ import { fileURLToPath } from "node:url";
9
+ import { renderDevUI } from "./dev-ui.js";
10
+ import { findAgentRoot, prepareAgent, readManifest } from "./project.js";
7
11
  function sameToken(left, right) {
8
12
  const leftBytes = Buffer.from(left);
9
13
  const rightBytes = Buffer.from(right);
10
14
  return (leftBytes.length === rightBytes.length &&
11
15
  timingSafeEqual(leftBytes, rightBytes));
12
16
  }
13
- async function readBody(request) {
17
+ async function readBody(request, limit = 2 * 1024 * 1024) {
14
18
  const chunks = [];
15
19
  let size = 0;
16
20
  for await (const chunk of request) {
17
21
  const buffer = Buffer.from(chunk);
18
22
  size += buffer.byteLength;
19
- if (size > 2 * 1024 * 1024) {
20
- throw new Error("Connection request is too large");
21
- }
23
+ if (size > limit)
24
+ throw new Error("Request is too large");
22
25
  chunks.push(buffer);
23
26
  }
24
27
  return Buffer.concat(chunks);
25
28
  }
26
- async function startConnectionProxy(config) {
29
+ function authorized(request, token) {
30
+ const header = request.headers.authorization;
31
+ return (typeof header === "string" &&
32
+ header.startsWith("Bearer ") &&
33
+ sameToken(header.slice(7), token));
34
+ }
35
+ function sendJSON(response, status, body) {
36
+ response.writeHead(status, { "content-type": "application/json" });
37
+ response.end(JSON.stringify(body));
38
+ }
39
+ function openBrowser(url) {
40
+ if (process.env.OPENCOMPUTER_NO_OPEN === "1")
41
+ return;
42
+ const command = process.platform === "darwin"
43
+ ? { file: "open", args: [url] }
44
+ : process.platform === "win32"
45
+ ? { file: "cmd", args: ["/c", "start", "", url] }
46
+ : { file: "xdg-open", args: [url] };
47
+ const child = spawn(command.file, command.args, {
48
+ detached: true,
49
+ stdio: "ignore",
50
+ });
51
+ child.on("error", () => undefined);
52
+ child.unref();
53
+ }
54
+ async function startGateway(config) {
27
55
  if (!config.apiKey) {
28
- throw new Error("Not logged in. Run `opencomputer login` before testing connected tools.");
56
+ throw new Error("Not logged in. Run `opencomputer login` before starting dev mode.");
29
57
  }
30
58
  const token = randomBytes(32).toString("base64url");
31
59
  const server = createServer((request, response) => {
32
60
  void (async () => {
33
- if (request.method !== "POST" || request.url !== "/google/fetch") {
34
- response.writeHead(404).end();
61
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
62
+ const header = request.headers.authorization;
63
+ if (!header?.startsWith("Bearer ") ||
64
+ !sameToken(header.slice(7), token)) {
65
+ response.writeHead(401).end();
35
66
  return;
36
67
  }
37
- const authorization = request.headers.authorization;
38
- if (!authorization?.startsWith("Bearer ") ||
39
- !sameToken(authorization.slice(7), token)) {
40
- response.writeHead(401).end();
68
+ let target;
69
+ if (request.method === "POST" && url.pathname === "/google/fetch") {
70
+ target = `${config.apiUrl}/api/managed-agents/connections/google/fetch`;
71
+ }
72
+ else if (url.pathname.startsWith("/openrouter/")) {
73
+ target =
74
+ `${config.apiUrl}/api/managed-agents/openrouter` +
75
+ `${url.pathname.slice("/openrouter".length)}${url.search}`;
76
+ }
77
+ else {
78
+ response.writeHead(404).end();
41
79
  return;
42
80
  }
43
- const upstream = await fetch(`${config.apiUrl}/api/managed-agents/connections/google/fetch`, {
44
- method: "POST",
81
+ const upstream = await fetch(target, {
82
+ method: request.method,
45
83
  headers: {
46
- "content-type": "application/json",
84
+ "content-type": request.headers["content-type"] ?? "application/json",
47
85
  "x-api-key": config.apiKey,
48
86
  },
49
- body: await readBody(request),
50
- signal: AbortSignal.timeout(30_000),
87
+ body: request.method === "GET" || request.method === "HEAD"
88
+ ? undefined
89
+ : await readBody(request),
90
+ signal: AbortSignal.timeout(60_000),
51
91
  });
52
92
  response.writeHead(upstream.status, {
53
93
  "content-type": upstream.headers.get("content-type") ?? "application/json",
54
94
  });
55
95
  response.end(Buffer.from(await upstream.arrayBuffer()));
56
96
  })().catch((error) => {
57
- response.writeHead(502, { "content-type": "application/json" });
58
- response.end(JSON.stringify({
97
+ sendJSON(response, 502, {
59
98
  error: {
60
- message: error instanceof Error
61
- ? error.message
62
- : "Connection proxy failed",
99
+ message: error instanceof Error ? error.message : "Gateway request failed",
63
100
  },
64
- }));
101
+ });
65
102
  });
66
103
  });
67
- await new Promise((resolve, reject) => {
104
+ await new Promise((done, reject) => {
68
105
  server.once("error", reject);
69
- server.listen(0, "127.0.0.1", resolve);
106
+ server.listen(0, "127.0.0.1", done);
70
107
  });
71
108
  const address = server.address();
72
109
  if (!address || typeof address === "string") {
73
110
  server.close();
74
- throw new Error("Could not start the local connection proxy");
111
+ throw new Error("Could not start the local OpenComputer gateway");
75
112
  }
76
113
  return {
77
114
  url: `http://127.0.0.1:${String(address.port)}`,
78
115
  token,
79
- close: () => new Promise((resolve) => server.close(() => resolve())),
116
+ close: () => {
117
+ server.closeAllConnections();
118
+ return new Promise((done) => server.close(() => done()));
119
+ },
120
+ };
121
+ }
122
+ function addBundledRuntimeToPath() {
123
+ const packagePath = fileURLToPath(import.meta.resolve("opencode-ai/package.json"));
124
+ const bin = resolve(dirname(packagePath), "..", ".bin");
125
+ const current = process.env.PATH ?? "";
126
+ if (!current.split(delimiter).includes(bin)) {
127
+ process.env.PATH = current ? `${bin}${delimiter}${current}` : bin;
128
+ }
129
+ }
130
+ function modelParts() {
131
+ const full = process.env.OPENCOMPUTER_MODEL ??
132
+ "openrouter/anthropic/claude-sonnet-4.6";
133
+ const separator = full.indexOf("/");
134
+ return {
135
+ providerID: separator === -1 ? "openrouter" : full.slice(0, separator),
136
+ modelID: separator === -1 ? full : full.slice(separator + 1),
137
+ full,
80
138
  };
81
139
  }
82
- export async function runOpenCode(args, config) {
140
+ async function streamTurn(client, directory, sessionID, prompt, emit) {
141
+ const subscription = await client.event.subscribe({ directory });
142
+ const assistantMessages = new Set();
143
+ const textByPart = new Map();
144
+ const messageByPart = new Map();
145
+ const emittedLengths = new Map();
146
+ const tools = new Map();
147
+ const toolStates = new Map();
148
+ const completedText = [];
149
+ const emitText = (partID) => {
150
+ const messageID = messageByPart.get(partID);
151
+ if (!messageID || !assistantMessages.has(messageID))
152
+ return;
153
+ const text = textByPart.get(partID) ?? "";
154
+ const offset = emittedLengths.get(partID) ?? 0;
155
+ if (text.length > offset) {
156
+ emit({ type: "message.delta", data: { text: text.slice(offset) } });
157
+ emittedLengths.set(partID, text.length);
158
+ }
159
+ };
160
+ const emitTool = (part) => {
161
+ if (!assistantMessages.has(part.messageID))
162
+ return;
163
+ const previous = toolStates.get(part.callID);
164
+ const current = part.state.status;
165
+ if (previous === current)
166
+ return;
167
+ toolStates.set(part.callID, current);
168
+ if (current === "running") {
169
+ emit({
170
+ type: "tool.started",
171
+ data: { tool: part.tool, input: part.state.input },
172
+ });
173
+ }
174
+ else if (current === "completed") {
175
+ emit({ type: "tool.completed", data: { tool: part.tool } });
176
+ }
177
+ else if (current === "error") {
178
+ emit({
179
+ type: "tool.failed",
180
+ data: { tool: part.tool, message: part.state.error },
181
+ });
182
+ }
183
+ };
184
+ try {
185
+ const model = modelParts();
186
+ const started = await client.session.promptAsync({
187
+ sessionID,
188
+ directory,
189
+ model: { providerID: model.providerID, modelID: model.modelID },
190
+ parts: [{ type: "text", text: prompt }],
191
+ });
192
+ if (started.error)
193
+ throw new Error(JSON.stringify(started.error));
194
+ for await (const event of subscription.stream) {
195
+ if (event.type === "message.updated") {
196
+ const info = event.properties.info;
197
+ if (info.sessionID === sessionID && info.role === "assistant") {
198
+ assistantMessages.add(info.id);
199
+ for (const [partID, messageID] of messageByPart) {
200
+ if (messageID === info.id)
201
+ emitText(partID);
202
+ }
203
+ for (const part of tools.values()) {
204
+ if (part.messageID === info.id)
205
+ emitTool(part);
206
+ }
207
+ }
208
+ }
209
+ else if (event.type === "message.part.updated") {
210
+ const part = event.properties.part;
211
+ if (part.sessionID !== sessionID)
212
+ continue;
213
+ if (part.type === "text") {
214
+ messageByPart.set(part.id, part.messageID);
215
+ textByPart.set(part.id, part.text);
216
+ emitText(part.id);
217
+ }
218
+ else if (part.type === "tool") {
219
+ tools.set(part.callID, part);
220
+ emitTool(part);
221
+ }
222
+ }
223
+ else if (event.type === "session.error" &&
224
+ event.properties.sessionID === sessionID) {
225
+ throw new Error(JSON.stringify(event.properties.error));
226
+ }
227
+ else if (event.type === "session.idle" &&
228
+ event.properties.sessionID === sessionID) {
229
+ break;
230
+ }
231
+ }
232
+ }
233
+ finally {
234
+ await subscription.stream.return(undefined);
235
+ }
236
+ for (const [partID, text] of textByPart) {
237
+ const messageID = messageByPart.get(partID);
238
+ if (messageID && assistantMessages.has(messageID))
239
+ completedText.push(text);
240
+ }
241
+ const text = completedText.join("");
242
+ emit({ type: "message.completed", data: { text } });
243
+ return text;
244
+ }
245
+ async function createRuntimeSession(client, directory) {
246
+ const created = await client.session.create({ directory });
247
+ if (!created.data)
248
+ throw new Error("The local agent session did not start");
249
+ return created.data.id;
250
+ }
251
+ function statePath(root) {
252
+ return resolve(root, ".opencomputer", "dev.json");
253
+ }
254
+ async function readDevState(root) {
255
+ try {
256
+ const state = JSON.parse(await readFile(statePath(root), "utf8"));
257
+ if (state.version !== 1 || !state.url || !state.token)
258
+ return null;
259
+ const response = await fetch(`${state.url}/health`, {
260
+ headers: { authorization: `Bearer ${state.token}` },
261
+ signal: AbortSignal.timeout(1_000),
262
+ });
263
+ return response.ok ? state : null;
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ }
269
+ async function startDevService(config) {
83
270
  const root = await findAgentRoot();
84
271
  if (!root) {
85
272
  throw new Error("No OpenComputer agent repository found. Run `opencomputer init <template>` first.");
86
273
  }
87
- const runtime = await prepareAgent(root);
88
- const connections = await startConnectionProxy(config);
89
- const localExecutable = resolve(root, "node_modules", ".bin", process.platform === "win32" ? "opencode.cmd" : "opencode");
90
- const executable = await access(localExecutable)
91
- .then(() => localExecutable)
92
- .catch(() => process.platform === "win32" ? "opencode.cmd" : "opencode");
93
- const openCodeArgs = args[0] === "run"
94
- ? [...args, "--dir", runtime]
95
- : args.length === 0
96
- ? [runtime]
97
- : args;
98
- try {
99
- await new Promise((resolve, reject) => {
100
- const child = spawn(executable, openCodeArgs, {
101
- cwd: runtime,
102
- env: {
103
- ...process.env,
104
- OPENCOMPUTER_CONNECTIONS_URL: connections.url,
105
- OPENCOMPUTER_CONNECTION_TOKEN: connections.token,
106
- OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: process.env.OPENCOMPUTER_MAX_OUTPUT_TOKENS ??
107
- process.env.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX ??
108
- "16384",
274
+ const existing = await readDevState(root);
275
+ if (existing) {
276
+ throw new Error(`OpenComputer dev is already running at ${existing.url}`);
277
+ }
278
+ const directory = await prepareAgent(root);
279
+ const manifest = await readManifest(root);
280
+ const gateway = await startGateway(config);
281
+ addBundledRuntimeToPath();
282
+ const abortController = new AbortController();
283
+ const model = modelParts();
284
+ const instance = await createOpencode({
285
+ signal: abortController.signal,
286
+ port: 0,
287
+ timeout: 45_000,
288
+ config: {
289
+ model: model.full,
290
+ enabled_providers: ["openrouter"],
291
+ provider: {
292
+ openrouter: {
293
+ options: { baseURL: `${gateway.url}/openrouter/api/v1` },
109
294
  },
110
- stdio: "inherit",
111
- });
112
- child.once("error", (error) => {
113
- reject(error instanceof Error && "code" in error && error.code === "ENOENT"
114
- ? new Error("OpenCode is not installed. Run `npm install` first.")
115
- : error);
116
- });
117
- child.once("exit", (code, signal) => {
118
- if (code === 0)
119
- resolve();
295
+ },
296
+ autoupdate: false,
297
+ share: "disabled",
298
+ },
299
+ });
300
+ const authenticated = await instance.client.auth.set({
301
+ providerID: "openrouter",
302
+ auth: { type: "api", key: gateway.token },
303
+ });
304
+ if (authenticated.error || authenticated.data !== true) {
305
+ throw new Error("The embedded agent runtime rejected its local credential");
306
+ }
307
+ const token = randomBytes(32).toString("base64url");
308
+ const nonce = randomBytes(18).toString("base64url");
309
+ const sessions = new Map();
310
+ const running = new Set();
311
+ const server = createServer((request, response) => {
312
+ void (async () => {
313
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
314
+ if (request.method === "GET" && url.pathname === "/") {
315
+ response.writeHead(200, {
316
+ "content-type": "text/html; charset=utf-8",
317
+ "cache-control": "no-store",
318
+ "content-security-policy": `default-src 'none'; script-src 'nonce-${nonce}'; ` +
319
+ "style-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; frame-ancestors 'none'",
320
+ });
321
+ response.end(renderDevUI(manifest.name, nonce));
322
+ return;
323
+ }
324
+ if (!authorized(request, token)) {
325
+ sendJSON(response, 401, { message: "Unauthorized" });
326
+ return;
327
+ }
328
+ if (request.method === "GET" && url.pathname === "/health") {
329
+ sendJSON(response, 200, {
330
+ ok: true,
331
+ agentId: manifest.id,
332
+ sessions: sessions.size,
333
+ });
334
+ return;
335
+ }
336
+ const streamSession = async (session, prompt, created = false) => {
337
+ if (running.has(session.id)) {
338
+ sendJSON(response, 409, { message: "This session is already running" });
339
+ return;
340
+ }
341
+ running.add(session.id);
342
+ session.messages.push({ role: "user", text: prompt });
343
+ if (!session.title)
344
+ session.title = prompt.slice(0, 60);
345
+ session.updatedAt = new Date().toISOString();
346
+ response.writeHead(200, {
347
+ "content-type": "application/x-ndjson",
348
+ "cache-control": "no-store",
349
+ });
350
+ const emit = (event) => {
351
+ response.write(`${JSON.stringify(event)}\n`);
352
+ };
353
+ if (created) {
354
+ emit({ type: "session.created", data: { sessionId: session.id } });
355
+ }
356
+ try {
357
+ const text = await streamTurn(instance.client, directory, session.id, prompt, emit);
358
+ session.messages.push({ role: "assistant", text });
359
+ session.updatedAt = new Date().toISOString();
360
+ }
361
+ catch (error) {
362
+ emit({
363
+ type: "session.failed",
364
+ data: {
365
+ message: error instanceof Error ? error.message : String(error),
366
+ },
367
+ });
368
+ }
369
+ finally {
370
+ running.delete(session.id);
371
+ }
372
+ response.end();
373
+ };
374
+ if (request.method === "GET" && url.pathname === "/sessions") {
375
+ sendJSON(response, 200, {
376
+ sessions: [...sessions.values()]
377
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
378
+ .map(({ id, title, createdAt, updatedAt, messages }) => ({
379
+ id,
380
+ title,
381
+ createdAt,
382
+ updatedAt,
383
+ messageCount: messages.length,
384
+ })),
385
+ });
386
+ return;
387
+ }
388
+ if (request.method === "POST" && url.pathname === "/sessions") {
389
+ const raw = (await readBody(request)).toString("utf8");
390
+ const body = (raw ? JSON.parse(raw) : {});
391
+ const id = await createRuntimeSession(instance.client, directory);
392
+ const now = new Date().toISOString();
393
+ const session = {
394
+ id,
395
+ title: "",
396
+ createdAt: now,
397
+ updatedAt: now,
398
+ messages: [],
399
+ };
400
+ sessions.set(id, session);
401
+ if (typeof body.prompt === "string" && body.prompt.trim()) {
402
+ response.setHeader("x-opencomputer-session-id", id);
403
+ await streamSession(session, body.prompt.trim(), true);
404
+ }
120
405
  else {
121
- reject(new Error(`OpenCode exited ${signal ? `with ${signal}` : `with code ${String(code)}`}`));
406
+ sendJSON(response, 201, session);
122
407
  }
123
- });
408
+ return;
409
+ }
410
+ const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)$/);
411
+ if (sessionMatch?.[1]) {
412
+ const id = decodeURIComponent(sessionMatch[1]);
413
+ const session = sessions.get(id);
414
+ if (!session) {
415
+ sendJSON(response, 404, { message: "Session not found" });
416
+ return;
417
+ }
418
+ if (request.method === "GET") {
419
+ sendJSON(response, 200, session);
420
+ return;
421
+ }
422
+ if (request.method === "POST") {
423
+ const body = JSON.parse((await readBody(request)).toString("utf8"));
424
+ if (typeof body.prompt !== "string" || !body.prompt.trim()) {
425
+ sendJSON(response, 400, { message: "A prompt is required" });
426
+ return;
427
+ }
428
+ await streamSession(session, body.prompt.trim());
429
+ return;
430
+ }
431
+ }
432
+ if (request.method === "GET" && url.pathname === "/api") {
433
+ sendJSON(response, 200, {
434
+ service: "OpenComputer local agent",
435
+ agentId: manifest.id,
436
+ endpoints: ["GET /sessions", "POST /sessions", "POST /sessions/:id"],
437
+ });
438
+ return;
439
+ }
440
+ sendJSON(response, 404, { message: "Route not found" });
441
+ })().catch((error) => {
442
+ if (!response.headersSent) {
443
+ sendJSON(response, 500, {
444
+ message: error instanceof Error ? error.message : String(error),
445
+ });
446
+ }
447
+ else {
448
+ response.end();
449
+ }
124
450
  });
451
+ });
452
+ await new Promise((done, reject) => {
453
+ server.once("error", reject);
454
+ server.listen(Number(process.env.OPENCOMPUTER_DEV_PORT ?? 0), "127.0.0.1", done);
455
+ });
456
+ const address = server.address();
457
+ if (!address || typeof address === "string") {
458
+ throw new Error("The OpenComputer dev service did not receive a port");
459
+ }
460
+ const state = {
461
+ version: 1,
462
+ pid: process.pid,
463
+ url: `http://127.0.0.1:${String(address.port)}`,
464
+ token,
465
+ agentRoot: root,
466
+ agentId: manifest.id,
467
+ startedAt: new Date().toISOString(),
468
+ };
469
+ await mkdir(dirname(statePath(root)), { recursive: true, mode: 0o700 });
470
+ await writeFile(statePath(root), `${JSON.stringify(state, null, 2)}\n`, {
471
+ mode: 0o600,
472
+ });
473
+ const webUrl = `${state.url}/#token=${encodeURIComponent(token)}`;
474
+ process.stdout.write(`OpenComputer dev service ready\n` +
475
+ `Agent: ${manifest.name} (${manifest.id})\n` +
476
+ `Web: ${webUrl}\n` +
477
+ `Local API: ${state.url}\n` +
478
+ `Session: opencomputer session\n`);
479
+ openBrowser(webUrl);
480
+ await new Promise((done) => {
481
+ process.once("SIGINT", done);
482
+ process.once("SIGTERM", done);
483
+ });
484
+ await rm(statePath(root), { force: true });
485
+ server.closeAllConnections();
486
+ await new Promise((done) => server.close(() => done()));
487
+ abortController.abort();
488
+ instance.server.close();
489
+ await gateway.close();
490
+ }
491
+ async function runLocalSession(prompt, state, sessionID) {
492
+ const endpoint = sessionID
493
+ ? `${state.url}/sessions/${encodeURIComponent(sessionID)}`
494
+ : `${state.url}/sessions`;
495
+ const response = await fetch(endpoint, {
496
+ method: "POST",
497
+ headers: {
498
+ authorization: `Bearer ${state.token}`,
499
+ "content-type": "application/json",
500
+ },
501
+ body: JSON.stringify({ prompt }),
502
+ });
503
+ if (!response.ok || !response.body) {
504
+ const detail = await response.text().catch(() => "");
505
+ throw new Error(`The local agent service returned ${String(response.status)}` +
506
+ (detail ? `: ${detail}` : ""));
507
+ }
508
+ let resolvedSessionID = sessionID ?? response.headers.get("x-opencomputer-session-id") ?? undefined;
509
+ const decoder = new TextDecoder();
510
+ let buffered = "";
511
+ let streamedText = false;
512
+ for await (const chunk of response.body) {
513
+ buffered += decoder.decode(chunk, { stream: true });
514
+ const lines = buffered.split("\n");
515
+ buffered = lines.pop() ?? "";
516
+ for (const line of lines) {
517
+ if (!line.trim())
518
+ continue;
519
+ const event = JSON.parse(line);
520
+ if (event.type === "session.created") {
521
+ resolvedSessionID = String(event.data.sessionId);
522
+ process.stderr.write(`Session ${resolvedSessionID}\n`);
523
+ }
524
+ else if (event.type === "message.delta") {
525
+ streamedText = true;
526
+ process.stdout.write(String(event.data.text ?? ""));
527
+ }
528
+ else if (event.type === "message.completed") {
529
+ if (!streamedText)
530
+ process.stdout.write(String(event.data.text ?? ""));
531
+ process.stdout.write("\n");
532
+ }
533
+ else if (event.type === "tool.started") {
534
+ process.stderr.write(`⚙ ${String(event.data.tool ?? "tool")} ${JSON.stringify(event.data.input ?? {})}\n`);
535
+ }
536
+ else if (event.type === "tool.completed") {
537
+ process.stderr.write(`✓ ${String(event.data.tool ?? "tool")}\n`);
538
+ }
539
+ else if (event.type === "tool.failed") {
540
+ process.stderr.write(`✗ ${String(event.data.tool ?? "tool")}: ${String(event.data.message ?? "failed")}\n`);
541
+ }
542
+ else if (event.type === "session.failed") {
543
+ throw new Error(String(event.data.message ?? "Local session failed"));
544
+ }
545
+ }
546
+ }
547
+ if (!resolvedSessionID)
548
+ throw new Error("The local session did not return an ID");
549
+ return resolvedSessionID;
550
+ }
551
+ async function stopOwnedDev(child) {
552
+ if (!child || child.exitCode !== null)
553
+ return;
554
+ child.kill("SIGTERM");
555
+ await Promise.race([
556
+ new Promise((done) => child.once("exit", () => done())),
557
+ new Promise((done) => setTimeout(done, 3_000)),
558
+ ]);
559
+ if (child.exitCode === null)
560
+ child.kill("SIGKILL");
561
+ }
562
+ async function ensureDevService(config) {
563
+ const root = await findAgentRoot();
564
+ if (!root) {
565
+ throw new Error("No OpenComputer agent repository found. Run `opencomputer init <template>` first.");
566
+ }
567
+ const existing = await readDevState(root);
568
+ if (existing)
569
+ return { state: existing };
570
+ const environment = {
571
+ ...process.env,
572
+ OPENCOMPUTER_API_URL: config.apiUrl,
573
+ OPENCOMPUTER_NO_OPEN: "1",
574
+ };
575
+ if (config.apiKey)
576
+ environment.OPENCOMPUTER_API_KEY = config.apiKey;
577
+ const child = spawn(process.execPath, [process.argv[1], "dev"], {
578
+ cwd: root,
579
+ env: environment,
580
+ stdio: ["ignore", "ignore", "pipe"],
581
+ });
582
+ let errors = "";
583
+ child.stderr?.on("data", (chunk) => {
584
+ errors = `${errors}${chunk.toString("utf8")}`.slice(-8_000);
585
+ });
586
+ const deadline = Date.now() + 60_000;
587
+ while (Date.now() < deadline) {
588
+ const state = await readDevState(root);
589
+ if (state)
590
+ return { state, owned: child };
591
+ if (child.exitCode !== null) {
592
+ throw new Error(errors.trim() || "The local development service exited");
593
+ }
594
+ await new Promise((done) => setTimeout(done, 100));
595
+ }
596
+ await stopOwnedDev(child);
597
+ throw new Error("Timed out starting the local development service");
598
+ }
599
+ async function runSessionShell(config) {
600
+ const target = await ensureDevService(config);
601
+ const readline = createInterface({
602
+ input: process.stdin,
603
+ output: process.stdout,
604
+ terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY),
605
+ });
606
+ let sessionID;
607
+ process.stdout.write("Local agent ready. Enter a prompt; use /exit or Ctrl-C to quit.\n\n");
608
+ readline.on("SIGINT", () => readline.close());
609
+ try {
610
+ for (;;) {
611
+ let line;
612
+ try {
613
+ line = await readline.question("opencomputer> ");
614
+ }
615
+ catch {
616
+ break;
617
+ }
618
+ const prompt = line.trim();
619
+ if (prompt === "/exit" || prompt === "/quit")
620
+ break;
621
+ if (!prompt)
622
+ continue;
623
+ try {
624
+ sessionID = await runLocalSession(prompt, target.state, sessionID);
625
+ }
626
+ catch (error) {
627
+ process.stderr.write(`turn failed: ${error instanceof Error ? error.message : String(error)}\n`);
628
+ }
629
+ }
630
+ }
631
+ finally {
632
+ readline.close();
633
+ await stopOwnedDev(target.owned);
634
+ }
635
+ }
636
+ async function runOneShotSession(prompt, config) {
637
+ const target = await ensureDevService(config);
638
+ try {
639
+ await runLocalSession(prompt, target.state);
125
640
  }
126
641
  finally {
127
- await connections.close();
642
+ await stopOwnedDev(target.owned);
643
+ }
644
+ }
645
+ export async function runLocalAgent(args, config) {
646
+ if (args[0] === "dev") {
647
+ if (args.length > 1)
648
+ throw new Error(`Unexpected local argument: ${args[1]}`);
649
+ await startDevService(config);
650
+ return;
651
+ }
652
+ if (args[0] === "run") {
653
+ const prompt = args.slice(1).join(" ").trim();
654
+ if (!prompt)
655
+ throw new Error("A prompt is required");
656
+ await runOneShotSession(prompt, config);
657
+ return;
658
+ }
659
+ if (args[0] === "shell") {
660
+ if (args.length > 1)
661
+ throw new Error(`Unexpected local argument: ${args[1]}`);
662
+ await runSessionShell(config);
663
+ return;
128
664
  }
665
+ throw new Error(`Unexpected local argument: ${args[0] ?? "none"}`);
129
666
  }
130
667
  //# sourceMappingURL=local.js.map