pairai 0.3.2 → 0.4.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.
Files changed (3) hide show
  1. package/lib.ts +10 -8
  2. package/package.json +3 -1
  3. package/pairai.ts +206 -6
package/lib.ts CHANGED
@@ -28,16 +28,18 @@ export function validateProvider(value: string): Provider {
28
28
 
29
29
  /**
30
30
  * Auto-detect provider based on environment and filesystem.
31
+ * Returns null when detection is ambiguous (0 or 2+ matches).
31
32
  */
32
- export function detectProvider(): Provider {
33
+ export function detectProvider(): Provider | null {
33
34
  if (process.env.GEMINI_CLI) return "gemini";
34
- try { if (statSync(".cursor").isDirectory()) return "cursor"; } catch {}
35
- try { if (statSync(".windsurf").isDirectory()) return "windsurf"; } catch {}
36
- try { if (statSync(".vscode").isDirectory()) return "copilot"; } catch {}
37
- try { if (statSync(".codex").isDirectory()) return "codex"; } catch {}
38
- try { if (statSync(".amazonq").isDirectory()) return "amazonq"; } catch {}
39
- try { if (statSync(".gemini").isDirectory()) return "gemini"; } catch {}
40
- return "claude";
35
+ const found: Provider[] = [];
36
+ try { if (statSync(".cursor").isDirectory()) found.push("cursor"); } catch {}
37
+ try { if (statSync(".windsurf").isDirectory()) found.push("windsurf"); } catch {}
38
+ try { if (statSync(".vscode").isDirectory()) found.push("copilot"); } catch {}
39
+ try { if (statSync(".codex").isDirectory()) found.push("codex"); } catch {}
40
+ try { if (statSync(".amazonq").isDirectory()) found.push("amazonq"); } catch {}
41
+ try { if (statSync(".gemini").isDirectory()) found.push("gemini"); } catch {}
42
+ return found.length === 1 ? found[0] : null;
41
43
  }
42
44
 
43
45
  export interface ProviderConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pairai",
3
- "version": "0.3.2",
3
+ "version": "0.4.2",
4
4
  "description": "pairai CLI — connect AI agents to collaborate via the pairai hub",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,6 +29,8 @@
29
29
  "README.md"
30
30
  ],
31
31
  "dependencies": {
32
+ "@inquirer/input": "^5.0.10",
33
+ "@inquirer/select": "^5.1.2",
32
34
  "@modelcontextprotocol/sdk": "^1.10.0",
33
35
  "nanoid": "^5.0.0",
34
36
  "tsx": "^4.0.0"
package/pairai.ts CHANGED
@@ -24,11 +24,23 @@ import { join, dirname } from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
25
  import { validateProvider, detectProvider, checkExistingConfig, formatKeyBackupBox, acquireLock, releaseLock, getProviderConfig, localEncrypt as _localEncrypt, localDecrypt as _localDecrypt } from "./lib.js";
26
26
  import type { Provider } from "./lib.js";
27
+ import select from "@inquirer/select";
28
+ import input from "@inquirer/input";
27
29
 
28
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
29
31
  const PKG = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8"));
30
32
  const VERSION: string = PKG.version;
31
33
 
34
+ const PROVIDER_CHOICES: { name: string; value: Provider }[] = [
35
+ { name: "Claude Code", value: "claude" },
36
+ { name: "Gemini CLI", value: "gemini" },
37
+ { name: "Cursor", value: "cursor" },
38
+ { name: "GitHub Copilot (VS Code)", value: "copilot" },
39
+ { name: "Windsurf", value: "windsurf" },
40
+ { name: "OpenAI Codex CLI", value: "codex" },
41
+ { name: "Amazon Q", value: "amazonq" },
42
+ ];
43
+
32
44
  const args = process.argv.slice(2);
33
45
  const command = args[0];
34
46
 
@@ -108,16 +120,39 @@ if (command === "setup") {
108
120
  process.exit(1);
109
121
  }
110
122
  }
111
- const provider = (providerArg as Provider) ?? detectProvider();
123
+ let provider: Provider;
124
+ if (providerArg) {
125
+ provider = providerArg as Provider;
126
+ } else {
127
+ const detected = detectProvider();
128
+ if (detected) {
129
+ provider = detected;
130
+ } else if (process.stdin.isTTY) {
131
+ provider = await select({
132
+ message: "Select your AI tool:",
133
+ choices: PROVIDER_CHOICES,
134
+ });
135
+ } else {
136
+ console.error('Cannot auto-detect provider. Use --provider flag (e.g. npx pairai setup "My Agent" --provider cursor)');
137
+ process.exit(1);
138
+ }
139
+ }
112
140
  const globalIdx = rest.indexOf("--global");
113
141
  const useGlobal = globalIdx !== -1 ? (rest.splice(globalIdx, 1), true) : false;
114
- const agentName = rest.find((a) => !a.startsWith("--"));
142
+ let agentName = rest.find((a) => !a.startsWith("--"));
115
143
 
116
144
  const forceIdx = rest.indexOf("--force");
117
145
  const useForce = forceIdx !== -1 ? (rest.splice(forceIdx, 1), true) : false;
118
146
  if (!agentName) {
119
- console.error('Usage: npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini|cursor|copilot|windsurf|codex|amazonq] [--global] [--force]');
120
- process.exit(1);
147
+ if (process.stdin.isTTY) {
148
+ agentName = await input({
149
+ message: 'What should we call your agent? Other agents and users will see this name. (e.g. "Alice\'s Assistant", "Travel Bot")',
150
+ validate: (v) => v.trim().length > 0 && v.trim().length <= 64 ? true : "Name must be 1-64 characters",
151
+ });
152
+ } else {
153
+ console.error('Usage: npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini|cursor|copilot|windsurf|codex|amazonq] [--global] [--force]');
154
+ process.exit(1);
155
+ }
121
156
  }
122
157
 
123
158
  // Check for existing config to avoid accidental overwrites
@@ -342,7 +377,9 @@ async function loadAgentInfo() {
342
377
  const me = (await hubGet("/agents/me")) as { id: string; name: string; publicKey?: string };
343
378
  myAgentId = me.id;
344
379
  myPublicKey = me.publicKey ?? "";
345
- } catch {}
380
+ } catch (err) {
381
+ console.error("[pairai] failed to load agent info:", (err as Error).message);
382
+ }
346
383
  }
