@oxygen-agent/cli 1.782.1 → 1.799.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.782.1
37
+ Version: 1.799.1
package/dist/index.js CHANGED
@@ -6998,7 +6998,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6998
6998
  .option("--label <label>", "Display label for the new column. Required unless --prompt-key supplies a default title.")
6999
6999
  .option("--key <key>", "Optional stable column key. Defaults to a normalized label.")
7000
7000
  .option("--data-type <type>", "Column data type: text, numeric, boolean, jsonb, or timestamptz.")
7001
- .option("--kind <kind>", "Column kind: manual, research, ai, formula, enrichment, tool, bind, or lookup. Defaults to manual. Use research for anything you would look up on the web.")
7001
+ .option("--kind <kind>", "Column kind: manual, research, ai, formula, enrichment, tool, bind, or lookup. Defaults to manual. Use research for anything you would look up on the web. `lookup` reads a value out of another table; to LINK two tables row-to-row use `oxygen tables relate` instead \u2014 relation columns are two-sided and cannot be added here.")
7002
7002
  .option("--semantic-type <type>", "Optional semantic type such as company_domain.")
7003
7003
  .option("--definition-json <json>", "Optional JSON object with column definition metadata.")
7004
7004
  .option("--prompt <text-or-file>", "AI or research column prompt, or a path to a prompt file — a value that resolves to a readable file is read as one, matching --prompt everywhere else in this CLI. On its own it sets kind=ai and picks the data type (text, or jsonb with an output schema); pair it with --kind research to search the web per row instead. Reference other columns inline as {{column_key}} — no --input-mapping needed; unknown keys are rejected here instead of failing per row. Merges into --definition-json (the escape hatch for everything else); a `prompt` in both is an error.")
@@ -14722,13 +14722,21 @@ Run completion:
14722
14722
  .description("List workflow automations.")
14723
14723
  .addOption(new Option("--include-archived", "Deprecated no-op retained for compatibility.").hideHelp())
14724
14724
  .option("--tag <tag>", "Only workflows carrying this workspace tag (see `oxygen tags list`).")
14725
+ .option("--node-testable", "Only canonical graph workflows, the ones `workflows call --node` can scope a test to. Legacy recipes and v1 step lists own their own execution order and are excluded.")
14726
+ .option("--search <text>", "Match on name, slug, trigger event, or an integration the workflow uses — so \"meeting\" or \"hubspot\" finds it without knowing what someone named it.")
14725
14727
  .option("--json", "Print a JSON envelope.")
14728
+ .addHelpText("after", "\nEach row carries `format` (graph, recipe or steps) and `nodeTestable`, so you can tell which workflows support single-step testing without opening them.\n")
14726
14729
  .action(async (options) => {
14727
14730
  await handleAsyncAction("workflows list", options, async () => {
14728
14731
  const params = new URLSearchParams();
14729
14732
  const tag = readOption(options.tag);
14730
14733
  if (tag)
14731
14734
  params.set("tag", tag);
14735
+ if (options.nodeTestable)
14736
+ params.set("node_testable", "true");
14737
+ const search = readOption(options.search);
14738
+ if (search)
14739
+ params.set("search", search);
14732
14740
  const qs = params.toString() ? `?${params.toString()}` : "";
14733
14741
  const data = await requestOxygen(`/api/cli/workflows${qs}`);
14734
14742
  if (!options.json)
@@ -14785,41 +14793,52 @@ Run completion:
14785
14793
  }), options));
14786
14794
  }))
