@withone/cli 1.12.9 → 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 +293 -1005
- package/package.json +1 -1
- package/skills/one-flow/SKILL.md +139 -0
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
// src/lib/flow-runner.ts
|
|
2
|
+
import fs2 from "fs";
|
|
3
|
+
import path2 from "path";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
|
|
6
|
+
// src/lib/flow-engine.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { exec } from "child_process";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
|
|
12
|
+
// src/lib/api.ts
|
|
13
|
+
var API_BASE = "https://api.withone.ai/v1";
|
|
14
|
+
var ApiError = class extends Error {
|
|
15
|
+
constructor(status, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.name = "ApiError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var OneApi = class {
|
|
22
|
+
constructor(apiKey) {
|
|
23
|
+
this.apiKey = apiKey;
|
|
24
|
+
}
|
|
25
|
+
async request(path3) {
|
|
26
|
+
return this.requestFull({ path: path3 });
|
|
27
|
+
}
|
|
28
|
+
async requestFull(opts) {
|
|
29
|
+
let url = `${API_BASE}${opts.path}`;
|
|
30
|
+
if (opts.queryParams && Object.keys(opts.queryParams).length > 0) {
|
|
31
|
+
const params = new URLSearchParams(opts.queryParams);
|
|
32
|
+
url += `?${params.toString()}`;
|
|
33
|
+
}
|
|
34
|
+
const headers = {
|
|
35
|
+
"x-one-secret": this.apiKey,
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
...opts.headers
|
|
38
|
+
};
|
|
39
|
+
const fetchOpts = {
|
|
40
|
+
method: opts.method || "GET",
|
|
41
|
+
headers
|
|
42
|
+
};
|
|
43
|
+
if (opts.body !== void 0) {
|
|
44
|
+
fetchOpts.body = JSON.stringify(opts.body);
|
|
45
|
+
}
|
|
46
|
+
const response = await fetch(url, fetchOpts);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const text = await response.text();
|
|
49
|
+
throw new ApiError(response.status, text || `HTTP ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
return response.json();
|
|
52
|
+
}
|
|
53
|
+
async validateApiKey() {
|
|
54
|
+
try {
|
|
55
|
+
await this.listConnections();
|
|
56
|
+
return true;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof ApiError && error.status === 401) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async listConnections() {
|
|
65
|
+
const allConnections = [];
|
|
66
|
+
let page = 1;
|
|
67
|
+
let totalPages = 1;
|
|
68
|
+
do {
|
|
69
|
+
const response = await this.request(`/vault/connections?page=${page}&limit=100`);
|
|
70
|
+
allConnections.push(...response.rows || []);
|
|
71
|
+
totalPages = response.pages || 1;
|
|
72
|
+
page++;
|
|
73
|
+
} while (page <= totalPages);
|
|
74
|
+
return allConnections;
|
|
75
|
+
}
|
|
76
|
+
async listPlatforms() {
|
|
77
|
+
const allPlatforms = [];
|
|
78
|
+
let page = 1;
|
|
79
|
+
let totalPages = 1;
|
|
80
|
+
do {
|
|
81
|
+
const response = await this.request(`/available-connectors?page=${page}&limit=100`);
|
|
82
|
+
allPlatforms.push(...response.rows || []);
|
|
83
|
+
totalPages = response.pages || 1;
|
|
84
|
+
page++;
|
|
85
|
+
} while (page <= totalPages);
|
|
86
|
+
return allPlatforms;
|
|
87
|
+
}
|
|
88
|
+
async searchActions(platform, query, agentType) {
|
|
89
|
+
const isKnowledgeAgent = !agentType || agentType === "knowledge";
|
|
90
|
+
const queryParams = {
|
|
91
|
+
query,
|
|
92
|
+
limit: "5"
|
|
93
|
+
};
|
|
94
|
+
if (isKnowledgeAgent) {
|
|
95
|
+
queryParams.knowledgeAgent = "true";
|
|
96
|
+
} else {
|
|
97
|
+
queryParams.executeAgent = "true";
|
|
98
|
+
}
|
|
99
|
+
const response = await this.requestFull({
|
|
100
|
+
path: `/available-actions/search/${platform}`,
|
|
101
|
+
queryParams
|
|
102
|
+
});
|
|
103
|
+
return response || [];
|
|
104
|
+
}
|
|
105
|
+
async getActionDetails(actionId) {
|
|
106
|
+
const response = await this.requestFull({
|
|
107
|
+
path: "/knowledge",
|
|
108
|
+
queryParams: { _id: actionId }
|
|
109
|
+
});
|
|
110
|
+
const actions = response?.rows || [];
|
|
111
|
+
if (actions.length === 0) {
|
|
112
|
+
throw new ApiError(404, `Action with ID ${actionId} not found`);
|
|
113
|
+
}
|
|
114
|
+
return actions[0];
|
|
115
|
+
}
|
|
116
|
+
async getActionKnowledge(actionId) {
|
|
117
|
+
const action = await this.getActionDetails(actionId);
|
|
118
|
+
if (!action.knowledge || !action.method) {
|
|
119
|
+
return {
|
|
120
|
+
knowledge: "No knowledge was found",
|
|
121
|
+
method: "No method was found"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
knowledge: action.knowledge,
|
|
126
|
+
method: action.method
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async executePassthroughRequest(args, preloadedAction) {
|
|
130
|
+
const action = preloadedAction ?? await this.getActionDetails(args.actionId);
|
|
131
|
+
const method = action.method;
|
|
132
|
+
const contentType = args.isFormData ? "multipart/form-data" : args.isFormUrlEncoded ? "application/x-www-form-urlencoded" : "application/json";
|
|
133
|
+
const requestHeaders = {
|
|
134
|
+
"x-one-secret": this.apiKey,
|
|
135
|
+
"x-one-connection-key": args.connectionKey,
|
|
136
|
+
"x-one-action-id": action._id,
|
|
137
|
+
"Content-Type": contentType,
|
|
138
|
+
...args.headers
|
|
139
|
+
};
|
|
140
|
+
const finalActionPath = args.pathVariables ? replacePathVariables(action.path, args.pathVariables) : action.path;
|
|
141
|
+
const normalizedPath = finalActionPath.startsWith("/") ? finalActionPath : `/${finalActionPath}`;
|
|
142
|
+
const url = `${API_BASE.replace("/v1", "")}/v1/passthrough${normalizedPath}`;
|
|
143
|
+
const isCustomAction = action.tags?.includes("custom");
|
|
144
|
+
let requestData = args.data;
|
|
145
|
+
if (isCustomAction && method?.toLowerCase() !== "get") {
|
|
146
|
+
requestData = {
|
|
147
|
+
...args.data,
|
|
148
|
+
connectionKey: args.connectionKey
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
let queryString = "";
|
|
152
|
+
if (args.queryParams && Object.keys(args.queryParams).length > 0) {
|
|
153
|
+
const params = new URLSearchParams(
|
|
154
|
+
Object.entries(args.queryParams).map(([k, v]) => [k, String(v)])
|
|
155
|
+
);
|
|
156
|
+
queryString = `?${params.toString()}`;
|
|
157
|
+
}
|
|
158
|
+
const fullUrl = `${url}${queryString}`;
|
|
159
|
+
const fetchOpts = {
|
|
160
|
+
method,
|
|
161
|
+
headers: requestHeaders
|
|
162
|
+
};
|
|
163
|
+
if (method?.toLowerCase() !== "get" && requestData !== void 0) {
|
|
164
|
+
if (args.isFormUrlEncoded) {
|
|
165
|
+
const params = new URLSearchParams();
|
|
166
|
+
if (requestData && typeof requestData === "object" && !Array.isArray(requestData)) {
|
|
167
|
+
Object.entries(requestData).forEach(([key, value]) => {
|
|
168
|
+
if (typeof value === "object") {
|
|
169
|
+
params.append(key, JSON.stringify(value));
|
|
170
|
+
} else {
|
|
171
|
+
params.append(key, String(value));
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
fetchOpts.body = params.toString();
|
|
176
|
+
} else if (args.isFormData) {
|
|
177
|
+
const boundary = `----FormBoundary${Date.now()}`;
|
|
178
|
+
requestHeaders["Content-Type"] = `multipart/form-data; boundary=${boundary}`;
|
|
179
|
+
let body = "";
|
|
180
|
+
if (requestData && typeof requestData === "object" && !Array.isArray(requestData)) {
|
|
181
|
+
Object.entries(requestData).forEach(([key, value]) => {
|
|
182
|
+
body += `--${boundary}\r
|
|
183
|
+
`;
|
|
184
|
+
body += `Content-Disposition: form-data; name="${key}"\r
|
|
185
|
+
\r
|
|
186
|
+
`;
|
|
187
|
+
body += typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
188
|
+
body += "\r\n";
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
body += `--${boundary}--\r
|
|
192
|
+
`;
|
|
193
|
+
fetchOpts.body = body;
|
|
194
|
+
fetchOpts.headers = requestHeaders;
|
|
195
|
+
} else {
|
|
196
|
+
fetchOpts.body = JSON.stringify(requestData);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const sanitizedConfig = {
|
|
200
|
+
url: fullUrl,
|
|
201
|
+
method,
|
|
202
|
+
headers: {
|
|
203
|
+
...requestHeaders,
|
|
204
|
+
"x-one-secret": "***REDACTED***"
|
|
205
|
+
},
|
|
206
|
+
params: args.queryParams ? Object.fromEntries(
|
|
207
|
+
Object.entries(args.queryParams).map(([k, v]) => [k, String(v)])
|
|
208
|
+
) : void 0,
|
|
209
|
+
data: requestData
|
|
210
|
+
};
|
|
211
|
+
if (args.dryRun) {
|
|
212
|
+
return {
|
|
213
|
+
requestConfig: sanitizedConfig,
|
|
214
|
+
responseData: null
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const response = await fetch(fullUrl, fetchOpts);
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
const text = await response.text();
|
|
220
|
+
throw new ApiError(response.status, text || `HTTP ${response.status}`);
|
|
221
|
+
}
|
|
222
|
+
const responseText = await response.text();
|
|
223
|
+
const responseData = responseText ? JSON.parse(responseText) : {};
|
|
224
|
+
return {
|
|
225
|
+
requestConfig: sanitizedConfig,
|
|
226
|
+
responseData
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async waitForConnection(platform, timeoutMs = 5 * 60 * 1e3, pollIntervalMs = 5e3, onPoll) {
|
|
230
|
+
const startTime = Date.now();
|
|
231
|
+
const existingConnections = await this.listConnections();
|
|
232
|
+
const existingIds = new Set(existingConnections.map((c) => c.id));
|
|
233
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
234
|
+
await sleep(pollIntervalMs);
|
|
235
|
+
onPoll?.();
|
|
236
|
+
const currentConnections = await this.listConnections();
|
|
237
|
+
const newConnection = currentConnections.find(
|
|
238
|
+
(c) => c.platform.toLowerCase() === platform.toLowerCase() && !existingIds.has(c.id)
|
|
239
|
+
);
|
|
240
|
+
if (newConnection) {
|
|
241
|
+
return newConnection;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
throw new TimeoutError(`Timed out waiting for ${platform} connection`);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
var TimeoutError = class extends Error {
|
|
248
|
+
constructor(message) {
|
|
249
|
+
super(message);
|
|
250
|
+
this.name = "TimeoutError";
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
function sleep(ms) {
|
|
254
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
255
|
+
}
|
|
256
|
+
function replacePathVariables(path3, variables) {
|
|
257
|
+
if (!path3) return path3;
|
|
258
|
+
let result = path3;
|
|
259
|
+
result = result.replace(/\{\{([^}]+)\}\}/g, (_match, variable) => {
|
|
260
|
+
const trimmedVariable = variable.trim();
|
|
261
|
+
const value = variables[trimmedVariable];
|
|
262
|
+
if (value === void 0 || value === null || value === "") {
|
|
263
|
+
throw new Error(`Missing value for path variable: ${trimmedVariable}`);
|
|
264
|
+
}
|
|
265
|
+
return encodeURIComponent(value.toString()).replace(/%3A/gi, ":");
|
|
266
|
+
});
|
|
267
|
+
result = result.replace(/\{([^}]+)\}/g, (_match, variable) => {
|
|
268
|
+
const trimmedVariable = variable.trim();
|
|
269
|
+
const value = variables[trimmedVariable];
|
|
270
|
+
if (value === void 0 || value === null || value === "") {
|
|
271
|
+
throw new Error(`Missing value for path variable: ${trimmedVariable}`);
|
|
272
|
+
}
|
|
273
|
+
return encodeURIComponent(value.toString()).replace(/%3A/gi, ":");
|
|
274
|
+
});
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
var PERMISSION_METHODS = {
|
|
278
|
+
read: ["GET"],
|
|
279
|
+
write: ["GET", "POST", "PUT", "PATCH"],
|
|
280
|
+
admin: null
|
|
281
|
+
};
|
|
282
|
+
function filterByPermissions(actions, permissions) {
|
|
283
|
+
const allowed = PERMISSION_METHODS[permissions];
|
|
284
|
+
if (allowed === null) return actions;
|
|
285
|
+
return actions.filter((a) => allowed.includes(a.method.toUpperCase()));
|
|
286
|
+
}
|
|
287
|
+
function isMethodAllowed(method, permissions) {
|
|
288
|
+
const allowed = PERMISSION_METHODS[permissions];
|
|
289
|
+
if (allowed === null) return true;
|
|
290
|
+
return allowed.includes(method.toUpperCase());
|
|
291
|
+
}
|
|
292
|
+
function isActionAllowed(actionId, allowedActionIds) {
|
|
293
|
+
return allowedActionIds.includes("*") || allowedActionIds.includes(actionId);
|
|
294
|
+
}
|
|
295
|
+
function buildActionKnowledgeWithGuidance(knowledge, method, platform, actionId) {
|
|
296
|
+
const baseUrl = "https://api.withone.ai";
|
|
297
|
+
return `${knowledge}
|
|
298
|
+
|
|
299
|
+
API REQUEST STRUCTURE
|
|
300
|
+
======================
|
|
301
|
+
URL: ${baseUrl}/v1/passthrough/{{PATH}}
|
|
302
|
+
|
|
303
|
+
IMPORTANT: When constructing the URL, only include the API endpoint path after the base URL.
|
|
304
|
+
Do NOT include the full third-party API URL.
|
|
305
|
+
|
|
306
|
+
Examples:
|
|
307
|
+
Correct: ${baseUrl}/v1/passthrough/crm/v3/objects/contacts/search
|
|
308
|
+
Incorrect: ${baseUrl}/v1/passthrough/https://api.hubapi.com/crm/v3/objects/contacts/search
|
|
309
|
+
|
|
310
|
+
METHOD: ${method}
|
|
311
|
+
|
|
312
|
+
HEADERS:
|
|
313
|
+
- x-one-secret: {{process.env.ONE_SECRET}}
|
|
314
|
+
- x-one-connection-key: {{process.env.ONE_${platform.toUpperCase()}_CONNECTION_KEY}}
|
|
315
|
+
- x-one-action-id: ${actionId}
|
|
316
|
+
- ... (other headers)
|
|
317
|
+
|
|
318
|
+
BODY: {{BODY}}
|
|
319
|
+
|
|
320
|
+
QUERY PARAMS: {{QUERY_PARAMS}}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/lib/flow-engine.ts
|
|
324
|
+
var execAsync = promisify(exec);
|
|
325
|
+
function sleep2(ms) {
|
|
326
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
327
|
+
}
|
|
328
|
+
function resolveSelector(selectorPath, context) {
|
|
329
|
+
if (!selectorPath.startsWith("$.")) return selectorPath;
|
|
330
|
+
const parts = selectorPath.slice(2).split(/\.|\[/).map((p) => p.replace(/\]$/, ""));
|
|
331
|
+
let current = context;
|
|
332
|
+
for (const part of parts) {
|
|
333
|
+
if (current === null || current === void 0) return void 0;
|
|
334
|
+
if (part === "*" && Array.isArray(current)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (Array.isArray(current) && part === "*") {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (Array.isArray(current)) {
|
|
341
|
+
const idx = Number(part);
|
|
342
|
+
if (!isNaN(idx)) {
|
|
343
|
+
current = current[idx];
|
|
344
|
+
} else {
|
|
345
|
+
current = current.map((item) => item?.[part]);
|
|
346
|
+
}
|
|
347
|
+
} else if (typeof current === "object") {
|
|
348
|
+
current = current[part];
|
|
349
|
+
} else {
|
|
350
|
+
return void 0;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return current;
|
|
354
|
+
}
|
|
355
|
+
function interpolateString(str, context) {
|
|
356
|
+
return str.replace(/\{\{(\$\.[^}]+)\}\}/g, (_match, selector) => {
|
|
357
|
+
const value = resolveSelector(selector, context);
|
|
358
|
+
if (value === void 0 || value === null) return "";
|
|
359
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
360
|
+
return String(value);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function resolveValue(value, context) {
|
|
364
|
+
if (typeof value === "string") {
|
|
365
|
+
if (value.startsWith("$.") && !value.includes("{{")) {
|
|
366
|
+
return resolveSelector(value, context);
|
|
367
|
+
}
|
|
368
|
+
if (value.includes("{{$.")) {
|
|
369
|
+
return interpolateString(value, context);
|
|
370
|
+
}
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
if (Array.isArray(value)) {
|
|
374
|
+
return value.map((item) => resolveValue(item, context));
|
|
375
|
+
}
|
|
376
|
+
if (value && typeof value === "object") {
|
|
377
|
+
const resolved = {};
|
|
378
|
+
for (const [k, v] of Object.entries(value)) {
|
|
379
|
+
resolved[k] = resolveValue(v, context);
|
|
380
|
+
}
|
|
381
|
+
return resolved;
|
|
382
|
+
}
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
385
|
+
function evaluateExpression(expr, context) {
|
|
386
|
+
const fn = new Function("$", `return (${expr})`);
|
|
387
|
+
return fn(context);
|
|
388
|
+
}
|
|
389
|
+
function getByDotPath(obj, dotPath) {
|
|
390
|
+
const parts = dotPath.split(".");
|
|
391
|
+
let current = obj;
|
|
392
|
+
for (const part of parts) {
|
|
393
|
+
if (current === null || current === void 0) return void 0;
|
|
394
|
+
if (typeof current === "object") {
|
|
395
|
+
current = current[part];
|
|
396
|
+
} else {
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return current;
|
|
401
|
+
}
|
|
402
|
+
function setByDotPath(obj, dotPath, value) {
|
|
403
|
+
const parts = dotPath.split(".");
|
|
404
|
+
let current = obj;
|
|
405
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
406
|
+
if (current[parts[i]] === void 0 || current[parts[i]] === null) {
|
|
407
|
+
current[parts[i]] = {};
|
|
408
|
+
}
|
|
409
|
+
current = current[parts[i]];
|
|
410
|
+
}
|
|
411
|
+
current[parts[parts.length - 1]] = value;
|
|
412
|
+
}
|
|
413
|
+
var ALLOWED_MODULES = {
|
|
414
|
+
buffer: () => import("buffer"),
|
|
415
|
+
crypto: () => import("crypto"),
|
|
416
|
+
url: () => import("url"),
|
|
417
|
+
path: () => import("path")
|
|
418
|
+
};
|
|
419
|
+
var BLOCKED_MODULES = /* @__PURE__ */ new Set([
|
|
420
|
+
"fs",
|
|
421
|
+
"http",
|
|
422
|
+
"https",
|
|
423
|
+
"net",
|
|
424
|
+
"child_process",
|
|
425
|
+
"process",
|
|
426
|
+
"os",
|
|
427
|
+
"cluster",
|
|
428
|
+
"dgram",
|
|
429
|
+
"tls",
|
|
430
|
+
"vm",
|
|
431
|
+
"worker_threads"
|
|
432
|
+
]);
|
|
433
|
+
function createSandboxedRequire() {
|
|
434
|
+
const cache = {};
|
|
435
|
+
return async (moduleName) => {
|
|
436
|
+
const clean = moduleName.replace(/^node:/, "");
|
|
437
|
+
if (BLOCKED_MODULES.has(clean)) {
|
|
438
|
+
throw new Error(`Module "${moduleName}" is blocked in code steps`);
|
|
439
|
+
}
|
|
440
|
+
if (!ALLOWED_MODULES[clean]) {
|
|
441
|
+
throw new Error(`Module "${moduleName}" not available. Allowed: ${Object.keys(ALLOWED_MODULES).join(", ")}`);
|
|
442
|
+
}
|
|
443
|
+
if (!cache[clean]) cache[clean] = await ALLOWED_MODULES[clean]();
|
|
444
|
+
return cache[clean];
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async function executeActionStep(step, context, api, permissions, allowedActionIds) {
|
|
448
|
+
const action = step.action;
|
|
449
|
+
const platform = resolveValue(action.platform, context);
|
|
450
|
+
const actionId = resolveValue(action.actionId, context);
|
|
451
|
+
const connectionKey = resolveValue(action.connectionKey, context);
|
|
452
|
+
const data = action.data ? resolveValue(action.data, context) : void 0;
|
|
453
|
+
const pathVars = action.pathVars ? resolveValue(action.pathVars, context) : void 0;
|
|
454
|
+
const queryParams = action.queryParams ? resolveValue(action.queryParams, context) : void 0;
|
|
455
|
+
const headers = action.headers ? resolveValue(action.headers, context) : void 0;
|
|
456
|
+
if (!isActionAllowed(actionId, allowedActionIds)) {
|
|
457
|
+
throw new Error(`Action "${actionId}" is not in the allowed action list`);
|
|
458
|
+
}
|
|
459
|
+
const actionDetails = await api.getActionDetails(actionId);
|
|
460
|
+
if (!isMethodAllowed(actionDetails.method, permissions)) {
|
|
461
|
+
throw new Error(`Method "${actionDetails.method}" is not allowed under "${permissions}" permission level`);
|
|
462
|
+
}
|
|
463
|
+
const result = await api.executePassthroughRequest({
|
|
464
|
+
platform,
|
|
465
|
+
actionId,
|
|
466
|
+
connectionKey,
|
|
467
|
+
data,
|
|
468
|
+
pathVariables: pathVars,
|
|
469
|
+
queryParams,
|
|
470
|
+
headers
|
|
471
|
+
}, actionDetails);
|
|
472
|
+
return {
|
|
473
|
+
status: "success",
|
|
474
|
+
response: result.responseData,
|
|
475
|
+
output: result.responseData
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function executeTransformStep(step, context) {
|
|
479
|
+
const output = evaluateExpression(step.transform.expression, context);
|
|
480
|
+
return { status: "success", output, response: output };
|
|
481
|
+
}
|
|
482
|
+
async function executeCodeStep(step, context) {
|
|
483
|
+
const source = step.code.source;
|
|
484
|
+
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
485
|
+
}).constructor;
|
|
486
|
+
const sandboxedRequire = createSandboxedRequire();
|
|
487
|
+
const fn = new AsyncFunction("$", "require", source);
|
|
488
|
+
const output = await fn(context, sandboxedRequire);
|
|
489
|
+
return { status: "success", output, response: output };
|
|
490
|
+
}
|
|
491
|
+
async function executeConditionStep(step, context, api, permissions, allowedActionIds, options, flowStack) {
|
|
492
|
+
const condition = step.condition;
|
|
493
|
+
const result = evaluateExpression(condition.expression, context);
|
|
494
|
+
const branch = result ? condition.then : condition.else || [];
|
|
495
|
+
const branchResults = await executeSteps(branch, context, api, permissions, allowedActionIds, options, void 0, flowStack);
|
|
496
|
+
return {
|
|
497
|
+
status: "success",
|
|
498
|
+
output: { conditionResult: !!result, stepsExecuted: branchResults },
|
|
499
|
+
response: { conditionResult: !!result }
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
async function executeLoopStep(step, context, api, permissions, allowedActionIds, options, flowStack) {
|
|
503
|
+
const loop = step.loop;
|
|
504
|
+
const items = resolveValue(loop.over, context);
|
|
505
|
+
if (!Array.isArray(items)) {
|
|
506
|
+
throw new Error(`Loop "over" must resolve to an array, got ${typeof items}`);
|
|
507
|
+
}
|
|
508
|
+
const maxIterations = loop.maxIterations || 1e3;
|
|
509
|
+
const bounded = items.slice(0, maxIterations);
|
|
510
|
+
const savedLoop = { ...context.loop };
|
|
511
|
+
const iterationResults = [];
|
|
512
|
+
if (loop.maxConcurrency && loop.maxConcurrency > 1) {
|
|
513
|
+
const results2 = new Array(bounded.length);
|
|
514
|
+
for (let batchStart = 0; batchStart < bounded.length; batchStart += loop.maxConcurrency) {
|
|
515
|
+
const batch = bounded.slice(batchStart, batchStart + loop.maxConcurrency);
|
|
516
|
+
const batchResults = await Promise.all(
|
|
517
|
+
batch.map(async (item, batchIdx) => {
|
|
518
|
+
const i = batchStart + batchIdx;
|
|
519
|
+
const iterContext = {
|
|
520
|
+
...context,
|
|
521
|
+
loop: {
|
|
522
|
+
[loop.as]: item,
|
|
523
|
+
item,
|
|
524
|
+
i,
|
|
525
|
+
...loop.indexAs ? { [loop.indexAs]: i } : {}
|
|
526
|
+
},
|
|
527
|
+
steps: { ...context.steps }
|
|
528
|
+
};
|
|
529
|
+
const beforeKeys = new Set(Object.keys(iterContext.steps));
|
|
530
|
+
await executeSteps(loop.steps, iterContext, api, permissions, allowedActionIds, options, void 0, flowStack);
|
|
531
|
+
const iterResult = {};
|
|
532
|
+
for (const [key, val] of Object.entries(iterContext.steps)) {
|
|
533
|
+
if (!beforeKeys.has(key) || iterContext.steps[key] !== context.steps[key]) {
|
|
534
|
+
iterResult[key] = val;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
iterationResults[i] = iterResult;
|
|
538
|
+
Object.assign(context.steps, iterContext.steps);
|
|
539
|
+
return iterContext.loop[loop.as];
|
|
540
|
+
})
|
|
541
|
+
);
|
|
542
|
+
for (let j = 0; j < batchResults.length; j++) {
|
|
543
|
+
results2[batchStart + j] = batchResults[j];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
context.loop = savedLoop;
|
|
547
|
+
return {
|
|
548
|
+
status: "success",
|
|
549
|
+
output: results2,
|
|
550
|
+
response: { items: results2, iterations: iterationResults }
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
const results = [];
|
|
554
|
+
for (let i = 0; i < bounded.length; i++) {
|
|
555
|
+
context.loop = {
|
|
556
|
+
[loop.as]: bounded[i],
|
|
557
|
+
item: bounded[i],
|
|
558
|
+
i
|
|
559
|
+
};
|
|
560
|
+
if (loop.indexAs) {
|
|
561
|
+
context.loop[loop.indexAs] = i;
|
|
562
|
+
}
|
|
563
|
+
const beforeKeys = new Set(Object.keys(context.steps));
|
|
564
|
+
await executeSteps(loop.steps, context, api, permissions, allowedActionIds, options, void 0, flowStack);
|
|
565
|
+
const iterResult = {};
|
|
566
|
+
for (const [key, val] of Object.entries(context.steps)) {
|
|
567
|
+
if (!beforeKeys.has(key) || context.steps[key] !== (beforeKeys.has(key) ? void 0 : val)) {
|
|
568
|
+
iterResult[key] = val;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
iterationResults.push(iterResult);
|
|
572
|
+
results.push(context.loop[loop.as]);
|
|
573
|
+
}
|
|
574
|
+
context.loop = savedLoop;
|
|
575
|
+
return {
|
|
576
|
+
status: "success",
|
|
577
|
+
output: results,
|
|
578
|
+
response: { items: results, iterations: iterationResults }
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function executeParallelStep(step, context, api, permissions, allowedActionIds, options, flowStack) {
|
|
582
|
+
const parallel = step.parallel;
|
|
583
|
+
const maxConcurrency = parallel.maxConcurrency || 5;
|
|
584
|
+
const steps = parallel.steps;
|
|
585
|
+
const results = [];
|
|
586
|
+
for (let i = 0; i < steps.length; i += maxConcurrency) {
|
|
587
|
+
const batch = steps.slice(i, i + maxConcurrency);
|
|
588
|
+
const batchResults = await Promise.all(
|
|
589
|
+
batch.map((s) => executeSingleStep(s, context, api, permissions, allowedActionIds, options, flowStack))
|
|
590
|
+
);
|
|
591
|
+
for (let j = 0; j < batch.length; j++) {
|
|
592
|
+
context.steps[batch[j].id] = batchResults[j];
|
|
593
|
+
}
|
|
594
|
+
results.push(...batchResults);
|
|
595
|
+
}
|
|
596
|
+
return { status: "success", output: results, response: results };
|
|
597
|
+
}
|
|
598
|
+
function executeFileReadStep(step, context) {
|
|
599
|
+
const config = step.fileRead;
|
|
600
|
+
const filePath = resolveValue(config.path, context);
|
|
601
|
+
const resolvedPath = path.resolve(filePath);
|
|
602
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
603
|
+
const output = config.parseJson ? JSON.parse(content) : content;
|
|
604
|
+
return { status: "success", output, response: output };
|
|
605
|
+
}
|
|
606
|
+
function executeFileWriteStep(step, context) {
|
|
607
|
+
const config = step.fileWrite;
|
|
608
|
+
const filePath = resolveValue(config.path, context);
|
|
609
|
+
const content = resolveValue(config.content, context);
|
|
610
|
+
const resolvedPath = path.resolve(filePath);
|
|
611
|
+
const dir = path.dirname(resolvedPath);
|
|
612
|
+
if (!fs.existsSync(dir)) {
|
|
613
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
614
|
+
}
|
|
615
|
+
const stringContent = typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
616
|
+
if (config.append) {
|
|
617
|
+
fs.appendFileSync(resolvedPath, stringContent);
|
|
618
|
+
} else {
|
|
619
|
+
fs.writeFileSync(resolvedPath, stringContent);
|
|
620
|
+
}
|
|
621
|
+
return { status: "success", output: { path: resolvedPath, bytesWritten: stringContent.length }, response: { path: resolvedPath } };
|
|
622
|
+
}
|
|
623
|
+
async function executeWhileStep(step, context, api, permissions, allowedActionIds, options, flowStack) {
|
|
624
|
+
const config = step.while;
|
|
625
|
+
const maxIterations = config.maxIterations ?? 100;
|
|
626
|
+
const results = [];
|
|
627
|
+
context.steps[step.id] = {
|
|
628
|
+
status: "success",
|
|
629
|
+
output: { lastResult: void 0, iteration: 0, results: [] }
|
|
630
|
+
};
|
|
631
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
632
|
+
if (iteration > 0) {
|
|
633
|
+
const conditionResult = evaluateExpression(config.condition, context);
|
|
634
|
+
if (!conditionResult) break;
|
|
635
|
+
}
|
|
636
|
+
await executeSteps(config.steps, context, api, permissions, allowedActionIds, options, void 0, flowStack);
|
|
637
|
+
const lastStepId = config.steps[config.steps.length - 1]?.id;
|
|
638
|
+
const lastResult = lastStepId ? context.steps[lastStepId]?.output : void 0;
|
|
639
|
+
results.push(lastResult);
|
|
640
|
+
context.steps[step.id] = {
|
|
641
|
+
status: "success",
|
|
642
|
+
output: { lastResult, iteration, results }
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
status: "success",
|
|
647
|
+
output: { lastResult: results[results.length - 1], iteration: results.length, results },
|
|
648
|
+
response: { iterations: results.length, results }
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
async function executeSubflowStep(step, context, api, permissions, allowedActionIds, options, flowStack) {
|
|
652
|
+
const config = step.flow;
|
|
653
|
+
const resolvedKey = resolveValue(config.key, context);
|
|
654
|
+
const resolvedInputs = config.inputs ? resolveValue(config.inputs, context) : {};
|
|
655
|
+
if (flowStack.includes(resolvedKey)) {
|
|
656
|
+
throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
|
|
657
|
+
}
|
|
658
|
+
const { loadFlow: loadFlow2 } = await import("./flow-runner-JHEAUFDL.js");
|
|
659
|
+
const subFlow = loadFlow2(resolvedKey);
|
|
660
|
+
const subContext = await executeFlow(
|
|
661
|
+
subFlow,
|
|
662
|
+
resolvedInputs,
|
|
663
|
+
api,
|
|
664
|
+
permissions,
|
|
665
|
+
allowedActionIds,
|
|
666
|
+
options,
|
|
667
|
+
void 0,
|
|
668
|
+
[...flowStack, resolvedKey]
|
|
669
|
+
);
|
|
670
|
+
return {
|
|
671
|
+
status: "success",
|
|
672
|
+
output: subContext.steps,
|
|
673
|
+
response: subContext
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async function executePaginateStep(step, context, api, permissions, allowedActionIds, options) {
|
|
677
|
+
const config = step.paginate;
|
|
678
|
+
const maxPages = config.maxPages ?? 10;
|
|
679
|
+
const allResults = [];
|
|
680
|
+
let pageToken = void 0;
|
|
681
|
+
let pages = 0;
|
|
682
|
+
for (let page = 0; page < maxPages; page++) {
|
|
683
|
+
const actionConfig = JSON.parse(JSON.stringify(config.action));
|
|
684
|
+
if (pageToken !== void 0 && pageToken !== null) {
|
|
685
|
+
const resolved = resolveValue(actionConfig, context);
|
|
686
|
+
setByDotPath(resolved, config.inputTokenParam, pageToken);
|
|
687
|
+
const syntheticStep = {
|
|
688
|
+
id: `${step.id}__page${page}`,
|
|
689
|
+
name: `${step.id} page ${page}`,
|
|
690
|
+
type: "action",
|
|
691
|
+
action: resolved
|
|
692
|
+
};
|
|
693
|
+
const result = await executeActionStep(syntheticStep, context, api, permissions, allowedActionIds);
|
|
694
|
+
const response = result.response;
|
|
695
|
+
const pageResults = getByDotPath(response, config.resultsField);
|
|
696
|
+
if (Array.isArray(pageResults)) allResults.push(...pageResults);
|
|
697
|
+
pageToken = getByDotPath(response, config.pageTokenField);
|
|
698
|
+
pages++;
|
|
699
|
+
options.onEvent?.({ event: "step:page", stepId: step.id, page: pages });
|
|
700
|
+
if (pageToken === void 0 || pageToken === null) break;
|
|
701
|
+
} else if (page === 0) {
|
|
702
|
+
const resolvedAction = resolveValue(actionConfig, context);
|
|
703
|
+
const syntheticStep = {
|
|
704
|
+
id: `${step.id}__page0`,
|
|
705
|
+
name: `${step.id} page 0`,
|
|
706
|
+
type: "action",
|
|
707
|
+
action: resolvedAction
|
|
708
|
+
};
|
|
709
|
+
const result = await executeActionStep(syntheticStep, context, api, permissions, allowedActionIds);
|
|
710
|
+
const response = result.response;
|
|
711
|
+
const pageResults = getByDotPath(response, config.resultsField);
|
|
712
|
+
if (Array.isArray(pageResults)) allResults.push(...pageResults);
|
|
713
|
+
pageToken = getByDotPath(response, config.pageTokenField);
|
|
714
|
+
pages++;
|
|
715
|
+
options.onEvent?.({ event: "step:page", stepId: step.id, page: pages });
|
|
716
|
+
if (pageToken === void 0 || pageToken === null) break;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
status: "success",
|
|
721
|
+
output: allResults,
|
|
722
|
+
response: { pages, totalResults: allResults.length, results: allResults }
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
async function executeBashStep(step, context, options) {
|
|
726
|
+
if (!options.allowBash) {
|
|
727
|
+
throw new Error("Bash steps require --allow-bash flag for security");
|
|
728
|
+
}
|
|
729
|
+
const config = step.bash;
|
|
730
|
+
const command = resolveValue(config.command, context);
|
|
731
|
+
const cwd = config.cwd ? resolveValue(config.cwd, context) : process.cwd();
|
|
732
|
+
const env = config.env ? { ...process.env, ...resolveValue(config.env, context) } : process.env;
|
|
733
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
734
|
+
timeout: config.timeout || 3e4,
|
|
735
|
+
cwd,
|
|
736
|
+
env,
|
|
737
|
+
maxBuffer: 10 * 1024 * 1024
|
|
738
|
+
});
|
|
739
|
+
const output = config.parseJson ? JSON.parse(stdout) : stdout.trim();
|
|
740
|
+
return {
|
|
741
|
+
status: "success",
|
|
742
|
+
output,
|
|
743
|
+
response: { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 }
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
async function executeSingleStep(step, context, api, permissions, allowedActionIds, options, flowStack = []) {
|
|
747
|
+
if (step.if) {
|
|
748
|
+
const condResult = evaluateExpression(step.if, context);
|
|
749
|
+
if (!condResult) {
|
|
750
|
+
const result = { status: "skipped" };
|
|
751
|
+
context.steps[step.id] = result;
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (step.unless) {
|
|
756
|
+
const condResult = evaluateExpression(step.unless, context);
|
|
757
|
+
if (condResult) {
|
|
758
|
+
const result = { status: "skipped" };
|
|
759
|
+
context.steps[step.id] = result;
|
|
760
|
+
return result;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const startTime = Date.now();
|
|
764
|
+
let lastError;
|
|
765
|
+
const maxAttempts = step.onError?.strategy === "retry" && step.onError.retries ? step.onError.retries + 1 : 1;
|
|
766
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
767
|
+
try {
|
|
768
|
+
if (attempt > 1) {
|
|
769
|
+
options.onEvent?.({
|
|
770
|
+
event: "step:retry",
|
|
771
|
+
stepId: step.id,
|
|
772
|
+
attempt,
|
|
773
|
+
maxRetries: step.onError.retries
|
|
774
|
+
});
|
|
775
|
+
const delay = step.onError?.retryDelayMs || 1e3;
|
|
776
|
+
await sleep2(delay);
|
|
777
|
+
}
|
|
778
|
+
if (options.mock && (step.type === "action" || step.type === "paginate" || step.type === "bash")) {
|
|
779
|
+
const resolvedConfig = step[step.type] ? resolveValue(step[step.type], context) : {};
|
|
780
|
+
options.onEvent?.({ event: "step:mock", stepId: step.id, type: step.type, config: resolvedConfig });
|
|
781
|
+
const result2 = {
|
|
782
|
+
status: "success",
|
|
783
|
+
output: { _mock: true, ...resolvedConfig },
|
|
784
|
+
response: { _mock: true },
|
|
785
|
+
durationMs: Date.now() - startTime
|
|
786
|
+
};
|
|
787
|
+
context.steps[step.id] = result2;
|
|
788
|
+
return result2;
|
|
789
|
+
}
|
|
790
|
+
let result;
|
|
791
|
+
switch (step.type) {
|
|
792
|
+
case "action":
|
|
793
|
+
result = await executeActionStep(step, context, api, permissions, allowedActionIds);
|
|
794
|
+
break;
|
|
795
|
+
case "transform":
|
|
796
|
+
result = executeTransformStep(step, context);
|
|
797
|
+
break;
|
|
798
|
+
case "code":
|
|
799
|
+
result = await executeCodeStep(step, context);
|
|
800
|
+
break;
|
|
801
|
+
case "condition":
|
|
802
|
+
result = await executeConditionStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
803
|
+
break;
|
|
804
|
+
case "loop":
|
|
805
|
+
result = await executeLoopStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
806
|
+
break;
|
|
807
|
+
case "parallel":
|
|
808
|
+
result = await executeParallelStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
809
|
+
break;
|
|
810
|
+
case "file-read":
|
|
811
|
+
result = executeFileReadStep(step, context);
|
|
812
|
+
break;
|
|
813
|
+
case "file-write":
|
|
814
|
+
result = executeFileWriteStep(step, context);
|
|
815
|
+
break;
|
|
816
|
+
case "while":
|
|
817
|
+
result = await executeWhileStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
818
|
+
break;
|
|
819
|
+
case "flow":
|
|
820
|
+
result = await executeSubflowStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
821
|
+
break;
|
|
822
|
+
case "paginate":
|
|
823
|
+
result = await executePaginateStep(step, context, api, permissions, allowedActionIds, options);
|
|
824
|
+
break;
|
|
825
|
+
case "bash":
|
|
826
|
+
result = await executeBashStep(step, context, options);
|
|
827
|
+
break;
|
|
828
|
+
default:
|
|
829
|
+
throw new Error(`Unknown step type: ${step.type}`);
|
|
830
|
+
}
|
|
831
|
+
result.durationMs = Date.now() - startTime;
|
|
832
|
+
if (attempt > 1) result.retries = attempt - 1;
|
|
833
|
+
context.steps[step.id] = result;
|
|
834
|
+
return result;
|
|
835
|
+
} catch (err) {
|
|
836
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
837
|
+
if (attempt === maxAttempts) {
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const errorMessage = lastError?.message || "Unknown error";
|
|
843
|
+
const strategy = step.onError?.strategy || "fail";
|
|
844
|
+
if (strategy === "continue") {
|
|
845
|
+
const result = {
|
|
846
|
+
status: "failed",
|
|
847
|
+
error: errorMessage,
|
|
848
|
+
durationMs: Date.now() - startTime
|
|
849
|
+
};
|
|
850
|
+
context.steps[step.id] = result;
|
|
851
|
+
return result;
|
|
852
|
+
}
|
|
853
|
+
if (strategy === "fallback" && step.onError?.fallbackStepId) {
|
|
854
|
+
const result = {
|
|
855
|
+
status: "failed",
|
|
856
|
+
error: errorMessage,
|
|
857
|
+
durationMs: Date.now() - startTime
|
|
858
|
+
};
|
|
859
|
+
context.steps[step.id] = result;
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
throw lastError;
|
|
863
|
+
}
|
|
864
|
+
async function executeSteps(steps, context, api, permissions, allowedActionIds, options, completedStepIds, flowStack = []) {
|
|
865
|
+
const results = [];
|
|
866
|
+
for (const step of steps) {
|
|
867
|
+
if (completedStepIds?.has(step.id)) {
|
|
868
|
+
results.push(context.steps[step.id] || { status: "success" });
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
options.onEvent?.({
|
|
872
|
+
event: "step:start",
|
|
873
|
+
stepId: step.id,
|
|
874
|
+
stepName: step.name,
|
|
875
|
+
type: step.type,
|
|
876
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
877
|
+
});
|
|
878
|
+
try {
|
|
879
|
+
const result = await executeSingleStep(step, context, api, permissions, allowedActionIds, options, flowStack);
|
|
880
|
+
results.push(result);
|
|
881
|
+
options.onEvent?.({
|
|
882
|
+
event: "step:complete",
|
|
883
|
+
stepId: step.id,
|
|
884
|
+
status: result.status,
|
|
885
|
+
durationMs: result.durationMs,
|
|
886
|
+
retries: result.retries
|
|
887
|
+
});
|
|
888
|
+
} catch (error) {
|
|
889
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
890
|
+
options.onEvent?.({
|
|
891
|
+
event: "step:error",
|
|
892
|
+
stepId: step.id,
|
|
893
|
+
error: errorMsg,
|
|
894
|
+
strategy: step.onError?.strategy || "fail"
|
|
895
|
+
});
|
|
896
|
+
throw error;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return results;
|
|
900
|
+
}
|
|
901
|
+
async function executeFlow(flow, inputs, api, permissions, allowedActionIds, options = {}, resumeState, flowStack = []) {
|
|
902
|
+
for (const [name, decl] of Object.entries(flow.inputs)) {
|
|
903
|
+
if (decl.required !== false && inputs[name] === void 0 && decl.default === void 0) {
|
|
904
|
+
throw new Error(`Missing required input: "${name}" \u2014 ${decl.description || ""}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
const resolvedInputs = {};
|
|
908
|
+
for (const [name, decl] of Object.entries(flow.inputs)) {
|
|
909
|
+
if (inputs[name] !== void 0) {
|
|
910
|
+
resolvedInputs[name] = inputs[name];
|
|
911
|
+
} else if (decl.default !== void 0) {
|
|
912
|
+
resolvedInputs[name] = decl.default;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const context = resumeState?.context || {
|
|
916
|
+
input: resolvedInputs,
|
|
917
|
+
env: process.env,
|
|
918
|
+
steps: {},
|
|
919
|
+
loop: {}
|
|
920
|
+
};
|
|
921
|
+
const completedStepIds = resumeState ? new Set(resumeState.completedSteps) : void 0;
|
|
922
|
+
if (options.dryRun && !options.mock) {
|
|
923
|
+
options.onEvent?.({
|
|
924
|
+
event: "flow:dry-run",
|
|
925
|
+
flowKey: flow.key,
|
|
926
|
+
resolvedInputs,
|
|
927
|
+
steps: flow.steps.map((s) => ({ id: s.id, name: s.name, type: s.type })),
|
|
928
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
929
|
+
});
|
|
930
|
+
return context;
|
|
931
|
+
}
|
|
932
|
+
options.onEvent?.({
|
|
933
|
+
event: options.mock ? "flow:mock-start" : "flow:start",
|
|
934
|
+
flowKey: flow.key,
|
|
935
|
+
totalSteps: flow.steps.length,
|
|
936
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
937
|
+
});
|
|
938
|
+
const flowStart = Date.now();
|
|
939
|
+
try {
|
|
940
|
+
await executeSteps(flow.steps, context, api, permissions, allowedActionIds, options, completedStepIds, flowStack);
|
|
941
|
+
const stepEntries = Object.values(context.steps);
|
|
942
|
+
const completed = stepEntries.filter((s) => s.status === "success").length;
|
|
943
|
+
const failed = stepEntries.filter((s) => s.status === "failed").length;
|
|
944
|
+
const skipped = stepEntries.filter((s) => s.status === "skipped").length;
|
|
945
|
+
options.onEvent?.({
|
|
946
|
+
event: "flow:complete",
|
|
947
|
+
flowKey: flow.key,
|
|
948
|
+
status: "success",
|
|
949
|
+
durationMs: Date.now() - flowStart,
|
|
950
|
+
stepsCompleted: completed,
|
|
951
|
+
stepsFailed: failed,
|
|
952
|
+
stepsSkipped: skipped
|
|
953
|
+
});
|
|
954
|
+
} catch (error) {
|
|
955
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
956
|
+
options.onEvent?.({
|
|
957
|
+
event: "flow:error",
|
|
958
|
+
flowKey: flow.key,
|
|
959
|
+
status: "failed",
|
|
960
|
+
error: errorMsg,
|
|
961
|
+
durationMs: Date.now() - flowStart
|
|
962
|
+
});
|
|
963
|
+
throw error;
|
|
964
|
+
}
|
|
965
|
+
return context;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/lib/flow-runner.ts
|
|
969
|
+
var FLOWS_DIR = ".one/flows";
|
|
970
|
+
var RUNS_DIR = ".one/flows/.runs";
|
|
971
|
+
var LOGS_DIR = ".one/flows/.logs";
|
|
972
|
+
function ensureDir(dir) {
|
|
973
|
+
if (!fs2.existsSync(dir)) {
|
|
974
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function generateRunId() {
|
|
978
|
+
return crypto.randomBytes(6).toString("hex");
|
|
979
|
+
}
|
|
980
|
+
var FlowRunner = class _FlowRunner {
|
|
981
|
+
runId;
|
|
982
|
+
flowKey;
|
|
983
|
+
state;
|
|
984
|
+
logPath;
|
|
985
|
+
statePath;
|
|
986
|
+
paused = false;
|
|
987
|
+
constructor(flow, inputs, runId) {
|
|
988
|
+
this.runId = runId || generateRunId();
|
|
989
|
+
this.flowKey = flow.key;
|
|
990
|
+
ensureDir(RUNS_DIR);
|
|
991
|
+
ensureDir(LOGS_DIR);
|
|
992
|
+
this.statePath = path2.join(RUNS_DIR, `${flow.key}-${this.runId}.state.json`);
|
|
993
|
+
this.logPath = path2.join(LOGS_DIR, `${flow.key}-${this.runId}.log`);
|
|
994
|
+
this.state = {
|
|
995
|
+
runId: this.runId,
|
|
996
|
+
flowKey: flow.key,
|
|
997
|
+
status: "running",
|
|
998
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
999
|
+
inputs,
|
|
1000
|
+
completedSteps: [],
|
|
1001
|
+
context: {
|
|
1002
|
+
input: inputs,
|
|
1003
|
+
env: {},
|
|
1004
|
+
steps: {},
|
|
1005
|
+
loop: {}
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
getRunId() {
|
|
1010
|
+
return this.runId;
|
|
1011
|
+
}
|
|
1012
|
+
getLogPath() {
|
|
1013
|
+
return this.logPath;
|
|
1014
|
+
}
|
|
1015
|
+
getStatePath() {
|
|
1016
|
+
return this.statePath;
|
|
1017
|
+
}
|
|
1018
|
+
requestPause() {
|
|
1019
|
+
this.paused = true;
|
|
1020
|
+
}
|
|
1021
|
+
log(level, msg, data) {
|
|
1022
|
+
const entry = {
|
|
1023
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1024
|
+
level,
|
|
1025
|
+
msg,
|
|
1026
|
+
...data
|
|
1027
|
+
};
|
|
1028
|
+
fs2.appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
|
|
1029
|
+
}
|
|
1030
|
+
saveState() {
|
|
1031
|
+
fs2.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2));
|
|
1032
|
+
}
|
|
1033
|
+
createEventHandler(externalHandler) {
|
|
1034
|
+
return (event) => {
|
|
1035
|
+
this.log(
|
|
1036
|
+
event.event.includes("error") ? "warn" : "info",
|
|
1037
|
+
event.event,
|
|
1038
|
+
event
|
|
1039
|
+
);
|
|
1040
|
+
if (event.event === "step:complete" && event.stepId) {
|
|
1041
|
+
this.state.completedSteps.push(event.stepId);
|
|
1042
|
+
this.state.currentStepId = void 0;
|
|
1043
|
+
this.saveState();
|
|
1044
|
+
}
|
|
1045
|
+
if (event.event === "step:start" && event.stepId) {
|
|
1046
|
+
this.state.currentStepId = event.stepId;
|
|
1047
|
+
}
|
|
1048
|
+
externalHandler?.(event);
|
|
1049
|
+
if (this.paused && event.event === "step:complete") {
|
|
1050
|
+
this.state.status = "paused";
|
|
1051
|
+
this.state.pausedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1052
|
+
this.saveState();
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
async execute(flow, api, permissions, allowedActionIds, options = {}) {
|
|
1057
|
+
this.log("info", "Flow started", { flowKey: this.flowKey, runId: this.runId });
|
|
1058
|
+
this.state.status = "running";
|
|
1059
|
+
const liveContext = {
|
|
1060
|
+
input: this.state.inputs,
|
|
1061
|
+
env: process.env,
|
|
1062
|
+
steps: {},
|
|
1063
|
+
loop: {}
|
|
1064
|
+
};
|
|
1065
|
+
this.state.context = liveContext;
|
|
1066
|
+
this.saveState();
|
|
1067
|
+
const eventHandler = this.createEventHandler(options.onEvent);
|
|
1068
|
+
try {
|
|
1069
|
+
const context = await executeFlow(
|
|
1070
|
+
flow,
|
|
1071
|
+
this.state.inputs,
|
|
1072
|
+
api,
|
|
1073
|
+
permissions,
|
|
1074
|
+
allowedActionIds,
|
|
1075
|
+
{ ...options, onEvent: eventHandler },
|
|
1076
|
+
{ context: liveContext, completedSteps: [] }
|
|
1077
|
+
);
|
|
1078
|
+
this.state.status = "completed";
|
|
1079
|
+
this.state.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1080
|
+
this.state.context = context;
|
|
1081
|
+
this.saveState();
|
|
1082
|
+
this.log("info", "Flow completed", { status: "success" });
|
|
1083
|
+
return context;
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1086
|
+
this.state.status = "failed";
|
|
1087
|
+
this.state.context.steps = this.state.context.steps || {};
|
|
1088
|
+
this.saveState();
|
|
1089
|
+
this.log("error", "Flow failed", { error: errorMsg });
|
|
1090
|
+
throw error;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async resume(flow, api, permissions, allowedActionIds, options = {}) {
|
|
1094
|
+
this.log("info", "Flow resumed", { flowKey: this.flowKey, runId: this.runId });
|
|
1095
|
+
this.state.status = "running";
|
|
1096
|
+
this.state.pausedAt = void 0;
|
|
1097
|
+
this.state.context.env = process.env;
|
|
1098
|
+
this.saveState();
|
|
1099
|
+
const eventHandler = this.createEventHandler(options.onEvent);
|
|
1100
|
+
try {
|
|
1101
|
+
const context = await executeFlow(
|
|
1102
|
+
flow,
|
|
1103
|
+
this.state.inputs,
|
|
1104
|
+
api,
|
|
1105
|
+
permissions,
|
|
1106
|
+
allowedActionIds,
|
|
1107
|
+
{ ...options, onEvent: eventHandler },
|
|
1108
|
+
{
|
|
1109
|
+
context: this.state.context,
|
|
1110
|
+
completedSteps: this.state.completedSteps
|
|
1111
|
+
}
|
|
1112
|
+
);
|
|
1113
|
+
this.state.status = "completed";
|
|
1114
|
+
this.state.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1115
|
+
this.state.context = context;
|
|
1116
|
+
this.saveState();
|
|
1117
|
+
this.log("info", "Flow completed after resume", { status: "success" });
|
|
1118
|
+
return context;
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1121
|
+
this.state.status = "failed";
|
|
1122
|
+
this.saveState();
|
|
1123
|
+
this.log("error", "Flow failed after resume", { error: errorMsg });
|
|
1124
|
+
throw error;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
static loadRunState(runId) {
|
|
1128
|
+
ensureDir(RUNS_DIR);
|
|
1129
|
+
const files = fs2.readdirSync(RUNS_DIR).filter((f) => f.includes(runId) && f.endsWith(".state.json"));
|
|
1130
|
+
if (files.length === 0) return null;
|
|
1131
|
+
try {
|
|
1132
|
+
const content = fs2.readFileSync(path2.join(RUNS_DIR, files[0]), "utf-8");
|
|
1133
|
+
return JSON.parse(content);
|
|
1134
|
+
} catch {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
static fromRunState(state) {
|
|
1139
|
+
const runner = Object.create(_FlowRunner.prototype);
|
|
1140
|
+
runner.runId = state.runId;
|
|
1141
|
+
runner.flowKey = state.flowKey;
|
|
1142
|
+
runner.state = state;
|
|
1143
|
+
runner.paused = false;
|
|
1144
|
+
runner.statePath = path2.join(RUNS_DIR, `${state.flowKey}-${state.runId}.state.json`);
|
|
1145
|
+
runner.logPath = path2.join(LOGS_DIR, `${state.flowKey}-${state.runId}.log`);
|
|
1146
|
+
return runner;
|
|
1147
|
+
}
|
|
1148
|
+
static listRuns(flowKey) {
|
|
1149
|
+
ensureDir(RUNS_DIR);
|
|
1150
|
+
const files = fs2.readdirSync(RUNS_DIR).filter((f) => f.endsWith(".state.json"));
|
|
1151
|
+
const runs = [];
|
|
1152
|
+
for (const file of files) {
|
|
1153
|
+
try {
|
|
1154
|
+
const content = fs2.readFileSync(path2.join(RUNS_DIR, file), "utf-8");
|
|
1155
|
+
const state = JSON.parse(content);
|
|
1156
|
+
if (!flowKey || state.flowKey === flowKey) {
|
|
1157
|
+
runs.push(state);
|
|
1158
|
+
}
|
|
1159
|
+
} catch {
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return runs.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
function resolveFlowPath(keyOrPath) {
|
|
1166
|
+
if (keyOrPath.includes("/") || keyOrPath.includes("\\") || keyOrPath.endsWith(".json")) {
|
|
1167
|
+
return path2.resolve(keyOrPath);
|
|
1168
|
+
}
|
|
1169
|
+
return path2.resolve(FLOWS_DIR, `${keyOrPath}.flow.json`);
|
|
1170
|
+
}
|
|
1171
|
+
function loadFlow(keyOrPath) {
|
|
1172
|
+
const flowPath = resolveFlowPath(keyOrPath);
|
|
1173
|
+
if (!fs2.existsSync(flowPath)) {
|
|
1174
|
+
throw new Error(`Flow not found: ${flowPath}`);
|
|
1175
|
+
}
|
|
1176
|
+
const content = fs2.readFileSync(flowPath, "utf-8");
|
|
1177
|
+
return JSON.parse(content);
|
|
1178
|
+
}
|
|
1179
|
+
function listFlows() {
|
|
1180
|
+
const flowsDir = path2.resolve(FLOWS_DIR);
|
|
1181
|
+
if (!fs2.existsSync(flowsDir)) return [];
|
|
1182
|
+
const files = fs2.readdirSync(flowsDir).filter((f) => f.endsWith(".flow.json"));
|
|
1183
|
+
const flows = [];
|
|
1184
|
+
for (const file of files) {
|
|
1185
|
+
try {
|
|
1186
|
+
const content = fs2.readFileSync(path2.join(flowsDir, file), "utf-8");
|
|
1187
|
+
const flow = JSON.parse(content);
|
|
1188
|
+
flows.push({
|
|
1189
|
+
key: flow.key,
|
|
1190
|
+
name: flow.name,
|
|
1191
|
+
description: flow.description,
|
|
1192
|
+
inputCount: Object.keys(flow.inputs).length,
|
|
1193
|
+
stepCount: flow.steps.length,
|
|
1194
|
+
path: path2.join(flowsDir, file)
|
|
1195
|
+
});
|
|
1196
|
+
} catch {
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return flows;
|
|
1200
|
+
}
|
|
1201
|
+
function saveFlow(flow, outputPath) {
|
|
1202
|
+
const flowPath = outputPath ? path2.resolve(outputPath) : path2.resolve(FLOWS_DIR, `${flow.key}.flow.json`);
|
|
1203
|
+
const dir = path2.dirname(flowPath);
|
|
1204
|
+
ensureDir(dir);
|
|
1205
|
+
fs2.writeFileSync(flowPath, JSON.stringify(flow, null, 2) + "\n");
|
|
1206
|
+
return flowPath;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
export {
|
|
1210
|
+
OneApi,
|
|
1211
|
+
TimeoutError,
|
|
1212
|
+
filterByPermissions,
|
|
1213
|
+
isMethodAllowed,
|
|
1214
|
+
isActionAllowed,
|
|
1215
|
+
buildActionKnowledgeWithGuidance,
|
|
1216
|
+
FlowRunner,
|
|
1217
|
+
resolveFlowPath,
|
|
1218
|
+
loadFlow,
|
|
1219
|
+
listFlows,
|
|
1220
|
+
saveFlow
|
|
1221
|
+
};
|