agentic-relay 3.8.0
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/CLAUDE.md +56 -0
- package/README.md +178 -0
- package/bin/cli.mjs +36 -0
- package/bin/install.mjs +887 -0
- package/bin/lib/api.mjs +188 -0
- package/bin/lib/cache.mjs +161 -0
- package/bin/lib/commands.mjs +495 -0
- package/bin/lib/config.mjs +69 -0
- package/bin/lib/deliverable.mjs +70 -0
- package/bin/lib/driver.mjs +165 -0
- package/bin/lib/fetch.mjs +189 -0
- package/bin/lib/parse.mjs +148 -0
- package/bin/lib/pool.mjs +26 -0
- package/bin/relay-core.mjs +382 -0
- package/bin/test/budget.test.mjs +31 -0
- package/bin/test/cache.test.mjs +71 -0
- package/bin/test/driver.test.mjs +49 -0
- package/bin/test/fetch.test.mjs +128 -0
- package/bin/test/parse.test.mjs +70 -0
- package/bin/test/session.test.mjs +67 -0
- package/package.json +49 -0
- package/schema/deliverable.json +47 -0
- package/skill/SKILL.md +194 -0
package/bin/install.mjs
ADDED
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Relay Installer v3 — multi-agent (skill, MCP, deeplink modes)
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
*
|
|
8
|
+
* Interactive (human):
|
|
9
|
+
* npx agentic-relay
|
|
10
|
+
*
|
|
11
|
+
* Non-interactive (AI agent / scripted):
|
|
12
|
+
* npx agentic-relay --all --api-key=am_live_xxx
|
|
13
|
+
* npx agentic-relay --target=claude,codex,gemini --api-key=am_live_xxx
|
|
14
|
+
* npx agentic-relay --target=cursor,boltai
|
|
15
|
+
*
|
|
16
|
+
* Flags:
|
|
17
|
+
* --target=a,b,c Comma-separated list of agent IDs to install (see AGENTS map)
|
|
18
|
+
* --all Install for every Tier 1 + Tier 2 agent (skips manual)
|
|
19
|
+
* --project Also install project-scoped configs (Roo Code) when in a repo
|
|
20
|
+
* --api-key=KEY Set API key non-interactively
|
|
21
|
+
* --librechat=PATH Path to librechat.yaml (required for LibreChat install)
|
|
22
|
+
*
|
|
23
|
+
* Modes:
|
|
24
|
+
* skill Drop SKILL.md + (if supported) append CLAUDE.md/AGENTS.md
|
|
25
|
+
* mcp_json Idempotent JSON merge of agentrelay entry under {mcpServers|servers}
|
|
26
|
+
* mcp_yaml Idempotent YAML block merge under mcpServers:
|
|
27
|
+
* mcp_witsy Bespoke nested merge inside Witsy's settings.json
|
|
28
|
+
* project_roo Workspace-scoped .roo/mcp.json + .roo/rules/agent-relay.md (needs --project + git repo)
|
|
29
|
+
* deeplink Print URL the agent registers (tome://, raycast://) — user clicks Install
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
33
|
+
import { homedir } from "os";
|
|
34
|
+
import { join, dirname } from "path";
|
|
35
|
+
import { fileURLToPath } from "url";
|
|
36
|
+
import { createInterface } from "readline/promises";
|
|
37
|
+
import { stdin, stdout } from "process";
|
|
38
|
+
import { execSync } from "child_process";
|
|
39
|
+
|
|
40
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const HOME = homedir();
|
|
42
|
+
|
|
43
|
+
const SECTION_START = "# Agent Relay MCP Server";
|
|
44
|
+
const CREDENTIALS_PATH = join(HOME, ".config", "agent-relay", "credentials.json");
|
|
45
|
+
const LEGACY_CREDENTIALS_PATH = join(HOME, ".config", "penguin", "credentials.json");
|
|
46
|
+
const SIGNUP_URL = "https://attentionmarket-auth.vercel.app";
|
|
47
|
+
const API_BASE =
|
|
48
|
+
"https://peruwnbrqkvmrldhpoom.supabase.co/functions/v1/api-v1";
|
|
49
|
+
const MCP_URL = "https://peruwnbrqkvmrldhpoom.supabase.co/functions/v1/mcp";
|
|
50
|
+
const SERVER_NAME = "agent-relay";
|
|
51
|
+
|
|
52
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
53
|
+
// Agent matrix
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
55
|
+
|
|
56
|
+
// Mode legend:
|
|
57
|
+
// skill — drop SKILL.md + optionally append CLAUDE.md content to instructions
|
|
58
|
+
// mcp_json — idempotent merge of agentrelay entry into a JSON config
|
|
59
|
+
// mcp_yaml — idempotent merge into a YAML config (LibreChat)
|
|
60
|
+
// mcp_witsy — Witsy-specific nested merge
|
|
61
|
+
// project_roo — workspace-scoped Roo Code (only with --project flag inside a repo)
|
|
62
|
+
// deeplink — print URL, optionally `open` it on macOS
|
|
63
|
+
|
|
64
|
+
const AGENTS = {
|
|
65
|
+
// ── Tier 1A: skill mode ──────────────────────────────────────────
|
|
66
|
+
claude: {
|
|
67
|
+
mode: "skill",
|
|
68
|
+
label: "Claude Code",
|
|
69
|
+
skillDir: join(HOME, ".claude", "skills", "agent-relay"),
|
|
70
|
+
instructionsPath: join(HOME, ".claude", "CLAUDE.md"),
|
|
71
|
+
detectPath: join(HOME, ".claude"),
|
|
72
|
+
},
|
|
73
|
+
codex: {
|
|
74
|
+
mode: "skill",
|
|
75
|
+
label: "Codex CLI",
|
|
76
|
+
skillDir: join(HOME, ".codex", "skills", "agent-relay"),
|
|
77
|
+
instructionsPath: join(HOME, ".codex", "AGENTS.md"),
|
|
78
|
+
detectPath: join(HOME, ".codex"),
|
|
79
|
+
},
|
|
80
|
+
gemini: {
|
|
81
|
+
mode: "skill",
|
|
82
|
+
label: "Gemini CLI",
|
|
83
|
+
skillDir: join(HOME, ".gemini", "skills", "agent-relay"),
|
|
84
|
+
instructionsPath: join(HOME, ".gemini", "GEMINI.md"),
|
|
85
|
+
detectPath: join(HOME, ".gemini"),
|
|
86
|
+
},
|
|
87
|
+
goose: {
|
|
88
|
+
mode: "skill",
|
|
89
|
+
label: "Goose",
|
|
90
|
+
skillDir: join(HOME, ".config", "goose", "skills", "agent-relay"),
|
|
91
|
+
instructionsPath: null,
|
|
92
|
+
detectPath: join(HOME, ".config", "goose"),
|
|
93
|
+
},
|
|
94
|
+
windsurf: {
|
|
95
|
+
mode: "skill",
|
|
96
|
+
label: "Windsurf",
|
|
97
|
+
skillDir: join(HOME, ".codeium", "windsurf", "skills", "agent-relay"),
|
|
98
|
+
instructionsPath: null,
|
|
99
|
+
detectPath: join(HOME, ".codeium", "windsurf"),
|
|
100
|
+
},
|
|
101
|
+
openclaw: {
|
|
102
|
+
mode: "skill",
|
|
103
|
+
label: "OpenClaw",
|
|
104
|
+
skillDir: join(HOME, ".openclaw", "skills", "agent-relay"),
|
|
105
|
+
instructionsPath: null,
|
|
106
|
+
detectPath: join(HOME, ".openclaw"),
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// ── Tier 1B: MCP JSON mode ───────────────────────────────────────
|
|
110
|
+
cursor: {
|
|
111
|
+
mode: "mcp_json",
|
|
112
|
+
label: "Cursor",
|
|
113
|
+
configPath: join(HOME, ".cursor", "mcp.json"),
|
|
114
|
+
schemaKey: "mcpServers",
|
|
115
|
+
detectPath: join(HOME, ".cursor"),
|
|
116
|
+
},
|
|
117
|
+
claude_desktop: {
|
|
118
|
+
mode: "mcp_json",
|
|
119
|
+
label: "Claude Desktop",
|
|
120
|
+
configPath: join(
|
|
121
|
+
HOME,
|
|
122
|
+
"Library",
|
|
123
|
+
"Application Support",
|
|
124
|
+
"Claude",
|
|
125
|
+
"claude_desktop_config.json",
|
|
126
|
+
),
|
|
127
|
+
schemaKey: "mcpServers",
|
|
128
|
+
detectPath: join(HOME, "Library", "Application Support", "Claude"),
|
|
129
|
+
},
|
|
130
|
+
vscode: {
|
|
131
|
+
mode: "mcp_json",
|
|
132
|
+
label: "VS Code",
|
|
133
|
+
configPath: join(
|
|
134
|
+
HOME,
|
|
135
|
+
"Library",
|
|
136
|
+
"Application Support",
|
|
137
|
+
"Code",
|
|
138
|
+
"User",
|
|
139
|
+
"mcp.json",
|
|
140
|
+
),
|
|
141
|
+
schemaKey: "servers", // NOTE: VS Code uses "servers" not "mcpServers"
|
|
142
|
+
detectPath: join(HOME, "Library", "Application Support", "Code"),
|
|
143
|
+
},
|
|
144
|
+
vscode_insiders: {
|
|
145
|
+
mode: "mcp_json",
|
|
146
|
+
label: "VS Code Insiders",
|
|
147
|
+
configPath: join(
|
|
148
|
+
HOME,
|
|
149
|
+
"Library",
|
|
150
|
+
"Application Support",
|
|
151
|
+
"Code - Insiders",
|
|
152
|
+
"User",
|
|
153
|
+
"mcp.json",
|
|
154
|
+
),
|
|
155
|
+
schemaKey: "servers",
|
|
156
|
+
detectPath: join(HOME, "Library", "Application Support", "Code - Insiders"),
|
|
157
|
+
},
|
|
158
|
+
cline: {
|
|
159
|
+
mode: "mcp_json",
|
|
160
|
+
label: "Cline",
|
|
161
|
+
configPath: join(
|
|
162
|
+
HOME,
|
|
163
|
+
"Library",
|
|
164
|
+
"Application Support",
|
|
165
|
+
"Code",
|
|
166
|
+
"User",
|
|
167
|
+
"globalStorage",
|
|
168
|
+
"saoudrizwan.claude-dev",
|
|
169
|
+
"settings",
|
|
170
|
+
"cline_mcp_settings.json",
|
|
171
|
+
),
|
|
172
|
+
schemaKey: "mcpServers",
|
|
173
|
+
detectPath: join(
|
|
174
|
+
HOME,
|
|
175
|
+
"Library",
|
|
176
|
+
"Application Support",
|
|
177
|
+
"Code",
|
|
178
|
+
"User",
|
|
179
|
+
"globalStorage",
|
|
180
|
+
"saoudrizwan.claude-dev",
|
|
181
|
+
),
|
|
182
|
+
},
|
|
183
|
+
boltai: {
|
|
184
|
+
mode: "mcp_json",
|
|
185
|
+
label: "BoltAI",
|
|
186
|
+
configPath: join(HOME, ".boltai", "mcp.json"),
|
|
187
|
+
schemaKey: "mcpServers",
|
|
188
|
+
detectPath: join(HOME, ".boltai"),
|
|
189
|
+
},
|
|
190
|
+
amazon_q_cli: {
|
|
191
|
+
mode: "mcp_json",
|
|
192
|
+
label: "Amazon Q (CLI)",
|
|
193
|
+
configPath: join(HOME, ".aws", "amazonq", "mcp.json"),
|
|
194
|
+
schemaKey: "mcpServers",
|
|
195
|
+
detectPath: join(HOME, ".aws", "amazonq"),
|
|
196
|
+
},
|
|
197
|
+
amazon_q_ide: {
|
|
198
|
+
mode: "mcp_json",
|
|
199
|
+
label: "Amazon Q (IDE)",
|
|
200
|
+
configPath: join(HOME, ".aws", "amazonq", "default.json"),
|
|
201
|
+
schemaKey: "mcpServers",
|
|
202
|
+
detectPath: join(HOME, ".aws", "amazonq"),
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// ── Bespoke writers ───────────────────────────────────────────────
|
|
206
|
+
witsy: {
|
|
207
|
+
mode: "mcp_witsy",
|
|
208
|
+
label: "Witsy",
|
|
209
|
+
configPath: join(HOME, "Library", "Application Support", "Witsy", "settings.json"),
|
|
210
|
+
detectPath: join(HOME, "Library", "Application Support", "Witsy"),
|
|
211
|
+
},
|
|
212
|
+
librechat: {
|
|
213
|
+
mode: "mcp_yaml",
|
|
214
|
+
label: "LibreChat",
|
|
215
|
+
configPath: null, // user-supplied via --librechat=path
|
|
216
|
+
schemaKey: "mcpServers",
|
|
217
|
+
detectPath: null,
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
// ── Tier 1C: project-scoped (requires --project + git repo) ──────
|
|
221
|
+
roo_code: {
|
|
222
|
+
mode: "project_roo",
|
|
223
|
+
label: "Roo Code (project)",
|
|
224
|
+
projectScoped: true,
|
|
225
|
+
detectPath: null,
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
// ── Tier 2: deeplink ─────────────────────────────────────────────
|
|
229
|
+
tome: {
|
|
230
|
+
mode: "deeplink",
|
|
231
|
+
label: "Tome",
|
|
232
|
+
deeplink: () =>
|
|
233
|
+
`tome://mcp/install?config=${encodeURIComponent(
|
|
234
|
+
JSON.stringify({ mcpServers: { [SERVER_NAME]: { url: MCP_URL } } }),
|
|
235
|
+
)}`,
|
|
236
|
+
detectPath: join(HOME, "Library", "Application Support", "Tome"),
|
|
237
|
+
},
|
|
238
|
+
raycast: {
|
|
239
|
+
mode: "deeplink",
|
|
240
|
+
label: "Raycast",
|
|
241
|
+
deeplink: () =>
|
|
242
|
+
// Raycast MCP extension URL — users install/configure via the extension UI.
|
|
243
|
+
`raycast://extensions/EvanZhouDev/mcp/manage-mcp`,
|
|
244
|
+
detectPath: join(HOME, "Library", "Application Support", "com.raycast.macos"),
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Tier 3 (manual) targets are intentionally not in this build per the plan.
|
|
249
|
+
// Will be added in a follow-up release.
|
|
250
|
+
|
|
251
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
252
|
+
// Logging helpers
|
|
253
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
254
|
+
|
|
255
|
+
const log = (m) => console.log(`\x1b[36m[agent-relay]\x1b[0m ${m}`);
|
|
256
|
+
const success = (m) => console.log(`\x1b[32m[agent-relay]\x1b[0m ${m}`);
|
|
257
|
+
const warn = (m) => console.log(`\x1b[33m[agent-relay]\x1b[0m ${m}`);
|
|
258
|
+
const error = (m) => console.error(`\x1b[31m[agent-relay]\x1b[0m ${m}`);
|
|
259
|
+
|
|
260
|
+
function escapeRegex(str) {
|
|
261
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
265
|
+
// CLI args
|
|
266
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
267
|
+
|
|
268
|
+
function parseArgs() {
|
|
269
|
+
const args = process.argv.slice(2);
|
|
270
|
+
const flags = {
|
|
271
|
+
targets: null,
|
|
272
|
+
all: args.includes("--all"),
|
|
273
|
+
project: args.includes("--project"),
|
|
274
|
+
apiKey: null,
|
|
275
|
+
librechat: null,
|
|
276
|
+
// Legacy single-flag shortcuts (kept for backward compat)
|
|
277
|
+
legacyClaude: args.includes("--claude"),
|
|
278
|
+
legacyCodex: args.includes("--codex"),
|
|
279
|
+
legacyCursor: args.includes("--cursor"),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
for (const arg of args) {
|
|
283
|
+
if (arg.startsWith("--target=")) {
|
|
284
|
+
flags.targets = arg
|
|
285
|
+
.slice("--target=".length)
|
|
286
|
+
.split(",")
|
|
287
|
+
.map((s) => s.trim())
|
|
288
|
+
.filter(Boolean);
|
|
289
|
+
} else if (arg.startsWith("--api-key=")) {
|
|
290
|
+
flags.apiKey = arg.slice("--api-key=".length);
|
|
291
|
+
} else if (arg.startsWith("--librechat=")) {
|
|
292
|
+
flags.librechat = arg.slice("--librechat=".length);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Support `--api-key VALUE` (space-separated) too
|
|
297
|
+
const keyIdx = args.indexOf("--api-key");
|
|
298
|
+
if (keyIdx !== -1 && args[keyIdx + 1] && !args[keyIdx + 1].startsWith("--")) {
|
|
299
|
+
flags.apiKey = args[keyIdx + 1];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return flags;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
306
|
+
// Skill installer
|
|
307
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
308
|
+
|
|
309
|
+
function installSkill(skillContent, instructionsContent, agent) {
|
|
310
|
+
try {
|
|
311
|
+
mkdirSync(agent.skillDir, { recursive: true });
|
|
312
|
+
writeFileSync(join(agent.skillDir, "SKILL.md"), skillContent);
|
|
313
|
+
success(`${agent.label}: skill → ${agent.skillDir}/SKILL.md`);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
warn(`${agent.label}: could not write skill — ${err.message}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (agent.instructionsPath) {
|
|
319
|
+
installInstructions(instructionsContent, agent.instructionsPath, agent.label);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function installInstructions(instructions, path, label) {
|
|
324
|
+
try {
|
|
325
|
+
const dir = dirname(path);
|
|
326
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
327
|
+
|
|
328
|
+
const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
329
|
+
|
|
330
|
+
if (existing.includes(SECTION_START)) {
|
|
331
|
+
const sectionRegex = new RegExp(
|
|
332
|
+
`${escapeRegex(SECTION_START)}[\\s\\S]*?(?=\\n# (?!Agent Relay)|\\s*$)`,
|
|
333
|
+
);
|
|
334
|
+
writeFileSync(path, existing.replace(sectionRegex, instructions));
|
|
335
|
+
success(`${label}: instructions updated → ${path}`);
|
|
336
|
+
} else {
|
|
337
|
+
const sep = existing.length > 0 ? "\n\n" : "";
|
|
338
|
+
writeFileSync(path, existing + sep + instructions + "\n");
|
|
339
|
+
success(`${label}: instructions added → ${path}`);
|
|
340
|
+
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
warn(`${label}: could not write instructions — ${err.message}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
347
|
+
// MCP JSON installer (handles both `mcpServers` and `servers` keys)
|
|
348
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
349
|
+
|
|
350
|
+
function installMcpJson(agent) {
|
|
351
|
+
try {
|
|
352
|
+
const dir = dirname(agent.configPath);
|
|
353
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
354
|
+
|
|
355
|
+
let config = {};
|
|
356
|
+
if (existsSync(agent.configPath)) {
|
|
357
|
+
try {
|
|
358
|
+
const raw = readFileSync(agent.configPath, "utf-8").trim();
|
|
359
|
+
config = raw.length > 0 ? JSON.parse(raw) : {};
|
|
360
|
+
} catch (parseErr) {
|
|
361
|
+
warn(`${agent.label}: existing config is not valid JSON — overwriting (${parseErr.message})`);
|
|
362
|
+
config = {};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const key = agent.schemaKey;
|
|
367
|
+
if (!config[key] || typeof config[key] !== "object") {
|
|
368
|
+
config[key] = {};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// VS Code's "servers" entries support {type, url} format
|
|
372
|
+
// Other agents use {url} or {command, args}
|
|
373
|
+
if (key === "servers") {
|
|
374
|
+
config[key][SERVER_NAME] = { type: "http", url: MCP_URL };
|
|
375
|
+
} else {
|
|
376
|
+
config[key][SERVER_NAME] = { url: MCP_URL };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
|
|
380
|
+
success(`${agent.label}: MCP server registered → ${agent.configPath}`);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
warn(`${agent.label}: could not write MCP config — ${err.message}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
387
|
+
// Witsy bespoke installer — settings.json has nested mcp config
|
|
388
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
389
|
+
|
|
390
|
+
function installWitsy(agent) {
|
|
391
|
+
try {
|
|
392
|
+
const dir = dirname(agent.configPath);
|
|
393
|
+
if (!existsSync(dir)) {
|
|
394
|
+
warn(`${agent.label}: not detected (skipping). Install Witsy and run again to configure.`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let config = {};
|
|
399
|
+
if (existsSync(agent.configPath)) {
|
|
400
|
+
try {
|
|
401
|
+
config = JSON.parse(readFileSync(agent.configPath, "utf-8"));
|
|
402
|
+
} catch (parseErr) {
|
|
403
|
+
warn(`${agent.label}: existing settings.json invalid — skipping (${parseErr.message})`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Witsy stores MCP servers under config.mcp.servers as an array of {uuid, url, label, ...}
|
|
409
|
+
// We add a single agentrelay entry, replacing any previous one.
|
|
410
|
+
if (!config.mcp || typeof config.mcp !== "object") config.mcp = {};
|
|
411
|
+
if (!Array.isArray(config.mcp.servers)) config.mcp.servers = [];
|
|
412
|
+
|
|
413
|
+
// Remove any existing agentrelay entry by label match
|
|
414
|
+
config.mcp.servers = config.mcp.servers.filter(
|
|
415
|
+
(s) => !(s && (s.label === SERVER_NAME || s.uuid === SERVER_NAME)),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
config.mcp.servers.push({
|
|
419
|
+
uuid: SERVER_NAME,
|
|
420
|
+
label: SERVER_NAME,
|
|
421
|
+
type: "http",
|
|
422
|
+
url: MCP_URL,
|
|
423
|
+
enabled: true,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + "\n");
|
|
427
|
+
success(`${agent.label}: MCP server registered → ${agent.configPath}`);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
warn(`${agent.label}: could not write Witsy config — ${err.message}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
434
|
+
// LibreChat YAML installer (line-based block patcher, no YAML lib)
|
|
435
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
436
|
+
|
|
437
|
+
function installLibreChat(agent, librechatPath) {
|
|
438
|
+
if (!librechatPath) {
|
|
439
|
+
warn(`${agent.label}: needs --librechat=<path> flag pointing to your librechat.yaml`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (!existsSync(librechatPath)) {
|
|
443
|
+
warn(`${agent.label}: librechat.yaml not found at ${librechatPath}`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const yaml = readFileSync(librechatPath, "utf-8");
|
|
449
|
+
const lines = yaml.split("\n");
|
|
450
|
+
|
|
451
|
+
// Build the agentrelay block (2-space indent inside mcpServers:)
|
|
452
|
+
const relayBlock = [
|
|
453
|
+
` ${SERVER_NAME}:`,
|
|
454
|
+
` type: streamable-http`,
|
|
455
|
+
` url: ${MCP_URL}`,
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
// Find existing mcpServers: section
|
|
459
|
+
const mcpIdx = lines.findIndex((l) => /^mcpServers\s*:/.test(l));
|
|
460
|
+
|
|
461
|
+
if (mcpIdx === -1) {
|
|
462
|
+
// No mcpServers section — append a new one at the bottom
|
|
463
|
+
const trailing = yaml.endsWith("\n") ? "" : "\n";
|
|
464
|
+
writeFileSync(
|
|
465
|
+
librechatPath,
|
|
466
|
+
yaml + trailing + "\n" + "mcpServers:\n" + relayBlock.join("\n") + "\n",
|
|
467
|
+
);
|
|
468
|
+
success(`${agent.label}: mcpServers section added → ${librechatPath}`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Find the existing agentrelay entry inside mcpServers
|
|
473
|
+
let entryStart = -1;
|
|
474
|
+
let entryEnd = lines.length;
|
|
475
|
+
for (let i = mcpIdx + 1; i < lines.length; i++) {
|
|
476
|
+
const line = lines[i];
|
|
477
|
+
if (/^\S/.test(line) && line.trim()) {
|
|
478
|
+
// hit a top-level key — end of mcpServers section
|
|
479
|
+
entryEnd = i;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
if (new RegExp(`^ ${SERVER_NAME}\\s*:`).test(line)) {
|
|
483
|
+
entryStart = i;
|
|
484
|
+
// find end of this entry (next 2-space key or top-level)
|
|
485
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
486
|
+
const ln = lines[j];
|
|
487
|
+
if (/^\S/.test(ln) && ln.trim()) {
|
|
488
|
+
entryEnd = j;
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
if (/^ \S/.test(ln) && !/^ /.test(ln)) {
|
|
492
|
+
entryEnd = j;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
let updated;
|
|
501
|
+
if (entryStart !== -1) {
|
|
502
|
+
// Replace existing agentrelay entry
|
|
503
|
+
updated = [
|
|
504
|
+
...lines.slice(0, entryStart),
|
|
505
|
+
...relayBlock,
|
|
506
|
+
...lines.slice(entryEnd),
|
|
507
|
+
].join("\n");
|
|
508
|
+
} else {
|
|
509
|
+
// Insert agentrelay entry at the start of mcpServers section
|
|
510
|
+
updated = [
|
|
511
|
+
...lines.slice(0, mcpIdx + 1),
|
|
512
|
+
...relayBlock,
|
|
513
|
+
...lines.slice(mcpIdx + 1),
|
|
514
|
+
].join("\n");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
writeFileSync(librechatPath, updated);
|
|
518
|
+
success(`${agent.label}: agentrelay entry merged → ${librechatPath}`);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
warn(`${agent.label}: could not patch librechat.yaml — ${err.message}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
525
|
+
// Roo Code project installer (workspace-scoped)
|
|
526
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
527
|
+
|
|
528
|
+
function installRoo(agent, instructionsContent) {
|
|
529
|
+
let repoRoot;
|
|
530
|
+
try {
|
|
531
|
+
repoRoot = execSync("git rev-parse --show-toplevel", {
|
|
532
|
+
encoding: "utf-8",
|
|
533
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
534
|
+
}).trim();
|
|
535
|
+
} catch {
|
|
536
|
+
warn(`${agent.label}: not in a git repo. Run with --project from a repo root.`);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const rooDir = join(repoRoot, ".roo");
|
|
541
|
+
const rulesDir = join(rooDir, "rules");
|
|
542
|
+
try {
|
|
543
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
544
|
+
|
|
545
|
+
// Write .roo/mcp.json
|
|
546
|
+
const mcpPath = join(rooDir, "mcp.json");
|
|
547
|
+
let mcpConfig = {};
|
|
548
|
+
if (existsSync(mcpPath)) {
|
|
549
|
+
try { mcpConfig = JSON.parse(readFileSync(mcpPath, "utf-8")); } catch {}
|
|
550
|
+
}
|
|
551
|
+
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
552
|
+
mcpConfig.mcpServers[SERVER_NAME] = { url: MCP_URL };
|
|
553
|
+
writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
554
|
+
|
|
555
|
+
// Write .roo/rules/agent-relay.md
|
|
556
|
+
const rulesPath = join(rulesDir, "agent-relay.md");
|
|
557
|
+
writeFileSync(rulesPath, instructionsContent + "\n");
|
|
558
|
+
|
|
559
|
+
success(`${agent.label}: project config written → ${rooDir}/`);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
warn(`${agent.label}: could not write project config — ${err.message}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
566
|
+
// Deeplink "installer"
|
|
567
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
568
|
+
|
|
569
|
+
function installDeeplink(agent) {
|
|
570
|
+
const url = agent.deeplink();
|
|
571
|
+
log(`${agent.label}: open this URL to install Agent Relay in the app:`);
|
|
572
|
+
console.log(` \x1b[4m${url}\x1b[0m`);
|
|
573
|
+
|
|
574
|
+
// Try to open it on macOS
|
|
575
|
+
if (process.platform === "darwin") {
|
|
576
|
+
try {
|
|
577
|
+
execSync(`open "${url}"`, { stdio: "ignore" });
|
|
578
|
+
success(`${agent.label}: opened ${agent.label} install URL`);
|
|
579
|
+
} catch {
|
|
580
|
+
// ignore — user can click the URL manually
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
586
|
+
// API key
|
|
587
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
588
|
+
|
|
589
|
+
function saveApiKey(key) {
|
|
590
|
+
mkdirSync(dirname(CREDENTIALS_PATH), { recursive: true });
|
|
591
|
+
writeFileSync(
|
|
592
|
+
CREDENTIALS_PATH,
|
|
593
|
+
JSON.stringify({ api_key: key }, null, 2) + "\n",
|
|
594
|
+
);
|
|
595
|
+
success(`API key saved → ${CREDENTIALS_PATH}`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function getExistingKey() {
|
|
599
|
+
// Prefer the new path, but recognise a legacy ~/.config/penguin key too so an
|
|
600
|
+
// existing user isn't re-prompted (resolveApiKey applies the same fallback).
|
|
601
|
+
for (const path of [CREDENTIALS_PATH, LEGACY_CREDENTIALS_PATH]) {
|
|
602
|
+
if (!existsSync(path)) continue;
|
|
603
|
+
try {
|
|
604
|
+
const creds = JSON.parse(readFileSync(path, "utf-8"));
|
|
605
|
+
if (creds.api_key && creds.api_key.startsWith("am_live_")) return creds.api_key;
|
|
606
|
+
} catch {}
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Link a bare global `agentrelay` command so users skip `npx` after install.
|
|
613
|
+
* Installs the exact package copy being run (works whether invoked via npx or a
|
|
614
|
+
* checkout). No-ops if `agentrelay` already resolves; warns + falls back to a
|
|
615
|
+
* manual instruction if the global prefix isn't writable (EACCES etc.).
|
|
616
|
+
*/
|
|
617
|
+
function linkGlobalCli() {
|
|
618
|
+
try {
|
|
619
|
+
execSync("agentrelay version", { stdio: "ignore" });
|
|
620
|
+
success("`agentrelay` already on your PATH");
|
|
621
|
+
return;
|
|
622
|
+
} catch {
|
|
623
|
+
// not linked yet — proceed to install it
|
|
624
|
+
}
|
|
625
|
+
const pkgRoot = join(__dirname, "..");
|
|
626
|
+
try {
|
|
627
|
+
execSync(`npm install -g "${pkgRoot}"`, { stdio: "ignore" });
|
|
628
|
+
success("linked global `agentrelay` CLI");
|
|
629
|
+
} catch {
|
|
630
|
+
warn("could not auto-link the global CLI (permissions?). Run once: npm i -g agentic-relay");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function setupApiKeyInteractive() {
|
|
635
|
+
const existing = getExistingKey();
|
|
636
|
+
if (existing) {
|
|
637
|
+
success(`API key already configured (${CREDENTIALS_PATH})`);
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
console.log("");
|
|
642
|
+
log("API key setup");
|
|
643
|
+
console.log("");
|
|
644
|
+
console.log(" Your API key starts with \x1b[1mam_live_\x1b[0m.");
|
|
645
|
+
console.log(
|
|
646
|
+
` Don't have one? Sign up at \x1b[4m${SIGNUP_URL}\x1b[0m (Account tab).`,
|
|
647
|
+
);
|
|
648
|
+
console.log("");
|
|
649
|
+
|
|
650
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
651
|
+
try {
|
|
652
|
+
for (;;) {
|
|
653
|
+
const key = (await rl.question(" Paste your API key: ")).trim();
|
|
654
|
+
if (key.startsWith("am_live_") && key.length > 20) {
|
|
655
|
+
saveApiKey(key);
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
if (key === "" || key.toLowerCase() === "skip") {
|
|
659
|
+
warn("Skipped API key setup. Agents will prompt you for it later.");
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
console.log(
|
|
663
|
+
' Key should start with "am_live_". Try again, or type "skip" to set it up later.',
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
} finally {
|
|
667
|
+
rl.close();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
672
|
+
// Verify
|
|
673
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
674
|
+
|
|
675
|
+
async function verifyInstall() {
|
|
676
|
+
const key = getExistingKey();
|
|
677
|
+
if (!key) return;
|
|
678
|
+
|
|
679
|
+
log("Verifying API key…");
|
|
680
|
+
try {
|
|
681
|
+
const resp = await fetch(`${API_BASE}/search`, {
|
|
682
|
+
method: "POST",
|
|
683
|
+
headers: {
|
|
684
|
+
"Content-Type": "application/json",
|
|
685
|
+
"X-AM-API-Key": key,
|
|
686
|
+
},
|
|
687
|
+
body: JSON.stringify({ query: "test", max_results: 1 }),
|
|
688
|
+
});
|
|
689
|
+
const data = await resp.json();
|
|
690
|
+
if (resp.ok && data.capabilities) {
|
|
691
|
+
success("API key verified — search is working");
|
|
692
|
+
} else {
|
|
693
|
+
warn(`API returned ${resp.status}: ${data.error || "unknown error"}`);
|
|
694
|
+
}
|
|
695
|
+
} catch (err) {
|
|
696
|
+
warn(`Verification failed: ${err.message}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
701
|
+
// Detection + interactive selection
|
|
702
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
703
|
+
|
|
704
|
+
function detectInstalledAgents() {
|
|
705
|
+
const installed = [];
|
|
706
|
+
for (const [id, agent] of Object.entries(AGENTS)) {
|
|
707
|
+
if (agent.projectScoped) continue; // skip project-only agents
|
|
708
|
+
if (!agent.detectPath) continue;
|
|
709
|
+
if (existsSync(agent.detectPath)) installed.push(id);
|
|
710
|
+
}
|
|
711
|
+
return installed;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function selectTargetsInteractive() {
|
|
715
|
+
const installed = detectInstalledAgents();
|
|
716
|
+
const allIds = Object.keys(AGENTS).filter((id) => !AGENTS[id].projectScoped);
|
|
717
|
+
|
|
718
|
+
console.log(" Detected installed agents (will install Agent Relay in these by default):");
|
|
719
|
+
if (installed.length === 0) {
|
|
720
|
+
console.log(" \x1b[33m(none detected)\x1b[0m");
|
|
721
|
+
} else {
|
|
722
|
+
installed.forEach((id) => console.log(` • ${AGENTS[id].label}`));
|
|
723
|
+
}
|
|
724
|
+
console.log("");
|
|
725
|
+
|
|
726
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
727
|
+
try {
|
|
728
|
+
const ans = (
|
|
729
|
+
await rl.question(
|
|
730
|
+
" Press ENTER to install for detected agents, or type comma-separated IDs (or 'all'): ",
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
.trim()
|
|
734
|
+
.toLowerCase();
|
|
735
|
+
|
|
736
|
+
if (!ans) {
|
|
737
|
+
if (installed.length > 0) return installed;
|
|
738
|
+
// Nothing detected — show full list and re-prompt
|
|
739
|
+
console.log("");
|
|
740
|
+
console.log(" Available agent IDs:");
|
|
741
|
+
allIds.forEach((id) =>
|
|
742
|
+
console.log(` ${id.padEnd(18)} ${AGENTS[id].label}`),
|
|
743
|
+
);
|
|
744
|
+
console.log("");
|
|
745
|
+
const ans2 = (
|
|
746
|
+
await rl.question(" Type comma-separated IDs (or 'all'): ")
|
|
747
|
+
).trim().toLowerCase();
|
|
748
|
+
if (ans2 === "all") return allIds;
|
|
749
|
+
return ans2.split(",").map((s) => s.trim()).filter((s) => AGENTS[s]);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (ans === "all") return allIds;
|
|
753
|
+
return ans.split(",").map((s) => s.trim()).filter((s) => AGENTS[s]);
|
|
754
|
+
} finally {
|
|
755
|
+
rl.close();
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
760
|
+
// Main
|
|
761
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
762
|
+
|
|
763
|
+
async function main() {
|
|
764
|
+
console.log("");
|
|
765
|
+
console.log(" \x1b[1mAgent Relay Installer\x1b[0m");
|
|
766
|
+
console.log(
|
|
767
|
+
" AI-native search for software, tools, services & business solutions",
|
|
768
|
+
);
|
|
769
|
+
console.log("");
|
|
770
|
+
|
|
771
|
+
const flags = parseArgs();
|
|
772
|
+
|
|
773
|
+
// Resolve targets
|
|
774
|
+
let targets = [];
|
|
775
|
+
if (flags.all) {
|
|
776
|
+
targets = Object.keys(AGENTS).filter((id) => !AGENTS[id].projectScoped);
|
|
777
|
+
if (flags.project) {
|
|
778
|
+
// include project-scoped only when --project is also set
|
|
779
|
+
targets.push(...Object.keys(AGENTS).filter((id) => AGENTS[id].projectScoped));
|
|
780
|
+
}
|
|
781
|
+
} else if (flags.targets && flags.targets.length > 0) {
|
|
782
|
+
targets = flags.targets.filter((id) => {
|
|
783
|
+
if (!AGENTS[id]) {
|
|
784
|
+
warn(`Unknown agent id: ${id}`);
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
return true;
|
|
788
|
+
});
|
|
789
|
+
} else if (flags.legacyClaude || flags.legacyCodex || flags.legacyCursor) {
|
|
790
|
+
if (flags.legacyClaude) targets.push("claude");
|
|
791
|
+
if (flags.legacyCodex) targets.push("codex");
|
|
792
|
+
if (flags.legacyCursor) targets.push("cursor");
|
|
793
|
+
} else {
|
|
794
|
+
targets = await selectTargetsInteractive();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (!targets || targets.length === 0) {
|
|
798
|
+
error("No agents selected. Exiting.");
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
log(
|
|
803
|
+
`Installing for: ${targets.map((t) => AGENTS[t]?.label || t).join(", ")}`,
|
|
804
|
+
);
|
|
805
|
+
console.log("");
|
|
806
|
+
|
|
807
|
+
// Load bundled files
|
|
808
|
+
const skillContent = readFileSync(
|
|
809
|
+
join(__dirname, "..", "skill", "SKILL.md"),
|
|
810
|
+
"utf-8",
|
|
811
|
+
);
|
|
812
|
+
const instructionsContent = readFileSync(
|
|
813
|
+
join(__dirname, "..", "CLAUDE.md"),
|
|
814
|
+
"utf-8",
|
|
815
|
+
).trim();
|
|
816
|
+
|
|
817
|
+
// Run handlers per target
|
|
818
|
+
for (const id of targets) {
|
|
819
|
+
const agent = AGENTS[id];
|
|
820
|
+
if (!agent) continue;
|
|
821
|
+
|
|
822
|
+
switch (agent.mode) {
|
|
823
|
+
case "skill":
|
|
824
|
+
installSkill(skillContent, instructionsContent, agent);
|
|
825
|
+
break;
|
|
826
|
+
case "mcp_json":
|
|
827
|
+
installMcpJson(agent);
|
|
828
|
+
break;
|
|
829
|
+
case "mcp_witsy":
|
|
830
|
+
installWitsy(agent);
|
|
831
|
+
break;
|
|
832
|
+
case "mcp_yaml":
|
|
833
|
+
installLibreChat(agent, flags.librechat);
|
|
834
|
+
break;
|
|
835
|
+
case "project_roo":
|
|
836
|
+
if (!flags.project) {
|
|
837
|
+
warn(`${agent.label}: pass --project to install project-scoped config`);
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
installRoo(agent, instructionsContent);
|
|
841
|
+
break;
|
|
842
|
+
case "deeplink":
|
|
843
|
+
installDeeplink(agent);
|
|
844
|
+
break;
|
|
845
|
+
default:
|
|
846
|
+
warn(`${agent.label}: unknown mode ${agent.mode}`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// API key
|
|
851
|
+
console.log("");
|
|
852
|
+
if (flags.apiKey) {
|
|
853
|
+
if (flags.apiKey.startsWith("am_live_") && flags.apiKey.length > 20) {
|
|
854
|
+
saveApiKey(flags.apiKey);
|
|
855
|
+
} else {
|
|
856
|
+
error(`Invalid API key format: "${flags.apiKey}". Must start with "am_live_".`);
|
|
857
|
+
}
|
|
858
|
+
} else {
|
|
859
|
+
await setupApiKeyInteractive();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
await verifyInstall();
|
|
863
|
+
|
|
864
|
+
// Put a bare `agentrelay` command on PATH so users don't need npx every time.
|
|
865
|
+
console.log("");
|
|
866
|
+
linkGlobalCli();
|
|
867
|
+
|
|
868
|
+
console.log("");
|
|
869
|
+
success("Done! Agent Relay is installed.");
|
|
870
|
+
console.log("");
|
|
871
|
+
console.log(
|
|
872
|
+
" Your agents will now search Agent Relay before web search or training knowledge.",
|
|
873
|
+
);
|
|
874
|
+
console.log("");
|
|
875
|
+
console.log(` Try it: agentrelay search "weather api"`);
|
|
876
|
+
console.log(` API key: ${CREDENTIALS_PATH}`);
|
|
877
|
+
console.log("");
|
|
878
|
+
log(
|
|
879
|
+
"For MCP-based agents, you may need to authenticate once via the agent's MCP console.",
|
|
880
|
+
);
|
|
881
|
+
console.log("");
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
main().catch((err) => {
|
|
885
|
+
error(`Install failed: ${err.message}`);
|
|
886
|
+
process.exit(1);
|
|
887
|
+
});
|