@openma/cli 0.1.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/README.md +40 -0
- package/dist/index.js +1457 -0
- package/package.json +37 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1457 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
function loadConfig() {
|
|
7
|
+
const baseUrl = process.env.OMA_BASE_URL || "http://localhost:8787";
|
|
8
|
+
const apiKey = process.env.OMA_API_KEY || "";
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
console.error("Error: OMA_API_KEY environment variable is required");
|
|
11
|
+
console.error(" export OMA_API_KEY=your-api-key");
|
|
12
|
+
console.error("\n Generate one at: " + baseUrl + " \u2192 API Keys");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
return { baseUrl, apiKey, json: false };
|
|
16
|
+
}
|
|
17
|
+
async function apiFetch(config, path, init) {
|
|
18
|
+
const url = `${config.baseUrl}${path}`;
|
|
19
|
+
const res = await fetch(url, {
|
|
20
|
+
...init,
|
|
21
|
+
headers: {
|
|
22
|
+
"x-api-key": config.apiKey,
|
|
23
|
+
"content-type": "application/json",
|
|
24
|
+
// Identify as a browser-compatible client. Node's default `node` UA
|
|
25
|
+
// gets rejected by Cloudflare's bot fight rules on api.openma.dev with
|
|
26
|
+
// a 1010 ban; a Mozilla-style UA passes the integrity check while
|
|
27
|
+
// still naming the actual client and product page for log readers.
|
|
28
|
+
"user-agent": "Mozilla/5.0 (compatible; OpenManagedAgents-CLI/0.1; +https://openma.dev)",
|
|
29
|
+
...init?.headers
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const body = await res.text();
|
|
34
|
+
throw new Error(`${res.status} ${res.statusText}: ${body}`);
|
|
35
|
+
}
|
|
36
|
+
return res.json();
|
|
37
|
+
}
|
|
38
|
+
function flag(args, name) {
|
|
39
|
+
const idx = args.indexOf(name);
|
|
40
|
+
return idx !== -1 ? args[idx + 1] : void 0;
|
|
41
|
+
}
|
|
42
|
+
function table(rows) {
|
|
43
|
+
if (!rows.length) return;
|
|
44
|
+
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => (r[i] || "").length)));
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
console.log(row.map((c, i) => (c || "").padEnd(widths[i])).join(" "));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function capsPreview(caps) {
|
|
50
|
+
if (!caps.length) return "0";
|
|
51
|
+
if (caps.length <= 2) return caps.join(",");
|
|
52
|
+
return `${caps.slice(0, 2).join(",")}+${caps.length - 2}`;
|
|
53
|
+
}
|
|
54
|
+
function isPubliclyReachable(url) {
|
|
55
|
+
try {
|
|
56
|
+
const u = new URL(url);
|
|
57
|
+
if (u.hostname === "localhost" || u.hostname === "127.0.0.1") return false;
|
|
58
|
+
if (/^192\.168\./.test(u.hostname)) return false;
|
|
59
|
+
if (/^10\./.test(u.hostname)) return false;
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
var commands = [
|
|
66
|
+
// Agents
|
|
67
|
+
{
|
|
68
|
+
group: "Agents",
|
|
69
|
+
match: ["agents", "list"],
|
|
70
|
+
usage: "oma agents list",
|
|
71
|
+
desc: "List agents",
|
|
72
|
+
http: "GET /v1/agents?limit=N&order=asc|desc",
|
|
73
|
+
async run(config) {
|
|
74
|
+
const { data } = await apiFetch(config, "/v1/agents?limit=100");
|
|
75
|
+
if (!data.length) {
|
|
76
|
+
console.log("No agents. Create one with: oma agents create");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
table([["NAME", "ID", "MODEL", "CREATED"], ...data.map((a) => [a.name, a.id, typeof a.model === "string" ? a.model : a.model?.id || "", new Date(a.created_at).toLocaleDateString()])]);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
group: "Agents",
|
|
84
|
+
match: ["agents", "create"],
|
|
85
|
+
usage: "oma agents create <name> [--model <id>]",
|
|
86
|
+
desc: "Create agent",
|
|
87
|
+
http: "POST /v1/agents {name, model, system, tools, skills?, mcp_servers?, callable_agents?}",
|
|
88
|
+
async run(config, args) {
|
|
89
|
+
const name = flag(args, "--name") || args.find((a) => !a.startsWith("--"));
|
|
90
|
+
const model = flag(args, "--model") || "claude-sonnet-4-6";
|
|
91
|
+
const system = flag(args, "--system") || "";
|
|
92
|
+
if (!name) {
|
|
93
|
+
console.error("Usage: oma agents create <name> [--model <id>] [--system <prompt>]");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const agent = await apiFetch(config, "/v1/agents", { method: "POST", body: JSON.stringify({ name, model, system, tools: [{ type: "agent_toolset_20260401" }] }) });
|
|
97
|
+
console.log(`Agent created: ${agent.name} (${agent.id})`);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
group: "Agents",
|
|
102
|
+
match: ["agents", "get"],
|
|
103
|
+
needsArg: true,
|
|
104
|
+
usage: "oma agents get <id>",
|
|
105
|
+
desc: "Get agent details",
|
|
106
|
+
http: "GET /v1/agents/:id",
|
|
107
|
+
async run(config, args) {
|
|
108
|
+
const a = await apiFetch(config, `/v1/agents/${args[0]}`);
|
|
109
|
+
console.log(`Name: ${a.name}
|
|
110
|
+
ID: ${a.id}
|
|
111
|
+
Model: ${typeof a.model === "string" ? a.model : a.model?.id}
|
|
112
|
+
Version: v${a.version}`);
|
|
113
|
+
if (a.description) console.log(`Desc: ${a.description}`);
|
|
114
|
+
if (a.system) console.log(`System: ${a.system.slice(0, 100)}${a.system.length > 100 ? "..." : ""}`);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
group: "Agents",
|
|
119
|
+
match: ["agents", "delete"],
|
|
120
|
+
needsArg: true,
|
|
121
|
+
usage: "oma agents delete <id>",
|
|
122
|
+
desc: "Delete agent",
|
|
123
|
+
http: "DELETE /v1/agents/:id",
|
|
124
|
+
async run(config, args) {
|
|
125
|
+
await apiFetch(config, `/v1/agents/${args[0]}`, { method: "DELETE" });
|
|
126
|
+
console.log(`Agent deleted: ${args[0]}`);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
// Sessions
|
|
130
|
+
{
|
|
131
|
+
group: "Sessions",
|
|
132
|
+
match: ["sessions", "list"],
|
|
133
|
+
usage: "oma sessions list",
|
|
134
|
+
desc: "List sessions",
|
|
135
|
+
http: "GET /v1/sessions?agent_id=X&limit=N",
|
|
136
|
+
async run(config) {
|
|
137
|
+
const { data } = await apiFetch(config, "/v1/sessions?limit=20");
|
|
138
|
+
if (!data.length) {
|
|
139
|
+
console.log("No sessions.");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
table([["TITLE", "ID", "STATUS", "AGENT", "CREATED"], ...data.map((s) => [s.title || "Untitled", s.id, s.status || "idle", s.agent_id, new Date(s.created_at).toLocaleDateString()])]);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
group: "Sessions",
|
|
147
|
+
match: ["sessions", "create"],
|
|
148
|
+
usage: "oma sessions create --agent <id> --env <id> [--title <t>]",
|
|
149
|
+
desc: "Create session",
|
|
150
|
+
http: "POST /v1/sessions {agent, environment_id, title?, vault_ids?, resources?}",
|
|
151
|
+
async run(config, args) {
|
|
152
|
+
const agentId = flag(args, "--agent");
|
|
153
|
+
const envId = flag(args, "--env");
|
|
154
|
+
const title = flag(args, "--title") || "";
|
|
155
|
+
if (!agentId || !envId) {
|
|
156
|
+
console.error("Usage: oma sessions create --agent <id> --env <id> [--title <text>]");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
const session = await apiFetch(config, "/v1/sessions", { method: "POST", body: JSON.stringify({ agent: agentId, environment_id: envId, title }) });
|
|
160
|
+
console.log(`Session created: ${session.id}`);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
group: "Sessions",
|
|
165
|
+
match: ["sessions", "message"],
|
|
166
|
+
needsArg: true,
|
|
167
|
+
usage: "oma sessions message <id> <text>",
|
|
168
|
+
desc: "Send message to session",
|
|
169
|
+
http: 'POST /v1/sessions/:id/events {events:[{type:"user.message",content:[{type:"text",text:"..."}]}]}',
|
|
170
|
+
async run(config, args) {
|
|
171
|
+
const text = args.slice(1).join(" ");
|
|
172
|
+
await apiFetch(config, `/v1/sessions/${args[0]}/events`, { method: "POST", body: JSON.stringify({ events: [{ type: "user.message", content: [{ type: "text", text }] }] }) });
|
|
173
|
+
console.log("Message sent.");
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
// Environments
|
|
177
|
+
{
|
|
178
|
+
group: "Environments",
|
|
179
|
+
match: ["envs", "list"],
|
|
180
|
+
usage: "oma envs list",
|
|
181
|
+
desc: "List environments",
|
|
182
|
+
http: "GET /v1/environments",
|
|
183
|
+
async run(config) {
|
|
184
|
+
const { data } = await apiFetch(config, "/v1/environments");
|
|
185
|
+
if (!data.length) {
|
|
186
|
+
console.log("No environments. Create one with: oma envs create <name>");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
table([["NAME", "ID", "STATUS"], ...data.map((e) => [e.name, e.id, e.status || "ready"])]);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
group: "Environments",
|
|
194
|
+
match: ["envs", "create"],
|
|
195
|
+
needsArg: true,
|
|
196
|
+
usage: "oma envs create <name>",
|
|
197
|
+
desc: "Create environment",
|
|
198
|
+
http: 'POST /v1/environments {name, config:{type:"cloud"}}',
|
|
199
|
+
async run(config, args) {
|
|
200
|
+
const env = await apiFetch(config, "/v1/environments", { method: "POST", body: JSON.stringify({ name: args.join(" "), config: { type: "cloud" } }) });
|
|
201
|
+
console.log(`Environment created: ${env.name} (${env.id})`);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
// Model Cards
|
|
205
|
+
{
|
|
206
|
+
group: "Model Cards",
|
|
207
|
+
match: ["models", "list"],
|
|
208
|
+
usage: "oma models list",
|
|
209
|
+
desc: "List model cards",
|
|
210
|
+
http: "GET /v1/model_cards",
|
|
211
|
+
async run(config) {
|
|
212
|
+
const { data } = await apiFetch(config, "/v1/model_cards");
|
|
213
|
+
if (!data.length) {
|
|
214
|
+
console.log("No model cards. Create one with: oma models create");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
table([["NAME", "PROVIDER", "MODEL", "KEY", "DEFAULT"], ...data.map((c) => [c.name, c.provider, c.model_id, `****${c.api_key_preview || ""}`, c.is_default ? "yes" : ""])]);
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
group: "Model Cards",
|
|
222
|
+
match: ["models", "create"],
|
|
223
|
+
usage: "oma models create --name <n> --model-id <id> --api-key <key> [--provider <p>]",
|
|
224
|
+
desc: "Create model card",
|
|
225
|
+
http: "POST /v1/model_cards {name, provider, model_id, api_key, base_url?, is_default?}",
|
|
226
|
+
async run(config, args) {
|
|
227
|
+
const name = flag(args, "--name");
|
|
228
|
+
const provider = flag(args, "--provider") || "ant";
|
|
229
|
+
const modelId = flag(args, "--model-id");
|
|
230
|
+
const apiKey = flag(args, "--api-key");
|
|
231
|
+
const baseUrl = flag(args, "--base-url");
|
|
232
|
+
if (!name || !modelId || !apiKey) {
|
|
233
|
+
console.error("Usage: oma models create --name <name> --model-id <id> --api-key <key> [--provider ant|oai|ant-compatible|oai-compatible] [--base-url <url>]");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
const card = await apiFetch(config, "/v1/model_cards", { method: "POST", body: JSON.stringify({ name, provider, model_id: modelId, api_key: apiKey, base_url: baseUrl }) });
|
|
237
|
+
console.log(`Model card created: ${card.name} (${card.id})`);
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
// API Keys
|
|
241
|
+
{
|
|
242
|
+
group: "API Keys",
|
|
243
|
+
match: ["keys", "list"],
|
|
244
|
+
usage: "oma keys list",
|
|
245
|
+
desc: "List API keys",
|
|
246
|
+
http: "GET /v1/api_keys",
|
|
247
|
+
async run(config) {
|
|
248
|
+
const { data } = await apiFetch(config, "/v1/api_keys");
|
|
249
|
+
if (!data.length) {
|
|
250
|
+
console.log("No API keys. Create one with: oma keys create");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
table([["NAME", "ID", "PREFIX", "CREATED"], ...data.map((k) => [k.name, k.id, k.prefix + "...", new Date(k.created_at).toLocaleDateString()])]);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
group: "API Keys",
|
|
258
|
+
match: ["keys", "create"],
|
|
259
|
+
usage: "oma keys create [name]",
|
|
260
|
+
desc: "Create API key",
|
|
261
|
+
http: "POST /v1/api_keys {name?} \u2014 raw key returned once",
|
|
262
|
+
async run(config, args) {
|
|
263
|
+
const key = await apiFetch(config, "/v1/api_keys", { method: "POST", body: JSON.stringify({ name: args.join(" ") || "CLI key" }) });
|
|
264
|
+
console.log(`API key created: ${key.name}
|
|
265
|
+
|
|
266
|
+
${key.key}
|
|
267
|
+
|
|
268
|
+
Save this key \u2014 it won't be shown again.`);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
group: "API Keys",
|
|
273
|
+
match: ["keys", "revoke"],
|
|
274
|
+
needsArg: true,
|
|
275
|
+
usage: "oma keys revoke <id>",
|
|
276
|
+
desc: "Revoke API key",
|
|
277
|
+
http: "DELETE /v1/api_keys/:id",
|
|
278
|
+
async run(config, args) {
|
|
279
|
+
await apiFetch(config, `/v1/api_keys/${args[0]}`, { method: "DELETE" });
|
|
280
|
+
console.log(`API key revoked: ${args[0]}`);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
// Vaults & Credentials
|
|
284
|
+
{
|
|
285
|
+
group: "Vaults",
|
|
286
|
+
match: ["vaults", "list"],
|
|
287
|
+
usage: "oma vaults list",
|
|
288
|
+
desc: "List vaults",
|
|
289
|
+
http: "GET /v1/vaults",
|
|
290
|
+
async run(config) {
|
|
291
|
+
const { data } = await apiFetch(config, "/v1/vaults");
|
|
292
|
+
if (!data.length) {
|
|
293
|
+
console.log("No vaults. Create one with: oma vaults create <name>");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
table([["NAME", "ID", "CREATED"], ...data.map((v) => [v.name, v.id, new Date(v.created_at).toLocaleDateString()])]);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
group: "Vaults",
|
|
301
|
+
match: ["vaults", "create"],
|
|
302
|
+
needsArg: true,
|
|
303
|
+
usage: "oma vaults create <name>",
|
|
304
|
+
desc: "Create vault",
|
|
305
|
+
http: "POST /v1/vaults {name}",
|
|
306
|
+
async run(config, args) {
|
|
307
|
+
const vault = await apiFetch(config, "/v1/vaults", { method: "POST", body: JSON.stringify({ name: args.join(" ") }) });
|
|
308
|
+
console.log(`Vault created: ${vault.name} (${vault.id})`);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
group: "Vaults",
|
|
313
|
+
match: ["creds", "list"],
|
|
314
|
+
needsArg: true,
|
|
315
|
+
usage: "oma creds list <vault-id>",
|
|
316
|
+
desc: "List credentials",
|
|
317
|
+
http: "GET /v1/vaults/:id/credentials",
|
|
318
|
+
async run(config, args) {
|
|
319
|
+
const { data } = await apiFetch(config, `/v1/vaults/${args[0]}/credentials`);
|
|
320
|
+
if (!data.length) {
|
|
321
|
+
console.log("No credentials in this vault.");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
table([["NAME", "TYPE", "DETAIL"], ...data.map((c) => [c.display_name, c.auth.type, c.auth.mcp_server_url || c.auth.command_prefixes?.join(", ") || ""])]);
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
group: "Vaults",
|
|
329
|
+
match: ["secret", "add"],
|
|
330
|
+
usage: "oma secret add --vault <id> --name <n> --cmd <pfx> --env <var> --token <t>",
|
|
331
|
+
desc: "Add secret credential",
|
|
332
|
+
http: "POST /v1/vaults/:id/credentials {display_name, auth:{type, command_prefixes, env_var, token}}",
|
|
333
|
+
async run(config, args) {
|
|
334
|
+
const vaultId = flag(args, "--vault");
|
|
335
|
+
const name = flag(args, "--name");
|
|
336
|
+
const cmd = flag(args, "--cmd");
|
|
337
|
+
const envVar = flag(args, "--env");
|
|
338
|
+
const token = flag(args, "--token");
|
|
339
|
+
if (!vaultId || !name || !cmd || !envVar || !token) {
|
|
340
|
+
console.error("Usage: oma secret add --vault <id> --name <name> --cmd <prefixes> --env <var> --token <token>");
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
const cred = await apiFetch(config, `/v1/vaults/${vaultId}/credentials`, { method: "POST", body: JSON.stringify({ display_name: name, auth: { type: "command_secret", command_prefixes: cmd.split(",").map((s) => s.trim()), env_var: envVar, token } }) });
|
|
344
|
+
console.log(`Secret added: ${name} (${cred.id})`);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
// Skills
|
|
348
|
+
{
|
|
349
|
+
group: "Skills",
|
|
350
|
+
match: ["skills", "list"],
|
|
351
|
+
usage: "oma skills list",
|
|
352
|
+
desc: "List skills",
|
|
353
|
+
http: "GET /v1/skills?source=custom|builtin",
|
|
354
|
+
async run(config) {
|
|
355
|
+
const { data } = await apiFetch(config, "/v1/skills");
|
|
356
|
+
if (!data.length) {
|
|
357
|
+
console.log("No skills.");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
table([["NAME", "ID", "SOURCE"], ...data.map((s) => [s.display_title || s.name, s.id, s.source])]);
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
group: "Skills",
|
|
365
|
+
match: ["skills", "install"],
|
|
366
|
+
needsArg: true,
|
|
367
|
+
usage: "oma skills install <slug>",
|
|
368
|
+
desc: "Install from ClawHub",
|
|
369
|
+
http: "POST /v1/clawhub/install {slug}",
|
|
370
|
+
async run(config, args) {
|
|
371
|
+
console.log(`Installing ${args[0]} from ClawHub...`);
|
|
372
|
+
const skill = await apiFetch(config, "/v1/clawhub/install", { method: "POST", body: JSON.stringify({ slug: args[0] }) });
|
|
373
|
+
console.log(`Installed: ${skill.display_title} (${skill.id})`);
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
// MCP Connect
|
|
377
|
+
{
|
|
378
|
+
group: "MCP Servers",
|
|
379
|
+
match: ["connect"],
|
|
380
|
+
needsArg: true,
|
|
381
|
+
usage: "oma connect <server|url> --vault <id>",
|
|
382
|
+
desc: "Connect via OAuth",
|
|
383
|
+
http: "GET /v1/oauth/authorize?mcp_server_url=X&vault_id=Y (redirect)",
|
|
384
|
+
async run(config, args) {
|
|
385
|
+
const vaultId = flag(args, "--vault");
|
|
386
|
+
if (!vaultId) {
|
|
387
|
+
console.error("Usage: oma connect <server> --vault <vault-id>");
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
await connectMcp(config, resolveServerUrl(args[0]), vaultId);
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
// Linear integration
|
|
394
|
+
{
|
|
395
|
+
group: "Linear",
|
|
396
|
+
match: ["linear", "list"],
|
|
397
|
+
usage: "oma linear list",
|
|
398
|
+
desc: "List connected Linear workspaces",
|
|
399
|
+
http: "GET /v1/integrations/linear/installations",
|
|
400
|
+
async run(config) {
|
|
401
|
+
const { data } = await apiFetch(config, "/v1/integrations/linear/installations");
|
|
402
|
+
if (config.json) {
|
|
403
|
+
console.log(JSON.stringify(data, null, 2));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (!data.length) {
|
|
407
|
+
console.log("No Linear workspaces connected. Publish an agent with: oma linear publish <agent-id> --env <env-id>");
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
table([["WORKSPACE", "INSTALLATION ID", "KIND", "CREATED"], ...data.map((i) => [i.workspace_name, i.id, i.install_kind, new Date(i.created_at).toLocaleDateString()])]);
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
group: "Linear",
|
|
415
|
+
match: ["linear", "pubs"],
|
|
416
|
+
needsArg: true,
|
|
417
|
+
usage: "oma linear pubs <installation-id>",
|
|
418
|
+
desc: "List agents published to a workspace",
|
|
419
|
+
http: "GET /v1/integrations/linear/installations/:id/publications",
|
|
420
|
+
async run(config, args) {
|
|
421
|
+
const { data } = await apiFetch(config, `/v1/integrations/linear/installations/${args[0]}/publications`);
|
|
422
|
+
if (config.json) {
|
|
423
|
+
console.log(JSON.stringify(data, null, 2));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (!data.length) {
|
|
427
|
+
console.log("No publications. Publish with: oma linear publish <agent-id> --env <env-id>");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
table([["PERSONA", "PUBLICATION ID", "AGENT", "STATUS", "CAPS"], ...data.map((p) => [p.persona.name, p.id, p.agent_id, p.status, capsPreview(p.capabilities)])]);
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
group: "Linear",
|
|
435
|
+
match: ["linear", "get"],
|
|
436
|
+
needsArg: true,
|
|
437
|
+
usage: "oma linear get <publication-id>",
|
|
438
|
+
desc: "Show one publication",
|
|
439
|
+
http: "GET /v1/integrations/linear/publications/:id",
|
|
440
|
+
async run(config, args) {
|
|
441
|
+
const p = await apiFetch(config, `/v1/integrations/linear/publications/${args[0]}`);
|
|
442
|
+
if (config.json) {
|
|
443
|
+
console.log(JSON.stringify(p, null, 2));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
console.log(`Persona: ${p.persona.name}
|
|
447
|
+
ID: ${p.id}
|
|
448
|
+
Agent: ${p.agent_id}
|
|
449
|
+
Environment: ${p.environment_id}
|
|
450
|
+
Installation: ${p.installation_id}
|
|
451
|
+
Mode: ${p.mode}
|
|
452
|
+
Status: ${p.status}
|
|
453
|
+
Granularity: ${p.session_granularity}
|
|
454
|
+
Capabilities: ${p.capabilities.join(", ")}`);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
group: "Linear",
|
|
459
|
+
match: ["linear", "publish"],
|
|
460
|
+
needsArg: true,
|
|
461
|
+
usage: "oma linear publish <agent-id> --env <env-id> [--persona <name>] [--avatar <url>]",
|
|
462
|
+
desc: "Step 1: register agent \u2192 returns Linear App config",
|
|
463
|
+
http: "POST /v1/integrations/linear/start-a1 {agentId, environmentId, personaName, personaAvatarUrl?, returnUrl}",
|
|
464
|
+
async run(config, args) {
|
|
465
|
+
const agentId = args[0];
|
|
466
|
+
const envId = flag(args, "--env");
|
|
467
|
+
const persona = flag(args, "--persona") || "";
|
|
468
|
+
const avatar = flag(args, "--avatar") || null;
|
|
469
|
+
if (!envId) {
|
|
470
|
+
console.error("Usage: oma linear publish <agent-id> --env <env-id> [--persona <name>] [--avatar <url>]");
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
let personaName = persona;
|
|
474
|
+
if (!personaName) {
|
|
475
|
+
const agent = await apiFetch(config, `/v1/agents/${agentId}`).catch(() => null);
|
|
476
|
+
personaName = agent?.name || agentId;
|
|
477
|
+
}
|
|
478
|
+
const r = await apiFetch(
|
|
479
|
+
config,
|
|
480
|
+
"/v1/integrations/linear/start-a1",
|
|
481
|
+
{ method: "POST", body: JSON.stringify({ agentId, environmentId: envId, personaName, personaAvatarUrl: avatar, returnUrl: `${config.baseUrl}/integrations/linear` }) }
|
|
482
|
+
);
|
|
483
|
+
if (config.json) {
|
|
484
|
+
console.log(JSON.stringify(r, null, 2));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
console.log(`
|
|
488
|
+
Step 1 complete. Now register a Linear OAuth App (Linear \u2192 Settings \u2192 API \u2192 New OAuth app):
|
|
489
|
+
`);
|
|
490
|
+
console.log(` App name: ${r.suggestedAppName}`);
|
|
491
|
+
console.log(` Callback URL: ${r.callbackUrl}`);
|
|
492
|
+
console.log(` Webhook URL: ${r.webhookUrl}`);
|
|
493
|
+
console.log(` Webhook secret: (generated by Linear \u2014 see step 2)`);
|
|
494
|
+
if (!isPubliclyReachable(r.callbackUrl) || !r.callbackUrl.startsWith("https://")) {
|
|
495
|
+
console.log(`
|
|
496
|
+
\u26A0 Linear requires HTTPS on a publicly-reachable host for callback/webhook URLs.`);
|
|
497
|
+
console.log(`The URLs above point at a local / non-HTTPS origin \u2014 Linear's form will reject them.`);
|
|
498
|
+
console.log(`Fix: deploy the integrations worker (or run a tunnel like cloudflared/ngrok) and set`);
|
|
499
|
+
console.log(`GATEWAY_ORIGIN to that public HTTPS host before publishing.`);
|
|
500
|
+
}
|
|
501
|
+
console.log(`
|
|
502
|
+
Step 2 \u2014 submit the credentials Linear gives you:
|
|
503
|
+
`);
|
|
504
|
+
console.log(` oma linear submit <FORM_TOKEN> \\
|
|
505
|
+
--client-id <CLIENT_ID> --client-secret <CLIENT_SECRET> --webhook-secret <lin_wh_\u2026>
|
|
506
|
+
`);
|
|
507
|
+
console.log(`The webhook secret is on the same Linear App page, under "Webhooks \u2192 Signing secret"`);
|
|
508
|
+
console.log(`(starts with \`lin_wh_\`). Linear auto-generates it and ignores any value you paste in,`);
|
|
509
|
+
console.log(`so OMA can't predict it \u2014 you have to copy it back here.
|
|
510
|
+
`);
|
|
511
|
+
console.log(`Form token (expires ~30 min):
|
|
512
|
+
${r.formToken}
|
|
513
|
+
`);
|
|
514
|
+
console.log(`Or, to send the Linear App registration to a workspace admin instead:`);
|
|
515
|
+
console.log(` oma linear handoff ${r.formToken}`);
|
|
516
|
+
console.log(`
|
|
517
|
+
For scripts, re-run with --json to get the raw response.`);
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
group: "Linear",
|
|
522
|
+
match: ["linear", "submit"],
|
|
523
|
+
needsArg: true,
|
|
524
|
+
usage: "oma linear submit <form-token> --client-id <id> --client-secret <secret> --webhook-secret <lin_wh_\u2026>",
|
|
525
|
+
desc: "Step 2: validate creds \u2192 returns OAuth install URL",
|
|
526
|
+
http: "POST /v1/integrations/linear/credentials {formToken, clientId, clientSecret, webhookSecret}",
|
|
527
|
+
async run(config, args) {
|
|
528
|
+
const formToken = args[0];
|
|
529
|
+
const clientId = flag(args, "--client-id");
|
|
530
|
+
const clientSecret = flag(args, "--client-secret");
|
|
531
|
+
const webhookSecret = flag(args, "--webhook-secret");
|
|
532
|
+
if (!clientId || !clientSecret || !webhookSecret) {
|
|
533
|
+
console.error(
|
|
534
|
+
"Usage: oma linear submit <form-token> --client-id <id> --client-secret <secret> --webhook-secret <lin_wh_\u2026>\n webhook-secret is the 'Signing secret' on the Linear App's Webhooks panel."
|
|
535
|
+
);
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
const r = await apiFetch(
|
|
539
|
+
config,
|
|
540
|
+
"/v1/integrations/linear/credentials",
|
|
541
|
+
{ method: "POST", body: JSON.stringify({ formToken, clientId, clientSecret, webhookSecret }) }
|
|
542
|
+
).catch((err) => {
|
|
543
|
+
if (/form_token_invalid|JwtSigner\.verify/i.test(err.message)) {
|
|
544
|
+
console.error(`Form token rejected. Re-run \`oma linear publish <agent-id> --env <env-id>\` to mint a fresh token.`);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
throw err;
|
|
548
|
+
});
|
|
549
|
+
if (config.json) {
|
|
550
|
+
console.log(JSON.stringify(r, null, 2));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
console.log(`
|
|
554
|
+
Step 2 complete. Open this URL in a browser to authorize the install in Linear:
|
|
555
|
+
`);
|
|
556
|
+
console.log(` ${r.url}
|
|
557
|
+
`);
|
|
558
|
+
console.log(`After approval Linear redirects to the callback; the publication then transitions to 'live'.`);
|
|
559
|
+
console.log(`Verify with: oma linear list && oma linear pubs <installation-id>`);
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
group: "Linear",
|
|
564
|
+
match: ["linear", "handoff"],
|
|
565
|
+
needsArg: true,
|
|
566
|
+
usage: "oma linear handoff <form-token>",
|
|
567
|
+
desc: "Step 2 alt: 7-day shareable URL for an admin",
|
|
568
|
+
http: "POST /v1/integrations/linear/handoff-link {formToken}",
|
|
569
|
+
async run(config, args) {
|
|
570
|
+
const r = await apiFetch(
|
|
571
|
+
config,
|
|
572
|
+
"/v1/integrations/linear/handoff-link",
|
|
573
|
+
{ method: "POST", body: JSON.stringify({ formToken: args[0] }) }
|
|
574
|
+
).catch((err) => {
|
|
575
|
+
if (/form_token_invalid|JwtSigner\.verify/i.test(err.message)) {
|
|
576
|
+
console.error(`Form token rejected. Re-run \`oma linear publish <agent-id> --env <env-id>\` to mint a fresh token.`);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
throw err;
|
|
580
|
+
});
|
|
581
|
+
if (config.json) {
|
|
582
|
+
console.log(JSON.stringify(r, null, 2));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
console.log(`
|
|
586
|
+
Send this URL to your Linear workspace admin:
|
|
587
|
+
${r.url}
|
|
588
|
+
Expires in ${r.expiresInDays} days.`);
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
group: "Linear",
|
|
593
|
+
match: ["linear", "update"],
|
|
594
|
+
needsArg: true,
|
|
595
|
+
usage: "oma linear update <publication-id> [--persona <name>] [--avatar <url>] [--caps <a,b,c>]",
|
|
596
|
+
desc: "Update persona / capabilities of a publication",
|
|
597
|
+
http: "PATCH /v1/integrations/linear/publications/:id {persona?, capabilities?}",
|
|
598
|
+
async run(config, args) {
|
|
599
|
+
const id = args[0];
|
|
600
|
+
const personaName = flag(args, "--persona");
|
|
601
|
+
const avatarRaw = flag(args, "--avatar");
|
|
602
|
+
const capsRaw = flag(args, "--caps");
|
|
603
|
+
const patch = {};
|
|
604
|
+
if (personaName !== void 0 || avatarRaw !== void 0) {
|
|
605
|
+
patch.persona = {
|
|
606
|
+
...personaName !== void 0 ? { name: personaName } : {},
|
|
607
|
+
// --avatar "" clears the avatar (sends null), --avatar <url> sets it.
|
|
608
|
+
...avatarRaw !== void 0 ? { avatarUrl: avatarRaw === "" ? null : avatarRaw } : {}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
if (capsRaw !== void 0) {
|
|
612
|
+
patch.capabilities = capsRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
613
|
+
}
|
|
614
|
+
if (!Object.keys(patch).length) {
|
|
615
|
+
console.error("Nothing to update. Pass at least --persona, --avatar, or --caps.");
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
const updated = await apiFetch(config, `/v1/integrations/linear/publications/${id}`, {
|
|
619
|
+
method: "PATCH",
|
|
620
|
+
body: JSON.stringify(patch)
|
|
621
|
+
});
|
|
622
|
+
if (config.json) {
|
|
623
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
console.log(`Updated: ${updated.persona.name} (${updated.id}) \u2014 caps: ${updated.capabilities.length}`);
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
group: "Linear",
|
|
631
|
+
match: ["linear", "unpublish"],
|
|
632
|
+
needsArg: true,
|
|
633
|
+
usage: "oma linear unpublish <publication-id>",
|
|
634
|
+
desc: "Mark a publication unpublished",
|
|
635
|
+
http: "DELETE /v1/integrations/linear/publications/:id",
|
|
636
|
+
async run(config, args) {
|
|
637
|
+
try {
|
|
638
|
+
await apiFetch(config, `/v1/integrations/linear/publications/${args[0]}`, { method: "DELETE" });
|
|
639
|
+
} catch (err) {
|
|
640
|
+
if (/^404 /.test(err.message)) {
|
|
641
|
+
console.error(`No publication with id ${args[0]}.`);
|
|
642
|
+
console.error(`Find valid publication ids with: oma linear list && oma linear pubs <installation-id>`);
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
throw err;
|
|
646
|
+
}
|
|
647
|
+
console.log(`Unpublished: ${args[0]}`);
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
// GitHub integration — mirrors `oma linear *` shape exactly.
|
|
651
|
+
{
|
|
652
|
+
group: "GitHub",
|
|
653
|
+
match: ["github", "list"],
|
|
654
|
+
usage: "oma github list",
|
|
655
|
+
desc: "List connected GitHub installations",
|
|
656
|
+
http: "GET /v1/integrations/github/installations",
|
|
657
|
+
async run(config) {
|
|
658
|
+
const { data } = await apiFetch(config, "/v1/integrations/github/installations");
|
|
659
|
+
if (config.json) {
|
|
660
|
+
console.log(JSON.stringify(data, null, 2));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (!data.length) {
|
|
664
|
+
console.log("No GitHub installations. Publish an agent with: oma github publish <agent-id> --env <env-id>");
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
table([["ORG/USER", "INSTALLATION ID", "BOT LOGIN", "CREATED"], ...data.map((i) => [i.workspace_name, i.id, i.bot_login, new Date(i.created_at).toLocaleDateString()])]);
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
group: "GitHub",
|
|
672
|
+
match: ["github", "pubs"],
|
|
673
|
+
needsArg: true,
|
|
674
|
+
usage: "oma github pubs <installation-id>",
|
|
675
|
+
desc: "List agents published to a GitHub install",
|
|
676
|
+
http: "GET /v1/integrations/github/installations/:id/publications",
|
|
677
|
+
async run(config, args) {
|
|
678
|
+
const { data } = await apiFetch(config, `/v1/integrations/github/installations/${args[0]}/publications`);
|
|
679
|
+
if (config.json) {
|
|
680
|
+
console.log(JSON.stringify(data, null, 2));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (!data.length) {
|
|
684
|
+
console.log("No publications. Publish with: oma github publish <agent-id> --env <env-id>");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
table([["PERSONA", "PUBLICATION ID", "AGENT", "STATUS", "CAPS"], ...data.map((p) => [p.persona.name, p.id, p.agent_id, p.status, capsPreview(p.capabilities)])]);
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
group: "GitHub",
|
|
692
|
+
match: ["github", "get"],
|
|
693
|
+
needsArg: true,
|
|
694
|
+
usage: "oma github get <publication-id>",
|
|
695
|
+
desc: "Show one GitHub publication",
|
|
696
|
+
http: "GET /v1/integrations/github/publications/:id",
|
|
697
|
+
async run(config, args) {
|
|
698
|
+
const p = await apiFetch(config, `/v1/integrations/github/publications/${args[0]}`);
|
|
699
|
+
if (config.json) {
|
|
700
|
+
console.log(JSON.stringify(p, null, 2));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
console.log(`Persona: ${p.persona.name}
|
|
704
|
+
ID: ${p.id}
|
|
705
|
+
Agent: ${p.agent_id}
|
|
706
|
+
Environment: ${p.environment_id}
|
|
707
|
+
Installation: ${p.installation_id}
|
|
708
|
+
Mode: ${p.mode}
|
|
709
|
+
Status: ${p.status}
|
|
710
|
+
Granularity: ${p.session_granularity}
|
|
711
|
+
Capabilities: ${p.capabilities.join(", ")}`);
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
group: "GitHub",
|
|
716
|
+
match: ["github", "bind"],
|
|
717
|
+
needsArg: true,
|
|
718
|
+
usage: "oma github bind <agent-id> --env <env-id> [--persona <name>] [--avatar <url>]",
|
|
719
|
+
desc: "Bind agent to GitHub via App Manifest (one-click)",
|
|
720
|
+
http: "POST /v1/integrations/github/start-a1 {agentId, environmentId, personaName, personaAvatarUrl?, returnUrl}",
|
|
721
|
+
async run(config, args) {
|
|
722
|
+
const agentId = args[0];
|
|
723
|
+
const envId = flag(args, "--env");
|
|
724
|
+
const persona = flag(args, "--persona") || "";
|
|
725
|
+
const avatar = flag(args, "--avatar") || null;
|
|
726
|
+
if (!envId) {
|
|
727
|
+
console.error("Usage: oma github bind <agent-id> --env <env-id> [--persona <name>] [--avatar <url>]");
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
let personaName = persona;
|
|
731
|
+
if (!personaName) {
|
|
732
|
+
const agent = await apiFetch(config, `/v1/agents/${agentId}`).catch(() => null);
|
|
733
|
+
personaName = agent?.name || agentId;
|
|
734
|
+
}
|
|
735
|
+
const r = await apiFetch(
|
|
736
|
+
config,
|
|
737
|
+
"/v1/integrations/github/start-a1",
|
|
738
|
+
{ method: "POST", body: JSON.stringify({ agentId, environmentId: envId, personaName, personaAvatarUrl: avatar, returnUrl: `${config.baseUrl}/integrations/github` }) }
|
|
739
|
+
);
|
|
740
|
+
if (config.json) {
|
|
741
|
+
console.log(JSON.stringify(r, null, 2));
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
console.log(`
|
|
745
|
+
Binding "${r.suggestedAppName}" to GitHub.`);
|
|
746
|
+
if (!isPubliclyReachable(r.setupUrl) || !r.setupUrl.startsWith("https://")) {
|
|
747
|
+
console.log(`
|
|
748
|
+
\u26A0 GitHub requires HTTPS on a publicly-reachable host for Setup / Webhook URLs.`);
|
|
749
|
+
console.log(`The gateway URL above is local / non-HTTPS \u2014 GitHub will reject it.`);
|
|
750
|
+
console.log(`Fix: deploy the integrations worker (or run a tunnel like cloudflared/ngrok)`);
|
|
751
|
+
console.log(`and set GATEWAY_ORIGIN to that public HTTPS host before retrying.`);
|
|
752
|
+
}
|
|
753
|
+
console.log(`
|
|
754
|
+
\u2192 Open this URL to register the GitHub App in one click:
|
|
755
|
+
`);
|
|
756
|
+
console.log(` ${r.manifestStartUrl}
|
|
757
|
+
`);
|
|
758
|
+
console.log(`After confirming on GitHub you'll bounce through to "Install on org" automatically.`);
|
|
759
|
+
console.log(`Verify with: oma github list && oma github pubs <installation-id>
|
|
760
|
+
`);
|
|
761
|
+
console.log(`Manual fallback (if you want to register the App by hand instead):`);
|
|
762
|
+
console.log(` oma github submit ${r.formToken} --app-id <ID> --private-key-file <PEM> --webhook-secret <SECRET>`);
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
group: "GitHub",
|
|
767
|
+
match: ["github", "submit"],
|
|
768
|
+
needsArg: true,
|
|
769
|
+
usage: "oma github submit <form-token> --app-id <id> (--private-key <pem> | --private-key-file <path>) --webhook-secret <secret> [--client-id X] [--client-secret Y]",
|
|
770
|
+
desc: "Step 2: validate App credentials \u2192 returns install URL",
|
|
771
|
+
http: "POST /v1/integrations/github/credentials {formToken, appId, privateKey, webhookSecret, clientId?, clientSecret?}",
|
|
772
|
+
async run(config, args) {
|
|
773
|
+
const formToken = args[0];
|
|
774
|
+
const appId = flag(args, "--app-id");
|
|
775
|
+
const privateKeyInline = flag(args, "--private-key");
|
|
776
|
+
const privateKeyFile = flag(args, "--private-key-file");
|
|
777
|
+
const webhookSecret = flag(args, "--webhook-secret");
|
|
778
|
+
const clientId = flag(args, "--client-id");
|
|
779
|
+
const clientSecret = flag(args, "--client-secret");
|
|
780
|
+
if (!appId || !webhookSecret || !privateKeyInline && !privateKeyFile) {
|
|
781
|
+
console.error(
|
|
782
|
+
"Usage: oma github submit <form-token> --app-id <id> --private-key-file <path> --webhook-secret <secret>\n --private-key-file points at the .pem you downloaded from the App's settings page."
|
|
783
|
+
);
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
let privateKey;
|
|
787
|
+
if (privateKeyInline) {
|
|
788
|
+
privateKey = privateKeyInline.replace(/\\n/g, "\n");
|
|
789
|
+
} else {
|
|
790
|
+
const fs = await import("node:fs/promises");
|
|
791
|
+
privateKey = await fs.readFile(privateKeyFile, "utf8");
|
|
792
|
+
}
|
|
793
|
+
const r = await apiFetch(
|
|
794
|
+
config,
|
|
795
|
+
"/v1/integrations/github/credentials",
|
|
796
|
+
{ method: "POST", body: JSON.stringify({ formToken, appId, privateKey, webhookSecret, clientId, clientSecret }) }
|
|
797
|
+
).catch((err) => {
|
|
798
|
+
if (/form_token_invalid|JwtSigner\.verify/i.test(err.message)) {
|
|
799
|
+
console.error(`Form token rejected. Re-run \`oma github publish <agent-id> --env <env-id>\` to mint a fresh token.`);
|
|
800
|
+
process.exit(1);
|
|
801
|
+
}
|
|
802
|
+
if (/credentials_mismatch|appId mismatch/i.test(err.message)) {
|
|
803
|
+
console.error(`appId / private key mismatch. Both must come from the same GitHub App's settings page.`);
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
throw err;
|
|
807
|
+
});
|
|
808
|
+
if (config.json) {
|
|
809
|
+
console.log(JSON.stringify(r, null, 2));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
console.log(`
|
|
813
|
+
Step 2 complete. Bot will appear as @${r.botLogin}.`);
|
|
814
|
+
console.log(`
|
|
815
|
+
Open this URL in a browser and pick which org / repos to install on:
|
|
816
|
+
`);
|
|
817
|
+
console.log(` ${r.url}
|
|
818
|
+
`);
|
|
819
|
+
console.log(`After approval GitHub redirects to the setup URL; the publication transitions to 'live'.`);
|
|
820
|
+
console.log(`Verify with: oma github list && oma github pubs <installation-id>`);
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
group: "GitHub",
|
|
825
|
+
match: ["github", "handoff"],
|
|
826
|
+
needsArg: true,
|
|
827
|
+
usage: "oma github handoff <form-token>",
|
|
828
|
+
desc: "Step 2 alt: 7-day shareable URL for an org owner",
|
|
829
|
+
http: "POST /v1/integrations/github/handoff-link {formToken}",
|
|
830
|
+
async run(config, args) {
|
|
831
|
+
const r = await apiFetch(
|
|
832
|
+
config,
|
|
833
|
+
"/v1/integrations/github/handoff-link",
|
|
834
|
+
{ method: "POST", body: JSON.stringify({ formToken: args[0] }) }
|
|
835
|
+
).catch((err) => {
|
|
836
|
+
if (/form_token_invalid|JwtSigner\.verify/i.test(err.message)) {
|
|
837
|
+
console.error(`Form token rejected. Re-run \`oma github publish <agent-id> --env <env-id>\` to mint a fresh token.`);
|
|
838
|
+
process.exit(1);
|
|
839
|
+
}
|
|
840
|
+
throw err;
|
|
841
|
+
});
|
|
842
|
+
if (config.json) {
|
|
843
|
+
console.log(JSON.stringify(r, null, 2));
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
console.log(`
|
|
847
|
+
Send this URL to your GitHub org owner:
|
|
848
|
+
${r.url}
|
|
849
|
+
Expires in ${r.expiresInDays} days.`);
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
group: "GitHub",
|
|
854
|
+
match: ["github", "update"],
|
|
855
|
+
needsArg: true,
|
|
856
|
+
usage: "oma github update <publication-id> [--persona <name>] [--avatar <url>] [--caps <a,b,c>]",
|
|
857
|
+
desc: "Update persona / capabilities of a GitHub publication",
|
|
858
|
+
http: "PATCH /v1/integrations/github/publications/:id {persona?, capabilities?}",
|
|
859
|
+
async run(config, args) {
|
|
860
|
+
const id = args[0];
|
|
861
|
+
const personaName = flag(args, "--persona");
|
|
862
|
+
const avatarRaw = flag(args, "--avatar");
|
|
863
|
+
const capsRaw = flag(args, "--caps");
|
|
864
|
+
const patch = {};
|
|
865
|
+
if (personaName !== void 0 || avatarRaw !== void 0) {
|
|
866
|
+
patch.persona = {
|
|
867
|
+
...personaName !== void 0 ? { name: personaName } : {},
|
|
868
|
+
...avatarRaw !== void 0 ? { avatarUrl: avatarRaw === "" ? null : avatarRaw } : {}
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
if (capsRaw !== void 0) {
|
|
872
|
+
patch.capabilities = capsRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
873
|
+
}
|
|
874
|
+
if (!Object.keys(patch).length) {
|
|
875
|
+
console.error("Nothing to update. Pass at least --persona, --avatar, or --caps.");
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
const updated = await apiFetch(config, `/v1/integrations/github/publications/${id}`, {
|
|
879
|
+
method: "PATCH",
|
|
880
|
+
body: JSON.stringify(patch)
|
|
881
|
+
});
|
|
882
|
+
if (config.json) {
|
|
883
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
console.log(`Updated: ${updated.persona.name} (${updated.id}) \u2014 caps: ${updated.capabilities.length}`);
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
group: "GitHub",
|
|
891
|
+
match: ["github", "unpublish"],
|
|
892
|
+
needsArg: true,
|
|
893
|
+
usage: "oma github unpublish <publication-id>",
|
|
894
|
+
desc: "Mark a GitHub publication unpublished",
|
|
895
|
+
http: "DELETE /v1/integrations/github/publications/:id",
|
|
896
|
+
async run(config, args) {
|
|
897
|
+
try {
|
|
898
|
+
await apiFetch(config, `/v1/integrations/github/publications/${args[0]}`, { method: "DELETE" });
|
|
899
|
+
} catch (err) {
|
|
900
|
+
if (/^404 /.test(err.message)) {
|
|
901
|
+
console.error(`No publication with id ${args[0]}.`);
|
|
902
|
+
console.error(`Find valid publication ids with: oma github list && oma github pubs <installation-id>`);
|
|
903
|
+
process.exit(1);
|
|
904
|
+
}
|
|
905
|
+
throw err;
|
|
906
|
+
}
|
|
907
|
+
console.log(`Unpublished: ${args[0]}`);
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
// Slack integration — mirrors Linear's surface (A1 per-publication App).
|
|
911
|
+
{
|
|
912
|
+
group: "Slack",
|
|
913
|
+
match: ["slack", "list"],
|
|
914
|
+
usage: "oma slack list",
|
|
915
|
+
desc: "List connected Slack workspaces",
|
|
916
|
+
http: "GET /v1/integrations/slack/installations",
|
|
917
|
+
async run(config) {
|
|
918
|
+
const { data } = await apiFetch(config, "/v1/integrations/slack/installations");
|
|
919
|
+
if (config.json) {
|
|
920
|
+
console.log(JSON.stringify(data, null, 2));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (!data.length) {
|
|
924
|
+
console.log("No Slack workspaces connected. Publish an agent with: oma slack publish <agent-id> --env <env-id>");
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
table([["WORKSPACE", "INSTALLATION ID", "KIND", "CREATED"], ...data.map((i) => [i.workspace_name, i.id, i.install_kind, new Date(i.created_at).toLocaleDateString()])]);
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
group: "Slack",
|
|
932
|
+
match: ["slack", "pubs"],
|
|
933
|
+
needsArg: true,
|
|
934
|
+
usage: "oma slack pubs <installation-id>",
|
|
935
|
+
desc: "List agents published to a workspace",
|
|
936
|
+
http: "GET /v1/integrations/slack/installations/:id/publications",
|
|
937
|
+
async run(config, args) {
|
|
938
|
+
const { data } = await apiFetch(config, `/v1/integrations/slack/installations/${args[0]}/publications`);
|
|
939
|
+
if (config.json) {
|
|
940
|
+
console.log(JSON.stringify(data, null, 2));
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (!data.length) {
|
|
944
|
+
console.log("No publications. Publish with: oma slack publish <agent-id> --env <env-id>");
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
table([["PERSONA", "PUBLICATION ID", "AGENT", "STATUS", "CAPS"], ...data.map((p) => [p.persona.name, p.id, p.agent_id, p.status, capsPreview(p.capabilities)])]);
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
group: "Slack",
|
|
952
|
+
match: ["slack", "get"],
|
|
953
|
+
needsArg: true,
|
|
954
|
+
usage: "oma slack get <publication-id>",
|
|
955
|
+
desc: "Show one publication",
|
|
956
|
+
http: "GET /v1/integrations/slack/publications/:id",
|
|
957
|
+
async run(config, args) {
|
|
958
|
+
const p = await apiFetch(config, `/v1/integrations/slack/publications/${args[0]}`);
|
|
959
|
+
if (config.json) {
|
|
960
|
+
console.log(JSON.stringify(p, null, 2));
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
console.log(`Persona: ${p.persona.name}
|
|
964
|
+
ID: ${p.id}
|
|
965
|
+
Agent: ${p.agent_id}
|
|
966
|
+
Environment: ${p.environment_id}
|
|
967
|
+
Installation: ${p.installation_id}
|
|
968
|
+
Mode: ${p.mode}
|
|
969
|
+
Status: ${p.status}
|
|
970
|
+
Granularity: ${p.session_granularity}
|
|
971
|
+
Capabilities: ${p.capabilities.join(", ")}`);
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
group: "Slack",
|
|
976
|
+
match: ["slack", "publish"],
|
|
977
|
+
needsArg: true,
|
|
978
|
+
usage: "oma slack publish <agent-id> --env <env-id> [--persona <name>] [--avatar <url>]",
|
|
979
|
+
desc: "Step 1: register agent \u2192 returns Slack App config",
|
|
980
|
+
http: "POST /v1/integrations/slack/start-a1 {agentId, environmentId, personaName, personaAvatarUrl?, returnUrl}",
|
|
981
|
+
async run(config, args) {
|
|
982
|
+
const agentId = args[0];
|
|
983
|
+
const envId = flag(args, "--env");
|
|
984
|
+
const persona = flag(args, "--persona") || "";
|
|
985
|
+
const avatar = flag(args, "--avatar") || null;
|
|
986
|
+
if (!envId) {
|
|
987
|
+
console.error("Usage: oma slack publish <agent-id> --env <env-id> [--persona <name>] [--avatar <url>]");
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
let personaName = persona;
|
|
991
|
+
if (!personaName) {
|
|
992
|
+
const agent = await apiFetch(config, `/v1/agents/${agentId}`).catch(() => null);
|
|
993
|
+
personaName = agent?.name || agentId;
|
|
994
|
+
}
|
|
995
|
+
const r = await apiFetch(
|
|
996
|
+
config,
|
|
997
|
+
"/v1/integrations/slack/start-a1",
|
|
998
|
+
{ method: "POST", body: JSON.stringify({ agentId, environmentId: envId, personaName, personaAvatarUrl: avatar, returnUrl: `${config.baseUrl}/integrations/slack` }) }
|
|
999
|
+
);
|
|
1000
|
+
if (config.json) {
|
|
1001
|
+
console.log(JSON.stringify(r, null, 2));
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (r.manifestLaunchUrl) {
|
|
1005
|
+
console.log(`
|
|
1006
|
+
Step 1 complete. One-click setup \u2014 open this URL to have Slack create the App for you:
|
|
1007
|
+
`);
|
|
1008
|
+
console.log(` ${r.manifestLaunchUrl}
|
|
1009
|
+
`);
|
|
1010
|
+
console.log(`Slack will pre-fill name, scopes, events, and redirect URL from a manifest.`);
|
|
1011
|
+
console.log(`Confirm Create on Slack, then come back and paste the secrets it shows you.
|
|
1012
|
+
`);
|
|
1013
|
+
console.log(`Or set up manually:`);
|
|
1014
|
+
} else {
|
|
1015
|
+
console.log(`
|
|
1016
|
+
Step 1 complete. Now create a Slack App (https://api.slack.com/apps \u2192 Create New App \u2192 From scratch):
|
|
1017
|
+
`);
|
|
1018
|
+
}
|
|
1019
|
+
console.log(` App name: ${r.suggestedAppName}`);
|
|
1020
|
+
console.log(` Redirect URL: ${r.callbackUrl}`);
|
|
1021
|
+
console.log(` Events Request URL: ${r.webhookUrl}`);
|
|
1022
|
+
console.log(`
|
|
1023
|
+
In the App settings:`);
|
|
1024
|
+
console.log(` \u2022 OAuth & Permissions \u2192 paste Redirect URL`);
|
|
1025
|
+
console.log(` \u2022 Event Subscriptions \u2192 paste Events Request URL, wait for green "Verified" check`);
|
|
1026
|
+
console.log(` \u2022 Subscribe to bot events: app_mention, message.channels, message.im,`);
|
|
1027
|
+
console.log(` message.groups, message.mpim, tokens_revoked, app_uninstalled`);
|
|
1028
|
+
if (!isPubliclyReachable(r.callbackUrl) || !r.callbackUrl.startsWith("https://")) {
|
|
1029
|
+
console.log(`
|
|
1030
|
+
\u26A0 Slack requires HTTPS on a publicly-reachable host for Redirect / Events URLs.`);
|
|
1031
|
+
console.log(`The URLs above point at a local / non-HTTPS origin \u2014 Slack's Verify button will fail.`);
|
|
1032
|
+
console.log(`Fix: deploy the integrations worker (or run a tunnel like cloudflared/ngrok) and set`);
|
|
1033
|
+
console.log(`GATEWAY_ORIGIN to that public HTTPS host before publishing.`);
|
|
1034
|
+
}
|
|
1035
|
+
console.log(`
|
|
1036
|
+
Step 2 \u2014 submit the credentials Slack gives you (Basic Information page):
|
|
1037
|
+
`);
|
|
1038
|
+
console.log(` oma slack submit <FORM_TOKEN> \\
|
|
1039
|
+
--client-id <CLIENT_ID> --client-secret <CLIENT_SECRET> --signing-secret <SIGNING_SECRET>
|
|
1040
|
+
`);
|
|
1041
|
+
console.log(`The Signing Secret is on the same Basic Information page; Slack uses it to`);
|
|
1042
|
+
console.log(`sign every webhook event.
|
|
1043
|
+
`);
|
|
1044
|
+
console.log(`Form token (expires ~60 min):
|
|
1045
|
+
${r.formToken}
|
|
1046
|
+
`);
|
|
1047
|
+
console.log(`Or, to send the Slack App registration to a workspace admin instead:`);
|
|
1048
|
+
console.log(` oma slack handoff ${r.formToken}`);
|
|
1049
|
+
console.log(`
|
|
1050
|
+
For scripts, re-run with --json to get the raw response.`);
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
group: "Slack",
|
|
1055
|
+
match: ["slack", "submit"],
|
|
1056
|
+
needsArg: true,
|
|
1057
|
+
usage: "oma slack submit <form-token> --client-id <id> --client-secret <secret> --signing-secret <secret>",
|
|
1058
|
+
desc: "Step 2: validate creds \u2192 returns OAuth install URL",
|
|
1059
|
+
http: "POST /v1/integrations/slack/credentials {formToken, clientId, clientSecret, signingSecret}",
|
|
1060
|
+
async run(config, args) {
|
|
1061
|
+
const formToken = args[0];
|
|
1062
|
+
const clientId = flag(args, "--client-id");
|
|
1063
|
+
const clientSecret = flag(args, "--client-secret");
|
|
1064
|
+
const signingSecret = flag(args, "--signing-secret");
|
|
1065
|
+
if (!clientId || !clientSecret || !signingSecret) {
|
|
1066
|
+
console.error(
|
|
1067
|
+
"Usage: oma slack submit <form-token> --client-id <id> --client-secret <secret> --signing-secret <secret>\n signing-secret is the 'Signing Secret' on the Slack App's Basic Information page."
|
|
1068
|
+
);
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
const r = await apiFetch(
|
|
1072
|
+
config,
|
|
1073
|
+
"/v1/integrations/slack/credentials",
|
|
1074
|
+
{ method: "POST", body: JSON.stringify({ formToken, clientId, clientSecret, signingSecret }) }
|
|
1075
|
+
).catch((err) => {
|
|
1076
|
+
if (/form_token_invalid|JwtSigner\.verify/i.test(err.message)) {
|
|
1077
|
+
console.error(`Form token rejected. Re-run \`oma slack publish <agent-id> --env <env-id>\` to mint a fresh token.`);
|
|
1078
|
+
process.exit(1);
|
|
1079
|
+
}
|
|
1080
|
+
throw err;
|
|
1081
|
+
});
|
|
1082
|
+
if (config.json) {
|
|
1083
|
+
console.log(JSON.stringify(r, null, 2));
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
console.log(`
|
|
1087
|
+
Step 2 complete. Open this URL in a browser to authorize the install in Slack:
|
|
1088
|
+
`);
|
|
1089
|
+
console.log(` ${r.url}
|
|
1090
|
+
`);
|
|
1091
|
+
console.log(`After approval Slack redirects to the callback; the publication then transitions to 'live'.`);
|
|
1092
|
+
console.log(`Verify with: oma slack list && oma slack pubs <installation-id>`);
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
group: "Slack",
|
|
1097
|
+
match: ["slack", "handoff"],
|
|
1098
|
+
needsArg: true,
|
|
1099
|
+
usage: "oma slack handoff <form-token>",
|
|
1100
|
+
desc: "Step 2 alt: 7-day shareable URL for an admin",
|
|
1101
|
+
http: "POST /v1/integrations/slack/handoff-link {formToken}",
|
|
1102
|
+
async run(config, args) {
|
|
1103
|
+
const r = await apiFetch(
|
|
1104
|
+
config,
|
|
1105
|
+
"/v1/integrations/slack/handoff-link",
|
|
1106
|
+
{ method: "POST", body: JSON.stringify({ formToken: args[0] }) }
|
|
1107
|
+
).catch((err) => {
|
|
1108
|
+
if (/form_token_invalid|JwtSigner\.verify/i.test(err.message)) {
|
|
1109
|
+
console.error(`Form token rejected. Re-run \`oma slack publish <agent-id> --env <env-id>\` to mint a fresh token.`);
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
throw err;
|
|
1113
|
+
});
|
|
1114
|
+
if (config.json) {
|
|
1115
|
+
console.log(JSON.stringify(r, null, 2));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
console.log(`
|
|
1119
|
+
Send this URL to your Slack workspace admin:
|
|
1120
|
+
${r.url}
|
|
1121
|
+
Expires in ${r.expiresInDays} days.`);
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
group: "Slack",
|
|
1126
|
+
match: ["slack", "update"],
|
|
1127
|
+
needsArg: true,
|
|
1128
|
+
usage: "oma slack update <publication-id> [--persona <name>] [--avatar <url>] [--caps <a,b,c>]",
|
|
1129
|
+
desc: "Update persona / capabilities of a publication",
|
|
1130
|
+
http: "PATCH /v1/integrations/slack/publications/:id {persona?, capabilities?}",
|
|
1131
|
+
async run(config, args) {
|
|
1132
|
+
const id = args[0];
|
|
1133
|
+
const personaName = flag(args, "--persona");
|
|
1134
|
+
const avatarRaw = flag(args, "--avatar");
|
|
1135
|
+
const capsRaw = flag(args, "--caps");
|
|
1136
|
+
const patch = {};
|
|
1137
|
+
if (personaName !== void 0 || avatarRaw !== void 0) {
|
|
1138
|
+
patch.persona = {
|
|
1139
|
+
...personaName !== void 0 ? { name: personaName } : {},
|
|
1140
|
+
...avatarRaw !== void 0 ? { avatarUrl: avatarRaw === "" ? null : avatarRaw } : {}
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
if (capsRaw !== void 0) {
|
|
1144
|
+
patch.capabilities = capsRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1145
|
+
}
|
|
1146
|
+
if (!Object.keys(patch).length) {
|
|
1147
|
+
console.error("Nothing to update. Pass at least --persona, --avatar, or --caps.");
|
|
1148
|
+
process.exit(1);
|
|
1149
|
+
}
|
|
1150
|
+
const updated = await apiFetch(config, `/v1/integrations/slack/publications/${id}`, {
|
|
1151
|
+
method: "PATCH",
|
|
1152
|
+
body: JSON.stringify(patch)
|
|
1153
|
+
});
|
|
1154
|
+
if (config.json) {
|
|
1155
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
console.log(`Updated: ${updated.persona.name} (${updated.id}) \u2014 caps: ${updated.capabilities.length}`);
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
group: "Slack",
|
|
1163
|
+
match: ["slack", "unpublish"],
|
|
1164
|
+
needsArg: true,
|
|
1165
|
+
usage: "oma slack unpublish <publication-id>",
|
|
1166
|
+
desc: "Mark a publication unpublished",
|
|
1167
|
+
http: "DELETE /v1/integrations/slack/publications/:id",
|
|
1168
|
+
async run(config, args) {
|
|
1169
|
+
try {
|
|
1170
|
+
await apiFetch(config, `/v1/integrations/slack/publications/${args[0]}`, { method: "DELETE" });
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
if (/^404 /.test(err.message)) {
|
|
1173
|
+
console.error(`No publication with id ${args[0]}.`);
|
|
1174
|
+
console.error(`Find valid publication ids with: oma slack list && oma slack pubs <installation-id>`);
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
throw err;
|
|
1178
|
+
}
|
|
1179
|
+
console.log(`Unpublished: ${args[0]}`);
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
// Memory
|
|
1183
|
+
{
|
|
1184
|
+
group: "Memory",
|
|
1185
|
+
match: ["memory", "reconcile"],
|
|
1186
|
+
usage: "oma memory reconcile [--store <id>] [--limit N] [--all]",
|
|
1187
|
+
desc: "Re-embed memories whose Vectorize sync is stale (vector_synced_at IS NULL).",
|
|
1188
|
+
http: "POST /v1/memory_stores/_reconcile {store_id?, limit?}",
|
|
1189
|
+
async run(config, args) {
|
|
1190
|
+
const storeId = flag(args, "--store");
|
|
1191
|
+
const limitStr = flag(args, "--limit");
|
|
1192
|
+
const all = args.includes("--all");
|
|
1193
|
+
const limit = limitStr ? Number(limitStr) : 200;
|
|
1194
|
+
if (!storeId && !all) {
|
|
1195
|
+
console.error("Specify either --store <id> or --all (drains all stores in the tenant).");
|
|
1196
|
+
process.exit(1);
|
|
1197
|
+
}
|
|
1198
|
+
let totalScanned = 0, totalFixed = 0, totalFailing = 0;
|
|
1199
|
+
const errors = [];
|
|
1200
|
+
for (let pass = 1; pass <= 100; pass++) {
|
|
1201
|
+
const body = { limit };
|
|
1202
|
+
if (storeId) body.store_id = storeId;
|
|
1203
|
+
const result = await apiFetch(config, "/v1/memory_stores/_reconcile", {
|
|
1204
|
+
method: "POST",
|
|
1205
|
+
body: JSON.stringify(body)
|
|
1206
|
+
});
|
|
1207
|
+
totalScanned += result.scanned;
|
|
1208
|
+
totalFixed += result.fixed;
|
|
1209
|
+
totalFailing = result.still_failing;
|
|
1210
|
+
for (const e of result.sample_errors) if (errors.length < 10) errors.push(e);
|
|
1211
|
+
if (config.json) {
|
|
1212
|
+
console.log(JSON.stringify({ pass, ...result }));
|
|
1213
|
+
} else {
|
|
1214
|
+
console.log(`pass ${pass}: scanned=${result.scanned} fixed=${result.fixed} failing=${result.still_failing}`);
|
|
1215
|
+
}
|
|
1216
|
+
if (result.scanned === 0) break;
|
|
1217
|
+
if (result.fixed === 0 && result.still_failing > 0) {
|
|
1218
|
+
console.error("No progress this pass \u2014 remaining rows keep failing. Stopping.");
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (config.json) {
|
|
1223
|
+
console.log(JSON.stringify({ totals: { scanned: totalScanned, fixed: totalFixed, failing: totalFailing }, errors }));
|
|
1224
|
+
} else {
|
|
1225
|
+
console.log(`
|
|
1226
|
+
Done. scanned=${totalScanned} fixed=${totalFixed} still_failing=${totalFailing}`);
|
|
1227
|
+
if (errors.length) {
|
|
1228
|
+
console.log("\nSample errors:");
|
|
1229
|
+
for (const e of errors) console.log(` ${e.memory_id}: ${e.error}`);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
];
|
|
1235
|
+
var extraEndpoints = [
|
|
1236
|
+
{ group: "Agents", http: "POST /v1/agents/:id Update agent" },
|
|
1237
|
+
{ group: "Agents", http: "GET /v1/agents/:id/versions List versions" },
|
|
1238
|
+
{ group: "Agents", http: "POST /v1/agents/:id/archive Archive agent" },
|
|
1239
|
+
{ group: "Sessions", http: "GET /v1/sessions/:id Get session (status, usage)" },
|
|
1240
|
+
{ group: "Sessions", http: "GET /v1/sessions/:id/events?limit=N Get events (JSON)" },
|
|
1241
|
+
{ group: "Sessions", http: "GET /v1/sessions/:id/stream Stream events (SSE)" },
|
|
1242
|
+
{ group: "Sessions", http: "POST /v1/sessions/:id/archive Archive session" },
|
|
1243
|
+
{ group: "Sessions", http: "DELETE /v1/sessions/:id Delete session" },
|
|
1244
|
+
{ group: "Sessions", http: "POST /v1/sessions/:id/files Promote sandbox file {path}" },
|
|
1245
|
+
{ group: "Sessions", http: "POST /v1/sessions/:id/resources Add resource {type, file_id?, memory_store_id?}" },
|
|
1246
|
+
{ group: "Sessions", http: "GET /v1/sessions/:id/threads List threads (multi-agent)" },
|
|
1247
|
+
{ group: "Environments", http: "GET /v1/environments/:id Get environment" },
|
|
1248
|
+
{ group: "Environments", http: "PUT /v1/environments/:id Update environment" },
|
|
1249
|
+
{ group: "Environments", http: "DELETE /v1/environments/:id Delete environment" },
|
|
1250
|
+
{ group: "Model Cards", http: "GET /v1/model_cards/:id Get model card" },
|
|
1251
|
+
{ group: "Model Cards", http: "POST /v1/model_cards/:id Update model card" },
|
|
1252
|
+
{ group: "Model Cards", http: "DELETE /v1/model_cards/:id Delete model card" },
|
|
1253
|
+
{ group: "Model Cards", http: "POST /v1/models/list Fetch provider models {provider, api_key}" },
|
|
1254
|
+
{ group: "Vaults", http: "DELETE /v1/vaults/:id Delete vault" },
|
|
1255
|
+
{ group: "Vaults", http: "DELETE /v1/vaults/:id/credentials/:cid Delete credential" },
|
|
1256
|
+
{ group: "Linear", http: "PATCH /v1/integrations/linear/publications/:id Update persona / capabilities" },
|
|
1257
|
+
{ group: "GitHub", http: "PATCH /v1/integrations/github/publications/:id Update persona / capabilities" },
|
|
1258
|
+
{ group: "OAuth", http: "GET /v1/oauth/callback OAuth callback (internal)" },
|
|
1259
|
+
{ group: "OAuth", http: "POST /v1/oauth/refresh Refresh token {vault_id, credential_id}" },
|
|
1260
|
+
{ group: "Skills", http: "POST /v1/skills Create skill {files:[{filename,content}]}" },
|
|
1261
|
+
{ group: "Skills", http: "GET /v1/skills/:id Get skill" },
|
|
1262
|
+
{ group: "Skills", http: "DELETE /v1/skills/:id Delete skill" },
|
|
1263
|
+
{ group: "Skills", http: "POST /v1/skills/:id/versions Create new version {files}" },
|
|
1264
|
+
{ group: "Skills", http: "GET /v1/skills/:id/versions List versions" },
|
|
1265
|
+
{ group: "Files", http: "POST /v1/files Upload (multipart or JSON {filename,content,encoding?})" },
|
|
1266
|
+
{ group: "Files", http: "GET /v1/files?scope_id=X&limit=N List files (cursor-paginated)" },
|
|
1267
|
+
{ group: "Files", http: "GET /v1/files/:id Get file metadata" },
|
|
1268
|
+
{ group: "Files", http: "GET /v1/files/:id/content Download file content" },
|
|
1269
|
+
{ group: "Files", http: "DELETE /v1/files/:id Delete file" },
|
|
1270
|
+
{ group: "Memory", http: "POST /v1/memory_stores Create store {name}" },
|
|
1271
|
+
{ group: "Memory", http: "GET /v1/memory_stores List stores" },
|
|
1272
|
+
{ group: "Memory", http: "DELETE /v1/memory_stores/:id Delete store" },
|
|
1273
|
+
{ group: "Memory", http: "POST /v1/memory_stores/:id/memories Write memory {path, content} (max 100KB)" },
|
|
1274
|
+
{ group: "Memory", http: "GET /v1/memory_stores/:id/memories?prefix=X List memories (metadata)" },
|
|
1275
|
+
{ group: "Memory", http: "GET /v1/memory_stores/:id/memories/:mid Get memory with content" },
|
|
1276
|
+
{ group: "Memory", http: "DELETE /v1/memory_stores/:id/memories/:mid Delete memory" },
|
|
1277
|
+
{ group: "Memory", http: "POST /v1/memory_stores/_reconcile {store_id?,limit?} Re-embed stale rows" },
|
|
1278
|
+
{ group: "Evals", http: "POST /v1/evals/runs Create eval run {agent_id, environment_id, tasks:[...]}" },
|
|
1279
|
+
{ group: "Evals", http: "GET /v1/evals/runs List eval runs" },
|
|
1280
|
+
{ group: "Evals", http: "GET /v1/evals/runs/:id Get eval run results" },
|
|
1281
|
+
{ group: "ClawHub", http: "GET /v1/clawhub/search?q=X Search ClawHub registry" }
|
|
1282
|
+
];
|
|
1283
|
+
var KNOWN_SERVERS = {
|
|
1284
|
+
airtable: "https://mcp.airtable.com/mcp",
|
|
1285
|
+
amplitude: "https://mcp.amplitude.com/mcp",
|
|
1286
|
+
apollo: "https://mcp.apollo.io/mcp",
|
|
1287
|
+
asana: "https://mcp.asana.com/v2/mcp",
|
|
1288
|
+
atlassian: "https://mcp.atlassian.com/v1/mcp",
|
|
1289
|
+
clickup: "https://mcp.clickup.com/mcp",
|
|
1290
|
+
github: "https://api.githubcopilot.com/mcp/",
|
|
1291
|
+
intercom: "https://mcp.intercom.com/mcp",
|
|
1292
|
+
linear: "https://mcp.linear.app/mcp",
|
|
1293
|
+
notion: "https://mcp.notion.com/mcp",
|
|
1294
|
+
sentry: "https://mcp.sentry.dev/mcp",
|
|
1295
|
+
slack: "https://mcp.slack.com/mcp"
|
|
1296
|
+
};
|
|
1297
|
+
function resolveServerUrl(nameOrUrl) {
|
|
1298
|
+
if (nameOrUrl.startsWith("http://") || nameOrUrl.startsWith("https://")) return nameOrUrl;
|
|
1299
|
+
const url = KNOWN_SERVERS[nameOrUrl.toLowerCase()];
|
|
1300
|
+
if (!url) {
|
|
1301
|
+
console.error(`Unknown server: ${nameOrUrl}. Known: ${Object.keys(KNOWN_SERVERS).join(", ")}`);
|
|
1302
|
+
process.exit(1);
|
|
1303
|
+
}
|
|
1304
|
+
return url;
|
|
1305
|
+
}
|
|
1306
|
+
async function connectMcp(config, mcpServerUrl, vaultId) {
|
|
1307
|
+
const port = 19284 + Math.floor(Math.random() * 1e3);
|
|
1308
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
1309
|
+
const authUrl = `${config.baseUrl}/v1/oauth/authorize?mcp_server_url=${encodeURIComponent(mcpServerUrl)}&vault_id=${encodeURIComponent(vaultId)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
1310
|
+
console.log(`Opening browser...
|
|
1311
|
+
${authUrl}
|
|
1312
|
+
`);
|
|
1313
|
+
try {
|
|
1314
|
+
const p = process.platform;
|
|
1315
|
+
if (p === "darwin") execSync(`open "${authUrl}"`);
|
|
1316
|
+
else if (p === "linux") execSync(`xdg-open "${authUrl}"`);
|
|
1317
|
+
else if (p === "win32") execSync(`start "${authUrl}"`);
|
|
1318
|
+
} catch {
|
|
1319
|
+
}
|
|
1320
|
+
return new Promise((resolve) => {
|
|
1321
|
+
const server = createServer((req, res) => {
|
|
1322
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
1323
|
+
if (url.pathname === "/callback") {
|
|
1324
|
+
const service = url.searchParams.get("service");
|
|
1325
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1326
|
+
res.end(`<html><body><h2>Connected${service ? ` to ${service}` : ""}!</h2><script>window.close()</script></body></html>`);
|
|
1327
|
+
console.log(`Connected to ${service || mcpServerUrl}`);
|
|
1328
|
+
server.close();
|
|
1329
|
+
resolve();
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
server.listen(port);
|
|
1333
|
+
setTimeout(() => {
|
|
1334
|
+
console.log("Timed out.");
|
|
1335
|
+
server.close();
|
|
1336
|
+
resolve();
|
|
1337
|
+
}, 3e5);
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
function apiRef(resource) {
|
|
1341
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1342
|
+
for (const c of commands) {
|
|
1343
|
+
const list = groups.get(c.group) || [];
|
|
1344
|
+
list.push(` ${c.http}`);
|
|
1345
|
+
groups.set(c.group, list);
|
|
1346
|
+
}
|
|
1347
|
+
for (const e of extraEndpoints) {
|
|
1348
|
+
const list = groups.get(e.group) || [];
|
|
1349
|
+
list.push(` ${e.http}`);
|
|
1350
|
+
groups.set(e.group, list);
|
|
1351
|
+
}
|
|
1352
|
+
const normalized = resource?.toLowerCase();
|
|
1353
|
+
const groupAlias = {
|
|
1354
|
+
agents: "Agents",
|
|
1355
|
+
sessions: "Sessions",
|
|
1356
|
+
environments: "Environments",
|
|
1357
|
+
models: "Model Cards",
|
|
1358
|
+
vaults: "Vaults",
|
|
1359
|
+
oauth: "OAuth",
|
|
1360
|
+
skills: "Skills",
|
|
1361
|
+
files: "Files",
|
|
1362
|
+
memory: "Memory",
|
|
1363
|
+
keys: "API Keys",
|
|
1364
|
+
evals: "Evals",
|
|
1365
|
+
clawhub: "ClawHub",
|
|
1366
|
+
"mcp": "MCP Servers",
|
|
1367
|
+
linear: "Linear",
|
|
1368
|
+
github: "GitHub",
|
|
1369
|
+
integrations: "Linear"
|
|
1370
|
+
};
|
|
1371
|
+
if (normalized && groupAlias[normalized]) {
|
|
1372
|
+
const name = groupAlias[normalized];
|
|
1373
|
+
const lines = groups.get(name);
|
|
1374
|
+
if (lines) {
|
|
1375
|
+
console.log(`
|
|
1376
|
+
${name}
|
|
1377
|
+
${lines.join("\n")}
|
|
1378
|
+
`);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
console.log(`
|
|
1383
|
+
oma api \u2014 HTTP API Quick Reference
|
|
1384
|
+
Auth: all /v1/* endpoints require x-api-key header
|
|
1385
|
+
`);
|
|
1386
|
+
for (const [name, lines] of groups) {
|
|
1387
|
+
console.log(`${name}
|
|
1388
|
+
${lines.join("\n")}
|
|
1389
|
+
`);
|
|
1390
|
+
}
|
|
1391
|
+
if (normalized && !groupAlias[normalized]) {
|
|
1392
|
+
console.log(`Unknown resource: ${resource}
|
|
1393
|
+
Available: ${Object.keys(groupAlias).join(", ")}`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
function usage() {
|
|
1397
|
+
console.log(`
|
|
1398
|
+
oma \u2014 Open Managed Agents CLI
|
|
1399
|
+
|
|
1400
|
+
Usage:`);
|
|
1401
|
+
let lastGroup = "";
|
|
1402
|
+
for (const c of commands) {
|
|
1403
|
+
if (c.group !== lastGroup) {
|
|
1404
|
+
console.log(`
|
|
1405
|
+
${c.group}:`);
|
|
1406
|
+
lastGroup = c.group;
|
|
1407
|
+
}
|
|
1408
|
+
console.log(` ${c.usage.padEnd(42)} ${c.desc}`);
|
|
1409
|
+
}
|
|
1410
|
+
console.log(`
|
|
1411
|
+
API Reference:
|
|
1412
|
+
oma api Show all HTTP endpoints
|
|
1413
|
+
oma api <resource> Show endpoints for a resource
|
|
1414
|
+
|
|
1415
|
+
Environment:
|
|
1416
|
+
OMA_BASE_URL API base (default: http://localhost:8787)
|
|
1417
|
+
OMA_API_KEY API key (required)
|
|
1418
|
+
`);
|
|
1419
|
+
}
|
|
1420
|
+
async function main() {
|
|
1421
|
+
let args = process.argv.slice(2);
|
|
1422
|
+
if (!args.length || ["-h", "--help", "help"].includes(args[0])) {
|
|
1423
|
+
usage();
|
|
1424
|
+
process.exit(0);
|
|
1425
|
+
}
|
|
1426
|
+
if (args[0] === "api") {
|
|
1427
|
+
apiRef(args[1]);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const wantJson = args.includes("--json");
|
|
1431
|
+
args = args.filter((a) => a !== "--json");
|
|
1432
|
+
const config = loadConfig();
|
|
1433
|
+
config.json = wantJson;
|
|
1434
|
+
let needsArgMatch = null;
|
|
1435
|
+
for (const c of commands) {
|
|
1436
|
+
const verbMatch = c.match.length === 1 ? args[0] === c.match[0] : args[0] === c.match[0] && args[1] === c.match[1];
|
|
1437
|
+
if (!verbMatch) continue;
|
|
1438
|
+
if (c.needsArg && !args[c.match.length]) {
|
|
1439
|
+
needsArgMatch = c;
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
const rest = args.slice(c.match.length);
|
|
1443
|
+
return c.run(config, rest);
|
|
1444
|
+
}
|
|
1445
|
+
if (needsArgMatch) {
|
|
1446
|
+
console.error(`${needsArgMatch.usage}
|
|
1447
|
+
${needsArgMatch.desc}`);
|
|
1448
|
+
process.exit(1);
|
|
1449
|
+
}
|
|
1450
|
+
console.error(`Unknown command: ${args.join(" ")}`);
|
|
1451
|
+
usage();
|
|
1452
|
+
process.exit(1);
|
|
1453
|
+
}
|
|
1454
|
+
main().catch((err) => {
|
|
1455
|
+
console.error(err.message);
|
|
1456
|
+
process.exit(1);
|
|
1457
|
+
});
|