@withone/cli 1.29.0 → 1.31.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 CHANGED
@@ -246,6 +246,30 @@ one actions execute stripe <actionId> <connectionKey> \
246
246
  | `--form-data` | Send as multipart/form-data |
247
247
  | `--form-url-encoded` | Send as application/x-www-form-urlencoded |
248
248
  | `--dry-run` | Show the request without executing it |
249
+ | `--mock` | Return example response without making an API call |
250
+ | `--skip-validation` | Skip input validation against the action schema |
251
+
252
+ The CLI validates required parameters (path variables, query params, body fields) against the action schema before executing. Missing params return a clear error with the flag name and description. Pass `--skip-validation` to bypass.
253
+
254
+ #### Parallel execution
255
+
256
+ Execute multiple actions concurrently with `--parallel`, separating each action with `--`:
257
+
258
+ ```bash
259
+ one --agent actions execute --parallel \
260
+ gmail send-email conn123 -d '{"to":"a@b.com","subject":"Hi","body":"Hello"}' \
261
+ -- slack post-message conn456 -d '{"channel":"#general","text":"Done"}' \
262
+ -- google-sheets append-row conn789 -d '{"values":["x","y"]}'
263
+ ```
264
+
265
+ Each segment follows the same format: `<platform> <actionId> <connectionKey> [-d ...] [--path-vars ...] [--query-params ...]`. All segments are validated upfront before any execution starts. Results are collected via `Promise.allSettled` — if one fails, the rest still complete.
266
+
267
+ | Option | What it does |
268
+ |--------|-------------|
269
+ | `--parallel` | Enable parallel mode |
270
+ | `--max-concurrency <n>` | Max concurrent actions per batch (default: 5) |
271
+
272
+ Agent-mode output includes `parallel: true`, per-action `status`/`durationMs`/`response`, plus `totalDurationMs`, `succeeded`, and `failed` counts.
249
273
 
250
274
  ### `one cache`
251
275
 
@@ -271,6 +295,48 @@ Default TTL is 1 hour. Configure via `ONE_CACHE_TTL` environment variable or `ca
271
295
 
272
296
  Note: `actions execute` is never cached — it always hits the API fresh.
273
297
 
298
+ ### `one sync`
299
+
300
+ Sync platform data into local SQLite for instant queries, full-text search, scheduled refresh, and change-driven automation. The sync engine (`better-sqlite3`) is an optional dependency — install it once per machine:
301
+
302
+ ```bash
303
+ one sync install && one sync doctor
304
+ ```
305
+
306
+ ```bash
307
+ # Discover → init (one command: infer + auto-resolve key + auto-test) → run
308
+ one sync models stripe
309
+ one sync init stripe balanceTransactions # connectionKey auto-resolved, test auto-run
310
+ one sync run stripe --since 90d
311
+
312
+ # Query, search, SQL
313
+ one sync query stripe/balanceTransactions --where "status=available" --limit 20
314
+ one sync search "refund"
315
+ one sync sql stripe "SELECT count(*) FROM balanceTransactions"
316
+
317
+ # Schedule unattended syncs + change hooks
318
+ one sync schedule add stripe --every 1h
319
+ one sync init stripe balanceTransactions --config '{"onInsert":"one flow execute handle-new-txn"}'
320
+
321
+ # Deletion detection
322
+ one sync run stripe --full-refresh
323
+ ```
324
+
325
+ | Subcommand | What it does |
326
+ |------------|-------------|
327
+ | `install` / `doctor` | Install + verify the SQLite engine |
328
+ | `models <platform>` | Discover available data models |
329
+ | `init <platform> <model>` | Create profile (auto-infers all fields, auto-resolves key, auto-runs test) |
330
+ | `test <platform>/<model>` | Validate + auto-fix profile from real API response (also runs inside init) |
331
+ | `run <platform>` | Sync data (`--full-refresh`, `--since`, `--dry-run`) |
332
+ | `query <platform>/<model>` | Query with `--where`, `--after/before`, `--refresh` |
333
+ | `search <query>` | FTS5 across all synced data |
334
+ | `sql <platform> <sql>` | Raw SELECT queries |
335
+ | `schedule add/list/status/remove/repair` | Cron-backed scheduled syncs with drift detection |
336
+ | `remove <platform>` | Delete local data (`--dry-run` to preview) |
337
+
338
+ Change hooks (`onInsert`, `onUpdate`, `onChange`) fire per-page during sync — pipe to a shell command, a flow, or an event log. Run `one guide sync` for the full reference.
339
+
274
340
  ### `one guide [topic]`
275
341
 
276
342
  Get the full CLI usage guide, designed for AI agents that only have the binary (no MCP, no IDE skills).
@@ -285,7 +351,7 @@ one --agent guide # full guide as structured JSON
285
351
  one --agent guide flows # single topic as JSON
286
352
  ```