14787
14795
  .addCommand(new Command("call")
14788
- .description("Enqueue a workflow run asynchronously and directly without simulating its trigger. Follow the returned run_id with `oxygen workflows tail <run_id>`.")
14796
+ .description("Run a workflow — or preview it first. `--preview` shows every step that leaves Oxygen, the accounts it would use, and what one run costs, without running anything. Enqueues asynchronously without simulating the trigger; follow the returned run_id with `oxygen workflows tail <run_id>`.")
14789
14797
  .argument("[workflow]", "Workflow id, slug, or name.")
14790
14798
  .option("--workflow <workflow>", "Workflow id, slug, or name.")
14791
14799
  .option("--workflow-id <workflow_id>", "Workflow id or slug.")
14792
14800
  .option("--workflow-name <workflow_name>", "Workflow name.")
14793
14801
  .option("--input-json <json>", "Workflow input object. Defaults to {}.")
14794
- .requiredOption("--mode <mode>", "Execution mode. smoke-test/dry-run: 0 credits, no paid provider calls or external writes; internal reads still execute. live: may spend/write and requires approval + cap.")
14802
+ .option("--mode <mode>", "Execution mode. smoke-test/dry-run: 0 credits, no paid provider calls or external writes; internal reads still execute. live: may spend/write and requires approval + cap. Not needed with --preview, which runs nothing.")
14795
14803
  .option("--idempotency-key <key>", "Optional idempotency key.")
14796
14804
  .option("--max-credits <n>", "Required credit ceiling for live calls.")
14797
14805
  .option("--approved", "Required for live calls after inspecting a dry run.")
14798
14806
  .option("--revision <n>", "Run a specific saved version instead of the live one. Dry-run and smoke-test only: publish a version to run it live.")
14799
14807
  .option("--node <node_id>", "Test one node: runs that node plus only the predecessors it needs, from the same saved graph. Dry-run and smoke-test only.")
14800
- .option("--preview", "Show what a live run of this exact version would do — every step that leaves Oxygen, the accounts it would use, and the billable-step floor — without running anything.")
14808
+ .option("--preview", "Show what a live run of this exact version would do — every step that leaves Oxygen, the accounts it would use, and the minimum number of billable steps and credits it would cost — without running anything. AI and enrichment steps bill per row and can cost more than that floor. Needs no --mode: it always describes a live run.")
14801
14809
  .option("--include-bundle", "Include durable recipe bundles in JSON output.")
14802
14810
  .option("--json", "Print a JSON envelope.")
14803
14811
  .addHelpText("after", "\nSafety: smoke_test and dry_run share one boundary: 0 credits, no paid provider calls, no external writes. Oxygen internal reads use current workspace data, and oxygen.http_json_request may make a real outbound GET. Every mode creates an inspectable Workflow run record.\n\n--node scopes a test to one step. Oxygen plans the slice on the server from the exact saved version and records it on the run, so the run shows precisely which nodes were allowed to execute; you never submit a node list. Selecting the trigger, a disabled node, or a node unreachable from the trigger is refused with the reason. Only canonical graph workflows support it.\n")
14804
14812
  .action(async (workflowArg, options) => {
14805
14813
  const maxCredits = readPositiveNumber(options.maxCredits);
14806
14814
  const revisionVersion = readPositiveNumber(options.revision);
14807
- await handleAsyncAction("workflows call", options, async () => prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/call", {
14808
- method: "POST",
14809
- body: {
14810
- workflow: readOption(workflowArg) ?? readOption(options.workflow),
14811
- ...(readOption(options.workflowId) ? { workflow_id: readOption(options.workflowId) } : {}),
14812
- ...(readOption(options.workflowName) ? { workflow_name: readOption(options.workflowName) } : {}),
14813
- input: options.inputJson ? parseJsonObject(options.inputJson) : {},
14814
- ...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
14815
- ...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
14816
- ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
14817
- ...(options.approved ? { approved: true } : {}),
14818
- ...(revisionVersion !== undefined ? { revision_version: revisionVersion } : {}),
14819
- ...(readOption(options.node) ? { test_node_id: readOption(options.node) } : {}),
14820
- ...(options.preview ? { preview: true } : {}),
14821
- },
14822
- }), options));
14815
+ await handleAsyncAction("workflows call", options, async () => {
14816
+ // --mode stopped being a commander requiredOption so that --preview,
14817
+ // which executes nothing, no longer forces the caller to pick a mode
14818
+ // the server then discards. A real call still needs one. Thrown
14819
+ // INSIDE handleAsyncAction so it prints the CLI's error envelope and
14820
+ // exits 2 like any other usage error — thrown outside, it escaped as
14821
+ // an unhandled rejection and showed the user a Node stack trace.
14822
+ if (!options.preview && !readOption(options.mode)) {
14823
+ throw new OxygenError("invalid_request", "--mode is required. Use smoke-test, dry-run, or live. To see what a live run would do without running it, use --preview — it runs nothing and needs no mode.", { exitCode: 2 });
14824
+ }
14825
+ return prepareWorkflowCliOutput(await requestOxygen("/api/cli/workflows/call", {
14826
+ method: "POST",
14827
+ body: {
14828
+ workflow: readOption(workflowArg) ?? readOption(options.workflow),
14829
+ ...(readOption(options.workflowId) ? { workflow_id: readOption(options.workflowId) } : {}),
14830
+ ...(readOption(options.workflowName) ? { workflow_name: readOption(options.workflowName) } : {}),
14831
+ input: options.inputJson ? parseJsonObject(options.inputJson) : {},
14832
+ ...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
14833
+ ...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
14834
+ ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
14835
+ ...(options.approved ? { approved: true } : {}),
14836
+ ...(revisionVersion !== undefined ? { revision_version: revisionVersion } : {}),
14837
+ ...(readOption(options.node) ? { test_node_id: readOption(options.node) } : {}),
14838
+ ...(options.preview ? { preview: true } : {}),
14839
+ },
14840
+ }), options);
14841
+ });
14823
14842
  }))
