@oxygen-agent/cli 1.46.0 → 1.50.37

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,7 +9,7 @@ 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";
@@ -17,6 +18,7 @@ import { clearCredentials, defaultApiUrl, listCredentialProfiles, normalizeApiUr
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 { updateCli } from "./update.js";
20
22
  const BROWSER_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
21
23
  const OXYGEN_SPINNER_INTERVAL_MS = 90;
22
24
  const OXYGEN_SPINNER_FRAMES = [
@@ -43,7 +45,6 @@ const TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS = 600;
43
45
  const TABLE_INGESTION_WAIT_DEFAULT_INTERVAL_SECONDS = 5;
44
46
  const WORKFLOW_TAIL_DEFAULT_TIMEOUT_SECONDS = 600;
45
47
  const WORKFLOW_TAIL_DEFAULT_INTERVAL_SECONDS = 2;
46
- const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
47
48
  const CLI_MODULE_DIR = dirname(fileURLToPath(import.meta.url));
48
49
  const RECIPE_ESBUILD_NODE_PATHS = [
49
50
  resolve("node_modules"),
@@ -518,9 +519,9 @@ export function createProgram() {
518
519
  await handleAsyncAction("tables import", options, async () => importRows(table, options));
519
520
  }))
520
521
  .addCommand(new Command("export")
521
- .description("Export workspace table rows as JSON, JSONL, or CSV.")
522
+ .description("Export workspace table rows as JSON, JSONL, CSV, or a human-readable table.")
522
523
  .argument("<table>", "Table id or slug.")
523
- .option("--format <format>", "json, jsonl, or csv. Defaults to json.")
524
+ .option("--format <format>", "json, jsonl, csv, or table. Defaults to json. Use table for a typed, human-readable rendering with thousands grouping.")
524
525
  .option("--output <path>", "Write export content to a file.")
525
526
  .option("--limit <n>", "Maximum rows to export. Defaults to 100; hard cap is 1000.")
526
527
  .option("--json", "Print a JSON envelope.")
@@ -617,6 +618,17 @@ export function createProgram() {
617
618
  method: "POST",
618
619
  body: { table },
619
620
  }));
621
+ }))
622
+ .addCommand(new Command("move")
623
+ .description("Move a workspace table to a different project.")
624
+ .argument("<table>", "Table id or slug.")
625
+ .requiredOption("--project <project>", "Destination project id or slug.")
626
+ .option("--json", "Print a JSON envelope.")
627
+ .action(async (table, options) => {
628
+ await handleAsyncAction("tables move", options, async () => requestOxygen("/api/cli/tables/move", {
629
+ method: "POST",
630
+ body: { table, project: options.project },
631
+ }));
620
632
  }));
621
633
  tablesCommand.addCommand(new Command("webhook")
622
634
  .description("Create and manage direct table webhooks.")
@@ -721,6 +733,139 @@ export function createProgram() {
721
733
  body: { id: assetId },
722
734
  }));
723
735
  })));
