@oxygen-agent/cli 1.109.10 → 1.123.0
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 +1 -1
- package/dist/http-client.js +21 -15
- package/dist/index.js +331 -13
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +56 -0
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +34 -2
- package/node_modules/@oxygen/recipe-sdk/dist/index.js +61 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/billing.js +2 -1
- 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 +73 -8
- package/node_modules/@oxygen/workflows/dist/index.js +154 -3
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/http-client.js
CHANGED
|
@@ -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
|
|
149
|
+
function warnIfCliIsOlderThanApi(serverVersion, apiUrl) {
|
|
150
|
+
if (!serverVersion)
|
|
150
151
|
return;
|
|
151
152
|
if (!isVersionGreater(serverVersion, OXYGEN_VERSION))
|
|
152
153
|
return;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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,
|
|
2452
|
+
await handleAsyncAction("tools search", options, () => {
|
|
2239
2453
|
const params = new URLSearchParams();
|
|
2240
2454
|
params.set("query", query ?? "");
|
|
2241
|
-
|
|
2242
|
-
|
|
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.
|
|
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.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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(
|
|
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
|
|
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(
|
|
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 {};
|
package/dist/runtime.js
ADDED
|
@@ -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: "
|
|
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: "
|
|
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.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.123.0";
|
|
2
2
|
export declare 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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.");
|