@withone/cli 1.29.0 → 1.32.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;
@@ -54,10 +66,12 @@ var OneApi = class {
54
66
  if (!text) return {};
55
67
  return JSON.parse(text);
56
68
  }
69
+ async whoami() {
70
+ return this.request("/users/whoami");
71
+ }
57
72
  async validateApiKey() {
58
73
  try {
59
- await this.listConnections();
60
- return true;
74
+ return await this.whoami();
61
75
  } catch (error) {
62
76
  if (error instanceof ApiError && error.status === 401) {
63
77
  return false;
@@ -92,6 +106,10 @@ var OneApi = class {
92
106
  } while (page <= totalPages);
93
107
  return allPlatforms;
94
108
  }
109
+ async listAvailableActions(platform, limit = 500) {
110
+ const response = await this.request(`/available-actions/${platform}?limit=${limit}`);
111
+ return response.rows || [];
112
+ }
95
113
  async searchActions(platform, query, agentType) {
96
114
  const isKnowledgeAgent = agentType === "knowledge";
97
115
  const queryParams = {
@@ -160,7 +178,8 @@ var OneApi = class {
160
178
  }
161
179
  if (!response.ok) {
162
180
  const text2 = await response.text();
163
- throw new ApiError(response.status, text2 || `HTTP ${response.status}`);
181
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
182
+ throw new ApiError(response.status, text2 || `HTTP ${response.status}`, retryAfter);
164
183
  }
165
184
  const etag = response.headers.get("etag") ?? null;
166
185
  const text = await response.text();
@@ -443,6 +462,73 @@ Read the API documentation below to identify which parameters are path variables
443
462
  ${knowledge}`;
444
463
  }
445
464
 
465
+ // src/lib/validate.ts
466
+ var SCHEMA_GROUP_TO_FLAG = {
467
+ path: "--path-vars",
468
+ query: "--query-params",
469
+ body: "-d"
470
+ };
471
+ function validateActionInput(action, args) {
472
+ const inputSchema = action.ioSchema?.inputSchema;
473
+ if (!inputSchema?.properties) return { valid: true };
474
+ const argMap = {
475
+ path: args.pathVariables,
476
+ query: args.queryParams,
477
+ body: args.data
478
+ };
479
+ const missing = [];
480
+ for (const [group, flag] of Object.entries(SCHEMA_GROUP_TO_FLAG)) {
481
+ const groupSchema = inputSchema.properties[group];
482
+ if (!groupSchema?.required?.length) continue;
483
+ const provided = argMap[group] ?? {};
484
+ for (const param of groupSchema.required) {
485
+ if (provided[param] === void 0 || provided[param] === null || provided[param] === "") {
486
+ missing.push({
487
+ flag,
488
+ param,
489
+ description: groupSchema.properties?.[param]?.description
490
+ });
491
+ }
492
+ }
493
+ }
494
+ if (missing.length === 0) return { valid: true };
495
+ return { valid: false, missing };
496
+ }
497
+
498
+ // src/lib/dot-path.ts
499
+ function getByDotPath(obj, dotPath) {
500
+ const parts = dotPath.split(".").flatMap((part) => {
501
+ const bracketMatch = part.match(/^([^[]+)\[(\d+)\]$/);
502
+ if (bracketMatch) {
503
+ return [bracketMatch[1], bracketMatch[2]];
504
+ }
505
+ return [part];
506
+ });
507
+ let current = obj;
508
+ for (const part of parts) {
509
+ if (current === null || current === void 0) return void 0;
510
+ if (Array.isArray(current) && /^\d+$/.test(part)) {
511
+ current = current[parseInt(part, 10)];
512
+ } else if (typeof current === "object") {
513
+ current = current[part];
514
+ } else {
515
+ return void 0;
516
+ }
517
+ }
518
+ return current;
519
+ }
520
+ function setByDotPath(obj, dotPath, value) {
521
+ const parts = dotPath.split(".");
522
+ let current = obj;
523
+ for (let i = 0; i < parts.length - 1; i++) {
524
+ if (current[parts[i]] === void 0 || current[parts[i]] === null) {
525
+ current[parts[i]] = {};
526
+ }
527
+ current = current[parts[i]];
528
+ }
529
+ current[parts[parts.length - 1]] = value;
530
+ }
531
+
446
532
  // src/lib/flow-engine.ts
447
533
  var execAsync = promisify(exec);
448
534
  function sleep2(ms) {
@@ -609,30 +695,6 @@ function evaluateExpression(expr, context) {
609
695
  const fn = new Function("$", `return (${expr})`);
610
696
  return fn(context);
611
697
  }
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
698
  var ALLOWED_MODULES = {
637
699
  buffer: () => import("buffer"),
638
700
  crypto: () => import("crypto"),
@@ -672,7 +734,7 @@ function stripCodeFences(text) {
672
734
  const match = trimmed.match(/^```(?:\w*)\s*\n([\s\S]*?)\n\s*```\s*$/);
673
735
  return match ? match[1].trim() : trimmed;
674
736
  }
675
- async function executeActionStep(step, context, api, permissions, allowedActionIds) {
737
+ async function executeActionStep(step, context, api, permissions, allowedActionIds, options) {
676
738
  const action = step.action;
677
739
  const platform = resolveValue(action.platform, context);
678
740
  const actionId = resolveValue(action.actionId, context);
@@ -688,6 +750,13 @@ async function executeActionStep(step, context, api, permissions, allowedActionI
688
750
  if (!isMethodAllowed(actionDetails.method, permissions)) {
689
751
  throw new Error(`Method "${actionDetails.method}" is not allowed under "${permissions}" permission level`);
690
752
  }
753
+ if (!options.skipValidation) {
754
+ const validation = validateActionInput(actionDetails, { data, pathVariables: pathVars, queryParams });
755
+ if (!validation.valid) {
756
+ const details = validation.missing.map((m) => `${m.flag} is missing "${m.param}"`).join("; ");
757
+ throw new Error(`Validation failed for step "${step.id}": ${details}. Pass --skip-validation to bypass.`);
758
+ }
759
+ }
691
760
  const result = await api.executePassthroughRequest({
692
761
  platform,
693
762
  actionId,
@@ -973,7 +1042,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
973
1042
  if (flowStack.includes(resolvedKey)) {
974
1043
  throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
975
1044
  }
976
- const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-AK5W4GLF.js");
1045
+ const { loadFlowWithMeta: loadFlowWithMeta2 } = await import("./flow-runner-6IFPWOS4.js");
977
1046
  const { flow: subFlow, rootDir: subRootDir } = loadFlowWithMeta2(resolvedKey);
978
1047
  const subContext = await executeFlow(
979
1048
  subFlow,
@@ -1033,7 +1102,7 @@ async function executePaginateStep(step, context, api, permissions, allowedActio
1033
1102
  type: "action",
1034
1103
  action: resolved
1035
1104
  };
1036
- const result = await executeActionStep(syntheticStep, context, api, permissions, allowedActionIds);
1105
+ const result = await executeActionStep(syntheticStep, context, api, permissions, allowedActionIds, options);
1037
1106
  const response = result.response;
1038
1107
  const pageResults = getByDotPath(response, config.resultsField);
1039
1108
  if (Array.isArray(pageResults)) allResults.push(...pageResults);
@@ -1049,7 +1118,7 @@ async function executePaginateStep(step, context, api, permissions, allowedActio
1049
1118
  type: "action",
1050
1119
  action: resolvedAction
1051
1120
  };
1052
- const result = await executeActionStep(syntheticStep, context, api, permissions, allowedActionIds);
1121
+ const result = await executeActionStep(syntheticStep, context, api, permissions, allowedActionIds, options);
1053
1122
  const response = result.response;
1054
1123
  const pageResults = getByDotPath(response, config.resultsField);
1055
1124
  if (Array.isArray(pageResults)) allResults.push(...pageResults);
@@ -1204,10 +1273,32 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
1204
1273
  checkRequires(step, context);
1205
1274
  if (options.mock && (step.type === "action" || step.type === "paginate" || step.type === "bash")) {
1206
1275
  const resolvedConfig = step[step.type] ? resolveValue(step[step.type], context) : {};
1276
+ let mockOutput = { _mock: true, ...resolvedConfig };
1277
+ if (step.type === "action" && step.action) {
1278
+ const actionId = resolveValue(step.action.actionId, context);
1279
+ try {
1280
+ const actionDetails = await api.getActionDetails(actionId);
1281
+ if (!options.skipValidation) {
1282
+ const data = step.action.data ? resolveValue(step.action.data, context) : void 0;
1283
+ const pathVars = step.action.pathVars ? resolveValue(step.action.pathVars, context) : void 0;
1284
+ const queryParams = step.action.queryParams ? resolveValue(step.action.queryParams, context) : void 0;
1285
+ const validation = validateActionInput(actionDetails, { data, pathVariables: pathVars, queryParams });
1286
+ if (!validation.valid) {
1287
+ const details = validation.missing.map((m) => `${m.flag} is missing "${m.param}"`).join("; ");
1288
+ throw new Error(`Validation failed for step "${step.id}": ${details}. Pass --skip-validation to bypass.`);
1289
+ }
1290
+ }
1291
+ if (actionDetails.ioSchema?.ioExample?.output) {
1292
+ mockOutput = actionDetails.ioSchema.ioExample.output;
1293
+ }
1294
+ } catch (e) {
1295
+ if (e instanceof Error && e.message.startsWith("Validation failed")) throw e;
1296
+ }
1297
+ }
1207
1298
  options.onEvent?.({ event: "step:mock", stepId: step.id, type: step.type, config: resolvedConfig });
1208
1299
  const result2 = {
1209
1300
  status: "success",
1210
- output: { _mock: true, ...resolvedConfig },
1301
+ output: mockOutput,
1211
1302
  response: { _mock: true },
1212
1303
  durationMs: Date.now() - startTime
1213
1304
  };
@@ -1217,7 +1308,7 @@ async function executeSingleStep(step, context, api, permissions, allowedActionI
1217
1308
  const dispatch = async () => {
1218
1309
  switch (step.type) {
1219
1310
  case "action":
1220
- return await executeActionStep(step, context, api, permissions, allowedActionIds);
1311
+ return await executeActionStep(step, context, api, permissions, allowedActionIds, options);
1221
1312
  case "transform":
1222
1313
  return executeTransformStep(step, context);
1223
1314
  case "code":
@@ -2538,16 +2629,19 @@ function saveFlow(flow, outputPath) {
2538
2629
  }
2539
2630
 
2540
2631
  export {
2632
+ ApiError,
2541
2633
  OneApi,
2542
2634
  TimeoutError,
2543
2635
  filterByPermissions,
2544
2636
  isMethodAllowed,
2545
2637
  isActionAllowed,
2546
2638
  buildActionKnowledgeWithGuidance,
2639
+ validateActionInput,
2547
2640
  FLOW_SCHEMA,
2548
2641
  getStepTypeDescriptor,
2549
2642
  getNestedStepsKeys,
2550
2643
  generateFlowGuide,
2644
+ getByDotPath,
2551
2645
  FlowRunner,
2552
2646
  resolveFlowPath,
2553
2647
  getFlowRootDir,
@@ -11,7 +11,7 @@ import {
11
11
  saveFlow,
12
12
  summarizeFlowInputs,
13
13
  walkSteps
14
- } from "./chunk-AZV4EGKT.js";
14
+ } from "./chunk-T7LTS2IE.js";
15
15
  export {
16
16
  FlowRunner,
17
17
  collectStepTypes,