@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/README.md +13 -1
- package/dist/credentials.d.ts +25 -0
- package/dist/credentials.js +214 -45
- package/dist/index.js +1107 -181
- package/dist/local-custom-http-column.js +1 -1
- package/dist/skills.d.ts +41 -0
- package/dist/skills.js +325 -0
- package/dist/update.d.ts +34 -0
- package/dist/update.js +123 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/billing.js +2 -1
- package/node_modules/@oxygen/shared/dist/cell-format.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/cell-format.js +277 -0
- package/node_modules/@oxygen/shared/dist/column-types.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/column-types.js +3 -2
- package/node_modules/@oxygen/shared/dist/file-import.js +1 -1
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/index.js +2 -0
- package/node_modules/@oxygen/shared/dist/log.js +1 -1
- package/node_modules/@oxygen/shared/dist/provider-request-outcomes.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/provider-request-outcomes.js +5 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/workflows/dist/index.d.ts +145 -143
- package/node_modules/@oxygen/workflows/dist/index.js +66 -29
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
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 () =>
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
-
|
|
2822
|
-
|
|
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
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
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
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
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
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
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
|
|
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",
|
|
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
|
|
3378
|
-
|
|
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(
|
|
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
|
-
]
|
|
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
|
|
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
|
-
|
|
4213
|
+
const lines = [
|
|
3396
4214
|
"",
|
|
3397
4215
|
`${c.green("[OK]")} ${c.bold("CLI profile selected")}`,
|
|
3398
4216
|
"",
|
|
3399
|
-
` ${c.dim("Profile")}
|
|
3400
|
-
` ${c.dim("
|
|
3401
|
-
` ${c.dim("
|
|
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);
|