@openma/cli 0.1.0-beta.4 → 0.2.0-beta.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.
Files changed (2) hide show
  1. package/dist/index.js +240 -49
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -134,6 +134,120 @@ async function apiFetch(config, path, init) {
134
134
  }
135
135
  return res.json();
136
136
  }
137
+ async function rawStream(config, path, init) {
138
+ const res = await fetch(`${config.baseUrl}${path}`, {
139
+ ...init,
140
+ headers: {
141
+ "x-api-key": config.apiKey,
142
+ "user-agent": "Mozilla/5.0 (compatible; OpenManagedAgents-CLI/0.1; +https://openma.dev)",
143
+ ...init?.headers
144
+ }
145
+ });
146
+ if (!res.ok) {
147
+ const body = await res.text();
148
+ throw new Error(`${res.status} ${res.statusText}: ${body}`);
149
+ }
150
+ return res;
151
+ }
152
+ async function* parseSSE(res) {
153
+ const reader = res.body.getReader();
154
+ const dec = new TextDecoder();
155
+ let buf = "";
156
+ for (; ; ) {
157
+ const { done, value } = await reader.read();
158
+ if (done) return;
159
+ buf += dec.decode(value, { stream: true });
160
+ let i;
161
+ while ((i = buf.indexOf("\n\n")) !== -1) {
162
+ const chunk = buf.slice(0, i);
163
+ buf = buf.slice(i + 2);
164
+ const line = chunk.split("\n").find((l) => l.startsWith("data: "));
165
+ if (!line) continue;
166
+ try {
167
+ yield JSON.parse(line.slice(6));
168
+ } catch {
169
+ }
170
+ }
171
+ }
172
+ }
173
+ var tty = !!process.stdout.isTTY;
174
+ var dim = (s) => tty ? `\x1B[2m${s}\x1B[0m` : s;
175
+ var cyan = (s) => tty ? `\x1B[36m${s}\x1B[0m` : s;
176
+ var yellow = (s) => tty ? `\x1B[33m${s}\x1B[0m` : s;
177
+ async function streamChat(config, sessionId, text) {
178
+ const res = await rawStream(config, `/v1/sessions/${sessionId}/messages`, {
179
+ method: "POST",
180
+ headers: { "content-type": "application/json", accept: "text/event-stream" },
181
+ body: JSON.stringify({ content: text })
182
+ });
183
+ let textOpen = false;
184
+ let thinkOpen = false;
185
+ for await (const ev of parseSSE(res)) {
186
+ const t = ev.type;
187
+ switch (t) {
188
+ case "agent.message_chunk":
189
+ process.stdout.write(String(ev.delta ?? ""));
190
+ textOpen = true;
191
+ break;
192
+ case "agent.message_stream_end":
193
+ case "agent.message":
194
+ if (textOpen) {
195
+ process.stdout.write("\n");
196
+ textOpen = false;
197
+ }
198
+ break;
199
+ case "agent.thinking_chunk":
200
+ if (!thinkOpen) {
201
+ process.stderr.write(dim("\u{1F4AD} "));
202
+ thinkOpen = true;
203
+ }
204
+ process.stderr.write(dim(String(ev.delta ?? "")));
205
+ break;
206
+ case "agent.thinking_stream_end":
207
+ case "agent.thinking":
208
+ if (thinkOpen) {
209
+ process.stderr.write("\n");
210
+ thinkOpen = false;
211
+ }
212
+ break;
213
+ case "agent.tool_use_input_stream_start":
214
+ process.stdout.write(cyan(`\u2192 tool: ${ev.tool_name ?? "?"}`) + dim(" preparing\u2026\n"));
215
+ break;
216
+ case "agent.tool_use":
217
+ case "agent.mcp_tool_use":
218
+ case "agent.custom_tool_use":
219
+ process.stdout.write(cyan(`\u2192 tool: ${ev.name}`) + " " + dim(JSON.stringify(ev.input ?? {})) + "\n");
220
+ break;
221
+ case "agent.tool_result":
222
+ case "agent.mcp_tool_result": {
223
+ const out = typeof ev.content === "string" ? ev.content : JSON.stringify(ev.content ?? "");
224
+ const trimmed = out.length > 280 ? out.slice(0, 280) + "\u2026" : out;
225
+ process.stdout.write(dim("\u2190 ") + trimmed + "\n");
226
+ break;
227
+ }
228
+ case "session.warning":
229
+ process.stderr.write(yellow(`\u26A0 ${ev.source}: ${ev.message}`) + "\n");
230
+ break;
231
+ case "session.error":
232
+ process.stderr.write(yellow(`\u2717 error: ${ev.error}`) + "\n");
233
+ break;
234
+ case "session.status_idle":
235
+ if (textOpen) {
236
+ process.stdout.write("\n");
237
+ textOpen = false;
238
+ }
239
+ return;
240
+ }
241
+ }
242
+ }
243
+ async function tailSession(config, sessionId) {
244
+ const res = await rawStream(config, `/v1/sessions/${sessionId}/events/stream`, {
245
+ headers: { accept: "text/event-stream" }
246
+ });
247
+ for await (const ev of parseSSE(res)) {
248
+ process.stdout.write(JSON.stringify(ev) + "\n");
249
+ }
250
+ }
137
251
  function flag(args, name) {
138
252
  const idx = args.indexOf(name);
139
253
  return idx !== -1 ? args[idx + 1] : void 0;
@@ -185,39 +299,66 @@ async function authLogin(baseUrl, requestedTenant) {
185
299
  return;
186
300
  }
187
301
  const got = u.searchParams;
188
- if (got.get("error")) {
189
- const err = String(got.get("error"));
190
- res.writeHead(400, { "Content-Type": "text/html" }).end(approvalPage("Cancelled", `Login was cancelled: ${err}. You can close this tab.`));
302
+ const finish = (statusCode, page, action) => {
303
+ res.writeHead(statusCode, { "Content-Type": "text/html" }).end(page);
191
304
  clearTimeout(timeout);
192
305
  server.close();
193
- reject(new Error(`Login cancelled: ${err}`));
194
- return;
306
+ if (typeof server.closeAllConnections === "function") {
307
+ server.closeAllConnections();
308
+ }
309
+ action();
310
+ };
311
+ if (got.get("error")) {
312
+ const err = String(got.get("error"));
313
+ return finish(
314
+ 400,
315
+ approvalPage("Cancelled", `Login was cancelled: ${err}.`),
316
+ () => reject(new Error(`Login cancelled: ${err}`))
317
+ );
195
318
  }
196
319
  if (got.get("state") !== state) {
197
- res.writeHead(400, { "Content-Type": "text/html" }).end(approvalPage("State mismatch", "The login response didn't match what this CLI session expected. Please try again."));
198
- clearTimeout(timeout);
199
- server.close();
200
- reject(new Error("State mismatch \u2014 refusing the callback"));
201
- return;
320
+ return finish(
321
+ 400,
322
+ approvalPage("State mismatch", "The login response didn't match what this CLI session expected. Please try again."),
323
+ () => reject(new Error("State mismatch \u2014 refusing the callback"))
324
+ );
202
325
  }
203
- const token = got.get("token");
204
- const tenant = got.get("tenant");
205
- const user = got.get("user");
206
- const key_id = got.get("key_id");
207
- if (!token || !tenant || !user || !key_id) {
208
- res.writeHead(400, { "Content-Type": "text/html" }).end(approvalPage("Incomplete callback", "The browser handoff is missing required fields."));
209
- clearTimeout(timeout);
210
- server.close();
211
- reject(new Error("Callback missing required fields"));
212
- return;
326
+ const tokensB64 = got.get("tokens");
327
+ const userId = got.get("user") ?? "";
328
+ let tokens = [];
329
+ if (tokensB64) {
330
+ try {
331
+ const json = Buffer.from(tokensB64, "base64").toString("utf8");
332
+ tokens = JSON.parse(json);
333
+ } catch {
334
+ return finish(
335
+ 400,
336
+ approvalPage("Bad payload", "The browser handoff payload was unreadable."),
337
+ () => reject(new Error("Failed to decode tokens from callback"))
338
+ );
339
+ }
340
+ } else {
341
+ const token = got.get("token");
342
+ const tenant = got.get("tenant");
343
+ const key_id = got.get("key_id");
344
+ if (token && tenant && key_id) {
345
+ tokens = [
346
+ { tenant_id: tenant, tenant_name: "", role: "owner", token, key_id }
347
+ ];
348
+ }
213
349
  }
214
- res.writeHead(200, { "Content-Type": "text/html" }).end(approvalPage("Signed in", "You can close this tab and return to your terminal."));
215
- clearTimeout(timeout);
216
- server.close();
217
- if (typeof server.closeAllConnections === "function") {
218
- server.closeAllConnections();
350
+ if (tokens.length === 0) {
351
+ return finish(
352
+ 400,
353
+ approvalPage("Incomplete callback", "The browser handoff is missing required fields."),
354
+ () => reject(new Error("Callback missing required fields"))
355
+ );
219
356
  }
220
- resolve({ token, tenant, user, key_id });
357
+ return finish(
358
+ 200,
359
+ approvalPage("Signed in", "You can close this tab and return to your terminal."),
360
+ () => resolve({ tokens, user: userId })
361
+ );
221
362
  } catch (err) {
222
363
  res.writeHead(500).end();
223
364
  clearTimeout(timeout);
@@ -232,37 +373,44 @@ async function authLogin(baseUrl, requestedTenant) {
232
373
  console.log(`(If the browser doesn't open, copy the URL above into one manually.)`);
233
374
  openBrowser(loginUrl);
234
375
  });
235
- const tempConfig = { baseUrl, apiKey: result.token, json: false, source: "stored" };
376
+ const firstToken = result.tokens[0];
377
+ const tempConfig = { baseUrl, apiKey: firstToken.token, json: false, source: "stored" };
236
378
  const me = await apiFetch(tempConfig, "/v1/me");
237
379
  const existing = readCredentials();
238
- const role = me.tenants.find((t) => t.id === me.tenant.id)?.role ?? "owner";
380
+ const sameUser = existing && existing.user.id === (me.user?.id ?? result.user);
381
+ const tenantsMap = sameUser ? { ...existing.tenants } : {};
382
+ const now = (/* @__PURE__ */ new Date()).toISOString();
383
+ for (const t of result.tokens) {
384
+ const membership = me.tenants.find((m) => m.id === t.tenant_id);
385
+ tenantsMap[t.tenant_id] = {
386
+ name: membership?.name ?? t.tenant_name ?? "",
387
+ role: membership?.role ?? t.role ?? "owner",
388
+ token: t.token,
389
+ key_id: t.key_id,
390
+ created_at: now
391
+ };
392
+ }
393
+ const newActive = requestedTenant && tenantsMap[requestedTenant] && requestedTenant || (sameUser && existing.tenants[existing.active_tenant_id] ? existing.active_tenant_id : "") || result.tokens[0].tenant_id;
239
394
  const updated = {
240
395
  version: 2,
241
396
  base_url: baseUrl,
242
397
  user: me.user ?? { id: result.user, email: "", name: null },
243
- active_tenant_id: me.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
- }
398
+ active_tenant_id: newActive,
399
+ tenants: tenantsMap
254
400
  };
255
401
  writeCredentials(updated);
402
+ const activeProfile = updated.tenants[updated.active_tenant_id];
256
403
  console.log(`\u2713 Signed in as ${me.user?.email ?? me.user?.id}`);
257
- console.log(` Tenant : ${displayTenantName(me.tenant)} (${me.tenant.id})`);
258
- console.log(` Stored : ${credentialsPath()}`);
259
- const knownCount = Object.keys(updated.tenants).length;
404
+ console.log(` Active tenant : ${activeProfile.name || updated.active_tenant_id} (${updated.active_tenant_id})`);
405
+ console.log(` Stored : ${credentialsPath()}`);
406
+ const totalStored = Object.keys(tenantsMap).length;
407
+ if (totalStored > 1) {
408
+ console.log(` Authorized for ${totalStored} workspaces \u2014 switch with: oma auth tenant use <id>`);
409
+ }
260
410
  const totalAvailable = me.tenants.length;
261
- if (totalAvailable > knownCount) {
262
- const missing = totalAvailable - knownCount;
263
- console.log(` ${missing} more tenant${missing === 1 ? "" : "s"} available \u2014 add with: oma auth login --tenant <id>`);
264
- } else if (knownCount > 1) {
265
- console.log(` ${knownCount} tenants stored \u2014 switch with: oma auth tenant use <id>`);
411
+ if (totalAvailable > totalStored) {
412
+ const missing = totalAvailable - totalStored;
413
+ console.log(` ${missing} more workspace${missing === 1 ? "" : "s"} unauthorized \u2014 add with: oma auth login --tenant <id>`);
266
414
  }
267
415
  }
268
416
  function displayTenantName(t) {
@@ -503,7 +651,7 @@ Version: v${a.version}`);
503
651
  match: ["sessions", "message"],
504
652
  needsArg: true,
505
653
  usage: "oma sessions message <id> <text>",
506
- desc: "Send message to session",
654
+ desc: "Fire-and-forget user message (no streaming back)",
507
655
  http: 'POST /v1/sessions/:id/events {events:[{type:"user.message",content:[{type:"text",text:"..."}]}]}',
508
656
  async run(config, args) {
509
657
  const text = args.slice(1).join(" ");
@@ -511,6 +659,48 @@ Version: v${a.version}`);
511
659
  console.log("Message sent.");
512
660
  }
513
661
  },
