@oxygen-agent/cli 1.109.11 → 1.123.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 and support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.109.11
37
+ Version: 1.123.1
@@ -1,6 +1,7 @@
1
1
  import { OXYGEN_VERSION, OxygenError } from "@oxygen/shared";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { defaultApiUrl, loadCredentials } from "./credentials.js";
4
+ import { resolveCliUpdateGuidance } from "./runtime.js";
4
5
  const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
5
6
  const CLI_COMPATIBILITY_CHECK_TIMEOUT_MS = 5_000;
6
7
  const PROD_API_HOSTNAME = "oxygen-agent.com";
@@ -77,13 +78,13 @@ path, options = {}) {
77
78
  }
78
79
  const envelope = await readEnvelope(response);
79
80
  const compatibility = readEnvelopeCompatibility(envelope);
80
- assertCliMeetsMinimumApiVersion(compatibility, options);
81
- warnIfCliIsOlderThanApi(compatibility.version);
81
+ assertCliMeetsMinimumApiVersion(compatibility, options, apiUrl);
82
+ warnIfCliIsOlderThanApi(compatibility.version, apiUrl);
82
83
  if (!response.ok || !envelope.ok) {
83
84
  const failure = envelope;
84
85
  const responseTraceId = response.headers.get("x-oxygen-trace-id") ?? traceId;
85
86
  throw new OxygenError(failure.error.code, failure.error.message, {
86
- details: withTraceDetails(failure.error.details, responseTraceId, compatibility),
87
+ details: withTraceDetails(failure.error.details, responseTraceId, compatibility, apiUrl),
87
88
  exitCode: response.status >= 500 ? 1 : 1,
88
89
  });
89
90
  }
@@ -97,8 +98,8 @@ export async function ensureFreshCliForApiUrl(apiUrl, options = {}) {
97
98
  if (!compatibility) {
98
99
  return;
99
100
  }
100
- assertCliMeetsMinimumApiVersion(compatibility, {});
101
- warnIfCliIsOlderThanApi(compatibility.version);
101
+ assertCliMeetsMinimumApiVersion(compatibility, {}, apiUrl);
102
+ warnIfCliIsOlderThanApi(compatibility.version, apiUrl);
102
103
  cliCompatibilityCheckKeys.add(checkKey);
103
104
  }
104
105
  function resolveSelectedOrganization(credentials, explicit) {
@@ -145,14 +146,18 @@ async function readHealthCompatibility(apiUrl, fetchImpl) {
145
146
  }
146
147
  }
147
148
  const staleCliWarningVersions = new Set();
