@starlink-awaken/agentmesh 1.0.3 → 1.2.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +60 -41
  2. package/README.zh-CN.md +137 -167
  3. package/config/gateway.yaml +78 -0
  4. package/dist/src/cli/connect.d.ts +26 -0
  5. package/dist/src/cli/connect.js +544 -0
  6. package/dist/src/cli/setup.d.ts +2 -0
  7. package/dist/src/cli/setup.js +97 -0
  8. package/dist/src/cli.js +410 -0
  9. package/dist/src/core/logger.d.ts +14 -0
  10. package/dist/src/core/logger.js +57 -0
  11. package/dist/{index.js → src/index.js} +40 -8
  12. package/dist/src/model-gateway/circuit-breaker.d.ts +21 -0
  13. package/dist/src/model-gateway/circuit-breaker.js +86 -0
  14. package/dist/src/model-gateway/health.d.ts +12 -0
  15. package/dist/src/model-gateway/health.js +80 -0
  16. package/dist/src/model-gateway/providers.d.ts +4 -0
  17. package/dist/src/model-gateway/providers.js +113 -0
  18. package/dist/src/model-gateway/quota.d.ts +4 -0
  19. package/dist/src/model-gateway/quota.js +107 -0
  20. package/dist/src/model-gateway/rate-limit.d.ts +12 -0
  21. package/dist/src/model-gateway/rate-limit.js +51 -0
  22. package/dist/src/model-gateway/retry.d.ts +14 -0
  23. package/dist/src/model-gateway/retry.js +48 -0
  24. package/dist/src/model-gateway/router.d.ts +4 -0
  25. package/dist/src/model-gateway/router.js +79 -0
  26. package/dist/src/model-gateway/routes.d.ts +2 -0
  27. package/dist/src/model-gateway/routes.js +172 -0
  28. package/dist/src/model-gateway/types.d.ts +47 -0
  29. package/dist/src/model-gateway/types.js +1 -0
  30. package/dist/tests/core/context-manager.test.d.ts +1 -0
  31. package/dist/tests/core/context-manager.test.js +35 -0
  32. package/dist/tests/core/router.test.d.ts +1 -0
  33. package/dist/tests/core/router.test.js +79 -0
  34. package/dist/tests/model-gateway/circuit-breaker.test.d.ts +1 -0
  35. package/dist/tests/model-gateway/circuit-breaker.test.js +84 -0
  36. package/dist/tests/model-gateway/providers.test.d.ts +1 -0
  37. package/dist/tests/model-gateway/providers.test.js +80 -0
  38. package/dist/tests/model-gateway/quota.test.d.ts +1 -0
  39. package/dist/tests/model-gateway/quota.test.js +60 -0
  40. package/dist/tests/model-gateway/rate-limit.test.d.ts +1 -0
  41. package/dist/tests/model-gateway/rate-limit.test.js +42 -0
  42. package/dist/tests/model-gateway/retry.test.d.ts +1 -0
  43. package/dist/tests/model-gateway/retry.test.js +47 -0
  44. package/dist/tests/model-gateway/router.test.d.ts +1 -0
  45. package/dist/tests/model-gateway/router.test.js +108 -0
  46. package/dist/tests/model-gateway/routes.test.d.ts +1 -0
  47. package/dist/tests/model-gateway/routes.test.js +83 -0
  48. package/docs/api.md +187 -460
  49. package/docs/architecture.md +138 -0
  50. package/docs/configuration.md +188 -0
  51. package/package.json +3 -1
  52. package/dist/cli.js +0 -246
  53. /package/dist/{adapters → src/adapters}/base.d.ts +0 -0
  54. /package/dist/{adapters → src/adapters}/base.js +0 -0
  55. /package/dist/{adapters → src/adapters}/claude-code.d.ts +0 -0
  56. /package/dist/{adapters → src/adapters}/claude-code.js +0 -0
  57. /package/dist/{adapters → src/adapters}/openclaw.d.ts +0 -0
  58. /package/dist/{adapters → src/adapters}/openclaw.js +0 -0
  59. /package/dist/{adapters → src/adapters}/process.d.ts +0 -0
  60. /package/dist/{adapters → src/adapters}/process.js +0 -0
  61. /package/dist/{cli.d.ts → src/cli.d.ts} +0 -0
  62. /package/dist/{core → src/core}/agent-registry.d.ts +0 -0
  63. /package/dist/{core → src/core}/agent-registry.js +0 -0
  64. /package/dist/{core → src/core}/config.d.ts +0 -0
  65. /package/dist/{core → src/core}/config.js +0 -0
  66. /package/dist/{core → src/core}/context-manager.d.ts +0 -0
  67. /package/dist/{core → src/core}/context-manager.js +0 -0
  68. /package/dist/{core → src/core}/event-bus.d.ts +0 -0
  69. /package/dist/{core → src/core}/event-bus.js +0 -0
  70. /package/dist/{core → src/core}/metrics.d.ts +0 -0
  71. /package/dist/{core → src/core}/metrics.js +0 -0
  72. /package/dist/{core → src/core}/router.d.ts +0 -0
  73. /package/dist/{core → src/core}/router.js +0 -0
  74. /package/dist/{core → src/core}/task-manager.d.ts +0 -0
  75. /package/dist/{core → src/core}/task-manager.js +0 -0
  76. /package/dist/{core → src/core}/vector-store.d.ts +0 -0
  77. /package/dist/{core → src/core}/vector-store.js +0 -0
  78. /package/dist/{index.d.ts → src/index.d.ts} +0 -0
  79. /package/dist/{routes → src/routes}/api.d.ts +0 -0
  80. /package/dist/{routes → src/routes}/api.js +0 -0
  81. /package/dist/{routes → src/routes}/websocket.d.ts +0 -0
  82. /package/dist/{routes → src/routes}/websocket.js +0 -0
  83. /package/dist/{types → src/types}/index.d.ts +0 -0
  84. /package/dist/{types → src/types}/index.js +0 -0
