@oxygen-agent/cli 1.46.0 → 1.64.5

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,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { execFileSync, spawnSync } from "node:child_process";
2
+ // skipcq: JS-0271 bin entry source; build chmod+x on dist/index.js
3
+ import { execFileSync } from "node:child_process";
3
4
  import { createHash } from "node:crypto";
4
5
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
6
  import { tmpdir } from "node:os";
@@ -8,17 +9,21 @@ import { createInterface } from "node:readline/promises";
8
9
  import { stdin as input, stdout as output } from "node:process";
9
10
  import { fileURLToPath, pathToFileURL } from "node:url";
10
11
  import { Command } from "commander";
11
- import { OXYGEN_VERSION, OxygenError, success, toFailure } from "@oxygen/shared";
12
+ import { formatCellForDisplay, OXYGEN_VERSION, OxygenError, success, toFailure } from "@oxygen/shared";
12
13
  import { inferRowsFileFormat, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
13
14
  import { assertRecipeBundleSafe, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowManifest, } from "@oxygen/workflows";
14
15
  import { isRecipeDefinition } from "@oxygen/recipe-sdk";
15
16
  import { createBrowserLoginSession, openBrowser } from "./browser-login.js";
16
- import { clearCredentials, defaultApiUrl, listCredentialProfiles, normalizeApiUrl, saveCredentials, switchCredentialProfile, } from "./credentials.js";
17
+ import { clearCredentials, defaultApiUrl, listCredentialProfiles, normalizeApiUrl, pickProfileNameForIdentity, resolveActiveProfile, saveCredentials, switchCredentialProfile, } from "./credentials.js";
17
18
  import { requestOxygen } from "./http-client.js";
18
19
  import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
19
20
  import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
21
+ import { doctorAgentSkills, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, } from "./skills.js";
22
+ import { updateCli } from "./update.js";
20
23
  const BROWSER_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
21
24
  const OXYGEN_SPINNER_INTERVAL_MS = 90;
25
+ const INITIAL_OXYGEN_PROFILE_ENV = process.env.OXYGEN_PROFILE?.trim() || null;
26
+ let globalProfileFlag = null;
22
27
  const OXYGEN_SPINNER_FRAMES = [
23
28
  "[Oxygen ]",
24
29
  "[ Oxygen ]",
@@ -43,7 +48,6 @@ const TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS = 600;
43
48
  const TABLE_INGESTION_WAIT_DEFAULT_INTERVAL_SECONDS = 5;
44
49
  const WORKFLOW_TAIL_DEFAULT_TIMEOUT_SECONDS = 600;
45
50
  const WORKFLOW_TAIL_DEFAULT_INTERVAL_SECONDS = 2;
46
- const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
47
51
  const CLI_MODULE_DIR = dirname(fileURLToPath(import.meta.url));
48
52
  const RECIPE_ESBUILD_NODE_PATHS = [
49
53
  resolve("node_modules"),
@@ -160,8 +164,10 @@ export function createProgram() {
160
164
  .option("--profile <name>", "Use a stored CLI profile for this command.");
161
165
  program.hook("preAction", () => {
162
166
  const options = program.opts();
163
- if (options.profile)
167
+ if (options.profile) {
168
+ globalProfileFlag = options.profile;
164
169
  process.env.OXYGEN_PROFILE = options.profile;
170
+ }
165
171
  });
166
172
  program
167
173
  .command("login")
@@ -208,6 +214,20 @@ export function createProgram() {
208
214
  .option("--json", "Print a JSON envelope.")
209
215
  .action(async (profile, options) => {
210
216
  await handleLogoutAction({ ...options, profile });
217
+ }))
218
+ .addCommand(new Command("env")
219
+ .description("Print shell `export` lines that pin this terminal to a stored CLI profile.")
220
+ .argument("<profile>", "Stored profile name.")
221
+ .option("--unset", "Print `unset` lines instead of `export` lines.")
222
+ .option("--json", "Print a JSON envelope.")
223
+ .action(async (profile, options) => {
224
+ await handleProfilesEnvAction(profile, options);
225
+ }))
226
+ .addCommand(new Command("current")
227
+ .description("Show the profile the next command will use, and where it came from.")
228
+ .option("--json", "Print a JSON envelope.")
229
+ .action(async (options) => {
230
+ await handleProfilesCurrentAction(options);
211
231
  }));
212
232
  program
213
233
  .command("logout")
@@ -262,7 +282,7 @@ export function createProgram() {
262
282
  .description("Show the current Oxygen CLI identity.")
263
283
  .option("--json", "Print a JSON envelope.")
264
284
  .action(async (options) => {
265
- await handleAsyncAction("whoami", options, async () => requestOxygen("/api/cli/whoami"));
285
+ await handleWhoamiAction(options);
266
286
  });
267
287
  program
268
288
  .command("status")
@@ -518,14 +538,33 @@ export function createProgram() {
518
538
  await handleAsyncAction("tables import", options, async () => importRows(table, options));
519
539
  }))
520
540
  .addCommand(new Command("export")
521
- .description("Export workspace table rows as JSON, JSONL, or CSV.")
541
+ .description("Export workspace table rows as JSON, JSONL, CSV, or a human-readable table.")
522
542
  .argument("<table>", "Table id or slug.")
523
- .option("--format <format>", "json, jsonl, or csv. Defaults to json.")
543
+ .option("--format <format>", "json, jsonl, csv, or table. Defaults to json. Use table for a typed, human-readable rendering with thousands grouping.")
524
544
  .option("--output <path>", "Write export content to a file.")
525
545
  .option("--limit <n>", "Maximum rows to export. Defaults to 100; hard cap is 1000.")
526
546
  .option("--json", "Print a JSON envelope.")
527
547
  .action(async (table, options) => {
528
548
  await handleAsyncAction("tables export", options, async () => exportRows(table, options));
549
+ }))
550
+ .addCommand(new Command("export-bundle")
551
+ .description("Export a workspace table as a portable bundle (schema + every row) for cross-environment / cross-org copies.")
552
+ .argument("<table>", "Table id or slug to export.")
553
+ .option("--output <path>", "Write the bundle JSON to a file. Defaults to stdout.")
554
+ .option("--page-size <n>", "Rows per cursor-paginated request. Defaults to 500; hard cap is 1000.")
555
+ .option("--json", "Print a JSON envelope (omit row payload — use --output to keep the rows).")
556
+ .action(async (table, options) => {
557
+ await handleAsyncAction("tables export-bundle", options, async () => exportTableBundle(table, options));
558
+ }))
559
+ .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.")
561
+ .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.")
565
+ .option("--json", "Print a JSON envelope.")
566
+ .action(async (options) => {
567
+ await handleAsyncAction("tables import-bundle", options, async () => importTableBundle(options));
529
568
  }))
530
569
  .addCommand(new Command("list")
531
570
  .description("List workspace tables in the current tenant database.")
@@ -617,6 +656,34 @@ export function createProgram() {
617
656
  method: "POST",
618
657
  body: { table },
619
658
  }));
659
+ }))
660
+ .addCommand(new Command("move")
661
+ .description("Move a workspace table to a different project.")
662
+ .argument("<table>", "Table id or slug.")
663
+ .requiredOption("--project <project>", "Destination project id or slug.")
664
+ .option("--json", "Print a JSON envelope.")
665
+ .action(async (table, options) => {
666
+ await handleAsyncAction("tables move", options, async () => requestOxygen("/api/cli/tables/move", {
667
+ method: "POST",
668
+ body: { table, project: options.project },
669
+ }));
670
+ }))
671
+ .addCommand(new Command("recover-pending")
672
+ .description("Re-poll BetterContact async enrichments that timed out mid-poll and write any newly-found phones back to the table.")
673
+ .argument("<table>", "Table id or slug.")
674
+ .option("--max-items <n>", "Max pending cache rows to poll in this run. Defaults to 1000.")
675
+ .option("--json", "Print a JSON envelope.")
676
+ .action(async (table, options) => {
677
+ await handleAsyncAction("tables recover-pending", options, async () => {
678
+ const maxItems = options.maxItems ? Number.parseInt(options.maxItems, 10) : undefined;
679
+ return requestOxygen("/api/cli/tables/recover-pending", {
680
+ method: "POST",
681
+ body: {
682
+ table,
683
+ ...(Number.isFinite(maxItems) ? { max_items: maxItems } : {}),
684
+ },
685
+ });
686
+ });
620
687
  }));
621
688
  tablesCommand.addCommand(new Command("webhook")
622
689
  .description("Create and manage direct table webhooks.")
@@ -649,6 +716,22 @@ export function createProgram() {
649
716
  program
650
717
  .command("context")
651
718
  .description("Workspace-level GTM context commands.")
719
+ .addCommand(new Command("resolve")
720
+ .description("Resolve task-scoped workspace GTM context with readiness and revision provenance.")
721
+ .option("--purpose <purpose>", "general, lead_sourcing, qualification, outbound_copy, or workflow_design.")
722
+ .option("--asset-type <csv>", "Comma-separated context asset types to include.")
723
+ .option("--asset-status <status>", "draft, active, archived, or all. Defaults to active.")
724
+ .option("--tags <csv>", "Comma-separated asset tags that must be present.")
725
+ .option("--include-archived", "Include archived assets when no asset status is set.")
726
+ .option("--max-assets <n>", "Maximum assets to include. Defaults to 10, max 50.")
727
+ .option("--require-ready", "Exit with a conflict error when required context sections are missing.")
728
+ .option("--json", "Print a JSON envelope.")
729
+ .action(async (options) => {
730
+ await handleAsyncAction("context resolve", options, async () => requestOxygen("/api/cli/context/resolve", {
731
+ method: "POST",
732
+ body: buildContextResolveBody(options),
733
+ }));
734
+ }))
652
735
  .addCommand(new Command("profile")
653
736
  .description("Company profile and ICP memory.")
654
737
  .addCommand(new Command("get")
@@ -721,21 +804,164 @@ export function createProgram() {
721
804
  body: { id: assetId },
722
805
  }));
723
806
  })));
