@picahq/cli 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +166 -95
  2. package/dist/index.js +871 -195
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import { createRequire } from "module";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/init.ts
8
- import * as p from "@clack/prompts";
9
- import pc2 from "picocolors";
8
+ import * as p2 from "@clack/prompts";
9
+ import pc3 from "picocolors";
10
10
 
11
11
  // src/lib/config.ts
12
12
  import fs from "fs";
@@ -41,17 +41,43 @@ function getApiKey() {
41
41
  const config = readConfig();
42
42
  return config?.apiKey ?? null;
43
43
  }
44
+ function getAccessControl() {
45
+ return readConfig()?.accessControl ?? {};
46
+ }
47
+ function updateAccessControl(settings) {
48
+ const config = readConfig();
49
+ if (!config) return;
50
+ const cleaned = {};
51
+ if (settings.permissions && settings.permissions !== "admin") {
52
+ cleaned.permissions = settings.permissions;
53
+ }
54
+ if (settings.connectionKeys && !(settings.connectionKeys.length === 1 && settings.connectionKeys[0] === "*")) {
55
+ cleaned.connectionKeys = settings.connectionKeys;
56
+ }
57
+ if (settings.actionIds && !(settings.actionIds.length === 1 && settings.actionIds[0] === "*")) {
58
+ cleaned.actionIds = settings.actionIds;
59
+ }
60
+ if (settings.knowledgeAgent) {
61
+ cleaned.knowledgeAgent = true;
62
+ }
63
+ if (Object.keys(cleaned).length === 0) {
64
+ delete config.accessControl;
65
+ } else {
66
+ config.accessControl = cleaned;
67
+ }
68
+ writeConfig(config);
69
+ }
44
70
 
45
71
  // src/lib/agents.ts
46
72
  import fs2 from "fs";
47
73
  import path2 from "path";
48
74
  import os2 from "os";
49
75
  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));
76
+ function expandPath(p6) {
77
+ if (p6.startsWith("~/")) {
78
+ return path2.join(os2.homedir(), p6.slice(2));
53
79
  }
54
- return p4;
80
+ return p6;
55
81
  }
56
82
  function getClaudeDesktopConfigPath() {
57
83
  switch (process.platform) {
@@ -179,20 +205,35 @@ function writeAgentConfig(agent, config, scope = "global") {
179
205
  fs2.writeFileSync(configPath, JSON.stringify(config, null, 2));
180
206
  }
181
207
  }
182
- function getMcpServerConfig(apiKey) {
208
+ function getMcpServerConfig(apiKey, accessControl) {
209
+ const env = {
210
+ PICA_SECRET: apiKey
211
+ };
212
+ if (accessControl) {
213
+ if (accessControl.permissions && accessControl.permissions !== "admin") {
214
+ env.PICA_PERMISSIONS = accessControl.permissions;
215
+ }
216
+ if (accessControl.connectionKeys && !(accessControl.connectionKeys.length === 1 && accessControl.connectionKeys[0] === "*")) {
217
+ env.PICA_CONNECTION_KEYS = accessControl.connectionKeys.join(",");
218
+ }
219
+ if (accessControl.actionIds && !(accessControl.actionIds.length === 1 && accessControl.actionIds[0] === "*")) {
220
+ env.PICA_ACTION_IDS = accessControl.actionIds.join(",");
221
+ }
222
+ if (accessControl.knowledgeAgent) {
223
+ env.PICA_KNOWLEDGE_AGENT = "true";
224
+ }
225
+ }
183
226
  return {
184
227
  command: "npx",
185
228
  args: ["-y", "@picahq/mcp"],
186
- env: {
187
- PICA_SECRET: apiKey
188
- }
229
+ env
189
230
  };
190
231
  }
191
- function installMcpConfig(agent, apiKey, scope = "global") {
232
+ function installMcpConfig(agent, apiKey, scope = "global", accessControl) {
192
233
  const config = readAgentConfig(agent, scope);
193
234
  const configKey = agent.configKey;
194
235
  const mcpServers = config[configKey] || {};
195
- mcpServers["pica"] = getMcpServerConfig(apiKey);
236
+ mcpServers["pica"] = getMcpServerConfig(apiKey, accessControl);
196
237
  config[configKey] = mcpServers;
197
238
  writeAgentConfig(agent, config, scope);
198
239
  }
@@ -247,8 +288,8 @@ var PicaApi = class {
247
288
  }
248
289
  const response = await fetch(url, fetchOpts);
249
290
  if (!response.ok) {
250
- const text3 = await response.text();
251
- throw new ApiError(response.status, text3 || `HTTP ${response.status}`);
291
+ const text4 = await response.text();
292
+ throw new ApiError(response.status, text4 || `HTTP ${response.status}`);
252
293
  }
253
294
  return response.json();
254
295
  }
@@ -279,6 +320,140 @@ var PicaApi = class {
279
320
  } while (page <= totalPages);
280
321
  return allPlatforms;
281
322
  }
323
+ async searchActions(platform, query, agentType) {
324
+ const isKnowledgeAgent = !agentType || agentType === "knowledge";
325
+ const queryParams = {
326
+ query,
327
+ limit: "5"
328
+ };
329
+ if (isKnowledgeAgent) {
330
+ queryParams.knowledgeAgent = "true";
331
+ } else {
332
+ queryParams.executeAgent = "true";
333
+ }
334
+ const response = await this.requestFull({
335
+ path: `/available-actions/search/${platform}`,
336
+ queryParams
337
+ });
338
+ return response || [];
339
+ }
340
+ async getActionDetails(actionId) {
341
+ const response = await this.requestFull({
342
+ path: "/knowledge",
343
+ queryParams: { _id: actionId }
344
+ });
345
+ const actions2 = response?.rows || [];
346
+ if (actions2.length === 0) {
347
+ throw new ApiError(404, `Action with ID ${actionId} not found`);
348
+ }
349
+ return actions2[0];
350
+ }
351
+ async getActionKnowledge(actionId) {
352
+ const action = await this.getActionDetails(actionId);
353
+ if (!action.knowledge || !action.method) {
354
+ return {
355
+ knowledge: "No knowledge was found",
356
+ method: "No method was found"
357
+ };
358
+ }
359
+ return {
360
+ knowledge: action.knowledge,
361
+ method: action.method
362
+ };
363
+ }
364
+ async executePassthroughRequest(args, preloadedAction) {
365
+ const action = preloadedAction ?? await this.getActionDetails(args.actionId);
366
+ const method = action.method;
367
+ const contentType = args.isFormData ? "multipart/form-data" : args.isFormUrlEncoded ? "application/x-www-form-urlencoded" : "application/json";
368
+ const requestHeaders = {
369
+ "x-pica-secret": this.apiKey,
370
+ "x-pica-connection-key": args.connectionKey,
371
+ "x-pica-action-id": action._id,
372
+ "Content-Type": contentType,
373
+ ...args.headers
374
+ };
375
+ const finalActionPath = args.pathVariables ? replacePathVariables(action.path, args.pathVariables) : action.path;
376
+ const normalizedPath = finalActionPath.startsWith("/") ? finalActionPath : `/${finalActionPath}`;
377
+ const url = `${API_BASE.replace("/v1", "")}/v1/passthrough${normalizedPath}`;
378
+ const isCustomAction = action.tags?.includes("custom");
379
+ let requestData = args.data;
380
+ if (isCustomAction && method?.toLowerCase() !== "get") {
381
+ requestData = {
382
+ ...args.data,
383
+ connectionKey: args.connectionKey
384
+ };
385
+ }
386
+ let queryString = "";
387
+ if (args.queryParams && Object.keys(args.queryParams).length > 0) {
388
+ const params = new URLSearchParams(
389
+ Object.entries(args.queryParams).map(([k, v]) => [k, String(v)])
390
+ );
391
+ queryString = `?${params.toString()}`;
392
+ }
393
+ const fullUrl = `${url}${queryString}`;
394
+ const fetchOpts = {
395
+ method,
396
+ headers: requestHeaders
397
+ };
398
+ if (method?.toLowerCase() !== "get" && requestData !== void 0) {
399
+ if (args.isFormUrlEncoded) {
400
+ const params = new URLSearchParams();
401
+ if (requestData && typeof requestData === "object" && !Array.isArray(requestData)) {
402
+ Object.entries(requestData).forEach(([key, value]) => {
403
+ if (typeof value === "object") {
404
+ params.append(key, JSON.stringify(value));
405
+ } else {
406
+ params.append(key, String(value));
407
+ }
408
+ });
409
+ }
410
+ fetchOpts.body = params.toString();
411
+ } else if (args.isFormData) {
412
+ const boundary = `----FormBoundary${Date.now()}`;
413
+ requestHeaders["Content-Type"] = `multipart/form-data; boundary=${boundary}`;
414
+ let body = "";
415
+ if (requestData && typeof requestData === "object" && !Array.isArray(requestData)) {
416
+ Object.entries(requestData).forEach(([key, value]) => {
417
+ body += `--${boundary}\r
418
+ `;
419
+ body += `Content-Disposition: form-data; name="${key}"\r
420
+ \r
421
+ `;
422
+ body += typeof value === "object" ? JSON.stringify(value) : String(value);
423
+ body += "\r\n";
424
+ });
425
+ }
426
+ body += `--${boundary}--\r
427
+ `;
428
+ fetchOpts.body = body;
429
+ fetchOpts.headers = requestHeaders;
430
+ } else {
431
+ fetchOpts.body = JSON.stringify(requestData);
432
+ }
433
+ }
434
+ const sanitizedConfig = {
435
+ url: fullUrl,
436
+ method,
437
+ headers: {
438
+ ...requestHeaders,
439
+ "x-pica-secret": "***REDACTED***"
440
+ },
441
+ params: args.queryParams ? Object.fromEntries(
442
+ Object.entries(args.queryParams).map(([k, v]) => [k, String(v)])
443
+ ) : void 0,
444
+ data: requestData
445
+ };
446
+ const response = await fetch(fullUrl, fetchOpts);
447
+ if (!response.ok) {
448
+ const text4 = await response.text();
449
+ throw new ApiError(response.status, text4 || `HTTP ${response.status}`);
450
+ }
451
+ const responseData = await response.json();
452
+ return {
453
+ requestConfig: sanitizedConfig,
454
+ responseData
455
+ };
456
+ }
282
457
  async waitForConnection(platform, timeoutMs = 5 * 60 * 1e3, pollIntervalMs = 5e3, onPoll) {
283
458
  const startTime = Date.now();
284
459
  const existingConnections = await this.listConnections();
@@ -306,6 +481,72 @@ var TimeoutError = class extends Error {
306
481
  function sleep(ms) {
307
482
  return new Promise((resolve) => setTimeout(resolve, ms));
308
483
  }
484
+ function replacePathVariables(path3, variables) {
485
+ if (!path3) return path3;
486
+ let result = path3;
487
+ result = result.replace(/\{\{([^}]+)\}\}/g, (_match, variable) => {
488
+ const trimmedVariable = variable.trim();
489
+ const value = variables[trimmedVariable];
490
+ if (value === void 0 || value === null || value === "") {
491
+ throw new Error(`Missing value for path variable: ${trimmedVariable}`);
492
+ }
493
+ return encodeURIComponent(value.toString());
494
+ });
495
+ result = result.replace(/\{([^}]+)\}/g, (_match, variable) => {
496
+ const trimmedVariable = variable.trim();
497
+ const value = variables[trimmedVariable];
498
+ if (value === void 0 || value === null || value === "") {
499
+ throw new Error(`Missing value for path variable: ${trimmedVariable}`);
500
+ }
501
+ return encodeURIComponent(value.toString());
502
+ });
503
+ return result;
504
+ }
505
+ var PERMISSION_METHODS = {
506
+ read: ["GET"],
507
+ write: ["GET", "POST", "PUT", "PATCH"],
508
+ admin: null
509
+ };
510
+ function filterByPermissions(actions2, permissions) {
511
+ const allowed = PERMISSION_METHODS[permissions];
512
+ if (allowed === null) return actions2;
513
+ return actions2.filter((a) => allowed.includes(a.method.toUpperCase()));
514
+ }
515
+ function isMethodAllowed(method, permissions) {
516
+ const allowed = PERMISSION_METHODS[permissions];
517
+ if (allowed === null) return true;
518
+ return allowed.includes(method.toUpperCase());
519
+ }
520
+ function isActionAllowed(actionId, allowedActionIds) {
521
+ return allowedActionIds.includes("*") || allowedActionIds.includes(actionId);
522
+ }
523
+ function buildActionKnowledgeWithGuidance(knowledge, method, platform, actionId) {
524
+ const baseUrl = "https://api.picaos.com";
525
+ return `${knowledge}
526
+
527
+ API REQUEST STRUCTURE
528
+ ======================
529
+ URL: ${baseUrl}/v1/passthrough/{{PATH}}
530
+
531
+ IMPORTANT: When constructing the URL, only include the API endpoint path after the base URL.
532
+ Do NOT include the full third-party API URL.
533
+
534
+ Examples:
535
+ Correct: ${baseUrl}/v1/passthrough/crm/v3/objects/contacts/search
536
+ Incorrect: ${baseUrl}/v1/passthrough/https://api.hubapi.com/crm/v3/objects/contacts/search
537
+
538
+ METHOD: ${method}
539
+
540
+ HEADERS:
541
+ - x-pica-secret: {{process.env.PICA_SECRET}}
542
+ - x-pica-connection-key: {{process.env.PICA_${platform.toUpperCase()}_CONNECTION_KEY}}
543
+ - x-pica-action-id: ${actionId}
544
+ - ... (other headers)
545
+
546
+ BODY: {{BODY}}
547
+
548
+ QUERY PARAMS: {{QUERY_PARAMS}}`;
549
+ }
309
550
 
310
551
  // src/lib/browser.ts
311
552
  import open from "open";
@@ -324,11 +565,171 @@ async function openApiKeyPage() {
324
565
  await open(getApiKeyUrl());
325
566
  }
326
567
 
568
+ // src/commands/config.ts
569
+ import * as p from "@clack/prompts";
570
+ import pc from "picocolors";
571
+ async function configCommand() {
572
+ const config = readConfig();
573
+ if (!config) {
574
+ p.log.error(`No Pica config found. Run ${pc.cyan("pica init")} first.`);
575
+ return;
576
+ }
577
+ p.intro(pc.bgCyan(pc.black(" Pica Access Control ")));
578
+ const current = getAccessControl();
579
+ console.log();
580
+ console.log(` ${pc.bold("Current Access Control")}`);
581
+ console.log(` ${pc.dim("\u2500".repeat(42))}`);
582
+ console.log(` ${pc.dim("Permissions:")} ${current.permissions ?? "admin"}`);
583
+ console.log(` ${pc.dim("Connections:")} ${formatList(current.connectionKeys)}`);
584
+ console.log(` ${pc.dim("Action IDs:")} ${formatList(current.actionIds)}`);
585
+ console.log(` ${pc.dim("Knowledge only:")} ${current.knowledgeAgent ? "yes" : "no"}`);
586
+ console.log();
587
+ const permissions = await p.select({
588
+ message: "Permission level",
589
+ options: [
590
+ { value: "admin", label: "Admin", hint: "Full access (GET, POST, PUT, PATCH, DELETE)" },
591
+ { value: "write", label: "Write", hint: "GET, POST, PUT, PATCH" },
592
+ { value: "read", label: "Read", hint: "GET only" }
593
+ ],
594
+ initialValue: current.permissions ?? "admin"
595
+ });
596
+ if (p.isCancel(permissions)) {
597
+ p.outro("No changes made.");
598
+ return;
599
+ }
600
+ const connectionMode = await p.select({
601
+ message: "Connection scope",
602
+ options: [
603
+ { value: "all", label: "All connections" },
604
+ { value: "specific", label: "Select specific connections" }
605
+ ],
606
+ initialValue: current.connectionKeys ? "specific" : "all"
607
+ });
608
+ if (p.isCancel(connectionMode)) {
609
+ p.outro("No changes made.");
610
+ return;
611
+ }
612
+ let connectionKeys;
613
+ if (connectionMode === "specific") {
614
+ connectionKeys = await selectConnections(config.apiKey);
615
+ if (connectionKeys === void 0) {
616
+ p.outro("No changes made.");
617
+ return;
618
+ }
619
+ if (connectionKeys.length === 0) {
620
+ p.log.info(`No connections found. Defaulting to all. Use ${pc.cyan("pica add")} to connect platforms.`);
621
+ connectionKeys = void 0;
622
+ }
623
+ }
624
+ const actionMode = await p.select({
625
+ message: "Action scope",
626
+ options: [
627
+ { value: "all", label: "All actions" },
628
+ { value: "specific", label: "Restrict to specific action IDs" }
629
+ ],
630
+ initialValue: current.actionIds ? "specific" : "all"
631
+ });
632
+ if (p.isCancel(actionMode)) {
633
+ p.outro("No changes made.");
634
+ return;
635
+ }
636
+ let actionIds;
637
+ if (actionMode === "specific") {
638
+ const actionInput = await p.text({
639
+ message: "Enter action IDs (comma-separated):",
640
+ placeholder: "action-id-1, action-id-2",
641
+ initialValue: current.actionIds?.join(", ") ?? "",
642
+ validate: (value) => {
643
+ if (!value.trim()) return "At least one action ID is required";
644
+ return void 0;
645
+ }
646
+ });
647
+ if (p.isCancel(actionInput)) {
648
+ p.outro("No changes made.");
649
+ return;
650
+ }
651
+ actionIds = actionInput.split(",").map((s) => s.trim()).filter(Boolean);
652
+ }
653
+ const knowledgeAgent = await p.confirm({
654
+ message: "Enable knowledge-only mode? (disables action execution)",
655
+ initialValue: current.knowledgeAgent ?? false
656
+ });
657
+ if (p.isCancel(knowledgeAgent)) {
658
+ p.outro("No changes made.");
659
+ return;
660
+ }
661
+ const settings = {
662
+ permissions,
663
+ connectionKeys: connectionKeys ?? ["*"],
664
+ actionIds: actionIds ?? ["*"],
665
+ knowledgeAgent
666
+ };
667
+ updateAccessControl(settings);
668
+ const ac = getAccessControl();
669
+ const statuses = getAgentStatuses();
670
+ const reinstalled = [];
671
+ for (const s of statuses) {
672
+ if (s.globalMcp) {
673
+ installMcpConfig(s.agent, config.apiKey, "global", ac);
674
+ reinstalled.push(`${s.agent.name} (global)`);
675
+ }
676
+ if (s.projectMcp) {
677
+ installMcpConfig(s.agent, config.apiKey, "project", ac);
678
+ reinstalled.push(`${s.agent.name} (project)`);
679
+ }
680
+ }
681
+ if (reinstalled.length > 0) {
682
+ p.log.success(`Updated MCP configs: ${reinstalled.join(", ")}`);
683
+ }
684
+ p.outro("Access control updated.");
685
+ }
686
+ async function selectConnections(apiKey) {
687
+ const spinner6 = p.spinner();
688
+ spinner6.start("Fetching connections...");
689
+ let connections;
690
+ try {
691
+ const api = new PicaApi(apiKey);
692
+ const rawConnections = await api.listConnections();
693
+ connections = rawConnections.map((c) => ({ platform: c.platform, key: c.key }));
694
+ spinner6.stop(`Found ${connections.length} connection(s)`);
695
+ } catch {
696
+ spinner6.stop("Could not fetch connections");
697
+ const manual = await p.text({
698
+ message: "Enter connection keys manually (comma-separated):",
699
+ placeholder: "conn_key_1, conn_key_2",
700
+ validate: (value) => {
701
+ if (!value.trim()) return "At least one connection key is required";
702
+ return void 0;
703
+ }
704
+ });
705
+ if (p.isCancel(manual)) return void 0;
706
+ return manual.split(",").map((s) => s.trim()).filter(Boolean);
707
+ }
708
+ if (connections.length === 0) {
709
+ return [];
710
+ }
711
+ const selected = await p.multiselect({
712
+ message: "Select connections:",
713
+ options: connections.map((c) => ({
714
+ value: c.key,
715
+ label: `${c.platform}`,
716
+ hint: c.key
717
+ }))
718
+ });
719
+ if (p.isCancel(selected)) return void 0;
720
+ return selected;
721
+ }
722
+ function formatList(list) {
723
+ if (!list || list.length === 0) return "all";
724
+ if (list.length === 1 && list[0] === "*") return "all";
725
+ return list.join(", ");
726
+ }
727
+
327
728
  // src/commands/init.ts
328
729
  import open2 from "open";
329
730
 
330
731
  // src/lib/table.ts
331
- import pc from "picocolors";
732
+ import pc2 from "picocolors";
332
733
  function printTable(columns, rows) {
333
734
  if (rows.length === 0) return;
334
735
  const gap = " ";
@@ -340,10 +741,10 @@ function printTable(columns, rows) {
340
741
  });
341
742
  const header = columns.map((col, i) => {
342
743
  const padded = col.align === "right" ? col.label.padStart(widths[i]) : col.label.padEnd(widths[i]);
343
- return pc.dim(padded);
744
+ return pc2.dim(padded);
344
745
  }).join(gap);
345
746
  console.log(`${indent}${header}`);
346
- const separator = columns.map((_, i) => pc.dim("\u2500".repeat(widths[i]))).join(gap);
747
+ const separator = columns.map((_, i) => pc2.dim("\u2500".repeat(widths[i]))).join(gap);
347
748
  console.log(`${indent}${separator}`);
348
749
  for (const row of rows) {
349
750
  const line = columns.map((col, i) => {
@@ -367,7 +768,7 @@ function stripAnsi(str) {
367
768
  async function initCommand(options) {
368
769
  const existingConfig = readConfig();
369
770
  if (existingConfig) {
370
- p.intro(pc2.bgCyan(pc2.black(" Pica ")));
771
+ p2.intro(pc3.bgCyan(pc3.black(" Pica ")));
371
772
  await handleExistingConfig(existingConfig.apiKey, options);
372
773
  return;
373
774
  }
@@ -378,10 +779,10 @@ async function handleExistingConfig(apiKey, options) {
378
779
  const statuses = getAgentStatuses();
379
780
  const masked = maskApiKey(apiKey);
380
781
  console.log();
381
- console.log(` ${pc2.bold("Current Setup")}`);
382
- console.log(` ${pc2.dim("\u2500".repeat(42))}`);
383
- console.log(` ${pc2.dim("API Key:")} ${masked}`);
384
- console.log(` ${pc2.dim("Config:")} ${getConfigPath()}`);
782
+ console.log(` ${pc3.bold("Current Setup")}`);
783
+ console.log(` ${pc3.dim("\u2500".repeat(42))}`);
784
+ console.log(` ${pc3.dim("API Key:")} ${masked}`);
785
+ console.log(` ${pc3.dim("Config:")} ${getConfigPath()}`);
385
786
  console.log();
386
787
  printTable(
387
788
  [
@@ -391,15 +792,24 @@ async function handleExistingConfig(apiKey, options) {
391
792
  ],
392
793
  statuses.map((s) => ({
393
794
  agent: s.agent.name,
394
- global: !s.detected ? pc2.dim("-") : s.globalMcp ? pc2.green("\u25CF yes") : pc2.yellow("\u25CB no"),
395
- project: s.projectMcp === null ? pc2.dim("-") : s.projectMcp ? pc2.green("\u25CF yes") : pc2.yellow("\u25CB no")
795
+ global: !s.detected ? pc3.dim("-") : s.globalMcp ? pc3.green("\u25CF yes") : pc3.yellow("\u25CB no"),
796
+ project: s.projectMcp === null ? pc3.dim("-") : s.projectMcp ? pc3.green("\u25CF yes") : pc3.yellow("\u25CB no")
396
797
  }))
397
798
  );
398
799
  const notDetected = statuses.filter((s) => !s.detected);
399
800
  if (notDetected.length > 0) {
400
- console.log(` ${pc2.dim("- = not detected on this machine")}`);
801
+ console.log(` ${pc3.dim("- = not detected on this machine")}`);
802
+ }
803
+ const ac = getAccessControl();
804
+ if (Object.keys(ac).length > 0) {
805
+ console.log(` ${pc3.bold("Access Control")}`);
806
+ console.log(` ${pc3.dim("\u2500".repeat(42))}`);
807
+ if (ac.permissions) console.log(` ${pc3.dim("Permissions:")} ${ac.permissions}`);
808
+ if (ac.connectionKeys) console.log(` ${pc3.dim("Connections:")} ${ac.connectionKeys.join(", ")}`);
809
+ if (ac.actionIds) console.log(` ${pc3.dim("Action IDs:")} ${ac.actionIds.join(", ")}`);
810
+ if (ac.knowledgeAgent) console.log(` ${pc3.dim("Knowledge only:")} yes`);
811
+ console.log();
401
812
  }
402
- console.log();
403
813
  const actionOptions = [];
404
814
  actionOptions.push({
405
815
  value: "update-key",
@@ -421,16 +831,21 @@ async function handleExistingConfig(apiKey, options) {
421
831
  hint: agentsMissingProject.map((s) => s.agent.name).join(", ")
422
832
  });
423
833
  }
834
+ actionOptions.push({
835
+ value: "access-control",
836
+ label: "Configure access control",
837
+ hint: "permissions, connections, actions"
838
+ });
424
839
  actionOptions.push({
425
840
  value: "start-fresh",
426
841
  label: "Start fresh (reconfigure everything)"
427
842
  });
428
- const action = await p.select({
843
+ const action = await p2.select({
429
844
  message: "What would you like to do?",
430
845
  options: actionOptions
431
846
  });
432
- if (p.isCancel(action)) {
433
- p.outro("No changes made.");
847
+ if (p2.isCancel(action)) {
848
+ p2.outro("No changes made.");
434
849
  return;
435
850
  }
436
851
  switch (action) {
@@ -443,26 +858,29 @@ async function handleExistingConfig(apiKey, options) {
443
858
  case "install-project":
444
859
  await handleInstallProject(apiKey, agentsMissingProject);
445
860
  break;
861
+ case "access-control":
862
+ await configCommand();
863
+ break;
446
864
  case "start-fresh":
447
865
  await freshSetup({ yes: true });
448
866
  break;
449
867
  }
450
868
  }
451
869
  async function handleUpdateKey(statuses) {
452
- p.note(`Get your API key at:
453
- ${pc2.cyan(getApiKeyUrl())}`, "API Key");
454
- const openBrowser = await p.confirm({
870
+ p2.note(`Get your API key at:
871
+ ${pc3.cyan(getApiKeyUrl())}`, "API Key");
872
+ const openBrowser = await p2.confirm({
455
873
  message: "Open browser to get API key?",
456
874
  initialValue: true
457
875
  });
458
- if (p.isCancel(openBrowser)) {
459
- p.cancel("Cancelled.");
876
+ if (p2.isCancel(openBrowser)) {
877
+ p2.cancel("Cancelled.");
460
878
  process.exit(0);
461
879
  }
462
880
  if (openBrowser) {
463
881
  await openApiKeyPage();
464
882
  }
465
- const newKey = await p.text({
883
+ const newKey = await p2.text({
466
884
  message: "Enter your new Pica API key:",
467
885
  placeholder: "sk_live_...",
468
886
  validate: (value) => {
@@ -473,28 +891,29 @@ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
473
891
  return void 0;
474
892
  }
475
893
  });
476
- if (p.isCancel(newKey)) {
477
- p.cancel("Cancelled.");
894
+ if (p2.isCancel(newKey)) {
895
+ p2.cancel("Cancelled.");
478
896
  process.exit(0);
479
897
  }
480
- const spinner4 = p.spinner();
481
- spinner4.start("Validating API key...");
898
+ const spinner6 = p2.spinner();
899
+ spinner6.start("Validating API key...");
482
900
  const api = new PicaApi(newKey);
483
901
  const isValid = await api.validateApiKey();
484
902
  if (!isValid) {
485
- spinner4.stop("Invalid API key");
486
- p.cancel(`Invalid API key. Get a valid key at ${getApiKeyUrl()}`);
903
+ spinner6.stop("Invalid API key");
904
+ p2.cancel(`Invalid API key. Get a valid key at ${getApiKeyUrl()}`);
487
905
  process.exit(1);
488
906
  }
489
- spinner4.stop("API key validated");
907
+ spinner6.stop("API key validated");
908
+ const ac = getAccessControl();
490
909
  const reinstalled = [];
491
910
  for (const s of statuses) {
492
911
  if (s.globalMcp) {
493
- installMcpConfig(s.agent, newKey, "global");
912
+ installMcpConfig(s.agent, newKey, "global", ac);
494
913
  reinstalled.push(`${s.agent.name} (global)`);
495
914
  }
496
915
  if (s.projectMcp) {
497
- installMcpConfig(s.agent, newKey, "project");
916
+ installMcpConfig(s.agent, newKey, "project", ac);
498
917
  reinstalled.push(`${s.agent.name} (project)`);
499
918
  }
500
919
  }
@@ -502,108 +921,111 @@ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
502
921
  writeConfig({
503
922
  apiKey: newKey,
504
923
  installedAgents: config?.installedAgents ?? [],
505
- createdAt: config?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
924
+ createdAt: config?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
925
+ accessControl: config?.accessControl
506
926
  });
507
927
  if (reinstalled.length > 0) {
508
- p.log.success(`Updated MCP configs: ${reinstalled.join(", ")}`);
928
+ p2.log.success(`Updated MCP configs: ${reinstalled.join(", ")}`);
509
929
  }
510
- p.outro("API key updated.");
930
+ p2.outro("API key updated.");
511
931
  }
512
932
  async function handleInstallMore(apiKey, missing) {
933
+ const ac = getAccessControl();
513
934
  if (missing.length === 1) {
514
935
  const agent = missing[0].agent;
515
- const confirm2 = await p.confirm({
936
+ const confirm3 = await p2.confirm({
516
937
  message: `Install Pica MCP to ${agent.name}?`,
517
938
  initialValue: true
518
939
  });
519
- if (p.isCancel(confirm2) || !confirm2) {
520
- p.outro("No changes made.");
940
+ if (p2.isCancel(confirm3) || !confirm3) {
941
+ p2.outro("No changes made.");
521
942
  return;
522
943
  }
523
- installMcpConfig(agent, apiKey, "global");
944
+ installMcpConfig(agent, apiKey, "global", ac);
524
945
  updateConfigAgents(agent.id);
525
- p.log.success(`${agent.name}: MCP installed`);
526
- p.outro("Done.");
946
+ p2.log.success(`${agent.name}: MCP installed`);
947
+ p2.outro("Done.");
527
948
  return;
528
949
  }
529
- const selected = await p.multiselect({
950
+ const selected = await p2.multiselect({
530
951
  message: "Select agents to install MCP:",
531
952
  options: missing.map((s) => ({
532
953
  value: s.agent.id,
533
954
  label: s.agent.name
534
955
  }))
535
956
  });
536
- if (p.isCancel(selected)) {
537
- p.outro("No changes made.");
957
+ if (p2.isCancel(selected)) {
958
+ p2.outro("No changes made.");
538
959
  return;
539
960
  }
540
961
  const agents = missing.filter((s) => selected.includes(s.agent.id));
541
962
  for (const s of agents) {
542
- installMcpConfig(s.agent, apiKey, "global");
963
+ installMcpConfig(s.agent, apiKey, "global", ac);
543
964
  updateConfigAgents(s.agent.id);
544
- p.log.success(`${s.agent.name}: MCP installed`);
965
+ p2.log.success(`${s.agent.name}: MCP installed`);
545
966
  }
546
- p.outro("Done.");
967
+ p2.outro("Done.");
547
968
  }
548
969
  async function handleInstallProject(apiKey, missing) {
970
+ const ac = getAccessControl();
549
971
  if (missing.length === 1) {
550
972
  const agent = missing[0].agent;
551
- const confirm2 = await p.confirm({
973
+ const confirm3 = await p2.confirm({
552
974
  message: `Install project-level MCP for ${agent.name}?`,
553
975
  initialValue: true
554
976
  });
555
- if (p.isCancel(confirm2) || !confirm2) {
556
- p.outro("No changes made.");
977
+ if (p2.isCancel(confirm3) || !confirm3) {
978
+ p2.outro("No changes made.");
557
979
  return;
558
980
  }
559
- installMcpConfig(agent, apiKey, "project");
981
+ installMcpConfig(agent, apiKey, "project", ac);
560
982
  const configPath = getAgentConfigPath(agent, "project");
561
- p.log.success(`${agent.name}: ${configPath} created`);
562
- p.note(
563
- pc2.yellow("Project config files can be committed to share with your team.\n") + pc2.yellow("Team members will need their own API key."),
983
+ p2.log.success(`${agent.name}: ${configPath} created`);
984
+ p2.note(
985
+ pc3.yellow("Project config files can be committed to share with your team.\n") + pc3.yellow("Team members will need their own API key."),
564
986
  "Tip"
565
987
  );
566
- p.outro("Done.");
988
+ p2.outro("Done.");
567
989
  return;
568
990
  }
569
- const selected = await p.multiselect({
991
+ const selected = await p2.multiselect({
570
992
  message: "Select agents for project-level MCP:",
571
993
  options: missing.map((s) => ({
572
994
  value: s.agent.id,
573
995
  label: s.agent.name
574
996
  }))
575
997
  });
576
- if (p.isCancel(selected)) {
577
- p.outro("No changes made.");
998
+ if (p2.isCancel(selected)) {
999
+ p2.outro("No changes made.");
578
1000
  return;
579
1001
  }
580
1002
  const agents = missing.filter((s) => selected.includes(s.agent.id));
581
1003
  for (const s of agents) {
582
- installMcpConfig(s.agent, apiKey, "project");
1004
+ installMcpConfig(s.agent, apiKey, "project", ac);
583
1005
  const configPath = getAgentConfigPath(s.agent, "project");
584
- p.log.success(`${s.agent.name}: ${configPath} created`);
1006
+ p2.log.success(`${s.agent.name}: ${configPath} created`);
585
1007
  }
586
- p.note(
587
- pc2.yellow("Project config files can be committed to share with your team.\n") + pc2.yellow("Team members will need their own API key."),
1008
+ p2.note(
1009
+ pc3.yellow("Project config files can be committed to share with your team.\n") + pc3.yellow("Team members will need their own API key."),
588
1010
  "Tip"
589
1011
  );
590
- p.outro("Done.");
1012
+ p2.outro("Done.");
591
1013
  }
592
1014
  async function freshSetup(options) {
593
- p.note(`Get your API key at:
594
- ${pc2.cyan(getApiKeyUrl())}`, "API Key");
595
- const openBrowser = await p.confirm({
1015
+ p2.note(`Get your API key at:
1016
+ ${pc3.cyan(getApiKeyUrl())}`, "API Key");
1017
+ const openBrowser = await p2.confirm({
596
1018
  message: "Open browser to get API key?",
597
1019
  initialValue: true
598
1020
  });
599
- if (p.isCancel(openBrowser)) {
600
- p.cancel("Setup cancelled.");
1021
+ if (p2.isCancel(openBrowser)) {
1022
+ p2.cancel("Setup cancelled.");
601
1023
  process.exit(0);
602
1024
  }
603
1025
  if (openBrowser) {
604
1026
  await openApiKeyPage();
605
1027
  }
606
- const apiKey = await p.text({
1028
+ const apiKey = await p2.text({
607
1029
  message: "Enter your Pica API key:",
608
1030
  placeholder: "sk_live_...",
609
1031
  validate: (value) => {
@@ -614,27 +1036,27 @@ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
614
1036
  return void 0;
615
1037
  }
616
1038
  });
617
- if (p.isCancel(apiKey)) {
618
- p.cancel("Setup cancelled.");
1039
+ if (p2.isCancel(apiKey)) {
1040
+ p2.cancel("Setup cancelled.");
619
1041
  process.exit(0);
620
1042
  }
621
- const spinner4 = p.spinner();
622
- spinner4.start("Validating API key...");
1043
+ const spinner6 = p2.spinner();
1044
+ spinner6.start("Validating API key...");
623
1045
  const api = new PicaApi(apiKey);
624
1046
  const isValid = await api.validateApiKey();
625
1047
  if (!isValid) {
626
- spinner4.stop("Invalid API key");
627
- p.cancel(`Invalid API key. Get a valid key at ${getApiKeyUrl()}`);
1048
+ spinner6.stop("Invalid API key");
1049
+ p2.cancel(`Invalid API key. Get a valid key at ${getApiKeyUrl()}`);
628
1050
  process.exit(1);
629
1051
  }
630
- spinner4.stop("API key validated");
1052
+ spinner6.stop("API key validated");
631
1053
  writeConfig({
632
1054
  apiKey,
633
1055
  installedAgents: [],
634
1056
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
635
1057
  });
636
1058
  const allAgents = getAllAgents();
637
- const agentChoice = await p.select({
1059
+ const agentChoice = await p2.select({
638
1060
  message: "Where do you want to install the MCP?",
639
1061
  options: [
640
1062
  {
@@ -648,8 +1070,8 @@ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
648
1070
  }))
649
1071
  ]
650
1072
  });
651
- if (p.isCancel(agentChoice)) {
652
- p.cancel("Setup cancelled.");
1073
+ if (p2.isCancel(agentChoice)) {
1074
+ p2.cancel("Setup cancelled.");
653
1075
  process.exit(0);
654
1076
  }
655
1077
  const selectedAgents = agentChoice === "all" ? allAgents : allAgents.filter((a) => a.id === agentChoice);
@@ -660,7 +1082,7 @@ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
660
1082
  } else if (options.project) {
661
1083
  scope = "project";
662
1084
  } else if (hasProjectScopeAgent) {
663
- const scopeChoice = await p.select({
1085
+ const scopeChoice = await p2.select({
664
1086
  message: "How do you want to install it?",
665
1087
  options: [
666
1088
  {
@@ -675,8 +1097,8 @@ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
675
1097
  }
676
1098
  ]
677
1099
  });
678
- if (p.isCancel(scopeChoice)) {
679
- p.cancel("Setup cancelled.");
1100
+ if (p2.isCancel(scopeChoice)) {
1101
+ p2.cancel("Setup cancelled.");
680
1102
  process.exit(0);
681
1103
  }
682
1104
  scope = scopeChoice;
@@ -686,12 +1108,12 @@ ${pc2.cyan(getApiKeyUrl())}`, "API Key");
686
1108
  const nonProjectAgents = selectedAgents.filter((a) => !supportsProjectScope(a));
687
1109
  if (projectAgents.length === 0) {
688
1110
  const supported = allAgents.filter((a) => supportsProjectScope(a)).map((a) => a.name).join(", ");
689
- p.note(
1111
+ p2.note(
690
1112
  `${selectedAgents.map((a) => a.name).join(", ")} does not support project-level MCP.
691
1113
  Project scope is supported by: ${supported}`,
692
1114
  "Not Supported"
693
1115
  );
694
- p.cancel("Run again and choose global scope or a different agent.");
1116
+ p2.cancel("Run again and choose global scope or a different agent.");
695
1117
  process.exit(1);
696
1118
  }
697
1119
  for (const agent of projectAgents) {
@@ -699,15 +1121,15 @@ Project scope is supported by: ${supported}`,
699
1121
  installMcpConfig(agent, apiKey, "project");
700
1122
  const configPath = getAgentConfigPath(agent, "project");
701
1123
  const status = wasInstalled ? "updated" : "created";
702
- p.log.success(`${agent.name}: ${configPath} ${status}`);
1124
+ p2.log.success(`${agent.name}: ${configPath} ${status}`);
703
1125
  }
704
1126
  if (nonProjectAgents.length > 0) {
705
- p.log.info(`Installing globally for agents without project scope support:`);
1127
+ p2.log.info(`Installing globally for agents without project scope support:`);
706
1128
  for (const agent of nonProjectAgents) {
707
1129
  const wasInstalled = isMcpInstalled(agent, "global");
708
1130
  installMcpConfig(agent, apiKey, "global");
709
1131
  const status = wasInstalled ? "updated" : "installed";
710
- p.log.success(`${agent.name}: MCP ${status} (global)`);
1132
+ p2.log.success(`${agent.name}: MCP ${status} (global)`);
711
1133
  }
712
1134
  }
713
1135
  const allInstalled = [...projectAgents, ...nonProjectAgents];
@@ -716,23 +1138,23 @@ Project scope is supported by: ${supported}`,
716
1138
  installedAgents: allInstalled.map((a) => a.id),
717
1139
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
718
1140
  });
719
- const configPaths = projectAgents.map((a) => ` ${a.name}: ${pc2.dim(getAgentConfigPath(a, "project"))}`).join("\n");
720
- let summary = `Config saved to: ${pc2.dim(getConfigPath())}
1141
+ const configPaths = projectAgents.map((a) => ` ${a.name}: ${pc3.dim(getAgentConfigPath(a, "project"))}`).join("\n");
1142
+ let summary = `Config saved to: ${pc3.dim(getConfigPath())}
721
1143
  MCP configs:
722
1144
  ${configPaths}
723
1145
 
724
1146
  `;
725
1147
  if (nonProjectAgents.length > 0) {
726
- const globalPaths = nonProjectAgents.map((a) => ` ${a.name}: ${pc2.dim(getAgentConfigPath(a, "global"))}`).join("\n");
1148
+ const globalPaths = nonProjectAgents.map((a) => ` ${a.name}: ${pc3.dim(getAgentConfigPath(a, "global"))}`).join("\n");
727
1149
  summary += `Global configs:
728
1150
  ${globalPaths}
729
1151
 
730
1152
  `;
731
1153
  }
732
- 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.");
733
- p.note(summary, "Setup Complete");
1154
+ summary += pc3.yellow("Note: Project config files can be committed to share with your team.\n") + pc3.yellow("Team members will need their own API key.");
1155
+ p2.note(summary, "Setup Complete");
734
1156
  await promptConnectIntegrations(apiKey);
735
- p.outro("Your AI agents now have access to Pica integrations!");
1157
+ p2.outro("Your AI agents now have access to Pica integrations!");
736
1158
  return;
737
1159
  }
738
1160
  const installedAgentIds = [];
@@ -741,34 +1163,34 @@ ${globalPaths}
741
1163
  installMcpConfig(agent, apiKey, "global");
742
1164
  installedAgentIds.push(agent.id);
743
1165
  const status = wasInstalled ? "updated" : "installed";
744
- p.log.success(`${agent.name}: MCP ${status}`);
1166
+ p2.log.success(`${agent.name}: MCP ${status}`);
745
1167
  }
746
1168
  writeConfig({
747
1169
  apiKey,
748
1170
  installedAgents: installedAgentIds,
749
1171
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
750
1172
  });
751
- p.note(
752
- `Config saved to: ${pc2.dim(getConfigPath())}`,
1173
+ p2.note(
1174
+ `Config saved to: ${pc3.dim(getConfigPath())}`,
753
1175
  "Setup Complete"
754
1176
  );
755
1177
  await promptConnectIntegrations(apiKey);
756
- p.outro("Your AI agents now have access to Pica integrations!");
1178
+ p2.outro("Your AI agents now have access to Pica integrations!");
757
1179
  }
758
1180
  function printBanner() {
759
1181
  console.log();
760
- console.log(pc2.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
761
- console.log(pc2.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
762
- console.log(pc2.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
763
- console.log(pc2.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
764
- console.log(pc2.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
765
- console.log(pc2.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
766
- console.log(pc2.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
767
- console.log(pc2.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
768
- console.log(pc2.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
769
- console.log(pc2.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
1182
+ console.log(pc3.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
1183
+ console.log(pc3.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
1184
+ console.log(pc3.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
1185
+ console.log(pc3.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
1186
+ console.log(pc3.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
1187
+ console.log(pc3.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
1188
+ console.log(pc3.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
1189
+ console.log(pc3.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
1190
+ console.log(pc3.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
1191
+ console.log(pc3.cyan(" \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588"));
770
1192
  console.log();
771
- console.log(pc2.dim(" U N I V E R S A L I N T E G R A T I O N S F O R A I"));
1193
+ console.log(pc3.dim(" U N I V E R S A L I N T E G R A T I O N S F O R A I"));
772
1194
  console.log();
773
1195
  }
774
1196
  var TOP_INTEGRATIONS = [
@@ -802,48 +1224,48 @@ async function promptConnectIntegrations(apiKey) {
802
1224
  { value: "skip", label: "Skip for now", hint: "you can always run pica add later" }
803
1225
  ];
804
1226
  const message = first ? "Connect your first integration?" : "Connect another?";
805
- const choice = await p.select({ message, options });
806
- if (p.isCancel(choice) || choice === "skip") {
1227
+ const choice = await p2.select({ message, options });
1228
+ if (p2.isCancel(choice) || choice === "skip") {
807
1229
  break;
808
1230
  }
809
1231
  if (choice === "more") {
810
1232
  try {
811
1233
  await open2("https://app.picaos.com/connections");
812
- p.log.info("Opened Pica dashboard in browser.");
1234
+ p2.log.info("Opened Pica dashboard in browser.");
813
1235
  } catch {
814
- p.note("https://app.picaos.com/connections", "Open in browser");
1236
+ p2.note("https://app.picaos.com/connections", "Open in browser");
815
1237
  }
816
- p.log.info(`Connect from the dashboard, or use ${pc2.cyan("pica add <platform>")}`);
1238
+ p2.log.info(`Connect from the dashboard, or use ${pc3.cyan("pica add <platform>")}`);
817
1239
  break;
818
1240
  }
819
1241
  const platform = choice;
820
1242
  const integration = TOP_INTEGRATIONS.find((i) => i.value === platform);
821
1243
  const label = integration?.label ?? platform;
822
- p.log.info(`Opening browser to connect ${pc2.cyan(label)}...`);
1244
+ p2.log.info(`Opening browser to connect ${pc3.cyan(label)}...`);
823
1245
  try {
824
1246
  await openConnectionPage(platform);
825
1247
  } catch {
826
1248
  const url = getConnectionUrl(platform);
827
- p.log.warn("Could not open browser automatically.");
828
- p.note(url, "Open manually");
1249
+ p2.log.warn("Could not open browser automatically.");
1250
+ p2.note(url, "Open manually");
829
1251
  }
830
- const spinner4 = p.spinner();
831
- spinner4.start("Waiting for connection... (complete auth in browser)");
1252
+ const spinner6 = p2.spinner();
1253
+ spinner6.start("Waiting for connection... (complete auth in browser)");
832
1254
  try {
833
1255
  await api.waitForConnection(platform, 5 * 60 * 1e3, 5e3);
834
- spinner4.stop(`${label} connected!`);
835
- p.log.success(`${pc2.green("\u2713")} ${label} is now available to your AI agents`);
1256
+ spinner6.stop(`${label} connected!`);
1257
+ p2.log.success(`${pc3.green("\u2713")} ${label} is now available to your AI agents`);
836
1258
  connected.push(platform);
837
1259
  first = false;
838
1260
  } catch (error) {
839
- spinner4.stop("Connection timed out");
1261
+ spinner6.stop("Connection timed out");
840
1262
  if (error instanceof TimeoutError) {
841
- p.log.warn(`No worries. Connect later with: ${pc2.cyan(`pica add ${platform}`)}`);
1263
+ p2.log.warn(`No worries. Connect later with: ${pc3.cyan(`pica add ${platform}`)}`);
842
1264
  }
843
1265
  first = false;
844
1266
  }
845
1267
  if (TOP_INTEGRATIONS.every((i) => connected.includes(i.value))) {
846
- p.log.success("All top integrations connected!");
1268
+ p2.log.success("All top integrations connected!");
847
1269
  break;
848
1270
  }
849
1271
  }
@@ -862,23 +1284,23 @@ function updateConfigAgents(agentId) {
862
1284
  }
863
1285
 
864
1286
  // src/commands/connection.ts
865
- import * as p2 from "@clack/prompts";
866
- import pc3 from "picocolors";
1287
+ import * as p3 from "@clack/prompts";
1288
+ import pc4 from "picocolors";
867
1289
 
868
1290
  // src/lib/platforms.ts
869
1291
  function findPlatform(platforms, query) {
870
1292
  const normalizedQuery = query.toLowerCase().trim();
871
1293
  const exact = platforms.find(
872
- (p4) => p4.platform.toLowerCase() === normalizedQuery || p4.name.toLowerCase() === normalizedQuery
1294
+ (p6) => p6.platform.toLowerCase() === normalizedQuery || p6.name.toLowerCase() === normalizedQuery
873
1295
  );
874
1296
  if (exact) return exact;
875
1297
  return null;
876
1298
  }
877
1299
  function findSimilarPlatforms(platforms, query, limit = 3) {
878
1300
  const normalizedQuery = query.toLowerCase().trim();
879
- const scored = platforms.map((p4) => {
880
- const name = p4.name.toLowerCase();
881
- const slug = p4.platform.toLowerCase();
1301
+ const scored = platforms.map((p6) => {
1302
+ const name = p6.name.toLowerCase();
1303
+ const slug = p6.platform.toLowerCase();
882
1304
  let score = 0;
883
1305
  if (name.includes(normalizedQuery) || slug.includes(normalizedQuery)) {
884
1306
  score = 10;
@@ -887,7 +1309,7 @@ function findSimilarPlatforms(platforms, query, limit = 3) {
887
1309
  } else {
888
1310
  score = countMatchingChars(normalizedQuery, slug);
889
1311
  }
890
- return { platform: p4, score };
1312
+ return { platform: p6, score };
891
1313
  }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
892
1314
  return scored.map((item) => item.platform);
893
1315
  }
@@ -902,22 +1324,22 @@ function countMatchingChars(a, b) {
902
1324
 
903
1325
  // src/commands/connection.ts
904
1326
  async function connectionAddCommand(platformArg) {
905
- p2.intro(pc3.bgCyan(pc3.black(" Pica ")));
1327
+ p3.intro(pc4.bgCyan(pc4.black(" Pica ")));
906
1328
  const apiKey = getApiKey();
907
1329
  if (!apiKey) {
908
- p2.cancel("Not configured. Run `pica init` first.");
1330
+ p3.cancel("Not configured. Run `pica init` first.");
909
1331
  process.exit(1);
910
1332
  }
911
1333
  const api = new PicaApi(apiKey);
912
- const spinner4 = p2.spinner();
913
- spinner4.start("Loading platforms...");
1334
+ const spinner6 = p3.spinner();
1335
+ spinner6.start("Loading platforms...");
914
1336
  let platforms;
915
1337
  try {
916
1338
  platforms = await api.listPlatforms();
917
- spinner4.stop(`${platforms.length} platforms available`);
1339
+ spinner6.stop(`${platforms.length} platforms available`);
918
1340
  } catch (error) {
919
- spinner4.stop("Failed to load platforms");
920
- p2.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1341
+ spinner6.stop("Failed to load platforms");
1342
+ p3.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
921
1343
  process.exit(1);
922
1344
  }
923
1345
  let platform;
@@ -928,29 +1350,29 @@ async function connectionAddCommand(platformArg) {
928
1350
  } else {
929
1351
  const similar = findSimilarPlatforms(platforms, platformArg);
930
1352
  if (similar.length > 0) {
931
- p2.log.warn(`Unknown platform: ${platformArg}`);
932
- const suggestion = await p2.select({
1353
+ p3.log.warn(`Unknown platform: ${platformArg}`);
1354
+ const suggestion = await p3.select({
933
1355
  message: "Did you mean:",
934
1356
  options: [
935
1357
  ...similar.map((s) => ({ value: s.platform, label: `${s.name} (${s.platform})` })),
936
1358
  { value: "__other__", label: "None of these" }
937
1359
  ]
938
1360
  });
939
- if (p2.isCancel(suggestion) || suggestion === "__other__") {
940
- p2.note(`Run ${pc3.cyan("pica platforms")} to see all available platforms.`);
941
- p2.cancel("Connection cancelled.");
1361
+ if (p3.isCancel(suggestion) || suggestion === "__other__") {
1362
+ p3.note(`Run ${pc4.cyan("pica platforms")} to see all available platforms.`);
1363
+ p3.cancel("Connection cancelled.");
942
1364
  process.exit(0);
943
1365
  }
944
1366
  platform = suggestion;
945
1367
  } else {
946
- p2.cancel(`Unknown platform: ${platformArg}
1368
+ p3.cancel(`Unknown platform: ${platformArg}
947
1369
 
948
- Run ${pc3.cyan("pica platforms")} to see available platforms.`);
1370
+ Run ${pc4.cyan("pica platforms")} to see available platforms.`);
949
1371
  process.exit(1);
950
1372
  }
951
1373
  }
952
1374
  } else {
953
- const platformInput = await p2.text({
1375
+ const platformInput = await p3.text({
954
1376
  message: "Which platform do you want to connect?",
955
1377
  placeholder: "gmail, slack, hubspot...",
956
1378
  validate: (value) => {
@@ -958,51 +1380,51 @@ Run ${pc3.cyan("pica platforms")} to see available platforms.`);
958
1380
  return void 0;
959
1381
  }
960
1382
  });
961
- if (p2.isCancel(platformInput)) {
962
- p2.cancel("Connection cancelled.");
1383
+ if (p3.isCancel(platformInput)) {
1384
+ p3.cancel("Connection cancelled.");
963
1385
  process.exit(0);
964
1386
  }
965
1387
  const found = findPlatform(platforms, platformInput);
966
1388
  if (found) {
967
1389
  platform = found.platform;
968
1390
  } else {
969
- p2.cancel(`Unknown platform: ${platformInput}
1391
+ p3.cancel(`Unknown platform: ${platformInput}
970
1392
 
971
- Run ${pc3.cyan("pica platforms")} to see available platforms.`);
1393
+ Run ${pc4.cyan("pica platforms")} to see available platforms.`);
972
1394
  process.exit(1);
973
1395
  }
974
1396
  }
975
1397
  const url = getConnectionUrl(platform);
976
- p2.log.info(`Opening browser to connect ${pc3.cyan(platform)}...`);
977
- p2.note(pc3.dim(url), "URL");
1398
+ p3.log.info(`Opening browser to connect ${pc4.cyan(platform)}...`);
1399
+ p3.note(pc4.dim(url), "URL");
978
1400
  try {
979
1401
  await openConnectionPage(platform);
980
1402
  } catch {
981
- p2.log.warn("Could not open browser automatically.");
982
- p2.note(`Open this URL manually:
1403
+ p3.log.warn("Could not open browser automatically.");
1404
+ p3.note(`Open this URL manually:
983
1405
  ${url}`);
984
1406
  }
985
- const pollSpinner = p2.spinner();
1407
+ const pollSpinner = p3.spinner();
986
1408
  pollSpinner.start("Waiting for connection... (complete auth in browser)");
987
1409
  try {
988
1410
  const connection2 = await api.waitForConnection(platform, 5 * 60 * 1e3, 5e3);
989
1411
  pollSpinner.stop(`${platform} connected!`);
990
- p2.log.success(`${pc3.green("\u2713")} ${connection2.platform} is now available to your AI agents.`);
991
- p2.outro("Connection complete!");
1412
+ p3.log.success(`${pc4.green("\u2713")} ${connection2.platform} is now available to your AI agents.`);
1413
+ p3.outro("Connection complete!");
992
1414
  } catch (error) {
993
1415
  pollSpinner.stop("Connection timed out");
994
1416
  if (error instanceof TimeoutError) {
995
- p2.note(
1417
+ p3.note(
996
1418
  `Possible issues:
997
1419
  - OAuth flow was not completed in the browser
998
1420
  - Browser popup was blocked
999
1421
  - Wrong account selected
1000
1422
 
1001
- Try again with: ${pc3.cyan(`pica connection add ${platform}`)}`,
1423
+ Try again with: ${pc4.cyan(`pica connection add ${platform}`)}`,
1002
1424
  "Timed Out"
1003
1425
  );
1004
1426
  } else {
1005
- p2.log.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1427
+ p3.log.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1006
1428
  }
1007
1429
  process.exit(1);
1008
1430
  }
@@ -1010,20 +1432,20 @@ Try again with: ${pc3.cyan(`pica connection add ${platform}`)}`,
1010
1432
  async function connectionListCommand() {
1011
1433
  const apiKey = getApiKey();
1012
1434
  if (!apiKey) {
1013
- p2.cancel("Not configured. Run `pica init` first.");
1435
+ p3.cancel("Not configured. Run `pica init` first.");
1014
1436
  process.exit(1);
1015
1437
  }
1016
1438
  const api = new PicaApi(apiKey);
1017
- const spinner4 = p2.spinner();
1018
- spinner4.start("Loading connections...");
1439
+ const spinner6 = p3.spinner();
1440
+ spinner6.start("Loading connections...");
1019
1441
  try {
1020
1442
  const connections = await api.listConnections();
1021
- spinner4.stop(`${connections.length} connection${connections.length === 1 ? "" : "s"} found`);
1443
+ spinner6.stop(`${connections.length} connection${connections.length === 1 ? "" : "s"} found`);
1022
1444
  if (connections.length === 0) {
1023
- p2.note(
1445
+ p3.note(
1024
1446
  `No connections yet.
1025
1447
 
1026
- Add one with: ${pc3.cyan("pica connection add gmail")}`,
1448
+ Add one with: ${pc4.cyan("pica connection add gmail")}`,
1027
1449
  "No Connections"
1028
1450
  );
1029
1451
  return;
@@ -1040,46 +1462,46 @@ Add one with: ${pc3.cyan("pica connection add gmail")}`,
1040
1462
  { key: "status", label: "" },
1041
1463
  { key: "platform", label: "Platform" },
1042
1464
  { key: "state", label: "Status" },
1043
- { key: "key", label: "Connection Key", color: pc3.dim }
1465
+ { key: "key", label: "Connection Key", color: pc4.dim }
1044
1466
  ],
1045
1467
  rows
1046
1468
  );
1047
1469
  console.log();
1048
- p2.note(`Add more with: ${pc3.cyan("pica connection add <platform>")}`, "Tip");
1470
+ p3.note(`Add more with: ${pc4.cyan("pica connection add <platform>")}`, "Tip");
1049
1471
  } catch (error) {
1050
- spinner4.stop("Failed to load connections");
1051
- p2.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1472
+ spinner6.stop("Failed to load connections");
1473
+ p3.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1052
1474
  process.exit(1);
1053
1475
  }
1054
1476
  }
1055
1477
  function getStatusIndicator(state) {
1056
1478
  switch (state) {
1057
1479
  case "operational":
1058
- return pc3.green("\u25CF");
1480
+ return pc4.green("\u25CF");
1059
1481
  case "degraded":
1060
- return pc3.yellow("\u25CF");
1482
+ return pc4.yellow("\u25CF");
1061
1483
  case "failed":
1062
- return pc3.red("\u25CF");
1484
+ return pc4.red("\u25CF");
1063
1485
  default:
1064
- return pc3.dim("\u25CB");
1486
+ return pc4.dim("\u25CB");
1065
1487
  }
1066
1488
  }
1067
1489
 
1068
1490
  // src/commands/platforms.ts
1069
- import * as p3 from "@clack/prompts";
1070
- import pc4 from "picocolors";
1491
+ import * as p4 from "@clack/prompts";
1492
+ import pc5 from "picocolors";
1071
1493
  async function platformsCommand(options) {
1072
1494
  const apiKey = getApiKey();
1073
1495
  if (!apiKey) {
1074
- p3.cancel("Not configured. Run `pica init` first.");
1496
+ p4.cancel("Not configured. Run `pica init` first.");
1075
1497
  process.exit(1);
1076
1498
  }
1077
1499
  const api = new PicaApi(apiKey);
1078
- const spinner4 = p3.spinner();
1079
- spinner4.start("Loading platforms...");
1500
+ const spinner6 = p4.spinner();
1501
+ spinner6.start("Loading platforms...");
1080
1502
  try {
1081
1503
  const platforms = await api.listPlatforms();
1082
- spinner4.stop(`${platforms.length} platforms available`);
1504
+ spinner6.stop(`${platforms.length} platforms available`);
1083
1505
  if (options.json) {
1084
1506
  console.log(JSON.stringify(platforms, null, 2));
1085
1507
  return;
@@ -1097,7 +1519,7 @@ async function platformsCommand(options) {
1097
1519
  const categoryPlatforms = byCategory.get(options.category);
1098
1520
  if (!categoryPlatforms) {
1099
1521
  const categories = [...byCategory.keys()].sort();
1100
- p3.note(`Available categories:
1522
+ p4.note(`Available categories:
1101
1523
  ${categories.join(", ")}`, "Unknown Category");
1102
1524
  process.exit(1);
1103
1525
  }
@@ -1129,13 +1551,247 @@ async function platformsCommand(options) {
1129
1551
  );
1130
1552
  }
1131
1553
  console.log();
1132
- p3.note(`Connect with: ${pc4.cyan("pica connection add <platform>")}`, "Tip");
1554
+ p4.note(`Connect with: ${pc5.cyan("pica connection add <platform>")}`, "Tip");
1133
1555
  } catch (error) {
1134
- spinner4.stop("Failed to load platforms");
1135
- p3.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1556
+ spinner6.stop("Failed to load platforms");
1557
+ p4.cancel(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
1558
+ process.exit(1);
1559
+ }
1560
+ }
1561
+
1562
+ // src/commands/actions.ts
1563
+ import * as p5 from "@clack/prompts";
1564
+ import pc6 from "picocolors";
1565
+ function getConfig() {
1566
+ const apiKey = getApiKey();
1567
+ if (!apiKey) {
1568
+ p5.cancel("Not configured. Run `pica init` first.");
1569
+ process.exit(1);
1570
+ }
1571
+ const ac = getAccessControl();
1572
+ const permissions = ac.permissions || "admin";
1573
+ const connectionKeys = ac.connectionKeys || ["*"];
1574
+ const actionIds = ac.actionIds || ["*"];
1575
+ const knowledgeAgent = ac.knowledgeAgent || false;
1576
+ return { apiKey, permissions, connectionKeys, actionIds, knowledgeAgent };
1577
+ }
1578
+ async function actionsSearchCommand(platform, query, options) {
1579
+ p5.intro(pc6.bgCyan(pc6.black(" Pica ")));
1580
+ const { apiKey, permissions, actionIds, knowledgeAgent } = getConfig();
1581
+ const api = new PicaApi(apiKey);
1582
+ const spinner6 = p5.spinner();
1583
+ spinner6.start(`Searching actions on ${pc6.cyan(platform)} for "${query}"...`);
1584
+ try {
1585
+ const agentType = knowledgeAgent ? "knowledge" : options.type;
1586
+ let actions2 = await api.searchActions(platform, query, agentType);
1587
+ actions2 = filterByPermissions(actions2, permissions);
1588
+ actions2 = actions2.filter((a) => isActionAllowed(a.systemId, actionIds));
1589
+ const cleanedActions = actions2.map((action) => ({
1590
+ actionId: action.systemId,
1591
+ title: action.title,
1592
+ method: action.method,
1593
+ path: action.path
1594
+ }));
1595
+ if (cleanedActions.length === 0) {
1596
+ spinner6.stop("No actions found");
1597
+ p5.note(
1598
+ `No actions found for platform '${platform}' matching query '${query}'.
1599
+
1600
+ Suggestions:
1601
+ - Try a more general query (e.g., 'list', 'get', 'search', 'create')
1602
+ - Verify the platform name is correct
1603
+ - Check available platforms with ${pc6.cyan("pica platforms")}
1604
+
1605
+ Examples of good queries:
1606
+ - "search contacts"
1607
+ - "send email"
1608
+ - "create customer"
1609
+ - "list orders"`,
1610
+ "No Results"
1611
+ );
1612
+ return;
1613
+ }
1614
+ spinner6.stop(
1615
+ `Found ${cleanedActions.length} action(s) for '${platform}' matching '${query}'`
1616
+ );
1617
+ console.log();
1618
+ const rows = cleanedActions.map((a) => ({
1619
+ method: colorMethod(a.method),
1620
+ title: a.title,
1621
+ actionId: a.actionId,
1622
+ path: a.path
1623
+ }));
1624
+ printTable(
1625
+ [
1626
+ { key: "method", label: "Method" },
1627
+ { key: "title", label: "Title" },
1628
+ { key: "actionId", label: "Action ID", color: pc6.dim },
1629
+ { key: "path", label: "Path", color: pc6.dim }
1630
+ ],
1631
+ rows
1632
+ );
1633
+ console.log();
1634
+ p5.note(
1635
+ `Get details: ${pc6.cyan(`pica actions knowledge ${platform} <actionId>`)}
1636
+ Execute: ${pc6.cyan(`pica actions execute ${platform} <actionId> <connectionKey>`)}`,
1637
+ "Next Steps"
1638
+ );
1639
+ } catch (error) {
1640
+ spinner6.stop("Search failed");
1641
+ p5.cancel(
1642
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
1643
+ );
1644
+ process.exit(1);
1645
+ }
1646
+ }
1647
+ async function actionsKnowledgeCommand(platform, actionId) {
1648
+ p5.intro(pc6.bgCyan(pc6.black(" Pica ")));
1649
+ const { apiKey, actionIds, connectionKeys } = getConfig();
1650
+ const api = new PicaApi(apiKey);
1651
+ if (!isActionAllowed(actionId, actionIds)) {
1652
+ p5.cancel(`Action "${actionId}" is not in the allowed action list.`);
1653
+ process.exit(1);
1654
+ }
1655
+ if (!connectionKeys.includes("*")) {
1656
+ const spinner7 = p5.spinner();
1657
+ spinner7.start("Checking connections...");
1658
+ try {
1659
+ const connections = await api.listConnections();
1660
+ const connectedPlatforms = connections.map((c) => c.platform);
1661
+ if (!connectedPlatforms.includes(platform)) {
1662
+ spinner7.stop("Platform not connected");
1663
+ p5.cancel(`Platform "${platform}" has no allowed connections.`);
1664
+ process.exit(1);
1665
+ }
1666
+ spinner7.stop("Connection verified");
1667
+ } catch (error) {
1668
+ spinner7.stop("Failed to check connections");
1669
+ p5.cancel(
1670
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
1671
+ );
1672
+ process.exit(1);
1673
+ }
1674
+ }
1675
+ const spinner6 = p5.spinner();
1676
+ spinner6.start(`Loading knowledge for action ${pc6.dim(actionId)}...`);
1677
+ try {
1678
+ const { knowledge, method } = await api.getActionKnowledge(actionId);
1679
+ const knowledgeWithGuidance = buildActionKnowledgeWithGuidance(
1680
+ knowledge,
1681
+ method,
1682
+ platform,
1683
+ actionId
1684
+ );
1685
+ spinner6.stop("Knowledge loaded");
1686
+ console.log();
1687
+ console.log(knowledgeWithGuidance);
1688
+ console.log();
1689
+ p5.note(
1690
+ `Execute: ${pc6.cyan(`pica actions execute ${platform} ${actionId} <connectionKey>`)}`,
1691
+ "Next Step"
1692
+ );
1693
+ } catch (error) {
1694
+ spinner6.stop("Failed to load knowledge");
1695
+ p5.cancel(
1696
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
1697
+ );
1698
+ process.exit(1);
1699
+ }
1700
+ }
1701
+ async function actionsExecuteCommand(platform, actionId, connectionKey, options) {
1702
+ p5.intro(pc6.bgCyan(pc6.black(" Pica ")));
1703
+ const { apiKey, permissions, actionIds, connectionKeys, knowledgeAgent } = getConfig();
1704
+ if (knowledgeAgent) {
1705
+ p5.cancel(
1706
+ `Action execution is disabled (knowledge-only mode).
1707
+ Configure with: ${pc6.cyan("pica config")}`
1708
+ );
1709
+ process.exit(1);
1710
+ }
1711
+ if (!isActionAllowed(actionId, actionIds)) {
1712
+ p5.cancel(`Action "${actionId}" is not in the allowed action list.`);
1713
+ process.exit(1);
1714
+ }
1715
+ if (!connectionKeys.includes("*") && !connectionKeys.includes(connectionKey)) {
1716
+ p5.cancel(`Connection key "${connectionKey}" is not allowed.`);
1717
+ process.exit(1);
1718
+ }
1719
+ const api = new PicaApi(apiKey);
1720
+ const spinner6 = p5.spinner();
1721
+ spinner6.start("Loading action details...");
1722
+ try {
1723
+ const actionDetails = await api.getActionDetails(actionId);
1724
+ if (!isMethodAllowed(actionDetails.method, permissions)) {
1725
+ spinner6.stop("Permission denied");
1726
+ p5.cancel(
1727
+ `Method "${actionDetails.method}" is not allowed under "${permissions}" permission level.`
1728
+ );
1729
+ process.exit(1);
1730
+ }
1731
+ spinner6.stop(`Action: ${actionDetails.title} [${actionDetails.method}]`);
1732
+ const data = options.data ? parseJsonArg(options.data, "--data") : void 0;
1733
+ const pathVariables = options.pathVars ? parseJsonArg(options.pathVars, "--path-vars") : void 0;
1734
+ const queryParams = options.queryParams ? parseJsonArg(options.queryParams, "--query-params") : void 0;
1735
+ const headers = options.headers ? parseJsonArg(options.headers, "--headers") : void 0;
1736
+ const execSpinner = p5.spinner();
1737
+ execSpinner.start("Executing action...");
1738
+ const result = await api.executePassthroughRequest(
1739
+ {
1740
+ platform,
1741
+ actionId,
1742
+ connectionKey,
1743
+ data,
1744
+ pathVariables,
1745
+ queryParams,
1746
+ headers,
1747
+ isFormData: options.formData,
1748
+ isFormUrlEncoded: options.formUrlEncoded
1749
+ },
1750
+ actionDetails
1751
+ );
1752
+ execSpinner.stop("Action executed successfully");
1753
+ console.log();
1754
+ console.log(pc6.dim("Request:"));
1755
+ console.log(
1756
+ pc6.dim(
1757
+ ` ${result.requestConfig.method} ${result.requestConfig.url}`
1758
+ )
1759
+ );
1760
+ console.log();
1761
+ console.log(pc6.bold("Response:"));
1762
+ console.log(JSON.stringify(result.responseData, null, 2));
1763
+ } catch (error) {
1764
+ spinner6.stop("Execution failed");
1765
+ p5.cancel(
1766
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
1767
+ );
1768
+ process.exit(1);
1769
+ }
1770
+ }
1771
+ function parseJsonArg(value, argName) {
1772
+ try {
1773
+ return JSON.parse(value);
1774
+ } catch {
1775
+ p5.cancel(`Invalid JSON for ${argName}: ${value}`);
1136
1776
  process.exit(1);
1137
1777
  }
1138
1778
  }
1779
+ function colorMethod(method) {
1780
+ switch (method.toUpperCase()) {
1781
+ case "GET":
1782
+ return pc6.green(method);
1783
+ case "POST":
1784
+ return pc6.yellow(method);
1785
+ case "PUT":
1786
+ return pc6.blue(method);
1787
+ case "PATCH":
1788
+ return pc6.magenta(method);
1789
+ case "DELETE":
1790
+ return pc6.red(method);
1791
+ default:
1792
+ return method;
1793
+ }
1794
+ }
1139
1795
 
1140
1796
  // src/index.ts
1141
1797
  var require2 = createRequire(import.meta.url);
@@ -1145,6 +1801,9 @@ program.name("pica").description("CLI for managing Pica").version(version);
1145
1801
  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) => {
1146
1802
  await initCommand(options);
1147
1803
  });
1804
+ program.command("config").description("Configure MCP access control (permissions, connections, actions)").action(async () => {
1805
+ await configCommand();
1806
+ });
1148
1807
  var connection = program.command("connection").description("Manage connections");
1149
1808
  connection.command("add [platform]").alias("a").description("Add a new connection").action(async (platform) => {
1150
1809
  await connectionAddCommand(platform);
@@ -1155,6 +1814,23 @@ connection.command("list").alias("ls").description("List your connections").acti
1155
1814
  program.command("platforms").alias("p").description("List available platforms").option("-c, --category <category>", "Filter by category").option("--json", "Output as JSON").action(async (options) => {
1156
1815
  await platformsCommand(options);
1157
1816
  });
1817
+ var actions = program.command("actions").alias("a").description("Search, explore, and execute platform actions");
1818
+ actions.command("search <platform> <query>").description("Search for actions on a platform").option("-t, --type <type>", "Agent type: execute or knowledge (default: knowledge)").action(async (platform, query, options) => {
1819
+ await actionsSearchCommand(platform, query, options);
1820
+ });
1821
+ actions.command("knowledge <platform> <actionId>").alias("k").description("Get detailed documentation for an action").action(async (platform, actionId) => {
1822
+ await actionsKnowledgeCommand(platform, actionId);
1823
+ });
1824
+ actions.command("execute <platform> <actionId> <connectionKey>").alias("x").description("Execute an action on a connected platform").option("-d, --data <json>", "Request body as JSON").option("--path-vars <json>", "Path variables as JSON").option("--query-params <json>", "Query parameters as JSON").option("--headers <json>", "Additional headers as JSON").option("--form-data", "Send as multipart/form-data").option("--form-url-encoded", "Send as application/x-www-form-urlencoded").action(async (platform, actionId, connectionKey, options) => {
1825
+ await actionsExecuteCommand(platform, actionId, connectionKey, {
1826
+ data: options.data,
1827
+ pathVars: options.pathVars,
1828
+ queryParams: options.queryParams,
1829
+ headers: options.headers,
1830
+ formData: options.formData,
1831
+ formUrlEncoded: options.formUrlEncoded
1832
+ });
1833
+ });
1158
1834
  program.command("add [platform]").description("Shortcut for: connection add").action(async (platform) => {
1159
1835
  await connectionAddCommand(platform);
1160
1836
  });