pairai 0.2.0 → 0.2.2

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.2.0",
4
- "description": "Connect AI agents to collaborate via the pairai hub — channel server for Claude Code",
3
+ "version": "0.2.2",
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 "Agent Name" [--hub URL] [--provider claude|gemini] [--global]
7
- *
8
- * Runtime (spawned by Claude Code / Gemini CLI, not called manually):
5
+ * Commands:
6
+ * npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global] [--force]
9
7
  * 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
8
+ * npx pairai upgrade — update to latest version (preserves keys and config)
9
+ * npx pairai version — show current version
10
+ *
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 {
@@ -18,11 +19,67 @@ import {
18
19
  } from "node:crypto";
19
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
 
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
+
26
83
  function detectProvider(): "claude" | "gemini" {
27
84
  if (process.env.GEMINI_CLI) return "gemini";
28
85
  try { if (statSync(".gemini").isDirectory()) return "gemini"; } catch {}
@@ -36,16 +93,41 @@ if (command === "setup") {
36
93
  const hubIdx = rest.indexOf("--hub");
37
94
  const hubUrl = hubIdx !== -1 ? rest.splice(hubIdx, 2)[1] : "https://pairai.pro";
38
95
  const providerIdx = rest.indexOf("--provider");
39
- const provider = providerIdx !== -1 ? rest.splice(providerIdx, 2)[1] as "claude" | "gemini" : detectProvider();
96
+ const providerArg = providerIdx !== -1 ? rest.splice(providerIdx, 2)[1] : undefined;
97
+ if (providerArg && providerArg !== "claude" && providerArg !== "gemini") {
98
+ console.error(` Unknown provider "${providerArg}". Must be "claude" or "gemini".`);
99
+ process.exit(1);
100
+ }
101
+ const provider = providerArg ?? detectProvider();
40
102
  const globalIdx = rest.indexOf("--global");
41
103
  const useGlobal = globalIdx !== -1 ? (rest.splice(globalIdx, 1), true) : false;
42
104
  const agentName = rest.find((a) => !a.startsWith("--"));
43
105
 
106
+ const forceIdx = rest.indexOf("--force");
107
+ const useForce = forceIdx !== -1 ? (rest.splice(forceIdx, 1), true) : false;
44
108
  if (!agentName) {
45
- console.error('Usage: npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]');
109
+ console.error('Usage: npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global] [--force]');
46
110
  process.exit(1);
47
111
  }
48
112
 
113
+ // Check for existing config to avoid accidental overwrites
114
+ const existingConfigPath = provider === "gemini"
115
+ ? join(useGlobal ? join(homedir(), ".gemini") : join(process.cwd(), ".gemini"), "settings.json")
116
+ : join(process.cwd(), ".mcp.json");
117
+ const mcpKey = provider === "gemini" ? "pairai" : "pairai-channel";
118
+ if (!useForce && existsSync(existingConfigPath)) {
119
+ try {
120
+ const existing = JSON.parse(readFileSync(existingConfigPath, "utf-8"));
121
+ const servers = existing.mcpServers ?? {};
122
+ if (servers[mcpKey]) {
123
+ console.error(`\n pairai is already configured in ${existingConfigPath}`);
124
+ console.error(` Running setup again would overwrite the existing API key and config.`);
125
+ console.error(`\n To force a fresh setup, run: npx pairai setup "${agentName}" --force\n`);
126
+ process.exit(1);
127
+ }
128
+ } catch {}
129
+ }
130
+
49
131
  console.log(`\n Registering "${agentName}" on ${hubUrl}...\n`);
50
132
 
51
133
  console.log(" Generating RSA-4096 keypair...");
@@ -75,7 +157,24 @@ if (command === "setup") {
75
157
  mkdirSync(keyDir, { recursive: true });
76
158
  const keyPath = join(keyDir, `${id}.pem`);
77
159
  writeFileSync(keyPath, privateKey, { mode: 0o600 });
78
- console.log(` Private key: ${keyPath}\n`);
160
+ console.log(` Private key: ${keyPath}`);
161
+ const lines = [
162
+ "BACK UP YOUR PRIVATE KEY",
163
+ "",
164
+ keyPath,
165
+ "",
166
+ "This key is stored only on your machine.",
167
+ "The hub never sees it. If lost, you must re-register",
168
+ "and re-pair — all encrypted history becomes unreadable.",
169
+ "",
170
+ "Copy it to a password manager or secure backup now.",
171
+ ];
172
+ const w = Math.max(...lines.map((l) => l.length)) + 2;
173
+ console.log();
174
+ console.log(` ┌${"─".repeat(w + 2)}┐`);
175
+ for (const l of lines) console.log(` │ ${l.padEnd(w)}│`);
176
+ console.log(` └${"─".repeat(w + 2)}┘`);
177
+ console.log();
79
178
 
80
179
  if (provider === "gemini") {
81
180
  // Write .gemini/settings.json (project or global)
@@ -96,7 +195,7 @@ if (command === "setup") {
96
195
  if (!existing.mcpServers) existing.mcpServers = {};
97
196
  existing.mcpServers.pairai = {
98
197
  command: "npx",
99
- args: ["pairai", "serve"],
198
+ args: [`pairai@${VERSION}`, "serve"],
100
199
  env: {
101
200
  PAIRAI_HUB_URL: hubUrl,
102
201
  PAIRAI_AGENT_CRED: apiKey,
@@ -105,19 +204,19 @@ if (command === "setup") {
105
204
  };
106
205
 
107
206
  writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n");
108
- console.log(` Config written to ${settingsPath}\n`);
109
- console.log(` Done! Next steps:`);
207
+ console.log(` Config: ${settingsPath}`);
208
+ console.log();
209
+ console.log(` Next steps:`);
110
210
  console.log(` 1. Restart Gemini CLI to activate the pairai MCP server`);
111
211
  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`);
212
+ console.log(` 3. Share the code with another agent to connect`);
114
213
  } else {
115
214
  // Write .mcp.json for Claude Code
116
215
  const mcpConfig = {
117
216
  mcpServers: {
118
217
  "pairai-channel": {
119
218
  command: "npx",
120
- args: ["pairai", "serve"],
219
+ args: [`pairai@${VERSION}`, "serve"],
121
220
  env: { PAIRAI_AGENT_CRED: apiKey, PAIRAI_HUB_URL: hubUrl, PAIRAI_KEY_FILE: keyPath },
122
221
  },
123
222
  },
@@ -126,20 +225,27 @@ if (command === "setup") {
126
225
  const cwd = process.cwd();
127
226
  const mcpJsonPath = join(cwd, ".mcp.json");
128
227
  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`);
228
+ console.log(` Config: ${mcpJsonPath}`);
229
+ console.log();
230
+ console.log(` Next steps:`);
231
+ console.log(` 1. Start Claude Code in this directory`);
232
+ console.log(` 2. Ask Claude: "Generate a pairing code"`);
233
+ console.log(` 3. Share the code with another agent to connect`);
132
234
  }
133
235
 
236
+ console.log();
134
237
  process.exit(0);
135
238
  }
136
239
 
137
240
  // ── Serve: stdio MCP channel server ──────────────────────────────────────────
138
241
 
139
242
  if (command !== "serve") {
243
+ console.error(`pairai v${VERSION}\n`);
140
244
  console.error("Usage:");
141
- console.error(' npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]');
245
+ console.error(' npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global] [--force]');
142
246
  console.error(" npx pairai serve [--provider claude|gemini]");
247
+ console.error(" npx pairai upgrade — update to latest version");
248
+ console.error(" npx pairai version — show current version");
143
249
  process.exit(1);
144
250
  }
145
251
 
@@ -149,7 +255,12 @@ const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelco
149
255
 
150
256
  const serveArgs = args.slice(1);
151
257
  const serveProviderIdx = serveArgs.indexOf("--provider");
152
- const serveProvider = serveProviderIdx !== -1 ? serveArgs[serveProviderIdx + 1] : "claude";
258
+ const serveProviderArg = serveProviderIdx !== -1 ? serveArgs[serveProviderIdx + 1] : undefined;
259
+ if (serveProviderArg && serveProviderArg !== "claude" && serveProviderArg !== "gemini") {
260
+ console.error(` Unknown provider "${serveProviderArg}". Must be "claude" or "gemini".`);
261
+ process.exit(1);
262
+ }
263
+ const serveProvider = serveProviderArg ?? "claude";
153
264
 
154
265
  const HUB_URL = process.env.PAIRAI_HUB_URL ?? process.env.PAIRAI_URL ?? "https://pairai.pro";
155
266
  const API_KEY = process.env.PAIRAI_AGENT_CRED ?? process.env.PAIRAI_API_KEY;
@@ -271,7 +382,8 @@ function localDecrypt(
271
382
  // ── MCP Server ───────────────────────────────────────────────────────────────
272
383
 
273
384
  const instructions = [
274
- 'You are connected to the pairai agent hub. Messages from other AI agents arrive as notifications.',
385
+ "You are connected to the pairai agent hub. Messages from other AI agents arrive as notifications.",
386
+ "The channel server polls for updates automatically — you don't need to poll manually.",
275
387
  "",
276
388
  "Notification attributes:",
277
389
  " task_id — the task this message belongs to",
@@ -279,10 +391,11 @@ const instructions = [
279
391
  " from_agent — name of the agent who sent it",
280
392
  " event_type — 'new_task' or 'new_message'",
281
393
  "",
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.",
394
+ "When you receive a notification:",
395
+ " - To respond, use the reply tool with the task_id and your message.",
396
+ " - To accept a task, use the update_status tool with status 'working'.",
397
+ " - To finish a task, use the update_status tool with status 'completed'.",
398
+ " - To ask for more info, use update_status with 'input-required' and send a message.",
286
399
  ].join("\n");
287
400
 
288
401
  const capabilities = serveProvider === "claude"
@@ -423,10 +536,38 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
423
536
  }
424
537
 
425
538
  if (name === "pairai_create_task") {
539
+ const { target_agent_id, title, description } = args as {
540
+ target_agent_id: string; title: string; description?: string;
541
+ };
542
+
543
+ // Auto-encrypt when both agents have keys and we have a private key
544
+ await loadPublicKeys();
545
+ const otherPub = pubKeyCache.get(target_agent_id);
546
+ if (PRIVATE_KEY && otherPub && myPublicKey) {
547
+ const { nanoid } = await import("nanoid");
548
+ const taskId = nanoid();
549
+ const payload = JSON.stringify({ title, description: description ?? "" });
550
+ const { ciphertext, signature, encryptedKeys } = localEncrypt(payload, taskId, {
551
+ [myAgentId]: myPublicKey,
552
+ [target_agent_id]: otherPub,
553
+ });
554
+ await hubPost("/tasks", {
555
+ id: taskId,
556
+ targetAgentId: target_agent_id,
557
+ title: "Encrypted Task",
558
+ description: ciphertext,
559
+ encrypted: true,
560
+ descriptionKeys: encryptedKeys,
561
+ senderSignature: signature,
562
+ });
563
+ return { content: [{ type: "text" as const, text: `Task created (encrypted). ID: ${taskId}` }] };
564
+ }
565
+
566
+ // Fallback: plaintext (no keys available)
426
567
  const data = await hubPost("/tasks", {
427
- targetAgentId: args.target_agent_id,
428
- title: args.title,
429
- description: args.description,
568
+ targetAgentId: target_agent_id,
569
+ title,
570
+ description,
430
571
  });
431
572
  return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
432
573
  }