148
- function warnIfCliIsOlderThanApi(serverVersion) {
149
- if (!serverVersion || staleCliWarningVersions.has(serverVersion))
149
+ function warnIfCliIsOlderThanApi(serverVersion, apiUrl) {
150
+ if (!serverVersion)
150
151
  return;
151
152
  if (!isVersionGreater(serverVersion, OXYGEN_VERSION))
152
153
  return;
153
- staleCliWarningVersions.add(serverVersion);
154
- process.stderr.write(`[oxygen] CLI version ${OXYGEN_VERSION} is older than Oxygen API version ${serverVersion}. `
155
- + "Run `oxygen update` before using operational commands.\n");
154
+ const guidance = resolveCliUpdateGuidance(apiUrl);
155
+ const warningKey = `${serverVersion}|${guidance.channel}|${guidance.binaryName}`;
156
+ if (staleCliWarningVersions.has(warningKey))
157
+ return;
158
+ staleCliWarningVersions.add(warningKey);
159
+ process.stderr.write(`[${guidance.binaryName}] CLI version ${OXYGEN_VERSION} is older than Oxygen API version ${serverVersion}. `
160
+ + `${guidance.warningInstruction}\n`);
156
161
  }
157
162
  function isProdApiUrl(apiUrl) {
158
163
  try {
@@ -162,18 +167,19 @@ function isProdApiUrl(apiUrl) {
162
167
  return false;
163
168
  }
164
169
  }
165
- function assertCliMeetsMinimumApiVersion(compatibility, options) {
170
+ function assertCliMeetsMinimumApiVersion(compatibility, options, apiUrl) {
166
171
  if (options.enforceMinimumCliVersion === false)
167
172
  return;
168
173
  const minimumCliVersion = compatibility.minimumCliVersion;
169
174
  if (!minimumCliVersion || !isVersionGreater(minimumCliVersion, OXYGEN_VERSION))
170
175
  return;
171
- throw new OxygenError("cli_update_required", "This Oxygen API requires a newer CLI. Run `oxygen update` before using this command.", {
176
+ const guidance = resolveCliUpdateGuidance(apiUrl);
177
+ throw new OxygenError("cli_update_required", `This Oxygen API requires a newer CLI. ${guidance.failureInstruction}`, {
172
178
  details: {
173
179
  client_version: OXYGEN_VERSION,
174
180
  ...(compatibility.version ? { server_version: compatibility.version } : {}),
175
181
  minimum_cli_version: minimumCliVersion,
176
- cli_update_command: "oxygen update",
182
+ ...guidance.details,
177
183
  },
178
184
  exitCode: 1,
179
185
  });
@@ -191,7 +197,7 @@ function readEnvelopeCompatibility(envelope) {
191
197
  }
192
198
  return compatibility;
193
199
  }
194
- function withTraceDetails(details, traceId, compatibility) {
200
+ function withTraceDetails(details, traceId, compatibility, apiUrl) {
195
201
  const serverVersion = compatibility.version;
196
202
  const fields = {
197
203
  trace_id: traceId,
@@ -204,7 +210,7 @@ function withTraceDetails(details, traceId, compatibility) {
204
210
  if ((serverVersion && isVersionGreater(serverVersion, OXYGEN_VERSION))
205
211
  || (compatibility.minimumCliVersion
206
212
  && isVersionGreater(compatibility.minimumCliVersion, OXYGEN_VERSION))) {
207
- fields.cli_update_command = "oxygen update";
213
+ Object.assign(fields, resolveCliUpdateGuidance(apiUrl).details);
208
214
  }
209
215
  if (details && typeof details === "object" && !Array.isArray(details)) {
210
216
  return {
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
19
19
  import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
20
20
  import { addSessionOutput, addSessionStatus, getSessionUsage, startSession, updateSessionStep, } from "./session.js";
21
21
  import { doctorAgentSkills, installAgentSkills, listAgentSkills, runAutomaticSkillsInstall, } from "./skills.js";
22
+ import { resolveCliBinaryName } from "./runtime.js";
22
23
  import { updateCli } from "./update.js";
23
24
  const BROWSER_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
24
25
  const OXYGEN_SPINNER_INTERVAL_MS = 90;
@@ -172,8 +173,9 @@ function readSpecFileBody(path) {
172
173
  }
173
174
  export function createProgram() {
174
175
  const program = new Command();
176
+ const binaryName = resolveCliBinaryName();
175
177
  program
176
- .name("oxygen")
178
+ .name(binaryName)
177
179
  .description("CLI/API-first GTM platform for GTM tool and workflow primitives.")
178
180
  .version(OXYGEN_VERSION)
179
181
  .option("--profile <name>", "Use a stored CLI profile for this command.")
@@ -358,18 +360,54 @@ export function createProgram() {
358
360
  .option("--json", "Print a JSON envelope.")
359
361
  .action(async (organization, options) => {
360
362
  await handleOrgUseAction(organization, options, "orgs select");
363
+ }))
364
+ .addCommand(new Command("billing-link")
365
+ .description("Bill a workspace through another organization you administer.")
366
+ .requiredOption("--owner <organization>", "Billing owner organization id, Clerk org id, or slug.")
367
+ .option("--organization <organization>", "Workspace organization to link. Defaults to the active organization.")
368
+ .option("--organization-id <id>", "Alias for --organization.")
369
+ .option("--monthly-credit-cap <credits>", "Optional monthly credit cap for this workspace.")
370
+ .option("--json", "Print a JSON envelope.")
371
+ .action(async (options) => {
372
+ await handleAsyncAction("orgs billing-link", options, () => requestOxygen("/api/cli/orgs/billing-link", {
373
+ method: "POST",
374
+ body: {
375
+ billing_owner_organization_id: readOption(options.owner),
376
+ ...(readOption(options.organization) || readOption(options.organizationId)
377
+ ? { organization_id: readOption(options.organization) ?? readOption(options.organizationId) }
378
+ : {}),
379
+ ...(readOption(options.monthlyCreditCap)
380
+ ? { monthly_credit_cap: readPositiveNumber(options.monthlyCreditCap) }
381
+ : {}),
382
+ },
383
+ }));
384
+ }))
385
+ .addCommand(new Command("billing-unlink")
386
+ .description("Return a workspace to owning its own billing.")
387
+ .option("--organization <organization>", "Workspace organization to unlink. Defaults to the active organization.")
388
+ .option("--organization-id <id>", "Alias for --organization.")
389
+ .option("--json", "Print a JSON envelope.")
390
+ .action(async (options) => {
391
+ await handleAsyncAction("orgs billing-unlink", options, () => requestOxygen("/api/cli/orgs/billing-unlink", {
392
+ method: "POST",
393
+ body: {
394
+ ...(readOption(options.organization) || readOption(options.organizationId)
395
+ ? { organization_id: readOption(options.organization) ?? readOption(options.organizationId) }
396
+ : {}),
397
+ },
398
+ }));
361
399
  }));
362
400
  program
363
401
  .command("db")
364
402
  .description("Tenant database commands.")
365
403
  .addCommand(new Command("status")
366
- .description("Show the current organization's tenant database status.")
404
+ .description("Show the current organization's tenant database status. Staff can pass global --org to inspect another org.")
367
405
  .option("--json", "Print a JSON envelope.")
368
406
  .action(async (options) => {
369
407
  await handleAsyncAction("db status", options, async () => requestOxygen("/api/cli/db/status"));
370
408
  }))
371
409
  .addCommand(new Command("provision")
372
- .description("Provision a managed Neon tenant database for the current organization.")
410
+ .description("Provision a managed Neon tenant database for the current organization. Staff can pass global --org to repair another org.")
373
411
  .option("--json", "Print a JSON envelope.")
374
412
  .action(async (options) => {
375
413
  await handleAsyncAction("db provision", options, async () => requestOxygen("/api/cli/db/provision", { method: "POST", body: {} }));
@@ -1798,6 +1836,27 @@ export function createProgram() {
1798
1836
  body: {},
1799
1837
  }));
1800
1838
  }));
1839
+ program
1840
+ .command("sourcing")
1841
+ .description("Plan account, people, profile, and enrichment-handoff sourcing workflows.")
1842
+ .addCommand(new Command("plan")
1843
+ .description("Classify a sourcing request and recommend provider routes without executing paid tools.")
1844
+ .requiredOption("--prompt <file|text>", "Sourcing prompt text, or a local file path containing the prompt.")
1845
+ .option("--strategy <id>", "Optional strategy id such as account_first_lead_sourcing or known_account_org_chart.")
1846
+ .option("--table <table_id>", "Optional target table id or slug for follow-up commands.")
1847
+ .option("--materialize-preview", "Create a preview table for the plan. Defaults to no table side effect.")
1848
+ .option("--json", "Print a JSON envelope.")
1849
+ .action(async (options) => {
1850
+ await handleAsyncAction("sourcing plan", options, () => requestOxygen("/api/cli/sourcing/plan", {
1851
+ method: "POST",
1852
+ body: {
1853
+ prompt: readFileIfPresent(options.prompt),
1854
+ ...(readOption(options.strategy) ? { strategy: readOption(options.strategy) } : {}),
1855
+ ...(readOption(options.table) ? { target_table_id: readOption(options.table) } : {}),
1856
+ ...(options.materializePreview ? { materialize_preview: true } : {}),
1857
+ },
1858
+ }));
1859
+ }));
1801
1860
  program
1802
1861
  .command("lead-sourcing")
1803
1862
  .description("ICP-gated lead sourcing planning and audit commands.")
@@ -1829,9 +1888,161 @@ export function createProgram() {
1829
1888
  },
1830
1889
  }));
1831
1890
  }));
1891
+ program
1892
+ .command("search")
1893
+ .description("Agent-operable company search, web search, and scraping jobs.")
1894
+ .addCommand(new Command("plan")
1895
+ .description("Plan an Oxygen search or scrape route before running provider jobs.")
1896
+ .requiredOption("--goal <file|text>", "Search/scrape goal text, or a local file path containing the goal.")
1897
+ .option("--kind <kind>", "Route kind: company_search, people_search, signal_search, web_search, web_scrape, local_business_search, or source_specific_scrape.")
1898
+ .option("--target-count <n>", "Optional target row count.")
1899
+ .option("--geography <text>", "Optional geography hint.")
1900
+ .option("--known-urls <urls>", "Comma-separated known URLs for scrape routes.")
1901
+ .option("--provider-hints <providers>", "Comma-separated provider hints.")
1902
+ .option("--json", "Print a JSON envelope.")
1903
+ .action(async (options) => {
1904
+ await handleAsyncAction("search plan", options, () => requestOxygen("/api/cli/search/plan", {
1905
+ method: "POST",
1906
+ body: {
1907
+ goal: readFileIfPresent(options.goal),
1908
+ ...(readOption(options.kind) ? { kind: readOption(options.kind) } : {}),
1909
+ ...(readPositiveInt(options.targetCount) ? { target_count: readPositiveInt(options.targetCount) } : {}),
1910
+ ...(readOption(options.geography) ? { geography: readOption(options.geography) } : {}),
1911
+ ...(readOption(options.knownUrls) ? { known_urls: readCsvOption(options.knownUrls) } : {}),
1912
+ ...(readOption(options.providerHints) ? { provider_hints: readCsvOption(options.providerHints) } : {}),
1913
+ },
1914
+ }));
1915
+ }))
1916
+ .addCommand(new Command("run")
1917
+ .description("Preview or enqueue a durable table-backed search/scrape job from a search plan.")
1918
+ .argument("<table>", "Target table id or slug for live runs.")
1919
+ .requiredOption("--plan <file|json>", "Search plan JSON returned by oxygen search plan.")
1920
+ .option("--route <route_id>", "Route id from the plan. Defaults to recommended_route_id.")
1921
+ .requiredOption("--request-json <json>", "Provider request JSON for the selected route.")
1922
+ .option("--mode <mode>", "dry_run or live. Defaults to dry_run.")
1923
+ .option("--max-credits <n>", "Required credit ceiling for live runs.")
1924
+ .option("--max-pages <n>", "Maximum provider pages to fetch. Defaults to the route's page cap.")
1925
+ .option("--rows-path <path>", "Override route rows path.")
1926
+ .option("--row-mapping-json <json>", "Override route row mapping.")
1927
+ .option("--cursor-path <path>", "Override route cursor path.")
1928
+ .option("--cursor-request-key <path>", "Override request key that receives the next cursor.")
1929
+ .option("--upsert-key <key>", "Column key used to upsert instead of inserting.")
1930
+ .option("--connection-id <connection_id>", "Optional provider integration connection id.")
1931
+ .option("--json", "Print a JSON envelope.")
1932
+ .action(async (table, options) => {
1933
+ await handleAsyncAction("search run", options, () => requestOxygen("/api/cli/search/run", {
1934
+ method: "POST",
1935
+ body: {
1936
+ table,
1937
+ plan: parseJsonObject(readFileIfPresent(options.plan)),
1938
+ request: parseJsonObject(options.requestJson),
1939
+ ...(readOption(options.route) ? { route_id: readOption(options.route) } : {}),
1940
+ ...(readOption(options.mode) ? { mode: readOption(options.mode) } : {}),
1941
+ ...(readPositiveNumber(options.maxCredits) ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
1942
+ ...(readPositiveInt(options.maxPages) ? { max_pages: readPositiveInt(options.maxPages) } : {}),
1943
+ ...(readOption(options.rowsPath) ? { rows_path: readOption(options.rowsPath) } : {}),
1944
+ ...(options.rowMappingJson ? { row_mapping: parseJsonObject(options.rowMappingJson) } : {}),
1945
+ ...(readOption(options.cursorPath) ? { cursor_path: readOption(options.cursorPath) } : {}),
1946
+ ...(readOption(options.cursorRequestKey) ? { cursor_request_key: readOption(options.cursorRequestKey) } : {}),
1947
+ ...(readOption(options.upsertKey) ? { upsert_key: readOption(options.upsertKey) } : {}),
1948
+ ...(readOption(options.connectionId) ? { connection_id: readOption(options.connectionId) } : {}),
1949
+ },
1950
+ }));
1951
+ }))
1952
+ .addCommand(new Command("runs")
1953
+ .description("Inspect and control durable search/scrape runs.")
1954
+ .addCommand(new Command("get")
1955
+ .description("Get one durable search/scrape run.")
1956
+ .argument("<run_id>", "Search run UUID.")
1957
+ .option("--json", "Print a JSON envelope.")
1958
+ .action(async (runId, options) => {
1959
+ await handleAsyncAction("search runs get", options, () => requestOxygen(`/api/cli/search/runs/${encodeURIComponent(runId)}`));
1960
+ }))
1961
+ .addCommand(new Command("items")
1962
+ .description("List search/scrape run items.")
1963
+ .argument("<run_id>", "Search run UUID.")
1964
+ .option("--status <status>", "Filter by pending, leased, completed, failed, skipped, or canceled.")
1965
+ .option("--limit <n>", "Maximum items to return. Defaults to 100.")
1966
+ .option("--json", "Print a JSON envelope.")
1967
+ .action(async (runId, options) => {
1968
+ await handleAsyncAction("search runs items", options, () => {
1969
+ const query = new URLSearchParams();
1970
+ if (readOption(options.status))
1971
+ query.set("status", readOption(options.status) ?? "");
1972
+ const limit = readPositiveInt(options.limit);
1973
+ if (limit)
1974
+ query.set("limit", String(limit));
1975
+ const suffix = query.toString() ? `?${query.toString()}` : "";
1976
+ return requestOxygen(`/api/cli/search/runs/${encodeURIComponent(runId)}/items${suffix}`);
1977
+ });
1978
+ }))
1979
+ .addCommand(new Command("wait")
1980
+ .description("Poll a durable search/scrape run until it finishes.")
1981
+ .argument("<run_id>", "Search run UUID.")
1982
+ .option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
1983
+ .option("--interval-seconds <n>", "Polling interval. Defaults to 5.")
1984
+ .option("--json", "Print a JSON envelope.")
1985
+ .action(async (runId, options) => {
1986
+ await handleAsyncAction("search runs wait", options, () => waitForSearchRun(runId, options));
1987
+ }))
1988
+ .addCommand(new Command("cancel")
1989
+ .description("Request cancellation for a queued or running search/scrape run.")
1990
+ .argument("<run_id>", "Search run UUID.")
1991
+ .option("--json", "Print a JSON envelope.")
1992
+ .action(async (runId, options) => {
1993
+ await handleAsyncAction("search runs cancel", options, () => requestOxygen(`/api/cli/search/runs/${encodeURIComponent(runId)}/cancel`, {
1994
+ method: "POST",
1995
+ body: {},
1996
+ }));
1997
+ }))
1998
+ .addCommand(new Command("retry-failed")
1999
+ .description("Requeue failed search/scrape run items.")
2000
+ .argument("<run_id>", "Search run UUID.")
2001
+ .option("--json", "Print a JSON envelope.")
2002
+ .action(async (runId, options) => {
2003
+ await handleAsyncAction("search runs retry-failed", options, () => requestOxygen(`/api/cli/search/runs/${encodeURIComponent(runId)}/retry-failed`, {
2004
+ method: "POST",
2005
+ body: {},
2006
+ }));
2007
+ })));
1832
2008
  program
1833
2009
  .command("companies")
1834
2010
  .description("Company prospecting and account enrichment workflows.")
2011
+ .addCommand(new Command("search")
2012
+ .description("Plan, dry-run, or queue provider-backed company search.")
2013
+ .addCommand(new Command("plan")
2014
+ .description("Compile a company-search prompt into ordered provider routes without provider calls.")
2015
+ .requiredOption("--prompt <text-or-file>", "Company-search prompt, or a path to a prompt file.")
2016
+ .option("--target-count <n>", "Desired company count for routing and estimates.")
2017
+ .option("--source-intent <intent>", "Override detected intent: structured, technology, hiring, local, known_source, web, or fallback.")
2018
+ .option("--materialize-preview", "Create a preview table with route rows.")
2019
+ .option("--json", "Print a JSON envelope.")
2020
+ .action(async (options) => {
2021
+ await handleAsyncAction("companies search plan", options, () => requestOxygen("/api/cli/companies/search/plan", {
2022
+ method: "POST",
2023
+ body: readCompaniesSearchPlanBody(options),
2024
+ }));
2025
+ }))
2026
+ .addCommand(new Command("run")
2027
+ .description("Return a dry-run request or queue a live company-search ingestion run.")
2028
+ .option("--prompt <text-or-file>", "Company-search prompt, or a path to a prompt file.")
2029
+ .option("--plan-json <json-or-file>", "Plan JSON returned by companies search plan, or a path to a JSON file.")
2030
+ .option("--route-id <id>", "Route id from the plan to execute.")
2031
+ .option("--tool-id <tool>", "Tool id from the plan to execute.")
2032
+ .option("--table <table>", "Existing table id or slug to receive rows. If omitted for live, Oxygen creates a table.")
2033
+ .option("--mode <mode>", "dry_run or live. Defaults to dry_run.")
2034
+ .option("--max-pages <n>", "Maximum provider pages to ingest. Defaults to the route estimate.")
2035
+ .option("--max-credits <n>", "Required credit ceiling for live runs.")
2036
+ .option("--target-count <n>", "Desired company count when planning from --prompt.")
2037
+ .option("--source-intent <intent>", "Override detected intent when planning from --prompt.")
2038
+ .option("--approved", "Required for live runs after inspecting dry-run output.")
2039
+ .option("--json", "Print a JSON envelope.")
2040
+ .action(async (options) => {
2041
+ await handleAsyncAction("companies search run", options, () => requestOxygen("/api/cli/companies/search/run", {
2042
+ method: "POST",
2043
+ body: readCompaniesSearchRunBody(options),
2044
+ }));
2045
+ })))
1835
2046
  .addCommand(new Command("enrich")
1836
2047
  .description("Preview or run a company enrichment waterfall over an existing table.")
1837
2048
  .addCommand(new Command("preview")
@@ -2231,19 +2442,28 @@ export function createProgram() {
2231
2442
  .description("Search the tool catalog.")
2232
2443
  .argument("[query]", "Search text.")
2233
2444
  .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.")
2445
+ .option("--terse", "Alias for --verbosity minimal.")
2234
2446
  .option("--only-runnable", "Only return tools runnable by the active organization.")
2447
+ .option("--category <category>", "Filter by Oxygen tool category, such as company_search, people_search, research, or local.")
2235
2448
  .option("--capability <tag>", "Filter by capability tag, such as mobile_phone.")
2449
+ .option("--limit <n>", "Maximum number of tools to return. Capped at 100.")
2236
2450
  .option("--json", "Print a JSON envelope.")
2237
2451
  .action(async (query, options) => {
2238
- await handleAsyncAction("tools search", options, async () => {
2452
+ await handleAsyncAction("tools search", options, () => {
2239
2453
  const params = new URLSearchParams();
2240
2454
  params.set("query", query ?? "");
2241
- if (readOption(options.verbosity))
2242
- params.set("verbosity", readOption(options.verbosity) ?? "");
2455
+ const verbosity = options.terse ? "minimal" : readOption(options.verbosity);
2456
+ if (verbosity)
2457
+ params.set("verbosity", verbosity);
2243
2458
  if (options.onlyRunnable)
2244
2459
  params.set("only_runnable", "true");
2460
+ if (readOption(options.category))
2461
+ params.set("category", readOption(options.category) ?? "");
2245
2462
  if (readOption(options.capability))
2246
2463
  params.set("capability", readOption(options.capability) ?? "");
2464
+ const limit = readPositiveInt(options.limit);
2465
+ if (limit !== undefined)
2466
+ params.set("limit", String(Math.min(limit, 100)));
2247
2467
  return requestOxygen(`/api/cli/tools/search?${params.toString()}`);
2248
2468
  });
2249
2469
  }))
@@ -2340,7 +2560,9 @@ export function createProgram() {
2340
2560
  .option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
2341
2561
  .option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
2342
2562
  .option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
2343
- .option("--provider-order <providers>", "Comma-separated provider order. Work email defaults to blitzapi,prospeo,leadmagic,hunter,contactout,bettercontact,ai_ark; phone defaults to blitzapi,prospeo,leadmagic,contactout,bettercontact,ai_ark.")
2563
+ .option("--provider-order <providers>", "Comma-separated provider order. Overrides the default waterfall profile.")
2564
+ .option("--email-waterfall-profile <profile>", "Work-email waterfall profile: auto, name_domain, linkedin_url, or first_last_domain.")
2565
+ .option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
2344
2566
  .option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
2345
2567
  .option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
2346
2568
  .option("--limit <n>", "Rows to estimate. Defaults to 10.")
@@ -2371,7 +2593,9 @@ export function createProgram() {
2371
2593
  .option("--capability <capability>", "Capability to enrich: mobile_phone or work_email. Defaults to mobile_phone.")
2372
2594
  .option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
2373
2595
  .option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
2374
- .option("--provider-order <providers>", "Comma-separated provider order. Work email defaults to blitzapi,prospeo,leadmagic,hunter,contactout,bettercontact,ai_ark; phone defaults to blitzapi,prospeo,leadmagic,contactout,bettercontact,ai_ark.")
2596
+ .option("--provider-order <providers>", "Comma-separated provider order. Overrides the default waterfall profile.")
2597
+ .option("--email-waterfall-profile <profile>", "Work-email waterfall profile: auto, name_domain, linkedin_url, or first_last_domain.")
2598
+ .option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
2375
2599
  .option("--verify-phone", "Validate found phone numbers with ClearoutPhone before returning them.")
2376
2600
  .option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
2377
2601
  .option("--limit <n>", "Rows to queue.")
@@ -2744,7 +2968,7 @@ export function createProgram() {
2744
2968
  .option("--workflow-id <workflow_id>", "Workflow id or slug.")
2745
2969
  .option("--workflow-name <workflow_name>", "Workflow name.")
2746
2970
  .option("--input-json <json>", "Workflow input object. Defaults to {}.")
2747
- .option("--mode <mode>", "Execution mode: live, dry-run, or smoke-test.")
2971
+ .requiredOption("--mode <mode>", "Execution mode: live, dry-run, or smoke-test.")
2748
2972
  .option("--idempotency-key <key>", "Optional idempotency key.")
2749
2973
  .option("--include-bundle", "Include durable recipe bundles in JSON output.")
2750
2974
  .option("--json", "Print a JSON envelope.")
@@ -2772,7 +2996,7 @@ export function createProgram() {
2772
2996
  .option("--headers-json <json>", "Optional delivery headers object.")
2773
2997
  .option("--external-event-id <id>", "Provider event id for idempotency and inspection.")
2774
2998
  .option("--idempotency-key <key>", "Optional explicit event idempotency key.")
2775
- .option("--mode <mode>", "Execution mode: live, dry-run, or smoke-test.")
2999
+ .requiredOption("--mode <mode>", "Execution mode: live, dry-run, or smoke-test.")
2776
3000
  .option("--include-bundle", "Include durable recipe bundles in JSON output.")
2777
3001
  .option("--json", "Print a JSON envelope.")
2778
3002
  .action(async (options) => {
@@ -3019,6 +3243,7 @@ async function tryCompileRecipeFile(absolutePath, source) {
3019
3243
  ...(recipe.status ? { status: recipe.status } : {}),
3020
3244
  ...(recipe.trigger ? { trigger: recipe.trigger } : {}),
3021
3245
  ...(recipe.inputSchema ? { inputSchema: recipe.inputSchema } : {}),
3246
+ ...(recipe.visualPlan ? { visualPlan: recipe.visualPlan } : {}),
3022
3247
  bundle,
3023
3248
  toolsUsed: recipe.tools,
3024
3249
  sourceHash,
@@ -3178,7 +3403,8 @@ export function toolStep(input) {
3178
3403
  }
3179
3404
  `;
3180
3405
  }
3181
- async function tailWorkflowRun(runId, options) {
3406
+ async function tailWorkflowRun(// skipcq: JS-R1005 -- CLI tailer coordinates polling, status transitions, and final output formatting.
3407
+ runId, options) {
3182
3408
  const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
3183
3409
  ?? WORKFLOW_TAIL_DEFAULT_TIMEOUT_SECONDS;
3184
3410
  const intervalSeconds = readPositiveInt(options.intervalSeconds)
@@ -3192,9 +3418,13 @@ async function tailWorkflowRun(runId, options) {
3192
3418
  method: "POST",
3193
3419
  body: { run_id: runId },
3194
3420
  });
3195
- const run = latest.run ?? latest;
3421
+ const run = isRecord(latest.run) ? latest.run : latest;
3196
3422
  const status = readRecordString(run, "status");
3197
3423
  if (isTerminalWorkflowRunStatus(status)) {
3424
+ const workflowUrl = readRecordString(latest, "workflowUrl");
3425
+ const runUrl = readRecordString(latest, "runUrl");
3426
+ const webUrl = readRecordString(latest, "web_url");
3427
+ const deepLink = readRecordString(latest, "deepLink");
3198
3428
  return {
3199
3429
  run,
3200
3430
  workflowRunId: readRecordString(run, "id") ?? runId,
@@ -3202,6 +3432,10 @@ async function tailWorkflowRun(runId, options) {
3202
3432
  terminal: true,
3203
3433
  polls,
3204
3434
  elapsedMs: Date.now() - startedAt,
3435
+ ...(workflowUrl ? { workflowUrl } : {}),
3436
+ ...(runUrl ? { runUrl } : {}),
3437
+ ...(webUrl ? { web_url: webUrl } : {}),
3438
+ ...(deepLink ? { deepLink } : {}),
3205
3439
  };
3206
3440
  }
3207
3441
  const remainingMs = deadline - Date.now();
@@ -3388,6 +3622,45 @@ async function recoverBackgroundColumnRun(table, traceId) {
3388
3622
  function isNetworkTimeoutError(error) {
3389
3623
  return error instanceof OxygenError && error.code === "network_timeout";
3390
3624
  }
3625
+ function readCompaniesSearchPlanBody(options) {
3626
+ const targetCount = readPositiveInt(options.targetCount);
3627
+ return {
3628
+ prompt: readFileIfPresent(options.prompt),
3629
+ ...(targetCount !== undefined ? { target_count: targetCount } : {}),
3630
+ ...(options.sourceIntent ? { source_intent: options.sourceIntent } : {}),
3631
+ ...(options.materializePreview ? { materialize_preview: true } : {}),
3632
+ };
3633
+ }
3634
+ function readCompaniesSearchRunBody(options) {
3635
+ const prompt = options.prompt ? readFileIfPresent(options.prompt) : null;
3636
+ const plan = options.planJson ? readCompanySearchPlanJson(options.planJson) : null;
3637
+ if (!prompt && !plan) {
3638
+ throw new OxygenError("invalid_request", "Pass --prompt or --plan-json.", { exitCode: 1 });
3639
+ }
3640
+ const maxPages = readPositiveInt(options.maxPages);
3641
+ const maxCredits = readPositiveNumber(options.maxCredits);
3642
+ const targetCount = readPositiveInt(options.targetCount);
3643
+ return {
3644
+ ...(prompt ? { prompt } : {}),
3645
+ ...(plan ? { plan } : {}),
3646
+ ...(options.routeId ? { route_id: options.routeId } : {}),
3647
+ ...(options.toolId ? { tool_id: options.toolId } : {}),
3648
+ ...(options.table ? { table: options.table } : {}),
3649
+ ...(options.mode ? { mode: options.mode } : {}),
3650
+ ...(maxPages !== undefined ? { max_pages: maxPages } : {}),
3651
+ ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
3652
+ ...(targetCount !== undefined ? { target_count: targetCount } : {}),
3653
+ ...(options.sourceIntent ? { source_intent: options.sourceIntent } : {}),
3654
+ ...(options.approved ? { approved: true } : {}),
3655
+ };
3656
+ }
3657
+ function readCompanySearchPlanJson(value) {
3658
+ const parsed = parseJsonObject(readFileIfPresent(value));
3659
+ const data = parsed.data;
3660
+ return data && typeof data === "object" && !Array.isArray(data)
3661
+ ? data
3662
+ : parsed;
3663
+ }
3391
3664
  function readCompaniesEnrichBody(table, options) {
3392
3665
  const body = { table };
3393
3666
  const fields = readCsvOption(options.missingFields);
@@ -3795,6 +4068,44 @@ async function waitForTableIngestionRun(runId, options) {
3795
4068
  await sleep(Math.min(intervalSeconds * 1000, remainingMs));
3796
4069
  }
3797
4070
  }
4071
+ async function waitForSearchRun(runId, options) {
4072
+ const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
4073
+ ?? TABLE_INGESTION_WAIT_DEFAULT_TIMEOUT_SECONDS;
4074
+ const intervalSeconds = readPositiveInt(options.intervalSeconds)
4075
+ ?? TABLE_INGESTION_WAIT_DEFAULT_INTERVAL_SECONDS;
4076
+ const startedAt = Date.now();
4077
+ const deadline = startedAt + timeoutSeconds * 1000;
4078
+ let polls = 0;
4079
+ while (true) {
4080
+ polls += 1;
4081
+ const latestRun = await requestOxygen(`/api/cli/search/runs/${encodeURIComponent(runId)}`);
4082
+ const status = readRecordString(latestRun, "status");
4083
+ if (isTerminalTableIngestionStatus(status)) {
4084
+ return {
4085
+ searchRun: latestRun,
4086
+ searchRunId: readRecordString(latestRun, "id") ?? runId,
4087
+ status,
4088
+ terminal: true,
4089
+ polls,
4090
+ elapsedMs: Date.now() - startedAt,
4091
+ ...(readRecordString(latestRun, "web_url") ? { web_url: readRecordString(latestRun, "web_url") } : {}),
4092
+ };
4093
+ }
4094
+ const remainingMs = deadline - Date.now();
4095
+ if (remainingMs <= 0) {
4096
+ throw new OxygenError("search_run_wait_timeout", "Timed out waiting for search run to finish.", {
4097
+ details: {
4098
+ search_run_id: runId,
4099
+ status: status ?? null,
4100
+ timeout_seconds: timeoutSeconds,
4101
+ polls,
4102
+ },
4103
+ exitCode: 1,
4104
+ });
4105
+ }
4106
+ await sleep(Math.min(intervalSeconds * 1000, remainingMs));
4107
+ }
4108
+ }
3798
4109
  async function waitForTableActionRun(runId, options) {
3799
4110
  const timeoutSeconds = readPositiveInt(options.timeoutSeconds)
3800
4111
  ?? TABLE_ACTION_RUN_WAIT_DEFAULT_TIMEOUT_SECONDS;
@@ -5532,7 +5843,8 @@ function buildContextAssetUpsertBody(options) {
5532
5843
  ...(options.dataJson ? { data: parseJsonObject(options.dataJson) } : {}),
5533
5844
  };
5534
5845
  }
5535
- function buildEnrichColumnBody(table, options) {
5846
+ function buildEnrichColumnBody(// skipcq: JS-R1005 -- CLI body builder maps enrichment aliases, selection, provider order, and safety caps.
5847
+ table, options) {
5536
5848
  const limit = readPositiveInt(options.limit);
5537
5849
  const filterSelection = readFilterSelectionOption(options.filterJson);
5538
5850
  const explicitSelection = readSelectionJsonOption(options.selectionJson);
@@ -5566,6 +5878,12 @@ function buildEnrichColumnBody(table, options) {
5566
5878
  ? { on_existing_manual_column: readOption(options.onExistingManualColumn) }
5567
5879
  : {}),
5568
5880
  ...(readOption(options.providerOrder) ? { provider_order: readCsvOption(options.providerOrder) } : {}),
5881
+ ...(readOption(options.emailWaterfallProfile)
5882
+ ? { email_waterfall_profile: readOption(options.emailWaterfallProfile) }
5883
+ : {}),
5884
+ ...(readOption(options.emailPatternValidation)
5885
+ ? { email_pattern_validation: readOption(options.emailPatternValidation) }
5886
+ : {}),
5569
5887
  ...(options.verifyPhone ? { verify_phone: true } : {}),
5570
5888
  ...(readOption(options.phoneVerificationCredentialMode)
5571
5889
  ? { phone_verification_credential_mode: readOption(options.phoneVerificationCredentialMode) }
@@ -0,0 +1,15 @@
1
+ declare const DEV_CLI_BINARY = "oxygen-dev";
2
+ declare const PROD_CLI_BINARY = "oxygen";
3
+ export type CliUpdateGuidance = {
4
+ binaryName: typeof DEV_CLI_BINARY | typeof PROD_CLI_BINARY;
5
+ channel: "dev" | "npm";
6
+ warningInstruction: string;
7
+ failureInstruction: string;
8
+ details: {
9
+ cli_update_command?: string;
10
+ cli_update_instruction?: string;
11
+ };
12
+ };
13
+ export declare function resolveCliBinaryName(env?: NodeJS.ProcessEnv, argv?: readonly string[]): typeof DEV_CLI_BINARY | typeof PROD_CLI_BINARY;
14
+ export declare function resolveCliUpdateGuidance(apiUrl: string | undefined, env?: NodeJS.ProcessEnv, argv?: readonly string[]): CliUpdateGuidance;
15
+ export {};
@@ -0,0 +1,56 @@
1
+ import { basename } from "node:path";
2
+ const PROD_API_HOSTNAME = "oxygen-agent.com";
3
+ const DEV_CLI_BINARY = "oxygen-dev";
4
+ const PROD_CLI_BINARY = "oxygen";
5
+ export function resolveCliBinaryName(env = process.env, argv = process.argv) {
6
+ const explicit = normalizeBinaryName(env.OXYGEN_CLI_BINARY ?? env.OXYGEN_CLI_NAME);
7
+ if (explicit)
8
+ return explicit;
9
+ const invoked = normalizeBinaryName(argv[1]);
10
+ if (invoked)
11
+ return invoked;
12
+ return PROD_CLI_BINARY;
13
+ }
14
+ export function resolveCliUpdateGuidance(apiUrl, env = process.env, argv = process.argv) {
15
+ const binaryName = resolveCliBinaryName(env, argv);
16
+ const devLike = binaryName === DEV_CLI_BINARY || (apiUrl ? !isProdApiUrl(apiUrl) : false);
17
+ if (devLike) {
18
+ return {
19
+ binaryName,
20
+ channel: "dev",
21
+ warningInstruction: "Refresh the dev CLI (`oxygen-dev`) from the dev branch before using operational commands.",
22
+ failureInstruction: "Refresh the dev CLI (`oxygen-dev`) from the dev branch before using this command.",
23
+ details: {
24
+ cli_update_instruction: "Refresh the dev CLI (`oxygen-dev`) from the dev branch.",
25
+ },
26
+ };
27
+ }
28
+ return {
29
+ binaryName,
30
+ channel: "npm",
31
+ warningInstruction: "Run `oxygen update` before using operational commands.",
32
+ failureInstruction: "Run `oxygen update` before using this command.",
33
+ details: {
34
+ cli_update_command: "oxygen update",
35
+ },
36
+ };
37
+ }
38
+ function normalizeBinaryName(value) {
39
+ const normalized = basename(value ?? "")
40
+ .replace(/\.(?:cmd|ps1|bat|js)$/i, "")
41
+ .trim()
42
+ .toLowerCase();
43
+ if (normalized === DEV_CLI_BINARY)
44
+ return DEV_CLI_BINARY;
45
+ if (normalized === PROD_CLI_BINARY)
46
+ return PROD_CLI_BINARY;
47
+ return null;
48
+ }
49
+ function isProdApiUrl(apiUrl) {
50
+ try {
51
+ return new URL(apiUrl).hostname === PROD_API_HOSTNAME;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
@@ -1,5 +1,5 @@
1
- import type { JsonSchema, WorkflowStepEffect, WorkflowMode, WorkflowStatus, WorkflowTriggerManifest } from "@oxygen/workflows";
2
- export type { JsonSchema, WorkflowTriggerManifest, WorkflowMode, WorkflowStatus, WorkflowStepEffect };
1
+ import type { JsonSchema, RecipeVisualPlanManifest, WorkflowStepEffect, WorkflowMode, WorkflowStatus, WorkflowTriggerManifest } from "@oxygen/workflows";
2
+ export type { JsonSchema, RecipeVisualPlanManifest, WorkflowTriggerManifest, WorkflowMode, WorkflowStatus, WorkflowStepEffect };
3
3
  export type RecipeLogLevel = "debug" | "info" | "warn" | "error";
4
4
  export type RecipeRuntime = "durable";
5
5
  export type RecipeToolRunOptions = {
@@ -106,6 +106,32 @@ export type RecipeContext = {
106
106
  export type DurableRecipeContext = RecipeContext;
107
107
  export type RecipeRunFunction = (ctx: RecipeContext) => unknown | Promise<unknown>;
108
108
  export type DurableRecipeRunFunction = RecipeRunFunction;
109
+ export type RecipeVisualBaseStep = {
110
+ id: string;
111
+ label: string;
112
+ description?: string;
113
+ checkpointId?: string;
114
+ checkpointKey?: string;
115
+ };
116
+ export type RecipeVisualToolStep = RecipeVisualBaseStep & {
117
+ kind: "tool";
118
+ tool: string;
119
+ effect?: WorkflowStepEffect;
120
+ };
121
+ export type RecipeVisualCodeStep = RecipeVisualBaseStep & {
122
+ kind: "step";
123
+ };
124
+ export type RecipeVisualBranchStep = RecipeVisualBaseStep & {
125
+ kind: "branch";
126
+ conditionLabel?: string;
127
+ thenId: string;
128
+ elseId?: string;
129
+ joinId?: string;
130
+ };
131
+ export type RecipeVisualStep = RecipeVisualToolStep | RecipeVisualCodeStep | RecipeVisualBranchStep;
132
+ export type RecipeVisualPlan = {
133
+ steps: RecipeVisualStep[];
134
+ };
109
135
  export type RecipeDefinition = {
110
136
  readonly __oxygen_recipe: true;
111
137
  id: string;
@@ -114,6 +140,7 @@ export type RecipeDefinition = {
114
140
  status?: WorkflowStatus;
115
141
  trigger?: WorkflowTriggerManifest;
116
142
  inputSchema?: JsonSchema;
143
+ visualPlan?: RecipeVisualPlanManifest;
117
144
  tools: string[];
118
145
  run: RecipeRunFunction;
119
146
  };
@@ -125,6 +152,7 @@ export type DefineRecipeInput = {
125
152
  status?: WorkflowStatus;
126
153
  trigger?: WorkflowTriggerManifest;
127
154
  inputSchema?: JsonSchema;
155
+ visualPlan?: RecipeVisualPlan;
128
156
  run: RecipeRunFunction;
129
157
  };
130
158
  export type DurableRecipeDefinition = RecipeDefinition;
@@ -133,3 +161,7 @@ export type DefineDurableRecipeInput = DefineRecipeInput & {
133
161
  };
134
162
  export declare function defineRecipe(input: DefineRecipeInput): RecipeDefinition;
135
163
  export declare function isRecipeDefinition(value: unknown): value is RecipeDefinition;
164
+ export declare function recipeVisualPlan(steps: RecipeVisualStep[]): RecipeVisualPlan;
165
+ export declare function recipeToolAction(input: Omit<RecipeVisualToolStep, "kind">): RecipeVisualToolStep;
166
+ export declare function recipeStepAction(input: Omit<RecipeVisualCodeStep, "kind">): RecipeVisualCodeStep;
167
+ export declare function recipeBranchAction(input: Omit<RecipeVisualBranchStep, "kind">): RecipeVisualBranchStep;
@@ -23,6 +23,7 @@ export function defineRecipe(input) {
23
23
  }
24
24
  return tool.trim();
25
25
  }))).sort();
26
+ const visualPlan = normalizeVisualPlan(input.visualPlan);
26
27
  return {
27
28
  __oxygen_recipe: true,
28
29
  id: input.id,
@@ -32,6 +33,7 @@ export function defineRecipe(input) {
32
33
  ...(input.status ? { status: input.status } : {}),
33
34
  ...(input.trigger ? { trigger: input.trigger } : {}),
34
35
  ...(input.inputSchema ? { inputSchema: input.inputSchema } : {}),
36
+ ...(visualPlan ? { visualPlan } : {}),
35
37
  run: input.run,
36
38
  };
37
39
  }
@@ -40,3 +42,62 @@ export function isRecipeDefinition(value) {
40
42
  && typeof value === "object"
41
43
  && value.__oxygen_recipe === true;
42
44
  }
45
+ export function recipeVisualPlan(steps) {
46
+ return { steps };
47
+ }
48
+ export function recipeToolAction(input) {
49
+ return { kind: "tool", ...withCheckpointAlias("tool", input) };
50
+ }
51
+ export function recipeStepAction(input) {
52
+ return { kind: "step", ...withCheckpointAlias("step", input) };
53
+ }
54
+ export function recipeBranchAction(input) {
55
+ return { kind: "branch", ...withCheckpointAlias("step", input) };
56
+ }
57
+ function withCheckpointAlias(prefix, input) {
58
+ if (input.checkpointId || !input.checkpointKey)
59
+ return input;
60
+ return {
61
+ ...input,
62
+ checkpointId: `${prefix}:${input.checkpointKey}`,
63
+ };
64
+ }
65
+ function normalizeVisualPlan(input) {
66
+ if (!input)
67
+ return undefined;
68
+ return {
69
+ version: 1,
70
+ steps: input.steps.map((step) => {
71
+ const checkpointId = step.checkpointId
72
+ ?? (step.checkpointKey ? `${step.kind === "tool" ? "tool" : "step"}:${step.checkpointKey}` : undefined);
73
+ const base = {
74
+ id: step.id,
75
+ label: step.label,
76
+ ...(step.description ? { description: step.description } : {}),
77
+ ...(checkpointId ? { checkpoint_id: checkpointId } : {}),
78
+ };
79
+ if (step.kind === "tool") {
80
+ return {
81
+ kind: "tool",
82
+ ...base,
83
+ tool: step.tool,
84
+ ...(step.effect ? { effect: step.effect } : {}),
85
+ };
86
+ }
87
+ if (step.kind === "branch") {
88
+ return {
89
+ kind: "branch",
90
+ ...base,
91
+ ...(step.conditionLabel ? { condition_label: step.conditionLabel } : {}),
92
+ then_id: step.thenId,
93
+ ...(step.elseId ? { else_id: step.elseId } : {}),
94
+ ...(step.joinId ? { join_id: step.joinId } : {}),
95
+ };
96
+ }
97
+ return {
98
+ kind: "step",
99
+ ...base,
100
+ };
101
+ }),
102
+ };
103
+ }
@@ -32,9 +32,9 @@ export declare const BASE_PRICING_PLANS: {
32
32
  readonly weeklyCreditsLimit: 500;
33
33
  readonly rolloverCap: 500;
34
34
  readonly byokEnabled: false;
35
- readonly description: "Try OXYGEN with managed email, phone enrichment, and AI credits.";
35
+ readonly description: "Get $10 in credits on us every month — try OXYGEN with managed email, phone enrichment, and AI credits.";
36
36
  readonly ctaLabel: "Start free";
37
- readonly features: readonly ["All integrations", "Workflows", "No card required"];
37
+ readonly features: readonly ["$10 in credits every month", "All integrations", "Workflows", "No card required"];
38
38
  };
39
39
  readonly starter: {
40
40
  readonly tier: "starter";
@@ -11,9 +11,10 @@ export const BASE_PRICING_PLANS = {
11
11
  weeklyCreditsLimit: 500,
12
12
  rolloverCap: 500,
13
13
  byokEnabled: false,
14
- description: "Try OXYGEN with managed email, phone enrichment, and AI credits.",
14
+ description: "Get $10 in credits on us every month — try OXYGEN with managed email, phone enrichment, and AI credits.",
15
15
  ctaLabel: "Start free",
16
16
  features: [
17
+ "$10 in credits every month",
17
18
  "All integrations",
18
19
  "Workflows",
19
20
  "No card required",
@@ -1,2 +1,2 @@
1
- export declare const OXYGEN_VERSION = "1.109.11";
1
+ export declare const OXYGEN_VERSION = "1.123.1";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.0.0";
@@ -1,3 +1,3 @@
1
- export const OXYGEN_VERSION = "1.109.11";
1
+ export const OXYGEN_VERSION = "1.123.1";
2
2
  // Bump this only when deployed CLI/API contracts require a newer CLI.
3
3
  export const OXYGEN_MINIMUM_CLI_VERSION = "1.0.0";
@@ -9,7 +9,7 @@ export declare const DEFAULT_WORKFLOW_CRON_TIMEZONE = "UTC";
9
9
  export type WorkflowMode = "dry_run" | "live" | "smoke_test";
10
10
  export type WorkflowTriggerType = "api" | "webhook" | "cron" | "event";
11
11
  export type WorkflowStatus = "active" | "disabled";
12
- export type WorkflowStepKind = "transform" | "tool";
12
+ export type WorkflowStepKind = "transform" | "tool" | "branch";
13
13
  export type WorkflowStepEffect = "none" | "external_read" | "external_write";
14
14
  export type RecipeRuntime = "durable";
15
15
  export type WorkflowEventFilterOp = "eq" | "neq" | "exists" | "not_exists";
@@ -69,7 +69,48 @@ export type WorkflowToolStepManifest = {
69
69
  mode?: WorkflowMode;
70
70
  payload_source: string;
71
71
  };
72
- export type WorkflowStepManifest = WorkflowTransformStepManifest | WorkflowToolStepManifest;
72
+ export type WorkflowBranchStepManifest = {
73
+ kind: "branch";
74
+ id: string;
75
+ description?: string;
76
+ condition_source: string;
77
+ then_id: string;
78
+ else_id?: string;
79
+ join_id?: string;
80
+ };
81
+ export type WorkflowStepManifest = WorkflowTransformStepManifest | WorkflowToolStepManifest | WorkflowBranchStepManifest;
82
+ export type RecipeVisualToolStepManifest = {
83
+ kind: "tool";
84
+ id: string;
85
+ label: string;
86
+ description?: string;
87
+ tool: string;
88
+ effect?: WorkflowStepEffect;
89
+ checkpoint_id?: string;
90
+ };
91
+ export type RecipeVisualCodeStepManifest = {
92
+ kind: "step";
93
+ id: string;
94
+ label: string;
95
+ description?: string;
96
+ checkpoint_id?: string;
97
+ };
98
+ export type RecipeVisualBranchStepManifest = {
99
+ kind: "branch";
100
+ id: string;
101
+ label: string;
102
+ description?: string;
103
+ condition_label?: string;
104
+ then_id: string;
105
+ else_id?: string;
106
+ join_id?: string;
107
+ checkpoint_id?: string;
108
+ };
109
+ export type RecipeVisualStepManifest = RecipeVisualToolStepManifest | RecipeVisualCodeStepManifest | RecipeVisualBranchStepManifest;
110
+ export type RecipeVisualPlanManifest = {
111
+ version: 1;
112
+ steps: RecipeVisualStepManifest[];
113
+ };
73
114
  export type WorkflowManifest = {
74
115
  manifest_version: 1;
75
116
  workflow: {
@@ -98,6 +139,7 @@ export type RecipeManifest = {
98
139
  bundle: string;
99
140
  bundle_format: "esm";
100
141
  tools_used: string[];
142
+ visual_plan?: RecipeVisualPlanManifest;
101
143
  source_hash: string;
102
144
  compiler_version: typeof DURABLE_RECIPE_COMPILER_VERSION;
103
145
  created_at: string;
@@ -176,7 +218,7 @@ export type WorkflowCallInput = {
176
218
  workflow_id?: string;
177
219
  workflow_name?: string;
178
220
  input?: Record<string, unknown>;
179
- mode?: WorkflowMode;
221
+ mode: WorkflowMode;
180
222
  idempotency_key?: string;
181
223
  };
182
224
  type WorkflowFunction = (context: Record<string, unknown>) => unknown | Promise<unknown>;
@@ -208,7 +250,17 @@ export type WorkflowToolStepDefinition = {
208
250
  mode?: WorkflowMode;
209
251
  payload: WorkflowFunction;
210
252
  };
211
- export type WorkflowStepDefinition = WorkflowTransformStepDefinition | WorkflowToolStepDefinition;
253
+ export type WorkflowBranchStepDefinition = {
254
+ readonly __oxygen_workflow_step: true;
255
+ kind: "branch";
256
+ id: string;
257
+ description?: string;
258
+ condition: WorkflowFunction;
259
+ then: string;
260
+ else?: string;
261
+ join?: string;
262
+ };
263
+ export type WorkflowStepDefinition = WorkflowTransformStepDefinition | WorkflowToolStepDefinition | WorkflowBranchStepDefinition;
212
264
  export type WorkflowLintIssue = {
213
265
  path: string;
214
266
  code: string;
@@ -271,12 +323,21 @@ export declare function toolStep(input: {
271
323
  mode?: WorkflowMode;
272
324
  payload: WorkflowFunction;
273
325
  }): WorkflowToolStepDefinition;
326
+ export declare function branchStep(input: {
327
+ id: string;
328
+ description?: string;
329
+ condition: WorkflowFunction;
330
+ then: string;
331
+ else?: string;
332
+ join?: string;
333
+ }): WorkflowBranchStepDefinition;
274
334
  export declare function isWorkflowDefinition(value: unknown): value is WorkflowDefinition;
275
335
  export declare function isWorkflowManifest(value: unknown): value is WorkflowManifest;
276
336
  export declare function isRecipeManifest(value: unknown): value is RecipeManifest;
277
337
  export declare function isDurableRecipeManifest(value: unknown): value is RecipeManifest;
278
338
  export declare function isAnyWorkflowManifest(value: unknown): value is AnyWorkflowManifest;
279
- export declare function compileWorkflowDefinition(definition: WorkflowDefinition, options?: {
339
+ export declare function compileWorkflowDefinition(// skipcq: JS-R1005 -- compiler validates workflow metadata, trigger, steps, branch targets, and defaults together.
340
+ definition: WorkflowDefinition, options?: {
280
341
  source?: string;
281
342
  sourceHash?: string;
282
343
  createdAt?: Date;
@@ -289,6 +350,7 @@ export declare function buildRecipeManifest(input: {
289
350
  inputSchema?: JsonSchema;
290
351
  bundle: string;
291
352
  toolsUsed: string[];
353
+ visualPlan?: RecipeVisualPlanManifest;
292
354
  sourceHash?: string;
293
355
  createdAt?: Date;
294
356
  }): RecipeManifest;
@@ -370,6 +432,7 @@ export declare const workflowCallSchema: {
370
432
  readonly type: "string";
371
433
  };
372
434
  };
435
+ readonly required: readonly ["mode"];
373
436
  };
374
437
  export declare const workflowEventEmitSchema: {
375
438
  readonly $schema: "https://json-schema.org/draft/2020-12/schema";
@@ -405,7 +468,7 @@ export declare const workflowEventEmitSchema: {
405
468
  readonly enum: readonly ["dry_run", "live", "smoke_test"];
406
469
  };
407
470
  };
408
- readonly required: readonly ["source", "event", "payload"];
471
+ readonly required: readonly ["source", "event", "payload", "mode"];
409
472
  };
410
473
  export declare const workflowTriggerSchema: {
411
474
  readonly $schema: "https://json-schema.org/draft/2020-12/schema";
@@ -528,6 +591,7 @@ export declare function getWorkflowSchema(subject?: "apply" | "call" | "event" |
528
591
  readonly type: "string";
529
592
  };
530
593
  };
594
+ readonly required: readonly ["mode"];
531
595
  } | {
532
596
  readonly $schema: "https://json-schema.org/draft/2020-12/schema";
533
597
  readonly title: "OXYGEN Workflow Event Emit Input";
@@ -562,7 +626,7 @@ export declare function getWorkflowSchema(subject?: "apply" | "call" | "event" |
562
626
  readonly enum: readonly ["dry_run", "live", "smoke_test"];
563
627
  };
564
628
  };
565
- readonly required: readonly ["source", "event", "payload"];
629
+ readonly required: readonly ["source", "event", "payload", "mode"];
566
630
  } | {
567
631
  readonly $schema: "https://json-schema.org/draft/2020-12/schema";
568
632
  readonly title: "OXYGEN Workflow Trigger";
@@ -667,6 +731,7 @@ export declare function getWorkflowSchema(subject?: "apply" | "call" | "event" |
667
731
  readonly type: "string";
668
732
  };
669
733
  };
734
+ readonly required: readonly ["mode"];
670
735
  };
671
736
  event: {
672
737
  readonly $schema: "https://json-schema.org/draft/2020-12/schema";
@@ -702,7 +767,7 @@ export declare function getWorkflowSchema(subject?: "apply" | "call" | "event" |
702
767
  readonly enum: readonly ["dry_run", "live", "smoke_test"];
703
768
  };
704
769
  };
705
- readonly required: readonly ["source", "event", "payload"];
770
+ readonly required: readonly ["source", "event", "payload", "mode"];
706
771
  };
707
772
  trigger: {
708
773
  readonly $schema: "https://json-schema.org/draft/2020-12/schema";
@@ -115,6 +115,18 @@ export function toolStep(input) {
115
115
  payload: input.payload,
116
116
  };
117
117
  }
118
+ export function branchStep(input) {
119
+ return {
120
+ __oxygen_workflow_step: true,
121
+ kind: "branch",
122
+ id: input.id,
123
+ ...(input.description ? { description: input.description } : {}),
124
+ condition: input.condition,
125
+ then: input.then,
126
+ ...(input.else !== undefined ? { else: input.else } : {}),
127
+ ...(input.join !== undefined ? { join: input.join } : {}),
128
+ };
129
+ }
118
130
  export function isWorkflowDefinition(value) {
119
131
  return isRecord(value) && value.__oxygen_workflow_definition === true;
120
132
  }
@@ -134,7 +146,8 @@ export function isDurableRecipeManifest(value) {
134
146
  export function isAnyWorkflowManifest(value) {
135
147
  return isWorkflowManifest(value) || isRecipeManifest(value);
136
148
  }
137
- export function compileWorkflowDefinition(definition, options = {}) {
149
+ export function compileWorkflowDefinition(// skipcq: JS-R1005 -- compiler validates workflow metadata, trigger, steps, branch targets, and defaults together.
150
+ definition, options = {}) {
138
151
  const sourceHash = options.sourceHash
139
152
  ?? hashWorkflowSource(options.source ?? JSON.stringify(definition, workflowJsonReplacer));
140
153
  const manifest = {
@@ -156,6 +169,17 @@ export function compileWorkflowDefinition(definition, options = {}) {
156
169
  run_source: serializeWorkflowFunction(step.run, `steps.${step.id}.run`),
157
170
  };
158
171
  }
172
+ if (step.kind === "branch") {
173
+ return {
174
+ kind: "branch",
175
+ id: step.id,
176
+ ...(step.description ? { description: step.description } : {}),
177
+ condition_source: serializeWorkflowFunction(step.condition, `steps.${step.id}.condition`),
178
+ then_id: step.then,
179
+ ...(step.else !== undefined ? { else_id: step.else } : {}),
180
+ ...(step.join !== undefined ? { join_id: step.join } : {}),
181
+ };
182
+ }
159
183
  return {
160
184
  kind: "tool",
161
185
  id: step.id,
@@ -192,6 +216,7 @@ export function buildRecipeManifest(input) {
192
216
  bundle,
193
217
  bundle_format: "esm",
194
218
  tools_used: Array.from(new Set((input.toolsUsed ?? []).filter(Boolean))).sort(),
219
+ ...(input.visualPlan ? { visual_plan: input.visualPlan } : {}),
195
220
  source_hash: sourceHash,
196
221
  compiler_version: DURABLE_RECIPE_COMPILER_VERSION,
197
222
  created_at: (input.createdAt ?? new Date()).toISOString(),
@@ -233,6 +258,15 @@ value, options = {}) {
233
258
  add("$.steps", "missing_steps", "At least one workflow step is required.");
234
259
  }
235
260
  else {
261
+ // Pre-pass: collect id → declared-index so branch targets can be
262
+ // validated for existence + forward-only direction in the main pass.
263
+ const idToIndex = new Map();
264
+ value.steps.forEach((step, index) => {
265
+ if (isRecord(step) && isNonEmptyString(step.id)) {
266
+ if (!idToIndex.has(step.id))
267
+ idToIndex.set(step.id, index);
268
+ }
269
+ });
236
270
  const ids = new Set();
237
271
  value.steps.forEach((step, index) => {
238
272
  const path = `$.steps.${index}`;
@@ -287,7 +321,23 @@ value, options = {}) {
287
321
  }
288
322
  return;
289
323
  }
290
- add(`${path}.kind`, "invalid_step_kind", "Step kind must be transform or tool.");
324
+ if (step.kind === "branch") {
325
+ if (!isNonEmptyString(step.condition_source)) {
326
+ add(`${path}.condition_source`, "missing_step_code", "Branch step condition_source is required.");
327
+ }
328
+ else {
329
+ validatePureFunctionSource(step.condition_source, `${path}.condition_source`, add);
330
+ }
331
+ validateBranchTarget(step.then_id, `${path}.then_id`, "then_id", index, idToIndex, add);
332
+ if (step.else_id !== undefined) {
333
+ validateBranchTarget(step.else_id, `${path}.else_id`, "else_id", index, idToIndex, add);
334
+ }
335
+ if (step.join_id !== undefined) {
336
+ validateBranchTarget(step.join_id, `${path}.join_id`, "join_id", index, idToIndex, add);
337
+ }
338
+ return;
339
+ }
340
+ add(`${path}.kind`, "invalid_step_kind", "Step kind must be transform, tool, or branch.");
291
341
  });
292
342
  }
293
343
  if (!isNonEmptyString(value.source_hash)) {
@@ -364,11 +414,97 @@ value, options = {}) {
364
414
  else {
365
415
  add("$.tools_used", "missing_recipe_tools", "Durable recipes must declare at least one allowed tool.");
366
416
  }
417
+ const declaredTools = Array.isArray(value.tools_used)
418
+ ? new Set(value.tools_used.filter((toolId) => typeof toolId === "string" && toolId.trim().length > 0))
419
+ : new Set();
420
+ if (value.visual_plan !== undefined) {
421
+ validateRecipeVisualPlan(value.visual_plan, "$.visual_plan", declaredTools, add);
422
+ }
367
423
  if (!isNonEmptyString(value.source_hash)) {
368
424
  add("$.source_hash", "missing_source_hash", "Recipe manifest source_hash is required.");
369
425
  }
370
426
  return { ok: issues.length === 0, issues };
371
427
  }
428
+ function validateRecipeVisualPlan(value, path, declaredTools, add) {
429
+ if (!isRecord(value)) {
430
+ add(path, "invalid_visual_plan", "Recipe visual_plan must be an object.");
431
+ return;
432
+ }
433
+ if (value.version !== 1) {
434
+ add(`${path}.version`, "invalid_visual_plan_version", "Recipe visual_plan version must be 1.");
435
+ }
436
+ if (!Array.isArray(value.steps)) {
437
+ add(`${path}.steps`, "invalid_visual_plan_steps", "Recipe visual_plan steps must be an array.");
438
+ return;
439
+ }
440
+ if (value.steps.length === 0) {
441
+ add(`${path}.steps`, "missing_visual_plan_steps", "Recipe visual_plan must declare at least one step.");
442
+ return;
443
+ }
444
+ const idToIndex = new Map();
445
+ value.steps.forEach((step, index) => {
446
+ if (isRecord(step) && isNonEmptyString(step.id) && !idToIndex.has(step.id)) {
447
+ idToIndex.set(step.id, index);
448
+ }
449
+ });
450
+ const ids = new Set();
451
+ value.steps.forEach((step, index) => {
452
+ const stepPath = `${path}.steps.${index}`;
453
+ if (!isRecord(step)) {
454
+ add(stepPath, "invalid_visual_plan_step", "Recipe visual_plan step must be an object.");
455
+ return;
456
+ }
457
+ if (!isNonEmptyString(step.id)) {
458
+ add(`${stepPath}.id`, "invalid_visual_plan_step_id", "Recipe visual_plan step id is required.");
459
+ }
460
+ else if (ids.has(step.id)) {
461
+ add(`${stepPath}.id`, "duplicate_visual_plan_step_id", `Recipe visual_plan step id '${step.id}' is duplicated.`);
462
+ }
463
+ else {
464
+ ids.add(step.id);
465
+ }
466
+ if (!isNonEmptyString(step.label)) {
467
+ add(`${stepPath}.label`, "invalid_visual_plan_step_label", "Recipe visual_plan step label is required.");
468
+ }
469
+ if (step.description !== undefined && typeof step.description !== "string") {
470
+ add(`${stepPath}.description`, "invalid_visual_plan_description", "Recipe visual_plan description must be a string.");
471
+ }
472
+ if (step.checkpoint_id !== undefined && !isNonEmptyString(step.checkpoint_id)) {
473
+ add(`${stepPath}.checkpoint_id`, "invalid_visual_plan_checkpoint", "Recipe visual_plan checkpoint_id must be a non-empty string.");
474
+ }
475
+ if (step.kind === "tool") {
476
+ if (!isNonEmptyString(step.tool)) {
477
+ add(`${stepPath}.tool`, "missing_visual_plan_tool", "Recipe visual_plan tool step requires a tool id.");
478
+ }
479
+ else if (declaredTools.size > 0 && !declaredTools.has(step.tool)) {
480
+ add(`${stepPath}.tool`, "visual_plan_tool_not_declared", `Recipe visual_plan tool '${step.tool}' must be present in tools_used.`);
481
+ }
482
+ if (step.effect !== undefined
483
+ && step.effect !== "none"
484
+ && step.effect !== "external_read"
485
+ && step.effect !== "external_write") {
486
+ add(`${stepPath}.effect`, "invalid_visual_plan_effect", "Recipe visual_plan tool effect is invalid.");
487
+ }
488
+ return;
489
+ }
490
+ if (step.kind === "step")
491
+ return;
492
+ if (step.kind === "branch") {
493
+ if (step.condition_label !== undefined && typeof step.condition_label !== "string") {
494
+ add(`${stepPath}.condition_label`, "invalid_visual_plan_condition", "Recipe visual_plan condition_label must be a string.");
495
+ }
496
+ validateBranchTarget(step.then_id, `${stepPath}.then_id`, "then_id", index, idToIndex, add);
497
+ if (step.else_id !== undefined) {
498
+ validateBranchTarget(step.else_id, `${stepPath}.else_id`, "else_id", index, idToIndex, add);
499
+ }
500
+ if (step.join_id !== undefined) {
501
+ validateBranchTarget(step.join_id, `${stepPath}.join_id`, "join_id", index, idToIndex, add);
502
+ }
503
+ return;
504
+ }
505
+ add(`${stepPath}.kind`, "invalid_visual_plan_step_kind", "Recipe visual_plan step kind must be tool, step, or branch.");
506
+ });
507
+ }
372
508
  export function assertRecipeManifest(value, options = {}) {
373
509
  const result = lintRecipeManifest(value, options);
374
510
  if (result.ok)
@@ -710,6 +846,7 @@ export const workflowCallSchema = {
710
846
  mode: { enum: ["dry_run", "live", "smoke_test"] },
711
847
  idempotency_key: { type: "string" },
712
848
  },
849
+ required: ["mode"],
713
850
  };
714
851
  export const workflowEventEmitSchema = {
715
852
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -726,7 +863,7 @@ export const workflowEventEmitSchema = {
726
863
  idempotency_key: { type: "string" },
727
864
  mode: { enum: ["dry_run", "live", "smoke_test"] },
728
865
  },
729
- required: ["source", "event", "payload"],
866
+ required: ["source", "event", "payload", "mode"],
730
867
  };
731
868
  export const workflowTriggerSchema = {
732
869
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -999,6 +1136,20 @@ function serializeWorkflowFunction(fn, path) {
999
1136
  }
1000
1137
  return source;
1001
1138
  }
1139
+ function validateBranchTarget(target, path, fieldLabel, branchIndex, idToIndex, add) {
1140
+ if (typeof target !== "string" || target.trim().length === 0) {
1141
+ add(path, `missing_branch_${fieldLabel}`, `Branch step ${fieldLabel} is required.`);
1142
+ return;
1143
+ }
1144
+ const targetIndex = idToIndex.get(target);
1145
+ if (targetIndex === undefined) {
1146
+ add(path, "invalid_branch_target", `Branch ${fieldLabel} references unknown step id '${target}'.`);
1147
+ return;
1148
+ }
1149
+ if (targetIndex <= branchIndex) {
1150
+ add(path, "branch_back_jump", `Branch ${fieldLabel} must reference a step that comes after the branch in declared order.`);
1151
+ }
1152
+ }
1002
1153
  function validatePureFunctionSource(source, path, add) {
1003
1154
  if (!source.trim()) {
1004
1155
  add(path, "empty_function_source", "Workflow function source cannot be empty.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.109.11",
3
+ "version": "1.123.1",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",