347
384
 
348
385
  async function loadPublicKeys() {
@@ -351,7 +388,9 @@ async function loadPublicKeys() {
351
388
  for (const c of conns) {
352
389
  if (c.publicKey) pubKeyCache.set(c.agentId, c.publicKey);
353
390
  }
354
- } catch {}
391
+ } catch (err) {
392
+ console.error("[pairai] failed to load public keys:", (err as Error).message);
393
+ }
355
394
  }
356
395
 
357
396
  function localEncrypt(plaintext: string, taskId: string, recipientPubKeys: Record<string, string>) {
@@ -477,6 +516,17 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
477
516
  required: ["code"],
478
517
  },
479
518
  },
519
+ {
520
+ name: "pairai_connect_directly",
521
+ description: "Connect directly to an agent that has auto-accept enabled — no pairing code needed. Use pairai_discover_agents to find agents with autoAccept: true.",
522
+ inputSchema: {
523
+ type: "object" as const,
524
+ properties: {
525
+ agent_id: { type: "string", description: "ID of the agent to connect with" },
526
+ },
527
+ required: ["agent_id"],
528
+ },
529
+ },
480
530
  {
481
531
  name: "pairai_update_profile",
482
532
  description: "Update your agent's profile — name, description, capabilities, and metadata. Returns the updated profile.",
@@ -488,6 +538,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
488
538
  capabilities: { type: "array", items: { type: "string" }, description: "List of capabilities, e.g. ['scheduling', 'code-review']" },
489
539
  metadata: { type: "object", description: "Arbitrary JSON metadata (max 4KB)" },
490
540
  discoverable: { type: "boolean", description: "Whether to appear in the public agent directory" },
541
+ autoAccept: { type: "boolean", description: "Whether to accept direct connections without pairing codes" },
491
542
  defaultApprovalRule: { type: "string", enum: ["auto", "require"], description: "Default approval rule for new connections" },
492
543
  },
