@openma/cli 0.1.0-beta.4 → 0.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +254 -50
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -113,9 +113,22 @@ function loadConfigOptional() {
|
|
|
113
113
|
}
|
|
114
114
|
return { baseUrl: envBase || "https://openma.dev", apiKey: "", json: false, source: "missing" };
|
|
115
115
|
}
|
|
116
|
+
async function fetchWithRetry(url, init, maxRetries = 3) {
|
|
117
|
+
let attempt = 0;
|
|
118
|
+
while (true) {
|
|
119
|
+
const res = await fetch(url, init);
|
|
120
|
+
if (res.ok || res.status !== 503) return res;
|
|
121
|
+
const retryAfter = res.headers.get("retry-after");
|
|
122
|
+
if (!retryAfter || attempt >= maxRetries) return res;
|
|
123
|
+
const seconds = Math.min(Math.max(parseInt(retryAfter, 10) || 0, 1), 30);
|
|
124
|
+
await res.body?.cancel().catch(() => void 0);
|
|
125
|
+
await new Promise((r) => setTimeout(r, seconds * 1e3));
|
|
126
|
+
attempt += 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
116
129
|
async function apiFetch(config, path, init) {
|
|
117
130
|
const url = `${config.baseUrl}${path}`;
|
|
118
|
-
const res = await
|
|
131
|
+
const res = await fetchWithRetry(url, {
|
|
119
132
|
...init,
|
|
120
133
|
headers: {
|
|
121
134
|
"x-api-key": config.apiKey,
|
|
@@ -134,6 +147,120 @@ async function apiFetch(config, path, init) {
|
|
|
134
147
|
}
|
|
135
148
|
return res.json();
|
|
136
149
|
}
|
|
150
|
+
async function rawStream(config, path, init) {
|
|
151
|
+
const res = await fetchWithRetry(`${config.baseUrl}${path}`, {
|
|
152
|
+
...init,
|
|
153
|
+
headers: {
|
|
154
|
+
"x-api-key": config.apiKey,
|
|
155
|
+
"user-agent": "Mozilla/5.0 (compatible; OpenManagedAgents-CLI/0.1; +https://openma.dev)",
|
|
156
|
+
...init?.headers
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const body = await res.text();
|
|
161
|
+
throw new Error(`${res.status} ${res.statusText}: ${body}`);
|
|
162
|
+
}
|
|
163
|
+
return res;
|
|
164
|
+
}
|
|
165
|
+
async function* parseSSE(res) {
|
|
166
|
+
const reader = res.body.getReader();
|
|
167
|
+
const dec = new TextDecoder();
|
|
168
|
+
let buf = "";
|
|
169
|
+
for (; ; ) {
|
|
170
|
+
const { done, value } = await reader.read();
|
|
171
|
+
if (done) return;
|
|
172
|
+
buf += dec.decode(value, { stream: true });
|
|
173
|
+
let i;
|
|
174
|
+
while ((i = buf.indexOf("\n\n")) !== -1) {
|
|
175
|
+
const chunk = buf.slice(0, i);
|
|
176
|
+
buf = buf.slice(i + 2);
|
|
177
|
+
const line = chunk.split("\n").find((l) => l.startsWith("data: "));
|
|
178
|
+
if (!line) continue;
|
|
179
|
+
try {
|
|
180
|
+
yield JSON.parse(line.slice(6));
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
var tty = !!process.stdout.isTTY;
|
|
187
|
+
var dim = (s) => tty ? `\x1B[2m${s}\x1B[0m` : s;
|
|
188
|
+
var cyan = (s) => tty ? `\x1B[36m${s}\x1B[0m` : s;
|
|
189
|
+
var yellow = (s) => tty ? `\x1B[33m${s}\x1B[0m` : s;
|
|
190
|
+
async function streamChat(config, sessionId, text) {
|
|
191
|
+
const res = await rawStream(config, `/v1/sessions/${sessionId}/messages`, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: { "content-type": "application/json", accept: "text/event-stream" },
|
|
194
|
+
body: JSON.stringify({ content: text })
|
|
195
|
+
});
|
|
196
|
+
let textOpen = false;
|
|
197
|
+
let thinkOpen = false;
|
|
198
|
+
for await (const ev of parseSSE(res)) {
|
|
199
|
+
const t = ev.type;
|
|
200
|
+
switch (t) {
|
|
201
|
+
case "agent.message_chunk":
|
|
202
|
+
process.stdout.write(String(ev.delta ?? ""));
|
|
203
|
+
textOpen = true;
|
|
204
|
+
break;
|
|
205
|
+
case "agent.message_stream_end":
|
|
206
|
+
case "agent.message":
|
|
207
|
+
if (textOpen) {
|
|
208
|
+
process.stdout.write("\n");
|
|
209
|
+
textOpen = false;
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
case "agent.thinking_chunk":
|
|
213
|
+
if (!thinkOpen) {
|
|
214
|
+
process.stderr.write(dim("\u{1F4AD} "));
|
|
215
|
+
thinkOpen = true;
|
|
216
|
+
}
|
|
217
|
+
process.stderr.write(dim(String(ev.delta ?? "")));
|
|
218
|
+
break;
|
|
219
|
+
case "agent.thinking_stream_end":
|
|
220
|
+
case "agent.thinking":
|
|
221
|
+
if (thinkOpen) {
|
|
222
|
+
process.stderr.write("\n");
|
|
223
|
+
thinkOpen = false;
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
case "agent.tool_use_input_stream_start":
|
|
227
|
+
process.stdout.write(cyan(`\u2192 tool: ${ev.tool_name ?? "?"}`) + dim(" preparing\u2026\n"));
|
|
228
|
+
break;
|
|
229
|
+
case "agent.tool_use":
|
|
230
|
+
case "agent.mcp_tool_use":
|
|
231
|
+
case "agent.custom_tool_use":
|
|
232
|
+
process.stdout.write(cyan(`\u2192 tool: ${ev.name}`) + " " + dim(JSON.stringify(ev.input ?? {})) + "\n");
|
|
233
|
+
break;
|
|
234
|
+
case "agent.tool_result":
|
|
235
|
+
case "agent.mcp_tool_result": {
|
|
236
|
+
const out = typeof ev.content === "string" ? ev.content : JSON.stringify(ev.content ?? "");
|
|
237
|
+
const trimmed = out.length > 280 ? out.slice(0, 280) + "\u2026" : out;
|
|
238
|
+
process.stdout.write(dim("\u2190 ") + trimmed + "\n");
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "session.warning":
|
|
242
|
+
process.stderr.write(yellow(`\u26A0 ${ev.source}: ${ev.message}`) + "\n");
|
|
243
|
+
break;
|
|
244
|
+
case "session.error":
|
|
245
|
+
process.stderr.write(yellow(`\u2717 error: ${ev.error}`) + "\n");
|
|
246
|
+
break;
|
|
247
|
+
case "session.status_idle":
|
|
248
|
+
if (textOpen) {
|
|
249
|
+
process.stdout.write("\n");
|
|
250
|
+
textOpen = false;
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function tailSession(config, sessionId) {
|
|
257
|
+
const res = await rawStream(config, `/v1/sessions/${sessionId}/events/stream`, {
|
|
258
|
+
headers: { accept: "text/event-stream" }
|
|
259
|
+
});
|
|
260
|
+
for await (const ev of parseSSE(res)) {
|
|
261
|
+
process.stdout.write(JSON.stringify(ev) + "\n");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
137
264
|
function flag(args, name) {
|
|
138
265
|
const idx = args.indexOf(name);
|
|
139
266
|
return idx !== -1 ? args[idx + 1] : void 0;
|
|
@@ -185,39 +312,66 @@ async function authLogin(baseUrl, requestedTenant) {
|
|
|
185
312
|
return;
|
|
186
313
|
}
|
|
187
314
|
const got = u.searchParams;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
res.writeHead(400, { "Content-Type": "text/html" }).end(approvalPage("Cancelled", `Login was cancelled: ${err}. You can close this tab.`));
|
|
315
|
+
const finish = (statusCode, page, action) => {
|
|
316
|
+
res.writeHead(statusCode, { "Content-Type": "text/html" }).end(page);
|
|
191
317
|
clearTimeout(timeout);
|
|
192
318
|
server.close();
|
|
193
|
-
|
|
194
|
-
|
|
319
|
+
if (typeof server.closeAllConnections === "function") {
|
|
320
|
+
server.closeAllConnections();
|
|
321
|
+
}
|
|
322
|
+
action();
|
|
323
|
+
};
|
|
324
|
+
if (got.get("error")) {
|
|
325
|
+
const err = String(got.get("error"));
|
|
326
|
+
return finish(
|
|
327
|
+
400,
|
|
328
|
+
approvalPage("Cancelled", `Login was cancelled: ${err}.`),
|
|
329
|
+
() => reject(new Error(`Login cancelled: ${err}`))
|
|
330
|
+
);
|
|
195
331
|
}
|
|
196
332
|
if (got.get("state") !== state) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
333
|
+
return finish(
|
|
334
|
+
400,
|
|
335
|
+
approvalPage("State mismatch", "The login response didn't match what this CLI session expected. Please try again."),
|
|
336
|
+
() => reject(new Error("State mismatch \u2014 refusing the callback"))
|
|
337
|
+
);
|
|
202
338
|
}
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
339
|
+
const tokensB64 = got.get("tokens");
|
|
340
|
+
const userId = got.get("user") ?? "";
|
|
341
|
+
let tokens = [];
|
|
342
|
+
if (tokensB64) {
|
|
343
|
+
try {
|
|
344
|
+
const json = Buffer.from(tokensB64, "base64").toString("utf8");
|
|
345
|
+
tokens = JSON.parse(json);
|
|
346
|
+
} catch {
|
|
347
|
+
return finish(
|
|
348
|
+
400,
|
|
349
|
+
approvalPage("Bad payload", "The browser handoff payload was unreadable."),
|
|
350
|
+
() => reject(new Error("Failed to decode tokens from callback"))
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
const token = got.get("token");
|
|
355
|
+
const tenant = got.get("tenant");
|
|
356
|
+
const key_id = got.get("key_id");
|
|
357
|
+
if (token && tenant && key_id) {
|
|
358
|
+
tokens = [
|
|
359
|
+
{ tenant_id: tenant, tenant_name: "", role: "owner", token, key_id }
|
|
360
|
+
];
|
|
361
|
+
}
|
|
213
362
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
363
|
+
if (tokens.length === 0) {
|
|
364
|
+
return finish(
|
|
365
|
+
400,
|
|
366
|
+
approvalPage("Incomplete callback", "The browser handoff is missing required fields."),
|
|
367
|
+
() => reject(new Error("Callback missing required fields"))
|
|
368
|
+
);
|
|
219
369
|
}
|
|
220
|
-
|
|
370
|
+
return finish(
|
|
371
|
+
200,
|
|
372
|
+
approvalPage("Signed in", "You can close this tab and return to your terminal."),
|
|
373
|
+
() => resolve({ tokens, user: userId })
|
|
374
|
+
);
|
|
221
375
|
} catch (err) {
|
|
222
376
|
res.writeHead(500).end();
|
|
223
377
|
clearTimeout(timeout);
|
|
@@ -232,37 +386,44 @@ async function authLogin(baseUrl, requestedTenant) {
|
|
|
232
386
|
console.log(`(If the browser doesn't open, copy the URL above into one manually.)`);
|
|
233
387
|
openBrowser(loginUrl);
|
|
234
388
|
});
|
|
235
|
-
const
|
|
389
|
+
const firstToken = result.tokens[0];
|
|
390
|
+
const tempConfig = { baseUrl, apiKey: firstToken.token, json: false, source: "stored" };
|
|
236
391
|
const me = await apiFetch(tempConfig, "/v1/me");
|
|
237
392
|
const existing = readCredentials();
|
|
238
|
-
const
|
|
393
|
+
const sameUser = existing && existing.user.id === (me.user?.id ?? result.user);
|
|
394
|
+
const tenantsMap = sameUser ? { ...existing.tenants } : {};
|
|
395
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
396
|
+
for (const t of result.tokens) {
|
|
397
|
+
const membership = me.tenants.find((m) => m.id === t.tenant_id);
|
|
398
|
+
tenantsMap[t.tenant_id] = {
|
|
399
|
+
name: membership?.name ?? t.tenant_name ?? "",
|
|
400
|
+
role: membership?.role ?? t.role ?? "owner",
|
|
401
|
+
token: t.token,
|
|
402
|
+
key_id: t.key_id,
|
|
403
|
+
created_at: now
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const newActive = requestedTenant && tenantsMap[requestedTenant] && requestedTenant || (sameUser && existing.tenants[existing.active_tenant_id] ? existing.active_tenant_id : "") || result.tokens[0].tenant_id;
|
|
239
407
|
const updated = {
|
|
240
408
|
version: 2,
|
|
241
409
|
base_url: baseUrl,
|
|
242
410
|
user: me.user ?? { id: result.user, email: "", name: null },
|
|
243
|
-
active_tenant_id:
|
|
244
|
-
tenants:
|
|
245
|
-
...existing && existing.user.id === (me.user?.id ?? result.user) ? existing.tenants : {},
|
|
246
|
-
[me.tenant.id]: {
|
|
247
|
-
name: me.tenant.name,
|
|
248
|
-
role,
|
|
249
|
-
token: result.token,
|
|
250
|
-
key_id: result.key_id,
|
|
251
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
252
|
-
}
|
|
253
|
-
}
|
|
411
|
+
active_tenant_id: newActive,
|
|
412
|
+
tenants: tenantsMap
|
|
254
413
|
};
|
|
255
414
|
writeCredentials(updated);
|
|
415
|
+
const activeProfile = updated.tenants[updated.active_tenant_id];
|
|
256
416
|
console.log(`\u2713 Signed in as ${me.user?.email ?? me.user?.id}`);
|
|
257
|
-
console.log(`
|
|
258
|
-
console.log(` Stored
|
|
259
|
-
const
|
|
417
|
+
console.log(` Active tenant : ${activeProfile.name || updated.active_tenant_id} (${updated.active_tenant_id})`);
|
|
418
|
+
console.log(` Stored : ${credentialsPath()}`);
|
|
419
|
+
const totalStored = Object.keys(tenantsMap).length;
|
|
420
|
+
if (totalStored > 1) {
|
|
421
|
+
console.log(` Authorized for ${totalStored} workspaces \u2014 switch with: oma auth tenant use <id>`);
|
|
422
|
+
}
|
|
260
423
|
const totalAvailable = me.tenants.length;
|
|
261
|
-
if (totalAvailable >
|
|
262
|
-
const missing = totalAvailable -
|
|
263
|
-
console.log(` ${missing} more
|
|
264
|
-
} else if (knownCount > 1) {
|
|
265
|
-
console.log(` ${knownCount} tenants stored \u2014 switch with: oma auth tenant use <id>`);
|
|
424
|
+
if (totalAvailable > totalStored) {
|
|
425
|
+
const missing = totalAvailable - totalStored;
|
|
426
|
+
console.log(` ${missing} more workspace${missing === 1 ? "" : "s"} unauthorized \u2014 add with: oma auth login --tenant <id>`);
|
|
266
427
|
}
|
|
267
428
|
}
|
|
268
429
|
function displayTenantName(t) {
|
|
@@ -503,7 +664,7 @@ Version: v${a.version}`);
|
|
|
503
664
|
match: ["sessions", "message"],
|
|
504
665
|
needsArg: true,
|
|
505
666
|
usage: "oma sessions message <id> <text>",
|
|
506
|
-
desc: "
|
|
667
|
+
desc: "Fire-and-forget user message (no streaming back)",
|
|
507
668
|
http: 'POST /v1/sessions/:id/events {events:[{type:"user.message",content:[{type:"text",text:"..."}]}]}',
|
|
508
669
|
async run(config, args) {
|
|
509
670
|
const text = args.slice(1).join(" ");
|
|
@@ -511,6 +672,48 @@ Version: v${a.version}`);
|
|
|
511
672
|
console.log("Message sent.");
|
|
512
673
|
}
|
|
513
674
|
},
|
|
675
|
+
{
|
|
676
|
+
group: "Sessions",
|
|
677
|
+
match: ["sessions", "chat"],
|
|
678
|
+
needsArg: true,
|
|
679
|
+
usage: "oma sessions chat <id> <text>",
|
|
680
|
+
desc: "Send a turn AND stream the reply token-by-token",
|
|
681
|
+
http: 'POST /v1/sessions/:id/messages {content:"..."} (text/event-stream)',
|
|
682
|
+
async run(config, args) {
|
|
683
|
+
const text = args.slice(1).join(" ");
|
|
684
|
+
if (!text) {
|
|
685
|
+
console.error("Usage: oma sessions chat <id> <text>");
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
await streamChat(config, args[0], text);
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
group: "Sessions",
|
|
693
|
+
match: ["sessions", "tail"],
|
|
694
|
+
needsArg: true,
|
|
695
|
+
usage: "oma sessions tail <id>",
|
|
696
|
+
desc: "Tail a session's full event stream (never closes)",
|
|
697
|
+
http: "GET /v1/sessions/:id/events/stream (text/event-stream)",
|
|
698
|
+
async run(config, args) {
|
|
699
|
+
await tailSession(config, args[0]);
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
group: "Sessions",
|
|
704
|
+
match: ["sessions", "logs"],
|
|
705
|
+
needsArg: true,
|
|
706
|
+
usage: "oma sessions logs <id>",
|
|
707
|
+
desc: "Dump the full persisted event log as JSON",
|
|
708
|
+
http: "GET /v1/sessions/:id/events?limit=1000",
|
|
709
|
+
async run(config, args) {
|
|
710
|
+
const { data } = await apiFetch(
|
|
711
|
+
config,
|
|
712
|
+
`/v1/sessions/${args[0]}/events?limit=1000&order=asc`
|
|
713
|
+
);
|
|
714
|
+
for (const e of data) console.log(JSON.stringify(e));
|
|
715
|
+
}
|
|
716
|
+
},
|
|
514
717
|
// Environments
|
|
515
718
|
{
|
|
516
719
|
group: "Environments",
|
|
@@ -1576,7 +1779,8 @@ var extraEndpoints = [
|
|
|
1576
1779
|
{ group: "Agents", http: "POST /v1/agents/:id/archive Archive agent" },
|
|
1577
1780
|
{ group: "Sessions", http: "GET /v1/sessions/:id Get session (status, usage)" },
|
|
1578
1781
|
{ group: "Sessions", http: "GET /v1/sessions/:id/events?limit=N Get events (JSON)" },
|
|
1579
|
-
{ group: "Sessions", http: "GET /v1/sessions/:id/stream
|
|
1782
|
+
{ group: "Sessions", http: "GET /v1/sessions/:id/events/stream Tail events (SSE; never closes)" },
|
|
1783
|
+
{ group: "Sessions", http: "POST /v1/sessions/:id/messages Chat one-shot: post turn + stream reply (SSE; closes on idle)" },
|
|
1580
1784
|
{ group: "Sessions", http: "POST /v1/sessions/:id/archive Archive session" },
|
|
1581
1785
|
{ group: "Sessions", http: "DELETE /v1/sessions/:id Delete session" },
|
|
1582
1786
|
{ group: "Sessions", http: "POST /v1/sessions/:id/files Promote sandbox file {path}" },
|