736
+ program
737
+ .command("templates")
738
+ .description("Reusable prompt templates layered into AI columns at run time.")
739
+ .addCommand(new Command("list")
740
+ .description("List prompt templates in the workspace.")
741
+ .option("--kind <kind>", "Filter by ai_column_system, scoring_rubric, or other.")
742
+ .option("--include-archived", "Include archived templates.")
743
+ .option("--json", "Print a JSON envelope.")
744
+ .action(async (options) => {
745
+ await handleAsyncAction("templates list", options, async () => {
746
+ const params = new URLSearchParams();
747
+ if (readOption(options.kind))
748
+ params.set("kind", readOption(options.kind));
749
+ if (options.includeArchived)
750
+ params.set("include_archived", "true");
751
+ const qs = params.toString() ? `?${params.toString()}` : "";
752
+ return requestOxygen(`/api/cli/templates${qs}`);
753
+ });
754
+ }))
755
+ .addCommand(new Command("get")
756
+ .description("Read one prompt template by id or slug.")
757
+ .argument("<id_or_slug>", "Template UUID or slug.")
758
+ .option("--json", "Print a JSON envelope.")
759
+ .action(async (idOrSlug, options) => {
760
+ await handleAsyncAction("templates get", options, async () => requestOxygen("/api/cli/templates/get", {
761
+ method: "POST",
762
+ body: idOrSlug.includes("-") && idOrSlug.length >= 32
763
+ ? { id: idOrSlug }
764
+ : { slug: idOrSlug },
765
+ }));
766
+ }))
767
+ .addCommand(new Command("upsert")
768
+ .description("Create or update a prompt template.")
769
+ .option("--id <id>", "Existing template UUID to update. Omit to create.")
770
+ .option("--slug <slug>", "Stable slug (kebab-case).")
771
+ .option("--name <name>", "Human-readable name.")
772
+ .option("--description <text>", "Short description.")
773
+ .option("--kind <kind>", "Template kind: ai_column_system, scoring_rubric, or other.")
774
+ .option("--body <text>", "Prompt body.")
775
+ .option("--body-file <path>", "Path to a file containing the prompt body.")
776
+ .option("--json", "Print a JSON envelope.")
777
+ .action(async (options) => {
778
+ await handleAsyncAction("templates upsert", options, async () => {
779
+ const body = {};
780
+ if (readOption(options.id))
781
+ body.id = readOption(options.id);
782
+ if (readOption(options.slug))
783
+ body.slug = readOption(options.slug);
784
+ if (readOption(options.name))
785
+ body.name = readOption(options.name);
786
+ if (readOption(options.description) !== undefined)
787
+ body.description = readOption(options.description);
788
+ if (readOption(options.kind))
789
+ body.kind = readOption(options.kind);
790
+ if (readOption(options.body))
791
+ body.body = readOption(options.body);
792
+ else if (readOption(options.bodyFile)) {
793
+ const path = readOption(options.bodyFile);
794
+ const fs = await import("node:fs/promises");
795
+ body.body = await fs.readFile(path, "utf8");
796
+ }
797
+ return requestOxygen("/api/cli/templates/upsert", { method: "POST", body });
798
+ });
799
+ }))
800
+ .addCommand(new Command("archive")
801
+ .description("Archive a prompt template. Seeded templates cannot be archived.")
802
+ .argument("<id>", "Template UUID.")
803
+ .option("--json", "Print a JSON envelope.")
804
+ .action(async (id, options) => {
805
+ await handleAsyncAction("templates archive", options, async () => requestOxygen("/api/cli/templates/archive", { method: "POST", body: { id } }));
806
+ }));
807
+ program
808
+ .command("reviews")
809
+ .description("Human-in-the-loop reviews for AI-generated outreach messages.")
810
+ .addCommand(new Command("list")
811
+ .description("List message reviews.")
812
+ .option("--status <status>", "Filter by pending, accepted, rejected, or superseded.")
813
+ .option("--table <table_id>", "Filter by table id.")
814
+ .option("--limit <n>", "Max rows to return (default 50, max 200).")
815
+ .option("--json", "Print a JSON envelope.")
816
+ .action(async (options) => {
817
+ await handleAsyncAction("reviews list", options, async () => {
818
+ const params = new URLSearchParams();
819
+ if (readOption(options.status))
820
+ params.set("status", readOption(options.status));
821
+ if (readOption(options.table))
822
+ params.set("table_id", readOption(options.table));
823
+ if (readOption(options.limit))
824
+ params.set("limit", readOption(options.limit));
825
+ const qs = params.toString() ? `?${params.toString()}` : "";
826
+ return requestOxygen(`/api/cli/message-reviews${qs}`);
827
+ });
828
+ }))
829
+ .addCommand(new Command("next")
830
+ .description("Read the oldest pending review.")
831
+ .option("--table <table_id>", "Filter by table id.")
832
+ .option("--json", "Print a JSON envelope.")
833
+ .action(async (options) => {
834
+ await handleAsyncAction("reviews next", options, async () => {
835
+ const params = new URLSearchParams();
836
+ if (readOption(options.table))
837
+ params.set("table_id", readOption(options.table));
838
+ const qs = params.toString() ? `?${params.toString()}` : "";
839
+ return requestOxygen(`/api/cli/message-reviews/next${qs}`);
840
+ });
841
+ }))
842
+ .addCommand(new Command("accept")
843
+ .description("Accept a pending message review.")
844
+ .argument("<review_id>", "Message review UUID.")
845
+ .option("--json", "Print a JSON envelope.")
846
+ .action(async (reviewId, options) => {
847
+ await handleAsyncAction("reviews accept", options, async () => requestOxygen("/api/cli/message-reviews/decide", {
848
+ method: "POST",
849
+ body: { id: reviewId, decision: "accept" },
850
+ }));
851
+ }))
852
+ .addCommand(new Command("reject")
853
+ .description("Reject a pending message review with optional highlights and auto-rerun.")
854
+ .argument("<review_id>", "Message review UUID.")
855
+ .option("--highlights-json <json>", "JSON array of {start,end,comment} highlight objects.")
856
+ .option("--auto-rerun", "Trigger a single-row rerun of the column after rejecting.")
857
+ .option("--json", "Print a JSON envelope.")
858
+ .action(async (reviewId, options) => {
859
+ await handleAsyncAction("reviews reject", options, async () => {
860
+ const body = { id: reviewId, decision: "reject" };
861
+ if (readOption(options.highlightsJson)) {
862
+ body.highlights = JSON.parse(readOption(options.highlightsJson));
863
+ }
864
+ if (options.autoRerun)
865
+ body.auto_rerun = true;
866
+ return requestOxygen("/api/cli/message-reviews/decide", { method: "POST", body });
867
+ });
868
+ }));
724
869
  program
