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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/pairai-channel.ts +102 -49
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pairai",
3
- "version": "0.1.2",
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,23 +77,59 @@ if (command === "setup") {
67
77
  writeFileSync(keyPath, privateKey, { mode: 0o600 });
68
78
  console.log(` Private key: ${keyPath}\n`);
69
79
 
70
- // Write .mcp.json directly (claude mcp add --env puts env vars in args, which breaks)
71
- const mcpConfig = {
72
- mcpServers: {
73
- "pairai-channel": {
74
- command: "npx",
75
- args: ["pairai", "serve"],
76
- env: { PAIRAI_API_KEY: apiKey, PAIRAI_URL: hubUrl, PAIRAI_PRIVATE_KEY_PATH: keyPath },
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
- const cwd = process.cwd();
82
- const mcpJsonPath = join(cwd, ".mcp.json");
83
- writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + "\n");
84
- console.log(` MCP config: ${mcpJsonPath}\n`);
85
- console.log(` Done! Start Claude Code with:\n`);
86
- console.log(` claude --dangerously-load-development-channels server:pairai-channel\n`);
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" register and configure');
96
- 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]");
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 HUB_URL = process.env.PAIRAI_URL ?? "https://pairai.pro";
105
- 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;
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('PAIRAI_API_KEY not set. Run "npx pairai setup" first.');
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 {