@picahq/cli 1.4.0 → 1.6.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.
Files changed (3) hide show
  1. package/README.md +3 -48
  2. package/dist/index.js +54 -434
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -45,6 +45,7 @@ If you already have a config, `pica init` shows your current setup instead of st
45
45
  Claude Desktop ● yes -
46
46
  Cursor ○ no ○ no
47
47
  Windsurf - -
48
+ Codex ● yes ○ no
48
49
 
49
50
  - = not detected on this machine
50
51
  ```
@@ -53,7 +54,7 @@ Then it offers targeted actions based on what's missing:
53
54
 
54
55
  - **Update API key** -- validates the new key, then re-installs to every agent that currently has the MCP (preserving global/project scopes)
55
56
  - **Install MCP to more agents** -- only shows detected agents missing the MCP
56
- - **Install MCP for this project** -- creates `.mcp.json` / `.cursor/mcp.json` in cwd for agents that support project scope
57
+ - **Install MCP for this project** -- creates `.mcp.json` / `.cursor/mcp.json` / `.codex/config.toml` in cwd for agents that support project scope
57
58
  - **Start fresh** -- full setup flow from scratch
58
59
 
59
60
  Options that don't apply are hidden. If every detected agent already has the MCP globally, "Install MCP to more agents" won't appear.
@@ -96,45 +97,6 @@ pica platforms
96
97
  pica platforms -c "CRM"
97
98
  ```
98
99
 
99
- ### Search actions
100
-
101
- Find available API actions on any connected platform:
102
-
103
- ```bash
104
- pica search gmail "send email"
105
- pica search slack "post message"
106
- pica search stripe "list payments"
107
- ```
108
-
109
- ```
110
- POST /gmail/v1/users/{{userId}}/messages/send
111
- Send Message
112
- conn_mod_def::ABC123::XYZ789
113
- ```
114
-
115
- ### Read API docs
116
-
117
- ```bash
118
- pica actions knowledge <actionId>
119
- pica actions k <actionId> --full # no truncation
120
- ```
121
-
122
- Shows method, path, path variables, parameter schemas, and request/response examples.
123
-
124
- ### Execute an action
125
-
126
- ```bash
127
- pica exec <actionId> \
128
- -c live::gmail::default::abc123 \
129
- -d '{"to": "test@example.com", "subject": "Hello", "body": "Hi there"}' \
130
- -p userId=me
131
- ```
132
-
133
- If you omit flags, the CLI prompts interactively:
134
- - Auto-selects the connection if only one exists for the platform
135
- - Prompts for each `{{path variable}}` not provided via `-p`
136
- - Prompts for the request body on POST/PUT/PATCH if `-d` is missing
137
-
138
100
  ## Commands
139
101
 
140
102
  | Command | Description |
@@ -143,9 +105,6 @@ If you omit flags, the CLI prompts interactively:
143
105
  | `pica add <platform>` | Connect a platform via OAuth |
144
106
  | `pica list` | List connections with keys |
145
107
  | `pica platforms` | Browse available platforms |
146
- | `pica search <platform> [query]` | Search for actions |
147
- | `pica actions knowledge <id>` | Get API docs for an action |
148
- | `pica exec <id>` | Execute an action |
149
108
 
150
109
  Every command supports `--json` for machine-readable output.
151
110
 
@@ -155,9 +114,6 @@ Every command supports `--json` for machine-readable output.
155
114
  |-------|------|
156
115
  | `pica ls` | `pica list` |
157
116
  | `pica p` | `pica platforms` |
158
- | `pica a search` | `pica actions search` |
159
- | `pica a k` | `pica actions knowledge` |
160
- | `pica a x` | `pica actions execute` |
161
117
 
162
118
  ## How it works
163
119
 
