@opencomputer/cli 0.3.4 → 0.3.7
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/README.md +26 -7
- package/dist/api.d.ts +9 -3
- package/dist/api.js +6 -6
- package/dist/api.js.map +1 -1
- package/dist/commands.js +34 -29
- package/dist/commands.js.map +1 -1
- package/dist/dev-ui.d.ts +1 -0
- package/dist/dev-ui.js +24 -0
- package/dist/dev-ui.js.map +1 -0
- package/dist/dev-ui.test.d.ts +1 -0
- package/dist/dev-ui.test.js +11 -0
- package/dist/dev-ui.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -6
- package/dist/index.js.map +1 -1
- package/dist/local.js +395 -87
- package/dist/local.js.map +1 -1
- package/dist/tui.d.ts +12 -0
- package/dist/tui.js +148 -0
- package/dist/tui.js.map +1 -0
- package/dist/ui/dev-ui.css +2 -0
- package/dist/ui/dev-ui.js +16 -0
- package/dist/ui/main-ogntZBtk.js +25176 -0
- package/package.json +19 -5
package/dist/local.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createOpencode, } from "@opencode-ai/sdk/v2";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
4
|
import { mkdir, readFile, rm, writeFile, } from "node:fs/promises";
|
|
4
5
|
import { createServer, } from "node:http";
|
|
5
6
|
import { delimiter, dirname, resolve } from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { renderDevUI } from "./dev-ui.js";
|
|
7
9
|
import { findAgentRoot, prepareAgent, readManifest } from "./project.js";
|
|
8
10
|
function sameToken(left, right) {
|
|
9
11
|
const leftBytes = Buffer.from(left);
|
|
@@ -33,6 +35,21 @@ function sendJSON(response, status, body) {
|
|
|
33
35
|
response.writeHead(status, { "content-type": "application/json" });
|
|
34
36
|
response.end(JSON.stringify(body));
|
|
35
37
|
}
|
|
38
|
+
function openBrowser(url) {
|
|
39
|
+
if (process.env.OPENCOMPUTER_NO_OPEN === "1")
|
|
40
|
+
return;
|
|
41
|
+
const command = process.platform === "darwin"
|
|
42
|
+
? { file: "open", args: [url] }
|
|
43
|
+
: process.platform === "win32"
|
|
44
|
+
? { file: "cmd", args: ["/c", "start", "", url] }
|
|
45
|
+
: { file: "xdg-open", args: [url] };
|
|
46
|
+
const child = spawn(command.file, command.args, {
|
|
47
|
+
detached: true,
|
|
48
|
+
stdio: "ignore",
|
|
49
|
+
});
|
|
50
|
+
child.on("error", () => undefined);
|
|
51
|
+
child.unref();
|
|
52
|
+
}
|
|
36
53
|
async function startGateway(config) {
|
|
37
54
|
if (!config.apiKey) {
|
|
38
55
|
throw new Error("Not logged in. Run `opencomputer login` before starting dev mode.");
|
|
@@ -95,7 +112,10 @@ async function startGateway(config) {
|
|
|
95
112
|
return {
|
|
96
113
|
url: `http://127.0.0.1:${String(address.port)}`,
|
|
97
114
|
token,
|
|
98
|
-
close: () =>
|
|
115
|
+
close: () => {
|
|
116
|
+
server.closeAllConnections();
|
|
117
|
+
return new Promise((done) => server.close(() => done()));
|
|
118
|
+
},
|
|
99
119
|
};
|
|
100
120
|
}
|
|
101
121
|
function addBundledRuntimeToPath() {
|
|
@@ -116,29 +136,50 @@ function modelParts() {
|
|
|
116
136
|
full,
|
|
117
137
|
};
|
|
118
138
|
}
|
|
119
|
-
async function streamTurn(client, directory, prompt, emit) {
|
|
120
|
-
const created = await client.session.create({ directory });
|
|
121
|
-
if (!created.data)
|
|
122
|
-
throw new Error("The local agent session did not start");
|
|
123
|
-
const sessionID = created.data.id;
|
|
124
|
-
emit({ type: "session.created", data: { sessionId: sessionID } });
|
|
139
|
+
async function streamTurn(client, directory, sessionID, prompt, emit) {
|
|
125
140
|
const subscription = await client.event.subscribe({ directory });
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
141
|
+
const textParts = new Map();
|
|
142
|
+
const reasoningParts = new Map();
|
|
143
|
+
const partTypes = new Map();
|
|
144
|
+
const pendingDeltas = new Map();
|
|
130
145
|
const tools = new Map();
|
|
131
146
|
const toolStates = new Map();
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
const assistantMessages = new Set();
|
|
148
|
+
const streamedTextParts = new Set();
|
|
149
|
+
const consumePendingDeltas = (partID, messageID) => {
|
|
150
|
+
for (const [candidatePartID, deltas] of pendingDeltas) {
|
|
151
|
+
if (partID && candidatePartID !== partID)
|
|
152
|
+
continue;
|
|
153
|
+
const partType = partTypes.get(candidatePartID);
|
|
154
|
+
if (!partType)
|
|
155
|
+
continue;
|
|
156
|
+
const remaining = [];
|
|
157
|
+
for (const pending of deltas) {
|
|
158
|
+
if ((messageID && pending.messageID !== messageID) ||
|
|
159
|
+
!assistantMessages.has(pending.messageID)) {
|
|
160
|
+
remaining.push(pending);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const parts = partType === "text" ? textParts : reasoningParts;
|
|
164
|
+
parts.set(candidatePartID, `${parts.get(candidatePartID) ?? ""}${pending.delta}`);
|
|
165
|
+
if (partType === "text" && !streamedTextParts.has(candidatePartID)) {
|
|
166
|
+
if (streamedTextParts.size > 0) {
|
|
167
|
+
emit({
|
|
168
|
+
type: "message.delta",
|
|
169
|
+
data: { text: "\n\n", partId: candidatePartID },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
streamedTextParts.add(candidatePartID);
|
|
173
|
+
}
|
|
174
|
+
emit({
|
|
175
|
+
type: partType === "text" ? "message.delta" : "reasoning.delta",
|
|
176
|
+
data: { text: pending.delta, partId: candidatePartID },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (remaining.length)
|
|
180
|
+
pendingDeltas.set(candidatePartID, remaining);
|
|
181
|
+
else
|
|
182
|
+
pendingDeltas.delete(candidatePartID);
|
|
142
183
|
}
|
|
143
184
|
};
|
|
144
185
|
const emitTool = (part) => {
|
|
@@ -152,20 +193,38 @@ async function streamTurn(client, directory, prompt, emit) {
|
|
|
152
193
|
if (current === "running") {
|
|
153
194
|
emit({
|
|
154
195
|
type: "tool.started",
|
|
155
|
-
data: {
|
|
196
|
+
data: {
|
|
197
|
+
callId: part.callID,
|
|
198
|
+
tool: part.tool,
|
|
199
|
+
title: part.state.title,
|
|
200
|
+
input: part.state.input,
|
|
201
|
+
},
|
|
156
202
|
});
|
|
157
203
|
}
|
|
158
204
|
else if (current === "completed") {
|
|
159
|
-
emit({
|
|
205
|
+
emit({
|
|
206
|
+
type: "tool.completed",
|
|
207
|
+
data: { callId: part.callID, tool: part.tool, title: part.state.title },
|
|
208
|
+
});
|
|
160
209
|
}
|
|
161
210
|
else if (current === "error") {
|
|
162
211
|
emit({
|
|
163
212
|
type: "tool.failed",
|
|
164
|
-
data: {
|
|
213
|
+
data: {
|
|
214
|
+
callId: part.callID,
|
|
215
|
+
tool: part.tool,
|
|
216
|
+
title: "title" in part.state ? part.state.title : undefined,
|
|
217
|
+
message: part.state.error,
|
|
218
|
+
},
|
|
165
219
|
});
|
|
166
220
|
}
|
|
167
221
|
};
|
|
168
222
|
try {
|
|
223
|
+
const firstEvent = subscription.stream.next();
|
|
224
|
+
await Promise.race([
|
|
225
|
+
firstEvent.then(() => undefined),
|
|
226
|
+
new Promise((done) => setTimeout(done, 100)),
|
|
227
|
+
]);
|
|
169
228
|
const model = modelParts();
|
|
170
229
|
const started = await client.session.promptAsync({
|
|
171
230
|
sessionID,
|
|
@@ -175,15 +234,28 @@ async function streamTurn(client, directory, prompt, emit) {
|
|
|
175
234
|
});
|
|
176
235
|
if (started.error)
|
|
177
236
|
throw new Error(JSON.stringify(started.error));
|
|
178
|
-
|
|
237
|
+
async function* events() {
|
|
238
|
+
const first = await firstEvent;
|
|
239
|
+
if (!first.done)
|
|
240
|
+
yield first.value;
|
|
241
|
+
yield* subscription.stream;
|
|
242
|
+
}
|
|
243
|
+
for await (const event of events()) {
|
|
244
|
+
if (event.type === "message.part.delta") {
|
|
245
|
+
const { sessionID: eventSessionID, messageID, partID, field, delta } = event.properties;
|
|
246
|
+
if (eventSessionID !== sessionID || field !== "text" || !delta)
|
|
247
|
+
continue;
|
|
248
|
+
const pending = pendingDeltas.get(partID) ?? [];
|
|
249
|
+
pending.push({ messageID, delta });
|
|
250
|
+
pendingDeltas.set(partID, pending);
|
|
251
|
+
consumePendingDeltas(partID, messageID);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
179
254
|
if (event.type === "message.updated") {
|
|
180
255
|
const info = event.properties.info;
|
|
181
256
|
if (info.sessionID === sessionID && info.role === "assistant") {
|
|
182
257
|
assistantMessages.add(info.id);
|
|
183
|
-
|
|
184
|
-
if (messageID === info.id)
|
|
185
|
-
emitText(partID);
|
|
186
|
-
}
|
|
258
|
+
consumePendingDeltas(undefined, info.id);
|
|
187
259
|
for (const part of tools.values()) {
|
|
188
260
|
if (part.messageID === info.id)
|
|
189
261
|
emitTool(part);
|
|
@@ -195,9 +267,20 @@ async function streamTurn(client, directory, prompt, emit) {
|
|
|
195
267
|
if (part.sessionID !== sessionID)
|
|
196
268
|
continue;
|
|
197
269
|
if (part.type === "text") {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
270
|
+
partTypes.set(part.id, "text");
|
|
271
|
+
if (assistantMessages.has(part.messageID) &&
|
|
272
|
+
part.text.length >= (textParts.get(part.id)?.length ?? 0)) {
|
|
273
|
+
textParts.set(part.id, part.text);
|
|
274
|
+
}
|
|
275
|
+
consumePendingDeltas(part.id, part.messageID);
|
|
276
|
+
}
|
|
277
|
+
else if (part.type === "reasoning") {
|
|
278
|
+
partTypes.set(part.id, "reasoning");
|
|
279
|
+
if (assistantMessages.has(part.messageID) &&
|
|
280
|
+
part.text.length >= (reasoningParts.get(part.id)?.length ?? 0)) {
|
|
281
|
+
reasoningParts.set(part.id, part.text);
|
|
282
|
+
}
|
|
283
|
+
consumePendingDeltas(part.id, part.messageID);
|
|
201
284
|
}
|
|
202
285
|
else if (part.type === "tool") {
|
|
203
286
|
tools.set(part.callID, part);
|
|
@@ -217,14 +300,22 @@ async function streamTurn(client, directory, prompt, emit) {
|
|
|
217
300
|
finally {
|
|
218
301
|
await subscription.stream.return(undefined);
|
|
219
302
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
completedText.push(text);
|
|
303
|
+
const reasoning = [...reasoningParts.values()].join("");
|
|
304
|
+
if (reasoning) {
|
|
305
|
+
emit({ type: "reasoning.completed", data: { text: reasoning } });
|
|
224
306
|
}
|
|
225
|
-
const text =
|
|
307
|
+
const text = [...textParts.values()]
|
|
308
|
+
.map((part) => part.trim())
|
|
309
|
+
.filter(Boolean)
|
|
310
|
+
.join("\n\n");
|
|
226
311
|
emit({ type: "message.completed", data: { text } });
|
|
227
|
-
return
|
|
312
|
+
return text;
|
|
313
|
+
}
|
|
314
|
+
async function createRuntimeSession(client, directory) {
|
|
315
|
+
const created = await client.session.create({ directory });
|
|
316
|
+
if (!created.data)
|
|
317
|
+
throw new Error("The local agent session did not start");
|
|
318
|
+
return created.data.id;
|
|
228
319
|
}
|
|
229
320
|
function statePath(root) {
|
|
230
321
|
return resolve(root, ".opencomputer", "dev.json");
|
|
@@ -259,22 +350,43 @@ async function startDevService(config) {
|
|
|
259
350
|
addBundledRuntimeToPath();
|
|
260
351
|
const abortController = new AbortController();
|
|
261
352
|
const model = modelParts();
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
353
|
+
const previousConnectionsURL = process.env.OPENCOMPUTER_CONNECTIONS_URL;
|
|
354
|
+
const previousConnectionToken = process.env.OPENCOMPUTER_CONNECTION_TOKEN;
|
|
355
|
+
process.env.OPENCOMPUTER_CONNECTIONS_URL = gateway.url;
|
|
356
|
+
process.env.OPENCOMPUTER_CONNECTION_TOKEN = gateway.token;
|
|
357
|
+
let instance;
|
|
358
|
+
try {
|
|
359
|
+
instance = await createOpencode({
|
|
360
|
+
signal: abortController.signal,
|
|
361
|
+
port: 0,
|
|
362
|
+
timeout: 45_000,
|
|
363
|
+
config: {
|
|
364
|
+
model: model.full,
|
|
365
|
+
enabled_providers: ["openrouter"],
|
|
366
|
+
provider: {
|
|
367
|
+
openrouter: {
|
|
368
|
+
options: { baseURL: `${gateway.url}/openrouter/api/v1` },
|
|
369
|
+
},
|
|
272
370
|
},
|
|
371
|
+
autoupdate: false,
|
|
372
|
+
share: "disabled",
|
|
273
373
|
},
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
if (previousConnectionsURL === undefined) {
|
|
378
|
+
delete process.env.OPENCOMPUTER_CONNECTIONS_URL;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
process.env.OPENCOMPUTER_CONNECTIONS_URL = previousConnectionsURL;
|
|
382
|
+
}
|
|
383
|
+
if (previousConnectionToken === undefined) {
|
|
384
|
+
delete process.env.OPENCOMPUTER_CONNECTION_TOKEN;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
process.env.OPENCOMPUTER_CONNECTION_TOKEN = previousConnectionToken;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
278
390
|
const authenticated = await instance.client.auth.set({
|
|
279
391
|
providerID: "openrouter",
|
|
280
392
|
auth: { type: "api", key: gateway.token },
|
|
@@ -283,15 +395,32 @@ async function startDevService(config) {
|
|
|
283
395
|
throw new Error("The embedded agent runtime rejected its local credential");
|
|
284
396
|
}
|
|
285
397
|
const token = randomBytes(32).toString("base64url");
|
|
398
|
+
const sessions = new Map();
|
|
399
|
+
const running = new Set();
|
|
286
400
|
const server = createServer((request, response) => {
|
|
287
401
|
void (async () => {
|
|
288
402
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
289
403
|
if (request.method === "GET" && url.pathname === "/") {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
404
|
+
response.writeHead(200, {
|
|
405
|
+
"content-type": "text/html; charset=utf-8",
|
|
406
|
+
"cache-control": "no-store",
|
|
407
|
+
"content-security-policy": "default-src 'none'; script-src 'self'; style-src 'self'; " +
|
|
408
|
+
"connect-src 'self'; img-src 'self' data:; base-uri 'none'; frame-ancestors 'none'",
|
|
294
409
|
});
|
|
410
|
+
response.end(renderDevUI(manifest.name));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const assetMatch = url.pathname.match(/^\/assets\/([A-Za-z0-9_.-]+\.(?:js|css))$/);
|
|
414
|
+
if (request.method === "GET" && assetMatch?.[1]) {
|
|
415
|
+
const filename = assetMatch[1];
|
|
416
|
+
const asset = await readFile(fileURLToPath(new URL(`./ui/${filename}`, import.meta.url)));
|
|
417
|
+
response.writeHead(200, {
|
|
418
|
+
"content-type": filename.endsWith(".css")
|
|
419
|
+
? "text/css; charset=utf-8"
|
|
420
|
+
: "text/javascript; charset=utf-8",
|
|
421
|
+
"cache-control": "no-store",
|
|
422
|
+
});
|
|
423
|
+
response.end(asset);
|
|
295
424
|
return;
|
|
296
425
|
}
|
|
297
426
|
if (!authorized(request, token)) {
|
|
@@ -299,15 +428,23 @@ async function startDevService(config) {
|
|
|
299
428
|
return;
|
|
300
429
|
}
|
|
301
430
|
if (request.method === "GET" && url.pathname === "/health") {
|
|
302
|
-
sendJSON(response, 200, {
|
|
431
|
+
sendJSON(response, 200, {
|
|
432
|
+
ok: true,
|
|
433
|
+
agentId: manifest.id,
|
|
434
|
+
sessions: sessions.size,
|
|
435
|
+
});
|
|
303
436
|
return;
|
|
304
437
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
sendJSON(response, 400, { message: "A prompt is required" });
|
|
438
|
+
const streamSession = async (session, prompt, created = false) => {
|
|
439
|
+
if (running.has(session.id)) {
|
|
440
|
+
sendJSON(response, 409, { message: "This session is already running" });
|
|
309
441
|
return;
|
|
310
442
|
}
|
|
443
|
+
running.add(session.id);
|
|
444
|
+
session.messages.push({ role: "user", text: prompt });
|
|
445
|
+
if (!session.title)
|
|
446
|
+
session.title = prompt.slice(0, 60);
|
|
447
|
+
session.updatedAt = new Date().toISOString();
|
|
311
448
|
response.writeHead(200, {
|
|
312
449
|
"content-type": "application/x-ndjson",
|
|
313
450
|
"cache-control": "no-store",
|
|
@@ -315,8 +452,13 @@ async function startDevService(config) {
|
|
|
315
452
|
const emit = (event) => {
|
|
316
453
|
response.write(`${JSON.stringify(event)}\n`);
|
|
317
454
|
};
|
|
455
|
+
if (created) {
|
|
456
|
+
emit({ type: "session.created", data: { sessionId: session.id } });
|
|
457
|
+
}
|
|
318
458
|
try {
|
|
319
|
-
await streamTurn(instance.client, directory,
|
|
459
|
+
const text = await streamTurn(instance.client, directory, session.id, prompt, emit);
|
|
460
|
+
session.messages.push({ role: "assistant", text });
|
|
461
|
+
session.updatedAt = new Date().toISOString();
|
|
320
462
|
}
|
|
321
463
|
catch (error) {
|
|
322
464
|
emit({
|
|
@@ -326,7 +468,75 @@ async function startDevService(config) {
|
|
|
326
468
|
},
|
|
327
469
|
});
|
|
328
470
|
}
|
|
471
|
+
finally {
|
|
472
|
+
running.delete(session.id);
|
|
473
|
+
}
|
|
329
474
|
response.end();
|
|
475
|
+
};
|
|
476
|
+
if (request.method === "GET" && url.pathname === "/sessions") {
|
|
477
|
+
sendJSON(response, 200, {
|
|
478
|
+
sessions: [...sessions.values()]
|
|
479
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
480
|
+
.map(({ id, title, createdAt, updatedAt, messages }) => ({
|
|
481
|
+
id,
|
|
482
|
+
title,
|
|
483
|
+
createdAt,
|
|
484
|
+
updatedAt,
|
|
485
|
+
messageCount: messages.length,
|
|
486
|
+
})),
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (request.method === "POST" && url.pathname === "/sessions") {
|
|
491
|
+
const raw = (await readBody(request)).toString("utf8");
|
|
492
|
+
const body = (raw ? JSON.parse(raw) : {});
|
|
493
|
+
const id = await createRuntimeSession(instance.client, directory);
|
|
494
|
+
const now = new Date().toISOString();
|
|
495
|
+
const session = {
|
|
496
|
+
id,
|
|
497
|
+
title: "",
|
|
498
|
+
createdAt: now,
|
|
499
|
+
updatedAt: now,
|
|
500
|
+
messages: [],
|
|
501
|
+
};
|
|
502
|
+
sessions.set(id, session);
|
|
503
|
+
if (typeof body.prompt === "string" && body.prompt.trim()) {
|
|
504
|
+
response.setHeader("x-opencomputer-session-id", id);
|
|
505
|
+
await streamSession(session, body.prompt.trim(), true);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
sendJSON(response, 201, session);
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)$/);
|
|
513
|
+
if (sessionMatch?.[1]) {
|
|
514
|
+
const id = decodeURIComponent(sessionMatch[1]);
|
|
515
|
+
const session = sessions.get(id);
|
|
516
|
+
if (!session) {
|
|
517
|
+
sendJSON(response, 404, { message: "Session not found" });
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (request.method === "GET") {
|
|
521
|
+
sendJSON(response, 200, session);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (request.method === "POST") {
|
|
525
|
+
const body = JSON.parse((await readBody(request)).toString("utf8"));
|
|
526
|
+
if (typeof body.prompt !== "string" || !body.prompt.trim()) {
|
|
527
|
+
sendJSON(response, 400, { message: "A prompt is required" });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
await streamSession(session, body.prompt.trim());
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (request.method === "GET" && url.pathname === "/api") {
|
|
535
|
+
sendJSON(response, 200, {
|
|
536
|
+
service: "OpenComputer local agent",
|
|
537
|
+
agentId: manifest.id,
|
|
538
|
+
endpoints: ["GET /sessions", "POST /sessions", "POST /sessions/:id"],
|
|
539
|
+
});
|
|
330
540
|
return;
|
|
331
541
|
}
|
|
332
542
|
sendJSON(response, 404, { message: "Route not found" });
|
|
@@ -362,30 +572,29 @@ async function startDevService(config) {
|
|
|
362
572
|
await writeFile(statePath(root), `${JSON.stringify(state, null, 2)}\n`, {
|
|
363
573
|
mode: 0o600,
|
|
364
574
|
});
|
|
575
|
+
const webUrl = `${state.url}/#token=${encodeURIComponent(token)}`;
|
|
365
576
|
process.stdout.write(`OpenComputer dev service ready\n` +
|
|
366
577
|
`Agent: ${manifest.name} (${manifest.id})\n` +
|
|
367
|
-
`
|
|
368
|
-
`
|
|
369
|
-
`
|
|
578
|
+
`Web: ${webUrl}\n` +
|
|
579
|
+
`Local API: ${state.url}\n` +
|
|
580
|
+
`Session: opencomputer session\n`);
|
|
581
|
+
openBrowser(webUrl);
|
|
370
582
|
await new Promise((done) => {
|
|
371
583
|
process.once("SIGINT", done);
|
|
372
584
|
process.once("SIGTERM", done);
|
|
373
585
|
});
|
|
374
586
|
await rm(statePath(root), { force: true });
|
|
587
|
+
server.closeAllConnections();
|
|
375
588
|
await new Promise((done) => server.close(() => done()));
|
|
376
589
|
abortController.abort();
|
|
377
590
|
instance.server.close();
|
|
378
591
|
await gateway.close();
|
|
379
592
|
}
|
|
380
|
-
async function runLocalSession(prompt) {
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
if (!state) {
|
|
386
|
-
throw new Error("OpenComputer dev is not running. Start `opencomputer dev` in another terminal.");
|
|
387
|
-
}
|
|
388
|
-
const response = await fetch(`${state.url}/sessions`, {
|
|
593
|
+
async function runLocalSession(prompt, state, sessionID, onEvent) {
|
|
594
|
+
const endpoint = sessionID
|
|
595
|
+
? `${state.url}/sessions/${encodeURIComponent(sessionID)}`
|
|
596
|
+
: `${state.url}/sessions`;
|
|
597
|
+
const response = await fetch(endpoint, {
|
|
389
598
|
method: "POST",
|
|
390
599
|
headers: {
|
|
391
600
|
authorization: `Bearer ${state.token}`,
|
|
@@ -394,8 +603,11 @@ async function runLocalSession(prompt) {
|
|
|
394
603
|
body: JSON.stringify({ prompt }),
|
|
395
604
|
});
|
|
396
605
|
if (!response.ok || !response.body) {
|
|
397
|
-
|
|
606
|
+
const detail = await response.text().catch(() => "");
|
|
607
|
+
throw new Error(`The local agent service returned ${String(response.status)}` +
|
|
608
|
+
(detail ? `: ${detail}` : ""));
|
|
398
609
|
}
|
|
610
|
+
let resolvedSessionID = sessionID ?? response.headers.get("x-opencomputer-session-id") ?? undefined;
|
|
399
611
|
const decoder = new TextDecoder();
|
|
400
612
|
let buffered = "";
|
|
401
613
|
let streamedText = false;
|
|
@@ -407,43 +619,139 @@ async function runLocalSession(prompt) {
|
|
|
407
619
|
if (!line.trim())
|
|
408
620
|
continue;
|
|
409
621
|
const event = JSON.parse(line);
|
|
622
|
+
onEvent?.(event);
|
|
410
623
|
if (event.type === "session.created") {
|
|
411
|
-
|
|
624
|
+
resolvedSessionID = String(event.data.sessionId);
|
|
625
|
+
if (!onEvent)
|
|
626
|
+
process.stderr.write(`Session ${resolvedSessionID}\n`);
|
|
412
627
|
}
|
|
413
628
|
else if (event.type === "message.delta") {
|
|
414
629
|
streamedText = true;
|
|
415
|
-
|
|
630
|
+
if (!onEvent)
|
|
631
|
+
process.stdout.write(String(event.data.text ?? ""));
|
|
416
632
|
}
|
|
417
633
|
else if (event.type === "message.completed") {
|
|
418
|
-
if (!
|
|
419
|
-
|
|
420
|
-
|
|
634
|
+
if (!onEvent) {
|
|
635
|
+
if (!streamedText)
|
|
636
|
+
process.stdout.write(String(event.data.text ?? ""));
|
|
637
|
+
process.stdout.write("\n");
|
|
638
|
+
}
|
|
421
639
|
}
|
|
422
640
|
else if (event.type === "tool.started") {
|
|
423
|
-
|
|
641
|
+
if (!onEvent) {
|
|
642
|
+
process.stderr.write(`⚙ ${String(event.data.tool ?? "tool")} ${JSON.stringify(event.data.input ?? {})}\n`);
|
|
643
|
+
}
|
|
424
644
|
}
|
|
425
645
|
else if (event.type === "tool.completed") {
|
|
426
|
-
|
|
646
|
+
if (!onEvent) {
|
|
647
|
+
process.stderr.write(`✓ ${String(event.data.tool ?? "tool")}\n`);
|
|
648
|
+
}
|
|
427
649
|
}
|
|
428
650
|
else if (event.type === "tool.failed") {
|
|
429
|
-
|
|
651
|
+
if (!onEvent) {
|
|
652
|
+
process.stderr.write(`✗ ${String(event.data.tool ?? "tool")}: ${String(event.data.message ?? "failed")}\n`);
|
|
653
|
+
}
|
|
430
654
|
}
|
|
431
655
|
else if (event.type === "session.failed") {
|
|
432
656
|
throw new Error(String(event.data.message ?? "Local session failed"));
|
|
433
657
|
}
|
|
434
658
|
}
|
|
435
659
|
}
|
|
660
|
+
if (!resolvedSessionID)
|
|
661
|
+
throw new Error("The local session did not return an ID");
|
|
662
|
+
return resolvedSessionID;
|
|
663
|
+
}
|
|
664
|
+
async function stopOwnedDev(child) {
|
|
665
|
+
if (!child || child.exitCode !== null)
|
|
666
|
+
return;
|
|
667
|
+
child.kill("SIGTERM");
|
|
668
|
+
await Promise.race([
|
|
669
|
+
new Promise((done) => child.once("exit", () => done())),
|
|
670
|
+
new Promise((done) => setTimeout(done, 3_000)),
|
|
671
|
+
]);
|
|
672
|
+
if (child.exitCode === null)
|
|
673
|
+
child.kill("SIGKILL");
|
|
674
|
+
}
|
|
675
|
+
async function ensureDevService(config) {
|
|
676
|
+
const root = await findAgentRoot();
|
|
677
|
+
if (!root) {
|
|
678
|
+
throw new Error("No OpenComputer agent repository found. Run `opencomputer init <template>` first.");
|
|
679
|
+
}
|
|
680
|
+
const existing = await readDevState(root);
|
|
681
|
+
if (existing)
|
|
682
|
+
return { state: existing };
|
|
683
|
+
const environment = {
|
|
684
|
+
...process.env,
|
|
685
|
+
OPENCOMPUTER_API_URL: config.apiUrl,
|
|
686
|
+
OPENCOMPUTER_NO_OPEN: "1",
|
|
687
|
+
};
|
|
688
|
+
if (config.apiKey)
|
|
689
|
+
environment.OPENCOMPUTER_API_KEY = config.apiKey;
|
|
690
|
+
const child = spawn(process.execPath, [process.argv[1], "dev"], {
|
|
691
|
+
cwd: root,
|
|
692
|
+
env: environment,
|
|
693
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
694
|
+
});
|
|
695
|
+
let errors = "";
|
|
696
|
+
child.stderr?.on("data", (chunk) => {
|
|
697
|
+
errors = `${errors}${chunk.toString("utf8")}`.slice(-8_000);
|
|
698
|
+
});
|
|
699
|
+
const deadline = Date.now() + 60_000;
|
|
700
|
+
while (Date.now() < deadline) {
|
|
701
|
+
const state = await readDevState(root);
|
|
702
|
+
if (state)
|
|
703
|
+
return { state, owned: child };
|
|
704
|
+
if (child.exitCode !== null) {
|
|
705
|
+
throw new Error(errors.trim() || "The local development service exited");
|
|
706
|
+
}
|
|
707
|
+
await new Promise((done) => setTimeout(done, 100));
|
|
708
|
+
}
|
|
709
|
+
await stopOwnedDev(child);
|
|
710
|
+
throw new Error("Timed out starting the local development service");
|
|
711
|
+
}
|
|
712
|
+
async function runSessionShell(config) {
|
|
713
|
+
const target = await ensureDevService(config);
|
|
714
|
+
try {
|
|
715
|
+
const manifest = await readManifest(target.state.agentRoot);
|
|
716
|
+
const { runSessionTUI } = await import("./tui.js");
|
|
717
|
+
await runSessionTUI({
|
|
718
|
+
agentName: manifest.name,
|
|
719
|
+
send: (prompt, sessionId, emit) => runLocalSession(prompt, target.state, sessionId, emit),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
finally {
|
|
723
|
+
await stopOwnedDev(target.owned);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function runOneShotSession(prompt, config) {
|
|
727
|
+
const target = await ensureDevService(config);
|
|
728
|
+
try {
|
|
729
|
+
await runLocalSession(prompt, target.state);
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
await stopOwnedDev(target.owned);
|
|
733
|
+
}
|
|
436
734
|
}
|
|
437
735
|
export async function runLocalAgent(args, config) {
|
|
736
|
+
if (args[0] === "dev") {
|
|
737
|
+
if (args.length > 1)
|
|
738
|
+
throw new Error(`Unexpected local argument: ${args[1]}`);
|
|
739
|
+
await startDevService(config);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
438
742
|
if (args[0] === "run") {
|
|
439
743
|
const prompt = args.slice(1).join(" ").trim();
|
|
440
744
|
if (!prompt)
|
|
441
745
|
throw new Error("A prompt is required");
|
|
442
|
-
await
|
|
746
|
+
await runOneShotSession(prompt, config);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (args[0] === "shell") {
|
|
750
|
+
if (args.length > 1)
|
|
751
|
+
throw new Error(`Unexpected local argument: ${args[1]}`);
|
|
752
|
+
await runSessionShell(config);
|
|
443
753
|
return;
|
|
444
754
|
}
|
|
445
|
-
|
|
446
|
-
throw new Error(`Unexpected local argument: ${args[0]}`);
|
|
447
|
-
await startDevService(config);
|
|
755
|
+
throw new Error(`Unexpected local argument: ${args[0] ?? "none"}`);
|
|
448
756
|
}
|
|
449
757
|
//# sourceMappingURL=local.js.map
|