725
870
  .command("columns")
726
871
  .description("Workspace table column commands.")
@@ -825,6 +970,24 @@ export function createProgram() {
825
970
  },
826
971
  });
827
972
  });
973
+ }))
974
+ .addCommand(new Command("rerun")
975
+ .description("Re-run a single AI column cell, optionally threading a prior message review's feedback into the prompt.")
976
+ .requiredOption("--table <table>", "Table id or slug.")
977
+ .requiredOption("--column <column>", "Column id or key.")
978
+ .requiredOption("--row <row_id>", "Row UUID.")
979
+ .option("--from-review-id <review_id>", "Prior message review whose feedback to thread into the regeneration prompt.")
980
+ .option("--json", "Print a JSON envelope.")
981
+ .action(async (options) => {
982
+ await handleAsyncAction("columns rerun", options, async () => requestOxygen("/api/cli/columns/rerun", {
983
+ method: "POST",
984
+ body: {
985
+ table: options.table,
986
+ column: options.column,
987
+ row_id: options.row,
988
+ ...(readOption(options.fromReviewId) ? { from_review_id: readOption(options.fromReviewId) } : {}),
989
+ },
990
+ }));
828
991
  }))
829
992
  .addCommand(new Command("materialize")
830
993
  .description("Materialize useful fields from a JSONB result column into target columns.")
@@ -1211,6 +1374,48 @@ export function createProgram() {
1211
1374
  },
1212
1375
  }));
1213
1376
  }));
1377
+ program
1378
+ .command("companies")
1379
+ .description("Company prospecting and account enrichment workflows.")
1380
+ .addCommand(new Command("enrich")
1381
+ .description("Preview or run a company enrichment waterfall over an existing table.")
1382
+ .addCommand(new Command("preview")
1383
+ .description("Inspect missing company fields, provider routing, and credit estimates without provider calls.")
1384
+ .argument("<table>", "Table id or slug.")
1385
+ .option("--missing-fields <fields>", "Comma-separated fields to fill: domain,linkedin_url,headcount,industry,funding,technologies,hiring_signals,company_profile.")
1386
+ .option("--providers <providers>", "Comma-separated provider order pool. Defaults to blitzapi,crustdata,ai_ark,prospeo,leadmagic.")
1387
+ .option("--all", "Preview all rows.")
1388
+ .option("--limit <n>", "Preview a limited row scope.")
1389
+ .option("--row-ids <ids>", "Comma-separated row ids.")
1390
+ .option("--filter-json <json>", "Filter object or array for row selection.")
1391
+ .option("--selection-json <json>", "Raw table action selection JSON.")
1392
+ .option("--json", "Print a JSON envelope.")
1393
+ .action(async (table, options) => {
1394
+ await handleAsyncAction("companies enrich preview", options, async () => requestOxygen("/api/cli/company-enrichment/preview", {
1395
+ method: "POST",
1396
+ body: readCompaniesEnrichBody(table, options),
1397
+ }));
1398
+ }))
1399
+ .addCommand(new Command("run")
1400
+ .description("Queue a live company enrichment waterfall, or return a dry-run plan.")
1401
+ .argument("<table>", "Table id or slug.")
1402
+ .option("--missing-fields <fields>", "Comma-separated fields to fill.")
1403
+ .option("--providers <providers>", "Comma-separated provider pool.")
1404
+ .option("--mode <mode>", "dry_run or live. Defaults to live.")
1405
+ .option("--max-credits <n>", "Required credit ceiling for live runs.")
1406
+ .option("--all", "Run on all rows.")
1407
+ .option("--limit <n>", "Run on a limited row scope.")
1408
+ .option("--row-ids <ids>", "Comma-separated row ids.")
1409
+ .option("--filter-json <json>", "Filter object or array for row selection.")
1410
+ .option("--selection-json <json>", "Raw table action selection JSON.")
1411
+ .option("--force", "Re-run the waterfall audit column even when it already has a value.")
1412
+ .option("--json", "Print a JSON envelope.")
1413
+ .action(async (table, options) => {
1414
+ await handleAsyncAction("companies enrich run", options, async () => requestOxygen("/api/cli/company-enrichment/run", {
1415
+ method: "POST",
1416
+ body: readCompaniesEnrichBody(table, options),
1417
+ }));
1418
+ })));
1214
1419
  program