@@ -173,6 +129,7 @@ All API calls route through Pica's passthrough proxy (`api.picaos.com/v1/passthr
173
129
  | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` | n/a |
174
130
  | Cursor | `~/.cursor/mcp.json` | `.cursor/mcp.json` |
175
131
  | Windsurf | `~/.codeium/windsurf/mcp_config.json` | n/a |
132
+ | Codex | `~/.codex/config.toml` | `.codex/config.toml` |
176
133
 
177
134
  Global installs make the MCP available everywhere. Project installs create config files in your current directory that can be committed and shared with your team (each team member needs their own API key).
178
135
 
@@ -193,11 +150,9 @@ src/
193
150
  init.ts # pica init (setup, status display, targeted actions)
194
151
  connection.ts # pica add, pica list
195
152
  platforms.ts # pica platforms
196
- actions.ts # pica search, actions knowledge, exec
197
153
  lib/
198
154
  api.ts # HTTP client for Pica API
199
155
  types.ts # TypeScript interfaces
200
- actions.ts # Action ID normalization, path variable helpers
201
156
  config.ts # ~/.pica/config.json read/write
202
157
  agents.ts # Agent detection, MCP config, status reporting
203
158
  platforms.ts # Platform search and fuzzy matching
package/dist/index.js CHANGED
@@ -46,11 +46,12 @@ function getApiKey() {
46
46
  import fs2 from "fs";
47
47
  import path2 from "path";
48
48
  import os2 from "os";
49
- function expandPath(p5) {
50
- if (p5.startsWith("~/")) {
51
- return path2.join(os2.homedir(), p5.slice(2));
49
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
50
+ function expandPath(p4) {
51
+ if (p4.startsWith("~/")) {
52
+ return path2.join(os2.homedir(), p4.slice(2));
52
53
  }
53
- return p5;
54
+ return p4;
54
55
  }
55
56
  function getClaudeDesktopConfigPath() {
56
57
  switch (process.platform) {
@@ -120,6 +121,15 @@ var AGENTS = [
120
121
  configPath: getWindsurfConfigPath(),
121
122
  configKey: "mcpServers",
122
123
  detectDir: getWindsurfDetectDir()
124
+ },
125
+ {
126
+ id: "codex",
127
+ name: "Codex",
128
+ configPath: "~/.codex/config.toml",
129
+ configKey: "mcp_servers",
130
+ detectDir: "~/.codex",
131
+ projectConfigPath: ".codex/config.toml",
132
+ configFormat: "toml"
123
133
  }
124
134
  ];
125
135
  function getAllAgents() {
@@ -141,6 +151,9 @@ function readAgentConfig(agent, scope = "global") {
141
151
  }
142
152
  try {
143
153
  const content = fs2.readFileSync(configPath, "utf-8");
154
+ if (agent.configFormat === "toml") {
155
+ return parseToml(content);
156
+ }
144
157
  return JSON.parse(content);
145
158
  } catch {
146
159
  return {};
@@ -152,7 +165,11 @@ function writeAgentConfig(agent, config, scope = "global") {
152
165
  if (!fs2.existsSync(configDir)) {
153
166
  fs2.mkdirSync(configDir, { recursive: true });
154
167
  }
155
- fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
168
+ if (agent.configFormat === "toml") {
169
+ fs2.writeFileSync(configPath, stringifyToml(config));
170
+ } else {
171
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
172
+ }
156
173
  }
157
174
  function getMcpServerConfig(apiKey) {
158
175
  return {
@@ -186,40 +203,6 @@ function getAgentStatuses() {
186
203
  });
187
204
  }
188
205
 
189
- // src/lib/actions.ts
190
- var ACTION_ID_PREFIX = "conn_mod_def::";
191
- function normalizeActionId(id) {
192
- if (id.startsWith(ACTION_ID_PREFIX)) return id;
193
- return `${ACTION_ID_PREFIX}${id}`;
194
- }
195
- function extractPathVariables(path3) {
196
- const matches = path3.match(/\{\{(\w+)\}\}/g);
197
- if (!matches) return [];
198
- return matches.map((m) => m.replace(/\{\{|\}\}/g, ""));
199
- }
200
- function replacePathVariables(path3, vars) {
201
- let result = path3;
202
- for (const [key, value] of Object.entries(vars)) {
203
- result = result.replace(`{{${key}}}`, encodeURIComponent(value));
204
- }
205
- return result;
206
- }
207
- function resolveTemplateVariables(path3, data, pathVars) {
208
- const variables = extractPathVariables(path3);
209
- const merged = { ...pathVars };
210
- const remaining = { ...data };
211
- for (const v of variables) {
212
- if (!merged[v] && data[v] != null) {
213
- merged[v] = String(data[v]);
214
- delete remaining[v];
215
- }
216
- }
217
- return {
218
- resolvedPath: replacePathVariables(path3, merged),
219
- remainingData: remaining
220
- };
221
- }
222
-
223
206
  // src/lib/api.ts
224
207
  var API_BASE = "https://api.picaos.com/v1";
225
208
  var ApiError = class extends Error {
@@ -256,8 +239,8 @@ var PicaApi = class {
256
239
  }
257
240
  const response = await fetch(url, fetchOpts);
258
241
  if (!response.ok) {
259
- const text4 = await response.text();
260
- throw new ApiError(response.status, text4 || `HTTP ${response.status}`);
242
+ const text3 = await response.text();
243
+ throw new ApiError(response.status, text3 || `HTTP ${response.status}`);
261
244
  }
262
245
  return response.json();
263
246
  }
@@ -288,45 +271,6 @@ var PicaApi = class {
288
271
  } while (page <= totalPages);
289
272
  return allPlatforms;
290
273
  }
291
- async searchActions(platform, query, limit = 10) {
292
- const queryParams = {
293
- limit: String(limit),
294
- executeAgent: "true"
295
- };
296
- if (query) queryParams.query = query;
297
- const response = await this.requestFull({
298
- path: `/available-actions/search/${encodeURIComponent(platform)}`,
299
- queryParams
300
- });
301
- return response.rows || [];
302
- }
303
- async getActionKnowledge(actionId) {
304
- const normalized = normalizeActionId(actionId);
305
- const response = await this.requestFull({
306
- path: "/knowledge",
307
- queryParams: { _id: normalized }
308
- });
309
- return response.rows?.[0] ?? null;
310
- }
311
- async executeAction(opts) {
312
- const headers = {
313
- "x-pica-connection-key": opts.connectionKey,
314
- "x-pica-action-id": normalizeActionId(opts.actionId),
315
- ...opts.headers
316
- };
317
- if (opts.isFormData) {
318
- headers["Content-Type"] = "multipart/form-data";
319
- } else if (opts.isFormUrlEncoded) {
320
- headers["Content-Type"] = "application/x-www-form-urlencoded";
321
- }
322
- return this.requestFull({
323
- path: `/passthrough${opts.path}`,
324
- method: opts.method.toUpperCase(),
325
- body: opts.data,
326
- headers,
327
- queryParams: opts.queryParams
328
- });
329
- }
330
274
  async waitForConnection(platform, timeoutMs = 5 * 60 * 1e3, pollIntervalMs = 5e3, onPoll) {
331
275
  const startTime = Date.now();
332
276
  const existingConnections = await this.listConnections();
@@ -525,16 +469,16 @@ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
525
469
  p.cancel("Cancelled.");
526
470
  process.exit(0);
527
471
  }
528
- const spinner5 = p.spinner();
529
- spinner5.start("Validating API key...");
472
+ const spinner4 = p.spinner();
473
+ spinner4.start("Validating API key...");
530
474
  const api = new PicaApi(newKey);
531
475
  const isValid = await api.validateApiKey();
532
476
  if (!isValid) {
533
- spinner5.stop("Invalid API key");
477
+ spinner4.stop("Invalid API key");
534
478
  p.cancel(`Invalid API key. Get a valid key at ${getApiKeyUrl()}`);
535
479
  process.exit(1);
536
480
  }
537
- spinner5.stop("API key validated");
481
+ spinner4.stop("API key validated");
538
482
  const reinstalled = [];
539
483
  for (const s of statuses) {
540
484
  if (s.globalMcp) {
@@ -666,16 +610,16 @@ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
666
610
  p.cancel("Setup cancelled.");
667
611
  process.exit(0);
668
612
  }
669
- const spinner5 = p.spinner();
670
- spinner5.start("Validating API key...");
613
+ const spinner4 = p.spinner();
614
+ spinner4.start("Validating API key...");
671
615
  const api = new PicaApi(apiKey);
672
616
  const isValid = await api.validateApiKey();
673
617
  if (!isValid) {
674
- spinner5.stop("Invalid API key");
618
+ spinner4.stop("Invalid API key");
675
619
  p.cancel(`Invalid API key. Get a valid key at ${getApiKeyUrl()}`);
676
620
  process.exit(1);
677
621
  }
678
- spinner5.stop("API key validated");
622
+ spinner4.stop("API key validated");
679
623
  writeConfig({
680
624
  apiKey,
681
625
  installedAgents: [],
@@ -688,7 +632,7 @@ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
688
632
  {
689
633
  value: "all",
690
634
  label: "All agents",
691
- hint: "Claude Code, Claude Desktop, Cursor, Windsurf"
635
+ hint: allAgents.map((a) => a.name).join(", ")
692
636
  },
693
637
  ...allAgents.map((agent) => ({
694
638
  value: agent.id,
@@ -875,16 +819,16 @@ async function promptConnectIntegrations(apiKey) {
875
819
  p.log.warn("Could not open browser automatically.");
876
820
  p.note(url, "Open manually");
877
821
  }
878
- const spinner5 = p.spinner();
879
- spinner5.start("Waiting for connection... (complete auth in browser)");
822
+ const spinner4 = p.spinner();
823
+ spinner4.start("Waiting for connection... (complete auth in browser)");
880
824
  try {
881
825
  await api.waitForConnection(platform, 5 * 60 * 1e3, 5e3);
882
- spinner5.stop(`${label} connected!`);
826
+ spinner4.stop(`${label} connected!`);
883
827
  p.log.success(`${pc2.green("\u2713")} ${label} is now available to your AI agents`);
884
828
  connected.push(platform);
885
829
  first = false;
886
830
  } catch (error) {
887
- spinner5.stop("Connection timed out");
831
+ spinner4.stop("Connection timed out");
888
832
  if (error instanceof TimeoutError) {
889
833
  p.log.warn(`No worries. Connect later with: ${pc2.cyan(`pica add ${platform}`)}`);
890
834
  }
@@ -917,16 +861,16 @@ import pc3 from "picocolors";
917
861
  function findPlatform(platforms, query) {
918
862
  const normalizedQuery = query.toLowerCase().trim();
919
863
  const exact = platforms.find(
920
- (p5) => p5.platform.toLowerCase() === normalizedQuery || p5.name.toLowerCase() === normalizedQuery
864
+ (p4) => p4.platform.toLowerCase() === normalizedQuery || p4.name.toLowerCase() === normalizedQuery
921
865
  );
922
866
  if (exact) return exact;
923
867
  return null;
924
868
  }
925
869
  function findSimilarPlatforms(platforms, query, limit = 3) {
926
870
  const normalizedQuery = query.toLowerCase().trim();
927
- const scored = platforms.map((p5) => {
928
- const name = p5.name.toLowerCase();
929
- const slug = p5.platform.toLowerCase();
871
+ const scored = platforms.map((p4) => {
872
+ const name = p4.name.toLowerCase();
873
+ const slug = p4.platform.toLowerCase();
930
874
  let score = 0;
931
875
  if (name.includes(normalizedQuery) || slug.includes(normalizedQuery)) {
932
876
  score = 10;
@@ -935,7 +879,7 @@ function findSimilarPlatforms(platforms, query, limit = 3) {
935
879
  } else {
936
880
  score = countMatchingChars(normalizedQuery, slug);
937
881
  }
938
- return { platform: p5, score };
882
+ return { platform: p4, score };
939
883
  }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
940
884
  return scored.map((item) => item.platform);
941
885
  }
@@ -957,14 +901,14 @@ async function connectionAddCommand(platformArg) {
957
901
  process.exit(1);
958
902
  }
959
903
  const api = new PicaApi(apiKey);
960
- const spinner5 = p2.spinner();
961
- spinner5.start("Loading platforms...");
904
+ const spinner4 = p2.spinner();
905
+ spinner4.start("Loading platforms...");
962
906
  let platforms;
963
907
  try {
964
908
  platforms = await api.listPlatforms();
965
- spinner5.stop(`${platforms.length} platforms available`);
909
+ spinner4.stop(`${platforms.length} platforms available`);
966
910
  } catch (error) {
967
- spinner5.stop("Failed to load platforms");
911
+ spinner4.stop("Failed to load platforms");
968
912
  p2.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
969
913
  process.exit(1);
970
914
  }
@@ -1062,11 +1006,11 @@ async function connectionListCommand() {
1062
1006
  process.exit(1);
1063
1007
  }
1064
1008
  const api = new PicaApi(apiKey);
1065
- const spinner5 = p2.spinner();
1066
- spinner5.start("Loading connections...");
1009
+ const spinner4 = p2.spinner();
1010
+ spinner4.start("Loading connections...");
1067
1011
  try {
1068
1012
  const connections = await api.listConnections();
1069
- spinner5.stop(`${connections.length} connection${connections.length === 1 ? "" : "s"} found`);
1013
+ spinner4.stop(`${connections.length} connection${connections.length === 1 ? "" : "s"} found`);
1070
1014
  if (connections.length === 0) {
1071
1015
  p2.note(
1072
1016
  `No connections yet.
