@withone/cli 1.23.0 → 1.25.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.
@@ -447,6 +447,22 @@ var execAsync = promisify(exec);
447
447
  function sleep2(ms) {
448
448
  return new Promise((resolve) => setTimeout(resolve, ms));
449
449
  }
450
+ var StepTimeoutError = class extends Error {
451
+ errorCode = "TIMEOUT";
452
+ constructor(stepId, timeoutMs) {
453
+ super(`Step "${stepId}" exceeded timeout of ${timeoutMs}ms`);
454
+ this.name = "StepTimeoutError";
455
+ }
456
+ };
457
+ function withTimeout(promise, timeoutMs, stepId) {
458
+ let timer;
459
+ const timeoutPromise = new Promise((_, reject) => {
460
+ timer = setTimeout(() => reject(new StepTimeoutError(stepId, timeoutMs)), timeoutMs);
461
+ });
462
+ return Promise.race([promise, timeoutPromise]).finally(() => {
463
+ if (timer) clearTimeout(timer);
464
+ });
465
+ }
450
466
  function computeRetryDelay(onError, attempt) {
451
467
  const base = onError.retryDelayMs ?? 1e3;
452
468
  const max = onError.maxDelayMs ?? 3e4;
@@ -898,7 +914,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
898
914
  if (flowStack.includes(resolvedKey)) {
899
915
  throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
900
916
  }
901
- const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-DFUFU2LM.js");
917
+ const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-MXRNWG4Y.js");
902
918
  const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
903
919
  const subContext = await executeFlow(
904
920
  subFlow,
@@ -910,10 +926,35 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
910
926
  void 0,
911
927
  [...flowStack, resolvedKey]
912
928
  );
929
+ const finalStep = subFlow.steps[subFlow.steps.length - 1];
930
+ const finalOutput = finalStep ? subContext.steps[finalStep.id]?.output : void 0;
931
+ let flattenedOutput;
932
+ if (finalOutput && typeof finalOutput === "object" && !Array.isArray(finalOutput)) {
933
+ const collisions = Object.keys(finalOutput).filter(
934
+ (k) => k in subContext.steps
935
+ );
936
+ if (collisions.length > 0) {
937
+ options.onEvent?.({
938
+ event: "flow:warning",
939
+ message: `Sub-flow "${resolvedKey}" final step output fields [${collisions.join(", ")}] collide with sub-step ids \u2014 flattened fields take precedence.`
940
+ });
941
+ }
942
+ flattenedOutput = {
943
+ ...subContext.steps,
944
+ ...finalOutput,
945
+ _steps: subContext.steps
946
+ };
947
+ } else {
948
+ flattenedOutput = {
949
+ ...subContext.steps,
950
+ _steps: subContext.steps,
951
+ ...finalOutput !== void 0 ? { _finalOutput: finalOutput } : {}
952
+ };
953
+ }
913
954
  return {
914
955
  status: "success",
915
- output: subContext.steps,
916
- response: subContext.steps
956
+ output: flattenedOutput,
957
+ response: flattenedOutput
917
958
  };
918
959
  }
919
960
  async function executePaginateStep(step, context, api, permissions, allowedActionIds, options) {
@@ -986,6 +1027,35 @@ async function executeBashStep(step, context, options) {
986
1027
  response: { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 }
987
1028
  };
988
1029
  }
1030
+ function isMissing(value) {
1031
+ if (value === void 0 || value === null) return true;
1032
+ if (typeof value === "string" && value.length === 0) return true;
1033
+ if (Array.isArray(value) && value.length === 0) return true;
1034
+ return false;
1035
+ }
1036
+ function explainMissing(selector, context) {
1037
+ const parts = selector.slice(2).split(".");
1038
+ if (parts[0] !== "steps" || parts.length < 2) return "";
1039
+ const stepId = parts[1].replace(/\[.*$/, "");
1040
+ const upstream = context.steps[stepId];
1041
+ if (!upstream) return ` (upstream step "${stepId}" has not run)`;
1042
+ if (upstream.status === "skipped") return ` (upstream step "${stepId}" was skipped)`;
1043
+ if (upstream.status === "failed") return ` (upstream step "${stepId}" failed: ${upstream.error ?? "unknown error"})`;
1044
+ if (upstream.status === "timeout") return ` (upstream step "${stepId}" timed out)`;
1045
+ return "";
1046
+ }
1047
+ function checkRequires(step, context) {
1048
+ if (!step.requires || step.requires.length === 0) return;
1049
+ for (const selector of step.requires) {
1050
+ const value = resolveSelector(selector, context);
1051
+ if (isMissing(value)) {
1052
+ const why = explainMissing(selector, context);
1053
+ throw new Error(
1054
+ `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}`
1055
+ );
1056
+ }
1057
+ }
1058
+ }
989
1059
  async function executeSingleStep(step, context, api, permissions, allowedActionIds, options, flowStack = []) {
990
1060
  if (step.if) {
991
1061
  const condResult = evaluateExpression(step.if, context);
@@ -1019,6 +1089,7 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
1019
1089
  });
1020
1090
  await sleep2(delay);
1021
1091
  }
