pairai 0.2.0 → 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.2.0",
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:
5
+ * Commands:
6
6
  * npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]
7
- *
8
- * Runtime (spawned by Claude Code / Gemini CLI, not called manually):
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 {}
@@ -75,7 +132,20 @@ if (command === "setup") {
75
132
  mkdirSync(keyDir, { recursive: true });
76
133
  const keyPath = join(keyDir, `${id}.pem`);
77
134
  writeFileSync(keyPath, privateKey, { mode: 0o600 });
78
- console.log(` Private key: ${keyPath}\n`);
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();
79
149
 
80
150
  if (provider === "gemini") {
81
151
  // Write .gemini/settings.json (project or global)
@@ -96,7 +166,7 @@ if (command === "setup") {
96
166
  if (!existing.mcpServers) existing.mcpServers = {};
97
167
  existing.mcpServers.pairai = {
98
168
  command: "npx",
99
- args: ["pairai", "serve"],
169
+ args: [`pairai@${VERSION}`, "serve"],
100
170
  env: {
101
171
  PAIRAI_HUB_URL: hubUrl,
102
172
  PAIRAI_AGENT_CRED: apiKey,
@@ -105,19 +175,19 @@ if (command === "setup") {
105
175
  };
106
176
 
107
177
  writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n");
108
- console.log(` Config written to ${settingsPath}\n`);
109
- console.log(` Done! Next steps:`);
178
+ console.log(` Config: ${settingsPath}`);
179
+ console.log();
180
+ console.log(` Next steps:`);
110
181
  console.log(` 1. Restart Gemini CLI to activate the pairai MCP server`);
111
182
  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`);
183
+ console.log(` 3. Share the code with another agent to connect`);
114
184
  } else {
115
185
  // Write .mcp.json for Claude Code
116
186
  const mcpConfig = {
117
187
  mcpServers: {
118
188
  "pairai-channel": {
119
189
  command: "npx",
120
- args: ["pairai", "serve"],
190
+ args: [`pairai@${VERSION}`, "serve"],
121
191
  env: { PAIRAI_AGENT_CRED: apiKey, PAIRAI_HUB_URL: hubUrl, PAIRAI_KEY_FILE: keyPath },
122
192
  },
123
193
  },
@@ -126,20 +196,27 @@ if (command === "setup") {
126
196
  const cwd = process.cwd();
127
197
  const mcpJsonPath = join(cwd, ".mcp.json");
128
198
  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`);
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`);
132
205
  }
133
206
 
207
+ console.log();
134
208
  process.exit(0);
135
209
  }
136
210
 
137
211
  // ── Serve: stdio MCP channel server ──────────────────────────────────────────
138
212
 
139
213
  if (command !== "serve") {
214
+ console.error(`pairai v${VERSION}\n`);
140
215
  console.error("Usage:");
141
216
  console.error(' npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]');
142
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");
143
220
  process.exit(1);
144
221
  }
145
222
 
@@ -271,7 +348,8 @@ function localDecrypt(
271
348
  // ── MCP Server ───────────────────────────────────────────────────────────────
272
349
 
273
350
  const instructions = [
274
- 'You are connected to the pairai agent hub. Messages from other AI agents arrive as notifications.',
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.",
275
353
  "",
276
354
  "Notification attributes:",
277
355
  " task_id — the task this message belongs to",
@@ -279,10 +357,11 @@ const instructions = [
279
357
  " from_agent — name of the agent who sent it",
280
358
  " event_type — 'new_task' or 'new_message'",
281
359
  "",
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.",
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.",
286
365
  ].join("\n");
287
366
 
288
367
  const capabilities = serveProvider === "claude"
@@ -423,10 +502,38 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
423
502
  }
424
503
 
425
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)
426
533
  const data = await hubPost("/tasks", {
427
- targetAgentId: args.target_agent_id,
428
- title: args.title,
429
- description: args.description,
534
+ targetAgentId: target_agent_id,
535
+ title,
536
+ description,
430
537
  });
431
538
  return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
432
539
  }