@withone/cli 1.17.2 → 1.18.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
@@ -225,6 +225,30 @@ one actions execute stripe <actionId> <connectionKey> \
225
225
  | `--form-url-encoded` | Send as application/x-www-form-urlencoded |
226
226
  | `--dry-run` | Show the request without executing it |
227
227
 
228
+ ### `one cache`
229
+
230
+ Manage the local cache for knowledge and search responses. The CLI automatically caches `actions knowledge` and `actions search` results so repeated calls serve instantly from disk.
231
+
232
+ ```bash
233
+ one cache list # List all cached entries with age and status
234
+ one cache list --expired # Show only expired entries
235
+ one cache clear # Clear all cached data
236
+ one cache clear <actionId> # Clear a specific entry
237
+ one cache update-all # Re-fetch fresh data for all cached entries
238
+ ```
239
+
240
+ Knowledge and search commands also support cache flags:
241
+
242
+ ```bash
243
+ one actions knowledge gmail <actionId> --no-cache # Skip cache, fetch fresh
244
+ one actions knowledge gmail <actionId> --cache-status # Check cache status
245
+ one actions search gmail "send email" --no-cache # Skip cache for search
246
+ ```
247
+
248
+ Default TTL is 1 hour. Configure via `ONE_CACHE_TTL` environment variable or `cacheTtl` in `~/.one/config.json`.
249
+
250
+ Note: `actions execute` is never cached — it always hits the API fresh.
251
+
228
252
  ### `one guide [topic]`
229
253
 
230
254
  Get the full CLI usage guide, designed for AI agents that only have the binary (no MCP, no IDE skills).
@@ -239,7 +263,7 @@ one --agent guide # full guide as structured JSON
239
263
  one --agent guide flows # single topic as JSON
