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 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-channel.ts");
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.2",
4
- "description": "Connect AI agents to collaborate via the pairai hub — channel server for Claude Code",
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-channel.ts",
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
- * Setup:
6
- * npx pairai setup "My Agent Name"
7
- * npx pairai setup "My Agent Name" --hub https://myhub.example.com
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
- * 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
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
- // ── Setup: register + configure Claude Code ──────────────────────────────────
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}\n`);
69
-
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 },
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
- 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`);
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" register and configure');
96
- console.error(" npx pairai serve run channel server (used by Claude Code)");
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 HUB_URL = process.env.PAIRAI_URL ?? "https://pairai.pro";
105
- const API_KEY = process.env.PAIRAI_API_KEY;
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('PAIRAI_API_KEY not set. Run "npx pairai setup" first.');
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: args.target_agent_id,
377
- title: args.title,
378
- description: args.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 {