@openma/cli 0.1.0-beta.3 → 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.
- package/dist/index.js +323 -56
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12,12 +12,39 @@ function credentialsPath() {
|
|
|
12
12
|
const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
|
|
13
13
|
return join(base, "oma", "credentials.json");
|
|
14
14
|
}
|
|
15
|
+
function migrateV1ToV2(v1) {
|
|
16
|
+
return {
|
|
17
|
+
version: 2,
|
|
18
|
+
base_url: v1.base_url,
|
|
19
|
+
user: v1.user,
|
|
20
|
+
active_tenant_id: v1.tenant.id,
|
|
21
|
+
tenants: {
|
|
22
|
+
[v1.tenant.id]: {
|
|
23
|
+
name: v1.tenant.name,
|
|
24
|
+
role: v1.tenants.find((t) => t.id === v1.tenant.id)?.role ?? "owner",
|
|
25
|
+
token: v1.token,
|
|
26
|
+
key_id: v1.key_id,
|
|
27
|
+
created_at: v1.created_at
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
15
32
|
function readCredentials() {
|
|
16
33
|
const path = credentialsPath();
|
|
17
34
|
try {
|
|
18
35
|
if (!existsSync(path)) return null;
|
|
19
36
|
const raw = readFileSync(path, "utf8");
|
|
20
|
-
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
if (parsed.version === 2) return parsed;
|
|
39
|
+
if (parsed.version === 1) {
|
|
40
|
+
const v2 = migrateV1ToV2(parsed);
|
|
41
|
+
try {
|
|
42
|
+
writeCredentials(v2);
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
return v2;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
21
48
|
} catch {
|
|
22
49
|
return null;
|
|
23
50
|
}
|
|
@@ -40,6 +67,7 @@ function clearCredentials() {
|
|
|
40
67
|
function loadConfig() {
|
|
41
68
|
const envBase = process.env.OMA_BASE_URL;
|
|
42
69
|
const envKey = process.env.OMA_API_KEY;
|
|
70
|
+
const envTenant = process.env.OMA_TENANT_ID;
|
|
43
71
|
const stored = readCredentials();
|
|
44
72
|
if (envKey) {
|
|
45
73
|
return {
|
|
@@ -50,9 +78,15 @@ function loadConfig() {
|
|
|
50
78
|
};
|
|
51
79
|
}
|
|
52
80
|
if (stored) {
|
|
81
|
+
const activeId = envTenant || stored.active_tenant_id;
|
|
82
|
+
const profile = stored.tenants[activeId];
|
|
83
|
+
if (!profile) {
|
|
84
|
+
console.error(`Error: tenant ${activeId} not in credentials. Run: oma auth tenant ls`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
53
87
|
return {
|
|
54
88
|
baseUrl: envBase || stored.base_url,
|
|
55
|
-
apiKey:
|
|
89
|
+
apiKey: profile.token,
|
|
56
90
|
json: false,
|
|
57
91
|
source: "stored"
|
|
58
92
|
};
|
|
@@ -65,12 +99,17 @@ function loadConfig() {
|
|
|
65
99
|
function loadConfigOptional() {
|
|
66
100
|
const envBase = process.env.OMA_BASE_URL;
|
|
67
101
|
const envKey = process.env.OMA_API_KEY;
|
|
102
|
+
const envTenant = process.env.OMA_TENANT_ID;
|
|
68
103
|
const stored = readCredentials();
|
|
69
104
|
if (envKey) {
|
|
70
105
|
return { baseUrl: envBase || stored?.base_url || "https://openma.dev", apiKey: envKey, json: false, source: "env" };
|
|
71
106
|
}
|
|
72
107
|
if (stored) {
|
|
73
|
-
|
|
108
|
+
const activeId = envTenant || stored.active_tenant_id;
|
|
109
|
+
const profile = stored.tenants[activeId];
|
|
110
|
+
if (profile) {
|
|
111
|
+
return { baseUrl: envBase || stored.base_url, apiKey: profile.token, json: false, source: "stored" };
|
|
112
|
+
}
|
|
74
113
|
}
|
|
75
114
|
return { baseUrl: envBase || "https://openma.dev", apiKey: "", json: false, source: "missing" };
|
|
76
115
|
}
|
|
@@ -95,6 +134,120 @@ async function apiFetch(config, path, init) {
|
|
|
95
134
|
}
|
|
96
135
|
return res.json();
|
|
97
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
|
+
}
|
|
98
251
|
function flag(args, name) {
|
|
99
252
|
const idx = args.indexOf(name);
|
|
100
253
|
return idx !== -1 ? args[idx + 1] : void 0;
|
|
@@ -146,39 +299,66 @@ async function authLogin(baseUrl, requestedTenant) {
|
|
|
146
299
|
return;
|
|
147
300
|
}
|
|
148
301
|
const got = u.searchParams;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
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);
|
|
152
304
|
clearTimeout(timeout);
|
|
153
305
|
server.close();
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
);
|
|
156
318
|
}
|
|
157
319
|
if (got.get("state") !== state) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
);
|
|
163
325
|
}
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
}
|
|
174
349
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
);
|
|
180
356
|
}
|
|
181
|
-
|
|
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
|
+
);
|
|
182
362
|
} catch (err) {
|
|
183
363
|
res.writeHead(500).end();
|
|
184
364
|
clearTimeout(timeout);
|
|
@@ -193,23 +373,44 @@ async function authLogin(baseUrl, requestedTenant) {
|
|
|
193
373
|
console.log(`(If the browser doesn't open, copy the URL above into one manually.)`);
|
|
194
374
|
openBrowser(loginUrl);
|
|
195
375
|
});
|
|
196
|
-
const
|
|
376
|
+
const firstToken = result.tokens[0];
|
|
377
|
+
const tempConfig = { baseUrl, apiKey: firstToken.token, json: false, source: "stored" };
|
|
197
378
|
const me = await apiFetch(tempConfig, "/v1/me");
|
|
198
|
-
|
|
199
|
-
|
|
379
|
+
const existing = readCredentials();
|
|
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;
|
|
394
|
+
const updated = {
|
|
395
|
+
version: 2,
|
|
200
396
|
base_url: baseUrl,
|
|
201
397
|
user: me.user ?? { id: result.user, email: "", name: null },
|
|
202
|
-
|
|
203
|
-
tenants:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
});
|
|
398
|
+
active_tenant_id: newActive,
|
|
399
|
+
tenants: tenantsMap
|
|
400
|
+
};
|
|
401
|
+
writeCredentials(updated);
|
|
402
|
+
const activeProfile = updated.tenants[updated.active_tenant_id];
|
|
208
403
|
console.log(`\u2713 Signed in as ${me.user?.email ?? me.user?.id}`);
|
|
209
|
-
console.log(`
|
|
210
|
-
console.log(` Stored
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
410
|
+
const totalAvailable = me.tenants.length;
|
|
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>`);
|
|
213
414
|
}
|
|
214
415
|
}
|
|
215
416
|
function displayTenantName(t) {
|
|
@@ -238,12 +439,13 @@ var commands = [
|
|
|
238
439
|
{
|
|
239
440
|
group: "Auth",
|
|
240
441
|
match: ["auth", "login"],
|
|
241
|
-
usage: "oma auth login [--base-url <url>]",
|
|
242
|
-
desc: "Open browser to authenticate;
|
|
442
|
+
usage: "oma auth login [--base-url <url>] [--tenant <id>]",
|
|
443
|
+
desc: "Open browser to authenticate; --tenant pre-picks a workspace",
|
|
243
444
|
http: "POST /v1/me/cli-tokens (browser handoff via /cli/login)",
|
|
244
445
|
async run(_config, args) {
|
|
245
446
|
const baseUrl = (flag(args, "--base-url") ?? process.env.OMA_BASE_URL ?? "https://openma.dev").replace(/\/+$/, "");
|
|
246
|
-
|
|
447
|
+
const tenant = flag(args, "--tenant");
|
|
448
|
+
await authLogin(baseUrl, tenant);
|
|
247
449
|
}
|
|
248
450
|
},
|
|
249
451
|
{
|
|
@@ -261,7 +463,7 @@ var commands = [
|
|
|
261
463
|
group: "Auth",
|
|
262
464
|
match: ["whoami"],
|
|
263
465
|
usage: "oma whoami",
|
|
264
|
-
desc: "Show current user, tenant, and base URL",
|
|
466
|
+
desc: "Show current user, active tenant, and base URL",
|
|
265
467
|
http: "GET /v1/me",
|
|
266
468
|
async run(config) {
|
|
267
469
|
try {
|
|
@@ -273,9 +475,12 @@ var commands = [
|
|
|
273
475
|
console.log(`Base URL : ${config.baseUrl}`);
|
|
274
476
|
console.log(`Source : ${config.source === "env" ? "OMA_API_KEY env var" : "stored credentials"}`);
|
|
275
477
|
console.log(`User : ${me.user?.email ?? me.user?.id ?? "(unknown \u2014 legacy key without user_id)"}`);
|
|
276
|
-
console.log(`Tenant : ${displayTenantName(me.tenant)} (${me.tenant.id})`);
|
|
277
478
|
if (me.tenants.length > 1) {
|
|
278
|
-
console.log(`
|
|
479
|
+
console.log(`Tenant : ${displayTenantName(me.tenant)} (${me.tenant.id})`);
|
|
480
|
+
const others = me.tenants.filter((t) => t.id !== me.tenant.id);
|
|
481
|
+
if (others.length) {
|
|
482
|
+
console.log(`Available: ${others.map((t) => t.id).join(", ")} \u2014 switch with: oma auth tenant use <id>`);
|
|
483
|
+
}
|
|
279
484
|
}
|
|
280
485
|
} catch (err) {
|
|
281
486
|
console.error(`whoami failed: ${err.message}`);
|
|
@@ -299,8 +504,20 @@ var commands = [
|
|
|
299
504
|
return;
|
|
300
505
|
}
|
|
301
506
|
const stored = readCredentials();
|
|
302
|
-
const
|
|
303
|
-
table([
|
|
507
|
+
const active = stored?.active_tenant_id;
|
|
508
|
+
table([
|
|
509
|
+
["", "ID", "NAME", "ROLE", "TOKEN"],
|
|
510
|
+
...data.map((t) => [
|
|
511
|
+
t.id === active ? "*" : " ",
|
|
512
|
+
t.id,
|
|
513
|
+
t.name || "\u2014",
|
|
514
|
+
t.role,
|
|
515
|
+
stored?.tenants[t.id] ? "stored" : "\u2014"
|
|
516
|
+
])
|
|
517
|
+
]);
|
|
518
|
+
console.log(`
|
|
519
|
+
* = active. "stored" = local token cached. To switch active: oma auth tenant use <id>`);
|
|
520
|
+
console.log(`To add a tenant the CLI hasn't seen yet: oma auth login --tenant <id>`);
|
|
304
521
|
}
|
|
305
522
|
},
|
|
306
523
|
{
|
|
@@ -308,9 +525,9 @@ var commands = [
|
|
|
308
525
|
match: ["auth", "tenant", "use"],
|
|
309
526
|
needsArg: true,
|
|
310
527
|
usage: "oma auth tenant use <tenant-id>",
|
|
311
|
-
desc: "Switch active tenant
|
|
312
|
-
http: "POST
|
|
313
|
-
async run(
|
|
528
|
+
desc: "Switch active tenant; if not yet authenticated for it, opens browser to mint",
|
|
529
|
+
http: "(local file update \u2014 or POST /v1/me/cli-tokens via browser if no cached token)",
|
|
530
|
+
async run(_config, args) {
|
|
314
531
|
const tenantId = args[0];
|
|
315
532
|
if (!tenantId) {
|
|
316
533
|
console.error("Usage: oma auth tenant use <tenant-id>");
|
|
@@ -321,7 +538,14 @@ var commands = [
|
|
|
321
538
|
console.error("No stored credentials. Run: oma auth login");
|
|
322
539
|
process.exit(1);
|
|
323
540
|
}
|
|
324
|
-
|
|
541
|
+
if (stored.tenants[tenantId]) {
|
|
542
|
+
const updated = { ...stored, active_tenant_id: tenantId };
|
|
543
|
+
writeCredentials(updated);
|
|
544
|
+
const profile = stored.tenants[tenantId];
|
|
545
|
+
console.log(`\u2713 Active tenant: ${profile.name || tenantId} (${tenantId})`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
console.log(`No cached token for ${tenantId}. Opening browser to authorize\u2026`);
|
|
325
549
|
await authLogin(stored.base_url, tenantId);
|
|
326
550
|
}
|
|
327
551
|
},
|
|
@@ -427,7 +651,7 @@ Version: v${a.version}`);
|
|
|
427
651
|
match: ["sessions", "message"],
|
|
428
652
|
needsArg: true,
|
|
429
653
|
usage: "oma sessions message <id> <text>",
|
|
430
|
-
desc: "
|
|
654
|
+
desc: "Fire-and-forget user message (no streaming back)",
|
|
431
655
|
http: 'POST /v1/sessions/:id/events {events:[{type:"user.message",content:[{type:"text",text:"..."}]}]}',
|
|
432
656
|
async run(config, args) {
|
|
433
657
|
const text = args.slice(1).join(" ");
|
|
@@ -435,6 +659,48 @@ Version: v${a.version}`);
|
|
|
435
659
|
console.log("Message sent.");
|
|
436
660
|
}
|
|
437
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
|
+
},
|
|
438
704
|
// Environments
|
|
439
705
|
{
|
|
440
706
|
group: "Environments",
|
|
@@ -1500,7 +1766,8 @@ var extraEndpoints = [
|
|
|
1500
1766
|
{ group: "Agents", http: "POST /v1/agents/:id/archive Archive agent" },
|
|
1501
1767
|
{ group: "Sessions", http: "GET /v1/sessions/:id Get session (status, usage)" },
|
|
1502
1768
|
{ group: "Sessions", http: "GET /v1/sessions/:id/events?limit=N Get events (JSON)" },
|
|
1503
|
-
{ group: "Sessions", http: "GET /v1/sessions/:id/stream
|
|
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)" },
|
|
1504
1771
|
{ group: "Sessions", http: "POST /v1/sessions/:id/archive Archive session" },
|
|
1505
1772
|
{ group: "Sessions", http: "DELETE /v1/sessions/:id Delete session" },
|
|
1506
1773
|
{ group: "Sessions", http: "POST /v1/sessions/:id/files Promote sandbox file {path}" },
|