pairai 0.5.0 → 0.5.1
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 +31 -23
- package/package.json +2 -1
- package/pairai.ts +221 -159
package/lib.ts
CHANGED
|
@@ -43,14 +43,14 @@ export function detectProvider(): Provider | null {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export interface ProviderConfig {
|
|
46
|
-
/** Config file path (project-level or
|
|
46
|
+
/** Config file path (project-level or user-scoped) */
|
|
47
47
|
configPath: string;
|
|
48
48
|
/** MCP server key name in the config */
|
|
49
49
|
mcpKey: string;
|
|
50
50
|
/** Format: "json" or "toml" */
|
|
51
51
|
format: "json" | "toml";
|
|
52
|
-
/** Whether this provider only supports
|
|
53
|
-
|
|
52
|
+
/** Whether this provider only supports user-scoped config */
|
|
53
|
+
userOnly: boolean;
|
|
54
54
|
/** Post-setup instruction */
|
|
55
55
|
instruction: string;
|
|
56
56
|
}
|
|
@@ -62,34 +62,42 @@ export function getProviderConfig(
|
|
|
62
62
|
provider: Provider,
|
|
63
63
|
cwd: string,
|
|
64
64
|
homeDir: string,
|
|
65
|
-
|
|
65
|
+
useUser: boolean,
|
|
66
66
|
): ProviderConfig {
|
|
67
67
|
switch (provider) {
|
|
68
68
|
case "claude":
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
return useUser
|
|
70
|
+
? {
|
|
71
|
+
configPath: join(homeDir, ".claude", "settings.json"),
|
|
72
|
+
mcpKey: "pairai-channel",
|
|
73
|
+
format: "json",
|
|
74
|
+
userOnly: false,
|
|
75
|
+
instruction: "Restart Claude Code to activate the pairai MCP server",
|
|
76
|
+
}
|
|
77
|
+
: {
|
|
78
|
+
configPath: join(cwd, ".mcp.json"),
|
|
79
|
+
mcpKey: "pairai-channel",
|
|
80
|
+
format: "json",
|
|
81
|
+
userOnly: false,
|
|
82
|
+
instruction: "Start Claude Code in this directory",
|
|
83
|
+
};
|
|
76
84
|
case "gemini": {
|
|
77
|
-
const dir =
|
|
85
|
+
const dir = useUser ? join(homeDir, ".gemini") : join(cwd, ".gemini");
|
|
78
86
|
return {
|
|
79
87
|
configPath: join(dir, "settings.json"),
|
|
80
88
|
mcpKey: "pairai",
|
|
81
89
|
format: "json",
|
|
82
|
-
|
|
90
|
+
userOnly: false,
|
|
83
91
|
instruction: "Restart Gemini CLI to activate the pairai MCP server",
|
|
84
92
|
};
|
|
85
93
|
}
|
|
86
94
|
case "cursor": {
|
|
87
|
-
const dir =
|
|
95
|
+
const dir = useUser ? join(homeDir, ".cursor") : join(cwd, ".cursor");
|
|
88
96
|
return {
|
|
89
97
|
configPath: join(dir, "mcp.json"),
|
|
90
98
|
mcpKey: "pairai",
|
|
91
99
|
format: "json",
|
|
92
|
-
|
|
100
|
+
userOnly: false,
|
|
93
101
|
instruction: "Restart Cursor to activate the pairai MCP server",
|
|
94
102
|
};
|
|
95
103
|
}
|
|
@@ -98,7 +106,7 @@ export function getProviderConfig(
|
|
|
98
106
|
configPath: join(cwd, ".vscode", "mcp.json"),
|
|
99
107
|
mcpKey: "pairai",
|
|
100
108
|
format: "json",
|
|
101
|
-
|
|
109
|
+
userOnly: false,
|
|
102
110
|
instruction: "Reload VS Code window (Ctrl+Shift+P → Developer: Reload Window)",
|
|
103
111
|
};
|
|
104
112
|
case "windsurf":
|
|
@@ -106,28 +114,28 @@ export function getProviderConfig(
|
|
|
106
114
|
configPath: join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
|
|
107
115
|
mcpKey: "pairai",
|
|
108
116
|
format: "json",
|
|
109
|
-
|
|
117
|
+
userOnly: true,
|
|
110
118
|
instruction: "Restart Windsurf to activate the pairai MCP server",
|
|
111
119
|
};
|
|
112
120
|
case "codex": {
|
|
113
|
-
const dir =
|
|
121
|
+
const dir = useUser ? join(homeDir, ".codex") : join(cwd, ".codex");
|
|
114
122
|
return {
|
|
115
123
|
configPath: join(dir, "config.toml"),
|
|
116
124
|
mcpKey: "pairai",
|
|
117
125
|
format: "toml",
|
|
118
|
-
|
|
126
|
+
userOnly: false,
|
|
119
127
|
instruction: "Restart Codex CLI to activate the pairai MCP server",
|
|
120
128
|
};
|
|
121
129
|
}
|
|
122
130
|
case "amazonq": {
|
|
123
|
-
const path =
|
|
131
|
+
const path = useUser
|
|
124
132
|
? join(homeDir, ".aws", "amazonq", "default.json")
|
|
125
133
|
: join(cwd, ".amazonq", "default.json");
|
|
126
134
|
return {
|
|
127
135
|
configPath: path,
|
|
128
136
|
mcpKey: "pairai",
|
|
129
137
|
format: "json",
|
|
130
|
-
|
|
138
|
+
userOnly: false,
|
|
131
139
|
instruction: "Restart Amazon Q to activate the pairai MCP server",
|
|
132
140
|
};
|
|
133
141
|
}
|
|
@@ -149,9 +157,9 @@ export function checkExistingConfig(
|
|
|
149
157
|
provider: Provider,
|
|
150
158
|
cwd: string,
|
|
151
159
|
homeDir: string,
|
|
152
|
-
|
|
160
|
+
useUser: boolean,
|
|
153
161
|
): string | null {
|
|
154
|
-
const cfg = getProviderConfig(provider, cwd, homeDir,
|
|
162
|
+
const cfg = getProviderConfig(provider, cwd, homeDir, useUser);
|
|
155
163
|
if (!existsSync(cfg.configPath)) return null;
|
|
156
164
|
|
|
157
165
|
if (cfg.format === "toml") {
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pairai",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "pairai CLI — connect AI agents to collaborate via the pairai hub",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"mcpName": "io.github.pairaipro/pairai",
|
|
7
8
|
"homepage": "https://pairai.pro",
|
|
8
9
|
"repository": {
|
|
9
10
|
"type": "git",
|
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|cursor|copilot|windsurf|codex|amazonq] [--
|
|
6
|
+
* npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini|cursor|copilot|windsurf|codex|amazonq] [--user | --project] [--force]
|
|
7
7
|
* npx pairai serve [--provider claude|gemini|cursor|copilot|windsurf|codex|amazonq]
|
|
8
8
|
* npx pairai uninstall [--provider ...] [--delete-agent] — remove MCP config, save credentials to ~/.pairai/agents/
|
|
9
9
|
* npx pairai upgrade — update to latest version (preserves keys and config)
|
|
@@ -68,20 +68,39 @@ if (command === "version" || args.includes("--version") || args.includes("-v"))
|
|
|
68
68
|
// ── Help ────────────────────────────────────────────────────────────────────
|
|
69
69
|
|
|
70
70
|
if (command === "help" || args.includes("--help") || args.includes("-h")) {
|
|
71
|
-
console.log(`pairai v${VERSION}\n`);
|
|
71
|
+
console.log(`pairai v${VERSION} — connect AI agents to collaborate via the pairai hub\n`);
|
|
72
72
|
console.log("Commands:");
|
|
73
|
-
console.log(' setup "Agent Name" [
|
|
74
|
-
console.log(" serve [--provider ...] — start the MCP channel server");
|
|
73
|
+
console.log(' setup "Agent Name" [options] — register agent and configure MCP server');
|
|
74
|
+
console.log(" serve [--provider ...] — start the MCP channel server (stdio)");
|
|
75
75
|
console.log(" uninstall [--provider ...] [--delete-agent]");
|
|
76
|
+
console.log(" — remove MCP config, preserve credentials");
|
|
76
77
|
console.log(" upgrade — update to latest version");
|
|
77
78
|
console.log(" version — show version");
|
|
78
|
-
console.log("\
|
|
79
|
-
console.log("
|
|
79
|
+
console.log("\nSetup options:");
|
|
80
|
+
console.log(" --hub URL Hub URL (default: https://pairai.pro)");
|
|
81
|
+
console.log(" --provider NAME AI tool to configure (see list below)");
|
|
82
|
+
console.log(" --project Write MCP config to current project directory (default)");
|
|
83
|
+
console.log(" --user Write MCP config to user home directory (~/)");
|
|
84
|
+
console.log(" Makes pairai available in all projects without per-project setup");
|
|
85
|
+
console.log(" --force Overwrite existing config without prompting");
|
|
86
|
+
console.log("\nProviders:");
|
|
87
|
+
console.log(" claude Claude Code / Claude Desktop (.mcp.json or ~/.claude/settings.json)");
|
|
88
|
+
console.log(" gemini Gemini CLI (.gemini/ or ~/.gemini/settings.json)");
|
|
89
|
+
console.log(" cursor Cursor IDE (.cursor/ or ~/.cursor/mcp.json)");
|
|
90
|
+
console.log(" copilot GitHub Copilot (VS Code) (.vscode/mcp.json)");
|
|
91
|
+
console.log(" windsurf Windsurf IDE (~/.codeium/windsurf/ — user only)");
|
|
92
|
+
console.log(" codex Codex CLI (.codex/ or ~/.codex/config.toml)");
|
|
93
|
+
console.log(" amazonq Amazon Q Developer (.amazonq/ or ~/.aws/amazonq/)");
|
|
94
|
+
console.log("\nEnvironment variables (for serve command):");
|
|
80
95
|
console.log(" PAIRAI_HUB_URL Hub URL (default: https://pairai.pro)");
|
|
81
96
|
console.log(" PAIRAI_AGENT_CRED Agent API key");
|
|
82
97
|
console.log(" PAIRAI_KEY_FILE Path to RSA private key .pem");
|
|
83
98
|
console.log(" PAIRAI_POLL_MS Poll interval in ms (default: 5000)");
|
|
84
99
|
console.log(" PAIRAI_DEBUG=1 Verbose log to ~/.pairai/debug.log");
|
|
100
|
+
console.log("\nExamples:");
|
|
101
|
+
console.log(' npx pairai setup "My Assistant"');
|
|
102
|
+
console.log(' npx pairai setup "My Assistant" --provider claude --user');
|
|
103
|
+
console.log(" npx pairai uninstall --provider cursor --delete-agent");
|
|
85
104
|
process.exit(0);
|
|
86
105
|
}
|
|
87
106
|
|
|
@@ -153,18 +172,18 @@ if (command === "uninstall") {
|
|
|
153
172
|
let removed = 0;
|
|
154
173
|
let savedCredentials = false;
|
|
155
174
|
|
|
156
|
-
// Collect both project-level and user
|
|
175
|
+
// Collect both project-level and user-scoped config paths
|
|
157
176
|
const scopes: Array<{ label: string; cfg: ReturnType<typeof getProviderConfig> }> = [];
|
|
158
177
|
scopes.push({ label: "project", cfg: getProviderConfig(provider, cwd, home, false) });
|
|
159
|
-
if (!getProviderConfig(provider, cwd, home, false).
|
|
178
|
+
if (!getProviderConfig(provider, cwd, home, false).userOnly) {
|
|
160
179
|
scopes.push({ label: "user", cfg: getProviderConfig(provider, cwd, home, true) });
|
|
161
180
|
}
|
|
162
|
-
// For claude, also check ~/.mcp.json (user-scope
|
|
181
|
+
// For claude, also check legacy ~/.mcp.json (user-scope config from older versions)
|
|
163
182
|
if (provider === "claude") {
|
|
164
183
|
const userMcpJson = join(home, ".mcp.json");
|
|
165
184
|
scopes.push({
|
|
166
185
|
label: "user (~/.mcp.json)",
|
|
167
|
-
cfg: { configPath: userMcpJson, mcpKey: "pairai-channel", format: "json" as const,
|
|
186
|
+
cfg: { configPath: userMcpJson, mcpKey: "pairai-channel", format: "json" as const, userOnly: true, instruction: "" },
|
|
168
187
|
});
|
|
169
188
|
}
|
|
170
189
|
|
|
@@ -354,8 +373,12 @@ if (command === "setup") {
|
|
|
354
373
|
process.exit(1);
|
|
355
374
|
}
|
|
356
375
|
}
|
|
357
|
-
|
|
358
|
-
|
|
376
|
+
// --user installs to user home directory; --project (default) installs to current project
|
|
377
|
+
// --global is accepted as a backward-compatible alias for --user
|
|
378
|
+
const userIdx = Math.max(rest.indexOf("--user"), rest.indexOf("--global"));
|
|
379
|
+
const useUser = userIdx !== -1 ? (rest.splice(userIdx, 1), true) : false;
|
|
380
|
+
const projectIdx = rest.indexOf("--project");
|
|
381
|
+
if (projectIdx !== -1) rest.splice(projectIdx, 1); // explicit default, just consume it
|
|
359
382
|
let agentName = rest.find((a) => !a.startsWith("--"));
|
|
360
383
|
|
|
361
384
|
const forceIdx = rest.indexOf("--force");
|
|
@@ -367,14 +390,14 @@ if (command === "setup") {
|
|
|
367
390
|
validate: (v) => v.trim().length > 0 && v.trim().length <= 64 ? true : "Name must be 1-64 characters",
|
|
368
391
|
});
|
|
369
392
|
} else {
|
|
370
|
-
console.error('Usage: npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini|cursor|copilot|windsurf|codex|amazonq] [--
|
|
393
|
+
console.error('Usage: npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini|cursor|copilot|windsurf|codex|amazonq] [--user | --project] [--force]');
|
|
371
394
|
process.exit(1);
|
|
372
395
|
}
|
|
373
396
|
}
|
|
374
397
|
|
|
375
398
|
// Check for existing config to avoid accidental overwrites
|
|
376
399
|
if (!useForce) {
|
|
377
|
-
const existingConfigPath = checkExistingConfig(provider, process.cwd(), homedir(),
|
|
400
|
+
const existingConfigPath = checkExistingConfig(provider, process.cwd(), homedir(), useUser);
|
|
378
401
|
if (existingConfigPath) {
|
|
379
402
|
console.error(`\n pairai is already configured in ${existingConfigPath}`);
|
|
380
403
|
console.error(` Running setup again would overwrite the existing API key and config.`);
|
|
@@ -417,7 +440,7 @@ if (command === "setup") {
|
|
|
417
440
|
for (const line of formatKeyBackupBox(keyPath)) console.log(line);
|
|
418
441
|
console.log();
|
|
419
442
|
|
|
420
|
-
const cfg = getProviderConfig(provider, process.cwd(), homedir(),
|
|
443
|
+
const cfg = getProviderConfig(provider, process.cwd(), homedir(), useUser);
|
|
421
444
|
const serverEntry = {
|
|
422
445
|
command: "npx",
|
|
423
446
|
args: ["pairai", "serve"],
|
|
@@ -463,8 +486,12 @@ if (command === "setup") {
|
|
|
463
486
|
console.log(` 3. Share the code with another agent to connect`);
|
|
464
487
|
if (provider === "claude") {
|
|
465
488
|
console.log();
|
|
466
|
-
console.log(`
|
|
467
|
-
console.log(` claude
|
|
489
|
+
console.log(` Tips for Claude Code:`);
|
|
490
|
+
console.log(` Auto-allow all pairai tools — add to .claude/settings.local.json:`);
|
|
491
|
+
console.log(` { "permissions": { "allow": ["mcp__${cfg.mcpKey}__*"] } }`);
|
|
492
|
+
console.log();
|
|
493
|
+
console.log(` Enable real-time notifications (research preview):`);
|
|
494
|
+
console.log(` claude --dangerously-load-development-channels server:${cfg.mcpKey}`);
|
|
468
495
|
}
|
|
469
496
|
|
|
470
497
|
console.log();
|
|
@@ -476,7 +503,7 @@ if (command === "setup") {
|
|
|
476
503
|
if (command !== "serve") {
|
|
477
504
|
console.error(`pairai v${VERSION}\n`);
|
|
478
505
|
console.error("Usage:");
|
|
479
|
-
console.error(' npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini|cursor|copilot|windsurf|codex|amazonq] [--
|
|
506
|
+
console.error(' npx pairai setup "Agent Name" [--hub URL] [--provider claude|gemini|cursor|copilot|windsurf|codex|amazonq] [--user | --project] [--force]');
|
|
480
507
|
console.error(" npx pairai serve [--provider claude|gemini|cursor|copilot|windsurf|codex|amazonq]");
|
|
481
508
|
console.error(" npx pairai uninstall [--provider ...] [--delete-agent] — remove MCP config, preserve keys");
|
|
482
509
|
console.error(" npx pairai upgrade — update to latest version");
|
|
@@ -1039,49 +1066,71 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1039
1066
|
|
|
1040
1067
|
if (name === "pairai_check_updates") {
|
|
1041
1068
|
await loadPublicKeys();
|
|
1042
|
-
const updates = (await hubGet("/
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1069
|
+
const updates = (await hubGet("/events")) as {
|
|
1070
|
+
events: Array<{
|
|
1071
|
+
id: number;
|
|
1072
|
+
type: string;
|
|
1073
|
+
taskId: string | null;
|
|
1074
|
+
fromAgentId: string | null;
|
|
1075
|
+
data: Record<string, unknown>;
|
|
1076
|
+
createdAt: string;
|
|
1077
|
+
}>;
|
|
1046
1078
|
cursor: number;
|
|
1079
|
+
hasMore: boolean;
|
|
1047
1080
|
};
|
|
1048
1081
|
|
|
1049
|
-
if (
|
|
1082
|
+
if (updates.events.length === 0) {
|
|
1050
1083
|
return { content: [{ type: "text" as const, text: "No updates. You're all caught up." }] };
|
|
1051
1084
|
}
|
|
1052
1085
|
|
|
1053
1086
|
const parts: string[] = [];
|
|
1054
1087
|
|
|
1055
|
-
|
|
1088
|
+
const taskEvents = updates.events.filter(e => e.type === "task.created" || e.type === "task.approval_required");
|
|
1089
|
+
if (taskEvents.length > 0) {
|
|
1056
1090
|
const enriched: string[] = [];
|
|
1057
|
-
for (const
|
|
1058
|
-
|
|
1059
|
-
const
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1091
|
+
for (const event of taskEvents) {
|
|
1092
|
+
if (!event.taskId) continue;
|
|
1093
|
+
const full = (await hubGet(`/tasks/${event.taskId}`)) as any;
|
|
1094
|
+
const desc = full.encrypted ? decryptTaskDescription(full, event.taskId) : (full.description ?? "");
|
|
1095
|
+
const title = desc.split("\n")[0] || full.title || "Untitled";
|
|
1096
|
+
const fromAgent = (event.data.fromAgentName as string) ?? event.fromAgentId ?? "unknown";
|
|
1097
|
+
enriched.push(`- "${title}" from ${fromAgent} (task ID: ${event.taskId})${event.type === "task.approval_required" ? " [APPROVAL REQUIRED]" : ""}`);
|
|
1062
1098
|
}
|
|
1063
|
-
parts.push(`**${
|
|
1099
|
+
parts.push(`**${taskEvents.length} pending task(s):**\n${enriched.join("\n")}`);
|
|
1064
1100
|
}
|
|
1065
1101
|
|
|
1066
|
-
|
|
1102
|
+
const msgEvents = updates.events.filter(e => e.type === "message.created");
|
|
1103
|
+
if (msgEvents.length > 0) {
|
|
1104
|
+
// Group by taskId for summary
|
|
1105
|
+
const byTask = new Map<string, typeof msgEvents>();
|
|
1106
|
+
for (const event of msgEvents) {
|
|
1107
|
+
if (!event.taskId) continue;
|
|
1108
|
+
const list = byTask.get(event.taskId) ?? [];
|
|
1109
|
+
list.push(event);
|
|
1110
|
+
byTask.set(event.taskId, list);
|
|
1111
|
+
}
|
|
1067
1112
|
const enriched: string[] = [];
|
|
1068
|
-
for (const
|
|
1069
|
-
const full = (await hubGet(`/tasks/${
|
|
1070
|
-
const
|
|
1071
|
-
const
|
|
1113
|
+
for (const [taskId, events] of byTask) {
|
|
1114
|
+
const full = (await hubGet(`/tasks/${taskId}`)) as any;
|
|
1115
|
+
const taskTitle = full.title ?? "Untitled";
|
|
1116
|
+
const msgs = (await hubGet(`/tasks/${taskId}/messages`)) as Array<any>;
|
|
1072
1117
|
const previews: string[] = [];
|
|
1073
|
-
for (const
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1118
|
+
for (const event of events) {
|
|
1119
|
+
const messageId = event.data.messageId as string | undefined;
|
|
1120
|
+
const msg = messageId ? msgs.find((m: any) => m.id === messageId) : msgs[msgs.length - 1];
|
|
1121
|
+
if (msg) {
|
|
1122
|
+
const d = full.encrypted ? decryptMessage(msg, taskId) : { content: msg.content, contentType: msg.contentType };
|
|
1123
|
+
previews.push(d.content.slice(0, 100));
|
|
1124
|
+
}
|
|
1076
1125
|
}
|
|
1077
|
-
enriched.push(`- ${
|
|
1126
|
+
enriched.push(`- ${events.length} new in "${taskTitle}" (task ID: ${taskId})\n Preview: ${previews.join(" | ")}`);
|
|
1078
1127
|
}
|
|
1079
1128
|
parts.push(`**Unread messages:**\n${enriched.join("\n")}`);
|
|
1080
1129
|
}
|
|
1081
1130
|
|
|
1082
1131
|
// Ack (idempotent — poll loop also acks after delivery).
|
|
1083
1132
|
if (updates.cursor > 0) {
|
|
1084
|
-
await hubPost("/
|
|
1133
|
+
await hubPost("/events/ack", { cursor: updates.cursor });
|
|
1085
1134
|
}
|
|
1086
1135
|
|
|
1087
1136
|
return { content: [{ type: "text" as const, text: parts.join("\n\n") }] };
|
|
@@ -1671,9 +1720,6 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1671
1720
|
|
|
1672
1721
|
// ── Polling ──────────────────────────────────────────────────────────────────
|
|
1673
1722
|
|
|
1674
|
-
const seenMessages = new Set<string>();
|
|
1675
|
-
const SEEN_MESSAGES_MAX = 10_000;
|
|
1676
|
-
|
|
1677
1723
|
const MIME_MAP: Record<string, string> = {
|
|
1678
1724
|
".md": "text/markdown", ".txt": "text/plain", ".json": "application/json",
|
|
1679
1725
|
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
@@ -1735,147 +1781,163 @@ function decryptTaskDescription(
|
|
|
1735
1781
|
return full.description ?? "";
|
|
1736
1782
|
}
|
|
1737
1783
|
|
|
1738
|
-
async function
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1784
|
+
async function deliverEventNotification(event: {
|
|
1785
|
+
id: number;
|
|
1786
|
+
type: string;
|
|
1787
|
+
taskId: string | null;
|
|
1788
|
+
fromAgentId: string | null;
|
|
1789
|
+
data: Record<string, unknown>;
|
|
1790
|
+
createdAt: string;
|
|
1791
|
+
}) {
|
|
1792
|
+
const fromAgent = (event.data.fromAgentName as string) ?? event.fromAgentId ?? "unknown";
|
|
1793
|
+
|
|
1794
|
+
if (event.type === "task.created" || event.type === "task.approval_required") {
|
|
1795
|
+
if (!event.taskId) return;
|
|
1796
|
+
|
|
1797
|
+
const full = (await hubGet(`/tasks/${event.taskId}`)) as {
|
|
1798
|
+
title?: string;
|
|
1799
|
+
description?: string;
|
|
1800
|
+
encrypted?: boolean;
|
|
1801
|
+
descriptionKeys?: any;
|
|
1802
|
+
senderSignature?: string;
|
|
1803
|
+
initiatorAgentId?: string;
|
|
1804
|
+
approvalStatus?: string | null;
|
|
1748
1805
|
};
|
|
1806
|
+
const taskMsgs = (await hubGet(`/tasks/${event.taskId}/messages`)) as Array<{
|
|
1807
|
+
content: string;
|
|
1808
|
+
contentType: string;
|
|
1809
|
+
senderAgentId: string;
|
|
1810
|
+
encryptedKeys?: any;
|
|
1811
|
+
senderSignature?: string;
|
|
1812
|
+
}>;
|
|
1749
1813
|
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
initiatorAgentId?: string;
|
|
1765
|
-
approvalStatus?: string | null;
|
|
1766
|
-
};
|
|
1767
|
-
const taskMsgs = (await hubGet(`/tasks/${task.id}/messages`)) as Array<{
|
|
1768
|
-
content: string;
|
|
1769
|
-
contentType: string;
|
|
1770
|
-
senderAgentId: string;
|
|
1771
|
-
encryptedKeys?: any;
|
|
1772
|
-
senderSignature?: string;
|
|
1773
|
-
}>;
|
|
1814
|
+
const desc = decryptTaskDescription(full, event.taskId);
|
|
1815
|
+
const taskTitle = desc.split("\n")[0] || full.title || "Untitled";
|
|
1816
|
+
const decryptedMessages = (taskMsgs ?? []).map((m) => {
|
|
1817
|
+
// Encrypted file messages: content is a file ID (short nanoid), not ciphertext
|
|
1818
|
+
if (m.contentType === "encrypted" && m.encryptedKeys && m.content.length < 30) {
|
|
1819
|
+
return `[File attachment — use pairai_download_file with task_id: "${event.taskId}", file_id: "${m.content}"]`;
|
|
1820
|
+
}
|
|
1821
|
+
try {
|
|
1822
|
+
const d = decryptMessage(m, event.taskId!);
|
|
1823
|
+
return d.content;
|
|
1824
|
+
} catch {
|
|
1825
|
+
return "[decryption failed]";
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1774
1828
|
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
const d = decryptMessage(m, task.id);
|
|
1783
|
-
return d.content;
|
|
1784
|
-
} catch {
|
|
1785
|
-
return "[decryption failed]";
|
|
1786
|
-
}
|
|
1787
|
-
});
|
|
1829
|
+
const isPendingApproval = full.approvalStatus === "pending" || event.type === "task.approval_required";
|
|
1830
|
+
const approvalPrefix = isPendingApproval ? "[APPROVAL REQUIRED] " : "";
|
|
1831
|
+
const approvalSuffix = isPendingApproval
|
|
1832
|
+
? `\n\nThis task requires your approval before the agent will act on it.\nUse pairai_approve_task or pairai_reject_task with task ID: ${event.taskId}`
|
|
1833
|
+
: "";
|
|
1834
|
+
|
|
1835
|
+
const body = approvalPrefix + [desc || taskTitle, ...decryptedMessages.map((c) => `> ${c}`)].join("\n\n") + approvalSuffix;
|
|
1788
1836
|
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
: ""
|
|
1837
|
+
await mcp.notification({
|
|
1838
|
+
method: "notifications/claude/channel",
|
|
1839
|
+
params: {
|
|
1840
|
+
content: body,
|
|
1841
|
+
meta: { task_id: event.taskId, task_title: taskTitle, from_agent: fromAgent, event_type: "new_task" },
|
|
1842
|
+
},
|
|
1843
|
+
});
|
|
1844
|
+
console.error(`[pairai] channel notification sent: new_task ${event.taskId} from ${fromAgent}${isPendingApproval ? " [APPROVAL REQUIRED]" : ""}`);
|
|
1845
|
+
|
|
1846
|
+
} else if (event.type === "message.created") {
|
|
1847
|
+
if (!event.taskId) return;
|
|
1848
|
+
|
|
1849
|
+
const msgs = (await hubGet(`/tasks/${event.taskId}/messages`)) as Array<{
|
|
1850
|
+
id: string;
|
|
1851
|
+
content: string;
|
|
1852
|
+
contentType: string;
|
|
1853
|
+
senderAgentId: string;
|
|
1854
|
+
encryptedKeys?: any;
|
|
1855
|
+
senderSignature?: string;
|
|
1856
|
+
}>;
|
|
1857
|
+
if (!msgs || msgs.length === 0) return;
|
|
1794
1858
|
|
|
1795
|
-
|
|
1859
|
+
const messageId = event.data.messageId as string | undefined;
|
|
1860
|
+
const msg = messageId ? msgs.find((m) => m.id === messageId) : msgs[msgs.length - 1];
|
|
1861
|
+
if (!msg) return;
|
|
1796
1862
|
|
|
1863
|
+
// Encrypted file messages: content is a file ID (short nanoid), not ciphertext
|
|
1864
|
+
const isEncryptedFile = msg.contentType === "encrypted" && msg.encryptedKeys && msg.content.length < 30;
|
|
1865
|
+
let decrypted: { content: string; contentType: string };
|
|
1866
|
+
if (isEncryptedFile) {
|
|
1867
|
+
decrypted = { content: `[File attachment — use pairai_download_file with task_id: "${event.taskId}", file_id: "${msg.content}"]`, contentType: "text" };
|
|
1868
|
+
} else {
|
|
1797
1869
|
try {
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
content: body,
|
|
1802
|
-
meta: { task_id: task.id, task_title: task.title, from_agent: task.fromAgent, event_type: "new_task" },
|
|
1803
|
-
},
|
|
1804
|
-
});
|
|
1805
|
-
console.error(`[pairai] channel notification sent: new_task ${task.id} from ${task.fromAgent}${isPendingApproval ? " [APPROVAL REQUIRED]" : ""}`);
|
|
1806
|
-
} catch (err) {
|
|
1807
|
-
console.error(`[pairai] channel notification FAILED: ${(err as Error).message}`);
|
|
1870
|
+
decrypted = decryptMessage(msg, event.taskId);
|
|
1871
|
+
} catch {
|
|
1872
|
+
decrypted = { content: "[decryption failed]", contentType: "text" };
|
|
1808
1873
|
}
|
|
1809
1874
|
}
|
|
1810
1875
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1876
|
+
const full = (await hubGet(`/tasks/${event.taskId}`)) as { title?: string };
|
|
1877
|
+
const taskTitle = full.title ?? "Untitled";
|
|
1878
|
+
|
|
1879
|
+
await mcp.notification({
|
|
1880
|
+
method: "notifications/claude/channel",
|
|
1881
|
+
params: {
|
|
1882
|
+
content: decrypted.content,
|
|
1883
|
+
meta: {
|
|
1884
|
+
task_id: event.taskId,
|
|
1885
|
+
task_title: taskTitle,
|
|
1886
|
+
from_agent: fromAgent,
|
|
1887
|
+
event_type: "new_message",
|
|
1888
|
+
content_type: decrypted.contentType,
|
|
1889
|
+
},
|
|
1890
|
+
},
|
|
1891
|
+
});
|
|
1892
|
+
console.error(`[pairai] channel notification sent: new_message in ${event.taskId}`);
|
|
1893
|
+
|
|
1894
|
+
} else {
|
|
1895
|
+
debugLog(`poll: skipping event type=${event.type} id=${event.id}`);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
async function poll() {
|
|
1900
|
+
try {
|
|
1901
|
+
// Refresh public keys to pick up new connections
|
|
1902
|
+
await loadPublicKeys();
|
|
1903
|
+
|
|
1904
|
+
const updates = (await hubGet("/events")) as {
|
|
1905
|
+
events: Array<{
|
|
1906
|
+
id: number;
|
|
1907
|
+
type: string;
|
|
1908
|
+
taskId: string | null;
|
|
1909
|
+
fromAgentId: string | null;
|
|
1910
|
+
data: Record<string, unknown>;
|
|
1911
|
+
createdAt: string;
|
|
1819
1912
|
}>;
|
|
1913
|
+
cursor: number;
|
|
1914
|
+
hasMore: boolean;
|
|
1915
|
+
};
|
|
1820
1916
|
|
|
1821
|
-
|
|
1822
|
-
if (!msgs || msgs.length === 0) continue;
|
|
1823
|
-
for (const msg of msgs.slice(-unread.count)) {
|
|
1824
|
-
const key = `msg:${msg.id}`;
|
|
1825
|
-
if (seenMessages.has(key)) { debugLog(`skip seen msg ${msg.id}`); continue; }
|
|
1826
|
-
seenMessages.add(key);
|
|
1917
|
+
debugLog(`poll: ${updates.events.length} events, cursor=${updates.cursor}, hasMore=${updates.hasMore}`);
|
|
1827
1918
|
|
|
1828
|
-
|
|
1829
|
-
const isEncryptedFile = msg.contentType === "encrypted" && msg.encryptedKeys && msg.content.length < 30;
|
|
1830
|
-
let decrypted: { content: string; contentType: string };
|
|
1831
|
-
if (isEncryptedFile) {
|
|
1832
|
-
decrypted = { content: `[File attachment — use pairai_download_file with task_id: "${unread.taskId}", file_id: "${msg.content}"]`, contentType: "text" };
|
|
1833
|
-
} else {
|
|
1834
|
-
try {
|
|
1835
|
-
decrypted = decryptMessage(msg, unread.taskId);
|
|
1836
|
-
} catch {
|
|
1837
|
-
decrypted = { content: "[decryption failed]", contentType: "text" };
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1919
|
+
if (updates.events.length === 0) return;
|
|
1840
1920
|
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
meta: {
|
|
1847
|
-
task_id: unread.taskId,
|
|
1848
|
-
task_title: unread.taskTitle,
|
|
1849
|
-
from_agent: msg.senderAgentId,
|
|
1850
|
-
event_type: "new_message",
|
|
1851
|
-
content_type: decrypted.contentType,
|
|
1852
|
-
},
|
|
1853
|
-
},
|
|
1854
|
-
});
|
|
1855
|
-
console.error(`[pairai] channel notification sent: new_message in ${unread.taskId}`);
|
|
1856
|
-
} catch (err) {
|
|
1857
|
-
console.error(`[pairai] channel notification FAILED: ${(err as Error).message}`);
|
|
1858
|
-
}
|
|
1921
|
+
for (const event of updates.events) {
|
|
1922
|
+
try {
|
|
1923
|
+
await deliverEventNotification(event);
|
|
1924
|
+
} catch (err) {
|
|
1925
|
+
console.error(`[pairai] notification delivery failed for event ${event.id}: ${(err as Error).message}`);
|
|
1859
1926
|
}
|
|
1860
1927
|
}
|
|
1861
1928
|
|
|
1862
|
-
// Ack
|
|
1863
|
-
// seenMessages remains as secondary belt-and-suspenders dedup.
|
|
1864
|
-
// See: docs/superpowers/specs/2026-04-04-notification-ack-design.md
|
|
1929
|
+
// Ack after successful delivery
|
|
1865
1930
|
if (updates.cursor > 0) {
|
|
1866
1931
|
try {
|
|
1867
|
-
await hubPost("/
|
|
1932
|
+
await hubPost("/events/ack", { cursor: updates.cursor });
|
|
1868
1933
|
debugLog(`poll: acked cursor=${updates.cursor}`);
|
|
1869
1934
|
} catch (err) {
|
|
1870
1935
|
debugLog(`poll: ack failed (will retry next cycle): ${(err as Error).message}`);
|
|
1871
1936
|
}
|
|
1872
1937
|
}
|
|
1873
1938
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
const excess = seenMessages.size - SEEN_MESSAGES_MAX;
|
|
1877
|
-
const iter = seenMessages.values();
|
|
1878
|
-
for (let i = 0; i < excess; i++) seenMessages.delete(iter.next().value!);
|
|
1939
|
+
if (updates.hasMore) {
|
|
1940
|
+
setImmediate(poll);
|
|
1879
1941
|
}
|
|
1880
1942
|
} catch (err) {
|
|
1881
1943
|
console.error(`[pairai] poll error: ${(err as Error).message}`);
|