@@ -0,0 +1,544 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * agentmesh connect / disconnect — 一键接入/断开各 AI 工具
4
+ *
5
+ * 支持: Codex Desktop, Claude Code, Gemini CLI, Cursor, Windsurf,
6
+ * KiloCode, Cline, OpenCode, OpenRouter, 通用 OPENAI_API_BASE
7
+ */
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
9
+ import { join, dirname } from 'node:path';
10
+ import { logger } from '../core/logger.js';
11
+ // ============================================================================
12
+ // Helpers
13
+ // ============================================================================
14
+ const HOME = Bun.env.HOME || '/tmp';
15
+ const BACKUP_DIR = join(HOME, '.config', 'agentmesh', 'backups');
16
+ const GATEWAY_URL = Bun.env.AGENT_MESH_URL || 'http://127.0.0.1:3000/v1';
17
+ const GATEWAY_HOST = new URL(GATEWAY_URL).host;
18
+ function ensureDir(path) {
19
+ if (!existsSync(path))
20
+ mkdirSync(path, { recursive: true });
21
+ }
22
+ function backupFile(originalPath) {
23
+ if (!existsSync(originalPath))
24
+ return null;
25
+ ensureDir(BACKUP_DIR);
26
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
27
+ const name = originalPath.replace(/\//g, '_').replace(/^_/, '');
28
+ const backupPath = join(BACKUP_DIR, `${name}.${ts}.bak`);
29
+ copyFileSync(originalPath, backupPath);
30
+ return backupPath;
31
+ }
32
+ // TOML 简单序列化(够用,不引入额外依赖)
33
+ function toToml(obj, indent = '') {
34
+ const lines = [];
35
+ for (const [key, value] of Object.entries(obj)) {
36
+ if (value === null || value === undefined)
37
+ continue;
38
+ if (typeof value === 'object' && !Array.isArray(value)) {
39
+ lines.push(`${indent}[${key}]`);
40
+ lines.push(toToml(value, indent));
41
+ }
42
+ else if (Array.isArray(value)) {
43
+ for (const item of value) {
44
+ if (typeof item === 'string') {
45
+ lines.push(`${indent}${key} = "${item}"`);
46
+ }
47
+ else {
48
+ lines.push(`${indent}${key} = ${JSON.stringify(item)}`);
49
+ }
50
+ }
51
+ }
52
+ else if (typeof value === 'string') {
53
+ lines.push(`${indent}${key} = "${value}"`);
54
+ }
55
+ else if (typeof value === 'boolean') {
56
+ lines.push(`${indent}${key} = ${value}`);
57
+ }
58
+ else {
59
+ lines.push(`${indent}${key} = ${value}`);
60
+ }
61
+ }
62
+ return lines.join('\n');
63
+ }
64
+ // ============================================================================
65
+ // Tool Adapters
66
+ // ============================================================================
67
+ // Codex Desktop (~/.codex/config.toml)
68
+ const codexDesktopAdapter = {
69
+ name: 'codex-desktop',
70
+ description: 'OpenAI Codex Desktop (macOS app)',
71
+ detect: () => existsSync(join(HOME, '.codex')),
72
+ getConfigPath: () => join(HOME, '.codex', 'config.toml'),
73
+ readConfig() {
74
+ const path = this.getConfigPath();
75
+ if (!path || !existsSync(path))
76
+ return null;
77
+ try {
78
+ const content = readFileSync(path, 'utf-8');
79
+ return { raw: content }; // TOML 解析简化处理
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ },
85
+ generateConfig(gwUrl) {
86
+ const path = this.getConfigPath();
87
+ const section = {
88
+ model: 'deepseek-v4-pro',
89
+ model_provider: 'agentmesh',
90
+ };
91
+ const providerSection = {
92
+ name: 'Agent Mesh Gateway',
93
+ base_url: gwUrl,
94
+ wire_api: 'responses',
95
+ env_key: 'OPENAI_API_KEY',
96
+ };
97
+ return {
98
+ path,
99
+ format: 'toml',
100
+ content: { model: section.model, model_provider: section.model_provider, model_providers: { agentmesh: providerSection } },
101
+ };
102
+ },
103
+ hasGatewayConfig(config) {
104
+ return config?.raw?.includes('agentmesh') || config?.raw?.includes('model_providers');
105
+ },
106
+ };
107
+ // Claude Code (~/.claude/settings.json)
108
+ const claudeCodeAdapter = {
109
+ name: 'claude-code',
110
+ description: 'Anthropic Claude Code CLI',
111
+ detect: () => existsSync(join(HOME, '.claude')),
112
+ getConfigPath: () => join(HOME, '.claude', 'settings.json'),
113
+ readConfig() {
114
+ const path = this.getConfigPath();
115
+ if (!path || !existsSync(path))
116
+ return {};
117
+ try {
118
+ return JSON.parse(readFileSync(path, 'utf-8'));
119
+ }
120
+ catch {
121
+ return {};
122
+ }
123
+ },
124
+ generateConfig(_gwUrl) {
125
+ return null; // Claude Code 不支持直接换模型后端,用通用 ENV 方式
126
+ },
127
+ hasGatewayConfig() { return false; },
128
+ };
129
+ // Shell Profile (~/.zshrc 或 ~/.bashrc) — 通用 OPENAI_API_BASE
130
+ const shellProfileAdapter = {
131
+ name: 'shell-env',
132
+ description: 'Shell 环境变量 (OPENAI_API_BASE)',
133
+ detect: () => existsSync(join(HOME, '.zshrc')) || existsSync(join(HOME, '.bashrc')),
134
+ getConfigPath() {
135
+ if (existsSync(join(HOME, '.zshrc')))
136
+ return join(HOME, '.zshrc');
137
+ if (existsSync(join(HOME, '.bashrc')))
138
+ return join(HOME, '.bashrc');
139
+ return null;
140
+ },
141
+ readConfig() {
142
+ const path = this.getConfigPath();
143
+ if (!path)
144
+ return null;
145
+ try {
146
+ const content = readFileSync(path, 'utf-8');
147
+ return { raw: content };
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ },
153
+ generateConfig(gwUrl) {
154
+ const path = this.getConfigPath();
155
+ if (!path)
156
+ return null;
157
+ const block = [
158
+ '',
159
+ '# >>> Agent Mesh Gateway (agentmesh connect) >>>',
160
+ `export OPENAI_API_BASE="${gwUrl}"`,
161
+ `export AGENT_MESH_URL="${gwUrl}"`,
162
+ `export AGENT_GATEWAY_URL="http://${GATEWAY_HOST}"`,
163
+ '# <<< Agent Mesh Gateway <<<',
164
+ '',
165
+ ].join('\n');
166
+ return { path, format: 'env', content: block };
167
+ },
168
+ hasGatewayConfig(config) {
169
+ return config?.raw?.includes('Agent Mesh Gateway');
170
+ },
171
+ };
172
+ // Cursor (~/Library/Application Support/Cursor/User/settings.json)
173
+ const cursorAdapter = {
174
+ name: 'cursor',
175
+ description: 'Cursor IDE',
176
+ detect: () => existsSync(join(HOME, 'Library', 'Application Support', 'Cursor')),
177
+ getConfigPath: () => join(HOME, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'),
178
+ readConfig() {
179
+ const path = this.getConfigPath();
180
+ if (!path || !existsSync(path))
181
+ return {};
182
+ try {
183
+ return JSON.parse(readFileSync(path, 'utf-8'));
184
+ }
185
+ catch {
186
+ return {};
187
+ }
188
+ },
189
+ generateConfig(gwUrl) {
190
+ const path = this.getConfigPath();
191
+ if (!path)
192
+ return null;
193
+ const overrides = {};
194
+ // Cursor 使用 VS Code 风格的 OpenAI 配置
195
+ overrides['openai.apiBase'] = gwUrl;
196
+ overrides['openai.customHeaders'] = { 'x-custom-provider': 'agentmesh' };
197
+ return { path, format: 'json-merge', content: overrides };
198
+ },
199
+ hasGatewayConfig(config) {
200
+ return config?.['openai.apiBase']?.includes('3000');
201
+ },
202
+ };
203
+ // 通用 OpenAI 兼容工具 — 通过环境变量提示
204
+ const openaiCompatAdapter = {
205
+ name: 'openai-compat',
206
+ description: '通用 OpenAI 兼容工具 (OpenCode, KiloCode, Cline, etc.)',
207
+ detect: () => true,
208
+ getConfigPath: () => null,
209
+ readConfig() { return null; },
210
+ generateConfig(_gwUrl) {
211
+ return null; // 环境变量方式已由 shell-env 处理
212
+ },
213
+ hasGatewayConfig() { return false; },
214
+ };
215
+ const ADAPTERS = [
216
+ codexDesktopAdapter,
217
+ claudeCodeAdapter,
218
+ cursorAdapter,
219
+ shellProfileAdapter,
220
+ openaiCompatAdapter,
221
+ ];
222
+ // ============================================================================
223
+ // Connect
224
+ // ============================================================================
225
+ export async function connectTools(targetTools, opts = {}) {
226
+ const results = [];
227
+ const gwUrl = opts.host && opts.port
228
+ ? `http://${opts.host}:${opts.port}/v1`
229
+ : GATEWAY_URL;
230
+ const targets = targetTools.length > 0
231
+ ? ADAPTERS.filter(a => targetTools.includes(a.name) || targetTools.includes('all'))
232
+ : ADAPTERS;
233
+ for (const adapter of targets) {
234
+ // 特殊处理 'all'
235
+ if (targetTools.includes('all') && adapter.name === 'openai-compat')
236
+ continue;
237
+ const installed = adapter.detect();
238
+ if (!installed) {
239
+ results.push({ tool: adapter.name, status: 'not_installed', detail: `${adapter.description} 未安装` });
240
+ continue;
241
+ }
242
+ const config = adapter.readConfig();
243
+ const generated = adapter.generateConfig(gwUrl);
244
+ if (!generated) {
245
+ // 环境变量类,打印提示
246
+ results.push({
247
+ tool: adapter.name,
248
+ status: 'ok',
249
+ detail: `${adapter.description}: 请设置环境变量 OPENAI_API_BASE=${gwUrl}`,
250
+ });
251
+ continue;
252
+ }
253
+ if (adapter.hasGatewayConfig(config)) {
254
+ results.push({ tool: adapter.name, status: 'skipped', detail: '已配置网关,跳过' });
255
+ continue;
256
+ }
257
+ if (opts.dryRun) {
258
+ results.push({
259
+ tool: adapter.name,
260
+ status: 'ok',
261
+ detail: `[DRY-RUN] 将修改 ${generated.path}`,
262
+ });
263
+ continue;
264
+ }
265
+ // 备份原文件
266
+ const backupPath = backupFile(generated.path);
267
+ try {
268
+ const dir = dirname(generated.path);
269
+ ensureDir(dir);
270
+ switch (generated.format) {
271
+ case 'toml': {
272
+ const tomlContent = toToml(generated.content);
273
+ // 追加到现有 TOML 文件
274
+ const existing = existsSync(generated.path)
275
+ ? readFileSync(generated.path, 'utf-8')
276
+ : '';
277
+ writeFileSync(generated.path, existing.trimEnd() + '\n\n# Added by agentmesh connect\n' + tomlContent + '\n');
278
+ break;
279
+ }
280
+ case 'json-merge': {
281
+ // JSON 合并:读取现有 JSON,合并新字段
282
+ const existingJson = existsSync(generated.path)
283
+ ? JSON.parse(readFileSync(generated.path, 'utf-8'))
284
+ : {};
285
+ const merged = { ...existingJson, ...generated.content };
286
+ writeFileSync(generated.path, JSON.stringify(merged, null, 2) + '\n');
287
+ break;
288
+ }
289
+ case 'env': {
290
+ // Shell profile:追加块
291
+ const existing = existsSync(generated.path)
292
+ ? readFileSync(generated.path, 'utf-8')
293
+ : '';
294
+ writeFileSync(generated.path, existing.trimEnd() + '\n' + generated.content);
295
+ break;
296
+ }
297
+ case 'json': {
298
+ writeFileSync(generated.path, JSON.stringify(generated.content, null, 2) + '\n');
299
+ break;
300
+ }
301
+ }
302
+ results.push({
303
+ tool: adapter.name,
304
+ status: 'ok',
305
+ detail: `已配置 → ${gwUrl}` + (backupPath ? ` (备份: ${backupPath})` : ''),
306
+ backupPath: backupPath || undefined,
307
+ });
308
+ }
309
+ catch (err) {
310
+ results.push({ tool: adapter.name, status: 'failed', detail: err.message });
311
+ }
312
+ }
313
+ return results;
314
+ }
315
+ // ============================================================================
316
+ // Disconnect
317
+ // ============================================================================
318
+ export async function disconnectTools(targetTools) {
319
+ const results = [];
320
+ const targets = targetTools.length > 0 && !targetTools.includes('all')
321
+ ? targetTools
322
+ : ['codex-desktop', 'cursor', 'shell-env'];
323
+ for (const toolName of targets) {
324
+ const adapter = ADAPTERS.find(a => a.name === toolName);
325
+ if (!adapter) {
326
+ results.push({ tool: toolName, status: 'not_configured', detail: '未知工具' });
327
+ continue;
328
+ }
329
+ const configPath = adapter.getConfigPath();
330
+ if (!configPath || !existsSync(configPath)) {
331
+ results.push({ tool: adapter.name, status: 'no_backup', detail: '配置文件不存在' });
332
+ continue;
333
+ }
334
+ // 对于 shell-env,移除标记块
335
+ if (adapter.name === 'shell-env') {
336
+ try {
337
+ let content = readFileSync(configPath, 'utf-8');
338
+ const marker = '# >>> Agent Mesh Gateway (agentmesh connect) >>>';
339
+ if (!content.includes(marker)) {
340
+ results.push({ tool: adapter.name, status: 'not_configured', detail: '未找到网关配置块' });
341
+ continue;
342
+ }
343
+ content = content.replace(/# >>> Agent Mesh Gateway[\s\S]*?# <<< Agent Mesh Gateway <<<\n?/g, '');
344
+ writeFileSync(configPath, content.trimEnd() + '\n');
345
+ results.push({ tool: adapter.name, status: 'ok', detail: '已移除环境变量配置' });
346
+ }
347
+ catch (err) {
348
+ results.push({ tool: adapter.name, status: 'failed', detail: err.message });
349
+ }
350
+ continue;
351
+ }
352
+ // 查找备份文件
353
+ const backupPattern = configPath.replace(/\//g, '_').replace(/^_/, '');
354
+ const files = existsSync(BACKUP_DIR)
355
+ ? Bun.spawnSync({ cmd: ['ls', '-t', BACKUP_DIR], stdout: 'pipe' }).stdout
356
+ : new Uint8Array();
357
+ const fileList = new TextDecoder().decode(files).split('\n').filter(f => f.startsWith(backupPattern));
358
+ if (fileList.length > 0) {
359
+ const latestBackup = join(BACKUP_DIR, fileList[0]);
360
+ try {
361
+ writeFileSync(configPath, readFileSync(latestBackup, 'utf-8'));
362
+ results.push({ tool: adapter.name, status: 'ok', detail: `已从备份恢复 (${latestBackup})` });
363
+ }
364
+ catch (err) {
365
+ results.push({ tool: adapter.name, status: 'failed', detail: err.message });
366
+ }
367
+ }
368
+ else {
369
+ results.push({ tool: adapter.name, status: 'no_backup', detail: `无备份文件,请手动恢复 ${configPath}` });
370
+ }
371
+ }
372
+ return results;
373
+ }
374
+ // ============================================================================
375
+ // List detected tools
376
+ // ============================================================================
377
+ export function listDetectedTools() {
378
+ return ADAPTERS.map(a => ({
379
+ name: a.name,
380
+ description: a.description,
381
+ installed: a.detect(),
382
+ configPath: a.getConfigPath() || undefined,
383
+ }));
384
+ }
385
+ // ============================================================================
386
+ // Interactive selection
387
+ // ============================================================================
388
+ export async function interactiveConnect() {
389
+ const tools = listDetectedTools().filter(t => t.installed && t.name !== 'openai-compat');
390
+ console.log(`
391
+ 🔍 检测到 ${tools.length} 个已安装的 AI 工具:
392
+
393
+ [0] 全部接入 (推荐)
394
+ `);
395
+ tools.forEach((t, i) => {
396
+ const hasConfig = t.configPath ? ` → ${t.configPath.replace(HOME, '~')}` : '';
397
+ console.log(` [${i + 1}] ${t.name.padEnd(18)} ${t.description}${hasConfig}`);
398
+ });
399
+ console.log(` [q] 退出`);
400
+ process.stdout.write(`\n 选择 (多个用逗号分隔, 默认 0): `);
401
+ const input = await readStdinLine();
402
+ const trimmed = input.trim() || '0';
403
+ if (trimmed === 'q' || trimmed === 'Q') {
404
+ console.log(' 已取消\n');
405
+ return;
406
+ }
407
+ const selections = trimmed.split(',').map(s => s.trim()).filter(Boolean);
408
+ let targets = [];
409
+ if (selections.includes('0')) {
410
+ targets = tools.map(t => t.name);
411
+ console.log(' 已选择: 全部工具');
412
+ }
413
+ else {
414
+ for (const sel of selections) {
415
+ const idx = parseInt(sel, 10);
416
+ if (idx >= 1 && idx <= tools.length) {
417
+ const tool = tools[idx - 1];
418
+ targets.push(tool.name);
419
+ console.log(` 已选择: ${tool.name}`);
420
+ }
421
+ }
422
+ }
423
+ if (targets.length === 0) {
424
+ console.log(' 未选择任何工具\n');
425
+ return;
426
+ }
427
+ console.log(`\n 预览变更:\n`);
428
+ const results = await connectTools(targets, { dryRun: true });
429
+ for (const r of results) {
430
+ const icon = r.status === 'ok' ? '✅' : r.status === 'skipped' ? '⏭️' : '⚫';
431
+ console.log(` ${icon} ${r.tool.padEnd(16)} ${r.detail}`);
432
+ }
433
+ process.stdout.write(`\n 确认执行? (y/N): `);
434
+ const confirm = (await readStdinLine()).trim().toLowerCase();
435
+ if (confirm === 'y' || confirm === 'yes') {
436
+ console.log(`\n 执行中...\n`);
437
+ const execResults = await connectTools(targets);
438
+ for (const r of execResults) {
439
+ const icon = r.status === 'ok' ? '✅' : r.status === 'skipped' ? '⏭️' : '❌';
440
+ console.log(` ${icon} ${r.tool.padEnd(16)} ${r.detail}`);
441
+ }
442
+ console.log(`\n ✅ 接入完成。使用 \`agentmesh disconnect\` 恢复\n`);
443
+ }
444
+ else {
445
+ console.log(' 已取消\n');
446
+ }
447
+ }
448
+ function readStdinLine() {
449
+ const decoder = new TextDecoder();
450
+ return new Promise((resolve) => {
451
+ const chunks = [];
452
+ const stream = Bun.stdin.stream();
453
+ const reader = stream.getReader();
454
+ function pump() {
455
+ reader.read().then(({ done, value }) => {
456
+ if (done || !value) {
457
+ resolve(decoder.decode(Bun.concatArrayBuffers(chunks.map(c => c.buffer))).trim());
458
+ return;
459
+ }
460
+ const text = decoder.decode(value);
461
+ if (text.includes('\n')) {
462
+ chunks.push(value);
463
+ resolve(decoder.decode(Bun.concatArrayBuffers(chunks.map(c => c.buffer))).trim());
464
+ return;
465
+ }
466
+ chunks.push(value);
467
+ pump();
468
+ });
469
+ }
470
+ pump();
471
+ });
472
+ }
473
+ // ============================================================================
474
+ // CLI entry
475
+ // ============================================================================
476
+ if (import.meta.main) {
477
+ const args = Bun.argv.slice(2);
478
+ const cmd = args[0];
479
+ const rest = args.slice(1);
480
+ const dryRun = rest.includes('--dry-run');
481
+ const targets = rest.filter(a => !a.startsWith('--'));
482
+ async function run() {
483
+ if (cmd === 'connect') {
484
+ const results = await connectTools(targets.length ? targets : ['all'], { dryRun });
485
+ console.log('\n 接入结果:\n');
486
+ for (const r of results) {
487
+ const icon = r.status === 'ok' ? '✅' : r.status === 'skipped' ? '⏭️' : '❌';
488
+ console.log(` ${icon} ${r.tool.padEnd(16)} ${r.detail}`);
489
+ }
490
+ if (dryRun)
491
+ console.log('\n ⚠️ DRY-RUN 模式,未实际修改文件');
492
+ else
493
+ console.log(`\n 💡 备份目录: ${BACKUP_DIR}\n 使用 \`agentmesh disconnect\` 恢复`);
494
+ }
495
+ else if (cmd === 'disconnect') {
496
+ const results = await disconnectTools(targets.length ? targets : ['all']);
497
+ console.log('\n 断开结果:\n');
498
+ for (const r of results) {
499
+ const icon = r.status === 'ok' ? '✅' : '❌';
500
+ console.log(` ${icon} ${r.tool.padEnd(16)} ${r.detail}`);
501
+ }
502
+ }
503
+ else if (cmd === 'list' || cmd === 'detect') {
504
+ const tools = listDetectedTools();
505
+ console.log('\n 检测到的工具:\n');
506
+ for (const t of tools) {
507
+ console.log(` ${t.installed ? '🟢' : '⚫'} ${t.name.padEnd(16)} ${t.description}`);
508
+ if (t.configPath)
509
+ console.log(` ${t.configPath}`);
510
+ }
511
+ }
512
+ else {
513
+ console.log(`
514
+ agentmesh connect — 一键接入 AI 工具
515
+
516
+ 用法:
517
+ agentmesh connect [tool...] [--dry-run]
518
+ agentmesh disconnect [tool...]
519
+ agentmesh connect list
520
+
521
+ 工具:
522
+ codex-desktop Codex Desktop (macOS)
523
+ claude-code Anthropic Claude Code (环境变量)
524
+ cursor Cursor IDE
525
+ shell-env 通用 Shell 环境变量
526
+ all 所有工具
527
+
528
+ 选项:
529
+ --dry-run 预览变更,不实际修改
530
+
531
+ 示例:
532
+ agentmesh connect all # 接入所有工具
533
+ agentmesh connect codex-desktop # 仅接入 Codex Desktop
534
+ agentmesh connect --dry-run # 预览将要修改的内容
535
+ agentmesh disconnect all # 恢复所有工具配置
536
+ agentmesh connect list # 列出可接入的工具
537
+ `);
538
+ }
539
+ }
540
+ run().catch(err => {
541
+ logger.error(`connect CLI error: ${err.message}`);
542
+ process.exit(1);
543
+ });
544
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export declare function runSetup(): Promise<void>;
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bun
2
+ // Agent Mesh Gateway CLI - 交互式初始化向导
3
+ import { existsSync, writeFileSync } from 'node:fs';
4
+ import { join, dirname } from 'node:path';
5
+ import { logger } from '../core/logger.js';
6
+ const CONFIG_DIR = join(Bun.env.HOME || '~', '.config', 'agentmesh');
7
+ const PROJECT_ROOT = dirname(dirname(import.meta.dir));
8
+ function prompt(msg, def) {
9
+ const suffix = def ? ` [${def}]` : '';
10
+ process.stdout.write(` ${msg}${suffix}: `);
11
+ const input = _readLine()?.trim();
12
+ return input || def || '';
13
+ }
14
+ function _readLine() {
15
+ // 同步读取 fallback(异步版本用 readLineAsync)
16
+ return undefined;
17
+ }
18
+ async function readLineAsync() {
19
+ const decoder = new TextDecoder();
20
+ for await (const chunk of Bun.stdin.stream()) {
21
+ const text = typeof chunk === 'string' ? chunk : decoder.decode(chunk);
22
+ return text.trim();
23
+ }
24
+ return '';
25
+ }
26
+ async function promptAsync(msg, def) {
27
+ const suffix = def ? ` [${def}]` : '';
28
+ process.stdout.write(` ${msg}${suffix}: `);
29
+ const input = await readLineAsync();
30
+ return input.trim() || def || '';
31
+ }
32
+ async function promptYesNoAsync(msg) {
33
+ const ans = (await promptAsync(`${msg} (y/n)`, 'y')).toLowerCase();
34
+ return ans === 'y' || ans === 'yes';
35
+ }
36
+ export async function runSetup() {
37
+ console.log(`
38
+ ╔═══════════════════════════════════════════════════╗
39
+ ║ Agent Mesh Gateway - Setup ║
40
+ ╚═══════════════════════════════════════════════════╝
41
+ `);
42
+ console.log('This wizard will help you configure the gateway.\n');
43
+ // 1. API Keys
44
+ console.log('── API Keys ──');
45
+ const deepseekKey = await promptAsync('DeepSeek API Key', Bun.env.DEEPSEEK_API_KEY);
46
+ const openaiKey = await promptAsync('OpenAI API Key', Bun.env.OPENAI_API_KEY);
47
+ const openrouterKey = await promptAsync('OpenRouter API Key', Bun.env.OPENROUTER_API_KEY);
48
+ // 2. Port
49
+ console.log('\n── Server ──');
50
+ const port = parseInt(await promptAsync('Port', '3000'), 10);
51
+ const logLevel = await promptAsync('Log level (debug/info/warn/error)', 'info');
52
+ // 3. Write .env
53
+ const envContent = `# Agent Mesh Gateway - Generated by setup
54
+ DEEPSEEK_API_KEY=${deepseekKey}
55
+ OPENAI_API_KEY=${openaiKey}
56
+ OPENROUTER_API_KEY=${openrouterKey}
57
+ LOG_LEVEL=${logLevel}
58
+ PORT=${port}
59
+ `;
60
+ const envPath = join(PROJECT_ROOT, '.env');
61
+ writeFileSync(envPath, envContent);
62
+ console.log(` ✅ .env written to ${envPath}`);
63
+ // 4. Config dir
64
+ if (!existsSync(CONFIG_DIR)) {
65
+ Bun.spawnSync(['mkdir', '-p', CONFIG_DIR]);
66
+ }
67
+ console.log(` ✅ Config directory: ${CONFIG_DIR}`);
68
+ // 5. Check tools
69
+ console.log('\n── Tool Checks ──');
70
+ const checks = [
71
+ ['bun', ['bun', '--version']],
72
+ ['codexbar', ['codexbar', '--version']],
73
+ ['docker', ['docker', '--version']],
74
+ ['ollama', ['ollama', '--version']],
75
+ ];
76
+ for (const [name, cmd] of checks) {
77
+ const proc = Bun.spawnSync({ cmd, stdout: 'pipe', stderr: 'pipe' });
78
+ const status = proc.exitCode === 0 ? '✅' : '⚠️ (not required)';
79
+ console.log(` ${status} ${name}`);
80
+ }
81
+ console.log(`
82
+ ╔═══════════════════════════════════════════════════╗
83
+ ║ Setup Complete! ║
84
+ ╠═══════════════════════════════════════════════════╣
85
+ ║ Start: bun run src/index.ts ║
86
+ ║ Or: ./start.sh ║
87
+ ║ Help: agentmesh help ║
88
+ ╚═══════════════════════════════════════════════════╝
89
+ `);
90
+ }
91
+ // 直接运行
92
+ if (import.meta.main) {
93
+ runSetup().catch(err => {
94
+ logger.error(`Setup failed: ${err.message}`);
95
+ process.exit(1);
96
+ });
97
+ }