pairai 0.2.2 → 0.2.4
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/package.json +1 -1
- package/pairai.ts +166 -49
package/package.json
CHANGED
package/pairai.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { writeFileSync, mkdirSync, readFileSync, statSync, existsSync } from "no
|
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { join, dirname } from "node:path";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { validateProvider, detectProvider, updateVersionInConfig, checkExistingConfig, formatKeyBackupBox } from "./lib.js";
|
|
24
25
|
|
|
25
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
27
|
const PKG = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
|
|
@@ -62,10 +63,7 @@ if (command === "upgrade") {
|
|
|
62
63
|
try {
|
|
63
64
|
if (!existsSync(p)) continue;
|
|
64
65
|
const content = readFileSync(p, "utf-8");
|
|
65
|
-
const updated = content
|
|
66
|
-
new RegExp(`pairai@${VERSION.replace(/\./g, "\\.")}`, "g"),
|
|
67
|
-
`pairai@${latest}`,
|
|
68
|
-
);
|
|
66
|
+
const updated = updateVersionInConfig(content, latest);
|
|
69
67
|
if (updated !== content) {
|
|
70
68
|
writeFileSync(p, updated);
|
|
71
69
|
console.log(` Updated version in ${p}`);
|
|
@@ -80,11 +78,8 @@ if (command === "upgrade") {
|
|
|
80
78
|
process.exit(0);
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
try { if (statSync(".gemini").isDirectory()) return "gemini"; } catch {}
|
|
86
|
-
return "claude";
|
|
87
|
-
}
|
|
81
|
+
// detectProvider, validateProvider, updateVersionInConfig, checkExistingConfig,
|
|
82
|
+
// formatKeyBackupBox are imported from ./lib.js
|
|
88
83
|
|
|
89
84
|
// ── Setup: register + configure ──────────────────────────────────────────────
|
|
90
85
|
|
|
@@ -94,11 +89,13 @@ if (command === "setup") {
|
|
|
94
89
|
const hubUrl = hubIdx !== -1 ? rest.splice(hubIdx, 2)[1] : "https://pairai.pro";
|
|
95
90
|
const providerIdx = rest.indexOf("--provider");
|
|
96
91
|
const providerArg = providerIdx !== -1 ? rest.splice(providerIdx, 2)[1] : undefined;
|
|
97
|
-
if (providerArg
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
if (providerArg) {
|
|
93
|
+
try { validateProvider(providerArg); } catch (e) {
|
|
94
|
+
console.error(` ${(e as Error).message}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
100
97
|
}
|
|
101
|
-
const provider = providerArg ?? detectProvider();
|
|
98
|
+
const provider = (providerArg as "claude" | "gemini") ?? detectProvider();
|
|
102
99
|
const globalIdx = rest.indexOf("--global");
|
|
103
100
|
const useGlobal = globalIdx !== -1 ? (rest.splice(globalIdx, 1), true) : false;
|
|
104
101
|
const agentName = rest.find((a) => !a.startsWith("--"));
|
|
@@ -111,21 +108,14 @@ if (command === "setup") {
|
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
// Check for existing config to avoid accidental overwrites
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (servers[mcpKey]) {
|
|
123
|
-
console.error(`\n pairai is already configured in ${existingConfigPath}`);
|
|
124
|
-
console.error(` Running setup again would overwrite the existing API key and config.`);
|
|
125
|
-
console.error(`\n To force a fresh setup, run: npx pairai setup "${agentName}" --force\n`);
|
|
126
|
-
process.exit(1);
|
|
127
|
-
}
|
|
128
|
-
} catch {}
|
|
111
|
+
if (!useForce) {
|
|
112
|
+
const existingConfigPath = checkExistingConfig(provider, process.cwd(), homedir(), useGlobal);
|
|
113
|
+
if (existingConfigPath) {
|
|
114
|
+
console.error(`\n pairai is already configured in ${existingConfigPath}`);
|
|
115
|
+
console.error(` Running setup again would overwrite the existing API key and config.`);
|
|
116
|
+
console.error(`\n To force a fresh setup, run: npx pairai setup "${agentName}" --force\n`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
129
119
|
}
|
|
130
120
|
|
|
131
121
|
console.log(`\n Registering "${agentName}" on ${hubUrl}...\n`);
|
|
@@ -158,22 +148,8 @@ if (command === "setup") {
|
|
|
158
148
|
const keyPath = join(keyDir, `${id}.pem`);
|
|
159
149
|
writeFileSync(keyPath, privateKey, { mode: 0o600 });
|
|
160
150
|
console.log(` Private key: ${keyPath}`);
|
|
161
|
-
const lines = [
|
|
162
|
-
"BACK UP YOUR PRIVATE KEY",
|
|
163
|
-
"",
|
|
164
|
-
keyPath,
|
|
165
|
-
"",
|
|
166
|
-
"This key is stored only on your machine.",
|
|
167
|
-
"The hub never sees it. If lost, you must re-register",
|
|
168
|
-
"and re-pair — all encrypted history becomes unreadable.",
|
|
169
|
-
"",
|
|
170
|
-
"Copy it to a password manager or secure backup now.",
|
|
171
|
-
];
|
|
172
|
-
const w = Math.max(...lines.map((l) => l.length)) + 2;
|
|
173
151
|
console.log();
|
|
174
|
-
console.log(
|
|
175
|
-
for (const l of lines) console.log(` │ ${l.padEnd(w)}│`);
|
|
176
|
-
console.log(` └${"─".repeat(w + 2)}┘`);
|
|
152
|
+
for (const line of formatKeyBackupBox(keyPath)) console.log(line);
|
|
177
153
|
console.log();
|
|
178
154
|
|
|
179
155
|
if (provider === "gemini") {
|
|
@@ -256,11 +232,13 @@ const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelco
|
|
|
256
232
|
const serveArgs = args.slice(1);
|
|
257
233
|
const serveProviderIdx = serveArgs.indexOf("--provider");
|
|
258
234
|
const serveProviderArg = serveProviderIdx !== -1 ? serveArgs[serveProviderIdx + 1] : undefined;
|
|
259
|
-
if (serveProviderArg
|
|
260
|
-
|
|
261
|
-
|
|
235
|
+
if (serveProviderArg) {
|
|
236
|
+
try { validateProvider(serveProviderArg); } catch (e) {
|
|
237
|
+
console.error(` ${(e as Error).message}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
262
240
|
}
|
|
263
|
-
const serveProvider = serveProviderArg ?? "claude";
|
|
241
|
+
const serveProvider = (serveProviderArg as "claude" | "gemini") ?? "claude";
|
|
264
242
|
|
|
265
243
|
const HUB_URL = process.env.PAIRAI_HUB_URL ?? process.env.PAIRAI_URL ?? "https://pairai.pro";
|
|
266
244
|
const API_KEY = process.env.PAIRAI_AGENT_CRED ?? process.env.PAIRAI_API_KEY;
|
|
@@ -436,6 +414,11 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
436
414
|
required: ["task_id", "status"],
|
|
437
415
|
},
|
|
438
416
|
},
|
|
417
|
+
{
|
|
418
|
+
name: "pairai_get_profile",
|
|
419
|
+
description: "Get your own agent profile — name, ID, description, capabilities.",
|
|
420
|
+
inputSchema: { type: "object" as const, properties: {} },
|
|
421
|
+
},
|
|
439
422
|
{
|
|
440
423
|
name: "pairai_list_connections",
|
|
441
424
|
description: "List agents you are connected with.",
|
|
@@ -470,6 +453,66 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
470
453
|
required: ["code"],
|
|
471
454
|
},
|
|
472
455
|
},
|
|
456
|
+
{
|
|
457
|
+
name: "pairai_update_profile",
|
|
458
|
+
description: "Update your agent's profile — name, description, capabilities, and metadata. Returns the updated profile.",
|
|
459
|
+
inputSchema: {
|
|
460
|
+
type: "object" as const,
|
|
461
|
+
properties: {
|
|
462
|
+
name: { type: "string", description: "Display name (1-64 chars)" },
|
|
463
|
+
description: { type: "string", description: "What this agent does (max 500 chars)" },
|
|
464
|
+
capabilities: { type: "array", items: { type: "string" }, description: "List of capabilities, e.g. ['scheduling', 'code-review']" },
|
|
465
|
+
metadata: { type: "object", description: "Arbitrary JSON metadata (max 4KB)" },
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: "pairai_set_alias",
|
|
471
|
+
description: "Set a local alias for a connected agent. Only you see this alias. Set to null to clear.",
|
|
472
|
+
inputSchema: {
|
|
473
|
+
type: "object" as const,
|
|
474
|
+
properties: {
|
|
475
|
+
connection_id: { type: "string", description: "Connection ID" },
|
|
476
|
+
alias: { type: ["string", "null"], description: "Local alias, or null to clear" },
|
|
477
|
+
},
|
|
478
|
+
required: ["connection_id"],
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: "pairai_list_tasks",
|
|
483
|
+
description: "List all tasks you are involved in (as initiator or target).",
|
|
484
|
+
inputSchema: {
|
|
485
|
+
type: "object" as const,
|
|
486
|
+
properties: {
|
|
487
|
+
status: { type: "string", enum: ["submitted", "working", "input-required", "completed", "failed", "cancelled"], description: "Filter by status" },
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: "pairai_get_task",
|
|
493
|
+
description: "Get full details of a task including all messages.",
|
|
494
|
+
inputSchema: {
|
|
495
|
+
type: "object" as const,
|
|
496
|
+
properties: {
|
|
497
|
+
task_id: { type: "string", description: "Task ID" },
|
|
498
|
+
},
|
|
499
|
+
required: ["task_id"],
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
name: "pairai_upload_file",
|
|
504
|
+
description: "Upload a file to a task. Provide base64-encoded content.",
|
|
505
|
+
inputSchema: {
|
|
506
|
+
type: "object" as const,
|
|
507
|
+
properties: {
|
|
508
|
+
task_id: { type: "string", description: "Task ID" },
|
|
509
|
+
filename: { type: "string", description: "Original filename, e.g. photo.png" },
|
|
510
|
+
mime_type: { type: "string", description: "MIME type, e.g. image/png" },
|
|
511
|
+
base64_content: { type: "string", description: "Base64-encoded file content" },
|
|
512
|
+
},
|
|
513
|
+
required: ["task_id", "filename", "mime_type", "base64_content"],
|
|
514
|
+
},
|
|
515
|
+
},
|
|
473
516
|
{
|
|
474
517
|
name: "pairai_create_encrypted_task",
|
|
475
518
|
description: "Create an encrypted task. Title and description are encrypted — the hub cannot read them.",
|
|
@@ -530,6 +573,11 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
530
573
|
return { content: [{ type: "text" as const, text: `Status → ${args.status}` }] };
|
|
531
574
|
}
|
|
532
575
|
|
|
576
|
+
if (name === "pairai_get_profile") {
|
|
577
|
+
const data = await hubGet("/agents/me");
|
|
578
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
579
|
+
}
|
|
580
|
+
|
|
533
581
|
if (name === "pairai_list_connections") {
|
|
534
582
|
const data = await hubGet("/connections");
|
|
535
583
|
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
@@ -584,6 +632,74 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
584
632
|
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
585
633
|
}
|
|
586
634
|
|
|
635
|
+
if (name === "pairai_update_profile") {
|
|
636
|
+
const body: Record<string, unknown> = {};
|
|
637
|
+
if (args.name !== undefined) body.name = args.name;
|
|
638
|
+
if (args.description !== undefined) body.description = args.description;
|
|
639
|
+
if (args.capabilities !== undefined) body.capabilities = args.capabilities;
|
|
640
|
+
if (args.metadata !== undefined) body.metadata = args.metadata;
|
|
641
|
+
const data = await hubPatch("/agents/me", body);
|
|
642
|
+
return { content: [{ type: "text" as const, text: "Profile updated.\n" + JSON.stringify(data, null, 2) }] };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (name === "pairai_set_alias") {
|
|
646
|
+
const { connection_id, alias } = args as { connection_id: string; alias?: string | null };
|
|
647
|
+
const data = await hubPatch(`/connections/${connection_id}`, { alias: alias ?? null });
|
|
648
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (name === "pairai_list_tasks") {
|
|
652
|
+
await loadPublicKeys();
|
|
653
|
+
const data = (await hubGet("/tasks")) as Array<{
|
|
654
|
+
id: string; status: string; title: string; encrypted?: boolean;
|
|
655
|
+
description?: string; descriptionKeys?: any; senderSignature?: string; initiatorAgentId?: string;
|
|
656
|
+
}>;
|
|
657
|
+
const filtered = args.status ? data.filter((t) => t.status === args.status) : data;
|
|
658
|
+
const decrypted = filtered.map((t) => {
|
|
659
|
+
if (t.encrypted) {
|
|
660
|
+
const desc = decryptTaskDescription(t, t.id);
|
|
661
|
+
return { ...t, title: desc.split("\n")[0] || t.title, description: desc };
|
|
662
|
+
}
|
|
663
|
+
return t;
|
|
664
|
+
});
|
|
665
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(decrypted, null, 2) }] };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (name === "pairai_get_task") {
|
|
669
|
+
await loadPublicKeys();
|
|
670
|
+
const data = (await hubGet(`/tasks/${args.task_id}`)) as {
|
|
671
|
+
id: string; encrypted?: boolean; description?: string; descriptionKeys?: any;
|
|
672
|
+
senderSignature?: string; initiatorAgentId?: string;
|
|
673
|
+
[key: string]: unknown;
|
|
674
|
+
};
|
|
675
|
+
const msgs = (await hubGet(`/tasks/${args.task_id}/messages`)) as Array<{
|
|
676
|
+
id: string; content: string; contentType: string; senderAgentId: string;
|
|
677
|
+
encryptedKeys?: any; senderSignature?: string;
|
|
678
|
+
}>;
|
|
679
|
+
if (data.encrypted) {
|
|
680
|
+
const desc = decryptTaskDescription(data, data.id);
|
|
681
|
+
data.description = desc;
|
|
682
|
+
}
|
|
683
|
+
const decryptedMsgs = msgs.map((m) => {
|
|
684
|
+
if (data.encrypted) {
|
|
685
|
+
const d = decryptMessage(m, data.id);
|
|
686
|
+
return { ...m, content: d.content, contentType: d.contentType };
|
|
687
|
+
}
|
|
688
|
+
return m;
|
|
689
|
+
});
|
|
690
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ...data, messages: decryptedMsgs }, null, 2) }] };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (name === "pairai_upload_file") {
|
|
694
|
+
const { task_id, filename, mime_type, base64_content } = args as {
|
|
695
|
+
task_id: string; filename: string; mime_type: string; base64_content: string;
|
|
696
|
+
};
|
|
697
|
+
const data = await hubPost(`/tasks/${task_id}/files/json`, {
|
|
698
|
+
filename, mimeType: mime_type, base64Content: base64_content,
|
|
699
|
+
});
|
|
700
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
701
|
+
}
|
|
702
|
+
|
|
587
703
|
if (name === "pairai_create_encrypted_task") {
|
|
588
704
|
if (!PRIVATE_KEY)
|
|
589
705
|
return { content: [{ type: "text" as const, text: "No private key configured. Re-run setup." }] };
|
|
@@ -636,7 +752,7 @@ function decryptMessage(
|
|
|
636
752
|
}
|
|
637
753
|
try {
|
|
638
754
|
const keys = typeof msg.encryptedKeys === "string" ? JSON.parse(msg.encryptedKeys) : msg.encryptedKeys;
|
|
639
|
-
const senderPub = pubKeyCache.get(msg.senderAgentId);
|
|
755
|
+
const senderPub = msg.senderAgentId === myAgentId ? myPublicKey : pubKeyCache.get(msg.senderAgentId);
|
|
640
756
|
const myKey = keys[myAgentId];
|
|
641
757
|
if (senderPub && myKey) {
|
|
642
758
|
const plain = localDecrypt(msg.content, msg.senderSignature, taskId, senderPub, myKey);
|
|
@@ -659,7 +775,7 @@ function decryptTaskDescription(
|
|
|
659
775
|
}
|
|
660
776
|
try {
|
|
661
777
|
const keys = typeof full.descriptionKeys === "string" ? JSON.parse(full.descriptionKeys) : full.descriptionKeys;
|
|
662
|
-
const senderPub = full.initiatorAgentId ? pubKeyCache.get(full.initiatorAgentId) : undefined;
|
|
778
|
+
const senderPub = full.initiatorAgentId === myAgentId ? myPublicKey : (full.initiatorAgentId ? pubKeyCache.get(full.initiatorAgentId) : undefined);
|
|
663
779
|
const myKey = keys[myAgentId];
|
|
664
780
|
if (senderPub && myKey) {
|
|
665
781
|
const plain = localDecrypt(full.description, full.senderSignature, taskId, senderPub, myKey);
|
|
@@ -735,6 +851,7 @@ async function poll() {
|
|
|
735
851
|
}>;
|
|
736
852
|
};
|
|
737
853
|
|
|
854
|
+
if (!full?.messages) continue;
|
|
738
855
|
for (const msg of full.messages.slice(-unread.count)) {
|
|
739
856
|
const key = `msg:${msg.id}`;
|
|
740
857
|
if (seenMessages.has(key)) continue;
|