@picahq/cli 1.9.3 → 1.10.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 +145 -0
- package/dist/index.js +2222 -71
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -305,8 +305,8 @@ var PicaApi = 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}`;
|
|
@@ -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];
|
|
@@ -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
|
}
|
|
@@ -1894,76 +1901,2220 @@ 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
|
|
1913
|
-
|
|
1914
|
-
Example \u2014 send an email through Gmail:
|
|
1915
|
-
$ pica list
|
|
1916
|
-
# Find: gmail operational live::gmail::default::abc123
|
|
1917
|
-
|
|
1918
|
-
$ pica actions search gmail "send email" -t execute
|
|
1919
|
-
# Find: POST Send Email conn_mod_def::xxx::yyy
|
|
1904
|
+
// src/commands/flow.ts
|
|
1905
|
+
import pc7 from "picocolors";
|
|
1920
1906
|
|
|
1921
|
-
|
|
1922
|
-
|
|
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
|
+
}
|
|
1923
2224
|
|
|
1924
|
-
|
|
1925
|
-
|
|
2225
|
+
// src/lib/flow-runner.ts
|
|
2226
|
+
import fs4 from "fs";
|
|
2227
|
+
import path4 from "path";
|
|
2228
|
+
import crypto from "crypto";
|
|
1926
2229
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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
|
+
}
|
|
1933
2260
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
})
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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()
|
|
1966
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
|
+
}
|
|
2611
|
+
|
|
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
|
+
}
|
|
2842
|
+
|
|
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 `pica init` first.");
|
|
2849
|
+
}
|
|
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(" Pica 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(`pica flow validate ${flow2.key}`)}
|
|
2936
|
+
Execute: ${pc7.cyan(`pica flow execute ${flow2.key}`)}`);
|
|
2937
|
+
}
|
|
2938
|
+
async function flowExecuteCommand(keyOrPath, options) {
|
|
2939
|
+
intro2(pc7.bgCyan(pc7.black(" Pica Flow ")));
|
|
2940
|
+
const { apiKey, permissions, actionIds } = getConfig2();
|
|
2941
|
+
const api = new PicaApi(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(" Pica 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: pica 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(" Pica 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(" Pica 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 PicaApi(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(" Pica 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 = `# Pica CLI \u2014 Agent Guide
|
|
3212
|
+
|
|
3213
|
+
## Setup
|
|
3214
|
+
|
|
3215
|
+
1. Run \`pica init\` to configure your API key
|
|
3216
|
+
2. Run \`pica add <platform>\` to connect platforms via OAuth
|
|
3217
|
+
3. Run \`pica --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
|
+
pica --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. \`pica --agent connection list\` \u2014 See connected platforms and connection keys
|
|
3240
|
+
2. \`pica --agent actions search <platform> <query>\` \u2014 Find actions
|
|
3241
|
+
3. \`pica --agent actions knowledge <platform> <actionId>\` \u2014 Read full docs (REQUIRED before execute)
|
|
3242
|
+
4. \`pica --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. \`pica --agent flow create <key> --definition '<json>'\`
|
|
3248
|
+
4. \`pica --agent flow execute <key> -i param=value\`
|
|
3249
|
+
|
|
3250
|
+
Platform names are always kebab-case (e.g., \`hub-spot\`, \`google-calendar\`).
|
|
3251
|
+
Run \`pica platforms\` to browse all 200+ available platforms.
|
|
3252
|
+
`;
|
|
3253
|
+
var GUIDE_ACTIONS = `# Pica Actions CLI Workflow
|
|
3254
|
+
|
|
3255
|
+
You have access to the Pica CLI which lets you interact with 200+ third-party platforms through their APIs. The CLI handles authentication, request building, and execution through Pica'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
|
+
pica --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
|
+
pica --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
|
+
pica --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
|
+
pica --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
|
+
pica --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
|
+
pica --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 \`pica 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
|
+
pica --agent actions execute shopify <actionId> <connectionKey>
|
|
3351
|
+
|
|
3352
|
+
# POST with data
|
|
3353
|
+
pica --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
|
+
pica --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 \`pica config\` \u2014 if execution is blocked, the user may need to adjust their permissions
|
|
3385
|
+
`;
|
|
3386
|
+
var GUIDE_FLOWS = `# Pica Flow \u2014 Multi-Step API Workflows
|
|
3387
|
+
|
|
3388
|
+
You have access to the Pica 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 \`pica --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
|
+
pica --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
|
+
pica --agent actions search <platform> "<query>" -t execute
|
|
3414
|
+
|
|
3415
|
+
# Read the full docs \u2014 REQUIRED before adding to a flow
|
|
3416
|
+
pica --agent actions knowledge <platform> <actionId>
|
|
3417
|
+
\`\`\`
|
|
3418
|
+
|
|
3419
|
+
**CRITICAL:** You MUST call \`pica 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
|
+
pica --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
|
+
pica --agent flow validate <key>
|
|
3440
|
+
\`\`\`
|
|
3441
|
+
|
|
3442
|
+
### Step 6: Execute
|
|
3443
|
+
|
|
3444
|
+
\`\`\`bash
|
|
3445
|
+
pica --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 pica connection list",
|
|
3461
|
+
"connection": { "platform": "stripe" }
|
|
3462
|
+
},
|
|
3463
|
+
"gmailConnectionKey": {
|
|
3464
|
+
"type": "string",
|
|
3465
|
+
"required": true,
|
|
3466
|
+
"description": "Gmail connection key from pica 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 Pica 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 \`pica --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: \`pica --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
|
+
pica --agent flow create <key> --definition '<json>'
|
|
3936
|
+
|
|
3937
|
+
# List all flows
|
|
3938
|
+
pica --agent flow list
|
|
3939
|
+
|
|
3940
|
+
# Validate a flow
|
|
3941
|
+
pica --agent flow validate <key>
|
|
3942
|
+
|
|
3943
|
+
# Execute a flow
|
|
3944
|
+
pica --agent flow execute <key> -i connectionKey=value -i param=value
|
|
3945
|
+
|
|
3946
|
+
# Execute with dry run (validate only)
|
|
3947
|
+
pica --agent flow execute <key> --dry-run -i connectionKey=value
|
|
3948
|
+
|
|
3949
|
+
# Execute with verbose output
|
|
3950
|
+
pica --agent flow execute <key> -v -i connectionKey=value
|
|
3951
|
+
|
|
3952
|
+
# List flow runs
|
|
3953
|
+
pica --agent flow runs [flowKey]
|
|
3954
|
+
|
|
3955
|
+
# Resume a paused/failed run
|
|
3956
|
+
pica --agent flow resume <runId>
|
|
3957
|
+
\`\`\`
|
|
3958
|
+
|
|
3959
|
+
## Important Notes
|
|
3960
|
+
|
|
3961
|
+
- **Always use \`--agent\` flag** for structured JSON output
|
|
3962
|
+
- **Always call \`pica 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 \`pica 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: "Pica CLI \u2014 Agent Guide: Overview", content: GUIDE_OVERVIEW };
|
|
3978
|
+
case "actions":
|
|
3979
|
+
return { title: "Pica CLI \u2014 Agent Guide: Actions", content: GUIDE_ACTIONS };
|
|
3980
|
+
case "flows":
|
|
3981
|
+
return { title: "Pica CLI \u2014 Agent Guide: Flows", content: GUIDE_FLOWS };
|
|
3982
|
+
case "all":
|
|
3983
|
+
return {
|
|
3984
|
+
title: "Pica 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(" Pica 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("pica 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("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.
|
|
4022
|
+
|
|
4023
|
+
Setup:
|
|
4024
|
+
pica init Set up API key and install MCP server
|
|
4025
|
+
pica add <platform> Connect a platform via OAuth (e.g. gmail, slack, shopify)
|
|
4026
|
+
pica config Configure access control (permissions, scoping)
|
|
4027
|
+
|
|
4028
|
+
Workflow (use these in order):
|
|
4029
|
+
1. pica list List your connected platforms and connection keys
|
|
4030
|
+
2. pica actions search <platform> <q> Search for actions using natural language
|
|
4031
|
+
3. pica actions knowledge <plat> <id> Get full docs for an action (ALWAYS do this before execute)
|
|
4032
|
+
4. pica actions execute <p> <id> <key> Execute the action
|
|
4033
|
+
|
|
4034
|
+
Guide:
|
|
4035
|
+
pica guide [topic] Full CLI guide (topics: overview, actions, flows, all)
|
|
4036
|
+
|
|
4037
|
+
Flows (multi-step workflows):
|
|
4038
|
+
pica flow list List saved flows
|
|
4039
|
+
pica flow create [key] Create a flow from JSON
|
|
4040
|
+
pica flow execute <key> Execute a flow
|
|
4041
|
+
pica flow validate <key> Validate a flow
|
|
4042
|
+
|
|
4043
|
+
Example \u2014 send an email through Gmail:
|
|
4044
|
+
$ pica list
|
|
4045
|
+
# Find: gmail operational live::gmail::default::abc123
|
|
4046
|
+
|
|
4047
|
+
$ pica actions search gmail "send email" -t execute
|
|
4048
|
+
# Find: POST Send Email conn_mod_def::xxx::yyy
|
|
4049
|
+
|
|
4050
|
+
$ pica actions knowledge gmail conn_mod_def::xxx::yyy
|
|
4051
|
+
# Read the docs: required fields are to, subject, body, connectionKey
|
|
4052
|
+
|
|
4053
|
+
$ pica 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 'pica 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 Pica 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
|
+
});
|
|
4070
|
+
var connection = program.command("connection").description("Manage connections");
|
|
4071
|
+
connection.command("add [platform]").alias("a").description("Add a new connection").action(async (platform) => {
|
|
4072
|
+
await connectionAddCommand(platform);
|
|
4073
|
+
});
|
|
4074
|
+
connection.command("list").alias("ls").description("List your connections").action(async () => {
|
|
4075
|
+
await connectionListCommand();
|
|
4076
|
+
});
|
|
4077
|
+
program.command("platforms").alias("p").description("List available platforms").option("-c, --category <category>", "Filter by category").option("--json", "Output as JSON").action(async (options) => {
|
|
4078
|
+
await platformsCommand(options);
|
|
4079
|
+
});
|
|
4080
|
+
var actions = program.command("actions").alias("a").description("Search, explore, and execute platform actions (workflow: search \u2192 knowledge \u2192 execute)");
|
|
4081
|
+
actions.command("search <platform> <query>").description('Search for actions on a platform (e.g. pica 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) => {
|
|
4082
|
+
await actionsSearchCommand(platform, query, options);
|
|
4083
|
+
});
|
|
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) => {
|
|
4085
|
+
await actionsKnowledgeCommand(platform, actionId);
|
|
4086
|
+
});
|
|
4087
|
+
actions.command("execute <platform> <actionId> <connectionKey>").alias("x").description('Execute an action \u2014 pass connectionKey from "pica 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) => {
|
|
4088
|
+
await actionsExecuteCommand(platform, actionId, connectionKey, {
|
|
4089
|
+
data: options.data,
|
|
4090
|
+
pathVars: options.pathVars,
|
|
4091
|
+
queryParams: options.queryParams,
|
|
4092
|
+
headers: options.headers,
|
|
4093
|
+
formData: options.formData,
|
|
4094
|
+
formUrlEncoded: options.formUrlEncoded
|
|
4095
|
+
});
|
|
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);
|
|
1967
4118
|
});
|
|
1968
4119
|
program.command("add [platform]").description("Shortcut for: connection add").action(async (platform) => {
|
|
1969
4120
|
await connectionAddCommand(platform);
|