1215
1420
  .command("worker")
1216
1421
  .description("Background worker commands.")
@@ -1644,8 +1849,9 @@ export function createProgram() {
1644
1849
  .description("Configure provider events that can trigger workflows.")
1645
1850
  .addCommand(new Command("list")
1646
1851
  .description("List supported provider events and this org's enabled subscriptions.")
1647
- .option("--source <source>", "Filter by event source, such as hubspot.")
1852
+ .option("--source <source>", "Filter by event source, such as hubspot or composio.gmail.")
1648
1853
  .option("--event <event>", "Filter by event type, such as contact.created.")
1854
+ .option("--toolkit <id>", "Filter by toolkit / integration id, such as gmail.")
1649
1855
  .option("--json", "Print a JSON envelope.")
1650
1856
  .action(async (options) => {
1651
1857
  await handleAsyncAction("integrations events list", options, async () => {
@@ -1654,25 +1860,47 @@ export function createProgram() {
1654
1860
  query.set("source", readOption(options.source) ?? "");
1655
1861
  if (readOption(options.event))
1656
1862
  query.set("event", readOption(options.event) ?? "");
1863
+ if (readOption(options.toolkit))
1864
+ query.set("toolkit", readOption(options.toolkit) ?? "");
1657
1865
  const suffix = query.toString() ? `?${query.toString()}` : "";
1658
1866
  return requestOxygen(`/api/cli/integrations/events${suffix}`);
1659
1867
  });
1660
1868
  }))
1661
1869
  .addCommand(new Command("enable")
1662
1870
  .description("Enable a provider event for a connected integration account.")
1663
- .requiredOption("--source <source>", "Event source, such as hubspot.")
1871
+ .requiredOption("--source <source>", "Event source, such as hubspot or composio.gmail.")
1664
1872
  .requiredOption("--event <event>", "Event type, such as contact.created.")
1665
1873
  .option("--connection-id <connection_id>", "Specific integration connection id. Defaults to the active default connection.")
1874
+ .option("--trigger-config <json>", "JSON object passed to the provider when registering the trigger (Composio triggers only).")
1666
1875
  .option("--json", "Print a JSON envelope.")
1667
1876
  .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
- }));
1877
+ await handleAsyncAction("integrations events enable", options, async () => {
1878
+ const triggerConfigRaw = readOption(options.triggerConfig);
1879
+ let triggerConfig;
1880
+ if (triggerConfigRaw) {
1881
+ try {
1882
+ const parsed = JSON.parse(triggerConfigRaw);
1883
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1884
+ throw new Error("--trigger-config must be a JSON object.");
1885
+ }
1886
+ triggerConfig = parsed;
1887
+ }
1888
+ catch (error) {
1889
+ throw new Error(error instanceof Error
1890
+ ? `Invalid --trigger-config: ${error.message}`
1891
+ : "Invalid --trigger-config");
1892
+ }
1893
+ }
1894
+ return requestOxygen("/api/cli/integrations/events/enable", {
1895
+ method: "POST",
1896
+ body: {
1897
+ source: readOption(options.source),
1898
+ event: readOption(options.event),
1899
+ ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
1900
+ ...(triggerConfig ? { trigger_config: triggerConfig } : {}),
1901
+ },
1902
+ });
1903
+ });
1676
1904
  }))