662
+ {
663
+ group: "Sessions",
664
+ match: ["sessions", "chat"],
665
+ needsArg: true,
666
+ usage: "oma sessions chat <id> <text>",
667
+ desc: "Send a turn AND stream the reply token-by-token",
668
+ http: 'POST /v1/sessions/:id/messages {content:"..."} (text/event-stream)',
669
+ async run(config, args) {
670
+ const text = args.slice(1).join(" ");
671
+ if (!text) {
672
+ console.error("Usage: oma sessions chat <id> <text>");
673
+ process.exit(1);
674
+ }
675
+ await streamChat(config, args[0], text);
676
+ }
677
+ },
678
+ {
679
+ group: "Sessions",
680
+ match: ["sessions", "tail"],
681
+ needsArg: true,
682
+ usage: "oma sessions tail <id>",
683
+ desc: "Tail a session's full event stream (never closes)",
684
+ http: "GET /v1/sessions/:id/events/stream (text/event-stream)",
685
+ async run(config, args) {
686
+ await tailSession(config, args[0]);
687
+ }
688
+ },
689
+ {
690
+ group: "Sessions",
691
+ match: ["sessions", "logs"],
692
+ needsArg: true,
693
+ usage: "oma sessions logs <id>",
694
+ desc: "Dump the full persisted event log as JSON",
695
+ http: "GET /v1/sessions/:id/events?limit=1000",
696
+ async run(config, args) {
697
+ const { data } = await apiFetch(
698
+ config,
699
+ `/v1/sessions/${args[0]}/events?limit=1000&order=asc`
700
+ );
701
+ for (const e of data) console.log(JSON.stringify(e));
702
+ }
703
+ },
514
704
  // Environments
