@oxygen-agent/cli 1.365.3 → 1.575.19

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.
Files changed (99) hide show
  1. package/README.md +1 -1
  2. package/dist/column-run-notices.d.ts +11 -0
  3. package/dist/column-run-notices.js +37 -0
  4. package/dist/command-manifest.js +13 -8
  5. package/dist/help.js +78 -16
  6. package/dist/index.js +3873 -514
  7. package/dist/skills.js +106 -1
  8. package/node_modules/@oxygen/formula/dist/coerce.d.ts +8 -0
  9. package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
  10. package/node_modules/@oxygen/formula/dist/evaluate.d.ts +31 -0
  11. package/node_modules/@oxygen/formula/dist/evaluate.js +248 -0
  12. package/node_modules/@oxygen/formula/dist/expression.d.ts +64 -0
  13. package/node_modules/@oxygen/formula/dist/expression.js +428 -0
  14. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +71 -0
  15. package/node_modules/@oxygen/formula/dist/formula-functions.js +1100 -0
  16. package/node_modules/@oxygen/formula/dist/index.d.ts +17 -0
  17. package/node_modules/@oxygen/formula/dist/index.js +17 -0
  18. package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +30 -0
  19. package/node_modules/@oxygen/formula/dist/value-normalizers.js +80 -0
  20. package/node_modules/@oxygen/formula/package.json +26 -0
  21. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +30 -0
  22. package/node_modules/@oxygen/recipe-sdk/dist/index.js +2 -2
  23. package/node_modules/@oxygen/shared/dist/billing-anchors.d.ts +60 -0
  24. package/node_modules/@oxygen/shared/dist/billing-anchors.js +135 -0
  25. package/node_modules/@oxygen/shared/dist/billing.d.ts +101 -5
  26. package/node_modules/@oxygen/shared/dist/billing.js +192 -8
  27. package/node_modules/@oxygen/shared/dist/call-outcomes.d.ts +59 -0
  28. package/node_modules/@oxygen/shared/dist/call-outcomes.js +73 -0
  29. package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
  30. package/node_modules/@oxygen/shared/dist/credit-guidance.js +3 -1
  31. package/node_modules/@oxygen/shared/dist/crm-reply-events.d.ts +35 -0
  32. package/node_modules/@oxygen/shared/dist/crm-reply-events.js +31 -0
  33. package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.d.ts +50 -0
  34. package/node_modules/@oxygen/shared/dist/dial-guardrail-overrides.js +65 -0
  35. package/node_modules/@oxygen/shared/dist/directory.d.ts +1 -1
  36. package/node_modules/@oxygen/shared/dist/directory.js +1 -0
  37. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +17 -1
  38. package/node_modules/@oxygen/shared/dist/hosted-ai.js +52 -3
  39. package/node_modules/@oxygen/shared/dist/index.d.ts +11 -0
  40. package/node_modules/@oxygen/shared/dist/index.js +15 -0
  41. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +77 -0
  42. package/node_modules/@oxygen/shared/dist/langfuse.js +231 -0
  43. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +31 -0
  44. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +56 -0
  45. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -4
  46. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +5 -4
  47. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +22 -0
  48. package/node_modules/@oxygen/shared/dist/linkedin-url.js +7 -4
  49. package/node_modules/@oxygen/shared/dist/log.js +56 -4
  50. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.d.ts +7 -0
  51. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.js +29 -0
  52. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +31 -0
  53. package/node_modules/@oxygen/shared/dist/object-storage.js +61 -0
  54. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +636 -0
  55. package/node_modules/@oxygen/shared/dist/plan-limits.js +199 -0
  56. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +89 -23
  57. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +88 -24
  58. package/node_modules/@oxygen/shared/dist/sequence-crm-events.d.ts +291 -0
  59. package/node_modules/@oxygen/shared/dist/sequence-crm-events.js +224 -0
  60. package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +42 -1
  61. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  62. package/node_modules/@oxygen/shared/dist/sequences.d.ts +287 -24
  63. package/node_modules/@oxygen/shared/dist/sequences.js +940 -60
  64. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +70 -0
  65. package/node_modules/@oxygen/shared/dist/spend-safety.js +106 -0
  66. package/node_modules/@oxygen/shared/dist/tags.d.ts +90 -1
  67. package/node_modules/@oxygen/shared/dist/tags.js +126 -6
  68. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  69. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  70. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.d.ts +1 -1
  71. package/node_modules/@oxygen/shared/dist/workflow-trigger-metadata.js +4 -0
  72. package/node_modules/@oxygen/shared/dist/workspace-agents.d.ts +8 -7
  73. package/node_modules/@oxygen/shared/dist/workspace-agents.js +34 -7
  74. package/node_modules/@oxygen/shared/package.json +95 -0
  75. package/node_modules/@oxygen/workflows/dist/event-dispatch.d.ts +126 -0
  76. package/node_modules/@oxygen/workflows/dist/event-dispatch.js +173 -0
  77. package/node_modules/@oxygen/workflows/dist/graph/expression.d.ts +78 -0
  78. package/node_modules/@oxygen/workflows/dist/graph/expression.js +700 -0
  79. package/node_modules/@oxygen/workflows/dist/graph/index.d.ts +20 -0
  80. package/node_modules/@oxygen/workflows/dist/graph/index.js +20 -0
  81. package/node_modules/@oxygen/workflows/dist/graph/lint.d.ts +4 -0
  82. package/node_modules/@oxygen/workflows/dist/graph/lint.js +812 -0
  83. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +501 -0
  84. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +200 -0
  85. package/node_modules/@oxygen/workflows/dist/graph/params.d.ts +86 -0
  86. package/node_modules/@oxygen/workflows/dist/graph/params.js +173 -0
  87. package/node_modules/@oxygen/workflows/dist/graph/remap.d.ts +48 -0
  88. package/node_modules/@oxygen/workflows/dist/graph/remap.js +213 -0
  89. package/node_modules/@oxygen/workflows/dist/graph/topology.d.ts +46 -0
  90. package/node_modules/@oxygen/workflows/dist/graph/topology.js +280 -0
  91. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +270 -0
  92. package/node_modules/@oxygen/workflows/dist/graph/types.js +93 -0
  93. package/node_modules/@oxygen/workflows/dist/index.d.ts +113 -1
  94. package/node_modules/@oxygen/workflows/dist/index.js +179 -13
  95. package/node_modules/@oxygen/workflows/dist/tool-effects.d.ts +1 -0
  96. package/node_modules/@oxygen/workflows/dist/tool-effects.js +19 -0
  97. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +135 -4
  98. package/node_modules/@oxygen/workflows/package.json +4 -0
  99. package/package.json +10 -5
