@picahq/cli 0.1.0 → 0.3.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/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@picahq/cli",
3
- "version": "0.1.0",
4
- "description": "CLI for managing Pica integrations",
3
+ "version": "0.3.0",
4
+ "description": "CLI for managing Pica",
5
5
  "type": "module",
6
+ "files": [
7
+ "bin",
8
+ "dist"
9
+ ],
6
10
  "bin": {
7
11
  "pica": "./bin/cli.js"
8
12
  },
@@ -26,4 +30,4 @@
26
30
  "engines": {
27
31
  "node": ">=18"
28
32
  }
29
- }
33
+ }
@@ -1,11 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm run build:*)",
5
- "Bash(npm link)",
6
- "Bash(pica:*)",
7
- "Bash(npm install:*)",
8
- "Bash(npm unlink:*)"
9
- ]
10
- }
11
- }
@@ -1,29 +0,0 @@
1
- name: Publish Package to NPM
2
- on:
3
- release:
4
- types: [created]
5
- jobs:
6
- build:
7
- runs-on: ubuntu-latest
8
- permissions:
9
- contents: read
10
- id-token: write
11
- steps:
12
- - uses: actions/checkout@v4
13
- - uses: actions/setup-node@v4
14
- with:
15
- node-version: '20.x'
16
- registry-url: 'https://registry.npmjs.org/'
17
- - name: Install latest npm
18
- run: npm install -g npm@latest
19
- - name: Install dependencies
20
- run: |
21
- if [ -f package-lock.json ]; then
22
- npm ci
23
- else
24
- npm install
25
- fi
26
- - name: Build
27
- run: npm run build
28
- - name: Publish
29
- run: npm publish --access public
package/SKILL.md DELETED
@@ -1,199 +0,0 @@
1
- ---
2
- name: pica
3
- description: Interact with third-party platforms (Gmail, Slack, HubSpot, Stripe, etc.) via Pica. Use when the user wants to search for available API actions, read API documentation, execute API calls, manage connections, or do anything involving third-party integrations.
4
- triggers:
5
- - "search actions"
6
- - "find actions"
7
- - "execute action"
8
- - "run action"
9
- - "list connections"
10
- - "add connection"
11
- - "list platforms"
12
- - "pica"
13
- - "integration"
14
- - "send email"
15
- - "check calendar"
16
- - "post to slack"
17
- ---
18
-
19
- # Pica
20
-
21
- Pica gives you access to 200+ third-party platforms (Gmail, Slack, HubSpot, Stripe, Shopify, etc.) through a single interface. It handles auth, rate limiting, and retries so you just call the action you need.
22
-
23
- ## How to interact with Pica
24
-
25
- Pica provides two interfaces: the **MCP tools** and the **CLI**. Use the right one for the job.
26
-
27
- ### MCP tools (primary, for AI agents)
28
-
29
- The Pica MCP is your primary interface. It gives you structured JSON in/out with no parsing overhead. Always prefer MCP tools for discovering, inspecting, and executing actions.
30
-
31
- The MCP exposes 5 tools:
32
-
33
- | Tool | Purpose |
34
- |------|---------|
35
- | `mcp__pica__list_pica_integrations` | List all available platforms and active connections |
36
- | `mcp__pica__search_pica_platform_actions` | Search for actions on a platform |
37
- | `mcp__pica__get_pica_action_knowledge` | Get full API docs for an action (MUST call before execute) |
38
- | `mcp__pica__execute_pica_action` | Execute an action on a connected platform |
39
-
40
- ### CLI (secondary, for setup and human-facing tasks)
41
-
42
- The `pica` CLI is better for tasks that require a browser (OAuth), interactive prompts, or human-readable output. Use it for:
43
-
44
- - **Setup**: `pica init` (configures API key and installs MCP)
45
- - **Adding connections**: `pica add gmail` (opens browser for OAuth)
46
- - **Browsing platforms**: `pica platforms` (visual, categorized list)
47
- - **Quick lookups by a human**: `pica list`, `pica search gmail "send"`
48
-
49
- ## When to use which
50
-
51
- | Task | Use |
52
- |------|-----|
53
- | Execute an API action | MCP |
54
- | Search for actions | MCP |
55
- | Read action docs | MCP |
56
- | List connections/platforms | MCP |
57
- | Add a new connection (OAuth) | CLI |
58
- | Initial setup | CLI |
59
- | Show a user what's available (visual output) | CLI |
60
-
61
- **Rule of thumb:** If you're doing the work, use MCP. If a human needs to interact (OAuth, setup), use CLI.
62
-
63
- ## Setup and prerequisites
64
-
65
- The MCP server is installed via `pica init`. If the MCP tools are not available, the CLI needs to be installed and init needs to run:
66
-
67
- ```bash
68
- pica --version
69
- ```
70
-
71
- If the command is not found, install it:
72
-
73
- ```bash
74
- cd /Users/moe/projects/one/connection-cli
75
- npm install
76
- npm run build
77
- npm link
78
- ```
79
-
80
- Then run setup:
81
-
82
- ```bash
83
- pica init
84
- ```
85
-
86
- This stores the API key in `~/.pica/config.json` and installs the MCP server into Claude Code, Claude Desktop, Cursor, and Windsurf. After init, the MCP tools will be available.
87
-
88
- ## MCP workflow
89
-
90
- This is the standard workflow for any integration task.
91
-
92
- ### Step 1: List connections and platforms
93
-
94
- ```
95
- mcp__pica__list_pica_integrations()
96
- ```
97
-
98
- Returns all active connections (with connection keys) and available platforms. Check this first to see what's connected.
99
-
100
- ### Step 2: Search for the right action
101
-
102
- ```
103
- mcp__pica__search_pica_platform_actions({
104
- platform: "gmail",
105
- query: "send email",
106
- agentType: "execute"
107
- })
108
- ```
109
-
110
- Use `agentType: "execute"` when the intent is to run the action. Use `"knowledge"` when the user wants to understand the API or write code against it.
111
-
112
- The platform name must be in kebab-case (e.g., `ship-station`, `hubspot`, `google-calendar`). Get the exact name from `list_pica_integrations`.
113
-
114
- ### Step 3: Get action knowledge (required before execute)
115
-
116
- ```
117
- mcp__pica__get_pica_action_knowledge({
118
- actionId: "conn_mod_def::ABC123::XYZ789",
119
- platform: "gmail"
120
- })
121
- ```
122
-
123
- This returns full API documentation: parameters, request/response schemas, caveats, examples. You MUST call this before executing to understand what the action expects.
124
-
125
- ### Step 4: Execute
126
-
127
- ```
128
- mcp__pica__execute_pica_action({
129
- actionId: "conn_mod_def::ABC123::XYZ789",
130
- connectionKey: "live::gmail::default::abc123",
131
- platform: "gmail",
132
- data: { to: "someone@example.com", subject: "Hello", body: "Hi there" },
133
- pathVariables: { userId: "me" }
134
- })
135
- ```
136
-
137
- Pass the `connectionKey` from step 1, the `actionId` from step 2, and the parameters from step 3.
138
-
139
- ## CLI reference
140
-
141
- For when you need the CLI (setup, OAuth, human-facing output):
142
-
143
- | Command | Description |
144
- |---------|-------------|
145
- | `pica init` | Set up API key and install MCP |
146
- | `pica list` | List connections with keys |
147
- | `pica add <platform>` | Add a new connection via OAuth (opens browser) |
148
- | `pica platforms` | Browse all available platforms |
149
- | `pica search <platform> [query]` | Search for actions |
150
- | `pica actions knowledge <id>` | Get API docs for an action |
151
- | `pica exec <id>` | Execute an action |
152
-
153
- ### CLI options for exec
154
-
155
- ```bash
156
- pica exec <actionId> \
157
- -c <connectionKey> \
158
- -d '{"key": "value"}' \
159
- -p pathVar=value \
160
- -q queryParam=value \
161
- --json
162
- ```
163
-
164
- All commands support `--json` for machine-readable output.
165
-
166
- ## Key concepts
167
-
168
- - **Connection key**: Identifies which authenticated connection to use. Format: `live::gmail::default::abc123` or `test::gmail::default::abc123`. Get these from `list_pica_integrations` or `pica list`.
169
- - **Action ID**: Identifies a specific API action. Always starts with `conn_mod_def::`. Get these from search results.
170
- - **Platform name**: Kebab-case identifier (e.g., `gmail`, `hubspot`, `google-calendar`, `ship-station`). Get the exact name from `list_pica_integrations`.
171
- - **Passthrough proxy**: All actions route through Pica's proxy which injects auth, handles rate limits, and normalizes responses. You never touch raw OAuth tokens.
172
- - **Pagination**: Some actions return `nextPageToken` or similar. Pass it back in subsequent requests to page through results.
173
-
174
- ## Common patterns
175
-
176
- ### Send an email
177
- 1. Search: `platform: "gmail", query: "send email"`
178
- 2. Knowledge: get the action docs
179
- 3. Execute with `data: { to, subject, body, connectionKey }`
180
-
181
- ### Read emails
182
- 1. Search: `platform: "gmail", query: "get emails"`
183
- 2. Knowledge: understand pagination (numberOfEmails, pageToken)
184
- 3. Execute with `data: { connectionKey, numberOfEmails, label, query }`
185
-
186
- ### Post to Slack
187
- 1. Search: `platform: "slack", query: "post message"`
188
- 2. Knowledge: get channel ID format
189
- 3. Execute with `data: { channel, text }`
190
-
191
- ### CRM operations
192
- 1. Search: `platform: "hubspot"` or `"attio"`, query: `"create contact"` / `"list contacts"` / etc.
193
- 2. Knowledge: understand required fields
194
- 3. Execute with the right data shape
195
-
196
- ### Calendar
197
- 1. Search: `platform: "google-calendar", query: "list events"`
198
- 2. Knowledge: understand time format, calendar ID
199
- 3. Execute with date range parameters
@@ -1,385 +0,0 @@
1
- import * as p from '@clack/prompts';
2
- import pc from 'picocolors';
3
- import { getApiKey } from '../lib/config.js';
4
- import { PicaApi } from '../lib/api.js';
5
- import { extractPathVariables, resolveTemplateVariables } from '../lib/actions.js';
6
- import { printTable } from '../lib/table.js';
7
- import type { PlatformAction, ActionKnowledge } from '../lib/types.js';
8
-
9
- function getApi(): PicaApi {
10
- const apiKey = getApiKey();
11
- if (!apiKey) {
12
- p.cancel('Not configured. Run `pica init` first.');
13
- process.exit(1);
14
- }
15
- return new PicaApi(apiKey);
16
- }
17
-
18
- function colorMethod(method: string): string {
19
- const m = method.toUpperCase();
20
- switch (m) {
21
- case 'GET': return pc.green(m);
22
- case 'POST': return pc.yellow(m);
23
- case 'PUT': return pc.blue(m);
24
- case 'PATCH': return pc.cyan(m);
25
- case 'DELETE': return pc.red(m);
26
- default: return pc.dim(m);
27
- }
28
- }
29
-
30
- function padMethod(method: string): string {
31
- return method.toUpperCase().padEnd(7);
32
- }
33
-
34
- // --- Search ---
35
-
36
- export async function actionsSearchCommand(
37
- platform: string,
38
- query?: string,
39
- options: { json?: boolean; limit?: string } = {}
40
- ): Promise<void> {
41
- const api = getApi();
42
-
43
- if (!query) {
44
- const input = await p.text({
45
- message: `Search actions on ${pc.cyan(platform)}:`,
46
- placeholder: 'send email, create contact, list orders...',
47
- });
48
- if (p.isCancel(input)) {
49
- p.cancel('Cancelled.');
50
- process.exit(0);
51
- }
52
- query = input;
53
- }
54
-
55
- const spinner = p.spinner();
56
- spinner.start(`Searching ${platform} actions...`);
57
-
58
- try {
59
- const limit = options.limit ? parseInt(options.limit, 10) : 10;
60
- const actions = await api.searchActions(platform, query, limit);
61
- spinner.stop(`${actions.length} action${actions.length === 1 ? '' : 's'} found`);
62
-
63
- if (options.json) {
64
- console.log(JSON.stringify(actions, null, 2));
65
- return;
66
- }
67
-
68
- if (actions.length === 0) {
69
- p.note(`No actions found for "${query}" on ${platform}.`, 'No Results');
70
- return;
71
- }
72
-
73
- console.log();
74
-
75
- const rows = actions.map(action => ({
76
- method: colorMethod(padMethod(action.method)),
77
- path: action.path,
78
- title: action.title,
79
- id: action._id,
80
- }));
81
-
82
- printTable(
83
- [
84
- { key: 'method', label: 'Method' },
85
- { key: 'title', label: 'Title' },
86
- { key: 'path', label: 'Path', color: pc.dim },
87
- { key: 'id', label: 'Action ID', color: pc.dim },
88
- ],
89
- rows
90
- );
91
-
92
- console.log();
93
- p.note(
94
- `Get docs: ${pc.cyan('pica actions knowledge <actionId>')}\n` +
95
- `Execute: ${pc.cyan('pica actions execute <actionId>')}`,
96
- 'Next Steps'
97
- );
98
- } catch (error) {
99
- spinner.stop('Search failed');
100
- p.cancel(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
101
- process.exit(1);
102
- }
103
- }
104
-
105
- // --- Knowledge ---
106
-
107
- export async function actionsKnowledgeCommand(
108
- actionId: string,
109
- options: { json?: boolean; full?: boolean } = {}
110
- ): Promise<void> {
111
- const api = getApi();
112
-
113
- const spinner = p.spinner();
114
- spinner.start('Loading action knowledge...');
115
-
116
- try {
117
- const knowledge = await api.getActionKnowledge(actionId);
118
- spinner.stop('Action knowledge loaded');
119
-
120
- if (!knowledge) {
121
- p.cancel(`No knowledge found for action: ${actionId}`);
122
- process.exit(1);
123
- }
124
-
125
- if (options.json) {
126
- console.log(JSON.stringify(knowledge, null, 2));
127
- return;
128
- }
129
-
130
- printKnowledge(knowledge, options.full);
131
- } catch (error) {
132
- spinner.stop('Failed to load knowledge');
133
- p.cancel(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
134
- process.exit(1);
135
- }
136
- }
137
-
138
- function printKnowledge(k: ActionKnowledge, full?: boolean): void {
139
- console.log();
140
- console.log(pc.bold(` ${k.title}`));
141
- console.log();
142
- console.log(` Platform: ${pc.cyan(k.connectionPlatform)}`);
143
- console.log(` Method: ${colorMethod(k.method)}`);
144
- console.log(` Path: ${k.path}`);
145
- console.log(` Base URL: ${pc.dim(k.baseUrl)}`);
146
-
147
- if (k.tags?.length) {
148
- console.log(` Tags: ${k.tags.map(t => pc.dim(t)).join(', ')}`);
149
- }
150
-
151
- const pathVars = extractPathVariables(k.path);
152
- if (pathVars.length > 0) {
153
- console.log(` Path Vars: ${pathVars.map(v => pc.yellow(`{{${v}}}`)).join(', ')}`);
154
- }
155
-
156
- console.log(` Active: ${k.active ? pc.green('yes') : pc.red('no')}`);
157
- console.log(` ID: ${pc.dim(k._id)}`);
158
-
159
- if (k.knowledge) {
160
- console.log();
161
- console.log(pc.bold(' API Documentation'));
162
- console.log(pc.dim(' ' + '─'.repeat(40)));
163
- console.log();
164
-
165
- const lines = k.knowledge.split('\n');
166
- const displayLines = full ? lines : lines.slice(0, 50);
167
-
168
- for (const line of displayLines) {
169
- console.log(` ${line}`);
170
- }
171
-
172
- if (!full && lines.length > 50) {
173
- console.log();
174
- console.log(pc.dim(` ... ${lines.length - 50} more lines. Use --full to see all.`));
175
- }
176
- }
177
-
178
- console.log();
179
- }
180
-
181
- // --- Execute ---
182
-
183
- export async function actionsExecuteCommand(
184
- actionId: string,
185
- options: {
186
- json?: boolean;
187
- connection?: string;
188
- data?: string;
189
- pathVar?: string[];
190
- query?: string[];
191
- formData?: boolean;
192
- formUrlencoded?: boolean;
193
- } = {}
194
- ): Promise<void> {
195
- const api = getApi();
196
-
197
- // 1. Fetch action knowledge to get method + path
198
- const spinner = p.spinner();
199
- spinner.start('Loading action details...');
200
-
201
- let knowledge: ActionKnowledge;
202
- try {
203
- const k = await api.getActionKnowledge(actionId);
204
- if (!k) {
205
- spinner.stop('Action not found');
206
- p.cancel(`No action found for: ${actionId}`);
207
- process.exit(1);
208
- }
209
- knowledge = k;
210
- spinner.stop(`${colorMethod(knowledge.method)} ${knowledge.path}`);
211
- } catch (error) {
212
- spinner.stop('Failed to load action');
213
- p.cancel(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
214
- process.exit(1);
215
- }
216
-
217
- // 2. Resolve connection key
218
- let connectionKey = options.connection;
219
- if (!connectionKey) {
220
- connectionKey = await resolveConnection(api, knowledge.connectionPlatform);
221
- }
222
-
223
- // 3. Resolve path variables
224
- const pathVarMap = parseKeyValuePairs(options.pathVar || []);
225
- const pathVars = extractPathVariables(knowledge.path);
226
- for (const v of pathVars) {
227
- if (!pathVarMap[v]) {
228
- const input = await p.text({
229
- message: `Value for path variable ${pc.yellow(`{{${v}}}`)}:`,
230
- validate: (val) => val.trim() ? undefined : 'Value is required',
231
- });
232
- if (p.isCancel(input)) {
233
- p.cancel('Cancelled.');
234
- process.exit(0);
235
- }
236
- pathVarMap[v] = input;
237
- }
238
- }
239
-
240
- // 4. Resolve body data
241
- let bodyData: Record<string, unknown> = {};
242
- if (options.data) {
243
- try {
244
- bodyData = JSON.parse(options.data);
245
- } catch {
246
- p.cancel('Invalid JSON in --data flag.');
247
- process.exit(1);
248
- }
249
- } else if (!['GET', 'DELETE', 'HEAD'].includes(knowledge.method.toUpperCase())) {
250
- const input = await p.text({
251
- message: 'Request body (JSON):',
252
- placeholder: '{"key": "value"} or leave empty',
253
- });
254
- if (p.isCancel(input)) {
255
- p.cancel('Cancelled.');
256
- process.exit(0);
257
- }
258
- if (input.trim()) {
259
- try {
260
- bodyData = JSON.parse(input);
261
- } catch {
262
- p.cancel('Invalid JSON.');
263
- process.exit(1);
264
- }
265
- }
266
- }
267
-
268
- // 5. Resolve query params
269
- const queryParams = parseKeyValuePairs(options.query || []);
270
-
271
- // 6. Resolve template variables in path
272
- const { resolvedPath, remainingData } = resolveTemplateVariables(
273
- knowledge.path,
274
- bodyData,
275
- pathVarMap
276
- );
277
-
278
- // 7. Show summary
279
- console.log();
280
- console.log(pc.bold(' Request Summary'));
281
- console.log(` ${colorMethod(knowledge.method)} ${knowledge.baseUrl}${resolvedPath}`);
282
- console.log(` Connection: ${pc.dim(connectionKey)}`);
283
- if (Object.keys(remainingData).length > 0) {
284
- console.log(` Body: ${pc.dim(JSON.stringify(remainingData))}`);
285
- }
286
- if (Object.keys(queryParams).length > 0) {
287
- console.log(` Query: ${pc.dim(JSON.stringify(queryParams))}`);
288
- }
289
- console.log();
290
-
291
- // 8. Execute
292
- const execSpinner = p.spinner();
293
- execSpinner.start('Executing...');
294
-
295
- try {
296
- const result = await api.executeAction({
297
- method: knowledge.method,
298
- path: resolvedPath,
299
- actionId,
300
- connectionKey,
301
- data: Object.keys(remainingData).length > 0 ? remainingData : undefined,
302
- queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
303
- isFormData: options.formData,
304
- isFormUrlEncoded: options.formUrlencoded,
305
- });
306
-
307
- execSpinner.stop(pc.green('Success'));
308
-
309
- if (options.json) {
310
- console.log(JSON.stringify(result, null, 2));
311
- } else {
312
- console.log();
313
- console.log(pc.bold(' Response'));
314
- console.log(pc.dim(' ' + '─'.repeat(40)));
315
- console.log();
316
- console.log(formatResponse(result));
317
- console.log();
318
- }
319
- } catch (error) {
320
- execSpinner.stop(pc.red('Failed'));
321
- const msg = error instanceof Error ? error.message : 'Unknown error';
322
- p.cancel(`Execution failed: ${msg}`);
323
- process.exit(1);
324
- }
325
- }
326
-
327
- // --- Helpers ---
328
-
329
- async function resolveConnection(api: PicaApi, platform: string): Promise<string> {
330
- const spinner = p.spinner();
331
- spinner.start('Loading connections...');
332
-
333
- const connections = await api.listConnections();
334
- const matching = connections.filter(
335
- c => c.platform.toLowerCase() === platform.toLowerCase()
336
- );
337
- spinner.stop(`${matching.length} ${platform} connection${matching.length === 1 ? '' : 's'} found`);
338
-
339
- if (matching.length === 0) {
340
- p.cancel(
341
- `No ${platform} connections found.\n\n` +
342
- `Add one with: ${pc.cyan(`pica connection add ${platform}`)}`
343
- );
344
- process.exit(1);
345
- }
346
-
347
- if (matching.length === 1) {
348
- p.log.info(`Using connection: ${pc.dim(matching[0].key)}`);
349
- return matching[0].key;
350
- }
351
-
352
- const selected = await p.select({
353
- message: `Multiple ${platform} connections found. Which one?`,
354
- options: matching.map(c => ({
355
- value: c.key,
356
- label: `${c.key}`,
357
- hint: c.state,
358
- })),
359
- });
360
-
361
- if (p.isCancel(selected)) {
362
- p.cancel('Cancelled.');
363
- process.exit(0);
364
- }
365
-
366
- return selected as string;
367
- }
368
-
369
- function parseKeyValuePairs(pairs: string[]): Record<string, string> {
370
- const result: Record<string, string> = {};
371
- for (const pair of pairs) {
372
- const eqIdx = pair.indexOf('=');
373
- if (eqIdx === -1) continue;
374
- const key = pair.slice(0, eqIdx);
375
- const value = pair.slice(eqIdx + 1);
376
- result[key] = value;
377
- }
378
- return result;
379
- }
380
-
381
- function formatResponse(data: unknown, indent = 2): string {
382
- const prefix = ' '.repeat(indent);
383
- const json = JSON.stringify(data, null, 2);
384
- return json.split('\n').map(line => `${prefix}${line}`).join('\n');
385
- }