493
544
  },
@@ -639,6 +690,78 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
639
690
  required: ["target_agent_id", "title"],
640
691
  },
641
692
  },
693
+ {
694
+ name: "pairai_delete_message",
695
+ description: "Delete (tombstone) a message you sent. The message content is replaced with [deleted].",
696
+ inputSchema: {
697
+ type: "object" as const,
698
+ properties: {
699
+ task_id: { type: "string", description: "Task ID" },
700
+ message_id: { type: "string", description: "Message ID to delete" },
701
+ },
702
+ required: ["task_id", "message_id"],
703
+ },
704
+ },
705
+ {
706
+ name: "pairai_delete_file",
707
+ description: "Delete a file you uploaded. Removes from disk and tombstones the associated message.",
708
+ inputSchema: {
709
+ type: "object" as const,
710
+ properties: {
711
+ file_id: { type: "string", description: "File ID to delete" },
712
+ },
713
+ required: ["file_id"],
714
+ },
715
+ },
716
+ {
717
+ name: "pairai_delete_task",
718
+ description: "Permanently delete a terminal task (completed, failed, cancelled) and all its messages and files.",
719
+ inputSchema: {
720
+ type: "object" as const,
721
+ properties: {
722
+ task_id: { type: "string", description: "Task ID to delete" },
723
+ },
724
+ required: ["task_id"],
725
+ },
726
+ },
727
+ {
728
+ name: "pairai_rotate_api_key",
729
+ description: "Generate a new API key. WARNING: old key immediately invalidated. Save the new key before doing anything else.",
730
+ inputSchema: {
731
+ type: "object" as const,
732
+ properties: {},
733
+ },
734
+ },
735
+ {
736
+ name: "pairai_delete_account",
737
+ description: "PERMANENTLY delete your agent and ALL associated data. IRREVERSIBLE.",
738
+ inputSchema: {
739
+ type: "object" as const,
740
+ properties: {},
741
+ },
742
+ },
743
+ {
744
+ name: "pairai_block_agent",
745
+ description: "Block an agent. They cannot discover or connect with you. Disconnects if connected.",
746
+ inputSchema: {
747
+ type: "object" as const,
748
+ properties: {
749
+ agent_id: { type: "string", description: "ID of the agent to block" },
750
+ },
751
+ required: ["agent_id"],
752
+ },
753
+ },
754
+ {
755
+ name: "pairai_unblock_agent",
756
+ description: "Unblock a previously blocked agent.",
757
+ inputSchema: {
758
+ type: "object" as const,
759
+ properties: {
760
+ agent_id: { type: "string", description: "ID of the agent to unblock" },
761
+ },
762
+ required: ["agent_id"],
763
+ },
764
+ },
642
765
  ],
643
766
  }));
644
767
 
@@ -809,6 +932,14 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
809
932
  return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
810
933
  }
811
934
 
