@oxygen-agent/cli 1.64.5 → 1.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // skipcq: JS-0271 — bin entry source; build chmod+x on dist/index.js
3
3
  import { execFileSync } from "node:child_process";
4
- import { createHash } from "node:crypto";
4
+ import { createHash, randomUUID } from "node:crypto";
5
5
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { tmpdir } from "node:os";
7
7
  import { basename, dirname, extname, resolve } from "node:path";
@@ -14,7 +14,7 @@ import { inferRowsFileFormat, normalizeRowsForNewTable, normalizeRowsFormat, par
14
14
  import { assertRecipeBundleSafe, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowManifest, } from "@oxygen/workflows";
15
15
  import { isRecipeDefinition } from "@oxygen/recipe-sdk";
16
16
  import { createBrowserLoginSession, openBrowser } from "./browser-login.js";
17
- import { clearCredentials, defaultApiUrl, listCredentialProfiles, normalizeApiUrl, pickProfileNameForIdentity, resolveActiveProfile, saveCredentials, switchCredentialProfile, } from "./credentials.js";
17
+ import { clearCredentials, defaultApiUrl, listCredentialProfiles, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
18
18
  import { requestOxygen } from "./http-client.js";
19
19
  import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
20
20
  import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
@@ -112,6 +112,21 @@ function parseJsonArray(value) {
112
112
  }
113
113
  return parsed;
114
114
  }
115
+ function readCustomIntegrationManifest(options) {
116
+ const manifestPath = readOption(options.manifest);
117
+ const manifestJson = readOption(options.manifestJson);
118
+ if (manifestPath && manifestJson) {
119
+ throw new OxygenError("invalid_request", "Pass either --manifest or --manifest-json, not both.", {
120
+ exitCode: 1,
121
+ });
122
+ }
123
+ if (!manifestPath && !manifestJson) {
124
+ throw new OxygenError("invalid_request", "Pass --manifest or --manifest-json.", {
125
+ exitCode: 1,
126
+ });
127
+ }
128
+ return parseJsonObject(manifestJson ?? readFileSync(resolve(manifestPath ?? ""), "utf8"));
129
+ }
115
130
  function readFileIfPresent(value) {
116
131
  const candidate = resolve(value);
117
132
  try {
@@ -161,13 +176,17 @@ export function createProgram() {
161
176
  .name("oxygen")
162
177
  .description("CLI/API-first GTM platform for GTM tool and workflow primitives.")
163
178
  .version(OXYGEN_VERSION)
164
- .option("--profile <name>", "Use a stored CLI profile for this command.");
179
+ .option("--profile <name>", "Use a stored CLI profile for this command.")
180
+ .option("--org <organization>", "Use an organization id, Clerk org id, or slug for this command.");
165
181
  program.hook("preAction", () => {
166
182
  const options = program.opts();
167
183
  if (options.profile) {
168
184
  globalProfileFlag = options.profile;
169
185
  process.env.OXYGEN_PROFILE = options.profile;
170
186
  }
187
+ if (options.org) {
188
+ process.env.OXYGEN_ORG = options.org;
189
+ }
171
190
  });
172
191
  program
173
192
  .command("login")
@@ -284,6 +303,22 @@ export function createProgram() {
284
303
  .action(async (options) => {
285
304
  await handleWhoamiAction(options);
286
305
  });
306
+ program
307
+ .command("onboarding")
308
+ .description("Onboarding helpers.")
309
+ .addCommand(new Command("start")
310
+ .description("Start onboarding and get the skill to load.")
311
+ .option("--json", "Print a JSON envelope.")
312
+ .action(async (options) => {
313
+ await handleOnboardingStartAction(options);
314
+ }))
315
+ .addCommand(new Command("reset")
316
+ .description("Clear the onboarding_started_at marker so onboarding re-runs from scratch. Company context data is preserved.")
317
+ .option("--confirm", "Required: confirm the reset. Without this flag the command refuses to run.")
318
+ .option("--json", "Print a JSON envelope.")
319
+ .action(async (options) => {
320
+ await handleOnboardingResetAction(options);
321
+ }));
287
322
  program
288
323
  .command("status")
289
324
  .description("Compare the local Oxygen CLI version against what's deployed in prod.")
@@ -308,16 +343,20 @@ export function createProgram() {
308
343
  .option("--json", "Print a JSON envelope.")
309
344
  .action(async (options) => {
310
345
  await handleAsyncAction("orgs list", options, async () => requestOxygen("/api/cli/orgs"));
346
+ }))
347
+ .addCommand(new Command("use")
348
+ .description("Select the active organization for this CLI profile.")
349
+ .argument("<organization>", "Organization id, Clerk org id, or slug returned by orgs list.")
350
+ .option("--json", "Print a JSON envelope.")
351
+ .action(async (organization, options) => {
352
+ await handleOrgUseAction(organization, options, "orgs use");
311
353
  }))
312
354
  .addCommand(new Command("select")
313
- .description("Select the active organization for an OAuth-backed CLI session.")
355
+ .description("Alias for orgs use.")
314
356
  .argument("<organization>", "Organization id, Clerk org id, or slug returned by orgs list.")
315
357
  .option("--json", "Print a JSON envelope.")
316
358
  .action(async (organization, options) => {
317
- await handleAsyncAction("orgs select", options, async () => requestOxygen("/api/cli/orgs/select", {
318
- method: "POST",
319
- body: { organization },
320
- }));
359
+ await handleOrgUseAction(organization, options, "orgs select");
321
360
  }));
322
361
  program
323
362
  .command("db")
@@ -557,11 +596,13 @@ export function createProgram() {
557
596
  await handleAsyncAction("tables export-bundle", options, async () => exportTableBundle(table, options));
558
597
  }))
559
598
  .addCommand(new Command("import-bundle")
560
- .description("Recreate a workspace table from an export-bundle file in this org. Restores columns (incl. enrichment/tool definitions) and inserts every row.")
599
+ .description("Recreate a workspace table from an export-bundle file in this org. Restores columns (incl. enrichment/tool definitions) and inserts every row. Pass --key to make the import idempotent (re-runnable), and --into to resume a failed import into the table it already created.")
561
600
  .requiredOption("--file <path>", "Bundle JSON file produced by `tables export-bundle`.")
562
- .option("--name <name>", "Override the table display name. Defaults to the bundle's table name.")
563
- .option("--project <project>", "Project id or slug for the new table. Defaults to General.")
564
- .option("--batch-size <n>", "Rows per insert request. Defaults to 500; paid orgs may use up to 5000.")
601
+ .option("--name <name>", "Override the table display name. Defaults to the bundle's table name. Ignored with --into.")
602
+ .option("--project <project>", "Project id or slug for the new table. Defaults to General. Ignored with --into.")
603
+ .option("--into <table>", "Resume into an existing table (id or slug) instead of creating a new one. Requires --key. Use the table id printed by the failed run.")
604
+ .option("--key <column>", "Upsert key column. When set, rows are matched by this key instead of blindly inserted, so re-running the import is idempotent (no duplicates). Required with --into.")
605
+ .option("--batch-size <n>", "Rows per write request. Defaults to 500; paid orgs may use up to 5000.")
565
606
  .option("--json", "Print a JSON envelope.")
566
607
  .action(async (options) => {
567
608
  await handleAsyncAction("tables import-bundle", options, async () => importTableBundle(options));
@@ -805,7 +846,238 @@ export function createProgram() {
805
846
  }));
806
847
  })));
807
848
  program
808
- .command("templates")
849
+ .command("blueprints")
850
+ .description("Portable Oxygen blueprints: bundle a workflow + tables + columns + prompts as shareable JSON.")
851
+ .addCommand(new Command("list")
852
+ .description("List Oxygen blueprints visible in this workspace (seeds + saved).")
853
+ .argument("[query]", "Search text.")
854
+ .option("--tag <tag>", "Filter by tag.")
855
+ .option("--include-seeds", "Include the shipped seed blueprints (default true).")
856
+ .option("--json", "Print a JSON envelope.")
857
+ .action(async (query, options) => {
858
+ await handleAsyncAction("blueprints list", options, () => {
859
+ const params = new URLSearchParams();
860
+ if (query)
861
+ params.set("query", query);
862
+ const tag = readOption(options.tag);
863
+ if (tag)
864
+ params.set("tag", tag);
865
+ if (options.includeSeeds === false)
866
+ params.set("include_seeds", "false");
867
+ const qs = params.toString() ? `?${params.toString()}` : "";
868
+ return requestOxygen(`/api/cli/blueprints${qs}`);
869
+ });
870
+ }))
871
+ .addCommand(new Command("describe")
872
+ .description("Describe one blueprint (seed or saved) by slug.")
873
+ .argument("<slug>", "Blueprint slug.")
874
+ .option("--json", "Print a JSON envelope.")
875
+ .action(async (slug, options) => {
876
+ await handleAsyncAction("blueprints describe", options, () => requestOxygen("/api/cli/blueprints/get", {
877
+ method: "POST",
878
+ body: { slug },
879
+ }));
880
+ }))
881
+ .addCommand(new Command("export")
882
+ .description("Build a portable blueprint JSON from an existing workflow.")
883
+ .requiredOption("--workflow <id>", "Workflow id or slug to export.")
884
+ .option("--tables <ids>", "Comma-separated list of table ids/slugs to bundle.")
885
+ .option("--prompts <slugs>", "Comma-separated list of prompt template slugs to bundle.")
886
+ .option("--blueprint-id <slug>", "Override the output blueprint slug.")
887
+ .option("--blueprint-name <name>", "Override the output blueprint name.")
888
+ .option("--blueprint-summary <text>", "Override the output blueprint summary.")
889
+ .option("--blueprint-tags <tags>", "Comma-separated tags for the exported blueprint.")
890
+ .option("--out <path>", "Write the exported envelope to this file path.")
891
+ .option("--json", "Print a JSON envelope.")
892
+ .action(async (options) => {
893
+ await handleAsyncAction("blueprints export", options, async () => {
894
+ const body = { workflow_id: options.workflow };
895
+ const tables = readOption(options.tables);
896
+ if (tables)
897
+ body.table_ids = splitCsv(tables);
898
+ const prompts = readOption(options.prompts);
899
+ if (prompts)
900
+ body.prompt_slugs = splitCsv(prompts);
901
+ const blueprintId = readOption(options.blueprintId);
902
+ if (blueprintId)
903
+ body.blueprint_id = blueprintId;
904
+ const blueprintName = readOption(options.blueprintName);
905
+ if (blueprintName)
906
+ body.blueprint_name = blueprintName;
907
+ const blueprintSummary = readOption(options.blueprintSummary);
908
+ if (blueprintSummary)
909
+ body.blueprint_summary = blueprintSummary;
910
+ const blueprintTags = readOption(options.blueprintTags);
911
+ if (blueprintTags)
912
+ body.blueprint_tags = splitCsv(blueprintTags);
913
+ const result = await requestOxygen("/api/cli/blueprints/export", {
914
+ method: "POST",
915
+ body,
916
+ });
917
+ const outPath = readOption(options.out);
918
+ if (outPath) {
919
+ const fs = await import("node:fs/promises");
920
+ const blueprint = result.blueprint;
921
+ await fs.writeFile(outPath, JSON.stringify(blueprint, null, 2), "utf8");
922
+ }
923
+ return result;
924
+ });
925
+ }))
926
+ .addCommand(new Command("preflight")
927
+ .description("Preflight a blueprint (slug, local file, or shared URL) against this workspace.")
928
+ .argument("[slug]", "Blueprint slug (for stored or seed blueprints).")
929
+ .option("--file <path>", "Read a blueprint envelope from a local JSON file.")
930
+ .option("--from-url <url>", "Fetch a shared blueprint envelope from a public Oxygen share URL.")
931
+ .option("--input-json <json>", "Seed blueprint input (parameters) as JSON.")
932
+ .option("--table-ref <ref=id...>", "Reuse an existing table for a blueprint ref (repeatable).", collectMultiple, [])
933
+ .option("--json", "Print a JSON envelope.")
934
+ .action(async (slug, options) => {
935
+ await handleAsyncAction("blueprints preflight", options, async () => {
936
+ const body = await buildBlueprintRequestBody(slug, options);
937
+ return requestOxygen("/api/cli/blueprints/preflight", { method: "POST", body });
938
+ });
939
+ }))
940
+ .addCommand(new Command("apply")
941
+ .description("Apply a blueprint to this workspace. Creates tables, columns, prompts, and a disabled workflow.")
942
+ .argument("[slug]", "Blueprint slug (for stored or seed blueprints).")
943
+ .option("--file <path>", "Read a blueprint envelope from a local JSON file.")
944
+ .option("--from-url <url>", "Fetch a shared blueprint envelope from a public Oxygen share URL.")
945
+ .option("--input-json <json>", "Seed blueprint input (parameters) as JSON.")
946
+ .option("--table-ref <ref=id...>", "Reuse an existing table for a blueprint ref (repeatable).", collectMultiple, [])
947
+ .option("--workflow-id <id>", "Override the resulting workflow id.")
948
+ .option("--workflow-name <name>", "Override the resulting workflow name.")
949
+ .option("--json", "Print a JSON envelope.")
950
+ .action(async (slug, options) => {
951
+ await handleAsyncAction("blueprints apply", options, async () => {
952
+ const body = await buildBlueprintRequestBody(slug, options);
953
+ const workflowId = readOption(options.workflowId);
954
+ if (workflowId)
955
+ body.workflow_id = workflowId;
956
+ const workflowName = readOption(options.workflowName);
957
+ if (workflowName)
958
+ body.workflow_name = workflowName;
959
+ return requestOxygen("/api/cli/blueprints/apply", { method: "POST", body });
960
+ });
961
+ }))
962
+ .addCommand(new Command("save")
963
+ .description("Persist a blueprint envelope into this workspace's gallery.")
964
+ .option("--file <path>", "Read a blueprint envelope from a local JSON file.")
965
+ .option("--slug <slug>", "Override the stored blueprint slug.")
966
+ .option("--name <name>", "Override the stored blueprint name.")
967
+ .option("--summary <text>", "Override the stored blueprint summary.")
968
+ .option("--json", "Print a JSON envelope.")
969
+ .action(async (options) => {
970
+ await handleAsyncAction("blueprints save", options, async () => {
971
+ const filePath = readOption(options.file);
972
+ if (!filePath)
973
+ throw new Error("--file is required for blueprints save");
974
+ const fs = await import("node:fs/promises");
975
+ const raw = await fs.readFile(filePath, "utf8");
976
+ const envelope = JSON.parse(raw);
977
+ const body = { envelope };
978
+ const slug = readOption(options.slug);
979
+ if (slug)
980
+ body.slug = slug;
981
+ const name = readOption(options.name);
982
+ if (name)
983
+ body.name = name;
984
+ const summary = readOption(options.summary);
985
+ if (summary)
986
+ body.summary = summary;
987
+ return requestOxygen("/api/cli/blueprints/save", { method: "POST", body });
988
+ });
989
+ }))
990
+ .addCommand(new Command("archive")
991
+ .description("Archive a saved blueprint (seed blueprints cannot be archived).")
992
+ .argument("<slug>", "Blueprint slug or id.")
993
+ .option("--json", "Print a JSON envelope.")
994
+ .action(async (slug, options) => {
995
+ await handleAsyncAction("blueprints archive", options, () => requestOxygen("/api/cli/blueprints/archive", { method: "POST", body: { slug } }));
996
+ }))
997
+ .addCommand(new Command("share")
998
+ .description("Create a public share URL for a saved blueprint (oxygen-agent.com/b/<code>).")
999
+ .argument("<slug>", "Blueprint slug or id to share.")
1000
+ .option("--expires-in-days <days>", "Optional expiry (default: no expiry).")
1001
+ .option("--disabled", "Create the share in disabled state.")
1002
+ .option("--json", "Print a JSON envelope.")
1003
+ .action(async (slug, options) => {
1004
+ await handleAsyncAction("blueprints share", options, () => {
1005
+ const body = { slug };
1006
+ const expiresInDays = readOption(options.expiresInDays);
1007
+ if (expiresInDays)
1008
+ body.expires_in_days = Number(expiresInDays);
1009
+ if (options.disabled)
1010
+ body.enabled = false;
1011
+ return requestOxygen("/api/cli/blueprints/share", { method: "POST", body });
1012
+ });
1013
+ }))
1014
+ .addCommand(new Command("shares")
1015
+ .description("List existing share URLs for this workspace.")
1016
+ .option("--json", "Print a JSON envelope.")
1017
+ .action(async (options) => {
1018
+ await handleAsyncAction("blueprints shares", options, () => requestOxygen("/api/cli/blueprints/share"));
1019
+ }))
1020
+ .addCommand(new Command("unshare")
1021
+ .description("Revoke a public share URL by short code.")
1022
+ .argument("<code>", "Share short code (the slug after /b/ in the URL).")
1023
+ .option("--json", "Print a JSON envelope.")
1024
+ .action(async (code, options) => {
1025
+ await handleAsyncAction("blueprints unshare", options, () => requestOxygen("/api/cli/blueprints/share/revoke", {
1026
+ method: "POST",
1027
+ body: { short_code: code },
1028
+ }));
1029
+ }))
1030
+ .addCommand(new Command("publish")
1031
+ .description("List a blueprint on the public Oxygen marketplace. Auto-creates a share URL if none exists.")
1032
+ .argument("<slug>", "Blueprint slug to publish.")
1033
+ .option("--category <name>", "Optional free-text category (e.g. outbound, enrichment).")
1034
+ .option("--kind <kind>", "Listing kind: community (default) or official (Oxygen-curated; gated).")
1035
+ .option("--approved", "Acknowledge the publish-safety warnings (required if the preview is not clean).")
1036
+ .option("--json", "Print a JSON envelope.")
1037
+ .action(async (slug, options) => {
1038
+ await handleAsyncAction("blueprints publish", options, () => {
1039
+ const body = { slug };
1040
+ const category = readOption(options.category);
1041
+ if (category)
1042
+ body.category = category;
1043
+ const kind = readOption(options.kind);
1044
+ if (kind)
1045
+ body.kind = kind;
1046
+ if (options.approved)
1047
+ body.approved = true;
1048
+ return requestOxygen("/api/cli/blueprints/publish", { method: "POST", body });
1049
+ });
1050
+ }))
1051
+ .addCommand(new Command("unpublish")
1052
+ .description("Remove a blueprint from the public marketplace. The share URL stays valid for direct visitors.")
1053
+ .argument("<slug>", "Blueprint slug to unlist.")
1054
+ .option("--json", "Print a JSON envelope.")
1055
+ .action(async (slug, options) => {
1056
+ await handleAsyncAction("blueprints unpublish", options, () => requestOxygen("/api/cli/blueprints/unpublish", {
1057
+ method: "POST",
1058
+ body: { slug },
1059
+ }));
1060
+ }))
1061
+ .addCommand(new Command("marketplace")
1062
+ .description("Browse the public Oxygen blueprint marketplace.")
1063
+ .option("--category <name>", "Filter by listing category.")
1064
+ .option("--kind <kind>", "Filter by listing kind: community or official.")
1065
+ .option("--json", "Print a JSON envelope.")
1066
+ .action(async (options) => {
1067
+ await handleAsyncAction("blueprints marketplace", options, () => {
1068
+ const params = new URLSearchParams();
1069
+ const category = readOption(options.category);
1070
+ if (category)
1071
+ params.set("category", category);
1072
+ const kind = readOption(options.kind);
1073
+ if (kind)
1074
+ params.set("kind", kind);
1075
+ const qs = params.toString() ? `?${params.toString()}` : "";
1076
+ return requestOxygen(`/api/blueprints/marketplace${qs}`, { requireAuth: false });
1077
+ });
1078
+ }));
1079
+ program
1080
+ .command("prompts")
809
1081
  .description("Reusable prompt templates layered into AI columns at run time.")
