@opencomputer/blue 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/README.md +287 -0
- package/dist/auth/cli-auth.d.ts +30 -0
- package/dist/auth/cli-auth.js +206 -0
- package/dist/auth/cli-auth.js.map +1 -0
- package/dist/channels/slack-progress.d.ts +27 -0
- package/dist/channels/slack-progress.js +149 -0
- package/dist/channels/slack-progress.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +790 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/agent.d.ts +10 -0
- package/dist/core/agent.js +112 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/artifact.d.ts +7 -0
- package/dist/core/artifact.js +46 -0
- package/dist/core/artifact.js.map +1 -0
- package/dist/dev/local-control-plane.d.ts +11 -0
- package/dist/dev/local-control-plane.js +639 -0
- package/dist/dev/local-control-plane.js.map +1 -0
- package/dist/dev/manager.d.ts +35 -0
- package/dist/dev/manager.js +589 -0
- package/dist/dev/manager.js.map +1 -0
- package/dist/openrouter/auth.d.ts +2 -0
- package/dist/openrouter/auth.js +144 -0
- package/dist/openrouter/auth.js.map +1 -0
- package/dist/protocol.d.ts +187 -0
- package/dist/protocol.js +14 -0
- package/dist/protocol.js.map +1 -0
- package/dist/runtime/harness.d.ts +30 -0
- package/dist/runtime/harness.js +370 -0
- package/dist/runtime/harness.js.map +1 -0
- package/dist/runtime/local-runtime.d.ts +2 -0
- package/dist/runtime/local-runtime.js +161 -0
- package/dist/runtime/local-runtime.js.map +1 -0
- package/dist/sdk/client.d.ts +28 -0
- package/dist/sdk/client.js +131 -0
- package/dist/sdk/client.js.map +1 -0
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.js +3 -0
- package/dist/sdk/index.js.map +1 -0
- package/package.json +95 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, resolve } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { DEFAULT_BLUE_API_URL, accessTokenFor, login, logout, readCredentials, } from "../auth/cli-auth.js";
|
|
9
|
+
import { addSlackChannel, findAgentRoot, initializeAgent, prepareAgent, } from "../core/agent.js";
|
|
10
|
+
import { buildAgentArtifact } from "../core/artifact.js";
|
|
11
|
+
import { findDevState, requestDevManager, startDevManager, } from "../dev/manager.js";
|
|
12
|
+
import { startLocalControlPlane } from "../dev/local-control-plane.js";
|
|
13
|
+
import { authenticateOpenRouter, readOpenRouterCredential, } from "../openrouter/auth.js";
|
|
14
|
+
import { BlueClient } from "../sdk/client.js";
|
|
15
|
+
const DEFAULT_EDGE = DEFAULT_BLUE_API_URL;
|
|
16
|
+
function argument(name) {
|
|
17
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
18
|
+
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
19
|
+
}
|
|
20
|
+
function positional(afterCommand) {
|
|
21
|
+
const booleanFlags = new Set([
|
|
22
|
+
"--keep",
|
|
23
|
+
"--local",
|
|
24
|
+
"--local-only",
|
|
25
|
+
"--remote",
|
|
26
|
+
]);
|
|
27
|
+
return process.argv
|
|
28
|
+
.slice(afterCommand)
|
|
29
|
+
.filter((value, index, all) => {
|
|
30
|
+
if (index > 0 &&
|
|
31
|
+
all[index - 1]?.startsWith("--") &&
|
|
32
|
+
!booleanFlags.has(all[index - 1])) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return !value.startsWith("--");
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function hasFlag(name) {
|
|
39
|
+
return process.argv.includes(`--${name}`);
|
|
40
|
+
}
|
|
41
|
+
function runtimePath() {
|
|
42
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "../runtime/local-runtime.js");
|
|
43
|
+
}
|
|
44
|
+
async function clientFor(edge) {
|
|
45
|
+
const explicit = argument("api-key") ?? process.env.BLUE_API_KEY;
|
|
46
|
+
return new BlueClient(edge, explicit ?? (await accessTokenFor(edge)));
|
|
47
|
+
}
|
|
48
|
+
function hostedEdge() {
|
|
49
|
+
return (argument("api-url") ??
|
|
50
|
+
argument("edge") ??
|
|
51
|
+
process.env.BLUE_API_URL ??
|
|
52
|
+
process.env.BLUE_EDGE_URL ??
|
|
53
|
+
DEFAULT_EDGE);
|
|
54
|
+
}
|
|
55
|
+
function spawnRuntime(edge, sessionId, token, harness, directory) {
|
|
56
|
+
const usingTypescript = import.meta.url.endsWith(".ts");
|
|
57
|
+
const blueRuntimeDirectory = resolve(directory, ".blue");
|
|
58
|
+
mkdirSync(blueRuntimeDirectory, { recursive: true });
|
|
59
|
+
return spawn(process.execPath, [
|
|
60
|
+
...(usingTypescript ? ["--import", "tsx"] : []),
|
|
61
|
+
runtimePath().replace(/\.js$/, usingTypescript ? ".ts" : ".js"),
|
|
62
|
+
"--edge",
|
|
63
|
+
edge,
|
|
64
|
+
"--session",
|
|
65
|
+
sessionId,
|
|
66
|
+
"--token",
|
|
67
|
+
token,
|
|
68
|
+
"--harness",
|
|
69
|
+
harness,
|
|
70
|
+
"--directory",
|
|
71
|
+
directory,
|
|
72
|
+
], {
|
|
73
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
74
|
+
env: {
|
|
75
|
+
...process.env,
|
|
76
|
+
BLUE_OPENCODE_SESSION_PATH: resolve(blueRuntimeDirectory, "opencode-session.json"),
|
|
77
|
+
OPENCODE_DB: resolve(blueRuntimeDirectory, "opencode.db"),
|
|
78
|
+
BLUE_RUNTIME_QUIET: "1",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function waitForEvent(socket, predicate, options = {}) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
let sawMessageDelta = false;
|
|
85
|
+
let sawReasoningDelta = false;
|
|
86
|
+
let startedAnswer = false;
|
|
87
|
+
const timeout = options.timeoutMs === undefined
|
|
88
|
+
? undefined
|
|
89
|
+
: setTimeout(() => {
|
|
90
|
+
cleanup();
|
|
91
|
+
reject(new Error(`Timed out waiting for session event after ${String(options.timeoutMs)}ms`));
|
|
92
|
+
}, options.timeoutMs);
|
|
93
|
+
const onMessage = (message) => {
|
|
94
|
+
let frame;
|
|
95
|
+
try {
|
|
96
|
+
frame = JSON.parse(String(message.data));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (frame.type === "event") {
|
|
102
|
+
if (options.rejectOnRuntimeDisconnect &&
|
|
103
|
+
frame.event.type === "runtime.disconnected") {
|
|
104
|
+
cleanup();
|
|
105
|
+
const code = Number(frame.event.data.code);
|
|
106
|
+
const providedReason = typeof frame.event.data.reason === "string"
|
|
107
|
+
? frame.event.data.reason.trim()
|
|
108
|
+
: "";
|
|
109
|
+
const reason = providedReason ||
|
|
110
|
+
(code === 4003
|
|
111
|
+
? "OpenRouter credential is unavailable. Run `blue login` or `blue auth openrouter`, then restart `blue dev`."
|
|
112
|
+
: "unknown reason");
|
|
113
|
+
reject(new Error(`Runtime disconnected${Number.isFinite(code) ? ` (${String(code)})` : ""}: ${reason}`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (frame.event.type === "reasoning.delta") {
|
|
117
|
+
if (!sawReasoningDelta) {
|
|
118
|
+
process.stdout.write("thinking: ");
|
|
119
|
+
sawReasoningDelta = true;
|
|
120
|
+
}
|
|
121
|
+
process.stdout.write(String(frame.event.data.text ?? ""));
|
|
122
|
+
}
|
|
123
|
+
if (frame.event.type === "message.delta") {
|
|
124
|
+
if (!startedAnswer && sawReasoningDelta) {
|
|
125
|
+
process.stdout.write("\nanswer: ");
|
|
126
|
+
}
|
|
127
|
+
startedAnswer = true;
|
|
128
|
+
sawMessageDelta = true;
|
|
129
|
+
process.stdout.write(String(frame.event.data.text ?? ""));
|
|
130
|
+
}
|
|
131
|
+
if (frame.event.type === "tool.started") {
|
|
132
|
+
process.stdout.write(`\ntool: ${String(frame.event.data.tool ?? "tool")} (${String(frame.event.data.title ?? "running")})\n`);
|
|
133
|
+
}
|
|
134
|
+
if (frame.event.type === "tool.completed") {
|
|
135
|
+
process.stdout.write(`tool: ${String(frame.event.data.tool ?? "tool")} completed\n`);
|
|
136
|
+
}
|
|
137
|
+
if (frame.event.type === "tool.failed") {
|
|
138
|
+
process.stdout.write(`tool: ${String(frame.event.data.tool ?? "tool")} failed\n`);
|
|
139
|
+
}
|
|
140
|
+
if (!sawMessageDelta &&
|
|
141
|
+
frame.event.type === "message.completed" &&
|
|
142
|
+
typeof frame.event.data.text === "string") {
|
|
143
|
+
process.stdout.write(frame.event.data.text);
|
|
144
|
+
}
|
|
145
|
+
if (predicate(frame.event)) {
|
|
146
|
+
cleanup();
|
|
147
|
+
resolve(frame.event);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (frame.type === "error") {
|
|
151
|
+
cleanup();
|
|
152
|
+
reject(new Error(frame.message));
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const onError = () => {
|
|
156
|
+
cleanup();
|
|
157
|
+
reject(new Error("Client WebSocket connection failed"));
|
|
158
|
+
};
|
|
159
|
+
const onClose = () => {
|
|
160
|
+
cleanup();
|
|
161
|
+
reject(new Error("Client WebSocket closed before the expected event"));
|
|
162
|
+
};
|
|
163
|
+
const cleanup = () => {
|
|
164
|
+
if (timeout)
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
socket.removeEventListener("message", onMessage);
|
|
167
|
+
socket.removeEventListener("error", onError);
|
|
168
|
+
socket.removeEventListener("close", onClose);
|
|
169
|
+
};
|
|
170
|
+
socket.addEventListener("message", onMessage);
|
|
171
|
+
socket.addEventListener("error", onError);
|
|
172
|
+
socket.addEventListener("close", onClose);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async function runRemoteTurn(edge, prompt) {
|
|
176
|
+
const agent = argument("agent") ?? (await inferDeployedAgent());
|
|
177
|
+
if (!agent) {
|
|
178
|
+
throw new Error("No agent repository was found. Pass --agent <agent>@<alias>.");
|
|
179
|
+
}
|
|
180
|
+
await createRemoteSession(edge, agent, prompt);
|
|
181
|
+
}
|
|
182
|
+
async function suspendRemoteSession(client, sessionId) {
|
|
183
|
+
process.stdout.write("runtime: suspending...\n");
|
|
184
|
+
const session = await client.suspendSession(sessionId);
|
|
185
|
+
process.stdout.write(`runtime: ${session.microvmState ?? "suspended"}\n`);
|
|
186
|
+
}
|
|
187
|
+
async function sendRemoteTurn(edge, sessionId, prompt) {
|
|
188
|
+
const client = await clientFor(edge);
|
|
189
|
+
const session = await client.getSession(sessionId);
|
|
190
|
+
if (!session.microvmId) {
|
|
191
|
+
throw new Error(`Session ${sessionId} has no MicroVM`);
|
|
192
|
+
}
|
|
193
|
+
if (session.microvmState !== "suspended" &&
|
|
194
|
+
session.microvmState !== "running") {
|
|
195
|
+
throw new Error(`Session ${sessionId} MicroVM is ${session.microvmState ?? "unknown"}, expected running or suspended`);
|
|
196
|
+
}
|
|
197
|
+
const existingEvents = await client.listEvents(sessionId);
|
|
198
|
+
const cursor = existingEvents.at(-1)?.seq ?? 0;
|
|
199
|
+
const socket = client.connect(sessionId, cursor);
|
|
200
|
+
try {
|
|
201
|
+
process.stdout.write(`session: ${sessionId}\n`);
|
|
202
|
+
if (session.microvmState === "suspended") {
|
|
203
|
+
const runtimeConnected = waitForEvent(socket, (event) => event.type === "runtime.connected", { timeoutMs: 60_000, rejectOnRuntimeDisconnect: true });
|
|
204
|
+
process.stdout.write("runtime: resuming...\n");
|
|
205
|
+
const resumedAt = Date.now();
|
|
206
|
+
await client.resumeSession(sessionId);
|
|
207
|
+
await runtimeConnected;
|
|
208
|
+
process.stdout.write(`runtime: resumed in ${String(Date.now() - resumedAt)}ms\n`);
|
|
209
|
+
}
|
|
210
|
+
const completion = waitForEvent(socket, (event) => event.type === "turn.completed" || event.type === "turn.failed", { rejectOnRuntimeDisconnect: true });
|
|
211
|
+
const turn = await client.createTurn(sessionId, prompt);
|
|
212
|
+
process.stdout.write("agent: waiting for model...\n");
|
|
213
|
+
const terminal = await completion;
|
|
214
|
+
process.stdout.write(`\nturn: ${turn.turnId} completed\n`);
|
|
215
|
+
if (hasFlag("keep")) {
|
|
216
|
+
process.stdout.write(`runtime: ${session.microvmId} running\n`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
await suspendRemoteSession(client, sessionId);
|
|
220
|
+
}
|
|
221
|
+
if (terminal.type === "turn.failed") {
|
|
222
|
+
throw new Error(String(terminal.data.message ?? "Turn failed"));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
socket.close();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function endRemoteSession(edge, sessionId) {
|
|
230
|
+
const client = await clientFor(edge);
|
|
231
|
+
const session = await client.getSession(sessionId);
|
|
232
|
+
if (session.microvmId && session.microvmState !== "terminated") {
|
|
233
|
+
await client.terminateSession(sessionId);
|
|
234
|
+
process.stdout.write(`runtime: ${session.microvmId} terminated\n`);
|
|
235
|
+
}
|
|
236
|
+
await client.endSession(sessionId);
|
|
237
|
+
process.stdout.write(`session: ${sessionId} ended\n`);
|
|
238
|
+
}
|
|
239
|
+
async function runTurn(edge, prompt) {
|
|
240
|
+
const harness = (argument("harness") ?? "mock");
|
|
241
|
+
if (harness !== "mock" && harness !== "opencode") {
|
|
242
|
+
throw new Error("--harness must be mock or opencode");
|
|
243
|
+
}
|
|
244
|
+
const prepared = await prepareAgent(argument("agent-dir") ?? process.cwd());
|
|
245
|
+
const client = new BlueClient(edge, argument("api-key") ?? process.env.BLUE_API_KEY);
|
|
246
|
+
const created = await client.createSession({
|
|
247
|
+
agentId: argument("agent") ?? `local/${prepared.name}`,
|
|
248
|
+
deploymentId: "development",
|
|
249
|
+
});
|
|
250
|
+
process.stdout.write(`session: ${created.session.id}\n`);
|
|
251
|
+
const socket = client.connect(created.session.id);
|
|
252
|
+
const runtime = spawnRuntime(edge, created.session.id, created.runtimeToken, harness, prepared.runtimeDirectory);
|
|
253
|
+
runtime.stdout?.on("data", (chunk) => {
|
|
254
|
+
if (process.env.BLUE_DEBUG)
|
|
255
|
+
process.stderr.write(chunk);
|
|
256
|
+
});
|
|
257
|
+
try {
|
|
258
|
+
const completion = waitForEvent(socket, (event) => event.type === "turn.completed" || event.type === "turn.failed");
|
|
259
|
+
const turn = await client.createTurn(created.session.id, prompt);
|
|
260
|
+
const terminal = await completion;
|
|
261
|
+
if (terminal.type === "turn.failed") {
|
|
262
|
+
throw new Error(String(terminal.data.message ?? "Turn failed"));
|
|
263
|
+
}
|
|
264
|
+
process.stdout.write(`\nturn: ${turn.turnId} completed\n`);
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
socket.close();
|
|
268
|
+
runtime.kill("SIGTERM");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function executePrompt(client, socket, sessionId, prompt) {
|
|
272
|
+
const completion = waitForEvent(socket, (event) => event.type === "turn.completed" || event.type === "turn.failed", { timeoutMs: 180_000, rejectOnRuntimeDisconnect: true });
|
|
273
|
+
const turn = await client.createTurn(sessionId, prompt);
|
|
274
|
+
const terminal = await completion;
|
|
275
|
+
process.stdout.write(`\nturn: ${turn.turnId} ${terminal.type.slice(5)}\n`);
|
|
276
|
+
if (terminal.type === "turn.failed") {
|
|
277
|
+
throw new Error(String(terminal.data.message ?? "Turn failed"));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function runRemoteSessionShell(client, socket, sessionId, exitAction = "leave the session running") {
|
|
281
|
+
const readline = createInterface({
|
|
282
|
+
input: process.stdin,
|
|
283
|
+
output: process.stdout,
|
|
284
|
+
terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
285
|
+
});
|
|
286
|
+
readline.on("SIGINT", () => readline.close());
|
|
287
|
+
process.stdout.write(`${terminalStyle(90, `Enter a prompt. Use /exit or Ctrl-C to ${exitAction}.`)}\n\n` +
|
|
288
|
+
terminalStyle(34, "blue> "));
|
|
289
|
+
try {
|
|
290
|
+
for await (const line of readline) {
|
|
291
|
+
const prompt = line.trim();
|
|
292
|
+
if (prompt === "/exit" || prompt === "/quit")
|
|
293
|
+
break;
|
|
294
|
+
if (prompt) {
|
|
295
|
+
try {
|
|
296
|
+
await executePrompt(client, socket, sessionId, prompt);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
300
|
+
if (message.startsWith("Runtime disconnected"))
|
|
301
|
+
throw error;
|
|
302
|
+
process.stderr.write(`${terminalStyle(31, "turn failed")} ${message}\n`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
process.stdout.write(terminalStyle(34, "blue> "));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
readline.close();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function sessionTarget() {
|
|
313
|
+
const explicitEdge = argument("edge");
|
|
314
|
+
if (explicitEdge)
|
|
315
|
+
return { edge: explicitEdge, dev: null };
|
|
316
|
+
if (hasFlag("local")) {
|
|
317
|
+
const dev = await findDevState();
|
|
318
|
+
if (!dev)
|
|
319
|
+
throw new Error("No local `blue dev` process was found");
|
|
320
|
+
return { edge: dev.edge, dev };
|
|
321
|
+
}
|
|
322
|
+
if (hasFlag("remote") || argument("agent")) {
|
|
323
|
+
return { edge: hostedEdge(), dev: null };
|
|
324
|
+
}
|
|
325
|
+
const dev = await findDevState();
|
|
326
|
+
if (dev)
|
|
327
|
+
return { edge: dev.edge, dev };
|
|
328
|
+
return { edge: hostedEdge(), dev: null };
|
|
329
|
+
}
|
|
330
|
+
async function createLocalSession(state, prompt) {
|
|
331
|
+
const client = new BlueClient(state.edge);
|
|
332
|
+
const created = await client.createSession({
|
|
333
|
+
agentId: `local/${state.agentId}`,
|
|
334
|
+
deploymentId: "development",
|
|
335
|
+
});
|
|
336
|
+
const socket = client.connect(created.session.id);
|
|
337
|
+
try {
|
|
338
|
+
const connected = waitForEvent(socket, (event) => event.type === "runtime.connected", { timeoutMs: 60_000, rejectOnRuntimeDisconnect: true });
|
|
339
|
+
await requestDevManager(state, "POST", created.session.id, created.runtimeToken);
|
|
340
|
+
await connected;
|
|
341
|
+
process.stdout.write(`session: ${created.session.id}\nagent: local/${state.agentId}\nruntime: ready\n`);
|
|
342
|
+
if (prompt) {
|
|
343
|
+
await executePrompt(client, socket, created.session.id, prompt);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
await runRemoteSessionShell(client, socket, created.session.id, "close the local client");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
socket.close();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function createRemoteSession(edge, agentReference, prompt) {
|
|
354
|
+
const client = await clientFor(edge);
|
|
355
|
+
const created = await client.createSession({ agentId: agentReference });
|
|
356
|
+
const deployment = created.deployment;
|
|
357
|
+
if (!deployment) {
|
|
358
|
+
throw new Error(`Blue did not resolve deployment for ${agentReference}`);
|
|
359
|
+
}
|
|
360
|
+
process.stdout.write(`session: ${created.session.id}\n` +
|
|
361
|
+
`agent: ${deployment.agentId}@${deployment.alias}\n` +
|
|
362
|
+
`deployment: ${deployment.id}\n`);
|
|
363
|
+
const socket = client.connect(created.session.id);
|
|
364
|
+
try {
|
|
365
|
+
const connected = waitForEvent(socket, (event) => event.type === "runtime.connected", { timeoutMs: 60_000, rejectOnRuntimeDisconnect: true });
|
|
366
|
+
process.stdout.write("runtime: starting...\n");
|
|
367
|
+
await connected;
|
|
368
|
+
process.stdout.write("runtime: ready\n");
|
|
369
|
+
if (prompt) {
|
|
370
|
+
await executePrompt(client, socket, created.session.id, prompt);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
await runRemoteSessionShell(client, socket, created.session.id, "suspend the session");
|
|
374
|
+
}
|
|
375
|
+
if (hasFlag("keep")) {
|
|
376
|
+
process.stdout.write("runtime: running\n");
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
await suspendRemoteSession(client, created.session.id);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
socket.close();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function inferDeployedAgent() {
|
|
387
|
+
const root = await findAgentRoot(argument("agent-dir") ?? process.cwd());
|
|
388
|
+
if (!root)
|
|
389
|
+
return undefined;
|
|
390
|
+
const agentId = basename(root)
|
|
391
|
+
.toLowerCase()
|
|
392
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
393
|
+
.replace(/^-+|-+$/g, "");
|
|
394
|
+
return agentId
|
|
395
|
+
? `${agentId}@${argument("alias") ?? "production"}`
|
|
396
|
+
: undefined;
|
|
397
|
+
}
|
|
398
|
+
async function sendLocalTurn(state, sessionId, prompt) {
|
|
399
|
+
const client = new BlueClient(state.edge);
|
|
400
|
+
await requestDevManager(state, "POST", sessionId);
|
|
401
|
+
const session = await client.getSession(sessionId);
|
|
402
|
+
if (!session.agentId.startsWith("local/")) {
|
|
403
|
+
throw new Error(`${sessionId} is not a local development session`);
|
|
404
|
+
}
|
|
405
|
+
const events = await client.listEvents(sessionId);
|
|
406
|
+
const socket = client.connect(sessionId, events.at(-1)?.seq ?? 0);
|
|
407
|
+
try {
|
|
408
|
+
await executePrompt(client, socket, sessionId, prompt);
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
socket.close();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function deployAgent(edge) {
|
|
415
|
+
const built = await buildAgentArtifact(argument("agent-dir") ?? process.cwd());
|
|
416
|
+
const client = await clientFor(edge);
|
|
417
|
+
const upload = await client.createDeploymentUpload({
|
|
418
|
+
agentId: built.agentId,
|
|
419
|
+
digest: built.digest,
|
|
420
|
+
size: built.body.byteLength,
|
|
421
|
+
contentType: "application/vnd.blue.agent+json",
|
|
422
|
+
});
|
|
423
|
+
const uploaded = await fetch(upload.uploadUrl, {
|
|
424
|
+
method: upload.method,
|
|
425
|
+
headers: upload.headers,
|
|
426
|
+
body: built.body,
|
|
427
|
+
});
|
|
428
|
+
if (!uploaded.ok) {
|
|
429
|
+
throw new Error(`Agent upload failed with status ${String(uploaded.status)}`);
|
|
430
|
+
}
|
|
431
|
+
const alias = argument("alias") ?? "production";
|
|
432
|
+
const deployment = await client.registerDeployment({
|
|
433
|
+
agentId: built.agentId,
|
|
434
|
+
alias,
|
|
435
|
+
artifact: upload.artifact,
|
|
436
|
+
});
|
|
437
|
+
process.stdout.write(`agent: ${deployment.agentId}\n` +
|
|
438
|
+
`deployment: ${deployment.id}\n` +
|
|
439
|
+
`alias: ${deployment.alias}\n` +
|
|
440
|
+
`artifact: s3://${deployment.artifact.bucket}/${deployment.artifact.key}\n` +
|
|
441
|
+
`ready in: ${String(built.elapsedMs)}ms\n`);
|
|
442
|
+
}
|
|
443
|
+
function printSession(session) {
|
|
444
|
+
const separator = session.deploymentId.lastIndexOf(":");
|
|
445
|
+
const deployment = separator >= 0
|
|
446
|
+
? session.deploymentId.slice(separator + 1)
|
|
447
|
+
: session.deploymentId;
|
|
448
|
+
process.stdout.write(`${session.id} ${session.status.padEnd(15)} ` +
|
|
449
|
+
`${session.agentId}@${deployment.slice(0, 12)} ` +
|
|
450
|
+
`${session.microvmState ?? "local"}\n`);
|
|
451
|
+
}
|
|
452
|
+
async function attachSession(edge, sessionId) {
|
|
453
|
+
const client = await clientFor(edge);
|
|
454
|
+
const events = await client.listEvents(sessionId);
|
|
455
|
+
for (const event of events) {
|
|
456
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
457
|
+
}
|
|
458
|
+
const socket = client.connect(sessionId, events.at(-1)?.seq ?? 0);
|
|
459
|
+
socket.addEventListener("message", (message) => {
|
|
460
|
+
const frame = JSON.parse(String(message.data));
|
|
461
|
+
if (frame.type === "event") {
|
|
462
|
+
process.stdout.write(`${JSON.stringify(frame.event)}\n`);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
process.stdout.write("attached; press Ctrl-C to exit\n");
|
|
466
|
+
await new Promise((resolvePromise) => {
|
|
467
|
+
process.once("SIGINT", () => {
|
|
468
|
+
socket.close();
|
|
469
|
+
resolvePromise();
|
|
470
|
+
});
|
|
471
|
+
socket.addEventListener("close", () => resolvePromise());
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function terminalStyle(code, text) {
|
|
475
|
+
return process.stdout.isTTY ? `\u001b[${String(code)}m${text}\u001b[0m` : text;
|
|
476
|
+
}
|
|
477
|
+
async function dev() {
|
|
478
|
+
const requestedPort = argument("port");
|
|
479
|
+
const port = requestedPort
|
|
480
|
+
? Number.parseInt(requestedPort, 10)
|
|
481
|
+
: undefined;
|
|
482
|
+
if (port !== undefined && (!Number.isFinite(port) || port < 1)) {
|
|
483
|
+
throw new Error("--port must be a positive integer");
|
|
484
|
+
}
|
|
485
|
+
const managerPort = Number.parseInt(argument("manager-port") ?? "0", 10);
|
|
486
|
+
const harness = (argument("harness") ?? "opencode");
|
|
487
|
+
if (harness !== "mock" && harness !== "opencode") {
|
|
488
|
+
throw new Error("--harness must be mock or opencode");
|
|
489
|
+
}
|
|
490
|
+
const prepared = await prepareAgent(argument("agent-dir") ?? process.cwd());
|
|
491
|
+
try {
|
|
492
|
+
process.loadEnvFile(resolve(prepared.root, ".env"));
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
if (!(error instanceof Error) ||
|
|
496
|
+
!("code" in error) ||
|
|
497
|
+
error.code !== "ENOENT") {
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
let useBlueOpenRouterGateway = false;
|
|
502
|
+
if (harness === "opencode" && !process.env.OPENROUTER_API_KEY) {
|
|
503
|
+
const credential = await readOpenRouterCredential(prepared.root);
|
|
504
|
+
if (credential) {
|
|
505
|
+
process.env.OPENROUTER_API_KEY = credential;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
await accessTokenFor(hostedEdge());
|
|
509
|
+
useBlueOpenRouterGateway = true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
process.env.BLUE_MODEL =
|
|
513
|
+
argument("model") ??
|
|
514
|
+
process.env.BLUE_MODEL ??
|
|
515
|
+
"openrouter/anthropic/claude-sonnet-4.6";
|
|
516
|
+
const local = await startLocalControlPlane({
|
|
517
|
+
port,
|
|
518
|
+
statePath: resolve(prepared.root, ".blue", "dev", "control-plane.json"),
|
|
519
|
+
openRouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
520
|
+
model: process.env.BLUE_MODEL,
|
|
521
|
+
});
|
|
522
|
+
const edge = local.url;
|
|
523
|
+
let manager;
|
|
524
|
+
try {
|
|
525
|
+
manager = await startDevManager({
|
|
526
|
+
edge,
|
|
527
|
+
blueApiUrl: hostedEdge(),
|
|
528
|
+
port: managerPort,
|
|
529
|
+
agentRoot: prepared.root,
|
|
530
|
+
agentId: prepared.name,
|
|
531
|
+
harness,
|
|
532
|
+
spawnRuntime: (sessionId, runtimeToken, directory) => spawnRuntime(edge, sessionId, runtimeToken, harness, directory),
|
|
533
|
+
});
|
|
534
|
+
if (useBlueOpenRouterGateway) {
|
|
535
|
+
process.env.OPENROUTER_API_KEY = manager.state.token;
|
|
536
|
+
process.env.BLUE_OPENROUTER_BASE_URL =
|
|
537
|
+
`${manager.state.manager}/openrouter/api/v1`;
|
|
538
|
+
}
|
|
539
|
+
process.stdout.write(`blue dev: ${prepared.name} ready at ${edge}\n` +
|
|
540
|
+
`manager: ${manager.state.manager}\n` +
|
|
541
|
+
`session: blue session\n` +
|
|
542
|
+
`prompt: blue session "your first message"\n` +
|
|
543
|
+
(manager.state.channels.slack
|
|
544
|
+
? manager.state.channels.slack.mode === "socket"
|
|
545
|
+
? "slack: Socket Mode connected\n"
|
|
546
|
+
: `slack: ${manager.state.channels.slack.webhook}\n`
|
|
547
|
+
: ""));
|
|
548
|
+
await new Promise((resolvePromise) => {
|
|
549
|
+
process.once("SIGINT", resolvePromise);
|
|
550
|
+
process.once("SIGTERM", resolvePromise);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
await manager?.close();
|
|
555
|
+
await local.close();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function demo() {
|
|
559
|
+
const requestedPort = argument("port");
|
|
560
|
+
const local = await startLocalControlPlane({
|
|
561
|
+
port: requestedPort
|
|
562
|
+
? Number.parseInt(requestedPort, 10)
|
|
563
|
+
: undefined,
|
|
564
|
+
statePath: resolve(".blue", "dev", "demo-control-plane.json"),
|
|
565
|
+
openRouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
566
|
+
model: argument("model") ??
|
|
567
|
+
process.env.BLUE_MODEL ??
|
|
568
|
+
"openrouter/anthropic/claude-sonnet-4.6",
|
|
569
|
+
});
|
|
570
|
+
try {
|
|
571
|
+
const prompt = positional(3).join(" ") || "Hello from Blue";
|
|
572
|
+
await runTurn(local.url, prompt);
|
|
573
|
+
}
|
|
574
|
+
finally {
|
|
575
|
+
await local.close();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function usage() {
|
|
579
|
+
process.stdout.write(`blue - OpenCode agent sessions
|
|
580
|
+
|
|
581
|
+
Usage:
|
|
582
|
+
blue login [--api-url URL]
|
|
583
|
+
blue logout
|
|
584
|
+
blue whoami
|
|
585
|
+
blue init <directory>
|
|
586
|
+
blue dev [--agent-dir path] [--model provider/model] [--port port]
|
|
587
|
+
blue demo [prompt] [--agent-dir path] [--harness mock|opencode]
|
|
588
|
+
blue run <prompt> [--agent <agent>@<alias>]
|
|
589
|
+
blue auth openrouter
|
|
590
|
+
blue channels add slack [--agent-dir path]
|
|
591
|
+
blue deploy [--agent-dir path] [--alias production] [--edge URL]
|
|
592
|
+
blue remote <prompt> [--edge URL] [--api-key key] [--keep]
|
|
593
|
+
blue agents list
|
|
594
|
+
blue session [prompt] [--local|--remote]
|
|
595
|
+
blue session create --local [prompt]
|
|
596
|
+
blue session create [--agent <agent>@<alias>] [prompt] [--keep]
|
|
597
|
+
blue session list [--local]
|
|
598
|
+
blue session inspect <session-id> [--local]
|
|
599
|
+
blue session attach <session-id> [--local]
|
|
600
|
+
blue session send <session-id> <prompt> [--local] [--keep]
|
|
601
|
+
blue session end <session-id> [--local]
|
|
602
|
+
`);
|
|
603
|
+
}
|
|
604
|
+
async function main() {
|
|
605
|
+
const command = process.argv[2];
|
|
606
|
+
if (command === "login") {
|
|
607
|
+
const credentials = await login(hostedEdge());
|
|
608
|
+
process.stdout.write(`Logged in${credentials.user?.email ? ` as ${credentials.user.email}` : ""}.\n`);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (command === "logout") {
|
|
612
|
+
process.stdout.write((await logout()) ? "Logged out.\n" : "Not logged in.\n");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (command === "whoami") {
|
|
616
|
+
const edge = hostedEdge();
|
|
617
|
+
const identity = await (await clientFor(edge)).whoami();
|
|
618
|
+
const credentials = await readCredentials();
|
|
619
|
+
process.stdout.write(`${credentials?.user?.email ?? identity.userId}\n` +
|
|
620
|
+
`account: ${identity.accountId}\n` +
|
|
621
|
+
(identity.organizationId
|
|
622
|
+
? `organization: ${identity.organizationId}\n`
|
|
623
|
+
: ""));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (command === "init") {
|
|
627
|
+
const directory = positional(3)[0];
|
|
628
|
+
if (!directory)
|
|
629
|
+
throw new Error("A target directory is required");
|
|
630
|
+
process.stdout.write(`created: ${await initializeAgent(directory)}\n`);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (command === "dev") {
|
|
634
|
+
await dev();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (command === "demo") {
|
|
638
|
+
await demo();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (command === "run") {
|
|
642
|
+
const prompt = positional(3).join(" ");
|
|
643
|
+
if (!prompt)
|
|
644
|
+
throw new Error("A prompt is required");
|
|
645
|
+
await runRemoteTurn(hostedEdge(), prompt);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (command === "auth") {
|
|
649
|
+
if (positional(3)[0] !== "openrouter") {
|
|
650
|
+
throw new Error("Only `blue auth openrouter` is supported");
|
|
651
|
+
}
|
|
652
|
+
if (!(await readOpenRouterCredential())) {
|
|
653
|
+
await authenticateOpenRouter();
|
|
654
|
+
}
|
|
655
|
+
process.stdout.write("OpenRouter connected locally.\n");
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (command === "channels") {
|
|
659
|
+
const [action, channel] = positional(3);
|
|
660
|
+
if (action !== "add" || channel !== "slack") {
|
|
661
|
+
throw new Error("Use `blue channels add slack`");
|
|
662
|
+
}
|
|
663
|
+
process.stdout.write(`created: ${await addSlackChannel(argument("agent-dir") ?? process.cwd())}\n`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (command === "deploy") {
|
|
667
|
+
await deployAgent(hostedEdge());
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (command === "remote") {
|
|
671
|
+
const prompt = positional(3).join(" ");
|
|
672
|
+
if (!prompt)
|
|
673
|
+
throw new Error("A prompt is required");
|
|
674
|
+
await runRemoteTurn(hostedEdge(), prompt);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (command === "session") {
|
|
678
|
+
const sessionArguments = positional(3);
|
|
679
|
+
const knownActions = new Set([
|
|
680
|
+
"create",
|
|
681
|
+
"list",
|
|
682
|
+
"inspect",
|
|
683
|
+
"attach",
|
|
684
|
+
"send",
|
|
685
|
+
"end",
|
|
686
|
+
]);
|
|
687
|
+
const shorthand = sessionArguments[0];
|
|
688
|
+
const action = shorthand && knownActions.has(shorthand) ? shorthand : "create";
|
|
689
|
+
const target = await sessionTarget();
|
|
690
|
+
if (action === "create") {
|
|
691
|
+
const prompt = (shorthand === "create"
|
|
692
|
+
? sessionArguments.slice(1)
|
|
693
|
+
: sessionArguments).join(" ") || undefined;
|
|
694
|
+
if (hasFlag("local") || (target.dev && !argument("agent"))) {
|
|
695
|
+
if (!target.dev)
|
|
696
|
+
throw new Error("Run `blue dev` first");
|
|
697
|
+
await createLocalSession(target.dev, prompt);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
const agent = argument("agent") ?? (await inferDeployedAgent());
|
|
701
|
+
if (!agent) {
|
|
702
|
+
throw new Error("No agent repository was found. Pass --agent <agent>@<alias>.");
|
|
703
|
+
}
|
|
704
|
+
await createRemoteSession(target.edge, agent, prompt);
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (action === "list") {
|
|
709
|
+
const sessions = await (await clientFor(target.edge)).listSessions();
|
|
710
|
+
if (sessions.length === 0) {
|
|
711
|
+
process.stdout.write("No sessions.\n");
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
sessions.forEach(printSession);
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const sessionId = positional(4)[0];
|
|
719
|
+
if (!sessionId)
|
|
720
|
+
throw new Error("A session ID is required");
|
|
721
|
+
if (action === "send") {
|
|
722
|
+
const prompt = positional(5).join(" ");
|
|
723
|
+
if (!prompt)
|
|
724
|
+
throw new Error("A prompt is required");
|
|
725
|
+
if (target.dev) {
|
|
726
|
+
await sendLocalTurn(target.dev, sessionId, prompt);
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
await sendRemoteTurn(target.edge, sessionId, prompt);
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (action === "end") {
|
|
734
|
+
if (target.dev) {
|
|
735
|
+
await requestDevManager(target.dev, "DELETE", sessionId);
|
|
736
|
+
await new BlueClient(target.edge).endSession(sessionId);
|
|
737
|
+
process.stdout.write(`session: ${sessionId} ended\n`);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
await endRemoteSession(target.edge, sessionId);
|
|
741
|
+
}
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (action === "inspect") {
|
|
745
|
+
const client = await clientFor(target.edge);
|
|
746
|
+
process.stdout.write(`${JSON.stringify(await client.getSession(sessionId), null, 2)}\n`);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (action === "attach") {
|
|
750
|
+
await attachSession(target.edge, sessionId);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
throw new Error("Use `blue session create`, `list`, `inspect`, `attach`, `send`, or `end`");
|
|
754
|
+
}
|
|
755
|
+
if (command === "agents") {
|
|
756
|
+
if (positional(3)[0] !== "list") {
|
|
757
|
+
throw new Error("Use `blue agents list`");
|
|
758
|
+
}
|
|
759
|
+
const agents = await (await clientFor(hostedEdge())).listAgents();
|
|
760
|
+
if (agents.length === 0) {
|
|
761
|
+
process.stdout.write("No deployed agents.\n");
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
for (const agent of agents) {
|
|
765
|
+
const separator = agent.activeDeploymentId.lastIndexOf(":");
|
|
766
|
+
const deployment = separator >= 0
|
|
767
|
+
? agent.activeDeploymentId.slice(separator + 1)
|
|
768
|
+
: agent.activeDeploymentId;
|
|
769
|
+
process.stdout.write(`${agent.id}@${agent.activeAlias} ` +
|
|
770
|
+
`${deployment.slice(0, 12)} ` +
|
|
771
|
+
`${String(agent.deploymentCount)} deployment(s)\n`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (command === "inspect") {
|
|
777
|
+
const sessionId = positional(3)[0];
|
|
778
|
+
if (!sessionId)
|
|
779
|
+
throw new Error("A session ID is required");
|
|
780
|
+
const client = await clientFor(hostedEdge());
|
|
781
|
+
process.stdout.write(`${JSON.stringify(await client.getSession(sessionId), null, 2)}\n`);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
usage();
|
|
785
|
+
}
|
|
786
|
+
main().catch((error) => {
|
|
787
|
+
process.stderr.write(`blue: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
788
|
+
process.exitCode = 1;
|
|
789
|
+
});
|
|
790
|
+
//# sourceMappingURL=index.js.map
|