807
+ program
808
+ .command("templates")
809
+ .description("Reusable prompt templates layered into AI columns at run time.")
810
+ .addCommand(new Command("list")
811
+ .description("List prompt templates in the workspace.")
812
+ .option("--kind <kind>", "Filter by ai_column_system, scoring_rubric, or other.")
813
+ .option("--include-archived", "Include archived templates.")
814
+ .option("--json", "Print a JSON envelope.")
815
+ .action(async (options) => {
816
+ await handleAsyncAction("templates list", options, async () => {
817
+ const params = new URLSearchParams();
818
+ if (readOption(options.kind))
819
+ params.set("kind", readOption(options.kind));
820
+ if (options.includeArchived)
821
+ params.set("include_archived", "true");
822
+ const qs = params.toString() ? `?${params.toString()}` : "";
823
+ return requestOxygen(`/api/cli/templates${qs}`);
824
+ });
825
+ }))
826
+ .addCommand(new Command("get")
827
+ .description("Read one prompt template by id or slug.")
828
+ .argument("<id_or_slug>", "Template UUID or slug.")
829
+ .option("--json", "Print a JSON envelope.")
830
+ .action(async (idOrSlug, options) => {
831
+ await handleAsyncAction("templates get", options, async () => requestOxygen("/api/cli/templates/get", {
832
+ method: "POST",
833
+ body: idOrSlug.includes("-") && idOrSlug.length >= 32
834
+ ? { id: idOrSlug }
835
+ : { slug: idOrSlug },
836
+ }));
837
+ }))
838
+ .addCommand(new Command("upsert")
839
+ .description("Create or update a prompt template.")
840
+ .option("--id <id>", "Existing template UUID to update. Omit to create.")
841
+ .option("--slug <slug>", "Stable slug (kebab-case).")
842
+ .option("--name <name>", "Human-readable name.")
843
+ .option("--description <text>", "Short description.")
844
+ .option("--kind <kind>", "Template kind: ai_column_system, scoring_rubric, or other.")
845
+ .option("--body <text>", "Prompt body.")
846
+ .option("--body-file <path>", "Path to a file containing the prompt body.")
847
+ .option("--json", "Print a JSON envelope.")
848
+ .action(async (options) => {
849
+ await handleAsyncAction("templates upsert", options, async () => {
850
+ const body = {};
851
+ if (readOption(options.id))
852
+ body.id = readOption(options.id);
853
+ if (readOption(options.slug))
854
+ body.slug = readOption(options.slug);
855
+ if (readOption(options.name))
856
+ body.name = readOption(options.name);
857
+ if (readOption(options.description) !== undefined)
858
+ body.description = readOption(options.description);
859
+ if (readOption(options.kind))
860
+ body.kind = readOption(options.kind);
861
+ if (readOption(options.body))
862
+ body.body = readOption(options.body);
863
+ else if (readOption(options.bodyFile)) {
864
+ const path = readOption(options.bodyFile);
865
+ const fs = await import("node:fs/promises");
866
+ body.body = await fs.readFile(path, "utf8");
867
+ }
868
+ return requestOxygen("/api/cli/templates/upsert", { method: "POST", body });
869
+ });
870
+ }))
871
+ .addCommand(new Command("archive")
872
+ .description("Archive a prompt template. Seeded templates cannot be archived.")
873
+ .argument("<id>", "Template UUID.")
874
+ .option("--json", "Print a JSON envelope.")
875
+ .action(async (id, options) => {
876
+ await handleAsyncAction("templates archive", options, async () => requestOxygen("/api/cli/templates/archive", { method: "POST", body: { id } }));
877
+ }));
878
+ program
879
+ .command("reviews")
880
+ .description("Human-in-the-loop reviews for AI-generated outreach messages.")
881
+ .addCommand(new Command("list")
882
+ .description("List message reviews.")
883
+ .option("--status <status>", "Filter by pending, accepted, rejected, or superseded.")
884
+ .option("--table <table_id>", "Filter by table id.")
885
+ .option("--limit <n>", "Max rows to return (default 50, max 200).")
886
+ .option("--json", "Print a JSON envelope.")
887
+ .action(async (options) => {
888
+ await handleAsyncAction("reviews list", options, async () => {
889
+ 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));
896
+ const qs = params.toString() ? `?${params.toString()}` : "";
897
+ return requestOxygen(`/api/cli/message-reviews${qs}`);
898
+ });
899
+ }))
900
+ .addCommand(new Command("next")
901
+ .description("Read the oldest pending review.")
902
+ .option("--table <table_id>", "Filter by table id.")
903
+ .option("--json", "Print a JSON envelope.")
904
+ .action(async (options) => {
905
+ await handleAsyncAction("reviews next", options, async () => {
906
+ const params = new URLSearchParams();
907
+ if (readOption(options.table))
908
+ params.set("table_id", readOption(options.table));
909
+ const qs = params.toString() ? `?${params.toString()}` : "";
910
+ return requestOxygen(`/api/cli/message-reviews/next${qs}`);
911
+ });
912
+ }))
913
+ .addCommand(new Command("accept")
914
+ .description("Accept a pending message review.")
915
+ .argument("<review_id>", "Message review UUID.")
916
+ .option("--json", "Print a JSON envelope.")
917
+ .action(async (reviewId, options) => {
918
+ await handleAsyncAction("reviews accept", options, async () => requestOxygen("/api/cli/message-reviews/decide", {
919
+ method: "POST",
920
+ body: { id: reviewId, decision: "accept" },
921
+ }));
922
+ }))
923
+ .addCommand(new Command("reject")
924
+ .description("Reject a pending message review with optional highlights and auto-rerun.")
925
+ .argument("<review_id>", "Message review UUID.")
926
+ .option("--highlights-json <json>", "JSON array of {start,end,comment} highlight objects.")
927
+ .option("--auto-rerun", "Trigger a single-row rerun of the column after rejecting.")
928
+ .option("--json", "Print a JSON envelope.")
929
+ .action(async (reviewId, options) => {
930
+ await handleAsyncAction("reviews reject", options, async () => {
931
+ const body = { id: reviewId, decision: "reject" };
932
+ if (readOption(options.highlightsJson)) {
933
+ body.highlights = JSON.parse(readOption(options.highlightsJson));
934
+ }
935
+ if (options.autoRerun)
936
+ body.auto_rerun = true;
937
+ return requestOxygen("/api/cli/message-reviews/decide", { method: "POST", body });
938
+ });
939
+ }));
724
940
  program
725
941
  .command("columns")
726
942
  .description("Workspace table column commands.")
727
943
  .addCommand(new Command("add")
728
944
  .description("Add a nullable column to a workspace table.")
729
945
  .argument("<table>", "Table id or slug.")
730
- .requiredOption("--label <label>", "Display label for the new column.")
946
+ .option("--label <label>", "Display label for the new column. Required unless --prompt-key supplies a default title.")
731
947
  .option("--key <key>", "Optional stable column key. Defaults to a normalized label.")
732
948
  .option("--data-type <type>", "Column data type: text, numeric, boolean, jsonb, or timestamptz.")
733
949
  .option("--kind <kind>", "Column kind. Defaults to manual.")
734
950
  .option("--semantic-type <type>", "Optional semantic type such as company_domain.")
735
951
  .option("--definition-json <json>", "Optional JSON object with column definition metadata.")
952
+ .option("--prompt-key <key>", "OXYGEN prompt-library key (e.g. email_draft_v1). Materializes prompt + output_schema and forces kind=ai.")
953
+ .option("--input-mapping <json>", "Required with --prompt-key. JSON object mapping prompt input names to column or literal refs.")
736
954
  .option("--json", "Print a JSON envelope.")
737
955
  .action(async (table, options) => {
738
- const column = { label: options.label };
956
+ if (!options.promptKey && !options.label) {
957
+ throw new OxygenError("invalid_request", "--label is required.", { exitCode: 1 });
958
+ }
959
+ if (options.promptKey && !options.inputMapping) {
960
+ throw new OxygenError("missing_input_mapping", "--input-mapping is required when --prompt-key is provided.", { exitCode: 1 });
961
+ }
962
+ const column = {};
963
+ if (options.label)
964
+ column.label = options.label;
739
965
  if (options.key)
740
966
  column.key = options.key;
741
967
  if (options.dataType)
@@ -746,12 +972,14 @@ export function createProgram() {
746
972
  column.semantic_type = options.semanticType;
747
973
  if (options.definitionJson)
748
974
  column.definition = parseJsonObject(options.definitionJson);
975
+ const body = { table, column };
976
+ if (options.promptKey)
977
+ body.prompt_key = options.promptKey;
978
+ if (options.inputMapping)
979
+ body.input_mapping = parseJsonObject(options.inputMapping);
749
980
  await handleAsyncAction("columns add", options, async () => requestOxygen("/api/cli/tables/columns", {
750
981
  method: "POST",
751
- body: {
752
- table,
753
- column,
754
- },
982
+ body,
755
983
  }));
756
984
  }))
757
985
  .addCommand(new Command("run")
@@ -766,6 +994,7 @@ export function createProgram() {
766
994
  .option("--connection-id <connection_id>", "Optional provider integration connection id.")
767
995
  .option("--background", "Create a durable background table action run instead of executing synchronously.")
768
996
  .option("--max-credits <n>", "Maximum managed/provider credits to reserve for a background run.")
997
+ .option("--max-concurrency <n>", "Maximum concurrent row items for a background run. Defaults to 50.")
769
998
  .option("--local", "Run a custom HTTP column in this CLI process so env-var secrets stay local.")
770
999
  .option("--local-concurrency <n>", "Maximum concurrent custom HTTP requests for --local. Defaults to 3.")
771
1000
  .option("--json", "Print a JSON envelope.")
@@ -773,6 +1002,7 @@ export function createProgram() {
773
1002
  const limit = readPositiveInt(options.limit);
774
1003
  const localConcurrency = readPositiveInt(options.localConcurrency);
775
1004
  const maxCredits = readPositiveNumber(options.maxCredits);
1005
+ const maxConcurrency = readPositiveInt(options.maxConcurrency);
776
1006
  const filterSelection = readFilterSelectionOption(options.filterJson);
777
1007
  const selectedModes = [
778
1008
  Boolean(options.all),
@@ -822,9 +1052,28 @@ export function createProgram() {
822
1052
  ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
823
1053
  ...(options.background ? { background: true } : {}),
824
1054
  ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
1055
+ ...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
825
1056
  },
826
1057
  });
827
1058
  });
1059
+ }))
1060
+ .addCommand(new Command("rerun")
1061
+ .description("Re-run a single AI column cell, optionally threading a prior message review's feedback into the prompt.")
1062
+ .requiredOption("--table <table>", "Table id or slug.")
1063
+ .requiredOption("--column <column>", "Column id or key.")
1064
+ .requiredOption("--row <row_id>", "Row UUID.")
1065
+ .option("--from-review-id <review_id>", "Prior message review whose feedback to thread into the regeneration prompt.")
1066
+ .option("--json", "Print a JSON envelope.")
1067
+ .action(async (options) => {
1068
+ await handleAsyncAction("columns rerun", options, async () => requestOxygen("/api/cli/columns/rerun", {
1069
+ method: "POST",
1070
+ body: {
1071
+ table: options.table,
1072
+ column: options.column,
1073
+ row_id: options.row,
1074
+ ...(readOption(options.fromReviewId) ? { from_review_id: readOption(options.fromReviewId) } : {}),
1075
+ },
1076
+ }));
828
1077
  }))
