pairai 0.3.2 → 0.4.2
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/lib.ts +10 -8
- package/package.json +3 -1
- package/pairai.ts +206 -6
package/lib.ts
CHANGED
|
@@ -28,16 +28,18 @@ export function validateProvider(value: string): Provider {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Auto-detect provider based on environment and filesystem.
|
|
31
|
+
* Returns null when detection is ambiguous (0 or 2+ matches).
|
|
31
32
|
*/
|
|
32
|
-
export function detectProvider(): Provider {
|
|
33
|
+
export function detectProvider(): Provider | null {
|
|
33
34
|
if (process.env.GEMINI_CLI) return "gemini";
|
|
34
|
-
|
|
35
|
-
try { if (statSync(".
|
|
36
|
-
try { if (statSync(".
|
|
37
|
-
try { if (statSync(".
|
|
38
|
-
try { if (statSync(".
|
|
39
|
-
try { if (statSync(".
|
|
40
|
-
|
|
35
|
+
const found: Provider[] = [];
|
|
36
|
+
try { if (statSync(".cursor").isDirectory()) found.push("cursor"); } catch {}
|
|
37
|
+
try { if (statSync(".windsurf").isDirectory()) found.push("windsurf"); } catch {}
|
|
38
|
+
try { if (statSync(".vscode").isDirectory()) found.push("copilot"); } catch {}
|
|
39
|
+
try { if (statSync(".codex").isDirectory()) found.push("codex"); } catch {}
|
|
40
|
+
try { if (statSync(".amazonq").isDirectory()) found.push("amazonq"); } catch {}
|
|
41
|
+
try { if (statSync(".gemini").isDirectory()) found.push("gemini"); } catch {}
|
|
42
|
+
return found.length === 1 ? found[0] : null;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export interface ProviderConfig {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pairai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "pairai CLI — connect AI agents to collaborate via the pairai hub",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
"README.md"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@inquirer/input": "^5.0.10",
|
|
33
|
+
"@inquirer/select": "^5.1.2",
|
|
32
34
|
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
33
35
|
"nanoid": "^5.0.0",
|
|
34
36
|
"tsx": "^4.0.0"
|
package/pairai.ts
CHANGED
|
@@ -24,11 +24,23 @@ import { join, dirname } from "node:path";
|
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { validateProvider, detectProvider, checkExistingConfig, formatKeyBackupBox, acquireLock, releaseLock, getProviderConfig, localEncrypt as _localEncrypt, localDecrypt as _localDecrypt } from "./lib.js";
|
|
26
26
|
import type { Provider } from "./lib.js";
|
|
27
|
+
import select from "@inquirer/select";
|
|
28
|
+
import input from "@inquirer/input";
|
|
27
29
|
|
|
28
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
31
|
const PKG = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
|
|
30
32
|
const VERSION: string = PKG.version;
|
|
31
33
|
|
|
34
|
+
const PROVIDER_CHOICES: { name: string; value: Provider }[] = [
|
|
35
|
+
{ name: "Claude Code", value: "claude" },
|
|
36
|
+
{ name: "Gemini CLI", value: "gemini" },
|
|
37
|
+
{ name: "Cursor", value: "cursor" },
|
|
38
|
+
{ name: "GitHub Copilot (VS Code)", value: "copilot" },
|
|
39
|
+
{ name: "Windsurf", value: "windsurf" },
|
|
40
|
+
{ name: "OpenAI Codex CLI", value: "codex" },
|
|
41
|
+
{ name: "Amazon Q", value: "amazonq" },
|
|
42
|
+
];
|
|
43
|
+
|
|
32
44
|
const args = process.argv.slice(2);
|
|
33
45
|
const command = args[0];
|
|
34
46
|
|
|
@@ -108,16 +120,39 @@ if (command === "setup") {
|
|
|
108
120
|
process.exit(1);
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
|
-
|
|
123
|
+
let provider: Provider;
|
|
124
|
+
if (providerArg) {
|
|
125
|
+
provider = providerArg as Provider;
|
|
126
|
+
} else {
|
|
127
|
+
const detected = detectProvider();
|
|
128
|
+
if (detected) {
|
|
129
|
+
provider = detected;
|
|
130
|
+
} else if (process.stdin.isTTY) {
|
|
131
|
+
provider = await select({
|
|
132
|
+
message: "Select your AI tool:",
|
|
133
|
+
choices: PROVIDER_CHOICES,
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
console.error('Cannot auto-detect provider. Use --provider flag (e.g. npx pairai setup "My Agent" --provider cursor)');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
112
140
|
const globalIdx = rest.indexOf("--global");
|
|
113
141
|
const useGlobal = globalIdx !== -1 ? (rest.splice(globalIdx, 1), true) : false;
|
|
114
|
-
|
|
142
|
+
let agentName = rest.find((a) => !a.startsWith("--"));
|
|
115
143
|
|
|
116
144
|
const forceIdx = rest.indexOf("--force");
|
|
117
145
|
const useForce = forceIdx !== -1 ? (rest.splice(forceIdx, 1), true) : false;
|
|
118
146
|
if (!agentName) {
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
if (process.stdin.isTTY) {
|
|
148
|
+
agentName = await input({
|
|
149
|
+
message: 'What should we call your agent? Other agents and users will see this name. (e.g. "Alice\'s Assistant", "Travel Bot")',
|
|
150
|
+
validate: (v) => v.trim().length > 0 && v.trim().length <= 64 ? true : "Name must be 1-64 characters",
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
console.error('Usage: npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini|cursor|copilot|windsurf|codex|amazonq] [--global] [--force]');
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
121
156
|
}
|
|
122
157
|
|
|
123
158
|
// Check for existing config to avoid accidental overwrites
|
|
@@ -342,7 +377,9 @@ async function loadAgentInfo() {
|
|
|
342
377
|
const me = (await hubGet("/agents/me")) as { id: string; name: string; publicKey?: string };
|
|
343
378
|
myAgentId = me.id;
|
|
344
379
|
myPublicKey = me.publicKey ?? "";
|
|
345
|
-
} catch {
|
|
380
|
+
} catch (err) {
|
|
381
|
+
console.error("[pairai] failed to load agent info:", (err as Error).message);
|
|
382
|
+
}
|
|
346
383
|
}
|
|
347
384
|
|
|
348
385
|
async function loadPublicKeys() {
|
|
@@ -351,7 +388,9 @@ async function loadPublicKeys() {
|
|
|
351
388
|
for (const c of conns) {
|
|
352
389
|
if (c.publicKey) pubKeyCache.set(c.agentId, c.publicKey);
|
|
353
390
|
}
|
|
354
|
-
} catch {
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.error("[pairai] failed to load public keys:", (err as Error).message);
|
|
393
|
+
}
|
|
355
394
|
}
|
|
356
395
|
|
|
357
396
|
function localEncrypt(plaintext: string, taskId: string, recipientPubKeys: Record<string, string>) {
|
|
@@ -477,6 +516,17 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
477
516
|
required: ["code"],
|
|
478
517
|
},
|
|
479
518
|
},
|
|
519
|
+
{
|
|
520
|
+
name: "pairai_connect_directly",
|
|
521
|
+
description: "Connect directly to an agent that has auto-accept enabled — no pairing code needed. Use pairai_discover_agents to find agents with autoAccept: true.",
|
|
522
|
+
inputSchema: {
|
|
523
|
+
type: "object" as const,
|
|
524
|
+
properties: {
|
|
525
|
+
agent_id: { type: "string", description: "ID of the agent to connect with" },
|
|
526
|
+
},
|
|
527
|
+
required: ["agent_id"],
|
|
528
|
+
},
|
|
529
|
+
},
|
|
480
530
|
{
|
|
481
531
|
name: "pairai_update_profile",
|
|
482
532
|
description: "Update your agent's profile — name, description, capabilities, and metadata. Returns the updated profile.",
|
|
@@ -488,6 +538,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
488
538
|
capabilities: { type: "array", items: { type: "string" }, description: "List of capabilities, e.g. ['scheduling', 'code-review']" },
|
|
489
539
|
metadata: { type: "object", description: "Arbitrary JSON metadata (max 4KB)" },
|
|
490
540
|
discoverable: { type: "boolean", description: "Whether to appear in the public agent directory" },
|
|
541
|
+
autoAccept: { type: "boolean", description: "Whether to accept direct connections without pairing codes" },
|
|
491
542
|
defaultApprovalRule: { type: "string", enum: ["auto", "require"], description: "Default approval rule for new connections" },
|
|
492
543
|
},
|
|
493
544
|
},
|
|
@@ -639,6 +690,78 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
639
690
|
required: ["target_agent_id", "title"],
|
|
640
691
|
},
|
|
641
692
|
},
|
|
693
|
+
{
|
|
694
|
+
name: "pairai_delete_message",
|
|
695
|
+
description: "Delete (tombstone) a message you sent. The message content is replaced with [deleted].",
|
|
696
|
+
inputSchema: {
|
|
697
|
+
type: "object" as const,
|
|
698
|
+
properties: {
|
|
699
|
+
task_id: { type: "string", description: "Task ID" },
|
|
700
|
+
message_id: { type: "string", description: "Message ID to delete" },
|
|
701
|
+
},
|
|
702
|
+
required: ["task_id", "message_id"],
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
name: "pairai_delete_file",
|
|
707
|
+
description: "Delete a file you uploaded. Removes from disk and tombstones the associated message.",
|
|
708
|
+
inputSchema: {
|
|
709
|
+
type: "object" as const,
|
|
710
|
+
properties: {
|
|
711
|
+
file_id: { type: "string", description: "File ID to delete" },
|
|
712
|
+
},
|
|
713
|
+
required: ["file_id"],
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
name: "pairai_delete_task",
|
|
718
|
+
description: "Permanently delete a terminal task (completed, failed, cancelled) and all its messages and files.",
|
|
719
|
+
inputSchema: {
|
|
720
|
+
type: "object" as const,
|
|
721
|
+
properties: {
|
|
722
|
+
task_id: { type: "string", description: "Task ID to delete" },
|
|
723
|
+
},
|
|
724
|
+
required: ["task_id"],
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
name: "pairai_rotate_api_key",
|
|
729
|
+
description: "Generate a new API key. WARNING: old key immediately invalidated. Save the new key before doing anything else.",
|
|
730
|
+
inputSchema: {
|
|
731
|
+
type: "object" as const,
|
|
732
|
+
properties: {},
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: "pairai_delete_account",
|
|
737
|
+
description: "PERMANENTLY delete your agent and ALL associated data. IRREVERSIBLE.",
|
|
738
|
+
inputSchema: {
|
|
739
|
+
type: "object" as const,
|
|
740
|
+
properties: {},
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
name: "pairai_block_agent",
|
|
745
|
+
description: "Block an agent. They cannot discover or connect with you. Disconnects if connected.",
|
|
746
|
+
inputSchema: {
|
|
747
|
+
type: "object" as const,
|
|
748
|
+
properties: {
|
|
749
|
+
agent_id: { type: "string", description: "ID of the agent to block" },
|
|
750
|
+
},
|
|
751
|
+
required: ["agent_id"],
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
name: "pairai_unblock_agent",
|
|
756
|
+
description: "Unblock a previously blocked agent.",
|
|
757
|
+
inputSchema: {
|
|
758
|
+
type: "object" as const,
|
|
759
|
+
properties: {
|
|
760
|
+
agent_id: { type: "string", description: "ID of the agent to unblock" },
|
|
761
|
+
},
|
|
762
|
+
required: ["agent_id"],
|
|
763
|
+
},
|
|
764
|
+
},
|
|
642
765
|
],
|
|
643
766
|
}));
|
|
644
767
|
|
|
@@ -809,6 +932,14 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
809
932
|
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
810
933
|
}
|
|
811
934
|
|
|
935
|
+
if (name === "pairai_connect_directly") {
|
|
936
|
+
const { agent_id } = args as { agent_id: string };
|
|
937
|
+
const data = await hubPost(`/connect/${agent_id}`);
|
|
938
|
+
// Refresh public keys after new connection
|
|
939
|
+
await loadPublicKeys();
|
|
940
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
941
|
+
}
|
|
942
|
+
|
|
812
943
|
if (name === "pairai_update_profile") {
|
|
813
944
|
const body: Record<string, unknown> = {};
|
|
814
945
|
if (args.name !== undefined) body.name = args.name;
|
|
@@ -816,6 +947,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
816
947
|
if (args.capabilities !== undefined) body.capabilities = args.capabilities;
|
|
817
948
|
if (args.metadata !== undefined) body.metadata = args.metadata;
|
|
818
949
|
if (args.discoverable !== undefined) body.discoverable = args.discoverable === "true" || args.discoverable === true;
|
|
950
|
+
if (args.autoAccept !== undefined) body.autoAccept = args.autoAccept === "true" || args.autoAccept === true;
|
|
819
951
|
if (args.defaultApprovalRule !== undefined) body.defaultApprovalRule = args.defaultApprovalRule;
|
|
820
952
|
const data = await hubPatch("/agents/me", body);
|
|
821
953
|
return { content: [{ type: "text" as const, text: "Profile updated.\n" + JSON.stringify(data, null, 2) }] };
|
|
@@ -1112,6 +1244,74 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1112
1244
|
return { content: [{ type: "text" as const, text: `Encrypted task created. ID: ${taskId}` }] };
|
|
1113
1245
|
}
|
|
1114
1246
|
|
|
1247
|
+
if (name === "pairai_delete_message") {
|
|
1248
|
+
const { task_id, message_id } = args as { task_id: string; message_id: string };
|
|
1249
|
+
try {
|
|
1250
|
+
await hubDelete(`/tasks/${task_id}/messages/${message_id}`);
|
|
1251
|
+
return { content: [{ type: "text" as const, text: "Message deleted." }] };
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (name === "pairai_delete_file") {
|
|
1258
|
+
const { file_id } = args as { file_id: string };
|
|
1259
|
+
try {
|
|
1260
|
+
await hubDelete(`/files/${file_id}`);
|
|
1261
|
+
return { content: [{ type: "text" as const, text: "File deleted." }] };
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (name === "pairai_delete_task") {
|
|
1268
|
+
const { task_id } = args as { task_id: string };
|
|
1269
|
+
try {
|
|
1270
|
+
const result = (await hubDelete(`/tasks/${task_id}`)) as { deletedMessages?: number; deletedFiles?: number };
|
|
1271
|
+
return { content: [{ type: "text" as const, text: `Task deleted. ${result.deletedMessages ?? 0} message(s) and ${result.deletedFiles ?? 0} file(s) removed.` }] };
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
if (name === "pairai_rotate_api_key") {
|
|
1278
|
+
try {
|
|
1279
|
+
const result = (await hubPost("/agents/me/rotate-key")) as { apiKey: string };
|
|
1280
|
+
return { content: [{ type: "text" as const, text: `New API key: ${result.apiKey}\n\nWARNING: Your old key is now invalid. Save this key immediately — it will not be shown again.` }] };
|
|
1281
|
+
} catch (err) {
|
|
1282
|
+
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (name === "pairai_delete_account") {
|
|
1287
|
+
try {
|
|
1288
|
+
await hubDelete("/agents/me");
|
|
1289
|
+
return { content: [{ type: "text" as const, text: "Account deleted." }] };
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (name === "pairai_block_agent") {
|
|
1296
|
+
const { agent_id } = args as { agent_id: string };
|
|
1297
|
+
try {
|
|
1298
|
+
await hubPost("/agents/me/block", { agentId: agent_id });
|
|
1299
|
+
return { content: [{ type: "text" as const, text: "Agent blocked." }] };
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (name === "pairai_unblock_agent") {
|
|
1306
|
+
const { agent_id } = args as { agent_id: string };
|
|
1307
|
+
try {
|
|
1308
|
+
await hubDelete(`/agents/me/block/${agent_id}`);
|
|
1309
|
+
return { content: [{ type: "text" as const, text: "Agent unblocked." }] };
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1115
1315
|
throw new Error(`Unknown tool: ${name}`);
|
|
1116
1316
|
});
|
|
1117
1317
|
|