@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.
- package/README.md +3 -48
- package/dist/index.js +54 -434
- 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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
260
|
-
throw new ApiError(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
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
879
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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((
|
|
928
|
-
const name =
|
|
929
|
-
const slug =
|
|
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:
|
|
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
|
|
961
|
-
|
|
904
|
+
const spinner4 = p2.spinner();
|
|
905
|
+
spinner4.start("Loading platforms...");
|
|
962
906
|
let platforms;
|
|
963
907
|
try {
|
|
964
908
|
platforms = await api.listPlatforms();
|
|
965
|
-
|
|
909
|
+
spinner4.stop(`${platforms.length} platforms available`);
|
|
966
910
|
} catch (error) {
|
|
967
|
-
|
|
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
|
|
1066
|
-
|
|
1009
|
+
const spinner4 = p2.spinner();
|
|
1010
|
+
spinner4.start("Loading connections...");
|
|
1067
1011
|
try {
|
|
1068
1012
|
const connections = await api.listConnections();
|
|
1069
|
-
|
|
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
|
-
|
|
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
|
|
1127
|
-
|
|
1070
|
+
const spinner4 = p3.spinner();
|
|
1071
|
+
spinner4.start("Loading platforms...");
|
|
1128
1072
|
try {
|
|
1129
1073
|
const platforms = await api.listPlatforms();
|
|
1130
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|