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 +1 -1
- package/package.json +3 -3
- package/{pairai-channel.ts → pairai.ts} +171 -30
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
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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}
|
|
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: [
|
|
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
|
|
109
|
-
console.log(
|
|
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
|
|
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: [
|
|
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(`
|
|
130
|
-
console.log(
|
|
131
|
-
console.log(`
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
283
|
-
"To
|
|
284
|
-
"To
|
|
285
|
-
"To
|
|
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:
|
|
428
|
-
title
|
|
429
|
-
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
|
}
|