@withone/cli 1.20.2 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/{chunk-DPOG6BQ5.js → chunk-KZOFPEHD.js} +806 -19
- package/dist/flow-runner-CXZ6AWXT.js +28 -0
- package/dist/index.js +90 -549
- package/package.json +1 -1
- package/skills/one/references/flows.md +101 -2
- package/dist/flow-runner-UWZL2FPJ.js +0 -14
|
@@ -6,7 +6,7 @@ import crypto from "crypto";
|
|
|
6
6
|
// src/lib/flow-engine.ts
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import path from "path";
|
|
9
|
-
import { exec } from "child_process";
|
|
9
|
+
import { exec, spawn } from "child_process";
|
|
10
10
|
import { promisify } from "util";
|
|
11
11
|
|
|
12
12
|
// src/lib/api.ts
|
|
@@ -606,15 +606,73 @@ function executeTransformStep(step, context) {
|
|
|
606
606
|
const output = evaluateExpression(step.transform.expression, context);
|
|
607
607
|
return { status: "success", output, response: output };
|
|
608
608
|
}
|
|
609
|
-
async function executeCodeStep(step, context) {
|
|
610
|
-
const
|
|
609
|
+
async function executeCodeStep(step, context, options) {
|
|
610
|
+
const config = step.code;
|
|
611
|
+
if (config.module) {
|
|
612
|
+
const output2 = await executeCodeModule(step.id, config.module, context, options);
|
|
613
|
+
return { status: "success", output: output2, response: output2 };
|
|
614
|
+
}
|
|
615
|
+
if (typeof config.source !== "string") {
|
|
616
|
+
throw new Error(`Code step "${step.id}" must define either "source" or "module"`);
|
|
617
|
+
}
|
|
611
618
|
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
612
619
|
}).constructor;
|
|
613
620
|
const sandboxedRequire = createSandboxedRequire();
|
|
614
|
-
const fn = new AsyncFunction("$", "require", source);
|
|
621
|
+
const fn = new AsyncFunction("$", "require", config.source);
|
|
615
622
|
const output = await fn(context, sandboxedRequire);
|
|
616
623
|
return { status: "success", output, response: output };
|
|
617
624
|
}
|
|
625
|
+
async function executeCodeModule(stepId, modulePath, context, options) {
|
|
626
|
+
const rootDir = options.rootDir;
|
|
627
|
+
if (!rootDir) {
|
|
628
|
+
throw new Error(`Code step "${stepId}" uses module "${modulePath}" but no flow rootDir is available. Flows that use code modules must be loaded via loadFlowWithMeta.`);
|
|
629
|
+
}
|
|
630
|
+
if (path.isAbsolute(modulePath)) {
|
|
631
|
+
throw new Error(`Code module path must be relative to the flow root, got absolute: "${modulePath}"`);
|
|
632
|
+
}
|
|
633
|
+
const absPath = path.resolve(rootDir, modulePath);
|
|
634
|
+
const relFromRoot = path.relative(rootDir, absPath);
|
|
635
|
+
if (relFromRoot.startsWith("..") || path.isAbsolute(relFromRoot)) {
|
|
636
|
+
throw new Error(`Code module "${modulePath}" resolves outside the flow directory`);
|
|
637
|
+
}
|
|
638
|
+
if (!fs.existsSync(absPath)) {
|
|
639
|
+
throw new Error(`Code module not found: ${absPath}`);
|
|
640
|
+
}
|
|
641
|
+
const { env: _omitEnv, ...safeContext } = context;
|
|
642
|
+
void _omitEnv;
|
|
643
|
+
const stdinPayload = JSON.stringify(safeContext);
|
|
644
|
+
return await new Promise((resolve, reject) => {
|
|
645
|
+
const child = spawn(process.execPath, [absPath], {
|
|
646
|
+
cwd: rootDir,
|
|
647
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
648
|
+
});
|
|
649
|
+
const stdoutChunks = [];
|
|
650
|
+
const stderrChunks = [];
|
|
651
|
+
child.stdout.on("data", (c) => stdoutChunks.push(c));
|
|
652
|
+
child.stderr.on("data", (c) => stderrChunks.push(c));
|
|
653
|
+
child.on("error", (err) => reject(err));
|
|
654
|
+
child.on("close", (code) => {
|
|
655
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
|
|
656
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8");
|
|
657
|
+
if (code !== 0) {
|
|
658
|
+
reject(new Error(`Code module "${modulePath}" exited with code ${code}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const trimmed = stdout.trim();
|
|
662
|
+
if (trimmed === "") {
|
|
663
|
+
resolve(void 0);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
resolve(JSON.parse(stripCodeFences(trimmed)));
|
|
668
|
+
} catch (err) {
|
|
669
|
+
reject(new Error(`Code module "${modulePath}" did not print valid JSON to stdout: ${err.message}`));
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
child.stdin.write(stdinPayload);
|
|
673
|
+
child.stdin.end();
|
|
674
|
+
});
|
|
675
|
+
}
|
|
618
676
|
async function executeConditionStep(step, context, api, permissions, allowedActionIds, options, flowStack) {
|
|
619
677
|
const condition = step.condition;
|
|
620
678
|
const result = evaluateExpression(condition.expression, context);
|
|
@@ -782,15 +840,15 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
|
|
|
782
840
|
if (flowStack.includes(resolvedKey)) {
|
|
783
841
|
throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
|
|
784
842
|
}
|
|
785
|
-
const {
|
|
786
|
-
const subFlow =
|
|
843
|
+
const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-CXZ6AWXT.js");
|
|
844
|
+
const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
|
|
787
845
|
const subContext = await executeFlow(
|
|
788
846
|
subFlow,
|
|
789
847
|
resolvedInputs,
|
|
790
848
|
api,
|
|
791
849
|
permissions,
|
|
792
850
|
allowedActionIds,
|
|
793
|
-
options,
|
|
851
|
+
{ ...options, rootDir: subRootDir },
|
|
794
852
|
void 0,
|
|
795
853
|
[...flowStack, resolvedKey]
|
|
796
854
|
);
|
|
@@ -923,7 +981,7 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
|
|
|
923
981
|
result = executeTransformStep(step, context);
|
|
924
982
|
break;
|
|
925
983
|
case "code":
|
|
926
|
-
result = await executeCodeStep(step, context);
|
|
984
|
+
result = await executeCodeStep(step, context, options);
|
|
927
985
|
break;
|
|
928
986
|
case "condition":
|
|
929
987
|
result = await executeConditionStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
@@ -1092,6 +1150,640 @@ async function executeFlow(flow, inputs, api, permissions, allowedActionIds, opt
|
|
|
1092
1150
|
return context;
|
|
1093
1151
|
}
|
|
1094
1152
|
|
|
1153
|
+
// src/lib/flow-schema.ts
|
|
1154
|
+
var FLOW_SCHEMA = {
|
|
1155
|
+
errorStrategies: ["fail", "continue", "retry", "fallback"],
|
|
1156
|
+
validInputTypes: ["string", "number", "boolean", "object", "array"],
|
|
1157
|
+
flowFields: {
|
|
1158
|
+
key: { type: "string", required: true, description: "Unique kebab-case identifier", pattern: /^[a-z0-9][a-z0-9-]*[a-z0-9]$/ },
|
|
1159
|
+
name: { type: "string", required: true, description: "Human-readable flow name" },
|
|
1160
|
+
description: { type: "string", required: false, description: "What this flow does" },
|
|
1161
|
+
version: { type: "string", required: false, description: "Semver or arbitrary version string" },
|
|
1162
|
+
inputs: { type: "object", required: true, description: "Input declarations (Record<string, InputDeclaration>)" },
|
|
1163
|
+
steps: { type: "array", required: true, description: "Ordered array of steps", stepsArray: true }
|
|
1164
|
+
},
|
|
1165
|
+
inputFields: {
|
|
1166
|
+
type: { type: "string", required: true, description: "Data type: string, number, boolean, object, array", enum: ["string", "number", "boolean", "object", "array"] },
|
|
1167
|
+
required: { type: "boolean", required: false, description: "Whether this input must be provided" },
|
|
1168
|
+
default: { type: "unknown", required: false, description: "Default value if not provided" },
|
|
1169
|
+
description: { type: "string", required: false, description: "Human-readable description" },
|
|
1170
|
+
connection: { type: "object", required: false, description: 'Connection metadata: { platform: "gmail" } \u2014 enables auto-resolution' }
|
|
1171
|
+
},
|
|
1172
|
+
stepCommonFields: {
|
|
1173
|
+
id: { type: "string", required: true, description: "Unique step identifier (used in selectors)" },
|
|
1174
|
+
name: { type: "string", required: true, description: "Human-readable step label" },
|
|
1175
|
+
type: { type: "string", required: true, description: "Step type (determines which config object is required)" },
|
|
1176
|
+
if: { type: "string", required: false, description: "JS expression \u2014 skip step if falsy" },
|
|
1177
|
+
unless: { type: "string", required: false, description: "JS expression \u2014 skip step if truthy" }
|
|
1178
|
+
},
|
|
1179
|
+
stepTypes: [
|
|
1180
|
+
{
|
|
1181
|
+
type: "action",
|
|
1182
|
+
configKey: "action",
|
|
1183
|
+
description: "Execute a platform API action",
|
|
1184
|
+
fields: {
|
|
1185
|
+
platform: { type: "string", required: true, description: "Platform name (kebab-case)" },
|
|
1186
|
+
actionId: { type: "string", required: true, description: "Action ID from `actions search`" },
|
|
1187
|
+
connectionKey: { type: "string", required: true, description: "Connection key (use $.input selector)" },
|
|
1188
|
+
data: { type: "object", required: false, description: "Request body (POST/PUT/PATCH)" },
|
|
1189
|
+
pathVars: { type: "object", required: false, description: "URL path variables" },
|
|
1190
|
+
queryParams: { type: "object", required: false, description: "Query parameters" },
|
|
1191
|
+
headers: { type: "object", required: false, description: "Additional headers" }
|
|
1192
|
+
},
|
|
1193
|
+
example: {
|
|
1194
|
+
id: "findCustomer",
|
|
1195
|
+
name: "Search Stripe customers",
|
|
1196
|
+
type: "action",
|
|
1197
|
+
action: {
|
|
1198
|
+
platform: "stripe",
|
|
1199
|
+
actionId: "conn_mod_def::xxx::yyy",
|
|
1200
|
+
connectionKey: "$.input.stripeConnectionKey",
|
|
1201
|
+
data: { query: "email:'{{$.input.customerEmail}}'" }
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
type: "transform",
|
|
1207
|
+
configKey: "transform",
|
|
1208
|
+
description: "Single JS expression with implicit return",
|
|
1209
|
+
fields: {
|
|
1210
|
+
expression: { type: "string", required: true, description: "JS expression evaluated with flow context as $" }
|
|
1211
|
+
},
|
|
1212
|
+
example: {
|
|
1213
|
+
id: "extractNames",
|
|
1214
|
+
name: "Extract customer names",
|
|
1215
|
+
type: "transform",
|
|
1216
|
+
transform: { expression: "$.steps.findCustomer.response.data.map(c => c.name)" }
|
|
1217
|
+
}
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
type: "code",
|
|
1221
|
+
configKey: "code",
|
|
1222
|
+
description: "JS code \u2014 inline source or an external .mjs module under the flow's lib/ folder",
|
|
1223
|
+
fields: {
|
|
1224
|
+
source: { type: "string", required: false, description: 'Inline JS function body (flow context as $, supports await). Mutually exclusive with "module".' },
|
|
1225
|
+
module: { type: "string", required: false, description: 'Relative path to a .mjs file under the flow folder (e.g. "lib/normalize.mjs"). Reads $ from stdin as JSON, writes result to stdout as JSON. Mutually exclusive with "source".' }
|
|
1226
|
+
},
|
|
1227
|
+
example: {
|
|
1228
|
+
id: "processData",
|
|
1229
|
+
name: "Process and enrich data",
|
|
1230
|
+
type: "code",
|
|
1231
|
+
code: { module: "lib/process-data.mjs" }
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
type: "condition",
|
|
1236
|
+
configKey: "condition",
|
|
1237
|
+
description: "If/then/else branching",
|
|
1238
|
+
fields: {
|
|
1239
|
+
expression: { type: "string", required: true, description: "JS expression \u2014 truthy runs then, falsy runs else" },
|
|
1240
|
+
then: { type: "array", required: true, description: "Steps to run when true", stepsArray: true },
|
|
1241
|
+
else: { type: "array", required: false, description: "Steps to run when false", stepsArray: true }
|
|
1242
|
+
},
|
|
1243
|
+
example: {
|
|
1244
|
+
id: "checkFound",
|
|
1245
|
+
name: "Check if customer exists",
|
|
1246
|
+
type: "condition",
|
|
1247
|
+
condition: {
|
|
1248
|
+
expression: "$.steps.search.response.data.length > 0",
|
|
1249
|
+
then: [{ id: "notify", name: "Send notification", type: "action", action: { platform: "slack", actionId: "...", connectionKey: "$.input.slackKey", data: { text: "Found!" } } }],
|
|
1250
|
+
else: [{ id: "logMiss", name: "Log not found", type: "transform", transform: { expression: "'Not found'" } }]
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
type: "loop",
|
|
1256
|
+
configKey: "loop",
|
|
1257
|
+
description: "Iterate over an array with optional concurrency",
|
|
1258
|
+
fields: {
|
|
1259
|
+
over: { type: "string", required: true, description: "Selector resolving to an array" },
|
|
1260
|
+
as: { type: "string", required: true, description: "Variable name for current item ($.loop.<as>)" },
|
|
1261
|
+
indexAs: { type: "string", required: false, description: "Variable name for index" },
|
|
1262
|
+
steps: { type: "array", required: true, description: "Steps to run per iteration", stepsArray: true },
|
|
1263
|
+
maxIterations: { type: "number", required: false, description: "Safety cap (default: no limit)" },
|
|
1264
|
+
maxConcurrency: { type: "number", required: false, description: "Parallel batch size (default: 1 = sequential)" }
|
|
1265
|
+
},
|
|
1266
|
+
example: {
|
|
1267
|
+
id: "processOrders",
|
|
1268
|
+
name: "Process each order",
|
|
1269
|
+
type: "loop",
|
|
1270
|
+
loop: {
|
|
1271
|
+
over: "$.steps.listOrders.response.data",
|
|
1272
|
+
as: "order",
|
|
1273
|
+
steps: [{ id: "createInvoice", name: "Create invoice", type: "action", action: { platform: "stripe", actionId: "...", connectionKey: "$.input.stripeKey", data: { amount: "$.loop.order.total" } } }]
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
type: "parallel",
|
|
1279
|
+
configKey: "parallel",
|
|
1280
|
+
description: "Run steps concurrently",
|
|
1281
|
+
fields: {
|
|
1282
|
+
steps: { type: "array", required: true, description: "Steps to run in parallel", stepsArray: true },
|
|
1283
|
+
maxConcurrency: { type: "number", required: false, description: "Max concurrent steps (default: 5)" }
|
|
1284
|
+
},
|
|
1285
|
+
example: {
|
|
1286
|
+
id: "lookups",
|
|
1287
|
+
name: "Parallel data lookups",
|
|
1288
|
+
type: "parallel",
|
|
1289
|
+
parallel: {
|
|
1290
|
+
steps: [
|
|
1291
|
+
{ id: "getStripe", name: "Get Stripe data", type: "action", action: { platform: "stripe", actionId: "...", connectionKey: "$.input.stripeKey" } },
|
|
1292
|
+
{ id: "getSlack", name: "Get Slack data", type: "action", action: { platform: "slack", actionId: "...", connectionKey: "$.input.slackKey" } }
|
|
1293
|
+
]
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
type: "file-read",
|
|
1299
|
+
configKey: "fileRead",
|
|
1300
|
+
description: "Read a file (optional JSON parse)",
|
|
1301
|
+
fields: {
|
|
1302
|
+
path: { type: "string", required: true, description: "File path to read" },
|
|
1303
|
+
parseJson: { type: "boolean", required: false, description: "Parse contents as JSON (default: false)" }
|
|
1304
|
+
},
|
|
1305
|
+
example: {
|
|
1306
|
+
id: "readConfig",
|
|
1307
|
+
name: "Read config file",
|
|
1308
|
+
type: "file-read",
|
|
1309
|
+
fileRead: { path: "./data/config.json", parseJson: true }
|
|
1310
|
+
}
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
type: "file-write",
|
|
1314
|
+
configKey: "fileWrite",
|
|
1315
|
+
description: "Write or append to a file",
|
|
1316
|
+
fields: {
|
|
1317
|
+
path: { type: "string", required: true, description: "File path to write" },
|
|
1318
|
+
content: { type: "unknown", required: true, description: "Content to write (supports selectors)" },
|
|
1319
|
+
append: { type: "boolean", required: false, description: "Append instead of overwrite (default: false)" }
|
|
1320
|
+
},
|
|
1321
|
+
example: {
|
|
1322
|
+
id: "writeResults",
|
|
1323
|
+
name: "Save results",
|
|
1324
|
+
type: "file-write",
|
|
1325
|
+
fileWrite: { path: "./output/results.json", content: "$.steps.transform.output" }
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
type: "while",
|
|
1330
|
+
configKey: "while",
|
|
1331
|
+
description: "Do-while loop with condition check",
|
|
1332
|
+
fields: {
|
|
1333
|
+
condition: { type: "string", required: true, description: "JS expression checked before each iteration (after first)" },
|
|
1334
|
+
steps: { type: "array", required: true, description: "Steps to run each iteration", stepsArray: true },
|
|
1335
|
+
maxIterations: { type: "number", required: false, description: "Safety cap (default: 100)" }
|
|
1336
|
+
},
|
|
1337
|
+
example: {
|
|
1338
|
+
id: "paginate",
|
|
1339
|
+
name: "Paginate through pages",
|
|
1340
|
+
type: "while",
|
|
1341
|
+
while: {
|
|
1342
|
+
condition: "$.steps.paginate.output.lastResult.nextPageToken != null",
|
|
1343
|
+
maxIterations: 50,
|
|
1344
|
+
steps: [{ id: "fetchPage", name: "Fetch next page", type: "action", action: { platform: "gmail", actionId: "...", connectionKey: "$.input.gmailKey" } }]
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
{
|
|
1349
|
+
type: "flow",
|
|
1350
|
+
configKey: "flow",
|
|
1351
|
+
description: "Execute a sub-flow (supports composition)",
|
|
1352
|
+
fields: {
|
|
1353
|
+
key: { type: "string", required: true, description: "Flow key or path of the sub-flow" },
|
|
1354
|
+
inputs: { type: "object", required: false, description: "Inputs to pass to the sub-flow (supports selectors)" }
|
|
1355
|
+
},
|
|
1356
|
+
example: {
|
|
1357
|
+
id: "enrich",
|
|
1358
|
+
name: "Run enrichment sub-flow",
|
|
1359
|
+
type: "flow",
|
|
1360
|
+
flow: { key: "enrich-customer", inputs: { email: "$.steps.getCustomer.response.email" } }
|
|
1361
|
+
}
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
type: "paginate",
|
|
1365
|
+
configKey: "paginate",
|
|
1366
|
+
description: "Auto-paginate API results into a single array",
|
|
1367
|
+
fields: {
|
|
1368
|
+
action: { type: "object", required: true, description: "Action config (same shape as action step: platform, actionId, connectionKey)" },
|
|
1369
|
+
pageTokenField: { type: "string", required: true, description: "Dot-path in response to next page token" },
|
|
1370
|
+
resultsField: { type: "string", required: true, description: "Dot-path in response to results array" },
|
|
1371
|
+
inputTokenParam: { type: "string", required: true, description: "Dot-path in action config where page token is injected" },
|
|
1372
|
+
maxPages: { type: "number", required: false, description: "Max pages to fetch (default: 10)" }
|
|
1373
|
+
},
|
|
1374
|
+
example: {
|
|
1375
|
+
id: "allMessages",
|
|
1376
|
+
name: "Fetch all Gmail messages",
|
|
1377
|
+
type: "paginate",
|
|
1378
|
+
paginate: {
|
|
1379
|
+
action: { platform: "gmail", actionId: "...", connectionKey: "$.input.gmailKey", queryParams: { maxResults: 100 } },
|
|
1380
|
+
pageTokenField: "nextPageToken",
|
|
1381
|
+
resultsField: "messages",
|
|
1382
|
+
inputTokenParam: "queryParams.pageToken",
|
|
1383
|
+
maxPages: 10
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
type: "bash",
|
|
1389
|
+
configKey: "bash",
|
|
1390
|
+
description: "Shell command (requires --allow-bash). Output shape: $.steps.<id>.output is the parsed JSON when parseJson:true, otherwise the trimmed stdout string. $.steps.<id>.response always exposes { stdout, stderr, exitCode }.",
|
|
1391
|
+
fields: {
|
|
1392
|
+
command: { type: "string", required: true, description: "Shell command to execute (supports selectors)" },
|
|
1393
|
+
timeout: { type: "number", required: false, description: "Timeout in ms (default: 30000)" },
|
|
1394
|
+
parseJson: { type: "boolean", required: false, description: "Parse stdout as JSON (default: false). When true, $.steps.<id>.output is the parsed object/array; when false, it is the trimmed stdout string." },
|
|
1395
|
+
cwd: { type: "string", required: false, description: "Working directory (supports selectors)" },
|
|
1396
|
+
env: { type: "object", required: false, description: "Additional environment variables" }
|
|
1397
|
+
},
|
|
1398
|
+
example: {
|
|
1399
|
+
id: "analyze",
|
|
1400
|
+
name: "Analyze with Claude",
|
|
1401
|
+
type: "bash",
|
|
1402
|
+
bash: {
|
|
1403
|
+
command: "cat /tmp/data.json | claude --print 'Analyze this data' --output-format json",
|
|
1404
|
+
timeout: 18e4,
|
|
1405
|
+
parseJson: true
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
]
|
|
1410
|
+
};
|
|
1411
|
+
var _coveredTypes = Object.fromEntries(
|
|
1412
|
+
FLOW_SCHEMA.stepTypes.map((st) => [st.type, true])
|
|
1413
|
+
);
|
|
1414
|
+
var _stepTypeMap = new Map(
|
|
1415
|
+
FLOW_SCHEMA.stepTypes.map((st) => [st.type, st])
|
|
1416
|
+
);
|
|
1417
|
+
function getStepTypeDescriptor(type) {
|
|
1418
|
+
return _stepTypeMap.get(type);
|
|
1419
|
+
}
|
|
1420
|
+
function getValidStepTypes() {
|
|
1421
|
+
return FLOW_SCHEMA.stepTypes.map((st) => st.type);
|
|
1422
|
+
}
|
|
1423
|
+
function getNestedStepsKeys() {
|
|
1424
|
+
const result = [];
|
|
1425
|
+
for (const st of FLOW_SCHEMA.stepTypes) {
|
|
1426
|
+
for (const [fieldName, fd] of Object.entries(st.fields)) {
|
|
1427
|
+
if (fd.stepsArray) {
|
|
1428
|
+
result.push({ configKey: st.configKey, fieldName });
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return result;
|
|
1433
|
+
}
|
|
1434
|
+
function generateFlowGuide() {
|
|
1435
|
+
const validTypes = getValidStepTypes();
|
|
1436
|
+
const sections = [];
|
|
1437
|
+
sections.push(`# One Flows \u2014 Reference
|
|
1438
|
+
|
|
1439
|
+
## Overview
|
|
1440
|
+
|
|
1441
|
+
Workflows live in \`.one/flows/\` (relative to your current working directory \u2014 the CLI does NOT walk up parent directories or fall back to a global location) and chain actions across platforms. Two layouts are supported:
|
|
1442
|
+
|
|
1443
|
+
- **Folder layout (REQUIRED for new flows)** \u2014 \`.one/flows/<key>/flow.json\`, with an optional \`lib/\` subfolder for JavaScript modules. This is like a skill: the folder groups the JSON spec with any JavaScript modules it needs, so the whole flow is shareable. **Always create new flows in this layout.**
|
|
1444
|
+
- **Single-file layout (DEPRECATED)** \u2014 \`.one/flows/<key>.flow.json\`. Still loads and runs for backward compatibility, but is deprecated. Do not create new flows in this layout. When editing an existing single-file flow, migrate it to the folder layout: move \`<key>.flow.json\` to \`<key>/flow.json\` and extract any non-trivial \`code.source\` blocks into \`<key>/lib/*.mjs\` modules.
|
|
1445
|
+
|
|
1446
|
+
When resolving a flow by key, the CLI checks the folder layout first, then the deprecated legacy file. The \`loadFlow\` helper in agent integrations behaves the same.
|
|
1447
|
+
|
|
1448
|
+
## Before you execute a flow you did NOT author \u2014 READ THIS
|
|
1449
|
+
|
|
1450
|
+
**Agents: always inspect a flow before running it.** Nothing about a flow's runtime requirements is guessable from its name. Before \`flow execute\`, do one of these:
|
|
1451
|
+
|
|
1452
|
+
1. Run \`one --agent flow list\` \u2014 the JSON output includes \`requiresBash\`, \`usesCodeModules\`, \`inputs\` (with \`autoResolvable\` flags), \`stepTypes\`, and the flow's \`description\`. This is the fastest path.
|
|
1453
|
+
2. Read the flow's \`description\` field directly from the JSON. Flow authors are required (see "Author conventions" below) to state any \`--allow-bash\` requirement and any non-auto-resolving inputs in the description.
|
|
1454
|
+
3. Run \`one --agent flow execute <key> --dry-run\` to see the resolved inputs and step plan without side effects.
|
|
1455
|
+
|
|
1456
|
+
If you skip this step you will hit errors like *"Workflow X contains bash steps. Re-run with --allow-bash."* \u2014 the CLI now pre-flights and fails fast, so you won't waste a long run, but the error is still avoidable by reading first.
|
|
1457
|
+
|
|
1458
|
+
## Author conventions \u2014 WRITE flows that are safe to execute blind
|
|
1459
|
+
|
|
1460
|
+
When you create a flow, its \`description\` field is the contract with future executors (human or agent). It MUST state:
|
|
1461
|
+
|
|
1462
|
+
- **\`--allow-bash\` if any step is type \`bash\`.** Example: *"Fetches recent Gmail threads and summarizes them with Claude Haiku. Requires \`--allow-bash\`."*
|
|
1463
|
+
- **Every input that does NOT have a \`connection\` hint.** Connection inputs auto-resolve when exactly one matching connection exists; everything else must be passed via \`-i name=value\` and the description must name it.
|
|
1464
|
+
- **Any files/directories the flow writes to** so operators know what will be modified on disk.
|
|
1465
|
+
|
|
1466
|
+
A good description is one paragraph. If a flow's description doesn't tell you how to run it, treat that as a bug in the flow and fix it.
|
|
1467
|
+
|
|
1468
|
+
## Commands
|
|
1469
|
+
|
|
1470
|
+
\`\`\`bash
|
|
1471
|
+
one --agent flow create <key> --definition '<json>' # Create (or --definition @file.json)
|
|
1472
|
+
one --agent flow create <key> --definition @flow.json # Create from file
|
|
1473
|
+
one --agent flow list # List
|
|
1474
|
+
one --agent flow validate <key> # Validate
|
|
1475
|
+
one --agent flow execute <key> -i name=value # Execute
|
|
1476
|
+
one --agent flow execute <key> --dry-run --mock # Test with mock data
|
|
1477
|
+
one --agent flow execute <key> --allow-bash # Enable bash steps
|
|
1478
|
+
one --agent flow runs [flowKey] # List past runs
|
|
1479
|
+
one --agent flow resume <runId> # Resume failed run
|
|
1480
|
+
one --agent flow scaffold [template] # Generate a starter template
|
|
1481
|
+
\`\`\`
|
|
1482
|
+
|
|
1483
|
+
You can also write the JSON file directly to \`.one/flows/<key>/flow.json\` \u2014 often easier than passing large JSON via --definition. (The legacy \`.one/flows/<key>.flow.json\` single-file location is deprecated; don't use it for new flows.)
|
|
1484
|
+
|
|
1485
|
+
## Code modules (flow \`lib/\` folder)
|
|
1486
|
+
|
|
1487
|
+
A \`code\` step can either inline JS (\`code.source\`) or reference an external \`.mjs\` module (\`code.module\`). Modules live under the flow's \`lib/\` folder and run as a child \`node\` process:
|
|
1488
|
+
|
|
1489
|
+
\`\`\`
|
|
1490
|
+
.one/flows/my-flow/
|
|
1491
|
+
\u251C\u2500\u2500 flow.json
|
|
1492
|
+
\u2514\u2500\u2500 lib/
|
|
1493
|
+
\u2514\u2500\u2500 process-data.mjs
|
|
1494
|
+
\`\`\`
|
|
1495
|
+
|
|
1496
|
+
**Module contract:** the flow context \`$\` is piped to stdin as JSON; the module writes its result to stdout as JSON. That's the whole interface \u2014 no framework imports, no magic.
|
|
1497
|
+
|
|
1498
|
+
\`\`\`js
|
|
1499
|
+
// lib/process-data.mjs
|
|
1500
|
+
const $ = JSON.parse(await new Response(process.stdin).text());
|
|
1501
|
+
const items = $.steps.fetch.response.data ?? [];
|
|
1502
|
+
process.stdout.write(JSON.stringify(items.filter(i => i.active)));
|
|
1503
|
+
\`\`\`
|
|
1504
|
+
|
|
1505
|
+
\`\`\`json
|
|
1506
|
+
{
|
|
1507
|
+
"id": "processData",
|
|
1508
|
+
"name": "Process and enrich data",
|
|
1509
|
+
"type": "code",
|
|
1510
|
+
"code": { "module": "lib/process-data.mjs" }
|
|
1511
|
+
}
|
|
1512
|
+
\`\`\`
|
|
1513
|
+
|
|
1514
|
+
Modules are full Node processes \u2014 \`fs\`, \`https\`, any npm package installed in the host project, etc. are all available. Use this for anything non-trivial; keep \`code.source\` for one-liners.
|
|
1515
|
+
|
|
1516
|
+
**Step output shape:** whatever JSON a module writes to stdout becomes both \`$.steps.<id>.output\` and \`$.steps.<id>.response\` (aliases). Downstream steps can reference either; convention is to use \`.output\` for code/transform step results and \`.response\` for action step API payloads.
|
|
1517
|
+
|
|
1518
|
+
## Migrating a legacy single-file flow to the folder layout
|
|
1519
|
+
|
|
1520
|
+
If you're editing an existing \`.one/flows/<key>.flow.json\`, migrate it \u2014 it takes a minute and the result is cleaner. Checklist:
|
|
1521
|
+
|
|
1522
|
+
1. \`mkdir -p .one/flows/<key>/lib\`
|
|
1523
|
+
2. Move the file: \`mv .one/flows/<key>.flow.json .one/flows/<key>/flow.json\`
|
|
1524
|
+
3. For each non-trivial \`code\` step with inline \`source\`, extract it into \`lib/<step-id>.mjs\` (see translation pattern below) and swap the step config from \`{ "source": "..." }\` to \`{ "module": "lib/<step-id>.mjs" }\`. One-liners can stay inline.
|
|
1525
|
+
4. Validate: \`one --agent flow validate <key>\`.
|
|
1526
|
+
5. Run it and confirm behavior is unchanged.
|
|
1527
|
+
|
|
1528
|
+
**Inline source \u2192 module translation pattern.** Inline \`code.source\` is an async function body where \`$\` is already in scope and you \`return\` the result. A module is a standalone script where you read \`$\` from stdin and write the result to stdout as JSON. The transform is mechanical:
|
|
1529
|
+
|
|
1530
|
+
Before (inline \`code.source\`):
|
|
1531
|
+
\`\`\`js
|
|
1532
|
+
const items = $.steps.fetch.response.data;
|
|
1533
|
+
const active = items.filter(i => i.active);
|
|
1534
|
+
return { active, count: active.length };
|
|
1535
|
+
\`\`\`
|
|
1536
|
+
|
|
1537
|
+
After (\`lib/<step-id>.mjs\`):
|
|
1538
|
+
\`\`\`js
|
|
1539
|
+
const $ = JSON.parse(await new Response(process.stdin).text());
|
|
1540
|
+
const items = $.steps.fetch.response.data;
|
|
1541
|
+
const active = items.filter(i => i.active);
|
|
1542
|
+
process.stdout.write(JSON.stringify({ active, count: active.length }));
|
|
1543
|
+
\`\`\`
|
|
1544
|
+
|
|
1545
|
+
The only differences: (1) prepend the stdin-read line, (2) replace \`return X\` with \`process.stdout.write(JSON.stringify(X))\`. That's it.
|
|
1546
|
+
|
|
1547
|
+
## Building a Workflow
|
|
1548
|
+
|
|
1549
|
+
1. **Design first** \u2014 clarify the end goal, map the full value chain, identify where AI analysis is needed
|
|
1550
|
+
2. **Discover connections** \u2014 \`one --agent connection list\`
|
|
1551
|
+
3. **Get knowledge** for every action \u2014 \`one --agent actions knowledge <platform> <actionId>\`
|
|
1552
|
+
4. **Construct JSON** \u2014 declare inputs, wire steps with selectors
|
|
1553
|
+
5. **Validate** \u2014 \`one --agent flow validate <key>\`
|
|
1554
|
+
6. **Execute** \u2014 \`one --agent flow execute <key> -i param=value\``);
|
|
1555
|
+
sections.push(`## Flow JSON Schema
|
|
1556
|
+
|
|
1557
|
+
\`\`\`json
|
|
1558
|
+
{
|
|
1559
|
+
"key": "my-workflow",
|
|
1560
|
+
"name": "My Workflow",
|
|
1561
|
+
"description": "What this flow does",
|
|
1562
|
+
"version": "1",
|
|
1563
|
+
"inputs": {
|
|
1564
|
+
"connectionKey": {
|
|
1565
|
+
"type": "string",
|
|
1566
|
+
"required": true,
|
|
1567
|
+
"description": "Platform connection key",
|
|
1568
|
+
"connection": { "platform": "stripe" }
|
|
1569
|
+
},
|
|
1570
|
+
"param": {
|
|
1571
|
+
"type": "string",
|
|
1572
|
+
"required": true,
|
|
1573
|
+
"description": "A user parameter"
|
|
1574
|
+
}
|
|
1575
|
+
},
|
|
1576
|
+
"steps": [
|
|
1577
|
+
{
|
|
1578
|
+
"id": "stepId",
|
|
1579
|
+
"name": "Human-readable step name",
|
|
1580
|
+
"type": "action",
|
|
1581
|
+
"action": {
|
|
1582
|
+
"platform": "stripe",
|
|
1583
|
+
"actionId": "conn_mod_def::xxx::yyy",
|
|
1584
|
+
"connectionKey": "$.input.connectionKey",
|
|
1585
|
+
"data": { "query": "{{$.input.param}}" }
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
]
|
|
1589
|
+
}
|
|
1590
|
+
\`\`\`
|
|
1591
|
+
|
|
1592
|
+
### Top-level fields
|
|
1593
|
+
|
|
1594
|
+
| Field | Type | Required | Description |
|
|
1595
|
+
|-------|------|----------|-------------|`);
|
|
1596
|
+
for (const [name, fd] of Object.entries(FLOW_SCHEMA.flowFields)) {
|
|
1597
|
+
sections.push(`| \`${name}\` | ${fd.type} | ${fd.required ? "yes" : "no"} | ${fd.description} |`);
|
|
1598
|
+
}
|
|
1599
|
+
sections.push(`
|
|
1600
|
+
### Input declarations
|
|
1601
|
+
|
|
1602
|
+
| Field | Type | Required | Description |
|
|
1603
|
+
|-------|------|----------|-------------|`);
|
|
1604
|
+
for (const [name, fd] of Object.entries(FLOW_SCHEMA.inputFields)) {
|
|
1605
|
+
sections.push(`| \`${name}\` | ${fd.type} | ${fd.required ? "yes" : "no"} | ${fd.description} |`);
|
|
1606
|
+
}
|
|
1607
|
+
sections.push(`
|
|
1608
|
+
### Step fields (all steps)
|
|
1609
|
+
|
|
1610
|
+
Every step MUST have \`id\`, \`name\`, and \`type\`. The \`type\` determines which config object is required.
|
|
1611
|
+
|
|
1612
|
+
| Field | Type | Required | Description |
|
|
1613
|
+
|-------|------|----------|-------------|`);
|
|
1614
|
+
for (const [name, fd] of Object.entries(FLOW_SCHEMA.stepCommonFields)) {
|
|
1615
|
+
sections.push(`| \`${name}\` | ${fd.type} | ${fd.required ? "yes" : "no"} | ${fd.description} |`);
|
|
1616
|
+
}
|
|
1617
|
+
sections.push(`| \`onError\` | object | no | Error handling: \`{ "strategy": "${FLOW_SCHEMA.errorStrategies.join(" | ")}", "retries": 3, "retryDelayMs": 1000 }\` |`);
|
|
1618
|
+
sections.push(`
|
|
1619
|
+
## Step Types
|
|
1620
|
+
|
|
1621
|
+
**IMPORTANT:** Each step type requires a config object nested under a specific key. The type name and config key differ for some types (noted below).
|
|
1622
|
+
|
|
1623
|
+
| Type | Config Key | Description |
|
|
1624
|
+
|------|-----------|-------------|`);
|
|
1625
|
+
for (const st of FLOW_SCHEMA.stepTypes) {
|
|
1626
|
+
const keyNote = st.type !== st.configKey ? ` \u26A0\uFE0F` : "";
|
|
1627
|
+
sections.push(`| \`${st.type}\` | \`${st.configKey}\`${keyNote} | ${st.description} |`);
|
|
1628
|
+
}
|
|
1629
|
+
sections.push(`
|
|
1630
|
+
## Step Type Reference`);
|
|
1631
|
+
for (const st of FLOW_SCHEMA.stepTypes) {
|
|
1632
|
+
sections.push(`
|
|
1633
|
+
### \`${st.type}\` \u2014 ${st.description}`);
|
|
1634
|
+
if (st.type !== st.configKey) {
|
|
1635
|
+
sections.push(`
|
|
1636
|
+
> **Note:** Type is \`"${st.type}"\` but config key is \`"${st.configKey}"\` (camelCase).`);
|
|
1637
|
+
}
|
|
1638
|
+
sections.push(`
|
|
1639
|
+
| Field | Type | Required | Description |
|
|
1640
|
+
|-------|------|----------|-------------|`);
|
|
1641
|
+
for (const [name, fd] of Object.entries(st.fields)) {
|
|
1642
|
+
sections.push(`| \`${name}\` | ${fd.type} | ${fd.required ? "yes" : "no"} | ${fd.description} |`);
|
|
1643
|
+
}
|
|
1644
|
+
sections.push(`
|
|
1645
|
+
\`\`\`json
|
|
1646
|
+
${JSON.stringify(st.example, null, 2)}
|
|
1647
|
+
\`\`\``);
|
|
1648
|
+
}
|
|
1649
|
+
sections.push(`
|
|
1650
|
+
## Selectors
|
|
1651
|
+
|
|
1652
|
+
| Pattern | Resolves To |
|
|
1653
|
+
|---------|-------------|
|
|
1654
|
+
| \`$.input.paramName\` | Input value |
|
|
1655
|
+
| \`$.steps.stepId.response\` | Full API response |
|
|
1656
|
+
| \`$.steps.stepId.response.data[0].email\` | Nested field |
|
|
1657
|
+
| \`$.steps.stepId.response.data[*].id\` | Wildcard array map |
|
|
1658
|
+
| \`$.env.MY_VAR\` | Environment variable |
|
|
1659
|
+
| \`$.loop.item\` / \`$.loop.i\` | Loop iteration |
|
|
1660
|
+
| \`"Hello {{$.steps.getUser.response.name}}"\` | String interpolation |
|
|
1661
|
+
|
|
1662
|
+
### When to use bare selectors vs \`{{...}}\` interpolation
|
|
1663
|
+
|
|
1664
|
+
- **Bare selectors** (\`$.input.x\`): Use for fields the engine resolves directly \u2014 \`connectionKey\`, \`over\`, \`path\`, \`expression\`, \`condition\`, and any field where the entire value is a single selector. The resolved value keeps its original type (object, array, number).
|
|
1665
|
+
- **Interpolation** (\`{{$.input.x}}\`): Use inside string values where the selector is embedded in text \u2014 e.g., \`"Hello {{$.steps.getUser.response.name}}"\`. The resolved value is always stringified. Use this in \`data\`, \`pathVars\`, and \`queryParams\` when mixing selectors with literal text.
|
|
1666
|
+
- **Rule of thumb**: If the value is purely a selector, use bare. If it's a string containing a selector, use \`{{...}}\`.
|
|
1667
|
+
|
|
1668
|
+
### Selectors vs expressions
|
|
1669
|
+
|
|
1670
|
+
Selectors in data fields (\`data\`, \`queryParams\`, \`pathVars\`, \`connectionKey\`) are **dot-path lookups only** \u2014 they do not support JavaScript operators like \`||\` or \`&&\`. For default values, use the \`default\` field on the input definition:
|
|
1671
|
+
|
|
1672
|
+
\`\`\`json
|
|
1673
|
+
{ "inputs": { "maxResults": { "type": "number", "default": 10 } } }
|
|
1674
|
+
\`\`\`
|
|
1675
|
+
|
|
1676
|
+
The \`if\`, \`unless\`, \`condition.expression\`, \`while.condition\`, \`transform.expression\`, and \`code.source\` fields **do** support full JavaScript expressions (e.g., \`$.input.email && $.input.email.length > 0\`).
|
|
1677
|
+
|
|
1678
|
+
### \`output\` vs \`response\` on step results
|
|
1679
|
+
|
|
1680
|
+
Every completed step produces both \`output\` and \`response\`:
|
|
1681
|
+
- **Action steps**: \`response\` is the raw API response. \`output\` is the same as \`response\`.
|
|
1682
|
+
- **Code/transform steps**: \`output\` is the return value. \`response\` is an alias for \`output\`.
|
|
1683
|
+
- **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.
|
|
1684
|
+
|
|
1685
|
+
## Error Handling
|
|
1686
|
+
|
|
1687
|
+
\`\`\`json
|
|
1688
|
+
{"onError": {"strategy": "retry", "retries": 3, "retryDelayMs": 1000}}
|
|
1689
|
+
\`\`\`
|
|
1690
|
+
|
|
1691
|
+
Strategies: \`${FLOW_SCHEMA.errorStrategies.join("`, `")}\`
|
|
1692
|
+
|
|
1693
|
+
Conditional execution: \`"if": "$.steps.prev.response.data.length > 0"\`
|
|
1694
|
+
|
|
1695
|
+
## Input Connection Auto-Resolution
|
|
1696
|
+
|
|
1697
|
+
When an input has \`"connection": { "platform": "stripe" }\`, the flow engine can automatically resolve the connection key at execution time. If the user has exactly one connection for that platform, the engine fills in the key without requiring \`-i connectionKey=...\`. If multiple connections exist, the user must specify which one. This is metadata for tooling \u2014 it does not affect the flow JSON structure, but it makes execution more convenient.
|
|
1698
|
+
|
|
1699
|
+
## Complete Example: Fetch Data, Transform, Notify
|
|
1700
|
+
|
|
1701
|
+
\`\`\`json
|
|
1702
|
+
{
|
|
1703
|
+
"key": "contacts-to-slack",
|
|
1704
|
+
"name": "CRM Contacts Summary to Slack",
|
|
1705
|
+
"description": "Fetch recent contacts from CRM, build a summary, post to Slack",
|
|
1706
|
+
"version": "1",
|
|
1707
|
+
"inputs": {
|
|
1708
|
+
"crmConnectionKey": {
|
|
1709
|
+
"type": "string",
|
|
1710
|
+
"required": true,
|
|
1711
|
+
"description": "CRM platform connection key",
|
|
1712
|
+
"connection": { "platform": "attio" }
|
|
1713
|
+
},
|
|
1714
|
+
"slackConnectionKey": {
|
|
1715
|
+
"type": "string",
|
|
1716
|
+
"required": true,
|
|
1717
|
+
"description": "Slack connection key",
|
|
1718
|
+
"connection": { "platform": "slack" }
|
|
1719
|
+
},
|
|
1720
|
+
"slackChannel": {
|
|
1721
|
+
"type": "string",
|
|
1722
|
+
"required": true,
|
|
1723
|
+
"description": "Slack channel name or ID"
|
|
1724
|
+
}
|
|
1725
|
+
},
|
|
1726
|
+
"steps": [
|
|
1727
|
+
{
|
|
1728
|
+
"id": "fetchContacts",
|
|
1729
|
+
"name": "Fetch recent contacts",
|
|
1730
|
+
"type": "action",
|
|
1731
|
+
"action": {
|
|
1732
|
+
"platform": "attio",
|
|
1733
|
+
"actionId": "ATTIO_LIST_PEOPLE_ACTION_ID",
|
|
1734
|
+
"connectionKey": "$.input.crmConnectionKey",
|
|
1735
|
+
"queryParams": { "limit": "10" }
|
|
1736
|
+
}
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
"id": "buildSummary",
|
|
1740
|
+
"name": "Build formatted summary",
|
|
1741
|
+
"type": "code",
|
|
1742
|
+
"code": {
|
|
1743
|
+
"source": "const contacts = $.steps.fetchContacts.response.data || [];\\nconst lines = contacts.map((c, i) => \`\${i+1}. \${c.name || 'Unknown'} \u2014 \${c.email || 'no email'}\`);\\nreturn { summary: \`Found \${contacts.length} contacts:\\n\${lines.join('\\n')}\` };"
|
|
1744
|
+
}
|
|
1745
|
+
},
|
|
1746
|
+
{
|
|
1747
|
+
"id": "notifySlack",
|
|
1748
|
+
"name": "Post summary to Slack",
|
|
1749
|
+
"type": "action",
|
|
1750
|
+
"action": {
|
|
1751
|
+
"platform": "slack",
|
|
1752
|
+
"actionId": "SLACK_SEND_MESSAGE_ACTION_ID",
|
|
1753
|
+
"connectionKey": "$.input.slackConnectionKey",
|
|
1754
|
+
"data": {
|
|
1755
|
+
"channel": "$.input.slackChannel",
|
|
1756
|
+
"text": "{{$.steps.buildSummary.output.summary}}"
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
]
|
|
1761
|
+
}
|
|
1762
|
+
\`\`\`
|
|
1763
|
+
|
|
1764
|
+
Note: Action IDs above are placeholders. Always use \`one --agent actions search <platform> "<query>"\` to find real IDs.
|
|
1765
|
+
|
|
1766
|
+
## AI-Augmented Pattern
|
|
1767
|
+
|
|
1768
|
+
For workflows that need analysis/summarization, use the file-write \u2192 bash \u2192 code pattern:
|
|
1769
|
+
|
|
1770
|
+
1. \`file-write\` \u2014 save data to temp file
|
|
1771
|
+
2. \`bash\` \u2014 \`claude --print\` analyzes it (\`parseJson: true\`, \`timeout: 180000\`)
|
|
1772
|
+
3. \`code\` \u2014 parse and structure the output
|
|
1773
|
+
|
|
1774
|
+
Set timeout to at least 180000ms (3 min). Run Claude-heavy flows sequentially, not in parallel.
|
|
1775
|
+
|
|
1776
|
+
## Notes
|
|
1777
|
+
|
|
1778
|
+
- Connection keys are **inputs**, not hardcoded
|
|
1779
|
+
- Action IDs in examples are placeholders \u2014 always use \`actions search\`
|
|
1780
|
+
- Inline \`code.source\` steps allow \`require('crypto' | 'buffer' | 'url' | 'path')\` \u2014 \`fs\`, \`http\`, \`child_process\` are blocked
|
|
1781
|
+
- For anything beyond one-liners, use \`code.module\` to point at a \`.mjs\` file in the flow's \`lib/\` folder \u2014 runs as a child \`node\` process with full Node APIs, reads \`$\` from stdin, writes JSON to stdout
|
|
1782
|
+
- Bash steps require \`--allow-bash\` flag
|
|
1783
|
+
- State is persisted after every step \u2014 resume picks up where it left off`);
|
|
1784
|
+
return sections.join("\n");
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1095
1787
|
// src/lib/flow-runner.ts
|
|
1096
1788
|
var FLOWS_DIR = ".one/flows";
|
|
1097
1789
|
var RUNS_DIR = ".one/flows/.runs";
|
|
@@ -1299,42 +1991,126 @@ function resolveFlowPath(keyOrPath) {
|
|
|
1299
1991
|
if (keyOrPath.includes("/") || keyOrPath.includes("\\") || keyOrPath.endsWith(".json")) {
|
|
1300
1992
|
return path2.resolve(keyOrPath);
|
|
1301
1993
|
}
|
|
1302
|
-
|
|
1994
|
+
const folderPath = path2.resolve(FLOWS_DIR, keyOrPath, "flow.json");
|
|
1995
|
+
const legacyPath = path2.resolve(FLOWS_DIR, `${keyOrPath}.flow.json`);
|
|
1996
|
+
if (fs2.existsSync(folderPath)) return folderPath;
|
|
1997
|
+
if (fs2.existsSync(legacyPath)) return legacyPath;
|
|
1998
|
+
return folderPath;
|
|
1999
|
+
}
|
|
2000
|
+
function getFlowRootDir(flowFilePath) {
|
|
2001
|
+
const dir = path2.dirname(flowFilePath);
|
|
2002
|
+
const base = path2.basename(flowFilePath);
|
|
2003
|
+
if (base === "flow.json") return dir;
|
|
2004
|
+
return dir;
|
|
2005
|
+
}
|
|
2006
|
+
function loadFlowWithMeta(keyOrPath) {
|
|
2007
|
+
const filePath = resolveFlowPath(keyOrPath);
|
|
2008
|
+
if (!fs2.existsSync(filePath)) {
|
|
2009
|
+
throw new Error(`Flow not found: ${filePath}`);
|
|
2010
|
+
}
|
|
2011
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
2012
|
+
const flow = JSON.parse(content);
|
|
2013
|
+
return { flow, filePath, rootDir: getFlowRootDir(filePath) };
|
|
1303
2014
|
}
|
|
1304
2015
|
function loadFlow(keyOrPath) {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
2016
|
+
return loadFlowWithMeta(keyOrPath).flow;
|
|
2017
|
+
}
|
|
2018
|
+
function walkSteps(steps, visit) {
|
|
2019
|
+
const nested = getNestedStepsKeys();
|
|
2020
|
+
for (const step of steps) {
|
|
2021
|
+
if (visit(step)) return true;
|
|
2022
|
+
for (const { configKey, fieldName } of nested) {
|
|
2023
|
+
const config = step[configKey];
|
|
2024
|
+
if (config && Array.isArray(config[fieldName])) {
|
|
2025
|
+
if (walkSteps(config[fieldName], visit)) return true;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
1308
2028
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
2029
|
+
return false;
|
|
2030
|
+
}
|
|
2031
|
+
function collectStepTypes(flow) {
|
|
2032
|
+
const types = /* @__PURE__ */ new Set();
|
|
2033
|
+
walkSteps(flow.steps, (step) => {
|
|
2034
|
+
types.add(step.type);
|
|
2035
|
+
});
|
|
2036
|
+
return Array.from(types).sort();
|
|
2037
|
+
}
|
|
2038
|
+
function flowRequiresBash(flow) {
|
|
2039
|
+
return walkSteps(flow.steps, (step) => step.type === "bash");
|
|
2040
|
+
}
|
|
2041
|
+
function flowUsesCodeModules(flow) {
|
|
2042
|
+
return walkSteps(flow.steps, (step) => step.type === "code" && !!step.code?.module);
|
|
2043
|
+
}
|
|
2044
|
+
function summarizeFlowInputs(flow) {
|
|
2045
|
+
return Object.entries(flow.inputs).map(([name, decl]) => ({
|
|
2046
|
+
name,
|
|
2047
|
+
type: decl.type,
|
|
2048
|
+
required: decl.required !== false,
|
|
2049
|
+
default: decl.default,
|
|
2050
|
+
description: decl.description,
|
|
2051
|
+
connection: decl.connection,
|
|
2052
|
+
// An input is auto-resolvable if it points to a connection (the engine
|
|
2053
|
+
// will pick it automatically when exactly one matching connection exists).
|
|
2054
|
+
autoResolvable: !!decl.connection
|
|
2055
|
+
}));
|
|
1311
2056
|
}
|
|
1312
2057
|
function listFlows() {
|
|
1313
2058
|
const flowsDir = path2.resolve(FLOWS_DIR);
|
|
1314
2059
|
if (!fs2.existsSync(flowsDir)) return [];
|
|
1315
|
-
const files = fs2.readdirSync(flowsDir).filter((f) => f.endsWith(".flow.json"));
|
|
1316
2060
|
const flows = [];
|
|
1317
|
-
|
|
2061
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
2062
|
+
const readFlowFile = (filePath) => {
|
|
1318
2063
|
try {
|
|
1319
|
-
const content = fs2.readFileSync(
|
|
2064
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
1320
2065
|
const flow = JSON.parse(content);
|
|
2066
|
+
if (seenKeys.has(flow.key)) return;
|
|
2067
|
+
seenKeys.add(flow.key);
|
|
1321
2068
|
flows.push({
|
|
1322
2069
|
key: flow.key,
|
|
1323
2070
|
name: flow.name,
|
|
1324
2071
|
description: flow.description,
|
|
1325
2072
|
inputCount: Object.keys(flow.inputs).length,
|
|
1326
2073
|
stepCount: flow.steps.length,
|
|
1327
|
-
path:
|
|
2074
|
+
path: filePath,
|
|
2075
|
+
layout: path2.basename(filePath) === "flow.json" ? "folder" : "legacy",
|
|
2076
|
+
stepTypes: collectStepTypes(flow),
|
|
2077
|
+
requiresBash: flowRequiresBash(flow),
|
|
2078
|
+
usesCodeModules: flowUsesCodeModules(flow),
|
|
2079
|
+
inputs: summarizeFlowInputs(flow)
|
|
1328
2080
|
});
|
|
1329
2081
|
} catch {
|
|
1330
2082
|
}
|
|
2083
|
+
};
|
|
2084
|
+
for (const entry of fs2.readdirSync(flowsDir, { withFileTypes: true })) {
|
|
2085
|
+
if (entry.name.startsWith(".")) continue;
|
|
2086
|
+
const full = path2.join(flowsDir, entry.name);
|
|
2087
|
+
if (entry.isDirectory()) {
|
|
2088
|
+
const flowJson = path2.join(full, "flow.json");
|
|
2089
|
+
if (fs2.existsSync(flowJson)) readFlowFile(flowJson);
|
|
2090
|
+
} else if (entry.isFile() && entry.name.endsWith(".flow.json")) {
|
|
2091
|
+
readFlowFile(full);
|
|
2092
|
+
}
|
|
1331
2093
|
}
|
|
1332
2094
|
return flows;
|
|
1333
2095
|
}
|
|
1334
2096
|
function saveFlow(flow, outputPath) {
|
|
1335
|
-
|
|
2097
|
+
let flowPath;
|
|
2098
|
+
if (outputPath) {
|
|
2099
|
+
flowPath = path2.resolve(outputPath);
|
|
2100
|
+
} else {
|
|
2101
|
+
const legacyPath = path2.resolve(FLOWS_DIR, `${flow.key}.flow.json`);
|
|
2102
|
+
const folderPath = path2.resolve(FLOWS_DIR, flow.key, "flow.json");
|
|
2103
|
+
if (fs2.existsSync(legacyPath) && !fs2.existsSync(folderPath)) {
|
|
2104
|
+
flowPath = legacyPath;
|
|
2105
|
+
} else {
|
|
2106
|
+
flowPath = folderPath;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
1336
2109
|
const dir = path2.dirname(flowPath);
|
|
1337
2110
|
ensureDir(dir);
|
|
2111
|
+
if (path2.basename(flowPath) === "flow.json") {
|
|
2112
|
+
ensureDir(path2.join(dir, "lib"));
|
|
2113
|
+
}
|
|
1338
2114
|
fs2.writeFileSync(flowPath, JSON.stringify(flow, null, 2) + "\n");
|
|
1339
2115
|
return flowPath;
|
|
1340
2116
|
}
|
|
@@ -1346,9 +2122,20 @@ export {
|
|
|
1346
2122
|
isMethodAllowed,
|
|
1347
2123
|
isActionAllowed,
|
|
1348
2124
|
buildActionKnowledgeWithGuidance,
|
|
2125
|
+
FLOW_SCHEMA,
|
|
2126
|
+
getStepTypeDescriptor,
|
|
2127
|
+
getNestedStepsKeys,
|
|
2128
|
+
generateFlowGuide,
|
|
1349
2129
|
FlowRunner,
|
|
1350
2130
|
resolveFlowPath,
|
|
2131
|
+
getFlowRootDir,
|
|
2132
|
+
loadFlowWithMeta,
|
|
1351
2133
|
loadFlow,
|
|
2134
|
+
walkSteps,
|
|
2135
|
+
collectStepTypes,
|
|
2136
|
+
flowRequiresBash,
|
|
2137
|
+
flowUsesCodeModules,
|
|
2138
|
+
summarizeFlowInputs,
|
|
1352
2139
|
listFlows,
|
|
1353
2140
|
saveFlow
|
|
1354
2141
|
};
|