1092
+ checkRequires(step, context);
1022
1093
  if (options.mock && (step.type === "action" || step.type === "paginate" || step.type === "bash")) {
1023
1094
  const resolvedConfig = step[step.type] ? resolveValue(step[step.type], context) : {};
1024
1095
  options.onEvent?.({ event: "step:mock", stepId: step.id, type: step.type, config: resolvedConfig });
@@ -1031,47 +1102,37 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
1031
1102
  context.steps[step.id] = result2;
1032
1103
  return result2;
1033
1104
  }
1034
- let result;
1035
- switch (step.type) {
1036
- case "action":
1037
- result = await executeActionStep(step, context, api, permissions, allowedActionIds);
1038
- break;
1039
- case "transform":
1040
- result = executeTransformStep(step, context);
1041
- break;
1042
- case "code":
1043
- result = await executeCodeStep(step, context, options);
1044
- break;
1045
- case "condition":
1046
- result = await executeConditionStep(step, context, api, permissions, allowedActionIds, options, flowStack);
1047
- break;
1048
- case "loop":
1049
- result = await executeLoopStep(step, context, api, permissions, allowedActionIds, options, flowStack);
1050
- break;
1051
- case "parallel":
1052
- result = await executeParallelStep(step, context, api, permissions, allowedActionIds, options, flowStack);
1053
- break;
1054
- case "file-read":
1055
- result = executeFileReadStep(step, context);
1056
- break;
1057
- case "file-write":
1058
- result = executeFileWriteStep(step, context);
1059
- break;
1060
- case "while":
1061
- result = await executeWhileStep(step, context, api, permissions, allowedActionIds, options, flowStack);
1062
- break;
1063
- case "flow":
1064
- result = await executeSubflowStep(step, context, api, permissions, allowedActionIds, options, flowStack);
1065
- break;
1066
- case "paginate":
1067
- result = await executePaginateStep(step, context, api, permissions, allowedActionIds, options);
1068
- break;
1069
- case "bash":
1070
- result = await executeBashStep(step, context, options);
1071
- break;
1072
- default:
1073
- throw new Error(`Unknown step type: ${step.type}`);
1074
- }
1105
+ const dispatch = async () => {
1106
+ switch (step.type) {
1107
+ case "action":
1108
+ return await executeActionStep(step, context, api, permissions, allowedActionIds);
1109
+ case "transform":
1110
+ return executeTransformStep(step, context);
1111
+ case "code":
1112
+ return await executeCodeStep(step, context, options);
1113
+ case "condition":
1114
+ return await executeConditionStep(step, context, api, permissions, allowedActionIds, options, flowStack);
1115
+ case "loop":
1116
+ return await executeLoopStep(step, context, api, permissions, allowedActionIds, options, flowStack);
1117
+ case "parallel":
1118
+ return await executeParallelStep(step, context, api, permissions, allowedActionIds, options, flowStack);
1119
+ case "file-read":
1120
+ return executeFileReadStep(step, context);
1121
+ case "file-write":
1122
+ return executeFileWriteStep(step, context);
1123
+ case "while":
1124
+ return await executeWhileStep(step, context, api, permissions, allowedActionIds, options, flowStack);
1125
+ case "flow":
1126
+ return await executeSubflowStep(step, context, api, permissions, allowedActionIds, options, flowStack);
1127
+ case "paginate":
1128
+ return await executePaginateStep(step, context, api, permissions, allowedActionIds, options);
1129
+ case "bash":
1130
+ return await executeBashStep(step, context, options);
1131
+ default:
1132
+ throw new Error(`Unknown step type: ${step.type}`);
1133
+ }
1134
+ };
1135
+ const result = step.timeoutMs ? await withTimeout(dispatch(), step.timeoutMs, step.id) : await dispatch();
1075
1136
  result.durationMs = Date.now() - startTime;