829
1078
  .addCommand(new Command("materialize")
830
1079
  .description("Materialize useful fields from a JSONB result column into target columns.")
@@ -966,10 +1215,12 @@ export function createProgram() {
966
1215
  .option("--force", "Run even when the target cell already has a value.")
967
1216
  .option("--connection-id <connection_id>", "Optional provider integration connection id.")
968
1217
  .option("--max-credits <n>", "Maximum managed/provider credits to reserve for this run.")
1218
+ .option("--max-concurrency <n>", "Maximum concurrent row items for this run.")
969
1219
  .option("--metadata-json <json>", "Optional metadata object to attach to the run.")
970
1220
  .option("--json", "Print a JSON envelope.")
971
1221
  .action(async (table, options) => {
972
1222
  const maxCredits = readPositiveNumber(options.maxCredits);
1223
+ const maxConcurrency = readPositiveInt(options.maxConcurrency);
973
1224
  await handleAsyncAction("table-runs create", options, async () => requestOxygen("/api/cli/table-action-runs", {
974
1225
  method: "POST",
975
1226
  body: {
@@ -979,6 +1230,7 @@ export function createProgram() {
979
1230
  ...(options.force ? { force: true } : {}),
980
1231
  ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
981
1232
  ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
1233
+ ...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
982
1234
  ...(options.metadataJson ? { metadata: parseJsonObject(options.metadataJson) } : {}),
983
1235
  },
984
1236
  }));
@@ -1211,6 +1463,48 @@ export function createProgram() {
1211
1463
  },
1212
1464
  }));
1213
1465
  }));
1466
+ program
1467
+ .command("companies")
1468
+ .description("Company prospecting and account enrichment workflows.")
1469
+ .addCommand(new Command("enrich")
1470
+ .description("Preview or run a company enrichment waterfall over an existing table.")
1471
+ .addCommand(new Command("preview")
1472
+ .description("Inspect missing company fields, provider routing, and credit estimates without provider calls.")
1473
+ .argument("<table>", "Table id or slug.")
1474
+ .option("--missing-fields <fields>", "Comma-separated fields to fill: domain,linkedin_url,headcount,industry,funding,technologies,hiring_signals,company_profile.")
1475
+ .option("--providers <providers>", "Comma-separated provider order pool. Defaults to blitzapi,crustdata,ai_ark,prospeo,leadmagic.")
1476
+ .option("--all", "Preview all rows.")
1477
+ .option("--limit <n>", "Preview a limited row scope.")
1478
+ .option("--row-ids <ids>", "Comma-separated row ids.")
1479
+ .option("--filter-json <json>", "Filter object or array for row selection.")
1480
+ .option("--selection-json <json>", "Raw table action selection JSON.")
1481
+ .option("--json", "Print a JSON envelope.")
1482
+ .action(async (table, options) => {
1483
+ await handleAsyncAction("companies enrich preview", options, async () => requestOxygen("/api/cli/company-enrichment/preview", {
1484
+ method: "POST",
1485
+ body: readCompaniesEnrichBody(table, options),
1486
+ }));
1487
+ }))
1488
+ .addCommand(new Command("run")
1489
+ .description("Queue a live company enrichment waterfall, or return a dry-run plan.")
1490
+ .argument("<table>", "Table id or slug.")
1491
+ .option("--missing-fields <fields>", "Comma-separated fields to fill.")
1492
+ .option("--providers <providers>", "Comma-separated provider pool.")
1493
+ .option("--mode <mode>", "dry_run or live. Defaults to live.")
1494
+ .option("--max-credits <n>", "Required credit ceiling for live runs.")
1495
+ .option("--all", "Run on all rows.")
1496
+ .option("--limit <n>", "Run on a limited row scope.")
1497
+ .option("--row-ids <ids>", "Comma-separated row ids.")
1498
+ .option("--filter-json <json>", "Filter object or array for row selection.")
1499
+ .option("--selection-json <json>", "Raw table action selection JSON.")
1500
+ .option("--force", "Re-run the waterfall audit column even when it already has a value.")
1501
+ .option("--json", "Print a JSON envelope.")
1502
+ .action(async (table, options) => {
1503
+ await handleAsyncAction("companies enrich run", options, async () => requestOxygen("/api/cli/company-enrichment/run", {
1504
+ method: "POST",
1505
+ body: readCompaniesEnrichBody(table, options),
1506
+ }));
1507
+ })));
1214
1508
  program
1215
1509
  .command("worker")
1216
1510
  .description("Background worker commands.")
@@ -1222,7 +1516,7 @@ export function createProgram() {
1222
1516
  }))
