pairai 0.1.2 → 0.2.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/package.json +1 -1
- package/pairai-channel.ts +102 -49
package/package.json
CHANGED
package/pairai-channel.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* pairai — connect AI agents via the pairai hub
|
|
4
4
|
*
|
|
5
5
|
* Setup:
|
|
6
|
-
* npx pairai setup "
|
|
7
|
-
* npx pairai setup "My Agent Name" --hub https://myhub.example.com
|
|
6
|
+
* npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]
|
|
8
7
|
*
|
|
9
|
-
* Runtime (spawned by Claude Code, not called manually):
|
|
10
|
-
* npx pairai serve
|
|
11
|
-
* Env:
|
|
8
|
+
* Runtime (spawned by Claude Code / Gemini CLI, not called manually):
|
|
9
|
+
* npx pairai serve [--provider claude|gemini]
|
|
10
|
+
* Env: PAIRAI_HUB_URL, PAIRAI_AGENT_CRED, PAIRAI_KEY_FILE, PAIRAI_POLL_MS
|
|
11
|
+
* Legacy: PAIRAI_URL, PAIRAI_API_KEY, PAIRAI_PRIVATE_KEY_PATH
|
|
12
12
|
*/
|
|
13
13
|
import { execSync } from "node:child_process";
|
|
14
14
|
import {
|
|
@@ -16,23 +16,33 @@ import {
|
|
|
16
16
|
publicEncrypt, privateDecrypt, sign, verify,
|
|
17
17
|
randomBytes, createCipheriv, createDecipheriv, constants,
|
|
18
18
|
} from "node:crypto";
|
|
19
|
-
import { writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
19
|
+
import { writeFileSync, mkdirSync, readFileSync, statSync, existsSync } from "node:fs";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
|
|
23
23
|
const args = process.argv.slice(2);
|
|
24
24
|
const command = args[0];
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
function detectProvider(): "claude" | "gemini" {
|
|
27
|
+
if (process.env.GEMINI_CLI) return "gemini";
|
|
28
|
+
try { if (statSync(".gemini").isDirectory()) return "gemini"; } catch {}
|
|
29
|
+
return "claude";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Setup: register + configure ──────────────────────────────────────────────
|
|
27
33
|
|
|
28
34
|
if (command === "setup") {
|
|
29
35
|
const rest = args.slice(1);
|
|
30
36
|
const hubIdx = rest.indexOf("--hub");
|
|
31
37
|
const hubUrl = hubIdx !== -1 ? rest.splice(hubIdx, 2)[1] : "https://pairai.pro";
|
|
38
|
+
const providerIdx = rest.indexOf("--provider");
|
|
39
|
+
const provider = providerIdx !== -1 ? rest.splice(providerIdx, 2)[1] as "claude" | "gemini" : detectProvider();
|
|
40
|
+
const globalIdx = rest.indexOf("--global");
|
|
41
|
+
const useGlobal = globalIdx !== -1 ? (rest.splice(globalIdx, 1), true) : false;
|
|
32
42
|
const agentName = rest.find((a) => !a.startsWith("--"));
|
|
33
43
|
|
|
34
44
|
if (!agentName) {
|
|
35
|
-
console.error('Usage: npx pairai setup "Agent Name" [--hub URL]');
|
|
45
|
+
console.error('Usage: npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]');
|
|
36
46
|
process.exit(1);
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -67,23 +77,59 @@ if (command === "setup") {
|
|
|
67
77
|
writeFileSync(keyPath, privateKey, { mode: 0o600 });
|
|
68
78
|
console.log(` Private key: ${keyPath}\n`);
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
if (provider === "gemini") {
|
|
81
|
+
// Write .gemini/settings.json (project or global)
|
|
82
|
+
const geminiDir = useGlobal
|
|
83
|
+
? join(homedir(), ".gemini")
|
|
84
|
+
: join(process.cwd(), ".gemini");
|
|
85
|
+
mkdirSync(geminiDir, { recursive: true });
|
|
86
|
+
const settingsPath = join(geminiDir, "settings.json");
|
|
87
|
+
|
|
88
|
+
// Merge with existing settings
|
|
89
|
+
let existing: any = {};
|
|
90
|
+
try {
|
|
91
|
+
if (existsSync(settingsPath)) {
|
|
92
|
+
existing = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
97
|
+
existing.mcpServers.pairai = {
|
|
98
|
+
command: "npx",
|
|
99
|
+
args: ["pairai", "serve"],
|
|
100
|
+
env: {
|
|
101
|
+
PAIRAI_HUB_URL: hubUrl,
|
|
102
|
+
PAIRAI_AGENT_CRED: apiKey,
|
|
103
|
+
PAIRAI_KEY_FILE: keyPath,
|
|
77
104
|
},
|
|
78
|
-
}
|
|
79
|
-
};
|
|
105
|
+
};
|
|
80
106
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n");
|
|
108
|
+
console.log(` Config written to ${settingsPath}\n`);
|
|
109
|
+
console.log(` Done! Next steps:`);
|
|
110
|
+
console.log(` 1. Restart Gemini CLI to activate the pairai MCP server`);
|
|
111
|
+
console.log(` 2. Ask Gemini: "Generate a pairing code"`);
|
|
112
|
+
console.log(` 3. Share the code with another agent to connect\n`);
|
|
113
|
+
console.log(` E2E encryption is enabled. Back up ${keyPath} — it cannot be recovered.\n`);
|
|
114
|
+
} else {
|
|
115
|
+
// Write .mcp.json for Claude Code
|
|
116
|
+
const mcpConfig = {
|
|
117
|
+
mcpServers: {
|
|
118
|
+
"pairai-channel": {
|
|
119
|
+
command: "npx",
|
|
120
|
+
args: ["pairai", "serve"],
|
|
121
|
+
env: { PAIRAI_AGENT_CRED: apiKey, PAIRAI_HUB_URL: hubUrl, PAIRAI_KEY_FILE: keyPath },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const cwd = process.cwd();
|
|
127
|
+
const mcpJsonPath = join(cwd, ".mcp.json");
|
|
128
|
+
writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
129
|
+
console.log(` MCP config: ${mcpJsonPath}\n`);
|
|
130
|
+
console.log(` Done! Start Claude Code with:\n`);
|
|
131
|
+
console.log(` claude --dangerously-load-development-channels server:pairai-channel\n`);
|
|
132
|
+
}
|
|
87
133
|
|
|
88
134
|
process.exit(0);
|
|
89
135
|
}
|
|
@@ -92,8 +138,8 @@ if (command === "setup") {
|
|
|
92
138
|
|
|
93
139
|
if (command !== "serve") {
|
|
94
140
|
console.error("Usage:");
|
|
95
|
-
console.error(' npx pairai setup "Agent Name"
|
|
96
|
-
console.error(" npx pairai serve
|
|
141
|
+
console.error(' npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]');
|
|
142
|
+
console.error(" npx pairai serve [--provider claude|gemini]");
|
|
97
143
|
process.exit(1);
|
|
98
144
|
}
|
|
99
145
|
|
|
@@ -101,14 +147,18 @@ const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
|
101
147
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
102
148
|
const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
103
149
|
|
|
104
|
-
const
|
|
105
|
-
const
|
|
150
|
+
const serveArgs = args.slice(1);
|
|
151
|
+
const serveProviderIdx = serveArgs.indexOf("--provider");
|
|
152
|
+
const serveProvider = serveProviderIdx !== -1 ? serveArgs[serveProviderIdx + 1] : "claude";
|
|
153
|
+
|
|
154
|
+
const HUB_URL = process.env.PAIRAI_HUB_URL ?? process.env.PAIRAI_URL ?? "https://pairai.pro";
|
|
155
|
+
const API_KEY = process.env.PAIRAI_AGENT_CRED ?? process.env.PAIRAI_API_KEY;
|
|
106
156
|
const POLL_MS = Number(process.env.PAIRAI_POLL_MS ?? "5000");
|
|
107
|
-
const PRIVATE_KEY_PATH = process.env.PAIRAI_PRIVATE_KEY_PATH;
|
|
157
|
+
const PRIVATE_KEY_PATH = process.env.PAIRAI_KEY_FILE ?? process.env.PAIRAI_PRIVATE_KEY_PATH;
|
|
108
158
|
const PRIVATE_KEY = PRIVATE_KEY_PATH ? readFileSync(PRIVATE_KEY_PATH, "utf-8") : null;
|
|
109
159
|
|
|
110
160
|
if (!API_KEY) {
|
|
111
|
-
console.error('
|
|
161
|
+
console.error('PAIRAI_AGENT_CRED not set. Run "npx pairai setup" first.');
|
|
112
162
|
process.exit(1);
|
|
113
163
|
}
|
|
114
164
|
|
|
@@ -220,28 +270,28 @@ function localDecrypt(
|
|
|
220
270
|
|
|
221
271
|
// ── MCP Server ───────────────────────────────────────────────────────────────
|
|
222
272
|
|
|
273
|
+
const instructions = [
|
|
274
|
+
'You are connected to the pairai agent hub. Messages from other AI agents arrive as notifications.',
|
|
275
|
+
"",
|
|
276
|
+
"Notification attributes:",
|
|
277
|
+
" task_id — the task this message belongs to",
|
|
278
|
+
" task_title — short description of the task",
|
|
279
|
+
" from_agent — name of the agent who sent it",
|
|
280
|
+
" event_type — 'new_task' or 'new_message'",
|
|
281
|
+
"",
|
|
282
|
+
"To respond, use the reply tool with the task_id and your message.",
|
|
283
|
+
"To accept a task, use the update_status tool with status 'working'.",
|
|
284
|
+
"To finish a task, use the update_status tool with status 'completed'.",
|
|
285
|
+
"To ask for more info, use the update_status tool with 'input-required' and send a message.",
|
|
286
|
+
].join("\n");
|
|
287
|
+
|
|
288
|
+
const capabilities = serveProvider === "claude"
|
|
289
|
+
? { experimental: { "claude/channel": {} }, tools: {} }
|
|
290
|
+
: { tools: {} };
|
|
291
|
+
|
|
223
292
|
const mcp = new Server(
|
|
224
293
|
{ 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
|
-
}
|
|
294
|
+
{ capabilities, instructions }
|
|
245
295
|
);
|
|
246
296
|
|
|
247
297
|
// ── Tools ────────────────────────────────────────────────────────────────────
|
|
@@ -334,6 +384,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
334
384
|
const taskData = (await hubGet(`/tasks/${task_id}`)) as any;
|
|
335
385
|
if (taskData.encrypted) {
|
|
336
386
|
// STRICT: never fall back to plaintext for encrypted tasks
|
|
387
|
+
await loadPublicKeys(); // refresh in case keys were added since last poll
|
|
337
388
|
const otherId =
|
|
338
389
|
taskData.initiatorAgentId === myAgentId ? taskData.targetAgentId : taskData.initiatorAgentId;
|
|
339
390
|
const otherPub = pubKeyCache.get(otherId);
|
|
@@ -400,6 +451,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
400
451
|
title: string;
|
|
401
452
|
description?: string;
|
|
402
453
|
};
|
|
454
|
+
// Refresh keys in case a new connection was established
|
|
455
|
+
await loadPublicKeys();
|
|
403
456
|
const otherPub = pubKeyCache.get(target_agent_id);
|
|
404
457
|
if (!otherPub || !myPublicKey)
|
|
405
458
|
return {
|