@@ -0,0 +1,200 @@
1
+ // The published JSON Schema for an oxygen-workflows-v2 manifest, served by
2
+ // `GET /api/cli/workflows/schema` (subject "graph") and by `oxygen workflows
3
+ // schema`. It is the authoring contract an agent reads before writing a graph.
4
+ //
5
+ // Scope: shape, not semantics. It pins the envelope, the node kinds, the edge
6
+ // shape, and the value-ref vocabulary — enough to author a manifest that will be
7
+ // recognized — and deliberately stops there. ./lint.ts stays the authority on
8
+ // per-kind required fields, topology (one trigger, reachability, the one legal
9
+ // back-edge), tool allowlists, formula syntax, and code-source purity; a second
10
+ // full copy of those rules here would drift from the linter that actually runs.
11
+ //
12
+ // Pure data: this module ships to the browser with the rest of the graph barrel.
13
+ import { MAX_WORKFLOW_NODE_RETRY_ATTEMPTS, MAX_WORKFLOW_NODE_RETRY_WAIT_SECONDS, WORKFLOW_GRAPH_COMPILER_VERSION, WORKFLOW_GRAPH_MANIFEST_VERSION, } from "./types.js";
14
+ const valueRefSchema = {
15
+ type: "object",
16
+ description: "How a node input is bound at run time. Scope paths are 'trigger.input.x', 'steps.<node_id>.output.y', or 'loop.<loop_node_id>.item.z'.",
17
+ properties: {
18
+ type: {
19
+ enum: ["literal", "template", "ref", "formula", "context_profile", "context_asset"],
20
+ },
21
+ value: { description: "literal: used verbatim. template: interpolated string, e.g. 'Hi {{ steps.find.output.first_name }}'." },
22
+ path: { type: "string", description: "ref: a scope path. context_*: an optional path inside the context value." },
23
+ expression: { type: "string", description: "formula: an OXYGEN formula-language expression." },
24
+ assetId: { type: "string", description: "context_asset: the Knowledge-layer asset id." },
25
+ },
26
+ required: ["type"],
27
+ };
28
+ const conditionSchema = {
29
+ type: "object",
30
+ description: "A structured predicate: { type: 'group', op: 'all' | 'any', not?: boolean, children: [...] } or { type: 'compare', left, op, right? }. 'is_empty' / 'is_not_empty' take no right operand.",
31
+ properties: {
32
+ type: { enum: ["group", "compare"] },
33
+ op: {
34
+ enum: [
35
+ "all",
36
+ "any",
37
+ "eq",
38
+ "neq",
39
+ "gt",
40
+ "gte",
41
+ "lt",
42
+ "lte",
43
+ "contains",
44
+ "not_contains",
45
+ "starts_with",
46
+ "ends_with",
47
+ "is_empty",
48
+ "is_not_empty",
49
+ "in",
50
+ "not_in",
51
+ "matches",
52
+ ],
53
+ },
54
+ not: { type: "boolean" },
55
+ children: { type: "array", items: { type: "object", additionalProperties: true } },
56
+ left: valueRefSchema,
57
+ right: valueRefSchema,
58
+ },
59
+ required: ["type"],
60
+ };
61
+ const nodeSchema = {
62
+ type: "object",
63
+ additionalProperties: true,
64
+ properties: {
65
+ id: {
66
+ type: "string",
67
+ description: "Unique node id. Becomes a key on the run scope ('steps.<id>.output'), so '__proto__', 'constructor' and 'prototype' are rejected.",
68
+ },
69
+ name: { type: "string" },
70
+ description: { type: "string" },
71
+ kind: {
72
+ enum: ["trigger", "tool", "filter", "switch", "loop", "merge", "set", "wait", "approval", "code", "workflow"],
73
+ },
74
+ ui: {
75
+ type: "object",
76
+ description: "Canvas position. Presentation only — it never affects execution order.",
77
+ properties: { x: { type: "number" }, y: { type: "number" } },
78
+ required: ["x", "y"],
79
+ },
80
+ disabled: { type: "boolean", description: "Kept in the graph but skipped at run time." },
81
+ continue_on_error: {
82
+ type: "boolean",
83
+ description: "Route failures to the 'error' handle instead of failing the run.",
84
+ },
85
+ retry: {
86
+ type: "object",
87
+ description: "Retry this node after a transient failure. It only ever narrows what the "
88
+ + "run would already do: a node whose effect is 'external_write' is never "
89
+ + "auto-retried in a live run whatever this says, because an ambiguous "
90
+ + "network error may have landed and repeating the call could write twice. "
91
+ + "The run's own attempt budget still caps the total.",
92
+ properties: {
93
+ max_attempts: {
94
+ type: "integer",
95
+ minimum: 1,
96
+ maximum: MAX_WORKFLOW_NODE_RETRY_ATTEMPTS,
97
+ description: "Attempts for this node, counting the first.",
98
+ },
99
+ wait_seconds: {
100
+ type: "number",
101
+ minimum: 0,
102
+ maximum: MAX_WORKFLOW_NODE_RETRY_WAIT_SECONDS,
103
+ description: "Fixed pause between attempts. Omit to use the run's own backoff.",
104
+ },
105
+ },
106
+ required: ["max_attempts"],
107
+ },
108
+ tool: { type: "string", description: "tool nodes: the tool id to call." },
109
+ effect: {
110
+ enum: ["none", "external_read", "external_write"],
111
+ description: "tool nodes: must equal the tool's canonical effect.",
112
+ },
113
+ mode: { enum: ["dry_run", "live", "smoke_test"] },
114
+ max_credits: { type: "number", exclusiveMinimum: 0, description: "Per-node managed-credit ceiling." },
115
+ params: {
116
+ type: "object",
117
+ description: "tool nodes: parameter name -> value ref.",
118
+ additionalProperties: valueRefSchema,
119
+ },
120
+ when: conditionSchema,
121
+ cases: {
122
+ type: "array",
123
+ description: "switch nodes: each case id names the outgoing edge handle it owns.",
124
+ items: {
125
+ type: "object",
126
+ properties: { id: { type: "string" }, label: { type: "string" }, when: conditionSchema },
127
+ required: ["id", "label", "when"],
128
+ },
129
+ },
130
+ fallthrough: { type: "boolean", description: "switch nodes: fire every matching case, not only the first." },
131
+ over: valueRefSchema,
132
+ item_name: { type: "string" },
133
+ max_iterations: {
134
+ type: "integer",
135
+ exclusiveMinimum: 0,
136
+ description: "loop nodes: declared iteration cap. Without it the workflow's automation-action estimate can only assume a single pass, and cron cadence checks treat the run cost as unbounded.",
137
+ },
138
+ concurrency: { type: "integer", exclusiveMinimum: 0 },
139
+ strategy: { enum: ["append", "first", "wait_all"], description: "merge nodes: how inbound branches are joined." },
140
+ fields: {
141
+ type: "array",
142
+ description: "set nodes: the object built from bound fields.",
143
+ items: {
144
+ type: "object",
145
+ properties: { key: { type: "string" }, value: valueRefSchema },
146
+ required: ["key", "value"],
147
+ },
148
+ },
149
+ keep_input: { type: "boolean" },
150
+ duration_seconds: { type: "number", exclusiveMinimum: 0, description: "wait nodes: exactly one of duration_seconds or until." },
151
+ until: valueRefSchema,
152
+ run_source: { type: "string", description: "code nodes: sandboxed pure-function source." },
153
+ workflow_id: { type: "string", description: "workflow nodes: the child workflow to run." },
154
+ input: { type: "object", description: "workflow nodes: child input name -> value ref.", additionalProperties: valueRefSchema },
155
+ },
156
+ required: ["id", "name", "kind", "ui"],
157
+ };
158
+ const edgeSchema = {
159
+ type: "object",
160
+ additionalProperties: false,
161
+ properties: {
162
+ id: { type: "string" },
163
+ source: { type: "string" },
164
+ source_handle: {
165
+ type: "string",
166
+ description: "Which output of the source node this edge leaves from: a switch case id, 'loop_body', 'loop_done', 'error', or omitted for the default output.",
167
+ },
168
+ target: { type: "string" },
169
+ },
170
+ required: ["id", "source", "target"],
171
+ };
172
+ export const workflowGraphManifestSchema = {
173
+ $schema: "https://json-schema.org/draft/2020-12/schema",
174
+ title: "OXYGEN Workflow Graph Manifest (oxygen-workflows-v2)",
175
+ description: "A node + edge workflow graph. Exactly one trigger node; every node reachable from it; cycles are illegal except a single back-edge from inside a loop's body to that loop node.",
176
+ type: "object",
177
+ additionalProperties: true,
178
+ properties: {
179
+ manifest_version: { const: WORKFLOW_GRAPH_MANIFEST_VERSION },
180
+ compiler_version: { const: WORKFLOW_GRAPH_COMPILER_VERSION },
181
+ workflow: {
182
+ type: "object",
183
+ properties: {
184
+ id: { type: "string" },
185
+ name: { type: "string" },
186
+ status: { enum: ["active", "disabled"] },
187
+ },
188
+ required: ["id", "name"],
189
+ },
190
+ specification: { type: "string" },
191
+ trigger: { type: "object", additionalProperties: true, description: "See the 'trigger' schema subject." },
192
+ input_schema: { type: "object", additionalProperties: true },
193
+ nodes: { type: "array", items: nodeSchema, minItems: 1 },
194
+ edges: { type: "array", items: edgeSchema },
195
+ max_credits: { type: "number", exclusiveMinimum: 0, description: "Per-RUN managed-credit ceiling." },
196
+ source_hash: { type: "string" },
197
+ created_at: { type: "string" },
198
+ },
199
+ required: ["manifest_version", "workflow", "nodes", "edges", "source_hash", "compiler_version"],
200
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Whether a tool node's inputs are actually filled in — the ONE implementation.
3
+ *
4
+ * This lived only in the editor, which produced the exact divergence the repo
5
+ * spends real effort avoiding elsewhere: the canvas refused a node while
6
+ * `/api/cli/workflows/apply` accepted the same manifest, and a CLI-authored
7
+ * workflow got no check at all. Doctrine is that the CLI does and the web sees;
8
+ * a guarantee the GUI enforces alone is not a guarantee.
9
+ *
10
+ * Kept browser-safe and dependency-free so both sides can call it: the editor
11
+ * over a loaded descriptor, the server linter over the tool registry.
12
+ */
13
+ /**
14
+ * A tool's fields plus whether its schema is CLOSED.
15
+ *
16
+ * The closed flag is separate from the field list because it is a property of the
17
+ * schema, not of any field, and it is optional so a caller that cannot determine it
18
+ * stays on the old permissive behaviour rather than inventing blockers.
19
+ */
20
+ export type ToolParamFields = {
21
+ fields: readonly ToolParamSpec[];
22
+ /** True only when the schema explicitly declares `additionalProperties: false`. */
23
+ closed?: boolean;
24
+ };
25
+ /** The subset of a tool's input schema these rules need. */
26
+ export type ToolParamSpec = {
27
+ name: string;
28
+ required: boolean;
29
+ /** The schema's human title, preferred when naming what is missing. */
30
+ title?: string;
31
+ };
32
+ /**
33
+ * Capabilities needing exactly one of a set of inputs, keyed by tool id.
34
+ *
35
+ * A deliberate table rather than inference. The relationship lives in prose in
36
+ * the field descriptions ("Mutually exclusive with…", "Omit when passing…"), and
37
+ * parsing prose to decide whether a node may save would be fragile and invisible.
38
+ * A short list is reviewable, and every group is checked against the tool's real
39
+ * fields first, so a renamed input degrades to no check rather than a blocker the
40
+ * author cannot clear.
41
+ */
42
+ export declare const ONE_OF_REQUIREMENTS: Record<string, readonly string[][]>;
43
+ /**
44
+ * What is still missing before this node can run, in words an author can act on.
45
+ * Empty means ready.
46
+ */
47
+ export declare function missingToolParams(input: {
48
+ toolId: string;
49
+ fields: readonly ToolParamSpec[];
50
+ params: Record<string, unknown>;
51
+ /**
52
+ * Whether the tool's schema declares `additionalProperties: false`. Only then is
53
+ * an unrecognised key reported — a tool that genuinely accepts extras (a custom
54
+ * HTTP tool) must not be given a blocker its author cannot clear.
55
+ */
56
+ closed?: boolean;
57
+ }): string[];
58
+ /**
59
+ * Bound keys the tool's schema does not define. Empty unless the schema is CLOSED.
60
+ *
61
+ * A key the schema does not define goes nowhere: every one of the 494
62
+ * workflow-callable `oxygen_*` schemas is `additionalProperties: false`, so the
63
+ * route either rejects the call or drops the value — and the canvas meanwhile shows
64
+ * it as configured. Same failure class as a bound-but-empty ref, and the reason a
65
+ * misspelled param name is worth its own message rather than being folded into
66
+ * "still needs X", which reads as the opposite of what happened.
67
+ *
68
+ * Keyed on an EXPLICIT `additionalProperties: false`. JSON Schema's default is open,
69
+ * and a provider or custom HTTP tool that omits the keyword must keep accepting
70
+ * extras rather than acquiring a blocker its author cannot clear.
71
+ */
72
+ export declare function unknownToolParams(input: {
73
+ fields: readonly ToolParamSpec[];
74
+ params: Record<string, unknown>;
75
+ closed?: boolean;
76
+ }): string[];
77
+ /**
78
+ * Whether a binding carries no value the provider will actually receive.
79
+ *
80
+ * Testing presence — `params[name] !== undefined` — was the mistake behind two
81
+ * one-click escapes: choosing "A fixed value" writes `{literal:""}` and blurring
82
+ * an empty JSON box commits `[]`. Both are present, so both cleared the warning,
83
+ * which is worse than an untouched field because the author reads a cleared
84
+ * warning as configured. A bound-but-empty ref is unset.
85
+ */
86
+ export declare function isValueRefEmpty(ref: unknown): boolean;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Capabilities needing exactly one of a set of inputs, keyed by tool id.
3
+ *
4
+ * A deliberate table rather than inference. The relationship lives in prose in
5
+ * the field descriptions ("Mutually exclusive with…", "Omit when passing…"), and
6
+ * parsing prose to decide whether a node may save would be fragile and invisible.
7
+ * A short list is reviewable, and every group is checked against the tool's real
8
+ * fields first, so a renamed input degrades to no check rather than a blocker the
9
+ * author cannot clear.
10
+ */
11
+ export const ONE_OF_REQUIREMENTS = {
12
+ // `leads` is "Omit when passing from_table: true"; `from_table` is mutually
13
+ // exclusive with it. Neither is in `required`, so both were optional and a node
14
+ // with neither saved, armed a cron, and enrolled nobody.
15
+ oxygen_sequences_enroll: [["leads", "from_table"]],
16
+ // Declares no `required` array at all and needs one of the two payload forms.
17
+ oxygen_tables_import_csv: [["csv_text", "csv_base64"]],
18
+ };
19
+ /**
20
+ * What is still missing before this node can run, in words an author can act on.
21
+ * Empty means ready.
22
+ */
23
+ export function missingToolParams(input) {
24
+ const { toolId, fields, params, closed } = input;
25
+ const label = (name) => fields.find((field) => field.name === name)?.title ?? name;
26
+ // An unknown key suppresses the missing-required report, because a misspelled
27
+ // name is simultaneously an unknown key AND the reason a required field looks
28
+ // unset — telling the author "still needs Domain" when they typed `domian` sends
29
+ // them to re-enter a value that is already there. The unknown key is reported
30
+ // separately by unknownToolParams, under its own message.
31
+ if (unknownToolParams(input).length > 0)
32
+ return [];
33
+ const missing = fields
34
+ .filter((field) => field.required && isValueRefEmpty(params[field.name]))
35
+ .map((field) => label(field.name));
36
+ if (missing.length > 0)
37
+ return missing;
38
+ // A capability declaring NOTHING required is the dangerous case, not the safe
39
+ // one: `oxygen_find_email` publishes no `required` array, so a node with every
40
+ // field blank cleared the check, saved, armed a cron and then previewed forever
41
+ // — finding nothing, spending nothing, reporting no error. Silence is the worst
42
+ // failure mode a scheduled workflow has.
43
+ if (fields.length > 0 && fields.every((field) => isValueRefEmpty(params[field.name]))) {
44
+ // ONE composed phrase, not the first three field names. Returning names made
45
+ // the caller render "oxygen_tables_list still needs project." plus two more —
46
+ // three issues naming three OPTIONAL filters, which reads as "fill in all of
47
+ // these" when any one will do. Caught by applying a real graph through the CLI:
48
+ // the rule was right and its wording sent the author the wrong way.
49
+ const examples = fields.slice(0, 3).map((field) => label(field.name));
50
+ return [`at least one input — try ${joinOr(examples)}`];
51
+ }
52
+ // A spend ceiling is required the moment the run goes live, and the schema says
53
+ // so only in prose: max_credits is "Required when mode=live". `mode` defaults to
54
+ // dry_run so a Test passes, and because the workflow's mode overrides whatever
55
+ // the author bound, a live run ALWAYS forces mode=live. Keyed on the field
56
+ // existing: a capability exposes a ceiling because it can spend.
57
+ const cap = fields.find((field) => field.name === "max_credits");
58
+ if (cap && !isPositiveNumberRef(params.max_credits))
59
+ return [label("max_credits")];
60
+ for (const group of ONE_OF_REQUIREMENTS[toolId] ?? []) {
61
+ const known = group.filter((name) => fields.some((field) => field.name === name));
62
+ if (known.length !== group.length)
63
+ continue;
64
+ const selected = group.filter((name) => isOneOfOptionSelected(params[name]));
65
+ if (selected.length === 0)
66
+ return [group.map(label).join(" or ")];
67
+ // The schemas call these mutually exclusive and the routes refuse both
68
+ // ("Pass either from_table: true or leads[] — not both"), so binding both is
69
+ // as unrunnable as binding neither.
70
+ if (selected.length > 1)
71
+ return [`only one of ${group.map(label).join(" or ")}`];
72
+ }
73
+ return [];
74
+ }
75
+ /**
76
+ * Bound keys the tool's schema does not define. Empty unless the schema is CLOSED.
77
+ *
78
+ * A key the schema does not define goes nowhere: every one of the 494
79
+ * workflow-callable `oxygen_*` schemas is `additionalProperties: false`, so the
80
+ * route either rejects the call or drops the value — and the canvas meanwhile shows
81
+ * it as configured. Same failure class as a bound-but-empty ref, and the reason a
82
+ * misspelled param name is worth its own message rather than being folded into
83
+ * "still needs X", which reads as the opposite of what happened.
84
+ *
85
+ * Keyed on an EXPLICIT `additionalProperties: false`. JSON Schema's default is open,
86
+ * and a provider or custom HTTP tool that omits the keyword must keep accepting
87
+ * extras rather than acquiring a blocker its author cannot clear.
88
+ */
89
+ export function unknownToolParams(input) {
90
+ if (input.closed !== true || input.fields.length === 0)
91
+ return [];
92
+ const known = new Set(input.fields.map((field) => field.name));
93
+ return Object.keys(input.params).filter((name) => !known.has(name));
94
+ }
95
+ /**
96
+ * Whether a binding carries no value the provider will actually receive.
97
+ *
98
+ * Testing presence — `params[name] !== undefined` — was the mistake behind two
99
+ * one-click escapes: choosing "A fixed value" writes `{literal:""}` and blurring
100
+ * an empty JSON box commits `[]`. Both are present, so both cleared the warning,
101
+ * which is worse than an untouched field because the author reads a cleared
102
+ * warning as configured. A bound-but-empty ref is unset.
103
+ */
104
+ export function isValueRefEmpty(ref) {
105
+ if (ref === undefined || ref === null)
106
+ return true;
107
+ if (typeof ref !== "object")
108
+ return false;
109
+ const value = ref;
110
+ switch (value.type) {
111
+ case "literal":
112
+ return isEmptyLiteral(value.value);
113
+ case "template":
114
+ return typeof value.value !== "string" || value.value.trim() === "";
115
+ case "formula":
116
+ return typeof value.expression !== "string" || value.expression.trim() === "";
117
+ case "ref":
118
+ return typeof value.path !== "string" || value.path.trim() === "";
119
+ default:
120
+ // context_profile / context_asset always resolve to something.
121
+ return false;
122
+ }
123
+ }
124
+ /** "a, b or c" — an English list, so the phrase reads as prose in the caller. */
125
+ function joinOr(values) {
126
+ if (values.length <= 1)
127
+ return values[0] ?? "";
128
+ return `${values.slice(0, -1).join(", ")} or ${values[values.length - 1]}`;
129
+ }
130
+ function isEmptyLiteral(value) {
131
+ if (value === undefined || value === null)
132
+ return true;
133
+ if (typeof value === "string")
134
+ return value.trim() === "";
135
+ if (Array.isArray(value))
136
+ return value.length === 0;
137
+ if (typeof value === "object")
138
+ return Object.keys(value).length === 0;
139
+ return false;
140
+ }
141
+ /**
142
+ * Whether a one-of option is actually TURNED ON.
143
+ *
144
+ * Stricter than "not empty" in exactly one way: a literal `false` does not select
145
+ * an option. `from_table` is a boolean switch, and toggling it on then off writes
146
+ * `{literal:false}` — present, non-empty, and meaning "do not enroll from the
147
+ * table". Scoped to one-of groups on purpose: a literal `false` elsewhere
148
+ * (`exclude_contacted`) is a real value and must stay one.
149
+ */
150
+ function isOneOfOptionSelected(ref) {
151
+ if (isValueRefEmpty(ref))
152
+ return false;
153
+ const value = ref;
154
+ return !(value.type === "literal" && value.value === false);
155
+ }
156
+ /**
157
+ * A spend ceiling has to be a POSITIVE number, not merely bound.
158
+ *
159
+ * `enforceLiveSpendCap` requires greater than zero and `readNumber` silently
160
+ * drops a non-number, so a cap of "" or 0 reaches the route as no cap at all and
161
+ * the live delivery fails with spend_cap_required — the failure the check exists
162
+ * to prevent. A ref, formula or template is accepted: only a literal can be
163
+ * judged before the run.
164
+ */
165
+ function isPositiveNumberRef(ref) {
166
+ if (isValueRefEmpty(ref))
167
+ return false;
168
+ const value = ref;
169
+ if (value.type !== "literal")
170
+ return true;
171
+ const numeric = typeof value.value === "string" ? Number(value.value) : value.value;
172
+ return typeof numeric === "number" && Number.isFinite(numeric) && numeric > 0;
173
+ }
@@ -0,0 +1,48 @@
1
+ import type { WorkflowCondition, WorkflowGraphNode, WorkflowValueRef } from "./types.js";
2
+ /**
3
+ * Rewriting the node ids a binding points at.
4
+ *
5
+ * Needed by anything that copies part of a graph: a duplicated selection gets
6
+ * fresh node ids, and every binding INSIDE the selection that referenced a member
7
+ * of it must follow. Miss one and the copy silently reads from the original —
8
+ * which is not a crash, not a lint error, and not visible on the canvas. It is a
9
+ * workflow that quietly does the wrong thing, so the mapping surface is exhausted
10
+ * here, in one tested place, rather than inline at the call site.
11
+ *
12
+ * A node id can hide in four shapes:
13
+ * ref `steps.<id>.output.x`, `loop.<id>.item.y`
14
+ * template `"Hi {{ steps.<id>.first_name }}"`
15
+ * literal a structured value carrying `{{ }}` tokens, which resolve
16
+ * condition either side of a compare, at any nesting depth
17
+ *
18
+ * formula a BARE identifier — `score * 2` reads the node named `score`
19
+ *
20
+ * The formula case was originally omitted here on the reasoning that "the formula
21
+ * tokenizer has no dotted paths, so a formula cannot name a node". That was wrong,
22
+ * and an audit disproved it by execution: a workflow formula's identifiers resolve
23
+ * through a scope whose keys ARE node ids, so `referencedNodeIds` reports `score`
24
+ * and the expression evaluates against `steps.score.output`. A duplicated formula
25
+ * therefore read the ORIGINAL node until this case existed.
26
+ */
27
+ /** Old node id → new node id. Ids absent from the map are left alone. */
28
+ export type NodeIdMap = ReadonlyMap<string, string>;
29
+ /** Rewrite the node ids in one scope path, leaving foreign syntax untouched. */
30
+ export declare function remapScopePath(path: string, ids: NodeIdMap): string;
31
+ /**
32
+ * Rewrite node ids inside `{{ ... }}` tokens, leaving every foreign token alone.
33
+ *
34
+ * Same discriminator the evaluator uses: only a token rooted at one of our scope
35
+ * roots is ours. `{{column}}` and `{{RANDOM|a|b}}` are Oxygen merge-tag syntax
36
+ * that travels inside these same strings and must survive untouched.
37
+ */
38
+ export declare function remapTokens(value: string, ids: NodeIdMap): string;
39
+ export declare function remapValueRef(ref: WorkflowValueRef, ids: NodeIdMap): WorkflowValueRef;
40
+ export declare function remapCondition(condition: WorkflowCondition, ids: NodeIdMap): WorkflowCondition;
41
+ /**
42
+ * Every binding in one node, rewritten.
43
+ *
44
+ * Switches on kind exhaustively rather than walking unknown keys: a node's shape
45
+ * is closed, and a generic deep walk would also rewrite things that merely look
46
+ * like paths (a `code` node's JavaScript, a switch case's label).
47
+ */
48
+ export declare function remapNodeRefs(node: WorkflowGraphNode, ids: NodeIdMap): WorkflowGraphNode;