@picahq/cli 0.2.0 → 1.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/dist/index.js ADDED
@@ -0,0 +1,1444 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { createRequire } from "module";
5
+ import { Command } from "commander";
6
+
7
+ // src/commands/init.ts
8
+ import * as p from "@clack/prompts";
9
+ import pc2 from "picocolors";
10
+
11
+ // src/lib/config.ts
12
+ import fs from "fs";
13
+ import path from "path";
14
+ import os from "os";
15
+ var CONFIG_DIR = path.join(os.homedir(), ".pica");
16
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
17
+ function getConfigPath() {
18
+ return CONFIG_FILE;
19
+ }
20
+ function configExists() {
21
+ return fs.existsSync(CONFIG_FILE);
22
+ }
23
+ function readConfig() {
24
+ if (!configExists()) {
25
+ return null;
26
+ }
27
+ try {
28
+ const content = fs.readFileSync(CONFIG_FILE, "utf-8");
29
+ return JSON.parse(content);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ function writeConfig(config) {
35
+ if (!fs.existsSync(CONFIG_DIR)) {
36
+ fs.mkdirSync(CONFIG_DIR, { mode: 448 });
37
+ }
38
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
39
+ }
40
+ function getApiKey() {
41
+ const config = readConfig();
42
+ return config?.apiKey ?? null;
43
+ }
44
+
45
+ // src/lib/agents.ts
46
+ import fs2 from "fs";
47
+ import path2 from "path";
48
+ import os2 from "os";
49
+ function expandPath(p5) {
50
+ if (p5.startsWith("~/")) {
51
+ return path2.join(os2.homedir(), p5.slice(2));
52
+ }
53
+ return p5;
54
+ }
55
+ function getClaudeDesktopConfigPath() {
56
+ switch (process.platform) {
57
+ case "darwin":
58
+ return "~/Library/Application Support/Claude/claude_desktop_config.json";
59
+ case "win32":
60
+ return path2.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
61
+ default:
62
+ return "~/.config/Claude/claude_desktop_config.json";
63
+ }
64
+ }
65
+ function getClaudeDesktopDetectDir() {
66
+ switch (process.platform) {
67
+ case "darwin":
68
+ return "~/Library/Application Support/Claude";
69
+ case "win32":
70
+ return path2.join(process.env.APPDATA || "", "Claude");
71
+ default:
72
+ return "~/.config/Claude";
73
+ }
74
+ }
75
+ function getWindsurfConfigPath() {
76
+ if (process.platform === "win32") {
77
+ return path2.join(process.env.USERPROFILE || os2.homedir(), ".codeium", "windsurf", "mcp_config.json");
78
+ }
79
+ return "~/.codeium/windsurf/mcp_config.json";
80
+ }
81
+ function getWindsurfDetectDir() {
82
+ if (process.platform === "win32") {
83
+ return path2.join(process.env.USERPROFILE || os2.homedir(), ".codeium", "windsurf");
84
+ }
85
+ return "~/.codeium/windsurf";
86
+ }
87
+ function getCursorConfigPath() {
88
+ if (process.platform === "win32") {
89
+ return path2.join(process.env.USERPROFILE || os2.homedir(), ".cursor", "mcp.json");
90
+ }
91
+ return "~/.cursor/mcp.json";
92
+ }
93
+ var AGENTS = [
94
+ {
95
+ id: "claude-code",
96
+ name: "Claude Code",
97
+ configPath: "~/.claude.json",
98
+ configKey: "mcpServers",
99
+ detectDir: "~/.claude",
100
+ projectConfigPath: ".mcp.json"
101
+ },
102
+ {
103
+ id: "claude-desktop",
104
+ name: "Claude Desktop",
105
+ configPath: getClaudeDesktopConfigPath(),
106
+ configKey: "mcpServers",
107
+ detectDir: getClaudeDesktopDetectDir()
108
+ },
109
+ {
110
+ id: "cursor",
111
+ name: "Cursor",
112
+ configPath: getCursorConfigPath(),
113
+ configKey: "mcpServers",
114
+ detectDir: "~/.cursor",
115
+ projectConfigPath: ".cursor/mcp.json"
116
+ },
117
+ {
118
+ id: "windsurf",
119
+ name: "Windsurf",
120
+ configPath: getWindsurfConfigPath(),
121
+ configKey: "mcpServers",
122
+ detectDir: getWindsurfDetectDir()
123
+ }
124
+ ];
125
+ function getAllAgents() {
126
+ return AGENTS;
127
+ }
128
+ function supportsProjectScope(agent) {
129
+ return agent.projectConfigPath !== void 0;
130
+ }
131
+ function getAgentConfigPath(agent, scope = "global") {
132
+ if (scope === "project" && agent.projectConfigPath) {
133
+ return path2.join(process.cwd(), agent.projectConfigPath);
134
+ }
135
+ return expandPath(agent.configPath);
136
+ }
137
+ function readAgentConfig(agent, scope = "global") {
138
+ const configPath = getAgentConfigPath(agent, scope);
139
+ if (!fs2.existsSync(configPath)) {
140
+ return {};
141
+ }
142
+ try {
143
+ const content = fs2.readFileSync(configPath, "utf-8");
144
+ return JSON.parse(content);
145
+ } catch {
146
+ return {};
147
+ }
148
+ }
149
+ function writeAgentConfig(agent, config, scope = "global") {
150
+ const configPath = getAgentConfigPath(agent, scope);
151
+ const configDir = path2.dirname(configPath);
152
+ if (!fs2.existsSync(configDir)) {
153
+ fs2.mkdirSync(configDir, { recursive: true });
154
+ }
155
+ fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
156
+ }
157
+ function getMcpServerConfig(apiKey) {
158
+ return {
159
+ command: "npx",
160
+ args: ["-y", "@picahq/mcp"],
161
+ env: {
162
+ PICA_SECRET: apiKey
163
+ }
164
+ };
165
+ }
166
+ function installMcpConfig(agent, apiKey, scope = "global") {
167
+ const config = readAgentConfig(agent, scope);
168
+ const configKey = agent.configKey;
169
+ const mcpServers = config[configKey] || {};
170
+ mcpServers["pica"] = getMcpServerConfig(apiKey);
171
+ config[configKey] = mcpServers;
172
+ writeAgentConfig(agent, config, scope);
173
+ }
174
+ function isMcpInstalled(agent, scope = "global") {
175
+ const config = readAgentConfig(agent, scope);
176
+ const configKey = agent.configKey;
177
+ const mcpServers = config[configKey];
178
+ return mcpServers?.["pica"] !== void 0;
179
+ }
180
+ function getAgentStatuses() {
181
+ return AGENTS.map((agent) => {
182
+ const detected = fs2.existsSync(expandPath(agent.detectDir));
183
+ const globalMcp = detected && isMcpInstalled(agent, "global");
184
+ const projectMcp = agent.projectConfigPath ? isMcpInstalled(agent, "project") : null;
185
+ return { agent, detected, globalMcp, projectMcp };
186
+ });
187
+ }
188
+
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
+ // src/lib/api.ts
224
+ var API_BASE = "https://api.picaos.com/v1";
225
+ var ApiError = class extends Error {
226
+ constructor(status, message) {
227
+ super(message);
228
+ this.status = status;
229
+ this.name = "ApiError";
230
+ }
231
+ };
232
+ var PicaApi = class {
233
+ constructor(apiKey) {
234
+ this.apiKey = apiKey;
235
+ }
236
+ async request(path3) {
237
+ return this.requestFull({ path: path3 });
238
+ }
239
+ async requestFull(opts) {
240
+ let url = `${API_BASE}${opts.path}`;
241
+ if (opts.queryParams && Object.keys(opts.queryParams).length > 0) {
242
+ const params = new URLSearchParams(opts.queryParams);
243
+ url += `?${params.toString()}`;
244
+ }
245
+ const headers = {
246
+ "x-pica-secret": this.apiKey,
247
+ "Content-Type": "application/json",
248
+ ...opts.headers
249
+ };
250
+ const fetchOpts = {
251
+ method: opts.method || "GET",
252
+ headers
253
+ };
254
+ if (opts.body !== void 0) {
255
+ fetchOpts.body = JSON.stringify(opts.body);
256
+ }
257
+ const response = await fetch(url, fetchOpts);
258
+ if (!response.ok) {
259
+ const text4 = await response.text();
260
+ throw new ApiError(response.status, text4 || `HTTP ${response.status}`);
261
+ }
262
+ return response.json();
263
+ }
264
+ async validateApiKey() {
265
+ try {
266
+ await this.listConnections();
267
+ return true;
268
+ } catch (error) {
269
+ if (error instanceof ApiError && error.status === 401) {
270
+ return false;
271
+ }
272
+ throw error;
273
+ }
274
+ }
275
+ async listConnections() {
276
+ const response = await this.request("/vault/connections");
277
+ return response.rows || [];
278
+ }
279
+ async listPlatforms() {
280
+ const allPlatforms = [];
281
+ let page = 1;
282
+ let totalPages = 1;
283
+ do {
284
+ const response = await this.request(`/available-connectors?page=${page}&limit=100`);
285
+ allPlatforms.push(...response.rows || []);
286
+ totalPages = response.pages || 1;
287
+ page++;
288
+ } while (page <= totalPages);
289
+ return allPlatforms;
290
+ }
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
+ async waitForConnection(platform, timeoutMs = 5 * 60 * 1e3, pollIntervalMs = 5e3, onPoll) {
331
+ const startTime = Date.now();
332
+ const existingConnections = await this.listConnections();
333
+ const existingIds = new Set(existingConnections.map((c) => c.id));
334
+ while (Date.now() - startTime < timeoutMs) {
335
+ await sleep(pollIntervalMs);
336
+ onPoll?.();
337
+ const currentConnections = await this.listConnections();
338
+ const newConnection = currentConnections.find(
339
+ (c) => c.platform.toLowerCase() === platform.toLowerCase() && !existingIds.has(c.id)
340
+ );
341
+ if (newConnection) {
342
+ return newConnection;
343
+ }
344
+ }
345
+ throw new TimeoutError(`Timed out waiting for ${platform} connection`);
346
+ }
347
+ };
348
+ var TimeoutError = class extends Error {
349
+ constructor(message) {
350
+ super(message);
351
+ this.name = "TimeoutError";
352
+ }
353
+ };
354
+ function sleep(ms) {
355
+ return new Promise((resolve) => setTimeout(resolve, ms));
356
+ }
357
+
358
+ // src/lib/browser.ts
359
+ import open from "open";
360
+ var PICA_APP_URL = "https://app.picaos.com";
361
+ function getConnectionUrl(platform) {
362
+ return `${PICA_APP_URL}/connections?#open=${platform}`;
363
+ }
364
+ function getApiKeyUrl() {
365
+ return `${PICA_APP_URL}/settings/api-keys`;
366
+ }
367
+ async function openConnectionPage(platform) {
368
+ const url = getConnectionUrl(platform);
369
+ await open(url);
370
+ }
371
+ async function openApiKeyPage() {
372
+ await open(getApiKeyUrl());
373
+ }
374
+
375
+ // src/lib/table.ts
376
+ import pc from "picocolors";
377
+ function printTable(columns, rows) {
378
+ if (rows.length === 0) return;
379
+ const gap = " ";
380
+ const indent = " ";
381
+ const widths = columns.map((col) => {
382
+ const headerLen = col.label.length;
383
+ const maxValueLen = Math.max(...rows.map((row) => stripAnsi(row[col.key] || "").length));
384
+ return Math.max(headerLen, maxValueLen);
385
+ });
386
+ const header = columns.map((col, i) => {
387
+ const padded = col.align === "right" ? col.label.padStart(widths[i]) : col.label.padEnd(widths[i]);
388
+ return pc.dim(padded);
389
+ }).join(gap);
390
+ console.log(`${indent}${header}`);
391
+ const separator = columns.map((_, i) => pc.dim("\u2500".repeat(widths[i]))).join(gap);
392
+ console.log(`${indent}${separator}`);
393
+ for (const row of rows) {
394
+ const line = columns.map((col, i) => {
395
+ const raw = row[col.key] || "";
396
+ const rawLen = stripAnsi(raw).length;
397
+ const padding = widths[i] - rawLen;
398
+ const colored = col.color ? col.color(raw) : raw;
399
+ if (col.align === "right") {
400
+ return " ".repeat(Math.max(0, padding)) + colored;
401
+ }
402
+ return colored + " ".repeat(Math.max(0, padding));
403
+ }).join(gap);
404
+ console.log(`${indent}${line}`);
405
+ }
406
+ }
407
+ function stripAnsi(str) {
408
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
409
+ }
410
+
411
+ // src/commands/init.ts
412
+ async function initCommand(options) {
413
+ p.intro(pc2.bgCyan(pc2.black(" Pica ")));
414
+ const existingConfig = readConfig();
415
+ if (existingConfig) {
416
+ await handleExistingConfig(existingConfig.apiKey, options);
417
+ return;
418
+ }
419
+ await freshSetup(options);
420
+ }
421
+ async function handleExistingConfig(apiKey, options) {
422
+ const statuses = getAgentStatuses();
423
+ const masked = maskApiKey(apiKey);
424
+ console.log();
425
+ console.log(` ${pc2.bold("Current Setup")}`);
426
+ console.log(` ${pc2.dim("\u2500".repeat(42))}`);
427
+ console.log(` ${pc2.dim("API Key:")} ${masked}`);
428
+ console.log(` ${pc2.dim("Config:")} ${getConfigPath()}`);
429
+ console.log();
430
+ printTable(
431
+ [
432
+ { key: "agent", label: "Agent" },
433
+ { key: "global", label: "Global" },
434
+ { key: "project", label: "Project" }
435
+ ],
436
+ statuses.map((s) => ({
437
+ agent: s.agent.name,
438
+ global: !s.detected ? pc2.dim("-") : s.globalMcp ? pc2.green("\u25CF yes") : pc2.yellow("\u25CB no"),
439
+ project: s.projectMcp === null ? pc2.dim("-") : s.projectMcp ? pc2.green("\u25CF yes") : pc2.yellow("\u25CB no")
440
+ }))
441
+ );
442
+ const notDetected = statuses.filter((s) => !s.detected);
443
+ if (notDetected.length > 0) {
444
+ console.log(` ${pc2.dim("- = not detected on this machine")}`);
445
+ }
446
+ console.log();
447
+ const actionOptions = [];
448
+ actionOptions.push({
449
+ value: "update-key",
450
+ label: "Update API key"
451
+ });
452
+ const agentsMissingGlobal = statuses.filter((s) => s.detected && !s.globalMcp);
453
+ if (agentsMissingGlobal.length > 0) {
454
+ actionOptions.push({
455
+ value: "install-more",
456
+ label: "Install MCP to more agents",
457
+ hint: agentsMissingGlobal.map((s) => s.agent.name).join(", ")
458
+ });
459
+ }
460
+ const agentsMissingProject = statuses.filter((s) => s.projectMcp === false);
461
+ if (agentsMissingProject.length > 0) {
462
+ actionOptions.push({
463
+ value: "install-project",
464
+ label: "Install MCP for this project",
465
+ hint: agentsMissingProject.map((s) => s.agent.name).join(", ")
466
+ });
467
+ }
468
+ actionOptions.push({
469
+ value: "start-fresh",
470
+ label: "Start fresh (reconfigure everything)"
471
+ });
472
+ const action = await p.select({
473
+ message: "What would you like to do?",
474
+ options: actionOptions
475
+ });
476
+ if (p.isCancel(action)) {
477
+ p.outro("No changes made.");
478
+ return;
479
+ }
480
+ switch (action) {
481
+ case "update-key":
482
+ await handleUpdateKey(statuses);
483
+ break;
484
+ case "install-more":
485
+ await handleInstallMore(apiKey, agentsMissingGlobal);
486
+ break;
487
+ case "install-project":
488
+ await handleInstallProject(apiKey, agentsMissingProject);
489
+ break;
490
+ case "start-fresh":
491
+ await freshSetup({ yes: true });
492
+ break;
493
+ }
494
+ }
495
+ async function handleUpdateKey(statuses) {
496
+ p.note(`Get your API key at:
497
+ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
498
+ const openBrowser = await p.confirm({
499
+ message: "Open browser to get API key?",
500
+ initialValue: true
501
+ });
502
+ if (p.isCancel(openBrowser)) {
503
+ p.cancel("Cancelled.");
504
+ process.exit(0);
505
+ }
506
+ if (openBrowser) {
507
+ await openApiKeyPage();
508
+ }
509
+ const newKey = await p.text({
510
+ message: "Enter your new Pica API key:",
511
+ placeholder: "sk_live_...",
512
+ validate: (value) => {
513
+ if (!value) return "API key is required";
514
+ if (!value.startsWith("sk_live_") && !value.startsWith("sk_test_")) {
515
+ return "API key should start with sk_live_ or sk_test_";
516
+ }
517
+ return void 0;
518
+ }
519
+ });
520
+ if (p.isCancel(newKey)) {
521
+ p.cancel("Cancelled.");
522
+ process.exit(0);
523
+ }
524
+ const spinner5 = p.spinner();
525
+ spinner5.start("Validating API key...");
526
+ const api = new PicaApi(newKey);
527
+ const isValid = await api.validateApiKey();
528
+ if (!isValid) {
529
+ spinner5.stop("Invalid API key");
530
+ p.cancel(`Invalid API key. Get a valid key at ${getApiKeyUrl()}`);
531
+ process.exit(1);
532
+ }
533
+ spinner5.stop("API key validated");
534
+ const reinstalled = [];
535
+ for (const s of statuses) {
536
+ if (s.globalMcp) {
537
+ installMcpConfig(s.agent, newKey, "global");
538
+ reinstalled.push(`${s.agent.name} (global)`);
539
+ }
540
+ if (s.projectMcp) {
541
+ installMcpConfig(s.agent, newKey, "project");
542
+ reinstalled.push(`${s.agent.name} (project)`);
543
+ }
544
+ }
545
+ const config = readConfig();
546
+ writeConfig({
547
+ apiKey: newKey,
548
+ installedAgents: config?.installedAgents ?? [],
549
+ createdAt: config?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
550
+ });
551
+ if (reinstalled.length > 0) {
552
+ p.log.success(`Updated MCP configs: ${reinstalled.join(", ")}`);
553
+ }
554
+ p.outro("API key updated.");
555
+ }
556
+ async function handleInstallMore(apiKey, missing) {
557
+ if (missing.length === 1) {
558
+ const agent = missing[0].agent;
559
+ const confirm2 = await p.confirm({
560
+ message: `Install Pica MCP to ${agent.name}?`,
561
+ initialValue: true
562
+ });
563
+ if (p.isCancel(confirm2) || !confirm2) {
564
+ p.outro("No changes made.");
565
+ return;
566
+ }
567
+ installMcpConfig(agent, apiKey, "global");
568
+ updateConfigAgents(agent.id);
569
+ p.log.success(`${agent.name}: MCP installed`);
570
+ p.outro("Done.");
571
+ return;
572
+ }
573
+ const selected = await p.multiselect({
574
+ message: "Select agents to install MCP:",
575
+ options: missing.map((s) => ({
576
+ value: s.agent.id,
577
+ label: s.agent.name
578
+ }))
579
+ });
580
+ if (p.isCancel(selected)) {
581
+ p.outro("No changes made.");
582
+ return;
583
+ }
584
+ const agents = missing.filter((s) => selected.includes(s.agent.id));
585
+ for (const s of agents) {
586
+ installMcpConfig(s.agent, apiKey, "global");
587
+ updateConfigAgents(s.agent.id);
588
+ p.log.success(`${s.agent.name}: MCP installed`);
589
+ }
590
+ p.outro("Done.");
591
+ }
592
+ async function handleInstallProject(apiKey, missing) {
593
+ if (missing.length === 1) {
594
+ const agent = missing[0].agent;
595
+ const confirm2 = await p.confirm({
596
+ message: `Install project-level MCP for ${agent.name}?`,
597
+ initialValue: true
598
+ });
599
+ if (p.isCancel(confirm2) || !confirm2) {
600
+ p.outro("No changes made.");
601
+ return;
602
+ }
603
+ installMcpConfig(agent, apiKey, "project");
604
+ const configPath = getAgentConfigPath(agent, "project");
605
+ p.log.success(`${agent.name}: ${configPath} created`);
606
+ p.note(
607
+ pc2.yellow("Project config files can be committed to share with your team.\n") + pc2.yellow("Team members will need their own API key."),
608
+ "Tip"
609
+ );
610
+ p.outro("Done.");
611
+ return;
612
+ }
613
+ const selected = await p.multiselect({
614
+ message: "Select agents for project-level MCP:",
615
+ options: missing.map((s) => ({
616
+ value: s.agent.id,
617
+ label: s.agent.name
618
+ }))
619
+ });
620
+ if (p.isCancel(selected)) {
621
+ p.outro("No changes made.");
622
+ return;
623
+ }
624
+ const agents = missing.filter((s) => selected.includes(s.agent.id));
625
+ for (const s of agents) {
626
+ installMcpConfig(s.agent, apiKey, "project");
627
+ const configPath = getAgentConfigPath(s.agent, "project");
628
+ p.log.success(`${s.agent.name}: ${configPath} created`);
629
+ }
630
+ p.note(
631
+ pc2.yellow("Project config files can be committed to share with your team.\n") + pc2.yellow("Team members will need their own API key."),
632
+ "Tip"
633
+ );
634
+ p.outro("Done.");
635
+ }
636
+ async function freshSetup(options) {
637
+ p.note(`Get your API key at:
638
+ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
639
+ const openBrowser = await p.confirm({
640
+ message: "Open browser to get API key?",
641
+ initialValue: true
642
+ });
643
+ if (p.isCancel(openBrowser)) {
644
+ p.cancel("Setup cancelled.");
645
+ process.exit(0);
646
+ }
647
+ if (openBrowser) {
648
+ await openApiKeyPage();
649
+ }
650
+ const apiKey = await p.text({
651
+ message: "Enter your Pica API key:",
652
+ placeholder: "sk_live_...",
653
+ validate: (value) => {
654
+ if (!value) return "API key is required";
655
+ if (!value.startsWith("sk_live_") && !value.startsWith("sk_test_")) {
656
+ return "API key should start with sk_live_ or sk_test_";
657
+ }
658
+ return void 0;
659
+ }
660
+ });
661
+ if (p.isCancel(apiKey)) {
662
+ p.cancel("Setup cancelled.");
663
+ process.exit(0);
664
+ }
665
+ const spinner5 = p.spinner();
666
+ spinner5.start("Validating API key...");
667
+ const api = new PicaApi(apiKey);
668
+ const isValid = await api.validateApiKey();
669
+ if (!isValid) {
670
+ spinner5.stop("Invalid API key");
671
+ p.cancel(`Invalid API key. Get a valid key at ${getApiKeyUrl()}`);
672
+ process.exit(1);
673
+ }
674
+ spinner5.stop("API key validated");
675
+ writeConfig({
676
+ apiKey,
677
+ installedAgents: [],
678
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
679
+ });
680
+ const allAgents = getAllAgents();
681
+ const agentChoice = await p.select({
682
+ message: "Where do you want to install the MCP?",
683
+ options: [
684
+ {
685
+ value: "all",
686
+ label: "All agents",
687
+ hint: "Claude Code, Claude Desktop, Cursor, Windsurf"
688
+ },
689
+ ...allAgents.map((agent) => ({
690
+ value: agent.id,
691
+ label: agent.name
692
+ }))
693
+ ]
694
+ });
695
+ if (p.isCancel(agentChoice)) {
696
+ p.cancel("Setup cancelled.");
697
+ process.exit(0);
698
+ }
699
+ const selectedAgents = agentChoice === "all" ? allAgents : allAgents.filter((a) => a.id === agentChoice);
700
+ let scope = "global";
701
+ const hasProjectScopeAgent = selectedAgents.some((a) => supportsProjectScope(a));
702
+ if (options.global) {
703
+ scope = "global";
704
+ } else if (options.project) {
705
+ scope = "project";
706
+ } else if (hasProjectScopeAgent) {
707
+ const scopeChoice = await p.select({
708
+ message: "How do you want to install it?",
709
+ options: [
710
+ {
711
+ value: "global",
712
+ label: "Global (Recommended)",
713
+ hint: "Available in all your projects"
714
+ },
715
+ {
716
+ value: "project",
717
+ label: "Project only",
718
+ hint: "Creates config files in current directory"
719
+ }
720
+ ]
721
+ });
722
+ if (p.isCancel(scopeChoice)) {
723
+ p.cancel("Setup cancelled.");
724
+ process.exit(0);
725
+ }
726
+ scope = scopeChoice;
727
+ }
728
+ if (scope === "project") {
729
+ const projectAgents = selectedAgents.filter((a) => supportsProjectScope(a));
730
+ const nonProjectAgents = selectedAgents.filter((a) => !supportsProjectScope(a));
731
+ if (projectAgents.length === 0) {
732
+ const supported = allAgents.filter((a) => supportsProjectScope(a)).map((a) => a.name).join(", ");
733
+ p.note(
734
+ `${selectedAgents.map((a) => a.name).join(", ")} does not support project-level MCP.
735
+ Project scope is supported by: ${supported}`,
736
+ "Not Supported"
737
+ );
738
+ p.cancel("Run again and choose global scope or a different agent.");
739
+ process.exit(1);
740
+ }
741
+ for (const agent of projectAgents) {
742
+ const wasInstalled = isMcpInstalled(agent, "project");
743
+ installMcpConfig(agent, apiKey, "project");
744
+ const configPath = getAgentConfigPath(agent, "project");
745
+ const status = wasInstalled ? "updated" : "created";
746
+ p.log.success(`${agent.name}: ${configPath} ${status}`);
747
+ }
748
+ if (nonProjectAgents.length > 0) {
749
+ p.log.info(`Installing globally for agents without project scope support:`);
750
+ for (const agent of nonProjectAgents) {
751
+ const wasInstalled = isMcpInstalled(agent, "global");
752
+ installMcpConfig(agent, apiKey, "global");
753
+ const status = wasInstalled ? "updated" : "installed";
754
+ p.log.success(`${agent.name}: MCP ${status} (global)`);
755
+ }
756
+ }
757
+ const allInstalled = [...projectAgents, ...nonProjectAgents];
758
+ writeConfig({
759
+ apiKey,
760
+ installedAgents: allInstalled.map((a) => a.id),
761
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
762
+ });
763
+ const configPaths = projectAgents.map((a) => ` ${a.name}: ${pc2.dim(getAgentConfigPath(a, "project"))}`).join("\n");
764
+ let summary = `Config saved to: ${pc2.dim(getConfigPath())}
765
+ MCP configs:
766
+ ${configPaths}
767
+
768
+ `;
769
+ if (nonProjectAgents.length > 0) {
770
+ const globalPaths = nonProjectAgents.map((a) => ` ${a.name}: ${pc2.dim(getAgentConfigPath(a, "global"))}`).join("\n");
771
+ summary += `Global configs:
772
+ ${globalPaths}
773
+
774
+ `;
775
+ }
776
+ summary += pc2.yellow("Note: Project config files can be committed to share with your team.\n") + pc2.yellow("Team members will need their own API key.\n\n") + `Next steps:
777
+ ${pc2.cyan("pica add gmail")} - Connect Gmail
778
+ ${pc2.cyan("pica platforms")} - See all 200+ integrations`;
779
+ p.note(summary, "Setup Complete");
780
+ p.outro("Pica MCP installed!");
781
+ return;
782
+ }
783
+ const installedAgentIds = [];
784
+ for (const agent of selectedAgents) {
785
+ const wasInstalled = isMcpInstalled(agent, "global");
786
+ installMcpConfig(agent, apiKey, "global");
787
+ installedAgentIds.push(agent.id);
788
+ const status = wasInstalled ? "updated" : "installed";
789
+ p.log.success(`${agent.name}: MCP ${status}`);
790
+ }
791
+ writeConfig({
792
+ apiKey,
793
+ installedAgents: installedAgentIds,
794
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
795
+ });
796
+ p.note(
797
+ `Config saved to: ${pc2.dim(getConfigPath())}
798
+
799
+ Next steps:
800
+ ${pc2.cyan("pica add gmail")} - Connect Gmail
801
+ ${pc2.cyan("pica platforms")} - See all 200+ integrations
802
+ ${pc2.cyan("pica connection list")} - View your connections`,
803
+ "Setup Complete"
804
+ );
805
+ p.outro("Your AI agents now have access to Pica integrations!");
806
+ }
807
+ function maskApiKey(key) {
808
+ if (key.length <= 12) return key.slice(0, 8) + "...";
809
+ return key.slice(0, 8) + "..." + key.slice(-4);
810
+ }
811
+ function updateConfigAgents(agentId) {
812
+ const config = readConfig();
813
+ if (!config) return;
814
+ if (!config.installedAgents.includes(agentId)) {
815
+ config.installedAgents.push(agentId);
816
+ writeConfig(config);
817
+ }
818
+ }
819
+
820
+ // src/commands/connection.ts
821
+ import * as p2 from "@clack/prompts";
822
+ import pc3 from "picocolors";
823
+
824
+ // src/lib/platforms.ts
825
+ function findPlatform(platforms, query) {
826
+ const normalizedQuery = query.toLowerCase().trim();
827
+ const exact = platforms.find(
828
+ (p5) => p5.platform.toLowerCase() === normalizedQuery || p5.name.toLowerCase() === normalizedQuery
829
+ );
830
+ if (exact) return exact;
831
+ return null;
832
+ }
833
+ function findSimilarPlatforms(platforms, query, limit = 3) {
834
+ const normalizedQuery = query.toLowerCase().trim();
835
+ const scored = platforms.map((p5) => {
836
+ const name = p5.name.toLowerCase();
837
+ const slug = p5.platform.toLowerCase();
838
+ let score = 0;
839
+ if (name.includes(normalizedQuery) || slug.includes(normalizedQuery)) {
840
+ score = 10;
841
+ } else if (normalizedQuery.includes(name) || normalizedQuery.includes(slug)) {
842
+ score = 5;
843
+ } else {
844
+ score = countMatchingChars(normalizedQuery, slug);
845
+ }
846
+ return { platform: p5, score };
847
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
848
+ return scored.map((item) => item.platform);
849
+ }
850
+ function countMatchingChars(a, b) {
851
+ let count = 0;
852
+ const bChars = new Set(b.split(""));
853
+ for (const char of a) {
854
+ if (bChars.has(char)) count++;
855
+ }
856
+ return count;
857
+ }
858
+
859
+ // src/commands/connection.ts
860
+ async function connectionAddCommand(platformArg) {
861
+ p2.intro(pc3.bgCyan(pc3.black(" Pica ")));
862
+ const apiKey = getApiKey();
863
+ if (!apiKey) {
864
+ p2.cancel("Not configured. Run `pica init` first.");
865
+ process.exit(1);
866
+ }
867
+ const api = new PicaApi(apiKey);
868
+ const spinner5 = p2.spinner();
869
+ spinner5.start("Loading platforms...");
870
+ let platforms;
871
+ try {
872
+ platforms = await api.listPlatforms();
873
+ spinner5.stop(`${platforms.length} platforms available`);
874
+ } catch (error) {
875
+ spinner5.stop("Failed to load platforms");
876
+ p2.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
877
+ process.exit(1);
878
+ }
879
+ let platform;
880
+ if (platformArg) {
881
+ const found = findPlatform(platforms, platformArg);
882
+ if (found) {
883
+ platform = found.platform;
884
+ } else {
885
+ const similar = findSimilarPlatforms(platforms, platformArg);
886
+ if (similar.length > 0) {
887
+ p2.log.warn(`Unknown platform: ${platformArg}`);
888
+ const suggestion = await p2.select({
889
+ message: "Did you mean:",
890
+ options: [
891
+ ...similar.map((s) => ({ value: s.platform, label: `${s.name} (${s.platform})` })),
892
+ { value: "__other__", label: "None of these" }
893
+ ]
894
+ });
895
+ if (p2.isCancel(suggestion) || suggestion === "__other__") {
896
+ p2.note(`Run ${pc3.cyan("pica platforms")} to see all available platforms.`);
897
+ p2.cancel("Connection cancelled.");
898
+ process.exit(0);
899
+ }
900
+ platform = suggestion;
901
+ } else {
902
+ p2.cancel(`Unknown platform: ${platformArg}
903
+
904
+ Run ${pc3.cyan("pica platforms")} to see available platforms.`);
905
+ process.exit(1);
906
+ }
907
+ }
908
+ } else {
909
+ const platformInput = await p2.text({
910
+ message: "Which platform do you want to connect?",
911
+ placeholder: "gmail, slack, hubspot...",
912
+ validate: (value) => {
913
+ if (!value.trim()) return "Platform name is required";
914
+ return void 0;
915
+ }
916
+ });
917
+ if (p2.isCancel(platformInput)) {
918
+ p2.cancel("Connection cancelled.");
919
+ process.exit(0);
920
+ }
921
+ const found = findPlatform(platforms, platformInput);
922
+ if (found) {
923
+ platform = found.platform;
924
+ } else {
925
+ p2.cancel(`Unknown platform: ${platformInput}
926
+
927
+ Run ${pc3.cyan("pica platforms")} to see available platforms.`);
928
+ process.exit(1);
929
+ }
930
+ }
931
+ const url = getConnectionUrl(platform);
932
+ p2.log.info(`Opening browser to connect ${pc3.cyan(platform)}...`);
933
+ p2.note(pc3.dim(url), "URL");
934
+ try {
935
+ await openConnectionPage(platform);
936
+ } catch {
937
+ p2.log.warn("Could not open browser automatically.");
938
+ p2.note(`Open this URL manually:
939
+ ${url}`);
940
+ }
941
+ const pollSpinner = p2.spinner();
942
+ pollSpinner.start("Waiting for connection... (complete auth in browser)");
943
+ try {
944
+ const connection2 = await api.waitForConnection(platform, 5 * 60 * 1e3, 5e3);
945
+ pollSpinner.stop(`${platform} connected!`);
946
+ p2.log.success(`${pc3.green("\u2713")} ${connection2.platform} is now available to your AI agents.`);
947
+ p2.outro("Connection complete!");
948
+ } catch (error) {
949
+ pollSpinner.stop("Connection timed out");
950
+ if (error instanceof TimeoutError) {
951
+ p2.note(
952
+ `Possible issues:
953
+ - OAuth flow was not completed in the browser
954
+ - Browser popup was blocked
955
+ - Wrong account selected
956
+
957
+ Try again with: ${pc3.cyan(`pica connection add ${platform}`)}`,
958
+ "Timed Out"
959
+ );
960
+ } else {
961
+ p2.log.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
962
+ }
963
+ process.exit(1);
964
+ }
965
+ }
966
+ async function connectionListCommand() {
967
+ const apiKey = getApiKey();
968
+ if (!apiKey) {
969
+ p2.cancel("Not configured. Run `pica init` first.");
970
+ process.exit(1);
971
+ }
972
+ const api = new PicaApi(apiKey);
973
+ const spinner5 = p2.spinner();
974
+ spinner5.start("Loading connections...");
975
+ try {
976
+ const connections = await api.listConnections();
977
+ spinner5.stop(`${connections.length} connection${connections.length === 1 ? "" : "s"} found`);
978
+ if (connections.length === 0) {
979
+ p2.note(
980
+ `No connections yet.
981
+
982
+ Add one with: ${pc3.cyan("pica connection add gmail")}`,
983
+ "No Connections"
984
+ );
985
+ return;
986
+ }
987
+ console.log();
988
+ const rows = connections.map((conn) => ({
989
+ status: getStatusIndicator(conn.state),
990
+ platform: conn.platform,
991
+ state: conn.state,
992
+ key: conn.key
993
+ }));
994
+ printTable(
995
+ [
996
+ { key: "status", label: "" },
997
+ { key: "platform", label: "Platform" },
998
+ { key: "state", label: "Status" },
999
+ { key: "key", label: "Connection Key", color: pc3.dim }
1000
+ ],
1001
+ rows
1002
+ );
1003
+ console.log();
1004
+ p2.note(`Add more with: ${pc3.cyan("pica connection add <platform>")}`, "Tip");
1005
+ } catch (error) {
1006
+ spinner5.stop("Failed to load connections");
1007
+ p2.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1008
+ process.exit(1);
1009
+ }
1010
+ }
1011
+ function getStatusIndicator(state) {
1012
+ switch (state) {
1013
+ case "operational":
1014
+ return pc3.green("\u25CF");
1015
+ case "degraded":
1016
+ return pc3.yellow("\u25CF");
1017
+ case "failed":
1018
+ return pc3.red("\u25CF");
1019
+ default:
1020
+ return pc3.dim("\u25CB");
1021
+ }
1022
+ }
1023
+
1024
+ // src/commands/platforms.ts
1025
+ import * as p3 from "@clack/prompts";
1026
+ import pc4 from "picocolors";
1027
+ async function platformsCommand(options) {
1028
+ const apiKey = getApiKey();
1029
+ if (!apiKey) {
1030
+ p3.cancel("Not configured. Run `pica init` first.");
1031
+ process.exit(1);
1032
+ }
1033
+ const api = new PicaApi(apiKey);
1034
+ const spinner5 = p3.spinner();
1035
+ spinner5.start("Loading platforms...");
1036
+ try {
1037
+ const platforms = await api.listPlatforms();
1038
+ spinner5.stop(`${platforms.length} platforms available`);
1039
+ if (options.json) {
1040
+ console.log(JSON.stringify(platforms, null, 2));
1041
+ return;
1042
+ }
1043
+ const byCategory = /* @__PURE__ */ new Map();
1044
+ for (const plat of platforms) {
1045
+ const category = plat.category || "Other";
1046
+ if (!byCategory.has(category)) {
1047
+ byCategory.set(category, []);
1048
+ }
1049
+ byCategory.get(category).push(plat);
1050
+ }
1051
+ console.log();
1052
+ if (options.category) {
1053
+ const categoryPlatforms = byCategory.get(options.category);
1054
+ if (!categoryPlatforms) {
1055
+ const categories = [...byCategory.keys()].sort();
1056
+ p3.note(`Available categories:
1057
+ ${categories.join(", ")}`, "Unknown Category");
1058
+ process.exit(1);
1059
+ }
1060
+ const rows = categoryPlatforms.sort((a, b) => a.platform.localeCompare(b.platform)).map((plat) => ({
1061
+ platform: plat.platform,
1062
+ name: plat.name,
1063
+ category: plat.category || "Other"
1064
+ }));
1065
+ printTable(
1066
+ [
1067
+ { key: "platform", label: "Platform" },
1068
+ { key: "name", label: "Name" }
1069
+ ],
1070
+ rows
1071
+ );
1072
+ } else {
1073
+ const rows = platforms.sort((a, b) => a.category.localeCompare(b.category) || a.platform.localeCompare(b.platform)).map((plat) => ({
1074
+ platform: plat.platform,
1075
+ name: plat.name,
1076
+ category: plat.category || "Other"
1077
+ }));
1078
+ printTable(
1079
+ [
1080
+ { key: "category", label: "Category" },
1081
+ { key: "platform", label: "Platform" },
1082
+ { key: "name", label: "Name" }
1083
+ ],
1084
+ rows
1085
+ );
1086
+ }
1087
+ console.log();
1088
+ p3.note(`Connect with: ${pc4.cyan("pica connection add <platform>")}`, "Tip");
1089
+ } catch (error) {
1090
+ spinner5.stop("Failed to load platforms");
1091
+ p3.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1092
+ process.exit(1);
1093
+ }
1094
+ }
1095
+
1096
+ // src/commands/actions.ts
1097
+ import * as p4 from "@clack/prompts";
1098
+ import pc5 from "picocolors";
1099
+ function getApi() {
1100
+ const apiKey = getApiKey();
1101
+ if (!apiKey) {
1102
+ p4.cancel("Not configured. Run `pica init` first.");
1103
+ process.exit(1);
1104
+ }
1105
+ return new PicaApi(apiKey);
1106
+ }
1107
+ function colorMethod(method) {
1108
+ const m = method.toUpperCase();
1109
+ switch (m) {
1110
+ case "GET":
1111
+ return pc5.green(m);
1112
+ case "POST":
1113
+ return pc5.yellow(m);
1114
+ case "PUT":
1115
+ return pc5.blue(m);
1116
+ case "PATCH":
1117
+ return pc5.cyan(m);
1118
+ case "DELETE":
1119
+ return pc5.red(m);
1120
+ default:
1121
+ return pc5.dim(m);
1122
+ }
1123
+ }
1124
+ function padMethod(method) {
1125
+ return method.toUpperCase().padEnd(7);
1126
+ }
1127
+ async function actionsSearchCommand(platform, query, options = {}) {
1128
+ const api = getApi();
1129
+ if (!query) {
1130
+ const input = await p4.text({
1131
+ message: `Search actions on ${pc5.cyan(platform)}:`,
1132
+ placeholder: "send email, create contact, list orders..."
1133
+ });
1134
+ if (p4.isCancel(input)) {
1135
+ p4.cancel("Cancelled.");
1136
+ process.exit(0);
1137
+ }
1138
+ query = input;
1139
+ }
1140
+ const spinner5 = p4.spinner();
1141
+ spinner5.start(`Searching ${platform} actions...`);
1142
+ try {
1143
+ const limit = options.limit ? parseInt(options.limit, 10) : 10;
1144
+ const actions2 = await api.searchActions(platform, query, limit);
1145
+ spinner5.stop(`${actions2.length} action${actions2.length === 1 ? "" : "s"} found`);
1146
+ if (options.json) {
1147
+ console.log(JSON.stringify(actions2, null, 2));
1148
+ return;
1149
+ }
1150
+ if (actions2.length === 0) {
1151
+ p4.note(`No actions found for "${query}" on ${platform}.`, "No Results");
1152
+ return;
1153
+ }
1154
+ console.log();
1155
+ const rows = actions2.map((action) => ({
1156
+ method: colorMethod(padMethod(action.method)),
1157
+ path: action.path,
1158
+ title: action.title,
1159
+ id: action._id
1160
+ }));
1161
+ printTable(
1162
+ [
1163
+ { key: "method", label: "Method" },
1164
+ { key: "title", label: "Title" },
1165
+ { key: "path", label: "Path", color: pc5.dim },
1166
+ { key: "id", label: "Action ID", color: pc5.dim }
1167
+ ],
1168
+ rows
1169
+ );
1170
+ console.log();
1171
+ p4.note(
1172
+ `Get docs: ${pc5.cyan("pica actions knowledge <actionId>")}
1173
+ Execute: ${pc5.cyan("pica actions execute <actionId>")}`,
1174
+ "Next Steps"
1175
+ );
1176
+ } catch (error) {
1177
+ spinner5.stop("Search failed");
1178
+ p4.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1179
+ process.exit(1);
1180
+ }
1181
+ }
1182
+ async function actionsKnowledgeCommand(actionId, options = {}) {
1183
+ const api = getApi();
1184
+ const spinner5 = p4.spinner();
1185
+ spinner5.start("Loading action knowledge...");
1186
+ try {
1187
+ const knowledge = await api.getActionKnowledge(actionId);
1188
+ spinner5.stop("Action knowledge loaded");
1189
+ if (!knowledge) {
1190
+ p4.cancel(`No knowledge found for action: ${actionId}`);
1191
+ process.exit(1);
1192
+ }
1193
+ if (options.json) {
1194
+ console.log(JSON.stringify(knowledge, null, 2));
1195
+ return;
1196
+ }
1197
+ printKnowledge(knowledge, options.full);
1198
+ } catch (error) {
1199
+ spinner5.stop("Failed to load knowledge");
1200
+ p4.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1201
+ process.exit(1);
1202
+ }
1203
+ }
1204
+ function printKnowledge(k, full) {
1205
+ console.log();
1206
+ console.log(pc5.bold(` ${k.title}`));
1207
+ console.log();
1208
+ console.log(` Platform: ${pc5.cyan(k.connectionPlatform)}`);
1209
+ console.log(` Method: ${colorMethod(k.method)}`);
1210
+ console.log(` Path: ${k.path}`);
1211
+ console.log(` Base URL: ${pc5.dim(k.baseUrl)}`);
1212
+ if (k.tags?.length) {
1213
+ console.log(` Tags: ${k.tags.map((t) => pc5.dim(t)).join(", ")}`);
1214
+ }
1215
+ const pathVars = extractPathVariables(k.path);
1216
+ if (pathVars.length > 0) {
1217
+ console.log(` Path Vars: ${pathVars.map((v) => pc5.yellow(`{{${v}}}`)).join(", ")}`);
1218
+ }
1219
+ console.log(` Active: ${k.active ? pc5.green("yes") : pc5.red("no")}`);
1220
+ console.log(` ID: ${pc5.dim(k._id)}`);
1221
+ if (k.knowledge) {
1222
+ console.log();
1223
+ console.log(pc5.bold(" API Documentation"));
1224
+ console.log(pc5.dim(" " + "\u2500".repeat(40)));
1225
+ console.log();
1226
+ const lines = k.knowledge.split("\n");
1227
+ const displayLines = full ? lines : lines.slice(0, 50);
1228
+ for (const line of displayLines) {
1229
+ console.log(` ${line}`);
1230
+ }
1231
+ if (!full && lines.length > 50) {
1232
+ console.log();
1233
+ console.log(pc5.dim(` ... ${lines.length - 50} more lines. Use --full to see all.`));
1234
+ }
1235
+ }
1236
+ console.log();
1237
+ }
1238
+ async function actionsExecuteCommand(actionId, options = {}) {
1239
+ const api = getApi();
1240
+ const spinner5 = p4.spinner();
1241
+ spinner5.start("Loading action details...");
1242
+ let knowledge;
1243
+ try {
1244
+ const k = await api.getActionKnowledge(actionId);
1245
+ if (!k) {
1246
+ spinner5.stop("Action not found");
1247
+ p4.cancel(`No action found for: ${actionId}`);
1248
+ process.exit(1);
1249
+ }
1250
+ knowledge = k;
1251
+ spinner5.stop(`${colorMethod(knowledge.method)} ${knowledge.path}`);
1252
+ } catch (error) {
1253
+ spinner5.stop("Failed to load action");
1254
+ p4.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1255
+ process.exit(1);
1256
+ }
1257
+ let connectionKey = options.connection;
1258
+ if (!connectionKey) {
1259
+ connectionKey = await resolveConnection(api, knowledge.connectionPlatform);
1260
+ }
1261
+ const pathVarMap = parseKeyValuePairs(options.pathVar || []);
1262
+ const pathVars = extractPathVariables(knowledge.path);
1263
+ for (const v of pathVars) {
1264
+ if (!pathVarMap[v]) {
1265
+ const input = await p4.text({
1266
+ message: `Value for path variable ${pc5.yellow(`{{${v}}}`)}:`,
1267
+ validate: (val) => val.trim() ? void 0 : "Value is required"
1268
+ });
1269
+ if (p4.isCancel(input)) {
1270
+ p4.cancel("Cancelled.");
1271
+ process.exit(0);
1272
+ }
1273
+ pathVarMap[v] = input;
1274
+ }
1275
+ }
1276
+ let bodyData = {};
1277
+ if (options.data) {
1278
+ try {
1279
+ bodyData = JSON.parse(options.data);
1280
+ } catch {
1281
+ p4.cancel("Invalid JSON in --data flag.");
1282
+ process.exit(1);
1283
+ }
1284
+ } else if (!["GET", "DELETE", "HEAD"].includes(knowledge.method.toUpperCase())) {
1285
+ const input = await p4.text({
1286
+ message: "Request body (JSON):",
1287
+ placeholder: '{"key": "value"} or leave empty'
1288
+ });
1289
+ if (p4.isCancel(input)) {
1290
+ p4.cancel("Cancelled.");
1291
+ process.exit(0);
1292
+ }
1293
+ if (input.trim()) {
1294
+ try {
1295
+ bodyData = JSON.parse(input);
1296
+ } catch {
1297
+ p4.cancel("Invalid JSON.");
1298
+ process.exit(1);
1299
+ }
1300
+ }
1301
+ }
1302
+ const queryParams = parseKeyValuePairs(options.query || []);
1303
+ const { resolvedPath, remainingData } = resolveTemplateVariables(
1304
+ knowledge.path,
1305
+ bodyData,
1306
+ pathVarMap
1307
+ );
1308
+ console.log();
1309
+ console.log(pc5.bold(" Request Summary"));
1310
+ console.log(` ${colorMethod(knowledge.method)} ${knowledge.baseUrl}${resolvedPath}`);
1311
+ console.log(` Connection: ${pc5.dim(connectionKey)}`);
1312
+ if (Object.keys(remainingData).length > 0) {
1313
+ console.log(` Body: ${pc5.dim(JSON.stringify(remainingData))}`);
1314
+ }
1315
+ if (Object.keys(queryParams).length > 0) {
1316
+ console.log(` Query: ${pc5.dim(JSON.stringify(queryParams))}`);
1317
+ }
1318
+ console.log();
1319
+ const execSpinner = p4.spinner();
1320
+ execSpinner.start("Executing...");
1321
+ try {
1322
+ const result = await api.executeAction({
1323
+ method: knowledge.method,
1324
+ path: resolvedPath,
1325
+ actionId,
1326
+ connectionKey,
1327
+ data: Object.keys(remainingData).length > 0 ? remainingData : void 0,
1328
+ queryParams: Object.keys(queryParams).length > 0 ? queryParams : void 0,
1329
+ isFormData: options.formData,
1330
+ isFormUrlEncoded: options.formUrlencoded
1331
+ });
1332
+ execSpinner.stop(pc5.green("Success"));
1333
+ if (options.json) {
1334
+ console.log(JSON.stringify(result, null, 2));
1335
+ } else {
1336
+ console.log();
1337
+ console.log(pc5.bold(" Response"));
1338
+ console.log(pc5.dim(" " + "\u2500".repeat(40)));
1339
+ console.log();
1340
+ console.log(formatResponse(result));
1341
+ console.log();
1342
+ }
1343
+ } catch (error) {
1344
+ execSpinner.stop(pc5.red("Failed"));
1345
+ const msg = error instanceof Error ? error.message : "Unknown error";
1346
+ p4.cancel(`Execution failed: ${msg}`);
1347
+ process.exit(1);
1348
+ }
1349
+ }
1350
+ async function resolveConnection(api, platform) {
1351
+ const spinner5 = p4.spinner();
1352
+ spinner5.start("Loading connections...");
1353
+ const connections = await api.listConnections();
1354
+ const matching = connections.filter(
1355
+ (c) => c.platform.toLowerCase() === platform.toLowerCase()
1356
+ );
1357
+ spinner5.stop(`${matching.length} ${platform} connection${matching.length === 1 ? "" : "s"} found`);
1358
+ if (matching.length === 0) {
1359
+ p4.cancel(
1360
+ `No ${platform} connections found.
1361
+
1362
+ Add one with: ${pc5.cyan(`pica connection add ${platform}`)}`
1363
+ );
1364
+ process.exit(1);
1365
+ }
1366
+ if (matching.length === 1) {
1367
+ p4.log.info(`Using connection: ${pc5.dim(matching[0].key)}`);
1368
+ return matching[0].key;
1369
+ }
1370
+ const selected = await p4.select({
1371
+ message: `Multiple ${platform} connections found. Which one?`,
1372
+ options: matching.map((c) => ({
1373
+ value: c.key,
1374
+ label: `${c.key}`,
1375
+ hint: c.state
1376
+ }))
1377
+ });
1378
+ if (p4.isCancel(selected)) {
1379
+ p4.cancel("Cancelled.");
1380
+ process.exit(0);
1381
+ }
1382
+ return selected;
1383
+ }
1384
+ function parseKeyValuePairs(pairs) {
1385
+ const result = {};
1386
+ for (const pair of pairs) {
1387
+ const eqIdx = pair.indexOf("=");
1388
+ if (eqIdx === -1) continue;
1389
+ const key = pair.slice(0, eqIdx);
1390
+ const value = pair.slice(eqIdx + 1);
1391
+ result[key] = value;
1392
+ }
1393
+ return result;
1394
+ }
1395
+ function formatResponse(data, indent = 2) {
1396
+ const prefix = " ".repeat(indent);
1397
+ const json = JSON.stringify(data, null, 2);
1398
+ return json.split("\n").map((line) => `${prefix}${line}`).join("\n");
1399
+ }
1400
+
1401
+ // src/index.ts
1402
+ var require2 = createRequire(import.meta.url);
1403
+ var { version } = require2("../package.json");
1404
+ var program = new Command();
1405
+ program.name("pica").description("CLI for managing Pica").version(version);
1406
+ program.command("init").description("Set up Pica and install MCP to your AI agents").option("-y, --yes", "Skip confirmations").option("-g, --global", "Install MCP globally (available in all projects)").option("-p, --project", "Install MCP for this project only (creates .mcp.json)").action(async (options) => {
1407
+ await initCommand(options);
1408
+ });
1409
+ var connection = program.command("connection").description("Manage connections");
1410
+ connection.command("add [platform]").alias("a").description("Add a new connection").action(async (platform) => {
1411
+ await connectionAddCommand(platform);
1412
+ });
1413
+ connection.command("list").alias("ls").description("List your connections").action(async () => {
1414
+ await connectionListCommand();
1415
+ });
1416
+ program.command("platforms").alias("p").description("List available platforms").option("-c, --category <category>", "Filter by category").option("--json", "Output as JSON").action(async (options) => {
1417
+ await platformsCommand(options);
1418
+ });
1419
+ program.command("add [platform]").description("Shortcut for: connection add").action(async (platform) => {
1420
+ await connectionAddCommand(platform);
1421
+ });
1422
+ program.command("list").alias("ls").description("Shortcut for: connection list").action(async () => {
1423
+ await connectionListCommand();
1424
+ });
1425
+ var actions = program.command("actions").alias("a").description("Discover and execute platform actions");
1426
+ 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) => {
1427
+ await actionsSearchCommand(platform, query, options);
1428
+ });
1429
+ 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) => {
1430
+ await actionsKnowledgeCommand(actionId, options);
1431
+ });
1432
+ 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) => {
1433
+ await actionsExecuteCommand(actionId, options);
1434
+ });
1435
+ 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) => {
1436
+ await actionsSearchCommand(platform, query, options);
1437
+ });
1438
+ 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) => {
1439
+ await actionsExecuteCommand(actionId, options);
1440
+ });
1441
+ function collectValues(value, previous) {
1442
+ return (previous || []).concat([value]);
1443
+ }
1444
+ program.parse();