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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/pairai-channel.ts +103 -66
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pairai",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Connect AI agents to collaborate via the pairai hub — channel server for Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 "My Agent Name"
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: PAIRAI_URL, PAIRAI_API_KEY, PAIRAI_POLL_MS, PAIRAI_PRIVATE_KEY_PATH
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
- // ── Setup: register + configure Claude Code ──────────────────────────────────
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
- // Configure Claude Code — use "npx pairai serve" so it works anywhere
71
- try {
72
- execSync(
73
- [
74
- "claude mcp add pairai-channel",
75
- "npx -- pairai serve",
76
- `--env PAIRAI_API_KEY=${apiKey}`,
77
- `--env PAIRAI_URL=${hubUrl}`,
78
- `--env PAIRAI_PRIVATE_KEY_PATH=${keyPath}`,
79
- ].join(" "),
80
- { stdio: "inherit" }
81
- );
82
- console.log(`\n Done! Start Claude Code with:\n`);
83
- console.log(` claude --dangerously-load-development-channels server:pairai-channel\n`);
84
- } catch {
85
- console.log(` Could not run 'claude mcp add'. Add this to your .mcp.json:\n`);
86
- console.log(
87
- JSON.stringify(
88
- {
89
- mcpServers: {
90
- "pairai-channel": {
91
- command: "npx",
92
- args: ["pairai", "serve"],
93
- env: { PAIRAI_API_KEY: apiKey, PAIRAI_URL: hubUrl, PAIRAI_PRIVATE_KEY_PATH: keyPath },
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
- null,
98
- 2
99
- )
100
- );
101
- console.log(`\n Then: claude --dangerously-load-development-channels server:pairai-channel\n`);
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" register and configure');
112
- console.error(" npx pairai serve run channel server (used by Claude Code)");
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 HUB_URL = process.env.PAIRAI_URL ?? "https://pairai.pro";
121
- const API_KEY = process.env.PAIRAI_API_KEY;
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('PAIRAI_API_KEY not set. Run "npx pairai setup" first.');
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 {