1223
1517
  .addCommand(new Command("failures")
1224
1518
  .description("List failed background action and ingestion items.")
1225
- .option("--queue <queue>", "all, actions, or ingestions. Defaults to all.")
1519
+ .option("--queue <queue>", "all, actions, ingestions, or bullmq. Defaults to all.")
1226
1520
  .option("--limit <n>", "Maximum failed items per queue. Defaults to 25; server cap is 100.")
1227
1521
  .option("--json", "Print a JSON envelope.")
1228
1522
  .action(async (options) => {
@@ -1328,6 +1622,7 @@ export function createProgram() {
1328
1622
  .addCommand(new Command("costs")
1329
1623
  .description("Show provider costs (COGS) per workspace. Staff only.")
1330
1624
  .option("--top <n>", "Limit number of workspace columns. Defaults to all.")
1625
+ .option("--credential-mode <mode>", "managed (default; real COGS), byok (customer-supplied credentials), or all.", "managed")
1331
1626
  .option("--json", "Print a JSON envelope.")
1332
1627
  .action(async (options) => {
1333
1628
  await handleAsyncAction("admin costs", options, async () => {
@@ -1335,6 +1630,10 @@ export function createProgram() {
1335
1630
  const top = readPositiveInt(options.top);
1336
1631
  if (top)
1337
1632
  params.set("top", String(top));
1633
+ const credentialMode = readOption(options.credentialMode);
1634
+ if (credentialMode && credentialMode !== "managed") {
1635
+ params.set("credential_mode", credentialMode);
1636
+ }
1338
1637
  const suffix = params.toString() ? `?${params.toString()}` : "";
1339
1638
  return requestOxygen(`/api/cli/admin/costs${suffix}`);
1340
1639
  });
@@ -1416,6 +1715,27 @@ export function createProgram() {
1416
1715
  program
1417
1716
  .command("cells")
1418
1717
  .description("Workspace cell provenance commands.")
1718
+ .addCommand(new Command("inspect")
1719
+ .description("Inspect one cell's value, row context, and recent history.")
1720
+ .argument("<table>", "Table id or slug.")
1721
+ .argument("<row_id>", "Workspace row UUID.")
1722
+ .argument("<column>", "Column id or key.")
1723
+ .option("--history-limit <n>", "Maximum recent cell changes to include. Defaults to 10; cap 50.")
1724
+ .option("--json", "Print a JSON envelope.")
1725
+ .action(async (table, rowId, column, options) => {
1726
+ await handleAsyncAction("cells inspect", options, async () => {
1727
+ const historyLimit = readPositiveInt(options.historyLimit);
1728
+ return requestOxygen("/api/cli/tables/cells/inspect", {
1729
+ method: "POST",
1730
+ body: {
1731
+ table,
1732
+ row_id: rowId,
1733
+ column,
1734
+ ...(historyLimit ? { history_limit: historyLimit } : {}),
1735
+ },
1736
+ });
1737
+ });
1738
+ }))
1419
1739
  .addCommand(new Command("history")
1420
1740
  .description("Show cell change history.")
1421
1741
  .argument("<table>", "Table id or slug.")
@@ -1514,7 +1834,7 @@ export function createProgram() {
1514
1834
  .addCommand(new Command("search")
1515
1835
  .description("Search the tool catalog.")
1516
1836
  .argument("[query]", "Search text.")
1517
- .option("--verbosity <verbosity>", "summary or full. Defaults to summary.")
1837
+ .option("--verbosity <verbosity>", "minimal, summary, or full. Defaults to summary. Use minimal for high-fanout discovery sweeps that need to stay under the MCP token budget.")
1518
1838
  .option("--only-runnable", "Only return tools runnable by the active organization.")
1519
1839
  .option("--capability <tag>", "Filter by capability tag, such as mobile_phone.")
1520
1840
  .option("--json", "Print a JSON envelope.")
@@ -1626,13 +1946,16 @@ export function createProgram() {
1626
1946
  .option("--selection-json <json>", "Full row selection object for server-side row selection.")
1627
1947
  .option("--only-missing", "Queue rows missing the capability's normalized output when no explicit selection is passed.")
1628
1948
  .option("--force", "Re-run rows with an existing target enrichment value.")
1949
+ .option("--max-concurrency <n>", "Maximum concurrent row items for this enrichment run. Defaults to 20.")
1629
1950
  .option("--json", "Print a JSON envelope.")
1630
1951
  .action(async (table, options) => {
1952
+ const maxConcurrency = readPositiveInt(options.maxConcurrency);
1631
1953
  await handleAsyncAction("enrich-column run", options, async () => requestOxygen("/api/cli/enrich-column/run", {
1632
1954
  method: "POST",
1633
1955
  body: {
1634
1956
  ...buildEnrichColumnBody(table, options),
1635
1957
  max_credits: readPositiveNumber(options.maxCredits),
1958
+ ...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
1636
1959
  ...(options.force ? { force: true } : {}),
1637
1960
  },
1638
1961
  }));
@@ -1644,8 +1967,9 @@ export function createProgram() {
1644
1967
  .description("Configure provider events that can trigger workflows.")
1645
1968
  .addCommand(new Command("list")
1646
1969
  .description("List supported provider events and this org's enabled subscriptions.")
1647
- .option("--source <source>", "Filter by event source, such as hubspot.")
1970
+ .option("--source <source>", "Filter by event source, such as hubspot or composio.gmail.")
1648
1971
  .option("--event <event>", "Filter by event type, such as contact.created.")
1972
+ .option("--toolkit <id>", "Filter by toolkit / integration id, such as gmail.")
1649
1973
  .option("--json", "Print a JSON envelope.")
1650
1974
  .action(async (options) => {
1651
1975
  await handleAsyncAction("integrations events list", options, async () => {
@@ -1654,25 +1978,47 @@ export function createProgram() {
1654
1978
  query.set("source", readOption(options.source) ?? "");
1655
1979
  if (readOption(options.event))
1656
1980
  query.set("event", readOption(options.event) ?? "");
1981
+ if (readOption(options.toolkit))
1982
+ query.set("toolkit", readOption(options.toolkit) ?? "");
1657
1983
  const suffix = query.toString() ? `?${query.toString()}` : "";
1658
1984
  return requestOxygen(`/api/cli/integrations/events${suffix}`);
1659
1985
  });
1660
1986
  }))
1661
1987
  .addCommand(new Command("enable")
1662
1988
  .description("Enable a provider event for a connected integration account.")
1663
- .requiredOption("--source <source>", "Event source, such as hubspot.")
1989
+ .requiredOption("--source <source>", "Event source, such as hubspot or composio.gmail.")
1664
1990
  .requiredOption("--event <event>", "Event type, such as contact.created.")
1665
1991
  .option("--connection-id <connection_id>", "Specific integration connection id. Defaults to the active default connection.")
1992
+ .option("--trigger-config <json>", "JSON object passed to the provider when registering the trigger (Composio triggers only).")
1666
1993
  .option("--json", "Print a JSON envelope.")
1667
1994
  .action(async (options) => {
1668
- await handleAsyncAction("integrations events enable", options, async () => requestOxygen("/api/cli/integrations/events/enable", {
1669
- method: "POST",
1670
- body: {
1671
- source: readOption(options.source),
1672
- event: readOption(options.event),
1673
- ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
1674
- },
1675
- }));
1995
+ await handleAsyncAction("integrations events enable", options, async () => {
1996
+ const triggerConfigRaw = readOption(options.triggerConfig);
1997
+ let triggerConfig;
1998
+ if (triggerConfigRaw) {
1999
+ try {
2000
+ const parsed = JSON.parse(triggerConfigRaw);
2001
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2002
+ throw new Error("--trigger-config must be a JSON object.");
2003
+ }
2004
+ triggerConfig = parsed;
2005
+ }
2006
+ catch (error) {
2007
+ throw new Error(error instanceof Error
2008
+ ? `Invalid --trigger-config: ${error.message}`
2009
+ : "Invalid --trigger-config");
2010
+ }
2011
+ }
2012
+ return requestOxygen("/api/cli/integrations/events/enable", {
2013
+ method: "POST",
2014
+ body: {
2015
+ source: readOption(options.source),
2016
+ event: readOption(options.event),
2017
+ ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
2018
+ ...(triggerConfig ? { trigger_config: triggerConfig } : {}),
2019
+ },
2020
+ });
2021
+ });
1676
2022
  }))
1677
2023
  .addCommand(new Command("disable")
1678
2024
  .description("Disable a provider event for a connected integration account.")
@@ -2053,11 +2399,25 @@ export function createProgram() {
2053
2399
  }));
2054
2400
  program
2055
2401
  .command("skills")
2056
- .description("Agent skill installation commands.")
2402
+ .description("Agent skill discovery and installation commands.")
2403
+ .addCommand(new Command("list")
2404
+ .description("List Oxygen agent skills available from the public skill index.")
2405
+ .option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
2406
+ .option("--json", "Print a JSON envelope.")
2407
+ .action(async (options) => {
2408
+ await handleAsyncAction("skills list", options, async () => listAgentSkills(options));
2409
+ }))
2410
+ .addCommand(new Command("doctor")
2411
+ .description("Check Oxygen skill index reachability and local installer prerequisites.")
2412
+ .option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
2413
+ .option("--json", "Print a JSON envelope.")
2414
+ .action(async (options) => {
2415
+ await handleAsyncAction("skills doctor", options, async () => doctorAgentSkills(options));
2416
+ }))
2057
2417
  .addCommand(new Command("install")
2058
2418
  .description("Install Oxygen agent skills into local agent skill directories.")
2059
2419
  .option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
2060
- .option("--agents <agents>", "Space or comma separated agents. Defaults to codex, claude-code, and cursor.")
2420
+ .option("--agents <agents...>", "Space or comma separated agents. Defaults to codex, claude-code, and cursor.")
2061
2421
  .option("--skill <skill>", "Skill name or '*'. Defaults to '*'.")
2062
2422
  .option("--project", "Install into the current project instead of global agent scope.")
2063
2423
  .option("--copy", "Copy skill files instead of symlinking when supported by npx skills.")
@@ -2183,6 +2543,7 @@ function referencesRecipeSdk(source) {
2183
2543
  // Escape Next static analysis (the CLI is bundled by tsc, but mirror the
2184
2544
  // worker's escape so both load identically).
2185
2545
  const dynamicRecipeImport = new Function("specifier", "return import(specifier);");
2546
+ // skipcq: JS-R1003
2186
2547
  async function importRecipeModule(specifier) {
2187
2548
  try {
2188
2549
  return await dynamicRecipeImport(specifier);
@@ -2384,6 +2745,55 @@ function readTableRunSelection(options) {
2384
2745
  exitCode: 1,
2385
2746
  });
2386
2747
  }
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;
2765
+ if (options.force !== undefined)
2766
+ body.force = Boolean(options.force);
2767
+ return body;
2768
+ }
2769
+ function readCompaniesEnrichSelection(options) {
2770
+ const explicitSelection = readSelectionJsonOption(options.selectionJson);
2771
+ const hasAll = Boolean(options.all);
2772
+ const limit = readPositiveInt(options.limit);
2773
+ const rowIds = readCsvOption(options.rowIds);
2774
+ const filterSelection = readFilterSelectionOption(options.filterJson);
2775
+ const selectedModes = [
2776
+ Boolean(explicitSelection),
2777
+ hasAll,
2778
+ Boolean(limit),
2779
+ rowIds.length > 0,
2780
+ Boolean(filterSelection),
2781
+ ].filter(Boolean).length;
2782
+ if (selectedModes > 1) {
2783
+ throw new OxygenError("invalid_company_enrichment", "Pass only one row scope option.", { exitCode: 1 });
2784
+ }
2785
+ if (explicitSelection)
2786
+ return explicitSelection;
2787
+ if (hasAll)
2788
+ return { mode: "all" };
2789
+ if (limit)
2790
+ return { mode: "limit", limit };
2791
+ if (rowIds.length > 0)
2792
+ return { mode: "row_ids", row_ids: rowIds };
2793
+ if (filterSelection)
2794
+ return filterSelection;
2795
+ return undefined;
2796
+ }
2387
2797
  function readFilterSelectionOption(value) {
2388
2798
  const filters = readFilterJsonOption(value);
2389
2799
  return filters ? { mode: "filter", filters } : undefined;
@@ -2443,58 +2853,6 @@ function normalizeSessionStepStatus(value) {
2443
2853
  exitCode: 1,
2444
2854
  });
2445
2855
  }
2446
- function installAgentSkills(options) {
2447
- const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
2448
- const indexUrl = `${apiUrl}/.well-known/skills/index.json`;
2449
- const agents = readWords(options.agents ?? "codex claude-code cursor");
2450
- const skill = readOption(options.skill) ?? "*";
2451
- const args = [
2452
- "skills",
2453
- "add",
2454
- indexUrl,
2455
- "--agents",
2456
- ...agents,
2457
- "--yes",
2458
- "--skill",
2459
- skill,
2460
- "--full-depth",
2461
- ];
2462
- if (!options.project)
2463
- args.push("--global");
2464
- if (options.copy)
2465
- args.push("--copy");
2466
- let output = "";
2467
- try {
2468
- output = execFileSync("npx", args, {
2469
- encoding: "utf8",
2470
- stdio: ["ignore", "pipe", "pipe"],
2471
- });
2472
- }
2473
- catch (error) {
2474
- const failure = error;
2475
- throw new OxygenError("skills_install_failed", "Unable to install Oxygen agent skills.", {
2476
- details: {
2477
- index_url: indexUrl,
2478
- stdout: failure.stdout?.slice(0, 2000) ?? "",
2479
- stderr: failure.stderr?.slice(0, 2000) ?? failure.message ?? "unknown",
2480
- },
2481
- exitCode: 1,
2482
- });
2483
- }
2484
- return {
2485
- indexUrl,
2486
- agents,
2487
- skill,
2488
- scope: options.project ? "project" : "global",
2489
- output,
2490
- };
2491
- }
2492
- function readWords(value) {
2493
- return value
2494
- .split(/[,\s]+/)
2495
- .map((entry) => entry.trim())
2496
- .filter(Boolean);
2497
- }
2498
2856
  async function importRows(table, options) {
2499
2857
  const format = normalizeRowsFormat(options.format, inferRowsFileFormat(options.file));
2500
2858
  const parsedRows = await readRowsFile(options.file, format, options.sheet);
@@ -2599,11 +2957,13 @@ async function prepareImportTarget(table, options, parsedRows) {
2599
2957
  exitCode: 1,
2600
2958
  });
2601
2959
  }
2960
+ const createdWebUrl = readRecordString(created, "web_url")
2961
+ ?? readRecordString(created, "deepLink");
2602
2962
  return {
2603
2963
  tableRef: createdSlug,
2604
2964
  rows: normalized.rows,
2605
2965
  createdTable: created,
2606
- tableWebUrl: tableWebUrl(createdSlug),
2966
+ tableWebUrl: createdWebUrl ?? tableWebUrl(createdSlug),
2607
2967
  upsertKey: normalizeCreatedTableUpsertKey(options.upsertKey, normalized.keyBySource),
2608
2968
  };
2609
2969
  }
@@ -2642,6 +3002,7 @@ async function enqueueImportRows(table, options, rows, format, batchSize, contex
2642
3002
  },
2643
3003
  });
2644
3004
  const ingestionRunId = readRequiredResponseString(created, "id");
3005
+ const queueWait = readRecord(created, "queue_wait");
2645
3006
  if (isTerminalTableIngestionStatus(readRecordString(created, "status"))) {
2646
3007
  return {
2647
3008
  ingestionRun: created,
@@ -2660,6 +3021,7 @@ async function enqueueImportRows(table, options, rows, format, batchSize, contex
2660
3021
  mode: upsertKey ? "upsert" : "insert",
2661
3022
  upsertKey: upsertKey ?? null,
2662
3023
  idempotencyKey,
3024
+ ...(queueWait ? { queue_wait: queueWait } : {}),
2663
3025
  };
2664
3026
  }
2665
3027
  let insertedItems = 0;
@@ -2681,6 +3043,7 @@ async function enqueueImportRows(table, options, rows, format, batchSize, contex
2681
3043
  duplicatePositions += readCount(appended.duplicatePositions);
2682
3044
  latestRun = appended.run ?? latestRun;
2683
3045
  }
3046
+ emitQueueWaitStderrNote(queueWait);
2684
3047
  return {
2685
3048
  ingestionRun: latestRun,
2686
3049
  ingestionRunId,
@@ -2698,8 +3061,16 @@ async function enqueueImportRows(table, options, rows, format, batchSize, contex
2698
3061
  mode: upsertKey ? "upsert" : "insert",
2699
3062
  upsertKey: upsertKey ?? null,
2700
3063
  idempotencyKey,
3064
+ ...(queueWait ? { queue_wait: queueWait } : {}),
2701
3065
  };
2702
3066
  }
3067
+ function emitQueueWaitStderrNote(queueWait) {
3068
+ if (!queueWait)
3069
+ return;
3070
+ const note = readRecordString(queueWait, "note");
3071
+ if (note)
3072
+ process.stderr.write(`note: ${note}\n`);
3073
+ }
2703
3074
  async function waitForTableIngestionRun(runId, options) {
2704
3075
  const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
2705
3076
  ?? TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS;
@@ -2786,18 +3157,259 @@ async function exportRows(table, options) {
2786
3157
  ...(limit ? { limit } : {}),
2787
3158
  },
2788
3159
  });
2789
- const content = formatRows(result.rows, format, result.columns);
3160
+ const formatted = formatRows(result.rows, format, result.columns);
2790
3161
  if (options.output) {
2791
- writeFileSync(options.output, content);
3162
+ writeFileSync(options.output, formatted.content);
2792
3163
  }
2793
3164
  return {
2794
3165
  table: result.table ?? null,
2795
3166
  format,
2796
3167
  rowCount: result.rows.length,
2797
3168
  output: options.output ?? null,
2798
- ...(options.output ? {} : { content }),
3169
+ ...(options.output ? {} : { content: formatted.content }),
3170
+ ...(formatted.rescuedCount > 0 ? { rescuedNumericCells: formatted.rescuedCount } : {}),
3171
+ };
3172
+ }
3173
+ const TABLE_BUNDLE_SCHEMA_VERSION = 1;
3174
+ const TABLE_BUNDLE_MAX_PAGE_SIZE = 1000;
3175
+ const TABLE_BUNDLE_DEFAULT_PAGE_SIZE = 500;
3176
+ async function exportTableBundle(table, options) {
3177
+ const pageSize = Math.min(readPositiveInt(options.pageSize) ?? TABLE_BUNDLE_DEFAULT_PAGE_SIZE, TABLE_BUNDLE_MAX_PAGE_SIZE);
3178
+ // Pull the canonical schema (incl. enrichment/tool definitions and
3179
+ // semantic types) from `describe`; the paginated query response only
3180
+ // surfaces the user-visible projection.
3181
+ const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table } });
3182
+ const tableMeta = describe.table ?? null;
3183
+ const columns = (describe.columns ?? []).map(toBundleColumn);
3184
+ const rows = [];
3185
+ let cursor = null;
3186
+ let expectedTotal = null;
3187
+ let pageCount = 0;
3188
+ let hasMoreFlag = false;
3189
+ do {
3190
+ const requestBody = { table, limit: pageSize };
3191
+ if (cursor)
3192
+ requestBody.cursor = cursor;
3193
+ const page = await requestOxygen("/api/cli/tables/query", { method: "POST", body: requestBody });
3194
+ pageCount += 1;
3195
+ if (expectedTotal === null) {
3196
+ expectedTotal = readBundleNumber(page.totalCount) ?? readBundleNumber(page.total_count) ?? null;
3197
+ }
3198
+ for (const row of page.rows ?? [])
3199
+ rows.push(row);
3200
+ cursor = typeof page.nextCursor === "string" && page.nextCursor.length > 0 ? page.nextCursor : null;
3201
+ hasMoreFlag = Boolean(page.hasMore);
3202
+ // Defensive: hasMore=false should always mean cursor=null. If a future
3203
+ // server change ever sets one without the other, stop iterating on
3204
+ // hasMore=false so we don't loop forever.
3205
+ if (!hasMoreFlag)
3206
+ cursor = null;
3207
+ } while (cursor);
3208
+ if (expectedTotal !== null && rows.length !== expectedTotal) {
3209
+ throw new OxygenError("bundle_export_incomplete", "Cursor pagination ended before every row was written. The bundle would be a silent under-export.", {
3210
+ details: {
3211
+ table,
3212
+ totalCount: expectedTotal,
3213
+ exportedCount: rows.length,
3214
+ missing: expectedTotal - rows.length,
3215
+ pages: pageCount,
3216
+ },
3217
+ exitCode: 1,
3218
+ });
3219
+ }
3220
+ const tableId = readRecordString(tableMeta, "id");
3221
+ const tableSlug = readRecordString(tableMeta, "slug");
3222
+ const projectSlug = readRecordString(tableMeta, "projectSlug");
3223
+ const bundle = {
3224
+ schemaVersion: TABLE_BUNDLE_SCHEMA_VERSION,
3225
+ exportedAt: new Date().toISOString(),
3226
+ table: {
3227
+ id: tableId ?? null,
3228
+ slug: tableSlug ?? null,
3229
+ name: readRecordString(tableMeta, "displayName") ?? readRecordString(tableMeta, "name") ?? null,
3230
+ projectSlug: projectSlug ?? null,
3231
+ },
3232
+ columns,
3233
+ rows,
3234
+ totals: {
3235
+ rowCount: rows.length,
3236
+ ...(expectedTotal !== null ? { sourceTotalCount: expectedTotal } : {}),
3237
+ pages: pageCount,
3238
+ pageSize,
3239
+ },
3240
+ };
3241
+ if (options.output) {
3242
+ writeFileSync(options.output, `${JSON.stringify(bundle, null, 2)}\n`);
3243
+ }
3244
+ const summary = {
3245
+ table_id: tableId ?? null,
3246
+ table_slug: tableSlug ?? null,
3247
+ schema_version: TABLE_BUNDLE_SCHEMA_VERSION,
3248
+ column_count: columns.length,
3249
+ row_count: rows.length,
3250
+ pages: pageCount,
3251
+ page_size: pageSize,
3252
+ output: options.output ?? null,
3253
+ ...(tableId ? { web_url: tableWebUrl(tableId) } : tableSlug ? { web_url: tableWebUrl(tableSlug) } : {}),
3254
+ };
3255
+ // When piping to stdout, also emit the full bundle so it can be redirected
3256
+ // into a file. With --output, the file is the source of truth; keep the
3257
+ // stdout response a small summary so it's readable.
3258
+ return options.output ? summary : { ...summary, bundle };
3259
+ }
3260
+ async function importTableBundle(options) {
3261
+ const path = options.file;
3262
+ if (!path || !path.trim()) {
3263
+ throw new OxygenError("invalid_input", "--file is required.", { exitCode: 1 });
3264
+ }
3265
+ const batchSize = normalizeImportBatchSize(options.batchSize);
3266
+ const raw = readFileSync(resolve(path), "utf8");
3267
+ const bundle = parseBundleFile(raw);
3268
+ const columns = bundle.columns.map(bundleColumnToCreateInput);
3269
+ if (columns.length === 0) {
3270
+ throw new OxygenError("invalid_bundle", "Bundle has no columns; nothing to import.", { exitCode: 1 });
3271
+ }
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 });
3275
+ }
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 });
3291
+ }
3292
+ const validColumnKeys = new Set(columns.map((column) => column.key).filter((key) => Boolean(key)));
3293
+ const stagedRows = bundle.rows.map((row) => stripRowForImport(row, validColumnKeys));
3294
+ let inserted = 0;
3295
+ for (let offset = 0; offset < stagedRows.length; offset += batchSize) {
3296
+ const batch = stagedRows.slice(offset, offset + batchSize);
3297
+ if (batch.length === 0)
3298
+ continue;
3299
+ await requestOxygen("/api/cli/tables/rows", {
3300
+ method: "POST",
3301
+ body: { table: newTableId, rows: batch },
3302
+ });
3303
+ inserted += batch.length;
3304
+ }
3305
+ return {
3306
+ table_id: newTableId,
3307
+ table_slug: newTableSlug ?? null,
3308
+ column_count: columns.length,
3309
+ row_count: inserted,
3310
+ source_table: bundle.tableSummary,
3311
+ schema_version: bundle.schemaVersion,
3312
+ web_url: tableWebUrl(newTableSlug ?? newTableId),
3313
+ };
3314
+ }
3315
+ function toBundleColumn(column) {
3316
+ // Persist only the fields `tables create` knows how to ingest. Drop ids,
3317
+ // physical column names, archivedAt — those are tenant-local and would
3318
+ // collide on import.
3319
+ return {
3320
+ key: column.key,
3321
+ ...(column.label !== undefined ? { label: column.label } : {}),
3322
+ ...(column.dataType !== undefined ? { dataType: column.dataType } : {}),
3323
+ ...(column.kind !== undefined ? { kind: column.kind } : {}),
3324
+ ...(column.semanticType !== undefined ? { semanticType: column.semanticType } : {}),
3325
+ ...(column.definition !== undefined ? { definition: column.definition } : {}),
3326
+ ...(column.position !== undefined ? { position: column.position } : {}),
3327
+ };
3328
+ }
3329
+ function bundleColumnToCreateInput(column) {
3330
+ const label = typeof column.label === "string" && column.label.trim()
3331
+ ? column.label.trim()
3332
+ : typeof column.key === "string"
3333
+ ? column.key
3334
+ : null;
3335
+ if (!label) {
3336
+ throw new OxygenError("invalid_bundle", "Bundle column is missing label/key.", {
3337
+ details: { column },
3338
+ exitCode: 1,
3339
+ });
3340
+ }
3341
+ return {
3342
+ label,
3343
+ ...(typeof column.key === "string" ? { key: column.key } : {}),
3344
+ ...(typeof column.dataType === "string" ? { dataType: column.dataType } : {}),
3345
+ ...(typeof column.kind === "string" ? { kind: column.kind } : {}),
3346
+ ...(typeof column.semanticType === "string" ? { semanticType: column.semanticType } : {}),
3347
+ ...(column.definition && typeof column.definition === "object" && !Array.isArray(column.definition)
3348
+ ? { definition: column.definition }
3349
+ : {}),
2799
3350
  };
2800
3351
  }
3352
+ function parseBundleFile(text) {
3353
+ let parsed;
3354
+ try {
3355
+ parsed = JSON.parse(text);
3356
+ }
3357
+ catch (error) {
3358
+ throw new OxygenError("invalid_bundle", "Bundle file is not valid JSON.", {
3359
+ details: { reason: error instanceof Error ? error.message : "unknown" },
3360
+ exitCode: 1,
3361
+ });
3362
+ }
3363
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3364
+ throw new OxygenError("invalid_bundle", "Bundle file must be a JSON object.", { exitCode: 1 });
3365
+ }
3366
+ const record = parsed;
3367
+ const schemaVersion = typeof record.schemaVersion === "number" ? record.schemaVersion : 1;
3368
+ if (schemaVersion !== TABLE_BUNDLE_SCHEMA_VERSION) {
3369
+ throw new OxygenError("unsupported_bundle_version", `Bundle schema version ${schemaVersion} is not supported by this CLI.`, {
3370
+ details: { supported: TABLE_BUNDLE_SCHEMA_VERSION, got: schemaVersion },
3371
+ exitCode: 1,
3372
+ });
3373
+ }
3374
+ const rawColumns = record.columns;
3375
+ if (!Array.isArray(rawColumns)) {
3376
+ throw new OxygenError("invalid_bundle", "Bundle is missing a columns array.", { exitCode: 1 });
3377
+ }
3378
+ const rawRows = record.rows;
3379
+ if (!Array.isArray(rawRows)) {
3380
+ throw new OxygenError("invalid_bundle", "Bundle is missing a rows array.", { exitCode: 1 });
3381
+ }
3382
+ const tableSummary = record.table && typeof record.table === "object" && !Array.isArray(record.table)
3383
+ ? record.table
3384
+ : null;
3385
+ return {
3386
+ schemaVersion,
3387
+ tableName: tableSummary
3388
+ ? (readRecordString(tableSummary, "name") ?? readRecordString(tableSummary, "displayName"))
3389
+ : null,
3390
+ tableSummary,
3391
+ columns: rawColumns.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)),
3392
+ rows: rawRows.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)),
3393
+ };
3394
+ }
3395
+ function stripRowForImport(row, validColumnKeys) {
3396
+ const out = {};
3397
+ for (const [key, value] of Object.entries(row)) {
3398
+ if (key === "_row_id" || key === "_created_at" || key === "_updated_at")
3399
+ continue;
3400
+ if (validColumnKeys.size > 0 && !validColumnKeys.has(key))
3401
+ continue;
3402
+ out[key] = value;
3403
+ }
3404
+ return out;
3405
+ }
3406
+ function readBundleNumber(value) {
3407
+ if (typeof value === "number" && Number.isFinite(value))
3408
+ return value;
3409
+ if (typeof value === "string" && value.trim() && Number.isFinite(Number(value)))
3410
+ return Number(value);
3411
+ return null;
3412
+ }
2801
3413
  async function readRowsFile(path, format, sheet) {
2802
3414
  return await parseRowsFileBuffer(readFileSync(path), format, sheet ? { sheet } : {});
2803
3415
  }