935
+ if (name === "pairai_connect_directly") {
936
+ const { agent_id } = args as { agent_id: string };
937
+ const data = await hubPost(`/connect/${agent_id}`);
938
+ // Refresh public keys after new connection
939
+ await loadPublicKeys();
940
+ return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
941
+ }
942
+
812
943
  if (name === "pairai_update_profile") {
813
944
  const body: Record<string, unknown> = {};
814
945
  if (args.name !== undefined) body.name = args.name;
@@ -816,6 +947,7 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
816
947
  if (args.capabilities !== undefined) body.capabilities = args.capabilities;
817
948
  if (args.metadata !== undefined) body.metadata = args.metadata;
818
949
  if (args.discoverable !== undefined) body.discoverable = args.discoverable === "true" || args.discoverable === true;
950
+ if (args.autoAccept !== undefined) body.autoAccept = args.autoAccept === "true" || args.autoAccept === true;
819
951
  if (args.defaultApprovalRule !== undefined) body.defaultApprovalRule = args.defaultApprovalRule;
820
952
  const data = await hubPatch("/agents/me", body);
821
953
  return { content: [{ type: "text" as const, text: "Profile updated.\n" + JSON.stringify(data, null, 2) }] };
@@ -1112,6 +1244,74 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
1112
1244
  return { content: [{ type: "text" as const, text: `Encrypted task created. ID: ${taskId}` }] };
1113
1245
  }
1114
1246
 
1247
+ if (name === "pairai_delete_message") {
1248
+ const { task_id, message_id } = args as { task_id: string; message_id: string };
1249
+ try {
1250
+ await hubDelete(`/tasks/${task_id}/messages/${message_id}`);
1251
+ return { content: [{ type: "text" as const, text: "Message deleted." }] };
1252
+ } catch (err) {
1253
+ return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
1254
+ }
1255
+ }
1256
+
1257
+ if (name === "pairai_delete_file") {
1258
+ const { file_id } = args as { file_id: string };
1259
+ try {
1260
+ await hubDelete(`/files/${file_id}`);
1261
+ return { content: [{ type: "text" as const, text: "File deleted." }] };
1262
+ } catch (err) {
1263
+ return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
1264
+ }
1265
+ }
1266
+
1267
+ if (name === "pairai_delete_task") {
1268
+ const { task_id } = args as { task_id: string };
1269
+ try {
1270
+ const result = (await hubDelete(`/tasks/${task_id}`)) as { deletedMessages?: number; deletedFiles?: number };
1271
+ return { content: [{ type: "text" as const, text: `Task deleted. ${result.deletedMessages ?? 0} message(s) and ${result.deletedFiles ?? 0} file(s) removed.` }] };
1272
+ } catch (err) {
1273
+ return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
1274
+ }
1275
+ }
1276
+
1277
+ if (name === "pairai_rotate_api_key") {
1278
+ try {
1279
+ const result = (await hubPost("/agents/me/rotate-key")) as { apiKey: string };
1280
+ return { content: [{ type: "text" as const, text: `New API key: ${result.apiKey}\n\nWARNING: Your old key is now invalid. Save this key immediately — it will not be shown again.` }] };
1281
+ } catch (err) {
1282
+ return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
1283
+ }
1284
+ }
1285
+
1286
+ if (name === "pairai_delete_account") {
1287
+ try {
1288
+ await hubDelete("/agents/me");
1289
+ return { content: [{ type: "text" as const, text: "Account deleted." }] };
1290
+ } catch (err) {
1291
+ return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
1292
+ }
1293
+ }
1294
+
1295
+ if (name === "pairai_block_agent") {
1296
+ const { agent_id } = args as { agent_id: string };
1297
+ try {
1298
+ await hubPost("/agents/me/block", { agentId: agent_id });
1299
+ return { content: [{ type: "text" as const, text: "Agent blocked." }] };
1300
+ } catch (err) {
1301
+ return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
1302
+ }
1303
+ }
1304
+
1305
+ if (name === "pairai_unblock_agent") {
1306
+ const { agent_id } = args as { agent_id: string };
1307
+ try {
1308
+ await hubDelete(`/agents/me/block/${agent_id}`);
1309
+ return { content: [{ type: "text" as const, text: "Agent unblocked." }] };
1310
+ } catch (err) {
1311
+ return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
1312
+ }
1313
+ }
1314
+
1115
1315
  throw new Error(`Unknown tool: ${name}`);
1116
1316
  });
1117
1317