@oxygen-agent/cli 1.45.4

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 (73) hide show
  1. package/README.md +25 -0
  2. package/dist/browser-login.d.ts +8 -0
  3. package/dist/browser-login.d.ts.map +1 -0
  4. package/dist/browser-login.js +181 -0
  5. package/dist/browser-login.js.map +1 -0
  6. package/dist/credentials.d.ts +28 -0
  7. package/dist/credentials.d.ts.map +1 -0
  8. package/dist/credentials.js +470 -0
  9. package/dist/credentials.js.map +1 -0
  10. package/dist/http-client.d.ts +12 -0
  11. package/dist/http-client.d.ts.map +1 -0
  12. package/dist/http-client.js +218 -0
  13. package/dist/http-client.js.map +1 -0
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +3523 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/local-custom-http-column.d.ts +9 -0
  19. package/dist/local-custom-http-column.d.ts.map +1 -0
  20. package/dist/local-custom-http-column.js +571 -0
  21. package/dist/local-custom-http-column.js.map +1 -0
  22. package/dist/session.d.ts +57 -0
  23. package/dist/session.d.ts.map +1 -0
  24. package/dist/session.js +194 -0
  25. package/dist/session.js.map +1 -0
  26. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +136 -0
  27. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts.map +1 -0
  28. package/node_modules/@oxygen/recipe-sdk/dist/index.js +43 -0
  29. package/node_modules/@oxygen/recipe-sdk/dist/index.js.map +1 -0
  30. package/node_modules/@oxygen/recipe-sdk/package.json +15 -0
  31. package/node_modules/@oxygen/shared/dist/billing.d.ts +124 -0
  32. package/node_modules/@oxygen/shared/dist/billing.d.ts.map +1 -0
  33. package/node_modules/@oxygen/shared/dist/billing.js +296 -0
  34. package/node_modules/@oxygen/shared/dist/billing.js.map +1 -0
  35. package/node_modules/@oxygen/shared/dist/column-types.d.ts +55 -0
  36. package/node_modules/@oxygen/shared/dist/column-types.d.ts.map +1 -0
  37. package/node_modules/@oxygen/shared/dist/column-types.js +161 -0
  38. package/node_modules/@oxygen/shared/dist/column-types.js.map +1 -0
  39. package/node_modules/@oxygen/shared/dist/credit-guidance.d.ts +15 -0
  40. package/node_modules/@oxygen/shared/dist/credit-guidance.d.ts.map +1 -0
  41. package/node_modules/@oxygen/shared/dist/credit-guidance.js +60 -0
  42. package/node_modules/@oxygen/shared/dist/credit-guidance.js.map +1 -0
  43. package/node_modules/@oxygen/shared/dist/file-import.d.ts +22 -0
  44. package/node_modules/@oxygen/shared/dist/file-import.d.ts.map +1 -0
  45. package/node_modules/@oxygen/shared/dist/file-import.js +232 -0
  46. package/node_modules/@oxygen/shared/dist/file-import.js.map +1 -0
  47. package/node_modules/@oxygen/shared/dist/index.d.ts +45 -0
  48. package/node_modules/@oxygen/shared/dist/index.d.ts.map +1 -0
  49. package/node_modules/@oxygen/shared/dist/index.js +49 -0
  50. package/node_modules/@oxygen/shared/dist/index.js.map +1 -0
  51. package/node_modules/@oxygen/shared/dist/log.d.ts +22 -0
  52. package/node_modules/@oxygen/shared/dist/log.d.ts.map +1 -0
  53. package/node_modules/@oxygen/shared/dist/log.js +58 -0
  54. package/node_modules/@oxygen/shared/dist/log.js.map +1 -0
  55. package/node_modules/@oxygen/shared/dist/redaction.d.ts +4 -0
  56. package/node_modules/@oxygen/shared/dist/redaction.d.ts.map +1 -0
  57. package/node_modules/@oxygen/shared/dist/redaction.js +106 -0
  58. package/node_modules/@oxygen/shared/dist/redaction.js.map +1 -0
  59. package/node_modules/@oxygen/shared/dist/telemetry.d.ts +9 -0
  60. package/node_modules/@oxygen/shared/dist/telemetry.d.ts.map +1 -0
  61. package/node_modules/@oxygen/shared/dist/telemetry.js +98 -0
  62. package/node_modules/@oxygen/shared/dist/telemetry.js.map +1 -0
  63. package/node_modules/@oxygen/shared/dist/version.d.ts +2 -0
  64. package/node_modules/@oxygen/shared/dist/version.d.ts.map +1 -0
  65. package/node_modules/@oxygen/shared/dist/version.js +2 -0
  66. package/node_modules/@oxygen/shared/dist/version.js.map +1 -0
  67. package/node_modules/@oxygen/shared/package.json +21 -0
  68. package/node_modules/@oxygen/workflows/dist/index.d.ts +680 -0
  69. package/node_modules/@oxygen/workflows/dist/index.d.ts.map +1 -0
  70. package/node_modules/@oxygen/workflows/dist/index.js +983 -0
  71. package/node_modules/@oxygen/workflows/dist/index.js.map +1 -0
  72. package/node_modules/@oxygen/workflows/package.json +15 -0
  73. package/package.json +44 -0