1076
1137
  if (attempt > 1) {
1077
1138
  result.retries = attempt - 1;
@@ -1093,10 +1154,13 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
1093
1154
  const errorMessage = lastError?.message || "Unknown error";
1094
1155
  const strategy = step.onError?.strategy || "fail";
1095
1156
  const retriesUsed = Math.max(0, maxAttempts - 1);
1157
+ const isTimeout = lastError instanceof StepTimeoutError;
1158
+ const errorCode = lastError?.errorCode;
1096
1159
  if (strategy === "continue") {
1097
1160
  const result = {
1098
- status: "failed",
1161
+ status: isTimeout ? "timeout" : "failed",
1099
1162
  error: errorMessage,
1163
+ ...errorCode ? { errorCode } : {},
1100
1164
  durationMs: Date.now() - startTime,
1101
1165
  retries: retriesUsed
1102
1166
  };
@@ -1105,8 +1169,9 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
1105
1169
  }
1106
1170
  if (strategy === "fallback" && step.onError?.fallbackStepId) {
1107
1171
  const result = {
1108
- status: "failed",
1172
+ status: isTimeout ? "timeout" : "failed",
1109
1173
  error: errorMessage,
1174
+ ...errorCode ? { errorCode } : {},
1110
1175
  durationMs: Date.now() - startTime,
1111
1176
  retries: retriesUsed
1112
1177
  };
@@ -1243,7 +1308,9 @@ var FLOW_SCHEMA = {
1243
1308
  name: { type: "string", required: true, description: "Human-readable step label" },
1244
1309
  type: { type: "string", required: true, description: "Step type (determines which config object is required)" },
1245
1310
  if: { type: "string", required: false, description: "JS expression \u2014 skip step if falsy" },
1246
- unless: { type: "string", required: false, description: "JS expression \u2014 skip step if truthy" }
1311
+ unless: { type: "string", required: false, description: "JS expression \u2014 skip step if truthy" },
1312
+ 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".' },
1313
+ 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." }
1247
1314
  },
1248
1315
  stepTypes: [
1249
1316
  {
@@ -1751,6 +1818,39 @@ Every completed step produces both \`output\` and \`response\`:
1751
1818
  - **Code/transform steps**: \`output\` is the return value. \`response\` is an alias for \`output\`.
1752
1819
  - **In practice**: Use \`$.steps.stepId.response\` for action steps (API data) and \`$.steps.stepId.output\` for code/transform steps (computed data). Both work interchangeably, but using the semantically correct one makes flows easier to read.
1753
1820
 
1821
+ ### Step result metadata (\`status\`, \`error\`, \`errorCode\`)
1822
+
1823
+ Every step result also exposes execution metadata that downstream steps can inspect:
1824
+
1825
+ | Field | Values | When set |
1826
+ |-------|--------|----------|
1827
+ | \`$.steps.X.status\` | \`"success"\` \\| \`"skipped"\` \\| \`"failed"\` \\| \`"timeout"\` | Always |
1828
+ | \`$.steps.X.error\` | error message string | When status is \`failed\` or \`timeout\` |
1829
+ | \`$.steps.X.errorCode\` | machine-readable code (e.g. \`"TIMEOUT"\`) | When the error has a code |
1830
+ | \`$.steps.X.durationMs\` | number | Always |
1831
+ | \`$.steps.X.retries\` | number | When the step was retried |
1832
+
1833
+ This lets downstream steps distinguish \`skipped\` (\`if\` condition false) from \`failed\` (error, \`onError:continue\`) from \`timeout\` (exceeded \`timeoutMs\`) \u2014 e.g. \`"if": "$.steps.enrichment.status === 'timeout'"\` to retry with a longer window.
1834
+
1835
+ ### Sub-flow output (flattened)
1836
+
1837
+ When a step has \`type: "flow"\`, the sub-flow's final step output is flattened onto the parent step's \`output\`:
1838
+
1839
+ \`\`\`jsonc
1840
+ // Sub-flow "sub-consts" has a final step "load" that returns { CHART_URL, API_KEY }
1841
+
1842
+ // Preferred (flattened):
1843
+ "{{$.steps.loadConfig.output.CHART_URL}}"
1844
+
1845
+ // Legacy nested path (still works for backward compatibility):
1846
+ "{{$.steps.loadConfig.output.load.output.CHART_URL}}"
1847
+
1848
+ // Escape hatch for programmatic access to the full sub-flow steps map:
1849
+ "{{$.steps.loadConfig.output._steps.load.output.CHART_URL}}"
1850
+ \`\`\`
1851
+
1852
+ If a sub-step id collides with a flattened field name, the flattened field wins and the engine emits a \`flow:warning\` event.
1853
+
1754
1854
  ## Error Handling
1755
1855
 
1756
1856
  \`\`\`json
@@ -11,7 +11,7 @@ import {
11
11
  saveFlow,
12
12
  summarizeFlowInputs,
13
13
  walkSteps
14
- } from "./chunk-Z45IBT5P.js";
14
+ } from "./chunk-ONGJ2QKB.js";
15
15
  export {
16
16
  FlowRunner,
17
17
  collectStepTypes,
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  loadFlowWithMeta,
17
17
  resolveFlowPath,
18
18
  saveFlow
19
- } from "./chunk-Z45IBT5P.js";
19
+ } from "./chunk-ONGJ2QKB.js";
20
20
 
21
21
  // src/index.ts
22
22
  import { createRequire as createRequire2 } from "module";
@@ -2320,6 +2320,20 @@ function validateStepsArray(steps, pathPrefix, errors) {
2320
2320
  errors.push({ path: `${path8}.type`, message: `Step type must be one of: ${validTypes.join(", ")}` });
2321
2321
  continue;
2322
2322
  }
2323
+ if (s.requires !== void 0) {
2324
+ if (!Array.isArray(s.requires)) {
2325
+ errors.push({ path: `${path8}.requires`, message: '"requires" must be an array of selector strings (e.g. ["$.steps.foo.output.bar"])' });
2326
+ } else {
2327
+ for (let r = 0; r < s.requires.length; r++) {
2328
+ const sel = s.requires[r];
2329
+ if (typeof sel !== "string") {
2330
+ errors.push({ path: `${path8}.requires[${r}]`, message: '"requires" entry must be a selector string' });
2331
+ } else if (!sel.startsWith("$.")) {
2332
+ errors.push({ path: `${path8}.requires[${r}]`, message: `"requires" entry "${sel}" must be a selector starting with "$." (e.g. "$.steps.foo.output.bar")` });
2333
+ }
2334
+ }
2335
+ }
2336
+ }
2323
2337
  if (s.onError && typeof s.onError === "object") {
2324
2338
  const oe = s.onError;
2325
2339
  if (!FLOW_SCHEMA.errorStrategies.includes(oe.strategy)) {
@@ -2529,6 +2543,14 @@ function validateSelectorReferences(flow2) {
2529
2543
  function checkStep(step, pathPrefix, preceding2) {
2530
2544
  if (step.if) checkSelectors(extractSelectors(step.if), `${pathPrefix}.if`, preceding2);
2531
2545
  if (step.unless) checkSelectors(extractSelectors(step.unless), `${pathPrefix}.unless`, preceding2);
2546
+ if (Array.isArray(step.requires)) {
2547
+ const reqs = step.requires;
2548
+ reqs.forEach((sel, i) => {
2549
+ if (typeof sel === "string" && sel.startsWith("$.")) {
2550
+ checkSelectors([sel], `${pathPrefix}.requires[${i}]`, preceding2);
2551
+ }
2552
+ });
2553
+ }
2532
2554
  const descriptor = getStepTypeDescriptor(step.type);
2533
2555
  if (descriptor) {
2534
2556
  const config2 = step[descriptor.configKey];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -424,6 +424,35 @@ Alternatively, pass values as environment variables (also shell-safe) and refere
424
424
  }
425
425
  ```
426
426
 
427
+ ## Step Input Contracts (`requires`)
428
+
429
+ 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.
430
+
431
+ ```json
432
+ {
433
+ "id": "summarizeFounder",
434
+ "type": "code",
435
+ "requires": [
436
+ "$.steps.fetchProfile.output.bio",
437
+ "$.input.founderName"
438
+ ],
439
+ "code": { "module": "lib/summarize.mjs" }
440
+ }
441
+ ```
442
+
443
+ 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:
444
+
445
+ ```
446
+ Step "summarizeFounder" requires $.steps.fetchProfile.output.bio but it
447
+ resolved to undefined (upstream step "fetchProfile" was skipped)
448
+ ```
449
+
450
+ The "because…" suffix tells you exactly why — skipped, failed, or timed out — so you can fix the upstream wiring instead of guessing.
451
+
452
+ `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.
453
+
454
+ Forward references are caught at flow load time: if `requires` points at a step declared after the current step, validation rejects the flow.
455
+
427
456
  ## Error Handling
428
457
 
429
458
  ```json