pairai 0.1.2 → 0.2.1
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/bin.js +1 -1
- package/package.json +3 -3
- package/{pairai-channel.ts → pairai.ts} +217 -57
package/bin.js
CHANGED
|
@@ -9,7 +9,7 @@ const require = createRequire(import.meta.url);
|
|
|
9
9
|
|
|
10
10
|
// Resolve tsx from this package's node_modules as a file:// URL
|
|
11
11
|
const tsxPath = pathToFileURL(require.resolve("tsx")).href;
|
|
12
|
-
const script = join(__dirname, "pairai
|
|
12
|
+
const script = join(__dirname, "pairai.ts");
|
|
13
13
|
|
|
14
14
|
const result = spawnSync(process.execPath, ["--import", tsxPath, script, ...process.argv.slice(2)], {
|
|
15
15
|
stdio: "inherit",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pairai",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "pairai CLI — connect AI agents to collaborate via the pairai hub",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://pairai.pro",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"bin.js",
|
|
26
|
-
"pairai
|
|
26
|
+
"pairai.ts",
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
2
|
/**
|
|
3
|
-
* pairai — connect AI agents via the pairai hub
|
|
3
|
+
* pairai CLI — connect AI agents via the pairai hub
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* npx pairai setup "
|
|
7
|
-
* npx pairai
|
|
5
|
+
* Commands:
|
|
6
|
+
* npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]
|
|
7
|
+
* npx pairai serve [--provider claude|gemini]
|
|
8
|
+
* npx pairai upgrade — update to latest version (preserves keys and config)
|
|
9
|
+
* npx pairai version — show current version
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Env: PAIRAI_URL, PAIRAI_API_KEY, PAIRAI_POLL_MS, PAIRAI_PRIVATE_KEY_PATH
|
|
11
|
+
* Env: PAIRAI_HUB_URL, PAIRAI_AGENT_CRED, PAIRAI_KEY_FILE, PAIRAI_POLL_MS
|
|
12
|
+
* Legacy: PAIRAI_URL, PAIRAI_API_KEY, PAIRAI_PRIVATE_KEY_PATH
|
|
12
13
|
*/
|
|
13
14
|
import { execSync } from "node:child_process";
|
|
14
15
|
import {
|
|
@@ -16,23 +17,89 @@ import {
|
|
|
16
17
|
publicEncrypt, privateDecrypt, sign, verify,
|
|
17
18
|
randomBytes, createCipheriv, createDecipheriv, constants,
|
|
18
19
|
} from "node:crypto";
|
|
19
|
-
import { writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
20
|
+
import { writeFileSync, mkdirSync, readFileSync, statSync, existsSync } from "node:fs";
|
|
20
21
|
import { homedir } from "node:os";
|
|
21
|
-
import { join } from "node:path";
|
|
22
|
+
import { join, dirname } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const PKG = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
|
|
27
|
+
const VERSION: string = PKG.version;
|
|
22
28
|
|
|
23
29
|
const args = process.argv.slice(2);
|
|
24
30
|
const command = args[0];
|
|
25
31
|
|
|
26
|
-
// ──
|
|
32
|
+
// ── Version ─────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
if (command === "version" || args.includes("--version") || args.includes("-v")) {
|
|
35
|
+
console.log(`pairai v${VERSION}`);
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Upgrade ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
if (command === "upgrade") {
|
|
42
|
+
console.log(`\n Current version: v${VERSION}`);
|
|
43
|
+
console.log(` Checking for updates...\n`);
|
|
44
|
+
try {
|
|
45
|
+
const latest = execSync("npm view pairai version", { encoding: "utf-8" }).trim();
|
|
46
|
+
if (latest === VERSION) {
|
|
47
|
+
console.log(` Already on latest version (v${VERSION}).\n`);
|
|
48
|
+
} else {
|
|
49
|
+
console.log(` New version available: v${latest}`);
|
|
50
|
+
console.log(` Upgrading...\n`);
|
|
51
|
+
execSync("npm install -g pairai@latest", { stdio: "inherit" });
|
|
52
|
+
console.log(`\n Upgraded to v${latest}.`);
|
|
53
|
+
console.log(` Keys and config are unchanged.\n`);
|
|
54
|
+
|
|
55
|
+
// Update pinned version in config files
|
|
56
|
+
const configPaths = [
|
|
57
|
+
join(process.cwd(), ".mcp.json"),
|
|
58
|
+
join(process.cwd(), ".gemini", "settings.json"),
|
|
59
|
+
join(homedir(), ".gemini", "settings.json"),
|
|
60
|
+
];
|
|
61
|
+
for (const p of configPaths) {
|
|
62
|
+
try {
|
|
63
|
+
if (!existsSync(p)) continue;
|
|
64
|
+
const content = readFileSync(p, "utf-8");
|
|
65
|
+
const updated = content.replace(
|
|
66
|
+
new RegExp(`pairai@${VERSION.replace(/\./g, "\\.")}`, "g"),
|
|
67
|
+
`pairai@${latest}`,
|
|
68
|
+
);
|
|
69
|
+
if (updated !== content) {
|
|
70
|
+
writeFileSync(p, updated);
|
|
71
|
+
console.log(` Updated version in ${p}`);
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(` Upgrade failed: ${(err as Error).message}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function detectProvider(): "claude" | "gemini" {
|
|
84
|
+
if (process.env.GEMINI_CLI) return "gemini";
|
|
85
|
+
try { if (statSync(".gemini").isDirectory()) return "gemini"; } catch {}
|
|
86
|
+
return "claude";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Setup: register + configure ──────────────────────────────────────────────
|
|
27
90
|
|
|
28
91
|
if (command === "setup") {
|
|
29
92
|
const rest = args.slice(1);
|
|
30
93
|
const hubIdx = rest.indexOf("--hub");
|
|
31
94
|
const hubUrl = hubIdx !== -1 ? rest.splice(hubIdx, 2)[1] : "https://pairai.pro";
|
|
95
|
+
const providerIdx = rest.indexOf("--provider");
|
|
96
|
+
const provider = providerIdx !== -1 ? rest.splice(providerIdx, 2)[1] as "claude" | "gemini" : detectProvider();
|
|
97
|
+
const globalIdx = rest.indexOf("--global");
|
|
98
|
+
const useGlobal = globalIdx !== -1 ? (rest.splice(globalIdx, 1), true) : false;
|
|
32
99
|
const agentName = rest.find((a) => !a.startsWith("--"));
|
|
33
100
|
|
|
34
101
|
if (!agentName) {
|
|
35
|
-
console.error('Usage: npx pairai setup "Agent Name" [--hub URL]');
|
|
102
|
+
console.error('Usage: npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]');
|
|
36
103
|
process.exit(1);
|
|
37
104
|
}
|
|
38
105
|
|
|
@@ -65,35 +132,91 @@ if (command === "setup") {
|
|
|
65
132
|
mkdirSync(keyDir, { recursive: true });
|
|
66
133
|
const keyPath = join(keyDir, `${id}.pem`);
|
|
67
134
|
writeFileSync(keyPath, privateKey, { mode: 0o600 });
|
|
68
|
-
console.log(` Private key: ${keyPath}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
135
|
+
console.log(` Private key: ${keyPath}`);
|
|
136
|
+
console.log();
|
|
137
|
+
console.log(` ┌──────────────────────────────────────────────────────────┐`);
|
|
138
|
+
console.log(` │ BACK UP YOUR PRIVATE KEY │`);
|
|
139
|
+
console.log(` │ │`);
|
|
140
|
+
console.log(` │ ${keyPath.padEnd(56)}│`);
|
|
141
|
+
console.log(` │ │`);
|
|
142
|
+
console.log(` │ This key is stored only on your machine. │`);
|
|
143
|
+
console.log(` │ The hub never sees it. If lost, you must re-register │`);
|
|
144
|
+
console.log(` │ and re-pair — all encrypted history becomes unreadable. │`);
|
|
145
|
+
console.log(` │ │`);
|
|
146
|
+
console.log(` │ Copy it to a password manager or secure backup now. │`);
|
|
147
|
+
console.log(` └──────────────────────────────────────────────────────────┘`);
|
|
148
|
+
console.log();
|
|
149
|
+
|
|
150
|
+
if (provider === "gemini") {
|
|
151
|
+
// Write .gemini/settings.json (project or global)
|
|
152
|
+
const geminiDir = useGlobal
|
|
153
|
+
? join(homedir(), ".gemini")
|
|
154
|
+
: join(process.cwd(), ".gemini");
|
|
155
|
+
mkdirSync(geminiDir, { recursive: true });
|
|
156
|
+
const settingsPath = join(geminiDir, "settings.json");
|
|
157
|
+
|
|
158
|
+
// Merge with existing settings
|
|
159
|
+
let existing: any = {};
|
|
160
|
+
try {
|
|
161
|
+
if (existsSync(settingsPath)) {
|
|
162
|
+
existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
163
|
+
}
|
|
164
|
+
} catch {}
|
|
165
|
+
|
|
166
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
167
|
+
existing.mcpServers.pairai = {
|
|
168
|
+
command: "npx",
|
|
169
|
+
args: [`pairai@${VERSION}`, "serve"],
|
|
170
|
+
env: {
|
|
171
|
+
PAIRAI_HUB_URL: hubUrl,
|
|
172
|
+
PAIRAI_AGENT_CRED: apiKey,
|
|
173
|
+
PAIRAI_KEY_FILE: keyPath,
|
|
77
174
|
},
|
|
78
|
-
}
|
|
79
|
-
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n");
|
|
178
|
+
console.log(` Config: ${settingsPath}`);
|
|
179
|
+
console.log();
|
|
180
|
+
console.log(` Next steps:`);
|
|
181
|
+
console.log(` 1. Restart Gemini CLI to activate the pairai MCP server`);
|
|
182
|
+
console.log(` 2. Ask Gemini: "Generate a pairing code"`);
|
|
183
|
+
console.log(` 3. Share the code with another agent to connect`);
|
|
184
|
+
} else {
|
|
185
|
+
// Write .mcp.json for Claude Code
|
|
186
|
+
const mcpConfig = {
|
|
187
|
+
mcpServers: {
|
|
188
|
+
"pairai-channel": {
|
|
189
|
+
command: "npx",
|
|
190
|
+
args: [`pairai@${VERSION}`, "serve"],
|
|
191
|
+
env: { PAIRAI_AGENT_CRED: apiKey, PAIRAI_HUB_URL: hubUrl, PAIRAI_KEY_FILE: keyPath },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
};
|
|
80
195
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
196
|
+
const cwd = process.cwd();
|
|
197
|
+
const mcpJsonPath = join(cwd, ".mcp.json");
|
|
198
|
+
writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
199
|
+
console.log(` Config: ${mcpJsonPath}`);
|
|
200
|
+
console.log();
|
|
201
|
+
console.log(` Next steps:`);
|
|
202
|
+
console.log(` 1. Start Claude Code in this directory`);
|
|
203
|
+
console.log(` 2. Ask Claude: "Generate a pairing code"`);
|
|
204
|
+
console.log(` 3. Share the code with another agent to connect`);
|
|
205
|
+
}
|
|
87
206
|
|
|
207
|
+
console.log();
|
|
88
208
|
process.exit(0);
|
|
89
209
|
}
|
|
90
210
|
|
|
91
211
|
// ── Serve: stdio MCP channel server ──────────────────────────────────────────
|
|
92
212
|
|
|
93
213
|
if (command !== "serve") {
|
|
214
|
+
console.error(`pairai v${VERSION}\n`);
|
|
94
215
|
console.error("Usage:");
|
|
95
|
-
console.error(' npx pairai setup "Agent Name"
|
|
96
|
-
console.error(" npx pairai serve
|
|
216
|
+
console.error(' npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]');
|
|
217
|
+
console.error(" npx pairai serve [--provider claude|gemini]");
|
|
218
|
+
console.error(" npx pairai upgrade — update to latest version");
|
|
219
|
+
console.error(" npx pairai version — show current version");
|
|
97
220
|
process.exit(1);
|
|
98
221
|
}
|
|
99
222
|
|
|
@@ -101,14 +224,18 @@ const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
|
101
224
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
102
225
|
const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
103
226
|
|
|
104
|
-
const
|
|
105
|
-
const
|
|
227
|
+
const serveArgs = args.slice(1);
|
|
228
|
+
const serveProviderIdx = serveArgs.indexOf("--provider");
|
|
229
|
+
const serveProvider = serveProviderIdx !== -1 ? serveArgs[serveProviderIdx + 1] : "claude";
|
|
230
|
+
|
|
231
|
+
const HUB_URL = process.env.PAIRAI_HUB_URL ?? process.env.PAIRAI_URL ?? "https://pairai.pro";
|
|
232
|
+
const API_KEY = process.env.PAIRAI_AGENT_CRED ?? process.env.PAIRAI_API_KEY;
|
|
106
233
|
const POLL_MS = Number(process.env.PAIRAI_POLL_MS ?? "5000");
|
|
107
|
-
const PRIVATE_KEY_PATH = process.env.PAIRAI_PRIVATE_KEY_PATH;
|
|
234
|
+
const PRIVATE_KEY_PATH = process.env.PAIRAI_KEY_FILE ?? process.env.PAIRAI_PRIVATE_KEY_PATH;
|
|
108
235
|
const PRIVATE_KEY = PRIVATE_KEY_PATH ? readFileSync(PRIVATE_KEY_PATH, "utf-8") : null;
|
|
109
236
|
|
|
110
237
|
if (!API_KEY) {
|
|
111
|
-
console.error('
|
|
238
|
+
console.error('PAIRAI_AGENT_CRED not set. Run "npx pairai setup" first.');
|
|
112
239
|
process.exit(1);
|
|
113
240
|
}
|
|
114
241
|
|
|
@@ -220,28 +347,30 @@ function localDecrypt(
|
|
|
220
347
|
|
|
221
348
|
// ── MCP Server ───────────────────────────────────────────────────────────────
|
|
222
349
|
|
|
350
|
+
const instructions = [
|
|
351
|
+
"You are connected to the pairai agent hub. Messages from other AI agents arrive as notifications.",
|
|
352
|
+
"The channel server polls for updates automatically — you don't need to poll manually.",
|
|
353
|
+
"",
|
|
354
|
+
"Notification attributes:",
|
|
355
|
+
" task_id — the task this message belongs to",
|
|
356
|
+
" task_title — short description of the task",
|
|
357
|
+
" from_agent — name of the agent who sent it",
|
|
358
|
+
" event_type — 'new_task' or 'new_message'",
|
|
359
|
+
"",
|
|
360
|
+
"When you receive a notification:",
|
|
361
|
+
" - To respond, use the reply tool with the task_id and your message.",
|
|
362
|
+
" - To accept a task, use the update_status tool with status 'working'.",
|
|
363
|
+
" - To finish a task, use the update_status tool with status 'completed'.",
|
|
364
|
+
" - To ask for more info, use update_status with 'input-required' and send a message.",
|
|
365
|
+
].join("\n");
|
|
366
|
+
|
|
367
|
+
const capabilities = serveProvider === "claude"
|
|
368
|
+
? { experimental: { "claude/channel": {} }, tools: {} }
|
|
369
|
+
: { tools: {} };
|
|
370
|
+
|
|
223
371
|
const mcp = new Server(
|
|
224
372
|
{ name: "pairai", version: "1.0.0" },
|
|
225
|
-
{
|
|
226
|
-
capabilities: {
|
|
227
|
-
experimental: { "claude/channel": {} },
|
|
228
|
-
tools: {},
|
|
229
|
-
},
|
|
230
|
-
instructions: [
|
|
231
|
-
'You are connected to the pairai agent hub. Messages from other AI agents arrive as <channel source="pairai" ...> notifications.',
|
|
232
|
-
"",
|
|
233
|
-
"Notification attributes:",
|
|
234
|
-
" task_id — the task this message belongs to",
|
|
235
|
-
" task_title — short description of the task",
|
|
236
|
-
" from_agent — name of the agent who sent it",
|
|
237
|
-
" event_type — 'new_task' or 'new_message'",
|
|
238
|
-
"",
|
|
239
|
-
"To respond, use the pairai_reply tool with the task_id and your message.",
|
|
240
|
-
"To accept a task, use pairai_update_status with status 'working'.",
|
|
241
|
-
"To finish a task, use pairai_update_status with status 'completed'.",
|
|
242
|
-
"To ask for more info, use pairai_update_status with 'input-required' and send a message.",
|
|
243
|
-
].join("\n"),
|
|
244
|
-
}
|
|
373
|
+
{ capabilities, instructions }
|
|
245
374
|
);
|
|
246
375
|
|
|
247
376
|
// ── Tools ────────────────────────────────────────────────────────────────────
|
|
@@ -334,6 +463,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
334
463
|
const taskData = (await hubGet(`/tasks/${task_id}`)) as any;
|
|
335
464
|
if (taskData.encrypted) {
|
|
336
465
|
// STRICT: never fall back to plaintext for encrypted tasks
|
|
466
|
+
await loadPublicKeys(); // refresh in case keys were added since last poll
|
|
337
467
|
const otherId =
|
|
338
468
|
taskData.initiatorAgentId === myAgentId ? taskData.targetAgentId : taskData.initiatorAgentId;
|
|
339
469
|
const otherPub = pubKeyCache.get(otherId);
|
|
@@ -372,10 +502,38 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
372
502
|
}
|
|
373
503
|
|
|
374
504
|
if (name === "pairai_create_task") {
|
|
505
|
+
const { target_agent_id, title, description } = args as {
|
|
506
|
+
target_agent_id: string; title: string; description?: string;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Auto-encrypt when both agents have keys and we have a private key
|
|
510
|
+
await loadPublicKeys();
|
|
511
|
+
const otherPub = pubKeyCache.get(target_agent_id);
|
|
512
|
+
if (PRIVATE_KEY && otherPub && myPublicKey) {
|
|
513
|
+
const { nanoid } = await import("nanoid");
|
|
514
|
+
const taskId = nanoid();
|
|
515
|
+
const payload = JSON.stringify({ title, description: description ?? "" });
|
|
516
|
+
const { ciphertext, signature, encryptedKeys } = localEncrypt(payload, taskId, {
|
|
517
|
+
[myAgentId]: myPublicKey,
|
|
518
|
+
[target_agent_id]: otherPub,
|
|
519
|
+
});
|
|
520
|
+
await hubPost("/tasks", {
|
|
521
|
+
id: taskId,
|
|
522
|
+
targetAgentId: target_agent_id,
|
|
523
|
+
title: "Encrypted Task",
|
|
524
|
+
description: ciphertext,
|
|
525
|
+
encrypted: true,
|
|
526
|
+
descriptionKeys: encryptedKeys,
|
|
527
|
+
senderSignature: signature,
|
|
528
|
+
});
|
|
529
|
+
return { content: [{ type: "text" as const, text: `Task created (encrypted). ID: ${taskId}` }] };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Fallback: plaintext (no keys available)
|
|
375
533
|
const data = await hubPost("/tasks", {
|
|
376
|
-
targetAgentId:
|
|
377
|
-
title
|
|
378
|
-
description
|
|
534
|
+
targetAgentId: target_agent_id,
|
|
535
|
+
title,
|
|
536
|
+
description,
|
|
379
537
|
});
|
|
380
538
|
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
381
539
|
}
|
|
@@ -400,6 +558,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
400
558
|
title: string;
|
|
401
559
|
description?: string;
|
|
402
560
|
};
|
|
561
|
+
// Refresh keys in case a new connection was established
|
|
562
|
+
await loadPublicKeys();
|
|
403
563
|
const otherPub = pubKeyCache.get(target_agent_id);
|
|
404
564
|
if (!otherPub || !myPublicKey)
|
|
405
565
|
return {
|