pairai 0.2.1 → 0.2.3

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 +158 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pairai",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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
@@ -3,7 +3,7 @@
3
3
  * pairai CLI — connect AI agents via the pairai hub
4
4
  *
5
5
  * Commands:
6
- * npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global]
6
+ * npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini] [--global] [--force]
7
7
  * npx pairai serve [--provider claude|gemini]
8
8
  * npx pairai upgrade — update to latest version (preserves keys and config)
9
9
  * npx pairai version — show current version
@@ -93,16 +93,41 @@ if (command === "setup") {
93
93
  const hubIdx = rest.indexOf("--hub");
94
94
  const hubUrl = hubIdx !== -1 ? rest.splice(hubIdx, 2)[1] : "https://pairai.pro";
95
95
  const providerIdx = rest.indexOf("--provider");
96
- const provider = providerIdx !== -1 ? rest.splice(providerIdx, 2)[1] as "claude" | "gemini" : detectProvider();
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();
97
102
  const globalIdx = rest.indexOf("--global");
98
103
  const useGlobal = globalIdx !== -1 ? (rest.splice(globalIdx, 1), true) : false;
99
104
  const agentName = rest.find((a) => !a.startsWith("--"));
100
105
 
106
+ const forceIdx = rest.indexOf("--force");
107
+ const useForce = forceIdx !== -1 ? (rest.splice(forceIdx, 1), true) : false;
101
108
  if (!agentName) {
102
- 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]');
103
110
  process.exit(1);
104
111
  }
105
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
+
106
131
  console.log(`\n Registering "${agentName}" on ${hubUrl}...\n`);
107
132
 
108
133
  console.log(" Generating RSA-4096 keypair...");
