@picahq/cli 1.8.0 → 1.9.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 +166 -95
  2. package/dist/index.js +488 -37
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,163 +1,234 @@
1
- # pica
1
+ # Pica CLI
2
2
 
3
- CLI for managing Pica integrations. Connect to 200+ platforms, discover their APIs, and execute actions from the terminal.
3
+ One CLI to connect AI agents to every API on the internet.
4
+
5
+ Pica gives your AI agent authenticated access to 200+ platforms — Gmail, Slack, Shopify, HubSpot, Stripe, Notion, and everything else — through a single interface. No API keys to juggle, no OAuth flows to build, no request formats to memorize. Connect a platform once, and your agent can search for actions, read the docs, and execute API calls in seconds.
4
6
 
5
7
  ## Install
6
8
 
7
9
  ```bash
8
- npm install
9
- npm run build
10
- npm link
10
+ npx @picahq/cli@latest init
11
11
  ```
12
12
 
13
- Requires Node.js 18+.
14
-
15
- ## Setup
13
+ Or install globally:
16
14
 
17
15
  ```bash
16
+ npm install -g @picahq/cli
18
17
  pica init
19
18
  ```
20
19
 
21
- This walks you through:
22
- 1. Entering your Pica API key (validates it)
23
- 2. Choosing which AI agents to install the MCP server into
24
- 3. Choosing global vs project-level installation
20
+ `pica init` walks you through setup: enter your [API key](https://app.picaos.com/settings/api-keys), pick your AI agents, and you're done. The MCP server gets installed automatically.
25
21
 
26
- Get your API key at [app.picaos.com/settings/api-keys](https://app.picaos.com/settings/api-keys).
22
+ Requires Node.js 18+.
23
+
24
+ ## Quick start
25
+
26
+ ```bash
27
+ # Connect a platform
28
+ pica add gmail
27
29
 
28
- Config is saved to `~/.pica/config.json`.
30
+ # See what you're connected to
31
+ pica list
29
32
 
30
- ### Re-running init
33
+ # Search for actions you can take
34
+ pica actions search gmail "send email" -t execute
31
35
 
32
- If you already have a config, `pica init` shows your current setup instead of starting over:
36
+ # Read the docs for an action
37
+ pica actions knowledge gmail <actionId>
33
38
 
39
+ # Execute it
40
+ pica actions execute gmail <actionId> <connectionKey> \
41
+ -d '{"to": "jane@example.com", "subject": "Hello", "body": "Sent from my AI agent"}'
34
42
  ```
35
- Pica
36
43
 
37
- Current Setup
38
- ──────────────────────────────────────────
39
- API Key: sk_test_...9j-Y
40
- Config: ~/.pica/config.json
44
+ That's it. Five commands to go from zero to sending an email through Gmail's API — fully authenticated, correctly formatted, without touching a single OAuth token.
41
45
 
42
- Agent Global Project
43
- ────────────── ────── ───────
44
- Claude Code ● yes ● yes
45
- Claude Desktop ● yes -
46
- Cursor ○ no ○ no
47
- Windsurf - -
48
- Codex ● yes ○ no
49
- Kiro ○ no ○ no
46
+ ## How it works
50
47
 
51
- - = not detected on this machine
48
+ ```
49
+ Your AI Agent
50
+
51
+ Pica CLI
52
+
53
+ Pica API (api.picaos.com/v1/passthrough)
54
+
55
+ Gmail / Slack / Shopify / HubSpot / Stripe / ...
52
56
  ```
53
57
 
54
- Then it offers targeted actions based on what's missing:
58
+ Every API call routes through Pica's passthrough proxy. Pica injects the right credentials, handles rate limiting, and normalizes responses. You never see or manage raw OAuth tokens — your connection key is all you need.
55
59
 
56
- - **Update API key** -- validates the new key, then re-installs to every agent that currently has the MCP (preserving global/project scopes)
57
- - **Install MCP to more agents** -- only shows detected agents missing the MCP
58
- - **Install MCP for this project** -- creates `.mcp.json` / `.cursor/mcp.json` / `.codex/config.toml` / `.kiro/settings/mcp.json` in cwd for agents that support project scope
59
- - **Start fresh** -- full setup flow from scratch
60
+ ## Commands
60
61
 
61
- Options that don't apply are hidden. If every detected agent already has the MCP globally, "Install MCP to more agents" won't appear.
62
+ ### `pica init`
62
63
 
63
- ### Init flags
64
+ Set up your API key and install the MCP server into your AI agents.
64
65
 
65
- | Flag | Effect |
66
- |------|--------|
67
- | `-y, --yes` | Skip confirmations |
68
- | `-g, --global` | Install MCP globally (default, available in all projects) |
69
- | `-p, --project` | Install MCP for this project only (creates config files in cwd) |
66
+ ```bash
67
+ pica init
68
+ ```
69
+
70
+ Supports Claude Code, Claude Desktop, Cursor, Windsurf, Codex, and Kiro. Installs globally by default, or per-project with `-p` so your team can share configs (each person uses their own API key).
71
+
72
+ If you've already set up, `pica init` shows your current status and lets you update your key, install to more agents, or reconfigure.
73
+
74
+ | Flag | What it does |
75
+ |------|-------------|
76
+ | `-y` | Skip confirmations |
77
+ | `-g` | Install globally (default) |
78
+ | `-p` | Install for current project only |
70
79
 
71
- ## Usage
80
+ ### `pica add <platform>`
72
81
 
73
- ### Connect a platform
82
+ Connect a new platform via OAuth.
74
83
 
75
84
  ```bash
85
+ pica add shopify
86
+ pica add hub-spot
76
87
  pica add gmail
77
88
  ```
78
89
 
79
- Opens a browser to complete OAuth. The CLI polls until the connection is live.
90
+ Opens your browser, you authorize, done. The CLI polls until the connection is live. Platform names are kebab-case — run `pica platforms` to see them all.
80
91
 
81
- ### List connections
92
+ ### `pica list`
93
+
94
+ List your active connections with their status and connection keys.
82
95
 
83
96
  ```bash
84
97
  pica list
85
98
  ```
86
99
 
87
100
  ```
88
- ● gmail operational
89
- live::gmail::default::abc123
90
- slack operational
91
- live::slack::default::def456
101
+ ● gmail operational live::gmail::default::abc123
102
+ ● slack operational live::slack::default::def456
103
+ shopify operational live::shopify::default::ghi789
92
104
  ```
93
105
 
94
- ### Browse platforms
106
+ You need the connection key (rightmost column) when executing actions.
107
+
108
+ ### `pica platforms`
109
+
110
+ Browse all 200+ available platforms.
95
111
 
96
112
  ```bash
97
- pica platforms
98
- pica platforms -c "CRM"
113
+ pica platforms # all platforms
114
+ pica platforms -c "CRM" # filter by category
115
+ pica platforms --json # machine-readable output
99
116
  ```
100
117
 
101
- ## Commands
118
+ ### `pica actions search <platform> <query>`
119
+
120
+ Search for API actions on a connected platform using natural language.
102
121
 
103
- | Command | Description |
104
- |---------|-------------|
105
- | `pica init` | Set up API key and install MCP |
106
- | `pica add <platform>` | Connect a platform via OAuth |
107
- | `pica list` | List connections with keys |
108
- | `pica platforms` | Browse available platforms |
122
+ ```bash
123
+ pica actions search shopify "list products"
124
+ pica actions search hub-spot "create contact" -t execute
125
+ pica actions search gmail "send email"
126
+ ```
109
127
 
110
- Every command supports `--json` for machine-readable output.
128
+ Returns the top 5 matching actions with their action IDs, HTTP methods, and paths. Use `-t execute` when you intend to run the action, or `-t knowledge` (default) when you want to learn about it or write code against it.
111
129
 
112
- ### Aliases
130
+ ### `pica actions knowledge <platform> <actionId>`
113
131
 
114
- | Short | Full |
115
- |-------|------|
116
- | `pica ls` | `pica list` |
117
- | `pica p` | `pica platforms` |
132
+ Get the full documentation for an action — parameters, validation rules, request/response structure, examples, and the exact API request format.
118
133
 
119
- ## How it works
134
+ ```bash
135
+ pica actions knowledge shopify 67890abcdef
136
+ ```
137
+
138
+ Always read the knowledge before executing. It tells you exactly what parameters are required, what format they need, and any platform-specific quirks.
139
+
140
+ ### `pica actions execute <platform> <actionId> <connectionKey>`
141
+
142
+ Execute an API action on a connected platform.
143
+
144
+ ```bash
145
+ # Simple GET
146
+ pica actions execute shopify <actionId> <connectionKey>
147
+
148
+ # POST with data
149
+ pica actions execute hub-spot <actionId> <connectionKey> \
150
+ -d '{"properties": {"email": "jane@example.com", "firstname": "Jane"}}'
151
+
152
+ # With path variables
153
+ pica actions execute shopify <actionId> <connectionKey> \
154
+ --path-vars '{"order_id": "12345"}'
155
+
156
+ # With query params
157
+ pica actions execute stripe <actionId> <connectionKey> \
158
+ --query-params '{"limit": "10"}'
159
+ ```
160
+
161
+ | Option | What it does |
162
+ |--------|-------------|
163
+ | `-d, --data <json>` | Request body (POST, PUT, PATCH) |
164
+ | `--path-vars <json>` | Replace `{variables}` in the URL path |
165
+ | `--query-params <json>` | Query string parameters |
166
+ | `--headers <json>` | Additional request headers |
167
+ | `--form-data` | Send as multipart/form-data |
168
+ | `--form-url-encoded` | Send as application/x-www-form-urlencoded |
169
+
170
+ ### `pica config`
171
+
172
+ Configure access control for the MCP server. Optional — full access is the default.
173
+
174
+ ```bash
175
+ pica config
176
+ ```
177
+
178
+ | Setting | Options | Default |
179
+ |---------|---------|---------|
180
+ | Permission level | `admin` / `write` / `read` | `admin` |
181
+ | Connection scope | All or specific connections | All |
182
+ | Action scope | All or specific action IDs | All |
183
+ | Knowledge-only mode | Enable/disable execution | Off |
184
+
185
+ Settings propagate automatically to all installed agent configs.
186
+
187
+ ## The workflow
188
+
189
+ The power of Pica is in the workflow. Every interaction follows the same pattern:
190
+
191
+ ```
192
+ pica list → What am I connected to?
193
+ pica actions search → What can I do?
194
+ pica actions knowledge → How do I do it?
195
+ pica actions execute → Do it.
196
+ ```
197
+
198
+ This is the same workflow whether you're sending emails, creating CRM contacts, processing payments, managing inventory, or posting to Slack. One pattern, any platform.
199
+
200
+ ## For AI agents
201
+
202
+ If you're an AI agent using the Pica MCP server, the tools map directly:
203
+
204
+ | MCP Tool | CLI Command |
205
+ |----------|------------|
206
+ | `list_pica_integrations` | `pica list` + `pica platforms` |
207
+ | `search_pica_platform_actions` | `pica actions search` |
208
+ | `get_pica_action_knowledge` | `pica actions knowledge` |
209
+ | `execute_pica_action` | `pica actions execute` |
120
210
 
121
- All API calls route through Pica's passthrough proxy (`api.picaos.com/v1/passthrough`), which injects auth credentials, handles rate limiting, and normalizes responses. Your connection keys tell Pica which credentials to use. You never touch raw OAuth tokens.
211
+ The workflow is the same: list search knowledge execute. Never skip the knowledge step it contains required parameter info and platform-specific details that are critical for building correct requests.
122
212
 
123
- ## MCP installation
213
+ ## MCP server installation
124
214
 
125
- `pica init` writes MCP server configs into the following locations:
215
+ `pica init` handles this automatically. Here's where configs go:
126
216
 
127
- | Agent | Global config | Project config |
128
- |-------|--------------|----------------|
217
+ | Agent | Global | Project |
218
+ |-------|--------|---------|
129
219
  | Claude Code | `~/.claude.json` | `.mcp.json` |
130
- | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` | n/a |
220
+ | Claude Desktop | Platform-specific app support dir | |
131
221
  | Cursor | `~/.cursor/mcp.json` | `.cursor/mcp.json` |
132
- | Windsurf | `~/.codeium/windsurf/mcp_config.json` | n/a |
222
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` | |
133
223
  | Codex | `~/.codex/config.toml` | `.codex/config.toml` |
134
224
  | Kiro | `~/.kiro/settings/mcp.json` | `.kiro/settings/mcp.json` |
135
225
 
136
- 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).
226
+ Project configs can be committed to your repo. Each team member runs `pica init` with their own API key.
137
227
 
138
228
  ## Development
139
229
 
140
230
  ```bash
141
231
  npm run dev # watch mode
142
232
  npm run build # production build
143
- npm run typecheck # type check without emitting
144
- ```
145
-
146
- ## Project structure
147
-
148
- ```
149
- src/
150
- index.ts # Commander setup and command registration
151
- commands/
152
- init.ts # pica init (setup, status display, targeted actions)
153
- connection.ts # pica add, pica list
154
- platforms.ts # pica platforms
155
- lib/
156
- api.ts # HTTP client for Pica API
157
- types.ts # TypeScript interfaces
158
- config.ts # ~/.pica/config.json read/write
159
- agents.ts # Agent detection, MCP config, status reporting
160
- platforms.ts # Platform search and fuzzy matching
161
- browser.ts # Open browser for OAuth and API key pages
162
- table.ts # Formatted table output
233
+ npm run typecheck # type check
163
234
  ```
package/dist/index.js CHANGED
@@ -73,11 +73,11 @@ import fs2 from "fs";
73
73
  import path2 from "path";
74
74
  import os2 from "os";
75
75
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
76
- function expandPath(p5) {
77
- if (p5.startsWith("~/")) {
78
- return path2.join(os2.homedir(), p5.slice(2));
76
+ function expandPath(p6) {
77
+ if (p6.startsWith("~/")) {
78
+ return path2.join(os2.homedir(), p6.slice(2));
79
79
  }
80
- return p5;
80
+ return p6;
81
81
  }
82
82
  function getClaudeDesktopConfigPath() {
83
83
  switch (process.platform) {
@@ -320,6 +320,140 @@ var PicaApi = class {
320
320
  } while (page <= totalPages);
321
321
  return allPlatforms;
322
322
  }
323
+ async searchActions(platform, query, agentType) {
324
+ const isKnowledgeAgent = !agentType || agentType === "knowledge";
325
+ const queryParams = {
326
+ query,
327
+ limit: "5"
328
+ };
329
+ if (isKnowledgeAgent) {
330
+ queryParams.knowledgeAgent = "true";
331
+ } else {
332
+ queryParams.executeAgent = "true";
333
+ }
334
+ const response = await this.requestFull({
335
+ path: `/available-actions/search/${platform}`,
336
+ queryParams
337
+ });
338
+ return response || [];
339
+ }
340
+ async getActionDetails(actionId) {
341
+ const response = await this.requestFull({
342
+ path: "/knowledge",
343
+ queryParams: { _id: actionId }
344
+ });
345
+ const actions2 = response?.rows || [];
346
+ if (actions2.length === 0) {
347
+ throw new ApiError(404, `Action with ID ${actionId} not found`);
348
+ }
349
+ return actions2[0];
350
+ }
351
+ async getActionKnowledge(actionId) {
352
+ const action = await this.getActionDetails(actionId);
353
+ if (!action.knowledge || !action.method) {
354
+ return {
355
+ knowledge: "No knowledge was found",
356
+ method: "No method was found"
357
+ };
358
+ }
359
+ return {
360
+ knowledge: action.knowledge,
361
+ method: action.method
362
+ };
363
+ }
364
+ async executePassthroughRequest(args, preloadedAction) {
365
+ const action = preloadedAction ?? await this.getActionDetails(args.actionId);
366
+ const method = action.method;
367
+ const contentType = args.isFormData ? "multipart/form-data" : args.isFormUrlEncoded ? "application/x-www-form-urlencoded" : "application/json";
368
+ const requestHeaders = {
369
+ "x-pica-secret": this.apiKey,
370
+ "x-pica-connection-key": args.connectionKey,
371
+ "x-pica-action-id": action._id,
372
+ "Content-Type": contentType,
373
+ ...args.headers
374
+ };
375
+ const finalActionPath = args.pathVariables ? replacePathVariables(action.path, args.pathVariables) : action.path;
376
+ const normalizedPath = finalActionPath.startsWith("/") ? finalActionPath : `/${finalActionPath}`;
377
+ const url = `${API_BASE.replace("/v1", "")}/v1/passthrough${normalizedPath}`;
378
+ const isCustomAction = action.tags?.includes("custom");
379
+ let requestData = args.data;
380
+ if (isCustomAction && method?.toLowerCase() !== "get") {
381
+ requestData = {
382
+ ...args.data,
383
+ connectionKey: args.connectionKey
384
+ };
385
+ }
386
+ let queryString = "";
387
+ if (args.queryParams && Object.keys(args.queryParams).length > 0) {
388
+ const params = new URLSearchParams(
389
+ Object.entries(args.queryParams).map(([k, v]) => [k, String(v)])
390
+ );
391
+ queryString = `?${params.toString()}`;
392
+ }
393
+ const fullUrl = `${url}${queryString}`;
394
+ const fetchOpts = {
395
+ method,
396
+ headers: requestHeaders
397
+ };
398
+ if (method?.toLowerCase() !== "get" && requestData !== void 0) {
399
+ if (args.isFormUrlEncoded) {
400
+ const params = new URLSearchParams();
401
+ if (requestData && typeof requestData === "object" && !Array.isArray(requestData)) {
402
+ Object.entries(requestData).forEach(([key, value]) => {
403
+ if (typeof value === "object") {
404
+ params.append(key, JSON.stringify(value));
405
+ } else {
406
+ params.append(key, String(value));
407
+ }
408
+ });
409
+ }
410
+ fetchOpts.body = params.toString();
411
+ } else if (args.isFormData) {
412
+ const boundary = `----FormBoundary${Date.now()}`;
413
+ requestHeaders["Content-Type"] = `multipart/form-data; boundary=${boundary}`;
414
+ let body = "";
415
+ if (requestData && typeof requestData === "object" && !Array.isArray(requestData)) {
416
+ Object.entries(requestData).forEach(([key, value]) => {
417
+ body += `--${boundary}\r
418
+ `;
419
+ body += `Content-Disposition: form-data; name="${key}"\r
420
+ \r
421
+ `;
422
+ body += typeof value === "object" ? JSON.stringify(value) : String(value);
423
+ body += "\r\n";
424
+ });
425
+ }
426
+ body += `--${boundary}--\r
427
+ `;
428
+ fetchOpts.body = body;
429
+ fetchOpts.headers = requestHeaders;
430
+ } else {
431
+ fetchOpts.body = JSON.stringify(requestData);
432
+ }
433
+ }
434
+ const sanitizedConfig = {
435
+ url: fullUrl,
436
+ method,
437
+ headers: {
438
+ ...requestHeaders,
439
+ "x-pica-secret": "***REDACTED***"
440
+ },
441
+ params: args.queryParams ? Object.fromEntries(
442
+ Object.entries(args.queryParams).map(([k, v]) => [k, String(v)])
443
+ ) : void 0,
444
+ data: requestData
445
+ };
446
+ const response = await fetch(fullUrl, fetchOpts);
447
+ if (!response.ok) {
448
+ const text4 = await response.text();
449
+ throw new ApiError(response.status, text4 || `HTTP ${response.status}`);
450
+ }
451
+ const responseData = await response.json();
452
+ return {
453
+ requestConfig: sanitizedConfig,
454
+ responseData
455
+ };
456
+ }
323
457
  async waitForConnection(platform, timeoutMs = 5 * 60 * 1e3, pollIntervalMs = 5e3, onPoll) {
324
458
  const startTime = Date.now();
325
459
  const existingConnections = await this.listConnections();
@@ -347,6 +481,72 @@ var TimeoutError = class extends Error {
347
481
  function sleep(ms) {
348
482
  return new Promise((resolve) => setTimeout(resolve, ms));
349
483
  }
484
+ function replacePathVariables(path3, variables) {
485
+ if (!path3) return path3;
486
+ let result = path3;
487
+ result = result.replace(/\{\{([^}]+)\}\}/g, (_match, variable) => {
488
+ const trimmedVariable = variable.trim();
489
+ const value = variables[trimmedVariable];
490
+ if (value === void 0 || value === null || value === "") {
491
+ throw new Error(`Missing value for path variable: ${trimmedVariable}`);
492
+ }
493
+ return encodeURIComponent(value.toString());
494
+ });
495
+ result = result.replace(/\{([^}]+)\}/g, (_match, variable) => {
496
+ const trimmedVariable = variable.trim();
497
+ const value = variables[trimmedVariable];
498
+ if (value === void 0 || value === null || value === "") {
499
+ throw new Error(`Missing value for path variable: ${trimmedVariable}`);
500
+ }
501
+ return encodeURIComponent(value.toString());
502
+ });
503
+ return result;
504
+ }
505
+ var PERMISSION_METHODS = {
506
+ read: ["GET"],
507
+ write: ["GET", "POST", "PUT", "PATCH"],
508
+ admin: null
509
+ };
510
+ function filterByPermissions(actions2, permissions) {
511
+ const allowed = PERMISSION_METHODS[permissions];
512
+ if (allowed === null) return actions2;
513
+ return actions2.filter((a) => allowed.includes(a.method.toUpperCase()));
514
+ }
515
+ function isMethodAllowed(method, permissions) {
516
+ const allowed = PERMISSION_METHODS[permissions];
517
+ if (allowed === null) return true;
518
+ return allowed.includes(method.toUpperCase());
519
+ }
520
+ function isActionAllowed(actionId, allowedActionIds) {
521
+ return allowedActionIds.includes("*") || allowedActionIds.includes(actionId);
522
+ }
523
+ function buildActionKnowledgeWithGuidance(knowledge, method, platform, actionId) {
524
+ const baseUrl = "https://api.picaos.com";
525
+ return `${knowledge}
526
+
527
+ API REQUEST STRUCTURE
528
+ ======================
529
+ URL: ${baseUrl}/v1/passthrough/{{PATH}}
530
+
531
+ IMPORTANT: When constructing the URL, only include the API endpoint path after the base URL.
532
+ Do NOT include the full third-party API URL.
533
+
534
+ Examples:
535
+ Correct: ${baseUrl}/v1/passthrough/crm/v3/objects/contacts/search
536
+ Incorrect: ${baseUrl}/v1/passthrough/https://api.hubapi.com/crm/v3/objects/contacts/search
537
+
538
+ METHOD: ${method}
539
+
540
+ HEADERS:
541
+ - x-pica-secret: {{process.env.PICA_SECRET}}
542
+ - x-pica-connection-key: {{process.env.PICA_${platform.toUpperCase()}_CONNECTION_KEY}}
543
+ - x-pica-action-id: ${actionId}
544
+ - ... (other headers)
545
+
546
+ BODY: {{BODY}}
547
+
548
+ QUERY PARAMS: {{QUERY_PARAMS}}`;
549
+ }
350
550
 
351
551
  // src/lib/browser.ts
352
552
  import open from "open";
@@ -484,16 +684,16 @@ async function configCommand() {
484
684
  p.outro("Access control updated.");
485
685
  }
486
686
  async function selectConnections(apiKey) {
487
- const spinner5 = p.spinner();
488
- spinner5.start("Fetching connections...");
687
+ const spinner6 = p.spinner();
688
+ spinner6.start("Fetching connections...");
489
689
  let connections;
490
690
  try {
491
691
  const api = new PicaApi(apiKey);
492
692
  const rawConnections = await api.listConnections();
493
693
  connections = rawConnections.map((c) => ({ platform: c.platform, key: c.key }));
494
- spinner5.stop(`Found ${connections.length} connection(s)`);
694
+ spinner6.stop(`Found ${connections.length} connection(s)`);
495
695
  } catch {
496
- spinner5.stop("Could not fetch connections");
696
+ spinner6.stop("Could not fetch connections");
497
697
  const manual = await p.text({
498
698
  message: "Enter connection keys manually (comma-separated):",
499
699
  placeholder: "conn_key_1, conn_key_2",
@@ -695,16 +895,16 @@ ${pc3.cyan(getApiKeyUrl())}`, "API Key");
695
895
  p2.cancel("Cancelled.");
696
896
  process.exit(0);
697
897
  }
698
- const spinner5 = p2.spinner();
699
- spinner5.start("Validating API key...");
898
+ const spinner6 = p2.spinner();
899
+ spinner6.start("Validating API key...");
700
900
  const api = new PicaApi(newKey);
701
901
  const isValid = await api.validateApiKey();
702
902
  if (!isValid) {
703
- spinner5.stop("Invalid API key");
903
+ spinner6.stop("Invalid API key");
704
904
  p2.cancel(`Invalid API key. Get a valid key at ${getApiKeyUrl()}`);
705
905
  process.exit(1);
706
906
  }
707
- spinner5.stop("API key validated");
907
+ spinner6.stop("API key validated");
708
908
  const ac = getAccessControl();
709
909
  const reinstalled = [];
710
910
  for (const s of statuses) {
@@ -840,16 +1040,16 @@ ${pc3.cyan(getApiKeyUrl())}`, "API Key");
840
1040
  p2.cancel("Setup cancelled.");
841
1041
  process.exit(0);
842
1042
  }
843
- const spinner5 = p2.spinner();
844
- spinner5.start("Validating API key...");
1043
+ const spinner6 = p2.spinner();
1044
+ spinner6.start("Validating API key...");
845
1045
  const api = new PicaApi(apiKey);
846
1046
  const isValid = await api.validateApiKey();
847
1047
  if (!isValid) {
848
- spinner5.stop("Invalid API key");
1048
+ spinner6.stop("Invalid API key");
849
1049
  p2.cancel(`Invalid API key. Get a valid key at ${getApiKeyUrl()}`);
850
1050
  process.exit(1);
851
1051
  }
852
- spinner5.stop("API key validated");
1052
+ spinner6.stop("API key validated");
853
1053
  writeConfig({
854
1054
  apiKey,
855
1055
  installedAgents: [],
@@ -1049,16 +1249,16 @@ async function promptConnectIntegrations(apiKey) {
1049
1249
  p2.log.warn("Could not open browser automatically.");
1050
1250
  p2.note(url, "Open manually");
1051
1251
  }
1052
- const spinner5 = p2.spinner();
1053
- spinner5.start("Waiting for connection... (complete auth in browser)");
1252
+ const spinner6 = p2.spinner();
1253
+ spinner6.start("Waiting for connection... (complete auth in browser)");
1054
1254
  try {
1055
1255
  await api.waitForConnection(platform, 5 * 60 * 1e3, 5e3);
1056
- spinner5.stop(`${label} connected!`);
1256
+ spinner6.stop(`${label} connected!`);
1057
1257
  p2.log.success(`${pc3.green("\u2713")} ${label} is now available to your AI agents`);
1058
1258
  connected.push(platform);
1059
1259
  first = false;
1060
1260
  } catch (error) {
1061
- spinner5.stop("Connection timed out");
1261
+ spinner6.stop("Connection timed out");
1062
1262
  if (error instanceof TimeoutError) {
1063
1263
  p2.log.warn(`No worries. Connect later with: ${pc3.cyan(`pica add ${platform}`)}`);
1064
1264
  }
@@ -1091,16 +1291,16 @@ import pc4 from "picocolors";
1091
1291
  function findPlatform(platforms, query) {
1092
1292
  const normalizedQuery = query.toLowerCase().trim();
1093
1293
  const exact = platforms.find(
1094
- (p5) => p5.platform.toLowerCase() === normalizedQuery || p5.name.toLowerCase() === normalizedQuery
1294
+ (p6) => p6.platform.toLowerCase() === normalizedQuery || p6.name.toLowerCase() === normalizedQuery
1095
1295
  );
1096
1296
  if (exact) return exact;
1097
1297
  return null;
1098
1298
  }
1099
1299
  function findSimilarPlatforms(platforms, query, limit = 3) {
1100
1300
  const normalizedQuery = query.toLowerCase().trim();
1101
- const scored = platforms.map((p5) => {
1102
- const name = p5.name.toLowerCase();
1103
- const slug = p5.platform.toLowerCase();
1301
+ const scored = platforms.map((p6) => {
1302
+ const name = p6.name.toLowerCase();
1303
+ const slug = p6.platform.toLowerCase();
1104
1304
  let score = 0;
1105
1305
  if (name.includes(normalizedQuery) || slug.includes(normalizedQuery)) {
1106
1306
  score = 10;
@@ -1109,7 +1309,7 @@ function findSimilarPlatforms(platforms, query, limit = 3) {
1109
1309
  } else {
1110
1310
  score = countMatchingChars(normalizedQuery, slug);
1111
1311
  }
1112
- return { platform: p5, score };
1312
+ return { platform: p6, score };
1113
1313
  }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
1114
1314
  return scored.map((item) => item.platform);
1115
1315
  }
@@ -1131,14 +1331,14 @@ async function connectionAddCommand(platformArg) {
1131
1331
  process.exit(1);
1132
1332
  }
1133
1333
  const api = new PicaApi(apiKey);
1134
- const spinner5 = p3.spinner();
1135
- spinner5.start("Loading platforms...");
1334
+ const spinner6 = p3.spinner();
1335
+ spinner6.start("Loading platforms...");
1136
1336
  let platforms;
1137
1337
  try {
1138
1338
  platforms = await api.listPlatforms();
1139
- spinner5.stop(`${platforms.length} platforms available`);
1339
+ spinner6.stop(`${platforms.length} platforms available`);
1140
1340
  } catch (error) {
1141
- spinner5.stop("Failed to load platforms");
1341
+ spinner6.stop("Failed to load platforms");
1142
1342
  p3.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1143
1343
  process.exit(1);
1144
1344
  }
@@ -1236,11 +1436,11 @@ async function connectionListCommand() {
1236
1436
  process.exit(1);
1237
1437
  }
1238
1438
  const api = new PicaApi(apiKey);
1239
- const spinner5 = p3.spinner();
1240
- spinner5.start("Loading connections...");
1439
+ const spinner6 = p3.spinner();
1440
+ spinner6.start("Loading connections...");
1241
1441
  try {
1242
1442
  const connections = await api.listConnections();
1243
- spinner5.stop(`${connections.length} connection${connections.length === 1 ? "" : "s"} found`);
1443
+ spinner6.stop(`${connections.length} connection${connections.length === 1 ? "" : "s"} found`);
1244
1444
  if (connections.length === 0) {
1245
1445
  p3.note(
1246
1446
  `No connections yet.
@@ -1269,7 +1469,7 @@ Add one with: ${pc4.cyan("pica connection add gmail")}`,
1269
1469
  console.log();
1270
1470
  p3.note(`Add more with: ${pc4.cyan("pica connection add <platform>")}`, "Tip");
1271
1471
  } catch (error) {
1272
- spinner5.stop("Failed to load connections");
1472
+ spinner6.stop("Failed to load connections");
1273
1473
  p3.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1274
1474
  process.exit(1);
1275
1475
  }
@@ -1297,11 +1497,11 @@ async function platformsCommand(options) {
1297
1497
  process.exit(1);
1298
1498
  }
1299
1499
  const api = new PicaApi(apiKey);
1300
- const spinner5 = p4.spinner();
1301
- spinner5.start("Loading platforms...");
1500
+ const spinner6 = p4.spinner();
1501
+ spinner6.start("Loading platforms...");
1302
1502
  try {
1303
1503
  const platforms = await api.listPlatforms();
1304
- spinner5.stop(`${platforms.length} platforms available`);
1504
+ spinner6.stop(`${platforms.length} platforms available`);
1305
1505
  if (options.json) {
1306
1506
  console.log(JSON.stringify(platforms, null, 2));
1307
1507
  return;
@@ -1353,12 +1553,246 @@ async function platformsCommand(options) {
1353
1553
  console.log();
1354
1554
  p4.note(`Connect with: ${pc5.cyan("pica connection add <platform>")}`, "Tip");
1355
1555
  } catch (error) {
1356
- spinner5.stop("Failed to load platforms");
1556
+ spinner6.stop("Failed to load platforms");
1357
1557
  p4.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1358
1558
  process.exit(1);
1359
1559
  }
1360
1560
  }
1361
1561
 
1562
+ // src/commands/actions.ts
1563
+ import * as p5 from "@clack/prompts";
1564
+ import pc6 from "picocolors";
1565
+ function getConfig() {
1566
+ const apiKey = getApiKey();
1567
+ if (!apiKey) {
1568
+ p5.cancel("Not configured. Run `pica init` first.");
1569
+ process.exit(1);
1570
+ }
1571
+ const ac = getAccessControl();
1572
+ const permissions = ac.permissions || "admin";
1573
+ const connectionKeys = ac.connectionKeys || ["*"];
1574
+ const actionIds = ac.actionIds || ["*"];
1575
+ const knowledgeAgent = ac.knowledgeAgent || false;
1576
+ return { apiKey, permissions, connectionKeys, actionIds, knowledgeAgent };
1577
+ }
1578
+ async function actionsSearchCommand(platform, query, options) {
1579
+ p5.intro(pc6.bgCyan(pc6.black(" Pica ")));
1580
+ const { apiKey, permissions, actionIds, knowledgeAgent } = getConfig();
1581
+ const api = new PicaApi(apiKey);
1582
+ const spinner6 = p5.spinner();
1583
+ spinner6.start(`Searching actions on ${pc6.cyan(platform)} for "${query}"...`);
1584
+ try {
1585
+ const agentType = knowledgeAgent ? "knowledge" : options.type;
1586
+ let actions2 = await api.searchActions(platform, query, agentType);
1587
+ actions2 = filterByPermissions(actions2, permissions);
1588
+ actions2 = actions2.filter((a) => isActionAllowed(a.systemId, actionIds));
1589
+ const cleanedActions = actions2.map((action) => ({
1590
+ actionId: action.systemId,
1591
+ title: action.title,
1592
+ method: action.method,
1593
+ path: action.path
1594
+ }));
1595
+ if (cleanedActions.length === 0) {
1596
+ spinner6.stop("No actions found");
1597
+ p5.note(
1598
+ `No actions found for platform '${platform}' matching query '${query}'.
1599
+
1600
+ Suggestions:
1601
+ - Try a more general query (e.g., 'list', 'get', 'search', 'create')
1602
+ - Verify the platform name is correct
1603
+ - Check available platforms with ${pc6.cyan("pica platforms")}
1604
+
1605
+ Examples of good queries:
1606
+ - "search contacts"
1607
+ - "send email"
1608
+ - "create customer"
1609
+ - "list orders"`,
1610
+ "No Results"
1611
+ );
1612
+ return;
1613
+ }
1614
+ spinner6.stop(
1615
+ `Found ${cleanedActions.length} action(s) for '${platform}' matching '${query}'`
1616
+ );
1617
+ console.log();
1618
+ const rows = cleanedActions.map((a) => ({
1619
+ method: colorMethod(a.method),
1620
+ title: a.title,
1621
+ actionId: a.actionId,
1622
+ path: a.path
1623
+ }));
1624
+ printTable(
1625
+ [
1626
+ { key: "method", label: "Method" },
1627
+ { key: "title", label: "Title" },
1628
+ { key: "actionId", label: "Action ID", color: pc6.dim },
1629
+ { key: "path", label: "Path", color: pc6.dim }
1630
+ ],
1631
+ rows
1632
+ );
1633
+ console.log();
1634
+ p5.note(
1635
+ `Get details: ${pc6.cyan(`pica actions knowledge ${platform} <actionId>`)}
1636
+ Execute: ${pc6.cyan(`pica actions execute ${platform} <actionId> <connectionKey>`)}`,
1637
+ "Next Steps"
1638
+ );
1639
+ } catch (error) {
1640
+ spinner6.stop("Search failed");
1641
+ p5.cancel(
1642
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
1643
+ );
1644
+ process.exit(1);
1645
+ }
1646
+ }
1647
+ async function actionsKnowledgeCommand(platform, actionId) {
1648
+ p5.intro(pc6.bgCyan(pc6.black(" Pica ")));
1649
+ const { apiKey, actionIds, connectionKeys } = getConfig();
1650
+ const api = new PicaApi(apiKey);
1651
+ if (!isActionAllowed(actionId, actionIds)) {
1652
+ p5.cancel(`Action "${actionId}" is not in the allowed action list.`);
1653
+ process.exit(1);
1654
+ }
1655
+ if (!connectionKeys.includes("*")) {
1656
+ const spinner7 = p5.spinner();
1657
+ spinner7.start("Checking connections...");
1658
+ try {
1659
+ const connections = await api.listConnections();
1660
+ const connectedPlatforms = connections.map((c) => c.platform);
1661
+ if (!connectedPlatforms.includes(platform)) {
1662
+ spinner7.stop("Platform not connected");
1663
+ p5.cancel(`Platform "${platform}" has no allowed connections.`);
1664
+ process.exit(1);
1665
+ }
1666
+ spinner7.stop("Connection verified");
1667
+ } catch (error) {
1668
+ spinner7.stop("Failed to check connections");
1669
+ p5.cancel(
1670
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
1671
+ );
1672
+ process.exit(1);
1673
+ }
1674
+ }
1675
+ const spinner6 = p5.spinner();
1676
+ spinner6.start(`Loading knowledge for action ${pc6.dim(actionId)}...`);
1677
+ try {
1678
+ const { knowledge, method } = await api.getActionKnowledge(actionId);
1679
+ const knowledgeWithGuidance = buildActionKnowledgeWithGuidance(
1680
+ knowledge,
1681
+ method,
1682
+ platform,
1683
+ actionId
1684
+ );
1685
+ spinner6.stop("Knowledge loaded");
1686
+ console.log();
1687
+ console.log(knowledgeWithGuidance);
1688
+ console.log();
1689
+ p5.note(
1690
+ `Execute: ${pc6.cyan(`pica actions execute ${platform} ${actionId} <connectionKey>`)}`,
1691
+ "Next Step"
1692
+ );
1693
+ } catch (error) {
1694
+ spinner6.stop("Failed to load knowledge");
1695
+ p5.cancel(
1696
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
1697
+ );
1698
+ process.exit(1);
1699
+ }
1700
+ }
1701
+ async function actionsExecuteCommand(platform, actionId, connectionKey, options) {
1702
+ p5.intro(pc6.bgCyan(pc6.black(" Pica ")));
1703
+ const { apiKey, permissions, actionIds, connectionKeys, knowledgeAgent } = getConfig();
1704
+ if (knowledgeAgent) {
1705
+ p5.cancel(
1706
+ `Action execution is disabled (knowledge-only mode).
1707
+ Configure with: ${pc6.cyan("pica config")}`
1708
+ );
1709
+ process.exit(1);
1710
+ }
1711
+ if (!isActionAllowed(actionId, actionIds)) {
1712
+ p5.cancel(`Action "${actionId}" is not in the allowed action list.`);
1713
+ process.exit(1);
1714
+ }
1715
+ if (!connectionKeys.includes("*") && !connectionKeys.includes(connectionKey)) {
1716
+ p5.cancel(`Connection key "${connectionKey}" is not allowed.`);
1717
+ process.exit(1);
1718
+ }
1719
+ const api = new PicaApi(apiKey);
1720
+ const spinner6 = p5.spinner();
1721
+ spinner6.start("Loading action details...");
1722
+ try {
1723
+ const actionDetails = await api.getActionDetails(actionId);
1724
+ if (!isMethodAllowed(actionDetails.method, permissions)) {
1725
+ spinner6.stop("Permission denied");
1726
+ p5.cancel(
1727
+ `Method "${actionDetails.method}" is not allowed under "${permissions}" permission level.`
1728
+ );
1729
+ process.exit(1);
1730
+ }
1731
+ spinner6.stop(`Action: ${actionDetails.title} [${actionDetails.method}]`);
1732
+ const data = options.data ? parseJsonArg(options.data, "--data") : void 0;
1733
+ const pathVariables = options.pathVars ? parseJsonArg(options.pathVars, "--path-vars") : void 0;
1734
+ const queryParams = options.queryParams ? parseJsonArg(options.queryParams, "--query-params") : void 0;
1735
+ const headers = options.headers ? parseJsonArg(options.headers, "--headers") : void 0;
1736
+ const execSpinner = p5.spinner();
1737
+ execSpinner.start("Executing action...");
1738
+ const result = await api.executePassthroughRequest(
1739
+ {
1740
+ platform,
1741
+ actionId,
1742
+ connectionKey,
1743
+ data,
1744
+ pathVariables,
1745
+ queryParams,
1746
+ headers,
1747
+ isFormData: options.formData,
1748
+ isFormUrlEncoded: options.formUrlEncoded
1749
+ },
1750
+ actionDetails
1751
+ );
1752
+ execSpinner.stop("Action executed successfully");
1753
+ console.log();
1754
+ console.log(pc6.dim("Request:"));
1755
+ console.log(
1756
+ pc6.dim(
1757
+ ` ${result.requestConfig.method} ${result.requestConfig.url}`
1758
+ )
1759
+ );
1760
+ console.log();
1761
+ console.log(pc6.bold("Response:"));
1762
+ console.log(JSON.stringify(result.responseData, null, 2));
1763
+ } catch (error) {
1764
+ spinner6.stop("Execution failed");
1765
+ p5.cancel(
1766
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
1767
+ );
1768
+ process.exit(1);
1769
+ }
1770
+ }
1771
+ function parseJsonArg(value, argName) {
1772
+ try {
1773
+ return JSON.parse(value);
1774
+ } catch {
1775
+ p5.cancel(`Invalid JSON for ${argName}: ${value}`);
1776
+ process.exit(1);
1777
+ }
1778
+ }
1779
+ function colorMethod(method) {
1780
+ switch (method.toUpperCase()) {
1781
+ case "GET":
1782
+ return pc6.green(method);
1783
+ case "POST":
1784
+ return pc6.yellow(method);
1785
+ case "PUT":
1786
+ return pc6.blue(method);
1787
+ case "PATCH":
1788
+ return pc6.magenta(method);
1789
+ case "DELETE":
1790
+ return pc6.red(method);
1791
+ default:
1792
+ return method;
1793
+ }
1794
+ }
1795
+
1362
1796
  // src/index.ts
1363
1797
  var require2 = createRequire(import.meta.url);
1364
1798
  var { version } = require2("../package.json");
@@ -1380,6 +1814,23 @@ connection.command("list").alias("ls").description("List your connections").acti
1380
1814
  program.command("platforms").alias("p").description("List available platforms").option("-c, --category <category>", "Filter by category").option("--json", "Output as JSON").action(async (options) => {
1381
1815
  await platformsCommand(options);
1382
1816
  });
1817
+ var actions = program.command("actions").alias("a").description("Search, explore, and execute platform actions");
1818
+ actions.command("search <platform> <query>").description("Search for actions on a platform").option("-t, --type <type>", "Agent type: execute or knowledge (default: knowledge)").action(async (platform, query, options) => {
1819
+ await actionsSearchCommand(platform, query, options);
1820
+ });
1821
+ actions.command("knowledge <platform> <actionId>").alias("k").description("Get detailed documentation for an action").action(async (platform, actionId) => {
1822
+ await actionsKnowledgeCommand(platform, actionId);
1823
+ });
1824
+ actions.command("execute <platform> <actionId> <connectionKey>").alias("x").description("Execute an action on a connected platform").option("-d, --data <json>", "Request body as JSON").option("--path-vars <json>", "Path variables as JSON").option("--query-params <json>", "Query parameters as JSON").option("--headers <json>", "Additional headers as JSON").option("--form-data", "Send as multipart/form-data").option("--form-url-encoded", "Send as application/x-www-form-urlencoded").action(async (platform, actionId, connectionKey, options) => {
1825
+ await actionsExecuteCommand(platform, actionId, connectionKey, {
1826
+ data: options.data,
1827
+ pathVars: options.pathVars,
1828
+ queryParams: options.queryParams,
1829
+ headers: options.headers,
1830
+ formData: options.formData,
1831
+ formUrlEncoded: options.formUrlEncoded
1832
+ });
1833
+ });
1383
1834
  program.command("add [platform]").description("Shortcut for: connection add").action(async (platform) => {
1384
1835
  await connectionAddCommand(platform);
1385
1836
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@picahq/cli",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "CLI for managing Pica",
5
5
  "type": "module",
6
6
  "files": [
@@ -31,4 +31,4 @@
31
31
  "engines": {
32
32
  "node": ">=18"
33
33
  }
34
- }
34
+ }