pairai 0.2.3 → 0.2.5
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/lib.ts +80 -0
- package/package.json +2 -1
- package/pairai.ts +62 -53
package/lib.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure/testable functions extracted from pairai CLI.
|
|
3
|
+
* Imported by both pairai.ts and unit tests.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate the --provider flag value.
|
|
10
|
+
* Returns the validated provider or throws with a message.
|
|
11
|
+
*/
|
|
12
|
+
export function validateProvider(value: string): "claude" | "gemini" {
|
|
13
|
+
if (value !== "claude" && value !== "gemini") {
|
|
14
|
+
throw new Error(`Unknown provider "${value}". Must be "claude" or "gemini".`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Auto-detect provider based on environment and filesystem.
|
|
21
|
+
*/
|
|
22
|
+
export function detectProvider(): "claude" | "gemini" {
|
|
23
|
+
if (process.env.GEMINI_CLI) return "gemini";
|
|
24
|
+
try { if (statSync(".gemini").isDirectory()) return "gemini"; } catch {}
|
|
25
|
+
return "claude";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Replace any `pairai@x.y.z` version pin in a config string with a new version.
|
|
30
|
+
*/
|
|
31
|
+
export function updateVersionInConfig(content: string, latest: string): string {
|
|
32
|
+
return content.replace(/pairai@\d+\.\d+\.\d+/g, `pairai@${latest}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if pairai is already configured in a config file.
|
|
37
|
+
* Returns the path if config exists with a pairai entry, null otherwise.
|
|
38
|
+
*/
|
|
39
|
+
export function checkExistingConfig(
|
|
40
|
+
provider: "claude" | "gemini",
|
|
41
|
+
cwd: string,
|
|
42
|
+
homeDir: string,
|
|
43
|
+
useGlobal: boolean,
|
|
44
|
+
): string | null {
|
|
45
|
+
const configPath = provider === "gemini"
|
|
46
|
+
? join(useGlobal ? join(homeDir, ".gemini") : join(cwd, ".gemini"), "settings.json")
|
|
47
|
+
: join(cwd, ".mcp.json");
|
|
48
|
+
const mcpKey = provider === "gemini" ? "pairai" : "pairai-channel";
|
|
49
|
+
|
|
50
|
+
if (!existsSync(configPath)) return null;
|
|
51
|
+
try {
|
|
52
|
+
const existing = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
53
|
+
const servers = existing.mcpServers ?? {};
|
|
54
|
+
if (servers[mcpKey]) return configPath;
|
|
55
|
+
} catch {}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the dynamic-width box for the private key backup warning.
|
|
61
|
+
*/
|
|
62
|
+
export function formatKeyBackupBox(keyPath: string): string[] {
|
|
63
|
+
const lines = [
|
|
64
|
+
"BACK UP YOUR PRIVATE KEY",
|
|
65
|
+
"",
|
|
66
|
+
keyPath,
|
|
67
|
+
"",
|
|
68
|
+
"This key is stored only on your machine.",
|
|
69
|
+
"The hub never sees it. If lost, you must re-register",
|
|
70
|
+
"and re-pair \u2014 all encrypted history becomes unreadable.",
|
|
71
|
+
"",
|
|
72
|
+
"Copy it to a password manager or secure backup now.",
|
|
73
|
+
];
|
|
74
|
+
const w = Math.max(...lines.map((l) => l.length)) + 2;
|
|
75
|
+
const out: string[] = [];
|
|
76
|
+
out.push(` \u250C${"\u2500".repeat(w + 2)}\u2510`);
|
|
77
|
+
for (const l of lines) out.push(` \u2502 ${l.padEnd(w)}\u2502`);
|
|
78
|
+
out.push(` \u2514${"\u2500".repeat(w + 2)}\u2518`);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pairai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "pairai CLI — connect AI agents to collaborate via the pairai hub",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"files": [
|
|
25
25
|
"bin.js",
|
|
26
26
|
"pairai.ts",
|
|
27
|
+
"lib.ts",
|
|
27
28
|
"README.md"
|
|
28
29
|
],
|
|
29
30
|
"dependencies": {
|
package/pairai.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { writeFileSync, mkdirSync, readFileSync, statSync, existsSync } from "no
|
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { join, dirname } from "node:path";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { validateProvider, detectProvider, updateVersionInConfig, checkExistingConfig, formatKeyBackupBox } from "./lib.js";
|
|
24
25
|
|
|
25
26
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
27
|
const PKG = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
|
|
@@ -62,10 +63,7 @@ if (command === "upgrade") {
|
|
|
62
63
|
try {
|
|
63
64
|
if (!existsSync(p)) continue;
|
|
64
65
|
const content = readFileSync(p, "utf-8");
|
|
65
|
-
const updated = content
|
|
66
|
-
new RegExp(`pairai@${VERSION.replace(/\./g, "\\.")}`, "g"),
|
|
67
|
-
`pairai@${latest}`,
|
|
68
|
-
);
|
|
66
|
+
const updated = updateVersionInConfig(content, latest);
|
|
69
67
|
if (updated !== content) {
|
|
70
68
|
writeFileSync(p, updated);
|
|
71
69
|
console.log(` Updated version in ${p}`);
|
|
@@ -80,11 +78,8 @@ if (command === "upgrade") {
|
|
|
80
78
|
process.exit(0);
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
try { if (statSync(".gemini").isDirectory()) return "gemini"; } catch {}
|
|
86
|
-
return "claude";
|
|
87
|
-
}
|
|
81
|
+
// detectProvider, validateProvider, updateVersionInConfig, checkExistingConfig,
|
|
82
|
+
// formatKeyBackupBox are imported from ./lib.js
|
|
88
83
|
|
|
89
84
|
// ── Setup: register + configure ──────────────────────────────────────────────
|
|
90
85
|
|
|
@@ -94,11 +89,13 @@ if (command === "setup") {
|
|
|
94
89
|
const hubUrl = hubIdx !== -1 ? rest.splice(hubIdx, 2)[1] : "https://pairai.pro";
|
|
95
90
|
const providerIdx = rest.indexOf("--provider");
|
|
96
91
|
const providerArg = providerIdx !== -1 ? rest.splice(providerIdx, 2)[1] : undefined;
|
|
97
|
-
if (providerArg
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
if (providerArg) {
|
|
93
|
+
try { validateProvider(providerArg); } catch (e) {
|
|
94
|
+
console.error(` ${(e as Error).message}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
100
97
|
}
|
|
101
|
-
const provider = providerArg ?? detectProvider();
|
|
98
|
+
const provider = (providerArg as "claude" | "gemini") ?? detectProvider();
|
|
102
99
|
const globalIdx = rest.indexOf("--global");
|
|
103
100
|
const useGlobal = globalIdx !== -1 ? (rest.splice(globalIdx, 1), true) : false;
|
|
104
101
|
const agentName = rest.find((a) => !a.startsWith("--"));
|
|
@@ -111,21 +108,14 @@ if (command === "setup") {
|
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
// Check for existing config to avoid accidental overwrites
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 {}
|
|
111
|
+
if (!useForce) {
|
|
112
|
+
const existingConfigPath = checkExistingConfig(provider, process.cwd(), homedir(), useGlobal);
|
|
113
|
+
if (existingConfigPath) {
|
|
114
|
+
console.error(`\n pairai is already configured in ${existingConfigPath}`);
|
|
115
|
+
console.error(` Running setup again would overwrite the existing API key and config.`);
|
|
116
|
+
console.error(`\n To force a fresh setup, run: npx pairai setup "${agentName}" --force\n`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
129
119
|
}
|
|
130
120
|
|
|
131
121
|
console.log(`\n Registering "${agentName}" on ${hubUrl}...\n`);
|
|
@@ -158,22 +148,8 @@ if (command === "setup") {
|
|
|
158
148
|
const keyPath = join(keyDir, `${id}.pem`);
|
|
159
149
|
writeFileSync(keyPath, privateKey, { mode: 0o600 });
|
|
160
150
|
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
151
|
console.log();
|
|
174
|
-
console.log(
|
|
175
|
-
for (const l of lines) console.log(` │ ${l.padEnd(w)}│`);
|
|
176
|
-
console.log(` └${"─".repeat(w + 2)}┘`);
|
|
152
|
+
for (const line of formatKeyBackupBox(keyPath)) console.log(line);
|
|
177
153
|
console.log();
|
|
178
154
|
|
|
179
155
|
if (provider === "gemini") {
|
|
@@ -256,11 +232,13 @@ const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelco
|
|
|
256
232
|
const serveArgs = args.slice(1);
|
|
257
233
|
const serveProviderIdx = serveArgs.indexOf("--provider");
|
|
258
234
|
const serveProviderArg = serveProviderIdx !== -1 ? serveArgs[serveProviderIdx + 1] : undefined;
|
|
259
|
-
if (serveProviderArg
|
|
260
|
-
|
|
261
|
-
|
|
235
|
+
if (serveProviderArg) {
|
|
236
|
+
try { validateProvider(serveProviderArg); } catch (e) {
|
|
237
|
+
console.error(` ${(e as Error).message}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
262
240
|
}
|
|
263
|
-
const serveProvider = serveProviderArg ?? "claude";
|
|
241
|
+
const serveProvider = (serveProviderArg as "claude" | "gemini") ?? "claude";
|
|
264
242
|
|
|
265
243
|
const HUB_URL = process.env.PAIRAI_HUB_URL ?? process.env.PAIRAI_URL ?? "https://pairai.pro";
|
|
266
244
|
const API_KEY = process.env.PAIRAI_AGENT_CRED ?? process.env.PAIRAI_API_KEY;
|
|
@@ -671,14 +649,45 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
671
649
|
}
|
|
672
650
|
|
|
673
651
|
if (name === "pairai_list_tasks") {
|
|
674
|
-
|
|
652
|
+
await loadPublicKeys();
|
|
653
|
+
const data = (await hubGet("/tasks")) as Array<{
|
|
654
|
+
id: string; status: string; title: string; encrypted?: boolean;
|
|
655
|
+
description?: string; descriptionKeys?: any; senderSignature?: string; initiatorAgentId?: string;
|
|
656
|
+
}>;
|
|
675
657
|
const filtered = args.status ? data.filter((t) => t.status === args.status) : data;
|
|
676
|
-
|
|
658
|
+
const decrypted = filtered.map((t) => {
|
|
659
|
+
if (t.encrypted) {
|
|
660
|
+
const desc = decryptTaskDescription(t, t.id);
|
|
661
|
+
return { ...t, title: desc.split("\n")[0] || t.title, description: desc };
|
|
662
|
+
}
|
|
663
|
+
return t;
|
|
664
|
+
});
|
|
665
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(decrypted, null, 2) }] };
|
|
677
666
|
}
|
|
678
667
|
|
|
679
668
|
if (name === "pairai_get_task") {
|
|
680
|
-
|
|
681
|
-
|
|
669
|
+
await loadPublicKeys();
|
|
670
|
+
const data = (await hubGet(`/tasks/${args.task_id}`)) as {
|
|
671
|
+
id: string; encrypted?: boolean; description?: string; descriptionKeys?: any;
|
|
672
|
+
senderSignature?: string; initiatorAgentId?: string;
|
|
673
|
+
[key: string]: unknown;
|
|
674
|
+
};
|
|
675
|
+
const msgs = (await hubGet(`/tasks/${args.task_id}/messages`)) as Array<{
|
|
676
|
+
id: string; content: string; contentType: string; senderAgentId: string;
|
|
677
|
+
encryptedKeys?: any; senderSignature?: string;
|
|
678
|
+
}>;
|
|
679
|
+
if (data.encrypted) {
|
|
680
|
+
const desc = decryptTaskDescription(data, data.id);
|
|
681
|
+
data.description = desc;
|
|
682
|
+
}
|
|
683
|
+
const decryptedMsgs = msgs.map((m) => {
|
|
684
|
+
if (data.encrypted) {
|
|
685
|
+
const d = decryptMessage(m, data.id);
|
|
686
|
+
return { ...m, content: d.content, contentType: d.contentType };
|
|
687
|
+
}
|
|
688
|
+
return m;
|
|
689
|
+
});
|
|
690
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ ...data, messages: decryptedMsgs }, null, 2) }] };
|
|
682
691
|
}
|
|
683
692
|
|
|
684
693
|
if (name === "pairai_upload_file") {
|
|
@@ -743,7 +752,7 @@ function decryptMessage(
|
|
|
743
752
|
}
|
|
744
753
|
try {
|
|
745
754
|
const keys = typeof msg.encryptedKeys === "string" ? JSON.parse(msg.encryptedKeys) : msg.encryptedKeys;
|
|
746
|
-
const senderPub = pubKeyCache.get(msg.senderAgentId);
|
|
755
|
+
const senderPub = msg.senderAgentId === myAgentId ? myPublicKey : pubKeyCache.get(msg.senderAgentId);
|
|
747
756
|
const myKey = keys[myAgentId];
|
|
748
757
|
if (senderPub && myKey) {
|
|
749
758
|
const plain = localDecrypt(msg.content, msg.senderSignature, taskId, senderPub, myKey);
|
|
@@ -766,7 +775,7 @@ function decryptTaskDescription(
|
|
|
766
775
|
}
|
|
767
776
|
try {
|
|
768
777
|
const keys = typeof full.descriptionKeys === "string" ? JSON.parse(full.descriptionKeys) : full.descriptionKeys;
|
|
769
|
-
const senderPub = full.initiatorAgentId ? pubKeyCache.get(full.initiatorAgentId) : undefined;
|
|
778
|
+
const senderPub = full.initiatorAgentId === myAgentId ? myPublicKey : (full.initiatorAgentId ? pubKeyCache.get(full.initiatorAgentId) : undefined);
|
|
770
779
|
const myKey = keys[myAgentId];
|
|
771
780
|
if (senderPub && myKey) {
|
|
772
781
|
const plain = localDecrypt(full.description, full.senderSignature, taskId, senderPub, myKey);
|