1677
1905
  .addCommand(new Command("disable")
1678
1906
  .description("Disable a provider event for a connected integration account.")
@@ -2183,6 +2411,7 @@ function referencesRecipeSdk(source) {
2183
2411
  // Escape Next static analysis (the CLI is bundled by tsc, but mirror the
2184
2412
  // worker's escape so both load identically).
2185
2413
  const dynamicRecipeImport = new Function("specifier", "return import(specifier);");
2414
+ // skipcq: JS-R1003
2186
2415
  async function importRecipeModule(specifier) {
2187
2416
  try {
2188
2417
  return await dynamicRecipeImport(specifier);
@@ -2384,6 +2613,55 @@ function readTableRunSelection(options) {
2384
2613
  exitCode: 1,
2385
2614
  });
2386
2615
  }
2616
+ function readCompaniesEnrichBody(table, options) {
2617
+ const body = { table };
2618
+ const fields = readCsvOption(options.missingFields);
2619
+ const providers = readCsvOption(options.providers);
2620
+ const selection = readCompaniesEnrichSelection(options);
2621
+ const mode = readOption(options.mode);
2622
+ const maxCredits = readPositiveNumber(options.maxCredits);
2623
+ if (fields.length > 0)
2624
+ body.missing_fields = fields;
2625
+ if (providers.length > 0)
2626
+ body.providers = providers;
2627
+ if (selection)
2628
+ body.selection = selection;
2629
+ if (mode)
2630
+ body.mode = mode;
2631
+ if (maxCredits !== undefined)
2632
+ body.max_credits = maxCredits;
2633
+ if (options.force !== undefined)
2634
+ body.force = Boolean(options.force);
2635
+ return body;
2636
+ }
2637
+ function readCompaniesEnrichSelection(options) {
2638
+ const explicitSelection = readSelectionJsonOption(options.selectionJson);
2639
+ const hasAll = Boolean(options.all);
2640
+ const limit = readPositiveInt(options.limit);
2641
+ const rowIds = readCsvOption(options.rowIds);
2642
+ const filterSelection = readFilterSelectionOption(options.filterJson);
2643
+ const selectedModes = [
2644
+ Boolean(explicitSelection),
2645
+ hasAll,
2646
+ Boolean(limit),
2647
+ rowIds.length > 0,
2648
+ Boolean(filterSelection),
2649
+ ].filter(Boolean).length;
2650
+ if (selectedModes > 1) {
2651
+ throw new OxygenError("invalid_company_enrichment", "Pass only one row scope option.", { exitCode: 1 });
2652
+ }
2653
+ if (explicitSelection)
2654
+ return explicitSelection;
2655
+ if (hasAll)
2656
+ return { mode: "all" };
2657
+ if (limit)
2658
+ return { mode: "limit", limit };
2659
+ if (rowIds.length > 0)
2660
+ return { mode: "row_ids", row_ids: rowIds };
2661
+ if (filterSelection)
2662
+ return filterSelection;
2663
+ return undefined;
2664
+ }
2387
2665
  function readFilterSelectionOption(value) {
2388
2666
  const filters = readFilterJsonOption(value);
2389
2667
  return filters ? { mode: "filter", filters } : undefined;
@@ -2599,11 +2877,13 @@ async function prepareImportTarget(table, options, parsedRows) {
2599
2877
  exitCode: 1,
2600
2878
  });
2601
2879
  }
2880
+ const createdWebUrl = readRecordString(created, "web_url")
2881
+ ?? readRecordString(created, "deepLink");
2602
2882
  return {
2603
2883
  tableRef: createdSlug,
2604
2884
  rows: normalized.rows,
2605
2885
  createdTable: created,
2606
- tableWebUrl: tableWebUrl(createdSlug),
2886
+ tableWebUrl: createdWebUrl ?? tableWebUrl(createdSlug),
2607
2887
  upsertKey: normalizeCreatedTableUpsertKey(options.upsertKey, normalized.keyBySource),
2608
2888
  };
2609
2889
  }
@@ -2786,16 +3066,17 @@ async function exportRows(table, options) {
2786
3066
  ...(limit ? { limit } : {}),
2787
3067
  },
2788
3068
  });
2789
- const content = formatRows(result.rows, format, result.columns);
3069
+ const formatted = formatRows(result.rows, format, result.columns);
2790
3070
  if (options.output) {
2791
- writeFileSync(options.output, content);
3071
+ writeFileSync(options.output, formatted.content);
2792
3072
  }