240
264
  ```
241
265
 
242
- Topics: `overview`, `actions`, `flows`, `all` (default).
266
+ Topics: `overview`, `actions`, `flows`, `relay`, `cache`, `all` (default).
243
267
 
244
268
  In agent mode (`--agent`), the JSON response includes the guide content and an `availableTopics` array so agents can discover what sections exist.
245
269
 
@@ -128,6 +128,81 @@ var OneApi = class {
128
128
  method: action.method
129
129
  };
130
130
  }
131
+ async requestWithMeta(opts) {
132
+ let url = `${API_BASE}${opts.path}`;
133
+ if (opts.queryParams && Object.keys(opts.queryParams).length > 0) {
134
+ const params = new URLSearchParams(opts.queryParams);
135
+ url += `?${params.toString()}`;
136
+ }
137
+ const headers = {
138
+ "x-one-secret": this.apiKey,
139
+ "Content-Type": "application/json",
140
+ ...opts.headers
141
+ };
142
+ if (opts.ifNoneMatch) {
143
+ headers["If-None-Match"] = opts.ifNoneMatch;
144
+ }
145
+ const fetchOpts = {
146
+ method: opts.method || "GET",
147
+ headers
148
+ };
149
+ if (opts.body !== void 0) {
150
+ fetchOpts.body = JSON.stringify(opts.body);
151
+ }
152
+ const response = await fetch(url, fetchOpts);
153
+ if (response.status === 304) {
154
+ return { data: null, etag: opts.ifNoneMatch ?? null, status: 304 };
155
+ }
156
+ if (!response.ok) {
157
+ const text2 = await response.text();
158
+ throw new ApiError(response.status, text2 || `HTTP ${response.status}`);
159
+ }
160
+ const etag = response.headers.get("etag") ?? null;
161
+ const text = await response.text();
162
+ const data = text ? JSON.parse(text) : {};
163
+ return { data, etag, status: response.status };
164
+ }
165
+ async getActionKnowledgeWithMeta(actionId, ifNoneMatch) {
166
+ const result = await this.requestWithMeta({
167
+ path: "/knowledge",
168
+ queryParams: { _id: actionId },
169
+ ifNoneMatch
170
+ });
171
+ if (result.status === 304) {
172
+ return { data: null, etag: result.etag, status: 304 };
173
+ }
174
+ const actions = result.data?.rows || [];
175
+ if (actions.length === 0) {
176
+ throw new ApiError(404, `Action with ID ${actionId} not found`);
177
+ }
178
+ const action = actions[0];
179
+ const knowledge = {
180
+ knowledge: action.knowledge || "No knowledge was found",
181
+ method: action.method || "No method was found"
182
+ };
183
+ return { data: knowledge, etag: result.etag, status: result.status };
184
+ }
185
+ async searchActionsWithMeta(platform, query, agentType, ifNoneMatch) {
186
+ const isKnowledgeAgent = !agentType || agentType === "knowledge";
187
+ const queryParams = {
188
+ query,
189
+ limit: "5"
190
+ };
191
+ if (isKnowledgeAgent) {
192
+ queryParams.knowledgeAgent = "true";
193
+ } else {
194
+ queryParams.executeAgent = "true";
195
+ }
196
+ const result = await this.requestWithMeta({
197
+ path: `/available-actions/search/${platform}`,
198
+ queryParams,
199
+ ifNoneMatch
200
+ });
201
+ if (result.status === 304) {
202
+ return { data: null, etag: result.etag, status: 304 };
203
+ }
204
+ return { data: result.data || [], etag: result.etag, status: result.status };
205
+ }
131
206
  async executePassthroughRequest(args, preloadedAction) {
132
207
  const action = preloadedAction ?? await this.getActionDetails(args.actionId);
133
208
  const method = action.method;
@@ -703,7 +778,7 @@ async function executeSubflowStep(step, context, api, permissions, allowedAction
703
778
  if (flowStack.includes(resolvedKey)) {
704
779
  throw new Error(`Circular flow detected: ${[...flowStack, resolvedKey].join(" \u2192 ")}`);
705
780
  }
706
- const { loadFlow: loadFlow2 } = await import("./flow-runner-AJGIUT6E.js");
781
+ const { loadFlow: loadFlow2 } = await import("./flow-runner-SU4JHSZW.js");
707
782
  const subFlow = loadFlow2(resolvedKey);
708
783
  const subContext = await executeFlow(
709
784
  subFlow,
@@ -4,7 +4,7 @@ import {
4
4
  loadFlow,
5
5
  resolveFlowPath,
6
6
  saveFlow
7
- } from "./chunk-QW4MBV4F.js";
7
+ } from "./chunk-CTL2YHUH.js";
8
8
  export {
9
9
  FlowRunner,
10
10
  listFlows,
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  loadFlow,
12
12
  resolveFlowPath,
13
13
  saveFlow
14
- } from "./chunk-QW4MBV4F.js";
14
+ } from "./chunk-CTL2YHUH.js";
15
15
 
16
16
  // src/index.ts
17
17
  import { createRequire as createRequire2 } from "module";
@@ -101,6 +101,15 @@ function getAccessControlFromAllSources() {
101
101
  function getAccessControl() {
102
102
  return readConfig()?.accessControl ?? {};
103
103
  }
104
+ function getCacheTtl() {
105
+ if (process.env.ONE_CACHE_TTL) {
106
+ const val = parseInt(process.env.ONE_CACHE_TTL, 10);
107
+ if (!isNaN(val) && val > 0) return val;
108
+ }
109
+ const config = readConfig();
110
+ if (config?.cacheTtl && config.cacheTtl > 0) return config.cacheTtl;
111
+ return 3600;
112
+ }
104
113
  function updateAccessControl(settings) {
105
114
  const config = readConfig();
106
115
  if (!config) return;
@@ -1350,6 +1359,123 @@ async function platformsCommand(options) {
1350
1359
  // src/commands/actions.ts
1351
1360
  import * as p6 from "@clack/prompts";
1352
1361
  import pc6 from "picocolors";
1362
+
1363
+ // src/lib/cache.ts
1364
+ import fs4 from "fs";
1365
+ import path4 from "path";
1366
+ import os4 from "os";
1367
+ var CACHE_BASE = path4.join(os4.homedir(), ".one", "cache");
1368
+ var KNOWLEDGE_DIR = path4.join(CACHE_BASE, "knowledge");
1369
+ var SEARCH_DIR = path4.join(CACHE_BASE, "search");
1370
+ function sanitizeFilename(input) {
1371
+ return input.replace(/[^a-zA-Z0-9_\-\.]/g, "_");
1372
+ }
1373
+ function knowledgeCachePath(actionId) {
1374
+ return path4.join(KNOWLEDGE_DIR, `${sanitizeFilename(actionId)}.json`);
1375
+ }
1376
+ function searchCachePath(platform, query, type) {
1377
+ const key = `${platform}_${sanitizeFilename(query)}_${type || "knowledge"}`;
1378
+ return path4.join(SEARCH_DIR, `${key}.json`);
1379
+ }
1380
+ function readCache(filePath) {
1381
+ try {
1382
+ const content = fs4.readFileSync(filePath, "utf-8");
1383
+ return JSON.parse(content);
1384
+ } catch {
1385
+ return null;
1386
+ }
1387
+ }
1388
+ function writeCache(filePath, entry) {
1389
+ try {
1390
+ const dir = path4.dirname(filePath);
1391
+ fs4.mkdirSync(dir, { recursive: true });
1392
+ fs4.writeFileSync(filePath, JSON.stringify(entry, null, 2));
1393
+ } catch {
1394
+ }
1395
+ }
1396
+ function isFresh(entry) {
1397
+ const cachedTime = new Date(entry.cachedAt).getTime();
1398
+ const now = Date.now();
1399
+ return now - cachedTime < entry.ttl * 1e3;
1400
+ }
1401
+ function getAge(entry) {
1402
+ return Math.floor((Date.now() - new Date(entry.cachedAt).getTime()) / 1e3);
1403
+ }
1404
+ function buildCacheMeta(entry, hit) {
1405
+ if (!entry) {
1406
+ return { hit: false, age: 0, fresh: false };
1407
+ }
1408
+ return {
1409
+ hit,
1410
+ age: getAge(entry),
1411
+ fresh: isFresh(entry)
1412
+ };
1413
+ }
1414
+ function formatAge(seconds) {
1415
+ if (seconds < 60) return `${seconds}s`;
1416
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
1417
+ if (seconds < 86400) {
1418
+ const h2 = Math.floor(seconds / 3600);
1419
+ const m = Math.floor(seconds % 3600 / 60);
1420
+ return m > 0 ? `${h2}h ${m}m` : `${h2}h`;
1421
+ }
1422
+ const d = Math.floor(seconds / 86400);
1423
+ const h = Math.floor(seconds % 86400 / 3600);
1424
+ return h > 0 ? `${d}d ${h}h` : `${d}d`;
1425
+ }
1426
+ function listCacheEntries() {
1427
+ const entries = [];
1428
+ for (const [dir, type] of [[KNOWLEDGE_DIR, "knowledge"], [SEARCH_DIR, "search"]]) {
1429
+ try {
1430
+ const files = fs4.readdirSync(dir);
1431
+ for (const file of files) {
1432
+ if (!file.endsWith(".json")) continue;
1433
+ const filePath = path4.join(dir, file);
1434
+ const entry = readCache(filePath);
1435
+ if (entry) {
1436
+ entries.push({ type, filePath, entry });
1437
+ }
1438
+ }
1439
+ } catch {
1440
+ }
1441
+ }
1442
+ return entries;
1443
+ }
1444
+ function clearAll() {
1445
+ let count = 0;
1446
+ for (const dir of [KNOWLEDGE_DIR, SEARCH_DIR]) {
1447
+ try {
1448
+ const files = fs4.readdirSync(dir);
1449
+ for (const file of files) {
1450
+ fs4.unlinkSync(path4.join(dir, file));
1451
+ count++;
1452
+ }
1453
+ fs4.rmdirSync(dir);
1454
+ } catch {
1455
+ }
1456
+ }
1457
+ return count;
1458
+ }
1459
+ function clearEntry(actionId) {
1460
+ const filePath = knowledgeCachePath(actionId);
1461
+ try {
1462
+ fs4.unlinkSync(filePath);
1463
+ return true;
1464
+ } catch {
1465
+ return false;
1466
+ }
1467
+ }
1468
+ function makeCacheEntry(key, data, etag) {
1469
+ return {
1470
+ key,
1471
+ etag,
1472
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
1473
+ ttl: getCacheTtl(),
1474
+ data
1475
+ };
1476
+ }
1477
+
1478
+ // src/commands/actions.ts
1353
1479
  function getConfig() {
1354
1480
  const apiKey = getApiKey();
1355
1481
  if (!apiKey) {
@@ -1377,17 +1503,65 @@ async function actionsSearchCommand(platform, query, options) {
1377
1503
  spinner5.start(`Searching actions on ${pc6.cyan(platform)} for "${query}"...`);
1378
1504
  try {
1379
1505
  const agentType = knowledgeAgent ? "knowledge" : options.type;
1380
- let actions2 = await api.searchActions(platform, query, agentType);
1381
- actions2 = filterByPermissions(actions2, permissions);
1382
- actions2 = actions2.filter((a) => isActionAllowed(a.systemId, actionIds));
1383
- const cleanedActions = actions2.map((action) => ({
1384
- actionId: action.systemId,
1385
- title: action.title,
1386
- method: action.method,
1387
- path: action.path
1388
- }));
1506
+ const useCache = options.cache !== false;
1507
+ const cachePath = searchCachePath(platform, query, agentType || "knowledge");
1508
+ const cached = useCache ? readCache(cachePath) : null;
1509
+ let cleanedActions;
1510
+ let cacheHit = false;
1511
+ if (cached && isFresh(cached)) {
1512
+ cleanedActions = cached.data.actions;
1513
+ cacheHit = true;
1514
+ } else {
1515
+ try {
1516
+ const result = await api.searchActionsWithMeta(
1517
+ platform,
1518
+ query,
1519
+ agentType,
1520
+ cached?.etag ?? void 0
1521
+ );
1522
+ if (result.status === 304 && cached) {
1523
+ cached.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
1524
+ writeCache(cachePath, cached);
1525
+ cleanedActions = cached.data.actions;
1526
+ cacheHit = true;
1527
+ } else {
1528
+ let actions2 = result.data;
1529
+ actions2 = filterByPermissions(actions2, permissions);
1530
+ actions2 = actions2.filter((a) => isActionAllowed(a.systemId, actionIds));
1531
+ cleanedActions = actions2.map((action) => ({
1532
+ actionId: action.systemId,
1533
+ title: action.title,
1534
+ method: action.method,
1535
+ path: action.path
1536
+ }));
1537
+ writeCache(cachePath, makeCacheEntry(
1538
+ `${platform}_${query}_${agentType || "knowledge"}`,
1539
+ { actions: cleanedActions },
1540
+ result.etag
1541
+ ));
1542
+ }
1543
+ } catch (fetchError) {
1544
+ if (cached) {
1545
+ process.stderr.write(
1546
+ `Warning: serving cached search results (network unavailable, cached ${formatAge(getAge(cached))} ago)
1547
+ `
1548
+ );
1549
+ cleanedActions = cached.data.actions;
1550
+ cacheHit = true;
1551
+ } else {
1552
+ throw fetchError;
1553
+ }
1554
+ }
1555
+ }
1389
1556
  if (isAgentMode()) {
1390
- json({ actions: cleanedActions });
1557
+ const response = { actions: cleanedActions };
1558
+ if (cacheHit && cached) {
1559
+ response._cache = buildCacheMeta(cached, true);
1560
+ } else {
1561
+ const freshEntry = readCache(cachePath);
1562
+ response._cache = buildCacheMeta(freshEntry, false);
1563
+ }
1564
+ json(response);
1391
1565
  return;
1392
1566
  }
1393
1567
  if (cleanedActions.length === 0) {
@@ -1441,7 +1615,29 @@ Execute: ${pc6.cyan(`one actions execute ${platform} <actionId> <connectionK
1441
1615
  );