14824
14843
  .addCommand(new Command("webhooks")
14825
14844
  .description("Workflow webhook trigger utilities.")
@@ -15018,9 +15037,12 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
15018
15037
  .option("--workflow <workflow>", "Workflow id, slug, or name.")
15019
15038
  .option("--workflow-id <workflow_id>", "Workflow id or slug.")
15020
15039
  .option("--status <status>", "Filter by queued, running, waiting, awaiting_approval, completed, failed, canceling, or canceled.")
15040
+ .option("--mode <mode>", "Filter by execution mode: smoke_test, dry_run, or live.")
15041
+ .option("--cursor <cursor>", "Continue from a previous page. Pass the nextCursor the last response returned.")
15021
15042
  .option("--limit <n>", "Maximum runs to return. Defaults to 50.")
15022
15043
  .option("--include-bundle", "Include durable recipe bundles in JSON output.")
15023
15044
  .option("--json", "Print a JSON envelope.")
15045
+ .addHelpText("after", "\nPaging is anchored to the exact last run of the previous page, so runs arriving while you page are never skipped or repeated. A null nextCursor means the history ended.\n")
15024
15046
  .action(async (options) => {
15025
15047
  await handleAsyncAction("workflows runs", options, async () => {
15026
15048
  const query = new URLSearchParams();
@@ -15029,6 +15051,10 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
15029
15051
  query.set("workflow", workflow);
15030
15052
  if (readOption(options.status))
15031
15053
  query.set("status", readOption(options.status) ?? "");
15054
+ if (readOption(options.mode))
15055
+ query.set("mode", readOption(options.mode) ?? "");
15056
+ if (readOption(options.cursor))
15057
+ query.set("cursor", readOption(options.cursor) ?? "");
15032
15058
  const limit = readPositiveInt(options.limit);
15033
15059
  if (limit)
15034
15060
  query.set("limit", String(limit));
@@ -78,6 +78,24 @@ export declare function readWorkflowStepFailure(error: unknown): WorkflowStepFai
78
78
  */
79
79
  export declare const AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE = "automation_actions_exceeded";
80
80
  export declare function isAutomationUsageQuotaError(error: unknown): boolean;
81
+ /**
82
+ * The credit-era sibling of the code above, and the one most callers actually want.
83
+ *
84
+ * `automation_actions_exceeded` can only be thrown when the plan carries a non-null
85
+ * `monthlyAutomationActions`. Pricing Model 2.0 ([v1.330.0]) set that field to null on
86
+ * every base plan, so on current plans the admission gate in captureAutomationActions()
87
+ * is unreachable and assertAutomationActionsAvailable() refuses with `insufficient_credits`
88
+ * instead — actions bill AUTOMATION_ACTION_CREDITS each out of the one credit pool.
89
+ *
90
+ * Both codes mean the same thing to a caller: "this automation work was REFUSED admission,
91
+ * the customer must feel it, and the run must stop cleanly." Anything else is a metering
92
+ * write that failed and must be stepped over. Matching only the quota code is how a
93
+ * credit-exhausted cron ends up rethrowing out of the scheduler's enqueue loop instead of
94
+ * taking the graceful pause path — abandoning the lease and every remaining due trigger
95
+ * for that tenant's sweep.
96
+ */
97
+ export declare const AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE = "insufficient_credits";
98
+ export declare function isAutomationAdmissionRefusal(error: unknown): boolean;
81
99
  /**
82
100
  * Node network primitives bury the actionable failure in `error.cause`: undici's
83
101
  * fetch rejects with a bare TypeError "fetch failed" and puts the real
@@ -215,6 +215,30 @@ export function isAutomationUsageQuotaError(error) {
215
215
  && typeof error === "object"
216
216
  && error.code === AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE;
217
217
  }
218
+ /**
219
+ * The credit-era sibling of the code above, and the one most callers actually want.
220
+ *
221
+ * `automation_actions_exceeded` can only be thrown when the plan carries a non-null
222
+ * `monthlyAutomationActions`. Pricing Model 2.0 ([v1.330.0]) set that field to null on
223
+ * every base plan, so on current plans the admission gate in captureAutomationActions()
224
+ * is unreachable and assertAutomationActionsAvailable() refuses with `insufficient_credits`
225
+ * instead — actions bill AUTOMATION_ACTION_CREDITS each out of the one credit pool.
226
+ *
227
+ * Both codes mean the same thing to a caller: "this automation work was REFUSED admission,
228
+ * the customer must feel it, and the run must stop cleanly." Anything else is a metering
229
+ * write that failed and must be stepped over. Matching only the quota code is how a
230
+ * credit-exhausted cron ends up rethrowing out of the scheduler's enqueue loop instead of
231
+ * taking the graceful pause path — abandoning the lease and every remaining due trigger
232
+ * for that tenant's sweep.
233
+ */
234
+ export const AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE = "insufficient_credits";
235
+ export function isAutomationAdmissionRefusal(error) {
236
+ if (!error || typeof error !== "object")
237
+ return false;
238
+ const code = error.code;
239
+ return code === AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE
240
+ || code === AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE;
241
+ }
218
242
  const MAX_CAUSE_DEPTH = 4;
219
243
  const MAX_CAUSE_MESSAGE_LENGTH = 200;
220
244
  export function errorCauseChain(error, maxDepth = MAX_CAUSE_DEPTH) {
@@ -1,4 +1,4 @@
1
- export declare const OXYGEN_VERSION = "1.782.1";
1
+ export declare const OXYGEN_VERSION = "1.799.1";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
3
3
  export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
4
4
  export declare const SUPPORT_AGENT_REPLY_MINIMUM_CLI_VERSION = "1.747.0";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.782.1";
1
+ export const OXYGEN_VERSION = "1.799.1";
2
2
  // The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
3
3
  // operational route. Raising it hard-rejects every older CLI from the entire
4
4
  // product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
@@ -14,6 +14,9 @@ export type CronCadenceAssessment = {
14
14
  floorActionsPer30Days: number;
15
15
  includedActions: number;
16
16
  floorShareOfIncluded: number;
17
+ basis: "plan_action_quota" | "plan_monthly_credits";
18
+ includedCredits: number | null;
19
+ floorCreditsPer30Days: number | null;
17
20
  minimumViableIntervalMinutes: number | null;
18
21
  verdict: "fits" | "impossible";
19
22
  };
@@ -22,6 +25,8 @@ export declare function assessCronCadenceViability(input: {
22
25
  cron: string | null | undefined;
23
26
  includedActions: number | null;
24
27
  overageEnabled: boolean;
28
+ includedCredits?: number | null;
29
+ creditsPerAction?: number | null;
25
30
  }): CronCadenceAssessment | null;
26
31
  export type CronAggressiveness = {
27
32
  level: "aggressive" | "very_aggressive";
@@ -287,26 +287,53 @@ const MINUTES_PER_30_DAYS = 43_200;
287
287
  // enabled (the org has opted into paying past the cap, so a cadence that
288
288
  // overruns it bills instead of failing).
289
289
  export function assessCronCadenceViability(input) {
290
- if (!input.cron || input.includedActions === null || input.overageEnabled)
290
+ if (!input.cron || input.overageEnabled)
291
291
  return null;
292
- if (!Number.isFinite(input.includedActions) || input.includedActions <= 0)
292
+ const creditsPerAction = typeof input.creditsPerAction === "number"
293
+ && Number.isFinite(input.creditsPerAction)
294
+ && input.creditsPerAction > 0
295
+ ? input.creditsPerAction
296
+ : null;
297
+ const includedCredits = typeof input.includedCredits === "number"
298
+ && Number.isFinite(input.includedCredits)
299
+ && input.includedCredits > 0
300
+ ? input.includedCredits
301
+ : null;
302
+ // Prefer a real action quota when the plan still has one; otherwise derive the
303
+ // equivalent action budget from monthly credits.
304
+ const basis = input.includedActions === null
305
+ ? "plan_monthly_credits"
306
+ : "plan_action_quota";
307
+ const includedActions = basis === "plan_action_quota"
308
+ ? input.includedActions
309
+ : (includedCredits !== null && creditsPerAction !== null
310
+ ? Math.floor(includedCredits / creditsPerAction)
311
+ : null);
312
+ if (includedActions === null)
313
+ return null;
314
+ if (!Number.isFinite(includedActions) || includedActions <= 0)
293
315
  return null;
294
316
  const scheduledRunsPer30Days = estimateCronRunsPer30Days(input.cron);
295
317
  if (scheduledRunsPer30Days === null)
296
318
  return null;
297
319
  const actionsPerRunFloor = estimateWorkflowRunAutomationActionsFloor(input.manifest);
298
320
  const floorActionsPer30Days = scheduledRunsPer30Days * actionsPerRunFloor;
299
- const runsAllowedPer30Days = Math.floor(input.includedActions / actionsPerRunFloor);
321
+ const runsAllowedPer30Days = Math.floor(includedActions / actionsPerRunFloor);
300
322
  return {
301
323
  scheduledRunsPer30Days,
302
324
  actionsPerRunFloor,
303
325
  floorActionsPer30Days,
304
- includedActions: input.includedActions,
305
- floorShareOfIncluded: floorActionsPer30Days / input.includedActions,
326
+ includedActions,
327
+ floorShareOfIncluded: floorActionsPer30Days / includedActions,
328
+ basis,
329
+ includedCredits: basis === "plan_monthly_credits" ? includedCredits : null,
330
+ floorCreditsPer30Days: basis === "plan_monthly_credits" && creditsPerAction !== null
331
+ ? Math.round(floorActionsPer30Days * creditsPerAction * 1000) / 1000
332
+ : null,
306
333
  minimumViableIntervalMinutes: runsAllowedPer30Days > 0
307
334
  ? Math.ceil(MINUTES_PER_30_DAYS / runsAllowedPer30Days)
308
335
  : null,
309
- verdict: floorActionsPer30Days > input.includedActions ? "impossible" : "fits",
336
+ verdict: floorActionsPer30Days > includedActions ? "impossible" : "fits",
310
337
  };
311
338
  }
312
339
  // Warn (never block) below this cadence; the billing gate above stays the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.782.1",
3
+ "version": "1.799.1",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",