pairai 0.2.2 → 0.2.4

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 (2) hide show
  1. package/package.json +1 -1
  2. package/pairai.ts +166 -49
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pairai",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "pairai CLI — connect AI agents to collaborate via the pairai hub",
5
5
  "type": "module",
6
6
  "license": "MIT",
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;
@@ -436,6 +414,11 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
436
414
  required: ["task_id", "status"],
437
415
  },
438
416
  },
417
+ {
418
+ name: "pairai_get_profile",
419
+ description: "Get your own agent profile — name, ID, description, capabilities.",
420
+ inputSchema: { type: "object" as const, properties: {} },
421
+ },
439
422
  {
440
423
  name: "pairai_list_connections",
441
424
  description: "List agents you are connected with.",
@@ -470,6 +453,66 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
470
453
  required: ["code"],
471
454
  },
472
455
  },
456
+ {
457
+ name: "pairai_update_profile",
458
+ description: "Update your agent's profile — name, description, capabilities, and metadata. Returns the updated profile.",
459
+ inputSchema: {
460
+ type: "object" as const,
461
+ properties: {
462
+ name: { type: "string", description: "Display name (1-64 chars)" },
463
+ description: { type: "string", description: "What this agent does (max 500 chars)" },
464
+ capabilities: { type: "array", items: { type: "string" }, description: "List of capabilities, e.g. ['scheduling', 'code-review']" },
465
+ metadata: { type: "object", description: "Arbitrary JSON metadata (max 4KB)" },
466
+ },
467
+ },
468
+ },
469
+ {
470
+ name: "pairai_set_alias",
471
+ description: "Set a local alias for a connected agent. Only you see this alias. Set to null to clear.",
472
+ inputSchema: {
473
+ type: "object" as const,
474
+ properties: {
475
+ connection_id: { type: "string", description: "Connection ID" },
476
+ alias: { type: ["string", "null"], description: "Local alias, or null to clear" },
477
+ },
478
+ required: ["connection_id"],
479
+ },
480
+ },
481
+ {
482
+ name: "pairai_list_tasks",
483
+ description: "List all tasks you are involved in (as initiator or target).",
484
+ inputSchema: {
485
+ type: "object" as const,
486
+ properties: {
487
+ status: { type: "string", enum: ["submitted", "working", "input-required", "completed", "failed", "cancelled"], description: "Filter by status" },
488
+ },
489
+ },
490
+ },
491
+ {
492
+ name: "pairai_get_task",
493
+ description: "Get full details of a task including all messages.",
494
+ inputSchema: {
495
+ type: "object" as const,
496
+ properties: {
497
+ task_id: { type: "string", description: "Task ID" },
498
+ },
499
+ required: ["task_id"],
500
+ },
501
+ },
502
+ {
503
+ name: "pairai_upload_file",
504
+ description: "Upload a file to a task. Provide base64-encoded content.",
505
+ inputSchema: {
506
+ type: "object" as const,
507
+ properties: {
508
+ task_id: { type: "string", description: "Task ID" },
509
+ filename: { type: "string", description: "Original filename, e.g. photo.png" },
510
+ mime_type: { type: "string", description: "MIME type, e.g. image/png" },
511
+ base64_content: { type: "string", description: "Base64-encoded file content" },
512
+ },
513
+ required: ["task_id", "filename", "mime_type", "base64_content"],
514
+ },
515
+ },
473
516
  {
474
517
  name: "pairai_create_encrypted_task",
475
518
  description: "Create an encrypted task. Title and description are encrypted — the hub cannot read them.",
@@ -530,6 +573,11 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
530
573
  return { content: [{ type: "text" as const, text: `Status → ${args.status}` }] };
531
574
  }
532
575
 
576
+ if (name === "pairai_get_profile") {
577
+ const data = await hubGet("/agents/me");
578
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
579
+ }
580
+
533
581
  if (name === "pairai_list_connections") {
534
582
  const data = await hubGet("/connections");
535
583
  return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
@@ -584,6 +632,74 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
584
632
  return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
585
633
  }
586
634
 
635
+ if (name === "pairai_update_profile") {
636
+ const body: Record<string, unknown> = {};
637
+ if (args.name !== undefined) body.name = args.name;
638
+ if (args.description !== undefined) body.description = args.description;
639
+ if (args.capabilities !== undefined) body.capabilities = args.capabilities;
640
+ if (args.metadata !== undefined) body.metadata = args.metadata;
641
+ const data = await hubPatch("/agents/me", body);
642
+ return { content: [{ type: "text" as const, text: "Profile updated.\n" + JSON.stringify(data, null, 2) }] };
643
+ }
644
+
645
+ if (name === "pairai_set_alias") {
646
+ const { connection_id, alias } = args as { connection_id: string; alias?: string | null };
647
+ const data = await hubPatch(`/connections/${connection_id}`, { alias: alias ?? null });
648
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
649
+ }
650
+
651
+ if (name === "pairai_list_tasks") {
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
+ }>;
657
+ const filtered = args.status ? data.filter((t) => t.status === args.status) : data;
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) }] };
666
+ }
667
+
668
+ if (name === "pairai_get_task") {
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) }] };
691
+ }
692
+
693
+ if (name === "pairai_upload_file") {
694
+ const { task_id, filename, mime_type, base64_content } = args as {
695
+ task_id: string; filename: string; mime_type: string; base64_content: string;
696
+ };
697
+ const data = await hubPost(`/tasks/${task_id}/files/json`, {
698
+ filename, mimeType: mime_type, base64Content: base64_content,
699
+ });
700
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
701
+ }
702
+
587
703
  if (name === "pairai_create_encrypted_task") {
588
704
  if (!PRIVATE_KEY)
589
705
  return { content: [{ type: "text" as const, text: "No private key configured. Re-run setup." }] };
@@ -636,7 +752,7 @@ function decryptMessage(
636
752
  }
637
753
  try {
638
754
  const keys = typeof msg.encryptedKeys === "string" ? JSON.parse(msg.encryptedKeys) : msg.encryptedKeys;
639
- const senderPub = pubKeyCache.get(msg.senderAgentId);
755
+ const senderPub = msg.senderAgentId === myAgentId ? myPublicKey : pubKeyCache.get(msg.senderAgentId);
640
756
  const myKey = keys[myAgentId];
641
757
  if (senderPub && myKey) {
642
758
  const plain = localDecrypt(msg.content, msg.senderSignature, taskId, senderPub, myKey);
@@ -659,7 +775,7 @@ function decryptTaskDescription(
659
775
  }
660
776
  try {
661
777
  const keys = typeof full.descriptionKeys === "string" ? JSON.parse(full.descriptionKeys) : full.descriptionKeys;
662
- const senderPub = full.initiatorAgentId ? pubKeyCache.get(full.initiatorAgentId) : undefined;
778
+ const senderPub = full.initiatorAgentId === myAgentId ? myPublicKey : (full.initiatorAgentId ? pubKeyCache.get(full.initiatorAgentId) : undefined);
663
779
  const myKey = keys[myAgentId];
664
780
  if (senderPub && myKey) {
665
781
  const plain = localDecrypt(full.description, full.senderSignature, taskId, senderPub, myKey);
@@ -735,6 +851,7 @@ async function poll() {
735
851
  }>;
736
852
  };
737
853
 
854
+ if (!full?.messages) continue;
738
855
  for (const msg of full.messages.slice(-unread.count)) {
739
856
  const key = `msg:${msg.id}`;
740
857
  if (seenMessages.has(key)) continue;