287
353
 
288
- Topics: `overview`, `actions`, `flows`, `relay`, `cache`, `all` (default).
354
+ Topics: `overview`, `actions`, `flows`, `relay`, `cache`, `sync`, `all` (default).
289
355
 
290
356
  In agent mode (`--agent`), the JSON response includes the guide content and an `availableTopics` array so agents can discover what sections exist.
291
357
 
@@ -333,7 +399,8 @@ Press Ctrl+C during execution to pause - the run can be resumed later with `one
333
399
  |--------|-------------|
334
400
  | `-i, --input <name=value>` | Input parameter (repeatable) |
335
401
  | `--dry-run` | Validate and show execution plan without running |
336
- | `--mock` | With `--dry-run`: execute transforms/code with mock API responses |
402
+ | `--mock` | With `--dry-run`: execute transforms/code with realistic mock API responses |
403
+ | `--skip-validation` | Skip input validation against action schemas |
337
404
  | `--allow-bash` | Allow bash step execution (disabled by default for security) |
338
405
  | `-v, --verbose` | Show full request/response for each step |
339
406
 
@@ -12,12 +12,24 @@ import { promisify } from "util";
12
12
 
13
13
  // src/lib/api.ts
14
14
  var ApiError = class extends Error {
15
- constructor(status, message) {
15
+ constructor(status, message, retryAfterSeconds) {
16
16
  super(message);
17
17
  this.status = status;
18
+ this.retryAfterSeconds = retryAfterSeconds;
18
19
  this.name = "ApiError";
19
20
  }
20
21
  };
22
+ function parseRetryAfter(value) {
23
+ if (!value) return void 0;
24
+ const seconds = parseInt(value, 10);
25
+ if (!isNaN(seconds)) return seconds;
26
+ const dateMs = Date.parse(value);
27
+ if (!isNaN(dateMs)) {
28
+ const delta = Math.ceil((dateMs - Date.now()) / 1e3);
29
+ return delta > 0 ? delta : 0;
30
+ }
31
+ return void 0;
32
+ }
21
33
  var OneApi = class {
22
34
  constructor(apiKey, apiBase) {
23
35
  this.apiKey = apiKey;
@@ -92,6 +104,10 @@ var OneApi = class {
92
104
  } while (page <= totalPages);
93
105
  return allPlatforms;
94
106
  }
107
+ async listAvailableActions(platform, limit = 500) {
108
+ const response = await this.request(`/available-actions/${platform}?limit=${limit}`);
109
+ return response.rows || [];
110
+ }
95
111
  async searchActions(platform, query, agentType) {
96
112
  const isKnowledgeAgent = agentType === "knowledge";
97
113
  const queryParams = {
@@ -160,7 +176,8 @@ var OneApi = class {
160
176
  }
161
177
  if (!response.ok) {
162
178
  const text2 = await response.text();
163
- throw new ApiError(response.status, text2 || `HTTP ${response.status}`);
179
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
180
+ throw new ApiError(response.status, text2 || `HTTP ${response.status}`, retryAfter);
164
181
  }
165
182
  const etag = response.headers.get("etag") ?? null;
166
183
  const text = await response.text();
@@ -443,6 +460,73 @@ Read the API documentation below to identify which parameters are path variables
443
460
  ${knowledge}`;
444
461
  }
445
462
 
