pairai 0.1.1 → 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 +103 -66
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,38 +77,58 @@ 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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
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 },
|
|
96
122
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
);
|
|
101
|
-
|
|
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`);
|
|
102
132
|
}
|
|
103
133
|
|
|
104
134
|
process.exit(0);
|
|
@@ -108,8 +138,8 @@ if (command === "setup") {
|
|
|
108
138
|
|
|
109
139
|
if (command !== "serve") {
|
|
110
140
|
console.error("Usage:");
|
|
111
|
-
console.error(' npx pairai setup "Agent Name"
|
|
112
|
-
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]");
|
|
113
143
|
process.exit(1);
|
|
114
144
|
}
|
|
115
145
|
|
|
@@ -117,14 +147,18 @@ const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
|
|
|
117
147
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
118
148
|
const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
|
|
119
149
|
|
|
120
|
-
const
|
|
121
|
-
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;
|
|
122
156
|
const POLL_MS = Number(process.env.PAIRAI_POLL_MS ?? "5000");
|
|
123
|
-
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;
|
|
124
158
|
const PRIVATE_KEY = PRIVATE_KEY_PATH ? readFileSync(PRIVATE_KEY_PATH, "utf-8") : null;
|
|
125
159
|
|
|
126
160
|
if (!API_KEY) {
|
|
127
|
-
console.error('
|
|
161
|
+
console.error('PAIRAI_AGENT_CRED not set. Run "npx pairai setup" first.');
|
|
128
162
|
process.exit(1);
|
|
129
163
|
}
|
|
130
164
|
|
|
@@ -144,7 +178,7 @@ async function hubGet(path: string) {
|
|
|
144
178
|
async function hubPost(path: string, body?: unknown) {
|
|
145
179
|
const res = await fetch(`${HUB_URL}${path}`, {
|
|
146
180
|
method: "POST",
|
|
147
|
-
headers,
|
|
181
|
+
headers: body ? headers : { Authorization: headers.Authorization },
|
|
148
182
|
body: body ? JSON.stringify(body) : undefined,
|
|
149
183
|
});
|
|
150
184
|
if (!res.ok) throw new Error(`POST ${path}: ${res.status}`);
|
|
@@ -236,28 +270,28 @@ function localDecrypt(
|
|
|
236
270
|
|
|
237
271
|
// ── MCP Server ───────────────────────────────────────────────────────────────
|
|
238
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
|
+
|
|
239
292
|
const mcp = new Server(
|
|
240
293
|
{ name: "pairai", version: "1.0.0" },
|
|
241
|
-
{
|
|
242
|
-
capabilities: {
|
|
243
|
-
experimental: { "claude/channel": {} },
|
|
244
|
-
tools: {},
|
|
245
|
-
},
|
|
246
|
-
instructions: [
|
|
247
|
-
'You are connected to the pairai agent hub. Messages from other AI agents arrive as <channel source="pairai" ...> notifications.',
|
|
248
|
-
"",
|
|
249
|
-
"Notification attributes:",
|
|
250
|
-
" task_id — the task this message belongs to",
|
|
251
|
-
" task_title — short description of the task",
|
|
252
|
-
" from_agent — name of the agent who sent it",
|
|
253
|
-
" event_type — 'new_task' or 'new_message'",
|
|
254
|
-
"",
|
|
255
|
-
"To respond, use the pairai_reply tool with the task_id and your message.",
|
|
256
|
-
"To accept a task, use pairai_update_status with status 'working'.",
|
|
257
|
-
"To finish a task, use pairai_update_status with status 'completed'.",
|
|
258
|
-
"To ask for more info, use pairai_update_status with 'input-required' and send a message.",
|
|
259
|
-
].join("\n"),
|
|
260
|
-
}
|
|
294
|
+
{ capabilities, instructions }
|
|
261
295
|
);
|
|
262
296
|
|
|
263
297
|
// ── Tools ────────────────────────────────────────────────────────────────────
|
|
@@ -350,6 +384,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
350
384
|
const taskData = (await hubGet(`/tasks/${task_id}`)) as any;
|
|
351
385
|
if (taskData.encrypted) {
|
|
352
386
|
// STRICT: never fall back to plaintext for encrypted tasks
|
|
387
|
+
await loadPublicKeys(); // refresh in case keys were added since last poll
|
|
353
388
|
const otherId =
|
|
354
389
|
taskData.initiatorAgentId === myAgentId ? taskData.targetAgentId : taskData.initiatorAgentId;
|
|
355
390
|
const otherPub = pubKeyCache.get(otherId);
|
|
@@ -416,6 +451,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
416
451
|
title: string;
|
|
417
452
|
description?: string;
|
|
418
453
|
};
|
|
454
|
+
// Refresh keys in case a new connection was established
|
|
455
|
+
await loadPublicKeys();
|
|
419
456
|
const otherPub = pubKeyCache.get(target_agent_id);
|
|
420
457
|
if (!otherPub || !myPublicKey)
|
|
421
458
|
return {
|