package/dist/index.js ADDED
@@ -0,0 +1,3523 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync, spawnSync } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { basename, dirname, extname, resolve } from "node:path";
7
+ import { createInterface } from "node:readline/promises";
8
+ import { stdin as input, stdout as output } from "node:process";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { Command } from "commander";
11
+ import { OXYGEN_VERSION, OxygenError, success, toFailure } from "@oxygen/shared";
12
+ import { inferRowsFileFormat, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
13
+ import { assertRecipeBundleSafe, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowManifest, } from "@oxygen/workflows";
14
+ import { isRecipeDefinition } from "@oxygen/recipe-sdk";
15
+ import { createBrowserLoginSession, openBrowser } from "./browser-login.js";
16
+ import { clearCredentials, defaultApiUrl, listCredentialProfiles, normalizeApiUrl, saveCredentials, switchCredentialProfile, } from "./credentials.js";
17
+ import { requestOxygen } from "./http-client.js";
18
+ import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
19
+ import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
20
+ const BROWSER_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
21
+ const OXYGEN_SPINNER_INTERVAL_MS = 90;
22
+ const OXYGEN_SPINNER_FRAMES = [
23
+ "[Oxygen ]",
24
+ "[ Oxygen ]",
25
+ "[ Oxygen]",
26
+ "[ nOxyge ]",
27
+ "[ enOxyg ]",
28
+ "[ genOxy ]",
29
+ "[ ygenOx ]",
30
+ "[ xygenO ]",
31
+ ];
32
+ const OXYGEN_WORDMARK = [
33
+ " ___ __ __ __ __ ___ _____ _ _",
34
+ " / _ \\ \\ \\/ / \\ \\ / / / __|| ____| \\ | |",
35
+ " | | | | \\ / \\ V / | | _| _| | \\| |",
36
+ " | |_| | / \\ | | | |_| | |___| |\\ |",
37
+ " \\___/ /_/\\_\\ |_| \\____|_____|_| \\_|",
38
+ ].join("\n");
39
+ const LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD = 500;
40
+ const TABLE_ACTION_RUN_WAIT_DEFAULT_TIMEOUT_SECONDS = 600;
41
+ const TABLE_ACTION_RUN_WAIT_DEFAULT_INTERVAL_SECONDS = 5;
42
+ const TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS = 600;
43
+ const TABLE_INGESTION_WAIT_DEFAULT_INTERVAL_SECONDS = 5;
44
+ const WORKFLOW_TAIL_DEFAULT_TIMEOUT_SECONDS = 600;
45
+ const WORKFLOW_TAIL_DEFAULT_INTERVAL_SECONDS = 2;
46
+ const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
47
+ const CLI_MODULE_DIR = dirname(fileURLToPath(import.meta.url));
48
+ const RECIPE_ESBUILD_NODE_PATHS = [
49
+ resolve("node_modules"),
50
+ resolve("../../node_modules"),
51
+ resolve(CLI_MODULE_DIR, "../node_modules"),
52
+ resolve(CLI_MODULE_DIR, "../../../node_modules"),
53
+ ];
54
+ function writeJson(value) {
55
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
56
+ }
57
+ function emitSuccess(command, data, options) {
58
+ if (options.json) {
59
+ writeJson(success(command, data));
60
+ return;
61
+ }
62
+ writeJson(data);
63
+ }
64
+ async function handleAsyncAction(command, options, action) {
65
+ try {
66
+ const data = await action();
67
+ emitSuccess(command, data, options);
68
+ }
69
+ catch (error) {
70
+ const failure = toFailure(command, error);
71
+ writeJson(failure);
72
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
73
+ }
74
+ }
75
+ function parseJsonObject(value) {
76
+ let parsed;
77
+ try {
78
+ parsed = JSON.parse(value);
79
+ }
80
+ catch (error) {
81
+ throw new OxygenError("invalid_json", "Input must be valid JSON.", {
82
+ details: { reason: error instanceof Error ? error.message : "unknown" },
83
+ exitCode: 1,
84
+ });
85
+ }
86
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
87
+ throw new OxygenError("invalid_json", "Input JSON must be an object.", {
88
+ exitCode: 1,
89
+ });
90
+ }
91
+ return parsed;
92
+ }
93
+ function parseJsonArray(value) {
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(value);
97
+ }
98
+ catch (error) {
99
+ throw new OxygenError("invalid_json", "Input must be valid JSON.", {
100
+ details: { reason: error instanceof Error ? error.message : "unknown" },
101
+ exitCode: 1,
102
+ });
103
+ }
104
+ if (!Array.isArray(parsed)) {
105
+ throw new OxygenError("invalid_json", "Input JSON must be an array.", {
106
+ exitCode: 1,
107
+ });
108
+ }
109
+ return parsed;
110
+ }
111
+ function readFileIfPresent(value) {
112
+ const candidate = resolve(value);
113
+ try {
114
+ return readFileSync(candidate, "utf8");
115
+ }
116
+ catch (error) {
117
+ const code = error && typeof error === "object" && "code" in error
118
+ ? String(error.code)
119
+ : "";
120
+ if (code === "ENOENT" || code === "ENAMETOOLONG" || code === "ENOTDIR")
121
+ return value;
122
+ throw error;
123
+ }
124
+ }
125
+ function resolveComposioRunMode(options) {
126
+ if (options.live === true && options.dryRun === true) {
127
+ throw new OxygenError("conflicting_flags", "Pass either --live or --dry-run, not both.", { exitCode: 1 });
128
+ }
129
+ if (options.live === true)
130
+ return "live";
131
+ if (options.dryRun === true)
132
+ return "dry_run";
133
+ const raw = readOption(options.mode);
134
+ if (raw) {
135
+ const normalized = raw.trim().toLowerCase().replace("-", "_");
136
+ if (normalized === "live" || normalized === "dry_run")
137
+ return normalized;
138
+ throw new OxygenError("invalid_mode", "--mode must be 'live' or 'dry_run'.", { details: { mode: raw }, exitCode: 1 });
139
+ }
140
+ return "dry_run";
141
+ }
142
+ function readSpecFileBody(path) {
143
+ const text = readFileSync(resolve(path), "utf8");
144
+ try {
145
+ return { spec: parseJsonObject(text) };
146
+ }
147
+ catch (error) {
148
+ if (error instanceof OxygenError && error.code === "invalid_json") {
149
+ return { spec_prompt: text };
150
+ }
151
+ throw error;
152
+ }
153
+ }
154
+ export function createProgram() {
155
+ const program = new Command();
156
+ program
157
+ .name("oxygen")
158
+ .description("CLI/API-first GTM platform for GTM tool and workflow primitives.")
159
+ .version(OXYGEN_VERSION)
160
+ .option("--profile <name>", "Use a stored CLI profile for this command.");
161
+ program.hook("preAction", () => {
162
+ const options = program.opts();
163
+ if (options.profile)
164
+ process.env.OXYGEN_PROFILE = options.profile;
165
+ });
166
+ program
167
+ .command("login")
168
+ .description("Connect this terminal to Oxygen.")
169
+ .option("--token <token>", "CLI API token created in the Oxygen dashboard.")
170
+ .option("--api-url <url>", "Oxygen API URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
171
+ .option("--profile <name>", "Store credentials under a named CLI profile and make it active.")
172
+ .option("--no-browser", "Skip the browser handoff and paste the token manually.")
173
+ .option("--json", "Print a JSON envelope.")
174
+ .action(async (options) => {
175
+ await handleLoginAction(options);
176
+ });
177
+ program
178
+ .command("auth")
179
+ .description("Authentication helpers.")
180
+ .addCommand(new Command("use-token")
181
+ .description("Store a CLI API token non-interactively.")
182
+ .requiredOption("--token <token>", "CLI API token.")
183
+ .option("--api-url <url>", "Oxygen API URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
184
+ .option("--profile <name>", "Store credentials under a named CLI profile and make it active.")
185
+ .option("--json", "Print a JSON envelope.")
186
+ .action(async (options) => {
187
+ await handleAuthUseTokenAction(options);
188
+ }));
189
+ program
190
+ .command("profiles")
191
+ .description("Manage stored CLI credential profiles.")
192
+ .addCommand(new Command("list")
193
+ .description("List stored CLI credential profiles.")
194
+ .option("--json", "Print a JSON envelope.")
195
+ .action(async (options) => {
196
+ await handleProfilesListAction(options);
197
+ }))
198
+ .addCommand(new Command("use")
199
+ .description("Switch the active CLI credential profile.")
200
+ .argument("<profile>", "Stored profile name.")
201
+ .option("--json", "Print a JSON envelope.")
202
+ .action(async (profile, options) => {
203
+ await handleProfilesUseAction(profile, options);
204
+ }))
205
+ .addCommand(new Command("remove")
206
+ .description("Remove one stored CLI credential profile.")
207
+ .argument("<profile>", "Stored profile name.")
208
+ .option("--json", "Print a JSON envelope.")
209
+ .action(async (profile, options) => {
210
+ await handleLogoutAction({ ...options, profile });
211
+ }));
212
+ program
213
+ .command("logout")
214
+ .description("Remove the active stored CLI API token.")
215
+ .option("--profile <name>", "Remove a named CLI profile instead of the active profile.")
216
+ .option("--all", "Remove all stored CLI profiles.")
217
+ .option("--json", "Print a JSON envelope.")
218
+ .action(async (options) => {
219
+ await handleLogoutAction(options);
220
+ });
221
+ program
222
+ .command("update")
223
+ .description("Update the Oxygen CLI from npm.")
224
+ .option("--package <npm_spec>", "Override the npm package spec.")
225
+ .option("--dry-run", "Print the update command without running it.")
226
+ .option("--json", "Print a JSON envelope.")
227
+ .action(async (options) => {
228
+ await handleUpdateAction(options);
229
+ });
230
+ program
231
+ .command("whoami")
232
+ .description("Show the current Oxygen CLI identity.")
233
+ .option("--json", "Print a JSON envelope.")
234
+ .action(async (options) => {
235
+ await handleAsyncAction("whoami", options, async () => requestOxygen("/api/cli/whoami"));
236
+ });
237
+ program
238
+ .command("status")
239
+ .description("Compare the local Oxygen CLI version against what's deployed in prod.")
240
+ .option("--json", "Print a JSON envelope.")
241
+ .action(async (options) => {
242
+ await handleAsyncAction("status", options, async () => {
243
+ const server = await requestOxygen("/api/health", { requireAuth: false });
244
+ return {
245
+ client_version: OXYGEN_VERSION,
246
+ server_version: server.server_version,
247
+ sha: server.sha,
248
+ region: server.region,
249
+ in_sync: server.server_version === OXYGEN_VERSION,
250
+ };
251
+ });
252
+ });
253
+ program
254
+ .command("orgs")
255
+ .description("Organization selection commands.")
256
+ .addCommand(new Command("list")
257
+ .description("List organizations available to the current CLI identity.")
258
+ .option("--json", "Print a JSON envelope.")
259
+ .action(async (options) => {
260
+ await handleAsyncAction("orgs list", options, async () => requestOxygen("/api/cli/orgs"));
261
+ }))
262
+ .addCommand(new Command("select")
263
+ .description("Select the active organization for an OAuth-backed CLI session.")
264
+ .argument("<organization>", "Organization id, Clerk org id, or slug returned by orgs list.")
265
+ .option("--json", "Print a JSON envelope.")
266
+ .action(async (organization, options) => {
267
+ await handleAsyncAction("orgs select", options, async () => requestOxygen("/api/cli/orgs/select", {
268
+ method: "POST",
269
+ body: { organization },
270
+ }));
271
+ }));
272
+ program
273
+ .command("db")
274
+ .description("Tenant database commands.")
275
+ .addCommand(new Command("status")
276
+ .description("Show the current organization's tenant database status.")
277
+ .option("--json", "Print a JSON envelope.")
278
+ .action(async (options) => {
279
+ await handleAsyncAction("db status", options, async () => requestOxygen("/api/cli/db/status"));
280
+ }))
281
+ .addCommand(new Command("provision")
282
+ .description("Provision a managed Neon tenant database for the current organization.")
283
+ .option("--json", "Print a JSON envelope.")
284
+ .action(async (options) => {
285
+ await handleAsyncAction("db provision", options, async () => requestOxygen("/api/cli/db/provision", { method: "POST", body: {} }));
286
+ }))
287
+ .addCommand(new Command("migrate")
288
+ .description("Apply pending tenant database migrations for the current organization.")
289
+ .option("--json", "Print a JSON envelope.")
290
+ .action(async (options) => {
291
+ await handleAsyncAction("db migrate", options, async () => requestOxygen("/api/cli/db/migrate", { method: "POST", body: {} }));
292
+ }))
293
+ .addCommand(new Command("migrate-all")
294
+ .description("Apply pending tenant database migrations across all ready tenants (staff only).")
295
+ .option("--limit <n>", "Maximum tenants to migrate in this batch. Defaults to 50; hard cap is 500.")
296
+ .option("--dry-run", "List pending tenants without applying migrations.")
297
+ .option("--json", "Print a JSON envelope.")
298
+ .action(async (options) => {
299
+ await handleAsyncAction("db migrate-all", options, async () => {
300
+ const limit = readPositiveInt(options.limit);
301
+ return requestOxygen("/api/cli/db/migrate-all", {
302
+ method: "POST",
303
+ body: {
304
+ ...(limit !== undefined ? { limit } : {}),
305
+ ...(options.dryRun ? { dry_run: true } : {}),
306
+ },
307
+ });
308
+ });
309
+ }))
310
+ .addCommand(new Command("cost-policy")
311
+ .description("Show tenant database cost controls and reconciliation status.")
312
+ .option("--json", "Print a JSON envelope.")
313
+ .action(async (options) => {
314
+ await handleAsyncAction("db cost-policy", options, async () => requestOxygen("/api/cli/db/cost-policy"));
315
+ }))
316
+ .addCommand(new Command("reconcile-cost-policy")
317
+ .description("Apply tenant database cost controls to the managed Neon endpoint.")
318
+ .option("--suspend-idle", "Request immediate Neon compute suspension when the tenant is idle.")
319
+ .option("--json", "Print a JSON envelope.")
320
+ .action(async (options) => {
321
+ await handleAsyncAction("db reconcile-cost-policy", options, async () => requestOxygen("/api/cli/db/reconcile-cost-policy", {
322
+ method: "POST",
323
+ body: {
324
+ ...(options.suspendIdle ? { suspend_idle: true } : {}),
325
+ },
326
+ }));
327
+ }))
328
+ .addCommand(new Command("attach")
329
+ .description("Attach an existing Postgres database as the current organization's tenant database.")
330
+ .requiredOption("--database-url <url>", "Owner connection string for the tenant Postgres database.")
331
+ .option("--json", "Print a JSON envelope.")
332
+ .action(async (options) => {
333
+ await handleAsyncAction("db attach", options, async () => requestOxygen("/api/cli/db/attach", {
334
+ method: "POST",
335
+ body: { database_url: options.databaseUrl },
336
+ }));
337
+ }))
338
+ .addCommand(new Command("query")
339
+ .description("Run one read-only SQL statement against the current tenant database.")
340
+ .requiredOption("--sql <sql>", "Single SELECT, WITH, or EXPLAIN statement.")
341
+ .option("--max-rows <n>", "Maximum rows to return. Defaults to 100; hard cap is 1000.")
342
+ .option("--json", "Print a JSON envelope.")
343
+ .action(async (options) => {
344
+ await handleAsyncAction("db query", options, async () => {
345
+ const maxRows = readPositiveInt(options.maxRows);
346
+ return requestOxygen("/api/cli/db/query", {
347
+ method: "POST",
348
+ body: {
349
+ sql: options.sql,
350
+ ...(maxRows ? { max_rows: maxRows } : {}),
351
+ },
352
+ });
353
+ });
354
+ }));
355
+ program
356
+ .command("projects")
357
+ .description("Tenant project commands.")
358
+ .addCommand(new Command("list")
359
+ .description("List table projects in the current tenant database.")
360
+ .option("--json", "Print a JSON envelope.")
361
+ .action(async (options) => {
362
+ await handleAsyncAction("projects list", options, async () => requestOxygen("/api/cli/projects"));
363
+ }))
364
+ .addCommand(new Command("create")
365
+ .description("Create a schema-backed table project.")
366
+ .argument("<name>", "Display name for the project.")
367
+ .option("--json", "Print a JSON envelope.")
368
+ .action(async (name, options) => {
369
+ await handleAsyncAction("projects create", options, async () => requestOxygen("/api/cli/projects", {
370
+ method: "POST",
371
+ body: { name },
372
+ }));
373
+ }));
374
+ const tablesCommand = program
375
+ .command("tables")
376
+ .description("Tenant workspace table commands.")
377
+ .addCommand(new Command("create")
378
+ .description("Create a real Postgres-backed workspace table.")
379
+ .argument("<name>", "Display name for the table.")
380
+ .requiredOption("--columns-json <json>", "JSON array of column definitions.")
381
+ .option("--project <project>", "Project id or slug. Defaults to General.")
382
+ .option("--json", "Print a JSON envelope.")
383
+ .action(async (name, options) => {
384
+ await handleAsyncAction("tables create", options, async () => requestOxygen("/api/cli/tables", {
385
+ method: "POST",
386
+ body: {
387
+ name,
388
+ columns: parseJsonArray(options.columnsJson),
389
+ ...(readOption(options.project) ? { project: readOption(options.project) } : {}),
390
+ },
391
+ }));
392
+ }))
393
+ .addCommand(new Command("duplicate")
394
+ .description("Duplicate a workspace table. Copies row values unless --schema-only is set.")
395
+ .argument("<table>", "Source table id or slug.")
396
+ .option("--name <name>", "Display name for the duplicated table. Defaults to '<source> Copy'.")
397
+ .option("--schema-only", "Duplicate columns and definitions without copying row values.")
398
+ .option("--json", "Print a JSON envelope.")
399
+ .action(async (table, options) => {
400
+ await handleAsyncAction("tables duplicate", options, async () => requestOxygen("/api/cli/tables/duplicate", {
401
+ method: "POST",
402
+ body: {
403
+ table,
404
+ ...(readOption(options.name) ? { name: readOption(options.name) } : {}),
405
+ ...(options.schemaOnly ? { include_rows: false } : {}),
406
+ },
407
+ }));
408
+ }))
409
+ .addCommand(new Command("insert")
410
+ .description("Insert rows into a workspace table.")
411
+ .argument("<table>", "Table id or slug.")
412
+ .requiredOption("--rows-json <json>", "JSON array of row objects keyed by column key.")
413
+ .option("--json", "Print a JSON envelope.")
414
+ .action(async (table, options) => {
415
+ await handleAsyncAction("tables insert", options, async () => requestOxygen("/api/cli/tables/rows", {
416
+ method: "POST",
417
+ body: {
418
+ table,
419
+ rows: parseJsonArray(options.rowsJson),
420
+ },
421
+ }));
422
+ }))
423
+ .addCommand(new Command("update")
424
+ .description("Update one workspace table row.")
425
+ .argument("<table>", "Table id or slug.")
426
+ .argument("<row_id>", "Workspace row UUID.")
427
+ .requiredOption("--values-json <json>", "JSON object of values keyed by column key.")
428
+ .option("--json", "Print a JSON envelope.")
429
+ .action(async (table, rowId, options) => {
430
+ await handleAsyncAction("tables update", options, async () => requestOxygen("/api/cli/tables/rows/update", {
431
+ method: "POST",
432
+ body: {
433
+ table,
434
+ row_id: rowId,
435
+ values: parseJsonObject(options.valuesJson),
436
+ },
437
+ }));
438
+ }))
439
+ .addCommand(new Command("delete-row")
440
+ .description("Delete one workspace table row.")
441
+ .argument("<table>", "Table id or slug.")
442
+ .argument("<row_id>", "Workspace row UUID.")
443
+ .option("--json", "Print a JSON envelope.")
444
+ .action(async (table, rowId, options) => {
445
+ await handleAsyncAction("tables delete-row", options, async () => requestOxygen("/api/cli/tables/rows/delete", {
446
+ method: "POST",
447
+ body: {
448
+ table,
449
+ row_id: rowId,
450
+ },
451
+ }));
452
+ }))
453
+ .addCommand(new Command("upsert")
454
+ .description("Insert or update rows in a workspace table by a column key.")
455
+ .argument("<table>", "Table id or slug.")
456
+ .requiredOption("--key <key>", "Column key used to match existing rows.")
457
+ .requiredOption("--rows-json <json>", "JSON array of row objects keyed by column key.")
458
+ .option("--return <mode>", "Response shape: summary, ids, or rows. Defaults to summary.")
459
+ .option("--dry-run", "Preview inserts, updates, duplicate keys, and field conflicts without writing rows.")
460
+ .option("--json", "Print a JSON envelope.")
461
+ .action(async (table, options) => {
462
+ await handleAsyncAction("tables upsert", options, async () => requestOxygen("/api/cli/tables/rows/upsert", {
463
+ method: "POST",
464
+ body: {
465
+ table,
466
+ key: options.key,
467
+ rows: parseJsonArray(options.rowsJson),
468
+ ...(readOption(options["return"]) ? { return: readOption(options["return"]) } : {}),
469
+ ...(options.dryRun ? { dry_run: true } : {}),
470
+ },
471
+ }));
472
+ }))
473
+ .addCommand(new Command("import")
474
+ .description("Import JSON, JSONL, CSV, or XLSX rows into a workspace table.")
475
+ .argument("[table]", "Table id or slug. Omit when using --create.")
476
+ .requiredOption("--file <path>", "Input file path.")
477
+ .option("--format <format>", "json, jsonl, csv, or xlsx. Defaults from file extension.")
478
+ .option("--sheet <name>", "Worksheet name for XLSX imports. Defaults to the first sheet.")
479
+ .option("--create <name>", "Create a new table with columns inferred from the file before importing.")
480
+ .option("--project <project>", "Project id or slug for --create. Defaults to General.")
481
+ .option("--upsert-key <key>", "Column key used to upsert instead of inserting.")
482
+ .option("--batch-size <n>", "Rows per API request. Defaults to 500; paid orgs may use up to 5000.")
483
+ .option("--background", "Enqueue durable import chunks for the background worker.")
484
+ .option("--sync", `Force foreground import even for files above ${LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD} rows.`)
485
+ .option("--max-concurrency <n>", "Maximum concurrent import chunks for background mode. Defaults to 5.")
486
+ .option("--json", "Print a JSON envelope.")
487
+ .action(async (table, options) => {
488
+ await handleAsyncAction("tables import", options, async () => importRows(table, options));
489
+ }))
490
+ .addCommand(new Command("export")
491
+ .description("Export workspace table rows as JSON, JSONL, or CSV.")
492
+ .argument("<table>", "Table id or slug.")
493
+ .option("--format <format>", "json, jsonl, or csv. Defaults to json.")
494
+ .option("--output <path>", "Write export content to a file.")
495
+ .option("--limit <n>", "Maximum rows to export. Defaults to 100; hard cap is 1000.")
496
+ .option("--json", "Print a JSON envelope.")
497
+ .action(async (table, options) => {
498
+ await handleAsyncAction("tables export", options, async () => exportRows(table, options));
499
+ }))
500
+ .addCommand(new Command("list")
501
+ .description("List workspace tables in the current tenant database.")
502
+ .option("--project <project>", "Project id or slug to filter by.")
503
+ .option("--json", "Print a JSON envelope.")
504
+ .action(async (options) => {
505
+ await handleAsyncAction("tables list", options, async () => requestOxygen(readOption(options.project)
506
+ ? `/api/cli/tables?project=${encodeURIComponent(readOption(options.project))}`
507
+ : "/api/cli/tables"));
508
+ }))
509
+ .addCommand(new Command("query")
510
+ .description("Query a workspace table by id or slug.")
511
+ .argument("<table>", "Table id or slug.")
512
+ .option("--limit <n>", "Maximum rows to return. Defaults to 100; hard cap is 1000.")
513
+ .option("--cursor <cursor>", "Pagination cursor returned by a previous query.")
514
+ .option("--fields <columns>", "Comma-separated column keys or ids to include.")
515
+ .option("--filter-json <json>", "Filter object or array, e.g. '{\"column\":\"mobile_phone_e164\",\"op\":\"is_null\"}'.")
516
+ .option("--json", "Print a JSON envelope.")
517
+ .action(async (table, options) => {
518
+ await handleAsyncAction("tables query", options, async () => {
519
+ const limit = readPositiveInt(options.limit);
520
+ const filters = readFilterJsonOption(options.filterJson);
521
+ return requestOxygen("/api/cli/tables/query", {
522
+ method: "POST",
523
+ body: {
524
+ table,
525
+ ...(limit ? { limit } : {}),
526
+ ...(readOption(options.cursor) ? { cursor: readOption(options.cursor) } : {}),
527
+ ...(readOption(options.fields) ? { fields: readCsvOption(options.fields) } : {}),
528
+ ...(filters ? { filters } : {}),
529
+ },
530
+ });
531
+ });
532
+ }))
533
+ .addCommand(new Command("preview")
534
+ .description("Preview a workspace table with projected rows and summary stats.")
535
+ .argument("<table>", "Table id or slug.")
536
+ .option("--limit <n>", "Maximum preview rows to return. Defaults to 25; hard cap is 1000.")
537
+ .option("--fields <columns>", "Comma-separated column keys or ids to include.")
538
+ .option("--include-cell-states", "Include per-cell run state in the preview response.")
539
+ .option("--summary-only", "Return table metadata and stats without row JSON.")
540
+ .option("--json", "Print a JSON envelope.")
541
+ .action(async (table, options) => {
542
+ await handleAsyncAction("tables preview", options, async () => {
543
+ const limit = readPositiveInt(options.limit);
544
+ return requestOxygen("/api/cli/tables/preview", {
545
+ method: "POST",
546
+ body: {
547
+ table,
548
+ ...(limit ? { limit } : {}),
549
+ ...(readOption(options.fields) ? { fields: readCsvOption(options.fields) } : {}),
550
+ ...(options.includeCellStates ? { include_cell_states: true } : {}),
551
+ ...(options.summaryOnly ? { summary_only: true } : {}),
552
+ },
553
+ });
554
+ });
555
+ }))
556
+ .addCommand(new Command("describe")
557
+ .description("Describe a workspace table and its columns.")
558
+ .argument("<table>", "Table id or slug.")
559
+ .option("--include-archived", "Include archived columns.")
560
+ .option("--json", "Print a JSON envelope.")
561
+ .action(async (table, options) => {
562
+ await handleAsyncAction("tables describe", options, async () => requestOxygen("/api/cli/tables/describe", {
563
+ method: "POST",
564
+ body: {
565
+ table,
566
+ ...(options.includeArchived ? { include_archived: true } : {}),
567
+ },
568
+ }));
569
+ }))
570
+ .addCommand(new Command("rename")
571
+ .description("Rename a workspace table display name and slug.")
572
+ .argument("<table>", "Table id or slug.")
573
+ .requiredOption("--name <name>", "New table display name.")
574
+ .option("--json", "Print a JSON envelope.")
575
+ .action(async (table, options) => {
576
+ await handleAsyncAction("tables rename", options, async () => requestOxygen("/api/cli/tables/rename", {
577
+ method: "POST",
578
+ body: { table, name: options.name },
579
+ }));
580
+ }))
581
+ .addCommand(new Command("archive")
582
+ .description("Archive a workspace table without dropping physical data.")
583
+ .argument("<table>", "Table id or slug.")
584
+ .option("--json", "Print a JSON envelope.")
585
+ .action(async (table, options) => {
586
+ await handleAsyncAction("tables archive", options, async () => requestOxygen("/api/cli/tables/archive", {
587
+ method: "POST",
588
+ body: { table },
589
+ }));
590
+ }));
591
+ tablesCommand.addCommand(new Command("webhook")
592
+ .description("Create and manage direct table webhooks.")
593
+ .addCommand(new Command("create")
594
+ .description("Create a direct webhook endpoint that writes inbound JSON into a table.")
595
+ .argument("<table>", "Table id or slug.")
596
+ .option("--name <name>", "Display name for the webhook.")
597
+ .option("--mode <mode>", "insert or upsert. Defaults to upsert when --upsert-key is set, otherwise insert.")
598
+ .option("--upsert-key <key>", "Column key used to upsert rows. Required for upsert mode.")
599
+ .option("--items-path <path>", "Dot path to an array of row objects in the webhook payload.")
600
+ .option("--event-id-path <path>", "Dot path to the event id. Defaults to event_id, eventId, id, or payload hash.")
601
+ .option("--event-type-path <path>", "Dot path to the event type. Defaults to type or event.")
602
+ .option("--occurred-at-path <path>", "Dot path to the event timestamp.")
603
+ .option("--json", "Print a JSON envelope.")
604
+ .action(async (table, options) => {
605
+ await handleAsyncAction("tables webhook create", options, async () => requestOxygen("/api/cli/tables/webhooks", {
606
+ method: "POST",
607
+ body: {
608
+ table,
609
+ ...(readOption(options.name) ? { name: readOption(options.name) } : {}),
610
+ ...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
611
+ ...(readOption(options.upsertKey) ? { upsert_key: readOption(options.upsertKey) } : {}),
612
+ ...(readOption(options.itemsPath) ? { items_path: readOption(options.itemsPath) } : {}),
613
+ ...(readOption(options.eventIdPath) ? { event_id_path: readOption(options.eventIdPath) } : {}),
614
+ ...(readOption(options.eventTypePath) ? { event_type_path: readOption(options.eventTypePath) } : {}),
615
+ ...(readOption(options.occurredAtPath) ? { occurred_at_path: readOption(options.occurredAtPath) } : {}),
616
+ },
617
+ }));
618
+ })));
619
+ program
620
+ .command("context")
621
+ .description("Workspace-level GTM context commands.")
622
+ .addCommand(new Command("profile")
623
+ .description("Company profile and ICP memory.")
624
+ .addCommand(new Command("get")
625
+ .description("Read the current workspace company profile.")
626
+ .option("--json", "Print a JSON envelope.")
627
+ .action(async (options) => {
628
+ await handleAsyncAction("context profile get", options, async () => requestOxygen("/api/cli/context/profile"));
629
+ }))
630
+ .addCommand(new Command("update")
631
+ .description("Merge one or more profile sections into workspace GTM memory.")
632
+ .requiredOption("--data-json <json>", "JSON object with company, offering, icp, market, gtm_stack, or custom sections.")
633
+ .option("--summary <text>", "Optional concise summary for the profile.")
634
+ .option("--json", "Print a JSON envelope.")
635
+ .action(async (options) => {
636
+ await handleAsyncAction("context profile update", options, async () => requestOxygen("/api/cli/context/profile/update", {
637
+ method: "POST",
638
+ body: {
639
+ data: parseJsonObject(options.dataJson ?? "{}"),
640
+ ...(readOption(options.summary) ? { summary: readOption(options.summary) } : {}),
641
+ },
642
+ }));
643
+ })))
644
+ .addCommand(new Command("assets")
645
+ .description("Playbooks, strategies, campaigns, personas, and research notes.")
646
+ .addCommand(new Command("list")
647
+ .description("List workspace context assets.")
648
+ .option("--type <type>", "Filter by playbook, strategy, campaign, positioning, persona, competitor, research_note, or other.")
649
+ .option("--status <status>", "Filter by draft, active, or archived.")
650
+ .option("--tags <csv>", "Comma-separated tags that must be present.")
651
+ .option("--include-archived", "Include archived assets when no status filter is set.")
652
+ .option("--json", "Print a JSON envelope.")
653
+ .action(async (options) => {
654
+ await handleAsyncAction("context assets list", options, async () => requestOxygen(`/api/cli/context/assets${contextAssetsQuery(options)}`));
655
+ }))
656
+ .addCommand(new Command("get")
657
+ .description("Read one context asset.")
658
+ .argument("<asset_id>", "Context asset UUID.")
659
+ .option("--json", "Print a JSON envelope.")
660
+ .action(async (assetId, options) => {
661
+ await handleAsyncAction("context asset get", options, async () => requestOxygen("/api/cli/context/assets/get", {
662
+ method: "POST",
663
+ body: { id: assetId },
664
+ }));
665
+ }))
666
+ .addCommand(new Command("upsert")
667
+ .description("Create or update a context asset.")
668
+ .option("--id <asset_id>", "Existing asset UUID to update. Omit to create.")
669
+ .option("--type <type>", "Asset type. Defaults to other on create.")
670
+ .option("--title <title>", "Asset title. Required on create.")
671
+ .option("--status <status>", "draft, active, or archived.")
672
+ .option("--tags <csv>", "Comma-separated asset tags.")
673
+ .option("--summary <text>", "Short asset summary.")
674
+ .option("--body <text>", "Asset body text.")
675
+ .option("--data-json <json>", "Flexible JSON object for structured asset data.")
676
+ .option("--asset-json <json>", "Full asset JSON object. CLI flags override matching fields.")
677
+ .option("--json", "Print a JSON envelope.")
678
+ .action(async (options) => {
679
+ await handleAsyncAction("context asset upsert", options, async () => requestOxygen("/api/cli/context/assets/upsert", {
680
+ method: "POST",
681
+ body: buildContextAssetUpsertBody(options),
682
+ }));
683
+ }))
684
+ .addCommand(new Command("archive")
685
+ .description("Archive a context asset.")
686
+ .argument("<asset_id>", "Context asset UUID.")
687
+ .option("--json", "Print a JSON envelope.")
688
+ .action(async (assetId, options) => {
689
+ await handleAsyncAction("context asset archive", options, async () => requestOxygen("/api/cli/context/assets/archive", {
690
+ method: "POST",
691
+ body: { id: assetId },
692
+ }));
693
+ })));
694
+ program
695
+ .command("columns")
696
+ .description("Workspace table column commands.")
697
+ .addCommand(new Command("add")
698
+ .description("Add a nullable column to a workspace table.")
699
+ .argument("<table>", "Table id or slug.")
700
+ .requiredOption("--label <label>", "Display label for the new column.")
701
+ .option("--key <key>", "Optional stable column key. Defaults to a normalized label.")
702
+ .option("--data-type <type>", "Column data type: text, numeric, boolean, jsonb, or timestamptz.")
703
+ .option("--kind <kind>", "Column kind. Defaults to manual.")
704
+ .option("--semantic-type <type>", "Optional semantic type such as company_domain.")
705
+ .option("--definition-json <json>", "Optional JSON object with column definition metadata.")
706
+ .option("--json", "Print a JSON envelope.")
707
+ .action(async (table, options) => {
708
+ const column = { label: options.label };
709
+ if (options.key)
710
+ column.key = options.key;
711
+ if (options.dataType)
712
+ column.data_type = options.dataType;
713
+ if (options.kind)
714
+ column.kind = options.kind;
715
+ if (options.semanticType)
716
+ column.semantic_type = options.semanticType;
717
+ if (options.definitionJson)
718
+ column.definition = parseJsonObject(options.definitionJson);
719
+ await handleAsyncAction("columns add", options, async () => requestOxygen("/api/cli/tables/columns", {
720
+ method: "POST",
721
+ body: {
722
+ table,
723
+ column,
724
+ },
725
+ }));
726
+ }))
727
+ .addCommand(new Command("run")
728
+ .description("Run an executable AI, tool, formula, or local custom HTTP column for one row or a bounded batch.")
729
+ .argument("<table>", "Table id or slug.")
730
+ .argument("<column>", "Column id or key.")
731
+ .option("--row-id <row_id>", "Workspace row id to run.")
732
+ .option("--limit <n>", "Maximum rows to inspect when row-id is omitted. Defaults to 10; sync hard cap is 25.")
733
+ .option("--all", "Run all rows. Requires --background.")
734
+ .option("--filter-json <json>", "Row selector filter object or array for background runs. Do not combine with --all, --limit, or --row-id.")
735
+ .option("--force", "Run even when the target cell already has a value.")
736
+ .option("--connection-id <connection_id>", "Optional provider integration connection id.")
737
+ .option("--background", "Create a durable background table action run instead of executing synchronously.")
738
+ .option("--max-credits <n>", "Maximum managed/provider credits to reserve for a background run.")
739
+ .option("--local", "Run a custom HTTP column in this CLI process so env-var secrets stay local.")
740
+ .option("--local-concurrency <n>", "Maximum concurrent custom HTTP requests for --local. Defaults to 3.")
741
+ .option("--json", "Print a JSON envelope.")
742
+ .action(async (table, column, options) => {
743
+ const limit = readPositiveInt(options.limit);
744
+ const localConcurrency = readPositiveInt(options.localConcurrency);
745
+ const maxCredits = readPositiveNumber(options.maxCredits);
746
+ const filterSelection = readFilterSelectionOption(options.filterJson);
747
+ const selectedModes = [
748
+ Boolean(options.all),
749
+ limit !== undefined,
750
+ Boolean(readOption(options.rowId)),
751
+ Boolean(filterSelection),
752
+ ].filter(Boolean).length;
753
+ if (selectedModes > 1) {
754
+ throw new OxygenError("invalid_selection", "Pass only one of --all, --limit, --row-id, or --filter-json.", {
755
+ exitCode: 1,
756
+ });
757
+ }
758
+ if (options.all && !options.background) {
759
+ throw new OxygenError("invalid_column_run", "--all requires --background.", {
760
+ exitCode: 1,
761
+ });
762
+ }
763
+ await handleAsyncAction("columns run", options, async () => {
764
+ if (options.local) {
765
+ if (options.background) {
766
+ throw new OxygenError("invalid_column_run", "Pass either --local or --background, not both.", {
767
+ exitCode: 1,
768
+ });
769
+ }
770
+ if (filterSelection) {
771
+ throw new OxygenError("invalid_column_run", "--filter-json requires --background for columns run.", {
772
+ exitCode: 1,
773
+ });
774
+ }
775
+ return runLocalCustomHttpColumn(table, column, {
776
+ rowId: readOption(options.rowId),
777
+ force: Boolean(options.force),
778
+ ...(limit ? { limit } : {}),
779
+ ...(localConcurrency ? { concurrency: localConcurrency } : {}),
780
+ });
781
+ }
782
+ return requestOxygen("/api/cli/tables/columns/run", {
783
+ method: "POST",
784
+ body: {
785
+ table,
786
+ column,
787
+ ...(options.all ? { selection: { mode: "all" } } : {}),
788
+ ...(filterSelection ? { selection: filterSelection } : {}),
789
+ ...(!options.all && readOption(options.rowId) ? { row_id: readOption(options.rowId) } : {}),
790
+ ...(!options.all && !filterSelection && limit ? { limit } : {}),
791
+ ...(options.force ? { force: true } : {}),
792
+ ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
793
+ ...(options.background ? { background: true } : {}),
794
+ ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
795
+ },
796
+ });
797
+ });
798
+ }))
799
+ .addCommand(new Command("materialize")
800
+ .description("Materialize useful fields from a JSONB result column into target columns.")
801
+ .argument("<table>", "Table id or slug.")
802
+ .argument("<source_column>", "JSONB source column id or key.")
803
+ .option("--preset <preset>", "Materialization preset. Currently supports work_email.")
804
+ .option("--mappings-json <json>", "JSON array of {source_path,target_column,label?,data_type?,semantic_type?} mappings.")
805
+ .option("--no-create-if-missing", "Require target companion columns to already exist.")
806
+ .option("--json", "Print a JSON envelope.")
807
+ .action(async (table, sourceColumn, options) => {
808
+ const mappings = options.mappingsJson ? parseJsonArray(options.mappingsJson) : undefined;
809
+ const preset = readOption(options.preset);
810
+ await handleAsyncAction("columns materialize", options, async () => requestOxygen("/api/cli/tables/columns/materialize", {
811
+ method: "POST",
812
+ body: {
813
+ table,
814
+ source_column: sourceColumn,
815
+ ...(mappings ? { mappings } : {}),
816
+ ...(!mappings ? { preset: preset ?? "work_email" } : preset ? { preset } : {}),
817
+ create_if_missing: options.createIfMissing !== false,
818
+ },
819
+ }));
820
+ }))
821
+ .addCommand(new Command("rename")
822
+ .description("Rename a workspace column key and optionally label.")
823
+ .argument("<table>", "Table id or slug.")
824
+ .argument("<column>", "Column id or key.")
825
+ .requiredOption("--key <key>", "New stable column key.")
826
+ .option("--label <label>", "Optional new display label.")
827
+ .option("--json", "Print a JSON envelope.")
828
+ .action(async (table, column, options) => {
829
+ await handleAsyncAction("columns rename", options, async () => requestOxygen("/api/cli/tables/columns/rename", {
830
+ method: "POST",
831
+ body: {
832
+ table,
833
+ column,
834
+ key: options.key,
835
+ ...(readOption(options.label) ? { label: readOption(options.label) } : {}),
836
+ },
837
+ }));
838
+ }))
839
+ .addCommand(new Command("update")
840
+ .description("Update workspace column display metadata.")
841
+ .argument("<table>", "Table id or slug.")
842
+ .argument("<column>", "Column id or key.")
843
+ .option("--label <label>", "New display label.")
844
+ .option("--semantic-type <type>", "New semantic type.")
845
+ .option("--definition-json <json>", "Definition metadata to merge into the column.")
846
+ .option("--json", "Print a JSON envelope.")
847
+ .action(async (table, column, options) => {
848
+ await handleAsyncAction("columns update", options, async () => requestOxygen("/api/cli/tables/columns/update", {
849
+ method: "POST",
850
+ body: {
851
+ table,
852
+ column,
853
+ ...(readOption(options.label) ? { label: readOption(options.label) } : {}),
854
+ ...(readOption(options.semanticType) ? { semantic_type: readOption(options.semanticType) } : {}),
855
+ ...(options.definitionJson ? { definition: parseJsonObject(options.definitionJson) } : {}),
856
+ },
857
+ }));
858
+ }))
859
+ .addCommand(new Command("retype")
860
+ .description("Convert a manual text column to a stronger data type, migrating stored values.")
861
+ .argument("<table>", "Table id or slug.")
862
+ .argument("<column>", "Column id or key.")
863
+ .requiredOption("--data-type <type>", "Target data type: text, numeric, boolean, or timestamptz.")
864
+ .option("--dry-run", "Preview the conversion (row counts and samples) without writing.")
865
+ .option("--json", "Print a JSON envelope.")
866
+ .action(async (table, column, options) => {
867
+ await handleAsyncAction("columns retype", options, async () => requestOxygen("/api/cli/tables/columns/retype", {
868
+ method: "POST",
869
+ body: {
870
+ table,
871
+ column,
872
+ data_type: options.dataType,
873
+ ...(options.dryRun ? { dry_run: true } : {}),
874
+ },
875
+ }));
876
+ }))
877
+ .addCommand(new Command("archive")
878
+ .description("Archive a workspace column without dropping physical data.")
879
+ .argument("<table>", "Table id or slug.")
880
+ .argument("<column>", "Column id or key.")
881
+ .option("--json", "Print a JSON envelope.")
882
+ .action(async (table, column, options) => {
883
+ await handleAsyncAction("columns archive", options, async () => requestOxygen("/api/cli/tables/columns/archive", {
884
+ method: "POST",
885
+ body: { table, column },
886
+ }));
887
+ }));
888
+ program
889
+ .command("action-column")
890
+ .description("Workspace table action-column setup commands.")
891
+ .addCommand(new Command("add-provider")
892
+ .description("Add a catalog-backed provider action picker column to a workspace table.")
893
+ .argument("[table]", "Table id or slug.")
894
+ .requiredOption("--provider <provider>", "Provider id, such as attio.")
895
+ .option("--table <table>", "Table id or slug.")
896
+ .option("--label <label>", "Display label for the new column.")
897
+ .option("--key <key>", "Optional stable column key. Defaults to <provider>_actions.")
898
+ .option("--operation-filter <csv>", "Optional comma-separated operation, tool id, category, or tag filter.")
899
+ .option("--dry-run", "Preview the provider action column without creating it.")
900
+ .option("--json", "Print a JSON envelope.")
901
+ .action(async (table, options) => {
902
+ await handleAsyncAction("action-column add-provider", options, async () => {
903
+ const tableId = readOption(table) ?? readOption(options.table);
904
+ if (!tableId) {
905
+ throw new OxygenError("invalid_request", "Pass a table argument or --table <table>.", {
906
+ exitCode: 1,
907
+ });
908
+ }
909
+ return requestOxygen("/api/cli/action-columns/provider", {
910
+ method: "POST",
911
+ body: {
912
+ table: tableId,
913
+ provider: options.provider,
914
+ ...(readOption(options.label) ? { label: readOption(options.label) } : {}),
915
+ ...(readOption(options.key) ? { key: readOption(options.key) } : {}),
916
+ ...(readOption(options.operationFilter)
917
+ ? { operation_filter: readCsvOption(options.operationFilter) }
918
+ : {}),
919
+ ...(options.dryRun ? { dry_run: true } : {}),
920
+ },
921
+ });
922
+ });
923
+ }));
924
+ program
925
+ .command("table-runs")
926
+ .description("Durable background table action run commands.")
927
+ .addCommand(new Command("create")
928
+ .description("Create a durable background run for table tool-column actions.")
929
+ .argument("<table>", "Table id or slug.")
930
+ .option("--column <column>", "Column id or key for a single tool-column action.")
931
+ .option("--actions-json <json>", "JSON array of actions, each with type tool_column and column.")
932
+ .option("--limit <n>", "Run against the newest n rows.")
933
+ .option("--all", "Run against all rows, up to the server cap.")
934
+ .option("--row-ids <csv>", "Comma-separated row UUIDs to run.")
935
+ .option("--filter-json <json>", "Filter object or array for server-side row selection.")
936
+ .option("--force", "Run even when the target cell already has a value.")
937
+ .option("--connection-id <connection_id>", "Optional provider integration connection id.")
938
+ .option("--max-credits <n>", "Maximum managed/provider credits to reserve for this run.")
939
+ .option("--metadata-json <json>", "Optional metadata object to attach to the run.")
940
+ .option("--json", "Print a JSON envelope.")
941
+ .action(async (table, options) => {
942
+ const maxCredits = readPositiveNumber(options.maxCredits);
943
+ await handleAsyncAction("table-runs create", options, async () => requestOxygen("/api/cli/table-action-runs", {
944
+ method: "POST",
945
+ body: {
946
+ table,
947
+ actions: readTableRunActions(options),
948
+ selection: readTableRunSelection(options),
949
+ ...(options.force ? { force: true } : {}),
950
+ ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
951
+ ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
952
+ ...(options.metadataJson ? { metadata: parseJsonObject(options.metadataJson) } : {}),
953
+ },
954
+ }));
955
+ }))
956
+ .addCommand(new Command("get")
957
+ .description("Get one durable table action run.")
958
+ .argument("<run_id>", "Table action run UUID.")
959
+ .option("--json", "Print a JSON envelope.")
960
+ .action(async (runId, options) => {
961
+ await handleAsyncAction("table-runs get", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}`));
962
+ }))
963
+ .addCommand(new Command("items")
964
+ .description("List row items for a durable table action run.")
965
+ .argument("<run_id>", "Table action run UUID.")
966
+ .option("--status <status>", "Filter by pending, leased, completed, failed, skipped, or canceled.")
967
+ .option("--limit <n>", "Maximum items to return. Defaults to 100.")
968
+ .option("--json", "Print a JSON envelope.")
969
+ .action(async (runId, options) => {
970
+ await handleAsyncAction("table-runs items", options, async () => {
971
+ const query = new URLSearchParams();
972
+ if (readOption(options.status))
973
+ query.set("status", readOption(options.status) ?? "");
974
+ const limit = readPositiveInt(options.limit);
975
+ if (limit)
976
+ query.set("limit", String(limit));
977
+ const suffix = query.toString() ? `?${query.toString()}` : "";
978
+ return requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/items${suffix}`);
979
+ });
980
+ }))
981
+ .addCommand(new Command("wait")
982
+ .description("Poll a durable table action run until it finishes.")
983
+ .argument("<run_id>", "Table action run UUID.")
984
+ .option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
985
+ .option("--interval-seconds <n>", "Polling interval. Defaults to 5.")
986
+ .option("--json", "Print a JSON envelope.")
987
+ .action(async (runId, options) => {
988
+ await handleAsyncAction("table-runs wait", options, async () => waitForTableActionRun(runId, options));
989
+ }))
990
+ .addCommand(new Command("provider-summary")
991
+ .description("Summarize provider attempts, upstream request events, and credit capture/release for a table action run.")
992
+ .argument("<run_id>", "Table action run UUID.")
993
+ .option("--json", "Print a JSON envelope.")
994
+ .action(async (runId, options) => {
995
+ await handleAsyncAction("table-runs provider-summary", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/provider-summary`));
996
+ }))
997
+ .addCommand(new Command("cancel")
998
+ .description("Request cancellation for a durable table action run.")
999
+ .argument("<run_id>", "Table action run UUID.")
1000
+ .option("--json", "Print a JSON envelope.")
1001
+ .action(async (runId, options) => {
1002
+ await handleAsyncAction("table-runs cancel", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/cancel`, {
1003
+ method: "POST",
1004
+ body: {},
1005
+ }));
1006
+ }))
1007
+ .addCommand(new Command("pause")
1008
+ .description("Pause a durable table action run before it starts more row jobs.")
1009
+ .argument("<run_id>", "Table action run UUID.")
1010
+ .option("--json", "Print a JSON envelope.")
1011
+ .action(async (runId, options) => {
1012
+ await handleAsyncAction("table-runs pause", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/pause`, {
1013
+ method: "POST",
1014
+ body: {},
1015
+ }));
1016
+ }))
1017
+ .addCommand(new Command("resume")
1018
+ .description("Resume a paused durable table action run.")
1019
+ .argument("<run_id>", "Table action run UUID.")
1020
+ .option("--json", "Print a JSON envelope.")
1021
+ .action(async (runId, options) => {
1022
+ await handleAsyncAction("table-runs resume", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/resume`, {
1023
+ method: "POST",
1024
+ body: {},
1025
+ }));
1026
+ }))
1027
+ .addCommand(new Command("retry-failed")
1028
+ .description("Requeue failed items for a durable table action run.")
1029
+ .argument("<run_id>", "Table action run UUID.")
1030
+ .option("--json", "Print a JSON envelope.")
1031
+ .action(async (runId, options) => {
1032
+ await handleAsyncAction("table-runs retry-failed", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/retry-failed`, {
1033
+ method: "POST",
1034
+ body: {},
1035
+ }));
1036
+ }));
1037
+ program
1038
+ .command("table-ingestions")
1039
+ .description("Durable background table ingestion commands.")
1040
+ .addCommand(new Command("create-tool-page")
1041
+ .description("Create a paged tool-source ingestion run for a workspace table.")
1042
+ .argument("<table>", "Table id or slug.")
1043
+ .option("--tool <tool_id>", "Tool id such as blitzapi.people_search or hubspot.contacts_list.")
1044
+ .option("--tool-id <tool_id>", "Alias for --tool.")
1045
+ .requiredOption("--request-json <json>", "Tool request JSON object for the first page.")
1046
+ .option("--rows-path <path>", "Dotted path to result rows. Defaults to response.results.")
1047
+ .option("--row-mapping-json <json>", "Object mapping target column keys to dotted source-row paths.")
1048
+ .option("--cursor-path <path>", "Dotted path to the next cursor in the tool result.")
1049
+ .option("--cursor-request-key <path>", "Dotted request key to receive the next cursor.")
1050
+ .option("--max-pages <n>", "Maximum pages to fetch. Defaults to 1.")
1051
+ .option("--upsert-key <key>", "Column key used to upsert instead of inserting.")
1052
+ .option("--connection-id <connection_id>", "Optional provider integration connection id.")
1053
+ .option("--max-concurrency <n>", "Maximum concurrent ingestion items for this run. Defaults to 5.")
1054
+ .option("--metadata-json <json>", "Optional metadata object to attach to the run.")
1055
+ .option("--json", "Print a JSON envelope.")
1056
+ .action(async (table, options) => {
1057
+ await handleAsyncAction("table-ingestions create-tool-page", options, async () => {
1058
+ const toolId = readOption(options.tool) ?? readOption(options.toolId);
1059
+ if (!toolId) {
1060
+ throw new OxygenError("invalid_table_ingestion", "Pass --tool or --tool-id.", {
1061
+ exitCode: 1,
1062
+ });
1063
+ }
1064
+ const maxPages = readPositiveInt(options.maxPages);
1065
+ const maxConcurrency = readPositiveInt(options.maxConcurrency);
1066
+ const rowMappingJson = readOption(options.rowMappingJson);
1067
+ const payload = {
1068
+ tool_id: toolId,
1069
+ request: parseJsonObject(options.requestJson),
1070
+ ...(readOption(options.rowsPath) ? { rows_path: readOption(options.rowsPath) } : {}),
1071
+ ...(rowMappingJson ? { row_mapping: parseJsonObject(rowMappingJson) } : {}),
1072
+ ...(readOption(options.cursorPath) ? { cursor_path: readOption(options.cursorPath) } : {}),
1073
+ ...(readOption(options.cursorRequestKey) ? { cursor_request_key: readOption(options.cursorRequestKey) } : {}),
1074
+ ...(maxPages ? { max_pages: maxPages } : {}),
1075
+ };
1076
+ return requestOxygen("/api/cli/table-ingestion-runs", {
1077
+ method: "POST",
1078
+ body: {
1079
+ table,
1080
+ source_type: "tool_page",
1081
+ mode: options.upsertKey ? "upsert" : "insert",
1082
+ ...(readOption(options.upsertKey) ? { upsert_key: readOption(options.upsertKey) } : {}),
1083
+ ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
1084
+ ...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
1085
+ metadata: {
1086
+ command: "table-ingestions create-tool-page",
1087
+ ...(options.metadataJson ? parseJsonObject(options.metadataJson) : {}),
1088
+ },
1089
+ items: [
1090
+ {
1091
+ position: 0,
1092
+ payload,
1093
+ },
1094
+ ],
1095
+ },
1096
+ });
1097
+ });
1098
+ }))
1099
+ .addCommand(new Command("get")
1100
+ .description("Get one durable table ingestion run.")
1101
+ .argument("<run_id>", "Table ingestion run UUID.")
1102
+ .option("--json", "Print a JSON envelope.")
1103
+ .action(async (runId, options) => {
1104
+ await handleAsyncAction("table-ingestions get", options, async () => requestOxygen(`/api/cli/table-ingestion-runs/${encodeURIComponent(runId)}`));
1105
+ }))
1106
+ .addCommand(new Command("items")
1107
+ .description("List items for a durable table ingestion run.")
1108
+ .argument("<run_id>", "Table ingestion run UUID.")
1109
+ .option("--status <status>", "Filter by pending, leased, completed, failed, skipped, or canceled.")
1110
+ .option("--limit <n>", "Maximum items to return. Defaults to 100.")
1111
+ .option("--json", "Print a JSON envelope.")
1112
+ .action(async (runId, options) => {
1113
+ await handleAsyncAction("table-ingestions items", options, async () => {
1114
+ const query = new URLSearchParams();
1115
+ if (readOption(options.status))
1116
+ query.set("status", readOption(options.status) ?? "");
1117
+ const limit = readPositiveInt(options.limit);
1118
+ if (limit)
1119
+ query.set("limit", String(limit));
1120
+ const suffix = query.toString() ? `?${query.toString()}` : "";
1121
+ return requestOxygen(`/api/cli/table-ingestion-runs/${encodeURIComponent(runId)}/items${suffix}`);
1122
+ });
1123
+ }))
1124
+ .addCommand(new Command("wait")
1125
+ .description("Poll a durable table ingestion run until it finishes.")
1126
+ .argument("<run_id>", "Table ingestion run UUID.")
1127
+ .option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
1128
+ .option("--interval-seconds <n>", "Polling interval. Defaults to 5.")
1129
+ .option("--json", "Print a JSON envelope.")
1130
+ .action(async (runId, options) => {
1131
+ await handleAsyncAction("table-ingestions wait", options, async () => waitForTableIngestionRun(runId, options));
1132
+ }))
1133
+ .addCommand(new Command("cancel")
1134
+ .description("Request cancellation for a durable table ingestion run.")
1135
+ .argument("<run_id>", "Table ingestion run UUID.")
1136
+ .option("--json", "Print a JSON envelope.")
1137
+ .action(async (runId, options) => {
1138
+ await handleAsyncAction("table-ingestions cancel", options, async () => requestOxygen(`/api/cli/table-ingestion-runs/${encodeURIComponent(runId)}/cancel`, {
1139
+ method: "POST",
1140
+ body: {},
1141
+ }));
1142
+ }))
1143
+ .addCommand(new Command("retry-failed")
1144
+ .description("Requeue failed items for a durable table ingestion run.")
1145
+ .argument("<run_id>", "Table ingestion run UUID.")
1146
+ .option("--json", "Print a JSON envelope.")
1147
+ .action(async (runId, options) => {
1148
+ await handleAsyncAction("table-ingestions retry-failed", options, async () => requestOxygen(`/api/cli/table-ingestion-runs/${encodeURIComponent(runId)}/retry-failed`, {
1149
+ method: "POST",
1150
+ body: {},
1151
+ }));
1152
+ }));
1153
+ program
1154
+ .command("lead-sourcing")
1155
+ .description("ICP-gated lead sourcing planning and audit commands.")
1156
+ .addCommand(new Command("plan")
1157
+ .description("Compile a natural-language ICP into an account-first lead sourcing plan.")
1158
+ .requiredOption("--prompt <file|text>", "ICP prompt text, or a local file path containing the prompt.")
1159
+ .option("--materialize-preview", "Create a preview table for the plan. Defaults to no table side effect.")
1160
+ .option("--json", "Print a JSON envelope.")
1161
+ .action(async (options) => {
1162
+ await handleAsyncAction("lead-sourcing plan", options, async () => requestOxygen("/api/cli/lead-sourcing/plan", {
1163
+ method: "POST",
1164
+ body: {
1165
+ prompt: readFileIfPresent(options.prompt),
1166
+ ...(options.materializePreview ? { materialize_preview: true } : {}),
1167
+ },
1168
+ }));
1169
+ }))
1170
+ .addCommand(new Command("audit")
1171
+ .description("Audit a sourced lead table against a lead-sourcing spec.")
1172
+ .argument("<table>", "Table id or slug.")
1173
+ .requiredOption("--spec <file>", "JSON LeadSourcingSpec file, or a text prompt file to compile into a spec.")
1174
+ .option("--json", "Print a JSON envelope.")
1175
+ .action(async (table, options) => {
1176
+ await handleAsyncAction("lead-sourcing audit", options, async () => requestOxygen("/api/cli/lead-sourcing/audit", {
1177
+ method: "POST",
1178
+ body: {
1179
+ table,
1180
+ ...readSpecFileBody(options.spec),
1181
+ },
1182
+ }));
1183
+ }));
1184
+ program
1185
+ .command("worker")
1186
+ .description("Background worker commands.")
1187
+ .addCommand(new Command("queue-stats")
1188
+ .description("Show background action and ingestion queue health.")
1189
+ .option("--json", "Print a JSON envelope.")
1190
+ .action(async (options) => {
1191
+ await handleAsyncAction("worker queue-stats", options, async () => requestOxygen("/api/cli/worker/queue-stats"));
1192
+ }))
1193
+ .addCommand(new Command("failures")
1194
+ .description("List failed background action and ingestion items.")
1195
+ .option("--queue <queue>", "all, actions, or ingestions. Defaults to all.")
1196
+ .option("--limit <n>", "Maximum failed items per queue. Defaults to 25; server cap is 100.")
1197
+ .option("--json", "Print a JSON envelope.")
1198
+ .action(async (options) => {
1199
+ await handleAsyncAction("worker failures", options, async () => {
1200
+ const query = new URLSearchParams();
1201
+ if (readOption(options.queue))
1202
+ query.set("queue", readOption(options.queue) ?? "");
1203
+ const limit = readPositiveInt(options.limit);
1204
+ if (limit)
1205
+ query.set("limit", String(limit));
1206
+ const suffix = query.toString() ? `?${query.toString()}` : "";
1207
+ return requestOxygen(`/api/cli/worker/failures${suffix}`);
1208
+ });
1209
+ }))
1210
+ .addCommand(new Command("repair")
1211
+ .description("Repair stale background action and ingestion queue state.")
1212
+ .option("--json", "Print a JSON envelope.")
1213
+ .action(async (options) => {
1214
+ await handleAsyncAction("worker repair", options, async () => requestOxygen("/api/cli/worker/repair", {
1215
+ method: "POST",
1216
+ body: {},
1217
+ }));
1218
+ }))
1219
+ .addCommand(new Command("run-once")
1220
+ .description("Compatibility alias for worker repair; enqueues BullMQ repair work for the current organization.")
1221
+ .option("--claim-limit <n>", "Compatibility option; ignored by the BullMQ worker.")
1222
+ .option("--concurrency <n>", "Compatibility option; ignored by the BullMQ worker.")
1223
+ .option("--enrichment-concurrency <n>", "Compatibility option; ignored by the BullMQ worker.")
1224
+ .option("--lease-seconds <n>", "Compatibility option; ignored by the BullMQ worker.")
1225
+ .option("--provider-timeout-ms <n>", "Compatibility option; ignored by the BullMQ worker.")
1226
+ .option("--recipe-timeout-ms <n>", "Compatibility option; ignored by the BullMQ worker.")
1227
+ .option("--json", "Print a JSON envelope.")
1228
+ .action(async (options) => {
1229
+ const claimLimit = readPositiveInt(options.claimLimit);
1230
+ const concurrency = readPositiveInt(options.concurrency);
1231
+ const enrichmentConcurrency = readPositiveInt(options.enrichmentConcurrency);
1232
+ const leaseSeconds = readPositiveInt(options.leaseSeconds);
1233
+ const providerTimeoutMs = readPositiveInt(options.providerTimeoutMs);
1234
+ const recipeTimeoutMs = readPositiveInt(options.recipeTimeoutMs);
1235
+ await handleAsyncAction("worker run-once", options, async () => requestOxygen("/api/cli/worker/run-once", {
1236
+ method: "POST",
1237
+ body: {
1238
+ ...(claimLimit ? { claim_limit: claimLimit } : {}),
1239
+ ...(concurrency ? { concurrency } : {}),
1240
+ ...(enrichmentConcurrency ? { enrichment_concurrency: enrichmentConcurrency } : {}),
1241
+ ...(leaseSeconds ? { lease_seconds: leaseSeconds } : {}),
1242
+ ...(providerTimeoutMs ? { provider_timeout_ms: providerTimeoutMs } : {}),
1243
+ ...(recipeTimeoutMs ? { recipe_timeout_ms: recipeTimeoutMs } : {}),
1244
+ },
1245
+ }));
1246
+ }));
1247
+ program
1248
+ .command("billing")
1249
+ .description("Plan and managed credit commands.")
1250
+ .addCommand(new Command("balance")
1251
+ .description("Show the current plan and managed credit balance.")
1252
+ .option("--json", "Print a JSON envelope.")
1253
+ .action(async (options) => {
1254
+ await handleAsyncAction("billing balance", options, async () => requestOxygen("/api/cli/billing/balance"));
1255
+ }))
1256
+ .addCommand(new Command("usage")
1257
+ .description("Show recent credit ledger events.")
1258
+ .option("--days <n>", "Lookback window in days. Defaults to 30.")
1259
+ .option("--limit <n>", "Maximum events to return. Defaults to 50.")
1260
+ .option("--json", "Print a JSON envelope.")
1261
+ .action(async (options) => {
1262
+ await handleAsyncAction("billing usage", options, async () => {
1263
+ const params = new URLSearchParams();
1264
+ const days = readPositiveInt(options.days);
1265
+ const limit = readPositiveInt(options.limit);
1266
+ if (days)
1267
+ params.set("days", String(days));
1268
+ if (limit)
1269
+ params.set("limit", String(limit));
1270
+ const suffix = params.toString() ? `?${params.toString()}` : "";
1271
+ return requestOxygen(`/api/cli/billing/usage${suffix}`);
1272
+ });
1273
+ }))
1274
+ .addCommand(new Command("grant")
1275
+ .description("Grant managed credits to the current organization (staff only).")
1276
+ .requiredOption("--credits <credits>", "Managed credits to grant.")
1277
+ .requiredOption("--idempotency-key <key>", "Stable key that makes retries safe.")
1278
+ .option("--organization-id <id>", "Organization id to credit. Defaults to the current organization.")
1279
+ .option("--org-id <id>", "Alias for --organization-id.")
1280
+ .option("--description <text>", "Ledger description for the admin grant.")
1281
+ .option("--json", "Print a JSON envelope.")
1282
+ .action(async (options) => {
1283
+ await handleAsyncAction("billing grant", options, async () => requestOxygen("/api/cli/billing/grant", {
1284
+ method: "POST",
1285
+ body: {
1286
+ credits: readPositiveNumber(options.credits),
1287
+ idempotency_key: readOption(options.idempotencyKey),
1288
+ ...(readOption(options.organizationId) || readOption(options.orgId)
1289
+ ? { organization_id: readOption(options.organizationId) ?? readOption(options.orgId) }
1290
+ : {}),
1291
+ ...(readOption(options.description) ? { description: readOption(options.description) } : {}),
1292
+ },
1293
+ }));
1294
+ }));
1295
+ program
1296
+ .command("admin")
1297
+ .description("Staff-only commands.")
1298
+ .addCommand(new Command("costs")
1299
+ .description("Show provider costs (COGS) per workspace. Staff only.")
1300
+ .option("--top <n>", "Limit number of workspace columns. Defaults to all.")
1301
+ .option("--json", "Print a JSON envelope.")
1302
+ .action(async (options) => {
1303
+ await handleAsyncAction("admin costs", options, async () => {
1304
+ const params = new URLSearchParams();
1305
+ const top = readPositiveInt(options.top);
1306
+ if (top)
1307
+ params.set("top", String(top));
1308
+ const suffix = params.toString() ? `?${params.toString()}` : "";
1309
+ return requestOxygen(`/api/cli/admin/costs${suffix}`);
1310
+ });
1311
+ }));
1312
+ program
1313
+ .command("observability")
1314
+ .description("Redacted operation event commands for the current organization.")
1315
+ .addCommand(new Command("events")
1316
+ .description("List recent redacted operation events and failures.")
1317
+ .option("--status <status>", "Filter by completed, queued, completed_with_errors, or failed.")
1318
+ .option("--trace-id <trace_id>", "Filter by trace id.")
1319
+ .option("--run-id <run_id>", "Filter by workspace run id.")
1320
+ .option("--limit <n>", "Maximum events to return. Defaults to 50.")
1321
+ .option("--json", "Print a JSON envelope.")
1322
+ .action(async (options) => {
1323
+ await handleAsyncAction("observability events", options, async () => {
1324
+ const params = new URLSearchParams();
1325
+ const status = readOption(options.status);
1326
+ const traceId = readOption(options.traceId);
1327
+ const runId = readOption(options.runId);
1328
+ const limit = readPositiveInt(options.limit);
1329
+ if (status)
1330
+ params.set("status", status);
1331
+ if (traceId)
1332
+ params.set("trace_id", traceId);
1333
+ if (runId)
1334
+ params.set("run_id", runId);
1335
+ if (limit)
1336
+ params.set("limit", String(limit));
1337
+ const suffix = params.toString() ? `?${params.toString()}` : "";
1338
+ return requestOxygen(`/api/cli/observability/events${suffix}`);
1339
+ });
1340
+ }));
1341
+ program
1342
+ .command("runs")
1343
+ .description("Tenant run provenance commands.")
1344
+ .addCommand(new Command("list")
1345
+ .description("List recent tenant runs.")
1346
+ .option("--limit <n>", "Maximum runs to return. Defaults to 50.")
1347
+ .option("--json", "Print a JSON envelope.")
1348
+ .action(async (options) => {
1349
+ await handleAsyncAction("runs list", options, async () => {
1350
+ const limit = readPositiveInt(options.limit);
1351
+ return requestOxygen(`/api/cli/runs${limit ? `?limit=${limit}` : ""}`);
1352
+ });
1353
+ }))
1354
+ .addCommand(new Command("get")
1355
+ .description("Get one run with events.")
1356
+ .argument("<run_id>", "Run UUID.")
1357
+ .option("--json", "Print a JSON envelope.")
1358
+ .action(async (runId, options) => {
1359
+ await handleAsyncAction("runs get", options, async () => requestOxygen("/api/cli/runs/get", {
1360
+ method: "POST",
1361
+ body: { run_id: runId },
1362
+ }));
1363
+ }));
1364
+ program
1365
+ .command("rows")
1366
+ .description("Workspace row provenance commands.")
1367
+ .addCommand(new Command("history")
1368
+ .description("Show row change history.")
1369
+ .argument("<table>", "Table id or slug.")
1370
+ .argument("<row_id>", "Workspace row UUID.")
1371
+ .option("--limit <n>", "Maximum changes to return. Defaults to 50.")
1372
+ .option("--json", "Print a JSON envelope.")
1373
+ .action(async (table, rowId, options) => {
1374
+ await handleAsyncAction("rows history", options, async () => {
1375
+ const limit = readPositiveInt(options.limit);
1376
+ return requestOxygen("/api/cli/tables/rows/history", {
1377
+ method: "POST",
1378
+ body: {
1379
+ table,
1380
+ row_id: rowId,
1381
+ ...(limit ? { limit } : {}),
1382
+ },
1383
+ });
1384
+ });
1385
+ }));
1386
+ program
1387
+ .command("cells")
1388
+ .description("Workspace cell provenance commands.")
1389
+ .addCommand(new Command("history")
1390
+ .description("Show cell change history.")
1391
+ .argument("<table>", "Table id or slug.")
1392
+ .argument("<row_id>", "Workspace row UUID.")
1393
+ .argument("<column>", "Column id or key.")
1394
+ .option("--limit <n>", "Maximum changes to return. Defaults to 50.")
1395
+ .option("--json", "Print a JSON envelope.")
1396
+ .action(async (table, rowId, column, options) => {
1397
+ await handleAsyncAction("cells history", options, async () => {
1398
+ const limit = readPositiveInt(options.limit);
1399
+ return requestOxygen("/api/cli/tables/cells/history", {
1400
+ method: "POST",
1401
+ body: {
1402
+ table,
1403
+ row_id: rowId,
1404
+ column,
1405
+ ...(limit ? { limit } : {}),
1406
+ },
1407
+ });
1408
+ });
1409
+ }));
1410
+ program
1411
+ .command("session")
1412
+ .description("Local agent session plan and output commands.")
1413
+ .addCommand(new Command("start")
1414
+ .description("Start a local Oxygen agent session or update a step.")
1415
+ .option("--steps <json>", "JSON array of step labels.")
1416
+ .option("--user-prompt <text>", "Original user request for local session context.")
1417
+ .option("--update <index>", "Update a step index instead of replacing the plan.")
1418
+ .option("--status <status>", "Step status: pending, running, completed, error, or skipped.")
1419
+ .option("--session-id <id>", "Session id to update. Defaults to the current session.")
1420
+ .option("--json", "Print a JSON envelope.")
1421
+ .action(async (options) => {
1422
+ await handleAsyncAction("session start", options, async () => {
1423
+ const updateIndex = readNonNegativeInt(options.update);
1424
+ if (updateIndex !== undefined) {
1425
+ return updateSessionStep({
1426
+ sessionId: readOption(options.sessionId),
1427
+ index: updateIndex,
1428
+ status: normalizeSessionStepStatus(options.status),
1429
+ });
1430
+ }
1431
+ if (!options.steps) {
1432
+ throw new OxygenError("invalid_session", "Pass --steps as a JSON array of labels.", {
1433
+ exitCode: 1,
1434
+ });
1435
+ }
1436
+ return startSession({
1437
+ steps: parseStringArray(options.steps),
1438
+ userPrompt: readOption(options.userPrompt),
1439
+ });
1440
+ });
1441
+ }))
1442
+ .addCommand(new Command("status")
1443
+ .description("Attach a live status message to the current local session.")
1444
+ .requiredOption("--message <message>", "Status message.")
1445
+ .option("--step-index <index>", "Optional step index. Defaults to the running step.")
1446
+ .option("--session-id <id>", "Session id. Defaults to the current session.")
1447
+ .option("--json", "Print a JSON envelope.")
1448
+ .action(async (options) => {
1449
+ await handleAsyncAction("session status", options, async () => addSessionStatus({
1450
+ sessionId: readOption(options.sessionId),
1451
+ message: options.message,
1452
+ stepIndex: readNonNegativeInt(options.stepIndex) ?? null,
1453
+ }));
1454
+ }))
1455
+ .addCommand(new Command("output")
1456
+ .description("Register a local session output file or table.")
1457
+ .option("--csv <path>", "CSV output path.")
1458
+ .option("--file <path>", "Generic output file path.")
1459
+ .option("--table <table>", "Workspace table id or slug.")
1460
+ .option("--label <label>", "Human-readable output label.")
1461
+ .option("--meta-json <json>", "Optional JSON metadata object.")
1462
+ .option("--session-id <id>", "Session id. Defaults to the current session.")
1463
+ .option("--json", "Print a JSON envelope.")
1464
+ .action(async (options) => {
1465
+ await handleAsyncAction("session output", options, async () => addSessionOutput({
1466
+ sessionId: readOption(options.sessionId),
1467
+ csv: readOption(options.csv),
1468
+ file: readOption(options.file),
1469
+ table: readOption(options.table),
1470
+ label: readOption(options.label),
1471
+ meta: options.metaJson ? parseJsonObject(options.metaJson) : null,
1472
+ }));
1473
+ }))
1474
+ .addCommand(new Command("usage")
1475
+ .description("Summarize the current local session. Org credit usage is available through billing usage.")
1476
+ .option("--session-id <id>", "Session id. Defaults to the current session.")
1477
+ .option("--json", "Print a JSON envelope.")
1478
+ .action(async (options) => {
1479
+ await handleAsyncAction("session usage", options, async () => getSessionUsage({ sessionId: readOption(options.sessionId) }));
1480
+ }));
1481
+ program
1482
+ .command("tools")
1483
+ .description("Tool catalog commands.")
1484
+ .addCommand(new Command("search")
1485
+ .description("Search the tool catalog.")
1486
+ .argument("[query]", "Search text.")
1487
+ .option("--verbosity <verbosity>", "summary or full. Defaults to summary.")
1488
+ .option("--only-runnable", "Only return tools runnable by the active organization.")
1489
+ .option("--capability <tag>", "Filter by capability tag, such as mobile_phone.")
1490
+ .option("--json", "Print a JSON envelope.")
1491
+ .action(async (query, options) => {
1492
+ await handleAsyncAction("tools search", options, async () => {
1493
+ const params = new URLSearchParams();
1494
+ params.set("query", query ?? "");
1495
+ if (readOption(options.verbosity))
1496
+ params.set("verbosity", readOption(options.verbosity) ?? "");
1497
+ if (options.onlyRunnable)
1498
+ params.set("only_runnable", "true");
1499
+ if (readOption(options.capability))
1500
+ params.set("capability", readOption(options.capability) ?? "");
1501
+ return requestOxygen(`/api/cli/tools/search?${params.toString()}`);
1502
+ });
1503
+ }))
1504
+ .addCommand(new Command("get")
1505
+ .description("Get a tool descriptor.")
1506
+ .argument("<tool_id>", "Tool id.")
1507
+ .option("--json", "Print a JSON envelope.")
1508
+ .action(async (toolId, options) => {
1509
+ await handleAsyncAction("tools get", options, async () => requestOxygen(`/api/cli/tools/${encodeURIComponent(toolId)}`));
1510
+ }))
1511
+ .addCommand(new Command("run")
1512
+ .description("Run an executable Oxygen tool through the HTTPS API.")
1513
+ .argument("<tool_id>", "Tool id.")
1514
+ .requiredOption("--input-json <json>", "Tool input as a JSON object.")
1515
+ .option("--mode <mode>", "Execution mode: live, dry-run, or dry_run. Mutating tools default to dry-run.")
1516
+ .option("--org <organization_id>", "Optional organization id. Must match the stored CLI token.")
1517
+ .option("--org-id <organization_id>", "Optional organization id. Must match the stored CLI token.")
1518
+ .option("--connection-id <connection_id>", "Specific integration connection id. Defaults to the org's active default connection.")
1519
+ .option("--fields <paths>", "Comma-separated response projection paths for large tool outputs.")
1520
+ .option("--return <mode>", "Legacy response shape: raw, compact, or summary. Defaults to raw.")
1521
+ .option("--return-mode <mode>", "Response shape: raw, compact, or summary. Prefer summary for large search responses.")
1522
+ .option("--oxygen-cursor <cursor>", "Short Oxygen cursor returned as oxygen_next_cursor by a previous tool run.")
1523
+ .option("--json", "Print a JSON envelope.")
1524
+ .action(async (toolId, options) => {
1525
+ await handleAsyncAction("tools run", options, async () => requestOxygen("/api/cli/tools/run", {
1526
+ method: "POST",
1527
+ body: {
1528
+ tool_id: toolId,
1529
+ input: parseJsonObject(options.inputJson),
1530
+ ...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
1531
+ ...(readOption(options.org) ? { org_id: readOption(options.org) } : {}),
1532
+ ...(readOption(options.orgId) ? { org_id: readOption(options.orgId) } : {}),
1533
+ ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
1534
+ ...(readOption(options.fields) ? { fields: readCsvOption(options.fields) } : {}),
1535
+ ...(readOption(options["return"]) ? { return: readOption(options["return"]) } : {}),
1536
+ ...(readOption(options.returnMode) ? { return_mode: readOption(options.returnMode) } : {}),
1537
+ ...(readOption(options.oxygenCursor) ? { oxygen_cursor: readOption(options.oxygenCursor) } : {}),
1538
+ },
1539
+ }));
1540
+ }));
1541
+ program
1542
+ .command("enrich-column")
1543
+ .description("High-level table enrichment helpers.")
1544
+ .addCommand(new Command("preview")
1545
+ .description("Preflight an enrichment column without provider calls or credit usage.")
1546
+ .argument("<table>", "Table id or slug.")
1547
+ .option("--source-column <column>", "Source column key or id. For mobile_phone this is the LinkedIn URL column.")
1548
+ .option("--full-name-column <column>", "Column key or id containing the person's full name for work_email. Pair with company-domain/name for Prospeo, LeadMagic, Hunter, ContactOut, or BetterContact.")
1549
+ .option("--first-name-column <column>", "Column key or id containing the person's first name for work_email.")
1550
+ .option("--last-name-column <column>", "Column key or id containing the person's last name for work_email.")
1551
+ .option("--linkedin-url-column <column>", "Column key or id containing the person's LinkedIn URL. Required by Blitz API and enough for Prospeo, Hunter, ContactOut, or BetterContact.")
1552
+ .option("--email-column <column>", "Column key or id containing a known work email for providers that accept email as an identity fallback.")
1553
+ .option("--company-domain-column <column>", "Column key or id containing the company domain for work_email. Pair with full-name/first+last for name+company providers.")
1554
+ .option("--company-name-column <column>", "Column key or id containing the company name for work_email when no domain is available.")
1555
+ .option("--company-linkedin-url-column <column>", "Column key or id containing the company's LinkedIn URL for company identity fallback.")
1556
+ .option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
1557
+ .option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
1558
+ .option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
1559
+ .option("--provider-order <providers>", "Comma-separated provider order. Work email defaults to blitzapi,prospeo,leadmagic,hunter,contactout,bettercontact; phone defaults to blitzapi,prospeo,leadmagic,contactout,bettercontact.")
1560
+ .option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
1561
+ .option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
1562
+ .option("--limit <n>", "Rows to estimate. Defaults to 10.")
1563
+ .option("--all", "Estimate all rows.")
1564
+ .option("--filter-json <json>", "Filter object or array, e.g. '{\"column\":\"mobile_phone_e164\",\"op\":\"is_null\"}'.")
1565
+ .option("--selection-json <json>", "Full row selection object, e.g. '{\"mode\":\"filter\",\"filters\":[{\"column\":\"mobile_phone_e164\",\"op\":\"is_null\"}],\"limit\":100}'.")
1566
+ .option("--only-missing", "Estimate rows missing the capability's normalized output when no explicit selection is passed.")
1567
+ .option("--json", "Print a JSON envelope.")
1568
+ .action(async (table, options) => {
1569
+ await handleAsyncAction("enrich-column preview", options, async () => requestOxygen("/api/cli/enrich-column/preview", {
1570
+ method: "POST",
1571
+ body: buildEnrichColumnBody(table, options),
1572
+ }));
1573
+ }))
1574
+ .addCommand(new Command("run")
1575
+ .description("Create or reuse an enrichment column and queue a background run.")
1576
+ .argument("<table>", "Table id or slug.")
1577
+ .option("--source-column <column>", "Source column key or id. For mobile_phone this is the LinkedIn URL column.")
1578
+ .option("--full-name-column <column>", "Column key or id containing the person's full name for work_email. Pair with company-domain/name for Prospeo, LeadMagic, Hunter, ContactOut, or BetterContact.")
1579
+ .option("--first-name-column <column>", "Column key or id containing the person's first name for work_email.")
1580
+ .option("--last-name-column <column>", "Column key or id containing the person's last name for work_email.")
1581
+ .option("--linkedin-url-column <column>", "Column key or id containing the person's LinkedIn URL. Required by Blitz API and enough for Prospeo, Hunter, ContactOut, or BetterContact.")
1582
+ .option("--email-column <column>", "Column key or id containing a known work email for providers that accept email as an identity fallback.")
1583
+ .option("--company-domain-column <column>", "Column key or id containing the company domain for work_email. Pair with full-name/first+last for name+company providers.")
1584
+ .option("--company-name-column <column>", "Column key or id containing the company name for work_email when no domain is available.")
1585
+ .option("--company-linkedin-url-column <column>", "Column key or id containing the company's LinkedIn URL for company identity fallback.")
1586
+ .requiredOption("--max-credits <credits>", "Required credit ceiling for the queued run.")
1587
+ .option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
1588
+ .option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
1589
+ .option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
1590
+ .option("--provider-order <providers>", "Comma-separated provider order. Work email defaults to blitzapi,prospeo,leadmagic,hunter,contactout,bettercontact; phone defaults to blitzapi,prospeo,leadmagic,contactout,bettercontact.")
1591
+ .option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
1592
+ .option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
1593
+ .option("--limit <n>", "Rows to queue.")
1594
+ .option("--all", "Queue all rows.")
1595
+ .option("--filter-json <json>", "Filter object or array for server-side row selection.")
1596
+ .option("--selection-json <json>", "Full row selection object for server-side row selection.")
1597
+ .option("--only-missing", "Queue rows missing the capability's normalized output when no explicit selection is passed.")
1598
+ .option("--force", "Re-run rows with an existing target enrichment value.")
1599
+ .option("--json", "Print a JSON envelope.")
1600
+ .action(async (table, options) => {
1601
+ await handleAsyncAction("enrich-column run", options, async () => requestOxygen("/api/cli/enrich-column/run", {
1602
+ method: "POST",
1603
+ body: {
1604
+ ...buildEnrichColumnBody(table, options),
1605
+ max_credits: readPositiveNumber(options.maxCredits),
1606
+ ...(options.force ? { force: true } : {}),
1607
+ },
1608
+ }));
1609
+ }));
1610
+ program
1611
+ .command("integrations")
1612
+ .description("Integration connection and event trigger commands.")
1613
+ .addCommand(new Command("events")
1614
+ .description("Configure provider events that can trigger workflows.")
1615
+ .addCommand(new Command("list")
1616
+ .description("List supported provider events and this org's enabled subscriptions.")
1617
+ .option("--source <source>", "Filter by event source, such as hubspot.")
1618
+ .option("--event <event>", "Filter by event type, such as contact.created.")
1619
+ .option("--json", "Print a JSON envelope.")
1620
+ .action(async (options) => {
1621
+ await handleAsyncAction("integrations events list", options, async () => {
1622
+ const query = new URLSearchParams();
1623
+ if (readOption(options.source))
1624
+ query.set("source", readOption(options.source) ?? "");
1625
+ if (readOption(options.event))
1626
+ query.set("event", readOption(options.event) ?? "");
1627
+ const suffix = query.toString() ? `?${query.toString()}` : "";
1628
+ return requestOxygen(`/api/cli/integrations/events${suffix}`);
1629
+ });
1630
+ }))
1631
+ .addCommand(new Command("enable")
1632
+ .description("Enable a provider event for a connected integration account.")
1633
+ .requiredOption("--source <source>", "Event source, such as hubspot.")
1634
+ .requiredOption("--event <event>", "Event type, such as contact.created.")
1635
+ .option("--connection-id <connection_id>", "Specific integration connection id. Defaults to the active default connection.")
1636
+ .option("--json", "Print a JSON envelope.")
1637
+ .action(async (options) => {
1638
+ await handleAsyncAction("integrations events enable", options, async () => requestOxygen("/api/cli/integrations/events/enable", {
1639
+ method: "POST",
1640
+ body: {
1641
+ source: readOption(options.source),
1642
+ event: readOption(options.event),
1643
+ ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
1644
+ },
1645
+ }));
1646
+ }))
1647
+ .addCommand(new Command("disable")
1648
+ .description("Disable a provider event for a connected integration account.")
1649
+ .requiredOption("--source <source>", "Event source, such as hubspot.")
1650
+ .requiredOption("--event <event>", "Event type, such as contact.created.")
1651
+ .option("--connection-id <connection_id>", "Specific integration connection id. Defaults to the active default connection.")
1652
+ .option("--json", "Print a JSON envelope.")
1653
+ .action(async (options) => {
1654
+ await handleAsyncAction("integrations events disable", options, async () => requestOxygen("/api/cli/integrations/events/disable", {
1655
+ method: "POST",
1656
+ body: {
1657
+ source: readOption(options.source),
1658
+ event: readOption(options.event),
1659
+ ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
1660
+ },
1661
+ }));
1662
+ })))
1663
+ .addCommand(new Command("list")
1664
+ .description("List supported Composio integrations and this org's connections.")
1665
+ .option("--json", "Print a JSON envelope.")
1666
+ .action(async (options) => {
1667
+ await handleAsyncAction("integrations list", options, async () => requestOxygen("/api/cli/integrations/composio/list"));
1668
+ }))
1669
+ .addCommand(new Command("connect")
1670
+ .description("Connect a Composio integration. OAuth toolkits return a redirect URL; API-key toolkits accept --api-key.")
1671
+ .argument("<integration_id>", "Integration id, such as 'slack' or 'exa'.")
1672
+ .option("--api-key <value>", "API key for Composio API-key toolkits (e.g. Exa, SerpAPI, Resend).")
1673
+ .option("--json", "Print a JSON envelope.")
1674
+ .action(async (integrationId, options) => {
1675
+ await handleAsyncAction("integrations connect", options, async () => {
1676
+ const apiKey = readOption(options.apiKey)?.trim();
1677
+ return requestOxygen("/api/cli/integrations/composio/start", {
1678
+ method: "POST",
1679
+ body: {
1680
+ integration_id: integrationId,
1681
+ ...(apiKey ? { api_key: apiKey } : {}),
1682
+ },
1683
+ });
1684
+ });
1685
+ }))
1686
+ .addCommand(new Command("disconnect")
1687
+ .description("Disconnect a Composio integration.")
1688
+ .argument("<integration_id>", "Integration id, such as 'slack'.")
1689
+ .option("--json", "Print a JSON envelope.")
1690
+ .action(async (integrationId, options) => {
1691
+ await handleAsyncAction("integrations disconnect", options, async () => requestOxygen("/api/cli/integrations/composio/disconnect", {
1692
+ method: "POST",
1693
+ body: { integration_id: integrationId },
1694
+ }));
1695
+ }))
1696
+ .addCommand(new Command("actions")
1697
+ .description("List Composio actions available for a connected integration.")
1698
+ .argument("<integration_id>", "Integration id, such as 'slack'.")
1699
+ .option("--json", "Print a JSON envelope.")
1700
+ .action(async (integrationId, options) => {
1701
+ await handleAsyncAction("integrations actions", options, async () => {
1702
+ const params = new URLSearchParams({ integration_id: integrationId });
1703
+ return requestOxygen(`/api/cli/integrations/composio/actions?${params.toString()}`);
1704
+ });
1705
+ }))
1706
+ .addCommand(new Command("run")
1707
+ .description("Run a Composio action for a connected integration.")
1708
+ .argument("<integration_id>", "Integration id, such as 'slack'.")
1709
+ .argument("<action_slug>", "Composio action slug, such as 'SLACK_SEND_MESSAGE'.")
1710
+ .option("--input <json>", "Arguments as JSON object. Default: empty object.")
1711
+ .option("--live", "Execute the action live. Default is dry-run.")
1712
+ .option("--dry-run", "Force dry-run (no provider call).")
1713
+ .option("--mode <mode>", "'live' or 'dry_run'. Overridden by --live/--dry-run if provided.")
1714
+ .option("--json", "Print a JSON envelope.")
1715
+ .action(async (integrationId, actionSlug, options) => {
1716
+ await handleAsyncAction("integrations run", options, async () => {
1717
+ const args = readOption(options.input)
1718
+ ? parseJsonObject(readOption(options.input))
1719
+ : {};
1720
+ const mode = resolveComposioRunMode(options);
1721
+ return requestOxygen("/api/cli/integrations/composio/run", {
1722
+ method: "POST",
1723
+ body: {
1724
+ integration_id: integrationId,
1725
+ action_slug: actionSlug,
1726
+ arguments: args,
1727
+ mode,
1728
+ },
1729
+ });
1730
+ });
1731
+ }));
1732
+ program
1733
+ .command("workflows")
1734
+ .description("Durable workflow automation commands.")
1735
+ .addCommand(new Command("templates")
1736
+ .description("Discover and run reusable Oxygen workflow templates.")
1737
+ .addCommand(new Command("search")
1738
+ .description("Search reusable workflow templates.")
1739
+ .argument("[query]", "Search text.")
1740
+ .option("--tag <tag>", "Filter by template tag.")
1741
+ .option("--json", "Print a JSON envelope.")
1742
+ .action(async (query, options) => {
1743
+ await handleAsyncAction("workflows templates search", options, async () => {
1744
+ const params = new URLSearchParams();
1745
+ params.set("query", query ?? "");
1746
+ if (readOption(options.tag))
1747
+ params.set("tag", readOption(options.tag) ?? "");
1748
+ return requestOxygen(`/api/cli/workflows/templates?${params.toString()}`);
1749
+ });
1750
+ }))
1751
+ .addCommand(new Command("describe")
1752
+ .description("Describe one workflow template.")
1753
+ .argument("<template_id>", "Workflow template id.")
1754
+ .option("--json", "Print a JSON envelope.")
1755
+ .action(async (templateId, options) => {
1756
+ await handleAsyncAction("workflows templates describe", options, async () => requestOxygen("/api/cli/workflows/templates/describe", {
1757
+ method: "POST",
1758
+ body: { template_id: templateId },
1759
+ }));
1760
+ }))
1761
+ .addCommand(new Command("preflight")
1762
+ .description("Validate workflow template inputs and preview effects without running it.")
1763
+ .argument("<template_id>", "Workflow template id.")
1764
+ .requiredOption("--input-json <json>", "Template input as a JSON object.")
1765
+ .option("--mode <mode>", "Execution mode: smoke_test, dry_run, or live.")
1766
+ .option("--max-credits <credits>", "Credit ceiling for paid live work.")
1767
+ .option("--json", "Print a JSON envelope.")
1768
+ .action(async (templateId, options) => {
1769
+ await handleAsyncAction("workflows templates preflight", options, async () => requestOxygen("/api/cli/workflows/templates/preflight", {
1770
+ method: "POST",
1771
+ body: workflowTemplateActionBody(templateId, options),
1772
+ }));
1773
+ }))
1774
+ .addCommand(new Command("apply")
1775
+ .description("Create or update a disabled workflow from a template.")
1776
+ .argument("<template_id>", "Workflow template id.")
1777
+ .requiredOption("--input-json <json>", "Template input as a JSON object.")
1778
+ .option("--workflow-id <workflow_id>", "Workflow id to create or update.")
1779
+ .option("--workflow-name <workflow_name>", "Workflow display name.")
1780
+ .option("--mode <mode>", "Mode used for preflight validation.")
1781
+ .option("--max-credits <credits>", "Credit ceiling to store in template defaults.")
1782
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
1783
+ .option("--json", "Print a JSON envelope.")
1784
+ .action(async (templateId, options) => {
1785
+ await handleAsyncAction("workflows templates apply", options, async () => prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/templates/apply", {
1786
+ method: "POST",
1787
+ body: workflowTemplateActionBody(templateId, options),
1788
+ }), options));
1789
+ }))
1790
+ .addCommand(new Command("run")
1791
+ .description("Apply a template as an active workflow and enqueue one run.")
1792
+ .argument("<template_id>", "Workflow template id.")
1793
+ .requiredOption("--input-json <json>", "Template input as a JSON object.")
1794
+ .requiredOption("--mode <mode>", "Execution mode: smoke_test, dry_run, or live.")
1795
+ .option("--workflow-id <workflow_id>", "Workflow id to create or update.")
1796
+ .option("--workflow-name <workflow_name>", "Workflow display name.")
1797
+ .option("--max-credits <credits>", "Required credit ceiling for paid live work.")
1798
+ .option("--approved", "Required for live template runs with side effects.")
1799
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
1800
+ .option("--json", "Print a JSON envelope.")
1801
+ .action(async (templateId, options) => {
1802
+ await handleAsyncAction("workflows templates run", options, async () => prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/templates/run", {
1803
+ method: "POST",
1804
+ body: workflowTemplateActionBody(templateId, options),
1805
+ }), options));
1806
+ })))
1807
+ .addCommand(new Command("schema")
1808
+ .description("Print workflow JSON schemas.")
1809
+ .option("--subject <subject>", "Schema subject: all, apply, call, event, trigger, or manifest.")
1810
+ .option("--json", "Print a JSON envelope.")
1811
+ .action(async (options) => {
1812
+ await handleAsyncAction("workflows schema", options, async () => requestOxygen(`/api/cli/workflows/schema?subject=${encodeURIComponent(options.subject ?? "all")}`));
1813
+ }))
1814
+ .addCommand(new Command("lint")
1815
+ .description("Compile and lint a workflow file without saving it.")
1816
+ .requiredOption("--file <path>", "Workflow module or manifest JSON file.")
1817
+ .option("--json", "Print a JSON envelope.")
1818
+ .action(async (options) => {
1819
+ await handleAsyncAction("workflows lint", options, async () => {
1820
+ const manifest = await compileWorkflowFile(options.file);
1821
+ return requestOxygen("/api/cli/workflows/lint", {
1822
+ method: "POST",
1823
+ body: { manifest },
1824
+ });
1825
+ });
1826
+ }))
1827
+ .addCommand(new Command("apply")
1828
+ .description("Compile and publish a workflow automation.")
1829
+ .requiredOption("--file <path>", "Workflow module or manifest JSON file.")
1830
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
1831
+ .option("--json", "Print a JSON envelope.")
1832
+ .action(async (options) => {
1833
+ await handleAsyncAction("workflows apply", options, async () => {
1834
+ const manifest = await compileWorkflowFile(options.file);
1835
+ return prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/apply", {
1836
+ method: "POST",
1837
+ body: { manifest },
1838
+ }), options);
1839
+ });
1840
+ }))
1841
+ .addCommand(new Command("list")
1842
+ .description("List workflow automations.")
1843
+ .option("--json", "Print a JSON envelope.")
1844
+ .action(async (options) => {
1845
+ await handleAsyncAction("workflows list", options, async () => requestOxygen("/api/cli/workflows"));
1846
+ }))
1847
+ .addCommand(new Command("get")
1848
+ .description("Get one workflow automation.")
1849
+ .argument("<workflow>", "Workflow id, slug, or name.")
1850
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
1851
+ .option("--json", "Print a JSON envelope.")
1852
+ .action(async (workflow, options) => {
1853
+ await handleAsyncAction("workflows get", options, async () => prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/get", {
1854
+ method: "POST",
1855
+ body: { workflow },
1856
+ }), options));
1857
+ }))
1858
+ .addCommand(new Command("duplicate")
1859
+ .description("Duplicate a workflow automation as a disabled copy by default.")
1860
+ .argument("<workflow>", "Source workflow id, slug, or name.")
1861
+ .option("--name <name>", "Display name for the duplicated workflow. Defaults to '<source> Copy'.")
1862
+ .option("--workflow-id <workflow_id>", "Workflow id for the duplicated workflow. Defaults to a unique '<source>-copy' id.")
1863
+ .option("--trigger-id <trigger_id>", "Webhook trigger id for duplicated webhook workflows.")
1864
+ .option("--status <status>", "active or disabled. Defaults to disabled.")
1865
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
1866
+ .option("--json", "Print a JSON envelope.")
1867
+ .action(async (workflow, options) => {
1868
+ await handleAsyncAction("workflows duplicate", options, async () => prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/duplicate", {
1869
+ method: "POST",
1870
+ body: {
1871
+ workflow,
1872
+ ...(readOption(options.name) ? { name: readOption(options.name) } : {}),
1873
+ ...(readOption(options.workflowId) ? { new_workflow_id: readOption(options.workflowId) } : {}),
1874
+ ...(readOption(options.triggerId) ? { trigger_id: readOption(options.triggerId) } : {}),
1875
+ ...(readOption(options.status) ? { status: readOption(options.status) } : {}),
1876
+ },
1877
+ }), options));
1878
+ }))
1879
+ .addCommand(new Command("call")
1880
+ .description("Enqueue a workflow run through its API trigger.")
1881
+ .argument("[workflow]", "Workflow id, slug, or name.")
1882
+ .option("--workflow <workflow>", "Workflow id, slug, or name.")
1883
+ .option("--workflow-id <workflow_id>", "Workflow id or slug.")
1884
+ .option("--workflow-name <workflow_name>", "Workflow name.")
1885
+ .option("--input-json <json>", "Workflow input object. Defaults to {}.")
1886
+ .option("--mode <mode>", "Execution mode: live, dry-run, or smoke-test.")
1887
+ .option("--idempotency-key <key>", "Optional idempotency key.")
1888
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
1889
+ .option("--json", "Print a JSON envelope.")
1890
+ .action(async (workflowArg, options) => {
1891
+ await handleAsyncAction("workflows call", options, async () => prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/call", {
1892
+ method: "POST",
1893
+ body: {
1894
+ workflow: readOption(workflowArg) ?? readOption(options.workflow),
1895
+ ...(readOption(options.workflowId) ? { workflow_id: readOption(options.workflowId) } : {}),
1896
+ ...(readOption(options.workflowName) ? { workflow_name: readOption(options.workflowName) } : {}),
1897
+ input: options.inputJson ? parseJsonObject(options.inputJson) : {},
1898
+ ...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
1899
+ ...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
1900
+ },
1901
+ }), options));
1902
+ }))
1903
+ .addCommand(new Command("events")
1904
+ .description("Workflow event trigger utilities.")
1905
+ .addCommand(new Command("emit")
1906
+ .description("Emit a normalized workflow event and enqueue matching event-triggered workflows.")
1907
+ .requiredOption("--source <source>", "Event source, such as instantly, calendar, or hubspot.")
1908
+ .requiredOption("--event <event>", "Event type, such as email.reply_received.")
1909
+ .requiredOption("--payload-json <json>", "Normalized event payload object.")
1910
+ .option("--raw-payload-json <json>", "Optional raw provider payload object.")
1911
+ .option("--headers-json <json>", "Optional delivery headers object.")
1912
+ .option("--external-event-id <id>", "Provider event id for idempotency and inspection.")
1913
+ .option("--idempotency-key <key>", "Optional explicit event idempotency key.")
1914
+ .option("--mode <mode>", "Execution mode: live, dry-run, or smoke-test.")
1915
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
1916
+ .option("--json", "Print a JSON envelope.")
1917
+ .action(async (options) => {
1918
+ await handleAsyncAction("workflows events emit", options, async () => {
1919
+ const rawPayloadJson = readOption(options.rawPayloadJson);
1920
+ const headersJson = readOption(options.headersJson);
1921
+ return prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/events/emit", {
1922
+ method: "POST",
1923
+ body: {
1924
+ source: readOption(options.source),
1925
+ event: readOption(options.event),
1926
+ payload: parseJsonObject(options.payloadJson),
1927
+ ...(rawPayloadJson ? { raw_payload: parseJsonObject(rawPayloadJson) } : {}),
1928
+ ...(headersJson ? { headers: parseJsonObject(headersJson) } : {}),
1929
+ ...(readOption(options.externalEventId) ? { external_event_id: readOption(options.externalEventId) } : {}),
1930
+ ...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
1931
+ ...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
1932
+ },
1933
+ }), options);
1934
+ });
1935
+ })))
1936
+ .addCommand(new Command("runs")
1937
+ .description("List workflow runs.")
1938
+ .option("--workflow <workflow>", "Workflow id, slug, or name.")
1939
+ .option("--workflow-id <workflow_id>", "Workflow id or slug.")
1940
+ .option("--status <status>", "Filter by queued, running, completed, failed, canceling, or canceled.")
1941
+ .option("--limit <n>", "Maximum runs to return. Defaults to 50.")
1942
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
1943
+ .option("--json", "Print a JSON envelope.")
1944
+ .action(async (options) => {
1945
+ await handleAsyncAction("workflows runs", options, async () => {
1946
+ const query = new URLSearchParams();
1947
+ const workflow = readOption(options.workflow) ?? readOption(options.workflowId);
1948
+ if (workflow)
1949
+ query.set("workflow", workflow);
1950
+ if (readOption(options.status))
1951
+ query.set("status", readOption(options.status) ?? "");
1952
+ const limit = readPositiveInt(options.limit);
1953
+ if (limit)
1954
+ query.set("limit", String(limit));
1955
+ const suffix = query.toString() ? `?${query.toString()}` : "";
1956
+ return prepareWorkflowCliOutput(await requestOxygen(`/api/cli/workflows/runs${suffix}`), options);
1957
+ });
1958
+ }))
1959
+ .addCommand(new Command("failures")
1960
+ .description("List failed workflow runs and trigger scheduler failures.")
1961
+ .option("--limit <n>", "Maximum failures per group. Defaults to 25; server cap is 100.")
1962
+ .option("--json", "Print a JSON envelope.")
1963
+ .action(async (options) => {
1964
+ await handleAsyncAction("workflows failures", options, async () => {
1965
+ const query = new URLSearchParams();
1966
+ const limit = readPositiveInt(options.limit);
1967
+ if (limit)
1968
+ query.set("limit", String(limit));
1969
+ const suffix = query.toString() ? `?${query.toString()}` : "";
1970
+ return requestOxygen(`/api/cli/workflows/failures${suffix}`);
1971
+ });
1972
+ }))
1973
+ .addCommand(new Command("run")
1974
+ .description("Get one workflow run.")
1975
+ .argument("<run_id>", "Workflow run UUID.")
1976
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
1977
+ .option("--json", "Print a JSON envelope.")
1978
+ .action(async (runId, options) => {
1979
+ await handleAsyncAction("workflows run", options, async () => prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/run", {
1980
+ method: "POST",
1981
+ body: { run_id: runId },
1982
+ }), options));
1983
+ }))
1984
+ .addCommand(new Command("tail")
1985
+ .description("Poll a workflow run until it finishes.")
1986
+ .argument("<run_id>", "Workflow run UUID.")
1987
+ .option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
1988
+ .option("--interval-seconds <n>", "Polling interval. Defaults to 2.")
1989
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
1990
+ .option("--json", "Print a JSON envelope.")
1991
+ .action(async (runId, options) => {
1992
+ await handleAsyncAction("workflows tail", options, async () => prepareWorkflowCliOutput(await tailWorkflowRun(runId, options), options));
1993
+ }))
1994
+ .addCommand(new Command("cancel")
1995
+ .description("Cancel a queued or running workflow run.")
1996
+ .argument("<run_id>", "Workflow run UUID.")
1997
+ .option("--json", "Print a JSON envelope.")
1998
+ .action(async (runId, options) => {
1999
+ await handleAsyncAction("workflows cancel", options, async () => requestOxygen("/api/cli/workflows/cancel", {
2000
+ method: "POST",
2001
+ body: { run_id: runId },
2002
+ }));
2003
+ }))
2004
+ .addCommand(new Command("enable")
2005
+ .description("Enable a workflow automation and its current trigger.")
2006
+ .argument("<workflow>", "Workflow id, slug, or name.")
2007
+ .option("--json", "Print a JSON envelope.")
2008
+ .action(async (workflow, options) => {
2009
+ await handleAsyncAction("workflows enable", options, async () => requestOxygen("/api/cli/workflows/enable", {
2010
+ method: "POST",
2011
+ body: { workflow },
2012
+ }));
2013
+ }))
2014
+ .addCommand(new Command("disable")
2015
+ .description("Disable a workflow automation and its current trigger.")
2016
+ .argument("<workflow>", "Workflow id, slug, or name.")
2017
+ .option("--json", "Print a JSON envelope.")
2018
+ .action(async (workflow, options) => {
2019
+ await handleAsyncAction("workflows disable", options, async () => requestOxygen("/api/cli/workflows/disable", {
2020
+ method: "POST",
2021
+ body: { workflow },
2022
+ }));
2023
+ }));
2024
+ program
2025
+ .command("skills")
2026
+ .description("Agent skill installation commands.")
2027
+ .addCommand(new Command("install")
2028
+ .description("Install Oxygen agent skills into local agent skill directories.")
2029
+ .option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
2030
+ .option("--agents <agents>", "Space or comma separated agents. Defaults to codex, claude-code, and cursor.")
2031
+ .option("--skill <skill>", "Skill name or '*'. Defaults to '*'.")
2032
+ .option("--project", "Install into the current project instead of global agent scope.")
2033
+ .option("--copy", "Copy skill files instead of symlinking when supported by npx skills.")
2034
+ .option("--json", "Print a JSON envelope.")
2035
+ .action(async (options) => {
2036
+ await handleAsyncAction("skills install", options, async () => installAgentSkills(options));
2037
+ }));
2038
+ return program;
2039
+ }
2040
+ async function compileWorkflowFile(filePath) {
2041
+ const absolutePath = resolve(filePath);
2042
+ const source = readFileSync(absolutePath, "utf8");
2043
+ const extension = extname(absolutePath).toLowerCase();
2044
+ if (extension === ".json") {
2045
+ const parsed = parseJsonObject(source);
2046
+ const manifest = isAnyWorkflowManifest(parsed)
2047
+ ? parsed
2048
+ : isAnyWorkflowManifest(parsed.manifest)
2049
+ ? parsed.manifest
2050
+ : null;
2051
+ if (!manifest) {
2052
+ throw new OxygenError("invalid_workflow_manifest", "Workflow JSON must be a manifest or { manifest } object.", {
2053
+ details: { file: filePath },
2054
+ exitCode: 1,
2055
+ });
2056
+ }
2057
+ if (isRecipeManifest(manifest))
2058
+ return manifest;
2059
+ assertWorkflowManifest(manifest);
2060
+ return manifest;
2061
+ }
2062
+ if (extension !== ".js" && extension !== ".mjs" && extension !== ".ts") {
2063
+ throw new OxygenError("invalid_workflow_file", "Workflow file must be .js, .mjs, .ts, or .json.", {
2064
+ details: { file: filePath },
2065
+ exitCode: 1,
2066
+ });
2067
+ }
2068
+ const recipeManifest = await tryCompileRecipeFile(absolutePath, source);
2069
+ if (recipeManifest)
2070
+ return recipeManifest;
2071
+ const mod = extension === ".ts"
2072
+ ? await importTranspiledWorkflowModule(absolutePath, source)
2073
+ : await importJavaScriptWorkflowModule(absolutePath, source);
2074
+ const exported = mod.default ?? mod.workflow ?? mod.manifest;
2075
+ if (isWorkflowDefinition(exported)) {
2076
+ return compileWorkflowDefinition(exported, {
2077
+ source,
2078
+ sourceHash: createHash("sha256").update(source).digest("hex"),
2079
+ });
2080
+ }
2081
+ if (isWorkflowManifest(exported)) {
2082
+ assertWorkflowManifest(exported);
2083
+ return exported;
2084
+ }
2085
+ throw new OxygenError("invalid_workflow_file", "Workflow module must export a workflow definition, recipe, or manifest.", {
2086
+ details: { file: filePath },
2087
+ exitCode: 1,
2088
+ });
2089
+ }
2090
+ async function tryCompileRecipeFile(absolutePath, source) {
2091
+ if (!referencesRecipeSdk(source))
2092
+ return null;
2093
+ const esbuild = await import("esbuild");
2094
+ const result = await esbuild.build({
2095
+ entryPoints: [absolutePath],
2096
+ bundle: true,
2097
+ write: false,
2098
+ format: "esm",
2099
+ platform: "node",
2100
+ target: "node22",
2101
+ nodePaths: RECIPE_ESBUILD_NODE_PATHS,
2102
+ minify: false,
2103
+ sourcemap: false,
2104
+ legalComments: "none",
2105
+ logLevel: "silent",
2106
+ metafile: true,
2107
+ });
2108
+ const out = result.outputFiles?.[0];
2109
+ if (!out) {
2110
+ throw new OxygenError("invalid_workflow_file", "esbuild produced no output for the recipe.", {
2111
+ details: { file: absolutePath },
2112
+ exitCode: 1,
2113
+ });
2114
+ }
2115
+ const bundle = out.text;
2116
+ try {
2117
+ assertRecipeBundleSafe(bundle);
2118
+ }
2119
+ catch (error) {
2120
+ throw new OxygenError("unsafe_recipe_bundle", error instanceof Error ? error.message : "Durable recipe bundle is unsafe.", {
2121
+ details: { file: absolutePath },
2122
+ exitCode: 1,
2123
+ });
2124
+ }
2125
+ let recipe;
2126
+ try {
2127
+ recipe = await loadRecipeFromBundle(bundle, absolutePath);
2128
+ }
2129
+ catch (error) {
2130
+ if (error instanceof OxygenError)
2131
+ throw error;
2132
+ throw new OxygenError("invalid_recipe_definition", error instanceof Error ? error.message : "Recipe definition is invalid.", {
2133
+ details: { file: absolutePath },
2134
+ exitCode: 1,
2135
+ });
2136
+ }
2137
+ const sourceHash = createHash("sha256").update(bundle).digest("hex");
2138
+ const manifest = buildRecipeManifest({
2139
+ workflowId: recipe.id,
2140
+ workflowName: recipe.name,
2141
+ ...(recipe.status ? { status: recipe.status } : {}),
2142
+ ...(recipe.trigger ? { trigger: recipe.trigger } : {}),
2143
+ ...(recipe.inputSchema ? { inputSchema: recipe.inputSchema } : {}),
2144
+ bundle,
2145
+ toolsUsed: recipe.tools,
2146
+ sourceHash,
2147
+ });
2148
+ return manifest;
2149
+ }
2150
+ function referencesRecipeSdk(source) {
2151
+ return /["']@oxygen\/recipe-sdk["']/.test(source);
2152
+ }
2153
+ // Escape Next static analysis (the CLI is bundled by tsc, but mirror the
2154
+ // worker's escape so both load identically).
2155
+ const dynamicRecipeImport = new Function("specifier", "return import(specifier);");
2156
+ async function importRecipeModule(specifier) {
2157
+ try {
2158
+ return await dynamicRecipeImport(specifier);
2159
+ }
2160
+ catch (error) {
2161
+ if (error instanceof TypeError && error.message.includes("dynamic import callback")) {
2162
+ return await import(specifier);
2163
+ }
2164
+ throw error;
2165
+ }
2166
+ }
2167
+ async function loadRecipeFromBundle(bundle, filePath) {
2168
+ const dir = mkdtempSync(`${tmpdir()}/oxygen-recipe-`);
2169
+ const compiledPath = `${dir}/recipe.mjs`;
2170
+ writeFileSync(compiledPath, bundle, "utf8");
2171
+ try {
2172
+ const url = pathToFileURL(compiledPath);
2173
+ url.searchParams.set("v", createHash("sha256").update(bundle).digest("hex").slice(0, 16));
2174
+ const mod = await importRecipeModule(url.href);
2175
+ const candidate = mod.default ?? mod.recipe ?? mod.workflow;
2176
+ if (!isRecipeDefinition(candidate)) {
2177
+ throw new OxygenError("invalid_workflow_file", "Recipe module must export a defineRecipe() result as default.", {
2178
+ details: { file: filePath },
2179
+ exitCode: 1,
2180
+ });
2181
+ }
2182
+ return candidate;
2183
+ }
2184
+ finally {
2185
+ rmSync(dir, { recursive: true, force: true });
2186
+ }
2187
+ }
2188
+ async function importJavaScriptWorkflowModule(absolutePath, source) {
2189
+ const url = pathToFileURL(absolutePath);
2190
+ url.searchParams.set("v", createHash("sha256").update(source).digest("hex").slice(0, 16));
2191
+ return await import(url.href);
2192
+ }
2193
+ async function importTranspiledWorkflowModule(absolutePath, source) {
2194
+ if (/\bfrom\s+["']\.\.?\//.test(source) || /\bimport\s*\(\s*["']\.\.?\//.test(source)) {
2195
+ throw new OxygenError("unsupported_workflow_file", "TypeScript workflow files cannot use relative imports in workflow modules.", {
2196
+ details: { file: absolutePath },
2197
+ exitCode: 1,
2198
+ });
2199
+ }
2200
+ const ts = await import("typescript");
2201
+ const transpiled = ts.transpileModule(source, {
2202
+ fileName: absolutePath,
2203
+ compilerOptions: {
2204
+ module: ts.ModuleKind.ESNext,
2205
+ target: ts.ScriptTarget.ES2022,
2206
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
2207
+ esModuleInterop: true,
2208
+ },
2209
+ }).outputText;
2210
+ const workflowsUrl = await import.meta.resolve("@oxygen/workflows");
2211
+ const rewritten = transpiled
2212
+ .replaceAll("from \"@oxygen/workflows\"", `from "${workflowsUrl}"`)
2213
+ .replaceAll("from '@oxygen/workflows'", `from "${workflowsUrl}"`);
2214
+ const dir = mkdtempSync(`${tmpdir()}/oxygen-workflow-`);
2215
+ const compiledPath = `${dir}/workflow.mjs`;
2216
+ writeFileSync(compiledPath, rewritten, "utf8");
2217
+ try {
2218
+ return await importJavaScriptWorkflowModule(compiledPath, rewritten);
2219
+ }
2220
+ finally {
2221
+ rmSync(dir, { recursive: true, force: true });
2222
+ }
2223
+ }
2224
+ async function tailWorkflowRun(runId, options) {
2225
+ const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
2226
+ ?? WORKFLOW_TAIL_DEFAULT_TIMEOUT_SECONDS;
2227
+ const intervalSeconds = readPositiveInt(options.intervalSeconds)
2228
+ ?? WORKFLOW_TAIL_DEFAULT_INTERVAL_SECONDS;
2229
+ const startedAt = Date.now();
2230
+ const deadline = startedAt + timeoutSeconds * 1000;
2231
+ let polls = 0;
2232
+ while (true) {
2233
+ polls += 1;
2234
+ const latest = await requestOxygen("/api/cli/workflows/run", {
2235
+ method: "POST",
2236
+ body: { run_id: runId },
2237
+ });
2238
+ const run = latest.run ?? latest;
2239
+ const status = readRecordString(run, "status");
2240
+ if (isTerminalWorkflowRunStatus(status)) {
2241
+ return {
2242
+ run,
2243
+ workflowRunId: readRecordString(run, "id") ?? runId,
2244
+ status,
2245
+ terminal: true,
2246
+ polls,
2247
+ elapsedMs: Date.now() - startedAt,
2248
+ };
2249
+ }
2250
+ const remainingMs = deadline - Date.now();
2251
+ if (remainingMs <= 0) {
2252
+ throw new OxygenError("workflow_tail_timeout", "Timed out waiting for workflow run to finish.", {
2253
+ details: {
2254
+ workflow_run_id: runId,
2255
+ status: status ?? null,
2256
+ timeout_seconds: timeoutSeconds,
2257
+ polls,
2258
+ },
2259
+ exitCode: 1,
2260
+ });
2261
+ }
2262
+ await sleep(Math.min(intervalSeconds * 1000, remainingMs));
2263
+ }
2264
+ }
2265
+ function workflowTemplateActionBody(templateId, options) {
2266
+ const inputJson = readOption(options.inputJson);
2267
+ const maxCredits = readPositiveNumber(options.maxCredits);
2268
+ return {
2269
+ template_id: templateId,
2270
+ input: inputJson ? parseJsonObject(inputJson) : {},
2271
+ ...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
2272
+ ...(readOption(options.workflowId) ? { workflow_id: readOption(options.workflowId) } : {}),
2273
+ ...(readOption(options.workflowName) ? { workflow_name: readOption(options.workflowName) } : {}),
2274
+ ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
2275
+ ...(options.approved ? { approved: true } : {}),
2276
+ };
2277
+ }
2278
+ function prepareWorkflowCliOutput(value, options) {
2279
+ const normalized = normalizeWorkflowRunErrors(value);
2280
+ return (options.includeBundle ? normalized : stripWorkflowBundles(normalized));
2281
+ }
2282
+ function stripWorkflowBundles(value) {
2283
+ if (Array.isArray(value))
2284
+ return value.map((entry) => stripWorkflowBundles(entry));
2285
+ if (!isRecord(value))
2286
+ return value;
2287
+ const shouldStripBundle = typeof value.bundle === "string"
2288
+ && (value.runtime === "durable"
2289
+ || value.bundle_format === "esm"
2290
+ || (typeof value.compiler_version === "string" && value.compiler_version.startsWith("oxygen-recipes-")));
2291
+ const output = {};
2292
+ for (const [key, entry] of Object.entries(value)) {
2293
+ if (shouldStripBundle && key === "bundle")
2294
+ continue;
2295
+ output[key] = stripWorkflowBundles(entry);
2296
+ }
2297
+ return output;
2298
+ }
2299
+ function normalizeWorkflowRunErrors(value) {
2300
+ if (Array.isArray(value))
2301
+ return value.map((entry) => normalizeWorkflowRunErrors(entry));
2302
+ if (!isRecord(value))
2303
+ return value;
2304
+ const output = {};
2305
+ for (const [key, entry] of Object.entries(value)) {
2306
+ output[key] = normalizeWorkflowRunErrors(entry);
2307
+ }
2308
+ if (output.status === "failed" && (output.error === null || output.error === undefined) && output.lastError !== null && output.lastError !== undefined) {
2309
+ output.error = output.lastError;
2310
+ }
2311
+ if ((output.error === null || output.error === undefined) && isRecord(output.run) && output.run.error !== undefined) {
2312
+ output.error = output.run.error;
2313
+ }
2314
+ return output;
2315
+ }
2316
+ function isTerminalWorkflowRunStatus(status) {
2317
+ return status === "completed" || status === "failed" || status === "canceled";
2318
+ }
2319
+ function readTableRunActions(options) {
2320
+ if (options.actionsJson && readOption(options.column)) {
2321
+ throw new OxygenError("invalid_table_run", "Pass either --actions-json or --column, not both.", {
2322
+ exitCode: 1,
2323
+ });
2324
+ }
2325
+ if (options.actionsJson)
2326
+ return parseJsonArray(options.actionsJson);
2327
+ const column = readOption(options.column);
2328
+ if (column)
2329
+ return [{ type: "tool_column", column }];
2330
+ throw new OxygenError("invalid_table_run", "Pass --column or --actions-json.", {
2331
+ exitCode: 1,
2332
+ });
2333
+ }
2334
+ function readTableRunSelection(options) {
2335
+ const hasAll = Boolean(options.all);
2336
+ const limit = readPositiveInt(options.limit);
2337
+ const rowIds = readCsvOption(options.rowIds);
2338
+ const filterSelection = readFilterSelectionOption(options.filterJson);
2339
+ const selectedModes = [hasAll, Boolean(limit), rowIds.length > 0, Boolean(filterSelection)].filter(Boolean).length;
2340
+ if (selectedModes > 1) {
2341
+ throw new OxygenError("invalid_table_run", "Pass only one of --all, --limit, --row-ids, or --filter-json.", {
2342
+ exitCode: 1,
2343
+ });
2344
+ }
2345
+ if (hasAll)
2346
+ return { mode: "all" };
2347
+ if (limit)
2348
+ return { mode: "limit", limit };
2349
+ if (rowIds.length > 0)
2350
+ return { mode: "row_ids", row_ids: rowIds };
2351
+ if (filterSelection)
2352
+ return filterSelection;
2353
+ throw new OxygenError("invalid_table_run", "Pass --all, --limit, --row-ids, or --filter-json.", {
2354
+ exitCode: 1,
2355
+ });
2356
+ }
2357
+ function readFilterSelectionOption(value) {
2358
+ const filters = readFilterJsonOption(value);
2359
+ return filters ? { mode: "filter", filters } : undefined;
2360
+ }
2361
+ function readSelectionJsonOption(value) {
2362
+ const raw = readOption(value);
2363
+ if (!raw)
2364
+ return undefined;
2365
+ return parseJsonObject(raw);
2366
+ }
2367
+ function readFilterJsonOption(value) {
2368
+ const raw = readOption(value);
2369
+ if (!raw)
2370
+ return undefined;
2371
+ let parsed;
2372
+ try {
2373
+ parsed = JSON.parse(raw);
2374
+ }
2375
+ catch (error) {
2376
+ throw new OxygenError("invalid_filter", "--filter-json must be valid JSON.", {
2377
+ details: { reason: error instanceof Error ? error.message : String(error) },
2378
+ exitCode: 1,
2379
+ });
2380
+ }
2381
+ const filters = Array.isArray(parsed) ? parsed : [parsed];
2382
+ if (filters.length === 0
2383
+ || filters.some((entry) => !entry || typeof entry !== "object" || Array.isArray(entry))) {
2384
+ throw new OxygenError("invalid_filter", "--filter-json must be a filter object or non-empty array of filter objects.", {
2385
+ exitCode: 1,
2386
+ });
2387
+ }
2388
+ return filters;
2389
+ }
2390
+ function parseStringArray(value) {
2391
+ const parsed = parseJsonArray(value);
2392
+ const labels = parsed
2393
+ .map((entry) => typeof entry === "string" ? entry.trim() : "")
2394
+ .filter(Boolean);
2395
+ if (labels.length !== parsed.length || labels.length === 0) {
2396
+ throw new OxygenError("invalid_session", "Session steps must be a non-empty JSON array of strings.", {
2397
+ exitCode: 1,
2398
+ });
2399
+ }
2400
+ return labels;
2401
+ }
2402
+ function normalizeSessionStepStatus(value) {
2403
+ const normalized = value?.trim().toLowerCase();
2404
+ if (normalized === "pending"
2405
+ || normalized === "running"
2406
+ || normalized === "completed"
2407
+ || normalized === "error"
2408
+ || normalized === "skipped") {
2409
+ return normalized;
2410
+ }
2411
+ throw new OxygenError("invalid_session_status", "Session status must be pending, running, completed, error, or skipped.", {
2412
+ details: { status: value },
2413
+ exitCode: 1,
2414
+ });
2415
+ }
2416
+ function installAgentSkills(options) {
2417
+ const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
2418
+ const indexUrl = `${apiUrl}/.well-known/skills/index.json`;
2419
+ const agents = readWords(options.agents ?? "codex claude-code cursor");
2420
+ const skill = readOption(options.skill) ?? "*";
2421
+ const args = [
2422
+ "skills",
2423
+ "add",
2424
+ indexUrl,
2425
+ "--agents",
2426
+ ...agents,
2427
+ "--yes",
2428
+ "--skill",
2429
+ skill,
2430
+ "--full-depth",
2431
+ ];
2432
+ if (!options.project)
2433
+ args.push("--global");
2434
+ if (options.copy)
2435
+ args.push("--copy");
2436
+ let output = "";
2437
+ try {
2438
+ output = execFileSync("npx", args, {
2439
+ encoding: "utf8",
2440
+ stdio: ["ignore", "pipe", "pipe"],
2441
+ });
2442
+ }
2443
+ catch (error) {
2444
+ const failure = error;
2445
+ throw new OxygenError("skills_install_failed", "Unable to install Oxygen agent skills.", {
2446
+ details: {
2447
+ index_url: indexUrl,
2448
+ stdout: failure.stdout?.slice(0, 2000) ?? "",
2449
+ stderr: failure.stderr?.slice(0, 2000) ?? failure.message ?? "unknown",
2450
+ },
2451
+ exitCode: 1,
2452
+ });
2453
+ }
2454
+ return {
2455
+ indexUrl,
2456
+ agents,
2457
+ skill,
2458
+ scope: options.project ? "project" : "global",
2459
+ output,
2460
+ };
2461
+ }
2462
+ function readWords(value) {
2463
+ return value
2464
+ .split(/[,\s]+/)
2465
+ .map((entry) => entry.trim())
2466
+ .filter(Boolean);
2467
+ }
2468
+ async function importRows(table, options) {
2469
+ const format = normalizeRowsFormat(options.format, inferRowsFileFormat(options.file));
2470
+ const parsedRows = await readRowsFile(options.file, format, options.sheet);
2471
+ if (parsedRows.length === 0) {
2472
+ throw new OxygenError("invalid_rows", "Import file did not contain any rows.", {
2473
+ exitCode: 1,
2474
+ });
2475
+ }
2476
+ const target = await prepareImportTarget(table, options, parsedRows);
2477
+ const batchSize = normalizeImportBatchSize(options.batchSize);
2478
+ const sourceHash = hashImportFile(options.file);
2479
+ const shouldUseBackground = options.background
2480
+ || (!options.sync && target.rows.length > LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD);
2481
+ if (shouldUseBackground) {
2482
+ return enqueueImportRows(target.tableRef, options, target.rows, format, batchSize, {
2483
+ autoBackground: !options.background,
2484
+ sourceHash,
2485
+ createdTable: target.createdTable,
2486
+ ...(target.upsertKey ? { upsertKey: target.upsertKey } : {}),
2487
+ });
2488
+ }
2489
+ let rowCount = 0;
2490
+ let insertedCount = 0;
2491
+ let updatedCount = 0;
2492
+ let warningCount = 0;
2493
+ const warnings = [];
2494
+ let warningsTruncated = false;
2495
+ let lastResult = null;
2496
+ for (const batch of chunk(target.rows, batchSize)) {
2497
+ const result = await requestOxygen(target.upsertKey ? "/api/cli/tables/rows/upsert" : "/api/cli/tables/rows", {
2498
+ method: "POST",
2499
+ timeoutMs: 300_000,
2500
+ body: {
2501
+ table: target.tableRef,
2502
+ rows: batch,
2503
+ ...(target.upsertKey ? { key: target.upsertKey } : {}),
2504
+ },
2505
+ });
2506
+ rowCount += readCount(result.rowCount);
2507
+ insertedCount += readCount(result.insertedCount);
2508
+ updatedCount += readCount(result.updatedCount);
2509
+ const batchWarningCount = readCount(result.warningCount);
2510
+ warningCount += batchWarningCount;
2511
+ const batchWarnings = Array.isArray(result.warnings) ? result.warnings : [];
2512
+ for (const warning of batchWarnings) {
2513
+ if (warnings.length < 20)
2514
+ warnings.push(warning);
2515
+ }
2516
+ warningsTruncated = warningsTruncated || result.warningsTruncated === true || warningCount > warnings.length;
2517
+ lastResult = result;
2518
+ }
2519
+ return {
2520
+ table: lastResult?.table ?? null,
2521
+ ...(target.createdTable ? { createdTable: target.createdTable } : {}),
2522
+ rowCount,
2523
+ insertedCount,
2524
+ updatedCount,
2525
+ ...(warningCount > 0 ? {
2526
+ warnings,
2527
+ warningCount,
2528
+ ...(warningsTruncated ? { warningsTruncated: true } : {}),
2529
+ } : {}),
2530
+ batchCount: Math.ceil(target.rows.length / batchSize),
2531
+ batchSize,
2532
+ ...(target.tableWebUrl ? { table_web_url: target.tableWebUrl } : {}),
2533
+ ...(readRecordString(lastResult, "web_url") ? { web_url: readRecordString(lastResult, "web_url") } : {}),
2534
+ };
2535
+ }
2536
+ async function prepareImportTarget(table, options, parsedRows) {
2537
+ if (options.create && table) {
2538
+ throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
2539
+ exitCode: 1,
2540
+ });
2541
+ }
2542
+ if (!options.create && !table) {
2543
+ throw new OxygenError("invalid_import_target", "Pass a table argument or --create <name>.", {
2544
+ exitCode: 1,
2545
+ });
2546
+ }
2547
+ if (!options.create) {
2548
+ return {
2549
+ tableRef: table,
2550
+ rows: parsedRows,
2551
+ createdTable: null,
2552
+ tableWebUrl: null,
2553
+ upsertKey: options.upsertKey,
2554
+ };
2555
+ }
2556
+ const normalized = normalizeRowsForNewTable(parsedRows);
2557
+ const created = await requestOxygen("/api/cli/tables", {
2558
+ method: "POST",
2559
+ body: {
2560
+ name: options.create,
2561
+ columns: normalized.columns,
2562
+ ...(readOption(options.project) ? { project: readOption(options.project) } : {}),
2563
+ },
2564
+ });
2565
+ const tableRecord = readRecord(created, "table");
2566
+ const createdSlug = readRecordString(tableRecord, "slug") ?? readRecordString(tableRecord, "id");
2567
+ if (!createdSlug) {
2568
+ throw new OxygenError("invalid_response", "Oxygen API response is missing the created table slug.", {
2569
+ exitCode: 1,
2570
+ });
2571
+ }
2572
+ return {
2573
+ tableRef: createdSlug,
2574
+ rows: normalized.rows,
2575
+ createdTable: created,
2576
+ tableWebUrl: tableWebUrl(createdSlug),
2577
+ upsertKey: normalizeCreatedTableUpsertKey(options.upsertKey, normalized.keyBySource),
2578
+ };
2579
+ }
2580
+ async function enqueueImportRows(table, options, rows, format, batchSize, context) {
2581
+ const batches = chunk(rows, batchSize);
2582
+ const maxConcurrency = readPositiveInt(options.maxConcurrency);
2583
+ const upsertKey = context.upsertKey ?? options.upsertKey;
2584
+ const idempotencyKey = buildImportIdempotencyKey({
2585
+ table,
2586
+ sourceHash: context.sourceHash,
2587
+ format,
2588
+ mode: upsertKey ? "upsert" : "insert",
2589
+ upsertKey: upsertKey ?? null,
2590
+ batchSize,
2591
+ });
2592
+ const created = await requestOxygen("/api/cli/table-ingestion-runs", {
2593
+ method: "POST",
2594
+ body: {
2595
+ table,
2596
+ source_type: `file.${format}`,
2597
+ mode: upsertKey ? "upsert" : "insert",
2598
+ ...(upsertKey ? { upsert_key: upsertKey } : {}),
2599
+ expected_item_count: batches.length,
2600
+ ...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
2601
+ metadata: {
2602
+ command: "tables import",
2603
+ file_name: basename(options.file),
2604
+ format,
2605
+ ...(options.sheet ? { sheet: options.sheet } : {}),
2606
+ requested_row_count: rows.length,
2607
+ batch_size: batchSize,
2608
+ auto_background: context.autoBackground,
2609
+ source_hash: context.sourceHash,
2610
+ idempotency_key: idempotencyKey,
2611
+ },
2612
+ },
2613
+ });
2614
+ const ingestionRunId = readRequiredResponseString(created, "id");
2615
+ if (isTerminalTableIngestionStatus(readRecordString(created, "status"))) {
2616
+ return {
2617
+ ingestionRun: created,
2618
+ ingestionRunId,
2619
+ ...(context.createdTable ? { createdTable: context.createdTable } : {}),
2620
+ ...(readRecordString(created, "web_url") ? { web_url: readRecordString(created, "web_url") } : {}),
2621
+ table_web_url: tableWebUrl(table),
2622
+ background: true,
2623
+ autoBackground: context.autoBackground,
2624
+ sourceType: `file.${format}`,
2625
+ rowCount: rows.length,
2626
+ enqueuedItems: 0,
2627
+ duplicatePositions: 0,
2628
+ batchCount: batches.length,
2629
+ batchSize,
2630
+ mode: upsertKey ? "upsert" : "insert",
2631
+ upsertKey: upsertKey ?? null,
2632
+ idempotencyKey,
2633
+ };
2634
+ }
2635
+ let insertedItems = 0;
2636
+ let duplicatePositions = 0;
2637
+ let latestRun = created;
2638
+ for (const [index, batch] of batches.entries()) {
2639
+ const appended = await requestOxygen(`/api/cli/table-ingestion-runs/${encodeURIComponent(ingestionRunId)}/items`, {
2640
+ method: "POST",
2641
+ body: {
2642
+ items: [
2643
+ {
2644
+ position: index,
2645
+ payload: { rows: batch },
2646
+ },
2647
+ ],
2648
+ },
2649
+ });
2650
+ insertedItems += readCount(appended.inserted);
2651
+ duplicatePositions += readCount(appended.duplicatePositions);
2652
+ latestRun = appended.run ?? latestRun;
2653
+ }
2654
+ return {
2655
+ ingestionRun: latestRun,
2656
+ ingestionRunId,
2657
+ ...(context.createdTable ? { createdTable: context.createdTable } : {}),
2658
+ ...(readRecordString(latestRun, "web_url") ? { web_url: readRecordString(latestRun, "web_url") } : {}),
2659
+ table_web_url: tableWebUrl(table),
2660
+ background: true,
2661
+ autoBackground: context.autoBackground,
2662
+ sourceType: `file.${format}`,
2663
+ rowCount: rows.length,
2664
+ enqueuedItems: insertedItems,
2665
+ duplicatePositions,
2666
+ batchCount: batches.length,
2667
+ batchSize,
2668
+ mode: upsertKey ? "upsert" : "insert",
2669
+ upsertKey: upsertKey ?? null,
2670
+ idempotencyKey,
2671
+ };
2672
+ }
2673
+ async function waitForTableIngestionRun(runId, options) {
2674
+ const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
2675
+ ?? TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS;
2676
+ const intervalSeconds = readPositiveInt(options.intervalSeconds)
2677
+ ?? TABLE_INGESTION_WAIT_DEFAULT_INTERVAL_SECONDS;
2678
+ const startedAt = Date.now();
2679
+ const deadline = startedAt + timeoutSeconds * 1000;
2680
+ let polls = 0;
2681
+ while (true) {
2682
+ polls += 1;
2683
+ const latestRun = await requestOxygen(`/api/cli/table-ingestion-runs/${encodeURIComponent(runId)}`);
2684
+ const status = readRecordString(latestRun, "status");
2685
+ if (isTerminalTableIngestionStatus(status)) {
2686
+ return {
2687
+ ingestionRun: latestRun,
2688
+ ingestionRunId: readRecordString(latestRun, "id") ?? runId,
2689
+ status,
2690
+ terminal: true,
2691
+ polls,
2692
+ elapsedMs: Date.now() - startedAt,
2693
+ ...(readRecordString(latestRun, "web_url") ? { web_url: readRecordString(latestRun, "web_url") } : {}),
2694
+ };
2695
+ }
2696
+ const remainingMs = deadline - Date.now();
2697
+ if (remainingMs <= 0) {
2698
+ throw new OxygenError("table_ingestion_wait_timeout", "Timed out waiting for table ingestion run to finish.", {
2699
+ details: {
2700
+ ingestion_run_id: runId,
2701
+ status: status ?? null,
2702
+ timeout_seconds: timeoutSeconds,
2703
+ polls,
2704
+ },
2705
+ exitCode: 1,
2706
+ });
2707
+ }
2708
+ await sleep(Math.min(intervalSeconds * 1000, remainingMs));
2709
+ }
2710
+ }
2711
+ async function waitForTableActionRun(runId, options) {
2712
+ const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
2713
+ ?? TABLE_ACTION_RUN_WAIT_DEFAULT_TIMEOUT_SECONDS;
2714
+ const intervalSeconds = readPositiveInt(options.intervalSeconds)
2715
+ ?? TABLE_ACTION_RUN_WAIT_DEFAULT_INTERVAL_SECONDS;
2716
+ const startedAt = Date.now();
2717
+ const deadline = startedAt + timeoutSeconds * 1000;
2718
+ let polls = 0;
2719
+ while (true) {
2720
+ polls += 1;
2721
+ const latestRun = await requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}`);
2722
+ const status = readRecordString(latestRun, "status");
2723
+ if (isTerminalTableActionRunStatus(status)) {
2724
+ return {
2725
+ actionRun: latestRun,
2726
+ actionRunId: readRecordString(latestRun, "id") ?? runId,
2727
+ status,
2728
+ terminal: true,
2729
+ polls,
2730
+ elapsedMs: Date.now() - startedAt,
2731
+ ...(readRecordString(latestRun, "web_url") ? { web_url: readRecordString(latestRun, "web_url") } : {}),
2732
+ };
2733
+ }
2734
+ const remainingMs = deadline - Date.now();
2735
+ if (remainingMs <= 0) {
2736
+ throw new OxygenError("table_action_run_wait_timeout", "Timed out waiting for table action run to finish.", {
2737
+ details: {
2738
+ action_run_id: runId,
2739
+ status: status ?? null,
2740
+ timeout_seconds: timeoutSeconds,
2741
+ polls,
2742
+ },
2743
+ exitCode: 1,
2744
+ });
2745
+ }
2746
+ await sleep(Math.min(intervalSeconds * 1000, remainingMs));
2747
+ }
2748
+ }
2749
+ async function exportRows(table, options) {
2750
+ const format = normalizeExportRowsFormat(options.format);
2751
+ const limit = readPositiveInt(options.limit);
2752
+ const result = await requestOxygen("/api/cli/tables/query", {
2753
+ method: "POST",
2754
+ body: {
2755
+ table,
2756
+ ...(limit ? { limit } : {}),
2757
+ },
2758
+ });
2759
+ const content = formatRows(result.rows, format, result.columns);
2760
+ if (options.output) {
2761
+ writeFileSync(options.output, content);
2762
+ }
2763
+ return {
2764
+ table: result.table ?? null,
2765
+ format,
2766
+ rowCount: result.rows.length,
2767
+ output: options.output ?? null,
2768
+ ...(options.output ? {} : { content }),
2769
+ };
2770
+ }
2771
+ async function readRowsFile(path, format, sheet) {
2772
+ return await parseRowsFileBuffer(readFileSync(path), format, sheet ? { sheet } : {});
2773
+ }
2774
+ function normalizeCreatedTableUpsertKey(value, keyBySource) {
2775
+ if (!value)
2776
+ return undefined;
2777
+ return keyBySource[value] ?? value;
2778
+ }
2779
+ function normalizeExportRowsFormat(value) {
2780
+ const normalized = value?.trim().toLowerCase() || "json";
2781
+ if (normalized === "json" || normalized === "jsonl" || normalized === "csv")
2782
+ return normalized;
2783
+ throw new OxygenError("invalid_format", "Export format must be json, jsonl, or csv.", {
2784
+ details: { format: value },
2785
+ exitCode: 1,
2786
+ });
2787
+ }
2788
+ function formatRows(rows, format, columns) {
2789
+ if (format === "json")
2790
+ return `${JSON.stringify(rows, null, 2)}\n`;
2791
+ if (format === "jsonl")
2792
+ return `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`;
2793
+ const keys = [
2794
+ "_row_id",
2795
+ "_created_at",
2796
+ "_updated_at",
2797
+ ...(columns?.map((column) => column.key) ?? []),
2798
+ ...rows.flatMap((row) => Object.keys(row)),
2799
+ ].filter((key, index, all) => all.indexOf(key) === index && rows.some((row) => key in row));
2800
+ return [
2801
+ keys.map(escapeCsvField).join(","),
2802
+ ...rows.map((row) => keys.map((key) => escapeCsvField(row[key])).join(",")),
2803
+ ].join("\n") + "\n";
2804
+ }
2805
+ function escapeCsvField(value) {
2806
+ const text = value === null || value === undefined
2807
+ ? ""
2808
+ : typeof value === "object"
2809
+ ? JSON.stringify(value)
2810
+ : String(value);
2811
+ return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, "\"\"")}"` : text;
2812
+ }
2813
+ function chunk(values, size) {
2814
+ const chunks = [];
2815
+ for (let index = 0; index < values.length; index += size) {
2816
+ chunks.push(values.slice(index, index + size));
2817
+ }
2818
+ return chunks;
2819
+ }
2820
+ function readCount(value) {
2821
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2822
+ }
2823
+ function readRecordString(value, key) {
2824
+ if (!value || typeof value !== "object" || Array.isArray(value))
2825
+ return null;
2826
+ const entry = value[key];
2827
+ return typeof entry === "string" ? entry : null;
2828
+ }
2829
+ function readRecord(value, key) {
2830
+ if (!value || typeof value !== "object" || Array.isArray(value))
2831
+ return null;
2832
+ const entry = value[key];
2833
+ return isRecord(entry) ? entry : null;
2834
+ }
2835
+ function isRecord(value) {
2836
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2837
+ }
2838
+ function tableWebUrl(tableIdOrSlug) {
2839
+ return `https://oxygen-agent.com/tables/${encodeURIComponent(tableIdOrSlug)}`;
2840
+ }
2841
+ function readRequiredResponseString(value, key) {
2842
+ const entry = value[key];
2843
+ if (typeof entry === "string" && entry.trim())
2844
+ return entry.trim();
2845
+ throw new OxygenError("invalid_response", `Oxygen API response is missing ${key}.`, {
2846
+ details: { key },
2847
+ exitCode: 1,
2848
+ });
2849
+ }
2850
+ function normalizeImportBatchSize(value) {
2851
+ const batchSize = readPositiveInt(value) ?? 500;
2852
+ if (batchSize > 5000) {
2853
+ throw new OxygenError("invalid_number", "Import batch size cannot exceed 5000.", {
2854
+ details: { value },
2855
+ exitCode: 1,
2856
+ });
2857
+ }
2858
+ return batchSize;
2859
+ }
2860
+ function hashImportFile(path) {
2861
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
2862
+ }
2863
+ function buildImportIdempotencyKey(input) {
2864
+ return [
2865
+ "tables-import",
2866
+ input.table,
2867
+ input.format,
2868
+ input.mode,
2869
+ input.upsertKey ?? "none",
2870
+ String(input.batchSize),
2871
+ input.sourceHash,
2872
+ ].join(":");
2873
+ }
2874
+ function isTerminalTableIngestionStatus(status) {
2875
+ return status === "completed"
2876
+ || status === "completed_with_errors"
2877
+ || status === "failed"
2878
+ || status === "canceled";
2879
+ }
2880
+ function isTerminalTableActionRunStatus(status) {
2881
+ return status === "completed"
2882
+ || status === "completed_with_errors"
2883
+ || status === "failed"
2884
+ || status === "canceled";
2885
+ }
2886
+ function sleep(ms) {
2887
+ return new Promise((resolve) => setTimeout(resolve, ms));
2888
+ }
2889
+ async function handleLoginAction(options) {
2890
+ try {
2891
+ const data = await login(options);
2892
+ if (options.json) {
2893
+ writeJson(success("login", data));
2894
+ }
2895
+ }
2896
+ catch (error) {
2897
+ const failure = toFailure("login", error);
2898
+ writeJson(failure);
2899
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
2900
+ }
2901
+ }
2902
+ async function handleAuthUseTokenAction(options) {
2903
+ try {
2904
+ const data = await useAuthToken(options);
2905
+ if (options.json) {
2906
+ writeJson(success("auth use-token", {
2907
+ logged_in: true,
2908
+ profile: data.profile,
2909
+ api_url: data.credentials.apiUrl,
2910
+ user: data.identity.user,
2911
+ organization: data.identity.organization,
2912
+ }));
2913
+ return;
2914
+ }
2915
+ process.stdout.write(formatLoginSuccess(data.identity, data.credentials, data.profile));
2916
+ }
2917
+ catch (error) {
2918
+ const failure = toFailure("auth use-token", error);
2919
+ if (options.json)
2920
+ writeJson(failure);
2921
+ else
2922
+ process.stderr.write(`${failure.error.message}\n`);
2923
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
2924
+ }
2925
+ }
2926
+ async function handleProfilesListAction(options) {
2927
+ try {
2928
+ const state = await listCredentialProfiles();
2929
+ const data = {
2930
+ active_profile: state.activeProfile,
2931
+ profiles: state.profiles.map((profile) => summarizeCredentialProfile(profile, profile.name === state.activeProfile)),
2932
+ };
2933
+ if (options.json) {
2934
+ writeJson(success("profiles list", data));
2935
+ return;
2936
+ }
2937
+ process.stdout.write(formatProfilesList(data));
2938
+ }
2939
+ catch (error) {
2940
+ const failure = toFailure("profiles list", error);
2941
+ writeJson(failure);
2942
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
2943
+ }
2944
+ }
2945
+ async function handleProfilesUseAction(profile, options) {
2946
+ try {
2947
+ const activeProfile = await switchCredentialProfile(profile);
2948
+ const data = {
2949
+ active_profile: activeProfile.name,
2950
+ profile: summarizeCredentialProfile(activeProfile, true),
2951
+ };
2952
+ if (options.json) {
2953
+ writeJson(success("profiles use", data));
2954
+ return;
2955
+ }
2956
+ process.stdout.write(formatProfileUseSuccess(data.profile));
2957
+ }
2958
+ catch (error) {
2959
+ const failure = toFailure("profiles use", error);
2960
+ writeJson(failure);
2961
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
2962
+ }
2963
+ }
2964
+ async function handleLogoutAction(options) {
2965
+ try {
2966
+ const clearOptions = {};
2967
+ if (options.profile !== undefined)
2968
+ clearOptions.profile = options.profile;
2969
+ if (options.all !== undefined)
2970
+ clearOptions.all = options.all;
2971
+ const result = await clearCredentials(process.env, clearOptions);
2972
+ if (options.json) {
2973
+ writeJson(success("logout", {
2974
+ logged_out: true,
2975
+ removed_profile: result.removedProfile,
2976
+ remaining_profiles: result.remainingProfiles,
2977
+ }));
2978
+ return;
2979
+ }
2980
+ process.stdout.write(formatLogoutSuccess(result));
2981
+ }
2982
+ catch (error) {
2983
+ const failure = toFailure("logout", error);
2984
+ writeJson(failure);
2985
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
2986
+ }
2987
+ }
2988
+ async function handleUpdateAction(options) {
2989
+ try {
2990
+ const result = updateCli(options);
2991
+ if (options.json) {
2992
+ writeJson(success("update", result));
2993
+ return;
2994
+ }
2995
+ process.stdout.write(formatUpdateSuccess(result));
2996
+ }
2997
+ catch (error) {
2998
+ const failure = toFailure("update", error);
2999
+ writeJson(failure);
3000
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
3001
+ }
3002
+ }
3003
+ function detectCliInstallPrefix() {
3004
+ try {
3005
+ const path = fileURLToPath(import.meta.url);
3006
+ const suffixes = [
3007
+ "/lib/node_modules/@oxygen-agent/cli/dist/index.js",
3008
+ "/lib/node_modules/@oxygen/cli/dist/index.js",
3009
+ ];
3010
+ for (const suffix of suffixes) {
3011
+ if (path.endsWith(suffix)) {
3012
+ return path.slice(0, -suffix.length);
3013
+ }
3014
+ }
3015
+ }
3016
+ catch {
3017
+ // Non-file URL (e.g. running from a bundler) — fall through.
3018
+ }
3019
+ return null;
3020
+ }
3021
+ function updateCli(options) {
3022
+ const packageSpec = readOption(options.package) ?? DEFAULT_CLI_PACKAGE_SPEC;
3023
+ const prefix = detectCliInstallPrefix();
3024
+ const args = prefix
3025
+ ? ["install", "-g", "--prefix", prefix, packageSpec]
3026
+ : ["install", "-g", packageSpec];
3027
+ const command = ["npm", ...args].join(" ");
3028
+ if (options.dryRun) {
3029
+ return {
3030
+ current_version: OXYGEN_VERSION,
3031
+ package: packageSpec,
3032
+ command,
3033
+ dry_run: true,
3034
+ updated: false,
3035
+ };
3036
+ }
3037
+ const result = spawnSync("npm", args, {
3038
+ encoding: "utf8",
3039
+ stdio: options.json ? ["ignore", "pipe", "pipe"] : "inherit",
3040
+ });
3041
+ if (result.error || result.status !== 0) {
3042
+ throw new OxygenError("cli_update_failed", "Unable to update the Oxygen CLI.", {
3043
+ details: {
3044
+ command,
3045
+ package: packageSpec,
3046
+ exit_code: result.status,
3047
+ reason: result.error instanceof Error ? result.error.message : null,
3048
+ stderr: typeof result.stderr === "string" && result.stderr.trim() ? result.stderr.trim().slice(0, 4000) : null,
3049
+ },
3050
+ exitCode: 1,
3051
+ });
3052
+ }
3053
+ return {
3054
+ current_version: OXYGEN_VERSION,
3055
+ package: packageSpec,
3056
+ command,
3057
+ dry_run: false,
3058
+ updated: true,
3059
+ };
3060
+ }
3061
+ async function login(options) {
3062
+ const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
3063
+ const token = readOption(options.token) ?? await promptForToken({
3064
+ apiUrl,
3065
+ browser: options.browser !== false,
3066
+ json: Boolean(options.json),
3067
+ });
3068
+ const credentials = {
3069
+ token,
3070
+ apiUrl,
3071
+ };
3072
+ const identity = await requestOxygen("/api/cli/whoami", { credentials });
3073
+ const saveOptions = {};
3074
+ if (options.profile !== undefined)
3075
+ saveOptions.profile = options.profile;
3076
+ const profile = await saveCredentials(credentials, process.env, saveOptions);
3077
+ if (!options.json) {
3078
+ process.stdout.write(formatLoginSuccess(identity, credentials, profile));
3079
+ }
3080
+ return {
3081
+ logged_in: true,
3082
+ profile,
3083
+ api_url: credentials.apiUrl,
3084
+ user: identity.user,
3085
+ organization: identity.organization,
3086
+ };
3087
+ }
3088
+ async function useAuthToken(options) {
3089
+ const token = readOption(options.token);
3090
+ if (!token) {
3091
+ throw new OxygenError("missing_token", "Pass a CLI API token with --token.", {
3092
+ exitCode: 1,
3093
+ });
3094
+ }
3095
+ const credentials = {
3096
+ token,
3097
+ apiUrl: normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl()),
3098
+ };
3099
+ const identity = await requestOxygen("/api/cli/whoami", { credentials });
3100
+ const saveOptions = {};
3101
+ if (options.profile !== undefined)
3102
+ saveOptions.profile = options.profile;
3103
+ const profile = await saveCredentials(credentials, process.env, saveOptions);
3104
+ return {
3105
+ identity,
3106
+ credentials,
3107
+ api_url: credentials.apiUrl,
3108
+ profile,
3109
+ };
3110
+ }
3111
+ async function promptForToken(options) {
3112
+ const fallbackLoginUrl = createCliLoginUrl(options.apiUrl);
3113
+ if (options.json) {
3114
+ throw new OxygenError("missing_token", "Pass a CLI API token with --token when using --json.", {
3115
+ details: { login_url: fallbackLoginUrl },
3116
+ exitCode: 1,
3117
+ });
3118
+ }
3119
+ if (!process.stdin.isTTY) {
3120
+ throw new OxygenError("missing_token", "Pass a CLI API token with --token, or run `oxygen login` in an interactive terminal.", {
3121
+ details: { login_url: fallbackLoginUrl },
3122
+ exitCode: 1,
3123
+ });
3124
+ }
3125
+ const browserSession = options.browser
3126
+ ? await createBrowserLoginSession(options.apiUrl).catch((error) => {
3127
+ output.write(`Browser login unavailable: ${readErrorMessage(error)}\n`);
3128
+ return null;
3129
+ })
3130
+ : null;
3131
+ if (browserSession) {
3132
+ const opened = openBrowser(browserSession.verificationUrl);
3133
+ if (opened) {
3134
+ output.write("Opened Oxygen in your browser.\n");
3135
+ }
3136
+ else {
3137
+ output.write("Open this URL in your browser:\n");
3138
+ output.write(`${browserSession.verificationUrl}\n`);
3139
+ }
3140
+ const spinner = startOxygenSpinner("Waiting for browser authorization");
3141
+ try {
3142
+ const token = (await waitForBrowserToken(browserSession)).trim();
3143
+ if (token) {
3144
+ spinner.succeed("Browser authorization received");
3145
+ return token;
3146
+ }
3147
+ }
3148
+ catch (error) {
3149
+ spinner.fail(`Browser login unavailable: ${readErrorMessage(error)}`);
3150
+ output.write("Paste a CLI API token instead.\n");
3151
+ }
3152
+ finally {
3153
+ spinner.stop();
3154
+ await browserSession.close();
3155
+ }
3156
+ }
3157
+ return promptForManualToken();
3158
+ }
3159
+ async function promptForManualToken() {
3160
+ const restoreEcho = muteTokenEcho();
3161
+ const rl = createInterface({ input, output });
3162
+ try {
3163
+ const token = (await rl.question("Oxygen API token: ")).trim();
3164
+ output.write("\n");
3165
+ if (!token) {
3166
+ throw new OxygenError("missing_token", "A CLI API token is required.", {
3167
+ exitCode: 1,
3168
+ });
3169
+ }
3170
+ return token;
3171
+ }
3172
+ finally {
3173
+ rl.close();
3174
+ restoreEcho();
3175
+ }
3176
+ }
3177
+ async function waitForBrowserToken(session) {
3178
+ let timer;
3179
+ const timeout = new Promise((_resolve, reject) => {
3180
+ timer = setTimeout(() => {
3181
+ reject(new OxygenError("browser_login_timeout", "Browser authorization timed out.", {
3182
+ exitCode: 1,
3183
+ }));
3184
+ }, BROWSER_LOGIN_TIMEOUT_MS);
3185
+ });
3186
+ try {
3187
+ return await Promise.race([session.waitForToken(), timeout]);
3188
+ }
3189
+ finally {
3190
+ if (timer)
3191
+ clearTimeout(timer);
3192
+ }
3193
+ }
3194
+ function createCliLoginUrl(apiUrl) {
3195
+ const url = new URL("/auth/sign-in", apiUrl);
3196
+ url.searchParams.set("redirect_url", "/settings/api-keys");
3197
+ url.searchParams.set("source", "oxygen_cli");
3198
+ return url.toString();
3199
+ }
3200
+ function readErrorMessage(error) {
3201
+ return error instanceof Error ? error.message : "unknown error";
3202
+ }
3203
+ function startOxygenSpinner(message) {
3204
+ if (!output.isTTY) {
3205
+ output.write(`${message}...\n`);
3206
+ return {
3207
+ succeed: (successMessage) => output.write(`${successMessage}.\n`),
3208
+ fail: (failureMessage) => output.write(`${failureMessage}.\n`),
3209
+ stop: () => undefined,
3210
+ };
3211
+ }
3212
+ let frameIndex = 0;
3213
+ let active = true;
3214
+ let lastLineLength = 0;
3215
+ const clearLine = () => {
3216
+ output.write(`\r${" ".repeat(lastLineLength)}\r`);
3217
+ lastLineLength = 0;
3218
+ };
3219
+ const render = () => {
3220
+ const frame = OXYGEN_SPINNER_FRAMES[frameIndex % OXYGEN_SPINNER_FRAMES.length];
3221
+ frameIndex += 1;
3222
+ const line = `${frame} ${message}...`;
3223
+ lastLineLength = Math.max(lastLineLength, line.length);
3224
+ output.write(`\r${line}${" ".repeat(Math.max(0, lastLineLength - line.length))}`);
3225
+ };
3226
+ render();
3227
+ const interval = setInterval(render, OXYGEN_SPINNER_INTERVAL_MS);
3228
+ const finish = (status, finalMessage) => {
3229
+ if (!active)
3230
+ return;
3231
+ active = false;
3232
+ clearInterval(interval);
3233
+ clearLine();
3234
+ output.write(`[OXYGEN ${status}] ${finalMessage}.\n`);
3235
+ };
3236
+ return {
3237
+ succeed: (successMessage) => finish("OK", successMessage),
3238
+ fail: (failureMessage) => finish("!!", failureMessage),
3239
+ stop: () => {
3240
+ if (!active)
3241
+ return;
3242
+ active = false;
3243
+ clearInterval(interval);
3244
+ clearLine();
3245
+ },
3246
+ };
3247
+ }
3248
+ function formatLoginSuccess(identity, credentials, profile) {
3249
+ const email = identity.user.email ?? identity.user.id;
3250
+ const org = identity.organization.name || identity.organization.slug || identity.organization.id;
3251
+ const key = identity.apiKey
3252
+ ? `${identity.apiKey.tokenPrefix}...${identity.apiKey.tokenSuffix}`
3253
+ : "stored";
3254
+ const fingerprint = createHash("sha256")
3255
+ .update(`oxygen-cli:${credentials.token}`)
3256
+ .digest("hex");
3257
+ const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
3258
+ const rows = [
3259
+ ["Account", email],
3260
+ ["Organization", org],
3261
+ ["Profile", profile],
3262
+ ["API", credentials.apiUrl],
3263
+ ["Token", key],
3264
+ ["Fingerprint", formatFingerprint(fingerprint)],
3265
+ ];
3266
+ const labelWidth = Math.max(...rows.map(([label]) => label.length));
3267
+ const wordmark = renderBox([
3268
+ "",
3269
+ ...OXYGEN_WORDMARK.split("\n").map(line => c.green(line)),
3270
+ "",
3271
+ ]);
3272
+ return [
3273
+ "",
3274
+ wordmark,
3275
+ "",
3276
+ `${c.green("[OK]")} ${c.bold("CLI connected")}`,
3277
+ "",
3278
+ ...rows.map(([label, value]) => ` ${c.dim(label.padEnd(labelWidth))} ${value}`),
3279
+ "",
3280
+ ].join("\n");
3281
+ }
3282
+ function formatFingerprint(hex) {
3283
+ return hex.slice(0, 20).match(/.{1,4}/g)?.join(":") ?? hex.slice(0, 20);
3284
+ }
3285
+ function summarizeCredentialProfile(profile, active) {
3286
+ return {
3287
+ name: profile.name,
3288
+ active,
3289
+ api_url: profile.apiUrl,
3290
+ token_fingerprint: formatFingerprint(createCredentialFingerprint(profile.token)),
3291
+ };
3292
+ }
3293
+ function createCredentialFingerprint(token) {
3294
+ return createHash("sha256")
3295
+ .update(`oxygen-cli:${token}`)
3296
+ .digest("hex");
3297
+ }
3298
+ function formatProfilesList(data) {
3299
+ const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
3300
+ if (data.profiles.length === 0) {
3301
+ return [
3302
+ "",
3303
+ `${c.dim("No stored Oxygen CLI profiles.")}`,
3304
+ "",
3305
+ ].join("\n");
3306
+ }
3307
+ const labelWidth = Math.max(...data.profiles.map((profile) => profile.name.length));
3308
+ return [
3309
+ "",
3310
+ `${c.bold("Oxygen CLI Profiles")}`,
3311
+ "",
3312
+ ...data.profiles.map((profile) => {
3313
+ const marker = profile.active ? "*" : " ";
3314
+ return [
3315
+ `${marker} ${profile.name.padEnd(labelWidth)}`,
3316
+ c.dim(profile.api_url),
3317
+ c.dim(profile.token_fingerprint),
3318
+ ].join(" ");
3319
+ }),
3320
+ "",
3321
+ ].join("\n");
3322
+ }
3323
+ function formatProfileUseSuccess(profile) {
3324
+ const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
3325
+ return [
3326
+ "",
3327
+ `${c.green("[OK]")} ${c.bold("CLI profile selected")}`,
3328
+ "",
3329
+ ` ${c.dim("Profile")} ${profile.name}`,
3330
+ ` ${c.dim("API")} ${profile.api_url}`,
3331
+ ` ${c.dim("Fingerprint")} ${profile.token_fingerprint}`,
3332
+ "",
3333
+ ].join("\n");
3334
+ }
3335
+ function formatLogoutSuccess(result) {
3336
+ const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
3337
+ const removed = result.removedProfile
3338
+ ? `profile "${result.removedProfile}" removed`
3339
+ : "removed";
3340
+ return [
3341
+ "",
3342
+ `${c.green("[OK]")} ${c.bold("CLI logged out")}`,
3343
+ "",
3344
+ ` ${c.dim("Credentials")} ${removed}`,
3345
+ ` ${c.dim("Profiles left")} ${String(result.remainingProfiles)}`,
3346
+ "",
3347
+ ].join("\n");
3348
+ }
3349
+ function formatUpdateSuccess(result) {
3350
+ const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
3351
+ return [
3352
+ "",
3353
+ `${c.green("[OK]")} ${c.bold(result.dry_run ? "CLI update command ready" : "CLI update completed")}`,
3354
+ "",
3355
+ ` ${c.dim("Current")} ${result.current_version}`,
3356
+ ` ${c.dim("Package")} ${result.package}`,
3357
+ ` ${c.dim("Command")} ${result.command}`,
3358
+ "",
3359
+ ].join("\n");
3360
+ }
3361
+ function renderBox(lines) {
3362
+ const width = Math.max(...lines.map(visibleLength), 0);
3363
+ const border = `+${"-".repeat(width + 2)}+`;
3364
+ const body = lines.map((line) => `| ${line}${" ".repeat(width - visibleLength(line))} |`);
3365
+ return [border, ...body, border].join("\n");
3366
+ }
3367
+ function visibleLength(value) {
3368
+ return value.replace(/\x1b\[[0-9;]*m/g, "").length;
3369
+ }
3370
+ function ansi(enabled) {
3371
+ const wrap = (open, close) => enabled
3372
+ ? (text) => `\x1b[${open}m${text}\x1b[${close}m`
3373
+ : (text) => text;
3374
+ return {
3375
+ bold: wrap(1, 22),
3376
+ dim: wrap(2, 22),
3377
+ green: wrap(32, 39),
3378
+ };
3379
+ }
3380
+ function readOption(value) {
3381
+ return value?.trim() ? value.trim() : null;
3382
+ }
3383
+ function readCsvOption(value) {
3384
+ const option = readOption(value);
3385
+ if (!option)
3386
+ return [];
3387
+ return option
3388
+ .split(",")
3389
+ .map((entry) => entry.trim())
3390
+ .filter(Boolean);
3391
+ }
3392
+ function contextAssetsQuery(options) {
3393
+ const query = new URLSearchParams();
3394
+ if (readOption(options.type))
3395
+ query.set("type", readOption(options.type) ?? "");
3396
+ if (readOption(options.status))
3397
+ query.set("status", readOption(options.status) ?? "");
3398
+ for (const tag of readCsvOption(options.tags))
3399
+ query.append("tag", tag);
3400
+ if (options.includeArchived)
3401
+ query.set("include_archived", "true");
3402
+ const value = query.toString();
3403
+ return value ? `?${value}` : "";
3404
+ }
3405
+ function buildContextAssetUpsertBody(options) {
3406
+ const asset = options.assetJson ? parseJsonObject(options.assetJson) : {};
3407
+ const tags = readCsvOption(options.tags);
3408
+ return {
3409
+ ...asset,
3410
+ ...(readOption(options.id) ? { id: readOption(options.id) } : {}),
3411
+ ...(readOption(options.type) ? { type: readOption(options.type) } : {}),
3412
+ ...(readOption(options.title) ? { title: readOption(options.title) } : {}),
3413
+ ...(readOption(options.status) ? { status: readOption(options.status) } : {}),
3414
+ ...(tags.length > 0 ? { tags } : {}),
3415
+ ...(readOption(options.summary) ? { summary: readOption(options.summary) } : {}),
3416
+ ...(readOption(options.body) ? { body: readOption(options.body) } : {}),
3417
+ ...(options.dataJson ? { data: parseJsonObject(options.dataJson) } : {}),
3418
+ };
3419
+ }
3420
+ function buildEnrichColumnBody(table, options) {
3421
+ const limit = readPositiveInt(options.limit);
3422
+ const filterSelection = readFilterSelectionOption(options.filterJson);
3423
+ const explicitSelection = readSelectionJsonOption(options.selectionJson);
3424
+ const selectedModes = [
3425
+ Boolean(options.all),
3426
+ limit !== undefined,
3427
+ Boolean(filterSelection),
3428
+ Boolean(explicitSelection),
3429
+ ].filter(Boolean).length;
3430
+ if (selectedModes > 1) {
3431
+ throw new OxygenError("invalid_selection", "Pass only one of --all, --limit, --filter-json, or --selection-json.", {
3432
+ exitCode: 1,
3433
+ });
3434
+ }
3435
+ return {
3436
+ table,
3437
+ ...(readOption(options.sourceColumn) ? { source_column: readOption(options.sourceColumn) } : {}),
3438
+ ...(readOption(options.fullNameColumn) ? { full_name_column: readOption(options.fullNameColumn) } : {}),
3439
+ ...(readOption(options.firstNameColumn) ? { first_name_column: readOption(options.firstNameColumn) } : {}),
3440
+ ...(readOption(options.lastNameColumn) ? { last_name_column: readOption(options.lastNameColumn) } : {}),
3441
+ ...(readOption(options.linkedinUrlColumn) ? { linkedin_url_column: readOption(options.linkedinUrlColumn) } : {}),
3442
+ ...(readOption(options.emailColumn) ? { email_column: readOption(options.emailColumn) } : {}),
3443
+ ...(readOption(options.companyDomainColumn) ? { company_domain_column: readOption(options.companyDomainColumn) } : {}),
3444
+ ...(readOption(options.companyNameColumn) ? { company_name_column: readOption(options.companyNameColumn) } : {}),
3445
+ ...(readOption(options.companyLinkedinUrlColumn)
3446
+ ? { company_linkedin_url_column: readOption(options.companyLinkedinUrlColumn) }
3447
+ : {}),
3448
+ ...(readOption(options.capability) ? { capability: readOption(options.capability) } : {}),
3449
+ ...(readOption(options.targetColumn) ? { target_column: readOption(options.targetColumn) } : {}),
3450
+ ...(readOption(options.onExistingManualColumn)
3451
+ ? { on_existing_manual_column: readOption(options.onExistingManualColumn) }
3452
+ : {}),
3453
+ ...(readOption(options.providerOrder) ? { provider_order: readCsvOption(options.providerOrder) } : {}),
3454
+ ...(options.verifyPhone ? { verify_phone: true } : {}),
3455
+ ...(readOption(options.phoneVerificationCredentialMode)
3456
+ ? { phone_verification_credential_mode: readOption(options.phoneVerificationCredentialMode) }
3457
+ : {}),
3458
+ ...(options.all ? { selection: { mode: "all" } } : {}),
3459
+ ...(filterSelection ? { selection: filterSelection } : {}),
3460
+ ...(explicitSelection ? { selection: explicitSelection } : {}),
3461
+ ...(limit !== undefined ? { limit } : {}),
3462
+ ...(options.onlyMissing ? { only_missing: true } : {}),
3463
+ };
3464
+ }
3465
+ function readPositiveInt(value) {
3466
+ const trimmed = value?.trim();
3467
+ if (!trimmed)
3468
+ return undefined;
3469
+ const parsed = Number(trimmed);
3470
+ if (!Number.isInteger(parsed) || parsed < 1) {
3471
+ throw new OxygenError("invalid_number", "Expected a positive integer.", {
3472
+ details: { value },
3473
+ exitCode: 1,
3474
+ });
3475
+ }
3476
+ return parsed;
3477
+ }
3478
+ function readPositiveNumber(value) {
3479
+ const trimmed = value?.trim();
3480
+ if (!trimmed)
3481
+ return undefined;
3482
+ const parsed = Number(trimmed);
3483
+ if (!Number.isFinite(parsed) || parsed <= 0) {
3484
+ throw new OxygenError("invalid_number", "Expected a positive number.", {
3485
+ details: { value },
3486
+ exitCode: 1,
3487
+ });
3488
+ }
3489
+ return parsed;
3490
+ }
3491
+ function readNonNegativeInt(value) {
3492
+ const trimmed = value?.trim();
3493
+ if (!trimmed)
3494
+ return undefined;
3495
+ const parsed = Number(trimmed);
3496
+ if (!Number.isInteger(parsed) || parsed < 0) {
3497
+ throw new OxygenError("invalid_number", "Expected a non-negative integer.", {
3498
+ details: { value },
3499
+ exitCode: 1,
3500
+ });
3501
+ }
3502
+ return parsed;
3503
+ }
3504
+ function muteTokenEcho() {
3505
+ if (!input.isTTY || process.platform === "win32")
3506
+ return () => undefined;
3507
+ try {
3508
+ execFileSync("stty", ["-echo"], { stdio: ["inherit", "ignore", "ignore"] });
3509
+ return () => {
3510
+ try {
3511
+ execFileSync("stty", ["echo"], { stdio: ["inherit", "ignore", "ignore"] });
3512
+ }
3513
+ catch {
3514
+ // Ignore restore failures; the shell normally restores echo on process exit.
3515
+ }
3516
+ };
3517
+ }
3518
+ catch {
3519
+ return () => undefined;
3520
+ }
3521
+ }
3522
+ await createProgram().parseAsync(process.argv);
3523
+ //# sourceMappingURL=index.js.map