@picahq/cli 1.9.4 → 1.11.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 +196 -51
- package/dist/index.js +2278 -127
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import pc3 from "picocolors";
|
|
|
12
12
|
import fs from "fs";
|
|
13
13
|
import path from "path";
|
|
14
14
|
import os from "os";
|
|
15
|
-
var CONFIG_DIR = path.join(os.homedir(), ".
|
|
15
|
+
var CONFIG_DIR = path.join(os.homedir(), ".one");
|
|
16
16
|
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
17
17
|
function getConfigPath() {
|
|
18
18
|
return CONFIG_FILE;
|
|
@@ -37,8 +37,8 @@ function writeConfig(config) {
|
|
|
37
37
|
}
|
|
38
38
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
39
39
|
}
|
|
40
|
-
function
|
|
41
|
-
const rcPath = path.join(process.cwd(), ".
|
|
40
|
+
function readOneRc() {
|
|
41
|
+
const rcPath = path.join(process.cwd(), ".onerc");
|
|
42
42
|
if (!fs.existsSync(rcPath)) return {};
|
|
43
43
|
try {
|
|
44
44
|
const content = fs.readFileSync(rcPath, "utf-8");
|
|
@@ -58,26 +58,26 @@ function readPicaRc() {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
function getApiKey() {
|
|
61
|
-
if (process.env.
|
|
62
|
-
const rc =
|
|
63
|
-
if (rc.
|
|
61
|
+
if (process.env.ONE_SECRET) return process.env.ONE_SECRET;
|
|
62
|
+
const rc = readOneRc();
|
|
63
|
+
if (rc.ONE_SECRET) return rc.ONE_SECRET;
|
|
64
64
|
return readConfig()?.apiKey ?? null;
|
|
65
65
|
}
|
|
66
66
|
function getAccessControlFromAllSources() {
|
|
67
|
-
const rc =
|
|
67
|
+
const rc = readOneRc();
|
|
68
68
|
const fileAc = getAccessControl();
|
|
69
69
|
const merged = { ...fileAc };
|
|
70
|
-
if (rc.
|
|
71
|
-
merged.permissions = rc.
|
|
70
|
+
if (rc.ONE_PERMISSIONS) {
|
|
71
|
+
merged.permissions = rc.ONE_PERMISSIONS;
|
|
72
72
|
}
|
|
73
|
-
if (rc.
|
|
74
|
-
merged.connectionKeys = rc.
|
|
73
|
+
if (rc.ONE_CONNECTION_KEYS) {
|
|
74
|
+
merged.connectionKeys = rc.ONE_CONNECTION_KEYS.split(",").map((s) => s.trim()).filter(Boolean);
|
|
75
75
|
}
|
|
76
|
-
if (rc.
|
|
77
|
-
merged.actionIds = rc.
|
|
76
|
+
if (rc.ONE_ACTION_IDS) {
|
|
77
|
+
merged.actionIds = rc.ONE_ACTION_IDS.split(",").map((s) => s.trim()).filter(Boolean);
|
|
78
78
|
}
|
|
79
|
-
if (rc.
|
|
80
|
-
merged.knowledgeAgent = rc.
|
|
79
|
+
if (rc.ONE_KNOWLEDGE_AGENT) {
|
|
80
|
+
merged.knowledgeAgent = rc.ONE_KNOWLEDGE_AGENT === "true";
|
|
81
81
|
}
|
|
82
82
|
return merged;
|
|
83
83
|
}
|
|
@@ -247,20 +247,20 @@ function writeAgentConfig(agent, config, scope = "global") {
|
|
|
247
247
|
}
|
|
248
248
|
function getMcpServerConfig(apiKey, accessControl) {
|
|
249
249
|
const env = {
|
|
250
|
-
|
|
250
|
+
ONE_SECRET: apiKey
|
|
251
251
|
};
|
|
252
252
|
if (accessControl) {
|
|
253
253
|
if (accessControl.permissions && accessControl.permissions !== "admin") {
|
|
254
|
-
env.
|
|
254
|
+
env.ONE_PERMISSIONS = accessControl.permissions;
|
|
255
255
|
}
|
|
256
256
|
if (accessControl.connectionKeys && !(accessControl.connectionKeys.length === 1 && accessControl.connectionKeys[0] === "*")) {
|
|
257
|
-
env.
|
|
257
|
+
env.ONE_CONNECTION_KEYS = accessControl.connectionKeys.join(",");
|
|
258
258
|
}
|
|
259
259
|
if (accessControl.actionIds && !(accessControl.actionIds.length === 1 && accessControl.actionIds[0] === "*")) {
|
|
260
|
-
env.
|
|
260
|
+
env.ONE_ACTION_IDS = accessControl.actionIds.join(",");
|
|
261
261
|
}
|
|
262
262
|
if (accessControl.knowledgeAgent) {
|
|
263
|
-
env.
|
|
263
|
+
env.ONE_KNOWLEDGE_AGENT = "true";
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
return {
|
|
@@ -273,7 +273,7 @@ function installMcpConfig(agent, apiKey, scope = "global", accessControl) {
|
|
|
273
273
|
const config = readAgentConfig(agent, scope);
|
|
274
274
|
const configKey = agent.configKey;
|
|
275
275
|
const mcpServers = config[configKey] || {};
|
|
276
|
-
mcpServers["
|
|
276
|
+
mcpServers["one"] = getMcpServerConfig(apiKey, accessControl);
|
|
277
277
|
config[configKey] = mcpServers;
|
|
278
278
|
writeAgentConfig(agent, config, scope);
|
|
279
279
|
}
|
|
@@ -281,7 +281,7 @@ function isMcpInstalled(agent, scope = "global") {
|
|
|
281
281
|
const config = readAgentConfig(agent, scope);
|
|
282
282
|
const configKey = agent.configKey;
|
|
283
283
|
const mcpServers = config[configKey];
|
|
284
|
-
return mcpServers?.["
|
|
284
|
+
return mcpServers?.["one"] !== void 0;
|
|
285
285
|
}
|
|
286
286
|
function getAgentStatuses() {
|
|
287
287
|
return AGENTS.map((agent) => {
|
|
@@ -293,7 +293,7 @@ function getAgentStatuses() {
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
// src/lib/api.ts
|
|
296
|
-
var API_BASE = "https://api.
|
|
296
|
+
var API_BASE = "https://api.withone.ai/v1";
|
|
297
297
|
var ApiError = class extends Error {
|
|
298
298
|
constructor(status, message) {
|
|
299
299
|
super(message);
|
|
@@ -301,12 +301,12 @@ var ApiError = class extends Error {
|
|
|
301
301
|
this.name = "ApiError";
|
|
302
302
|
}
|
|
303
303
|
};
|
|
304
|
-
var
|
|
304
|
+
var OneApi = class {
|
|
305
305
|
constructor(apiKey) {
|
|
306
306
|
this.apiKey = apiKey;
|
|
307
307
|
}
|
|
308
|
-
async request(
|
|
309
|
-
return this.requestFull({ path:
|
|
308
|
+
async request(path5) {
|
|
309
|
+
return this.requestFull({ path: path5 });
|
|
310
310
|
}
|
|
311
311
|
async requestFull(opts) {
|
|
312
312
|
let url = `${API_BASE}${opts.path}`;
|
|
@@ -315,7 +315,7 @@ var PicaApi = class {
|
|
|
315
315
|
url += `?${params.toString()}`;
|
|
316
316
|
}
|
|
317
317
|
const headers = {
|
|
318
|
-
"x-
|
|
318
|
+
"x-one-secret": this.apiKey,
|
|
319
319
|
"Content-Type": "application/json",
|
|
320
320
|
...opts.headers
|
|
321
321
|
};
|
|
@@ -406,9 +406,9 @@ var PicaApi = class {
|
|
|
406
406
|
const method = action.method;
|
|
407
407
|
const contentType = args.isFormData ? "multipart/form-data" : args.isFormUrlEncoded ? "application/x-www-form-urlencoded" : "application/json";
|
|
408
408
|
const requestHeaders = {
|
|
409
|
-
"x-
|
|
410
|
-
"x-
|
|
411
|
-
"x-
|
|
409
|
+
"x-one-secret": this.apiKey,
|
|
410
|
+
"x-one-connection-key": args.connectionKey,
|
|
411
|
+
"x-one-action-id": action._id,
|
|
412
412
|
"Content-Type": contentType,
|
|
413
413
|
...args.headers
|
|
414
414
|
};
|
|
@@ -476,7 +476,7 @@ var PicaApi = class {
|
|
|
476
476
|
method,
|
|
477
477
|
headers: {
|
|
478
478
|
...requestHeaders,
|
|
479
|
-
"x-
|
|
479
|
+
"x-one-secret": "***REDACTED***"
|
|
480
480
|
},
|
|
481
481
|
params: args.queryParams ? Object.fromEntries(
|
|
482
482
|
Object.entries(args.queryParams).map(([k, v]) => [k, String(v)])
|
|
@@ -488,7 +488,8 @@ var PicaApi = class {
|
|
|
488
488
|
const text4 = await response.text();
|
|
489
489
|
throw new ApiError(response.status, text4 || `HTTP ${response.status}`);
|
|
490
490
|
}
|
|
491
|
-
const
|
|
491
|
+
const responseText = await response.text();
|
|
492
|
+
const responseData = responseText ? JSON.parse(responseText) : {};
|
|
492
493
|
return {
|
|
493
494
|
requestConfig: sanitizedConfig,
|
|
494
495
|
responseData
|
|
@@ -521,9 +522,9 @@ var TimeoutError = class extends Error {
|
|
|
521
522
|
function sleep(ms) {
|
|
522
523
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
523
524
|
}
|
|
524
|
-
function replacePathVariables(
|
|
525
|
-
if (!
|
|
526
|
-
let result =
|
|
525
|
+
function replacePathVariables(path5, variables) {
|
|
526
|
+
if (!path5) return path5;
|
|
527
|
+
let result = path5;
|
|
527
528
|
result = result.replace(/\{\{([^}]+)\}\}/g, (_match, variable) => {
|
|
528
529
|
const trimmedVariable = variable.trim();
|
|
529
530
|
const value = variables[trimmedVariable];
|
|
@@ -561,7 +562,7 @@ function isActionAllowed(actionId, allowedActionIds) {
|
|
|
561
562
|
return allowedActionIds.includes("*") || allowedActionIds.includes(actionId);
|
|
562
563
|
}
|
|
563
564
|
function buildActionKnowledgeWithGuidance(knowledge, method, platform, actionId) {
|
|
564
|
-
const baseUrl = "https://api.
|
|
565
|
+
const baseUrl = "https://api.withone.ai";
|
|
565
566
|
return `${knowledge}
|
|
566
567
|
|
|
567
568
|
API REQUEST STRUCTURE
|
|
@@ -578,9 +579,9 @@ Examples:
|
|
|
578
579
|
METHOD: ${method}
|
|
579
580
|
|
|
580
581
|
HEADERS:
|
|
581
|
-
- x-
|
|
582
|
-
- x-
|
|
583
|
-
- x-
|
|
582
|
+
- x-one-secret: {{process.env.ONE_SECRET}}
|
|
583
|
+
- x-one-connection-key: {{process.env.ONE_${platform.toUpperCase()}_CONNECTION_KEY}}
|
|
584
|
+
- x-one-action-id: ${actionId}
|
|
584
585
|
- ... (other headers)
|
|
585
586
|
|
|
586
587
|
BODY: {{BODY}}
|
|
@@ -590,12 +591,12 @@ QUERY PARAMS: {{QUERY_PARAMS}}`;
|
|
|
590
591
|
|
|
591
592
|
// src/lib/browser.ts
|
|
592
593
|
import open from "open";
|
|
593
|
-
var
|
|
594
|
+
var ONE_APP_URL = "https://app.withone.ai";
|
|
594
595
|
function getConnectionUrl(platform) {
|
|
595
|
-
return `${
|
|
596
|
+
return `${ONE_APP_URL}/connections?#open=${platform}`;
|
|
596
597
|
}
|
|
597
598
|
function getApiKeyUrl() {
|
|
598
|
-
return `${
|
|
599
|
+
return `${ONE_APP_URL}/settings/api-keys`;
|
|
599
600
|
}
|
|
600
601
|
async function openConnectionPage(platform) {
|
|
601
602
|
const url = getConnectionUrl(platform);
|
|
@@ -616,7 +617,7 @@ function setAgentMode(value) {
|
|
|
616
617
|
_agentMode = value;
|
|
617
618
|
}
|
|
618
619
|
function isAgentMode() {
|
|
619
|
-
return _agentMode || process.env.
|
|
620
|
+
return _agentMode || process.env.ONE_AGENT === "1";
|
|
620
621
|
}
|
|
621
622
|
function createSpinner() {
|
|
622
623
|
if (isAgentMode()) {
|
|
@@ -629,6 +630,12 @@ function createSpinner() {
|
|
|
629
630
|
function intro2(msg) {
|
|
630
631
|
if (!isAgentMode()) p.intro(msg);
|
|
631
632
|
}
|
|
633
|
+
function outro2(msg) {
|
|
634
|
+
if (!isAgentMode()) p.outro(msg);
|
|
635
|
+
}
|
|
636
|
+
function note2(msg, title) {
|
|
637
|
+
if (!isAgentMode()) p.note(msg, title);
|
|
638
|
+
}
|
|
632
639
|
function json(data) {
|
|
633
640
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
634
641
|
}
|
|
@@ -648,10 +655,10 @@ async function configCommand() {
|
|
|
648
655
|
}
|
|
649
656
|
const config = readConfig();
|
|
650
657
|
if (!config) {
|
|
651
|
-
p2.log.error(`No
|
|
658
|
+
p2.log.error(`No One config found. Run ${pc.cyan("one init")} first.`);
|
|
652
659
|
return;
|
|
653
660
|
}
|
|
654
|
-
p2.intro(pc.bgCyan(pc.black("
|
|
661
|
+
p2.intro(pc.bgCyan(pc.black(" One Access Control ")));
|
|
655
662
|
const current = getAccessControl();
|
|
656
663
|
console.log();
|
|
657
664
|
console.log(` ${pc.bold("Current Access Control")}`);
|
|
@@ -694,7 +701,7 @@ async function configCommand() {
|
|
|
694
701
|
return;
|
|
695
702
|
}
|
|
696
703
|
if (connectionKeys.length === 0) {
|
|
697
|
-
p2.log.info(`No connections found. Defaulting to all. Use ${pc.cyan("
|
|
704
|
+
p2.log.info(`No connections found. Defaulting to all. Use ${pc.cyan("one add")} to connect platforms.`);
|
|
698
705
|
connectionKeys = void 0;
|
|
699
706
|
}
|
|
700
707
|
}
|
|
@@ -765,7 +772,7 @@ async function selectConnections(apiKey) {
|
|
|
765
772
|
spinner5.start("Fetching connections...");
|
|
766
773
|
let connections;
|
|
767
774
|
try {
|
|
768
|
-
const api = new
|
|
775
|
+
const api = new OneApi(apiKey);
|
|
769
776
|
const rawConnections = await api.listConnections();
|
|
770
777
|
connections = rawConnections.map((c) => ({ platform: c.platform, key: c.key }));
|
|
771
778
|
spinner5.stop(`Found ${connections.length} connection(s)`);
|
|
@@ -848,7 +855,7 @@ async function initCommand(options) {
|
|
|
848
855
|
}
|
|
849
856
|
const existingConfig = readConfig();
|
|
850
857
|
if (existingConfig) {
|
|
851
|
-
p3.intro(pc3.bgCyan(pc3.black("
|
|
858
|
+
p3.intro(pc3.bgCyan(pc3.black(" One ")));
|
|
852
859
|
await handleExistingConfig(existingConfig.apiKey, options);
|
|
853
860
|
return;
|
|
854
861
|
}
|
|
@@ -961,7 +968,7 @@ ${pc3.cyan(getApiKeyUrl())}`, "API Key");
|
|
|
961
968
|
await openApiKeyPage();
|
|
962
969
|
}
|
|
963
970
|
const newKey = await p3.text({
|
|
964
|
-
message: "Enter your new
|
|
971
|
+
message: "Enter your new One API key:",
|
|
965
972
|
placeholder: "sk_live_...",
|
|
966
973
|
validate: (value) => {
|
|
967
974
|
if (!value) return "API key is required";
|
|
@@ -977,7 +984,7 @@ ${pc3.cyan(getApiKeyUrl())}`, "API Key");
|
|
|
977
984
|
}
|
|
978
985
|
const spinner5 = p3.spinner();
|
|
979
986
|
spinner5.start("Validating API key...");
|
|
980
|
-
const api = new
|
|
987
|
+
const api = new OneApi(newKey);
|
|
981
988
|
const isValid = await api.validateApiKey();
|
|
982
989
|
if (!isValid) {
|
|
983
990
|
spinner5.stop("Invalid API key");
|
|
@@ -1014,7 +1021,7 @@ async function handleInstallMore(apiKey, missing) {
|
|
|
1014
1021
|
if (missing.length === 1) {
|
|
1015
1022
|
const agent = missing[0].agent;
|
|
1016
1023
|
const confirm3 = await p3.confirm({
|
|
1017
|
-
message: `Install
|
|
1024
|
+
message: `Install One MCP to ${agent.name}?`,
|
|
1018
1025
|
initialValue: true
|
|
1019
1026
|
});
|
|
1020
1027
|
if (p3.isCancel(confirm3) || !confirm3) {
|
|
@@ -1106,7 +1113,7 @@ ${pc3.cyan(getApiKeyUrl())}`, "API Key");
|
|
|
1106
1113
|
await openApiKeyPage();
|
|
1107
1114
|
}
|
|
1108
1115
|
const apiKey = await p3.text({
|
|
1109
|
-
message: "Enter your
|
|
1116
|
+
message: "Enter your One API key:",
|
|
1110
1117
|
placeholder: "sk_live_...",
|
|
1111
1118
|
validate: (value) => {
|
|
1112
1119
|
if (!value) return "API key is required";
|
|
@@ -1122,7 +1129,7 @@ ${pc3.cyan(getApiKeyUrl())}`, "API Key");
|
|
|
1122
1129
|
}
|
|
1123
1130
|
const spinner5 = p3.spinner();
|
|
1124
1131
|
spinner5.start("Validating API key...");
|
|
1125
|
-
const api = new
|
|
1132
|
+
const api = new OneApi(apiKey);
|
|
1126
1133
|
const isValid = await api.validateApiKey();
|
|
1127
1134
|
if (!isValid) {
|
|
1128
1135
|
spinner5.stop("Invalid API key");
|
|
@@ -1234,7 +1241,7 @@ ${globalPaths}
|
|
|
1234
1241
|
summary += pc3.yellow("Note: Project config files can be committed to share with your team.\n") + pc3.yellow("Team members will need their own API key.");
|
|
1235
1242
|
p3.note(summary, "Setup Complete");
|
|
1236
1243
|
await promptConnectIntegrations(apiKey);
|
|
1237
|
-
p3.outro("Your AI agents now have access to
|
|
1244
|
+
p3.outro("Your AI agents now have access to One integrations!");
|
|
1238
1245
|
return;
|
|
1239
1246
|
}
|
|
1240
1247
|
const installedAgentIds = [];
|
|
@@ -1255,7 +1262,7 @@ ${globalPaths}
|
|
|
1255
1262
|
"Setup Complete"
|
|
1256
1263
|
);
|
|
1257
1264
|
await promptConnectIntegrations(apiKey);
|
|
1258
|
-
p3.outro("Your AI agents now have access to
|
|
1265
|
+
p3.outro("Your AI agents now have access to One integrations!");
|
|
1259
1266
|
}
|
|
1260
1267
|
function printBanner() {
|
|
1261
1268
|
console.log();
|
|
@@ -1279,7 +1286,7 @@ var TOP_INTEGRATIONS = [
|
|
|
1279
1286
|
{ value: "notion", label: "Notion", hint: "Access pages, databases, and docs" }
|
|
1280
1287
|
];
|
|
1281
1288
|
async function promptConnectIntegrations(apiKey) {
|
|
1282
|
-
const api = new
|
|
1289
|
+
const api = new OneApi(apiKey);
|
|
1283
1290
|
const connected = [];
|
|
1284
1291
|
try {
|
|
1285
1292
|
const existing = await api.listConnections();
|
|
@@ -1301,7 +1308,7 @@ async function promptConnectIntegrations(apiKey) {
|
|
|
1301
1308
|
hint: i.hint
|
|
1302
1309
|
})),
|
|
1303
1310
|
{ value: "more", label: "Browse all 200+ platforms" },
|
|
1304
|
-
{ value: "skip", label: "Skip for now", hint: "you can always run
|
|
1311
|
+
{ value: "skip", label: "Skip for now", hint: "you can always run one add later" }
|
|
1305
1312
|
];
|
|
1306
1313
|
const message = first ? "Connect your first integration?" : "Connect another?";
|
|
1307
1314
|
const choice = await p3.select({ message, options });
|
|
@@ -1310,12 +1317,12 @@ async function promptConnectIntegrations(apiKey) {
|
|
|
1310
1317
|
}
|
|
1311
1318
|
if (choice === "more") {
|
|
1312
1319
|
try {
|
|
1313
|
-
await open2("https://app.
|
|
1314
|
-
p3.log.info("Opened
|
|
1320
|
+
await open2("https://app.withone.ai/connections");
|
|
1321
|
+
p3.log.info("Opened One dashboard in browser.");
|
|
1315
1322
|
} catch {
|
|
1316
|
-
p3.note("https://app.
|
|
1323
|
+
p3.note("https://app.withone.ai/connections", "Open in browser");
|
|
1317
1324
|
}
|
|
1318
|
-
p3.log.info(`Connect from the dashboard, or use ${pc3.cyan("
|
|
1325
|
+
p3.log.info(`Connect from the dashboard, or use ${pc3.cyan("one add <platform>")}`);
|
|
1319
1326
|
break;
|
|
1320
1327
|
}
|
|
1321
1328
|
const platform = choice;
|
|
@@ -1340,7 +1347,7 @@ async function promptConnectIntegrations(apiKey) {
|
|
|
1340
1347
|
} catch (error2) {
|
|
1341
1348
|
spinner5.stop("Connection timed out");
|
|
1342
1349
|
if (error2 instanceof TimeoutError) {
|
|
1343
|
-
p3.log.warn(`No worries. Connect later with: ${pc3.cyan(`
|
|
1350
|
+
p3.log.warn(`No worries. Connect later with: ${pc3.cyan(`one add ${platform}`)}`);
|
|
1344
1351
|
}
|
|
1345
1352
|
first = false;
|
|
1346
1353
|
}
|
|
@@ -1407,13 +1414,13 @@ async function connectionAddCommand(platformArg) {
|
|
|
1407
1414
|
if (isAgentMode()) {
|
|
1408
1415
|
error("This command requires interactive input. Run without --agent.");
|
|
1409
1416
|
}
|
|
1410
|
-
p4.intro(pc4.bgCyan(pc4.black("
|
|
1417
|
+
p4.intro(pc4.bgCyan(pc4.black(" One ")));
|
|
1411
1418
|
const apiKey = getApiKey();
|
|
1412
1419
|
if (!apiKey) {
|
|
1413
|
-
p4.cancel("Not configured. Run `
|
|
1420
|
+
p4.cancel("Not configured. Run `one init` first.");
|
|
1414
1421
|
process.exit(1);
|
|
1415
1422
|
}
|
|
1416
|
-
const api = new
|
|
1423
|
+
const api = new OneApi(apiKey);
|
|
1417
1424
|
const spinner5 = p4.spinner();
|
|
1418
1425
|
spinner5.start("Loading platforms...");
|
|
1419
1426
|
let platforms;
|
|
@@ -1442,7 +1449,7 @@ async function connectionAddCommand(platformArg) {
|
|
|
1442
1449
|
]
|
|
1443
1450
|
});
|
|
1444
1451
|
if (p4.isCancel(suggestion) || suggestion === "__other__") {
|
|
1445
|
-
p4.note(`Run ${pc4.cyan("
|
|
1452
|
+
p4.note(`Run ${pc4.cyan("one platforms")} to see all available platforms.`);
|
|
1446
1453
|
p4.cancel("Connection cancelled.");
|
|
1447
1454
|
process.exit(0);
|
|
1448
1455
|
}
|
|
@@ -1450,7 +1457,7 @@ async function connectionAddCommand(platformArg) {
|
|
|
1450
1457
|
} else {
|
|
1451
1458
|
p4.cancel(`Unknown platform: ${platformArg}
|
|
1452
1459
|
|
|
1453
|
-
Run ${pc4.cyan("
|
|
1460
|
+
Run ${pc4.cyan("one platforms")} to see available platforms.`);
|
|
1454
1461
|
process.exit(1);
|
|
1455
1462
|
}
|
|
1456
1463
|
}
|
|
@@ -1473,7 +1480,7 @@ Run ${pc4.cyan("pica platforms")} to see available platforms.`);
|
|
|
1473
1480
|
} else {
|
|
1474
1481
|
p4.cancel(`Unknown platform: ${platformInput}
|
|
1475
1482
|
|
|
1476
|
-
Run ${pc4.cyan("
|
|
1483
|
+
Run ${pc4.cyan("one platforms")} to see available platforms.`);
|
|
1477
1484
|
process.exit(1);
|
|
1478
1485
|
}
|
|
1479
1486
|
}
|
|
@@ -1503,7 +1510,7 @@ ${url}`);
|
|
|
1503
1510
|
- Browser popup was blocked
|
|
1504
1511
|
- Wrong account selected
|
|
1505
1512
|
|
|
1506
|
-
Try again with: ${pc4.cyan(`
|
|
1513
|
+
Try again with: ${pc4.cyan(`one connection add ${platform}`)}`,
|
|
1507
1514
|
"Timed Out"
|
|
1508
1515
|
);
|
|
1509
1516
|
} else {
|
|
@@ -1515,9 +1522,9 @@ Try again with: ${pc4.cyan(`pica connection add ${platform}`)}`,
|
|
|
1515
1522
|
async function connectionListCommand() {
|
|
1516
1523
|
const apiKey = getApiKey();
|
|
1517
1524
|
if (!apiKey) {
|
|
1518
|
-
error("Not configured. Run `
|
|
1525
|
+
error("Not configured. Run `one init` first.");
|
|
1519
1526
|
}
|
|
1520
|
-
const api = new
|
|
1527
|
+
const api = new OneApi(apiKey);
|
|
1521
1528
|
const spinner5 = createSpinner();
|
|
1522
1529
|
spinner5.start("Loading connections...");
|
|
1523
1530
|
try {
|
|
@@ -1537,7 +1544,7 @@ async function connectionListCommand() {
|
|
|
1537
1544
|
p4.note(
|
|
1538
1545
|
`No connections yet.
|
|
1539
1546
|
|
|
1540
|
-
Add one with: ${pc4.cyan("
|
|
1547
|
+
Add one with: ${pc4.cyan("one connection add gmail")}`,
|
|
1541
1548
|
"No Connections"
|
|
1542
1549
|
);
|
|
1543
1550
|
return;
|
|
@@ -1559,7 +1566,7 @@ Add one with: ${pc4.cyan("pica connection add gmail")}`,
|
|
|
1559
1566
|
rows
|
|
1560
1567
|
);
|
|
1561
1568
|
console.log();
|
|
1562
|
-
p4.note(`Add more with: ${pc4.cyan("
|
|
1569
|
+
p4.note(`Add more with: ${pc4.cyan("one connection add <platform>")}`, "Tip");
|
|
1563
1570
|
} catch (error2) {
|
|
1564
1571
|
spinner5.stop("Failed to load connections");
|
|
1565
1572
|
error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
|
|
@@ -1584,12 +1591,12 @@ import pc5 from "picocolors";
|
|
|
1584
1591
|
async function platformsCommand(options) {
|
|
1585
1592
|
const apiKey = getApiKey();
|
|
1586
1593
|
if (!apiKey) {
|
|
1587
|
-
error("Not configured. Run `
|
|
1594
|
+
error("Not configured. Run `one init` first.");
|
|
1588
1595
|
}
|
|
1589
1596
|
if (isAgentMode()) {
|
|
1590
1597
|
options.json = true;
|
|
1591
1598
|
}
|
|
1592
|
-
const api = new
|
|
1599
|
+
const api = new OneApi(apiKey);
|
|
1593
1600
|
const spinner5 = createSpinner();
|
|
1594
1601
|
spinner5.start("Loading platforms...");
|
|
1595
1602
|
try {
|
|
@@ -1648,7 +1655,7 @@ async function platformsCommand(options) {
|
|
|
1648
1655
|
);
|
|
1649
1656
|
}
|
|
1650
1657
|
console.log();
|
|
1651
|
-
p5.note(`Connect with: ${pc5.cyan("
|
|
1658
|
+
p5.note(`Connect with: ${pc5.cyan("one connection add <platform>")}`, "Tip");
|
|
1652
1659
|
} catch (error2) {
|
|
1653
1660
|
spinner5.stop("Failed to load platforms");
|
|
1654
1661
|
error(`Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`);
|
|
@@ -1661,7 +1668,7 @@ import pc6 from "picocolors";
|
|
|
1661
1668
|
function getConfig() {
|
|
1662
1669
|
const apiKey = getApiKey();
|
|
1663
1670
|
if (!apiKey) {
|
|
1664
|
-
error("Not configured. Run `
|
|
1671
|
+
error("Not configured. Run `one init` first.");
|
|
1665
1672
|
}
|
|
1666
1673
|
const ac = getAccessControlFromAllSources();
|
|
1667
1674
|
const permissions = ac.permissions || "admin";
|
|
@@ -1678,9 +1685,9 @@ function parseJsonArg(value, argName) {
|
|
|
1678
1685
|
}
|
|
1679
1686
|
}
|
|
1680
1687
|
async function actionsSearchCommand(platform, query, options) {
|
|
1681
|
-
intro2(pc6.bgCyan(pc6.black("
|
|
1688
|
+
intro2(pc6.bgCyan(pc6.black(" One ")));
|
|
1682
1689
|
const { apiKey, permissions, actionIds, knowledgeAgent } = getConfig();
|
|
1683
|
-
const api = new
|
|
1690
|
+
const api = new OneApi(apiKey);
|
|
1684
1691
|
const spinner5 = createSpinner();
|
|
1685
1692
|
spinner5.start(`Searching actions on ${pc6.cyan(platform)} for "${query}"...`);
|
|
1686
1693
|
try {
|
|
@@ -1706,7 +1713,7 @@ async function actionsSearchCommand(platform, query, options) {
|
|
|
1706
1713
|
Suggestions:
|
|
1707
1714
|
- Try a more general query (e.g., 'list', 'get', 'search', 'create')
|
|
1708
1715
|
- Verify the platform name is correct
|
|
1709
|
-
- Check available platforms with ${pc6.cyan("
|
|
1716
|
+
- Check available platforms with ${pc6.cyan("one platforms")}
|
|
1710
1717
|
|
|
1711
1718
|
Examples of good queries:
|
|
1712
1719
|
- "search contacts"
|
|
@@ -1738,8 +1745,8 @@ Examples of good queries:
|
|
|
1738
1745
|
);
|
|
1739
1746
|
console.log();
|
|
1740
1747
|
p6.note(
|
|
1741
|
-
`Get details: ${pc6.cyan(`
|
|
1742
|
-
Execute: ${pc6.cyan(`
|
|
1748
|
+
`Get details: ${pc6.cyan(`one actions knowledge ${platform} <actionId>`)}
|
|
1749
|
+
Execute: ${pc6.cyan(`one actions execute ${platform} <actionId> <connectionKey>`)}`,
|
|
1743
1750
|
"Next Steps"
|
|
1744
1751
|
);
|
|
1745
1752
|
} catch (error2) {
|
|
@@ -1750,9 +1757,9 @@ Execute: ${pc6.cyan(`pica actions execute ${platform} <actionId> <connection
|
|
|
1750
1757
|
}
|
|
1751
1758
|
}
|
|
1752
1759
|
async function actionsKnowledgeCommand(platform, actionId) {
|
|
1753
|
-
intro2(pc6.bgCyan(pc6.black("
|
|
1760
|
+
intro2(pc6.bgCyan(pc6.black(" One ")));
|
|
1754
1761
|
const { apiKey, actionIds, connectionKeys } = getConfig();
|
|
1755
|
-
const api = new
|
|
1762
|
+
const api = new OneApi(apiKey);
|
|
1756
1763
|
if (!isActionAllowed(actionId, actionIds)) {
|
|
1757
1764
|
error(`Action "${actionId}" is not in the allowed action list.`);
|
|
1758
1765
|
}
|
|
@@ -1793,7 +1800,7 @@ async function actionsKnowledgeCommand(platform, actionId) {
|
|
|
1793
1800
|
console.log(knowledgeWithGuidance);
|
|
1794
1801
|
console.log();
|
|
1795
1802
|
p6.note(
|
|
1796
|
-
`Execute: ${pc6.cyan(`
|
|
1803
|
+
`Execute: ${pc6.cyan(`one actions execute ${platform} ${actionId} <connectionKey>`)}`,
|
|
1797
1804
|
"Next Step"
|
|
1798
1805
|
);
|
|
1799
1806
|
} catch (error2) {
|
|
@@ -1804,7 +1811,7 @@ async function actionsKnowledgeCommand(platform, actionId) {
|
|
|
1804
1811
|
}
|
|
1805
1812
|
}
|
|
1806
1813
|
async function actionsExecuteCommand(platform, actionId, connectionKey, options) {
|
|
1807
|
-
intro2(pc6.bgCyan(pc6.black("
|
|
1814
|
+
intro2(pc6.bgCyan(pc6.black(" One ")));
|
|
1808
1815
|
const { apiKey, permissions, actionIds, connectionKeys, knowledgeAgent } = getConfig();
|
|
1809
1816
|
if (knowledgeAgent) {
|
|
1810
1817
|
error(
|
|
@@ -1817,7 +1824,7 @@ async function actionsExecuteCommand(platform, actionId, connectionKey, options)
|
|
|
1817
1824
|
if (!connectionKeys.includes("*") && !connectionKeys.includes(connectionKey)) {
|
|
1818
1825
|
error(`Connection key "${connectionKey}" is not allowed.`);
|
|
1819
1826
|
}
|
|
1820
|
-
const api = new
|
|
1827
|
+
const api = new OneApi(apiKey);
|
|
1821
1828
|
const spinner5 = createSpinner();
|
|
1822
1829
|
spinner5.start("Loading action details...");
|
|
1823
1830
|
try {
|
|
@@ -1894,50 +1901,2172 @@ function colorMethod(method) {
|
|
|
1894
1901
|
}
|
|
1895
1902
|
}
|
|
1896
1903
|
|
|
1897
|
-
// src/
|
|
1898
|
-
|
|
1899
|
-
var { version } = require2("../package.json");
|
|
1900
|
-
var program = new Command();
|
|
1901
|
-
program.name("pica").option("--agent", "Machine-readable JSON output (no colors, spinners, or prompts)").description(`Pica CLI \u2014 Connect AI agents to 200+ platforms through one interface.
|
|
1902
|
-
|
|
1903
|
-
Setup:
|
|
1904
|
-
pica init Set up API key and install MCP server
|
|
1905
|
-
pica add <platform> Connect a platform via OAuth (e.g. gmail, slack, shopify)
|
|
1906
|
-
pica config Configure access control (permissions, scoping)
|
|
1907
|
-
|
|
1908
|
-
Workflow (use these in order):
|
|
1909
|
-
1. pica list List your connected platforms and connection keys
|
|
1910
|
-
2. pica actions search <platform> <q> Search for actions using natural language
|
|
1911
|
-
3. pica actions knowledge <plat> <id> Get full docs for an action (ALWAYS do this before execute)
|
|
1912
|
-
4. pica actions execute <p> <id> <key> Execute the action
|
|
1904
|
+
// src/commands/flow.ts
|
|
1905
|
+
import pc7 from "picocolors";
|
|
1913
1906
|
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1907
|
+
// src/lib/flow-validator.ts
|
|
1908
|
+
var VALID_STEP_TYPES = [
|
|
1909
|
+
"action",
|
|
1910
|
+
"transform",
|
|
1911
|
+
"code",
|
|
1912
|
+
"condition",
|
|
1913
|
+
"loop",
|
|
1914
|
+
"parallel",
|
|
1915
|
+
"file-read",
|
|
1916
|
+
"file-write"
|
|
1917
|
+
];
|
|
1918
|
+
var VALID_INPUT_TYPES = ["string", "number", "boolean", "object", "array"];
|
|
1919
|
+
var VALID_ERROR_STRATEGIES = ["fail", "continue", "retry", "fallback"];
|
|
1920
|
+
function validateFlowSchema(flow2) {
|
|
1921
|
+
const errors = [];
|
|
1922
|
+
if (!flow2 || typeof flow2 !== "object") {
|
|
1923
|
+
errors.push({ path: "", message: "Flow must be a JSON object" });
|
|
1924
|
+
return errors;
|
|
1925
|
+
}
|
|
1926
|
+
const f = flow2;
|
|
1927
|
+
if (!f.key || typeof f.key !== "string") {
|
|
1928
|
+
errors.push({ path: "key", message: 'Flow must have a string "key"' });
|
|
1929
|
+
} else if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(f.key) && f.key.length > 1) {
|
|
1930
|
+
errors.push({ path: "key", message: "Flow key must be kebab-case (lowercase letters, numbers, hyphens)" });
|
|
1931
|
+
}
|
|
1932
|
+
if (!f.name || typeof f.name !== "string") {
|
|
1933
|
+
errors.push({ path: "name", message: 'Flow must have a string "name"' });
|
|
1934
|
+
}
|
|
1935
|
+
if (f.description !== void 0 && typeof f.description !== "string") {
|
|
1936
|
+
errors.push({ path: "description", message: '"description" must be a string' });
|
|
1937
|
+
}
|
|
1938
|
+
if (f.version !== void 0 && typeof f.version !== "string") {
|
|
1939
|
+
errors.push({ path: "version", message: '"version" must be a string' });
|
|
1940
|
+
}
|
|
1941
|
+
if (!f.inputs || typeof f.inputs !== "object" || Array.isArray(f.inputs)) {
|
|
1942
|
+
errors.push({ path: "inputs", message: 'Flow must have an "inputs" object' });
|
|
1943
|
+
} else {
|
|
1944
|
+
const inputs = f.inputs;
|
|
1945
|
+
for (const [name, decl] of Object.entries(inputs)) {
|
|
1946
|
+
const prefix = `inputs.${name}`;
|
|
1947
|
+
if (!decl || typeof decl !== "object" || Array.isArray(decl)) {
|
|
1948
|
+
errors.push({ path: prefix, message: "Input declaration must be an object" });
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
const d = decl;
|
|
1952
|
+
if (!d.type || !VALID_INPUT_TYPES.includes(d.type)) {
|
|
1953
|
+
errors.push({ path: `${prefix}.type`, message: `Input type must be one of: ${VALID_INPUT_TYPES.join(", ")}` });
|
|
1954
|
+
}
|
|
1955
|
+
if (d.connection !== void 0) {
|
|
1956
|
+
if (!d.connection || typeof d.connection !== "object") {
|
|
1957
|
+
errors.push({ path: `${prefix}.connection`, message: "Connection metadata must be an object" });
|
|
1958
|
+
} else {
|
|
1959
|
+
const conn = d.connection;
|
|
1960
|
+
if (!conn.platform || typeof conn.platform !== "string") {
|
|
1961
|
+
errors.push({ path: `${prefix}.connection.platform`, message: 'Connection must have a string "platform"' });
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (!Array.isArray(f.steps)) {
|
|
1968
|
+
errors.push({ path: "steps", message: 'Flow must have a "steps" array' });
|
|
1969
|
+
} else {
|
|
1970
|
+
validateStepsArray(f.steps, "steps", errors);
|
|
1971
|
+
}
|
|
1972
|
+
return errors;
|
|
1973
|
+
}
|
|
1974
|
+
function validateStepsArray(steps, pathPrefix, errors) {
|
|
1975
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1976
|
+
const step = steps[i];
|
|
1977
|
+
const path5 = `${pathPrefix}[${i}]`;
|
|
1978
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
1979
|
+
errors.push({ path: path5, message: "Step must be an object" });
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
const s = step;
|
|
1983
|
+
if (!s.id || typeof s.id !== "string") {
|
|
1984
|
+
errors.push({ path: `${path5}.id`, message: 'Step must have a string "id"' });
|
|
1985
|
+
}
|
|
1986
|
+
if (!s.name || typeof s.name !== "string") {
|
|
1987
|
+
errors.push({ path: `${path5}.name`, message: 'Step must have a string "name"' });
|
|
1988
|
+
}
|
|
1989
|
+
if (!s.type || !VALID_STEP_TYPES.includes(s.type)) {
|
|
1990
|
+
errors.push({ path: `${path5}.type`, message: `Step type must be one of: ${VALID_STEP_TYPES.join(", ")}` });
|
|
1991
|
+
}
|
|
1992
|
+
if (s.onError && typeof s.onError === "object") {
|
|
1993
|
+
const oe = s.onError;
|
|
1994
|
+
if (!VALID_ERROR_STRATEGIES.includes(oe.strategy)) {
|
|
1995
|
+
errors.push({ path: `${path5}.onError.strategy`, message: `Error strategy must be one of: ${VALID_ERROR_STRATEGIES.join(", ")}` });
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
const type = s.type;
|
|
1999
|
+
if (type === "action") {
|
|
2000
|
+
if (!s.action || typeof s.action !== "object") {
|
|
2001
|
+
errors.push({ path: `${path5}.action`, message: 'Action step must have an "action" config object' });
|
|
2002
|
+
} else {
|
|
2003
|
+
const a = s.action;
|
|
2004
|
+
if (!a.platform) errors.push({ path: `${path5}.action.platform`, message: 'Action must have "platform"' });
|
|
2005
|
+
if (!a.actionId) errors.push({ path: `${path5}.action.actionId`, message: 'Action must have "actionId"' });
|
|
2006
|
+
if (!a.connectionKey) errors.push({ path: `${path5}.action.connectionKey`, message: 'Action must have "connectionKey"' });
|
|
2007
|
+
}
|
|
2008
|
+
} else if (type === "transform") {
|
|
2009
|
+
if (!s.transform || typeof s.transform !== "object") {
|
|
2010
|
+
errors.push({ path: `${path5}.transform`, message: 'Transform step must have a "transform" config object' });
|
|
2011
|
+
} else {
|
|
2012
|
+
const t = s.transform;
|
|
2013
|
+
if (!t.expression || typeof t.expression !== "string") {
|
|
2014
|
+
errors.push({ path: `${path5}.transform.expression`, message: 'Transform must have a string "expression"' });
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
} else if (type === "code") {
|
|
2018
|
+
if (!s.code || typeof s.code !== "object") {
|
|
2019
|
+
errors.push({ path: `${path5}.code`, message: 'Code step must have a "code" config object' });
|
|
2020
|
+
} else {
|
|
2021
|
+
const c = s.code;
|
|
2022
|
+
if (!c.source || typeof c.source !== "string") {
|
|
2023
|
+
errors.push({ path: `${path5}.code.source`, message: 'Code must have a string "source"' });
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
} else if (type === "condition") {
|
|
2027
|
+
if (!s.condition || typeof s.condition !== "object") {
|
|
2028
|
+
errors.push({ path: `${path5}.condition`, message: 'Condition step must have a "condition" config object' });
|
|
2029
|
+
} else {
|
|
2030
|
+
const c = s.condition;
|
|
2031
|
+
if (!c.expression || typeof c.expression !== "string") {
|
|
2032
|
+
errors.push({ path: `${path5}.condition.expression`, message: 'Condition must have a string "expression"' });
|
|
2033
|
+
}
|
|
2034
|
+
if (!Array.isArray(c.then)) {
|
|
2035
|
+
errors.push({ path: `${path5}.condition.then`, message: 'Condition must have a "then" steps array' });
|
|
2036
|
+
} else {
|
|
2037
|
+
validateStepsArray(c.then, `${path5}.condition.then`, errors);
|
|
2038
|
+
}
|
|
2039
|
+
if (c.else !== void 0) {
|
|
2040
|
+
if (!Array.isArray(c.else)) {
|
|
2041
|
+
errors.push({ path: `${path5}.condition.else`, message: 'Condition "else" must be a steps array' });
|
|
2042
|
+
} else {
|
|
2043
|
+
validateStepsArray(c.else, `${path5}.condition.else`, errors);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
} else if (type === "loop") {
|
|
2048
|
+
if (!s.loop || typeof s.loop !== "object") {
|
|
2049
|
+
errors.push({ path: `${path5}.loop`, message: 'Loop step must have a "loop" config object' });
|
|
2050
|
+
} else {
|
|
2051
|
+
const l = s.loop;
|
|
2052
|
+
if (!l.over || typeof l.over !== "string") {
|
|
2053
|
+
errors.push({ path: `${path5}.loop.over`, message: 'Loop must have a string "over" selector' });
|
|
2054
|
+
}
|
|
2055
|
+
if (!l.as || typeof l.as !== "string") {
|
|
2056
|
+
errors.push({ path: `${path5}.loop.as`, message: 'Loop must have a string "as" variable name' });
|
|
2057
|
+
}
|
|
2058
|
+
if (!Array.isArray(l.steps)) {
|
|
2059
|
+
errors.push({ path: `${path5}.loop.steps`, message: 'Loop must have a "steps" array' });
|
|
2060
|
+
} else {
|
|
2061
|
+
validateStepsArray(l.steps, `${path5}.loop.steps`, errors);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
} else if (type === "parallel") {
|
|
2065
|
+
if (!s.parallel || typeof s.parallel !== "object") {
|
|
2066
|
+
errors.push({ path: `${path5}.parallel`, message: 'Parallel step must have a "parallel" config object' });
|
|
2067
|
+
} else {
|
|
2068
|
+
const par = s.parallel;
|
|
2069
|
+
if (!Array.isArray(par.steps)) {
|
|
2070
|
+
errors.push({ path: `${path5}.parallel.steps`, message: 'Parallel must have a "steps" array' });
|
|
2071
|
+
} else {
|
|
2072
|
+
validateStepsArray(par.steps, `${path5}.parallel.steps`, errors);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
} else if (type === "file-read") {
|
|
2076
|
+
if (!s.fileRead || typeof s.fileRead !== "object") {
|
|
2077
|
+
errors.push({ path: `${path5}.fileRead`, message: 'File-read step must have a "fileRead" config object' });
|
|
2078
|
+
} else {
|
|
2079
|
+
const fr = s.fileRead;
|
|
2080
|
+
if (!fr.path || typeof fr.path !== "string") {
|
|
2081
|
+
errors.push({ path: `${path5}.fileRead.path`, message: 'File-read must have a string "path"' });
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
} else if (type === "file-write") {
|
|
2085
|
+
if (!s.fileWrite || typeof s.fileWrite !== "object") {
|
|
2086
|
+
errors.push({ path: `${path5}.fileWrite`, message: 'File-write step must have a "fileWrite" config object' });
|
|
2087
|
+
} else {
|
|
2088
|
+
const fw = s.fileWrite;
|
|
2089
|
+
if (!fw.path || typeof fw.path !== "string") {
|
|
2090
|
+
errors.push({ path: `${path5}.fileWrite.path`, message: 'File-write must have a string "path"' });
|
|
2091
|
+
}
|
|
2092
|
+
if (fw.content === void 0) {
|
|
2093
|
+
errors.push({ path: `${path5}.fileWrite.content`, message: 'File-write must have "content"' });
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
function validateStepIds(flow2) {
|
|
2100
|
+
const errors = [];
|
|
2101
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2102
|
+
function collectIds(steps, pathPrefix) {
|
|
2103
|
+
for (let i = 0; i < steps.length; i++) {
|
|
2104
|
+
const step = steps[i];
|
|
2105
|
+
const path5 = `${pathPrefix}[${i}]`;
|
|
2106
|
+
if (seen.has(step.id)) {
|
|
2107
|
+
errors.push({ path: `${path5}.id`, message: `Duplicate step ID: "${step.id}"` });
|
|
2108
|
+
} else {
|
|
2109
|
+
seen.add(step.id);
|
|
2110
|
+
}
|
|
2111
|
+
if (step.condition) {
|
|
2112
|
+
if (step.condition.then) collectIds(step.condition.then, `${path5}.condition.then`);
|
|
2113
|
+
if (step.condition.else) collectIds(step.condition.else, `${path5}.condition.else`);
|
|
2114
|
+
}
|
|
2115
|
+
if (step.loop?.steps) collectIds(step.loop.steps, `${path5}.loop.steps`);
|
|
2116
|
+
if (step.parallel?.steps) collectIds(step.parallel.steps, `${path5}.parallel.steps`);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
collectIds(flow2.steps, "steps");
|
|
2120
|
+
return errors;
|
|
2121
|
+
}
|
|
2122
|
+
function validateSelectorReferences(flow2) {
|
|
2123
|
+
const errors = [];
|
|
2124
|
+
const inputNames = new Set(Object.keys(flow2.inputs));
|
|
2125
|
+
function getAllStepIds(steps) {
|
|
2126
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2127
|
+
for (const step of steps) {
|
|
2128
|
+
ids.add(step.id);
|
|
2129
|
+
if (step.condition) {
|
|
2130
|
+
for (const id of getAllStepIds(step.condition.then)) ids.add(id);
|
|
2131
|
+
if (step.condition.else) {
|
|
2132
|
+
for (const id of getAllStepIds(step.condition.else)) ids.add(id);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (step.loop?.steps) {
|
|
2136
|
+
for (const id of getAllStepIds(step.loop.steps)) ids.add(id);
|
|
2137
|
+
}
|
|
2138
|
+
if (step.parallel?.steps) {
|
|
2139
|
+
for (const id of getAllStepIds(step.parallel.steps)) ids.add(id);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return ids;
|
|
2143
|
+
}
|
|
2144
|
+
const allStepIds = getAllStepIds(flow2.steps);
|
|
2145
|
+
function extractSelectors(value) {
|
|
2146
|
+
const selectors = [];
|
|
2147
|
+
if (typeof value === "string") {
|
|
2148
|
+
if (value.startsWith("$.")) {
|
|
2149
|
+
selectors.push(value);
|
|
2150
|
+
}
|
|
2151
|
+
const interpolated = value.matchAll(/\{\{(\$\.[^}]+)\}\}/g);
|
|
2152
|
+
for (const match of interpolated) {
|
|
2153
|
+
selectors.push(match[1]);
|
|
2154
|
+
}
|
|
2155
|
+
} else if (Array.isArray(value)) {
|
|
2156
|
+
for (const item of value) {
|
|
2157
|
+
selectors.push(...extractSelectors(item));
|
|
2158
|
+
}
|
|
2159
|
+
} else if (value && typeof value === "object") {
|
|
2160
|
+
for (const v of Object.values(value)) {
|
|
2161
|
+
selectors.push(...extractSelectors(v));
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
return selectors;
|
|
2165
|
+
}
|
|
2166
|
+
function checkSelectors(selectors, path5) {
|
|
2167
|
+
for (const selector of selectors) {
|
|
2168
|
+
const parts = selector.split(".");
|
|
2169
|
+
if (parts.length < 3) continue;
|
|
2170
|
+
const root = parts[1];
|
|
2171
|
+
if (root === "input") {
|
|
2172
|
+
const inputName = parts[2];
|
|
2173
|
+
if (!inputNames.has(inputName)) {
|
|
2174
|
+
errors.push({ path: path5, message: `Selector "${selector}" references undefined input "${inputName}"` });
|
|
2175
|
+
}
|
|
2176
|
+
} else if (root === "steps") {
|
|
2177
|
+
const stepId = parts[2];
|
|
2178
|
+
if (!allStepIds.has(stepId)) {
|
|
2179
|
+
errors.push({ path: path5, message: `Selector "${selector}" references undefined step "${stepId}"` });
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function checkStep(step, pathPrefix) {
|
|
2185
|
+
if (step.if) checkSelectors(extractSelectors(step.if), `${pathPrefix}.if`);
|
|
2186
|
+
if (step.unless) checkSelectors(extractSelectors(step.unless), `${pathPrefix}.unless`);
|
|
2187
|
+
if (step.action) {
|
|
2188
|
+
checkSelectors(extractSelectors(step.action), `${pathPrefix}.action`);
|
|
2189
|
+
}
|
|
2190
|
+
if (step.transform) {
|
|
2191
|
+
}
|
|
2192
|
+
if (step.condition) {
|
|
2193
|
+
checkStep({ id: "__cond_expr", name: "", type: "transform", transform: { expression: "" } }, pathPrefix);
|
|
2194
|
+
step.condition.then.forEach((s, i) => checkStep(s, `${pathPrefix}.condition.then[${i}]`));
|
|
2195
|
+
step.condition.else?.forEach((s, i) => checkStep(s, `${pathPrefix}.condition.else[${i}]`));
|
|
2196
|
+
}
|
|
2197
|
+
if (step.loop) {
|
|
2198
|
+
checkSelectors(extractSelectors(step.loop.over), `${pathPrefix}.loop.over`);
|
|
2199
|
+
step.loop.steps.forEach((s, i) => checkStep(s, `${pathPrefix}.loop.steps[${i}]`));
|
|
2200
|
+
}
|
|
2201
|
+
if (step.parallel) {
|
|
2202
|
+
step.parallel.steps.forEach((s, i) => checkStep(s, `${pathPrefix}.parallel.steps[${i}]`));
|
|
2203
|
+
}
|
|
2204
|
+
if (step.fileRead) {
|
|
2205
|
+
checkSelectors(extractSelectors(step.fileRead.path), `${pathPrefix}.fileRead.path`);
|
|
2206
|
+
}
|
|
2207
|
+
if (step.fileWrite) {
|
|
2208
|
+
checkSelectors(extractSelectors(step.fileWrite.path), `${pathPrefix}.fileWrite.path`);
|
|
2209
|
+
checkSelectors(extractSelectors(step.fileWrite.content), `${pathPrefix}.fileWrite.content`);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
flow2.steps.forEach((step, i) => checkStep(step, `steps[${i}]`));
|
|
2213
|
+
return errors;
|
|
2214
|
+
}
|
|
2215
|
+
function validateFlow(flow2) {
|
|
2216
|
+
const schemaErrors = validateFlowSchema(flow2);
|
|
2217
|
+
if (schemaErrors.length > 0) return schemaErrors;
|
|
2218
|
+
const f = flow2;
|
|
2219
|
+
return [
|
|
2220
|
+
...validateStepIds(f),
|
|
2221
|
+
...validateSelectorReferences(f)
|
|
2222
|
+
];
|
|
2223
|
+
}
|
|
1917
2224
|
|
|
1918
|
-
|
|
1919
|
-
|
|
2225
|
+
// src/lib/flow-runner.ts
|
|
2226
|
+
import fs4 from "fs";
|
|
2227
|
+
import path4 from "path";
|
|
2228
|
+
import crypto from "crypto";
|
|
1920
2229
|
|
|
1921
|
-
|
|
1922
|
-
|
|
2230
|
+
// src/lib/flow-engine.ts
|
|
2231
|
+
import fs3 from "fs";
|
|
2232
|
+
import path3 from "path";
|
|
2233
|
+
function sleep2(ms) {
|
|
2234
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2235
|
+
}
|
|
2236
|
+
function resolveSelector(selectorPath, context) {
|
|
2237
|
+
if (!selectorPath.startsWith("$.")) return selectorPath;
|
|
2238
|
+
const parts = selectorPath.slice(2).split(/\.|\[/).map((p7) => p7.replace(/\]$/, ""));
|
|
2239
|
+
let current = context;
|
|
2240
|
+
for (const part of parts) {
|
|
2241
|
+
if (current === null || current === void 0) return void 0;
|
|
2242
|
+
if (part === "*" && Array.isArray(current)) {
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
if (Array.isArray(current) && part === "*") {
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
if (Array.isArray(current)) {
|
|
2249
|
+
const idx = Number(part);
|
|
2250
|
+
if (!isNaN(idx)) {
|
|
2251
|
+
current = current[idx];
|
|
2252
|
+
} else {
|
|
2253
|
+
current = current.map((item) => item?.[part]);
|
|
2254
|
+
}
|
|
2255
|
+
} else if (typeof current === "object") {
|
|
2256
|
+
current = current[part];
|
|
2257
|
+
} else {
|
|
2258
|
+
return void 0;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
return current;
|
|
2262
|
+
}
|
|
2263
|
+
function interpolateString(str, context) {
|
|
2264
|
+
return str.replace(/\{\{(\$\.[^}]+)\}\}/g, (_match, selector) => {
|
|
2265
|
+
const value = resolveSelector(selector, context);
|
|
2266
|
+
if (value === void 0 || value === null) return "";
|
|
2267
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
2268
|
+
return String(value);
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
function resolveValue(value, context) {
|
|
2272
|
+
if (typeof value === "string") {
|
|
2273
|
+
if (value.startsWith("$.") && !value.includes("{{")) {
|
|
2274
|
+
return resolveSelector(value, context);
|
|
2275
|
+
}
|
|
2276
|
+
if (value.includes("{{$.")) {
|
|
2277
|
+
return interpolateString(value, context);
|
|
2278
|
+
}
|
|
2279
|
+
return value;
|
|
2280
|
+
}
|
|
2281
|
+
if (Array.isArray(value)) {
|
|
2282
|
+
return value.map((item) => resolveValue(item, context));
|
|
2283
|
+
}
|
|
2284
|
+
if (value && typeof value === "object") {
|
|
2285
|
+
const resolved = {};
|
|
2286
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2287
|
+
resolved[k] = resolveValue(v, context);
|
|
2288
|
+
}
|
|
2289
|
+
return resolved;
|
|
2290
|
+
}
|
|
2291
|
+
return value;
|
|
2292
|
+
}
|
|
2293
|
+
function evaluateExpression(expr, context) {
|
|
2294
|
+
const fn = new Function("$", `return (${expr})`);
|
|
2295
|
+
return fn(context);
|
|
2296
|
+
}
|
|
2297
|
+
async function executeActionStep(step, context, api, permissions, allowedActionIds) {
|
|
2298
|
+
const action = step.action;
|
|
2299
|
+
const platform = resolveValue(action.platform, context);
|
|
2300
|
+
const actionId = resolveValue(action.actionId, context);
|
|
2301
|
+
const connectionKey = resolveValue(action.connectionKey, context);
|
|
2302
|
+
const data = action.data ? resolveValue(action.data, context) : void 0;
|
|
2303
|
+
const pathVars = action.pathVars ? resolveValue(action.pathVars, context) : void 0;
|
|
2304
|
+
const queryParams = action.queryParams ? resolveValue(action.queryParams, context) : void 0;
|
|
2305
|
+
const headers = action.headers ? resolveValue(action.headers, context) : void 0;
|
|
2306
|
+
if (!isActionAllowed(actionId, allowedActionIds)) {
|
|
2307
|
+
throw new Error(`Action "${actionId}" is not in the allowed action list`);
|
|
2308
|
+
}
|
|
2309
|
+
const actionDetails = await api.getActionDetails(actionId);
|
|
2310
|
+
if (!isMethodAllowed(actionDetails.method, permissions)) {
|
|
2311
|
+
throw new Error(`Method "${actionDetails.method}" is not allowed under "${permissions}" permission level`);
|
|
2312
|
+
}
|
|
2313
|
+
const result = await api.executePassthroughRequest({
|
|
2314
|
+
platform,
|
|
2315
|
+
actionId,
|
|
2316
|
+
connectionKey,
|
|
2317
|
+
data,
|
|
2318
|
+
pathVariables: pathVars,
|
|
2319
|
+
queryParams,
|
|
2320
|
+
headers
|
|
2321
|
+
}, actionDetails);
|
|
2322
|
+
return {
|
|
2323
|
+
status: "success",
|
|
2324
|
+
response: result.responseData,
|
|
2325
|
+
output: result.responseData
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
function executeTransformStep(step, context) {
|
|
2329
|
+
const output = evaluateExpression(step.transform.expression, context);
|
|
2330
|
+
return { status: "success", output, response: output };
|
|
2331
|
+
}
|
|
2332
|
+
async function executeCodeStep(step, context) {
|
|
2333
|
+
const source = step.code.source;
|
|
2334
|
+
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
2335
|
+
}).constructor;
|
|
2336
|
+
const fn = new AsyncFunction("$", source);
|
|
2337
|
+
const output = await fn(context);
|
|
2338
|
+
return { status: "success", output, response: output };
|
|
2339
|
+
}
|
|
2340
|
+
async function executeConditionStep(step, context, api, permissions, allowedActionIds, options) {
|
|
2341
|
+
const condition = step.condition;
|
|
2342
|
+
const result = evaluateExpression(condition.expression, context);
|
|
2343
|
+
const branch = result ? condition.then : condition.else || [];
|
|
2344
|
+
const branchResults = await executeSteps(branch, context, api, permissions, allowedActionIds, options);
|
|
2345
|
+
return {
|
|
2346
|
+
status: "success",
|
|
2347
|
+
output: { conditionResult: !!result, stepsExecuted: branchResults },
|
|
2348
|
+
response: { conditionResult: !!result }
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
async function executeLoopStep(step, context, api, permissions, allowedActionIds, options) {
|
|
2352
|
+
const loop = step.loop;
|
|
2353
|
+
const items = resolveValue(loop.over, context);
|
|
2354
|
+
if (!Array.isArray(items)) {
|
|
2355
|
+
throw new Error(`Loop "over" must resolve to an array, got ${typeof items}`);
|
|
2356
|
+
}
|
|
2357
|
+
const maxIterations = loop.maxIterations || 1e3;
|
|
2358
|
+
const results = [];
|
|
2359
|
+
const savedLoop = { ...context.loop };
|
|
2360
|
+
for (let i = 0; i < Math.min(items.length, maxIterations); i++) {
|
|
2361
|
+
context.loop = {
|
|
2362
|
+
[loop.as]: items[i],
|
|
2363
|
+
item: items[i],
|
|
2364
|
+
i
|
|
2365
|
+
};
|
|
2366
|
+
if (loop.indexAs) {
|
|
2367
|
+
context.loop[loop.indexAs] = i;
|
|
2368
|
+
}
|
|
2369
|
+
await executeSteps(loop.steps, context, api, permissions, allowedActionIds, options);
|
|
2370
|
+
results.push(context.loop[loop.as]);
|
|
2371
|
+
}
|
|
2372
|
+
context.loop = savedLoop;
|
|
2373
|
+
return { status: "success", output: results, response: results };
|
|
2374
|
+
}
|
|
2375
|
+
async function executeParallelStep(step, context, api, permissions, allowedActionIds, options) {
|
|
2376
|
+
const parallel = step.parallel;
|
|
2377
|
+
const maxConcurrency = parallel.maxConcurrency || 5;
|
|
2378
|
+
const steps = parallel.steps;
|
|
2379
|
+
const results = [];
|
|
2380
|
+
for (let i = 0; i < steps.length; i += maxConcurrency) {
|
|
2381
|
+
const batch = steps.slice(i, i + maxConcurrency);
|
|
2382
|
+
const batchResults = await Promise.all(
|
|
2383
|
+
batch.map((s) => executeSingleStep(s, context, api, permissions, allowedActionIds, options))
|
|
2384
|
+
);
|
|
2385
|
+
results.push(...batchResults);
|
|
2386
|
+
}
|
|
2387
|
+
return { status: "success", output: results, response: results };
|
|
2388
|
+
}
|
|
2389
|
+
function executeFileReadStep(step, context) {
|
|
2390
|
+
const config = step.fileRead;
|
|
2391
|
+
const filePath = resolveValue(config.path, context);
|
|
2392
|
+
const resolvedPath = path3.resolve(filePath);
|
|
2393
|
+
const content = fs3.readFileSync(resolvedPath, "utf-8");
|
|
2394
|
+
const output = config.parseJson ? JSON.parse(content) : content;
|
|
2395
|
+
return { status: "success", output, response: output };
|
|
2396
|
+
}
|
|
2397
|
+
function executeFileWriteStep(step, context) {
|
|
2398
|
+
const config = step.fileWrite;
|
|
2399
|
+
const filePath = resolveValue(config.path, context);
|
|
2400
|
+
const content = resolveValue(config.content, context);
|
|
2401
|
+
const resolvedPath = path3.resolve(filePath);
|
|
2402
|
+
const dir = path3.dirname(resolvedPath);
|
|
2403
|
+
if (!fs3.existsSync(dir)) {
|
|
2404
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
2405
|
+
}
|
|
2406
|
+
const stringContent = typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
2407
|
+
if (config.append) {
|
|
2408
|
+
fs3.appendFileSync(resolvedPath, stringContent);
|
|
2409
|
+
} else {
|
|
2410
|
+
fs3.writeFileSync(resolvedPath, stringContent);
|
|
2411
|
+
}
|
|
2412
|
+
return { status: "success", output: { path: resolvedPath, bytesWritten: stringContent.length }, response: { path: resolvedPath } };
|
|
2413
|
+
}
|
|
2414
|
+
async function executeSingleStep(step, context, api, permissions, allowedActionIds, options) {
|
|
2415
|
+
if (step.if) {
|
|
2416
|
+
const condResult = evaluateExpression(step.if, context);
|
|
2417
|
+
if (!condResult) {
|
|
2418
|
+
const result = { status: "skipped" };
|
|
2419
|
+
context.steps[step.id] = result;
|
|
2420
|
+
return result;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
if (step.unless) {
|
|
2424
|
+
const condResult = evaluateExpression(step.unless, context);
|
|
2425
|
+
if (condResult) {
|
|
2426
|
+
const result = { status: "skipped" };
|
|
2427
|
+
context.steps[step.id] = result;
|
|
2428
|
+
return result;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
const startTime = Date.now();
|
|
2432
|
+
let lastError;
|
|
2433
|
+
const maxAttempts = step.onError?.strategy === "retry" && step.onError.retries ? step.onError.retries + 1 : 1;
|
|
2434
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
2435
|
+
try {
|
|
2436
|
+
if (attempt > 1) {
|
|
2437
|
+
options.onEvent?.({
|
|
2438
|
+
event: "step:retry",
|
|
2439
|
+
stepId: step.id,
|
|
2440
|
+
attempt,
|
|
2441
|
+
maxRetries: step.onError.retries
|
|
2442
|
+
});
|
|
2443
|
+
const delay = step.onError?.retryDelayMs || 1e3;
|
|
2444
|
+
await sleep2(delay);
|
|
2445
|
+
}
|
|
2446
|
+
let result;
|
|
2447
|
+
switch (step.type) {
|
|
2448
|
+
case "action":
|
|
2449
|
+
result = await executeActionStep(step, context, api, permissions, allowedActionIds);
|
|
2450
|
+
break;
|
|
2451
|
+
case "transform":
|
|
2452
|
+
result = executeTransformStep(step, context);
|
|
2453
|
+
break;
|
|
2454
|
+
case "code":
|
|
2455
|
+
result = await executeCodeStep(step, context);
|
|
2456
|
+
break;
|
|
2457
|
+
case "condition":
|
|
2458
|
+
result = await executeConditionStep(step, context, api, permissions, allowedActionIds, options);
|
|
2459
|
+
break;
|
|
2460
|
+
case "loop":
|
|
2461
|
+
result = await executeLoopStep(step, context, api, permissions, allowedActionIds, options);
|
|
2462
|
+
break;
|
|
2463
|
+
case "parallel":
|
|
2464
|
+
result = await executeParallelStep(step, context, api, permissions, allowedActionIds, options);
|
|
2465
|
+
break;
|
|
2466
|
+
case "file-read":
|
|
2467
|
+
result = executeFileReadStep(step, context);
|
|
2468
|
+
break;
|
|
2469
|
+
case "file-write":
|
|
2470
|
+
result = executeFileWriteStep(step, context);
|
|
2471
|
+
break;
|
|
2472
|
+
default:
|
|
2473
|
+
throw new Error(`Unknown step type: ${step.type}`);
|
|
2474
|
+
}
|
|
2475
|
+
result.durationMs = Date.now() - startTime;
|
|
2476
|
+
if (attempt > 1) result.retries = attempt - 1;
|
|
2477
|
+
context.steps[step.id] = result;
|
|
2478
|
+
return result;
|
|
2479
|
+
} catch (err) {
|
|
2480
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2481
|
+
if (attempt === maxAttempts) {
|
|
2482
|
+
break;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
const errorMessage = lastError?.message || "Unknown error";
|
|
2487
|
+
const strategy = step.onError?.strategy || "fail";
|
|
2488
|
+
if (strategy === "continue") {
|
|
2489
|
+
const result = {
|
|
2490
|
+
status: "failed",
|
|
2491
|
+
error: errorMessage,
|
|
2492
|
+
durationMs: Date.now() - startTime
|
|
2493
|
+
};
|
|
2494
|
+
context.steps[step.id] = result;
|
|
2495
|
+
return result;
|
|
2496
|
+
}
|
|
2497
|
+
if (strategy === "fallback" && step.onError?.fallbackStepId) {
|
|
2498
|
+
const result = {
|
|
2499
|
+
status: "failed",
|
|
2500
|
+
error: errorMessage,
|
|
2501
|
+
durationMs: Date.now() - startTime
|
|
2502
|
+
};
|
|
2503
|
+
context.steps[step.id] = result;
|
|
2504
|
+
return result;
|
|
2505
|
+
}
|
|
2506
|
+
throw lastError;
|
|
2507
|
+
}
|
|
2508
|
+
async function executeSteps(steps, context, api, permissions, allowedActionIds, options, completedStepIds) {
|
|
2509
|
+
const results = [];
|
|
2510
|
+
for (const step of steps) {
|
|
2511
|
+
if (completedStepIds?.has(step.id)) {
|
|
2512
|
+
results.push(context.steps[step.id] || { status: "success" });
|
|
2513
|
+
continue;
|
|
2514
|
+
}
|
|
2515
|
+
options.onEvent?.({
|
|
2516
|
+
event: "step:start",
|
|
2517
|
+
stepId: step.id,
|
|
2518
|
+
stepName: step.name,
|
|
2519
|
+
type: step.type,
|
|
2520
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2521
|
+
});
|
|
2522
|
+
try {
|
|
2523
|
+
const result = await executeSingleStep(step, context, api, permissions, allowedActionIds, options);
|
|
2524
|
+
results.push(result);
|
|
2525
|
+
options.onEvent?.({
|
|
2526
|
+
event: "step:complete",
|
|
2527
|
+
stepId: step.id,
|
|
2528
|
+
status: result.status,
|
|
2529
|
+
durationMs: result.durationMs,
|
|
2530
|
+
retries: result.retries
|
|
2531
|
+
});
|
|
2532
|
+
} catch (error2) {
|
|
2533
|
+
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
2534
|
+
options.onEvent?.({
|
|
2535
|
+
event: "step:error",
|
|
2536
|
+
stepId: step.id,
|
|
2537
|
+
error: errorMsg,
|
|
2538
|
+
strategy: step.onError?.strategy || "fail"
|
|
2539
|
+
});
|
|
2540
|
+
throw error2;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return results;
|
|
2544
|
+
}
|
|
2545
|
+
async function executeFlow(flow2, inputs, api, permissions, allowedActionIds, options = {}, resumeState) {
|
|
2546
|
+
for (const [name, decl] of Object.entries(flow2.inputs)) {
|
|
2547
|
+
if (decl.required !== false && inputs[name] === void 0 && decl.default === void 0) {
|
|
2548
|
+
throw new Error(`Missing required input: "${name}" \u2014 ${decl.description || ""}`);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
const resolvedInputs = {};
|
|
2552
|
+
for (const [name, decl] of Object.entries(flow2.inputs)) {
|
|
2553
|
+
if (inputs[name] !== void 0) {
|
|
2554
|
+
resolvedInputs[name] = inputs[name];
|
|
2555
|
+
} else if (decl.default !== void 0) {
|
|
2556
|
+
resolvedInputs[name] = decl.default;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
const context = resumeState?.context || {
|
|
2560
|
+
input: resolvedInputs,
|
|
2561
|
+
env: process.env,
|
|
2562
|
+
steps: {},
|
|
2563
|
+
loop: {}
|
|
2564
|
+
};
|
|
2565
|
+
const completedStepIds = resumeState ? new Set(resumeState.completedSteps) : void 0;
|
|
2566
|
+
if (options.dryRun) {
|
|
2567
|
+
options.onEvent?.({
|
|
2568
|
+
event: "flow:dry-run",
|
|
2569
|
+
flowKey: flow2.key,
|
|
2570
|
+
resolvedInputs,
|
|
2571
|
+
steps: flow2.steps.map((s) => ({ id: s.id, name: s.name, type: s.type })),
|
|
2572
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2573
|
+
});
|
|
2574
|
+
return context;
|
|
2575
|
+
}
|
|
2576
|
+
options.onEvent?.({
|
|
2577
|
+
event: "flow:start",
|
|
2578
|
+
flowKey: flow2.key,
|
|
2579
|
+
totalSteps: flow2.steps.length,
|
|
2580
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2581
|
+
});
|
|
2582
|
+
const flowStart = Date.now();
|
|
2583
|
+
try {
|
|
2584
|
+
await executeSteps(flow2.steps, context, api, permissions, allowedActionIds, options, completedStepIds);
|
|
2585
|
+
const stepEntries = Object.values(context.steps);
|
|
2586
|
+
const completed = stepEntries.filter((s) => s.status === "success").length;
|
|
2587
|
+
const failed = stepEntries.filter((s) => s.status === "failed").length;
|
|
2588
|
+
const skipped = stepEntries.filter((s) => s.status === "skipped").length;
|
|
2589
|
+
options.onEvent?.({
|
|
2590
|
+
event: "flow:complete",
|
|
2591
|
+
flowKey: flow2.key,
|
|
2592
|
+
status: "success",
|
|
2593
|
+
durationMs: Date.now() - flowStart,
|
|
2594
|
+
stepsCompleted: completed,
|
|
2595
|
+
stepsFailed: failed,
|
|
2596
|
+
stepsSkipped: skipped
|
|
2597
|
+
});
|
|
2598
|
+
} catch (error2) {
|
|
2599
|
+
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
2600
|
+
options.onEvent?.({
|
|
2601
|
+
event: "flow:error",
|
|
2602
|
+
flowKey: flow2.key,
|
|
2603
|
+
status: "failed",
|
|
2604
|
+
error: errorMsg,
|
|
2605
|
+
durationMs: Date.now() - flowStart
|
|
2606
|
+
});
|
|
2607
|
+
throw error2;
|
|
2608
|
+
}
|
|
2609
|
+
return context;
|
|
2610
|
+
}
|
|
1923
2611
|
|
|
1924
|
-
|
|
1925
|
-
|
|
2612
|
+
// src/lib/flow-runner.ts
|
|
2613
|
+
var FLOWS_DIR = ".one/flows";
|
|
2614
|
+
var RUNS_DIR = ".one/flows/.runs";
|
|
2615
|
+
var LOGS_DIR = ".one/flows/.logs";
|
|
2616
|
+
function ensureDir(dir) {
|
|
2617
|
+
if (!fs4.existsSync(dir)) {
|
|
2618
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
function generateRunId() {
|
|
2622
|
+
return crypto.randomBytes(6).toString("hex");
|
|
2623
|
+
}
|
|
2624
|
+
var FlowRunner = class _FlowRunner {
|
|
2625
|
+
runId;
|
|
2626
|
+
flowKey;
|
|
2627
|
+
state;
|
|
2628
|
+
logPath;
|
|
2629
|
+
statePath;
|
|
2630
|
+
paused = false;
|
|
2631
|
+
constructor(flow2, inputs, runId) {
|
|
2632
|
+
this.runId = runId || generateRunId();
|
|
2633
|
+
this.flowKey = flow2.key;
|
|
2634
|
+
ensureDir(RUNS_DIR);
|
|
2635
|
+
ensureDir(LOGS_DIR);
|
|
2636
|
+
this.statePath = path4.join(RUNS_DIR, `${flow2.key}-${this.runId}.state.json`);
|
|
2637
|
+
this.logPath = path4.join(LOGS_DIR, `${flow2.key}-${this.runId}.log`);
|
|
2638
|
+
this.state = {
|
|
2639
|
+
runId: this.runId,
|
|
2640
|
+
flowKey: flow2.key,
|
|
2641
|
+
status: "running",
|
|
2642
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2643
|
+
inputs,
|
|
2644
|
+
completedSteps: [],
|
|
2645
|
+
context: {
|
|
2646
|
+
input: inputs,
|
|
2647
|
+
env: {},
|
|
2648
|
+
steps: {},
|
|
2649
|
+
loop: {}
|
|
2650
|
+
}
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
getRunId() {
|
|
2654
|
+
return this.runId;
|
|
2655
|
+
}
|
|
2656
|
+
getLogPath() {
|
|
2657
|
+
return this.logPath;
|
|
2658
|
+
}
|
|
2659
|
+
getStatePath() {
|
|
2660
|
+
return this.statePath;
|
|
2661
|
+
}
|
|
2662
|
+
requestPause() {
|
|
2663
|
+
this.paused = true;
|
|
2664
|
+
}
|
|
2665
|
+
log(level, msg, data) {
|
|
2666
|
+
const entry = {
|
|
2667
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2668
|
+
level,
|
|
2669
|
+
msg,
|
|
2670
|
+
...data
|
|
2671
|
+
};
|
|
2672
|
+
fs4.appendFileSync(this.logPath, JSON.stringify(entry) + "\n");
|
|
2673
|
+
}
|
|
2674
|
+
saveState() {
|
|
2675
|
+
fs4.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2));
|
|
2676
|
+
}
|
|
2677
|
+
createEventHandler(externalHandler) {
|
|
2678
|
+
return (event) => {
|
|
2679
|
+
this.log(
|
|
2680
|
+
event.event.includes("error") ? "warn" : "info",
|
|
2681
|
+
event.event,
|
|
2682
|
+
event
|
|
2683
|
+
);
|
|
2684
|
+
if (event.event === "step:complete" && event.stepId) {
|
|
2685
|
+
this.state.completedSteps.push(event.stepId);
|
|
2686
|
+
this.state.currentStepId = void 0;
|
|
2687
|
+
}
|
|
2688
|
+
if (event.event === "step:start" && event.stepId) {
|
|
2689
|
+
this.state.currentStepId = event.stepId;
|
|
2690
|
+
}
|
|
2691
|
+
externalHandler?.(event);
|
|
2692
|
+
if (this.paused && event.event === "step:complete") {
|
|
2693
|
+
this.state.status = "paused";
|
|
2694
|
+
this.state.pausedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2695
|
+
this.saveState();
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
async execute(flow2, api, permissions, allowedActionIds, options = {}) {
|
|
2700
|
+
this.log("info", "Flow started", { flowKey: this.flowKey, runId: this.runId });
|
|
2701
|
+
this.state.status = "running";
|
|
2702
|
+
this.saveState();
|
|
2703
|
+
const eventHandler = this.createEventHandler(options.onEvent);
|
|
2704
|
+
try {
|
|
2705
|
+
const context = await executeFlow(
|
|
2706
|
+
flow2,
|
|
2707
|
+
this.state.inputs,
|
|
2708
|
+
api,
|
|
2709
|
+
permissions,
|
|
2710
|
+
allowedActionIds,
|
|
2711
|
+
{ ...options, onEvent: eventHandler }
|
|
2712
|
+
);
|
|
2713
|
+
this.state.status = "completed";
|
|
2714
|
+
this.state.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2715
|
+
this.state.context = context;
|
|
2716
|
+
this.saveState();
|
|
2717
|
+
this.log("info", "Flow completed", { status: "success" });
|
|
2718
|
+
return context;
|
|
2719
|
+
} catch (error2) {
|
|
2720
|
+
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
2721
|
+
this.state.status = "failed";
|
|
2722
|
+
this.state.context.steps = this.state.context.steps || {};
|
|
2723
|
+
this.saveState();
|
|
2724
|
+
this.log("error", "Flow failed", { error: errorMsg });
|
|
2725
|
+
throw error2;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
async resume(flow2, api, permissions, allowedActionIds, options = {}) {
|
|
2729
|
+
this.log("info", "Flow resumed", { flowKey: this.flowKey, runId: this.runId });
|
|
2730
|
+
this.state.status = "running";
|
|
2731
|
+
this.state.pausedAt = void 0;
|
|
2732
|
+
this.saveState();
|
|
2733
|
+
const eventHandler = this.createEventHandler(options.onEvent);
|
|
2734
|
+
try {
|
|
2735
|
+
const context = await executeFlow(
|
|
2736
|
+
flow2,
|
|
2737
|
+
this.state.inputs,
|
|
2738
|
+
api,
|
|
2739
|
+
permissions,
|
|
2740
|
+
allowedActionIds,
|
|
2741
|
+
{ ...options, onEvent: eventHandler },
|
|
2742
|
+
{
|
|
2743
|
+
context: this.state.context,
|
|
2744
|
+
completedSteps: this.state.completedSteps
|
|
2745
|
+
}
|
|
2746
|
+
);
|
|
2747
|
+
this.state.status = "completed";
|
|
2748
|
+
this.state.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2749
|
+
this.state.context = context;
|
|
2750
|
+
this.saveState();
|
|
2751
|
+
this.log("info", "Flow completed after resume", { status: "success" });
|
|
2752
|
+
return context;
|
|
2753
|
+
} catch (error2) {
|
|
2754
|
+
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
2755
|
+
this.state.status = "failed";
|
|
2756
|
+
this.saveState();
|
|
2757
|
+
this.log("error", "Flow failed after resume", { error: errorMsg });
|
|
2758
|
+
throw error2;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
static loadRunState(runId) {
|
|
2762
|
+
ensureDir(RUNS_DIR);
|
|
2763
|
+
const files = fs4.readdirSync(RUNS_DIR).filter((f) => f.includes(runId) && f.endsWith(".state.json"));
|
|
2764
|
+
if (files.length === 0) return null;
|
|
2765
|
+
try {
|
|
2766
|
+
const content = fs4.readFileSync(path4.join(RUNS_DIR, files[0]), "utf-8");
|
|
2767
|
+
return JSON.parse(content);
|
|
2768
|
+
} catch {
|
|
2769
|
+
return null;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
static fromRunState(state) {
|
|
2773
|
+
const runner = Object.create(_FlowRunner.prototype);
|
|
2774
|
+
runner.runId = state.runId;
|
|
2775
|
+
runner.flowKey = state.flowKey;
|
|
2776
|
+
runner.state = state;
|
|
2777
|
+
runner.paused = false;
|
|
2778
|
+
runner.statePath = path4.join(RUNS_DIR, `${state.flowKey}-${state.runId}.state.json`);
|
|
2779
|
+
runner.logPath = path4.join(LOGS_DIR, `${state.flowKey}-${state.runId}.log`);
|
|
2780
|
+
return runner;
|
|
2781
|
+
}
|
|
2782
|
+
static listRuns(flowKey) {
|
|
2783
|
+
ensureDir(RUNS_DIR);
|
|
2784
|
+
const files = fs4.readdirSync(RUNS_DIR).filter((f) => f.endsWith(".state.json"));
|
|
2785
|
+
const runs = [];
|
|
2786
|
+
for (const file of files) {
|
|
2787
|
+
try {
|
|
2788
|
+
const content = fs4.readFileSync(path4.join(RUNS_DIR, file), "utf-8");
|
|
2789
|
+
const state = JSON.parse(content);
|
|
2790
|
+
if (!flowKey || state.flowKey === flowKey) {
|
|
2791
|
+
runs.push(state);
|
|
2792
|
+
}
|
|
2793
|
+
} catch {
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return runs.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
function resolveFlowPath(keyOrPath) {
|
|
2800
|
+
if (keyOrPath.includes("/") || keyOrPath.includes("\\") || keyOrPath.endsWith(".json")) {
|
|
2801
|
+
return path4.resolve(keyOrPath);
|
|
2802
|
+
}
|
|
2803
|
+
return path4.resolve(FLOWS_DIR, `${keyOrPath}.flow.json`);
|
|
2804
|
+
}
|
|
2805
|
+
function loadFlow(keyOrPath) {
|
|
2806
|
+
const flowPath = resolveFlowPath(keyOrPath);
|
|
2807
|
+
if (!fs4.existsSync(flowPath)) {
|
|
2808
|
+
throw new Error(`Flow not found: ${flowPath}`);
|
|
2809
|
+
}
|
|
2810
|
+
const content = fs4.readFileSync(flowPath, "utf-8");
|
|
2811
|
+
return JSON.parse(content);
|
|
2812
|
+
}
|
|
2813
|
+
function listFlows() {
|
|
2814
|
+
const flowsDir = path4.resolve(FLOWS_DIR);
|
|
2815
|
+
if (!fs4.existsSync(flowsDir)) return [];
|
|
2816
|
+
const files = fs4.readdirSync(flowsDir).filter((f) => f.endsWith(".flow.json"));
|
|
2817
|
+
const flows = [];
|
|
2818
|
+
for (const file of files) {
|
|
2819
|
+
try {
|
|
2820
|
+
const content = fs4.readFileSync(path4.join(flowsDir, file), "utf-8");
|
|
2821
|
+
const flow2 = JSON.parse(content);
|
|
2822
|
+
flows.push({
|
|
2823
|
+
key: flow2.key,
|
|
2824
|
+
name: flow2.name,
|
|
2825
|
+
description: flow2.description,
|
|
2826
|
+
inputCount: Object.keys(flow2.inputs).length,
|
|
2827
|
+
stepCount: flow2.steps.length,
|
|
2828
|
+
path: path4.join(flowsDir, file)
|
|
2829
|
+
});
|
|
2830
|
+
} catch {
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
return flows;
|
|
2834
|
+
}
|
|
2835
|
+
function saveFlow(flow2, outputPath) {
|
|
2836
|
+
const flowPath = outputPath ? path4.resolve(outputPath) : path4.resolve(FLOWS_DIR, `${flow2.key}.flow.json`);
|
|
2837
|
+
const dir = path4.dirname(flowPath);
|
|
2838
|
+
ensureDir(dir);
|
|
2839
|
+
fs4.writeFileSync(flowPath, JSON.stringify(flow2, null, 2) + "\n");
|
|
2840
|
+
return flowPath;
|
|
2841
|
+
}
|
|
1926
2842
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
const
|
|
1931
|
-
if (
|
|
1932
|
-
|
|
2843
|
+
// src/commands/flow.ts
|
|
2844
|
+
import fs5 from "fs";
|
|
2845
|
+
function getConfig2() {
|
|
2846
|
+
const apiKey = getApiKey();
|
|
2847
|
+
if (!apiKey) {
|
|
2848
|
+
error("Not configured. Run `one init` first.");
|
|
1933
2849
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2850
|
+
const ac = getAccessControlFromAllSources();
|
|
2851
|
+
const permissions = ac.permissions || "admin";
|
|
2852
|
+
const connectionKeys = ac.connectionKeys || ["*"];
|
|
2853
|
+
const actionIds = ac.actionIds || ["*"];
|
|
2854
|
+
return { apiKey, permissions, connectionKeys, actionIds };
|
|
2855
|
+
}
|
|
2856
|
+
function parseInputs(inputArgs) {
|
|
2857
|
+
const inputs = {};
|
|
2858
|
+
for (const arg of inputArgs) {
|
|
2859
|
+
const eqIndex = arg.indexOf("=");
|
|
2860
|
+
if (eqIndex === -1) {
|
|
2861
|
+
error(`Invalid input format: "${arg}" \u2014 expected name=value`);
|
|
2862
|
+
}
|
|
2863
|
+
const key = arg.slice(0, eqIndex);
|
|
2864
|
+
const value = arg.slice(eqIndex + 1);
|
|
2865
|
+
try {
|
|
2866
|
+
inputs[key] = JSON.parse(value);
|
|
2867
|
+
} catch {
|
|
2868
|
+
inputs[key] = value;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
return inputs;
|
|
2872
|
+
}
|
|
2873
|
+
function collect(value, previous) {
|
|
2874
|
+
return previous.concat([value]);
|
|
2875
|
+
}
|
|
2876
|
+
async function autoResolveConnectionInputs(flow2, inputs, api) {
|
|
2877
|
+
const resolved = { ...inputs };
|
|
2878
|
+
const connectionInputs = Object.entries(flow2.inputs).filter(
|
|
2879
|
+
([, decl]) => decl.connection && !resolved[decl.connection.platform]
|
|
2880
|
+
);
|
|
2881
|
+
if (connectionInputs.length === 0) return resolved;
|
|
2882
|
+
const missing = connectionInputs.filter(([name]) => !resolved[name]);
|
|
2883
|
+
if (missing.length === 0) return resolved;
|
|
2884
|
+
const connections = await api.listConnections();
|
|
2885
|
+
for (const [name, decl] of missing) {
|
|
2886
|
+
const platform = decl.connection.platform;
|
|
2887
|
+
const matching = connections.filter((c) => c.platform.toLowerCase() === platform.toLowerCase());
|
|
2888
|
+
if (matching.length === 1) {
|
|
2889
|
+
resolved[name] = matching[0].key;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
return resolved;
|
|
2893
|
+
}
|
|
2894
|
+
async function flowCreateCommand(key, options) {
|
|
2895
|
+
intro2(pc7.bgCyan(pc7.black(" One Flow ")));
|
|
2896
|
+
let flow2;
|
|
2897
|
+
if (options.definition) {
|
|
2898
|
+
try {
|
|
2899
|
+
flow2 = JSON.parse(options.definition);
|
|
2900
|
+
} catch {
|
|
2901
|
+
error("Invalid JSON in --definition");
|
|
2902
|
+
}
|
|
2903
|
+
} else if (!process.stdin.isTTY) {
|
|
2904
|
+
const chunks = [];
|
|
2905
|
+
for await (const chunk of process.stdin) {
|
|
2906
|
+
chunks.push(chunk);
|
|
2907
|
+
}
|
|
2908
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
2909
|
+
try {
|
|
2910
|
+
flow2 = JSON.parse(raw);
|
|
2911
|
+
} catch {
|
|
2912
|
+
error("Invalid JSON from stdin");
|
|
2913
|
+
}
|
|
2914
|
+
} else {
|
|
2915
|
+
error("Interactive flow creation not yet supported. Use --definition <json> or pipe JSON via stdin.");
|
|
2916
|
+
}
|
|
2917
|
+
if (key) {
|
|
2918
|
+
flow2.key = key;
|
|
2919
|
+
}
|
|
2920
|
+
const errors = validateFlow(flow2);
|
|
2921
|
+
if (errors.length > 0) {
|
|
2922
|
+
if (isAgentMode()) {
|
|
2923
|
+
json({ error: "Validation failed", errors });
|
|
2924
|
+
process.exit(1);
|
|
2925
|
+
}
|
|
2926
|
+
error(`Validation failed:
|
|
2927
|
+
${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}`);
|
|
2928
|
+
}
|
|
2929
|
+
const flowPath = saveFlow(flow2, options.output);
|
|
2930
|
+
if (isAgentMode()) {
|
|
2931
|
+
json({ created: true, key: flow2.key, path: flowPath });
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
note2(`Flow "${flow2.name}" saved to ${flowPath}`, "Created");
|
|
2935
|
+
outro2(`Validate: ${pc7.cyan(`one flow validate ${flow2.key}`)}
|
|
2936
|
+
Execute: ${pc7.cyan(`one flow execute ${flow2.key}`)}`);
|
|
2937
|
+
}
|
|
2938
|
+
async function flowExecuteCommand(keyOrPath, options) {
|
|
2939
|
+
intro2(pc7.bgCyan(pc7.black(" One Flow ")));
|
|
2940
|
+
const { apiKey, permissions, actionIds } = getConfig2();
|
|
2941
|
+
const api = new OneApi(apiKey);
|
|
2942
|
+
const spinner5 = createSpinner();
|
|
2943
|
+
spinner5.start(`Loading flow "${keyOrPath}"...`);
|
|
2944
|
+
let flow2;
|
|
2945
|
+
try {
|
|
2946
|
+
flow2 = loadFlow(keyOrPath);
|
|
2947
|
+
} catch (err) {
|
|
2948
|
+
spinner5.stop("Flow not found");
|
|
2949
|
+
error(err instanceof Error ? err.message : String(err));
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
spinner5.stop(`Flow: ${flow2.name} (${flow2.steps.length} steps)`);
|
|
2953
|
+
const inputs = parseInputs(options.input || []);
|
|
2954
|
+
const resolvedInputs = await autoResolveConnectionInputs(flow2, inputs, api);
|
|
2955
|
+
const runner = new FlowRunner(flow2, resolvedInputs);
|
|
2956
|
+
const logPath = runner.getLogPath();
|
|
2957
|
+
const runId = runner.getRunId();
|
|
2958
|
+
const sigintHandler = () => {
|
|
2959
|
+
runner.requestPause();
|
|
2960
|
+
if (!isAgentMode()) {
|
|
2961
|
+
console.log(`
|
|
2962
|
+
${pc7.yellow("Pausing after current step completes...")} (run ID: ${runId})`);
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
process.on("SIGINT", sigintHandler);
|
|
2966
|
+
const onEvent = (event) => {
|
|
2967
|
+
if (isAgentMode()) {
|
|
2968
|
+
json(event);
|
|
2969
|
+
} else if (options.verbose) {
|
|
2970
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().split("T")[1].slice(0, 8);
|
|
2971
|
+
if (event.event === "step:start") {
|
|
2972
|
+
console.log(` ${pc7.dim(ts)} ${pc7.cyan("\u25B6")} ${event.stepName} ${pc7.dim(`(${event.type})`)}`);
|
|
2973
|
+
} else if (event.event === "step:complete") {
|
|
2974
|
+
const status = event.status === "success" ? pc7.green("\u2713") : event.status === "skipped" ? pc7.dim("\u25CB") : pc7.red("\u2717");
|
|
2975
|
+
console.log(` ${pc7.dim(ts)} ${status} ${event.stepId} ${pc7.dim(`${event.durationMs}ms`)}`);
|
|
2976
|
+
} else if (event.event === "step:error") {
|
|
2977
|
+
console.log(` ${pc7.dim(ts)} ${pc7.red("\u2717")} ${event.stepId}: ${event.error}`);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
};
|
|
2981
|
+
const execSpinner = createSpinner();
|
|
2982
|
+
if (!options.verbose && !isAgentMode()) {
|
|
2983
|
+
execSpinner.start("Executing flow...");
|
|
2984
|
+
}
|
|
2985
|
+
try {
|
|
2986
|
+
const context = await runner.execute(flow2, api, permissions, actionIds, {
|
|
2987
|
+
dryRun: options.dryRun,
|
|
2988
|
+
verbose: options.verbose,
|
|
2989
|
+
onEvent
|
|
2990
|
+
});
|
|
2991
|
+
process.off("SIGINT", sigintHandler);
|
|
2992
|
+
if (!options.verbose && !isAgentMode()) {
|
|
2993
|
+
execSpinner.stop("Flow completed");
|
|
2994
|
+
}
|
|
2995
|
+
if (isAgentMode()) {
|
|
2996
|
+
json({
|
|
2997
|
+
event: "flow:result",
|
|
2998
|
+
runId,
|
|
2999
|
+
logFile: logPath,
|
|
3000
|
+
status: "success",
|
|
3001
|
+
steps: context.steps
|
|
3002
|
+
});
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
const stepEntries = Object.entries(context.steps);
|
|
3006
|
+
const succeeded = stepEntries.filter(([, r]) => r.status === "success").length;
|
|
3007
|
+
const failed = stepEntries.filter(([, r]) => r.status === "failed").length;
|
|
3008
|
+
const skipped = stepEntries.filter(([, r]) => r.status === "skipped").length;
|
|
3009
|
+
console.log();
|
|
3010
|
+
console.log(` ${pc7.green("\u2713")} ${succeeded} succeeded ${failed > 0 ? pc7.red(`\u2717 ${failed} failed`) : ""} ${skipped > 0 ? pc7.dim(`\u25CB ${skipped} skipped`) : ""}`);
|
|
3011
|
+
console.log(` ${pc7.dim(`Run ID: ${runId}`)}`);
|
|
3012
|
+
console.log(` ${pc7.dim(`Log: ${logPath}`)}`);
|
|
3013
|
+
if (options.dryRun) {
|
|
3014
|
+
note2("Dry run \u2014 no steps were executed", "Dry Run");
|
|
3015
|
+
}
|
|
3016
|
+
} catch (error2) {
|
|
3017
|
+
process.off("SIGINT", sigintHandler);
|
|
3018
|
+
if (!options.verbose && !isAgentMode()) {
|
|
3019
|
+
execSpinner.stop("Flow failed");
|
|
3020
|
+
}
|
|
3021
|
+
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
3022
|
+
if (isAgentMode()) {
|
|
3023
|
+
json({
|
|
3024
|
+
event: "flow:result",
|
|
3025
|
+
runId,
|
|
3026
|
+
logFile: logPath,
|
|
3027
|
+
status: "failed",
|
|
3028
|
+
error: errorMsg
|
|
3029
|
+
});
|
|
3030
|
+
process.exit(1);
|
|
3031
|
+
}
|
|
3032
|
+
console.log(` ${pc7.dim(`Run ID: ${runId}`)}`);
|
|
3033
|
+
console.log(` ${pc7.dim(`Log: ${logPath}`)}`);
|
|
3034
|
+
error(`Flow failed: ${errorMsg}`);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
async function flowListCommand() {
|
|
3038
|
+
intro2(pc7.bgCyan(pc7.black(" One Flow ")));
|
|
3039
|
+
const flows = listFlows();
|
|
3040
|
+
if (isAgentMode()) {
|
|
3041
|
+
json({ flows });
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
if (flows.length === 0) {
|
|
3045
|
+
note2("No flows found in .one/flows/\n\nCreate one with: one flow create", "Flows");
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
console.log();
|
|
3049
|
+
printTable(
|
|
3050
|
+
[
|
|
3051
|
+
{ key: "key", label: "Key" },
|
|
3052
|
+
{ key: "name", label: "Name" },
|
|
3053
|
+
{ key: "description", label: "Description" },
|
|
3054
|
+
{ key: "inputCount", label: "Inputs" },
|
|
3055
|
+
{ key: "stepCount", label: "Steps" }
|
|
3056
|
+
],
|
|
3057
|
+
flows.map((f) => ({
|
|
3058
|
+
key: f.key,
|
|
3059
|
+
name: f.name,
|
|
3060
|
+
description: f.description || "",
|
|
3061
|
+
inputCount: String(f.inputCount),
|
|
3062
|
+
stepCount: String(f.stepCount)
|
|
3063
|
+
}))
|
|
3064
|
+
);
|
|
3065
|
+
console.log();
|
|
3066
|
+
}
|
|
3067
|
+
async function flowValidateCommand(keyOrPath) {
|
|
3068
|
+
intro2(pc7.bgCyan(pc7.black(" One Flow ")));
|
|
3069
|
+
const spinner5 = createSpinner();
|
|
3070
|
+
spinner5.start(`Validating "${keyOrPath}"...`);
|
|
3071
|
+
let flowData;
|
|
3072
|
+
try {
|
|
3073
|
+
const flowPath = resolveFlowPath(keyOrPath);
|
|
3074
|
+
const content = fs5.readFileSync(flowPath, "utf-8");
|
|
3075
|
+
flowData = JSON.parse(content);
|
|
3076
|
+
} catch (err) {
|
|
3077
|
+
spinner5.stop("Validation failed");
|
|
3078
|
+
error(`Could not read flow: ${err instanceof Error ? err.message : String(err)}`);
|
|
3079
|
+
}
|
|
3080
|
+
const errors = validateFlow(flowData);
|
|
3081
|
+
if (errors.length > 0) {
|
|
3082
|
+
spinner5.stop("Validation failed");
|
|
3083
|
+
if (isAgentMode()) {
|
|
3084
|
+
json({ valid: false, errors });
|
|
3085
|
+
process.exit(1);
|
|
3086
|
+
}
|
|
3087
|
+
console.log();
|
|
3088
|
+
for (const e of errors) {
|
|
3089
|
+
console.log(` ${pc7.red("\u2717")} ${pc7.dim(e.path)}: ${e.message}`);
|
|
3090
|
+
}
|
|
3091
|
+
console.log();
|
|
3092
|
+
error(`${errors.length} validation error(s) found`);
|
|
3093
|
+
}
|
|
3094
|
+
spinner5.stop("Flow is valid");
|
|
3095
|
+
if (isAgentMode()) {
|
|
3096
|
+
json({ valid: true, key: flowData.key });
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
note2(`Flow "${flowData.key}" passed all validation checks`, "Valid");
|
|
3100
|
+
}
|
|
3101
|
+
async function flowResumeCommand(runId) {
|
|
3102
|
+
intro2(pc7.bgCyan(pc7.black(" One Flow ")));
|
|
3103
|
+
const state = FlowRunner.loadRunState(runId);
|
|
3104
|
+
if (!state) {
|
|
3105
|
+
error(`Run "${runId}" not found`);
|
|
3106
|
+
}
|
|
3107
|
+
if (state.status !== "paused" && state.status !== "failed") {
|
|
3108
|
+
error(`Run "${runId}" is ${state.status} \u2014 can only resume paused or failed runs`);
|
|
3109
|
+
}
|
|
3110
|
+
const { apiKey, permissions, actionIds } = getConfig2();
|
|
3111
|
+
const api = new OneApi(apiKey);
|
|
3112
|
+
let flow2;
|
|
3113
|
+
try {
|
|
3114
|
+
flow2 = loadFlow(state.flowKey);
|
|
3115
|
+
} catch (err) {
|
|
3116
|
+
error(`Could not load flow "${state.flowKey}": ${err instanceof Error ? err.message : String(err)}`);
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
const runner = FlowRunner.fromRunState(state);
|
|
3120
|
+
const onEvent = (event) => {
|
|
3121
|
+
if (isAgentMode()) {
|
|
3122
|
+
json(event);
|
|
3123
|
+
}
|
|
3124
|
+
};
|
|
3125
|
+
const spinner5 = createSpinner();
|
|
3126
|
+
spinner5.start(`Resuming run ${runId} (${state.completedSteps.length} steps already completed)...`);
|
|
3127
|
+
try {
|
|
3128
|
+
const context = await runner.resume(flow2, api, permissions, actionIds, { onEvent });
|
|
3129
|
+
spinner5.stop("Flow completed");
|
|
3130
|
+
if (isAgentMode()) {
|
|
3131
|
+
json({
|
|
3132
|
+
event: "flow:result",
|
|
3133
|
+
runId,
|
|
3134
|
+
logFile: runner.getLogPath(),
|
|
3135
|
+
status: "success",
|
|
3136
|
+
steps: context.steps
|
|
3137
|
+
});
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
console.log(` ${pc7.green("\u2713")} Resumed and completed successfully`);
|
|
3141
|
+
console.log(` ${pc7.dim(`Log: ${runner.getLogPath()}`)}`);
|
|
3142
|
+
} catch (error2) {
|
|
3143
|
+
spinner5.stop("Resume failed");
|
|
3144
|
+
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
3145
|
+
if (isAgentMode()) {
|
|
3146
|
+
json({ event: "flow:result", runId, status: "failed", error: errorMsg });
|
|
3147
|
+
process.exit(1);
|
|
3148
|
+
}
|
|
3149
|
+
error(`Resume failed: ${errorMsg}`);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
async function flowRunsCommand(flowKey) {
|
|
3153
|
+
intro2(pc7.bgCyan(pc7.black(" One Flow ")));
|
|
3154
|
+
const runs = FlowRunner.listRuns(flowKey);
|
|
3155
|
+
if (isAgentMode()) {
|
|
3156
|
+
json({
|
|
3157
|
+
runs: runs.map((r) => ({
|
|
3158
|
+
runId: r.runId,
|
|
3159
|
+
flowKey: r.flowKey,
|
|
3160
|
+
status: r.status,
|
|
3161
|
+
startedAt: r.startedAt,
|
|
3162
|
+
completedAt: r.completedAt,
|
|
3163
|
+
pausedAt: r.pausedAt,
|
|
3164
|
+
completedSteps: r.completedSteps.length
|
|
3165
|
+
}))
|
|
3166
|
+
});
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
if (runs.length === 0) {
|
|
3170
|
+
note2(flowKey ? `No runs found for flow "${flowKey}"` : "No flow runs found", "Runs");
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
console.log();
|
|
3174
|
+
printTable(
|
|
3175
|
+
[
|
|
3176
|
+
{ key: "runId", label: "Run ID" },
|
|
3177
|
+
{ key: "flowKey", label: "Flow" },
|
|
3178
|
+
{ key: "status", label: "Status" },
|
|
3179
|
+
{ key: "startedAt", label: "Started" },
|
|
3180
|
+
{ key: "steps", label: "Steps Done" }
|
|
3181
|
+
],
|
|
3182
|
+
runs.map((r) => ({
|
|
3183
|
+
runId: r.runId,
|
|
3184
|
+
flowKey: r.flowKey,
|
|
3185
|
+
status: colorStatus(r.status),
|
|
3186
|
+
startedAt: r.startedAt,
|
|
3187
|
+
steps: String(r.completedSteps.length)
|
|
3188
|
+
}))
|
|
3189
|
+
);
|
|
3190
|
+
console.log();
|
|
3191
|
+
}
|
|
3192
|
+
function colorStatus(status) {
|
|
3193
|
+
switch (status) {
|
|
3194
|
+
case "completed":
|
|
3195
|
+
return pc7.green(status);
|
|
3196
|
+
case "running":
|
|
3197
|
+
return pc7.cyan(status);
|
|
3198
|
+
case "paused":
|
|
3199
|
+
return pc7.yellow(status);
|
|
3200
|
+
case "failed":
|
|
3201
|
+
return pc7.red(status);
|
|
3202
|
+
default:
|
|
3203
|
+
return status;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
// src/commands/guide.ts
|
|
3208
|
+
import pc8 from "picocolors";
|
|
3209
|
+
|
|
3210
|
+
// src/lib/guide-content.ts
|
|
3211
|
+
var GUIDE_OVERVIEW = `# One CLI \u2014 Agent Guide
|
|
3212
|
+
|
|
3213
|
+
## Setup
|
|
3214
|
+
|
|
3215
|
+
1. Run \`one init\` to configure your API key
|
|
3216
|
+
2. Run \`one add <platform>\` to connect platforms via OAuth
|
|
3217
|
+
3. Run \`one --agent connection list\` to verify connections
|
|
3218
|
+
|
|
3219
|
+
## The --agent Flag
|
|
3220
|
+
|
|
3221
|
+
Always use \`--agent\` for machine-readable JSON output. It disables colors, spinners, and interactive prompts.
|
|
3222
|
+
|
|
3223
|
+
\`\`\`bash
|
|
3224
|
+
one --agent <command>
|
|
3225
|
+
\`\`\`
|
|
3226
|
+
|
|
3227
|
+
All commands return JSON. If an \`error\` key is present, the command failed.
|
|
3228
|
+
|
|
3229
|
+
## Topics
|
|
3230
|
+
|
|
3231
|
+
This guide has three sections you can request individually:
|
|
3232
|
+
|
|
3233
|
+
- **overview** \u2014 This section. Setup, flag usage, and discovery workflow.
|
|
3234
|
+
- **actions** \u2014 Full workflow for searching, reading docs, and executing platform actions.
|
|
3235
|
+
- **flows** \u2014 Building and executing multi-step API workflows (JSON-based).
|
|
3236
|
+
|
|
3237
|
+
## Discovery Workflow
|
|
3238
|
+
|
|
3239
|
+
1. \`one --agent connection list\` \u2014 See connected platforms and connection keys
|
|
3240
|
+
2. \`one --agent actions search <platform> <query>\` \u2014 Find actions
|
|
3241
|
+
3. \`one --agent actions knowledge <platform> <actionId>\` \u2014 Read full docs (REQUIRED before execute)
|
|
3242
|
+
4. \`one --agent actions execute <platform> <actionId> <connectionKey>\` \u2014 Execute the action
|
|
3243
|
+
|
|
3244
|
+
For multi-step workflows, use flows:
|
|
3245
|
+
1. Discover actions with the workflow above
|
|
3246
|
+
2. Build a flow JSON definition
|
|
3247
|
+
3. \`one --agent flow create <key> --definition '<json>'\`
|
|
3248
|
+
4. \`one --agent flow execute <key> -i param=value\`
|
|
3249
|
+
|
|
3250
|
+
Platform names are always kebab-case (e.g., \`hub-spot\`, \`google-calendar\`).
|
|
3251
|
+
Run \`one platforms\` to browse all 200+ available platforms.
|
|
3252
|
+
`;
|
|
3253
|
+
var GUIDE_ACTIONS = `# One Actions CLI Workflow
|
|
3254
|
+
|
|
3255
|
+
You have access to the One CLI which lets you interact with 200+ third-party platforms through their APIs. The CLI handles authentication, request building, and execution through One's passthrough proxy.
|
|
3256
|
+
|
|
3257
|
+
## The Workflow
|
|
3258
|
+
|
|
3259
|
+
Always follow this sequence \u2014 each step builds on the previous one:
|
|
3260
|
+
|
|
3261
|
+
1. **List connections** to see what platforms the user has connected
|
|
3262
|
+
2. **Search actions** to find the right API action for what the user wants to do
|
|
3263
|
+
3. **Get knowledge** to understand the action's parameters, requirements, and structure
|
|
3264
|
+
4. **Execute** the action with the correct parameters
|
|
3265
|
+
|
|
3266
|
+
Never skip the knowledge step before executing \u2014 it contains critical information about required parameters, validation rules, and request structure that you need to build a correct request.
|
|
3267
|
+
|
|
3268
|
+
## Commands
|
|
3269
|
+
|
|
3270
|
+
### 1. List Connections
|
|
3271
|
+
|
|
3272
|
+
\`\`\`bash
|
|
3273
|
+
one --agent connection list
|
|
3274
|
+
\`\`\`
|
|
3275
|
+
|
|
3276
|
+
Returns JSON with all connected platforms, their status, and connection keys. You need the **connection key** for executing actions, and the **platform name** (kebab-case) for searching actions.
|
|
3277
|
+
|
|
3278
|
+
Output format:
|
|
3279
|
+
\`\`\`json
|
|
3280
|
+
{"connections": [{"platform": "gmail", "state": "active", "key": "conn_abc123"}, ...]}
|
|
3281
|
+
\`\`\`
|
|
3282
|
+
|
|
3283
|
+
### 2. Search Actions
|
|
3284
|
+
|
|
3285
|
+
\`\`\`bash
|
|
3286
|
+
one --agent actions search <platform> <query>
|
|
3287
|
+
\`\`\`
|
|
3288
|
+
|
|
3289
|
+
Search for actions on a specific platform using natural language. Returns JSON with up to 5 matching actions including their action IDs, HTTP methods, and paths.
|
|
3290
|
+
|
|
3291
|
+
- \`<platform>\` \u2014 Platform name in kebab-case exactly as shown in the connections list (e.g., \`gmail\`, \`shopify\`, \`hub-spot\`)
|
|
3292
|
+
- \`<query>\` \u2014 Natural language description of what you want to do (e.g., \`"send email"\`, \`"list contacts"\`, \`"create order"\`)
|
|
3293
|
+
|
|
3294
|
+
Options:
|
|
3295
|
+
- \`-t, --type <execute|knowledge>\` \u2014 Use \`execute\` when the user wants to perform an action, \`knowledge\` when they want documentation or want to write code. Defaults to \`knowledge\`.
|
|
3296
|
+
|
|
3297
|
+
Example:
|
|
3298
|
+
\`\`\`bash
|
|
3299
|
+
one --agent actions search gmail "send email" -t execute
|
|
3300
|
+
\`\`\`
|
|
3301
|
+
|
|
3302
|
+
Output format:
|
|
3303
|
+
\`\`\`json
|
|
3304
|
+
{"actions": [{"_id": "abc123", "title": "Send Email", "tags": [...], "method": "POST", "path": "/messages/send"}, ...]}
|
|
3305
|
+
\`\`\`
|
|
3306
|
+
|
|
3307
|
+
### 3. Get Action Knowledge
|
|
3308
|
+
|
|
3309
|
+
\`\`\`bash
|
|
3310
|
+
one --agent actions knowledge <platform> <actionId>
|
|
3311
|
+
\`\`\`
|
|
3312
|
+
|
|
3313
|
+
Get comprehensive documentation for an action including parameters, requirements, validation rules, request/response structure, and examples. Returns JSON with the full API knowledge and HTTP method.
|
|
3314
|
+
|
|
3315
|
+
Always call this before executing \u2014 it tells you exactly what parameters are required and how to structure the request.
|
|
3316
|
+
|
|
3317
|
+
Example:
|
|
3318
|
+
\`\`\`bash
|
|
3319
|
+
one --agent actions knowledge gmail 67890abcdef
|
|
3320
|
+
\`\`\`
|
|
3321
|
+
|
|
3322
|
+
Output format:
|
|
3323
|
+
\`\`\`json
|
|
3324
|
+
{"knowledge": "...full API documentation and guidance...", "method": "POST"}
|
|
3325
|
+
\`\`\`
|
|
3326
|
+
|
|
3327
|
+
### 4. Execute Action
|
|
3328
|
+
|
|
3329
|
+
\`\`\`bash
|
|
3330
|
+
one --agent actions execute <platform> <actionId> <connectionKey> [options]
|
|
3331
|
+
\`\`\`
|
|
3332
|
+
|
|
3333
|
+
Execute an action on a connected platform. Returns JSON with the request details and response data. You must have retrieved the knowledge for this action first.
|
|
3334
|
+
|
|
3335
|
+
- \`<platform>\` \u2014 Platform name in kebab-case
|
|
3336
|
+
- \`<actionId>\` \u2014 Action ID from the search results
|
|
3337
|
+
- \`<connectionKey>\` \u2014 Connection key from \`one connection list\`
|
|
3338
|
+
|
|
3339
|
+
Options:
|
|
3340
|
+
- \`-d, --data <json>\` \u2014 Request body as JSON string (for POST, PUT, PATCH)
|
|
3341
|
+
- \`--path-vars <json>\` \u2014 Path variables as JSON (for URLs with \`{id}\` placeholders)
|
|
3342
|
+
- \`--query-params <json>\` \u2014 Query parameters as JSON
|
|
3343
|
+
- \`--headers <json>\` \u2014 Additional headers as JSON
|
|
3344
|
+
- \`--form-data\` \u2014 Send as multipart/form-data instead of JSON
|
|
3345
|
+
- \`--form-url-encoded\` \u2014 Send as application/x-www-form-urlencoded
|
|
3346
|
+
|
|
3347
|
+
Examples:
|
|
3348
|
+
\`\`\`bash
|
|
3349
|
+
# Simple GET request
|
|
3350
|
+
one --agent actions execute shopify <actionId> <connectionKey>
|
|
3351
|
+
|
|
3352
|
+
# POST with data
|
|
3353
|
+
one --agent actions execute hub-spot <actionId> <connectionKey> \\
|
|
3354
|
+
-d '{"properties": {"email": "jane@example.com", "firstname": "Jane"}}'
|
|
3355
|
+
|
|
3356
|
+
# With path variables and query params
|
|
3357
|
+
one --agent actions execute shopify <actionId> <connectionKey> \\
|
|
3358
|
+
--path-vars '{"order_id": "12345"}' \\
|
|
3359
|
+
--query-params '{"limit": "10"}'
|
|
3360
|
+
\`\`\`
|
|
3361
|
+
|
|
3362
|
+
Output format:
|
|
3363
|
+
\`\`\`json
|
|
3364
|
+
{"request": {"method": "POST", "url": "https://..."}, "response": {...}}
|
|
3365
|
+
\`\`\`
|
|
3366
|
+
|
|
3367
|
+
## Error Handling
|
|
3368
|
+
|
|
3369
|
+
All errors return JSON in agent mode:
|
|
3370
|
+
\`\`\`json
|
|
3371
|
+
{"error": "Error message here"}
|
|
3372
|
+
\`\`\`
|
|
3373
|
+
|
|
3374
|
+
Parse the output as JSON. If the \`error\` key is present, the command failed \u2014 report the error message to the user.
|
|
3375
|
+
|
|
3376
|
+
## Important Notes
|
|
3377
|
+
|
|
3378
|
+
- **Always use \`--agent\` flag** \u2014 it produces structured JSON output without spinners, colors, or interactive prompts
|
|
3379
|
+
- Platform names are always **kebab-case** (e.g., \`hub-spot\` not \`HubSpot\`, \`ship-station\` not \`ShipStation\`)
|
|
3380
|
+
- Always use the **exact action ID** from search results \u2014 don't guess or construct them
|
|
3381
|
+
- Always read the knowledge output carefully \u2014 it tells you which parameters are required vs optional, what format they need to be in, and any caveats specific to that API
|
|
3382
|
+
- JSON values passed to \`-d\`, \`--path-vars\`, \`--query-params\`, and \`--headers\` must be valid JSON strings (use single quotes around the JSON to avoid shell escaping issues)
|
|
3383
|
+
- If search returns no results, try broader queries (e.g., \`"list"\` instead of \`"list active premium customers"\`)
|
|
3384
|
+
- The execute command respects access control settings configured via \`one config\` \u2014 if execution is blocked, the user may need to adjust their permissions
|
|
3385
|
+
`;
|
|
3386
|
+
var GUIDE_FLOWS = `# One Flow \u2014 Multi-Step API Workflows
|
|
3387
|
+
|
|
3388
|
+
You have access to the One CLI's flow engine, which lets you create and execute multi-step API workflows as JSON files. Flows chain actions across platforms \u2014 e.g., look up a Stripe customer, then send them a welcome email via Gmail.
|
|
3389
|
+
|
|
3390
|
+
## 1. Overview
|
|
3391
|
+
|
|
3392
|
+
- Flows are JSON files stored at \`.one/flows/<key>.flow.json\`
|
|
3393
|
+
- All dynamic values (including connection keys) are declared as **inputs**
|
|
3394
|
+
- Each flow has a unique **key** used to reference and execute it
|
|
3395
|
+
- Executed via \`one --agent flow execute <key> -i name=value\`
|
|
3396
|
+
|
|
3397
|
+
## 2. Building a Flow \u2014 Step-by-Step Process
|
|
3398
|
+
|
|
3399
|
+
**You MUST follow this process to build a correct flow:**
|
|
3400
|
+
|
|
3401
|
+
### Step 1: Discover connections
|
|
3402
|
+
|
|
3403
|
+
\`\`\`bash
|
|
3404
|
+
one --agent connection list
|
|
3405
|
+
\`\`\`
|
|
3406
|
+
|
|
3407
|
+
Find out which platforms are connected and get their connection keys.
|
|
3408
|
+
|
|
3409
|
+
### Step 2: For EACH API action needed, get the knowledge
|
|
3410
|
+
|
|
3411
|
+
\`\`\`bash
|
|
3412
|
+
# Find the action ID
|
|
3413
|
+
one --agent actions search <platform> "<query>" -t execute
|
|
3414
|
+
|
|
3415
|
+
# Read the full docs \u2014 REQUIRED before adding to a flow
|
|
3416
|
+
one --agent actions knowledge <platform> <actionId>
|
|
3417
|
+
\`\`\`
|
|
3418
|
+
|
|
3419
|
+
**CRITICAL:** You MUST call \`one actions knowledge\` for every action you include in the flow. The knowledge output tells you the exact request body structure, required fields, path variables, and query parameters. Without this, your flow JSON will have incorrect data shapes.
|
|
3420
|
+
|
|
3421
|
+
### Step 3: Construct the flow JSON
|
|
3422
|
+
|
|
3423
|
+
Using the knowledge gathered, build the flow JSON with:
|
|
3424
|
+
- All inputs declared (connection keys + user parameters)
|
|
3425
|
+
- Each step with the correct actionId, platform, and data structure (from knowledge)
|
|
3426
|
+
- Data wired between steps using \`$.input.*\` and \`$.steps.*\` selectors
|
|
3427
|
+
|
|
3428
|
+
### Step 4: Write the flow file
|
|
3429
|
+
|
|
3430
|
+
\`\`\`bash
|
|
3431
|
+
one --agent flow create <key> --definition '<json>'
|
|
3432
|
+
\`\`\`
|
|
3433
|
+
|
|
3434
|
+
Or write directly to \`.one/flows/<key>.flow.json\`.
|
|
3435
|
+
|
|
3436
|
+
### Step 5: Validate
|
|
3437
|
+
|
|
3438
|
+
\`\`\`bash
|
|
3439
|
+
one --agent flow validate <key>
|
|
3440
|
+
\`\`\`
|
|
3441
|
+
|
|
3442
|
+
### Step 6: Execute
|
|
3443
|
+
|
|
3444
|
+
\`\`\`bash
|
|
3445
|
+
one --agent flow execute <key> -i connectionKey=xxx -i param=value
|
|
3446
|
+
\`\`\`
|
|
3447
|
+
|
|
3448
|
+
## 3. Flow JSON Schema Reference
|
|
3449
|
+
|
|
3450
|
+
\`\`\`json
|
|
3451
|
+
{
|
|
3452
|
+
"key": "welcome-customer",
|
|
3453
|
+
"name": "Welcome New Customer",
|
|
3454
|
+
"description": "Look up a Stripe customer and send them a welcome email via Gmail",
|
|
3455
|
+
"version": "1",
|
|
3456
|
+
"inputs": {
|
|
3457
|
+
"stripeConnectionKey": {
|
|
3458
|
+
"type": "string",
|
|
3459
|
+
"required": true,
|
|
3460
|
+
"description": "Stripe connection key from one connection list",
|
|
3461
|
+
"connection": { "platform": "stripe" }
|
|
3462
|
+
},
|
|
3463
|
+
"gmailConnectionKey": {
|
|
3464
|
+
"type": "string",
|
|
3465
|
+
"required": true,
|
|
3466
|
+
"description": "Gmail connection key from one connection list",
|
|
3467
|
+
"connection": { "platform": "gmail" }
|
|
3468
|
+
},
|
|
3469
|
+
"customerEmail": {
|
|
3470
|
+
"type": "string",
|
|
3471
|
+
"required": true,
|
|
3472
|
+
"description": "Customer email to look up"
|
|
3473
|
+
}
|
|
3474
|
+
},
|
|
3475
|
+
"steps": [
|
|
3476
|
+
{
|
|
3477
|
+
"id": "stepId",
|
|
3478
|
+
"name": "Human-readable label",
|
|
3479
|
+
"type": "action",
|
|
3480
|
+
"action": {
|
|
3481
|
+
"platform": "stripe",
|
|
3482
|
+
"actionId": "the-action-id-from-search",
|
|
3483
|
+
"connectionKey": "$.input.stripeConnectionKey",
|
|
3484
|
+
"data": {},
|
|
3485
|
+
"pathVars": {},
|
|
3486
|
+
"queryParams": {},
|
|
3487
|
+
"headers": {}
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
]
|
|
3491
|
+
}
|
|
3492
|
+
\`\`\`
|
|
3493
|
+
|
|
3494
|
+
### Input declarations
|
|
3495
|
+
|
|
3496
|
+
| Field | Type | Description |
|
|
3497
|
+
|---|---|---|
|
|
3498
|
+
| \`type\` | string | \`string\`, \`number\`, \`boolean\`, \`object\`, \`array\` |
|
|
3499
|
+
| \`required\` | boolean | Whether this input must be provided (default: true) |
|
|
3500
|
+
| \`default\` | any | Default value if not provided |
|
|
3501
|
+
| \`description\` | string | Human-readable description |
|
|
3502
|
+
| \`connection\` | object | Connection metadata: \`{ "platform": "gmail" }\` \u2014 enables auto-resolution |
|
|
3503
|
+
|
|
3504
|
+
**Connection inputs** have a \`connection\` field. If the user has exactly one connection for that platform, the engine auto-resolves it.
|
|
3505
|
+
|
|
3506
|
+
## 4. Selector Syntax Reference
|
|
3507
|
+
|
|
3508
|
+
| Pattern | Resolves To |
|
|
3509
|
+
|---|---|
|
|
3510
|
+
| \`$.input.gmailConnectionKey\` | Input value (including connection keys) |
|
|
3511
|
+
| \`$.input.customerEmail\` | Any input parameter |
|
|
3512
|
+
| \`$.steps.stepId.response\` | Full API response from a step |
|
|
3513
|
+
| \`$.steps.stepId.response.data[0].email\` | Nested field with array index |
|
|
3514
|
+
| \`$.steps.stepId.response.data[*].id\` | Wildcard \u2014 maps array to field |
|
|
3515
|
+
| \`$.env.MY_VAR\` | Environment variable |
|
|
3516
|
+
| \`$.loop.item\` | Current loop item |
|
|
3517
|
+
| \`$.loop.i\` | Current loop index |
|
|
3518
|
+
| \`"Hello {{$.steps.getUser.response.data.name}}"\` | String interpolation |
|
|
3519
|
+
|
|
3520
|
+
**Rules:**
|
|
3521
|
+
- A value that is purely \`$.xxx\` resolves to the raw type (object, array, number)
|
|
3522
|
+
- A string containing \`{{$.xxx}}\` does string interpolation (stringifies objects)
|
|
3523
|
+
- Selectors inside objects/arrays are resolved recursively
|
|
3524
|
+
|
|
3525
|
+
## 5. Step Types Reference
|
|
3526
|
+
|
|
3527
|
+
### \`action\` \u2014 Execute a One API action
|
|
3528
|
+
|
|
3529
|
+
\`\`\`json
|
|
3530
|
+
{
|
|
3531
|
+
"id": "findCustomer",
|
|
3532
|
+
"name": "Search Stripe customers",
|
|
3533
|
+
"type": "action",
|
|
3534
|
+
"action": {
|
|
3535
|
+
"platform": "stripe",
|
|
3536
|
+
"actionId": "conn_mod_def::xxx::yyy",
|
|
3537
|
+
"connectionKey": "$.input.stripeConnectionKey",
|
|
3538
|
+
"data": {
|
|
3539
|
+
"query": "email:'{{$.input.customerEmail}}'"
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
\`\`\`
|
|
3544
|
+
|
|
3545
|
+
### \`transform\` \u2014 Transform data with a JS expression
|
|
3546
|
+
|
|
3547
|
+
\`\`\`json
|
|
3548
|
+
{
|
|
3549
|
+
"id": "extractNames",
|
|
3550
|
+
"name": "Extract customer names",
|
|
3551
|
+
"type": "transform",
|
|
3552
|
+
"transform": {
|
|
3553
|
+
"expression": "$.steps.findCustomer.response.data.map(c => c.name)"
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
\`\`\`
|
|
3557
|
+
|
|
3558
|
+
The expression is evaluated with the full flow context as \`$\`.
|
|
3559
|
+
|
|
3560
|
+
### \`code\` \u2014 Run multi-line JavaScript
|
|
3561
|
+
|
|
3562
|
+
Unlike \`transform\` (single expression, implicit return), \`code\` runs a full function body with explicit \`return\`. Use it when you need variables, loops, try/catch, or \`await\`.
|
|
3563
|
+
|
|
3564
|
+
\`\`\`json
|
|
3565
|
+
{
|
|
3566
|
+
"id": "processData",
|
|
3567
|
+
"name": "Process and enrich data",
|
|
3568
|
+
"type": "code",
|
|
3569
|
+
"code": {
|
|
3570
|
+
"source": "const customers = $.steps.listCustomers.response.data;\\nconst enriched = customers.map(c => ({\\n ...c,\\n tier: c.spend > 1000 ? 'gold' : 'silver'\\n}));\\nreturn enriched;"
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
\`\`\`
|
|
3574
|
+
|
|
3575
|
+
The \`source\` field contains a JS function body. The flow context is available as \`$\`. The function is async, so you can use \`await\`. The return value is stored as the step result.
|
|
3576
|
+
|
|
3577
|
+
### \`condition\` \u2014 If/then/else branching
|
|
3578
|
+
|
|
3579
|
+
\`\`\`json
|
|
3580
|
+
{
|
|
3581
|
+
"id": "checkFound",
|
|
3582
|
+
"name": "Check if customer was found",
|
|
3583
|
+
"type": "condition",
|
|
3584
|
+
"condition": {
|
|
3585
|
+
"expression": "$.steps.findCustomer.response.data.length > 0",
|
|
3586
|
+
"then": [
|
|
3587
|
+
{ "id": "sendEmail", "name": "Send welcome email", "type": "action", "action": { "..." : "..." } }
|
|
3588
|
+
],
|
|
3589
|
+
"else": [
|
|
3590
|
+
{ "id": "logNotFound", "name": "Log not found", "type": "transform", "transform": { "expression": "'Customer not found'" } }
|
|
3591
|
+
]
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
\`\`\`
|
|
3595
|
+
|
|
3596
|
+
### \`loop\` \u2014 Iterate over an array
|
|
3597
|
+
|
|
3598
|
+
\`\`\`json
|
|
3599
|
+
{
|
|
3600
|
+
"id": "processOrders",
|
|
3601
|
+
"name": "Process each order",
|
|
3602
|
+
"type": "loop",
|
|
3603
|
+
"loop": {
|
|
3604
|
+
"over": "$.steps.listOrders.response.data",
|
|
3605
|
+
"as": "order",
|
|
3606
|
+
"indexAs": "i",
|
|
3607
|
+
"maxIterations": 1000,
|
|
3608
|
+
"steps": [
|
|
3609
|
+
{
|
|
3610
|
+
"id": "createInvoice",
|
|
3611
|
+
"name": "Create invoice for order",
|
|
3612
|
+
"type": "action",
|
|
3613
|
+
"action": {
|
|
3614
|
+
"platform": "quickbooks",
|
|
3615
|
+
"actionId": "...",
|
|
3616
|
+
"connectionKey": "$.input.qbConnectionKey",
|
|
3617
|
+
"data": { "amount": "$.loop.order.total" }
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
]
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
\`\`\`
|
|
3624
|
+
|
|
3625
|
+
### \`parallel\` \u2014 Run steps concurrently
|
|
3626
|
+
|
|
3627
|
+
\`\`\`json
|
|
3628
|
+
{
|
|
3629
|
+
"id": "parallelLookups",
|
|
3630
|
+
"name": "Look up in parallel",
|
|
3631
|
+
"type": "parallel",
|
|
3632
|
+
"parallel": {
|
|
3633
|
+
"maxConcurrency": 5,
|
|
3634
|
+
"steps": [
|
|
3635
|
+
{ "id": "getStripe", "name": "Get Stripe data", "type": "action", "action": { "...": "..." } },
|
|
3636
|
+
{ "id": "getHubspot", "name": "Get HubSpot data", "type": "action", "action": { "...": "..." } }
|
|
3637
|
+
]
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
\`\`\`
|
|
3641
|
+
|
|
3642
|
+
### \`file-read\` \u2014 Read from filesystem
|
|
3643
|
+
|
|
3644
|
+
\`\`\`json
|
|
3645
|
+
{
|
|
3646
|
+
"id": "readConfig",
|
|
3647
|
+
"name": "Read config file",
|
|
3648
|
+
"type": "file-read",
|
|
3649
|
+
"fileRead": { "path": "./data/config.json", "parseJson": true }
|
|
3650
|
+
}
|
|
3651
|
+
\`\`\`
|
|
3652
|
+
|
|
3653
|
+
### \`file-write\` \u2014 Write to filesystem
|
|
3654
|
+
|
|
3655
|
+
\`\`\`json
|
|
3656
|
+
{
|
|
3657
|
+
"id": "writeResults",
|
|
3658
|
+
"name": "Save results",
|
|
3659
|
+
"type": "file-write",
|
|
3660
|
+
"fileWrite": {
|
|
3661
|
+
"path": "./output/results.json",
|
|
3662
|
+
"content": "$.steps.transform.output",
|
|
3663
|
+
"append": false
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
\`\`\`
|
|
3667
|
+
|
|
3668
|
+
## 6. Error Handling
|
|
3669
|
+
|
|
3670
|
+
### \`onError\` strategies
|
|
3671
|
+
|
|
3672
|
+
\`\`\`json
|
|
3673
|
+
{
|
|
3674
|
+
"id": "riskyStep",
|
|
3675
|
+
"name": "Might fail",
|
|
3676
|
+
"type": "action",
|
|
3677
|
+
"onError": {
|
|
3678
|
+
"strategy": "retry",
|
|
3679
|
+
"retries": 3,
|
|
3680
|
+
"retryDelayMs": 1000
|
|
3681
|
+
},
|
|
3682
|
+
"action": { "...": "..." }
|
|
3683
|
+
}
|
|
3684
|
+
\`\`\`
|
|
3685
|
+
|
|
3686
|
+
| Strategy | Behavior |
|
|
3687
|
+
|---|---|
|
|
3688
|
+
| \`fail\` | Stop the flow immediately (default) |
|
|
3689
|
+
| \`continue\` | Mark step as failed, continue to next step |
|
|
3690
|
+
| \`retry\` | Retry up to N times with delay |
|
|
3691
|
+
| \`fallback\` | On failure, execute a different step |
|
|
3692
|
+
|
|
3693
|
+
### Conditional execution
|
|
3694
|
+
|
|
3695
|
+
Skip a step based on previous results:
|
|
3696
|
+
|
|
3697
|
+
\`\`\`json
|
|
3698
|
+
{
|
|
3699
|
+
"id": "sendEmail",
|
|
3700
|
+
"name": "Send email only if customer found",
|
|
3701
|
+
"type": "action",
|
|
3702
|
+
"if": "$.steps.findCustomer.response.data.length > 0",
|
|
3703
|
+
"action": { "...": "..." }
|
|
3704
|
+
}
|
|
3705
|
+
\`\`\`
|
|
3706
|
+
|
|
3707
|
+
## 7. Updating Existing Flows
|
|
3708
|
+
|
|
3709
|
+
To modify an existing flow:
|
|
3710
|
+
|
|
3711
|
+
1. Read the flow JSON file at \`.one/flows/<key>.flow.json\`
|
|
3712
|
+
2. Understand its current structure
|
|
3713
|
+
3. Use \`one --agent actions knowledge <platform> <actionId>\` for any new actions
|
|
3714
|
+
4. Modify the JSON (add/remove/update steps, change data mappings, add inputs)
|
|
3715
|
+
5. Write back the updated flow file
|
|
3716
|
+
6. Validate: \`one --agent flow validate <key>\`
|
|
3717
|
+
|
|
3718
|
+
## 8. Complete Examples
|
|
3719
|
+
|
|
3720
|
+
### Example 1: Simple 2-step \u2014 Search Stripe customer, send Gmail email
|
|
3721
|
+
|
|
3722
|
+
\`\`\`json
|
|
3723
|
+
{
|
|
3724
|
+
"key": "welcome-customer",
|
|
3725
|
+
"name": "Welcome New Customer",
|
|
3726
|
+
"description": "Look up a Stripe customer and send them a welcome email",
|
|
3727
|
+
"version": "1",
|
|
3728
|
+
"inputs": {
|
|
3729
|
+
"stripeConnectionKey": {
|
|
3730
|
+
"type": "string",
|
|
3731
|
+
"required": true,
|
|
3732
|
+
"description": "Stripe connection key",
|
|
3733
|
+
"connection": { "platform": "stripe" }
|
|
3734
|
+
},
|
|
3735
|
+
"gmailConnectionKey": {
|
|
3736
|
+
"type": "string",
|
|
3737
|
+
"required": true,
|
|
3738
|
+
"description": "Gmail connection key",
|
|
3739
|
+
"connection": { "platform": "gmail" }
|
|
3740
|
+
},
|
|
3741
|
+
"customerEmail": {
|
|
3742
|
+
"type": "string",
|
|
3743
|
+
"required": true,
|
|
3744
|
+
"description": "Customer email to look up"
|
|
3745
|
+
}
|
|
3746
|
+
},
|
|
3747
|
+
"steps": [
|
|
3748
|
+
{
|
|
3749
|
+
"id": "findCustomer",
|
|
3750
|
+
"name": "Search for customer in Stripe",
|
|
3751
|
+
"type": "action",
|
|
3752
|
+
"action": {
|
|
3753
|
+
"platform": "stripe",
|
|
3754
|
+
"actionId": "STRIPE_SEARCH_CUSTOMERS_ACTION_ID",
|
|
3755
|
+
"connectionKey": "$.input.stripeConnectionKey",
|
|
3756
|
+
"data": {
|
|
3757
|
+
"query": "email:'{{$.input.customerEmail}}'"
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
},
|
|
3761
|
+
{
|
|
3762
|
+
"id": "sendWelcome",
|
|
3763
|
+
"name": "Send welcome email via Gmail",
|
|
3764
|
+
"type": "action",
|
|
3765
|
+
"if": "$.steps.findCustomer.response.data && $.steps.findCustomer.response.data.length > 0",
|
|
3766
|
+
"action": {
|
|
3767
|
+
"platform": "gmail",
|
|
3768
|
+
"actionId": "GMAIL_SEND_EMAIL_ACTION_ID",
|
|
3769
|
+
"connectionKey": "$.input.gmailConnectionKey",
|
|
3770
|
+
"data": {
|
|
3771
|
+
"to": "{{$.input.customerEmail}}",
|
|
3772
|
+
"subject": "Welcome, {{$.steps.findCustomer.response.data[0].name}}!",
|
|
3773
|
+
"body": "Thank you for being a customer. We're glad to have you!"
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
]
|
|
3778
|
+
}
|
|
3779
|
+
\`\`\`
|
|
3780
|
+
|
|
3781
|
+
### Example 2: Conditional \u2014 Check if HubSpot contact exists, create or update
|
|
3782
|
+
|
|
3783
|
+
\`\`\`json
|
|
3784
|
+
{
|
|
3785
|
+
"key": "sync-hubspot-contact",
|
|
3786
|
+
"name": "Sync Contact to HubSpot",
|
|
3787
|
+
"description": "Check if a contact exists in HubSpot, create if new or update if existing",
|
|
3788
|
+
"version": "1",
|
|
3789
|
+
"inputs": {
|
|
3790
|
+
"hubspotConnectionKey": {
|
|
3791
|
+
"type": "string",
|
|
3792
|
+
"required": true,
|
|
3793
|
+
"connection": { "platform": "hub-spot" }
|
|
3794
|
+
},
|
|
3795
|
+
"email": { "type": "string", "required": true },
|
|
3796
|
+
"firstName": { "type": "string", "required": true },
|
|
3797
|
+
"lastName": { "type": "string", "required": true }
|
|
3798
|
+
},
|
|
3799
|
+
"steps": [
|
|
3800
|
+
{
|
|
3801
|
+
"id": "searchContact",
|
|
3802
|
+
"name": "Search for existing contact",
|
|
3803
|
+
"type": "action",
|
|
3804
|
+
"action": {
|
|
3805
|
+
"platform": "hub-spot",
|
|
3806
|
+
"actionId": "HUBSPOT_SEARCH_CONTACTS_ACTION_ID",
|
|
3807
|
+
"connectionKey": "$.input.hubspotConnectionKey",
|
|
3808
|
+
"data": {
|
|
3809
|
+
"filterGroups": [{ "filters": [{ "propertyName": "email", "operator": "EQ", "value": "$.input.email" }] }]
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
},
|
|
3813
|
+
{
|
|
3814
|
+
"id": "createOrUpdate",
|
|
3815
|
+
"name": "Create or update contact",
|
|
3816
|
+
"type": "condition",
|
|
3817
|
+
"condition": {
|
|
3818
|
+
"expression": "$.steps.searchContact.response.total > 0",
|
|
3819
|
+
"then": [
|
|
3820
|
+
{
|
|
3821
|
+
"id": "updateContact",
|
|
3822
|
+
"name": "Update existing contact",
|
|
3823
|
+
"type": "action",
|
|
3824
|
+
"action": {
|
|
3825
|
+
"platform": "hub-spot",
|
|
3826
|
+
"actionId": "HUBSPOT_UPDATE_CONTACT_ACTION_ID",
|
|
3827
|
+
"connectionKey": "$.input.hubspotConnectionKey",
|
|
3828
|
+
"pathVars": { "contactId": "$.steps.searchContact.response.results[0].id" },
|
|
3829
|
+
"data": {
|
|
3830
|
+
"properties": { "firstname": "$.input.firstName", "lastname": "$.input.lastName" }
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
],
|
|
3835
|
+
"else": [
|
|
3836
|
+
{
|
|
3837
|
+
"id": "createContact",
|
|
3838
|
+
"name": "Create new contact",
|
|
3839
|
+
"type": "action",
|
|
3840
|
+
"action": {
|
|
3841
|
+
"platform": "hub-spot",
|
|
3842
|
+
"actionId": "HUBSPOT_CREATE_CONTACT_ACTION_ID",
|
|
3843
|
+
"connectionKey": "$.input.hubspotConnectionKey",
|
|
3844
|
+
"data": {
|
|
3845
|
+
"properties": { "email": "$.input.email", "firstname": "$.input.firstName", "lastname": "$.input.lastName" }
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
]
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
]
|
|
3853
|
+
}
|
|
3854
|
+
\`\`\`
|
|
3855
|
+
|
|
3856
|
+
### Example 3: Loop \u2014 Iterate over Shopify orders, create invoices
|
|
3857
|
+
|
|
3858
|
+
\`\`\`json
|
|
3859
|
+
{
|
|
3860
|
+
"key": "shopify-to-invoices",
|
|
3861
|
+
"name": "Shopify Orders to Invoices",
|
|
3862
|
+
"description": "Fetch recent Shopify orders and create an invoice for each",
|
|
3863
|
+
"version": "1",
|
|
3864
|
+
"inputs": {
|
|
3865
|
+
"shopifyConnectionKey": {
|
|
3866
|
+
"type": "string",
|
|
3867
|
+
"required": true,
|
|
3868
|
+
"connection": { "platform": "shopify" }
|
|
3869
|
+
},
|
|
3870
|
+
"qbConnectionKey": {
|
|
3871
|
+
"type": "string",
|
|
3872
|
+
"required": true,
|
|
3873
|
+
"connection": { "platform": "quick-books" }
|
|
3874
|
+
}
|
|
3875
|
+
},
|
|
3876
|
+
"steps": [
|
|
3877
|
+
{
|
|
3878
|
+
"id": "listOrders",
|
|
3879
|
+
"name": "List recent Shopify orders",
|
|
3880
|
+
"type": "action",
|
|
3881
|
+
"action": {
|
|
3882
|
+
"platform": "shopify",
|
|
3883
|
+
"actionId": "SHOPIFY_LIST_ORDERS_ACTION_ID",
|
|
3884
|
+
"connectionKey": "$.input.shopifyConnectionKey",
|
|
3885
|
+
"queryParams": { "status": "any", "limit": "50" }
|
|
3886
|
+
}
|
|
3887
|
+
},
|
|
3888
|
+
{
|
|
3889
|
+
"id": "createInvoices",
|
|
3890
|
+
"name": "Create invoice for each order",
|
|
3891
|
+
"type": "loop",
|
|
3892
|
+
"loop": {
|
|
3893
|
+
"over": "$.steps.listOrders.response.orders",
|
|
3894
|
+
"as": "order",
|
|
3895
|
+
"indexAs": "i",
|
|
3896
|
+
"steps": [
|
|
3897
|
+
{
|
|
3898
|
+
"id": "createInvoice",
|
|
3899
|
+
"name": "Create QuickBooks invoice",
|
|
3900
|
+
"type": "action",
|
|
3901
|
+
"onError": { "strategy": "continue" },
|
|
3902
|
+
"action": {
|
|
3903
|
+
"platform": "quick-books",
|
|
3904
|
+
"actionId": "QB_CREATE_INVOICE_ACTION_ID",
|
|
3905
|
+
"connectionKey": "$.input.qbConnectionKey",
|
|
3906
|
+
"data": {
|
|
3907
|
+
"Line": [
|
|
3908
|
+
{
|
|
3909
|
+
"Amount": "$.loop.order.total_price",
|
|
3910
|
+
"Description": "Shopify Order #{{$.loop.order.order_number}}"
|
|
3911
|
+
}
|
|
3912
|
+
]
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
]
|
|
3917
|
+
}
|
|
3918
|
+
},
|
|
3919
|
+
{
|
|
3920
|
+
"id": "summary",
|
|
3921
|
+
"name": "Generate summary",
|
|
3922
|
+
"type": "transform",
|
|
3923
|
+
"transform": {
|
|
3924
|
+
"expression": "({ totalOrders: $.steps.listOrders.response.orders.length, processed: $.steps.createInvoices.output.length })"
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
]
|
|
3928
|
+
}
|
|
3929
|
+
\`\`\`
|
|
3930
|
+
|
|
3931
|
+
## CLI Commands Reference
|
|
3932
|
+
|
|
3933
|
+
\`\`\`bash
|
|
3934
|
+
# Create a flow
|
|
3935
|
+
one --agent flow create <key> --definition '<json>'
|
|
3936
|
+
|
|
3937
|
+
# List all flows
|
|
3938
|
+
one --agent flow list
|
|
3939
|
+
|
|
3940
|
+
# Validate a flow
|
|
3941
|
+
one --agent flow validate <key>
|
|
3942
|
+
|
|
3943
|
+
# Execute a flow
|
|
3944
|
+
one --agent flow execute <key> -i connectionKey=value -i param=value
|
|
3945
|
+
|
|
3946
|
+
# Execute with dry run (validate only)
|
|
3947
|
+
one --agent flow execute <key> --dry-run -i connectionKey=value
|
|
3948
|
+
|
|
3949
|
+
# Execute with verbose output
|
|
3950
|
+
one --agent flow execute <key> -v -i connectionKey=value
|
|
3951
|
+
|
|
3952
|
+
# List flow runs
|
|
3953
|
+
one --agent flow runs [flowKey]
|
|
3954
|
+
|
|
3955
|
+
# Resume a paused/failed run
|
|
3956
|
+
one --agent flow resume <runId>
|
|
3957
|
+
\`\`\`
|
|
3958
|
+
|
|
3959
|
+
## Important Notes
|
|
3960
|
+
|
|
3961
|
+
- **Always use \`--agent\` flag** for structured JSON output
|
|
3962
|
+
- **Always call \`one actions knowledge\`** before adding an action step to a flow
|
|
3963
|
+
- Platform names are **kebab-case** (e.g., \`hub-spot\`, not \`HubSpot\`)
|
|
3964
|
+
- Connection keys are **inputs**, not hardcoded \u2014 makes flows portable and shareable
|
|
3965
|
+
- Use \`$.input.*\` for input values, \`$.steps.*\` for step results
|
|
3966
|
+
- Action IDs in examples (like \`STRIPE_SEARCH_CUSTOMERS_ACTION_ID\`) are placeholders \u2014 always use \`one actions search\` to find the real IDs
|
|
3967
|
+
`;
|
|
3968
|
+
var TOPICS = [
|
|
3969
|
+
{ topic: "overview", description: "Setup, --agent flag, discovery workflow" },
|
|
3970
|
+
{ topic: "actions", description: "Search, read docs, and execute platform actions" },
|
|
3971
|
+
{ topic: "flows", description: "Build and execute multi-step API workflows" },
|
|
3972
|
+
{ topic: "all", description: "Complete guide (all topics combined)" }
|
|
3973
|
+
];
|
|
3974
|
+
function getGuideContent(topic) {
|
|
3975
|
+
switch (topic) {
|
|
3976
|
+
case "overview":
|
|
3977
|
+
return { title: "One CLI \u2014 Agent Guide: Overview", content: GUIDE_OVERVIEW };
|
|
3978
|
+
case "actions":
|
|
3979
|
+
return { title: "One CLI \u2014 Agent Guide: Actions", content: GUIDE_ACTIONS };
|
|
3980
|
+
case "flows":
|
|
3981
|
+
return { title: "One CLI \u2014 Agent Guide: Flows", content: GUIDE_FLOWS };
|
|
3982
|
+
case "all":
|
|
3983
|
+
return {
|
|
3984
|
+
title: "One CLI \u2014 Agent Guide: Complete",
|
|
3985
|
+
content: [GUIDE_OVERVIEW, GUIDE_ACTIONS, GUIDE_FLOWS].join("\n---\n\n")
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
function getAvailableTopics() {
|
|
3990
|
+
return TOPICS;
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
// src/commands/guide.ts
|
|
3994
|
+
var VALID_TOPICS = ["overview", "actions", "flows", "all"];
|
|
3995
|
+
async function guideCommand(topic = "all") {
|
|
3996
|
+
if (!VALID_TOPICS.includes(topic)) {
|
|
3997
|
+
error(
|
|
3998
|
+
`Unknown topic "${topic}". Available topics: ${VALID_TOPICS.join(", ")}`
|
|
3999
|
+
);
|
|
4000
|
+
}
|
|
4001
|
+
const { title, content } = getGuideContent(topic);
|
|
4002
|
+
const availableTopics = getAvailableTopics();
|
|
4003
|
+
if (isAgentMode()) {
|
|
4004
|
+
json({ topic, title, content, availableTopics });
|
|
4005
|
+
return;
|
|
4006
|
+
}
|
|
4007
|
+
intro2(pc8.bgCyan(pc8.black(" One Guide ")));
|
|
4008
|
+
console.log();
|
|
4009
|
+
console.log(content);
|
|
4010
|
+
console.log(pc8.dim("\u2500".repeat(60)));
|
|
4011
|
+
console.log(
|
|
4012
|
+
pc8.dim("Available topics: ") + availableTopics.map((t) => pc8.cyan(t.topic)).join(", ")
|
|
4013
|
+
);
|
|
4014
|
+
console.log(pc8.dim(`Run ${pc8.cyan("one guide <topic>")} for a specific section.`));
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
// src/index.ts
|
|
4018
|
+
var require2 = createRequire(import.meta.url);
|
|
4019
|
+
var { version } = require2("../package.json");
|
|
4020
|
+
var program = new Command();
|
|
4021
|
+
program.name("one").option("--agent", "Machine-readable JSON output (no colors, spinners, or prompts)").description(`One CLI \u2014 Connect AI agents to 200+ platforms through one interface.
|
|
4022
|
+
|
|
4023
|
+
Setup:
|
|
4024
|
+
one init Set up API key and install MCP server
|
|
4025
|
+
one add <platform> Connect a platform via OAuth (e.g. gmail, slack, shopify)
|
|
4026
|
+
one config Configure access control (permissions, scoping)
|
|
4027
|
+
|
|
4028
|
+
Workflow (use these in order):
|
|
4029
|
+
1. one list List your connected platforms and connection keys
|
|
4030
|
+
2. one actions search <platform> <q> Search for actions using natural language
|
|
4031
|
+
3. one actions knowledge <plat> <id> Get full docs for an action (ALWAYS do this before execute)
|
|
4032
|
+
4. one actions execute <p> <id> <key> Execute the action
|
|
4033
|
+
|
|
4034
|
+
Guide:
|
|
4035
|
+
one guide [topic] Full CLI guide (topics: overview, actions, flows, all)
|
|
4036
|
+
|
|
4037
|
+
Flows (multi-step workflows):
|
|
4038
|
+
one flow list List saved flows
|
|
4039
|
+
one flow create [key] Create a flow from JSON
|
|
4040
|
+
one flow execute <key> Execute a flow
|
|
4041
|
+
one flow validate <key> Validate a flow
|
|
4042
|
+
|
|
4043
|
+
Example \u2014 send an email through Gmail:
|
|
4044
|
+
$ one list
|
|
4045
|
+
# Find: gmail operational live::gmail::default::abc123
|
|
4046
|
+
|
|
4047
|
+
$ one actions search gmail "send email" -t execute
|
|
4048
|
+
# Find: POST Send Email conn_mod_def::xxx::yyy
|
|
4049
|
+
|
|
4050
|
+
$ one actions knowledge gmail conn_mod_def::xxx::yyy
|
|
4051
|
+
# Read the docs: required fields are to, subject, body, connectionKey
|
|
4052
|
+
|
|
4053
|
+
$ one actions execute gmail conn_mod_def::xxx::yyy live::gmail::default::abc123 \\
|
|
4054
|
+
-d '{"to":"j@example.com","subject":"Hello","body":"Hi!","connectionKey":"live::gmail::default::abc123"}'
|
|
4055
|
+
|
|
4056
|
+
Platform names are always kebab-case (e.g. hub-spot, ship-station, google-calendar).
|
|
4057
|
+
Run 'one platforms' to browse all 200+ available platforms.`).version(version);
|
|
4058
|
+
program.hook("preAction", (thisCommand) => {
|
|
4059
|
+
const opts = program.opts();
|
|
4060
|
+
if (opts.agent) {
|
|
4061
|
+
setAgentMode(true);
|
|
4062
|
+
}
|
|
4063
|
+
});
|
|
4064
|
+
program.command("init").description("Set up One and install MCP to your AI agents").option("-y, --yes", "Skip confirmations").option("-g, --global", "Install MCP globally (available in all projects)").option("-p, --project", "Install MCP for this project only (creates .mcp.json)").action(async (options) => {
|
|
4065
|
+
await initCommand(options);
|
|
4066
|
+
});
|
|
4067
|
+
program.command("config").description("Configure MCP access control (permissions, connections, actions)").action(async () => {
|
|
4068
|
+
await configCommand();
|
|
4069
|
+
});
|
|
1941
4070
|
var connection = program.command("connection").description("Manage connections");
|
|
1942
4071
|
connection.command("add [platform]").alias("a").description("Add a new connection").action(async (platform) => {
|
|
1943
4072
|
await connectionAddCommand(platform);
|
|
@@ -1949,13 +4078,13 @@ program.command("platforms").alias("p").description("List available platforms").
|
|
|
1949
4078
|
await platformsCommand(options);
|
|
1950
4079
|
});
|
|
1951
4080
|
var actions = program.command("actions").alias("a").description("Search, explore, and execute platform actions (workflow: search \u2192 knowledge \u2192 execute)");
|
|
1952
|
-
actions.command("search <platform> <query>").description('Search for actions on a platform (e.g.
|
|
4081
|
+
actions.command("search <platform> <query>").description('Search for actions on a platform (e.g. one actions search gmail "send email")').option("-t, --type <type>", "execute (to run it) or knowledge (to learn about it). Default: knowledge").action(async (platform, query, options) => {
|
|
1953
4082
|
await actionsSearchCommand(platform, query, options);
|
|
1954
4083
|
});
|
|
1955
4084
|
actions.command("knowledge <platform> <actionId>").alias("k").description("Get full docs for an action \u2014 MUST call before execute to know required params").action(async (platform, actionId) => {
|
|
1956
4085
|
await actionsKnowledgeCommand(platform, actionId);
|
|
1957
4086
|
});
|
|
1958
|
-
actions.command("execute <platform> <actionId> <connectionKey>").alias("x").description('Execute an action \u2014 pass connectionKey from "
|
|
4087
|
+
actions.command("execute <platform> <actionId> <connectionKey>").alias("x").description('Execute an action \u2014 pass connectionKey from "one list", actionId from "actions search"').option("-d, --data <json>", "Request body as JSON").option("--path-vars <json>", "Path variables as JSON").option("--query-params <json>", "Query parameters as JSON").option("--headers <json>", "Additional headers as JSON").option("--form-data", "Send as multipart/form-data").option("--form-url-encoded", "Send as application/x-www-form-urlencoded").action(async (platform, actionId, connectionKey, options) => {
|
|
1959
4088
|
await actionsExecuteCommand(platform, actionId, connectionKey, {
|
|
1960
4089
|
data: options.data,
|
|
1961
4090
|
pathVars: options.pathVars,
|
|
@@ -1965,6 +4094,28 @@ actions.command("execute <platform> <actionId> <connectionKey>").alias("x").desc
|
|
|
1965
4094
|
formUrlEncoded: options.formUrlEncoded
|
|
1966
4095
|
});
|
|
1967
4096
|
});
|
|
4097
|
+
var flow = program.command("flow").alias("f").description("Create, execute, and manage multi-step API workflows");
|
|
4098
|
+
flow.command("create [key]").description("Create a new flow from JSON definition").option("--definition <json>", "Flow definition as JSON string").option("-o, --output <path>", "Custom output path (default .one/flows/<key>.flow.json)").action(async (key, options) => {
|
|
4099
|
+
await flowCreateCommand(key, options);
|
|
4100
|
+
});
|
|
4101
|
+
flow.command("execute <keyOrPath>").alias("x").description("Execute a flow 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) => {
|
|
4102
|
+
await flowExecuteCommand(keyOrPath, options);
|
|
4103
|
+
});
|
|
4104
|
+
flow.command("list").alias("ls").description("List all flows in .one/flows/").action(async () => {
|
|
4105
|
+
await flowListCommand();
|
|
4106
|
+
});
|
|
4107
|
+
flow.command("validate <keyOrPath>").description("Validate a flow JSON file").action(async (keyOrPath) => {
|
|
4108
|
+
await flowValidateCommand(keyOrPath);
|
|
4109
|
+
});
|
|
4110
|
+
flow.command("resume <runId>").description("Resume a paused or failed flow run").action(async (runId) => {
|
|
4111
|
+
await flowResumeCommand(runId);
|
|
4112
|
+
});
|
|
4113
|
+
flow.command("runs [flowKey]").description("List flow runs (optionally filtered by flow key)").action(async (flowKey) => {
|
|
4114
|
+
await flowRunsCommand(flowKey);
|
|
4115
|
+
});
|
|
4116
|
+
program.command("guide [topic]").description("Full CLI usage guide for agents (topics: overview, actions, flows, all)").action(async (topic) => {
|
|
4117
|
+
await guideCommand(topic);
|
|
4118
|
+
});
|
|
1968
4119
|
program.command("add [platform]").description("Shortcut for: connection add").action(async (platform) => {
|
|
1969
4120
|
await connectionAddCommand(platform);
|
|
1970
4121
|
});
|