@@ -1095,7 +1039,7 @@ Add one with: ${pc3.cyan("pica connection add gmail")}`,
1095
1039
  console.log();
1096
1040
  p2.note(`Add more with: ${pc3.cyan("pica connection add <platform>")}`, "Tip");
1097
1041
  } catch (error) {
1098
- spinner5.stop("Failed to load connections");
1042
+ spinner4.stop("Failed to load connections");
1099
1043
  p2.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1100
1044
  process.exit(1);
1101
1045
  }
@@ -1123,11 +1067,11 @@ async function platformsCommand(options) {
1123
1067
  process.exit(1);
1124
1068
  }
1125
1069
  const api = new PicaApi(apiKey);
1126
- const spinner5 = p3.spinner();
1127
- spinner5.start("Loading platforms...");
1070
+ const spinner4 = p3.spinner();
1071
+ spinner4.start("Loading platforms...");
1128
1072
  try {
1129
1073
  const platforms = await api.listPlatforms();
1130
- spinner5.stop(`${platforms.length} platforms available`);
1074
+ spinner4.stop(`${platforms.length} platforms available`);
1131
1075
  if (options.json) {
1132
1076
  console.log(JSON.stringify(platforms, null, 2));
1133
1077
  return;
@@ -1179,317 +1123,12 @@ async function platformsCommand(options) {
1179
1123
  console.log();
1180
1124
  p3.note(`Connect with: ${pc4.cyan("pica connection add <platform>")}`, "Tip");
1181
1125
  } catch (error) {
1182
- spinner5.stop("Failed to load platforms");
1126
+ spinner4.stop("Failed to load platforms");
1183
1127
  p3.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1184
1128
  process.exit(1);
1185
1129
  }
1186
1130
  }
1187
1131
 
1188
- // src/commands/actions.ts
1189
- import * as p4 from "@clack/prompts";
1190
- import pc5 from "picocolors";
1191
- function getApi() {
1192
- const apiKey = getApiKey();
1193
- if (!apiKey) {
1194
- p4.cancel("Not configured. Run `pica init` first.");
1195
- process.exit(1);
1196
- }
1197
- return new PicaApi(apiKey);
1198
- }
1199
- function colorMethod(method) {
1200
- const m = method.toUpperCase();
1201
- switch (m) {
1202
- case "GET":
1203
- return pc5.green(m);
1204
- case "POST":
1205
- return pc5.yellow(m);
1206
- case "PUT":
1207
- return pc5.blue(m);
1208
- case "PATCH":
1209
- return pc5.cyan(m);
1210
- case "DELETE":
1211
- return pc5.red(m);
1212
- default:
1213
- return pc5.dim(m);
1214
- }
1215
- }
1216
- function padMethod(method) {
1217
- return method.toUpperCase().padEnd(7);
1218
- }
1219
- async function actionsSearchCommand(platform, query, options = {}) {
1220
- const api = getApi();
1221
- if (!query) {
1222
- const input = await p4.text({
1223
- message: `Search actions on ${pc5.cyan(platform)}:`,
1224
- placeholder: "send email, create contact, list orders..."
1225
- });
1226
- if (p4.isCancel(input)) {
1227
- p4.cancel("Cancelled.");
1228
- process.exit(0);
1229
- }
1230
- query = input;
1231
- }
1232
- const spinner5 = p4.spinner();
1233
- spinner5.start(`Searching ${platform} actions...`);
1234
- try {
1235
- const limit = options.limit ? parseInt(options.limit, 10) : 10;
1236
- const actions2 = await api.searchActions(platform, query, limit);
1237
- spinner5.stop(`${actions2.length} action${actions2.length === 1 ? "" : "s"} found`);
1238
- if (options.json) {
1239
- console.log(JSON.stringify(actions2, null, 2));
1240
- return;
1241
- }
1242
- if (actions2.length === 0) {
1243
- p4.note(`No actions found for "${query}" on ${platform}.`, "No Results");
1244
- return;
1245
- }
1246
- console.log();
1247
- const rows = actions2.map((action) => ({
1248
- method: colorMethod(padMethod(action.method)),
1249
- path: action.path,
1250
- title: action.title,
1251
- id: action._id
1252
- }));
1253
- printTable(
1254
- [
1255
- { key: "method", label: "Method" },
1256
- { key: "title", label: "Title" },
1257
- { key: "path", label: "Path", color: pc5.dim },
1258
- { key: "id", label: "Action ID", color: pc5.dim }
1259
- ],
1260
- rows
1261
- );
1262
- console.log();
1263
- p4.note(
1264
- `Get docs: ${pc5.cyan("pica actions knowledge <actionId>")}
1265
- Execute: ${pc5.cyan("pica actions execute <actionId>")}`,
1266
- "Next Steps"
1267
- );
1268
- } catch (error) {
1269
- spinner5.stop("Search failed");
1270
- p4.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1271
- process.exit(1);
1272
- }
1273
- }
1274
- async function actionsKnowledgeCommand(actionId, options = {}) {
1275
- const api = getApi();
1276
- const spinner5 = p4.spinner();
1277
- spinner5.start("Loading action knowledge...");
1278
- try {
1279
- const knowledge = await api.getActionKnowledge(actionId);
1280
- spinner5.stop("Action knowledge loaded");
1281
- if (!knowledge) {
1282
- p4.cancel(`No knowledge found for action: ${actionId}`);
1283
- process.exit(1);
1284
- }
1285
- if (options.json) {
1286
- console.log(JSON.stringify(knowledge, null, 2));
1287
- return;
1288
- }
1289
- printKnowledge(knowledge, options.full);
1290
- } catch (error) {
1291
- spinner5.stop("Failed to load knowledge");
1292
- p4.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1293
- process.exit(1);
1294
- }
1295
- }
1296
- function printKnowledge(k, full) {
1297
- console.log();
1298
- console.log(pc5.bold(` ${k.title}`));
1299
- console.log();
1300
- console.log(` Platform: ${pc5.cyan(k.connectionPlatform)}`);
1301
- console.log(` Method: ${colorMethod(k.method)}`);
1302
- console.log(` Path: ${k.path}`);
1303
- console.log(` Base URL: ${pc5.dim(k.baseUrl)}`);
1304
- if (k.tags?.length) {
1305
- console.log(` Tags: ${k.tags.map((t) => pc5.dim(t)).join(", ")}`);
1306
- }
1307
- const pathVars = extractPathVariables(k.path);
1308
- if (pathVars.length > 0) {
1309
- console.log(` Path Vars: ${pathVars.map((v) => pc5.yellow(`{{${v}}}`)).join(", ")}`);
1310
- }
1311
- console.log(` Active: ${k.active ? pc5.green("yes") : pc5.red("no")}`);
1312
- console.log(` ID: ${pc5.dim(k._id)}`);
1313
- if (k.knowledge) {
1314
- console.log();
1315
- console.log(pc5.bold(" API Documentation"));
1316
- console.log(pc5.dim(" " + "\u2500".repeat(40)));
1317
- console.log();
1318
- const lines = k.knowledge.split("\n");
1319
- const displayLines = full ? lines : lines.slice(0, 50);
1320
- for (const line of displayLines) {
1321
- console.log(` ${line}`);
1322
- }
1323
- if (!full && lines.length > 50) {
1324
- console.log();
1325
- console.log(pc5.dim(` ... ${lines.length - 50} more lines. Use --full to see all.`));
1326
- }
1327
- }
1328
- console.log();
1329
- }
1330
- async function actionsExecuteCommand(actionId, options = {}) {
1331
- const api = getApi();
1332
- const spinner5 = p4.spinner();
1333
- spinner5.start("Loading action details...");
1334
- let knowledge;
1335
- try {
1336
- const k = await api.getActionKnowledge(actionId);
1337
- if (!k) {
1338
- spinner5.stop("Action not found");
1339
- p4.cancel(`No action found for: ${actionId}`);
1340
- process.exit(1);
1341
- }
1342
- knowledge = k;
1343
- spinner5.stop(`${colorMethod(knowledge.method)} ${knowledge.path}`);
1344
- } catch (error) {
1345
- spinner5.stop("Failed to load action");
1346
- p4.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1347
- process.exit(1);
1348
- }
1349
- let connectionKey = options.connection;
1350
- if (!connectionKey) {
1351
- connectionKey = await resolveConnection(api, knowledge.connectionPlatform);
1352
- }
1353
- const pathVarMap = parseKeyValuePairs(options.pathVar || []);
1354
- const pathVars = extractPathVariables(knowledge.path);
1355
- for (const v of pathVars) {
1356
- if (!pathVarMap[v]) {
1357
- const input = await p4.text({
1358
- message: `Value for path variable ${pc5.yellow(`{{${v}}}`)}:`,
1359
- validate: (val) => val.trim() ? void 0 : "Value is required"
1360
- });
1361
- if (p4.isCancel(input)) {
1362
- p4.cancel("Cancelled.");
1363
- process.exit(0);
1364
- }
1365
- pathVarMap[v] = input;
1366
- }
1367
- }
1368
- let bodyData = {};
1369
- if (options.data) {
1370
- try {
1371
- bodyData = JSON.parse(options.data);
1372
- } catch {
1373
- p4.cancel("Invalid JSON in --data flag.");
1374
- process.exit(1);
1375
- }
1376
- } else if (!["GET", "DELETE", "HEAD"].includes(knowledge.method.toUpperCase())) {
1377
- const input = await p4.text({
1378
- message: "Request body (JSON):",
1379
- placeholder: '{"key": "value"} or leave empty'
1380
- });
1381
- if (p4.isCancel(input)) {
1382
- p4.cancel("Cancelled.");
1383
- process.exit(0);
1384
- }
1385
- if (input.trim()) {
1386
- try {
1387
- bodyData = JSON.parse(input);
1388
- } catch {
1389
- p4.cancel("Invalid JSON.");
1390
- process.exit(1);
1391
- }
1392
- }
1393
- }
1394
- const queryParams = parseKeyValuePairs(options.query || []);
1395
- const { resolvedPath, remainingData } = resolveTemplateVariables(
1396
- knowledge.path,
1397
- bodyData,
1398
- pathVarMap
1399
- );
1400
- console.log();
1401
- console.log(pc5.bold(" Request Summary"));
1402
- console.log(` ${colorMethod(knowledge.method)} ${knowledge.baseUrl}${resolvedPath}`);
1403
- console.log(` Connection: ${pc5.dim(connectionKey)}`);
1404
- if (Object.keys(remainingData).length > 0) {
1405
- console.log(` Body: ${pc5.dim(JSON.stringify(remainingData))}`);
1406
- }
1407
- if (Object.keys(queryParams).length > 0) {
1408
- console.log(` Query: ${pc5.dim(JSON.stringify(queryParams))}`);
1409
- }
1410
- console.log();
1411
- const execSpinner = p4.spinner();
1412
- execSpinner.start("Executing...");
1413
- try {
1414
- const result = await api.executeAction({
1415
- method: knowledge.method,
1416
- path: resolvedPath,
1417
- actionId,
1418
- connectionKey,
1419
- data: Object.keys(remainingData).length > 0 ? remainingData : void 0,
1420
- queryParams: Object.keys(queryParams).length > 0 ? queryParams : void 0,
1421
- isFormData: options.formData,
1422
- isFormUrlEncoded: options.formUrlencoded
1423
- });
1424
- execSpinner.stop(pc5.green("Success"));
1425
- if (options.json) {
1426
- console.log(JSON.stringify(result, null, 2));
1427
- } else {
1428
- console.log();
1429
- console.log(pc5.bold(" Response"));
1430
- console.log(pc5.dim(" " + "\u2500".repeat(40)));
1431
- console.log();
1432
- console.log(formatResponse(result));
1433
- console.log();
1434
- }
1435
- } catch (error) {
1436
- execSpinner.stop(pc5.red("Failed"));
1437
- const msg = error instanceof Error ? error.message : "Unknown error";
1438
- p4.cancel(`Execution failed: ${msg}`);
1439
- process.exit(1);
1440
- }
1441
- }
1442
- async function resolveConnection(api, platform) {
1443
- const spinner5 = p4.spinner();
1444
- spinner5.start("Loading connections...");
1445
- const connections = await api.listConnections();
1446
- const matching = connections.filter(
1447
- (c) => c.platform.toLowerCase() === platform.toLowerCase()
1448
- );
1449
- spinner5.stop(`${matching.length} ${platform} connection${matching.length === 1 ? "" : "s"} found`);
1450
- if (matching.length === 0) {
1451
- p4.cancel(
1452
- `No ${platform} connections found.
1453
-
1454
- Add one with: ${pc5.cyan(`pica connection add ${platform}`)}`
1455
- );
1456
- process.exit(1);
1457
- }
1458
- if (matching.length === 1) {
1459
- p4.log.info(`Using connection: ${pc5.dim(matching[0].key)}`);
1460
- return matching[0].key;
1461
- }
1462
- const selected = await p4.select({
1463
- message: `Multiple ${platform} connections found. Which one?`,
1464
- options: matching.map((c) => ({
1465
- value: c.key,
1466
- label: `${c.key}`,
1467
- hint: c.state
1468
- }))
1469
- });
1470
- if (p4.isCancel(selected)) {
1471
- p4.cancel("Cancelled.");
1472
- process.exit(0);
1473
- }
1474
- return selected;
1475
- }
1476
- function parseKeyValuePairs(pairs) {
1477
- const result = {};
1478
- for (const pair of pairs) {
1479
- const eqIdx = pair.indexOf("=");
1480
- if (eqIdx === -1) continue;
1481
- const key = pair.slice(0, eqIdx);
1482
- const value = pair.slice(eqIdx + 1);
1483
- result[key] = value;
1484
- }
1485
- return result;
1486
- }
1487
- function formatResponse(data, indent = 2) {
1488
- const prefix = " ".repeat(indent);
1489
- const json = JSON.stringify(data, null, 2);
1490
- return json.split("\n").map((line) => `${prefix}${line}`).join("\n");
1491
- }
1492
-
1493
1132
  // src/index.ts
1494
1133
  var require2 = createRequire(import.meta.url);
1495
1134
  var { version } = require2("../package.json");
@@ -1514,23 +1153,4 @@ program.command("add [platform]").description("Shortcut for: connection add").ac
1514
1153
  program.command("list").alias("ls").description("Shortcut for: connection list").action(async () => {
1515
1154
  await connectionListCommand();
1516
1155
  });
1517
- var actions = program.command("actions").alias("a").description("Discover and execute platform actions");
1518
- actions.command("search <platform> [query]").description("Search actions on a platform").option("--json", "Output as JSON").option("-l, --limit <limit>", "Max results", "10").action(async (platform, query, options) => {
1519
- await actionsSearchCommand(platform, query, options);
1520
- });
1521
- actions.command("knowledge <actionId>").alias("k").description("Get API docs for an action").option("--json", "Output as JSON").option("--full", "Show full knowledge (no truncation)").action(async (actionId, options) => {
1522
- await actionsKnowledgeCommand(actionId, options);
1523
- });
1524
- actions.command("execute <actionId>").alias("x").description("Execute an action").option("-c, --connection <key>", "Connection key to use").option("-d, --data <json>", "Request body as JSON").option("-p, --path-var <key=value...>", "Path variable", collectValues).option("-q, --query <key=value...>", "Query parameter", collectValues).option("--form-data", "Send as multipart/form-data").option("--form-urlencoded", "Send as application/x-www-form-urlencoded").option("--json", "Output as JSON").action(async (actionId, options) => {
1525
- await actionsExecuteCommand(actionId, options);
1526
- });
1527
- program.command("search <platform> [query]").description("Shortcut for: actions search").option("--json", "Output as JSON").option("-l, --limit <limit>", "Max results", "10").action(async (platform, query, options) => {
1528
- await actionsSearchCommand(platform, query, options);
1529
- });
1530
- program.command("exec <actionId>").description("Shortcut for: actions execute").option("-c, --connection <key>", "Connection key to use").option("-d, --data <json>", "Request body as JSON").option("-p, --path-var <key=value...>", "Path variable", collectValues).option("-q, --query <key=value...>", "Query parameter", collectValues).option("--form-data", "Send as multipart/form-data").option("--form-urlencoded", "Send as application/x-www-form-urlencoded").option("--json", "Output as JSON").action(async (actionId, options) => {
1531
- await actionsExecuteCommand(actionId, options);
1532
- });
1533
- function collectValues(value, previous) {
1534
- return (previous || []).concat([value]);
1535
- }
1536
1156
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picahq/cli",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "CLI for managing Pica",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,7 +20,8 @@
20
20
  "@clack/prompts": "^0.9.1",
21
21
  "commander": "^13.1.0",
22
22
  "open": "^10.1.0",
23
- "picocolors": "^1.1.1"
23
+ "picocolors": "^1.1.1",
24
+ "smol-toml": "^1.6.0"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/node": "^22.13.1",