463
+ // src/lib/validate.ts
464
+ var SCHEMA_GROUP_TO_FLAG = {
465
+ path: "--path-vars",
466
+ query: "--query-params",
467
+ body: "-d"
468
+ };
469
+ function validateActionInput(action, args) {
470
+ const inputSchema = action.ioSchema?.inputSchema;
471
+ if (!inputSchema?.properties) return { valid: true };
472
+ const argMap = {
473
+ path: args.pathVariables,
474
+ query: args.queryParams,
475
+ body: args.data
476
+ };
477
+ const missing = [];
478
+ for (const [group, flag] of Object.entries(SCHEMA_GROUP_TO_FLAG)) {
479
+ const groupSchema = inputSchema.properties[group];
480
+ if (!groupSchema?.required?.length) continue;
481
+ const provided = argMap[group] ?? {};
482
+ for (const param of groupSchema.required) {
483
+ if (provided[param] === void 0 || provided[param] === null || provided[param] === "") {
484
+ missing.push({
485
+ flag,
486
+ param,
487
+ description: groupSchema.properties?.[param]?.description
488
+ });
489
+ }
490
+ }
491
+ }
492
+ if (missing.length === 0) return { valid: true };
493
+ return { valid: false, missing };
494
+ }
495
+
496
+ // src/lib/dot-path.ts
497
+ function getByDotPath(obj, dotPath) {
498
+ const parts = dotPath.split(".").flatMap((part) => {
499
+ const bracketMatch = part.match(/^([^[]+)\[(\d+)\]$/);
500
+ if (bracketMatch) {
501
+ return [bracketMatch[1], bracketMatch[2]];
502
+ }
503
+ return [part];
504
+ });
505
+ let current = obj;
506
+ for (const part of parts) {
507
+ if (current === null || current === void 0) return void 0;
508
+ if (Array.isArray(current) && /^\d+$/.test(part)) {
509
+ current = current[parseInt(part, 10)];
510
+ } else if (typeof current === "object") {
511
+ current = current[part];
512
+ } else {
513
+ return void 0;
514
+ }
515
+ }
516
+ return current;
517
+ }
518
+ function setByDotPath(obj, dotPath, value) {
519
+ const parts = dotPath.split(".");
520
+ let current = obj;
521
+ for (let i = 0; i < parts.length - 1; i++) {
522
+ if (current[parts[i]] === void 0 || current[parts[i]] === null) {
523
+ current[parts[i]] = {};
524
+ }
525
+ current = current[parts[i]];
526
+ }
527
+ current[parts[parts.length - 1]] = value;
528
+ }
529
+
446
530
  // src/lib/flow-engine.ts
447
531
  var execAsync = promisify(exec);
448
532
  function sleep2(ms) {
@@ -609,30 +693,6 @@ function evaluateExpression(expr, context) {
609
693
  const fn = new Function("$", `return (${expr})`);
610
694
  return fn(context);
611
695
  }
612
- function getByDotPath(obj, dotPath) {
613
- const parts = dotPath.split(".");
614
- let current = obj;
615
- for (const part of parts) {
616
- if (current === null || current === void 0) return void 0;
617
- if (typeof current === "object") {
618
- current = current[part];
619
- } else {
620
- return void 0;
621
- }
622
- }
623
- return current;
624
- }
625
- function setByDotPath(obj, dotPath, value) {
626
- const parts = dotPath.split(".");
627
- let current = obj;
628
- for (let i = 0; i < parts.length - 1; i++) {
629
- if (current[parts[i]] === void 0 || current[parts[i]] === null) {
630
- current[parts[i]] = {};
631
- }
632
- current = current[parts[i]];
633
- }
634
- current[parts[parts.length - 1]] = value;
635
- }
636
696
  var ALLOWED_MODULES = {
637
697
  buffer: () => import("buffer"),
638
698
  crypto: () => import("crypto"),
@@ -672,7 +732,7 @@ function stripCodeFences(text) {
672
732
  const match = trimmed.match(/^```(?:\w*)\s*\n([\s\S]*?)\n\s*```\s*$/);
673
733
  return match ? match[1].trim() : trimmed;
674
734
  }
675
- async function executeActionStep(step, context, api, permissions, allowedActionIds) {
735
+ async function executeActionStep(step, context, api, permissions, allowedActionIds, options) {
676
736
  const action = step.action;
677
737
  const platform = resolveValue(action.platform, context);
678
738
  const actionId = resolveValue(action.actionId, context);
@@ -688,6 +748,13 @@ async function executeActionStep(step, context, api, permissions, allowedActionI
688
748
  if (!isMethodAllowed(actionDetails.method, permissions)) {
689
749
  throw new Error(`Method "${actionDetails.method}" is not allowed under "${permissions}" permission level`);
690
750
  }
751
+ if (!options.skipValidation) {
752
+ const validation = validateActionInput(actionDetails, { data, pathVariables: pathVars, queryParams });
753
+ if (!validation.valid) {
754
+ const details = validation.missing.map((m) => `${m.flag} is missing "${m.param}"`).join("; ");
755
+ throw new Error(`Validation failed for step "${step.id}": ${details}. Pass --skip-validation to bypass.`);
756
+ }
757
+ }
691
758
  const result = await api.executePassthroughRequest({
692
759
  platform,
693
760
  actionId,
@@ -973,7 +1040,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
973
1040
  if (flowStack.includes(resolvedKey)) {
974
1041
  throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
975
1042
  }
976
- const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-AK5W4GLF.js");
1043
+ const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-HT74FKPD.js");
977
1044
  const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
978
1045
  const subContext = await executeFlow(
979
1046
  subFlow,
@@ -1033,7 +1100,7 @@ async function executePaginateStep(step, context, api, permissions, allowedActio
1033
1100
  type: "action",
1034
1101
  action: resolved
1035
1102
  };
1036
- const result = await executeActionStep(syntheticStep, context, api, permissions, allowedActionIds);
1103
+ const result = await executeActionStep(syntheticStep, context, api, permissions, allowedActionIds, options);
1037
1104
  const response = result.response;
1038
1105
  const pageResults = getByDotPath(response, config.resultsField);
1039
1106
  if (Array.isArray(pageResults)) allResults.push(...pageResults);
@@ -1049,7 +1116,7 @@ async function executePaginateStep(step, context, api, permissions, allowedActio
1049
1116
  type: "action",
1050
1117
  action: resolvedAction
1051
1118
  };
1052
- const result = await executeActionStep(syntheticStep, context, api, permissions, allowedActionIds);
1119
+ const result = await executeActionStep(syntheticStep, context, api, permissions, allowedActionIds, options);
1053
1120
  const response = result.response;
1054
1121
  const pageResults = getByDotPath(response, config.resultsField);
1055
1122
  if (Array.isArray(pageResults)) allResults.push(...pageResults);
@@ -1204,10 +1271,32 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
1204
1271
  checkRequires(step, context);
1205
1272
  if (options.mock && (step.type === "action" || step.type === "paginate" || step.type === "bash")) {
1206
1273
  const resolvedConfig = step[step.type] ? resolveValue(step[step.type], context) : {};
1274
+ let mockOutput = { _mock: true, ...resolvedConfig };
1275
+ if (step.type === "action" && step.action) {
1276
+ const actionId = resolveValue(step.action.actionId, context);
1277
+ try {
1278
+ const actionDetails = await api.getActionDetails(actionId);
1279
+ if (!options.skipValidation) {
1280
+ const data = step.action.data ? resolveValue(step.action.data, context) : void 0;
1281
+ const pathVars = step.action.pathVars ? resolveValue(step.action.pathVars, context) : void 0;
1282
+ const queryParams = step.action.queryParams ? resolveValue(step.action.queryParams, context) : void 0;
1283
+ const validation = validateActionInput(actionDetails, { data, pathVariables: pathVars, queryParams });
1284
+ if (!validation.valid) {
1285
+ const details = validation.missing.map((m) => `${m.flag} is missing "${m.param}"`).join("; ");
1286
+ throw new Error(`Validation failed for step "${step.id}": ${details}. Pass --skip-validation to bypass.`);
1287
+ }
1288
+ }
1289
+ if (actionDetails.ioSchema?.ioExample?.output) {
1290
+ mockOutput = actionDetails.ioSchema.ioExample.output;
1291
+ }
1292
+ } catch (e) {
1293
+ if (e instanceof Error && e.message.startsWith("Validation failed")) throw e;
1294
+ }
1295
+ }
1207
1296
  options.onEvent?.({ event: "step:mock", stepId: step.id, type: step.type, config: resolvedConfig });
1208
1297
  const result2 = {
1209
1298
  status: "success",
1210
- output: { _mock: true, ...resolvedConfig },
1299
+ output: mockOutput,
1211
1300
  response: { _mock: true },
1212
1301
  durationMs: Date.now() - startTime
1213
1302
  };
@@ -1217,7 +1306,7 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
1217
1306
  const dispatch = async () => {
1218
1307
  switch (step.type) {
1219
1308
  case "action":
1220
- return await executeActionStep(step, context, api, permissions, allowedActionIds);
1309
+ return await executeActionStep(step, context, api, permissions, allowedActionIds, options);
1221
1310
  case "transform":
1222
1311
  return executeTransformStep(step, context);
1223
1312
  case "code":
@@ -2538,16 +2627,19 @@ function saveFlow(flow, outputPath) {
2538
2627
  }
2539
2628
 
2540
2629
  export {
2630
+ ApiError,
2541
2631
  OneApi,
2542
2632
  TimeoutError,
2543
2633
  filterByPermissions,
2544
2634
  isMethodAllowed,
2545
2635
  isActionAllowed,
2546
2636
  buildActionKnowledgeWithGuidance,
2637
+ validateActionInput,
2547
2638
  FLOW_SCHEMA,
2548
2639
  getStepTypeDescriptor,
2549
2640
  getNestedStepsKeys,
2550
2641
  generateFlowGuide,
2642
+ getByDotPath,
2551
2643
  FlowRunner,
2552
2644
  resolveFlowPath,
2553
2645
  getFlowRootDir,
@@ -11,7 +11,7 @@ import {
11
11
  saveFlow,
12
12
  summarizeFlowInputs,
13
13
  walkSteps
14
- } from "./chunk-AZV4EGKT.js";
14
+ } from "./chunk-645Z3ARQ.js";
15
15
  export {
16
16
  FlowRunner,
17
17
  collectStepTypes,