@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.
- package/CHANGELOG.md +60 -41
- package/README.zh-CN.md +137 -167
- package/config/gateway.yaml +78 -0
- package/dist/src/cli/connect.d.ts +26 -0
- package/dist/src/cli/connect.js +544 -0
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +97 -0
- package/dist/src/cli.js +410 -0
- package/dist/src/core/logger.d.ts +14 -0
- package/dist/src/core/logger.js +57 -0
- package/dist/{index.js → src/index.js} +40 -8
- package/dist/src/model-gateway/circuit-breaker.d.ts +21 -0
- package/dist/src/model-gateway/circuit-breaker.js +86 -0
- package/dist/src/model-gateway/health.d.ts +12 -0
- package/dist/src/model-gateway/health.js +80 -0
- package/dist/src/model-gateway/providers.d.ts +4 -0
- package/dist/src/model-gateway/providers.js +113 -0
- package/dist/src/model-gateway/quota.d.ts +4 -0
- package/dist/src/model-gateway/quota.js +107 -0
- package/dist/src/model-gateway/rate-limit.d.ts +12 -0
- package/dist/src/model-gateway/rate-limit.js +51 -0
- package/dist/src/model-gateway/retry.d.ts +14 -0
- package/dist/src/model-gateway/retry.js +48 -0
- package/dist/src/model-gateway/router.d.ts +4 -0
- package/dist/src/model-gateway/router.js +79 -0
- package/dist/src/model-gateway/routes.d.ts +2 -0
- package/dist/src/model-gateway/routes.js +172 -0
- package/dist/src/model-gateway/types.d.ts +47 -0
- package/dist/src/model-gateway/types.js +1 -0
- package/dist/tests/core/context-manager.test.d.ts +1 -0
- package/dist/tests/core/context-manager.test.js +35 -0
- package/dist/tests/core/router.test.d.ts +1 -0
- package/dist/tests/core/router.test.js +79 -0
- package/dist/tests/model-gateway/circuit-breaker.test.d.ts +1 -0
- package/dist/tests/model-gateway/circuit-breaker.test.js +84 -0
- package/dist/tests/model-gateway/providers.test.d.ts +1 -0
- package/dist/tests/model-gateway/providers.test.js +80 -0
- package/dist/tests/model-gateway/quota.test.d.ts +1 -0
- package/dist/tests/model-gateway/quota.test.js +60 -0
- package/dist/tests/model-gateway/rate-limit.test.d.ts +1 -0
- package/dist/tests/model-gateway/rate-limit.test.js +42 -0
- package/dist/tests/model-gateway/retry.test.d.ts +1 -0
- package/dist/tests/model-gateway/retry.test.js +47 -0
- package/dist/tests/model-gateway/router.test.d.ts +1 -0
- package/dist/tests/model-gateway/router.test.js +108 -0
- package/dist/tests/model-gateway/routes.test.d.ts +1 -0
- package/dist/tests/model-gateway/routes.test.js +83 -0
- package/docs/api.md +187 -460
- package/docs/architecture.md +138 -0
- package/docs/configuration.md +188 -0
- package/package.json +3 -1
- package/dist/cli.js +0 -246
- /package/dist/{adapters → src/adapters}/base.d.ts +0 -0
- /package/dist/{adapters → src/adapters}/base.js +0 -0
- /package/dist/{adapters → src/adapters}/claude-code.d.ts +0 -0
- /package/dist/{adapters → src/adapters}/claude-code.js +0 -0
- /package/dist/{adapters → src/adapters}/openclaw.d.ts +0 -0
- /package/dist/{adapters → src/adapters}/openclaw.js +0 -0
- /package/dist/{adapters → src/adapters}/process.d.ts +0 -0
- /package/dist/{adapters → src/adapters}/process.js +0 -0
- /package/dist/{cli.d.ts → src/cli.d.ts} +0 -0
- /package/dist/{core → src/core}/agent-registry.d.ts +0 -0
- /package/dist/{core → src/core}/agent-registry.js +0 -0
- /package/dist/{core → src/core}/config.d.ts +0 -0
- /package/dist/{core → src/core}/config.js +0 -0
- /package/dist/{core → src/core}/context-manager.d.ts +0 -0
- /package/dist/{core → src/core}/context-manager.js +0 -0
- /package/dist/{core → src/core}/event-bus.d.ts +0 -0
- /package/dist/{core → src/core}/event-bus.js +0 -0
- /package/dist/{core → src/core}/metrics.d.ts +0 -0
- /package/dist/{core → src/core}/metrics.js +0 -0
- /package/dist/{core → src/core}/router.d.ts +0 -0
- /package/dist/{core → src/core}/router.js +0 -0
- /package/dist/{core → src/core}/task-manager.d.ts +0 -0
- /package/dist/{core → src/core}/task-manager.js +0 -0
- /package/dist/{core → src/core}/vector-store.d.ts +0 -0
- /package/dist/{core → src/core}/vector-store.js +0 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
- /package/dist/{routes → src/routes}/api.d.ts +0 -0
- /package/dist/{routes → src/routes}/api.js +0 -0
- /package/dist/{routes → src/routes}/websocket.d.ts +0 -0
- /package/dist/{routes → src/routes}/websocket.js +0 -0
- /package/dist/{types → src/types}/index.d.ts +0 -0
- /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,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
|
+
}
|