hyperbrane 0.1.0
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 +41 -0
- package/dist/agent/agentService.d.ts +31 -0
- package/dist/agent/agentService.js +388 -0
- package/dist/agent/agentStorage.d.ts +27 -0
- package/dist/agent/agentStorage.js +52 -0
- package/dist/agent/api.d.ts +264 -0
- package/dist/agent/api.js +14 -0
- package/dist/agent/gatewaySignIns.d.ts +5 -0
- package/dist/agent/gatewaySignIns.js +11 -0
- package/dist/agent/session/client.d.ts +14 -0
- package/dist/agent/session/client.js +47 -0
- package/dist/agent/session/sessionStorage.d.ts +66 -0
- package/dist/agent/session/sessionStorage.js +89 -0
- package/dist/agent/session/sharedConnection.d.ts +13 -0
- package/dist/agent/session/sharedConnection.js +55 -0
- package/dist/agent/session/worker/jsonRpc.d.ts +39 -0
- package/dist/agent/session/worker/jsonRpc.js +102 -0
- package/dist/agent/session/worker/protocol.d.ts +120 -0
- package/dist/agent/session/worker/protocol.js +32 -0
- package/dist/agent/session/workerConnection.d.ts +29 -0
- package/dist/agent/session/workerConnection.js +44 -0
- package/dist/agent/session/workerSseConnection.d.ts +15 -0
- package/dist/agent/session/workerSseConnection.js +26 -0
- package/dist/agent/workerBundle.d.ts +19 -0
- package/dist/agent/workerBundle.generated.d.ts +1 -0
- package/dist/agent/workerBundle.generated.js +2 -0
- package/dist/agent/workerBundle.js +31 -0
- package/dist/cli/agent.d.ts +3 -0
- package/dist/cli/agent.js +423 -0
- package/dist/cli/command.d.ts +46 -0
- package/dist/cli/command.js +98 -0
- package/dist/cli/dimension.d.ts +2 -0
- package/dist/cli/dimension.js +85 -0
- package/dist/cli/gateway.d.ts +6 -0
- package/dist/cli/gateway.js +245 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +125 -0
- package/dist/cli/portal.d.ts +2 -0
- package/dist/cli/portal.js +39 -0
- package/dist/cli/services.d.ts +18 -0
- package/dist/cli/services.js +33 -0
- package/dist/cli/streamPrinter.d.ts +23 -0
- package/dist/cli/streamPrinter.js +251 -0
- package/dist/cli/table.d.ts +7 -0
- package/dist/cli/table.js +54 -0
- package/dist/dimension/api.d.ts +67 -0
- package/dist/dimension/api.js +81 -0
- package/dist/dimension/http.d.ts +7 -0
- package/dist/dimension/http.js +77 -0
- package/dist/dimension/image.d.ts +13 -0
- package/dist/dimension/image.js +43 -0
- package/dist/dimension/imageFiles.generated.d.ts +1 -0
- package/dist/dimension/imageFiles.generated.js +5 -0
- package/dist/dimension/paths.d.ts +5 -0
- package/dist/dimension/paths.js +7 -0
- package/dist/dimension/service.d.ts +22 -0
- package/dist/dimension/service.js +242 -0
- package/dist/dimension/storage.d.ts +39 -0
- package/dist/dimension/storage.js +78 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +25 -0
- package/dist/lib/docker-api.d.ts +13787 -0
- package/dist/lib/docker-api.js +5 -0
- package/dist/lib/docker.d.ts +77 -0
- package/dist/lib/docker.js +214 -0
- package/dist/lib/image.d.ts +14 -0
- package/dist/lib/image.js +29 -0
- package/dist/lib/jsonDirectory.d.ts +28 -0
- package/dist/lib/jsonDirectory.js +102 -0
- package/dist/lib/sleep.d.ts +2 -0
- package/dist/lib/sleep.js +4 -0
- package/dist/lib/tar.d.ts +2 -0
- package/dist/lib/tar.js +65 -0
- package/dist/lib/version.d.ts +1 -0
- package/dist/lib/version.js +1 -0
- package/dist/portal-gateway/api.d.ts +98 -0
- package/dist/portal-gateway/api.js +12 -0
- package/dist/portal-gateway/fakes.d.ts +3 -0
- package/dist/portal-gateway/fakes.js +11 -0
- package/dist/portal-gateway/gatewayService.d.ts +37 -0
- package/dist/portal-gateway/gatewayService.js +380 -0
- package/dist/portal-gateway/gatewayStorage.d.ts +66 -0
- package/dist/portal-gateway/gatewayStorage.js +125 -0
- package/dist/portal-gateway/image.d.ts +6 -0
- package/dist/portal-gateway/image.js +13 -0
- package/dist/portal-gateway/imageFiles.generated.d.ts +1 -0
- package/dist/portal-gateway/imageFiles.generated.js +8 -0
- package/dist/portal-gateway/portalService.d.ts +48 -0
- package/dist/portal-gateway/portalService.js +327 -0
- package/dist/portal-gateway/portalStorage.d.ts +34 -0
- package/dist/portal-gateway/portalStorage.js +44 -0
- package/dist/portal-gateway/upstream.d.ts +157 -0
- package/dist/portal-gateway/upstream.js +4 -0
- package/dist/sandbox/api.d.ts +184 -0
- package/dist/sandbox/api.js +14 -0
- package/dist/sandbox/boatService.d.ts +52 -0
- package/dist/sandbox/boatService.js +335 -0
- package/dist/sandbox/containerService.d.ts +15 -0
- package/dist/sandbox/containerService.js +177 -0
- package/dist/sandbox/endpointProxy.d.ts +90 -0
- package/dist/sandbox/endpointProxy.js +701 -0
- package/dist/sandbox/exeDevAuth.d.ts +16 -0
- package/dist/sandbox/exeDevAuth.js +49 -0
- package/dist/sandbox/localService.d.ts +19 -0
- package/dist/sandbox/localService.js +182 -0
- package/dist/sandbox/sseConnection.d.ts +41 -0
- package/dist/sandbox/sseConnection.js +166 -0
- package/dist/sandbox/sshService.d.ts +30 -0
- package/dist/sandbox/sshService.js +647 -0
- package/dist/upstreams/anthropic.d.ts +7 -0
- package/dist/upstreams/anthropic.js +27 -0
- package/dist/upstreams/index.d.ts +6 -0
- package/dist/upstreams/index.js +7 -0
- package/dist/upstreams/openai.d.ts +7 -0
- package/dist/upstreams/openai.js +40 -0
- package/dist/vendor-types/claude/agent-sdk/agentSdkTypes.d.ts +1 -0
- package/dist/vendor-types/claude/agent-sdk/bridge.d.ts +388 -0
- package/dist/vendor-types/claude/agent-sdk/browser-sdk.d.ts +184 -0
- package/dist/vendor-types/claude/agent-sdk/core.d.ts +20 -0
- package/dist/vendor-types/claude/agent-sdk/extractFromBunfs.d.ts +1 -0
- package/dist/vendor-types/claude/agent-sdk/sdk-tools.d.ts +4153 -0
- package/dist/vendor-types/claude/agent-sdk/sdk.d.ts +9833 -0
- package/package.json +55 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { AGENT_BACKENDS } from "../agent/api.js";
|
|
2
|
+
import { CPULimit, MemoryLimit } from "../dimension/api.js";
|
|
3
|
+
import { QUIET_FLAG, UsageError, forEach, printList, requireFlag } from "./command.js";
|
|
4
|
+
import { createTerminalPrinter, preview } from "./streamPrinter.js";
|
|
5
|
+
import { formatTable } from "./table.js";
|
|
6
|
+
/** How often `session attach` looks for permission requests and questions to point out. */
|
|
7
|
+
const PENDING_POLL_MS = 1_000;
|
|
8
|
+
function backendArg(name) {
|
|
9
|
+
const backend = AGENT_BACKENDS.find((known) => known === name);
|
|
10
|
+
if (!backend)
|
|
11
|
+
throw new UsageError(`unknown backend ${name}: use one of ${AGENT_BACKENDS.join(", ")}`);
|
|
12
|
+
return backend;
|
|
13
|
+
}
|
|
14
|
+
/** "AGENT/SESSION" -> the session's ref. */
|
|
15
|
+
function sessionRef(arg) {
|
|
16
|
+
const slash = arg.indexOf("/");
|
|
17
|
+
if (slash <= 0 || slash === arg.length - 1)
|
|
18
|
+
throw new UsageError(`sessions are named AGENT/SESSION, not ${arg}`);
|
|
19
|
+
return { agentName: arg.slice(0, slash), sessionName: arg.slice(slash + 1) };
|
|
20
|
+
}
|
|
21
|
+
function refName({ agentName, sessionName }) {
|
|
22
|
+
return `${agentName}/${sessionName}`;
|
|
23
|
+
}
|
|
24
|
+
/** --env KEY=VALUE flags -> an environment. */
|
|
25
|
+
function envFlag(flags) {
|
|
26
|
+
const entries = flags.env;
|
|
27
|
+
if (!Array.isArray(entries) || entries.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
const env = {};
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const equals = entry.indexOf("=");
|
|
32
|
+
if (equals <= 0)
|
|
33
|
+
throw new UsageError(`--env takes KEY=VALUE, not ${entry}`);
|
|
34
|
+
env[entry.slice(0, equals)] = entry.slice(equals + 1);
|
|
35
|
+
}
|
|
36
|
+
return env;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Points out, once each, the permission requests and questions the session is waiting on, with the
|
|
40
|
+
* command that answers them. Returns the function that stops looking.
|
|
41
|
+
*/
|
|
42
|
+
function watchPending(agents, ref) {
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const name = refName(ref);
|
|
45
|
+
async function look() {
|
|
46
|
+
const [permissions, questions] = await Promise.all([agents.listPermissionRequests(ref), agents.listQuestionRequests(ref)]);
|
|
47
|
+
for (const request of permissions) {
|
|
48
|
+
if (seen.has(request.id))
|
|
49
|
+
continue;
|
|
50
|
+
seen.add(request.id);
|
|
51
|
+
process.stderr.write(`\n[permission] ${request.tool_name} ${preview(request.input)}\n` +
|
|
52
|
+
` hyperbrane session permission allow ${name} ${request.id} (or deny)\n`);
|
|
53
|
+
}
|
|
54
|
+
for (const request of questions) {
|
|
55
|
+
if (seen.has(request.id))
|
|
56
|
+
continue;
|
|
57
|
+
seen.add(request.id);
|
|
58
|
+
const asked = request.questions.map((question) => ` ${question.question} (${question.options.map((option) => option.label).join(" / ")})`);
|
|
59
|
+
process.stderr.write(`\n[question]\n${asked.join("\n")}\n hyperbrane session question answer ${name} ${request.id} ANSWER...\n`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const timer = setInterval(() => void look().catch(() => { }), PENDING_POLL_MS);
|
|
63
|
+
return () => clearInterval(timer);
|
|
64
|
+
}
|
|
65
|
+
export const agentCommands = {
|
|
66
|
+
summary: "Manage agents: session workers running in dimensions",
|
|
67
|
+
commands: {
|
|
68
|
+
ls: {
|
|
69
|
+
summary: "List agents",
|
|
70
|
+
flags: QUIET_FLAG,
|
|
71
|
+
async run(_args, flags, { agents }) {
|
|
72
|
+
const sessions = await agents.listSessions();
|
|
73
|
+
const rows = (await agents.listAgents()).map((agent) => [
|
|
74
|
+
agent.name,
|
|
75
|
+
agent.dimensionName,
|
|
76
|
+
String(sessions.filter((session) => session.agentName === agent.name).length),
|
|
77
|
+
]);
|
|
78
|
+
printList(flags, ["NAME", "DIMENSION", "SESSIONS"], rows);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
create: {
|
|
82
|
+
summary: "Start an agent in a dimension, or bring its worker up to date",
|
|
83
|
+
args: ["NAME"],
|
|
84
|
+
flags: {
|
|
85
|
+
dimension: { type: "string", short: "d", value: "DIMENSION", description: "Dimension to run in (required)" },
|
|
86
|
+
install: { type: "string", multiple: true, value: "BACKEND", description: `Install a backend too (${AGENT_BACKENDS.join(", ")}); repeatable` },
|
|
87
|
+
},
|
|
88
|
+
async run([name], flags, { agents }) {
|
|
89
|
+
await agents.createAgent({ name: name, dimensionName: requireFlag(flags, "dimension") });
|
|
90
|
+
const backends = Array.isArray(flags.install) ? flags.install.map(backendArg) : [];
|
|
91
|
+
for (const backend of backends)
|
|
92
|
+
await agents.install(name, backend);
|
|
93
|
+
console.log(name);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
install: {
|
|
97
|
+
summary: `Install or update backends in an agent's dimension (${AGENT_BACKENDS.join(", ")})`,
|
|
98
|
+
args: ["AGENT", "BACKEND..."],
|
|
99
|
+
async run([agent, ...backends], _flags, { agents }) {
|
|
100
|
+
backends.forEach(backendArg); // all known, before installing any
|
|
101
|
+
await forEach(backends, (backend) => agents.install(agent, backendArg(backend)));
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
rm: {
|
|
105
|
+
summary: "Stop one or more agents' workers (their sessions stay in the dimension)",
|
|
106
|
+
args: ["NAME..."],
|
|
107
|
+
async run(names, _flags, { agents }) {
|
|
108
|
+
await forEach(names, (name) => agents.stopAgent(name));
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
const modelCommands = {
|
|
114
|
+
summary: "Show or switch a session's model",
|
|
115
|
+
default: {
|
|
116
|
+
summary: "Show the session's model and the ones it can switch to (starts its CLI)",
|
|
117
|
+
args: ["AGENT/SESSION"],
|
|
118
|
+
flags: QUIET_FLAG,
|
|
119
|
+
async run([name], flags, { agents }) {
|
|
120
|
+
const ref = sessionRef(name);
|
|
121
|
+
const [models, sessions] = await Promise.all([agents.listModels(ref), agents.listSessions()]);
|
|
122
|
+
const current = sessions.find((session) => session.agentName === ref.agentName && session.name === ref.sessionName)?.model ?? null;
|
|
123
|
+
if (!flags.quiet)
|
|
124
|
+
console.log(`current: ${current ?? "(default)"}\n`);
|
|
125
|
+
const rows = models.map((model) => [model.value === current ? `${model.value} *` : model.value, model.displayName, model.description]);
|
|
126
|
+
printList(flags, ["MODEL", "NAME", "DESCRIPTION"], rows);
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
commands: {
|
|
130
|
+
set: {
|
|
131
|
+
summary: `Switch a session's model from its next turn on ("default" for the backend's own)`,
|
|
132
|
+
args: ["AGENT/SESSION", "MODEL"],
|
|
133
|
+
async run([name, model], _flags, { agents }) {
|
|
134
|
+
await agents.setModel(sessionRef(name), model === "default" ? null : model);
|
|
135
|
+
console.log(model);
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
/** A sign-in status as lines for the terminal: what to do next when it's waiting for the user. */
|
|
141
|
+
function describeAuth(name, status) {
|
|
142
|
+
if (status.state === "signedIn")
|
|
143
|
+
return "signed in";
|
|
144
|
+
if (status.state === "signedOut")
|
|
145
|
+
return status.error ? `signed out (last sign-in failed: ${status.error})` : "signed out";
|
|
146
|
+
const lines = ["waiting for you to sign in:", ` open ${status.url}`];
|
|
147
|
+
if (status.userCode)
|
|
148
|
+
lines.push(` and enter the code ${status.userCode}`);
|
|
149
|
+
if (status.needsCode)
|
|
150
|
+
lines.push(` then: hyperbrane session auth code ${name} CODE (the code the page shows)`);
|
|
151
|
+
return lines.join("\n");
|
|
152
|
+
}
|
|
153
|
+
const authCommands = {
|
|
154
|
+
summary: "Show or change how a session's CLI is signed in",
|
|
155
|
+
default: {
|
|
156
|
+
summary: "Show whether the session's CLI is signed in",
|
|
157
|
+
args: ["AGENT/SESSION"],
|
|
158
|
+
async run([name], _flags, { agents }) {
|
|
159
|
+
console.log(describeAuth(name, await agents.getAuthStatus(sessionRef(name))));
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
commands: {
|
|
163
|
+
login: {
|
|
164
|
+
summary: "Sign the CLI in with its own login; instant when the dimension's gateway already holds a sign-in",
|
|
165
|
+
args: ["AGENT/SESSION"],
|
|
166
|
+
async run([name], _flags, { agents }) {
|
|
167
|
+
console.log(describeAuth(name, await agents.login(sessionRef(name))));
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
code: {
|
|
171
|
+
summary: "Finish a sign-in with the code the sign-in page showed (claude)",
|
|
172
|
+
args: ["AGENT/SESSION", "CODE"],
|
|
173
|
+
async run([name, code], _flags, { agents }) {
|
|
174
|
+
console.log(describeAuth(name, await agents.submitAuthCode(sessionRef(name), code)));
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
const taskCommands = {
|
|
180
|
+
summary: "List or stop the tasks a session's agent keeps running in the background",
|
|
181
|
+
default: {
|
|
182
|
+
summary: "List the tasks a session's agent keeps running in the background",
|
|
183
|
+
args: ["AGENT/SESSION"],
|
|
184
|
+
flags: QUIET_FLAG,
|
|
185
|
+
async run([name], flags, { agents }) {
|
|
186
|
+
const rows = (await agents.listBackgroundTasks(sessionRef(name))).map((task) => [task.task_id, task.task_type, task.description]);
|
|
187
|
+
printList(flags, ["ID", "TYPE", "DESCRIPTION"], rows);
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
commands: {
|
|
191
|
+
stop: {
|
|
192
|
+
summary: "Stop one or more background tasks",
|
|
193
|
+
args: ["AGENT/SESSION", "TASK..."],
|
|
194
|
+
async run([name, ...tasks], _flags, { agents }) {
|
|
195
|
+
const ref = sessionRef(name);
|
|
196
|
+
await forEach(tasks, (task) => agents.stopTask(ref, task));
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
const permissionCommands = {
|
|
202
|
+
summary: "List and answer a session's permission requests: tool calls waiting to run",
|
|
203
|
+
commands: {
|
|
204
|
+
ls: {
|
|
205
|
+
summary: "List a session's tool calls waiting for permission",
|
|
206
|
+
args: ["AGENT/SESSION"],
|
|
207
|
+
flags: QUIET_FLAG,
|
|
208
|
+
async run([name], flags, { agents }) {
|
|
209
|
+
const rows = (await agents.listPermissionRequests(sessionRef(name))).map((request) => [
|
|
210
|
+
request.id,
|
|
211
|
+
request.tool_name,
|
|
212
|
+
preview(request.input),
|
|
213
|
+
]);
|
|
214
|
+
printList(flags, ["ID", "TOOL", "INPUT"], rows);
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
allow: {
|
|
218
|
+
summary: "Let one or more waiting tool calls run",
|
|
219
|
+
args: ["AGENT/SESSION", "REQUEST..."],
|
|
220
|
+
async run([name, ...requests], _flags, { agents }) {
|
|
221
|
+
const ref = sessionRef(name);
|
|
222
|
+
await forEach(requests, (request) => agents.respondPermissionRequest(ref, request, { behavior: "allow" }));
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
deny: {
|
|
226
|
+
summary: "Refuse one or more waiting tool calls",
|
|
227
|
+
args: ["AGENT/SESSION", "REQUEST..."],
|
|
228
|
+
flags: {
|
|
229
|
+
message: { type: "string", value: "TEXT", description: "What the agent is told (default: the user denied it)" },
|
|
230
|
+
},
|
|
231
|
+
async run([name, ...requests], flags, { agents }) {
|
|
232
|
+
const ref = sessionRef(name);
|
|
233
|
+
const message = typeof flags.message === "string" ? flags.message : "The user denied this tool call.";
|
|
234
|
+
await forEach(requests, (request) => agents.respondPermissionRequest(ref, request, { behavior: "deny", message }));
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
const questionCommands = {
|
|
240
|
+
summary: "List and answer the questions a session asks",
|
|
241
|
+
commands: {
|
|
242
|
+
ls: {
|
|
243
|
+
summary: "List a session's questions waiting for an answer",
|
|
244
|
+
args: ["AGENT/SESSION"],
|
|
245
|
+
async run([name], _flags, { agents }) {
|
|
246
|
+
const rows = (await agents.listQuestionRequests(sessionRef(name))).flatMap((request) => request.questions.map((question, index) => [
|
|
247
|
+
index === 0 ? request.id : "",
|
|
248
|
+
question.question,
|
|
249
|
+
question.options.map((option) => option.label).join(" / "),
|
|
250
|
+
]));
|
|
251
|
+
console.log(formatTable(["ID", "QUESTION", "OPTIONS"], rows));
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
answer: {
|
|
255
|
+
summary: "Answer a question request: one ANSWER per question, in order (an option's label, or your own words)",
|
|
256
|
+
args: ["AGENT/SESSION", "REQUEST", "ANSWER..."],
|
|
257
|
+
async run([name, requestId, ...answers], _flags, { agents }) {
|
|
258
|
+
const ref = sessionRef(name);
|
|
259
|
+
const request = (await agents.listQuestionRequests(ref)).find((pending) => pending.id === requestId);
|
|
260
|
+
if (!request)
|
|
261
|
+
throw new UsageError(`no pending question request ${requestId} in ${name}`);
|
|
262
|
+
if (answers.length !== request.questions.length) {
|
|
263
|
+
throw new UsageError(`request ${requestId} asks ${request.questions.length} question(s); give one answer for each`);
|
|
264
|
+
}
|
|
265
|
+
const byQuestion = Object.fromEntries(request.questions.map((question, index) => [question.question, answers[index]]));
|
|
266
|
+
await agents.respondQuestionRequest(ref, requestId, { answers: byQuestion });
|
|
267
|
+
console.log(requestId);
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
export const sessionCommands = {
|
|
273
|
+
summary: "Manage agent sessions, named AGENT/SESSION",
|
|
274
|
+
commands: {
|
|
275
|
+
ls: {
|
|
276
|
+
summary: "List sessions",
|
|
277
|
+
flags: QUIET_FLAG,
|
|
278
|
+
async run(_args, flags, { agents }) {
|
|
279
|
+
const rows = (await agents.listSessions())
|
|
280
|
+
.map((session) => [
|
|
281
|
+
`${session.agentName}/${session.name}`,
|
|
282
|
+
session.backend,
|
|
283
|
+
session.status,
|
|
284
|
+
session.model ?? "(default)",
|
|
285
|
+
session.nativeId ?? "",
|
|
286
|
+
])
|
|
287
|
+
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
288
|
+
printList(flags, ["NAME", "BACKEND", "STATUS", "MODEL", "NATIVE ID"], rows);
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
create: {
|
|
292
|
+
summary: "Create a session (its CLI starts with the first message)",
|
|
293
|
+
args: ["AGENT/SESSION"],
|
|
294
|
+
flags: {
|
|
295
|
+
backend: { type: "string", short: "b", value: "BACKEND", description: `Agent CLI to run: ${AGENT_BACKENDS.join(", ")} (required)` },
|
|
296
|
+
memory: { type: "string", short: "m", value: "BYTES", description: "Memory limit, e.g. 1G (required)" },
|
|
297
|
+
cpus: { type: "string", value: "CPUS", description: "CPU limit in cores, e.g. 1 or 500m (required)" },
|
|
298
|
+
cwd: { type: "string", value: "PATH", description: "Directory in the dimension to work in (default: the workspace)" },
|
|
299
|
+
env: { type: "string", short: "e", multiple: true, value: "KEY=VALUE", description: "Environment for the CLI; repeatable" },
|
|
300
|
+
},
|
|
301
|
+
async run([name], flags, { agents }) {
|
|
302
|
+
const ref = sessionRef(name);
|
|
303
|
+
await agents.createSession(ref.agentName, {
|
|
304
|
+
name: ref.sessionName,
|
|
305
|
+
backend: backendArg(requireFlag(flags, "backend")),
|
|
306
|
+
memoryLimit: MemoryLimit(requireFlag(flags, "memory")),
|
|
307
|
+
cpuLimit: CPULimit(requireFlag(flags, "cpus")),
|
|
308
|
+
cwd: typeof flags.cwd === "string" ? flags.cwd : undefined,
|
|
309
|
+
env: envFlag(flags),
|
|
310
|
+
});
|
|
311
|
+
console.log(name);
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
update: {
|
|
315
|
+
summary: "Change a session's limits (recorded; the dimension's limits are what's enforced)",
|
|
316
|
+
args: ["AGENT/SESSION"],
|
|
317
|
+
flags: {
|
|
318
|
+
memory: { type: "string", short: "m", value: "BYTES", description: "Memory limit, e.g. 1G" },
|
|
319
|
+
cpus: { type: "string", value: "CPUS", description: "CPU limit in cores, e.g. 1 or 500m" },
|
|
320
|
+
},
|
|
321
|
+
async run([name], flags, { agents }) {
|
|
322
|
+
if (typeof flags.memory !== "string" && typeof flags.cpus !== "string")
|
|
323
|
+
throw new UsageError("give --memory, --cpus, or both");
|
|
324
|
+
await agents.updateSession(sessionRef(name), {
|
|
325
|
+
memoryLimit: typeof flags.memory === "string" ? MemoryLimit(flags.memory) : undefined,
|
|
326
|
+
cpuLimit: typeof flags.cpus === "string" ? CPULimit(flags.cpus) : undefined,
|
|
327
|
+
});
|
|
328
|
+
console.log(name);
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
rm: {
|
|
332
|
+
summary: "Stop one or more sessions and forget them",
|
|
333
|
+
args: ["AGENT/SESSION..."],
|
|
334
|
+
async run(names, _flags, { agents }) {
|
|
335
|
+
await forEach(names, (name) => agents.stopSession(sessionRef(name)));
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
send: {
|
|
339
|
+
summary: "Send a message; prints its uuid (follow the reply with: session attach AGENT/SESSION --after UUID)",
|
|
340
|
+
args: ["AGENT/SESSION", "MESSAGE..."],
|
|
341
|
+
async run([name, ...words], _flags, { agents }) {
|
|
342
|
+
const uuid = crypto.randomUUID();
|
|
343
|
+
await agents.sendMessage(sessionRef(name), { type: "user", message: { role: "user", content: words.join(" ") }, parent_tool_use_id: null, uuid });
|
|
344
|
+
console.log(uuid);
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
attach: {
|
|
348
|
+
summary: "Print a session's stream as it happens, until Ctrl-C",
|
|
349
|
+
args: ["AGENT/SESSION"],
|
|
350
|
+
flags: {
|
|
351
|
+
after: { type: "string", value: "UUID", description: "Start after this message (default: from the beginning)" },
|
|
352
|
+
},
|
|
353
|
+
async run([name], flags, { agents }) {
|
|
354
|
+
const ref = sessionRef(name);
|
|
355
|
+
const stopWatching = watchPending(agents, ref);
|
|
356
|
+
const printer = createTerminalPrinter({ paced: true });
|
|
357
|
+
// Ctrl-C: out with what's still being typed, and the terminal back to normal text.
|
|
358
|
+
process.once("SIGINT", () => {
|
|
359
|
+
printer.close();
|
|
360
|
+
process.exit(130);
|
|
361
|
+
});
|
|
362
|
+
try {
|
|
363
|
+
for await (const message of agents.getStream(ref, { after_id: typeof flags.after === "string" ? flags.after : undefined })) {
|
|
364
|
+
printer.print(message);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
printer.close();
|
|
369
|
+
stopWatching();
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
logs: {
|
|
374
|
+
summary: "Print the latest of a session's conversation (older pages with --before)",
|
|
375
|
+
args: ["AGENT/SESSION"],
|
|
376
|
+
flags: {
|
|
377
|
+
limit: { type: "string", short: "n", value: "N", description: "How many messages, about (default 50)" },
|
|
378
|
+
before: { type: "string", value: "CURSOR", description: "The page before this cursor (printed after each page that has older ones)" },
|
|
379
|
+
},
|
|
380
|
+
async run([name], flags, { agents }) {
|
|
381
|
+
const limit = typeof flags.limit === "string" ? Number(flags.limit) : undefined;
|
|
382
|
+
if (limit !== undefined && !(Number.isInteger(limit) && limit > 0))
|
|
383
|
+
throw new UsageError(`--limit takes a positive number, not ${flags.limit}`);
|
|
384
|
+
const page = await agents.getTranscript(sessionRef(name), {
|
|
385
|
+
limit,
|
|
386
|
+
cursor: typeof flags.before === "string" ? flags.before : undefined,
|
|
387
|
+
});
|
|
388
|
+
const printer = createTerminalPrinter({ paced: false });
|
|
389
|
+
for (const { type, message } of page.messages)
|
|
390
|
+
printer.printTranscript(type, message.content);
|
|
391
|
+
printer.close();
|
|
392
|
+
if (page.nextCursor)
|
|
393
|
+
process.stderr.write(`(older: hyperbrane session logs ${name} --before ${page.nextCursor})\n`);
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
status: {
|
|
397
|
+
summary: "Show where a session's agent stands, its last message and those it holds queued",
|
|
398
|
+
args: ["AGENT/SESSION"],
|
|
399
|
+
async run([name], _flags, { agents }) {
|
|
400
|
+
const ref = sessionRef(name);
|
|
401
|
+
const [state, messages] = await Promise.all([agents.getSessionStatus(ref), agents.getLastUserMessages(ref)]);
|
|
402
|
+
console.log(state);
|
|
403
|
+
for (const { message, queued } of messages) {
|
|
404
|
+
const content = message.message.content;
|
|
405
|
+
const text = typeof content === "string" ? content : content.map((block) => (block.type === "text" ? block.text : "")).join("");
|
|
406
|
+
console.log(`${queued ? "queued" : "last "} ${preview(text)}`);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
interrupt: {
|
|
411
|
+
summary: "End the turn in progress",
|
|
412
|
+
args: ["AGENT/SESSION..."],
|
|
413
|
+
async run(names, _flags, { agents }) {
|
|
414
|
+
await forEach(names, (name) => agents.cancel(sessionRef(name)));
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
tasks: taskCommands,
|
|
418
|
+
model: modelCommands,
|
|
419
|
+
auth: authCommands,
|
|
420
|
+
permission: permissionCommands,
|
|
421
|
+
question: questionCommands,
|
|
422
|
+
},
|
|
423
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Services } from "./services.ts";
|
|
2
|
+
export type Flag = {
|
|
3
|
+
type: "string" | "boolean";
|
|
4
|
+
short?: string;
|
|
5
|
+
/** Shown in help, e.g. "BYTES". */
|
|
6
|
+
value?: string;
|
|
7
|
+
description: string;
|
|
8
|
+
/** May be given more than once; its value is then a string[]. */
|
|
9
|
+
multiple?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type Flags = Record<string, string | boolean | string[] | undefined>;
|
|
12
|
+
export type Command = {
|
|
13
|
+
summary: string;
|
|
14
|
+
/** Positional arguments: "NAME" is required, "[NAME]" optional, a trailing "NAME..." takes one or more. */
|
|
15
|
+
args?: string[];
|
|
16
|
+
flags?: Record<string, Flag>;
|
|
17
|
+
run(args: string[], flags: Flags, services: Services): Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
export type CommandGroup = {
|
|
20
|
+
summary: string;
|
|
21
|
+
/** A group may hold groups of its own: `hyperbrane session permission allow`. */
|
|
22
|
+
commands: Record<string, Command | CommandGroup>;
|
|
23
|
+
/** Runs when the next word isn't one of `commands`: `hyperbrane session model A/S` next to `... model set`. */
|
|
24
|
+
default?: Command;
|
|
25
|
+
};
|
|
26
|
+
export declare function isGroup(entry: Command | CommandGroup): entry is CommandGroup;
|
|
27
|
+
/** Bad invocation: printed with a pointer to --help. */
|
|
28
|
+
export declare class UsageError extends Error {
|
|
29
|
+
name: string;
|
|
30
|
+
}
|
|
31
|
+
/** Some items of a multi-item command failed; each error was already printed. */
|
|
32
|
+
export declare class PartialFailure extends Error {
|
|
33
|
+
name: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function parseCommand(command: Command, argv: string[]): {
|
|
36
|
+
args: string[];
|
|
37
|
+
flags: Flags;
|
|
38
|
+
};
|
|
39
|
+
export declare function requireFlag(flags: Flags, name: string): string;
|
|
40
|
+
/** docker rm style: every item is attempted, each success prints its name, failures print and fail the command. */
|
|
41
|
+
export declare function forEach(names: string[], action: (name: string) => Promise<void>): Promise<void>;
|
|
42
|
+
/** `ls` output: the table, or with -q only the first column. */
|
|
43
|
+
export declare function printList(flags: Flags, headers: string[], rows: string[][]): void;
|
|
44
|
+
export declare const QUIET_FLAG: Record<string, Flag>;
|
|
45
|
+
export declare function commandUsage(path: string, command: Command): string;
|
|
46
|
+
export declare function groupUsage(name: string, group: CommandGroup): string;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { formatTable } from "./table.js";
|
|
3
|
+
export function isGroup(entry) {
|
|
4
|
+
return "commands" in entry;
|
|
5
|
+
}
|
|
6
|
+
/** Bad invocation: printed with a pointer to --help. */
|
|
7
|
+
export class UsageError extends Error {
|
|
8
|
+
name = "UsageError";
|
|
9
|
+
}
|
|
10
|
+
/** Some items of a multi-item command failed; each error was already printed. */
|
|
11
|
+
export class PartialFailure extends Error {
|
|
12
|
+
name = "PartialFailure";
|
|
13
|
+
}
|
|
14
|
+
export function parseCommand(command, argv) {
|
|
15
|
+
const options = Object.fromEntries(Object.entries(command.flags ?? {}).map(([name, { type, short, multiple }]) => [
|
|
16
|
+
name,
|
|
17
|
+
{ type, ...(short ? { short } : {}), ...(multiple ? { multiple } : {}) },
|
|
18
|
+
]));
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = parseArgs({ args: argv, options, allowPositionals: true, strict: true });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
throw new UsageError(error.message);
|
|
25
|
+
}
|
|
26
|
+
const specs = command.args ?? [];
|
|
27
|
+
const required = specs.filter((spec) => !spec.startsWith("[")).length;
|
|
28
|
+
const variadic = specs.at(-1)?.endsWith("...") ?? false;
|
|
29
|
+
const count = parsed.positionals.length;
|
|
30
|
+
if (count < required)
|
|
31
|
+
throw new UsageError(`missing ${specs.slice(count).filter((s) => !s.startsWith("[")).join(" ")}`);
|
|
32
|
+
if (!variadic && count > specs.length)
|
|
33
|
+
throw new UsageError(`unexpected argument ${parsed.positionals[specs.length]}`);
|
|
34
|
+
return { args: parsed.positionals, flags: parsed.values };
|
|
35
|
+
}
|
|
36
|
+
export function requireFlag(flags, name) {
|
|
37
|
+
const value = flags[name];
|
|
38
|
+
if (typeof value !== "string" || value === "")
|
|
39
|
+
throw new UsageError(`--${name} is required`);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
/** docker rm style: every item is attempted, each success prints its name, failures print and fail the command. */
|
|
43
|
+
export async function forEach(names, action) {
|
|
44
|
+
let failed = false;
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
try {
|
|
47
|
+
await action(name);
|
|
48
|
+
console.log(name);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
failed = true;
|
|
52
|
+
console.error(`Error: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (failed)
|
|
56
|
+
throw new PartialFailure();
|
|
57
|
+
}
|
|
58
|
+
/** `ls` output: the table, or with -q only the first column. */
|
|
59
|
+
export function printList(flags, headers, rows) {
|
|
60
|
+
if (flags.quiet) {
|
|
61
|
+
for (const row of rows)
|
|
62
|
+
console.log(row[0]);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log(formatTable(headers, rows));
|
|
66
|
+
}
|
|
67
|
+
export const QUIET_FLAG = {
|
|
68
|
+
quiet: { type: "boolean", short: "q", description: "Only display names" },
|
|
69
|
+
};
|
|
70
|
+
export function commandUsage(path, command) {
|
|
71
|
+
const lines = [`Usage: hyperbrane ${path}${command.flags ? " [OPTIONS]" : ""}${(command.args ?? []).map((a) => ` ${a}`).join("")}`, "", command.summary];
|
|
72
|
+
if (command.flags) {
|
|
73
|
+
lines.push("", "Options:");
|
|
74
|
+
const entries = Object.entries(command.flags).map(([name, flag]) => [
|
|
75
|
+
`${flag.short ? `-${flag.short}, ` : " "}--${name}${flag.value ? ` ${flag.value}` : ""}`,
|
|
76
|
+
flag.description,
|
|
77
|
+
]);
|
|
78
|
+
const width = Math.max(...entries.map(([left]) => left.length)) + 3;
|
|
79
|
+
for (const [left, description] of entries)
|
|
80
|
+
lines.push(` ${left.padEnd(width)}${description}`);
|
|
81
|
+
}
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
export function groupUsage(name, group) {
|
|
85
|
+
const width = Math.max(...Object.keys(group.commands).map((command) => command.length)) + 3;
|
|
86
|
+
const byDefault = group.default ? [commandUsage(name, group.default).split("\n")[0], ""] : [];
|
|
87
|
+
return [
|
|
88
|
+
...byDefault,
|
|
89
|
+
`Usage: hyperbrane ${name} COMMAND`,
|
|
90
|
+
"",
|
|
91
|
+
group.summary,
|
|
92
|
+
"",
|
|
93
|
+
"Commands:",
|
|
94
|
+
...Object.entries(group.commands).map(([command, { summary }]) => ` ${command.padEnd(width)}${summary}`),
|
|
95
|
+
"",
|
|
96
|
+
`Run 'hyperbrane ${name} COMMAND --help' for more information on a command.`,
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { CPULimit, MemoryLimit } from "../dimension/api.js";
|
|
3
|
+
import { QUIET_FLAG, UsageError, forEach, printList, requireFlag } from "./command.js";
|
|
4
|
+
import { formatBytes, formatCpuPercent, formatDuration } from "./table.js";
|
|
5
|
+
function statusColumn(dimension) {
|
|
6
|
+
if (dimension.status === "running" && dimension.uptimeSeconds !== null) {
|
|
7
|
+
return `Up ${formatDuration(dimension.uptimeSeconds)}`;
|
|
8
|
+
}
|
|
9
|
+
return dimension.status[0].toUpperCase() + dimension.status.slice(1);
|
|
10
|
+
}
|
|
11
|
+
function dimensionRow(dimension) {
|
|
12
|
+
const usage = dimension.memoryUsage === null ? "--" : formatBytes(dimension.memoryUsage);
|
|
13
|
+
return [
|
|
14
|
+
dimension.name,
|
|
15
|
+
statusColumn(dimension),
|
|
16
|
+
dimension.cpuUsage === null ? "--" : formatCpuPercent(dimension.cpuUsage),
|
|
17
|
+
`${usage} / ${formatBytes(dimension.memoryLimit)}`,
|
|
18
|
+
String(dimension.cpuLimit),
|
|
19
|
+
dimension.directory,
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
export const dimensionCommands = {
|
|
23
|
+
summary: "Manage dimensions: containers bound to a host directory",
|
|
24
|
+
commands: {
|
|
25
|
+
ls: {
|
|
26
|
+
summary: "List dimensions",
|
|
27
|
+
flags: QUIET_FLAG,
|
|
28
|
+
async run(_args, flags, { dimensions }) {
|
|
29
|
+
const rows = (await dimensions.getDimensions())
|
|
30
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
31
|
+
.map(dimensionRow);
|
|
32
|
+
printList(flags, ["NAME", "STATUS", "CPU %", "MEM USAGE / LIMIT", "CPUS", "DIRECTORY"], rows);
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
create: {
|
|
36
|
+
summary: "Create a dimension, or finish creating one that was interrupted",
|
|
37
|
+
args: ["NAME"],
|
|
38
|
+
flags: {
|
|
39
|
+
dir: { type: "string", value: "PATH", description: "Host directory mounted at /workspace (default: current directory)" },
|
|
40
|
+
memory: { type: "string", short: "m", value: "BYTES", description: "Memory limit, e.g. 512M or 2G (required)" },
|
|
41
|
+
cpus: { type: "string", value: "CPUS", description: "CPU limit in cores, e.g. 1.5 or 500m (required)" },
|
|
42
|
+
},
|
|
43
|
+
async run([name], flags, { dimensions }) {
|
|
44
|
+
await dimensions.createDimension({
|
|
45
|
+
name: name,
|
|
46
|
+
directory: resolve(typeof flags.dir === "string" ? flags.dir : "."),
|
|
47
|
+
memoryLimit: MemoryLimit(requireFlag(flags, "memory")),
|
|
48
|
+
cpuLimit: CPULimit(requireFlag(flags, "cpus")),
|
|
49
|
+
});
|
|
50
|
+
console.log(name);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
update: {
|
|
54
|
+
summary: "Change a dimension's limits in place, without restarting it",
|
|
55
|
+
args: ["NAME"],
|
|
56
|
+
flags: {
|
|
57
|
+
memory: { type: "string", short: "m", value: "BYTES", description: "Memory limit, e.g. 2G" },
|
|
58
|
+
cpus: { type: "string", value: "CPUS", description: "CPU limit in cores, e.g. 2 or 1500m" },
|
|
59
|
+
},
|
|
60
|
+
async run([name], flags, { dimensions }) {
|
|
61
|
+
if (typeof flags.memory !== "string" && typeof flags.cpus !== "string")
|
|
62
|
+
throw new UsageError("give --memory, --cpus, or both");
|
|
63
|
+
await dimensions.updateDimension(name, {
|
|
64
|
+
memoryLimit: typeof flags.memory === "string" ? MemoryLimit(flags.memory) : undefined,
|
|
65
|
+
cpuLimit: typeof flags.cpus === "string" ? CPULimit(flags.cpus) : undefined,
|
|
66
|
+
});
|
|
67
|
+
console.log(name);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
restart: {
|
|
71
|
+
summary: "Restart one or more dimensions",
|
|
72
|
+
args: ["NAME..."],
|
|
73
|
+
async run(names, _flags, { dimensions }) {
|
|
74
|
+
await forEach(names, (name) => dimensions.restartDimension(name));
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
rm: {
|
|
78
|
+
summary: "Remove one or more dimensions (the host directory is kept)",
|
|
79
|
+
args: ["NAME..."],
|
|
80
|
+
async run(names, _flags, { dimensions }) {
|
|
81
|
+
await forEach(names, (name) => dimensions.destroyDimension(name));
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type CommandGroup } from "./command.ts";
|
|
2
|
+
export declare const gatewayCommands: CommandGroup;
|
|
3
|
+
export declare const upstreamCommands: CommandGroup;
|
|
4
|
+
export declare const keyCommands: CommandGroup;
|
|
5
|
+
export declare const credentialCommands: CommandGroup;
|
|
6
|
+
export declare const ruleCommands: CommandGroup;
|