@withone/cli 1.47.7 → 1.47.10
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 +22 -2
- package/dist/{chunk-KWGN3RJR.js → chunk-4AYHJFH3.js} +1977 -1716
- package/dist/{flow-runner-7VRNSKWU.js → flow-runner-JNEFFR2U.js} +1 -1
- package/dist/index.js +285 -10
- package/dist/schema-DXHEU47V.js +109 -0
- package/package.json +1 -1
- package/skills/one/SKILL.md +2 -1
- package/skills/one/references/flows.md +35 -1
package/dist/index.js
CHANGED
|
@@ -29,8 +29,9 @@ import {
|
|
|
29
29
|
saveFlow,
|
|
30
30
|
searchCachePath,
|
|
31
31
|
validateActionInput,
|
|
32
|
+
walkSteps,
|
|
32
33
|
writeCache
|
|
33
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-4AYHJFH3.js";
|
|
34
35
|
import {
|
|
35
36
|
memSqlCommand
|
|
36
37
|
} from "./chunk-QV3Y5N5G.js";
|
|
@@ -3597,7 +3598,7 @@ function validateOutputSchemas(flow2) {
|
|
|
3597
3598
|
walkValue(v, `${pathPrefix}.${k}`);
|
|
3598
3599
|
}
|
|
3599
3600
|
}
|
|
3600
|
-
function
|
|
3601
|
+
function walkSteps2(steps, pathPrefix) {
|
|
3601
3602
|
steps.forEach((step, i) => {
|
|
3602
3603
|
const stepPath = `${pathPrefix}[${i}]`;
|
|
3603
3604
|
if (step.if) checkText(step.if, `${stepPath}.if`);
|
|
@@ -3612,13 +3613,13 @@ function validateOutputSchemas(flow2) {
|
|
|
3612
3613
|
for (const { configKey, fieldName } of nestedKeys) {
|
|
3613
3614
|
const c = step[configKey];
|
|
3614
3615
|
if (c && Array.isArray(c[fieldName])) {
|
|
3615
|
-
|
|
3616
|
+
walkSteps2(c[fieldName], `${stepPath}.${configKey}.${fieldName}`);
|
|
3616
3617
|
}
|
|
3617
3618
|
}
|
|
3618
3619
|
}
|
|
3619
3620
|
});
|
|
3620
3621
|
}
|
|
3621
|
-
|
|
3622
|
+
walkSteps2(flow2.steps, "steps");
|
|
3622
3623
|
return errors;
|
|
3623
3624
|
}
|
|
3624
3625
|
function validateOutputSchemaShape(schema, location) {
|
|
@@ -3652,6 +3653,7 @@ function validateFlow(flow2, rootDir) {
|
|
|
3652
3653
|
...validateStepIds(f),
|
|
3653
3654
|
...validateSelectorReferences(f, rootDir),
|
|
3654
3655
|
...validateOutputSchemas(f),
|
|
3656
|
+
...validateFileReadSchemas(f),
|
|
3655
3657
|
...rootDir ? validateCodeModules(f, rootDir) : []
|
|
3656
3658
|
];
|
|
3657
3659
|
}
|
|
@@ -3692,6 +3694,108 @@ function validateCodeModules(flow2, rootDir) {
|
|
|
3692
3694
|
walk(flow2.steps, "steps");
|
|
3693
3695
|
return errors;
|
|
3694
3696
|
}
|
|
3697
|
+
var VALID_FILE_READ_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "object", "array", "null", "unknown"]);
|
|
3698
|
+
function isPlainObject(v) {
|
|
3699
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3700
|
+
}
|
|
3701
|
+
function validateFileReadFieldRule(raw, fieldPath, errors) {
|
|
3702
|
+
if (typeof raw === "string") {
|
|
3703
|
+
if (!VALID_FILE_READ_SCHEMA_TYPES.has(raw)) {
|
|
3704
|
+
errors.push({ path: fieldPath, message: `unknown type "${raw}". Allowed: ${[...VALID_FILE_READ_SCHEMA_TYPES].join(", ")}.` });
|
|
3705
|
+
}
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
if (!isPlainObject(raw)) {
|
|
3709
|
+
errors.push({ path: fieldPath, message: 'field rule must be a type string or an object (e.g. { "type": "string", "required": true })' });
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
const rule = raw;
|
|
3713
|
+
if (rule.type !== void 0 && (typeof rule.type !== "string" || !VALID_FILE_READ_SCHEMA_TYPES.has(rule.type))) {
|
|
3714
|
+
errors.push({ path: `${fieldPath}.type`, message: `unknown type "${String(rule.type)}". Allowed: ${[...VALID_FILE_READ_SCHEMA_TYPES].join(", ")}.` });
|
|
3715
|
+
}
|
|
3716
|
+
if (rule.required !== void 0 && typeof rule.required !== "boolean") {
|
|
3717
|
+
errors.push({ path: `${fieldPath}.required`, message: '"required" must be a boolean' });
|
|
3718
|
+
}
|
|
3719
|
+
if (rule.enum !== void 0) {
|
|
3720
|
+
if (!Array.isArray(rule.enum) || rule.enum.length === 0) {
|
|
3721
|
+
errors.push({ path: `${fieldPath}.enum`, message: '"enum" must be a non-empty array of allowed values' });
|
|
3722
|
+
} else if (rule.enum.some((e) => e !== null && typeof e === "object")) {
|
|
3723
|
+
errors.push({ path: `${fieldPath}.enum`, message: '"enum" entries must be primitives (string/number/boolean) or null' });
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
for (const k of ["minItems", "maxItems", "length"]) {
|
|
3727
|
+
const n = rule[k];
|
|
3728
|
+
if (n !== void 0 && (typeof n !== "number" || !Number.isInteger(n) || n < 0)) {
|
|
3729
|
+
errors.push({ path: `${fieldPath}.${k}`, message: `"${k}" must be a non-negative integer` });
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
if (typeof rule.minItems === "number" && typeof rule.maxItems === "number" && rule.minItems > rule.maxItems) {
|
|
3733
|
+
errors.push({ path: `${fieldPath}.minItems`, message: '"minItems" must be <= "maxItems"' });
|
|
3734
|
+
}
|
|
3735
|
+
if (rule.length !== void 0 && (rule.minItems !== void 0 || rule.maxItems !== void 0)) {
|
|
3736
|
+
errors.push({ path: `${fieldPath}.length`, message: '"length" cannot be combined with "minItems"/"maxItems"' });
|
|
3737
|
+
}
|
|
3738
|
+
const hasArrayConstraint = rule.items !== void 0 || rule.minItems !== void 0 || rule.maxItems !== void 0 || rule.length !== void 0;
|
|
3739
|
+
if (hasArrayConstraint && rule.type !== "array") {
|
|
3740
|
+
errors.push({ path: `${fieldPath}.type`, message: '"items"/"minItems"/"maxItems"/"length" require "type": "array"' });
|
|
3741
|
+
}
|
|
3742
|
+
if (rule.items !== void 0) {
|
|
3743
|
+
if (typeof rule.items === "string") {
|
|
3744
|
+
if (!VALID_FILE_READ_SCHEMA_TYPES.has(rule.items)) {
|
|
3745
|
+
errors.push({ path: `${fieldPath}.items`, message: `unknown item type "${rule.items}". Allowed: ${[...VALID_FILE_READ_SCHEMA_TYPES].join(", ")}.` });
|
|
3746
|
+
}
|
|
3747
|
+
} else if (isPlainObject(rule.items)) {
|
|
3748
|
+
validateFileReadFieldRule(rule.items, `${fieldPath}.items`, errors);
|
|
3749
|
+
} else {
|
|
3750
|
+
errors.push({ path: `${fieldPath}.items`, message: '"items" must be a type string or a field rule object' });
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
if (rule.properties !== void 0) {
|
|
3754
|
+
if (rule.type !== "object") {
|
|
3755
|
+
errors.push({ path: `${fieldPath}.properties`, message: '"properties" requires "type": "object"' });
|
|
3756
|
+
}
|
|
3757
|
+
errors.push(...validateFileReadSchemaShape(rule.properties, `${fieldPath}.properties`));
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
function validateFileReadSchemaShape(schema, location) {
|
|
3761
|
+
const errors = [];
|
|
3762
|
+
if (!isPlainObject(schema)) {
|
|
3763
|
+
errors.push({ path: location, message: '"schema" must be an object mapping field names to type/rules' });
|
|
3764
|
+
return errors;
|
|
3765
|
+
}
|
|
3766
|
+
for (const [field, raw] of Object.entries(schema)) {
|
|
3767
|
+
validateFileReadFieldRule(raw, `${location}.${field}`, errors);
|
|
3768
|
+
}
|
|
3769
|
+
return errors;
|
|
3770
|
+
}
|
|
3771
|
+
function validateFileReadSchemas(flow2) {
|
|
3772
|
+
const errors = [];
|
|
3773
|
+
const nestedKeys = getNestedStepsKeys();
|
|
3774
|
+
function walk(steps, pathPrefix) {
|
|
3775
|
+
for (let i = 0; i < steps.length; i++) {
|
|
3776
|
+
const step = steps[i];
|
|
3777
|
+
const stepPath = `${pathPrefix}[${i}]`;
|
|
3778
|
+
if (step.type === "file-read" && step.fileRead && step.fileRead.schema !== void 0) {
|
|
3779
|
+
const fr = step.fileRead;
|
|
3780
|
+
if (fr.parseJson !== true) {
|
|
3781
|
+
errors.push({
|
|
3782
|
+
path: `${stepPath}.fileRead.schema`,
|
|
3783
|
+
message: "fileRead.schema is only checked when parseJson:true \u2014 set parseJson:true or remove schema."
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
errors.push(...validateFileReadSchemaShape(fr.schema, `${stepPath}.fileRead.schema`));
|
|
3787
|
+
}
|
|
3788
|
+
for (const { configKey, fieldName } of nestedKeys) {
|
|
3789
|
+
const config2 = step[configKey];
|
|
3790
|
+
if (config2 && Array.isArray(config2[fieldName])) {
|
|
3791
|
+
walk(config2[fieldName], `${stepPath}.${configKey}.${fieldName}`);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
walk(flow2.steps, "steps");
|
|
3797
|
+
return errors;
|
|
3798
|
+
}
|
|
3695
3799
|
|
|
3696
3800
|
// src/commands/flow.ts
|
|
3697
3801
|
import fs5 from "fs";
|
|
@@ -3716,6 +3820,49 @@ async function writeFlowResultFile(filePath, meta, steps) {
|
|
|
3716
3820
|
await done;
|
|
3717
3821
|
return abs;
|
|
3718
3822
|
}
|
|
3823
|
+
function previewValue(value, max = 120) {
|
|
3824
|
+
if (value === void 0) return "<undefined>";
|
|
3825
|
+
let str;
|
|
3826
|
+
try {
|
|
3827
|
+
str = typeof value === "string" ? JSON.stringify(value) : JSON.stringify(value) ?? String(value);
|
|
3828
|
+
} catch {
|
|
3829
|
+
str = String(value);
|
|
3830
|
+
}
|
|
3831
|
+
if (str === void 0) str = String(value);
|
|
3832
|
+
return str.length > max ? `${str.slice(0, max - 1)}\u2026` : str;
|
|
3833
|
+
}
|
|
3834
|
+
function renderDryRef(ref) {
|
|
3835
|
+
if (ref.status === "resolved") {
|
|
3836
|
+
return `${pc7.green("\u2713")} ${ref.selector} ${pc7.dim("\u2192")} ${previewValue(ref.value)}`;
|
|
3837
|
+
}
|
|
3838
|
+
if (ref.status === "deferred") {
|
|
3839
|
+
return `${pc7.dim("\u25CB")} ${ref.selector} ${pc7.dim("\u2192 pending (produced by a later step)")}`;
|
|
3840
|
+
}
|
|
3841
|
+
return `${pc7.yellow("!")} ${ref.selector} ${pc7.yellow("\u2192 unresolved \u2014 check the input/env name")}`;
|
|
3842
|
+
}
|
|
3843
|
+
function renderDryResolution(steps) {
|
|
3844
|
+
const isExpr = (t) => t === "transform" || t === "condition" || t === "while";
|
|
3845
|
+
for (const s of steps) {
|
|
3846
|
+
const label = s.name ? `${s.stepId} ${pc7.dim(`"${s.name}"`)}` : s.stepId;
|
|
3847
|
+
console.log(` ${pc7.cyan("\u25B8")} ${label} ${pc7.dim(`(${s.type})`)}`);
|
|
3848
|
+
if (s.error !== void 0) {
|
|
3849
|
+
console.log(` ${pc7.red("error")} ${s.error}`);
|
|
3850
|
+
} else if (isExpr(s.type) && s.deferred) {
|
|
3851
|
+
const deps = s.references.map((r) => r.selector).join(", ");
|
|
3852
|
+
console.log(` ${pc7.dim("\u25CB pending \u2014 depends on")} ${deps} ${pc7.dim("(produced by a later step)")}`);
|
|
3853
|
+
} else if (isExpr(s.type)) {
|
|
3854
|
+
console.log(` ${pc7.dim("=")} ${previewValue(s.resolved)}`);
|
|
3855
|
+
}
|
|
3856
|
+
if (!(isExpr(s.type) && s.deferred)) {
|
|
3857
|
+
for (const ref of s.references) {
|
|
3858
|
+
console.log(` ${renderDryRef(ref)}`);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
if (!isExpr(s.type) && s.references.length === 0 && s.error === void 0) {
|
|
3862
|
+
console.log(` ${pc7.dim("(no interpolations)")}`);
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3719
3866
|
function getConfig2() {
|
|
3720
3867
|
const apiKey = getApiKey();
|
|
3721
3868
|
if (!apiKey) {
|
|
@@ -3870,6 +4017,20 @@ ${preflightErrors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}`);
|
|
|
3870
4017
|
}
|
|
3871
4018
|
error(msg);
|
|
3872
4019
|
}
|
|
4020
|
+
if (options.stopAfter) {
|
|
4021
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4022
|
+
walkSteps(flow2.steps, (s) => {
|
|
4023
|
+
ids.add(s.id);
|
|
4024
|
+
});
|
|
4025
|
+
if (!ids.has(options.stopAfter)) {
|
|
4026
|
+
const msg = `--stop-after target "${options.stopAfter}" is not a step in workflow "${flow2.key}". Known step ids: ${[...ids].join(", ")}`;
|
|
4027
|
+
if (isAgentMode()) {
|
|
4028
|
+
json({ error: msg, unknownStopAfter: options.stopAfter, stepIds: [...ids] });
|
|
4029
|
+
process.exit(1);
|
|
4030
|
+
}
|
|
4031
|
+
error(msg);
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
3873
4034
|
const inputs = parseInputs(options.input || []);
|
|
3874
4035
|
const resolvedInputs = await autoResolveConnectionInputs(flow2, inputs, api);
|
|
3875
4036
|
const runner = new FlowRunner(flow2, resolvedInputs);
|
|
@@ -3883,7 +4044,17 @@ ${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
|
|
|
3883
4044
|
}
|
|
3884
4045
|
};
|
|
3885
4046
|
process.on("SIGINT", sigintHandler);
|
|
4047
|
+
let dryRunSteps;
|
|
4048
|
+
let dryResolveTarget;
|
|
4049
|
+
let stoppedAfter;
|
|
3886
4050
|
const onEvent = (event) => {
|
|
4051
|
+
if (event.event === "flow:dry-run") {
|
|
4052
|
+
dryRunSteps = event.steps;
|
|
4053
|
+
} else if (event.event === "step:dry-resolve") {
|
|
4054
|
+
dryResolveTarget = event;
|
|
4055
|
+
} else if (event.event === "flow:stopped") {
|
|
4056
|
+
stoppedAfter = event.stoppedAfter;
|
|
4057
|
+
}
|
|
3887
4058
|
if (isAgentMode()) {
|
|
3888
4059
|
json(event);
|
|
3889
4060
|
} else if (options.verbose) {
|
|
@@ -3909,23 +4080,46 @@ ${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
|
|
|
3909
4080
|
verbose: options.verbose,
|
|
3910
4081
|
allowBash: options.allowBash,
|
|
3911
4082
|
skipValidation: options.skipValidation,
|
|
4083
|
+
stopAfter: options.stopAfter,
|
|
3912
4084
|
rootDir,
|
|
3913
4085
|
onEvent
|
|
3914
4086
|
});
|
|
3915
4087
|
process.off("SIGINT", sigintHandler);
|
|
3916
4088
|
if (!options.verbose && !isAgentMode()) {
|
|
3917
|
-
execSpinner.stop("Workflow completed");
|
|
4089
|
+
execSpinner.stop(stoppedAfter ? "Workflow stopped" : dryRunSteps ? "Dry run complete" : "Workflow completed");
|
|
3918
4090
|
}
|
|
3919
4091
|
const resultFile = options.outputFile ? await writeFlowResultFile(options.outputFile, { runId, logFile: logPath, status: "success" }, context.steps) : void 0;
|
|
4092
|
+
const finalStatus = stoppedAfter ? "stopped" : "success";
|
|
3920
4093
|
if (isAgentMode()) {
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
4094
|
+
const envelope = {
|
|
4095
|
+
event: "workflow:result",
|
|
4096
|
+
runId,
|
|
4097
|
+
logFile: logPath,
|
|
4098
|
+
statePath: runner.getStatePath(),
|
|
4099
|
+
status: finalStatus
|
|
4100
|
+
};
|
|
4101
|
+
if (resultFile) envelope.outputFile = resultFile;
|
|
4102
|
+
else envelope.steps = context.steps;
|
|
4103
|
+
if (options.dryRun) envelope.dryRun = true;
|
|
4104
|
+
if (stoppedAfter) envelope.stoppedAfter = stoppedAfter;
|
|
4105
|
+
json(envelope);
|
|
3924
4106
|
return;
|
|
3925
4107
|
}
|
|
3926
4108
|
if (resultFile) {
|
|
3927
4109
|
note(`Full result written to ${resultFile}`, "Output");
|
|
3928
4110
|
}
|
|
4111
|
+
if (dryRunSteps && !stoppedAfter) {
|
|
4112
|
+
console.log();
|
|
4113
|
+
renderDryResolution(dryRunSteps);
|
|
4114
|
+
console.log();
|
|
4115
|
+
const missing = dryRunSteps.reduce((n, s) => n + s.references.filter((r) => r.status === "missing").length, 0);
|
|
4116
|
+
note(
|
|
4117
|
+
`Dry run \u2014 no steps executed. Resolved ${dryRunSteps.length} step(s)` + (missing > 0 ? `; ${pc7.yellow(`${missing} unresolved input/env reference(s)`)}` : "") + `.
|
|
4118
|
+
${pc7.dim("$.steps.* refs resolve at runtime \u2014 re-run with --stop-after=<stepId> to resolve them against real output.")}`,
|
|
4119
|
+
"Dry Run"
|
|
4120
|
+
);
|
|
4121
|
+
return;
|
|
4122
|
+
}
|
|
3929
4123
|
const stepEntries = Object.entries(context.steps);
|
|
3930
4124
|
const succeeded = stepEntries.filter(([, r]) => r.status === "success").length;
|
|
3931
4125
|
const failed = stepEntries.filter(([, r]) => r.status === "failed").length;
|
|
@@ -3934,7 +4128,17 @@ ${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
|
|
|
3934
4128
|
console.log(` ${pc7.green("\u2713")} ${succeeded} succeeded ${failed > 0 ? pc7.red(`\u2717 ${failed} failed`) : ""} ${skipped > 0 ? pc7.dim(`\u25CB ${skipped} skipped`) : ""}`);
|
|
3935
4129
|
console.log(` ${pc7.dim(`Run ID: ${runId}`)}`);
|
|
3936
4130
|
console.log(` ${pc7.dim(`Log: ${logPath}`)}`);
|
|
3937
|
-
if (
|
|
4131
|
+
if (dryResolveTarget) {
|
|
4132
|
+
console.log();
|
|
4133
|
+
console.log(` ${pc7.dim(`Resolved (not executed) "${dryResolveTarget.stepId}":`)}`);
|
|
4134
|
+
renderDryResolution([dryResolveTarget]);
|
|
4135
|
+
}
|
|
4136
|
+
if (stoppedAfter) {
|
|
4137
|
+
note(
|
|
4138
|
+
`Stopped after step "${stoppedAfter}". Inspect step outputs with: ${pc7.cyan(`one flow inspect ${runId}`)}`,
|
|
4139
|
+
"Stopped"
|
|
4140
|
+
);
|
|
4141
|
+
} else if (options.dryRun) {
|
|
3938
4142
|
note("Dry run \u2014 no steps were executed", "Dry Run");
|
|
3939
4143
|
}
|
|
3940
4144
|
} catch (error2) {
|
|
@@ -4126,6 +4330,64 @@ async function flowRunsCommand(flowKey) {
|
|
|
4126
4330
|
);
|
|
4127
4331
|
console.log();
|
|
4128
4332
|
}
|
|
4333
|
+
async function flowInspectCommand(runId, options = {}) {
|
|
4334
|
+
intro(pc7.bgCyan(pc7.black(" One Workflow ")));
|
|
4335
|
+
const state = FlowRunner.loadRunState(runId);
|
|
4336
|
+
if (!state) {
|
|
4337
|
+
const msg = `No run found for id "${runId}". List runs with: one flow runs`;
|
|
4338
|
+
if (isAgentMode()) {
|
|
4339
|
+
json({ error: msg, runId });
|
|
4340
|
+
process.exit(1);
|
|
4341
|
+
}
|
|
4342
|
+
error(msg);
|
|
4343
|
+
return;
|
|
4344
|
+
}
|
|
4345
|
+
const statePath = FlowRunner.statePathFor(state.flowKey, state.runId);
|
|
4346
|
+
const stepEntries = Object.entries(state.context.steps || {});
|
|
4347
|
+
if (isAgentMode()) {
|
|
4348
|
+
json({
|
|
4349
|
+
runId: state.runId,
|
|
4350
|
+
flowKey: state.flowKey,
|
|
4351
|
+
status: state.status,
|
|
4352
|
+
startedAt: state.startedAt,
|
|
4353
|
+
completedAt: state.completedAt,
|
|
4354
|
+
pausedAt: state.pausedAt,
|
|
4355
|
+
currentStepId: state.currentStepId,
|
|
4356
|
+
inputs: state.inputs,
|
|
4357
|
+
steps: state.context.steps,
|
|
4358
|
+
statePath
|
|
4359
|
+
});
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
console.log();
|
|
4363
|
+
console.log(` ${pc7.bold(state.flowKey)} ${pc7.dim(`run ${state.runId}`)} ${colorStatus(state.status)}`);
|
|
4364
|
+
console.log(` ${pc7.dim(`Started: ${state.startedAt}${state.completedAt ? ` \xB7 Ended: ${state.completedAt}` : ""}`)}`);
|
|
4365
|
+
if (state.currentStepId) console.log(` ${pc7.dim(`Current step: ${state.currentStepId}`)}`);
|
|
4366
|
+
console.log();
|
|
4367
|
+
if (stepEntries.length === 0) {
|
|
4368
|
+
note("No step outputs recorded yet for this run.", "Steps");
|
|
4369
|
+
} else {
|
|
4370
|
+
for (const [id, result] of stepEntries) {
|
|
4371
|
+
const icon = result.status === "success" ? pc7.green("\u2713") : result.status === "skipped" ? pc7.dim("\u25CB") : result.status === "timeout" ? pc7.yellow("\u29D6") : pc7.red("\u2717");
|
|
4372
|
+
const dur = result.durationMs !== void 0 ? pc7.dim(` ${result.durationMs}ms`) : "";
|
|
4373
|
+
const retries = result.retries ? pc7.dim(` (${result.retries} retr${result.retries === 1 ? "y" : "ies"})`) : "";
|
|
4374
|
+
console.log(` ${icon} ${id} ${pc7.dim(`[${result.status}]`)}${dur}${retries}`);
|
|
4375
|
+
if (result.error) {
|
|
4376
|
+
console.log(` ${pc7.red("error")} ${result.error}${result.errorCode ? pc7.dim(` (${result.errorCode})`) : ""}`);
|
|
4377
|
+
}
|
|
4378
|
+
if (result.output !== void 0) {
|
|
4379
|
+
const json2 = JSON.stringify(result.output, null, options.full ? 2 : 0) ?? String(result.output);
|
|
4380
|
+
const shown = options.full || json2.length <= 240 ? json2 : `${json2.slice(0, 239)}\u2026 ${pc7.dim("(--full for all)")}`;
|
|
4381
|
+
const indented = options.full ? shown.split("\n").map((l) => ` ${l}`).join("\n") : ` ${pc7.dim("output")} ${shown}`;
|
|
4382
|
+
console.log(indented);
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
console.log();
|
|
4387
|
+
console.log(` ${pc7.dim(`State: ${statePath}`)}`);
|
|
4388
|
+
console.log(` ${pc7.dim(`Log: ${path5.join(".one/flows/.logs", `${state.flowKey}-${state.runId}.log`)}`)}`);
|
|
4389
|
+
console.log();
|
|
4390
|
+
}
|
|
4129
4391
|
function colorStatus(status) {
|
|
4130
4392
|
switch (status) {
|
|
4131
4393
|
case "completed":
|
|
@@ -8300,6 +8562,10 @@ function registerSyncSubcommands(sync) {
|
|
|
8300
8562
|
sync.command("sql <platform/model> <sql>").description("Run a read-only SELECT / WITH / EXPLAIN against the memory store (type-scoped helper)").action(async (platformModel, sql) => {
|
|
8301
8563
|
await syncSqlCommand(platformModel, sql);
|
|
8302
8564
|
});
|
|
8565
|
+
sync.command("schema <platform/model>").description("Inspect the JSON structure of synced records (field paths, types, examples) \u2014 useful before writing `sync sql` queries").action(async (platformModel) => {
|
|
8566
|
+
const { syncSchemaCommand } = await import("./schema-DXHEU47V.js");
|
|
8567
|
+
await syncSchemaCommand(platformModel);
|
|
8568
|
+
});
|
|
8303
8569
|
sync.command("delete <platform/model>").description('Delete records from local sync data (e.g. one sync delete notion/pages --id "abc-123")').option("--id <value>", "Delete record by ID").option("--where <conditions>", 'Delete records matching conditions (e.g. "status=archived")').option("--where-sql <predicate>", `Delete using a raw SQL WHERE clause (e.g. "json_extract(data, '$.type') = 'promotion'")`).option("--yes", "Skip confirmation prompt").action(async (platformModel, options) => {
|
|
8304
8570
|
await syncDeleteCommand(platformModel, options);
|
|
8305
8571
|
});
|
|
@@ -9448,6 +9714,9 @@ Chain actions across platforms as JSON workflow files with conditions, loops, pa
|
|
|
9448
9714
|
one --agent flow create <key> --definition '<json>' # Create a workflow
|
|
9449
9715
|
one --agent flow validate <key> # Validate it
|
|
9450
9716
|
one --agent flow execute <key> -i param=value # Execute it
|
|
9717
|
+
one --agent flow execute <key> --dry-run -i ... # Resolve interpolations, run nothing
|
|
9718
|
+
one --agent flow execute <key> --stop-after <stepId> # Run up to a step, then stop
|
|
9719
|
+
one --agent flow inspect <runId> # Per-step outputs of a past run
|
|
9451
9720
|
one --agent flow list # List all workflows
|
|
9452
9721
|
\`\`\`
|
|
9453
9722
|
|
|
@@ -9461,6 +9730,7 @@ one --agent flow list # List all workflows
|
|
|
9461
9730
|
- Use \`--skip-validation\` to bypass input validation on action steps
|
|
9462
9731
|
- Use \`--output-file <path>\` to stream the full result to a file instead of stdout \u2014 for large results that would otherwise be truncated or hit the JSON string-size limit; stdout (and \`--agent\` output) then carries an \`outputFile\` pointer instead of inline \`steps\`
|
|
9463
9732
|
- Step-level \`if\`/\`unless\` (and \`while\`/\`condition\` steps) are null-safe: a condition referencing a skipped or not-yet-run step's output (e.g. \`$.steps.maybeSkipped.output.x\`) evaluates to \`false\` instead of crashing the flow
|
|
9733
|
+
- Debugging: \`--dry-run\` resolves each step's interpolations and shows what they evaluate to without executing (\`$.steps.*\` refs are reported as deferred); \`--stop-after <stepId>\` runs up to a step then stops; \`--dry-run --stop-after <stepId>\` runs earlier steps for real then dry-resolves the target against their output; \`flow inspect <runId>\` shows a past run's per-step outputs (post-mortem, no re-run; \`--full\` for untruncated)
|
|
9464
9734
|
|
|
9465
9735
|
### 3. Relay \u2014 Webhook event forwarding between platforms
|
|
9466
9736
|
Receive webhooks from platforms (Stripe, GitHub, Airtable, Attio, Google Calendar) and forward event data to any connected platform using passthrough actions with Handlebars templates. No middleware, no code.
|
|
@@ -10019,6 +10289,7 @@ one --agent sync run stripe
|
|
|
10019
10289
|
one --agent mem sync run stripe # identical (alias)
|
|
10020
10290
|
|
|
10021
10291
|
# 5. Query + search (read from memory)
|
|
10292
|
+
one --agent sync schema stripe/customers # inspect field paths/types first
|
|
10022
10293
|
one --agent sync query stripe/balanceTransactions --where "status=available" --limit 20
|
|
10023
10294
|
one --agent sync query stripe/customers --where 'address.city=SF' # dotted --where paths
|
|
10024
10295
|
one --agent sync search "refund" --platform stripe # hybrid FTS + semantic
|
|
@@ -10259,6 +10530,7 @@ Every \`sync X\` command is also exposed as \`mem sync X\` \u2014 same handlers,
|
|
|
10259
10530
|
| \`sync suggest-searchable <plat>/<model>\` | Rank candidate \`memory.searchable\` paths by signal density; emits paste-ready config |
|
|
10260
10531
|
| \`sync run <platform>\` | Sync data (\`--full-refresh\`, \`--since\`, \`--dry-run\`, \`--no-memory\`) |
|
|
10261
10532
|
| \`sync query <plat>/<model>\` | Query memory with \`--where\` (dotted paths), \`--after/before\` |
|
|
10533
|
+
| \`sync schema <plat>/<model>\` | Inspect the JSON structure of synced records (field paths, types, examples) \u2014 run before writing \`--where\` / query paths |
|
|
10262
10534
|
| \`sync search "<query>"\` | Hybrid FTS + semantic across all synced data |
|
|
10263
10535
|
| \`sync list [platform]\` | Show profiles, record counts, freshness |
|
|
10264
10536
|
| \`sync schedule add/list/status/remove/repair\` | Manage cron schedules |
|
|
@@ -11355,7 +11627,7 @@ var flow = program.command("flow").alias("f").description("Create, execute, and
|
|
|
11355
11627
|
flow.command("create [key]").description("Create a new workflow from JSON definition").option("--definition <json>", "Workflow definition as JSON string").option("-o, --output <path>", "Custom output path (default .one/flows/<key>/flow.json)").action(async (key, options) => {
|
|
11356
11628
|
await flowCreateCommand(key, options);
|
|
11357
11629
|
});
|
|
11358
|
-
flow.command("execute <keyOrPath>").alias("x").description("Execute a workflow by key or file path").option("-i, --input <name=value>", "Input parameter (repeatable)", collect, []).option("--dry-run", "
|
|
11630
|
+
flow.command("execute <keyOrPath>").alias("x").description("Execute a workflow by key or file path").option("-i, --input <name=value>", "Input parameter (repeatable)", collect, []).option("--dry-run", "Resolve each step's interpolations and show what they evaluate to, without executing any step ($.steps.* refs resolve at runtime)").option("--mock", "With --dry-run: execute transforms/code with realistic mock API responses").option("--stop-after <stepId>", "Execute steps up to and including <stepId>, then stop (later steps are not run). With --dry-run, runs earlier steps for real and dry-resolves <stepId> against their output without executing it. See: one flow inspect").option("--skip-validation", "Skip input validation against action schemas").option("--allow-bash", "Allow bash step execution (disabled by default for security)").option("-v, --verbose", "Show full request/response for each step").option("--output-file <path>", "Write the full result to a file (streamed) instead of stdout \u2014 avoids truncation/string-limit errors for large results; stdout/agent output then carries an outputFile pointer").action(async (keyOrPath, options) => {
|
|
11359
11631
|
await flowExecuteCommand(keyOrPath, options);
|
|
11360
11632
|
});
|
|
11361
11633
|
flow.command("list").alias("ls").description("List all workflows in .one/flows/").action(async () => {
|
|
@@ -11370,6 +11642,9 @@ flow.command("resume <runId>").description("Resume a paused or failed workflow r
|
|
|
11370
11642
|
flow.command("runs [flowKey]").description("List workflow runs (optionally filtered by flow key)").action(async (flowKey) => {
|
|
11371
11643
|
await flowRunsCommand(flowKey);
|
|
11372
11644
|
});
|
|
11645
|
+
flow.command("inspect <runId>").description("Inspect a past run's per-step outputs (post-mortem, no re-run) from its saved state file").option("--full", "Show full step outputs (default truncates large values)").action(async (runId, options) => {
|
|
11646
|
+
await flowInspectCommand(runId, options);
|
|
11647
|
+
});
|
|
11373
11648
|
flow.command("scaffold [template]").description("Generate a workflow scaffold (templates: basic, conditional, loop, ai)").action(async (template) => {
|
|
11374
11649
|
await flowScaffoldCommand(template);
|
|
11375
11650
|
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAgentMode,
|
|
3
|
+
note,
|
|
4
|
+
okJson,
|
|
5
|
+
requireMemoryInit
|
|
6
|
+
} from "./chunk-HS5AHQ4V.js";
|
|
7
|
+
import {
|
|
8
|
+
getBackend
|
|
9
|
+
} from "./chunk-OADHUAEU.js";
|
|
10
|
+
import "./chunk-TXTRXV74.js";
|
|
11
|
+
import "./chunk-K6MWE2ZH.js";
|
|
12
|
+
|
|
13
|
+
// src/lib/memory/sync/schema.ts
|
|
14
|
+
import pc from "picocolors";
|
|
15
|
+
var SAMPLE_SIZE = 100;
|
|
16
|
+
function typeOf(v) {
|
|
17
|
+
if (v === null) return "null";
|
|
18
|
+
if (Array.isArray(v)) return "array";
|
|
19
|
+
return typeof v;
|
|
20
|
+
}
|
|
21
|
+
function exampleOf(v) {
|
|
22
|
+
if (typeof v === "string") return v.length > 60 ? `${v.slice(0, 57)}\u2026` : v;
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
function addPath(path, value, acc) {
|
|
26
|
+
const t = typeOf(value);
|
|
27
|
+
let typeLabel;
|
|
28
|
+
if (t === "array") {
|
|
29
|
+
const arr = value;
|
|
30
|
+
const elemType = arr.length ? typeOf(arr[0]) : "unknown";
|
|
31
|
+
typeLabel = `array[${elemType}]`;
|
|
32
|
+
} else {
|
|
33
|
+
typeLabel = t;
|
|
34
|
+
}
|
|
35
|
+
const entry = acc.get(path) ?? { types: /* @__PURE__ */ new Set(), hasExample: false, presence: 0 };
|
|
36
|
+
entry.types.add(typeLabel);
|
|
37
|
+
entry.presence += 1;
|
|
38
|
+
if (!entry.hasExample && t !== "object" && t !== "array" && value !== null && value !== void 0) {
|
|
39
|
+
entry.example = exampleOf(value);
|
|
40
|
+
entry.hasExample = true;
|
|
41
|
+
}
|
|
42
|
+
acc.set(path, entry);
|
|
43
|
+
if (t === "object") {
|
|
44
|
+
for (const [k, v] of Object.entries(value)) {
|
|
45
|
+
addPath(path ? `${path}.${k}` : k, v, acc);
|
|
46
|
+
}
|
|
47
|
+
} else if (t === "array") {
|
|
48
|
+
const arr = value;
|
|
49
|
+
if (arr.length && typeOf(arr[0]) === "object") {
|
|
50
|
+
for (const [k, v] of Object.entries(arr[0])) {
|
|
51
|
+
addPath(`${path}[].${k}`, v, acc);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function inferSyncSchema(dataRecords) {
|
|
57
|
+
const acc = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const data of dataRecords) {
|
|
59
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) continue;
|
|
60
|
+
for (const [k, v] of Object.entries(data)) addPath(k, v, acc);
|
|
61
|
+
}
|
|
62
|
+
return [...acc.entries()].map(([path, e]) => ({
|
|
63
|
+
path,
|
|
64
|
+
types: [...e.types].sort(),
|
|
65
|
+
...e.hasExample ? { example: e.example } : {},
|
|
66
|
+
presence: e.presence
|
|
67
|
+
})).sort((a, b) => a.path.localeCompare(b.path));
|
|
68
|
+
}
|
|
69
|
+
async function syncSchemaCommand(platformModel, _options = {}) {
|
|
70
|
+
requireMemoryInit();
|
|
71
|
+
const type = platformModel;
|
|
72
|
+
const backend = await getBackend();
|
|
73
|
+
let recordCount = 0;
|
|
74
|
+
try {
|
|
75
|
+
recordCount = await backend.count(type, { status: "active" });
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
const records = await backend.list(type, { limit: SAMPLE_SIZE, status: "active" });
|
|
79
|
+
if (records.length === 0) {
|
|
80
|
+
if (isAgentMode()) {
|
|
81
|
+
okJson({ type, recordCount: 0, sampled: 0, fields: [] });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
note(`No records found for "${type}". Run \`one sync run <platform>\` first, or check \`one --agent sync status\`.`, "Schema");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const fields = inferSyncSchema(records.map((r) => r.data));
|
|
88
|
+
if (isAgentMode()) {
|
|
89
|
+
okJson({ type, recordCount, sampled: records.length, fields });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(`${pc.bold(type)} ${pc.dim(`(${recordCount.toLocaleString()} record${recordCount === 1 ? "" : "s"}, sampled ${records.length})`)}`);
|
|
94
|
+
console.log();
|
|
95
|
+
const pathWidth = Math.min(Math.max(...fields.map((f) => f.path.length), 4) + 2, 50);
|
|
96
|
+
const typeWidth = Math.max(...fields.map((f) => f.types.join("|").length), 4) + 2;
|
|
97
|
+
for (const f of fields) {
|
|
98
|
+
const optional = f.presence < records.length ? pc.yellow(" ?") : "";
|
|
99
|
+
const ex = f.example !== void 0 ? pc.dim(JSON.stringify(f.example)) : "";
|
|
100
|
+
console.log(` ${f.path.padEnd(pathWidth)}${pc.cyan(f.types.join("|").padEnd(typeWidth))}${ex}${optional}`);
|
|
101
|
+
}
|
|
102
|
+
console.log();
|
|
103
|
+
console.log(pc.dim(` ? = present in only some sampled records (optional/sparse field)`));
|
|
104
|
+
console.log();
|
|
105
|
+
}
|
|
106
|
+
export {
|
|
107
|
+
inferSyncSchema,
|
|
108
|
+
syncSchemaCommand
|
|
109
|
+
};
|
package/package.json
CHANGED
package/skills/one/SKILL.md
CHANGED
|
@@ -229,6 +229,7 @@ one --agent sync test attio/attioPeople --show-searchable
|
|
|
229
229
|
|
|
230
230
|
# Run — memory is always written; pass --no-memory to skip (rare)
|
|
231
231
|
one --agent sync run stripe
|
|
232
|
+
one --agent sync schema stripe/customers # inspect field paths/types before querying
|
|
232
233
|
one --agent sync query stripe/balanceTransactions --where "status=available" --limit 20
|
|
233
234
|
one --agent sync search "refund" # hybrid across all synced platforms
|
|
234
235
|
one --agent sync list stripe # progress + freshness
|
|
@@ -260,7 +261,7 @@ Without declared paths, the default walker concatenates every string in the reco
|
|
|
260
261
|
One also supports more advanced patterns. Read the relevant reference file before using these:
|
|
261
262
|
|
|
262
263
|
- **Webhook Relay** — Receive webhooks from a platform and forward to another (e.g., Stripe event -> Slack message). Read `references/relay.md` in this skill's directory for the full workflow.
|
|
263
|
-
- **Multi-step Workflows** — Chain actions across platforms as JSON workflow files (like n8n/Zapier but file-based). Read `references/flows.md` in this skill's directory for the schema and examples.
|
|
264
|
+
- **Multi-step Workflows** — Chain actions across platforms as JSON workflow files (like n8n/Zapier but file-based). Read `references/flows.md` in this skill's directory for the schema and examples. To debug: `flow execute <key> --dry-run` (resolve interpolations without running), `--stop-after <stepId>` (run up to a step then stop), and `flow inspect <runId>` (a past run's per-step outputs).
|
|
264
265
|
|
|
265
266
|
## Adding New Connections
|
|
266
267
|
|
|
@@ -8,7 +8,7 @@ Nothing about a flow's runtime requirements is guessable from its name. Before `
|
|
|
8
8
|
|
|
9
9
|
1. **Recommended:** `one --agent flow list` — the JSON output includes `requiresBash`, `usesCodeModules`, `inputs` (with `autoResolvable`), `stepTypes`, and the flow's `description`. Fastest path to knowing what you need.
|
|
10
10
|
2. Read the flow's `description` field from the JSON. Authors are required (see "Author conventions" below) to state `--allow-bash` requirements and non-auto-resolving inputs there.
|
|
11
|
-
3. `one --agent flow execute <key> --dry-run` to
|
|
11
|
+
3. `one --agent flow execute <key> --dry-run` to resolve every step's interpolations and see what they evaluate to — without side effects (see "Debugging a flow" below).
|
|
12
12
|
|
|
13
13
|
If you skip this, you will hit errors like *"Workflow X contains bash steps. Re-run with --allow-bash."* — the CLI pre-flights and fails fast, but the error is entirely avoidable by reading first.
|
|
14
14
|
|
|
@@ -400,6 +400,28 @@ After a parallel step, access each substep's output by its `id`: `$.steps.fetchE
|
|
|
400
400
|
{ "id": "write", "type": "file-write", "fileWrite": { "path": "./output/results.json", "content": "$.steps.transform.output" } }
|
|
401
401
|
```
|
|
402
402
|
|
|
403
|
+
**Validate a read config (`fileRead.schema`).** With `parseJson: true` you can add an optional `schema` that is **enforced at runtime** — a mismatch throws (`errorCode: "SCHEMA_VALIDATION"`) and is handled by the step's `onError`, so a bad config file fails fast with a clear message instead of producing garbage downstream. (This is distinct from `step.outputSchema`, which is documentation/wiring only and never checked at runtime.)
|
|
404
|
+
|
|
405
|
+
```json
|
|
406
|
+
{
|
|
407
|
+
"id": "read", "type": "file-read",
|
|
408
|
+
"fileRead": {
|
|
409
|
+
"path": "./data/config.json", "parseJson": true,
|
|
410
|
+
"schema": {
|
|
411
|
+
"name": { "type": "string", "required": true },
|
|
412
|
+
"mode": { "type": "string", "required": true, "enum": ["dev", "staging", "prod"] },
|
|
413
|
+
"retries": { "type": "number" },
|
|
414
|
+
"tags": { "type": "array", "items": "string", "minItems": 1, "maxItems": 10 },
|
|
415
|
+
"coords": { "type": "array", "length": 2, "items": "number" },
|
|
416
|
+
"owner": { "type": "object", "required": true,
|
|
417
|
+
"properties": { "id": "string", "email": { "type": "string", "required": true } } }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Field rules (a richer field-rule format inspired by `outputSchema`): a value is a bare type string (`"string"`, optional) or `{ type, required, enum, items, minItems, maxItems, length, properties }`. Types: `string`/`number`/`boolean`/`object`/`array`/`null`/`unknown` (`unknown` passes anything). **Array constraints (`items`/`minItems`/`maxItems`/`length`) require `type: "array"`, and `properties` requires `type: "object"`** — `flow validate` rejects them otherwise (so they can't silently no-op). All violations are reported in one error; paths are dotted/indexed (`owner.email`, `tags[3]`); a non-object root reports `expected an object at the root but got <type>`. `flow validate` checks the schema's shape (and that `parseJson:true` is set). With `onError: { strategy: "continue" }` the step lands as `{ status: "failed", errorCode: "SCHEMA_VALIDATION" }` so downstream steps can branch on it; `retry`/`fallback` and `retryOn:["SCHEMA_VALIDATION"]` work too.
|
|
424
|
+
|
|
403
425
|
### `while` — Condition-driven loop (do-while)
|
|
404
426
|
|
|
405
427
|
```json
|
|
@@ -655,12 +677,24 @@ one --agent flow validate <key>
|
|
|
655
677
|
one --agent flow execute <key> -i key=value
|
|
656
678
|
one --agent flow execute <key> --dry-run -i key=value
|
|
657
679
|
one --agent flow execute <key> --dry-run --mock -i key=value
|
|
680
|
+
one --agent flow execute <key> --stop-after <stepId> -i key=value
|
|
681
|
+
one --agent flow execute <key> --dry-run --stop-after <stepId> -i key=value
|
|
658
682
|
one --agent flow execute <key> --skip-validation -i key=value
|
|
659
683
|
one --agent flow execute <key> --allow-bash -i key=value
|
|
660
684
|
one --agent flow runs [flowKey]
|
|
685
|
+
one --agent flow inspect <runId> # per-step outputs of a past run (add --full for untruncated)
|
|
661
686
|
one --agent flow resume <runId>
|
|
662
687
|
```
|
|
663
688
|
|
|
689
|
+
## Debugging a flow
|
|
690
|
+
|
|
691
|
+
Three tools that turn "re-run the whole 40-step flow to debug step 30" into near-zero-cost inspection:
|
|
692
|
+
|
|
693
|
+
- **`--dry-run`** — resolves every step's interpolations and shows what they evaluate to, *without executing any step*. `$.input.*` / `$.env.*` resolve immediately; `$.steps.*` references are reported as `deferred` (they're produced at runtime). Transform/condition/while steps show their *evaluated* result; if a transform can't evaluate yet because it reads a step that hasn't run, it's marked `deferred` (depends on a later step) rather than shown as an error — genuine problems (syntax errors, missing inputs, a missing field on a step that *did* run) still surface as errors. Catches wiring bugs (typo'd input names, wrong field paths) with zero API spend. In `--agent` mode this is the `flow:dry-run` event whose `steps[]` carry a `references` table (`{selector, value, status}` where status is `resolved` | `deferred` | `missing`), an evaluated `resolved` value, and a `deferred` / `error` flag for expression steps.
|
|
694
|
+
- **`--stop-after <stepId>`** — executes steps up to and including `<stepId>`, then stops; later steps don't run. Isolates where a long flow breaks. Emits a `flow:stopped` event; the run's partial state is saved.
|
|
695
|
+
- **`--dry-run --stop-after <stepId>`** — runs the steps *before* `<stepId>` for real, then dry-resolves `<stepId>` against that **real** accumulated context (so its `$.steps.*` references resolve to actual upstream output) and stops without executing it. The honest way to preview "what will step N actually receive". Emits a `step:dry-resolve` event for the target.
|
|
696
|
+
- **`flow inspect <runId>`** — prints a past run's per-step status, durations, errors, and outputs from its persisted state file (post-mortem, no re-run). Use after `--stop-after`, or on any failed run, to see exactly what each step produced. `--full` disables output truncation.
|
|
697
|
+
|
|
664
698
|
## Important Notes
|
|
665
699
|
|
|
666
700
|
- **Prefer passthrough actions over custom actions.** Custom actions add server-side fan-out that causes timeouts at scale. The flow runner handles pagination, retries, and rate limiting locally. Search with `-t knowledge` to find passthrough endpoints (e.g. GET `/gmail/v1/users/{userId}/threads` instead of POST `/gmail/get-threads`)
|