@withone/cli 1.12.8 → 1.13.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 +4 -2
- package/dist/chunk-NHI5URAV.js +1221 -0
- package/dist/flow-runner-JHEAUFDL.js +14 -0
- package/dist/index.js +326 -1007
- package/package.json +1 -1
- package/skills/one-flow/SKILL.md +139 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
FlowRunner,
|
|
4
|
+
OneApi,
|
|
5
|
+
TimeoutError,
|
|
6
|
+
buildActionKnowledgeWithGuidance,
|
|
7
|
+
filterByPermissions,
|
|
8
|
+
isActionAllowed,
|
|
9
|
+
isMethodAllowed,
|
|
10
|
+
listFlows,
|
|
11
|
+
loadFlow,
|
|
12
|
+
resolveFlowPath,
|
|
13
|
+
saveFlow
|
|
14
|
+
} from "./chunk-NHI5URAV.js";
|
|
2
15
|
|
|
3
16
|
// src/index.ts
|
|
4
17
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -295,317 +308,6 @@ function getAgentStatuses() {
|
|
|
295
308
|
});
|
|
296
309
|
}
|
|
297
310
|
|
|
298
|
-
// src/lib/api.ts
|
|
299
|
-
var API_BASE = "https://api.withone.ai/v1";
|
|
300
|
-
var ApiError = class extends Error {
|
|
301
|
-
constructor(status, message) {
|
|
302
|
-
super(message);
|
|
303
|
-
this.status = status;
|
|
304
|
-
this.name = "ApiError";
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
var OneApi = class {
|
|
308
|
-
constructor(apiKey) {
|
|
309
|
-
this.apiKey = apiKey;
|
|
310
|
-
}
|
|
311
|
-
async request(path6) {
|
|
312
|
-
return this.requestFull({ path: path6 });
|
|
313
|
-
}
|
|
314
|
-
async requestFull(opts) {
|
|
315
|
-
let url = `${API_BASE}${opts.path}`;
|
|
316
|
-
if (opts.queryParams && Object.keys(opts.queryParams).length > 0) {
|
|
317
|
-
const params = new URLSearchParams(opts.queryParams);
|
|
318
|
-
url += `?${params.toString()}`;
|
|
319
|
-
}
|
|
320
|
-
const headers = {
|
|
321
|
-
"x-one-secret": this.apiKey,
|
|
322
|
-
"Content-Type": "application/json",
|
|
323
|
-
...opts.headers
|
|
324
|
-
};
|
|
325
|
-
const fetchOpts = {
|
|
326
|
-
method: opts.method || "GET",
|
|
327
|
-
headers
|
|
328
|
-
};
|
|
329
|
-
if (opts.body !== void 0) {
|
|
330
|
-
fetchOpts.body = JSON.stringify(opts.body);
|
|
331
|
-
}
|
|
332
|
-
const response = await fetch(url, fetchOpts);
|
|
333
|
-
if (!response.ok) {
|
|
334
|
-
const text4 = await response.text();
|
|
335
|
-
throw new ApiError(response.status, text4 || `HTTP ${response.status}`);
|
|
336
|
-
}
|
|
337
|
-
return response.json();
|
|
338
|
-
}
|
|
339
|
-
async validateApiKey() {
|
|
340
|
-
try {
|
|
341
|
-
await this.listConnections();
|
|
342
|
-
return true;
|
|
343
|
-
} catch (error2) {
|
|
344
|
-
if (error2 instanceof ApiError && error2.status === 401) {
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
throw error2;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
async listConnections() {
|
|
351
|
-
const allConnections = [];
|
|
352
|
-
let page = 1;
|
|
353
|
-
let totalPages = 1;
|
|
354
|
-
do {
|
|
355
|
-
const response = await this.request(`/vault/connections?page=${page}&limit=100`);
|
|
356
|
-
allConnections.push(...response.rows || []);
|
|
357
|
-
totalPages = response.pages || 1;
|
|
358
|
-
page++;
|
|
359
|
-
} while (page <= totalPages);
|
|
360
|
-
return allConnections;
|
|
361
|
-
}
|
|
362
|
-
async listPlatforms() {
|
|
363
|
-
const allPlatforms = [];
|
|
364
|
-
let page = 1;
|
|
365
|
-
let totalPages = 1;
|
|
366
|
-
do {
|
|
367
|
-
const response = await this.request(`/available-connectors?page=${page}&limit=100`);
|
|
368
|
-
allPlatforms.push(...response.rows || []);
|
|
369
|
-
totalPages = response.pages || 1;
|
|
370
|
-
page++;
|
|
371
|
-
} while (page <= totalPages);
|
|
372
|
-
return allPlatforms;
|
|
373
|
-
}
|
|
374
|
-
async searchActions(platform, query, agentType) {
|
|
375
|
-
const isKnowledgeAgent = !agentType || agentType === "knowledge";
|
|
376
|
-
const queryParams = {
|
|
377
|
-
query,
|
|
378
|
-
limit: "5"
|
|
379
|
-
};
|
|
380
|
-
if (isKnowledgeAgent) {
|
|
381
|
-
queryParams.knowledgeAgent = "true";
|
|
382
|
-
} else {
|
|
383
|
-
queryParams.executeAgent = "true";
|
|
384
|
-
}
|
|
385
|
-
const response = await this.requestFull({
|
|
386
|
-
path: `/available-actions/search/${platform}`,
|
|
387
|
-
queryParams
|
|
388
|
-
});
|
|
389
|
-
return response || [];
|
|
390
|
-
}
|
|
391
|
-
async getActionDetails(actionId) {
|
|
392
|
-
const response = await this.requestFull({
|
|
393
|
-
path: "/knowledge",
|
|
394
|
-
queryParams: { _id: actionId }
|
|
395
|
-
});
|
|
396
|
-
const actions2 = response?.rows || [];
|
|
397
|
-
if (actions2.length === 0) {
|
|
398
|
-
throw new ApiError(404, `Action with ID ${actionId} not found`);
|
|
399
|
-
}
|
|
400
|
-
return actions2[0];
|
|
401
|
-
}
|
|
402
|
-
async getActionKnowledge(actionId) {
|
|
403
|
-
const action = await this.getActionDetails(actionId);
|
|
404
|
-
if (!action.knowledge || !action.method) {
|
|
405
|
-
return {
|
|
406
|
-
knowledge: "No knowledge was found",
|
|
407
|
-
method: "No method was found"
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
return {
|
|
411
|
-
knowledge: action.knowledge,
|
|
412
|
-
method: action.method
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
async executePassthroughRequest(args, preloadedAction) {
|
|
416
|
-
const action = preloadedAction ?? await this.getActionDetails(args.actionId);
|
|
417
|
-
const method = action.method;
|
|
418
|
-
const contentType = args.isFormData ? "multipart/form-data" : args.isFormUrlEncoded ? "application/x-www-form-urlencoded" : "application/json";
|
|
419
|
-
const requestHeaders = {
|
|
420
|
-
"x-one-secret": this.apiKey,
|
|
421
|
-
"x-one-connection-key": args.connectionKey,
|
|
422
|
-
"x-one-action-id": action._id,
|
|
423
|
-
"Content-Type": contentType,
|
|
424
|
-
...args.headers
|
|
425
|
-
};
|
|
426
|
-
const finalActionPath = args.pathVariables ? replacePathVariables(action.path, args.pathVariables) : action.path;
|
|
427
|
-
const normalizedPath = finalActionPath.startsWith("/") ? finalActionPath : `/${finalActionPath}`;
|
|
428
|
-
const url = `${API_BASE.replace("/v1", "")}/v1/passthrough${normalizedPath}`;
|
|
429
|
-
const isCustomAction = action.tags?.includes("custom");
|
|
430
|
-
let requestData = args.data;
|
|
431
|
-
if (isCustomAction && method?.toLowerCase() !== "get") {
|
|
432
|
-
requestData = {
|
|
433
|
-
...args.data,
|
|
434
|
-
connectionKey: args.connectionKey
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
let queryString = "";
|
|
438
|
-
if (args.queryParams && Object.keys(args.queryParams).length > 0) {
|
|
439
|
-
const params = new URLSearchParams(
|
|
440
|
-
Object.entries(args.queryParams).map(([k, v]) => [k, String(v)])
|
|
441
|
-
);
|
|
442
|
-
queryString = `?${params.toString()}`;
|
|
443
|
-
}
|
|
444
|
-
const fullUrl = `${url}${queryString}`;
|
|
445
|
-
const fetchOpts = {
|
|
446
|
-
method,
|
|
447
|
-
headers: requestHeaders
|
|
448
|
-
};
|
|
449
|
-
if (method?.toLowerCase() !== "get" && requestData !== void 0) {
|
|
450
|
-
if (args.isFormUrlEncoded) {
|
|
451
|
-
const params = new URLSearchParams();
|
|
452
|
-
if (requestData && typeof requestData === "object" && !Array.isArray(requestData)) {
|
|
453
|
-
Object.entries(requestData).forEach(([key, value]) => {
|
|
454
|
-
if (typeof value === "object") {
|
|
455
|
-
params.append(key, JSON.stringify(value));
|
|
456
|
-
} else {
|
|
457
|
-
params.append(key, String(value));
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
fetchOpts.body = params.toString();
|
|
462
|
-
} else if (args.isFormData) {
|
|
463
|
-
const boundary = `----FormBoundary${Date.now()}`;
|
|
464
|
-
requestHeaders["Content-Type"] = `multipart/form-data; boundary=${boundary}`;
|
|
465
|
-
let body = "";
|
|
466
|
-
if (requestData && typeof requestData === "object" && !Array.isArray(requestData)) {
|
|
467
|
-
Object.entries(requestData).forEach(([key, value]) => {
|
|
468
|
-
body += `--${boundary}\r
|
|
469
|
-
`;
|
|
470
|
-
body += `Content-Disposition: form-data; name="${key}"\r
|
|
471
|
-
\r
|
|
472
|
-
`;
|
|
473
|
-
body += typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
474
|
-
body += "\r\n";
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
body += `--${boundary}--\r
|
|
478
|
-
`;
|
|
479
|
-
fetchOpts.body = body;
|
|
480
|
-
fetchOpts.headers = requestHeaders;
|
|
481
|
-
} else {
|
|
482
|
-
fetchOpts.body = JSON.stringify(requestData);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
const sanitizedConfig = {
|
|
486
|
-
url: fullUrl,
|
|
487
|
-
method,
|
|
488
|
-
headers: {
|
|
489
|
-
...requestHeaders,
|
|
490
|
-
"x-one-secret": "***REDACTED***"
|
|
491
|
-
},
|
|
492
|
-
params: args.queryParams ? Object.fromEntries(
|
|
493
|
-
Object.entries(args.queryParams).map(([k, v]) => [k, String(v)])
|
|
494
|
-
) : void 0,
|
|
495
|
-
data: requestData
|
|
496
|
-
};
|
|
497
|
-
if (args.dryRun) {
|
|
498
|
-
return {
|
|
499
|
-
requestConfig: sanitizedConfig,
|
|
500
|
-
responseData: null
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
const response = await fetch(fullUrl, fetchOpts);
|
|
504
|
-
if (!response.ok) {
|
|
505
|
-
const text4 = await response.text();
|
|
506
|
-
throw new ApiError(response.status, text4 || `HTTP ${response.status}`);
|
|
507
|
-
}
|
|
508
|
-
const responseText = await response.text();
|
|
509
|
-
const responseData = responseText ? JSON.parse(responseText) : {};
|
|
510
|
-
return {
|
|
511
|
-
requestConfig: sanitizedConfig,
|
|
512
|
-
responseData
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
async waitForConnection(platform, timeoutMs = 5 * 60 * 1e3, pollIntervalMs = 5e3, onPoll) {
|
|
516
|
-
const startTime = Date.now();
|
|
517
|
-
const existingConnections = await this.listConnections();
|
|
518
|
-
const existingIds = new Set(existingConnections.map((c) => c.id));
|
|
519
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
520
|
-
await sleep(pollIntervalMs);
|
|
521
|
-
onPoll?.();
|
|
522
|
-
const currentConnections = await this.listConnections();
|
|
523
|
-
const newConnection = currentConnections.find(
|
|
524
|
-
(c) => c.platform.toLowerCase() === platform.toLowerCase() && !existingIds.has(c.id)
|
|
525
|
-
);
|
|
526
|
-
if (newConnection) {
|
|
527
|
-
return newConnection;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
throw new TimeoutError(`Timed out waiting for ${platform} connection`);
|
|
531
|
-
}
|
|
532
|
-
};
|
|
533
|
-
var TimeoutError = class extends Error {
|
|
534
|
-
constructor(message) {
|
|
535
|
-
super(message);
|
|
536
|
-
this.name = "TimeoutError";
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
|
-
function sleep(ms) {
|
|
540
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
541
|
-
}
|
|
542
|
-
function replacePathVariables(path6, variables) {
|
|
543
|
-
if (!path6) return path6;
|
|
544
|
-
let result = path6;
|
|
545
|
-
result = result.replace(/\{\{([^}]+)\}\}/g, (_match, variable) => {
|
|
546
|
-
const trimmedVariable = variable.trim();
|
|
547
|
-
const value = variables[trimmedVariable];
|
|
548
|
-
if (value === void 0 || value === null || value === "") {
|
|
549
|
-
throw new Error(`Missing value for path variable: ${trimmedVariable}`);
|
|
550
|
-
}
|
|
551
|
-
return encodeURIComponent(value.toString()).replace(/%3A/gi, ":");
|
|
552
|
-
});
|
|
553
|
-
result = result.replace(/\{([^}]+)\}/g, (_match, variable) => {
|
|
554
|
-
const trimmedVariable = variable.trim();
|
|
555
|
-
const value = variables[trimmedVariable];
|
|
556
|
-
if (value === void 0 || value === null || value === "") {
|
|
557
|
-
throw new Error(`Missing value for path variable: ${trimmedVariable}`);
|
|
558
|
-
}
|
|
559
|
-
return encodeURIComponent(value.toString()).replace(/%3A/gi, ":");
|
|
560
|
-
});
|
|
561
|
-
return result;
|
|
562
|
-
}
|
|
563
|
-
var PERMISSION_METHODS = {
|
|
564
|
-
read: ["GET"],
|
|
565
|
-
write: ["GET", "POST", "PUT", "PATCH"],
|
|
566
|
-
admin: null
|
|
567
|
-
};
|
|
568
|
-
function filterByPermissions(actions2, permissions) {
|
|
569
|
-
const allowed = PERMISSION_METHODS[permissions];
|
|
570
|
-
if (allowed === null) return actions2;
|
|
571
|
-
return actions2.filter((a) => allowed.includes(a.method.toUpperCase()));
|
|
572
|
-
}
|
|
573
|
-
function isMethodAllowed(method, permissions) {
|
|
574
|
-
const allowed = PERMISSION_METHODS[permissions];
|
|
575
|
-
if (allowed === null) return true;
|
|
576
|
-
return allowed.includes(method.toUpperCase());
|
|
577
|
-
}
|
|
578
|
-
function isActionAllowed(actionId, allowedActionIds) {
|
|
579
|
-
return allowedActionIds.includes("*") || allowedActionIds.includes(actionId);
|
|
580
|
-
}
|
|
581
|
-
function buildActionKnowledgeWithGuidance(knowledge, method, platform, actionId) {
|
|
582
|
-
const baseUrl = "https://api.withone.ai";
|
|
583
|
-
return `${knowledge}
|
|
584
|
-
|
|
585
|
-
API REQUEST STRUCTURE
|
|
586
|
-
======================
|
|
587
|
-
URL: ${baseUrl}/v1/passthrough/{{PATH}}
|
|
588
|
-
|
|
589
|
-
IMPORTANT: When constructing the URL, only include the API endpoint path after the base URL.
|
|
590
|
-
Do NOT include the full third-party API URL.
|
|
591
|
-
|
|
592
|
-
Examples:
|
|
593
|
-
Correct: ${baseUrl}/v1/passthrough/crm/v3/objects/contacts/search
|
|
594
|
-
Incorrect: ${baseUrl}/v1/passthrough/https://api.hubapi.com/crm/v3/objects/contacts/search
|
|
595
|
-
|
|
596
|
-
METHOD: ${method}
|
|
597
|
-
|
|
598
|
-
HEADERS:
|
|
599
|
-
- x-one-secret: {{process.env.ONE_SECRET}}
|
|
600
|
-
- x-one-connection-key: {{process.env.ONE_${platform.toUpperCase()}_CONNECTION_KEY}}
|
|
601
|
-
- x-one-action-id: ${actionId}
|
|
602
|
-
- ... (other headers)
|
|
603
|
-
|
|
604
|
-
BODY: {{BODY}}
|
|
605
|
-
|
|
606
|
-
QUERY PARAMS: {{QUERY_PARAMS}}`;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
311
|
// src/lib/browser.ts
|
|
610
312
|
import open from "open";
|
|
611
313
|
var ONE_APP_URL = "https://app.withone.ai";
|
|
@@ -2016,7 +1718,11 @@ var VALID_STEP_TYPES = [
|
|
|
2016
1718
|
"loop",
|
|
2017
1719
|
"parallel",
|
|
2018
1720
|
"file-read",
|
|
2019
|
-
"file-write"
|
|
1721
|
+
"file-write",
|
|
1722
|
+
"while",
|
|
1723
|
+
"flow",
|
|
1724
|
+
"paginate",
|
|
1725
|
+
"bash"
|
|
2020
1726
|
];
|
|
2021
1727
|
var VALID_INPUT_TYPES = ["string", "number", "boolean", "object", "array"];
|
|
2022
1728
|
var VALID_ERROR_STRATEGIES = ["fail", "continue", "retry", "fallback"];
|
|
@@ -2077,123 +1783,193 @@ function validateFlowSchema(flow2) {
|
|
|
2077
1783
|
function validateStepsArray(steps, pathPrefix, errors) {
|
|
2078
1784
|
for (let i = 0; i < steps.length; i++) {
|
|
2079
1785
|
const step = steps[i];
|
|
2080
|
-
const
|
|
1786
|
+
const path4 = `${pathPrefix}[${i}]`;
|
|
2081
1787
|
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
2082
|
-
errors.push({ path:
|
|
1788
|
+
errors.push({ path: path4, message: "Step must be an object" });
|
|
2083
1789
|
continue;
|
|
2084
1790
|
}
|
|
2085
1791
|
const s = step;
|
|
2086
1792
|
if (!s.id || typeof s.id !== "string") {
|
|
2087
|
-
errors.push({ path: `${
|
|
1793
|
+
errors.push({ path: `${path4}.id`, message: 'Step must have a string "id"' });
|
|
2088
1794
|
}
|
|
2089
1795
|
if (!s.name || typeof s.name !== "string") {
|
|
2090
|
-
errors.push({ path: `${
|
|
1796
|
+
errors.push({ path: `${path4}.name`, message: 'Step must have a string "name"' });
|
|
2091
1797
|
}
|
|
2092
1798
|
if (!s.type || !VALID_STEP_TYPES.includes(s.type)) {
|
|
2093
|
-
errors.push({ path: `${
|
|
1799
|
+
errors.push({ path: `${path4}.type`, message: `Step type must be one of: ${VALID_STEP_TYPES.join(", ")}` });
|
|
2094
1800
|
}
|
|
2095
1801
|
if (s.onError && typeof s.onError === "object") {
|
|
2096
1802
|
const oe = s.onError;
|
|
2097
1803
|
if (!VALID_ERROR_STRATEGIES.includes(oe.strategy)) {
|
|
2098
|
-
errors.push({ path: `${
|
|
1804
|
+
errors.push({ path: `${path4}.onError.strategy`, message: `Error strategy must be one of: ${VALID_ERROR_STRATEGIES.join(", ")}` });
|
|
2099
1805
|
}
|
|
2100
1806
|
}
|
|
2101
1807
|
const type = s.type;
|
|
2102
1808
|
if (type === "action") {
|
|
2103
1809
|
if (!s.action || typeof s.action !== "object") {
|
|
2104
|
-
errors.push({ path: `${
|
|
1810
|
+
errors.push({ path: `${path4}.action`, message: 'Action step must have an "action" config object' });
|
|
2105
1811
|
} else {
|
|
2106
1812
|
const a = s.action;
|
|
2107
|
-
if (!a.platform) errors.push({ path: `${
|
|
2108
|
-
if (!a.actionId) errors.push({ path: `${
|
|
2109
|
-
if (!a.connectionKey) errors.push({ path: `${
|
|
1813
|
+
if (!a.platform) errors.push({ path: `${path4}.action.platform`, message: 'Action must have "platform"' });
|
|
1814
|
+
if (!a.actionId) errors.push({ path: `${path4}.action.actionId`, message: 'Action must have "actionId"' });
|
|
1815
|
+
if (!a.connectionKey) errors.push({ path: `${path4}.action.connectionKey`, message: 'Action must have "connectionKey"' });
|
|
2110
1816
|
}
|
|
2111
1817
|
} else if (type === "transform") {
|
|
2112
1818
|
if (!s.transform || typeof s.transform !== "object") {
|
|
2113
|
-
errors.push({ path: `${
|
|
1819
|
+
errors.push({ path: `${path4}.transform`, message: 'Transform step must have a "transform" config object' });
|
|
2114
1820
|
} else {
|
|
2115
1821
|
const t = s.transform;
|
|
2116
1822
|
if (!t.expression || typeof t.expression !== "string") {
|
|
2117
|
-
errors.push({ path: `${
|
|
1823
|
+
errors.push({ path: `${path4}.transform.expression`, message: 'Transform must have a string "expression"' });
|
|
2118
1824
|
}
|
|
2119
1825
|
}
|
|
2120
1826
|
} else if (type === "code") {
|
|
2121
1827
|
if (!s.code || typeof s.code !== "object") {
|
|
2122
|
-
errors.push({ path: `${
|
|
1828
|
+
errors.push({ path: `${path4}.code`, message: 'Code step must have a "code" config object' });
|
|
2123
1829
|
} else {
|
|
2124
1830
|
const c = s.code;
|
|
2125
1831
|
if (!c.source || typeof c.source !== "string") {
|
|
2126
|
-
errors.push({ path: `${
|
|
1832
|
+
errors.push({ path: `${path4}.code.source`, message: 'Code must have a string "source"' });
|
|
2127
1833
|
}
|
|
2128
1834
|
}
|
|
2129
1835
|
} else if (type === "condition") {
|
|
2130
1836
|
if (!s.condition || typeof s.condition !== "object") {
|
|
2131
|
-
errors.push({ path: `${
|
|
1837
|
+
errors.push({ path: `${path4}.condition`, message: 'Condition step must have a "condition" config object' });
|
|
2132
1838
|
} else {
|
|
2133
1839
|
const c = s.condition;
|
|
2134
1840
|
if (!c.expression || typeof c.expression !== "string") {
|
|
2135
|
-
errors.push({ path: `${
|
|
1841
|
+
errors.push({ path: `${path4}.condition.expression`, message: 'Condition must have a string "expression"' });
|
|
2136
1842
|
}
|
|
2137
1843
|
if (!Array.isArray(c.then)) {
|
|
2138
|
-
errors.push({ path: `${
|
|
1844
|
+
errors.push({ path: `${path4}.condition.then`, message: 'Condition must have a "then" steps array' });
|
|
2139
1845
|
} else {
|
|
2140
|
-
validateStepsArray(c.then, `${
|
|
1846
|
+
validateStepsArray(c.then, `${path4}.condition.then`, errors);
|
|
2141
1847
|
}
|
|
2142
1848
|
if (c.else !== void 0) {
|
|
2143
1849
|
if (!Array.isArray(c.else)) {
|
|
2144
|
-
errors.push({ path: `${
|
|
1850
|
+
errors.push({ path: `${path4}.condition.else`, message: 'Condition "else" must be a steps array' });
|
|
2145
1851
|
} else {
|
|
2146
|
-
validateStepsArray(c.else, `${
|
|
1852
|
+
validateStepsArray(c.else, `${path4}.condition.else`, errors);
|
|
2147
1853
|
}
|
|
2148
1854
|
}
|
|
2149
1855
|
}
|
|
2150
1856
|
} else if (type === "loop") {
|
|
2151
1857
|
if (!s.loop || typeof s.loop !== "object") {
|
|
2152
|
-
errors.push({ path: `${
|
|
1858
|
+
errors.push({ path: `${path4}.loop`, message: 'Loop step must have a "loop" config object' });
|
|
2153
1859
|
} else {
|
|
2154
1860
|
const l = s.loop;
|
|
2155
1861
|
if (!l.over || typeof l.over !== "string") {
|
|
2156
|
-
errors.push({ path: `${
|
|
1862
|
+
errors.push({ path: `${path4}.loop.over`, message: 'Loop must have a string "over" selector' });
|
|
2157
1863
|
}
|
|
2158
1864
|
if (!l.as || typeof l.as !== "string") {
|
|
2159
|
-
errors.push({ path: `${
|
|
1865
|
+
errors.push({ path: `${path4}.loop.as`, message: 'Loop must have a string "as" variable name' });
|
|
2160
1866
|
}
|
|
2161
1867
|
if (!Array.isArray(l.steps)) {
|
|
2162
|
-
errors.push({ path: `${
|
|
1868
|
+
errors.push({ path: `${path4}.loop.steps`, message: 'Loop must have a "steps" array' });
|
|
2163
1869
|
} else {
|
|
2164
|
-
validateStepsArray(l.steps, `${
|
|
1870
|
+
validateStepsArray(l.steps, `${path4}.loop.steps`, errors);
|
|
2165
1871
|
}
|
|
2166
1872
|
}
|
|
2167
1873
|
} else if (type === "parallel") {
|
|
2168
1874
|
if (!s.parallel || typeof s.parallel !== "object") {
|
|
2169
|
-
errors.push({ path: `${
|
|
1875
|
+
errors.push({ path: `${path4}.parallel`, message: 'Parallel step must have a "parallel" config object' });
|
|
2170
1876
|
} else {
|
|
2171
1877
|
const par = s.parallel;
|
|
2172
1878
|
if (!Array.isArray(par.steps)) {
|
|
2173
|
-
errors.push({ path: `${
|
|
1879
|
+
errors.push({ path: `${path4}.parallel.steps`, message: 'Parallel must have a "steps" array' });
|
|
2174
1880
|
} else {
|
|
2175
|
-
validateStepsArray(par.steps, `${
|
|
1881
|
+
validateStepsArray(par.steps, `${path4}.parallel.steps`, errors);
|
|
2176
1882
|
}
|
|
2177
1883
|
}
|
|
2178
1884
|
} else if (type === "file-read") {
|
|
2179
1885
|
if (!s.fileRead || typeof s.fileRead !== "object") {
|
|
2180
|
-
errors.push({ path: `${
|
|
1886
|
+
errors.push({ path: `${path4}.fileRead`, message: 'File-read step must have a "fileRead" config object' });
|
|
2181
1887
|
} else {
|
|
2182
1888
|
const fr = s.fileRead;
|
|
2183
1889
|
if (!fr.path || typeof fr.path !== "string") {
|
|
2184
|
-
errors.push({ path: `${
|
|
1890
|
+
errors.push({ path: `${path4}.fileRead.path`, message: 'File-read must have a string "path"' });
|
|
2185
1891
|
}
|
|
2186
1892
|
}
|
|
2187
1893
|
} else if (type === "file-write") {
|
|
2188
1894
|
if (!s.fileWrite || typeof s.fileWrite !== "object") {
|
|
2189
|
-
errors.push({ path: `${
|
|
1895
|
+
errors.push({ path: `${path4}.fileWrite`, message: 'File-write step must have a "fileWrite" config object' });
|
|
2190
1896
|
} else {
|
|
2191
1897
|
const fw = s.fileWrite;
|
|
2192
1898
|
if (!fw.path || typeof fw.path !== "string") {
|
|
2193
|
-
errors.push({ path: `${
|
|
1899
|
+
errors.push({ path: `${path4}.fileWrite.path`, message: 'File-write must have a string "path"' });
|
|
2194
1900
|
}
|
|
2195
1901
|
if (fw.content === void 0) {
|
|
2196
|
-
errors.push({ path: `${
|
|
1902
|
+
errors.push({ path: `${path4}.fileWrite.content`, message: 'File-write must have "content"' });
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
} else if (type === "while") {
|
|
1906
|
+
if (!s.while || typeof s.while !== "object") {
|
|
1907
|
+
errors.push({ path: `${path4}.while`, message: 'While step must have a "while" config object' });
|
|
1908
|
+
} else {
|
|
1909
|
+
const w = s.while;
|
|
1910
|
+
if (!w.condition || typeof w.condition !== "string") {
|
|
1911
|
+
errors.push({ path: `${path4}.while.condition`, message: 'While must have a string "condition"' });
|
|
1912
|
+
}
|
|
1913
|
+
if (!Array.isArray(w.steps)) {
|
|
1914
|
+
errors.push({ path: `${path4}.while.steps`, message: 'While must have a "steps" array' });
|
|
1915
|
+
} else {
|
|
1916
|
+
validateStepsArray(w.steps, `${path4}.while.steps`, errors);
|
|
1917
|
+
}
|
|
1918
|
+
if (w.maxIterations !== void 0 && (typeof w.maxIterations !== "number" || w.maxIterations <= 0)) {
|
|
1919
|
+
errors.push({ path: `${path4}.while.maxIterations`, message: "maxIterations must be a positive number" });
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
} else if (type === "flow") {
|
|
1923
|
+
if (!s.flow || typeof s.flow !== "object") {
|
|
1924
|
+
errors.push({ path: `${path4}.flow`, message: 'Flow step must have a "flow" config object' });
|
|
1925
|
+
} else {
|
|
1926
|
+
const f = s.flow;
|
|
1927
|
+
if (!f.key || typeof f.key !== "string") {
|
|
1928
|
+
errors.push({ path: `${path4}.flow.key`, message: 'Flow must have a string "key"' });
|
|
1929
|
+
}
|
|
1930
|
+
if (f.inputs !== void 0 && (typeof f.inputs !== "object" || Array.isArray(f.inputs))) {
|
|
1931
|
+
errors.push({ path: `${path4}.flow.inputs`, message: "Flow inputs must be an object" });
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
} else if (type === "paginate") {
|
|
1935
|
+
if (!s.paginate || typeof s.paginate !== "object") {
|
|
1936
|
+
errors.push({ path: `${path4}.paginate`, message: 'Paginate step must have a "paginate" config object' });
|
|
1937
|
+
} else {
|
|
1938
|
+
const p7 = s.paginate;
|
|
1939
|
+
if (!p7.action || typeof p7.action !== "object") {
|
|
1940
|
+
errors.push({ path: `${path4}.paginate.action`, message: 'Paginate must have an "action" config object' });
|
|
1941
|
+
} else {
|
|
1942
|
+
const a = p7.action;
|
|
1943
|
+
if (!a.platform) errors.push({ path: `${path4}.paginate.action.platform`, message: 'Action must have "platform"' });
|
|
1944
|
+
if (!a.actionId) errors.push({ path: `${path4}.paginate.action.actionId`, message: 'Action must have "actionId"' });
|
|
1945
|
+
if (!a.connectionKey) errors.push({ path: `${path4}.paginate.action.connectionKey`, message: 'Action must have "connectionKey"' });
|
|
1946
|
+
}
|
|
1947
|
+
if (!p7.pageTokenField || typeof p7.pageTokenField !== "string") {
|
|
1948
|
+
errors.push({ path: `${path4}.paginate.pageTokenField`, message: 'Paginate must have a string "pageTokenField"' });
|
|
1949
|
+
}
|
|
1950
|
+
if (!p7.resultsField || typeof p7.resultsField !== "string") {
|
|
1951
|
+
errors.push({ path: `${path4}.paginate.resultsField`, message: 'Paginate must have a string "resultsField"' });
|
|
1952
|
+
}
|
|
1953
|
+
if (!p7.inputTokenParam || typeof p7.inputTokenParam !== "string") {
|
|
1954
|
+
errors.push({ path: `${path4}.paginate.inputTokenParam`, message: 'Paginate must have a string "inputTokenParam"' });
|
|
1955
|
+
}
|
|
1956
|
+
if (p7.maxPages !== void 0 && (typeof p7.maxPages !== "number" || p7.maxPages <= 0)) {
|
|
1957
|
+
errors.push({ path: `${path4}.paginate.maxPages`, message: "maxPages must be a positive number" });
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
} else if (type === "bash") {
|
|
1961
|
+
if (!s.bash || typeof s.bash !== "object") {
|
|
1962
|
+
errors.push({ path: `${path4}.bash`, message: 'Bash step must have a "bash" config object' });
|
|
1963
|
+
} else {
|
|
1964
|
+
const b = s.bash;
|
|
1965
|
+
if (!b.command || typeof b.command !== "string") {
|
|
1966
|
+
errors.push({ path: `${path4}.bash.command`, message: 'Bash must have a string "command"' });
|
|
1967
|
+
}
|
|
1968
|
+
if (b.timeout !== void 0 && (typeof b.timeout !== "number" || b.timeout <= 0)) {
|
|
1969
|
+
errors.push({ path: `${path4}.bash.timeout`, message: "timeout must be a positive number" });
|
|
1970
|
+
}
|
|
1971
|
+
if (b.parseJson !== void 0 && typeof b.parseJson !== "boolean") {
|
|
1972
|
+
errors.push({ path: `${path4}.bash.parseJson`, message: "parseJson must be a boolean" });
|
|
2197
1973
|
}
|
|
2198
1974
|
}
|
|
2199
1975
|
}
|
|
@@ -2205,18 +1981,19 @@ function validateStepIds(flow2) {
|
|
|
2205
1981
|
function collectIds(steps, pathPrefix) {
|
|
2206
1982
|
for (let i = 0; i < steps.length; i++) {
|
|
2207
1983
|
const step = steps[i];
|
|
2208
|
-
const
|
|
1984
|
+
const path4 = `${pathPrefix}[${i}]`;
|
|
2209
1985
|
if (seen.has(step.id)) {
|
|
2210
|
-
errors.push({ path: `${
|
|
1986
|
+
errors.push({ path: `${path4}.id`, message: `Duplicate step ID: "${step.id}"` });
|
|
2211
1987
|
} else {
|
|
2212
1988
|
seen.add(step.id);
|
|
2213
1989
|
}
|
|
2214
1990
|
if (step.condition) {
|
|
2215
|
-
if (step.condition.then) collectIds(step.condition.then, `${
|
|
2216
|
-
if (step.condition.else) collectIds(step.condition.else, `${
|
|
1991
|
+
if (step.condition.then) collectIds(step.condition.then, `${path4}.condition.then`);
|
|
1992
|
+
if (step.condition.else) collectIds(step.condition.else, `${path4}.condition.else`);
|
|
2217
1993
|
}
|
|
2218
|
-
if (step.loop?.steps) collectIds(step.loop.steps, `${
|
|
2219
|
-
if (step.parallel?.steps) collectIds(step.parallel.steps, `${
|
|
1994
|
+
if (step.loop?.steps) collectIds(step.loop.steps, `${path4}.loop.steps`);
|
|
1995
|
+
if (step.parallel?.steps) collectIds(step.parallel.steps, `${path4}.parallel.steps`);
|
|
1996
|
+
if (step.while?.steps) collectIds(step.while.steps, `${path4}.while.steps`);
|
|
2220
1997
|
}
|
|
2221
1998
|
}
|
|
2222
1999
|
collectIds(flow2.steps, "steps");
|
|
@@ -2241,6 +2018,9 @@ function validateSelectorReferences(flow2) {
|
|
|
2241
2018
|
if (step.parallel?.steps) {
|
|
2242
2019
|
for (const id of getAllStepIds(step.parallel.steps)) ids.add(id);
|
|
2243
2020
|
}
|
|
2021
|
+
if (step.while?.steps) {
|
|
2022
|
+
for (const id of getAllStepIds(step.while.steps)) ids.add(id);
|
|
2023
|
+
}
|
|
2244
2024
|
}
|
|
2245
2025
|
return ids;
|
|
2246
2026
|
}
|
|
@@ -2266,7 +2046,7 @@ function validateSelectorReferences(flow2) {
|
|
|
2266
2046
|
}
|
|
2267
2047
|
return selectors;
|
|
2268
2048
|
}
|
|
2269
|
-
function checkSelectors(selectors,
|
|
2049
|
+
function checkSelectors(selectors, path4) {
|
|
2270
2050
|
for (const selector of selectors) {
|
|
2271
2051
|
const parts = selector.split(".");
|
|
2272
2052
|
if (parts.length < 3) continue;
|
|
@@ -2274,12 +2054,12 @@ function validateSelectorReferences(flow2) {
|
|
|
2274
2054
|
if (root === "input") {
|
|
2275
2055
|
const inputName = parts[2];
|
|
2276
2056
|
if (!inputNames.has(inputName)) {
|
|
2277
|
-
errors.push({ path:
|
|
2057
|
+
errors.push({ path: path4, message: `Selector "${selector}" references undefined input "${inputName}"` });
|
|
2278
2058
|
}
|
|
2279
2059
|
} else if (root === "steps") {
|
|
2280
2060
|
const stepId = parts[2];
|
|
2281
2061
|
if (!allStepIds.has(stepId)) {
|
|
2282
|
-
errors.push({ path:
|
|
2062
|
+
errors.push({ path: path4, message: `Selector "${selector}" references undefined step "${stepId}"` });
|
|
2283
2063
|
}
|
|
2284
2064
|
}
|
|
2285
2065
|
}
|
|
@@ -2311,6 +2091,27 @@ function validateSelectorReferences(flow2) {
|
|
|
2311
2091
|
checkSelectors(extractSelectors(step.fileWrite.path), `${pathPrefix}.fileWrite.path`);
|
|
2312
2092
|
checkSelectors(extractSelectors(step.fileWrite.content), `${pathPrefix}.fileWrite.content`);
|
|
2313
2093
|
}
|
|
2094
|
+
if (step.while) {
|
|
2095
|
+
step.while.steps.forEach((s, i) => checkStep(s, `${pathPrefix}.while.steps[${i}]`));
|
|
2096
|
+
}
|
|
2097
|
+
if (step.flow) {
|
|
2098
|
+
checkSelectors(extractSelectors(step.flow.key), `${pathPrefix}.flow.key`);
|
|
2099
|
+
if (step.flow.inputs) {
|
|
2100
|
+
checkSelectors(extractSelectors(step.flow.inputs), `${pathPrefix}.flow.inputs`);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
if (step.paginate) {
|
|
2104
|
+
checkSelectors(extractSelectors(step.paginate.action), `${pathPrefix}.paginate.action`);
|
|
2105
|
+
}
|
|
2106
|
+
if (step.bash) {
|
|
2107
|
+
checkSelectors(extractSelectors(step.bash.command), `${pathPrefix}.bash.command`);
|
|
2108
|
+
if (step.bash.cwd) {
|
|
2109
|
+
checkSelectors(extractSelectors(step.bash.cwd), `${pathPrefix}.bash.cwd`);
|
|
2110
|
+
}
|
|
2111
|
+
if (step.bash.env) {
|
|
2112
|
+
checkSelectors(extractSelectors(step.bash.env), `${pathPrefix}.bash.env`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2314
2115
|
}
|
|
2315
2116
|
flow2.steps.forEach((step, i) => checkStep(step, `steps[${i}]`));
|
|
2316
2117
|
return errors;
|
|
@@ -2325,656 +2126,8 @@ function validateFlow(flow2) {
|
|
|
2325
2126
|
];
|
|
2326
2127
|
}
|
|
2327
2128
|
|
|
2328
|
-
// src/lib/flow-runner.ts
|
|
2329
|
-
import fs4 from "fs";
|
|
2330
|
-
import path5 from "path";
|
|
2331
|
-
import crypto from "crypto";
|
|
2332
|
-
|
|
2333
|
-
// src/lib/flow-engine.ts
|
|
2334
|
-
import fs3 from "fs";
|
|
2335
|
-
import path4 from "path";
|
|
2336
|
-
function sleep2(ms) {
|
|
2337
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2338
|
-
}
|
|
2339
|
-
function resolveSelector(selectorPath, context) {
|
|
2340
|
-
if (!selectorPath.startsWith("$.")) return selectorPath;
|
|
2341
|
-
const parts = selectorPath.slice(2).split(/\.|\[/).map((p7) => p7.replace(/\]$/, ""));
|
|
2342
|
-
let current = context;
|
|
2343
|
-
for (const part of parts) {
|
|
2344
|
-
if (current === null || current === void 0) return void 0;
|
|
2345
|
-
if (part === "*" && Array.isArray(current)) {
|
|
2346
|
-
continue;
|
|
2347
|
-
}
|
|
2348
|
-
if (Array.isArray(current) && part === "*") {
|
|
2349
|
-
continue;
|
|
2350
|
-
}
|
|
2351
|
-
if (Array.isArray(current)) {
|
|
2352
|
-
const idx = Number(part);
|
|
2353
|
-
if (!isNaN(idx)) {
|
|
2354
|
-
current = current[idx];
|
|
2355
|
-
} else {
|
|
2356
|
-
current = current.map((item) => item?.[part]);
|
|
2357
|
-
}
|
|
2358
|
-
} else if (typeof current === "object") {
|
|
2359
|
-
current = current[part];
|
|
2360
|
-
} else {
|
|
2361
|
-
return void 0;
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
return current;
|
|
2365
|
-
}
|
|
2366
|
-
function interpolateString(str, context) {
|
|
2367
|
-
return str.replace(/\{\{(\$\.[^}]+)\}\}/g, (_match, selector) => {
|
|
2368
|
-
const value = resolveSelector(selector, context);
|
|
2369
|
-
if (value === void 0 || value === null) return "";
|
|
2370
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
2371
|
-
return String(value);
|
|
2372
|
-
});
|
|
2373
|
-
}
|
|
2374
|
-
function resolveValue(value, context) {
|
|
2375
|
-
if (typeof value === "string") {
|
|
2376
|
-
if (value.startsWith("$.") && !value.includes("{{")) {
|
|
2377
|
-
return resolveSelector(value, context);
|
|
2378
|
-
}
|
|
2379
|
-
if (value.includes("{{$.")) {
|
|
2380
|
-
return interpolateString(value, context);
|
|
2381
|
-
}
|
|
2382
|
-
return value;
|
|
2383
|
-
}
|
|
2384
|
-
if (Array.isArray(value)) {
|
|
2385
|
-
return value.map((item) => resolveValue(item, context));
|
|
2386
|
-
}
|
|
2387
|
-
if (value && typeof value === "object") {
|
|
2388
|
-
const resolved = {};
|
|
2389
|
-
for (const [k, v] of Object.entries(value)) {
|
|
2390
|
-
resolved[k] = resolveValue(v, context);
|
|
2391
|
-
}
|
|
2392
|
-
return resolved;
|
|
2393
|
-
}
|
|
2394
|
-
return value;
|
|
2395
|
-
}
|
|
2396
|
-
function evaluateExpression(expr, context) {
|
|
2397
|
-
const fn = new Function("$", `return (${expr})`);
|
|
2398
|
-
return fn(context);
|
|
2399
|
-
}
|
|
2400
|
-
async function executeActionStep(step, context, api, permissions, allowedActionIds) {
|
|
2401
|
-
const action = step.action;
|
|
2402
|
-
const platform = resolveValue(action.platform, context);
|
|
2403
|
-
const actionId = resolveValue(action.actionId, context);
|
|
2404
|
-
const connectionKey = resolveValue(action.connectionKey, context);
|
|
2405
|
-
const data = action.data ? resolveValue(action.data, context) : void 0;
|
|
2406
|
-
const pathVars = action.pathVars ? resolveValue(action.pathVars, context) : void 0;
|
|
2407
|
-
const queryParams = action.queryParams ? resolveValue(action.queryParams, context) : void 0;
|
|
2408
|
-
const headers = action.headers ? resolveValue(action.headers, context) : void 0;
|
|
2409
|
-
if (!isActionAllowed(actionId, allowedActionIds)) {
|
|
2410
|
-
throw new Error(`Action "${actionId}" is not in the allowed action list`);
|
|
2411
|
-
}
|
|
2412
|
-
const actionDetails = await api.getActionDetails(actionId);
|
|
2413
|
-
if (!isMethodAllowed(actionDetails.method, permissions)) {
|
|
2414
|
-
throw new Error(`Method "${actionDetails.method}" is not allowed under "${permissions}" permission level`);
|
|
2415
|
-
}
|
|
2416
|
-
const result = await api.executePassthroughRequest({
|
|
2417
|
-
platform,
|
|
2418
|
-
actionId,
|
|
2419
|
-
connectionKey,
|
|
2420
|
-
data,
|
|
2421
|
-
pathVariables: pathVars,
|
|
2422
|
-
queryParams,
|
|
2423
|
-
headers
|
|
2424
|
-
}, actionDetails);
|
|
2425
|
-
return {
|
|
2426
|
-
status: "success",
|
|
2427
|
-
response: result.responseData,
|
|
2428
|
-
output: result.responseData
|
|
2429
|
-
};
|
|
2430
|
-
}
|
|
2431
|
-
function executeTransformStep(step, context) {
|
|
2432
|
-
const output = evaluateExpression(step.transform.expression, context);
|
|
2433
|
-
return { status: "success", output, response: output };
|
|
2434
|
-
}
|
|
2435
|
-
async function executeCodeStep(step, context) {
|
|
2436
|
-
const source = step.code.source;
|
|
2437
|
-
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
2438
|
-
}).constructor;
|
|
2439
|
-
const fn = new AsyncFunction("$", source);
|
|
2440
|
-
const output = await fn(context);
|
|
2441
|
-
return { status: "success", output, response: output };
|
|
2442
|
-
}
|
|
2443
|
-
async function executeConditionStep(step, context, api, permissions, allowedActionIds, options) {
|
|
2444
|
-
const condition = step.condition;
|
|
2445
|
-
const result = evaluateExpression(condition.expression, context);
|
|
2446
|
-
const branch = result ? condition.then : condition.else || [];
|
|
2447
|
-
const branchResults = await executeSteps(branch, context, api, permissions, allowedActionIds, options);
|
|
2448
|
-
return {
|
|
2449
|
-
status: "success",
|
|
2450
|
-
output: { conditionResult: !!result, stepsExecuted: branchResults },
|
|
2451
|
-
response: { conditionResult: !!result }
|
|
2452
|
-
};
|
|
2453
|
-
}
|
|
2454
|
-
async function executeLoopStep(step, context, api, permissions, allowedActionIds, options) {
|
|
2455
|
-
const loop = step.loop;
|
|
2456
|
-
const items = resolveValue(loop.over, context);
|
|
2457
|
-
if (!Array.isArray(items)) {
|
|
2458
|
-
throw new Error(`Loop "over" must resolve to an array, got ${typeof items}`);
|
|
2459
|
-
}
|
|
2460
|
-
const maxIterations = loop.maxIterations || 1e3;
|
|
2461
|
-
const bounded = items.slice(0, maxIterations);
|
|
2462
|
-
const savedLoop = { ...context.loop };
|
|
2463
|
-
if (loop.maxConcurrency && loop.maxConcurrency > 1) {
|
|
2464
|
-
const results2 = new Array(bounded.length);
|
|
2465
|
-
for (let batchStart = 0; batchStart < bounded.length; batchStart += loop.maxConcurrency) {
|
|
2466
|
-
const batch = bounded.slice(batchStart, batchStart + loop.maxConcurrency);
|
|
2467
|
-
const batchResults = await Promise.all(
|
|
2468
|
-
batch.map(async (item, batchIdx) => {
|
|
2469
|
-
const i = batchStart + batchIdx;
|
|
2470
|
-
const iterContext = {
|
|
2471
|
-
...context,
|
|
2472
|
-
loop: {
|
|
2473
|
-
[loop.as]: item,
|
|
2474
|
-
item,
|
|
2475
|
-
i,
|
|
2476
|
-
...loop.indexAs ? { [loop.indexAs]: i } : {}
|
|
2477
|
-
},
|
|
2478
|
-
steps: { ...context.steps }
|
|
2479
|
-
};
|
|
2480
|
-
await executeSteps(loop.steps, iterContext, api, permissions, allowedActionIds, options);
|
|
2481
|
-
Object.assign(context.steps, iterContext.steps);
|
|
2482
|
-
return iterContext.loop[loop.as];
|
|
2483
|
-
})
|
|
2484
|
-
);
|
|
2485
|
-
for (let j = 0; j < batchResults.length; j++) {
|
|
2486
|
-
results2[batchStart + j] = batchResults[j];
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
context.loop = savedLoop;
|
|
2490
|
-
return { status: "success", output: results2, response: results2 };
|
|
2491
|
-
}
|
|
2492
|
-
const results = [];
|
|
2493
|
-
for (let i = 0; i < bounded.length; i++) {
|
|
2494
|
-
context.loop = {
|
|
2495
|
-
[loop.as]: bounded[i],
|
|
2496
|
-
item: bounded[i],
|
|
2497
|
-
i
|
|
2498
|
-
};
|
|
2499
|
-
if (loop.indexAs) {
|
|
2500
|
-
context.loop[loop.indexAs] = i;
|
|
2501
|
-
}
|
|
2502
|
-
await executeSteps(loop.steps, context, api, permissions, allowedActionIds, options);
|
|
2503
|
-
results.push(context.loop[loop.as]);
|
|
2504
|
-
}
|
|
2505
|
-
context.loop = savedLoop;
|
|
2506
|
-
return { status: "success", output: results, response: results };
|
|
2507
|
-
}
|
|
2508
|
-
async function executeParallelStep(step, context, api, permissions, allowedActionIds, options) {
|
|
2509
|
-
const parallel = step.parallel;
|
|
2510
|
-
const maxConcurrency = parallel.maxConcurrency || 5;
|
|
2511
|
-
const steps = parallel.steps;
|
|
2512
|
-
const results = [];
|
|
2513
|
-
for (let i = 0; i < steps.length; i += maxConcurrency) {
|
|
2514
|
-
const batch = steps.slice(i, i + maxConcurrency);
|
|
2515
|
-
const batchResults = await Promise.all(
|
|
2516
|
-
batch.map((s) => executeSingleStep(s, context, api, permissions, allowedActionIds, options))
|
|
2517
|
-
);
|
|
2518
|
-
results.push(...batchResults);
|
|
2519
|
-
}
|
|
2520
|
-
return { status: "success", output: results, response: results };
|
|
2521
|
-
}
|
|
2522
|
-
function executeFileReadStep(step, context) {
|
|
2523
|
-
const config = step.fileRead;
|
|
2524
|
-
const filePath = resolveValue(config.path, context);
|
|
2525
|
-
const resolvedPath = path4.resolve(filePath);
|
|
2526
|
-
const content = fs3.readFileSync(resolvedPath, "utf-8");
|
|
2527
|
-
const output = config.parseJson ? JSON.parse(content) : content;
|
|
2528
|
-
return { status: "success", output, response: output };
|
|
2529
|
-
}
|
|
2530
|
-
function executeFileWriteStep(step, context) {
|
|
2531
|
-
const config = step.fileWrite;
|
|
2532
|
-
const filePath = resolveValue(config.path, context);
|
|
2533
|
-
const content = resolveValue(config.content, context);
|
|
2534
|
-
const resolvedPath = path4.resolve(filePath);
|
|
2535
|
-
const dir = path4.dirname(resolvedPath);
|
|
2536
|
-
if (!fs3.existsSync(dir)) {
|
|
2537
|
-
fs3.mkdirSync(dir, { recursive: true });
|
|
2538
|
-
}
|
|
2539
|
-
const stringContent = typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
2540
|
-
if (config.append) {
|
|
2541
|
-
fs3.appendFileSync(resolvedPath, stringContent);
|
|
2542
|
-
} else {
|
|
2543
|
-
fs3.writeFileSync(resolvedPath, stringContent);
|
|
2544
|
-
}
|
|
2545
|
-
return { status: "success", output: { path: resolvedPath, bytesWritten: stringContent.length }, response: { path: resolvedPath } };
|
|
2546
|
-
}
|
|
2547
|
-
async function executeSingleStep(step, context, api, permissions, allowedActionIds, options) {
|
|
2548
|
-
if (step.if) {
|
|
2549
|
-
const condResult = evaluateExpression(step.if, context);
|
|
2550
|
-
if (!condResult) {
|
|
2551
|
-
const result = { status: "skipped" };
|
|
2552
|
-
context.steps[step.id] = result;
|
|
2553
|
-
return result;
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
if (step.unless) {
|
|
2557
|
-
const condResult = evaluateExpression(step.unless, context);
|
|
2558
|
-
if (condResult) {
|
|
2559
|
-
const result = { status: "skipped" };
|
|
2560
|
-
context.steps[step.id] = result;
|
|
2561
|
-
return result;
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
const startTime = Date.now();
|
|
2565
|
-
let lastError;
|
|
2566
|
-
const maxAttempts = step.onError?.strategy === "retry" && step.onError.retries ? step.onError.retries + 1 : 1;
|
|
2567
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
2568
|
-
try {
|
|
2569
|
-
if (attempt > 1) {
|
|
2570
|
-
options.onEvent?.({
|
|
2571
|
-
event: "step:retry",
|
|
2572
|
-
stepId: step.id,
|
|
2573
|
-
attempt,
|
|
2574
|
-
maxRetries: step.onError.retries
|
|
2575
|
-
});
|
|
2576
|
-
const delay = step.onError?.retryDelayMs || 1e3;
|
|
2577
|
-
await sleep2(delay);
|
|
2578
|
-
}
|
|
2579
|
-
let result;
|
|
2580
|
-
switch (step.type) {
|
|
2581
|
-
case "action":
|
|
2582
|
-
result = await executeActionStep(step, context, api, permissions, allowedActionIds);
|
|
2583
|
-
break;
|
|
2584
|
-
case "transform":
|
|
2585
|
-
result = executeTransformStep(step, context);
|
|
2586
|
-
break;
|
|
2587
|
-
case "code":
|
|
2588
|
-
result = await executeCodeStep(step, context);
|
|
2589
|
-
break;
|
|
2590
|
-
case "condition":
|
|
2591
|
-
result = await executeConditionStep(step, context, api, permissions, allowedActionIds, options);
|
|
2592
|
-
break;
|
|
2593
|
-
case "loop":
|
|
2594
|
-
result = await executeLoopStep(step, context, api, permissions, allowedActionIds, options);
|
|
2595
|
-
break;
|
|
2596
|
-
case "parallel":
|
|
2597
|
-
result = await executeParallelStep(step, context, api, permissions, allowedActionIds, options);
|
|
2598
|
-
break;
|
|
2599
|
-
case "file-read":
|
|
2600
|
-
result = executeFileReadStep(step, context);
|
|
2601
|
-
break;
|
|
2602
|
-
case "file-write":
|
|
2603
|
-
result = executeFileWriteStep(step, context);
|
|
2604
|
-
break;
|
|
2605
|
-
default:
|
|
2606
|
-
throw new Error(`Unknown step type: ${step.type}`);
|
|
2607
|
-
}
|
|
2608
|
-
result.durationMs = Date.now() - startTime;
|
|
2609
|
-
if (attempt > 1) result.retries = attempt - 1;
|
|
2610
|
-
context.steps[step.id] = result;
|
|
2611
|
-
return result;
|
|
2612
|
-
} catch (err) {
|
|
2613
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2614
|
-
if (attempt === maxAttempts) {
|
|
2615
|
-
break;
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
2619
|
-
const errorMessage = lastError?.message || "Unknown error";
|
|
2620
|
-
const strategy = step.onError?.strategy || "fail";
|
|
2621
|
-
if (strategy === "continue") {
|
|
2622
|
-
const result = {
|
|
2623
|
-
status: "failed",
|
|
2624
|
-
error: errorMessage,
|
|
2625
|
-
durationMs: Date.now() - startTime
|
|
2626
|
-
};
|
|
2627
|
-
context.steps[step.id] = result;
|
|
2628
|
-
return result;
|
|
2629
|
-
}
|
|
2630
|
-
if (strategy === "fallback" && step.onError?.fallbackStepId) {
|
|
2631
|
-
const result = {
|
|
2632
|
-
status: "failed",
|
|
2633
|
-
error: errorMessage,
|
|
2634
|
-
durationMs: Date.now() - startTime
|
|
2635
|
-
};
|
|
2636
|
-
context.steps[step.id] = result;
|
|
2637
|
-
return result;
|
|
2638
|
-
}
|
|
2639
|
-
throw lastError;
|
|
2640
|
-
}
|
|
2641
|
-
async function executeSteps(steps, context, api, permissions, allowedActionIds, options, completedStepIds) {
|
|
2642
|
-
const results = [];
|
|
2643
|
-
for (const step of steps) {
|
|
2644
|
-
if (completedStepIds?.has(step.id)) {
|
|
2645
|
-
results.push(context.steps[step.id] || { status: "success" });
|
|
2646
|
-
continue;
|
|
2647
|
-
}
|
|
2648
|
-
options.onEvent?.({
|
|
2649
|
-
event: "step:start",
|
|
2650
|
-
stepId: step.id,
|
|
2651
|
-
stepName: step.name,
|
|
2652
|
-
type: step.type,
|
|
2653
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2654
|
-
});
|
|
2655
|
-
try {
|
|
2656
|
-
const result = await executeSingleStep(step, context, api, permissions, allowedActionIds, options);
|
|
2657
|
-
results.push(result);
|
|
2658
|
-
options.onEvent?.({
|
|
2659
|
-
event: "step:complete",
|
|
2660
|
-
stepId: step.id,
|
|
2661
|
-
status: result.status,
|
|
2662
|
-
durationMs: result.durationMs,
|
|
2663
|
-
retries: result.retries
|
|
2664
|
-
});
|
|
2665
|
-
} catch (error2) {
|
|
2666
|
-
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
2667
|
-
options.onEvent?.({
|
|
2668
|
-
event: "step:error",
|
|
2669
|
-
stepId: step.id,
|
|
2670
|
-
error: errorMsg,
|
|
2671
|
-
strategy: step.onError?.strategy || "fail"
|
|
2672
|
-
});
|
|
2673
|
-
throw error2;
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
return results;
|
|
2677
|
-
}
|
|
2678
|
-
async function executeFlow(flow2, inputs, api, permissions, allowedActionIds, options = {}, resumeState) {
|
|
2679
|
-
for (const [name, decl] of Object.entries(flow2.inputs)) {
|
|
2680
|
-
if (decl.required !== false && inputs[name] === void 0 && decl.default === void 0) {
|
|
2681
|
-
throw new Error(`Missing required input: "${name}" \u2014 ${decl.description || ""}`);
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
const resolvedInputs = {};
|
|
2685
|
-
for (const [name, decl] of Object.entries(flow2.inputs)) {
|
|
2686
|
-
if (inputs[name] !== void 0) {
|
|
2687
|
-
resolvedInputs[name] = inputs[name];
|
|
2688
|
-
} else if (decl.default !== void 0) {
|
|
2689
|
-
resolvedInputs[name] = decl.default;
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
const context = resumeState?.context || {
|
|
2693
|
-
input: resolvedInputs,
|
|
2694
|
-
env: process.env,
|
|
2695
|
-
steps: {},
|
|
2696
|
-
loop: {}
|
|
2697
|
-
};
|
|
2698
|
-
const completedStepIds = resumeState ? new Set(resumeState.completedSteps) : void 0;
|
|
2699
|
-
if (options.dryRun) {
|
|
2700
|
-
options.onEvent?.({
|
|
2701
|
-
event: "flow:dry-run",
|
|
2702
|
-
flowKey: flow2.key,
|
|
2703
|
-
resolvedInputs,
|
|
2704
|
-
steps: flow2.steps.map((s) => ({ id: s.id, name: s.name, type: s.type })),
|
|
2705
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2706
|
-
});
|
|
2707
|
-
return context;
|
|
2708
|
-
}
|
|
2709
|
-
options.onEvent?.({
|
|
2710
|
-
event: "flow:start",
|
|
2711
|
-
flowKey: flow2.key,
|
|
2712
|
-
totalSteps: flow2.steps.length,
|
|
2713
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2714
|
-
});
|
|
2715
|
-
const flowStart = Date.now();
|
|
2716
|
-
try {
|
|
2717
|
-
await executeSteps(flow2.steps, context, api, permissions, allowedActionIds, options, completedStepIds);
|
|
2718
|
-
const stepEntries = Object.values(context.steps);
|
|
2719
|
-
const completed = stepEntries.filter((s) => s.status === "success").length;
|
|
2720
|
-
const failed = stepEntries.filter((s) => s.status === "failed").length;
|
|
2721
|
-
const skipped = stepEntries.filter((s) => s.status === "skipped").length;
|
|
2722
|
-
options.onEvent?.({
|
|
2723
|
-
event: "flow:complete",
|
|
2724
|
-
flowKey: flow2.key,
|
|
2725
|
-
status: "success",
|
|
2726
|
-
durationMs: Date.now() - flowStart,
|
|
2727
|
-
stepsCompleted: completed,
|
|
2728
|
-
stepsFailed: failed,
|
|
2729
|
-
stepsSkipped: skipped
|
|
2730
|
-
});
|
|
2731
|
-
} catch (error2) {
|
|
2732
|
-
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
2733
|
-
options.onEvent?.({
|
|
2734
|
-
event: "flow:error",
|
|
2735
|
-
flowKey: flow2.key,
|
|
2736
|
-
status: "failed",
|
|
2737
|
-
error: errorMsg,
|
|
2738
|
-
durationMs: Date.now() - flowStart
|
|
2739
|
-
});
|
|
2740
|
-
throw error2;
|
|
2741
|
-
}
|
|
2742
|
-
return context;
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
// src/lib/flow-runner.ts
|
|
2746
|
-
var FLOWS_DIR = ".one/flows";
|
|
2747
|
-
var RUNS_DIR = ".one/flows/.runs";
|
|
2748
|
-
var LOGS_DIR = ".one/flows/.logs";
|
|
2749
|
-
function ensureDir(dir) {
|
|
2750
|
-
if (!fs4.existsSync(dir)) {
|
|
2751
|
-
fs4.mkdirSync(dir, { recursive: true });
|
|
2752
|
-
}
|
|
2753
|
-
}
|
|
2754
|
-
function generateRunId() {
|
|
2755
|
-
return crypto.randomBytes(6).toString("hex");
|
|
2756
|
-
}
|
|
2757
|
-
var FlowRunner = class _FlowRunner {
|
|
2758
|
-
runId;
|
|
2759
|
-
flowKey;
|
|
2760
|
-
state;
|
|
2761
|
-
logPath;
|
|
2762
|
-
statePath;
|
|
2763
|
-
paused = false;
|
|
2764
|
-
constructor(flow2, inputs, runId) {
|
|
2765
|
-
this.runId = runId || generateRunId();
|
|
2766
|
-
this.flowKey = flow2.key;
|
|
2767
|
-
ensureDir(RUNS_DIR);
|
|
2768
|
-
ensureDir(LOGS_DIR);
|
|
2769
|
-
this.statePath = path5.join(RUNS_DIR, `${flow2.key}-${this.runId}.state.json`);
|
|
2770
|
-
this.logPath = path5.join(LOGS_DIR, `${flow2.key}-${this.runId}.log`);
|
|
2771
|
-
this.state = {
|
|
2772
|
-
runId: this.runId,
|
|
2773
|
-
flowKey: flow2.key,
|
|
2774
|
-
status: "running",
|
|
2775
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2776
|
-
inputs,
|
|
2777
|
-
completedSteps: [],
|
|
2778
|
-
context: {
|
|
2779
|
-
input: inputs,
|
|
2780
|
-
env: {},
|
|
2781
|
-
steps: {},
|
|
2782
|
-
loop: {}
|
|
2783
|
-
}
|
|
2784
|
-
};
|
|
2785
|
-
}
|
|
2786
|
-
getRunId() {
|
|
2787
|
-
return this.runId;
|
|
2788
|
-
}
|
|
2789
|
-
getLogPath() {
|
|
2790
|
-
return this.logPath;
|
|
2791
|
-
}
|
|
2792
|
-
getStatePath() {
|
|
2793
|
-
return this.statePath;
|
|
2794
|
-
}
|
|
2795
|
-
requestPause() {
|
|
2796
|
-
this.paused = true;
|
|
2797
|
-
}
|
|
2798
|
-
log(level, msg, data) {
|
|
2799
|
-
const entry = {
|
|
2800
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2801
|
-
level,
|
|
2802
|
-
msg,
|
|
2803
|
-
...data
|
|
2804
|
-
};
|
|
2805
|
-
fs4.appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
|
|
2806
|
-
}
|
|
2807
|
-
saveState() {
|
|
2808
|
-
fs4.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2));
|
|
2809
|
-
}
|
|
2810
|
-
createEventHandler(externalHandler) {
|
|
2811
|
-
return (event) => {
|
|
2812
|
-
this.log(
|
|
2813
|
-
event.event.includes("error") ? "warn" : "info",
|
|
2814
|
-
event.event,
|
|
2815
|
-
event
|
|
2816
|
-
);
|
|
2817
|
-
if (event.event === "step:complete" && event.stepId) {
|
|
2818
|
-
this.state.completedSteps.push(event.stepId);
|
|
2819
|
-
this.state.currentStepId = void 0;
|
|
2820
|
-
}
|
|
2821
|
-
if (event.event === "step:start" && event.stepId) {
|
|
2822
|
-
this.state.currentStepId = event.stepId;
|
|
2823
|
-
}
|
|
2824
|
-
externalHandler?.(event);
|
|
2825
|
-
if (this.paused && event.event === "step:complete") {
|
|
2826
|
-
this.state.status = "paused";
|
|
2827
|
-
this.state.pausedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2828
|
-
this.saveState();
|
|
2829
|
-
}
|
|
2830
|
-
};
|
|
2831
|
-
}
|
|
2832
|
-
async execute(flow2, api, permissions, allowedActionIds, options = {}) {
|
|
2833
|
-
this.log("info", "Flow started", { flowKey: this.flowKey, runId: this.runId });
|
|
2834
|
-
this.state.status = "running";
|
|
2835
|
-
this.saveState();
|
|
2836
|
-
const eventHandler = this.createEventHandler(options.onEvent);
|
|
2837
|
-
try {
|
|
2838
|
-
const context = await executeFlow(
|
|
2839
|
-
flow2,
|
|
2840
|
-
this.state.inputs,
|
|
2841
|
-
api,
|
|
2842
|
-
permissions,
|
|
2843
|
-
allowedActionIds,
|
|
2844
|
-
{ ...options, onEvent: eventHandler }
|
|
2845
|
-
);
|
|
2846
|
-
this.state.status = "completed";
|
|
2847
|
-
this.state.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2848
|
-
this.state.context = context;
|
|
2849
|
-
this.saveState();
|
|
2850
|
-
this.log("info", "Flow completed", { status: "success" });
|
|
2851
|
-
return context;
|
|
2852
|
-
} catch (error2) {
|
|
2853
|
-
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
2854
|
-
this.state.status = "failed";
|
|
2855
|
-
this.state.context.steps = this.state.context.steps || {};
|
|
2856
|
-
this.saveState();
|
|
2857
|
-
this.log("error", "Flow failed", { error: errorMsg });
|
|
2858
|
-
throw error2;
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2861
|
-
async resume(flow2, api, permissions, allowedActionIds, options = {}) {
|
|
2862
|
-
this.log("info", "Flow resumed", { flowKey: this.flowKey, runId: this.runId });
|
|
2863
|
-
this.state.status = "running";
|
|
2864
|
-
this.state.pausedAt = void 0;
|
|
2865
|
-
this.saveState();
|
|
2866
|
-
const eventHandler = this.createEventHandler(options.onEvent);
|
|
2867
|
-
try {
|
|
2868
|
-
const context = await executeFlow(
|
|
2869
|
-
flow2,
|
|
2870
|
-
this.state.inputs,
|
|
2871
|
-
api,
|
|
2872
|
-
permissions,
|
|
2873
|
-
allowedActionIds,
|
|
2874
|
-
{ ...options, onEvent: eventHandler },
|
|
2875
|
-
{
|
|
2876
|
-
context: this.state.context,
|
|
2877
|
-
completedSteps: this.state.completedSteps
|
|
2878
|
-
}
|
|
2879
|
-
);
|
|
2880
|
-
this.state.status = "completed";
|
|
2881
|
-
this.state.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2882
|
-
this.state.context = context;
|
|
2883
|
-
this.saveState();
|
|
2884
|
-
this.log("info", "Flow completed after resume", { status: "success" });
|
|
2885
|
-
return context;
|
|
2886
|
-
} catch (error2) {
|
|
2887
|
-
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
2888
|
-
this.state.status = "failed";
|
|
2889
|
-
this.saveState();
|
|
2890
|
-
this.log("error", "Flow failed after resume", { error: errorMsg });
|
|
2891
|
-
throw error2;
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
static loadRunState(runId) {
|
|
2895
|
-
ensureDir(RUNS_DIR);
|
|
2896
|
-
const files = fs4.readdirSync(RUNS_DIR).filter((f) => f.includes(runId) && f.endsWith(".state.json"));
|
|
2897
|
-
if (files.length === 0) return null;
|
|
2898
|
-
try {
|
|
2899
|
-
const content = fs4.readFileSync(path5.join(RUNS_DIR, files[0]), "utf-8");
|
|
2900
|
-
return JSON.parse(content);
|
|
2901
|
-
} catch {
|
|
2902
|
-
return null;
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
2905
|
-
static fromRunState(state) {
|
|
2906
|
-
const runner = Object.create(_FlowRunner.prototype);
|
|
2907
|
-
runner.runId = state.runId;
|
|
2908
|
-
runner.flowKey = state.flowKey;
|
|
2909
|
-
runner.state = state;
|
|
2910
|
-
runner.paused = false;
|
|
2911
|
-
runner.statePath = path5.join(RUNS_DIR, `${state.flowKey}-${state.runId}.state.json`);
|
|
2912
|
-
runner.logPath = path5.join(LOGS_DIR, `${state.flowKey}-${state.runId}.log`);
|
|
2913
|
-
return runner;
|
|
2914
|
-
}
|
|
2915
|
-
static listRuns(flowKey) {
|
|
2916
|
-
ensureDir(RUNS_DIR);
|
|
2917
|
-
const files = fs4.readdirSync(RUNS_DIR).filter((f) => f.endsWith(".state.json"));
|
|
2918
|
-
const runs = [];
|
|
2919
|
-
for (const file of files) {
|
|
2920
|
-
try {
|
|
2921
|
-
const content = fs4.readFileSync(path5.join(RUNS_DIR, file), "utf-8");
|
|
2922
|
-
const state = JSON.parse(content);
|
|
2923
|
-
if (!flowKey || state.flowKey === flowKey) {
|
|
2924
|
-
runs.push(state);
|
|
2925
|
-
}
|
|
2926
|
-
} catch {
|
|
2927
|
-
}
|
|
2928
|
-
}
|
|
2929
|
-
return runs.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
2930
|
-
}
|
|
2931
|
-
};
|
|
2932
|
-
function resolveFlowPath(keyOrPath) {
|
|
2933
|
-
if (keyOrPath.includes("/") || keyOrPath.includes("\\") || keyOrPath.endsWith(".json")) {
|
|
2934
|
-
return path5.resolve(keyOrPath);
|
|
2935
|
-
}
|
|
2936
|
-
return path5.resolve(FLOWS_DIR, `${keyOrPath}.flow.json`);
|
|
2937
|
-
}
|
|
2938
|
-
function loadFlow(keyOrPath) {
|
|
2939
|
-
const flowPath = resolveFlowPath(keyOrPath);
|
|
2940
|
-
if (!fs4.existsSync(flowPath)) {
|
|
2941
|
-
throw new Error(`Flow not found: ${flowPath}`);
|
|
2942
|
-
}
|
|
2943
|
-
const content = fs4.readFileSync(flowPath, "utf-8");
|
|
2944
|
-
return JSON.parse(content);
|
|
2945
|
-
}
|
|
2946
|
-
function listFlows() {
|
|
2947
|
-
const flowsDir = path5.resolve(FLOWS_DIR);
|
|
2948
|
-
if (!fs4.existsSync(flowsDir)) return [];
|
|
2949
|
-
const files = fs4.readdirSync(flowsDir).filter((f) => f.endsWith(".flow.json"));
|
|
2950
|
-
const flows = [];
|
|
2951
|
-
for (const file of files) {
|
|
2952
|
-
try {
|
|
2953
|
-
const content = fs4.readFileSync(path5.join(flowsDir, file), "utf-8");
|
|
2954
|
-
const flow2 = JSON.parse(content);
|
|
2955
|
-
flows.push({
|
|
2956
|
-
key: flow2.key,
|
|
2957
|
-
name: flow2.name,
|
|
2958
|
-
description: flow2.description,
|
|
2959
|
-
inputCount: Object.keys(flow2.inputs).length,
|
|
2960
|
-
stepCount: flow2.steps.length,
|
|
2961
|
-
path: path5.join(flowsDir, file)
|
|
2962
|
-
});
|
|
2963
|
-
} catch {
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
return flows;
|
|
2967
|
-
}
|
|
2968
|
-
function saveFlow(flow2, outputPath) {
|
|
2969
|
-
const flowPath = outputPath ? path5.resolve(outputPath) : path5.resolve(FLOWS_DIR, `${flow2.key}.flow.json`);
|
|
2970
|
-
const dir = path5.dirname(flowPath);
|
|
2971
|
-
ensureDir(dir);
|
|
2972
|
-
fs4.writeFileSync(flowPath, JSON.stringify(flow2, null, 2) + "\n");
|
|
2973
|
-
return flowPath;
|
|
2974
|
-
}
|
|
2975
|
-
|
|
2976
2129
|
// src/commands/flow.ts
|
|
2977
|
-
import
|
|
2130
|
+
import fs3 from "fs";
|
|
2978
2131
|
function getConfig2() {
|
|
2979
2132
|
const apiKey = getApiKey();
|
|
2980
2133
|
if (!apiKey) {
|
|
@@ -3118,7 +2271,9 @@ ${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
|
|
|
3118
2271
|
try {
|
|
3119
2272
|
const context = await runner.execute(flow2, api, permissions, actionIds, {
|
|
3120
2273
|
dryRun: options.dryRun,
|
|
2274
|
+
mock: options.mock,
|
|
3121
2275
|
verbose: options.verbose,
|
|
2276
|
+
allowBash: options.allowBash,
|
|
3122
2277
|
onEvent
|
|
3123
2278
|
});
|
|
3124
2279
|
process.off("SIGINT", sigintHandler);
|
|
@@ -3204,7 +2359,7 @@ async function flowValidateCommand(keyOrPath) {
|
|
|
3204
2359
|
let flowData;
|
|
3205
2360
|
try {
|
|
3206
2361
|
const flowPath = resolveFlowPath(keyOrPath);
|
|
3207
|
-
const content =
|
|
2362
|
+
const content = fs3.readFileSync(flowPath, "utf-8");
|
|
3208
2363
|
flowData = JSON.parse(content);
|
|
3209
2364
|
} catch (err) {
|
|
3210
2365
|
spinner5.stop("Validation failed");
|
|
@@ -3802,6 +2957,128 @@ The \`source\` field contains a JS function body. The flow context is available
|
|
|
3802
2957
|
}
|
|
3803
2958
|
\`\`\`
|
|
3804
2959
|
|
|
2960
|
+
### \`while\` \u2014 Condition-driven loop (do-while)
|
|
2961
|
+
|
|
2962
|
+
Iterates until a condition becomes falsy. The first iteration always runs (do-while semantics), then the condition is checked before each subsequent iteration. Useful for pagination.
|
|
2963
|
+
|
|
2964
|
+
\`\`\`json
|
|
2965
|
+
{
|
|
2966
|
+
"id": "paginate",
|
|
2967
|
+
"name": "Paginate through all pages",
|
|
2968
|
+
"type": "while",
|
|
2969
|
+
"while": {
|
|
2970
|
+
"condition": "$.steps.paginate.output.lastResult.nextPageToken != null",
|
|
2971
|
+
"maxIterations": 50,
|
|
2972
|
+
"steps": [
|
|
2973
|
+
{
|
|
2974
|
+
"id": "fetchPage",
|
|
2975
|
+
"name": "Fetch next page",
|
|
2976
|
+
"type": "action",
|
|
2977
|
+
"action": {
|
|
2978
|
+
"platform": "gmail",
|
|
2979
|
+
"actionId": "GMAIL_LIST_MESSAGES_ACTION_ID",
|
|
2980
|
+
"connectionKey": "$.input.gmailKey",
|
|
2981
|
+
"queryParams": {
|
|
2982
|
+
"pageToken": "$.steps.paginate.output.lastResult.nextPageToken"
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
]
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
\`\`\`
|
|
2990
|
+
|
|
2991
|
+
| Field | Type | Description |
|
|
2992
|
+
|---|---|---|
|
|
2993
|
+
| \`condition\` | string | JS expression evaluated before each iteration (after iteration 0) |
|
|
2994
|
+
| \`maxIterations\` | number | Safety cap, default: 100 |
|
|
2995
|
+
| \`steps\` | FlowStep[] | Steps to execute each iteration |
|
|
2996
|
+
|
|
2997
|
+
The step output contains \`lastResult\` (last step's output from most recent iteration), \`iteration\` (count), and \`results\` (array of all iteration outputs). Reference via \`$.steps.<id>.output.lastResult\`.
|
|
2998
|
+
|
|
2999
|
+
### \`flow\` \u2014 Execute a sub-flow
|
|
3000
|
+
|
|
3001
|
+
Loads and executes another saved flow, enabling flow composition. Circular flows are detected and blocked.
|
|
3002
|
+
|
|
3003
|
+
\`\`\`json
|
|
3004
|
+
{
|
|
3005
|
+
"id": "processCustomer",
|
|
3006
|
+
"name": "Run customer enrichment flow",
|
|
3007
|
+
"type": "flow",
|
|
3008
|
+
"flow": {
|
|
3009
|
+
"key": "enrich-customer",
|
|
3010
|
+
"inputs": {
|
|
3011
|
+
"email": "$.steps.getCustomer.response.email",
|
|
3012
|
+
"connectionKey": "$.input.hubspotConnectionKey"
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
\`\`\`
|
|
3017
|
+
|
|
3018
|
+
| Field | Type | Description |
|
|
3019
|
+
|---|---|---|
|
|
3020
|
+
| \`key\` | string | Flow key or path (supports selectors) |
|
|
3021
|
+
| \`inputs\` | object | Input values mapped to the sub-flow's declared inputs (supports selectors) |
|
|
3022
|
+
|
|
3023
|
+
### \`paginate\` \u2014 Auto-collect paginated API results
|
|
3024
|
+
|
|
3025
|
+
Automatically pages through a paginated API, collecting all results into a single array.
|
|
3026
|
+
|
|
3027
|
+
\`\`\`json
|
|
3028
|
+
{
|
|
3029
|
+
"id": "allMessages",
|
|
3030
|
+
"name": "Fetch all Gmail messages",
|
|
3031
|
+
"type": "paginate",
|
|
3032
|
+
"paginate": {
|
|
3033
|
+
"action": {
|
|
3034
|
+
"platform": "gmail",
|
|
3035
|
+
"actionId": "GMAIL_LIST_MESSAGES_ACTION_ID",
|
|
3036
|
+
"connectionKey": "$.input.gmailKey",
|
|
3037
|
+
"queryParams": { "maxResults": 100 }
|
|
3038
|
+
},
|
|
3039
|
+
"pageTokenField": "nextPageToken",
|
|
3040
|
+
"resultsField": "messages",
|
|
3041
|
+
"inputTokenParam": "queryParams.pageToken",
|
|
3042
|
+
"maxPages": 10
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
\`\`\`
|
|
3046
|
+
|
|
3047
|
+
| Field | Type | Description |
|
|
3048
|
+
|---|---|---|
|
|
3049
|
+
| \`action\` | FlowActionConfig | The API action to call (same format as action steps) |
|
|
3050
|
+
| \`pageTokenField\` | string | Dot-path in the API response to the next page token |
|
|
3051
|
+
| \`resultsField\` | string | Dot-path in the API response to the results array |
|
|
3052
|
+
| \`inputTokenParam\` | string | Dot-path in the action config where the page token is injected |
|
|
3053
|
+
| \`maxPages\` | number | Maximum pages to fetch, default: 10 |
|
|
3054
|
+
|
|
3055
|
+
### \`bash\` \u2014 Execute shell commands
|
|
3056
|
+
|
|
3057
|
+
Runs a shell command. **Requires \`--allow-bash\` flag** for security.
|
|
3058
|
+
|
|
3059
|
+
\`\`\`json
|
|
3060
|
+
{
|
|
3061
|
+
"id": "analyzeData",
|
|
3062
|
+
"name": "Analyze data with Claude",
|
|
3063
|
+
"type": "bash",
|
|
3064
|
+
"bash": {
|
|
3065
|
+
"command": "claude --print 'Analyze: {{$.steps.fetchData.response}}' --output-format json",
|
|
3066
|
+
"timeout": 120000,
|
|
3067
|
+
"parseJson": true
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
\`\`\`
|
|
3071
|
+
|
|
3072
|
+
| Field | Type | Description |
|
|
3073
|
+
|---|---|---|
|
|
3074
|
+
| \`command\` | string | Shell command to execute (supports selectors and interpolation) |
|
|
3075
|
+
| \`timeout\` | number | Timeout in ms, default: 30000 |
|
|
3076
|
+
| \`parseJson\` | boolean | Parse stdout as JSON, default: false |
|
|
3077
|
+
| \`cwd\` | string | Working directory (supports selectors) |
|
|
3078
|
+
| \`env\` | object | Additional environment variables |
|
|
3079
|
+
|
|
3080
|
+
**Security:** Bash steps are blocked by default. Pass \`--allow-bash\` to \`one flow execute\` to enable them.
|
|
3081
|
+
|
|
3805
3082
|
## 6. Error Handling
|
|
3806
3083
|
|
|
3807
3084
|
### \`onError\` strategies
|
|
@@ -4083,6 +3360,12 @@ one --agent flow execute <key> -i connectionKey=value -i param=value
|
|
|
4083
3360
|
# Execute with dry run (validate only)
|
|
4084
3361
|
one --agent flow execute <key> --dry-run -i connectionKey=value
|
|
4085
3362
|
|
|
3363
|
+
# Execute with mock mode (dry-run + mock API responses, runs transforms/code normally)
|
|
3364
|
+
one --agent flow execute <key> --dry-run --mock -i connectionKey=value
|
|
3365
|
+
|
|
3366
|
+
# Execute with bash steps enabled
|
|
3367
|
+
one --agent flow execute <key> --allow-bash -i connectionKey=value
|
|
3368
|
+
|
|
4086
3369
|
# Execute with verbose output
|
|
4087
3370
|
one --agent flow execute <key> -v -i connectionKey=value
|
|
4088
3371
|
|
|
@@ -4101,6 +3384,11 @@ one --agent flow resume <runId>
|
|
|
4101
3384
|
- Connection keys are **inputs**, not hardcoded \u2014 makes workflows portable and shareable
|
|
4102
3385
|
- Use \`$.input.*\` for input values, \`$.steps.*\` for step results
|
|
4103
3386
|
- Action IDs in examples (like \`STRIPE_SEARCH_CUSTOMERS_ACTION_ID\`) are placeholders \u2014 always use \`one actions search\` to find the real IDs
|
|
3387
|
+
- **Parallel step outputs** are accessible both by index (\`$.steps.parallelStep.output[0]\`) and by substep ID (\`$.steps.substepId.response\`)
|
|
3388
|
+
- **Loop step outputs** include iteration details via \`$.steps.myLoop.response.iterations[0].innerStepId.response\`
|
|
3389
|
+
- **Code steps** support \`await require('crypto')\`, \`await require('buffer')\`, \`await require('url')\`, \`await require('path')\` \u2014 \`fs\`, \`http\`, \`child_process\`, etc. are blocked
|
|
3390
|
+
- **Bash steps** require \`--allow-bash\` flag for security
|
|
3391
|
+
- **State is persisted** after every step completion \u2014 resume picks up where it left off
|
|
4104
3392
|
`;
|
|
4105
3393
|
var TOPICS = [
|
|
4106
3394
|
{ topic: "overview", description: "Setup, --agent flag, discovery workflow" },
|
|
@@ -4406,9 +3694,14 @@ validation rules, and platform-specific details.
|
|
|
4406
3694
|
// src/commands/update.ts
|
|
4407
3695
|
import { createRequire } from "module";
|
|
4408
3696
|
import { spawn as spawn2 } from "child_process";
|
|
3697
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3698
|
+
import { homedir } from "os";
|
|
3699
|
+
import { join } from "path";
|
|
4409
3700
|
var require2 = createRequire(import.meta.url);
|
|
4410
3701
|
var { version: currentVersion } = require2("../package.json");
|
|
4411
|
-
|
|
3702
|
+
var CACHE_PATH = join(homedir(), ".one", "update-check.json");
|
|
3703
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3704
|
+
async function fetchLatestVersion() {
|
|
4412
3705
|
try {
|
|
4413
3706
|
const res = await fetch("https://registry.npmjs.org/@withone/cli/latest");
|
|
4414
3707
|
if (!res.ok) return null;
|
|
@@ -4418,6 +3711,32 @@ async function checkLatestVersion() {
|
|
|
4418
3711
|
return null;
|
|
4419
3712
|
}
|
|
4420
3713
|
}
|
|
3714
|
+
function readCache() {
|
|
3715
|
+
try {
|
|
3716
|
+
return JSON.parse(readFileSync(CACHE_PATH, "utf8"));
|
|
3717
|
+
} catch {
|
|
3718
|
+
return null;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
function writeCache(latestVersion) {
|
|
3722
|
+
try {
|
|
3723
|
+
mkdirSync(join(homedir(), ".one"), { recursive: true });
|
|
3724
|
+
writeFileSync(CACHE_PATH, JSON.stringify({ lastCheck: Date.now(), latestVersion }));
|
|
3725
|
+
} catch {
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
async function checkLatestVersion() {
|
|
3729
|
+
const version2 = await fetchLatestVersion();
|
|
3730
|
+
if (version2) writeCache(version2);
|
|
3731
|
+
return version2;
|
|
3732
|
+
}
|
|
3733
|
+
async function checkLatestVersionCached() {
|
|
3734
|
+
const cache = readCache();
|
|
3735
|
+
if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
|
|
3736
|
+
return cache.latestVersion;
|
|
3737
|
+
}
|
|
3738
|
+
return checkLatestVersion();
|
|
3739
|
+
}
|
|
4421
3740
|
function getCurrentVersion() {
|
|
4422
3741
|
return currentVersion;
|
|
4423
3742
|
}
|
|
@@ -4508,7 +3827,7 @@ program.hook("preAction", (thisCommand) => {
|
|
|
4508
3827
|
}
|
|
4509
3828
|
const commandName = thisCommand.args?.[0];
|
|
4510
3829
|
if (commandName !== "update") {
|
|
4511
|
-
updateCheckPromise =
|
|
3830
|
+
updateCheckPromise = checkLatestVersionCached();
|
|
4512
3831
|
}
|
|
4513
3832
|
});
|
|
4514
3833
|
program.hook("postAction", async () => {
|
|
@@ -4564,7 +3883,7 @@ var flow = program.command("flow").alias("f").description("Create, execute, and
|
|
|
4564
3883
|
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) => {
|
|
4565
3884
|
await flowCreateCommand(key, options);
|
|
4566
3885
|
});
|
|
4567
|
-
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", "Validate and show execution plan without running").option("-v, --verbose", "Show full request/response for each step").action(async (keyOrPath, options) => {
|
|
3886
|
+
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", "Validate and show execution plan without running").option("--mock", "With --dry-run: execute transforms/code with mock API responses").option("--allow-bash", "Allow bash step execution (disabled by default for security)").option("-v, --verbose", "Show full request/response for each step").action(async (keyOrPath, options) => {
|
|
4568
3887
|
await flowExecuteCommand(keyOrPath, options);
|
|
4569
3888
|
});
|
|
4570
3889
|
flow.command("list").alias("ls").description("List all workflows in .one/flows/").action(async () => {
|