@@ -2808,18 +3420,26 @@ function normalizeCreatedTableUpsertKey(value, keyBySource) {
2808
3420
  }
2809
3421
  function normalizeExportRowsFormat(value) {
2810
3422
  const normalized = value?.trim().toLowerCase() || "json";
2811
- if (normalized === "json" || normalized === "jsonl" || normalized === "csv")
3423
+ if (normalized === "json"
3424
+ || normalized === "jsonl"
3425
+ || normalized === "csv"
3426
+ || normalized === "table")
2812
3427
  return normalized;
2813
- throw new OxygenError("invalid_format", "Export format must be json, jsonl, or csv.", {
3428
+ throw new OxygenError("invalid_format", "Export format must be json, jsonl, csv, or table.", {
2814
3429
  details: { format: value },
2815
3430
  exitCode: 1,
2816
3431
  });
2817
3432
  }
2818
3433
  function formatRows(rows, format, columns) {
2819
- if (format === "json")
2820
- return `${JSON.stringify(rows, null, 2)}\n`;
2821
- if (format === "jsonl")
2822
- return `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`;
3434
+ if (format === "json") {
3435
+ return { content: `${JSON.stringify(rows, null, 2)}\n`, rescuedCount: 0 };
3436
+ }
3437
+ if (format === "jsonl") {
3438
+ return {
3439
+ content: `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`,
3440
+ rescuedCount: 0,
3441
+ };
3442
+ } // skipcq: JS-0246
2823
3443
  const keys = [
2824
3444
  "_row_id",
2825
3445
  "_created_at",
@@ -2827,10 +3447,64 @@ function formatRows(rows, format, columns) {
2827
3447
  ...(columns?.map((column) => column.key) ?? []),
2828
3448
  ...rows.flatMap((row) => Object.keys(row)),
2829
3449
  ].filter((key, index, all) => all.indexOf(key) === index && rows.some((row) => key in row));
2830
- return [
2831
- keys.map(escapeCsvField).join(","),
2832
- ...rows.map((row) => keys.map((key) => escapeCsvField(row[key])).join(",")),
2833
- ].join("\n") + "\n";
3450
+ if (format === "csv") {
3451
+ const csvBody = [
3452
+ keys.map(escapeCsvField).join(","),
3453
+ ...rows.map((row) => keys.map((key) => escapeCsvField(row[key])).join(",")),
3454
+ ].join("\n");
3455
+ return {
3456
+ content: `${csvBody}\n`,
3457
+ rescuedCount: 0,
3458
+ };
3459
+ }
3460
+ // "table": render with type-aware formatting and a Markdown-style frame.
3461
+ const columnByKey = new Map(columns?.map((c) => [c.key, c]) ?? []);
3462
+ let rescuedCount = 0;
3463
+ const headers = keys.map((key) => columnByKey.get(key)?.label ?? key);
3464
+ const formattedRows = rows.map((row) => keys.map((key) => {
3465
+ const column = columnByKey.get(key) ?? null;
3466
+ const formatted = formatCellForDisplay(row[key], column, {
3467
+ surface: "cli",
3468
+ onRescued: () => {
3469
+ rescuedCount += 1;
3470
+ },
3471
+ });
3472
+ // Pipe and any line-break char are the row/cell delimiters of the
3473
+ // Markdown frame — raw values containing them would corrupt the layout.
3474
+ // Match on the line-break class (not just `\r?\n`) so a standalone `\r`
3475
+ // doesn't slip through and split the row visually.
3476
+ return formatted.replace(/[\r\n]+/g, " ↵ ").replace(/\|/g, "\\|");
3477
+ }));
3478
+ const widths = headers.map((header, columnIndex) => {
3479
+ let max = header.length;
3480
+ for (const row of formattedRows) {
3481
+ const cell = row[columnIndex] ?? "";
3482
+ if (cell.length > max)
3483
+ max = cell.length;
3484
+ }
3485
+ return Math.min(max, 60);
3486
+ });
3487
+ const renderRow = (cells) => {
3488
+ const padded = cells.map((cell, i) => {
3489
+ const w = widths[i] ?? 0;
3490
+ return clipCell(cell, w).padEnd(w);
3491
+ });
3492
+ return `| ${padded.join(" | ")} |`;
3493
+ };
3494
+ const separator = `|${widths.map((w) => "-".repeat(w + 2)).join("|")}|`;
3495
+ const lines = [renderRow(headers), separator, ...formattedRows.map(renderRow)];
3496
+ if (rescuedCount > 0) {
3497
+ lines.push("");
3498
+ lines.push(`# Note: reformatted ${rescuedCount} cell${rescuedCount === 1 ? "" : "s"} that look numeric in text columns. Run \`oxygen columns retype --to numeric\` to make this permanent.`);
3499
+ }
3500
+ return { content: `${lines.join("\n")}\n`, rescuedCount };
3501
+ }
3502
+ function clipCell(value, width) {
3503
+ if (value.length <= width)
3504
+ return value;
3505
+ if (width <= 1)
3506
+ return value.slice(0, width);
3507
+ return `${value.slice(0, width - 1)}…`;
2834
3508
  }
2835
3509
  function escapeCsvField(value) {
2836
3510
  const text = value === null || value === undefined
@@ -2916,6 +3590,54 @@ function isTerminalTableActionRunStatus(status) {
2916
3590
  function sleep(ms) {
2917
3591
  return new Promise((resolve) => setTimeout(resolve, ms));
2918
3592
  }
3593
+ async function resolveActiveProfileWithSource() {
3594
+ const resolution = await resolveActiveProfile();
3595
+ let source;
3596
+ if (globalProfileFlag)
3597
+ source = "flag";
3598
+ else if (INITIAL_OXYGEN_PROFILE_ENV)
3599
+ source = "env";
3600
+ else
3601
+ source = resolution.source === "env" ? "env" : resolution.source;
3602
+ return { resolution, source };
3603
+ }
3604
+ async function handleWhoamiAction(options) {
3605
+ try {
3606
+ const identity = await requestOxygen("/api/cli/whoami");
3607
+ const context = await resolveActiveProfileWithSource();
3608
+ if (context.resolution.exists) {
3609
+ const cached = context.resolution.credentials?.identity;
3610
+ const orgChanged = !cached || cached.organization.id !== identity.organization.id;
3611
+ const userChanged = !cached || cached.user.id !== identity.user.id;
3612
+ if (orgChanged || userChanged) {
3613
+ const refreshed = {
3614
+ token: context.resolution.credentials.token,
3615
+ apiUrl: context.resolution.credentials.apiUrl,
3616
+ identity: identityFromWhoami(identity),
3617
+ };
3618
+ await saveCredentials(refreshed, process.env, {
3619
+ profile: context.resolution.name,
3620
+ activate: false,
3621
+ }).catch(() => undefined);
3622
+ }
3623
+ }
3624
+ const data = {
3625
+ ...identity,
3626
+ profile: context.resolution.exists ? context.resolution.name : null,
3627
+ profile_source: context.source,
3628
+ };
3629
+ if (options.json) {
3630
+ writeJson(success("whoami", data));
3631
+ return;
3632
+ }
3633
+ process.stdout.write(formatWhoami(identity, context));
3634
+ }
3635
+ catch (error) {
3636
+ const failure = toFailure("whoami", error);
3637
+ writeJson(failure);
3638
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
3639
+ }
3640
+ }
2919
3641
  async function handleLoginAction(options) {
2920
3642
  try {
2921
3643
  const data = await login(options);
@@ -2939,10 +3661,14 @@ async function handleAuthUseTokenAction(options) {
2939
3661
  api_url: data.credentials.apiUrl,
2940
3662
  user: data.identity.user,
2941
3663
  organization: data.identity.organization,
3664
+ renamed: data.renamed,
2942
3665
  }));
2943
3666
  return;
2944
3667
  }
2945
- process.stdout.write(formatLoginSuccess(data.identity, data.credentials, data.profile));
3668
+ process.stdout.write(formatLoginSuccess(data.identity, data.credentials, data.profile, { renamed: data.renamed }));
3669
+ const hint = await buildPostLoginHint(data.profile);
3670
+ if (hint)
3671
+ process.stdout.write(hint);
2946
3672
  }
2947
3673
  catch (error) {
2948
3674
  const failure = toFailure("auth use-token", error);
@@ -2975,6 +3701,8 @@ async function handleProfilesListAction(options) {
2975
3701
  async function handleProfilesUseAction(profile, options) {
2976
3702
  try {
2977
3703
  const activeProfile = await switchCredentialProfile(profile);
3704
+ const state = await listCredentialProfiles().catch(() => null);
3705
+ const totalProfiles = state?.profiles.length ?? 1;
2978
3706
  const data = {
2979
3707
  active_profile: activeProfile.name,
2980
3708
  profile: summarizeCredentialProfile(activeProfile, true),
@@ -2983,7 +3711,7 @@ async function handleProfilesUseAction(profile, options) {
2983
3711
  writeJson(success("profiles use", data));
2984
3712
  return;
2985
3713
  }
2986
- process.stdout.write(formatProfileUseSuccess(data.profile));
3714
+ process.stdout.write(formatProfileUseSuccess(data.profile, { totalProfiles }));
2987
3715
  }
2988
3716
  catch (error) {
2989
3717
  const failure = toFailure("profiles use", error);
@@ -2991,6 +3719,74 @@ async function handleProfilesUseAction(profile, options) {
2991
3719
  process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
2992
3720
  }
2993
3721
  }
3722
+ async function handleProfilesEnvAction(profile, options) {
3723
+ try {
3724
+ if (options.unset) {
3725
+ const data = {
3726
+ profile,
3727
+ unset: ["OXYGEN_PROFILE", "OXYGEN_API_URL"],
3728
+ };
3729
+ if (options.json) {
3730
+ writeJson(success("profiles env", data));
3731
+ return;
3732
+ }
3733
+ process.stdout.write("unset OXYGEN_PROFILE OXYGEN_API_URL\n");
3734
+ return;
3735
+ }
3736
+ const state = await listCredentialProfiles();
3737
+ const match = state.profiles.find((entry) => entry.name === profile);
3738
+ if (!match) {
3739
+ throw new OxygenError("profile_not_found", `Oxygen CLI profile "${profile}" is not stored.`, {
3740
+ details: { profile },
3741
+ exitCode: 1,
3742
+ });
3743
+ }
3744
+ const env = {
3745
+ OXYGEN_PROFILE: match.name,
3746
+ OXYGEN_API_URL: match.apiUrl,
3747
+ };
3748
+ if (options.json) {
3749
+ writeJson(success("profiles env", { profile: match.name, env }));
3750
+ return;
3751
+ }
3752
+ process.stdout.write(`export OXYGEN_PROFILE=${shellQuote(match.name)}\n`);
3753
+ process.stdout.write(`export OXYGEN_API_URL=${shellQuote(match.apiUrl)}\n`);
3754
+ }
3755
+ catch (error) {
3756
+ const failure = toFailure("profiles env", error);
3757
+ writeJson(failure);
3758
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
3759
+ }
3760
+ }
3761
+ async function handleProfilesCurrentAction(options) {
3762
+ try {
3763
+ const context = await resolveActiveProfileWithSource();
3764
+ const credentials = context.resolution.credentials;
3765
+ const data = {
3766
+ profile: context.resolution.exists ? context.resolution.name : null,
3767
+ profile_source: context.source,
3768
+ api_url: credentials?.apiUrl ?? null,
3769
+ organization: credentials?.identity?.organization ?? null,
3770
+ user: credentials?.identity?.user ?? null,
3771
+ stored: context.resolution.exists,
3772
+ };
3773
+ if (options.json) {
3774
+ writeJson(success("profiles current", data));
3775
+ return;
3776
+ }
3777
+ process.stdout.write(formatProfilesCurrent(context));
3778
+ }
3779
+ catch (error) {
3780
+ const failure = toFailure("profiles current", error);
3781
+ writeJson(failure);
3782
+ process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
3783
+ }
3784
+ }
3785
+ function shellQuote(value) {
3786
+ if (/^[A-Za-z0-9_.\-:\/@%+=]+$/.test(value))
3787
+ return value;
3788
+ return `'${value.replace(/'/g, `'\\''`)}'`;
3789
+ }
2994
3790
  async function handleLogoutAction(options) {
2995
3791
  try {
2996
3792
  const clearOptions = {};
@@ -3030,64 +3826,6 @@ async function handleUpdateAction(options) {
3030
3826
  process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
3031
3827
  }
3032
3828
  }
3033
- function detectCliInstallPrefix() {
3034
- try {
3035
- const path = fileURLToPath(import.meta.url);
3036
- const suffixes = [
3037
- "/lib/node_modules/@oxygen-agent/cli/dist/index.js",
3038
- "/lib/node_modules/@oxygen/cli/dist/index.js",
3039
- ];
3040
- for (const suffix of suffixes) {
3041
- if (path.endsWith(suffix)) {
3042
- return path.slice(0, -suffix.length);
3043
- }
3044
- }
3045
- }
3046
- catch {
3047
- // Non-file URL (e.g. running from a bundler) — fall through.
3048
- }
3049
- return null;
3050
- }
3051
- function updateCli(options) {
3052
- const packageSpec = readOption(options.package) ?? DEFAULT_CLI_PACKAGE_SPEC;
3053
- const prefix = detectCliInstallPrefix();
3054
- const args = prefix
3055
- ? ["install", "-g", "--prefix", prefix, packageSpec]
3056
- : ["install", "-g", packageSpec];
3057
- const command = ["npm", ...args].join(" ");
3058
- if (options.dryRun) {
3059
- return {
3060
- current_version: OXYGEN_VERSION,
3061
- package: packageSpec,
3062
- command,
3063
- dry_run: true,
3064
- updated: false,
3065
- };
3066
- }
3067
- const result = spawnSync("npm", args, {
3068
- encoding: "utf8",
3069
- stdio: options.json ? ["ignore", "pipe", "pipe"] : "inherit",
3070
- });
3071
- if (result.error || result.status !== 0) {
3072
- throw new OxygenError("cli_update_failed", "Unable to update the Oxygen CLI.", {
3073
- details: {
3074
- command,
3075
- package: packageSpec,
3076
- exit_code: result.status,
3077
- reason: result.error instanceof Error ? result.error.message : null,
3078
- stderr: typeof result.stderr === "string" && result.stderr.trim() ? result.stderr.trim().slice(0, 4000) : null,
3079
- },
3080
- exitCode: 1,
3081
- });
3082
- }
3083
- return {
3084
- current_version: OXYGEN_VERSION,
3085
- package: packageSpec,
3086
- command,
3087
- dry_run: false,
3088
- updated: true,
3089
- };
3090
- }
3091
3829
  function buildApiKeyCreateBody(options) {
3092
3830
  const body = {};
3093
3831
  const name = readOption(options.name);
@@ -3135,17 +3873,28 @@ async function login(options) {
3135
3873
  browser: options.browser !== false,
3136
3874
  json: Boolean(options.json),
3137
3875
  });
3138
- const credentials = {
3139
- token,
3140
- apiUrl,
3141
- };
3876
+ const credentials = { token, apiUrl };
3142
3877
  const identity = await requestOxygen("/api/cli/whoami", { credentials });
3143
- const saveOptions = {};
3144
- if (options.profile !== undefined)
3145
- saveOptions.profile = options.profile;
3146
- const profile = await saveCredentials(credentials, process.env, saveOptions);
3878
+ credentials.identity = identityFromWhoami(identity);
3879
+ const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
3880
+ let chosenProfile;
3881
+ let renamed = false;
3882
+ if (explicitProfile) {
3883
+ chosenProfile = explicitProfile;
3884
+ }
3885
+ else {
3886
+ const seed = identity.organization.slug?.trim() || identity.organization.id;
3887
+ const picked = await pickProfileNameForIdentity(identity.organization.id, seed);
3888
+ chosenProfile = picked.name;
3889
+ renamed = picked.renamed;
3890
+ }
3891
+ const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
3892
+ const skillsInstall = runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl });
3147
3893
  if (!options.json) {
3148
- process.stdout.write(formatLoginSuccess(identity, credentials, profile));
3894
+ process.stdout.write(formatLoginSuccess(identity, credentials, profile, { renamed, skillsInstall }));
3895
+ const hint = await buildPostLoginHint(profile);
3896
+ if (hint)
3897
+ process.stdout.write(hint);
3149
3898
  }
3150
3899
  return {
3151
3900
  logged_in: true,
@@ -3153,6 +3902,7 @@ async function login(options) {
3153
3902
  api_url: credentials.apiUrl,
3154
3903
  user: identity.user,
3155
3904
  organization: identity.organization,
3905
+ skills_install: skillsInstall,
3156
3906
  };
3157
3907
  }
3158
3908
  async function useAuthToken(options) {
@@ -3167,17 +3917,59 @@ async function useAuthToken(options) {
3167
3917
  apiUrl: normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl()),
3168
3918
  };
3169
3919
  const identity = await requestOxygen("/api/cli/whoami", { credentials });
3170
- const saveOptions = {};
3171
- if (options.profile !== undefined)
3172
- saveOptions.profile = options.profile;
3173
- const profile = await saveCredentials(credentials, process.env, saveOptions);
3920
+ credentials.identity = identityFromWhoami(identity);
3921
+ const explicitProfile = readOption(options.profile) ?? readEnvProfileName();
3922
+ let chosenProfile;
3923
+ let renamed = false;
3924
+ if (explicitProfile) {
3925
+ chosenProfile = explicitProfile;
3926
+ }
3927
+ else {
3928
+ const seed = identity.organization.slug?.trim() || identity.organization.id;
3929
+ const picked = await pickProfileNameForIdentity(identity.organization.id, seed);
3930
+ chosenProfile = picked.name;
3931
+ renamed = picked.renamed;
3932
+ }
3933
+ const profile = await saveCredentials(credentials, process.env, { profile: chosenProfile });
3174
3934
  return {
3175
3935
  identity,
3176
3936
  credentials,
3177
3937
  api_url: credentials.apiUrl,
3178
3938
  profile,
3939
+ renamed,
3179
3940
  };
3180
3941
  }
3942
+ function readEnvProfileName() {
3943
+ const value = process.env.OXYGEN_PROFILE?.trim();
3944
+ return value ? value : null;
3945
+ }
3946
+ function identityFromWhoami(response) {
3947
+ return {
3948
+ organization: {
3949
+ id: response.organization.id,
3950
+ slug: response.organization.slug ?? null,
3951
+ name: response.organization.name,
3952
+ },
3953
+ user: {
3954
+ id: response.user.id,
3955
+ email: response.user.email ?? null,
3956
+ },
3957
+ capturedAt: new Date().toISOString(),
3958
+ };
3959
+ }
3960
+ async function buildPostLoginHint(activeProfile) {
3961
+ if (output.isTTY !== true || process.env.NO_COLOR) {
3962
+ const state = await listCredentialProfiles().catch(() => null);
3963
+ if (!state || state.profiles.length < 2)
3964
+ return "";
3965
+ return `\n Pin this terminal: eval "$(oxygen profiles env ${activeProfile})"\n\n`;
3966
+ }
3967
+ const state = await listCredentialProfiles().catch(() => null);
3968
+ if (!state || state.profiles.length < 2)
3969
+ return "";
3970
+ const c = ansi(true);
3971
+ return `\n ${c.dim("Pin this terminal:")} ${c.bold(`eval "$(oxygen profiles env ${activeProfile})"`)}\n\n`;
3972
+ }
3181
3973
  async function promptForToken(options) {
3182
3974
  const fallbackLoginUrl = createCliLoginUrl(options.apiUrl);
3183
3975
  if (options.json) {
@@ -3315,9 +4107,11 @@ function startOxygenSpinner(message) {
3315
4107
  },
3316
4108
  };
3317
4109
  }
3318
- function formatLoginSuccess(identity, credentials, profile) {
4110
+ function formatLoginSuccess(identity, credentials, profile, options = {}) {
3319
4111
  const email = identity.user.email ?? identity.user.id;
3320
- const org = identity.organization.name || identity.organization.slug || identity.organization.id;
4112
+ const orgName = identity.organization.name || identity.organization.slug || identity.organization.id;
4113
+ const orgSlug = identity.organization.slug ?? null;
4114
+ const org = orgSlug && orgSlug !== orgName ? `${orgName} (${orgSlug})` : orgName;
3321
4115
  const key = identity.apiKey
3322
4116
  ? `${identity.apiKey.tokenPrefix}...${identity.apiKey.tokenSuffix}`
3323
4117
  : "stored";
@@ -3325,10 +4119,14 @@ function formatLoginSuccess(identity, credentials, profile) {
3325
4119
  .update(`oxygen-cli:${credentials.token}`)
3326
4120
  .digest("hex");
3327
4121
  const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
4122
+ const profileLabel = options.renamed
4123
+ ? `${profile} ${c.dim("(auto-renamed to avoid collision)")}`
4124
+ : profile;
4125
+ // skipcq: JS-0820 — not a React component; rule misfire on array of tuples
3328
4126
  const rows = [
3329
4127
  ["Account", email],
3330
4128
  ["Organization", org],
3331
- ["Profile", profile],
4129
+ ["Profile", profileLabel],
3332
4130
  ["API", credentials.apiUrl],
3333
4131
  ["Token", key],
3334
4132
  ["Fingerprint", formatFingerprint(fingerprint)],
@@ -3347,6 +4145,7 @@ function formatLoginSuccess(identity, credentials, profile) {
3347
4145
  "",
3348
4146
  ...rows.map(([label, value]) => ` ${c.dim(label.padEnd(labelWidth))} ${value}`),
3349
4147
  "",
4148
+ ...formatAutomaticSkillsInstallStatusLines(options.skillsInstall),
3350
4149
  ].join("\n");
3351
4150
  }
3352
4151
  function formatFingerprint(hex) {
@@ -3358,6 +4157,9 @@ function summarizeCredentialProfile(profile, active) {
3358
4157
  active,
3359
4158
  api_url: profile.apiUrl,
3360
4159
  token_fingerprint: formatFingerprint(createCredentialFingerprint(profile.token)),
4160
+ organization: profile.identity?.organization ?? null,
4161
+ user: profile.identity?.user ?? null,
4162
+ identity_captured_at: profile.identity?.capturedAt ?? null,
3361
4163
  };
3362
4164
  }
3363
4165
  function createCredentialFingerprint(token) {
@@ -3374,33 +4176,115 @@ function formatProfilesList(data) {
3374
4176
  "",
3375
4177
  ].join("\n");
3376
4178
  }
3377
- const labelWidth = Math.max(...data.profiles.map((profile) => profile.name.length));
3378
- return [
4179
+ const nameWidth = Math.max(...data.profiles.map((profile) => profile.name.length));
4180
+ const orgWidth = Math.max(...data.profiles.map((profile) => formatProfileOrgCell(profile).length));
4181
+ const lines = [
3379
4182
  "",
3380
4183
  `${c.bold("Oxygen CLI Profiles")}`,
3381
4184
  "",
3382
4185
  ...data.profiles.map((profile) => {
3383
4186
  const marker = profile.active ? "*" : " ";
4187
+ const orgCell = formatProfileOrgCell(profile);
3384
4188
  return [
3385
- `${marker} ${profile.name.padEnd(labelWidth)}`,
4189
+ `${marker} ${profile.name.padEnd(nameWidth)}`,
4190
+ orgCell.padEnd(orgWidth),
3386
4191
  c.dim(profile.api_url),
3387
4192
  c.dim(profile.token_fingerprint),
3388
4193
  ].join(" ");
3389
4194
  }),
3390
4195
  "",
3391
- ].join("\n");
4196
+ ];
4197
+ if (data.profiles.length >= 2) {
4198
+ lines.push(` ${c.dim("Pin a terminal:")} ${c.bold(`eval "$(oxygen profiles env <profile>)"`)}`);
4199
+ lines.push("");
4200
+ }
4201
+ return lines.join("\n");
3392
4202
  }
3393
- function formatProfileUseSuccess(profile) {
4203
+ function formatProfileOrgCell(profile) {
4204
+ if (!profile.organization)
4205
+ return "(unknown org — run `oxygen whoami` to refresh)";
4206
+ const { slug, name } = profile.organization;
4207
+ if (slug && slug !== name)
4208
+ return `${name} (${slug})`;
4209
+ return name || slug || profile.organization.id;
4210
+ }
4211
+ function formatProfileUseSuccess(profile, options) {
3394
4212
  const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
3395
- return [
4213
+ const lines = [
3396
4214
  "",
3397
4215
  `${c.green("[OK]")} ${c.bold("CLI profile selected")}`,
3398
4216
  "",
3399
- ` ${c.dim("Profile")} ${profile.name}`,
3400
- ` ${c.dim("API")} ${profile.api_url}`,
3401
- ` ${c.dim("Fingerprint")} ${profile.token_fingerprint}`,
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}`,
4221
+ "",
4222
+ ];
4223
+ 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})"`)}`);
4226
+ lines.push("");
4227
+ }
4228
+ return lines.join("\n");
4229
+ }
4230
+ function formatWhoami(identity, context) {
4231
+ const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
4232
+ const email = identity.user.email ?? identity.user.id;
4233
+ const orgName = identity.organization.name || identity.organization.slug || identity.organization.id;
4234
+ const orgSlug = identity.organization.slug ?? null;
4235
+ const org = orgSlug && orgSlug !== orgName ? `${orgName} (${orgSlug})` : orgName;
4236
+ const sourceLabel = describeProfileSource(context.source);
4237
+ const profileName = context.resolution.exists ? context.resolution.name : "(no stored profile)";
4238
+ const apiUrl = context.resolution.credentials?.apiUrl ?? defaultApiUrl();
4239
+ const rows = [
4240
+ ["Account", email],
4241
+ ["Organization", org],
4242
+ ["Profile", `${profileName} ${c.dim(`(${sourceLabel})`)}`],
4243
+ ["API", apiUrl],
4244
+ ];
4245
+ const labelWidth = Math.max(...rows.map(([label]) => label.length));
4246
+ return [
4247
+ "",
4248
+ ...rows.map(([label, value]) => ` ${c.dim(label.padEnd(labelWidth))} ${value}`),
3402
4249
  "",
3403
4250
  ].join("\n");
4251
+ } // skipcq: JS-C1002
4252
+ function formatProfilesCurrent(context) {
4253
+ const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
4254
+ const sourceLabel = describeProfileSource(context.source);
4255
+ if (!context.resolution.exists) {
4256
+ return [
4257
+ "",
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.")}`,
4261
+ "",
4262
+ ].join("\n");
4263
+ }
4264
+ 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)
4269
+ : "(unknown — run `oxygen whoami` to refresh)";
4270
+ return [
4271
+ "",
4272
+ `${c.bold("Active Oxygen CLI Profile")}`,
4273
+ "",
4274
+ ` ${c.dim("Profile")} ${context.resolution.name}`,
4275
+ ` ${c.dim("Source")} ${sourceLabel}`,
4276
+ ` ${c.dim("Organization")} ${orgCell}`,
4277
+ ` ${c.dim("API")} ${credentials.apiUrl}`,
4278
+ "",
4279
+ ].join("\n");
4280
+ }
4281
+ function describeProfileSource(source) {
4282
+ switch (source) {
4283
+ case "flag": return "from --profile flag";
4284
+ case "env": return "from OXYGEN_PROFILE";
4285
+ case "file": return "from stored active profile";
4286
+ case "default": return "default fallback";
4287
+ }
3404
4288
  }
3405
4289
  function formatLogoutSuccess(result) {
3406
4290
  const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
@@ -3412,7 +4296,7 @@ function formatLogoutSuccess(result) {
3412
4296
  `${c.green("[OK]")} ${c.bold("CLI logged out")}`,
3413
4297
  "",
3414
4298
  ` ${c.dim("Credentials")} ${removed}`,
3415
- ` ${c.dim("Profiles left")} ${String(result.remainingProfiles)}`,
4299
+ ` ${c.dim("Profiles left")} ${String(result.remainingProfiles)}`, // skipcq: JS-C1002
3416
4300
  "",
3417
4301
  ].join("\n");
3418
4302
  }
@@ -3426,7 +4310,33 @@ function formatUpdateSuccess(result) {
3426
4310
  ` ${c.dim("Package")} ${result.package}`,
3427
4311
  ` ${c.dim("Command")} ${result.command}`,
3428
4312
  "",
4313
+ ...formatAutomaticSkillsInstallStatusLines(result.skills_install),
3429
4314
  ].join("\n");
4315
+ } // skipcq: JS-C1002
4316
+ function formatAutomaticSkillsInstallStatusLines(result) {
4317
+ if (!result)
4318
+ return [];
4319
+ const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
4320
+ if (!result.attempted) {
4321
+ if (result.reason === "dry_run")
4322
+ return [];
4323
+ return [
4324
+ `${c.dim("Skills")} skipped (${result.reason ?? "not attempted"})`,
4325
+ "",
4326
+ ];
4327
+ }
4328
+ if (result.ok) {
4329
+ return [
4330
+ `${c.green("[OK]")} ${c.bold("Oxygen skills installed")}`,
4331
+ "",
4332
+ ];
4333
+ }
4334
+ 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}`,
4338
+ "",
4339
+ ];
3430
4340
  }
3431
4341
  function renderBox(lines) {
3432
4342
  const width = Math.max(...lines.map(visibleLength), 0);
@@ -3435,6 +4345,7 @@ function renderBox(lines) {
3435
4345
  return [border, ...body, border].join("\n");
3436
4346
  }
3437
4347
  function visibleLength(value) {
4348
+ // skipcq: JS-0004 — ESC (\x1b) is the ANSI CSI introducer; required to strip color codes
3438
4349
  return value.replace(/\x1b\[[0-9;]*m/g, "").length;
3439
4350
  }
3440
4351
  function ansi(enabled) {
@@ -3442,9 +4353,10 @@ function ansi(enabled) {
3442
4353
  ? (text) => `\x1b[${open}m${text}\x1b[${close}m`
3443
4354
  : (text) => text;
3444
4355
  return {
3445
- bold: wrap(1, 22),
4356
+ bold: wrap(1, 22), // skipcq: JS-0117 // skipcq: JS-W1035
3446
4357
  dim: wrap(2, 22),
3447
4358
  green: wrap(32, 39),
4359
+ yellow: wrap(33, 39),
3448
4360
  };
3449
4361
  }
3450
4362
  function readOption(value) {
@@ -3472,6 +4384,20 @@ function contextAssetsQuery(options) {
3472
4384
  const value = query.toString();
3473
4385
  return value ? `?${value}` : "";
3474
4386
  }
4387
+ function buildContextResolveBody(options) {
4388
+ const assetTypes = readCsvOption(options.assetType);
4389
+ const tags = readCsvOption(options.tags);
4390
+ const maxAssets = readNonNegativeInt(options.maxAssets);
4391
+ return {
4392
+ ...(readOption(options.purpose) ? { purpose: readOption(options.purpose) } : {}),
4393
+ ...(assetTypes.length > 0 ? { asset_types: assetTypes } : {}),
4394
+ ...(readOption(options.assetStatus) ? { asset_status: readOption(options.assetStatus) } : {}),
4395
+ ...(tags.length > 0 ? { tags } : {}),
4396
+ ...(options.includeArchived ? { include_archived: true } : {}),
4397
+ ...(maxAssets !== undefined ? { max_assets: maxAssets } : {}),
4398
+ ...(options.requireReady ? { require_ready: true } : {}),
4399
+ };
4400
+ }
3475
4401
  function buildContextAssetUpsertBody(options) {
3476
4402
  const asset = options.assetJson ? parseJsonObject(options.assetJson) : {};
3477
4403
  const tags = readCsvOption(options.tags);