1442
1616
  }
1443
1617
  }
1444
- async function actionsKnowledgeCommand(platform, actionId) {
1618
+ async function actionsKnowledgeCommand(platform, actionId, options) {
1619
+ const cachePath = knowledgeCachePath(actionId);
1620
+ if (options.cacheStatus) {
1621
+ const entry = readCache(cachePath);
1622
+ if (!entry) {
1623
+ json({
1624
+ cached: false,
1625
+ path: cachePath
1626
+ });
1627
+ } else {
1628
+ const age = getAge(entry);
1629
+ json({
1630
+ cached: true,
1631
+ cachedAt: entry.cachedAt,
1632
+ age: formatAge(age),
1633
+ ttl: entry.ttl,
1634
+ expired: !isFresh(entry),
1635
+ etag: entry.etag,
1636
+ path: cachePath
1637
+ });
1638
+ }
1639
+ return;
1640
+ }
1445
1641
  intro2(pc6.bgCyan(pc6.black(" One ")));
1446
1642
  const { apiKey, actionIds, connectionKeys } = getConfig();
1447
1643
  const api = new OneApi(apiKey);
@@ -1469,15 +1665,57 @@ async function actionsKnowledgeCommand(platform, actionId) {
1469
1665
  const spinner5 = createSpinner();
1470
1666
  spinner5.start(`Loading knowledge for action ${pc6.dim(actionId)}...`);
1471
1667
  try {
1472
- const { knowledge, method } = await api.getActionKnowledge(actionId);
1668
+ const useCache = options.cache !== false;
1669
+ const cached = useCache ? readCache(cachePath) : null;
1670
+ let knowledgeData;
1671
+ let cacheHit = false;
1672
+ let cacheEntry = cached;
1673
+ if (cached && isFresh(cached) && useCache) {
1674
+ knowledgeData = cached.data;
1675
+ cacheHit = true;
1676
+ } else {
1677
+ try {
1678
+ const result = await api.getActionKnowledgeWithMeta(
1679
+ actionId,
1680
+ cached?.etag ?? void 0
1681
+ );
1682
+ if (result.status === 304 && cached) {
1683
+ cached.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
1684
+ writeCache(cachePath, cached);
1685
+ knowledgeData = cached.data;
1686
+ cacheHit = true;
1687
+ } else {
1688
+ knowledgeData = result.data;
1689
+ const newEntry = makeCacheEntry(actionId, knowledgeData, result.etag);
1690
+ writeCache(cachePath, newEntry);
1691
+ cacheEntry = newEntry;
1692
+ }
1693
+ } catch (fetchError) {
1694
+ if (cached) {
1695
+ process.stderr.write(
1696
+ `Warning: serving cached knowledge (network unavailable, cached ${formatAge(getAge(cached))} ago)
1697
+ `
1698
+ );
1699
+ knowledgeData = cached.data;
1700
+ cacheHit = true;
1701
+ } else {
1702
+ throw fetchError;
1703
+ }
1704
+ }
1705
+ }
1473
1706
  const knowledgeWithGuidance = buildActionKnowledgeWithGuidance(
1474
- knowledge,
1475
- method,
1707
+ knowledgeData.knowledge,
1708
+ knowledgeData.method,
1476
1709
  platform,
1477
1710
  actionId
1478
1711
  );
1479
1712
  if (isAgentMode()) {
1480
- json({ knowledge: knowledgeWithGuidance, method });
1713
+ const response = {
1714
+ knowledge: knowledgeWithGuidance,
1715
+ method: knowledgeData.method,
1716
+ _cache: buildCacheMeta(cacheEntry, cacheHit)
1717
+ };
1718
+ json(response);
1481
1719
  return;
1482
1720
  }
1483
1721
  spinner5.stop("Knowledge loaded");
@@ -2197,26 +2435,26 @@ function validateStepsArray(steps, pathPrefix, errors) {
2197
2435
  const validTypes = FLOW_SCHEMA.stepTypes.map((st) => st.type);
2198
2436
  for (let i = 0; i < steps.length; i++) {
2199
2437
  const step = steps[i];
2200
- const path4 = `${pathPrefix}[${i}]`;
2438
+ const path5 = `${pathPrefix}[${i}]`;
2201
2439
  if (!step || typeof step !== "object" || Array.isArray(step)) {
2202
- errors.push({ path: path4, message: "Step must be an object" });
2440
+ errors.push({ path: path5, message: "Step must be an object" });
2203
2441
  continue;
2204
2442
  }
2205
2443
  const s = step;
2206
2444
  if (!s.id || typeof s.id !== "string") {
2207
- errors.push({ path: `${path4}.id`, message: 'Step must have a string "id"' });
2445
+ errors.push({ path: `${path5}.id`, message: 'Step must have a string "id"' });
2208
2446
  }
2209
2447
  if (!s.name || typeof s.name !== "string") {
2210
- errors.push({ path: `${path4}.name`, message: 'Step must have a string "name"' });
2448
+ errors.push({ path: `${path5}.name`, message: 'Step must have a string "name"' });
2211
2449
  }
2212
2450
  if (!s.type || !validTypes.includes(s.type)) {
2213
- errors.push({ path: `${path4}.type`, message: `Step type must be one of: ${validTypes.join(", ")}` });
2451
+ errors.push({ path: `${path5}.type`, message: `Step type must be one of: ${validTypes.join(", ")}` });
2214
2452
  continue;
2215
2453
  }
2216
2454
  if (s.onError && typeof s.onError === "object") {
2217
2455
  const oe = s.onError;
2218
2456
  if (!FLOW_SCHEMA.errorStrategies.includes(oe.strategy)) {
2219
- errors.push({ path: `${path4}.onError.strategy`, message: `Error strategy must be one of: ${FLOW_SCHEMA.errorStrategies.join(", ")}` });
2457
+ errors.push({ path: `${path5}.onError.strategy`, message: `Error strategy must be one of: ${FLOW_SCHEMA.errorStrategies.join(", ")}` });
2220
2458
  }
2221
2459
  }
2222
2460
  const descriptor = getStepTypeDescriptor(s.type);
@@ -2226,14 +2464,14 @@ function validateStepsArray(steps, pathPrefix, errors) {
2226
2464
  if (!configObj || typeof configObj !== "object") {
2227
2465
  const hint = detectFlatConfigHint(s, descriptor);
2228
2466
  errors.push({
2229
- path: `${path4}.${configKey}`,
2467
+ path: `${path5}.${configKey}`,
2230
2468
  message: `${capitalize(descriptor.type)} step must have a "${configKey}" config object${hint}`
2231
2469
  });
2232
2470
  continue;
2233
2471
  }
2234
2472
  const config = configObj;
2235
2473
  for (const [fieldName, fd] of Object.entries(descriptor.fields)) {
2236
- const fieldPath = `${path4}.${configKey}.${fieldName}`;
2474
+ const fieldPath = `${path5}.${configKey}.${fieldName}`;
2237
2475
  const value = config[fieldName];
2238
2476
  if (fd.required && (value === void 0 || value === null || value === "")) {
2239
2477
  errors.push({ path: fieldPath, message: `${capitalize(descriptor.type)} must have ${fd.type === "string" ? "a string" : fd.type === "array" ? "a" : "a"} "${fieldName}"` });
@@ -2285,16 +2523,16 @@ function validateStepIds(flow2) {
2285
2523
  function collectIds(steps, pathPrefix) {
2286
2524
  for (let i = 0; i < steps.length; i++) {
2287
2525
  const step = steps[i];
2288
- const path4 = `${pathPrefix}[${i}]`;
2526
+ const path5 = `${pathPrefix}[${i}]`;
2289
2527
  if (seen.has(step.id)) {
2290
- errors.push({ path: `${path4}.id`, message: `Duplicate step ID: "${step.id}"` });
2528
+ errors.push({ path: `${path5}.id`, message: `Duplicate step ID: "${step.id}"` });
2291
2529
  } else {
2292
2530
  seen.add(step.id);
2293
2531
  }
2294
2532
  for (const { configKey, fieldName } of nestedKeys) {
2295
2533
  const config = step[configKey];
2296
2534
  if (config && Array.isArray(config[fieldName])) {
2297
- collectIds(config[fieldName], `${path4}.${configKey}.${fieldName}`);
2535
+ collectIds(config[fieldName], `${path5}.${configKey}.${fieldName}`);
2298
2536
  }
2299
2537
  }
2300
2538
  }
@@ -2341,7 +2579,7 @@ function validateSelectorReferences(flow2) {
2341
2579
  }
2342
2580
  return selectors;
2343
2581
  }
2344
- function checkSelectors(selectors, path4) {
2582
+ function checkSelectors(selectors, path5) {
2345
2583
  for (const selector of selectors) {
2346
2584
  const parts = selector.split(".");
2347
2585
  if (parts.length < 3) continue;
@@ -2349,12 +2587,12 @@ function validateSelectorReferences(flow2) {
2349
2587
  if (root === "input") {
2350
2588
  const inputName = parts[2];
2351
2589
  if (!inputNames.has(inputName)) {
2352
- errors.push({ path: path4, message: `Selector "${selector}" references undefined input "${inputName}"` });
2590
+ errors.push({ path: path5, message: `Selector "${selector}" references undefined input "${inputName}"` });
2353
2591
  }
2354
2592
  } else if (root === "steps") {
2355
2593
  const stepId = parts[2];
2356
2594
  if (!allStepIds.has(stepId)) {
2357
- errors.push({ path: path4, message: `Selector "${selector}" references undefined step "${stepId}"` });
2595
+ errors.push({ path: path5, message: `Selector "${selector}" references undefined step "${stepId}"` });
2358
2596
  }
2359
2597
  }
2360
2598
  }
@@ -2402,7 +2640,7 @@ function validateFlow(flow2) {
2402
2640
  }
2403
2641
 
2404
2642
  // src/commands/flow.ts
2405
- import fs4 from "fs";
2643
+ import fs5 from "fs";
2406
2644
  function getConfig2() {
2407
2645
  const apiKey = getApiKey();
2408
2646
  if (!apiKey) {
@@ -2460,7 +2698,7 @@ async function flowCreateCommand(key, options) {
2460
2698
  if (raw.startsWith("@")) {
2461
2699
  const filePath = raw.slice(1);
2462
2700
  try {
2463
- raw = fs4.readFileSync(filePath, "utf-8");
2701
+ raw = fs5.readFileSync(filePath, "utf-8");
2464
2702
  } catch (err) {
2465
2703
  error(`Cannot read file "${filePath}": ${err.message}`);
2466
2704
  }
@@ -2643,7 +2881,7 @@ async function flowValidateCommand(keyOrPath) {
2643
2881
  let flowData;
2644
2882
  try {
2645
2883
  const flowPath = resolveFlowPath(keyOrPath);
2646
- const content = fs4.readFileSync(flowPath, "utf-8");
2884
+ const content = fs5.readFileSync(flowPath, "utf-8");
2647
2885
  flowData = JSON.parse(content);
2648
2886
  } catch (err) {
2649
2887
  spinner5.stop("Validation failed");
@@ -3312,8 +3550,122 @@ async function relayEventTypesCommand(platform) {
3312
3550
  }
3313
3551
  }
3314
3552
 
3315
- // src/commands/guide.ts
3553
+ // src/commands/cache.ts
3316
3554
  import pc9 from "picocolors";
3555
+ async function cacheClearCommand(actionId) {
3556
+ if (actionId) {
3557
+ const deleted = clearEntry(actionId);
3558
+ if (isAgentMode()) {
3559
+ json({ cleared: deleted, actionId });
3560
+ return;
3561
+ }
3562
+ if (deleted) {
3563
+ console.log(`Cleared cache for ${pc9.cyan(actionId)}`);
3564
+ } else {
3565
+ console.log(`No cache entry found for ${pc9.dim(actionId)}`);
3566
+ }
3567
+ } else {
3568
+ const count = clearAll();
3569
+ if (isAgentMode()) {
3570
+ json({ cleared: true, count });
3571
+ return;
3572
+ }
3573
+ console.log(`Cleared ${count} cached ${count === 1 ? "entry" : "entries"}`);
3574
+ }
3575
+ }
3576
+ async function cacheListCommand(options) {
3577
+ const entries = listCacheEntries();
3578
+ const filtered = options.expired ? entries.filter((e) => !isFresh(e.entry)) : entries;
3579
+ if (isAgentMode()) {
3580
+ json({
3581
+ entries: filtered.map((e) => ({
3582
+ type: e.type,
3583
+ key: e.entry.key,
3584
+ cachedAt: e.entry.cachedAt,
3585
+ age: formatAge(getAge(e.entry)),
3586
+ ttl: e.entry.ttl,
3587
+ fresh: isFresh(e.entry),
3588
+ etag: e.entry.etag,
3589
+ path: e.filePath
3590
+ }))
3591
+ });
3592
+ return;
3593
+ }
3594
+ if (filtered.length === 0) {
3595
+ console.log(options.expired ? "No expired cache entries" : "No cached entries");
3596
+ return;
3597
+ }
3598
+ const rows = filtered.map((e) => ({
3599
+ type: e.type,
3600
+ key: e.entry.key,
3601
+ age: formatAge(getAge(e.entry)),
3602
+ status: isFresh(e.entry) ? pc9.green("fresh") : pc9.yellow("expired")
3603
+ }));
3604
+ printTable(
3605
+ [
3606
+ { key: "type", label: "Type" },
3607
+ { key: "key", label: "Key" },
3608
+ { key: "age", label: "Age" },
3609
+ { key: "status", label: "Status" }
3610
+ ],
3611
+ rows
3612
+ );
3613
+ }
3614
+ async function cacheUpdateAllCommand() {
3615
+ const apiKey = getApiKey();
3616
+ if (!apiKey) {
3617
+ error("Not configured. Run `one init` first.");
3618
+ }
3619
+ const api = new OneApi(apiKey);
3620
+ const entries = listCacheEntries();
3621
+ if (entries.length === 0) {
3622
+ if (isAgentMode()) {
3623
+ json({ updated: 0, failed: 0, entries: [] });
3624
+ return;
3625
+ }
3626
+ console.log("No cached entries to update");
3627
+ return;
3628
+ }
3629
+ const spinner5 = createSpinner();
3630
+ spinner5.start(`Updating ${entries.length} cached ${entries.length === 1 ? "entry" : "entries"}...`);
3631
+ let updated = 0;
3632
+ let failed = 0;
3633
+ const errors = [];
3634
+ for (const e of entries) {
3635
+ try {
3636
+ if (e.type === "knowledge") {
3637
+ const result = await api.getActionKnowledgeWithMeta(e.entry.key);
3638
+ const newEntry = makeCacheEntry(e.entry.key, result.data, result.etag);
3639
+ writeCache(e.filePath, newEntry);
3640
+ updated++;
3641
+ } else {
3642
+ const refreshed = { ...e.entry, cachedAt: (/* @__PURE__ */ new Date()).toISOString() };
3643
+ writeCache(e.filePath, refreshed);
3644
+ updated++;
3645
+ }
3646
+ } catch (err) {
3647
+ failed++;
3648
+ errors.push({
3649
+ key: e.entry.key,
3650
+ error: err instanceof Error ? err.message : "Unknown error"
3651
+ });
3652
+ }
3653
+ }
3654
+ spinner5.stop(`Updated ${updated} ${updated === 1 ? "entry" : "entries"}${failed > 0 ? `, ${failed} failed` : ""}`);
3655
+ if (isAgentMode()) {
3656
+ json({ updated, failed, errors: errors.length > 0 ? errors : void 0 });
3657
+ return;
3658
+ }
3659
+ if (errors.length > 0) {
3660
+ console.log();
3661
+ for (const e of errors) {
3662
+ console.log(` ${pc9.red("\u2717")} ${e.key}: ${pc9.dim(e.error)}`);
3663
+ }
3664
+ }
3665
+ }
3666
+
3667
+ // src/commands/guide.ts
3668
+ import pc10 from "picocolors";
3317
3669
 
3318
3670
  // src/lib/guide-content.ts
3319
3671
  var GUIDE_OVERVIEW = `# One CLI \u2014 Agent Guide
@@ -3404,6 +3756,7 @@ Request specific sections:
3404
3756
  - \`one guide actions\` \u2014 Actions reference (search, knowledge, execute)
3405
3757
  - \`one guide flows\` \u2014 Workflow engine reference (step types, selectors, examples)
3406
3758
  - \`one guide relay\` \u2014 Webhook relay reference (templates, passthrough actions)
3759
+ - \`one guide cache\` \u2014 Cache management (TTL, flags, commands)
3407
3760
  - \`one guide all\` \u2014 Everything
3408
3761
 
3409
3762
  ## Important Notes
@@ -3555,11 +3908,93 @@ Any connected platform can be a destination via passthrough actions.
3555
3908
  3. \`relay deliveries --event-id <id>\` \u2014 check delivery status and errors
3556
3909
  4. \`relay event <id>\` \u2014 inspect full payload to verify template paths
3557
3910
  `;
3911
+ var GUIDE_CACHE = `# One Cache \u2014 Reference
3912
+
3913
+ ## Overview
3914
+
3915
+ The One CLI caches \`actions knowledge\` and \`actions search\` responses locally so repeated calls serve instantly from disk instead of hitting the API. This is the single biggest latency win for agents who call knowledge for the same actions repeatedly.
3916
+
3917
+ Cache location: \`~/.one/cache/knowledge/\` and \`~/.one/cache/search/\`
3918
+
3919
+ ## How It Works
3920
+
3921
+ - **First call**: fetches from the API, writes to cache, serves the response
3922
+ - **Subsequent calls (within TTL)**: serves from cache instantly, no API call
3923
+ - **After TTL expires**: makes a conditional request (ETag). If content unchanged, refreshes the cache timestamp. If changed, writes fresh data.
3924
+ - **Network failure with stale cache**: serves the stale cache with a warning \u2014 never fails hard when a cache exists
3925
+
3926
+ Default TTL: 3600 seconds (1 hour). Configure via \`ONE_CACHE_TTL\` env var or \`cacheTtl\` in \`~/.one/config.json\`.
3927
+
3928
+ ## What Gets Cached
3929
+
3930
+ | Cached | Not Cached |
3931
+ |--------|-----------|
3932
+ | \`actions knowledge\` (API docs, change infrequently) | \`actions execute\` (live data, always fresh) |
3933
+ | \`actions search\` results | \`connection list\` (changes with add/remove) |
3934
+
3935
+ ## Agent Mode \`_cache\` Metadata
3936
+
3937
+ In \`--agent\` mode, knowledge and search responses include a \`_cache\` field:
3938
+
3939
+ \`\`\`json
3940
+ {
3941
+ "knowledge": "...",
3942
+ "method": "POST",
3943
+ "_cache": {
3944
+ "hit": true,
3945
+ "age": 1423,
3946
+ "fresh": true
3947
+ }
3948
+ }
3949
+ \`\`\`
3950
+
3951
+ Use this to programmatically decide whether to force-refresh.
3952
+
3953
+ ## Cache Flags
3954
+
3955
+ \`\`\`bash
3956
+ # Skip cache, fetch fresh (result still gets cached for next time)
3957
+ one --agent actions knowledge <platform> <actionId> --no-cache
3958
+
3959
+ # Check cache status without fetching
3960
+ one --agent actions knowledge <platform> <actionId> --cache-status
3961
+
3962
+ # Same for search
3963
+ one --agent actions search <platform> "<query>" --no-cache
3964
+ \`\`\`
3965
+
3966
+ ## Cache Management Commands
3967
+
3968
+ \`\`\`bash
3969
+ one cache list # List all cached entries with age and status
3970
+ one cache list --expired # List only expired entries
3971
+ one cache clear # Delete all cached knowledge and search data
3972
+ one cache clear <actionId> # Delete one specific entry
3973
+ one cache update-all # Re-fetch fresh data for all cached entries
3974
+ \`\`\`
3975
+
3976
+ All cache commands respect \`--agent\` for JSON output.
3977
+
3978
+ ## When to Force-Refresh
3979
+
3980
+ - After a platform updates its API docs (rare)
3981
+ - If you suspect stale data is causing issues
3982
+ - Use \`one cache update-all\` to proactively warm the entire cache
3983
+
3984
+ ## Configuration
3985
+
3986
+ | Setting | Source | Example |
3987
+ |---------|--------|---------|
3988
+ | TTL (seconds) | \`ONE_CACHE_TTL\` env var | \`ONE_CACHE_TTL=7200\` |
3989
+ | TTL (seconds) | \`cacheTtl\` in \`~/.one/config.json\` | \`"cacheTtl": 7200\` |
3990
+ | Default | \u2014 | 3600 (1 hour) |
3991
+ `;
3558
3992
  var TOPICS = [
3559
3993
  { topic: "overview", description: "Setup, features, and quick start for each" },
3560
3994
  { topic: "actions", description: "Search, read docs, and execute platform actions" },
3561
3995
  { topic: "flows", description: "Build and execute multi-step workflows" },
3562
3996
  { topic: "relay", description: "Receive webhooks and forward to other platforms" },
3997
+ { topic: "cache", description: "Local caching for knowledge and search responses" },
3563
3998
  { topic: "all", description: "Complete guide (all topics combined)" }
3564
3999
  ];
3565
4000
  function getGuideContent(topic) {
@@ -3572,10 +4007,12 @@ function getGuideContent(topic) {
3572
4007
  return { title: "One CLI \u2014 Agent Guide: Workflows", content: GUIDE_FLOWS };
3573
4008
  case "relay":
3574
4009
  return { title: "One CLI \u2014 Agent Guide: Relay", content: GUIDE_RELAY };
4010
+ case "cache":
4011
+ return { title: "One CLI \u2014 Agent Guide: Cache", content: GUIDE_CACHE };
3575
4012
  case "all":
3576
4013
  return {
3577
4014
  title: "One CLI \u2014 Agent Guide: Complete",
3578
- content: [GUIDE_OVERVIEW, GUIDE_ACTIONS, GUIDE_FLOWS, GUIDE_RELAY].join("\n---\n\n")
4015
+ content: [GUIDE_OVERVIEW, GUIDE_ACTIONS, GUIDE_FLOWS, GUIDE_RELAY, GUIDE_CACHE].join("\n---\n\n")
3579
4016
  };
3580
4017
  }
3581
4018
  }
@@ -3584,7 +4021,7 @@ function getAvailableTopics() {
3584
4021
  }
3585
4022
 
3586
4023
  // src/commands/guide.ts
3587
- var VALID_TOPICS = ["overview", "actions", "flows", "relay", "all"];
4024
+ var VALID_TOPICS = ["overview", "actions", "flows", "relay", "cache", "all"];
3588
4025
  async function guideCommand(topic = "all") {
3589
4026
  if (!VALID_TOPICS.includes(topic)) {
3590
4027
  error(
@@ -3597,14 +4034,14 @@ async function guideCommand(topic = "all") {
3597
4034
  json({ topic, title, content, availableTopics });
3598
4035
  return;
3599
4036
  }
3600
- intro2(pc9.bgCyan(pc9.black(" One Guide ")));
4037
+ intro2(pc10.bgCyan(pc10.black(" One Guide ")));
3601
4038
  console.log();
3602
4039
  console.log(content);
3603
- console.log(pc9.dim("\u2500".repeat(60)));
4040
+ console.log(pc10.dim("\u2500".repeat(60)));
3604
4041
  console.log(
3605
- pc9.dim("Available topics: ") + availableTopics.map((t) => pc9.cyan(t.topic)).join(", ")
4042
+ pc10.dim("Available topics: ") + availableTopics.map((t) => pc10.cyan(t.topic)).join(", ")
3606
4043
  );
3607
- console.log(pc9.dim(`Run ${pc9.cyan("one guide <topic>")} for a specific section.`));
4044
+ console.log(pc10.dim(`Run ${pc10.cyan("one guide <topic>")} for a specific section.`));
3608
4045
  }
3609
4046
 
3610
4047
  // src/lib/platform-meta.ts
@@ -3947,14 +4384,14 @@ async function fetchLatestVersionInfo() {
3947
4384
  return null;
3948
4385
  }
3949
4386
  }
3950
- function readCache() {
4387
+ function readCache3() {
3951
4388
  try {
3952
4389
  return JSON.parse(readFileSync(CACHE_PATH, "utf8"));
3953
4390
  } catch {
3954
4391
  return null;
3955
4392
  }
3956
4393
  }
3957
- function writeCache(latestVersion, publishedAt) {
4394
+ function writeCache2(latestVersion, publishedAt) {
3958
4395
  try {
3959
4396
  mkdirSync(join(homedir(), ".one"), { recursive: true });
3960
4397
  writeFileSync(CACHE_PATH, JSON.stringify({ lastCheck: Date.now(), latestVersion, publishedAt }));
@@ -3963,16 +4400,16 @@ function writeCache(latestVersion, publishedAt) {
3963
4400
  }
3964
4401
  async function checkLatestVersion() {
3965
4402
  const info = await fetchLatestVersionInfo();
3966
- if (info) writeCache(info.version, info.publishedAt);
4403
+ if (info) writeCache2(info.version, info.publishedAt);
3967
4404
  return info?.version ?? null;
3968
4405
  }
3969
4406
  async function checkLatestVersionCached() {
3970
- const cache = readCache();
3971
- if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
3972
- return { version: cache.latestVersion, publishedAt: cache.publishedAt ?? null };
4407
+ const cache2 = readCache3();
4408
+ if (cache2 && Date.now() - cache2.lastCheck < CHECK_INTERVAL_MS) {
4409
+ return { version: cache2.latestVersion, publishedAt: cache2.publishedAt ?? null };
3973
4410
  }
3974
4411
  const info = await fetchLatestVersionInfo();
3975
- if (info) writeCache(info.version, info.publishedAt);
4412
+ if (info) writeCache2(info.version, info.publishedAt);
3976
4413
  return info;
3977
4414
  }
3978
4415
  function getCurrentVersion() {
@@ -4054,6 +4491,11 @@ program.name("one").option("--agent", "Machine-readable JSON output (no colors,
4054
4491
  one flow execute <key> Execute a workflow
4055
4492
  one flow validate <key> Validate a flow
4056
4493
 
4494
+ Cache:
4495
+ one cache list List cached entries with age and status
4496
+ one cache clear Clear all cached knowledge and search data
4497
+ one cache update-all Re-fetch fresh data for all cached entries
4498
+
4057
4499
  Webhook Relay:
4058
4500
  one relay create Create a relay endpoint for a connection
4059
4501
  one relay list List relay endpoints
@@ -4113,11 +4555,11 @@ program.command("platforms").alias("p").description("List available platforms").
4113
4555
  await platformsCommand(options);
4114
4556
  });
4115
4557
  var actions = program.command("actions").alias("a").description("Search, explore, and execute platform actions (workflow: search \u2192 knowledge \u2192 execute)");
4116
- actions.command("search <platform> <query>").description('Search for actions on a platform (e.g. one actions search gmail "send email")').option("-t, --type <type>", "execute (to run it) or knowledge (to learn about it). Default: knowledge").action(async (platform, query, options) => {
4558
+ actions.command("search <platform> <query>").description('Search for actions on a platform (e.g. one actions search gmail "send email")').option("-t, --type <type>", "execute (to run it) or knowledge (to learn about it). Default: knowledge").option("--no-cache", "Skip cache, fetch fresh from API").action(async (platform, query, options) => {
4117
4559
  await actionsSearchCommand(platform, query, options);
4118
4560
  });
4119
- 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) => {
4120
- await actionsKnowledgeCommand(platform, actionId);
4561
+ actions.command("knowledge <platform> <actionId>").alias("k").description("Get full docs for an action \u2014 MUST call before execute to know required params").option("--no-cache", "Skip cache, fetch fresh from API").option("--cache-status", "Print cache metadata without fetching").action(async (platform, actionId, options) => {
4562
+ await actionsKnowledgeCommand(platform, actionId, options);
4121
4563
  });
4122
4564
  actions.command("execute <platform> <actionId> <connectionKey>").alias("x").description('Execute an action \u2014 pass connectionKey from "one list", actionId from "actions search"').option("-d, --data <json>", "Request body as JSON").option("--path-vars <json>", "Path variables as JSON").option("--query-params <json>", "Query parameters as JSON").option("--headers <json>", "Additional headers as JSON").option("--form-data", "Send as multipart/form-data").option("--form-url-encoded", "Send as application/x-www-form-urlencoded").option("--dry-run", "Show request that would be sent without executing").action(async (platform, actionId, connectionKey, options) => {
4123
4565
  await actionsExecuteCommand(platform, actionId, connectionKey, {
@@ -4183,6 +4625,16 @@ relay.command("deliveries").description("List delivery attempts for an endpoint
4183
4625
  relay.command("event-types <platform>").description("List supported webhook event types for a platform").action(async (platform) => {
4184
4626
  await relayEventTypesCommand(platform);
4185
4627
  });
4628
+ var cache = program.command("cache").description("Manage the local knowledge and search cache");
4629
+ cache.command("clear [actionId]").description("Clear all cached data, or a specific action by ID").action(async (actionId) => {
4630
+ await cacheClearCommand(actionId);
4631
+ });
4632
+ cache.command("list").alias("ls").description("List all cached entries with age and status").option("--expired", "Show only expired entries").action(async (options) => {
4633
+ await cacheListCommand(options);
4634
+ });
4635
+ cache.command("update-all").description("Re-fetch fresh data for all cached entries").action(async () => {
4636
+ await cacheUpdateAllCommand();
4637
+ });
4186
4638
  program.command("guide [topic]").description("Full CLI usage guide for agents (topics: overview, actions, flows, relay, all)").action(async (topic) => {
4187
4639
  await guideCommand(topic);
4188
4640
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "1.17.2",
3
+ "version": "1.18.0",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [
@@ -100,6 +100,18 @@ All errors return JSON: `{"error": "message"}`. Parse output as JSON and check f
100
100
  - JSON values passed to `-d`, `--path-vars`, `--query-params` must be valid JSON (use single quotes around JSON to avoid shell escaping)
101
101
  - Do NOT pass path or query parameters inside the `-d` body flag
102
102
 
103
+ ## Caching
104
+
105
+ Knowledge and search responses are cached locally (`~/.one/cache/`). Subsequent calls for the same action serve instantly from disk.
106
+
107
+ - Cache is automatic — no setup required
108
+ - Default TTL: 1 hour (configurable via `ONE_CACHE_TTL` env var)
109
+ - In `--agent` mode, responses include a `_cache` field: `{"hit": true, "age": 1423, "fresh": true}`
110
+ - Use `--no-cache` to force a fresh fetch: `one --agent actions knowledge <platform> <actionId> --no-cache`
111
+ - Use `--cache-status` to check cache state without fetching
112
+ - Manage cache: `one cache list`, `one cache clear`, `one cache update-all`
113
+ - `actions execute` is NEVER cached — always fresh
114
+
103
115
  ## Beyond Single Actions
104
116
 
105
117
  One also supports more advanced patterns. Read the relevant reference file before using these: