@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.
- package/README.md +166 -95
- package/dist/index.js +871 -195
- 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
|
|
9
|
-
import
|
|
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(
|
|
51
|
-
if (
|
|
52
|
-
return path2.join(os2.homedir(),
|
|
76
|
+
function expandPath(p6) {
|
|
77
|
+
if (p6.startsWith("~/")) {
|
|
78
|
+
return path2.join(os2.homedir(), p6.slice(2));
|
|
53
79
|
}
|
|
54
|
-
return
|
|
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
|
|
251
|
-
throw new ApiError(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
|
|
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
|
|
744
|
+
return pc2.dim(padded);
|
|
344
745
|
}).join(gap);
|
|
345
746
|
console.log(`${indent}${header}`);
|
|
346
|
-
const separator = columns.map((_, i) =>
|
|
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
|
-
|
|
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(` ${
|
|
382
|
-
console.log(` ${
|
|
383
|
-
console.log(` ${
|
|
384
|
-
console.log(` ${
|
|
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 ?
|
|
395
|
-
project: s.projectMcp === null ?
|
|
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(` ${
|
|
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
|
|
843
|
+
const action = await p2.select({
|
|
429
844
|
message: "What would you like to do?",
|
|
430
845
|
options: actionOptions
|
|
431
846
|
});
|
|
432
|
-
if (
|
|
433
|
-
|
|
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
|
-
|
|
453
|
-
${
|
|
454
|
-
const openBrowser = await
|
|
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 (
|
|
459
|
-
|
|
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
|
|
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 (
|
|
477
|
-
|
|
894
|
+
if (p2.isCancel(newKey)) {
|
|
895
|
+
p2.cancel("Cancelled.");
|
|
478
896
|
process.exit(0);
|
|
479
897
|
}
|
|
480
|
-
const
|
|
481
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
928
|
+
p2.log.success(`Updated MCP configs: ${reinstalled.join(", ")}`);
|
|
509
929
|
}
|
|
510
|
-
|
|
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
|
|
936
|
+
const confirm3 = await p2.confirm({
|
|
516
937
|
message: `Install Pica MCP to ${agent.name}?`,
|
|
517
938
|
initialValue: true
|
|
518
939
|
});
|
|
519
|
-
if (
|
|
520
|
-
|
|
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
|
-
|
|
526
|
-
|
|
946
|
+
p2.log.success(`${agent.name}: MCP installed`);
|
|
947
|
+
p2.outro("Done.");
|
|
527
948
|
return;
|
|
528
949
|
}
|
|
529
|
-
const selected = await
|
|
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 (
|
|
537
|
-
|
|
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
|
-
|
|
965
|
+
p2.log.success(`${s.agent.name}: MCP installed`);
|
|
545
966
|
}
|
|
546
|
-
|
|
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
|
|
973
|
+
const confirm3 = await p2.confirm({
|
|
552
974
|
message: `Install project-level MCP for ${agent.name}?`,
|
|
553
975
|
initialValue: true
|
|
554
976
|
});
|
|
555
|
-
if (
|
|
556
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
988
|
+
p2.outro("Done.");
|
|
567
989
|
return;
|
|
568
990
|
}
|
|
569
|
-
const selected = await
|
|
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 (
|
|
577
|
-
|
|
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
|
-
|
|
1006
|
+
p2.log.success(`${s.agent.name}: ${configPath} created`);
|
|
585
1007
|
}
|
|
586
|
-
|
|
587
|
-
|
|
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
|
-
|
|
1012
|
+
p2.outro("Done.");
|
|
591
1013
|
}
|
|
592
1014
|
async function freshSetup(options) {
|
|
593
|
-
|
|
594
|
-
${
|
|
595
|
-
const openBrowser = await
|
|
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 (
|
|
600
|
-
|
|
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
|
|
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 (
|
|
618
|
-
|
|
1039
|
+
if (p2.isCancel(apiKey)) {
|
|
1040
|
+
p2.cancel("Setup cancelled.");
|
|
619
1041
|
process.exit(0);
|
|
620
1042
|
}
|
|
621
|
-
const
|
|
622
|
-
|
|
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
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
652
|
-
|
|
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
|
|
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 (
|
|
679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1124
|
+
p2.log.success(`${agent.name}: ${configPath} ${status}`);
|
|
703
1125
|
}
|
|
704
1126
|
if (nonProjectAgents.length > 0) {
|
|
705
|
-
|
|
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
|
-
|
|
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}: ${
|
|
720
|
-
let summary = `Config saved to: ${
|
|
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}: ${
|
|
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 +=
|
|
733
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
752
|
-
`Config saved to: ${
|
|
1173
|
+
p2.note(
|
|
1174
|
+
`Config saved to: ${pc3.dim(getConfigPath())}`,
|
|
753
1175
|
"Setup Complete"
|
|
754
1176
|
);
|
|
755
1177
|
await promptConnectIntegrations(apiKey);
|
|
756
|
-
|
|
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(
|
|
761
|
-
console.log(
|
|
762
|
-
console.log(
|
|
763
|
-
console.log(
|
|
764
|
-
console.log(
|
|
765
|
-
console.log(
|
|
766
|
-
console.log(
|
|
767
|
-
console.log(
|
|
768
|
-
console.log(
|
|
769
|
-
console.log(
|
|
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(
|
|
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
|
|
806
|
-
if (
|
|
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
|
-
|
|
1234
|
+
p2.log.info("Opened Pica dashboard in browser.");
|
|
813
1235
|
} catch {
|
|
814
|
-
|
|
1236
|
+
p2.note("https://app.picaos.com/connections", "Open in browser");
|
|
815
1237
|
}
|
|
816
|
-
|
|
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
|
-
|
|
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
|
-
|
|
828
|
-
|
|
1249
|
+
p2.log.warn("Could not open browser automatically.");
|
|
1250
|
+
p2.note(url, "Open manually");
|
|
829
1251
|
}
|
|
830
|
-
const
|
|
831
|
-
|
|
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
|
-
|
|
835
|
-
|
|
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
|
-
|
|
1261
|
+
spinner6.stop("Connection timed out");
|
|
840
1262
|
if (error instanceof TimeoutError) {
|
|
841
|
-
|
|
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
|
-
|
|
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
|
|
866
|
-
import
|
|
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
|
-
(
|
|
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((
|
|
880
|
-
const name =
|
|
881
|
-
const slug =
|
|
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:
|
|
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
|
-
|
|
1327
|
+
p3.intro(pc4.bgCyan(pc4.black(" Pica ")));
|
|
906
1328
|
const apiKey = getApiKey();
|
|
907
1329
|
if (!apiKey) {
|
|
908
|
-
|
|
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
|
|
913
|
-
|
|
1334
|
+
const spinner6 = p3.spinner();
|
|
1335
|
+
spinner6.start("Loading platforms...");
|
|
914
1336
|
let platforms;
|
|
915
1337
|
try {
|
|
916
1338
|
platforms = await api.listPlatforms();
|
|
917
|
-
|
|
1339
|
+
spinner6.stop(`${platforms.length} platforms available`);
|
|
918
1340
|
} catch (error) {
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
932
|
-
const suggestion = await
|
|
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 (
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
1368
|
+
p3.cancel(`Unknown platform: ${platformArg}
|
|
947
1369
|
|
|
948
|
-
Run ${
|
|
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
|
|
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 (
|
|
962
|
-
|
|
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
|
-
|
|
1391
|
+
p3.cancel(`Unknown platform: ${platformInput}
|
|
970
1392
|
|
|
971
|
-
Run ${
|
|
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
|
-
|
|
977
|
-
|
|
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
|
-
|
|
982
|
-
|
|
1403
|
+
p3.log.warn("Could not open browser automatically.");
|
|
1404
|
+
p3.note(`Open this URL manually:
|
|
983
1405
|
${url}`);
|
|
984
1406
|
}
|
|
985
|
-
const pollSpinner =
|
|
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
|
-
|
|
991
|
-
|
|
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
|
-
|
|
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: ${
|
|
1423
|
+
Try again with: ${pc4.cyan(`pica connection add ${platform}`)}`,
|
|
1002
1424
|
"Timed Out"
|
|
1003
1425
|
);
|
|
1004
1426
|
} else {
|
|
1005
|
-
|
|
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
|
-
|
|
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
|
|
1018
|
-
|
|
1439
|
+
const spinner6 = p3.spinner();
|
|
1440
|
+
spinner6.start("Loading connections...");
|
|
1019
1441
|
try {
|
|
1020
1442
|
const connections = await api.listConnections();
|
|
1021
|
-
|
|
1443
|
+
spinner6.stop(`${connections.length} connection${connections.length === 1 ? "" : "s"} found`);
|
|
1022
1444
|
if (connections.length === 0) {
|
|
1023
|
-
|
|
1445
|
+
p3.note(
|
|
1024
1446
|
`No connections yet.
|
|
1025
1447
|
|
|
1026
|
-
Add one with: ${
|
|
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:
|
|
1465
|
+
{ key: "key", label: "Connection Key", color: pc4.dim }
|
|
1044
1466
|
],
|
|
1045
1467
|
rows
|
|
1046
1468
|
);
|
|
1047
1469
|
console.log();
|
|
1048
|
-
|
|
1470
|
+
p3.note(`Add more with: ${pc4.cyan("pica connection add <platform>")}`, "Tip");
|
|
1049
1471
|
} catch (error) {
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
|
1480
|
+
return pc4.green("\u25CF");
|
|
1059
1481
|
case "degraded":
|
|
1060
|
-
return
|
|
1482
|
+
return pc4.yellow("\u25CF");
|
|
1061
1483
|
case "failed":
|
|
1062
|
-
return
|
|
1484
|
+
return pc4.red("\u25CF");
|
|
1063
1485
|
default:
|
|
1064
|
-
return
|
|
1486
|
+
return pc4.dim("\u25CB");
|
|
1065
1487
|
}
|
|
1066
1488
|
}
|
|
1067
1489
|
|
|
1068
1490
|
// src/commands/platforms.ts
|
|
1069
|
-
import * as
|
|
1070
|
-
import
|
|
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
|
-
|
|
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
|
|
1079
|
-
|
|
1500
|
+
const spinner6 = p4.spinner();
|
|
1501
|
+
spinner6.start("Loading platforms...");
|
|
1080
1502
|
try {
|
|
1081
1503
|
const platforms = await api.listPlatforms();
|
|
1082
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1554
|
+
p4.note(`Connect with: ${pc5.cyan("pica connection add <platform>")}`, "Tip");
|
|
1133
1555
|
} catch (error) {
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
});
|