@@ -133,18 +158,22 @@ if (command === "setup") {
133
158
  const keyPath = join(keyDir, `${id}.pem`);
134
159
  writeFileSync(keyPath, privateKey, { mode: 0o600 });
135
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;
136
173
  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(` └──────────────────────────────────────────────────────────┘`);
174
+ console.log(` ┌${"─".repeat(w + 2)}┐`);
175
+ for (const l of lines) console.log(` │ ${l.padEnd(w)}│`);
176
+ console.log(` └${"─".repeat(w + 2)}┘`);
148
177
  console.log();
149
178
 
150
179
  if (provider === "gemini") {
@@ -213,7 +242,7 @@ if (command === "setup") {
213
242
  if (command !== "serve") {
214
243
  console.error(`pairai v${VERSION}\n`);
215
244
  console.error("Usage:");
216
- 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]');
217
246
  console.error(" npx pairai serve [--provider claude|gemini]");
218
247
  console.error(" npx pairai upgrade — update to latest version");
219
248
  console.error(" npx pairai version — show current version");
@@ -226,7 +255,12 @@ const { ListToolsRequestSchema, CallToolRequestSchema } = await import("@modelco
226
255
 
227
256
  const serveArgs = args.slice(1);
228
257
  const serveProviderIdx = serveArgs.indexOf("--provider");
229
- const serveProvider = serveProviderIdx !== -1 ? serveArgs[serveProviderIdx + 1] : "claude";
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";
230
264
 
231
265
  const HUB_URL = process.env.PAIRAI_HUB_URL ?? process.env.PAIRAI_URL ?? "https://pairai.pro";
232
266
  const API_KEY = process.env.PAIRAI_AGENT_CRED ?? process.env.PAIRAI_API_KEY;
@@ -402,6 +436,11 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
402
436
  required: ["task_id", "status"],
403
437
  },
404
438
  },
439
+ {
440
+ name: "pairai_get_profile",
441
+ description: "Get your own agent profile — name, ID, description, capabilities.",
442
+ inputSchema: { type: "object" as const, properties: {} },
443
+ },
405
444
  {
406
445
  name: "pairai_list_connections",
407
446
  description: "List agents you are connected with.",
@@ -436,6 +475,66 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
436
475
  required: ["code"],
437
476
  },
438
477
  },
478
+ {
479
+ name: "pairai_update_profile",
480
+ description: "Update your agent's profile — name, description, capabilities, and metadata. Returns the updated profile.",
481
+ inputSchema: {
482
+ type: "object" as const,
483
+ properties: {
484
+ name: { type: "string", description: "Display name (1-64 chars)" },
485
+ description: { type: "string", description: "What this agent does (max 500 chars)" },
486
+ capabilities: { type: "array", items: { type: "string" }, description: "List of capabilities, e.g. ['scheduling', 'code-review']" },
487
+ metadata: { type: "object", description: "Arbitrary JSON metadata (max 4KB)" },
488
+ },
489
+ },
490
+ },
491
+ {
492
+ name: "pairai_set_alias",
493
+ description: "Set a local alias for a connected agent. Only you see this alias. Set to null to clear.",
494
+ inputSchema: {
495
+ type: "object" as const,
496
+ properties: {
497
+ connection_id: { type: "string", description: "Connection ID" },
498
+ alias: { type: ["string", "null"], description: "Local alias, or null to clear" },
499
+ },
500
+ required: ["connection_id"],
501
+ },
502
+ },
503
+ {
504
+ name: "pairai_list_tasks",
505
+ description: "List all tasks you are involved in (as initiator or target).",
506
+ inputSchema: {
507
+ type: "object" as const,
508
+ properties: {
509
+ status: { type: "string", enum: ["submitted", "working", "input-required", "completed", "failed", "cancelled"], description: "Filter by status" },
510
+ },
511
+ },
512
+ },
513
+ {
514
+ name: "pairai_get_task",
515
+ description: "Get full details of a task including all messages.",
516
+ inputSchema: {
517
+ type: "object" as const,
518
+ properties: {
519
+ task_id: { type: "string", description: "Task ID" },
520
+ },
521
+ required: ["task_id"],
522
+ },
523
+ },
524
+ {
525
+ name: "pairai_upload_file",
526
+ description: "Upload a file to a task. Provide base64-encoded content.",
527
+ inputSchema: {
528
+ type: "object" as const,
529
+ properties: {
530
+ task_id: { type: "string", description: "Task ID" },
531
+ filename: { type: "string", description: "Original filename, e.g. photo.png" },
532
+ mime_type: { type: "string", description: "MIME type, e.g. image/png" },
533
+ base64_content: { type: "string", description: "Base64-encoded file content" },
534
+ },
535
+ required: ["task_id", "filename", "mime_type", "base64_content"],
536
+ },
537
+ },
439
538
  {
440
539
  name: "pairai_create_encrypted_task",
441
540
  description: "Create an encrypted task. Title and description are encrypted — the hub cannot read them.",
@@ -496,6 +595,11 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
496
595
  return { content: [{ type: "text" as const, text: `Status → ${args.status}` }] };
497
596
  }
498
597
 
598
+ if (name === "pairai_get_profile") {
599
+ const data = await hubGet("/agents/me");
600
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
601
+ }
602
+
499
603
  if (name === "pairai_list_connections") {
500
604
  const data = await hubGet("/connections");
501
605
  return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
@@ -550,6 +654,43 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
550
654
  return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
551
655
  }
552
656
 
657
+ if (name === "pairai_update_profile") {
658
+ const body: Record<string, unknown> = {};
659
+ if (args.name !== undefined) body.name = args.name;
660
+ if (args.description !== undefined) body.description = args.description;
661
+ if (args.capabilities !== undefined) body.capabilities = args.capabilities;
662
+ if (args.metadata !== undefined) body.metadata = args.metadata;
663
+ const data = await hubPatch("/agents/me", body);
664
+ return { content: [{ type: "text" as const, text: "Profile updated.\n" + JSON.stringify(data, null, 2) }] };
665
+ }
666
+
667
+ if (name === "pairai_set_alias") {
668
+ const { connection_id, alias } = args as { connection_id: string; alias?: string | null };
669
+ const data = await hubPatch(`/connections/${connection_id}`, { alias: alias ?? null });
670
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
671
+ }
672
+
673
+ if (name === "pairai_list_tasks") {
674
+ const data = (await hubGet("/tasks")) as Array<{ status: string }>;
675
+ 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) }] };
677
+ }
678
+
679
+ 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) }] };
682
+ }
683
+
684
+ if (name === "pairai_upload_file") {
685
+ const { task_id, filename, mime_type, base64_content } = args as {
686
+ task_id: string; filename: string; mime_type: string; base64_content: string;
687
+ };
688
+ const data = await hubPost(`/tasks/${task_id}/files/json`, {
689
+ filename, mimeType: mime_type, base64Content: base64_content,
690
+ });
691
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
692
+ }
693
+
553
694
  if (name === "pairai_create_encrypted_task") {
554
695
  if (!PRIVATE_KEY)
555
696
  return { content: [{ type: "text" as const, text: "No private key configured. Re-run setup." }] };
@@ -701,6 +842,7 @@ async function poll() {
701
842
  }>;
702
843
  };
703
844
 
845
+ if (!full?.messages) continue;
704
846
  for (const msg of full.messages.slice(-unread.count)) {
705
847
  const key = `msg:${msg.id}`;
706
848
  if (seenMessages.has(key)) continue;