@picahq/cli 0.1.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/.claude/settings.local.json +11 -0
- package/.github/workflows/publish.yml +29 -0
- package/README.md +148 -0
- package/SKILL.md +199 -0
- package/bin/cli.js +3 -0
- package/package.json +29 -0
- package/src/commands/actions.ts +385 -0
- package/src/commands/connection.ts +196 -0
- package/src/commands/init.ts +548 -0
- package/src/commands/platforms.ts +92 -0
- package/src/index.ts +140 -0
- package/src/lib/actions.ts +59 -0
- package/src/lib/agents.ts +191 -0
- package/src/lib/api.ts +191 -0
- package/src/lib/browser.ts +20 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/platforms.ts +73 -0
- package/src/lib/table.ts +60 -0
- package/src/lib/types.ts +89 -0
- package/test/all-emails.json +3479 -0
- package/test/fetch-emails.ts +82 -0
- package/tsconfig.json +16 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { getApiKey } from '../lib/config.js';
|
|
4
|
+
import { PicaApi } from '../lib/api.js';
|
|
5
|
+
import { extractPathVariables, resolveTemplateVariables } from '../lib/actions.js';
|
|
6
|
+
import { printTable } from '../lib/table.js';
|
|
7
|
+
import type { PlatformAction, ActionKnowledge } from '../lib/types.js';
|
|
8
|
+
|
|
9
|
+
function getApi(): PicaApi {
|
|
10
|
+
const apiKey = getApiKey();
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
p.cancel('Not configured. Run `pica init` first.');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
return new PicaApi(apiKey);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function colorMethod(method: string): string {
|
|
19
|
+
const m = method.toUpperCase();
|
|
20
|
+
switch (m) {
|
|
21
|
+
case 'GET': return pc.green(m);
|
|
22
|
+
case 'POST': return pc.yellow(m);
|
|
23
|
+
case 'PUT': return pc.blue(m);
|
|
24
|
+
case 'PATCH': return pc.cyan(m);
|
|
25
|
+
case 'DELETE': return pc.red(m);
|
|
26
|
+
default: return pc.dim(m);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function padMethod(method: string): string {
|
|
31
|
+
return method.toUpperCase().padEnd(7);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Search ---
|
|
35
|
+
|
|
36
|
+
export async function actionsSearchCommand(
|
|
37
|
+
platform: string,
|
|
38
|
+
query?: string,
|
|
39
|
+
options: { json?: boolean; limit?: string } = {}
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const api = getApi();
|
|
42
|
+
|
|
43
|
+
if (!query) {
|
|
44
|
+
const input = await p.text({
|
|
45
|
+
message: `Search actions on ${pc.cyan(platform)}:`,
|
|
46
|
+
placeholder: 'send email, create contact, list orders...',
|
|
47
|
+
});
|
|
48
|
+
if (p.isCancel(input)) {
|
|
49
|
+
p.cancel('Cancelled.');
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
query = input;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const spinner = p.spinner();
|
|
56
|
+
spinner.start(`Searching ${platform} actions...`);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const limit = options.limit ? parseInt(options.limit, 10) : 10;
|
|
60
|
+
const actions = await api.searchActions(platform, query, limit);
|
|
61
|
+
spinner.stop(`${actions.length} action${actions.length === 1 ? '' : 's'} found`);
|
|
62
|
+
|
|
63
|
+
if (options.json) {
|
|
64
|
+
console.log(JSON.stringify(actions, null, 2));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (actions.length === 0) {
|
|
69
|
+
p.note(`No actions found for "${query}" on ${platform}.`, 'No Results');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log();
|
|
74
|
+
|
|
75
|
+
const rows = actions.map(action => ({
|
|
76
|
+
method: colorMethod(padMethod(action.method)),
|
|
77
|
+
path: action.path,
|
|
78
|
+
title: action.title,
|
|
79
|
+
id: action._id,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
printTable(
|
|
83
|
+
[
|
|
84
|
+
{ key: 'method', label: 'Method' },
|
|
85
|
+
{ key: 'title', label: 'Title' },
|
|
86
|
+
{ key: 'path', label: 'Path', color: pc.dim },
|
|
87
|
+
{ key: 'id', label: 'Action ID', color: pc.dim },
|
|
88
|
+
],
|
|
89
|
+
rows
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
console.log();
|
|
93
|
+
p.note(
|
|
94
|
+
`Get docs: ${pc.cyan('pica actions knowledge <actionId>')}\n` +
|
|
95
|
+
`Execute: ${pc.cyan('pica actions execute <actionId>')}`,
|
|
96
|
+
'Next Steps'
|
|
97
|
+
);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
spinner.stop('Search failed');
|
|
100
|
+
p.cancel(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Knowledge ---
|
|
106
|
+
|
|
107
|
+
export async function actionsKnowledgeCommand(
|
|
108
|
+
actionId: string,
|
|
109
|
+
options: { json?: boolean; full?: boolean } = {}
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const api = getApi();
|
|
112
|
+
|
|
113
|
+
const spinner = p.spinner();
|
|
114
|
+
spinner.start('Loading action knowledge...');
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const knowledge = await api.getActionKnowledge(actionId);
|
|
118
|
+
spinner.stop('Action knowledge loaded');
|
|
119
|
+
|
|
120
|
+
if (!knowledge) {
|
|
121
|
+
p.cancel(`No knowledge found for action: ${actionId}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (options.json) {
|
|
126
|
+
console.log(JSON.stringify(knowledge, null, 2));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
printKnowledge(knowledge, options.full);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
spinner.stop('Failed to load knowledge');
|
|
133
|
+
p.cancel(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function printKnowledge(k: ActionKnowledge, full?: boolean): void {
|
|
139
|
+
console.log();
|
|
140
|
+
console.log(pc.bold(` ${k.title}`));
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(` Platform: ${pc.cyan(k.connectionPlatform)}`);
|
|
143
|
+
console.log(` Method: ${colorMethod(k.method)}`);
|
|
144
|
+
console.log(` Path: ${k.path}`);
|
|
145
|
+
console.log(` Base URL: ${pc.dim(k.baseUrl)}`);
|
|
146
|
+
|
|
147
|
+
if (k.tags?.length) {
|
|
148
|
+
console.log(` Tags: ${k.tags.map(t => pc.dim(t)).join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const pathVars = extractPathVariables(k.path);
|
|
152
|
+
if (pathVars.length > 0) {
|
|
153
|
+
console.log(` Path Vars: ${pathVars.map(v => pc.yellow(`{{${v}}}`)).join(', ')}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(` Active: ${k.active ? pc.green('yes') : pc.red('no')}`);
|
|
157
|
+
console.log(` ID: ${pc.dim(k._id)}`);
|
|
158
|
+
|
|
159
|
+
if (k.knowledge) {
|
|
160
|
+
console.log();
|
|
161
|
+
console.log(pc.bold(' API Documentation'));
|
|
162
|
+
console.log(pc.dim(' ' + '─'.repeat(40)));
|
|
163
|
+
console.log();
|
|
164
|
+
|
|
165
|
+
const lines = k.knowledge.split('\n');
|
|
166
|
+
const displayLines = full ? lines : lines.slice(0, 50);
|
|
167
|
+
|
|
168
|
+
for (const line of displayLines) {
|
|
169
|
+
console.log(` ${line}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!full && lines.length > 50) {
|
|
173
|
+
console.log();
|
|
174
|
+
console.log(pc.dim(` ... ${lines.length - 50} more lines. Use --full to see all.`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Execute ---
|
|
182
|
+
|
|
183
|
+
export async function actionsExecuteCommand(
|
|
184
|
+
actionId: string,
|
|
185
|
+
options: {
|
|
186
|
+
json?: boolean;
|
|
187
|
+
connection?: string;
|
|
188
|
+
data?: string;
|
|
189
|
+
pathVar?: string[];
|
|
190
|
+
query?: string[];
|
|
191
|
+
formData?: boolean;
|
|
192
|
+
formUrlencoded?: boolean;
|
|
193
|
+
} = {}
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
const api = getApi();
|
|
196
|
+
|
|
197
|
+
// 1. Fetch action knowledge to get method + path
|
|
198
|
+
const spinner = p.spinner();
|
|
199
|
+
spinner.start('Loading action details...');
|
|
200
|
+
|
|
201
|
+
let knowledge: ActionKnowledge;
|
|
202
|
+
try {
|
|
203
|
+
const k = await api.getActionKnowledge(actionId);
|
|
204
|
+
if (!k) {
|
|
205
|
+
spinner.stop('Action not found');
|
|
206
|
+
p.cancel(`No action found for: ${actionId}`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
knowledge = k;
|
|
210
|
+
spinner.stop(`${colorMethod(knowledge.method)} ${knowledge.path}`);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
spinner.stop('Failed to load action');
|
|
213
|
+
p.cancel(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 2. Resolve connection key
|
|
218
|
+
let connectionKey = options.connection;
|
|
219
|
+
if (!connectionKey) {
|
|
220
|
+
connectionKey = await resolveConnection(api, knowledge.connectionPlatform);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 3. Resolve path variables
|
|
224
|
+
const pathVarMap = parseKeyValuePairs(options.pathVar || []);
|
|
225
|
+
const pathVars = extractPathVariables(knowledge.path);
|
|
226
|
+
for (const v of pathVars) {
|
|
227
|
+
if (!pathVarMap[v]) {
|
|
228
|
+
const input = await p.text({
|
|
229
|
+
message: `Value for path variable ${pc.yellow(`{{${v}}}`)}:`,
|
|
230
|
+
validate: (val) => val.trim() ? undefined : 'Value is required',
|
|
231
|
+
});
|
|
232
|
+
if (p.isCancel(input)) {
|
|
233
|
+
p.cancel('Cancelled.');
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
pathVarMap[v] = input;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 4. Resolve body data
|
|
241
|
+
let bodyData: Record<string, unknown> = {};
|
|
242
|
+
if (options.data) {
|
|
243
|
+
try {
|
|
244
|
+
bodyData = JSON.parse(options.data);
|
|
245
|
+
} catch {
|
|
246
|
+
p.cancel('Invalid JSON in --data flag.');
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
} else if (!['GET', 'DELETE', 'HEAD'].includes(knowledge.method.toUpperCase())) {
|
|
250
|
+
const input = await p.text({
|
|
251
|
+
message: 'Request body (JSON):',
|
|
252
|
+
placeholder: '{"key": "value"} or leave empty',
|
|
253
|
+
});
|
|
254
|
+
if (p.isCancel(input)) {
|
|
255
|
+
p.cancel('Cancelled.');
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
if (input.trim()) {
|
|
259
|
+
try {
|
|
260
|
+
bodyData = JSON.parse(input);
|
|
261
|
+
} catch {
|
|
262
|
+
p.cancel('Invalid JSON.');
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 5. Resolve query params
|
|
269
|
+
const queryParams = parseKeyValuePairs(options.query || []);
|
|
270
|
+
|
|
271
|
+
// 6. Resolve template variables in path
|
|
272
|
+
const { resolvedPath, remainingData } = resolveTemplateVariables(
|
|
273
|
+
knowledge.path,
|
|
274
|
+
bodyData,
|
|
275
|
+
pathVarMap
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// 7. Show summary
|
|
279
|
+
console.log();
|
|
280
|
+
console.log(pc.bold(' Request Summary'));
|
|
281
|
+
console.log(` ${colorMethod(knowledge.method)} ${knowledge.baseUrl}${resolvedPath}`);
|
|
282
|
+
console.log(` Connection: ${pc.dim(connectionKey)}`);
|
|
283
|
+
if (Object.keys(remainingData).length > 0) {
|
|
284
|
+
console.log(` Body: ${pc.dim(JSON.stringify(remainingData))}`);
|
|
285
|
+
}
|
|
286
|
+
if (Object.keys(queryParams).length > 0) {
|
|
287
|
+
console.log(` Query: ${pc.dim(JSON.stringify(queryParams))}`);
|
|
288
|
+
}
|
|
289
|
+
console.log();
|
|
290
|
+
|
|
291
|
+
// 8. Execute
|
|
292
|
+
const execSpinner = p.spinner();
|
|
293
|
+
execSpinner.start('Executing...');
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const result = await api.executeAction({
|
|
297
|
+
method: knowledge.method,
|
|
298
|
+
path: resolvedPath,
|
|
299
|
+
actionId,
|
|
300
|
+
connectionKey,
|
|
301
|
+
data: Object.keys(remainingData).length > 0 ? remainingData : undefined,
|
|
302
|
+
queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
303
|
+
isFormData: options.formData,
|
|
304
|
+
isFormUrlEncoded: options.formUrlencoded,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
execSpinner.stop(pc.green('Success'));
|
|
308
|
+
|
|
309
|
+
if (options.json) {
|
|
310
|
+
console.log(JSON.stringify(result, null, 2));
|
|
311
|
+
} else {
|
|
312
|
+
console.log();
|
|
313
|
+
console.log(pc.bold(' Response'));
|
|
314
|
+
console.log(pc.dim(' ' + '─'.repeat(40)));
|
|
315
|
+
console.log();
|
|
316
|
+
console.log(formatResponse(result));
|
|
317
|
+
console.log();
|
|
318
|
+
}
|
|
319
|
+
} catch (error) {
|
|
320
|
+
execSpinner.stop(pc.red('Failed'));
|
|
321
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
322
|
+
p.cancel(`Execution failed: ${msg}`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- Helpers ---
|
|
328
|
+
|
|
329
|
+
async function resolveConnection(api: PicaApi, platform: string): Promise<string> {
|
|
330
|
+
const spinner = p.spinner();
|
|
331
|
+
spinner.start('Loading connections...');
|
|
332
|
+
|
|
333
|
+
const connections = await api.listConnections();
|
|
334
|
+
const matching = connections.filter(
|
|
335
|
+
c => c.platform.toLowerCase() === platform.toLowerCase()
|
|
336
|
+
);
|
|
337
|
+
spinner.stop(`${matching.length} ${platform} connection${matching.length === 1 ? '' : 's'} found`);
|
|
338
|
+
|
|
339
|
+
if (matching.length === 0) {
|
|
340
|
+
p.cancel(
|
|
341
|
+
`No ${platform} connections found.\n\n` +
|
|
342
|
+
`Add one with: ${pc.cyan(`pica connection add ${platform}`)}`
|
|
343
|
+
);
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (matching.length === 1) {
|
|
348
|
+
p.log.info(`Using connection: ${pc.dim(matching[0].key)}`);
|
|
349
|
+
return matching[0].key;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const selected = await p.select({
|
|
353
|
+
message: `Multiple ${platform} connections found. Which one?`,
|
|
354
|
+
options: matching.map(c => ({
|
|
355
|
+
value: c.key,
|
|
356
|
+
label: `${c.key}`,
|
|
357
|
+
hint: c.state,
|
|
358
|
+
})),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (p.isCancel(selected)) {
|
|
362
|
+
p.cancel('Cancelled.');
|
|
363
|
+
process.exit(0);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return selected as string;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function parseKeyValuePairs(pairs: string[]): Record<string, string> {
|
|
370
|
+
const result: Record<string, string> = {};
|
|
371
|
+
for (const pair of pairs) {
|
|
372
|
+
const eqIdx = pair.indexOf('=');
|
|
373
|
+
if (eqIdx === -1) continue;
|
|
374
|
+
const key = pair.slice(0, eqIdx);
|
|
375
|
+
const value = pair.slice(eqIdx + 1);
|
|
376
|
+
result[key] = value;
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function formatResponse(data: unknown, indent = 2): string {
|
|
382
|
+
const prefix = ' '.repeat(indent);
|
|
383
|
+
const json = JSON.stringify(data, null, 2);
|
|
384
|
+
return json.split('\n').map(line => `${prefix}${line}`).join('\n');
|
|
385
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { getApiKey } from '../lib/config.js';
|
|
4
|
+
import { PicaApi, TimeoutError } from '../lib/api.js';
|
|
5
|
+
import { openConnectionPage, getConnectionUrl } from '../lib/browser.js';
|
|
6
|
+
import { findPlatform, findSimilarPlatforms } from '../lib/platforms.js';
|
|
7
|
+
import { printTable } from '../lib/table.js';
|
|
8
|
+
import type { Connection } from '../lib/types.js';
|
|
9
|
+
|
|
10
|
+
export async function connectionAddCommand(platformArg?: string): Promise<void> {
|
|
11
|
+
p.intro(pc.bgCyan(pc.black(' Pica ')));
|
|
12
|
+
|
|
13
|
+
const apiKey = getApiKey();
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
p.cancel('Not configured. Run `pica init` first.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const api = new PicaApi(apiKey);
|
|
20
|
+
|
|
21
|
+
// Get platform list for validation
|
|
22
|
+
const spinner = p.spinner();
|
|
23
|
+
spinner.start('Loading platforms...');
|
|
24
|
+
|
|
25
|
+
let platforms;
|
|
26
|
+
try {
|
|
27
|
+
platforms = await api.listPlatforms();
|
|
28
|
+
spinner.stop(`${platforms.length} platforms available`);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
spinner.stop('Failed to load platforms');
|
|
31
|
+
p.cancel(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Get or prompt for platform
|
|
36
|
+
let platform: string;
|
|
37
|
+
|
|
38
|
+
if (platformArg) {
|
|
39
|
+
const found = findPlatform(platforms, platformArg);
|
|
40
|
+
if (found) {
|
|
41
|
+
platform = found.platform;
|
|
42
|
+
} else {
|
|
43
|
+
const similar = findSimilarPlatforms(platforms, platformArg);
|
|
44
|
+
if (similar.length > 0) {
|
|
45
|
+
p.log.warn(`Unknown platform: ${platformArg}`);
|
|
46
|
+
const suggestion = await p.select({
|
|
47
|
+
message: 'Did you mean:',
|
|
48
|
+
options: [
|
|
49
|
+
...similar.map(s => ({ value: s.platform, label: `${s.name} (${s.platform})` })),
|
|
50
|
+
{ value: '__other__', label: 'None of these' },
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (p.isCancel(suggestion) || suggestion === '__other__') {
|
|
55
|
+
p.note(`Run ${pc.cyan('pica platforms')} to see all available platforms.`);
|
|
56
|
+
p.cancel('Connection cancelled.');
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
platform = suggestion as string;
|
|
61
|
+
} else {
|
|
62
|
+
p.cancel(`Unknown platform: ${platformArg}\n\nRun ${pc.cyan('pica platforms')} to see available platforms.`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
const platformInput = await p.text({
|
|
68
|
+
message: 'Which platform do you want to connect?',
|
|
69
|
+
placeholder: 'gmail, slack, hubspot...',
|
|
70
|
+
validate: (value) => {
|
|
71
|
+
if (!value.trim()) return 'Platform name is required';
|
|
72
|
+
return undefined;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (p.isCancel(platformInput)) {
|
|
77
|
+
p.cancel('Connection cancelled.');
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const found = findPlatform(platforms, platformInput);
|
|
82
|
+
if (found) {
|
|
83
|
+
platform = found.platform;
|
|
84
|
+
} else {
|
|
85
|
+
p.cancel(`Unknown platform: ${platformInput}\n\nRun ${pc.cyan('pica platforms')} to see available platforms.`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Open browser
|
|
91
|
+
const url = getConnectionUrl(platform);
|
|
92
|
+
p.log.info(`Opening browser to connect ${pc.cyan(platform)}...`);
|
|
93
|
+
p.note(pc.dim(url), 'URL');
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await openConnectionPage(platform);
|
|
97
|
+
} catch {
|
|
98
|
+
p.log.warn('Could not open browser automatically.');
|
|
99
|
+
p.note(`Open this URL manually:\n${url}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Poll for connection
|
|
103
|
+
const pollSpinner = p.spinner();
|
|
104
|
+
pollSpinner.start('Waiting for connection... (complete auth in browser)');
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const connection = await api.waitForConnection(platform, 5 * 60 * 1000, 5000);
|
|
108
|
+
pollSpinner.stop(`${platform} connected!`);
|
|
109
|
+
|
|
110
|
+
p.log.success(`${pc.green('✓')} ${connection.platform} is now available to your AI agents.`);
|
|
111
|
+
p.outro('Connection complete!');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
pollSpinner.stop('Connection timed out');
|
|
114
|
+
|
|
115
|
+
if (error instanceof TimeoutError) {
|
|
116
|
+
p.note(
|
|
117
|
+
`Possible issues:\n` +
|
|
118
|
+
` - OAuth flow was not completed in the browser\n` +
|
|
119
|
+
` - Browser popup was blocked\n` +
|
|
120
|
+
` - Wrong account selected\n\n` +
|
|
121
|
+
`Try again with: ${pc.cyan(`pica connection add ${platform}`)}`,
|
|
122
|
+
'Timed Out'
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
p.log.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function connectionListCommand(): Promise<void> {
|
|
133
|
+
const apiKey = getApiKey();
|
|
134
|
+
if (!apiKey) {
|
|
135
|
+
p.cancel('Not configured. Run `pica init` first.');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const api = new PicaApi(apiKey);
|
|
140
|
+
|
|
141
|
+
const spinner = p.spinner();
|
|
142
|
+
spinner.start('Loading connections...');
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const connections = await api.listConnections();
|
|
146
|
+
spinner.stop(`${connections.length} connection${connections.length === 1 ? '' : 's'} found`);
|
|
147
|
+
|
|
148
|
+
if (connections.length === 0) {
|
|
149
|
+
p.note(
|
|
150
|
+
`No connections yet.\n\n` +
|
|
151
|
+
`Add one with: ${pc.cyan('pica connection add gmail')}`,
|
|
152
|
+
'No Connections'
|
|
153
|
+
);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log();
|
|
158
|
+
|
|
159
|
+
const rows = connections.map(conn => ({
|
|
160
|
+
status: getStatusIndicator(conn.state),
|
|
161
|
+
platform: conn.platform,
|
|
162
|
+
state: conn.state,
|
|
163
|
+
key: conn.key,
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
printTable(
|
|
167
|
+
[
|
|
168
|
+
{ key: 'status', label: '' },
|
|
169
|
+
{ key: 'platform', label: 'Platform' },
|
|
170
|
+
{ key: 'state', label: 'Status' },
|
|
171
|
+
{ key: 'key', label: 'Connection Key', color: pc.dim },
|
|
172
|
+
],
|
|
173
|
+
rows
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
console.log();
|
|
177
|
+
p.note(`Add more with: ${pc.cyan('pica connection add <platform>')}`, 'Tip');
|
|
178
|
+
} catch (error) {
|
|
179
|
+
spinner.stop('Failed to load connections');
|
|
180
|
+
p.cancel(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getStatusIndicator(state: Connection['state']): string {
|
|
186
|
+
switch (state) {
|
|
187
|
+
case 'operational':
|
|
188
|
+
return pc.green('●');
|
|
189
|
+
case 'degraded':
|
|
190
|
+
return pc.yellow('●');
|
|
191
|
+
case 'failed':
|
|
192
|
+
return pc.red('●');
|
|
193
|
+
default:
|
|
194
|
+
return pc.dim('○');
|
|
195
|
+
}
|
|
196
|
+
}
|