810
1082
  .addCommand(new Command("list")
811
1083
  .description("List prompt templates in the workspace.")
@@ -813,10 +1085,85 @@ export function createProgram() {
813
1085
  .option("--include-archived", "Include archived templates.")
814
1086
  .option("--json", "Print a JSON envelope.")
815
1087
  .action(async (options) => {
816
- await handleAsyncAction("templates list", options, async () => {
1088
+ await handleAsyncAction("prompts list", options, () => {
817
1089
  const params = new URLSearchParams();
1090
+ const kind = readOption(options.kind);
1091
+ if (kind)
1092
+ params.set("kind", kind);
1093
+ if (options.includeArchived)
1094
+ params.set("include_archived", "true");
1095
+ const qs = params.toString() ? `?${params.toString()}` : "";
1096
+ return requestOxygen(`/api/cli/templates${qs}`);
1097
+ });
1098
+ }))
1099
+ .addCommand(new Command("get")
1100
+ .description("Read one prompt template by id or slug.")
1101
+ .argument("<id_or_slug>", "Template UUID or slug.")
1102
+ .option("--json", "Print a JSON envelope.")
1103
+ .action(async (idOrSlug, options) => {
1104
+ await handleAsyncAction("prompts get", options, () => requestOxygen("/api/cli/templates/get", {
1105
+ method: "POST",
1106
+ body: idOrSlug.includes("-") && idOrSlug.length >= 32
1107
+ ? { id: idOrSlug }
1108
+ : { slug: idOrSlug },
1109
+ }));
1110
+ }))
1111
+ .addCommand(new Command("upsert")
1112
+ .description("Create or update a prompt template.")
1113
+ .option("--id <id>", "Existing template UUID to update. Omit to create.")
1114
+ .option("--slug <slug>", "Stable slug (kebab-case).")
1115
+ .option("--name <name>", "Human-readable name.")
1116
+ .option("--description <text>", "Short description.")
1117
+ .option("--kind <kind>", "Template kind: ai_column_system, scoring_rubric, or other.")
1118
+ .option("--body <text>", "Prompt body.")
1119
+ .option("--body-file <path>", "Path to a file containing the prompt body.")
1120
+ .option("--json", "Print a JSON envelope.")
1121
+ .action(async (options) => {
1122
+ await handleAsyncAction("prompts upsert", options, async () => {
1123
+ const body = {};
1124
+ if (readOption(options.id))
1125
+ body.id = readOption(options.id);
1126
+ if (readOption(options.slug))
1127
+ body.slug = readOption(options.slug);
1128
+ if (readOption(options.name))
1129
+ body.name = readOption(options.name);
1130
+ if (readOption(options.description) !== undefined)
1131
+ body.description = readOption(options.description);
818
1132
  if (readOption(options.kind))
819
- params.set("kind", readOption(options.kind));
1133
+ body.kind = readOption(options.kind);
1134
+ if (readOption(options.body))
1135
+ body.body = readOption(options.body);
1136
+ else {
1137
+ const path = readOption(options.bodyFile);
1138
+ if (path) {
1139
+ const fs = await import("node:fs/promises");
1140
+ body.body = await fs.readFile(path, "utf8");
1141
+ }
1142
+ }
1143
+ return requestOxygen("/api/cli/templates/upsert", { method: "POST", body });
1144
+ });
1145
+ }))
1146
+ .addCommand(new Command("archive")
1147
+ .description("Archive a prompt template. Seeded templates cannot be archived.")
1148
+ .argument("<id>", "Template UUID.")
1149
+ .option("--json", "Print a JSON envelope.")
1150
+ .action(async (id, options) => {
1151
+ await handleAsyncAction("prompts archive", options, () => requestOxygen("/api/cli/templates/archive", { method: "POST", body: { id } }));
1152
+ }));
1153
+ program
1154
+ .command("templates")
1155
+ .description("Deprecated alias for 'oxygen prompts'. Will be removed in a future release.")
1156
+ .addCommand(new Command("list")
1157
+ .description("List prompt templates in the workspace.")
1158
+ .option("--kind <kind>", "Filter by ai_column_system, scoring_rubric, or other.")
1159
+ .option("--include-archived", "Include archived templates.")
1160
+ .option("--json", "Print a JSON envelope.")
1161
+ .action(async (options) => {
1162
+ await handleAsyncAction("templates list", options, () => {
1163
+ const params = new URLSearchParams();
1164
+ const kind = readOption(options.kind);
1165
+ if (kind)
1166
+ params.set("kind", kind);
820
1167
  if (options.includeArchived)
821
1168
  params.set("include_archived", "true");
822
1169
  const qs = params.toString() ? `?${params.toString()}` : "";
@@ -860,10 +1207,12 @@ export function createProgram() {
860
1207
  body.kind = readOption(options.kind);
861
1208
  if (readOption(options.body))
862
1209
  body.body = readOption(options.body);
863
- else if (readOption(options.bodyFile)) {
1210
+ else {
864
1211
  const path = readOption(options.bodyFile);
865
- const fs = await import("node:fs/promises");
866
- body.body = await fs.readFile(path, "utf8");
1212
+ if (path) {
1213
+ const fs = await import("node:fs/promises");
1214
+ body.body = await fs.readFile(path, "utf8");
1215
+ }
867
1216
  }
868
1217
  return requestOxygen("/api/cli/templates/upsert", { method: "POST", body });
869
1218
  });
@@ -885,14 +1234,17 @@ export function createProgram() {
885
1234
  .option("--limit <n>", "Max rows to return (default 50, max 200).")
886
1235
  .option("--json", "Print a JSON envelope.")
887
1236
  .action(async (options) => {
888
- await handleAsyncAction("reviews list", options, async () => {
1237
+ await handleAsyncAction("reviews list", options, () => {
889
1238
  const params = new URLSearchParams();
890
- if (readOption(options.status))
891
- params.set("status", readOption(options.status));
892
- if (readOption(options.table))
893
- params.set("table_id", readOption(options.table));
894
- if (readOption(options.limit))
895
- params.set("limit", readOption(options.limit));
1239
+ const status = readOption(options.status);
1240
+ const table = readOption(options.table);
1241
+ const limit = readOption(options.limit);
1242
+ if (status)
1243
+ params.set("status", status);
1244
+ if (table)
1245
+ params.set("table_id", table);
1246
+ if (limit)
1247
+ params.set("limit", limit);
896
1248
  const qs = params.toString() ? `?${params.toString()}` : "";
897
1249
  return requestOxygen(`/api/cli/message-reviews${qs}`);
898
1250
  });
@@ -902,10 +1254,11 @@ export function createProgram() {
902
1254
  .option("--table <table_id>", "Filter by table id.")
903
1255
  .option("--json", "Print a JSON envelope.")
904
1256
  .action(async (options) => {
905
- await handleAsyncAction("reviews next", options, async () => {
1257
+ await handleAsyncAction("reviews next", options, () => {
906
1258
  const params = new URLSearchParams();
907
- if (readOption(options.table))
908
- params.set("table_id", readOption(options.table));
1259
+ const table = readOption(options.table);
1260
+ if (table)
1261
+ params.set("table_id", table);
909
1262
  const qs = params.toString() ? `?${params.toString()}` : "";
910
1263
  return requestOxygen(`/api/cli/message-reviews/next${qs}`);
911
1264
  });
@@ -927,10 +1280,11 @@ export function createProgram() {
927
1280
  .option("--auto-rerun", "Trigger a single-row rerun of the column after rejecting.")
928
1281
  .option("--json", "Print a JSON envelope.")
929
1282
  .action(async (reviewId, options) => {
930
- await handleAsyncAction("reviews reject", options, async () => {
1283
+ await handleAsyncAction("reviews reject", options, () => {
931
1284
  const body = { id: reviewId, decision: "reject" };
932
- if (readOption(options.highlightsJson)) {
933
- body.highlights = JSON.parse(readOption(options.highlightsJson));
1285
+ const highlightsJson = readOption(options.highlightsJson);
1286
+ if (highlightsJson) {
1287
+ body.highlights = JSON.parse(highlightsJson);
934
1288
  }
935
1289
  if (options.autoRerun)
936
1290
  body.auto_rerun = true;
@@ -1039,21 +1393,21 @@ export function createProgram() {
1039
1393
  ...(localConcurrency ? { concurrency: localConcurrency } : {}),
1040
1394
  });
1041
1395
  }
1042
- return requestOxygen("/api/cli/tables/columns/run", {
1043
- method: "POST",
1044
- body: {
1045
- table,
1046
- column,
1047
- ...(options.all ? { selection: { mode: "all" } } : {}),
1048
- ...(filterSelection ? { selection: filterSelection } : {}),
1049
- ...(!options.all && readOption(options.rowId) ? { row_id: readOption(options.rowId) } : {}),
1050
- ...(!options.all && !filterSelection && limit ? { limit } : {}),
1051
- ...(options.force ? { force: true } : {}),
1052
- ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
1053
- ...(options.background ? { background: true } : {}),
1054
- ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
1055
- ...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
1056
- },
1396
+ const body = {
1397
+ table,
1398
+ column,
1399
+ ...(options.all ? { selection: { mode: "all" } } : {}),
1400
+ ...(filterSelection ? { selection: filterSelection } : {}),
1401
+ ...(!options.all && readOption(options.rowId) ? { row_id: readOption(options.rowId) } : {}),
1402
+ ...(!options.all && !filterSelection && limit ? { limit } : {}),
1403
+ ...(options.force ? { force: true } : {}),
1404
+ ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
1405
+ ...(options.background ? { background: true } : {}),
1406
+ ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
1407
+ ...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
1408
+ };
1409
+ return requestColumnsRun(body, table, {
1410
+ background: Boolean(options.background),
1057
1411
  });
1058
1412
  });
1059
1413
  }))
@@ -1121,10 +1475,11 @@ export function createProgram() {
1121
1475
  .argument("<column>", "Column id or key.")
1122
1476
  .option("--label <label>", "New display label.")
1123
1477
  .option("--semantic-type <type>", "New semantic type.")
1124
- .option("--definition-json <json>", "Definition metadata to merge into the column.")
1478
+ .option("--definition-json <json>", "Definition metadata to shallow-merge into the column; arrays are replaced wholesale and missing keys leave existing fields unchanged.")
1479
+ .option("--dry-run", "Return the would-be merged definition without writing.")
1125
1480
  .option("--json", "Print a JSON envelope.")
1126
1481
  .action(async (table, column, options) => {
1127
- await handleAsyncAction("columns update", options, async () => requestOxygen("/api/cli/tables/columns/update", {
1482
+ await handleAsyncAction("columns update", options, () => requestOxygen("/api/cli/tables/columns/update", {
1128
1483
  method: "POST",
1129
1484
  body: {
1130
1485
  table,
@@ -1132,6 +1487,7 @@ export function createProgram() {
1132
1487
  ...(readOption(options.label) ? { label: readOption(options.label) } : {}),
1133
1488
  ...(readOption(options.semanticType) ? { semantic_type: readOption(options.semanticType) } : {}),
1134
1489
  ...(options.definitionJson ? { definition: parseJsonObject(options.definitionJson) } : {}),
1490
+ ...(options.dryRun ? { dry_run: true } : {}),
1135
1491
  },
1136
1492
  }));
1137
1493
  }))
@@ -1234,10 +1590,19 @@ export function createProgram() {
1234
1590
  ...(options.metadataJson ? { metadata: parseJsonObject(options.metadataJson) } : {}),
1235
1591
  },
1236
1592
  }));
1593
+ }))
1594
+ .addCommand(new Command("list")
1595
+ .description("List durable table action runs for one table.")
1596
+ .requiredOption("--table <table>", "Table id or slug.")
1597
+ .option("--status <status>", "Filter by active, queued, running, paused, completed, completed_with_errors, failed, canceling, or canceled. Defaults to active.")
1598
+ .option("--limit <n>", "Maximum runs to return. Defaults to 20.")
1599
+ .option("--json", "Print a JSON envelope.")
1600
+ .action(async (options) => {
1601
+ await handleAsyncAction("table-runs list", options, () => requestOxygen(tableRunsListPath(options)));
1237
1602
  }))
1238
1603
  .addCommand(new Command("get")
1239
1604
  .description("Get one durable table action run.")
1240
- .argument("<run_id>", "Table action run UUID.")
1605
+ .argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
1241
1606
  .option("--json", "Print a JSON envelope.")
1242
1607
  .action(async (runId, options) => {
1243
1608
  await handleAsyncAction("table-runs get", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}`));
@@ -1262,7 +1627,7 @@ export function createProgram() {
1262
1627
  }))
1263
1628
  .addCommand(new Command("wait")
1264
1629
  .description("Poll a durable table action run until it finishes.")
1265
- .argument("<run_id>", "Table action run UUID.")
1630
+ .argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
1266
1631
  .option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
1267
1632
  .option("--interval-seconds <n>", "Polling interval. Defaults to 5.")
1268
1633
  .option("--json", "Print a JSON envelope.")
@@ -1278,7 +1643,7 @@ export function createProgram() {
1278
1643
  }))
1279
1644
  .addCommand(new Command("cancel")
1280
1645
  .description("Request cancellation for a durable table action run.")
1281
- .argument("<run_id>", "Table action run UUID.")
1646
+ .argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
1282
1647
  .option("--json", "Print a JSON envelope.")
1283
1648
  .action(async (runId, options) => {
1284
1649
  await handleAsyncAction("table-runs cancel", options, async () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}/cancel`, {
@@ -1532,7 +1897,7 @@ export function createProgram() {
1532
1897
  });
1533
1898
  }))
1534
1899
  .addCommand(new Command("repair")
1535
- .description("Repair stale background action and ingestion queue state.")
1900
+ .description("Repair stale background action, ingestion, and workflow queue state.")
1536
1901
  .option("--json", "Print a JSON envelope.")
1537
1902
  .action(async (options) => {
1538
1903
  await handleAsyncAction("worker repair", options, async () => requestOxygen("/api/cli/worker/repair", {
@@ -1828,6 +2193,36 @@ export function createProgram() {
1828
2193
  .action(async (options) => {
1829
2194
  await handleAsyncAction("session usage", options, async () => getSessionUsage({ sessionId: readOption(options.sessionId) }));
1830
2195
  }));
2196
+ program
2197
+ .command("custom-integrations")
2198
+ .description("Custom HTTP integration commands.")
2199
+ .addCommand(new Command("apply")
2200
+ .description("Create or update a custom HTTP integration manifest. Store credentials from the web Connections page.")
2201
+ .option("--manifest <path>", "Path to a custom HTTP manifest JSON file.")
2202
+ .option("--manifest-json <json>", "Custom HTTP manifest as a JSON object.")
2203
+ .option("--json", "Print a JSON envelope.")
2204
+ .action(async (options) => {
2205
+ await handleAsyncAction("custom-integrations apply", options, () => {
2206
+ const manifest = readCustomIntegrationManifest(options);
2207
+ return requestOxygen("/api/cli/custom-integrations", {
2208
+ method: "POST",
2209
+ body: { manifest },
2210
+ });
2211
+ });
2212
+ }))
2213
+ .addCommand(new Command("list")
2214
+ .description("List custom HTTP integrations for the active organization.")
2215
+ .option("--slug <slug>", "Filter to one custom integration slug.")
2216
+ .option("--json", "Print a JSON envelope.")
2217
+ .action(async (options) => {
2218
+ await handleAsyncAction("custom-integrations list", options, () => {
2219
+ const params = new URLSearchParams();
2220
+ if (readOption(options.slug))
2221
+ params.set("slug", readOption(options.slug) ?? "");
2222
+ const suffix = params.toString();
2223
+ return requestOxygen(`/api/cli/custom-integrations${suffix ? `?${suffix}` : ""}`);
2224
+ });
2225
+ }));
1831
2226
  program
1832
2227
  .command("tools")
1833
2228
  .description("Tool catalog commands.")
@@ -1858,11 +2253,48 @@ export function createProgram() {
1858
2253
  .action(async (toolId, options) => {
1859
2254
  await handleAsyncAction("tools get", options, async () => requestOxygen(`/api/cli/tools/${encodeURIComponent(toolId)}`));
1860
2255
  }))
2256
+ .addCommand(new Command("enums")
2257
+ .description("Provider enum catalogs for fields that accept normalized values.")
2258
+ .addCommand(new Command("list")
2259
+ .description("List enum catalogs for a provider (or all providers).")
2260
+ .option("--provider <provider>", "Filter to a single provider, e.g. blitzapi.")
2261
+ .option("--json", "Print a JSON envelope.")
2262
+ .action(async (options) => {
2263
+ await handleAsyncAction("tools enums list", options, () => {
2264
+ const params = new URLSearchParams();
2265
+ const provider = readOption(options.provider);
2266
+ if (provider)
2267
+ params.set("provider", provider);
2268
+ const suffix = params.toString();
2269
+ return requestOxygen(`/api/cli/tools/enums${suffix ? `?${suffix}` : ""}`);
2270
+ });
2271
+ }))
2272
+ .addCommand(new Command("get")
2273
+ .description("Fetch the accepted values for one provider enum catalog. Use this before drafting payloads with enumRef fields.")
2274
+ .argument("<provider>", "Provider name, e.g. blitzapi.")
2275
+ .argument("<enum_ref>", "Enum reference id, e.g. industry, employee_range, country_code.")
2276
+ .option("-q, --query <query>", "Case-insensitive substring or token search across the catalog values.")
2277
+ .option("--limit <limit>", "Maximum number of values to return. Defaults to 100. Maximum 1000.")
2278
+ .option("--json", "Print a JSON envelope.")
2279
+ .action(async (provider, enumRef, options) => {
2280
+ await handleAsyncAction("tools enums get", options, () => {
2281
+ const params = new URLSearchParams();
2282
+ const query = readOption(options.query);
2283
+ const limit = readOption(options.limit);
2284
+ if (query)
2285
+ params.set("q", query);
2286
+ if (limit)
2287
+ params.set("limit", limit);
2288
+ const suffix = params.toString();
2289
+ return requestOxygen(`/api/cli/tools/enums/${encodeURIComponent(provider)}/${encodeURIComponent(enumRef)}${suffix ? `?${suffix}` : ""}`);
2290
+ });
2291
+ })))
1861
2292
  .addCommand(new Command("run")
1862
2293
  .description("Run an executable Oxygen tool through the HTTPS API.")
1863
2294
  .argument("<tool_id>", "Tool id.")
1864
2295
  .requiredOption("--input-json <json>", "Tool input as a JSON object.")
1865
2296
  .option("--mode <mode>", "Execution mode: live, dry-run, or dry_run. Mutating tools default to dry-run.")
2297
+ .option("--credential-mode <mode>", "Credential mode: managed, user_api_key, or user_oauth.")
1866
2298
  .option("--org <organization_id>", "Optional organization id. Must match the stored CLI token.")
1867
2299
  .option("--org-id <organization_id>", "Optional organization id. Must match the stored CLI token.")
1868
2300
  .option("--connection-id <connection_id>", "Specific integration connection id. Defaults to the org's active default connection.")
@@ -1872,12 +2304,13 @@ export function createProgram() {
1872
2304
  .option("--oxygen-cursor <cursor>", "Short Oxygen cursor returned as oxygen_next_cursor by a previous tool run.")
1873
2305
  .option("--json", "Print a JSON envelope.")
1874
2306
  .action(async (toolId, options) => {
1875
- await handleAsyncAction("tools run", options, async () => requestOxygen("/api/cli/tools/run", {
2307
+ await handleAsyncAction("tools run", options, () => requestOxygen("/api/cli/tools/run", {
1876
2308
  method: "POST",
1877
2309
  body: {
1878
2310
  tool_id: toolId,
1879
2311
  input: parseJsonObject(options.inputJson),
1880
2312
  ...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
2313
+ ...(readOption(options.credentialMode) ? { credential_mode: readOption(options.credentialMode) } : {}),
1881
2314
  ...(readOption(options.org) ? { org_id: readOption(options.org) } : {}),
1882
2315
  ...(readOption(options.orgId) ? { org_id: readOption(options.orgId) } : {}),
1883
2316
  ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
@@ -1906,7 +2339,7 @@ export function createProgram() {
1906
2339
  .option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
1907
2340
  .option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
1908
2341
  .option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
1909
- .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.")
2342
+ .option("--provider-order <providers>", "Comma-separated provider order. Work email defaults to blitzapi,prospeo,leadmagic,hunter,contactout,bettercontact,ai_ark; phone defaults to blitzapi,prospeo,leadmagic,contactout,bettercontact,ai_ark.")
1910
2343
  .option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
1911
2344
  .option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
1912
2345
  .option("--limit <n>", "Rows to estimate. Defaults to 10.")
@@ -1937,7 +2370,7 @@ export function createProgram() {
1937
2370
  .option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
1938
2371
  .option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
1939
2372
  .option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
1940
- .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.")
2373
+ .option("--provider-order <providers>", "Comma-separated provider order. Work email defaults to blitzapi,prospeo,leadmagic,hunter,contactout,bettercontact,ai_ark; phone defaults to blitzapi,prospeo,leadmagic,contactout,bettercontact,ai_ark.")
1941
2374
  .option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
1942
2375
  .option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
1943
2376
  .option("--limit <n>", "Rows to queue.")
@@ -1960,6 +2393,25 @@ export function createProgram() {
1960
2393
  },
1961
2394
  }));
1962
2395
  }));
2396
+ program
2397
+ .command("enrichment")
2398
+ .description("High-level enrichment column definition helpers.")
2399
+ .addCommand(new Command("apply-default-cascade")
2400
+ .description("Patch an existing enrichment column to the server-side default provider cascade for its intent.")
2401
+ .argument("<table>", "Table id or slug.")
2402
+ .argument("<column>", "Enrichment column id or key.")
2403
+ .option("--dry-run", "Return the would-be merged definition without writing.")
2404
+ .option("--json", "Print a JSON envelope.")
2405
+ .action(async (table, column, options) => {
2406
+ await handleAsyncAction("enrichment apply-default-cascade", options, () => requestOxygen("/api/cli/enrichment/apply-default-cascade", {
2407
+ method: "POST",
2408
+ body: {
2409
+ table,
2410
+ column,
2411
+ ...(options.dryRun ? { dry_run: true } : {}),
2412
+ },
2413
+ }));
2414
+ }));
1963
2415
  program
1964
2416
  .command("integrations")
1965
2417
  .description("Integration connection and event trigger commands.")
@@ -2035,6 +2487,33 @@ export function createProgram() {
2035
2487
  ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
2036
2488
  },
2037
2489
  }));
2490
+ }))
2491
+ .addCommand(new Command("deliveries")
2492
+ .description("List recent provider webhook deliveries (Composio triggers, HubSpot webhooks, etc.) and the workflow runs they kicked off.")
2493
+ .option("--source <source>", "Filter by event source, such as hubspot or composio.gmail.")
2494
+ .option("--event <event>", "Filter by event type, such as contact.created or GMAIL_NEW_GMAIL_MESSAGE.")
2495
+ .option("--toolkit <id>", "Filter by integration/toolkit id, such as gmail or hubspot.")
2496
+ .option("--connection-id <connection_id>", "Filter by a specific integration connection id.")
2497
+ .option("--result <result>", "Filter by delivery result: success or error.")
2498
+ .option("--limit <n>", "Maximum deliveries to return (1-200). Defaults to 50.")
2499
+ .option("--json", "Print a JSON envelope.")
2500
+ .action(async (options) => {
2501
+ await handleAsyncAction("integrations events deliveries", options, () => {
2502
+ const query = new URLSearchParams();
2503
+ const set = (key, value) => {
2504
+ const trimmed = readOption(value);
2505
+ if (trimmed)
2506
+ query.set(key, trimmed);
2507
+ };
2508
+ set("source", options.source);
2509
+ set("event", options.event);
2510
+ set("toolkit", options.toolkit);
2511
+ set("connection_id", options.connectionId);
2512
+ set("result", options.result);
2513
+ set("limit", options.limit);
2514
+ const suffix = query.toString() ? `?${query.toString()}` : "";
2515
+ return requestOxygen(`/api/cli/integrations/events/deliveries${suffix}`);
2516
+ });
2038
2517
  })))
2039
2518
  .addCommand(new Command("list")
2040
2519
  .description("List supported Composio integrations and this org's connections.")
@@ -2062,11 +2541,16 @@ export function createProgram() {
2062
2541
  .addCommand(new Command("disconnect")
2063
2542
  .description("Disconnect a Composio integration.")
2064
2543
  .argument("<integration_id>", "Integration id, such as 'slack'.")
2544
+ .option("--connection-id <id>", "Disconnect a specific connected account when multiple exist.")
2065
2545
  .option("--json", "Print a JSON envelope.")
2066
2546
  .action(async (integrationId, options) => {
2067
- await handleAsyncAction("integrations disconnect", options, async () => requestOxygen("/api/cli/integrations/composio/disconnect", {
2547
+ const connectionId = readOption(options.connectionId)?.trim();
2548
+ await handleAsyncAction("integrations disconnect", options, () => requestOxygen("/api/cli/integrations/composio/disconnect", {
2068
2549
  method: "POST",
2069
- body: { integration_id: integrationId },
2550
+ body: {
2551
+ integration_id: integrationId,
2552
+ ...(connectionId ? { connection_id: connectionId } : {}),
2553
+ },
2070
2554
  }));
2071
2555
  }))
2072
2556
  .addCommand(new Command("actions")
@@ -2335,6 +2819,7 @@ export function createProgram() {
2335
2819
  .addCommand(new Command("failures")
2336
2820
  .description("List failed workflow runs and trigger scheduler failures.")
2337
2821
  .option("--limit <n>", "Maximum failures per group. Defaults to 25; server cap is 100.")
2822
+ .option("--include-bundle", "Include durable recipe bundles in JSON output.")
2338
2823
  .option("--json", "Print a JSON envelope.")
2339
2824
  .action(async (options) => {
2340
2825
  await handleAsyncAction("workflows failures", options, async () => {
@@ -2342,8 +2827,10 @@ export function createProgram() {
2342
2827
  const limit = readPositiveInt(options.limit);
2343
2828
  if (limit)
2344
2829
  query.set("limit", String(limit));
2830
+ if (options.includeBundle)
2831
+ query.set("include_bundle", "true");
2345
2832
  const suffix = query.toString() ? `?${query.toString()}` : "";
2346
- return requestOxygen(`/api/cli/workflows/failures${suffix}`);
2833
+ return prepareWorkflowCliOutput(await requestOxygen(`/api/cli/workflows/failures${suffix}`), options);
2347
2834
  });
2348
2835
  }))
2349
2836
  .addCommand(new Command("run")
@@ -2420,7 +2907,7 @@ export function createProgram() {
2420
2907
  .option("--agents <agents...>", "Space or comma separated agents. Defaults to codex, claude-code, and cursor.")
2421
2908
  .option("--skill <skill>", "Skill name or '*'. Defaults to '*'.")
2422
2909
  .option("--project", "Install into the current project instead of global agent scope.")
2423
- .option("--copy", "Copy skill files instead of symlinking when supported by npx skills.")
2910
+ .option("--copy", "Copy skill files instead of symlinking when supported by npx skills. Default on Windows, where symlinks need Developer Mode or admin.")
2424
2911
  .option("--json", "Print a JSON envelope.")
2425
2912
  .action(async (options) => {
2426
2913
  await handleAsyncAction("skills install", options, async () => installAgentSkills(options));
@@ -2598,11 +3085,11 @@ async function importTranspiledWorkflowModule(absolutePath, source) {
2598
3085
  esModuleInterop: true,
2599
3086
  },
2600
3087
  }).outputText;
2601
- const workflowsUrl = await import.meta.resolve("@oxygen/workflows");
3088
+ const dir = mkdtempSync(`${tmpdir()}/oxygen-workflow-`);
3089
+ const workflowsUrl = await resolveWorkflowRuntimeModuleUrl(dir);
2602
3090
  const rewritten = transpiled
2603
3091
  .replaceAll("from \"@oxygen/workflows\"", `from "${workflowsUrl}"`)
2604
3092
  .replaceAll("from '@oxygen/workflows'", `from "${workflowsUrl}"`);
2605
- const dir = mkdtempSync(`${tmpdir()}/oxygen-workflow-`);
2606
3093
  const compiledPath = `${dir}/workflow.mjs`;
2607
3094
  writeFileSync(compiledPath, rewritten, "utf8");
2608
3095
  try {
@@ -2612,6 +3099,84 @@ async function importTranspiledWorkflowModule(absolutePath, source) {
2612
3099
  rmSync(dir, { recursive: true, force: true });
2613
3100
  }
2614
3101
  }
3102
+ async function resolveWorkflowRuntimeModuleUrl(tempDir) {
3103
+ try {
3104
+ return await import.meta.resolve("@oxygen/workflows");
3105
+ }
3106
+ catch {
3107
+ const shimPath = `${tempDir}/oxygen-workflows-shim.mjs`;
3108
+ writeFileSync(shimPath, workflowRuntimeShimSource(), "utf8");
3109
+ return pathToFileURL(shimPath).href;
3110
+ }
3111
+ }
3112
+ function workflowRuntimeShimSource() {
3113
+ return `
3114
+ export function defineWorkflow(input) {
3115
+ return {
3116
+ __oxygen_workflow_definition: true,
3117
+ id: input.id,
3118
+ name: input.name,
3119
+ ...(input.status ? { status: input.status } : {}),
3120
+ ...(input.specification ? { specification: input.specification } : {}),
3121
+ ...(input.trigger ? { trigger: input.trigger } : {}),
3122
+ ...(input.inputSchema ? { inputSchema: input.inputSchema } : {}),
3123
+ steps: input.steps,
3124
+ };
3125
+ }
3126
+ export function apiTrigger(input = {}) {
3127
+ return { type: "api", ...(input.status ? { status: input.status } : {}) };
3128
+ }
3129
+ export function webhookTrigger(input) {
3130
+ return {
3131
+ type: "webhook",
3132
+ trigger_id: input.id,
3133
+ ...(input.name !== undefined ? { trigger_name: input.name } : {}),
3134
+ secret_required: input.secret ?? true,
3135
+ ...(input.idempotencyKeyPath !== undefined ? { idempotency_key_path: input.idempotencyKeyPath } : {}),
3136
+ ...(input.status ? { status: input.status } : {}),
3137
+ };
3138
+ }
3139
+ export function cronTrigger(input) {
3140
+ return {
3141
+ type: "cron",
3142
+ cron: input.cron,
3143
+ ...(input.timezone !== undefined ? { timezone: input.timezone } : {}),
3144
+ ...(input.status ? { status: input.status } : {}),
3145
+ };
3146
+ }
3147
+ export function eventTrigger(input) {
3148
+ return {
3149
+ type: "event",
3150
+ source: input.source,
3151
+ event: input.event,
3152
+ ...(input.filters ? { filters: input.filters } : {}),
3153
+ ...(input.idempotencyKeyPath !== undefined ? { idempotency_key_path: input.idempotencyKeyPath } : {}),
3154
+ ...(input.status ? { status: input.status } : {}),
3155
+ };
3156
+ }
3157
+ export function transformStep(input) {
3158
+ return {
3159
+ __oxygen_workflow_step: true,
3160
+ kind: "transform",
3161
+ id: input.id,
3162
+ ...(input.description ? { description: input.description } : {}),
3163
+ run: input.run,
3164
+ };
3165
+ }
3166
+ export function toolStep(input) {
3167
+ return {
3168
+ __oxygen_workflow_step: true,
3169
+ kind: "tool",
3170
+ id: input.id,
3171
+ ...(input.description ? { description: input.description } : {}),
3172
+ tool: input.tool,
3173
+ ...(input.effect ? { effect: input.effect } : {}),
3174
+ ...(input.mode ? { mode: input.mode } : {}),
3175
+ payload: input.payload,
3176
+ };
3177
+ }
3178
+ `;
3179
+ }
2615
3180
  async function tailWorkflowRun(runId, options) {
2616
3181
  const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
2617
3182
  ?? WORKFLOW_TAIL_DEFAULT_TIMEOUT_SECONDS;
@@ -2640,12 +3205,23 @@ async function tailWorkflowRun(runId, options) {
2640
3205
  }
2641
3206
  const remainingMs = deadline - Date.now();
2642
3207
  if (remainingMs <= 0) {
3208
+ const worker = isRecord(run.worker) ? run.worker : null;
3209
+ const queuedWithoutWorker = status === "queued"
3210
+ && worker !== null
3211
+ && worker.active === null
3212
+ && worker.lastClaim === null;
2643
3213
  throw new OxygenError("workflow_tail_timeout", "Timed out waiting for workflow run to finish.", {
2644
3214
  details: {
2645
3215
  workflow_run_id: runId,
2646
3216
  status: status ?? null,
2647
3217
  timeout_seconds: timeoutSeconds,
2648
3218
  polls,
3219
+ ...(queuedWithoutWorker
3220
+ ? {
3221
+ worker_status: "queued_no_worker",
3222
+ guidance: "No worker has claimed this workflow run. Check `oxygen worker queue-stats --json` for BullMQ and workflow queue health.",
3223
+ }
3224
+ : {}),
2649
3225
  },
2650
3226
  exitCode: 1,
2651
3227
  });
@@ -2745,23 +3321,89 @@ function readTableRunSelection(options) {
2745
3321
  exitCode: 1,
2746
3322
  });
2747
3323
  }
2748
- function readCompaniesEnrichBody(table, options) {
2749
- const body = { table };
2750
- const fields = readCsvOption(options.missingFields);
2751
- const providers = readCsvOption(options.providers);
2752
- const selection = readCompaniesEnrichSelection(options);
2753
- const mode = readOption(options.mode);
2754
- const maxCredits = readPositiveNumber(options.maxCredits);
2755
- if (fields.length > 0)
2756
- body.missing_fields = fields;
2757
- if (providers.length > 0)
2758
- body.providers = providers;
2759
- if (selection)
2760
- body.selection = selection;
2761
- if (mode)
2762
- body.mode = mode;
2763
- if (maxCredits !== undefined)
2764
- body.max_credits = maxCredits;
3324
+ function tableRunsListPath(options) {
3325
+ const table = readOption(options.table);
3326
+ if (!table) {
3327
+ throw new OxygenError("invalid_table_run", "Pass --table <table>.", {
3328
+ exitCode: 1,
3329
+ });
3330
+ }
3331
+ const query = new URLSearchParams({ table });
3332
+ const status = readOption(options.status);
3333
+ const limit = readPositiveInt(options.limit);
3334
+ if (status)
3335
+ query.set("status", status);
3336
+ if (limit)
3337
+ query.set("limit", String(limit));
3338
+ if (options.traceId)
3339
+ query.set("trace_id", options.traceId);
3340
+ return `/api/cli/table-action-runs?${query.toString()}`;
3341
+ }
3342
+ async function requestColumnsRun(body, table, options) {
3343
+ const traceId = randomUUID();
3344
+ try {
3345
+ return await requestOxygen("/api/cli/tables/columns/run", {
3346
+ method: "POST",
3347
+ body,
3348
+ traceId,
3349
+ });
3350
+ }
3351
+ catch (error) {
3352
+ if (!options.background || !isNetworkTimeoutError(error))
3353
+ throw error;
3354
+ const recovered = await recoverBackgroundColumnRun(table, traceId);
3355
+ if (!recovered)
3356
+ throw error;
3357
+ return recovered;
3358
+ }
3359
+ }
3360
+ async function recoverBackgroundColumnRun(table, traceId) {
3361
+ for (let attempt = 0; attempt < 3; attempt += 1) {
3362
+ const listed = await requestOxygen(tableRunsListPath({ table, status: "active", limit: "5", traceId }), { timeoutMs: 30_000 });
3363
+ const runs = Array.isArray(listed.runs)
3364
+ ? listed.runs.filter(isRecord)
3365
+ : [];
3366
+ const run = runs[0];
3367
+ if (run) {
3368
+ const actionRunId = readRecordString(run, "action_run_id") ?? readRecordString(run, "id");
3369
+ const workspaceRunId = readRecordString(run, "workspace_run_id") ?? readRecordString(run, "runId");
3370
+ return {
3371
+ ...run,
3372
+ ...(actionRunId ? { action_run_id: actionRunId } : {}),
3373
+ ...(workspaceRunId ? { workspace_run_id: workspaceRunId } : {}),
3374
+ timeout_recovered: true,
3375
+ recovery: {
3376
+ reason: "columns_run_background_timeout",
3377
+ trace_id: traceId,
3378
+ lookup_command: `oxygen table-runs list --table ${table} --status active --json`,
3379
+ },
3380
+ };
3381
+ }
3382
+ if (attempt < 2)
3383
+ await sleep(750);
3384
+ }
3385
+ return null;
3386
+ }
3387
+ function isNetworkTimeoutError(error) {
3388
+ return error instanceof OxygenError && error.code === "network_timeout";
3389
+ }
3390
+ function readCompaniesEnrichBody(table, options) {
3391
+ const body = { table };
3392
+ const fields = readCsvOption(options.missingFields);
3393
+ const providers = readCsvOption(options.providers);
3394
+ const selection = readCompaniesEnrichSelection(options);
3395
+ const mode = readOption(options.mode);
3396
+ const maxCredits = readPositiveNumber(options.maxCredits);
3397
+ if (fields.length > 0)
3398
+ body.missing_fields = fields;
3399
+ if (providers.length > 0)
3400
+ body.providers = providers;
3401
+ if (selection)
3402
+ body.selection = selection;
3403
+ if (mode)
3404
+ body.mode = mode;
3405
+ if (maxCredits !== undefined)
3406
+ body.max_credits = maxCredits;
2765
3407
  if (options.force !== undefined)
2766
3408
  body.force = Boolean(options.force);
2767
3409
  return body;
@@ -2861,19 +3503,26 @@ async function importRows(table, options) {
2861
3503
  exitCode: 1,
2862
3504
  });
2863
3505
  }
2864
- const target = await prepareImportTarget(table, options, parsedRows);
2865
3506
  const batchSize = normalizeImportBatchSize(options.batchSize);
2866
3507
  const sourceHash = hashImportFile(options.file);
2867
3508
  const shouldUseBackground = options.background
2868
- || (!options.sync && target.rows.length > LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD);
3509
+ || (!options.sync && parsedRows.length > LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD);
2869
3510
  if (shouldUseBackground) {
2870
- return enqueueImportRows(target.tableRef, options, target.rows, format, batchSize, {
3511
+ // Prefer the object-storage fast path: upload the raw file once via a
3512
+ // presigned URL (no request-body limit) so the worker COPY-loads it.
3513
+ // Falls back to the inline multipart import when storage is unconfigured.
3514
+ const staged = await tryEnqueueStagedFileImport(table, options, format, parsedRows, {
3515
+ autoBackground: !options.background,
3516
+ sourceHash,
3517
+ });
3518
+ if (staged)
3519
+ return staged;
3520
+ return enqueueImportFile(table, options, format, batchSize, {
2871
3521
  autoBackground: !options.background,
2872
3522
  sourceHash,
2873
- createdTable: target.createdTable,
2874
- ...(target.upsertKey ? { upsertKey: target.upsertKey } : {}),
2875
3523
  });
2876
3524
  }
3525
+ const target = await prepareImportTarget(table, options, parsedRows);
2877
3526
  let rowCount = 0;
2878
3527
  let insertedCount = 0;
2879
3528
  let updatedCount = 0;
@@ -2939,6 +3588,7 @@ async function prepareImportTarget(table, options, parsedRows) {
2939
3588
  createdTable: null,
2940
3589
  tableWebUrl: null,
2941
3590
  upsertKey: options.upsertKey,
3591
+ sourceKeyMap: null,
2942
3592
  };
2943
3593
  }
2944
3594
  const normalized = normalizeRowsForNewTable(parsedRows);
@@ -2965,103 +3615,138 @@ async function prepareImportTarget(table, options, parsedRows) {
2965
3615
  createdTable: created,
2966
3616
  tableWebUrl: createdWebUrl ?? tableWebUrl(createdSlug),
2967
3617
  upsertKey: normalizeCreatedTableUpsertKey(options.upsertKey, normalized.keyBySource),
3618
+ sourceKeyMap: normalized.keyBySource,
2968
3619
  };
2969
3620
  }
2970
- async function enqueueImportRows(table, options, rows, format, batchSize, context) {
2971
- const batches = chunk(rows, batchSize);
2972
- const maxConcurrency = readPositiveInt(options.maxConcurrency);
2973
- const upsertKey = context.upsertKey ?? options.upsertKey;
2974
- const idempotencyKey = buildImportIdempotencyKey({
2975
- table,
2976
- sourceHash: context.sourceHash,
2977
- format,
2978
- mode: upsertKey ? "upsert" : "insert",
2979
- upsertKey: upsertKey ?? null,
2980
- batchSize,
2981
- });
2982
- const created = await requestOxygen("/api/cli/table-ingestion-runs", {
3621
+ async function tryEnqueueStagedFileImport(// skipcq: JS-R1005
3622
+ table, options, format, parsedRows, context) {
3623
+ if (options.create && table) {
3624
+ throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
3625
+ exitCode: 1,
3626
+ });
3627
+ }
3628
+ if (!options.create && !table) {
3629
+ throw new OxygenError("invalid_import_target", "Pass a table argument or --create <name>.", {
3630
+ exitCode: 1,
3631
+ });
3632
+ }
3633
+ const fileBuffer = readFileSync(options.file);
3634
+ const filename = basename(options.file);
3635
+ // Probe for a presigned upload URL. If object storage isn't configured the
3636
+ // server returns object_storage_not_configured; signal the caller to fall
3637
+ // back to the inline multipart import by returning null.
3638
+ let presigned;
3639
+ try {
3640
+ presigned = await requestOxygen("/api/cli/tables/import-url", {
3641
+ method: "POST",
3642
+ body: { file_name: filename, byte_length: fileBuffer.byteLength },
3643
+ });
3644
+ }
3645
+ catch (error) {
3646
+ if (error instanceof OxygenError && error.code === "object_storage_not_configured") {
3647
+ return null;
3648
+ }
3649
+ throw error;
3650
+ }
3651
+ const uploadUrl = readRecordString(presigned, "upload_url");
3652
+ const storageKey = readRecordString(presigned, "storage_key");
3653
+ const storageBucket = readRecordString(presigned, "storage_bucket");
3654
+ const storageProvider = readRecordString(presigned, "storage_provider") ?? "s3";
3655
+ if (!uploadUrl || !storageKey)
3656
+ return null;
3657
+ // Create the table for --create (or resolve the existing ref) before the
3658
+ // upload so a presign success always pairs with a real target.
3659
+ const target = await prepareImportTarget(table, options, parsedRows);
3660
+ const controller = new AbortController();
3661
+ const timer = setTimeout(() => controller.abort(), 300_000);
3662
+ let putResponse;
3663
+ try {
3664
+ putResponse = await fetch(uploadUrl, {
3665
+ method: "PUT",
3666
+ body: new Uint8Array(fileBuffer),
3667
+ signal: controller.signal,
3668
+ });
3669
+ }
3670
+ finally {
3671
+ clearTimeout(timer);
3672
+ }
3673
+ if (!putResponse.ok) {
3674
+ throw new OxygenError("import_upload_failed", `Uploading the import file to object storage failed (HTTP ${putResponse.status}).`, { details: { status: putResponse.status }, exitCode: 1 });
3675
+ }
3676
+ const result = await requestOxygen("/api/cli/tables/import-staged", {
2983
3677
  method: "POST",
3678
+ timeoutMs: 120_000,
2984
3679
  body: {
2985
- table,
2986
- source_type: `file.${format}`,
2987
- mode: upsertKey ? "upsert" : "insert",
2988
- ...(upsertKey ? { upsert_key: upsertKey } : {}),
2989
- expected_item_count: batches.length,
2990
- ...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
2991
- metadata: {
2992
- command: "tables import",
2993
- file_name: basename(options.file),
2994
- format,
2995
- ...(options.sheet ? { sheet: options.sheet } : {}),
2996
- requested_row_count: rows.length,
2997
- batch_size: batchSize,
2998
- auto_background: context.autoBackground,
2999
- source_hash: context.sourceHash,
3000
- idempotency_key: idempotencyKey,
3001
- },
3680
+ table: target.tableRef,
3681
+ storage_provider: storageProvider,
3682
+ storage_bucket: storageBucket,
3683
+ storage_key: storageKey,
3684
+ format,
3685
+ file_name: filename,
3686
+ byte_length: fileBuffer.byteLength,
3687
+ sha256: context.sourceHash,
3688
+ row_count: parsedRows.length,
3689
+ ...(target.upsertKey ? { upsert_key: target.upsertKey } : {}),
3690
+ ...(target.sourceKeyMap ? { source_key_map: target.sourceKeyMap } : {}),
3002
3691
  },
3003
3692
  });
3004
- const ingestionRunId = readRequiredResponseString(created, "id");
3005
- const queueWait = readRecord(created, "queue_wait");
3006
- if (isTerminalTableIngestionStatus(readRecordString(created, "status"))) {
3007
- return {
3008
- ingestionRun: created,
3009
- ingestionRunId,
3010
- ...(context.createdTable ? { createdTable: context.createdTable } : {}),
3011
- ...(readRecordString(created, "web_url") ? { web_url: readRecordString(created, "web_url") } : {}),
3012
- table_web_url: tableWebUrl(table),
3013
- background: true,
3014
- autoBackground: context.autoBackground,
3015
- sourceType: `file.${format}`,
3016
- rowCount: rows.length,
3017
- enqueuedItems: 0,
3018
- duplicatePositions: 0,
3019
- batchCount: batches.length,
3020
- batchSize,
3021
- mode: upsertKey ? "upsert" : "insert",
3022
- upsertKey: upsertKey ?? null,
3023
- idempotencyKey,
3024
- ...(queueWait ? { queue_wait: queueWait } : {}),
3025
- };
3693
+ emitQueueWaitStderrNote(readRecord(result, "queue_wait"));
3694
+ return {
3695
+ ...result,
3696
+ ...(target.createdTable ? { createdTable: target.createdTable } : {}),
3697
+ ...(target.tableWebUrl ? { table_web_url: target.tableWebUrl } : {}),
3698
+ background: true,
3699
+ autoBackground: context.autoBackground,
3700
+ import_engine: "bulk_file_v1",
3701
+ storage_provider: storageProvider,
3702
+ };
3703
+ }
3704
+ async function enqueueImportFile(table, options, format, batchSize, context) {
3705
+ if (options.create && table) {
3706
+ throw new OxygenError("invalid_import_target", "Pass either a table argument or --create, not both.", {
3707
+ exitCode: 1,
3708
+ });
3026
3709
  }
3027
- let insertedItems = 0;
3028
- let duplicatePositions = 0;
3029
- let latestRun = created;
3030
- for (const [index, batch] of batches.entries()) {
3031
- const appended = await requestOxygen(`/api/cli/table-ingestion-runs/${encodeURIComponent(ingestionRunId)}/items`, {
3032
- method: "POST",
3033
- body: {
3034
- items: [
3035
- {
3036
- position: index,
3037
- payload: { rows: batch },
3038
- },
3039
- ],
3040
- },
3710
+ if (!options.create && !table) {
3711
+ throw new OxygenError("invalid_import_target", "Pass a table argument or --create <name>.", {
3712
+ exitCode: 1,
3041
3713
  });
3042
- insertedItems += readCount(appended.inserted);
3043
- duplicatePositions += readCount(appended.duplicatePositions);
3044
- latestRun = appended.run ?? latestRun;
3045
3714
  }
3046
- emitQueueWaitStderrNote(queueWait);
3715
+ const fileBuffer = readFileSync(options.file);
3716
+ const filename = basename(options.file);
3717
+ const form = new FormData();
3718
+ const file = new File([fileBuffer], filename);
3719
+ form.append("file", file, filename);
3720
+ form.append("filename", filename);
3721
+ form.append("format", format);
3722
+ form.append("batch_size", String(batchSize));
3723
+ form.append("auto_background", String(context.autoBackground));
3724
+ form.append("source_hash", context.sourceHash);
3725
+ if (table)
3726
+ form.append("table", table);
3727
+ if (options.create)
3728
+ form.append("table_name", options.create);
3729
+ const project = readOption(options.project);
3730
+ if (project)
3731
+ form.append("project", project);
3732
+ const upsertKey = readOption(options.upsertKey);
3733
+ if (upsertKey)
3734
+ form.append("upsert_key", upsertKey);
3735
+ if (options.sheet)
3736
+ form.append("sheet", options.sheet);
3737
+ const maxConcurrency = readPositiveInt(options.maxConcurrency);
3738
+ if (maxConcurrency)
3739
+ form.append("max_concurrency", String(maxConcurrency));
3740
+ const result = await requestOxygen("/api/cli/tables/import-file", {
3741
+ method: "POST",
3742
+ timeoutMs: 300_000,
3743
+ formData: form,
3744
+ });
3745
+ emitQueueWaitStderrNote(readRecord(result, "queue_wait"));
3047
3746
  return {
3048
- ingestionRun: latestRun,
3049
- ingestionRunId,
3050
- ...(context.createdTable ? { createdTable: context.createdTable } : {}),
3051
- ...(readRecordString(latestRun, "web_url") ? { web_url: readRecordString(latestRun, "web_url") } : {}),
3052
- table_web_url: tableWebUrl(table),
3747
+ ...result,
3053
3748
  background: true,
3054
3749
  autoBackground: context.autoBackground,
3055
- sourceType: `file.${format}`,
3056
- rowCount: rows.length,
3057
- enqueuedItems: insertedItems,
3058
- duplicatePositions,
3059
- batchCount: batches.length,
3060
- batchSize,
3061
- mode: upsertKey ? "upsert" : "insert",
3062
- upsertKey: upsertKey ?? null,
3063
- idempotencyKey,
3064
- ...(queueWait ? { queue_wait: queueWait } : {}),
3065
3750
  };
3066
3751
  }
3067
3752
  function emitQueueWaitStderrNote(queueWait) {
@@ -3257,7 +3942,8 @@ async function exportTableBundle(table, options) {
3257
3942
  // stdout response a small summary so it's readable.
3258
3943
  return options.output ? summary : { ...summary, bundle };
3259
3944
  }
3260
- async function importTableBundle(options) {
3945
+ async function importTableBundle(// skipcq: JS-R1005
3946
+ options) {
3261
3947
  const path = options.file;
3262
3948
  if (!path || !path.trim()) {
3263
3949
  throw new OxygenError("invalid_input", "--file is required.", { exitCode: 1 });
@@ -3269,49 +3955,169 @@ async function importTableBundle(options) {
3269
3955
  if (columns.length === 0) {
3270
3956
  throw new OxygenError("invalid_bundle", "Bundle has no columns; nothing to import.", { exitCode: 1 });
3271
3957
  }
3272
- const tableName = readOption(options.name) ?? bundle.tableName;
3273
- if (!tableName) {
3274
- throw new OxygenError("invalid_bundle", "Bundle has no table.name and no --name override was provided.", { exitCode: 1 });
3958
+ const validColumnKeys = new Set(columns.map((column) => column.key).filter((key) => Boolean(key)));
3959
+ const into = readOption(options.into);
3960
+ const upsertKey = readOption(options.key);
3961
+ // Resuming/appending into an existing table without an upsert key would
3962
+ // duplicate every already-imported row. Require --key so the resume matches
3963
+ // rows instead of re-inserting them.
3964
+ if (into && !upsertKey) {
3965
+ throw new OxygenError("invalid_input", "--into requires --key so rows already imported are matched instead of duplicated. Pass --key <uniqueColumn>.", { details: { into }, exitCode: 1 });
3275
3966
  }
3276
- const createResponse = await requestOxygen("/api/cli/tables", {
3277
- method: "POST",
3278
- body: {
3279
- name: tableName,
3280
- columns,
3281
- ...(readOption(options.project) ? { project: readOption(options.project) } : {}),
3282
- },
3283
- });
3284
- const createdTable = readRecord(createResponse, "table");
3285
- const newTableId = readRecordString(createResponse, "table_id")
3286
- ?? (createdTable ? readRecordString(createdTable, "id") : null);
3287
- const newTableSlug = readRecordString(createResponse, "table_slug")
3288
- ?? (createdTable ? readRecordString(createdTable, "slug") : null);
3289
- if (!newTableId) {
3290
- throw new OxygenError("invalid_response", "Table create response did not include an id; bundle import cannot proceed.", { exitCode: 1 });
3967
+ if (upsertKey) {
3968
+ if (upsertKey.startsWith("_")) {
3969
+ throw new OxygenError("invalid_input", "--key must be a workspace column key, not an internal field.", { details: { key: upsertKey }, exitCode: 1 });
3970
+ }
3971
+ if (validColumnKeys.size > 0 && !validColumnKeys.has(upsertKey)) {
3972
+ throw new OxygenError("invalid_input", `--key ${upsertKey} is not a column in this bundle.`, { details: { key: upsertKey, available_keys: [...validColumnKeys] }, exitCode: 1 });
3973
+ }
3974
+ }
3975
+ // Resolve the target table: resume into an existing one (--into), or create a
3976
+ // fresh one from the bundle schema.
3977
+ let newTableId;
3978
+ let newTableSlug;
3979
+ let createdTable;
3980
+ if (into) {
3981
+ const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table: into } });
3982
+ const tableMeta = describe.table ?? null;
3983
+ const resolvedId = tableMeta ? readRecordString(tableMeta, "id") : null;
3984
+ if (!resolvedId) {
3985
+ throw new OxygenError("table_not_found", `--into table ${into} was not found in this org.`, { details: { into }, exitCode: 1 });
3986
+ }
3987
+ newTableId = resolvedId;
3988
+ newTableSlug = tableMeta ? readRecordString(tableMeta, "slug") ?? null : null;
3989
+ createdTable = false;
3990
+ }
3991
+ else {
3992
+ const tableName = readOption(options.name) ?? bundle.tableName;
3993
+ if (!tableName) {
3994
+ throw new OxygenError("invalid_bundle", "Bundle has no table.name and no --name override was provided.", { exitCode: 1 });
3995
+ }
3996
+ const createResponse = await requestOxygen("/api/cli/tables", {
3997
+ method: "POST",
3998
+ body: {
3999
+ name: tableName,
4000
+ columns,
4001
+ ...(readOption(options.project) ? { project: readOption(options.project) } : {}),
4002
+ },
4003
+ });
4004
+ const created = readRecord(createResponse, "table");
4005
+ const createdId = readRecordString(createResponse, "table_id")
4006
+ ?? (created ? readRecordString(created, "id") : null);
4007
+ if (!createdId) {
4008
+ throw new OxygenError("invalid_response", "Table create response did not include an id; bundle import cannot proceed.", { exitCode: 1 });
4009
+ }
4010
+ newTableId = createdId;
4011
+ newTableSlug = readRecordString(createResponse, "table_slug")
4012
+ ?? (created ? readRecordString(created, "slug") : null);
4013
+ createdTable = true;
3291
4014
  }
3292
- const validColumnKeys = new Set(columns.map((column) => column.key).filter((key) => Boolean(key)));
3293
4015
  const stagedRows = bundle.rows.map((row) => stripRowForImport(row, validColumnKeys));
3294
- let inserted = 0;
4016
+ // In upsert mode every row must carry the key value; otherwise the upsert
4017
+ // route rejects the whole batch midway. Fail fast with a clear message before
4018
+ // writing anything so we never leave a half-imported table behind for a
4019
+ // predictable data problem.
4020
+ if (upsertKey) {
4021
+ const missingIndex = stagedRows.findIndex((row) => row[upsertKey] === undefined || row[upsertKey] === null);
4022
+ if (missingIndex >= 0) {
4023
+ throw new OxygenError("invalid_bundle", `Row ${missingIndex + 1} is missing a value for the upsert key "${upsertKey}".`, { details: { key: upsertKey, row_number: missingIndex + 1 }, exitCode: 1 });
4024
+ }
4025
+ }
4026
+ const resumeCommand = buildBundleResumeCommand({
4027
+ file: path,
4028
+ tableId: newTableId,
4029
+ key: upsertKey ?? null,
4030
+ batchSize,
4031
+ });
4032
+ let processed = 0;
4033
+ let insertedCount = 0;
4034
+ let updatedCount = 0;
3295
4035
  for (let offset = 0; offset < stagedRows.length; offset += batchSize) {
3296
4036
  const batch = stagedRows.slice(offset, offset + batchSize);
3297
4037
  if (batch.length === 0)
3298
4038
  continue;
3299
- await requestOxygen("/api/cli/tables/rows", {
3300
- method: "POST",
3301
- body: { table: newTableId, rows: batch },
3302
- });
3303
- inserted += batch.length;
4039
+ try {
4040
+ if (upsertKey) {
4041
+ const response = await requestOxygen("/api/cli/tables/rows/upsert", {
4042
+ method: "POST",
4043
+ body: { table: newTableId, key: upsertKey, rows: batch, return: "summary" },
4044
+ });
4045
+ insertedCount += readCount(response.insertedCount);
4046
+ updatedCount += readCount(response.updatedCount);
4047
+ }
4048
+ else {
4049
+ await requestOxygen("/api/cli/tables/rows", {
4050
+ method: "POST",
4051
+ body: { table: newTableId, rows: batch },
4052
+ });
4053
+ insertedCount += batch.length;
4054
+ }
4055
+ processed += batch.length;
4056
+ }
4057
+ catch (error) {
4058
+ // A mid-loop failure used to leave a partial table with no way forward.
4059
+ // Surface exactly where it stopped and how to resume so the import is
4060
+ // recoverable instead of a silent half-write.
4061
+ const cause = error instanceof OxygenError
4062
+ ? error.code
4063
+ : error instanceof Error
4064
+ ? error.message
4065
+ : "unknown";
4066
+ const recovery = upsertKey
4067
+ ? `Re-run with --into ${newTableId} --key ${upsertKey} to resume — already-imported rows are matched by "${upsertKey}", not duplicated.`
4068
+ : `Resume with: ${resumeCommand}`;
4069
+ throw new OxygenError("bundle_import_incomplete", `Bundle import stopped after ${processed}/${stagedRows.length} rows. ${recovery}`, {
4070
+ details: {
4071
+ table_id: newTableId,
4072
+ table_slug: newTableSlug,
4073
+ rows_total: stagedRows.length,
4074
+ rows_processed: processed,
4075
+ failed_batch_offset: offset,
4076
+ failed_batch_size: batch.length,
4077
+ mode: upsertKey ? "upsert" : "insert",
4078
+ ...(upsertKey ? { key: upsertKey } : {}),
4079
+ resume_command: resumeCommand,
4080
+ web_url: tableWebUrl(newTableSlug ?? newTableId),
4081
+ cause,
4082
+ },
4083
+ exitCode: 1,
4084
+ });
4085
+ }
3304
4086
  }
3305
4087
  return {
3306
4088
  table_id: newTableId,
3307
4089
  table_slug: newTableSlug ?? null,
4090
+ created_table: createdTable,
3308
4091
  column_count: columns.length,
3309
- row_count: inserted,
4092
+ row_count: processed,
4093
+ mode: upsertKey ? "upsert" : "insert",
4094
+ ...(upsertKey
4095
+ ? { upsert_key: upsertKey, inserted_count: insertedCount, updated_count: updatedCount }
4096
+ : {}),
3310
4097
  source_table: bundle.tableSummary,
3311
4098
  schema_version: bundle.schemaVersion,
3312
4099
  web_url: tableWebUrl(newTableSlug ?? newTableId),
3313
4100
  };
3314
4101
  }
4102
+ // Builds the exact CLI command that resumes a failed `import-bundle` into the
4103
+ // table it already created. Always targets that table with --into + --key so the
4104
+ // resume is idempotent; falls back to a <uniqueColumn> placeholder when the
4105
+ // original run had no key (insert mode can't be safely auto-resumed).
4106
+ function buildBundleResumeCommand(input) {
4107
+ const fileArg = /\s/.test(input.file) ? `"${input.file}"` : input.file;
4108
+ const parts = [
4109
+ "oxygen tables import-bundle",
4110
+ `--file ${fileArg}`,
4111
+ `--into ${input.tableId}`,
4112
+ `--key ${input.key ?? "<uniqueColumn>"}`,
4113
+ ];
4114
+ // 500 is the default in normalizeImportBatchSize; only echo --batch-size when
4115
+ // the user picked something else, to keep the resume command minimal.
4116
+ if (input.batchSize !== 500) {
4117
+ parts.push(`--batch-size ${input.batchSize}`);
4118
+ }
4119
+ return parts.join(" ");
4120
+ }
3315
4121
  function toBundleColumn(column) {
3316
4122
  // Persist only the fields `tables create` knows how to ingest. Drop ids,
3317
4123
  // physical column names, archivedAt — those are tenant-local and would
@@ -3458,7 +4264,7 @@ function formatRows(rows, format, columns) {
3458
4264
  };
3459
4265
  }
3460
4266
  // "table": render with type-aware formatting and a Markdown-style frame.
3461
- const columnByKey = new Map(columns?.map((c) => [c.key, c]) ?? []);
4267
+ const columnByKey = new Map(columns?.map((column) => [column.key, column]) ?? []);
3462
4268
  let rescuedCount = 0;
3463
4269
  const headers = keys.map((key) => columnByKey.get(key)?.label ?? key);
3464
4270
  const formattedRows = rows.map((row) => keys.map((key) => {
@@ -3486,12 +4292,12 @@ function formatRows(rows, format, columns) {
3486
4292
  });
3487
4293
  const renderRow = (cells) => {
3488
4294
  const padded = cells.map((cell, i) => {
3489
- const w = widths[i] ?? 0;
3490
- return clipCell(cell, w).padEnd(w);
4295
+ const width = widths[i] ?? 0;
4296
+ return clipCell(cell, width).padEnd(width);
3491
4297
  });
3492
4298
  return `| ${padded.join(" | ")} |`;
3493
4299
  };
3494
- const separator = `|${widths.map((w) => "-".repeat(w + 2)).join("|")}|`;
4300
+ const separator = `|${widths.map((width) => "-".repeat(width + 2)).join("|")}|`;
3495
4301
  const lines = [renderRow(headers), separator, ...formattedRows.map(renderRow)];
3496
4302
  if (rescuedCount > 0) {
3497
4303
  lines.push("");
@@ -3542,15 +4348,6 @@ function isRecord(value) {
3542
4348
  function tableWebUrl(tableIdOrSlug) {
3543
4349
  return `https://oxygen-agent.com/tables/${encodeURIComponent(tableIdOrSlug)}`;
3544
4350
  }
3545
- function readRequiredResponseString(value, key) {
3546
- const entry = value[key];
3547
- if (typeof entry === "string" && entry.trim())
3548
- return entry.trim();
3549
- throw new OxygenError("invalid_response", `Oxygen API response is missing ${key}.`, {
3550
- details: { key },
3551
- exitCode: 1,
3552
- });
3553
- }
3554
4351
  function normalizeImportBatchSize(value) {
3555
4352
  const batchSize = readPositiveInt(value) ?? 500;
3556
4353
  if (batchSize > 5000) {
@@ -3564,17 +4361,6 @@ function normalizeImportBatchSize(value) {
3564
4361
  function hashImportFile(path) {
3565
4362
  return createHash("sha256").update(readFileSync(path)).digest("hex");
3566
4363
  }
3567
- function buildImportIdempotencyKey(input) {
3568
- return [
3569
- "tables-import",
3570
- input.table,
3571
- input.format,
3572
- input.mode,
3573
- input.upsertKey ?? "none",
3574
- String(input.batchSize),
3575
- input.sourceHash,
3576
- ].join(":");
3577
- }
3578
4364
  function isTerminalTableIngestionStatus(status) {
3579
4365
  return status === "completed"
3580
4366
  || status === "completed_with_errors"
@@ -3606,15 +4392,20 @@ async function handleWhoamiAction(options) {
3606
4392
  const identity = await requestOxygen("/api/cli/whoami");
3607
4393
  const context = await resolveActiveProfileWithSource();
3608
4394
  if (context.resolution.exists) {
3609
- const cached = context.resolution.credentials?.identity;
3610
- const orgChanged = !cached || cached.organization.id !== identity.organization.id;
4395
+ const existingCredentials = context.resolution.credentials;
4396
+ const cached = existingCredentials?.identity;
4397
+ const cachedOrg = existingCredentials?.activeOrganization ?? cached?.organization ?? null;
4398
+ const orgChanged = !cachedOrg || cachedOrg.id !== identity.organization.id;
3611
4399
  const userChanged = !cached || cached.user.id !== identity.user.id;
3612
- if (orgChanged || userChanged) {
4400
+ if (existingCredentials && (orgChanged || userChanged)) {
3613
4401
  const refreshed = {
3614
- token: context.resolution.credentials.token,
3615
- apiUrl: context.resolution.credentials.apiUrl,
4402
+ token: existingCredentials.token,
4403
+ apiUrl: existingCredentials.apiUrl,
4404
+ activeOrganization: storedOrganizationFromOption(identity.organization),
3616
4405
  identity: identityFromWhoami(identity),
3617
4406
  };
4407
+ if (existingCredentials.authKind)
4408
+ refreshed.authKind = existingCredentials.authKind;
3618
4409
  await saveCredentials(refreshed, process.env, {
3619
4410
  profile: context.resolution.name,
3620
4411
  activate: false,
@@ -3638,6 +4429,90 @@ async function handleWhoamiAction(options) {
3638
4429
  process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
3639
4430
  }
3640
4431
  }
4432
+ async function handleOnboardingStartAction(options) {
4433
+ try {
4434
+ const data = await requestOxygen("/api/cli/onboarding/start", { method: "POST" });
4435
+ if (options.json) {
4436
+ writeJson(success("onboarding start", data));
4437
+ return;
4438
+ }
4439
+ process.stdout.write(formatOnboardingStart(data));
4440
+ }
4441
+ catch (error) {
4442
+ const failure = toFailure("onboarding start", error);
4443
+ writeJson(failure);
4444
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
4445
+ }
4446
+ }
4447
+ async function handleOnboardingResetAction(options) {
4448
+ if (!options.confirm) {
4449
+ const failure = toFailure("onboarding reset", new OxygenError("confirmation_required", "Refusing to reset onboarding without --confirm. Re-run with `oxygen onboarding reset --confirm`.", { exitCode: 1 }));
4450
+ writeJson(failure);
4451
+ process.exitCode = 1;
4452
+ return;
4453
+ }
4454
+ try {
4455
+ const data = await requestOxygen("/api/cli/onboarding/reset", {
4456
+ method: "POST",
4457
+ body: { confirm: true },
4458
+ });
4459
+ if (options.json) {
4460
+ writeJson(success("onboarding reset", data));
4461
+ return;
4462
+ }
4463
+ process.stdout.write(formatOnboardingReset(data));
4464
+ }
4465
+ catch (error) {
4466
+ const failure = toFailure("onboarding reset", error);
4467
+ writeJson(failure);
4468
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
4469
+ }
4470
+ }
4471
+ function formatOnboardingReset(data) {
4472
+ const lines = [];
4473
+ lines.push(data.message);
4474
+ if (data.cleared && data.cleared_keys.length > 0) {
4475
+ lines.push(`Cleared keys: ${data.cleared_keys.join(", ")}`);
4476
+ }
4477
+ if (data.next_steps.length > 0) {
4478
+ lines.push("");
4479
+ lines.push("Next:");
4480
+ for (const step of data.next_steps)
4481
+ lines.push(` - ${step}`);
4482
+ }
4483
+ if (data.web_url) {
4484
+ lines.push("");
4485
+ lines.push(`Continue in the web app: ${data.web_url}`);
4486
+ }
4487
+ return `${lines.join("\n")}\n`;
4488
+ }
4489
+ function formatOnboardingStart(data) {
4490
+ const lines = [];
4491
+ lines.push(`Onboarding ${data.context.already_started ? "already started" : "started"} for ${data.organization.name} (${data.organization.slug ?? data.organization.id}).`);
4492
+ if (data.user.domain) {
4493
+ lines.push(`Inferred work domain: ${data.user.domain}`);
4494
+ }
4495
+ else if (data.user.email) {
4496
+ lines.push(`No work domain inferred from ${data.user.email}; the skill will ask you for one.`);
4497
+ }
4498
+ lines.push("");
4499
+ lines.push(`Filled context sections: ${data.context.filled_sections.length > 0 ? data.context.filled_sections.join(", ") : "(none)"}`);
4500
+ lines.push(`Empty context sections: ${data.context.empty_sections.length > 0 ? data.context.empty_sections.join(", ") : "(none)"}`);
4501
+ lines.push("");
4502
+ lines.push(`Next: load and follow the \`${data.skill_to_load}\` skill.`);
4503
+ lines.push(` Skill URL: ${data.skill_url}`);
4504
+ if (data.next_steps.length > 0) {
4505
+ lines.push("");
4506
+ lines.push("Steps:");
4507
+ for (const step of data.next_steps)
4508
+ lines.push(` - ${step}`);
4509
+ }
4510
+ if (data.web_url) {
4511
+ lines.push("");
4512
+ lines.push(`View context in the web app: ${data.web_url}`);
4513
+ }
4514
+ return `${lines.join("\n")}\n`;
4515
+ }
3641
4516
  async function handleLoginAction(options) {
3642
4517
  try {
3643
4518
  const data = await login(options);
@@ -3653,19 +4528,21 @@ async function handleLoginAction(options) {
3653
4528
  }
3654
4529
  async function handleAuthUseTokenAction(options) {
3655
4530
  try {
3656
- const data = await useAuthToken(options);
4531
+ const data = await applyAuthToken(options);
3657
4532
  if (options.json) {
3658
4533
  writeJson(success("auth use-token", {
3659
4534
  logged_in: true,
3660
4535
  profile: data.profile,
3661
4536
  api_url: data.credentials.apiUrl,
3662
- user: data.identity.user,
3663
- organization: data.identity.organization,
4537
+ user: data.loginIdentity.user,
4538
+ organization: data.loginIdentity.activeOrganization,
4539
+ organizations: data.loginIdentity.organizations,
4540
+ selection_required: data.loginIdentity.selectionRequired,
3664
4541
  renamed: data.renamed,
3665
4542
  }));
3666
4543
  return;
3667
4544
  }
3668
- process.stdout.write(formatLoginSuccess(data.identity, data.credentials, data.profile, { renamed: data.renamed }));
4545
+ process.stdout.write(formatLoginSuccessForResolved(data.loginIdentity, data.profile, { renamed: data.renamed }));
3669
4546
  const hint = await buildPostLoginHint(data.profile);
3670
4547
  if (hint)
3671
4548
  process.stdout.write(hint);
@@ -3679,6 +4556,38 @@ async function handleAuthUseTokenAction(options) {
3679
4556
  process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
3680
4557
  }
3681
4558
  }
4559
+ async function handleOrgUseAction(organization, options, command) {
4560
+ try {
4561
+ const data = await requestOxygen("/api/cli/orgs/select", {
4562
+ method: "POST",
4563
+ body: { organization },
4564
+ });
4565
+ const context = await resolveActiveProfileWithSource();
4566
+ const credentials = context.resolution.credentials;
4567
+ const canPersistSelection = Boolean(context.resolution.exists &&
4568
+ credentials &&
4569
+ (credentials.authKind === "user_session" || credentials.token.startsWith("oxy_user_")) &&
4570
+ data.currentOrganization);
4571
+ if (canPersistSelection && data.currentOrganization) {
4572
+ await updateActiveOrganizationForProfile(context.resolution.name, storedOrganizationFromOption(data.currentOrganization));
4573
+ }
4574
+ const result = {
4575
+ ...data,
4576
+ profile: context.resolution.exists ? context.resolution.name : null,
4577
+ selection_persisted: canPersistSelection,
4578
+ };
4579
+ if (options.json) {
4580
+ writeJson(success(command, result));
4581
+ return;
4582
+ }
4583
+ writeJson(result);
4584
+ }
4585
+ catch (error) {
4586
+ const failure = toFailure(command, error);
4587
+ writeJson(failure);
4588
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
4589
+ }
4590
+ }
3682
4591
  async function handleProfilesListAction(options) {
3683
4592
  try {
3684
4593
  const state = await listCredentialProfiles();
@@ -3873,9 +4782,8 @@ async function login(options) {
3873
4782
  browser: options.browser !== false,
3874
4783
  json: Boolean(options.json),
3875
4784
  });
3876
- const credentials = { token, apiUrl };
3877
- const identity = await requestOxygen("/api/cli/whoami", { credentials });
3878
- credentials.identity = identityFromWhoami(identity);
4785
+ const loginIdentity = await resolveLoginIdentity(token, apiUrl);
4786
+ const credentials = loginIdentity.credentials;
3879
4787
  const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
3880
4788
  let chosenProfile;
3881
4789
  let renamed = false;
@@ -3883,15 +4791,14 @@ async function login(options) {
3883
4791
  chosenProfile = explicitProfile;
3884
4792
  }
3885
4793
  else {
3886
- const seed = identity.organization.slug?.trim() || identity.organization.id;
3887
- const picked = await pickProfileNameForIdentity(identity.organization.id, seed);
4794
+ const picked = await pickProfileForLogin(loginIdentity);
3888
4795
  chosenProfile = picked.name;
3889
4796
  renamed = picked.renamed;
3890
4797
  }
3891
4798
  const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
3892
4799
  const skillsInstall = runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl });
3893
4800
  if (!options.json) {
3894
- process.stdout.write(formatLoginSuccess(identity, credentials, profile, { renamed, skillsInstall }));
4801
+ process.stdout.write(formatLoginSuccessForResolved(loginIdentity, profile, { renamed, skillsInstall }));
3895
4802
  const hint = await buildPostLoginHint(profile);
3896
4803
  if (hint)
3897
4804
  process.stdout.write(hint);
@@ -3900,12 +4807,14 @@ async function login(options) {
3900
4807
  logged_in: true,
3901
4808
  profile,
3902
4809
  api_url: credentials.apiUrl,
3903
- user: identity.user,
3904
- organization: identity.organization,
4810
+ user: loginIdentity.user,
4811
+ organization: loginIdentity.activeOrganization,
4812
+ organizations: loginIdentity.organizations,
4813
+ selection_required: loginIdentity.selectionRequired,
3905
4814
  skills_install: skillsInstall,
3906
4815
  };
3907
4816
  }
3908
- async function useAuthToken(options) {
4817
+ async function applyAuthToken(options) {
3909
4818
  const token = readOption(options.token);
3910
4819
  if (!token) {
3911
4820
  throw new OxygenError("missing_token", "Pass a CLI API token with --token.", {
@@ -3916,8 +4825,7 @@ async function useAuthToken(options) {
3916
4825
  token,
3917
4826
  apiUrl: normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl()),
3918
4827
  };
3919
- const identity = await requestOxygen("/api/cli/whoami", { credentials });
3920
- credentials.identity = identityFromWhoami(identity);
4828
+ const loginIdentity = await resolveLoginIdentity(token, credentials.apiUrl);
3921
4829
  const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
3922
4830
  let chosenProfile;
3923
4831
  let renamed = false;
@@ -3925,31 +4833,83 @@ async function useAuthToken(options) {
3925
4833
  chosenProfile = explicitProfile;
3926
4834
  }
3927
4835
  else {
3928
- const seed = identity.organization.slug?.trim() || identity.organization.id;
3929
- const picked = await pickProfileNameForIdentity(identity.organization.id, seed);
4836
+ const picked = await pickProfileForLogin(loginIdentity);
3930
4837
  chosenProfile = picked.name;
3931
4838
  renamed = picked.renamed;
3932
4839
  }
3933
- const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
4840
+ const profile = await saveCredentials(loginIdentity.credentials, process.env, { profile: chosenProfile });
3934
4841
  return {
3935
- identity,
3936
- credentials,
3937
- api_url: credentials.apiUrl,
4842
+ identity: loginIdentity.whoami,
4843
+ loginIdentity,
4844
+ credentials: loginIdentity.credentials,
4845
+ api_url: loginIdentity.credentials.apiUrl,
3938
4846
  profile,
3939
4847
  renamed,
3940
4848
  };
3941
4849
  }
4850
+ async function resolveLoginIdentity(token, apiUrl) {
4851
+ if (isUserSessionToken(token)) {
4852
+ const credentials = {
4853
+ token,
4854
+ apiUrl,
4855
+ authKind: "user_session",
4856
+ };
4857
+ const orgs = await requestOxygen("/api/cli/orgs", { credentials });
4858
+ const activeOrganization = orgs.currentOrganization
4859
+ ? storedOrganizationFromOption(orgs.currentOrganization)
4860
+ : null;
4861
+ credentials.activeOrganization = activeOrganization;
4862
+ credentials.identity = identityFromUserAndOrganization(orgs.user, activeOrganization);
4863
+ return {
4864
+ credentials,
4865
+ whoami: null,
4866
+ user: orgs.user,
4867
+ activeOrganization,
4868
+ organizations: orgs.organizations,
4869
+ selectionRequired: Boolean(orgs.selectionRequired),
4870
+ };
4871
+ }
4872
+ const credentials = {
4873
+ token,
4874
+ apiUrl,
4875
+ };
4876
+ if (token.startsWith("oxy_live_"))
4877
+ credentials.authKind = "org_api_key";
4878
+ const identity = await requestOxygen("/api/cli/whoami", { credentials });
4879
+ const activeOrganization = storedOrganizationFromOption(identity.organization);
4880
+ credentials.authKind = identity.authType === "user_session" ? "user_session" : "org_api_key";
4881
+ credentials.activeOrganization = activeOrganization;
4882
+ credentials.identity = identityFromWhoami(identity);
4883
+ return {
4884
+ credentials,
4885
+ whoami: identity,
4886
+ user: identity.user,
4887
+ activeOrganization,
4888
+ organizations: [{ ...identity.organization, selected: true }],
4889
+ selectionRequired: false,
4890
+ };
4891
+ }
4892
+ function pickProfileForLogin(loginIdentity) {
4893
+ if (loginIdentity.credentials.authKind === "user_session") {
4894
+ const seed = loginIdentity.activeOrganization?.slug
4895
+ ?? loginIdentity.user.email
4896
+ ?? loginIdentity.user.id;
4897
+ return pickProfileNameForUserSession(loginIdentity.user.id, seed);
4898
+ }
4899
+ const organization = loginIdentity.activeOrganization;
4900
+ if (!organization) {
4901
+ return pickProfileNameForUserSession(loginIdentity.user.id, loginIdentity.user.email ?? loginIdentity.user.id);
4902
+ }
4903
+ const seed = organization.slug?.trim() || organization.id;
4904
+ return pickProfileNameForIdentity(organization.id, seed);
4905
+ }
3942
4906
  function readEnvProfileName() {
3943
4907
  const value = process.env.OXYGEN_PROFILE?.trim();
3944
4908
  return value ? value : null;
3945
4909
  }
3946
4910
  function identityFromWhoami(response) {
3947
4911
  return {
3948
- organization: {
3949
- id: response.organization.id,
3950
- slug: response.organization.slug ?? null,
3951
- name: response.organization.name,
3952
- },
4912
+ organization: storedOrganizationFromOption(response.organization),
3953
4913
  user: {
3954
4914
  id: response.user.id,
3955
4915
  email: response.user.email ?? null,
@@ -3957,6 +4917,26 @@ function identityFromWhoami(response) {
3957
4917
  capturedAt: new Date().toISOString(),
3958
4918
  };
3959
4919
  }
4920
+ function identityFromUserAndOrganization(user, organization) {
4921
+ return {
4922
+ organization,
4923
+ user: {
4924
+ id: user.id,
4925
+ email: user.email ?? null,
4926
+ },
4927
+ capturedAt: new Date().toISOString(),
4928
+ };
4929
+ }
4930
+ function storedOrganizationFromOption(organization) {
4931
+ return {
4932
+ id: organization.id,
4933
+ name: organization.name,
4934
+ slug: organization.slug ?? null,
4935
+ };
4936
+ }
4937
+ function isUserSessionToken(token) {
4938
+ return token.startsWith("oxy_user_");
4939
+ }
3960
4940
  async function buildPostLoginHint(activeProfile) {
3961
4941
  if (output.isTTY !== true || process.env.NO_COLOR) {
3962
4942
  const state = await listCredentialProfiles().catch(() => null);
@@ -3967,8 +4947,8 @@ async function buildPostLoginHint(activeProfile) {
3967
4947
  const state = await listCredentialProfiles().catch(() => null);
3968
4948
  if (!state || state.profiles.length < 2)
3969
4949
  return "";
3970
- const c = ansi(true);
3971
- return `\n ${c.dim("Pin this terminal:")} ${c.bold(`eval "$(oxygen profiles env ${activeProfile})"`)}\n\n`;
4950
+ const styles = ansi(true);
4951
+ return `\n ${styles.dim("Pin this terminal:")} ${styles.bold(`eval "$(oxygen profiles env ${activeProfile})"`)}\n\n`;
3972
4952
  }
3973
4953
  async function promptForToken(options) {
3974
4954
  const fallbackLoginUrl = createCliLoginUrl(options.apiUrl);
@@ -4055,7 +5035,7 @@ async function waitForBrowserToken(session) {
4055
5035
  }
4056
5036
  function createCliLoginUrl(apiUrl) {
4057
5037
  const url = new URL("/auth/sign-in", apiUrl);
4058
- url.searchParams.set("redirect_url", "/settings/api-keys");
5038
+ url.searchParams.set("redirect_url", "/settings/cli");
4059
5039
  url.searchParams.set("source", "oxygen_cli");
4060
5040
  return url.toString();
4061
5041
  }
@@ -4107,6 +5087,67 @@ function startOxygenSpinner(message) {
4107
5087
  },
4108
5088
  };
4109
5089
  }
5090
+ function formatLoginSuccessForResolved(loginIdentity, profile, options = {}) {
5091
+ if (loginIdentity.whoami) {
5092
+ return formatLoginSuccess(loginIdentity.whoami, loginIdentity.credentials, profile, options);
5093
+ }
5094
+ if (loginIdentity.activeOrganization) {
5095
+ return formatLoginSuccess({
5096
+ user: loginIdentity.user,
5097
+ organization: loginIdentity.activeOrganization,
5098
+ apiKey: {
5099
+ id: "user_session",
5100
+ name: "CLI user session",
5101
+ tokenPrefix: loginIdentity.credentials.token.slice(0, 17),
5102
+ tokenSuffix: loginIdentity.credentials.token.slice(-4),
5103
+ kind: "user_session",
5104
+ },
5105
+ authType: "user_session",
5106
+ }, loginIdentity.credentials, profile, options);
5107
+ }
5108
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
5109
+ const email = loginIdentity.user.email ?? loginIdentity.user.id;
5110
+ const fingerprint = createHash("sha256")
5111
+ .update(`oxygen-cli:${loginIdentity.credentials.token}`)
5112
+ .digest("hex");
5113
+ const profileLabel = options.renamed
5114
+ ? `${profile} ${styles.dim("(auto-renamed to avoid collision)")}`
5115
+ : profile;
5116
+ const rows = [
5117
+ ["Account", email],
5118
+ ["Organization", "not selected"],
5119
+ ["Profile", profileLabel],
5120
+ ["API", loginIdentity.credentials.apiUrl],
5121
+ ["Token", `${loginIdentity.credentials.token.slice(0, 17)}...${loginIdentity.credentials.token.slice(-4)}`],
5122
+ ["Fingerprint", formatFingerprint(fingerprint)],
5123
+ ];
5124
+ const labelWidth = Math.max(...rows.map(([label]) => label.length));
5125
+ const wordmark = renderBox([
5126
+ "",
5127
+ ...OXYGEN_WORDMARK.split("\n").map(line => styles.green(line)),
5128
+ "",
5129
+ ]);
5130
+ const orgLines = loginIdentity.organizations.length > 0
5131
+ ? [
5132
+ "",
5133
+ "Available organizations:",
5134
+ ...loginIdentity.organizations.map((organization) => ` - ${organization.name}${organization.slug ? ` (${organization.slug})` : ""}`),
5135
+ "",
5136
+ `Select one: ${styles.bold("oxygen orgs use <organization>")}`,
5137
+ ]
5138
+ : [];
5139
+ return [
5140
+ "",
5141
+ wordmark,
5142
+ "",
5143
+ `${styles.green("[OK]")} ${styles.bold("CLI connected")}`,
5144
+ "",
5145
+ ...rows.map(([label, value]) => ` ${styles.dim(label.padEnd(labelWidth))} ${value}`),
5146
+ ...orgLines,
5147
+ "",
5148
+ ...formatAutomaticSkillsInstallStatusLines(options.skillsInstall),
5149
+ ].join("\n");
5150
+ }
4110
5151
  function formatLoginSuccess(identity, credentials, profile, options = {}) {
4111
5152
  const email = identity.user.email ?? identity.user.id;
4112
5153
  const orgName = identity.organization.name || identity.organization.slug || identity.organization.id;
@@ -4118,9 +5159,9 @@ function formatLoginSuccess(identity, credentials, profile, options = {}) {
4118
5159
  const fingerprint = createHash("sha256")
4119
5160
  .update(`oxygen-cli:${credentials.token}`)
4120
5161
  .digest("hex");
4121
- const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
5162
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
4122
5163
  const profileLabel = options.renamed
4123
- ? `${profile} ${c.dim("(auto-renamed to avoid collision)")}`
5164
+ ? `${profile} ${styles.dim("(auto-renamed to avoid collision)")}`
4124
5165
  : profile;
4125
5166
  // skipcq: JS-0820 — not a React component; rule misfire on array of tuples
4126
5167
  const rows = [
@@ -4134,16 +5175,16 @@ function formatLoginSuccess(identity, credentials, profile, options = {}) {
4134
5175
  const labelWidth = Math.max(...rows.map(([label]) => label.length));
4135
5176
  const wordmark = renderBox([
4136
5177
  "",
4137
- ...OXYGEN_WORDMARK.split("\n").map(line => c.green(line)),
5178
+ ...OXYGEN_WORDMARK.split("\n").map(line => styles.green(line)),
4138
5179
  "",
4139
5180
  ]);
4140
5181
  return [
4141
5182
  "",
4142
5183
  wordmark,
4143
5184
  "",
4144
- `${c.green("[OK]")} ${c.bold("CLI connected")}`,
5185
+ `${styles.green("[OK]")} ${styles.bold("CLI connected")}`,
4145
5186
  "",
4146
- ...rows.map(([label, value]) => ` ${c.dim(label.padEnd(labelWidth))} ${value}`),
5187
+ ...rows.map(([label, value]) => ` ${styles.dim(label.padEnd(labelWidth))} ${value}`),
4147
5188
  "",
4148
5189
  ...formatAutomaticSkillsInstallStatusLines(options.skillsInstall),
4149
5190
  ].join("\n");
@@ -4157,7 +5198,7 @@ function summarizeCredentialProfile(profile, active) {
4157
5198
  active,
4158
5199
  api_url: profile.apiUrl,
4159
5200
  token_fingerprint: formatFingerprint(createCredentialFingerprint(profile.token)),
4160
- organization: profile.identity?.organization ?? null,
5201
+ organization: profile.activeOrganization ?? profile.identity?.organization ?? null,
4161
5202
  user: profile.identity?.user ?? null,
4162
5203
  identity_captured_at: profile.identity?.capturedAt ?? null,
4163
5204
  };
@@ -4168,11 +5209,11 @@ function createCredentialFingerprint(token) {
4168
5209
  .digest("hex");
4169
5210
  }
4170
5211
  function formatProfilesList(data) {
4171
- const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
5212
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
4172
5213
  if (data.profiles.length === 0) {
4173
5214
  return [
4174
5215
  "",
4175
- `${c.dim("No stored Oxygen CLI profiles.")}`,
5216
+ `${styles.dim("No stored Oxygen CLI profiles.")}`,
4176
5217
  "",
4177
5218
  ].join("\n");
4178
5219
  }
@@ -4180,7 +5221,7 @@ function formatProfilesList(data) {
4180
5221
  const orgWidth = Math.max(...data.profiles.map((profile) => formatProfileOrgCell(profile).length));
4181
5222
  const lines = [
4182
5223
  "",
4183
- `${c.bold("Oxygen CLI Profiles")}`,
5224
+ `${styles.bold("Oxygen CLI Profiles")}`,
4184
5225
  "",
4185
5226
  ...data.profiles.map((profile) => {
4186
5227
  const marker = profile.active ? "*" : " ";
@@ -4188,14 +5229,14 @@ function formatProfilesList(data) {
4188
5229
  return [
4189
5230
  `${marker} ${profile.name.padEnd(nameWidth)}`,
4190
5231
  orgCell.padEnd(orgWidth),
4191
- c.dim(profile.api_url),
4192
- c.dim(profile.token_fingerprint),
5232
+ styles.dim(profile.api_url),
5233
+ styles.dim(profile.token_fingerprint),
4193
5234
  ].join(" ");
4194
5235
  }),
4195
5236
  "",
4196
5237
  ];
4197
5238
  if (data.profiles.length >= 2) {
4198
- lines.push(` ${c.dim("Pin a terminal:")} ${c.bold(`eval "$(oxygen profiles env <profile>)"`)}`);
5239
+ lines.push(` ${styles.dim("Pin a terminal:")} ${styles.bold('eval "$(oxygen profiles env <profile>)"')}`);
4199
5240
  lines.push("");
4200
5241
  }
4201
5242
  return lines.join("\n");
@@ -4209,26 +5250,26 @@ function formatProfileOrgCell(profile) {
4209
5250
  return name || slug || profile.organization.id;
4210
5251
  }
4211
5252
  function formatProfileUseSuccess(profile, options) {
4212
- const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
5253
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
4213
5254
  const lines = [
4214
5255
  "",
4215
- `${c.green("[OK]")} ${c.bold("CLI profile selected")}`,
5256
+ `${styles.green("[OK]")} ${styles.bold("CLI profile selected")}`,
4216
5257
  "",
4217
- ` ${c.dim("Profile")} ${profile.name}`,
4218
- ` ${c.dim("Organization")} ${formatProfileOrgCell(profile)}`,
4219
- ` ${c.dim("API")} ${profile.api_url}`,
4220
- ` ${c.dim("Fingerprint")} ${profile.token_fingerprint}`,
5258
+ ` ${styles.dim("Profile")} ${profile.name}`,
5259
+ ` ${styles.dim("Organization")} ${formatProfileOrgCell(profile)}`,
5260
+ ` ${styles.dim("API")} ${profile.api_url}`,
5261
+ ` ${styles.dim("Fingerprint")} ${profile.token_fingerprint}`,
4221
5262
  "",
4222
5263
  ];
4223
5264
  if (options.totalProfiles >= 2) {
4224
- lines.push(` ${c.dim("Note: this changes the active profile for every shell.")}`);
4225
- lines.push(` ${c.dim("To pin this terminal only, run:")} ${c.bold(`eval "$(oxygen profiles env ${profile.name})"`)}`);
5265
+ lines.push(` ${styles.dim("Note: this changes the active profile for every shell.")}`);
5266
+ lines.push(` ${styles.dim("To pin this terminal only, run:")} ${styles.bold(`eval "$(oxygen profiles env ${profile.name})"`)}`);
4226
5267
  lines.push("");
4227
5268
  }
4228
5269
  return lines.join("\n");
4229
5270
  }
4230
5271
  function formatWhoami(identity, context) {
4231
- const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
5272
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
4232
5273
  const email = identity.user.email ?? identity.user.id;
4233
5274
  const orgName = identity.organization.name || identity.organization.slug || identity.organization.id;
4234
5275
  const orgSlug = identity.organization.slug ?? null;
@@ -4239,42 +5280,56 @@ function formatWhoami(identity, context) {
4239
5280
  const rows = [
4240
5281
  ["Account", email],
4241
5282
  ["Organization", org],
4242
- ["Profile", `${profileName} ${c.dim(`(${sourceLabel})`)}`],
5283
+ ["Profile", `${profileName} ${styles.dim(`(${sourceLabel})`)}`],
4243
5284
  ["API", apiUrl],
4244
5285
  ];
4245
5286
  const labelWidth = Math.max(...rows.map(([label]) => label.length));
4246
- return [
5287
+ const lines = [
4247
5288
  "",
4248
- ...rows.map(([label, value]) => ` ${c.dim(label.padEnd(labelWidth))} ${value}`),
5289
+ ...rows.map(([label, value]) => ` ${styles.dim(label.padEnd(labelWidth))} ${value}`),
4249
5290
  "",
4250
- ].join("\n");
5291
+ ];
5292
+ if (identity.onboarding && identity.onboarding.complete === false) {
5293
+ const missing = identity.onboarding.missing_sections.join(", ");
5294
+ lines.push(` ${styles.dim("Onboarding")} ${styles.bold("workspace context not loaded")} ${styles.dim(`(missing: ${missing})`)}`, ` ${styles.dim("→ Run")} ${styles.bold("oxygen onboarding start")} ${styles.dim("(or oxygen_onboarding_start in MCP)")}`, "");
5295
+ }
5296
+ return lines.join("\n");
4251
5297
  } // skipcq: JS-C1002
4252
5298
  function formatProfilesCurrent(context) {
4253
- const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
5299
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
4254
5300
  const sourceLabel = describeProfileSource(context.source);
4255
5301
  if (!context.resolution.exists) {
4256
5302
  return [
4257
5303
  "",
4258
- `${c.dim("No stored profile resolves for this shell.")}`,
4259
- ` ${c.dim("Source:")} ${sourceLabel}`,
4260
- ` ${c.dim("Run")} ${c.bold("oxygen login")} ${c.dim("to create one.")}`,
5304
+ `${styles.dim("No stored profile resolves for this shell.")}`,
5305
+ ` ${styles.dim("Source:")} ${sourceLabel}`,
5306
+ ` ${styles.dim("Run")} ${styles.bold("oxygen login")} ${styles.dim("to create one.")}`,
4261
5307
  "",
4262
5308
  ].join("\n");
4263
5309
  }
4264
5310
  const credentials = context.resolution.credentials;
4265
- const orgCell = credentials.identity
4266
- ? (credentials.identity.organization.slug && credentials.identity.organization.slug !== credentials.identity.organization.name
4267
- ? `${credentials.identity.organization.name} (${credentials.identity.organization.slug})`
4268
- : credentials.identity.organization.name)
5311
+ if (!credentials) {
5312
+ return [
5313
+ "",
5314
+ `${styles.dim("No stored profile resolves for this shell.")}`,
5315
+ ` ${styles.dim("Source:")} ${sourceLabel}`,
5316
+ "",
5317
+ ].join("\n");
5318
+ }
5319
+ const organization = credentials.activeOrganization ?? credentials.identity?.organization ?? null;
5320
+ const orgCell = organization
5321
+ ? (organization.slug && organization.slug !== organization.name
5322
+ ? `${organization.name} (${organization.slug})`
5323
+ : organization.name)
4269
5324
  : "(unknown — run `oxygen whoami` to refresh)";
4270
5325
  return [
4271
5326
  "",
4272
- `${c.bold("Active Oxygen CLI Profile")}`,
5327
+ `${styles.bold("Active Oxygen CLI Profile")}`,
4273
5328
  "",
4274
- ` ${c.dim("Profile")} ${context.resolution.name}`,
4275
- ` ${c.dim("Source")} ${sourceLabel}`,
4276
- ` ${c.dim("Organization")} ${orgCell}`,
4277
- ` ${c.dim("API")} ${credentials.apiUrl}`,
5329
+ ` ${styles.dim("Profile")} ${context.resolution.name}`,
5330
+ ` ${styles.dim("Source")} ${sourceLabel}`,
5331
+ ` ${styles.dim("Organization")} ${orgCell}`,
5332
+ ` ${styles.dim("API")} ${credentials.apiUrl}`,
4278
5333
  "",
4279
5334
  ].join("\n");
4280
5335
  }
@@ -4287,28 +5342,28 @@ function describeProfileSource(source) {
4287
5342
  }
4288
5343
  }
4289
5344
  function formatLogoutSuccess(result) {
4290
- const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
5345
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
4291
5346
  const removed = result.removedProfile
4292
5347
  ? `profile "${result.removedProfile}" removed`
4293
5348
  : "removed";
4294
5349
  return [
4295
5350
  "",
4296
- `${c.green("[OK]")} ${c.bold("CLI logged out")}`,
5351
+ `${styles.green("[OK]")} ${styles.bold("CLI logged out")}`,
4297
5352
  "",
4298
- ` ${c.dim("Credentials")} ${removed}`,
4299
- ` ${c.dim("Profiles left")} ${String(result.remainingProfiles)}`, // skipcq: JS-C1002
5353
+ ` ${styles.dim("Credentials")} ${removed}`,
5354
+ ` ${styles.dim("Profiles left")} ${String(result.remainingProfiles)}`, // skipcq: JS-C1002
4300
5355
  "",
4301
5356
  ].join("\n");
4302
5357
  }
4303
5358
  function formatUpdateSuccess(result) {
4304
- const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
5359
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
4305
5360
  return [
4306
5361
  "",
4307
- `${c.green("[OK]")} ${c.bold(result.dry_run ? "CLI update command ready" : "CLI update completed")}`,
5362
+ `${styles.green("[OK]")} ${styles.bold(result.dry_run ? "CLI update command ready" : "CLI update completed")}`,
4308
5363
  "",
4309
- ` ${c.dim("Current")} ${result.current_version}`,
4310
- ` ${c.dim("Package")} ${result.package}`,
4311
- ` ${c.dim("Command")} ${result.command}`,
5364
+ ` ${styles.dim("Current")} ${result.current_version}`,
5365
+ ` ${styles.dim("Package")} ${result.package}`,
5366
+ ` ${styles.dim("Command")} ${result.command}`,
4312
5367
  "",
4313
5368
  ...formatAutomaticSkillsInstallStatusLines(result.skills_install),
4314
5369
  ].join("\n");
@@ -4316,25 +5371,25 @@ function formatUpdateSuccess(result) {
4316
5371
  function formatAutomaticSkillsInstallStatusLines(result) {
4317
5372
  if (!result)
4318
5373
  return [];
4319
- const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
5374
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
4320
5375
  if (!result.attempted) {
4321
5376
  if (result.reason === "dry_run")
4322
5377
  return [];
4323
5378
  return [
4324
- `${c.dim("Skills")} skipped (${result.reason ?? "not attempted"})`,
5379
+ `${styles.dim("Skills")} skipped (${result.reason ?? "not attempted"})`,
4325
5380
  "",
4326
5381
  ];
4327
5382
  }
4328
5383
  if (result.ok) {
4329
5384
  return [
4330
- `${c.green("[OK]")} ${c.bold("Oxygen skills installed")}`,
5385
+ `${styles.green("[OK]")} ${styles.bold("Oxygen skills installed")}`,
4331
5386
  "",
4332
5387
  ];
4333
5388
  }
4334
5389
  return [
4335
- `${c.yellow("[WARN]")} Oxygen skills could not be installed; the CLI command still completed.`,
4336
- ` ${c.dim("Check")} ${"oxygen skills doctor --json"}`,
4337
- ` ${c.dim("Retry")} ${result.command}`,
5390
+ `${styles.yellow("[WARN]")} Oxygen skills could not be installed; the CLI command still completed.`,
5391
+ ` ${styles.dim("Check")} ${"oxygen skills doctor --json"}`,
5392
+ ` ${styles.dim("Retry")} ${result.command}`,
4338
5393
  "",
4339
5394
  ];
4340
5395
  }
@@ -4371,6 +5426,62 @@ function readCsvOption(value) {
4371
5426
  .map((entry) => entry.trim())
4372
5427
  .filter(Boolean);
4373
5428
  }
5429
+ function splitCsv(value) {
5430
+ return value
5431
+ .split(",")
5432
+ .map((entry) => entry.trim())
5433
+ .filter(Boolean);
5434
+ }
5435
+ function collectMultiple(value, previous) {
5436
+ return [...previous, value];
5437
+ }
5438
+ async function buildBlueprintRequestBody(// skipcq: JS-R1005
5439
+ slug, options) {
5440
+ const body = {};
5441
+ if (slug?.trim())
5442
+ body.slug = slug.trim();
5443
+ const filePath = readOption(options.file);
5444
+ if (filePath) {
5445
+ const fs = await import("node:fs/promises");
5446
+ const raw = await fs.readFile(filePath, "utf8");
5447
+ body.envelope = JSON.parse(raw);
5448
+ }
5449
+ const fromUrl = readOption(options.fromUrl);
5450
+ if (fromUrl) {
5451
+ const response = await fetch(fromUrl, { headers: { Accept: "application/json" } });
5452
+ if (!response.ok) {
5453
+ throw new Error(`Failed to fetch blueprint from URL (${response.status} ${response.statusText}).`);
5454
+ }
5455
+ const payload = await response.json();
5456
+ body.envelope = payload.blueprint
5457
+ ?? payload.envelope
5458
+ ?? payload;
5459
+ }
5460
+ const inputJson = readOption(options.inputJson);
5461
+ if (inputJson) {
5462
+ try {
5463
+ body.inputs = JSON.parse(inputJson);
5464
+ }
5465
+ catch {
5466
+ throw new Error("--input-json must be valid JSON.");
5467
+ }
5468
+ }
5469
+ const tableRefEntries = Array.isArray(options.tableRef) ? options.tableRef : [];
5470
+ if (tableRefEntries.length > 0) {
5471
+ const refs = {};
5472
+ for (const entry of tableRefEntries) {
5473
+ const [key, value] = entry.split("=");
5474
+ if (key && value)
5475
+ refs[key.trim()] = value.trim();
5476
+ }
5477
+ if (Object.keys(refs).length > 0)
5478
+ body.table_refs = refs;
5479
+ }
5480
+ if (!body.slug && !body.envelope) {
5481
+ throw new Error("Pass a blueprint slug, --file, or --from-url.");
5482
+ }
5483
+ return body;
5484
+ }
4374
5485
  function contextAssetsQuery(options) {
4375
5486
  const query = new URLSearchParams();
4376
5487
  if (readOption(options.type))