2793
3073
  return {
2794
3074
  table: result.table ?? null,
2795
3075
  format,
2796
3076
  rowCount: result.rows.length,
2797
3077
  output: options.output ?? null,
2798
- ...(options.output ? {} : { content }),
3078
+ ...(options.output ? {} : { content: formatted.content }),
3079
+ ...(formatted.rescuedCount > 0 ? { rescuedNumericCells: formatted.rescuedCount } : {}),
2799
3080
  };
2800
3081
  }
2801
3082
  async function readRowsFile(path, format, sheet) {
@@ -2808,18 +3089,26 @@ function normalizeCreatedTableUpsertKey(value, keyBySource) {
2808
3089
  }
2809
3090
  function normalizeExportRowsFormat(value) {
2810
3091
  const normalized = value?.trim().toLowerCase() || "json";
2811
- if (normalized === "json" || normalized === "jsonl" || normalized === "csv")
3092
+ if (normalized === "json"
3093
+ || normalized === "jsonl"
3094
+ || normalized === "csv"
3095
+ || normalized === "table")
2812
3096
  return normalized;
2813
- throw new OxygenError("invalid_format", "Export format must be json, jsonl, or csv.", {
3097
+ throw new OxygenError("invalid_format", "Export format must be json, jsonl, csv, or table.", {
2814
3098
  details: { format: value },
2815
3099
  exitCode: 1,
2816
3100
  });
2817
3101
  }
2818
3102
  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`;
3103
+ if (format === "json") {
3104
+ return { content: `${JSON.stringify(rows, null, 2)}\n`, rescuedCount: 0 };
3105
+ }
3106
+ if (format === "jsonl") {
3107
+ return {
3108
+ content: `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`,
3109
+ rescuedCount: 0,
3110
+ };
3111
+ } // skipcq: JS-0246
2823
3112
  const keys = [
2824
3113
  "_row_id",
2825
3114
  "_created_at",
@@ -2827,10 +3116,57 @@ function formatRows(rows, format, columns) {
2827
3116
  ...(columns?.map((column) => column.key) ?? []),
2828
3117
  ...rows.flatMap((row) => Object.keys(row)),
2829
3118
  ].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";
3119
+ if (format === "csv") {
3120
+ return {
3121
+ content: [
3122
+ keys.map(escapeCsvField).join(","),
3123
+ ...rows.map((row) => keys.map((key) => escapeCsvField(row[key])).join(",")),
3124
+ ].join("\n") + "\n",
3125
+ rescuedCount: 0,
3126
+ };
3127
+ }
3128
+ // "table": render with type-aware formatting and a Markdown-style frame.
3129
+ const columnByKey = new Map(columns?.map((c) => [c.key, c]) ?? []);
3130
+ let rescuedCount = 0;
3131
+ const headers = keys.map((key) => columnByKey.get(key)?.label ?? key);
3132
+ const formattedRows = rows.map((row) => keys.map((key) => {
3133
+ const column = columnByKey.get(key) ?? null;
3134
+ const formatted = formatCellForDisplay(row[key], column, {
3135
+ surface: "cli",
3136
+ onRescued: () => {
3137
+ rescuedCount += 1;
3138
+ },
3139
+ });
3140
+ // Pipe and any line-break char are the row/cell delimiters of the
3141
+ // Markdown frame — raw values containing them would corrupt the layout.
3142
+ // Match on the line-break class (not just `\r?\n`) so a standalone `\r`
3143
+ // doesn't slip through and split the row visually.
3144
+ return formatted.replace(/[\r\n]+/g, " ↵ ").replace(/\|/g, "\\|");
3145
+ }));
3146
+ const widths = headers.map((header, columnIndex) => {
3147
+ let max = header.length;
3148
+ for (const row of formattedRows) {
3149
+ const cell = row[columnIndex] ?? "";
3150
+ if (cell.length > max)
3151
+ max = cell.length;
3152
+ }
3153
+ return Math.min(max, 60);
3154
+ });
3155
+ const renderRow = (cells) => "| " + cells.map((cell, i) => clipCell(cell, widths[i]).padEnd(widths[i])).join(" | ") + " |";
3156
+ const separator = "|" + widths.map((w) => "-".repeat(w + 2)).join("|") + "|";
3157
+ const lines = [renderRow(headers), separator, ...formattedRows.map(renderRow)];
3158
+ if (rescuedCount > 0) {
3159
+ lines.push("");
3160
+ 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.`);
3161
+ }
3162
+ return { content: lines.join("\n") + "\n", rescuedCount };
3163
+ }
3164
+ function clipCell(value, width) {
3165
+ if (value.length <= width)
3166
+ return value;
3167
+ if (width <= 1)
3168
+ return value.slice(0, width);
3169
+ return value.slice(0, width - 1) + "…";
2834
3170
  }
