@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/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 path6 = `${pathPrefix}[${i}]`;
1786
+ const path4 = `${pathPrefix}[${i}]`;
2081
1787
  if (!step || typeof step !== "object" || Array.isArray(step)) {
2082
- errors.push({ path: path6, message: "Step must be an object" });
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: `${path6}.id`, message: 'Step must have a string "id"' });
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: `${path6}.name`, message: 'Step must have a string "name"' });
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: `${path6}.type`, message: `Step type must be one of: ${VALID_STEP_TYPES.join(", ")}` });
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: `${path6}.onError.strategy`, message: `Error strategy must be one of: ${VALID_ERROR_STRATEGIES.join(", ")}` });
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: `${path6}.action`, message: 'Action step must have an "action" config object' });
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: `${path6}.action.platform`, message: 'Action must have "platform"' });
2108
- if (!a.actionId) errors.push({ path: `${path6}.action.actionId`, message: 'Action must have "actionId"' });
2109
- if (!a.connectionKey) errors.push({ path: `${path6}.action.connectionKey`, message: 'Action must have "connectionKey"' });
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: `${path6}.transform`, message: 'Transform step must have a "transform" config object' });
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: `${path6}.transform.expression`, message: 'Transform must have a string "expression"' });
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: `${path6}.code`, message: 'Code step must have a "code" config object' });
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: `${path6}.code.source`, message: 'Code must have a string "source"' });
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: `${path6}.condition`, message: 'Condition step must have a "condition" config object' });
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: `${path6}.condition.expression`, message: 'Condition must have a string "expression"' });
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: `${path6}.condition.then`, message: 'Condition must have a "then" steps array' });
1844
+ errors.push({ path: `${path4}.condition.then`, message: 'Condition must have a "then" steps array' });
2139
1845
  } else {
2140
- validateStepsArray(c.then, `${path6}.condition.then`, errors);
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: `${path6}.condition.else`, message: 'Condition "else" must be a steps array' });
1850
+ errors.push({ path: `${path4}.condition.else`, message: 'Condition "else" must be a steps array' });
2145
1851
  } else {
2146
- validateStepsArray(c.else, `${path6}.condition.else`, errors);
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: `${path6}.loop`, message: 'Loop step must have a "loop" config object' });
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: `${path6}.loop.over`, message: 'Loop must have a string "over" selector' });
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: `${path6}.loop.as`, message: 'Loop must have a string "as" variable name' });
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: `${path6}.loop.steps`, message: 'Loop must have a "steps" array' });
1868
+ errors.push({ path: `${path4}.loop.steps`, message: 'Loop must have a "steps" array' });
2163
1869
  } else {
2164
- validateStepsArray(l.steps, `${path6}.loop.steps`, errors);
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: `${path6}.parallel`, message: 'Parallel step must have a "parallel" config object' });
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: `${path6}.parallel.steps`, message: 'Parallel must have a "steps" array' });
1879
+ errors.push({ path: `${path4}.parallel.steps`, message: 'Parallel must have a "steps" array' });
2174
1880
  } else {
2175
- validateStepsArray(par.steps, `${path6}.parallel.steps`, errors);
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: `${path6}.fileRead`, message: 'File-read step must have a "fileRead" config object' });
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: `${path6}.fileRead.path`, message: 'File-read must have a string "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: `${path6}.fileWrite`, message: 'File-write step must have a "fileWrite" config object' });
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: `${path6}.fileWrite.path`, message: 'File-write must have a string "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: `${path6}.fileWrite.content`, message: 'File-write must have "content"' });
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 path6 = `${pathPrefix}[${i}]`;
1984
+ const path4 = `${pathPrefix}[${i}]`;
2209
1985
  if (seen.has(step.id)) {
2210
- errors.push({ path: `${path6}.id`, message: `Duplicate step ID: "${step.id}"` });
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, `${path6}.condition.then`);
2216
- if (step.condition.else) collectIds(step.condition.else, `${path6}.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, `${path6}.loop.steps`);
2219
- if (step.parallel?.steps) collectIds(step.parallel.steps, `${path6}.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, path6) {
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: path6, message: `Selector "${selector}" references undefined input "${inputName}"` });
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: path6, message: `Selector "${selector}" references undefined step "${stepId}"` });
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 fs5 from "fs";
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 = fs5.readFileSync(flowPath, "utf-8");
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
- async function checkLatestVersion() {
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 = checkLatestVersion();
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 () => {