@phidiassj/aiyoperps-mcp-installer 0.5.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.
- package/README.md +27 -0
- package/bin/aiyoperps-mcp-installer.js +736 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# AiyoPerps MCP Installer
|
|
2
|
+
|
|
3
|
+
Interactive installer for registering the AiyoPerps MCP bridge with supported AI agent hosts.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y @phidiassj/aiyoperps-mcp-installer
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Optional MCP endpoint override:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y @phidiassj/aiyoperps-mcp-installer --url http://127.0.0.1:5078/mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Current host strategy
|
|
18
|
+
|
|
19
|
+
- `Codex`: patch `~/.codex/config.toml` by updating only the `mcp_servers.aiyoperps` block
|
|
20
|
+
- `Claude Code CLI`: use `claude mcp add-json` / `claude mcp remove`
|
|
21
|
+
- `Claude Desktop`: merge into the desktop JSON config
|
|
22
|
+
- `OpenClaw`: use `openclaw config set` / `openclaw config unset`
|
|
23
|
+
|
|
24
|
+
## Notes
|
|
25
|
+
|
|
26
|
+
- The installer creates `*.bak` backups before editing local config files.
|
|
27
|
+
- If a host is detected but not considered safe to modify, the installer reports it and skips automatic changes.
|
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import readline from 'node:readline/promises';
|
|
6
|
+
import { stdin, stdout, stderr, argv, env, exit, cwd } from 'node:process';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { parse as parseToml } from 'smol-toml';
|
|
9
|
+
|
|
10
|
+
const SERVER_NAME = 'aiyoperps';
|
|
11
|
+
const DEFAULT_URL = 'http://127.0.0.1:5078/mcp';
|
|
12
|
+
|
|
13
|
+
const cli = parseCliArgs(argv.slice(2));
|
|
14
|
+
const targetUrl = cli.url;
|
|
15
|
+
const interactive = stdin.isTTY && stdout.isTTY && !cli.nonInteractive;
|
|
16
|
+
|
|
17
|
+
const hosts = await detectHosts(targetUrl);
|
|
18
|
+
|
|
19
|
+
if (!interactive) {
|
|
20
|
+
await runNonInteractive(cli, hosts, targetUrl);
|
|
21
|
+
exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (cli.statusOnly) {
|
|
25
|
+
printStatus(hosts, targetUrl);
|
|
26
|
+
exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
printBanner(hosts, targetUrl);
|
|
33
|
+
|
|
34
|
+
while (true) {
|
|
35
|
+
stdout.write('\n1. install all\n2. install any of\n3. status\n4. uninstall\n5. exit\n');
|
|
36
|
+
const choice = (await rl.question('Select an action: ')).trim();
|
|
37
|
+
|
|
38
|
+
if (choice === '1') {
|
|
39
|
+
await installAll(hosts, targetUrl);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (choice === '2') {
|
|
44
|
+
await installAnyOf(rl, hosts, targetUrl);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (choice === '3') {
|
|
49
|
+
printStatus(hosts, targetUrl);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (choice === '4') {
|
|
54
|
+
await uninstallAnyOf(rl, hosts);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (choice === '5' || choice === '') {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
stdout.write('Invalid selection.\n');
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
rl.close();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function detectHosts(url) {
|
|
69
|
+
return [
|
|
70
|
+
await detectCodex(url),
|
|
71
|
+
await detectClaudeCode(url),
|
|
72
|
+
await detectClaudeDesktop(url),
|
|
73
|
+
await detectOpenClaw(url)
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function detectCodex(url) {
|
|
78
|
+
const configPath = resolveCodexConfigPath();
|
|
79
|
+
const exists = fs.existsSync(configPath);
|
|
80
|
+
const parentExists = fs.existsSync(path.dirname(configPath));
|
|
81
|
+
const detected = exists || parentExists;
|
|
82
|
+
|
|
83
|
+
let safelyWritable = parentExists;
|
|
84
|
+
let installed = false;
|
|
85
|
+
let reason = detected ? 'ready for TOML block patch' : 'config directory not found';
|
|
86
|
+
|
|
87
|
+
if (exists) {
|
|
88
|
+
try {
|
|
89
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
90
|
+
parseToml(content);
|
|
91
|
+
installed = hasCodexBlock(content);
|
|
92
|
+
reason = installed ? 'installed' : 'config parsed successfully';
|
|
93
|
+
} catch (error) {
|
|
94
|
+
safelyWritable = false;
|
|
95
|
+
reason = `config parse failed: ${error.message}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
id: 'codex',
|
|
101
|
+
name: 'Codex',
|
|
102
|
+
kind: 'file',
|
|
103
|
+
detected,
|
|
104
|
+
supported: safelyWritable,
|
|
105
|
+
installed,
|
|
106
|
+
reason,
|
|
107
|
+
path: configPath,
|
|
108
|
+
install: () => installCodex(configPath, url),
|
|
109
|
+
uninstall: () => uninstallCodex(configPath)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function detectClaudeCode(url) {
|
|
114
|
+
const version = runCommand('claude', ['--version']);
|
|
115
|
+
const detected = version.ok;
|
|
116
|
+
const mcpList = detected ? runCommand('claude', ['mcp', 'list']) : { ok: false, stdout: '' };
|
|
117
|
+
const installed = detected && mcpList.ok && mcpList.stdout.toLowerCase().includes(SERVER_NAME);
|
|
118
|
+
const reason = detected
|
|
119
|
+
? 'official CLI integration available via claude mcp'
|
|
120
|
+
: 'claude command not found in PATH';
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id: 'claude-code',
|
|
124
|
+
name: 'Claude Code CLI',
|
|
125
|
+
kind: 'cli',
|
|
126
|
+
detected,
|
|
127
|
+
supported: detected,
|
|
128
|
+
installed,
|
|
129
|
+
reason,
|
|
130
|
+
path: detected ? 'claude mcp (CLI-managed)' : '',
|
|
131
|
+
install: () => installClaudeCode(url),
|
|
132
|
+
uninstall: () => uninstallClaudeCode()
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function detectClaudeDesktop(url) {
|
|
137
|
+
const candidates = getClaudeDesktopCandidates();
|
|
138
|
+
const configPath = candidates.find(fileExists) ?? candidates[0];
|
|
139
|
+
const exists = fileExists(configPath);
|
|
140
|
+
const parentExists = fs.existsSync(path.dirname(configPath));
|
|
141
|
+
const detected = exists || parentExists;
|
|
142
|
+
let supported = parentExists;
|
|
143
|
+
let installed = false;
|
|
144
|
+
let reason = detected ? 'ready for JSON config merge' : 'config directory not found';
|
|
145
|
+
|
|
146
|
+
if (exists) {
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
149
|
+
installed = Boolean(parsed?.mcpServers?.[SERVER_NAME]);
|
|
150
|
+
reason = installed ? 'installed' : 'config parsed successfully';
|
|
151
|
+
} catch (error) {
|
|
152
|
+
supported = false;
|
|
153
|
+
reason = `config parse failed: ${error.message}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
id: 'claude-desktop',
|
|
159
|
+
name: 'Claude Desktop',
|
|
160
|
+
kind: 'file',
|
|
161
|
+
detected,
|
|
162
|
+
supported,
|
|
163
|
+
installed,
|
|
164
|
+
reason,
|
|
165
|
+
path: configPath,
|
|
166
|
+
install: () => installClaudeDesktop(configPath, url),
|
|
167
|
+
uninstall: () => uninstallClaudeDesktop(configPath)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function detectOpenClaw(url) {
|
|
172
|
+
const cli = runCommand('openclaw', ['--version']);
|
|
173
|
+
const configPath = resolveOpenClawConfigPath();
|
|
174
|
+
const detected = cli.ok || fileExists(configPath);
|
|
175
|
+
|
|
176
|
+
let installed = false;
|
|
177
|
+
let reason = cli.ok
|
|
178
|
+
? 'official CLI config helpers available'
|
|
179
|
+
: fileExists(configPath)
|
|
180
|
+
? 'config file found but CLI not detected'
|
|
181
|
+
: 'openclaw not detected';
|
|
182
|
+
|
|
183
|
+
if (cli.ok) {
|
|
184
|
+
const getResult = runCommand('openclaw', ['config', 'get', `mcp.servers.${SERVER_NAME}`]);
|
|
185
|
+
installed = getResult.ok && Boolean(getResult.stdout.trim());
|
|
186
|
+
if (installed) {
|
|
187
|
+
reason = 'installed';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
id: 'openclaw',
|
|
193
|
+
name: 'OpenClaw',
|
|
194
|
+
kind: 'cli',
|
|
195
|
+
detected,
|
|
196
|
+
supported: cli.ok,
|
|
197
|
+
installed,
|
|
198
|
+
reason,
|
|
199
|
+
path: cli.ok ? '~/.openclaw/openclaw.json (CLI-managed)' : configPath,
|
|
200
|
+
install: () => installOpenClaw(url),
|
|
201
|
+
uninstall: () => uninstallOpenClaw()
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function printBanner(hosts, url) {
|
|
206
|
+
stdout.write(`AiyoPerps MCP installer\nTarget MCP URL: ${url}\n`);
|
|
207
|
+
printStatus(hosts, url);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function printStatus(hosts, url) {
|
|
211
|
+
stdout.write(`\nDetected hosts for ${url}:\n`);
|
|
212
|
+
hosts.forEach((host, index) => {
|
|
213
|
+
const flags = [
|
|
214
|
+
host.detected ? 'detected' : 'not detected',
|
|
215
|
+
host.supported ? 'supported' : 'not safely writable',
|
|
216
|
+
host.installed ? 'installed' : 'not installed'
|
|
217
|
+
].join(', ');
|
|
218
|
+
|
|
219
|
+
stdout.write(
|
|
220
|
+
`${index + 1}. ${host.name}: ${flags}\n reason: ${host.reason}\n` +
|
|
221
|
+
`${host.path ? ` path: ${host.path}\n` : ''}`);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function installAll(hosts, url) {
|
|
226
|
+
const selected = hosts.filter(host => host.detected && host.supported);
|
|
227
|
+
if (selected.length === 0) {
|
|
228
|
+
stdout.write('No detected hosts are safe to modify.\n');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await runForHosts(selected, 'install');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function installAnyOf(rl, hosts, url) {
|
|
236
|
+
const eligible = hosts.filter(host => host.detected && host.supported);
|
|
237
|
+
if (eligible.length === 0) {
|
|
238
|
+
stdout.write('No detected hosts are safe to modify.\n');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
stdout.write('\nSelectable hosts:\n');
|
|
243
|
+
eligible.forEach((host, index) => {
|
|
244
|
+
stdout.write(`${index + 1}. ${host.name}\n`);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const raw = (await rl.question('Enter host numbers (comma separated): ')).trim();
|
|
248
|
+
const selected = parseSelections(raw, eligible);
|
|
249
|
+
if (selected.length === 0) {
|
|
250
|
+
stdout.write('Nothing selected.\n');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
await runForHosts(selected, 'install');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function uninstallAnyOf(rl, hosts) {
|
|
258
|
+
const eligible = hosts.filter(host => host.detected && host.supported && host.installed);
|
|
259
|
+
if (eligible.length === 0) {
|
|
260
|
+
stdout.write('No installed hosts found for uninstall.\n');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
stdout.write('\nInstalled hosts:\n');
|
|
265
|
+
eligible.forEach((host, index) => {
|
|
266
|
+
stdout.write(`${index + 1}. ${host.name}\n`);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const raw = (await rl.question('Enter host numbers to uninstall (comma separated, or "all"): ')).trim();
|
|
270
|
+
const selected = raw.toLowerCase() === 'all'
|
|
271
|
+
? eligible
|
|
272
|
+
: parseSelections(raw, eligible);
|
|
273
|
+
|
|
274
|
+
if (selected.length === 0) {
|
|
275
|
+
stdout.write('Nothing selected.\n');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
await runForHosts(selected, 'uninstall');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function runForHosts(hosts, action) {
|
|
283
|
+
for (const host of hosts) {
|
|
284
|
+
stdout.write(`\n${action} -> ${host.name}\n`);
|
|
285
|
+
try {
|
|
286
|
+
await host[action]();
|
|
287
|
+
host.installed = action === 'install';
|
|
288
|
+
host.reason = action === 'install' ? 'installed' : 'removed';
|
|
289
|
+
stdout.write(' success\n');
|
|
290
|
+
} catch (error) {
|
|
291
|
+
stdout.write(` failed: ${error.message}\n`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function runNonInteractive(cliArgs, hosts, url) {
|
|
297
|
+
if (cliArgs.uninstall.length > 0) {
|
|
298
|
+
const selected = selectHostsByIds(hosts, cliArgs.uninstall, {
|
|
299
|
+
requireDetected: true,
|
|
300
|
+
requireSupported: true,
|
|
301
|
+
requireInstalled: false
|
|
302
|
+
});
|
|
303
|
+
await runForHosts(selected, 'uninstall');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (cliArgs.installAll) {
|
|
308
|
+
await installAll(hosts, url);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (cliArgs.install.length > 0) {
|
|
313
|
+
const selected = selectHostsByIds(hosts, cliArgs.install, {
|
|
314
|
+
requireDetected: true,
|
|
315
|
+
requireSupported: true,
|
|
316
|
+
requireInstalled: false
|
|
317
|
+
});
|
|
318
|
+
await runForHosts(selected, 'install');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
printStatus(hosts, url);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function parseSelections(raw, hosts) {
|
|
326
|
+
const indexes = new Set(
|
|
327
|
+
raw
|
|
328
|
+
.split(',')
|
|
329
|
+
.map(value => Number.parseInt(value.trim(), 10))
|
|
330
|
+
.filter(Number.isInteger)
|
|
331
|
+
.filter(value => value >= 1 && value <= hosts.length));
|
|
332
|
+
|
|
333
|
+
return [...indexes].map(index => hosts[index - 1]);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function selectHostsByIds(hosts, ids, requirements) {
|
|
337
|
+
const wantAll = ids.includes('all');
|
|
338
|
+
const selected = wantAll
|
|
339
|
+
? hosts
|
|
340
|
+
: hosts.filter(host => ids.includes(host.id));
|
|
341
|
+
|
|
342
|
+
return selected.filter(host => {
|
|
343
|
+
if (requirements.requireDetected && !host.detected) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (requirements.requireSupported && !host.supported) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (requirements.requireInstalled && !host.installed) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return true;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function installCodex(configPath, url) {
|
|
360
|
+
ensureParentDir(configPath);
|
|
361
|
+
|
|
362
|
+
const original = fileExists(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
363
|
+
if (original) {
|
|
364
|
+
parseToml(original);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const block = buildCodexBlock(url);
|
|
368
|
+
let next = original;
|
|
369
|
+
|
|
370
|
+
if (hasCodexBlock(original)) {
|
|
371
|
+
next = replaceCodexBlock(original, block);
|
|
372
|
+
} else if (original.trim().length === 0) {
|
|
373
|
+
next = `${block}\n`;
|
|
374
|
+
} else {
|
|
375
|
+
next = `${original.replace(/\s*$/, '')}\n\n${block}\n`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
parseToml(next);
|
|
379
|
+
backupIfExists(configPath);
|
|
380
|
+
fs.writeFileSync(configPath, next, 'utf8');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function uninstallCodex(configPath) {
|
|
384
|
+
if (!fileExists(configPath)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const original = fs.readFileSync(configPath, 'utf8');
|
|
389
|
+
if (!hasCodexBlock(original)) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const next = removeCodexBlock(original);
|
|
394
|
+
if (next.trim()) {
|
|
395
|
+
parseToml(next);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
backupIfExists(configPath);
|
|
399
|
+
fs.writeFileSync(configPath, next.trim() ? `${next.replace(/\s*$/, '')}\n` : '', 'utf8');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function installClaudeDesktop(configPath, url) {
|
|
403
|
+
ensureParentDir(configPath);
|
|
404
|
+
|
|
405
|
+
const payload = fileExists(configPath)
|
|
406
|
+
? JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
407
|
+
: {};
|
|
408
|
+
|
|
409
|
+
payload.mcpServers ??= {};
|
|
410
|
+
payload.mcpServers[SERVER_NAME] = buildJsonServerConfig(url);
|
|
411
|
+
|
|
412
|
+
backupIfExists(configPath);
|
|
413
|
+
fs.writeFileSync(configPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function uninstallClaudeDesktop(configPath) {
|
|
417
|
+
if (!fileExists(configPath)) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const payload = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
422
|
+
if (!payload?.mcpServers?.[SERVER_NAME]) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
delete payload.mcpServers[SERVER_NAME];
|
|
427
|
+
if (Object.keys(payload.mcpServers).length === 0) {
|
|
428
|
+
delete payload.mcpServers;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
backupIfExists(configPath);
|
|
432
|
+
fs.writeFileSync(configPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function installClaudeCode(url) {
|
|
436
|
+
const commandLine = resolveCommandLineSpec(process.platform, url);
|
|
437
|
+
const spec = JSON.stringify({
|
|
438
|
+
type: 'stdio',
|
|
439
|
+
command: commandLine.command,
|
|
440
|
+
args: commandLine.args
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const command = ['mcp', 'add-json', SERVER_NAME, spec, '--scope', 'user'];
|
|
444
|
+
const first = runCommand('claude', command);
|
|
445
|
+
if (first.ok) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const fallback = runCommand('claude', ['mcp', 'add-json', '--scope', 'user', SERVER_NAME, spec]);
|
|
450
|
+
if (!fallback.ok) {
|
|
451
|
+
throw new Error(first.stderr || fallback.stderr || 'claude mcp add-json failed');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function uninstallClaudeCode() {
|
|
456
|
+
const first = runCommand('claude', ['mcp', 'remove', SERVER_NAME, '--scope', 'user']);
|
|
457
|
+
if (first.ok) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const fallback = runCommand('claude', ['mcp', 'remove', '--scope', 'user', SERVER_NAME]);
|
|
462
|
+
if (!fallback.ok) {
|
|
463
|
+
throw new Error(first.stderr || fallback.stderr || 'claude mcp remove failed');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function installOpenClaw(url) {
|
|
468
|
+
const value = JSON.stringify(buildJsonServerConfig(url));
|
|
469
|
+
const result = runCommand('openclaw', ['config', 'set', `mcp.servers.${SERVER_NAME}`, value, '--json']);
|
|
470
|
+
if (!result.ok) {
|
|
471
|
+
throw new Error(result.stderr || 'openclaw config set failed');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function uninstallOpenClaw() {
|
|
476
|
+
const result = runCommand('openclaw', ['config', 'unset', `mcp.servers.${SERVER_NAME}`]);
|
|
477
|
+
if (!result.ok) {
|
|
478
|
+
throw new Error(result.stderr || 'openclaw config unset failed');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function buildCodexBlock(url) {
|
|
483
|
+
const commandLine = resolveCommandLineSpec(process.platform, url);
|
|
484
|
+
const args = JSON.stringify(commandLine.args, null, 0);
|
|
485
|
+
return [
|
|
486
|
+
`[mcp_servers.${SERVER_NAME}]`,
|
|
487
|
+
`command = ${JSON.stringify(commandLine.command)}`,
|
|
488
|
+
`args = ${args}`
|
|
489
|
+
].join('\n');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function buildJsonServerConfig(url) {
|
|
493
|
+
const commandLine = resolveCommandLineSpec(process.platform, url);
|
|
494
|
+
return {
|
|
495
|
+
command: commandLine.command,
|
|
496
|
+
args: commandLine.args
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function hasCodexBlock(content) {
|
|
501
|
+
return new RegExp(`^\\[mcp_servers\\.${SERVER_NAME}\\]\\s*$`, 'm').test(content);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function replaceCodexBlock(content, block) {
|
|
505
|
+
const range = findCodexBlockRange(content);
|
|
506
|
+
if (!range) {
|
|
507
|
+
throw new Error('Existing Codex MCP block could not be safely replaced.');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const prefix = content.slice(0, range.start).replace(/\s*$/, '');
|
|
511
|
+
const suffix = content.slice(range.end).replace(/^\s*/, '');
|
|
512
|
+
return [prefix, block, suffix].filter(Boolean).join('\n\n');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function removeCodexBlock(content) {
|
|
516
|
+
const range = findCodexBlockRange(content);
|
|
517
|
+
if (!range) {
|
|
518
|
+
return content;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const prefix = content.slice(0, range.start).replace(/\s*$/, '');
|
|
522
|
+
const suffix = content.slice(range.end).replace(/^\s*/, '');
|
|
523
|
+
return [prefix, suffix].filter(Boolean).join('\n\n');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function findCodexBlockRange(content) {
|
|
527
|
+
const headerRegex = new RegExp(`^\\[mcp_servers\\.${SERVER_NAME}\\]\\s*$`, 'm');
|
|
528
|
+
const match = headerRegex.exec(content);
|
|
529
|
+
if (!match) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const start = match.index;
|
|
534
|
+
const afterHeader = start + match[0].length;
|
|
535
|
+
const rest = content.slice(afterHeader);
|
|
536
|
+
const nextHeaderMatch = /^\\[[^\\]]+\\]\\s*$/m.exec(rest);
|
|
537
|
+
const end = nextHeaderMatch ? afterHeader + nextHeaderMatch.index : content.length;
|
|
538
|
+
return { start, end };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function resolveTargetUrl(args) {
|
|
542
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
543
|
+
const arg = args[index];
|
|
544
|
+
if (arg.startsWith('--url=')) {
|
|
545
|
+
return arg.slice('--url='.length);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (arg === '--url' && index + 1 < args.length) {
|
|
549
|
+
return args[index + 1];
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return env.AIYOPERPS_MCP_URL || DEFAULT_URL;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function parseCliArgs(args) {
|
|
557
|
+
const install = [];
|
|
558
|
+
const uninstall = [];
|
|
559
|
+
let installAll = false;
|
|
560
|
+
let statusOnly = false;
|
|
561
|
+
let nonInteractive = false;
|
|
562
|
+
let url = env.AIYOPERPS_MCP_URL || DEFAULT_URL;
|
|
563
|
+
|
|
564
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
565
|
+
const arg = args[index];
|
|
566
|
+
if (arg === '--yes') {
|
|
567
|
+
installAll = true;
|
|
568
|
+
nonInteractive = true;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (arg === '--status') {
|
|
573
|
+
statusOnly = true;
|
|
574
|
+
nonInteractive = true;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (arg === '--install-all') {
|
|
579
|
+
installAll = true;
|
|
580
|
+
nonInteractive = true;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (arg.startsWith('--install=')) {
|
|
585
|
+
install.push(...splitCsvArg(arg.slice('--install='.length)));
|
|
586
|
+
nonInteractive = true;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (arg === '--install' && index + 1 < args.length) {
|
|
591
|
+
install.push(...splitCsvArg(args[index + 1]));
|
|
592
|
+
nonInteractive = true;
|
|
593
|
+
index += 1;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (arg.startsWith('--uninstall=')) {
|
|
598
|
+
uninstall.push(...splitCsvArg(arg.slice('--uninstall='.length)));
|
|
599
|
+
nonInteractive = true;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (arg === '--uninstall' && index + 1 < args.length) {
|
|
604
|
+
uninstall.push(...splitCsvArg(args[index + 1]));
|
|
605
|
+
nonInteractive = true;
|
|
606
|
+
index += 1;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (arg.startsWith('--url=')) {
|
|
611
|
+
url = arg.slice('--url='.length);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (arg === '--url' && index + 1 < args.length) {
|
|
616
|
+
url = args[index + 1];
|
|
617
|
+
index += 1;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
install,
|
|
624
|
+
uninstall,
|
|
625
|
+
installAll,
|
|
626
|
+
statusOnly,
|
|
627
|
+
nonInteractive,
|
|
628
|
+
url
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function splitCsvArg(value) {
|
|
633
|
+
return value
|
|
634
|
+
.split(',')
|
|
635
|
+
.map(item => item.trim())
|
|
636
|
+
.filter(Boolean);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function resolveCommandLineSpec(platform, url) {
|
|
640
|
+
const packageArgs = ['-y', '@phidiassj/aiyoperps-mcp-bridge', '--url', url];
|
|
641
|
+
if (platform === 'win32') {
|
|
642
|
+
return {
|
|
643
|
+
command: 'cmd',
|
|
644
|
+
args: ['/c', 'npx', ...packageArgs]
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return {
|
|
649
|
+
command: 'npx',
|
|
650
|
+
args: packageArgs
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function resolveCodexConfigPath() {
|
|
655
|
+
if (env.CODEX_CONFIG_PATH) {
|
|
656
|
+
return env.CODEX_CONFIG_PATH;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (process.platform === 'win32') {
|
|
660
|
+
const home = env.USERPROFILE || os.homedir();
|
|
661
|
+
return path.join(home, '.codex', 'config.toml');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return path.join(os.homedir(), '.codex', 'config.toml');
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function getClaudeDesktopCandidates() {
|
|
668
|
+
if (env.CLAUDE_DESKTOP_CONFIG_PATH) {
|
|
669
|
+
return [env.CLAUDE_DESKTOP_CONFIG_PATH];
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (process.platform === 'win32') {
|
|
673
|
+
const appData = env.APPDATA || path.join(env.USERPROFILE || os.homedir(), 'AppData', 'Roaming');
|
|
674
|
+
return [
|
|
675
|
+
path.join(appData, 'Claude', 'claude_desktop_config.json'),
|
|
676
|
+
path.join(appData, 'Claude', 'config.json')
|
|
677
|
+
];
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (process.platform === 'darwin') {
|
|
681
|
+
return [
|
|
682
|
+
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
683
|
+
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'config.json')
|
|
684
|
+
];
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return [
|
|
688
|
+
path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json'),
|
|
689
|
+
path.join(os.homedir(), '.config', 'Claude', 'config.json')
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function resolveOpenClawConfigPath() {
|
|
694
|
+
const explicit = env.OPENCLAW_CONFIG_PATH;
|
|
695
|
+
if (explicit) {
|
|
696
|
+
return explicit;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const stateDir = env.OPENCLAW_STATE_DIR || path.join(os.homedir(), '.openclaw');
|
|
700
|
+
return path.join(stateDir, 'openclaw.json');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function runCommand(command, args) {
|
|
704
|
+
const result = spawnSync(command, args, {
|
|
705
|
+
encoding: 'utf8',
|
|
706
|
+
cwd: cwd()
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
ok: result.status === 0,
|
|
711
|
+
status: result.status,
|
|
712
|
+
stdout: (result.stdout || '').trim(),
|
|
713
|
+
stderr: (result.stderr || '').trim(),
|
|
714
|
+
error: result.error
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function ensureParentDir(filePath) {
|
|
719
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function backupIfExists(filePath) {
|
|
723
|
+
if (!fileExists(filePath)) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
fs.copyFileSync(filePath, `${filePath}.bak`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function fileExists(filePath) {
|
|
731
|
+
try {
|
|
732
|
+
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
|
|
733
|
+
} catch {
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@phidiassj/aiyoperps-mcp-installer",
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "Interactive installer for registering AiyoPerps MCP with supported AI agent hosts.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"aiyoperps-mcp-installer": "./bin/aiyoperps-mcp-installer.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"smol-toml": "^1.4.2"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"aiyoperps",
|
|
22
|
+
"installer",
|
|
23
|
+
"codex",
|
|
24
|
+
"claude",
|
|
25
|
+
"openclaw"
|
|
26
|
+
]
|
|
27
|
+
}
|