@withone/cli 1.24.0 → 1.26.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.
|
@@ -914,7 +914,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
|
|
|
914
914
|
if (flowStack.includes(resolvedKey)) {
|
|
915
915
|
throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
|
|
916
916
|
}
|
|
917
|
-
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-
|
|
917
|
+
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-3DCROTPW.js");
|
|
918
918
|
const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
|
|
919
919
|
const subContext = await executeFlow(
|
|
920
920
|
subFlow,
|
|
@@ -1027,6 +1027,41 @@ async function executeBashStep(step, context, options) {
|
|
|
1027
1027
|
response: { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 }
|
|
1028
1028
|
};
|
|
1029
1029
|
}
|
|
1030
|
+
function describe(value) {
|
|
1031
|
+
if (value === null) return "null";
|
|
1032
|
+
if (Array.isArray(value)) return `array (${JSON.stringify(value)})`;
|
|
1033
|
+
if (typeof value === "object") return `object (${JSON.stringify(value)})`;
|
|
1034
|
+
return `${typeof value} (${JSON.stringify(value)})`;
|
|
1035
|
+
}
|
|
1036
|
+
function isMissing(value) {
|
|
1037
|
+
if (value === void 0 || value === null) return true;
|
|
1038
|
+
if (typeof value === "string" && value.length === 0) return true;
|
|
1039
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
function explainMissing(selector, context) {
|
|
1043
|
+
const parts = selector.slice(2).split(".");
|
|
1044
|
+
if (parts[0] !== "steps" || parts.length < 2) return "";
|
|
1045
|
+
const stepId = parts[1].replace(/\[.*$/, "");
|
|
1046
|
+
const upstream = context.steps[stepId];
|
|
1047
|
+
if (!upstream) return ` (upstream step "${stepId}" has not run)`;
|
|
1048
|
+
if (upstream.status === "skipped") return ` (upstream step "${stepId}" was skipped)`;
|
|
1049
|
+
if (upstream.status === "failed") return ` (upstream step "${stepId}" failed: ${upstream.error ?? "unknown error"})`;
|
|
1050
|
+
if (upstream.status === "timeout") return ` (upstream step "${stepId}" timed out)`;
|
|
1051
|
+
return "";
|
|
1052
|
+
}
|
|
1053
|
+
function checkRequires(step, context) {
|
|
1054
|
+
if (!step.requires || step.requires.length === 0) return;
|
|
1055
|
+
for (const selector of step.requires) {
|
|
1056
|
+
const value = resolveSelector(selector, context);
|
|
1057
|
+
if (isMissing(value)) {
|
|
1058
|
+
const why = explainMissing(selector, context);
|
|
1059
|
+
throw new Error(
|
|
1060
|
+
`Step "${step.id}" requires ${selector} but it resolved to ${value === void 0 ? "undefined" : value === null ? "null" : Array.isArray(value) ? "an empty array" : "an empty string"}${why}`
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1030
1065
|
async function executeSingleStep(step, context, api, permissions, allowedActionIds, options, flowStack = []) {
|
|
1031
1066
|
if (step.if) {
|
|
1032
1067
|
const condResult = evaluateExpression(step.if, context);
|
|
@@ -1060,6 +1095,7 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
|
|
|
1060
1095
|
});
|
|
1061
1096
|
await sleep2(delay);
|
|
1062
1097
|
}
|
|
1098
|
+
checkRequires(step, context);
|
|
1063
1099
|
if (options.mock && (step.type === "action" || step.type === "paginate" || step.type === "bash")) {
|
|
1064
1100
|
const resolvedConfig = step[step.type] ? resolveValue(step[step.type], context) : {};
|
|
1065
1101
|
options.onEvent?.({ event: "step:mock", stepId: step.id, type: step.type, config: resolvedConfig });
|
|
@@ -1188,18 +1224,68 @@ async function executeSteps(steps, context, api, permissions, allowedActionIds,
|
|
|
1188
1224
|
return results;
|
|
1189
1225
|
}
|
|
1190
1226
|
async function executeFlow(flow, inputs, api, permissions, allowedActionIds, options = {}, resumeState, flowStack = []) {
|
|
1191
|
-
for (const [name, decl] of Object.entries(flow.inputs)) {
|
|
1192
|
-
if (decl.required !== false && inputs[name] === void 0 && decl.default === void 0) {
|
|
1193
|
-
throw new Error(`Missing required input: "${name}" \u2014 ${decl.description || ""}`);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
1227
|
const resolvedInputs = {};
|
|
1197
1228
|
for (const [name, decl] of Object.entries(flow.inputs)) {
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1229
|
+
const provided = inputs[name];
|
|
1230
|
+
const isMissing2 = provided === void 0 || provided === null;
|
|
1231
|
+
if (isMissing2) {
|
|
1232
|
+
if (decl.required !== false && decl.default === void 0) {
|
|
1233
|
+
throw new Error(`Missing required input: "${name}"${decl.description ? ` \u2014 ${decl.description}` : ""}`);
|
|
1234
|
+
}
|
|
1235
|
+
if (decl.default !== void 0) {
|
|
1236
|
+
resolvedInputs[name] = decl.default;
|
|
1237
|
+
}
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
let value = provided;
|
|
1241
|
+
switch (decl.type) {
|
|
1242
|
+
case "string":
|
|
1243
|
+
if (typeof value !== "string") value = String(value);
|
|
1244
|
+
break;
|
|
1245
|
+
case "number":
|
|
1246
|
+
if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value))) {
|
|
1247
|
+
value = Number(value);
|
|
1248
|
+
}
|
|
1249
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
1250
|
+
throw new Error(`Input "${name}" must be a number, got ${describe(provided)}`);
|
|
1251
|
+
}
|
|
1252
|
+
break;
|
|
1253
|
+
case "boolean":
|
|
1254
|
+
if (value === "true" || value === "1" || value === 1) value = true;
|
|
1255
|
+
else if (value === "false" || value === "0" || value === 0) value = false;
|
|
1256
|
+
if (typeof value !== "boolean") {
|
|
1257
|
+
throw new Error(`Input "${name}" must be a boolean, got ${describe(provided)}`);
|
|
1258
|
+
}
|
|
1259
|
+
break;
|
|
1260
|
+
case "array":
|
|
1261
|
+
if (typeof value === "string") {
|
|
1262
|
+
try {
|
|
1263
|
+
value = JSON.parse(value);
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (!Array.isArray(value)) {
|
|
1268
|
+
throw new Error(`Input "${name}" must be an array, got ${describe(provided)}`);
|
|
1269
|
+
}
|
|
1270
|
+
break;
|
|
1271
|
+
case "object":
|
|
1272
|
+
if (typeof value === "string") {
|
|
1273
|
+
try {
|
|
1274
|
+
value = JSON.parse(value);
|
|
1275
|
+
} catch {
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1279
|
+
throw new Error(`Input "${name}" must be an object, got ${describe(provided)}`);
|
|
1280
|
+
}
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1283
|
+
if (Array.isArray(decl.enum) && decl.enum.length > 0) {
|
|
1284
|
+
if (!decl.enum.some((allowed) => allowed === value)) {
|
|
1285
|
+
throw new Error(`Input "${name}" must be one of ${JSON.stringify(decl.enum)}, got ${JSON.stringify(value)}`);
|
|
1286
|
+
}
|
|
1202
1287
|
}
|
|
1288
|
+
resolvedInputs[name] = value;
|
|
1203
1289
|
}
|
|
1204
1290
|
const context = resumeState?.context || {
|
|
1205
1291
|
input: resolvedInputs,
|
|
@@ -1207,6 +1293,7 @@ async function executeFlow(flow, inputs, api, permissions, allowedActionIds, opt
|
|
|
1207
1293
|
steps: {},
|
|
1208
1294
|
loop: {}
|
|
1209
1295
|
};
|
|
1296
|
+
context.input = resolvedInputs;
|
|
1210
1297
|
const completedStepIds = resumeState ? new Set(resumeState.completedSteps) : void 0;
|
|
1211
1298
|
if (options.dryRun && !options.mock) {
|
|
1212
1299
|
options.onEvent?.({
|
|
@@ -1271,7 +1358,8 @@ var FLOW_SCHEMA = {
|
|
|
1271
1358
|
required: { type: "boolean", required: false, description: "Whether this input must be provided" },
|
|
1272
1359
|
default: { type: "unknown", required: false, description: "Default value if not provided" },
|
|
1273
1360
|
description: { type: "string", required: false, description: "Human-readable description" },
|
|
1274
|
-
connection: { type: "object", required: false, description: 'Connection metadata: { platform: "gmail" } \u2014 enables auto-resolution' }
|
|
1361
|
+
connection: { type: "object", required: false, description: 'Connection metadata: { platform: "gmail" } \u2014 enables auto-resolution' },
|
|
1362
|
+
enum: { type: "array", required: false, description: "Allowed values. Resolved input must equal one of these (post-coercion)." }
|
|
1275
1363
|
},
|
|
1276
1364
|
stepCommonFields: {
|
|
1277
1365
|
id: { type: "string", required: true, description: "Unique step identifier (used in selectors)" },
|
|
@@ -1279,7 +1367,8 @@ var FLOW_SCHEMA = {
|
|
|
1279
1367
|
type: { type: "string", required: true, description: "Step type (determines which config object is required)" },
|
|
1280
1368
|
if: { type: "string", required: false, description: "JS expression \u2014 skip step if falsy" },
|
|
1281
1369
|
unless: { type: "string", required: false, description: "JS expression \u2014 skip step if truthy" },
|
|
1282
|
-
timeoutMs: { type: "number", required: false, description: 'Wall-clock timeout (ms). On expiry the step fails with errorCode:"TIMEOUT"; with onError:continue the result gets status:"timeout".' }
|
|
1370
|
+
timeoutMs: { type: "number", required: false, description: 'Wall-clock timeout (ms). On expiry the step fails with errorCode:"TIMEOUT"; with onError:continue the result gets status:"timeout".' },
|
|
1371
|
+
requires: { type: "array", required: false, description: "Presence preconditions: array of $.input.X or $.steps.X.output... selectors that must resolve to a non-empty value before the step runs. Failures honor onError." }
|
|
1283
1372
|
},
|
|
1284
1373
|
stepTypes: [
|
|
1285
1374
|
{
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
loadFlowWithMeta,
|
|
17
17
|
resolveFlowPath,
|
|
18
18
|
saveFlow
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-ZOIXA7MV.js";
|
|
20
20
|
|
|
21
21
|
// src/index.ts
|
|
22
22
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -2281,6 +2281,11 @@ function validateFlowSchema(flow2) {
|
|
|
2281
2281
|
if (!d.type || !FLOW_SCHEMA.validInputTypes.includes(d.type)) {
|
|
2282
2282
|
errors.push({ path: `${prefix}.type`, message: `Input type must be one of: ${FLOW_SCHEMA.validInputTypes.join(", ")}` });
|
|
2283
2283
|
}
|
|
2284
|
+
if (d.enum !== void 0) {
|
|
2285
|
+
if (!Array.isArray(d.enum) || d.enum.length === 0) {
|
|
2286
|
+
errors.push({ path: `${prefix}.enum`, message: '"enum" must be a non-empty array of allowed values' });
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2284
2289
|
if (d.connection !== void 0) {
|
|
2285
2290
|
if (!d.connection || typeof d.connection !== "object") {
|
|
2286
2291
|
errors.push({ path: `${prefix}.connection`, message: "Connection metadata must be an object" });
|
|
@@ -2320,6 +2325,20 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
2320
2325
|
errors.push({ path: `${path8}.type`, message: `Step type must be one of: ${validTypes.join(", ")}` });
|
|
2321
2326
|
continue;
|
|
2322
2327
|
}
|
|
2328
|
+
if (s.requires !== void 0) {
|
|
2329
|
+
if (!Array.isArray(s.requires)) {
|
|
2330
|
+
errors.push({ path: `${path8}.requires`, message: '"requires" must be an array of selector strings (e.g. ["$.steps.foo.output.bar"])' });
|
|
2331
|
+
} else {
|
|
2332
|
+
for (let r = 0; r < s.requires.length; r++) {
|
|
2333
|
+
const sel = s.requires[r];
|
|
2334
|
+
if (typeof sel !== "string") {
|
|
2335
|
+
errors.push({ path: `${path8}.requires[${r}]`, message: '"requires" entry must be a selector string' });
|
|
2336
|
+
} else if (!sel.startsWith("$.")) {
|
|
2337
|
+
errors.push({ path: `${path8}.requires[${r}]`, message: `"requires" entry "${sel}" must be a selector starting with "$." (e.g. "$.steps.foo.output.bar")` });
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2323
2342
|
if (s.onError && typeof s.onError === "object") {
|
|
2324
2343
|
const oe = s.onError;
|
|
2325
2344
|
if (!FLOW_SCHEMA.errorStrategies.includes(oe.strategy)) {
|
|
@@ -2529,6 +2548,14 @@ function validateSelectorReferences(flow2) {
|
|
|
2529
2548
|
function checkStep(step, pathPrefix, preceding2) {
|
|
2530
2549
|
if (step.if) checkSelectors(extractSelectors(step.if), `${pathPrefix}.if`, preceding2);
|
|
2531
2550
|
if (step.unless) checkSelectors(extractSelectors(step.unless), `${pathPrefix}.unless`, preceding2);
|
|
2551
|
+
if (Array.isArray(step.requires)) {
|
|
2552
|
+
const reqs = step.requires;
|
|
2553
|
+
reqs.forEach((sel, i) => {
|
|
2554
|
+
if (typeof sel === "string" && sel.startsWith("$.")) {
|
|
2555
|
+
checkSelectors([sel], `${pathPrefix}.requires[${i}]`, preceding2);
|
|
2556
|
+
}
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2532
2559
|
const descriptor = getStepTypeDescriptor(step.type);
|
|
2533
2560
|
if (descriptor) {
|
|
2534
2561
|
const config2 = step[descriptor.configKey];
|
package/package.json
CHANGED
|
@@ -186,9 +186,29 @@ one --agent flow execute <key> -i connectionKey=xxx -i param=value
|
|
|
186
186
|
| `default` | Default value if not provided |
|
|
187
187
|
| `description` | Human-readable description |
|
|
188
188
|
| `connection` | `{ "platform": "gmail" }` — enables auto-resolution |
|
|
189
|
+
| `enum` | Array of allowed values; rejected if input doesn't match (post-coercion) |
|
|
189
190
|
|
|
190
191
|
Connection inputs with a `connection` field auto-resolve if the user has exactly one connection for that platform.
|
|
191
192
|
|
|
193
|
+
**Validation, coercion, and enums.** At flow start the engine validates every declared input:
|
|
194
|
+
|
|
195
|
+
1. **Required check** — `required: true` (the default) inputs without a value or `default` cause `Missing required input: "X"`.
|
|
196
|
+
2. **Type coercion** — narrow, bidirectional fixes only:
|
|
197
|
+
- `number`: numeric strings (`"5"`) become `5`. Non-numeric strings throw.
|
|
198
|
+
- `boolean`: `"true"`/`"1"`/`1` → `true`; `"false"`/`"0"`/`0` → `false`. Anything else throws.
|
|
199
|
+
- `array` / `object`: JSON strings are parsed. Non-JSON throws.
|
|
200
|
+
- `string`: anything else is `String(value)`-coerced.
|
|
201
|
+
3. **Enum check** — if `enum` is set, the (coerced) value must be `===` one of the allowed entries. Errors quote both the allowed list and the actual value.
|
|
202
|
+
|
|
203
|
+
This eliminates the per-flow `if (!$.input.x) throw ...` boilerplate. Errors look like:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
Input "tier" must be a number, got string ("lots")
|
|
207
|
+
Input "stage" must be one of ["pre_seed","seed","series_a"], got "ipo"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The same checks run when a step calls a sub-flow, so type/enum guarantees hold across the call boundary.
|
|
211
|
+
|
|
192
212
|
## Selector Syntax
|
|
193
213
|
|
|
194
214
|
| Pattern | Resolves To |
|
|
@@ -424,6 +444,35 @@ Alternatively, pass values as environment variables (also shell-safe) and refere
|
|
|
424
444
|
}
|
|
425
445
|
```
|
|
426
446
|
|
|
447
|
+
## Step Input Contracts (`requires`)
|
|
448
|
+
|
|
449
|
+
Declare the data a step depends on so the engine fails fast — with a useful error — when an upstream value is missing. Without `requires`, a skipped or failed upstream step silently leaves `undefined` in the context and the consumer either crashes deep in user code or burns an LLM call on empty input.
|
|
450
|
+
|
|
451
|
+
```json
|
|
452
|
+
{
|
|
453
|
+
"id": "summarizeFounder",
|
|
454
|
+
"type": "code",
|
|
455
|
+
"requires": [
|
|
456
|
+
"$.steps.fetchProfile.output.bio",
|
|
457
|
+
"$.input.founderName"
|
|
458
|
+
],
|
|
459
|
+
"code": { "module": "lib/summarize.mjs" }
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Each entry is a `$.input.X` or `$.steps.X.output...` selector. A selector is considered missing when it resolves to `undefined`, `null`, `""`, or `[]` (empty objects are allowed). On a miss, the engine throws **before** the step runs:
|
|
464
|
+
|
|
465
|
+
```
|
|
466
|
+
Step "summarizeFounder" requires $.steps.fetchProfile.output.bio but it
|
|
467
|
+
resolved to undefined (upstream step "fetchProfile" was skipped)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
The "because…" suffix tells you exactly why — skipped, failed, or timed out — so you can fix the upstream wiring instead of guessing.
|
|
471
|
+
|
|
472
|
+
`requires` failures honor the step's `onError` strategy: pair `requires` with `onError: { strategy: "continue" }` to skip optional consumers gracefully, or leave the default `fail` to halt the flow on contract violations.
|
|
473
|
+
|
|
474
|
+
Forward references are caught at flow load time: if `requires` points at a step declared after the current step, validation rejects the flow.
|
|
475
|
+
|
|
427
476
|
## Error Handling
|
|
428
477
|
|
|
429
478
|
```json
|