515
705
  {
516
706
  group: "Environments",
@@ -1576,7 +1766,8 @@ var extraEndpoints = [
1576
1766
  { group: "Agents", http: "POST /v1/agents/:id/archive Archive agent" },
1577
1767
  { group: "Sessions", http: "GET /v1/sessions/:id Get session (status, usage)" },
1578
1768
  { group: "Sessions", http: "GET /v1/sessions/:id/events?limit=N Get events (JSON)" },
1579
- { group: "Sessions", http: "GET /v1/sessions/:id/stream Stream events (SSE)" },
1769
+ { group: "Sessions", http: "GET /v1/sessions/:id/events/stream Tail events (SSE; never closes)" },
1770
+ { group: "Sessions", http: "POST /v1/sessions/:id/messages Chat one-shot: post turn + stream reply (SSE; closes on idle)" },
1580
1771
  { group: "Sessions", http: "POST /v1/sessions/:id/archive Archive session" },
1581
1772
  { group: "Sessions", http: "DELETE /v1/sessions/:id Delete session" },
1582
1773
  { group: "Sessions", http: "POST /v1/sessions/:id/files Promote sandbox file {path}" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openma/cli",
3
- "version": "0.1.0-beta.4",
3
+ "version": "0.2.0-beta.0",
4
4
  "description": "CLI for the openma managed agents platform",
5
5
  "type": "module",
6
6
  "bin": {