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.
Files changed (3) hide show
  1. package/lib.ts +80 -0
  2. package/package.json +2 -1
  3. 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",
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.replace(
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
- 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
- }
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 && providerArg !== "claude" && providerArg !== "gemini") {
98
- console.error(` Unknown provider "${providerArg}". Must be "claude" or "gemini".`);
99
- process.exit(1);
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
- 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 {}
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(` ┌${"─".repeat(w + 2)}┐`);
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 && serveProviderArg !== "claude" && serveProviderArg !== "gemini") {
260
- console.error(` Unknown provider "${serveProviderArg}". Must be "claude" or "gemini".`);
261
- process.exit(1);
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
- const data = (await hubGet("/tasks")) as Array<{ status: string }>;
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
- return { content: [{ type: "text" as const, text: JSON.stringify(filtered, null, 2) }] };
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
- const data = await hubGet(`/tasks/${args.task_id}`);
681
- return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
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);