2835
3171
  function escapeCsvField(value) {
2836
3172
  const text = value === null || value === undefined
@@ -3030,64 +3366,6 @@ async function handleUpdateAction(options) {
3030
3366
  process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
3031
3367
  }
3032
3368
  }
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
3369
  function buildApiKeyCreateBody(options) {
3092
3370
  const body = {};
3093
3371
  const name = readOption(options.name);
@@ -3325,6 +3603,7 @@ function formatLoginSuccess(identity, credentials, profile) {
3325
3603
  .update(`oxygen-cli:${credentials.token}`)
3326
3604
  .digest("hex");
3327
3605
  const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
3606
+ // skipcq: JS-0820 — not a React component; rule misfire on array of tuples
3328
3607
  const rows = [
3329
3608
  ["Account", email],
3330
3609
  ["Organization", org],
@@ -3401,7 +3680,7 @@ function formatProfileUseSuccess(profile) {
3401
3680
  ` ${c.dim("Fingerprint")} ${profile.token_fingerprint}`,
3402
3681
  "",
3403
3682
  ].join("\n");
3404
- }
3683
+ } // skipcq: JS-C1002
3405
3684
  function formatLogoutSuccess(result) {
3406
3685
  const c = ansi(output.isTTY === true && !process.env.NO_COLOR);
3407
3686
  const removed = result.removedProfile
@@ -3412,7 +3691,7 @@ function formatLogoutSuccess(result) {
3412
3691
  `${c.green("[OK]")} ${c.bold("CLI logged out")}`,
3413
3692
  "",
3414
3693
  ` ${c.dim("Credentials")} ${removed}`,
3415
- ` ${c.dim("Profiles left")} ${String(result.remainingProfiles)}`,
3694
+ ` ${c.dim("Profiles left")} ${String(result.remainingProfiles)}`, // skipcq: JS-C1002
3416
3695
  "",
3417
3696
  ].join("\n");
3418
3697
  }
@@ -3427,7 +3706,7 @@ function formatUpdateSuccess(result) {
3427
3706
  ` ${c.dim("Command")} ${result.command}`,
3428
3707
  "",
3429
3708
  ].join("\n");
3430
- }
3709
+ } // skipcq: JS-C1002
3431
3710
  function renderBox(lines) {
3432
3711
  const width = Math.max(...lines.map(visibleLength), 0);
3433
3712
  const border = `+${"-".repeat(width + 2)}+`;
@@ -3435,6 +3714,7 @@ function renderBox(lines) {
3435
3714
  return [border, ...body, border].join("\n");
3436
3715
  }
3437
3716
  function visibleLength(value) {
3717
+ // skipcq: JS-0004 — ESC (\x1b) is the ANSI CSI introducer; required to strip color codes
3438
3718
  return value.replace(/\x1b\[[0-9;]*m/g, "").length;
3439
3719
  }
3440
3720
  function ansi(enabled) {
@@ -3442,7 +3722,7 @@ function ansi(enabled) {
3442
3722
  ? (text) => `\x1b[${open}m${text}\x1b[${close}m`
3443
3723
  : (text) => text;
3444
3724
  return {
3445
- bold: wrap(1, 22),
3725
+ bold: wrap(1, 22), // skipcq: JS-0117 // skipcq: JS-W1035
3446
3726
  dim: wrap(2, 22),
3447
3727
  green: wrap(32, 39),
3448
3728
  };
@@ -387,7 +387,7 @@ async function readLocalHttpResponseBody(response) {
387
387
  if (!text)
388
388
  return null;
389
389
  const contentType = response.headers.get("content-type") ?? "";
390
- const looksJson = contentType.toLowerCase().includes("json") || /^[\s\n\r]*[\[{]/.test(text);
390
+ const looksJson = contentType.toLowerCase().includes("json") || /^[\s\n\r]*[\[{]/.test(text); // skipcq: JS-0097
391
391
  if (!looksJson)
392
392
  return text;
393
393
  try {