imtoagent 0.3.20 → 0.3.22
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 +29 -1
- package/bin/imtoagent-real +136 -7
- package/modules/agent/claude-adapter.ts +15 -0
- package/modules/agent/codex-adapter.ts +14 -5
- package/modules/agent/opencode-adapter.ts +4 -4
- package/modules/utils/backend-check.ts +182 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# IMtoAgent — IM ↔ Agent Unified Gateway
|
|
2
2
|
|
|
3
3
|
Connect Feishu, Telegram, personal WeChat, and WeCom to AI coding agents like Claude Code, Codex (GPT), OpenCode, and more.
|
|
4
4
|
|
|
@@ -180,6 +180,34 @@ The interactive wizard will guide you through:
|
|
|
180
180
|
| `imtoagent status` | Check running status |
|
|
181
181
|
| `imtoagent restore` | Hot reload recovery |
|
|
182
182
|
| `imtoagent daemon` | Foreground daemon mode (crash auto-restart) |
|
|
183
|
+
| `imtoagent update-system` | Upgrade IMtoAgent itself via npm |
|
|
184
|
+
| `imtoagent update-backend [type]` | Upgrade an agent backend (claude/codex/opencode) |
|
|
185
|
+
|
|
186
|
+
### Upgrading
|
|
187
|
+
|
|
188
|
+
**Upgrade IMtoAgent itself:**
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
imtoagent update-system
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This runs `npm update -g imtoagent` and verifies the new version.
|
|
195
|
+
|
|
196
|
+
**Upgrade agent backends:**
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# Auto-detect current Bot's backend and upgrade it
|
|
200
|
+
imtoagent update-backend
|
|
201
|
+
|
|
202
|
+
# Upgrade a specific backend
|
|
203
|
+
imtoagent update-backend codex
|
|
204
|
+
imtoagent update-backend claude
|
|
205
|
+
imtoagent update-backend opencode
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The command detects how each backend was installed (npm, brew, or manual) and uses the correct upgrade command automatically.
|
|
209
|
+
|
|
210
|
+
### Running Modes
|
|
183
211
|
|
|
184
212
|
### Running Modes
|
|
185
213
|
|
package/bin/imtoagent-real
CHANGED
|
@@ -46,6 +46,14 @@ switch (command) {
|
|
|
46
46
|
case 'daemon':
|
|
47
47
|
await cmdDaemon();
|
|
48
48
|
break;
|
|
49
|
+
case 'update-system':
|
|
50
|
+
await cmdUpdateSystem();
|
|
51
|
+
break;
|
|
52
|
+
case 'update-backend': {
|
|
53
|
+
const backendType = process.argv[3] as 'claude' | 'codex' | 'opencode' | undefined;
|
|
54
|
+
await cmdUpdateBackend(backendType);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
49
57
|
case undefined: {
|
|
50
58
|
const dataDir = getDataDir();
|
|
51
59
|
const configPath = path.join(dataDir, 'config.json');
|
|
@@ -89,13 +97,16 @@ function printHelp() {
|
|
|
89
97
|
imtoagent — IM ↔ Agent Unified Gateway
|
|
90
98
|
|
|
91
99
|
Usage:
|
|
92
|
-
imtoagent setup
|
|
93
|
-
imtoagent start
|
|
94
|
-
imtoagent run
|
|
95
|
-
imtoagent stop
|
|
96
|
-
imtoagent status
|
|
97
|
-
imtoagent restore
|
|
98
|
-
imtoagent daemon
|
|
100
|
+
imtoagent setup Interactive setup wizard
|
|
101
|
+
imtoagent start Start gateway in background (returns immediately)
|
|
102
|
+
imtoagent run Start gateway in foreground (Ctrl+C to stop)
|
|
103
|
+
imtoagent stop Stop gateway
|
|
104
|
+
imtoagent status Check running status
|
|
105
|
+
imtoagent restore Hot reload
|
|
106
|
+
imtoagent daemon Foreground daemon with auto-restart (for launchd/systemd)
|
|
107
|
+
imtoagent update-system Upgrade imtoagent itself
|
|
108
|
+
imtoagent update-backend Upgrade current Bot's backend
|
|
109
|
+
imtoagent update-backend TYPE Upgrade specific backend (codex|claude|opencode)
|
|
99
110
|
|
|
100
111
|
Data directory: ${getDataDir()}
|
|
101
112
|
`);
|
|
@@ -485,3 +496,121 @@ async function cmdDaemon(): Promise<void> {
|
|
|
485
496
|
|
|
486
497
|
console.log('👋 Daemon stopped');
|
|
487
498
|
}
|
|
499
|
+
|
|
500
|
+
// ================================================================
|
|
501
|
+
// update-system — upgrade imtoagent itself
|
|
502
|
+
// ================================================================
|
|
503
|
+
async function cmdUpdateSystem(): Promise<void> {
|
|
504
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
505
|
+
const currentVer = pkg.version;
|
|
506
|
+
console.log('\n📦 Upgrading imtoagent...');
|
|
507
|
+
console.log(` Current version: ${currentVer}\n`);
|
|
508
|
+
|
|
509
|
+
console.log(' Running: npm update -g imtoagent\n');
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const child = Bun.spawn(['zsh', '-ic', 'npm update -g imtoagent'], {
|
|
513
|
+
stdout: 'pipe',
|
|
514
|
+
stderr: 'pipe',
|
|
515
|
+
stdin: 'ignore',
|
|
516
|
+
env: { ...process.env },
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const decoder = new TextDecoder();
|
|
520
|
+
|
|
521
|
+
const stdoutReader = child.stdout.getReader();
|
|
522
|
+
while (true) {
|
|
523
|
+
const { done, value } = await stdoutReader.read();
|
|
524
|
+
if (done) break;
|
|
525
|
+
process.stdout.write(decoder.decode(value, { stream: true }));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const stderrReader = child.stderr.getReader();
|
|
529
|
+
while (true) {
|
|
530
|
+
const { done, value } = await stderrReader.read();
|
|
531
|
+
if (done) break;
|
|
532
|
+
process.stderr.write(decoder.decode(value, { stream: true }));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const exitCode = await child.exited;
|
|
536
|
+
if (exitCode !== 0) {
|
|
537
|
+
console.error(`\n❌ imtoagent upgrade failed (exit code: ${exitCode})`);
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Read new version
|
|
542
|
+
let newVer = 'unknown';
|
|
543
|
+
try {
|
|
544
|
+
const newPkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
545
|
+
newVer = newPkg.version;
|
|
546
|
+
} catch {
|
|
547
|
+
// Can't read new version (possibly process swapped), check via CLI
|
|
548
|
+
try {
|
|
549
|
+
newVer = execSync('imtoagent --version', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
550
|
+
} catch {}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
console.log(`\n✅ imtoagent upgraded: ${currentVer} → ${newVer}`);
|
|
554
|
+
console.log(` Restart gateway with: imtoagent stop && imtoagent start`);
|
|
555
|
+
} catch (e: any) {
|
|
556
|
+
console.error(`\n❌ Error upgrading imtoagent: ${e.message || e}`);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ================================================================
|
|
562
|
+
// update-backend — upgrade a backend agent
|
|
563
|
+
// ================================================================
|
|
564
|
+
async function cmdUpdateBackend(backendType?: 'claude' | 'codex' | 'opencode'): Promise<void> {
|
|
565
|
+
const dataDir = getDataDir();
|
|
566
|
+
const configPath = path.join(dataDir, 'config.json');
|
|
567
|
+
|
|
568
|
+
// Determine which backend to upgrade
|
|
569
|
+
let targetType: 'claude' | 'codex' | 'opencode';
|
|
570
|
+
|
|
571
|
+
if (backendType) {
|
|
572
|
+
if (!['claude', 'codex', 'opencode'].includes(backendType)) {
|
|
573
|
+
console.error(`❌ Unknown backend: ${backendType}`);
|
|
574
|
+
console.error(` Valid: claude, codex, opencode`);
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
targetType = backendType;
|
|
578
|
+
} else {
|
|
579
|
+
// Auto-detect from config (use first bot's backend)
|
|
580
|
+
if (!fs.existsSync(configPath)) {
|
|
581
|
+
console.error('❌ No config found. Run "imtoagent setup" first.');
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
585
|
+
const bots = cfg.bots || [];
|
|
586
|
+
if (bots.length === 0) {
|
|
587
|
+
console.error('❌ No bots configured.');
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
targetType = bots[0].backend;
|
|
591
|
+
if (!targetType) {
|
|
592
|
+
console.error('❌ First bot has no backend configured.');
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
console.log(` Auto-detected backend: ${targetType} (from bot: ${bots[0].name})\n`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Show current status
|
|
599
|
+
const { checkBackend } = await import('../modules/utils/backend-check');
|
|
600
|
+
const info = checkBackend(targetType);
|
|
601
|
+
console.log(` ${info.label}: ${info.version || 'not installed'} (${info.installSource})`);
|
|
602
|
+
|
|
603
|
+
if (!info.installed) {
|
|
604
|
+
console.error(`\n❌ ${info.label} not installed.`);
|
|
605
|
+
console.error(` Install: imtoagent setup`);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Run upgrade
|
|
610
|
+
const { upgradeBackend } = await import('../modules/utils/backend-check');
|
|
611
|
+
const result = await upgradeBackend(targetType);
|
|
612
|
+
|
|
613
|
+
if (!result.success) {
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
@@ -100,6 +100,10 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
100
100
|
const { text, session, workingDir, model, systemPrompt: overrideSystemPrompt } = input;
|
|
101
101
|
const sessionAny = session as any; // 向后兼容:访问旧字段
|
|
102
102
|
|
|
103
|
+
// 进度回调
|
|
104
|
+
const onProgress = async (t: string) => { try { await input.sendProgress?.(t); } catch {} };
|
|
105
|
+
let turnCount = 0;
|
|
106
|
+
|
|
103
107
|
// 创建 AbortController 并注册(用于超时 + shutdown 清理)
|
|
104
108
|
const abortCtrl = new AbortController();
|
|
105
109
|
this.activeControllers.push(abortCtrl);
|
|
@@ -205,6 +209,16 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
205
209
|
const calls = extractToolCalls(msg);
|
|
206
210
|
toolCalls.push(...calls);
|
|
207
211
|
|
|
212
|
+
// 进度输出(仅 assistant 消息)
|
|
213
|
+
if (msg.type === 'assistant') {
|
|
214
|
+
turnCount++;
|
|
215
|
+
if (calls.length > 0) {
|
|
216
|
+
const names = calls.map(c => c.name).join(', ');
|
|
217
|
+
const summary = calls[0].summary.slice(0, 60);
|
|
218
|
+
onProgress(`🔧 Executing: ${names} — ${summary}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
208
222
|
// 处理最终结果
|
|
209
223
|
if (msg.type === 'result') {
|
|
210
224
|
const result = msgAny;
|
|
@@ -222,6 +236,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
222
236
|
};
|
|
223
237
|
|
|
224
238
|
if (timeoutId) clearTimeout(timeoutId);
|
|
239
|
+
if (turnCount > 0) onProgress(`✅ Turn ${turnCount} completed`);
|
|
225
240
|
const responseText = fullResponse || `✅ Completed (${toolCalls.length} steps)`;
|
|
226
241
|
|
|
227
242
|
return {
|
|
@@ -106,8 +106,9 @@ async function spawnCodexResume(cwd: string, threadId: string, prompt: string):
|
|
|
106
106
|
|
|
107
107
|
async function runViaAppServer(
|
|
108
108
|
cwd: string, prompt: string, chatId: string, session: Session,
|
|
109
|
-
isFresh: boolean
|
|
109
|
+
isFresh: boolean, onProgress?: (text: string) => Promise<void>
|
|
110
110
|
): Promise<{ threadId: string; response: string; usage: { inputTokens: number; outputTokens: number } }> {
|
|
111
|
+
let turnCount = 0;
|
|
111
112
|
const manager = getAppServerManager();
|
|
112
113
|
const client = await manager.getClient(chatId);
|
|
113
114
|
|
|
@@ -138,9 +139,14 @@ async function runViaAppServer(
|
|
|
138
139
|
case 'text_delta':
|
|
139
140
|
response += event.textDelta || '';
|
|
140
141
|
break;
|
|
141
|
-
case 'tool_call':
|
|
142
|
-
|
|
142
|
+
case 'tool_call': {
|
|
143
|
+
const name = (event as any).toolCall?.name || (event as any).tool?.name || 'Tool';
|
|
144
|
+
onProgress?.(`🔧 Executing: ${name}`);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
143
147
|
case 'turn_result':
|
|
148
|
+
turnCount++;
|
|
149
|
+
onProgress?.(`✅ Turn ${turnCount} completed`);
|
|
144
150
|
totalUsage.inputTokens += event.usage?.inputTokens || 0;
|
|
145
151
|
totalUsage.outputTokens += event.usage?.outputTokens || 0;
|
|
146
152
|
break;
|
|
@@ -189,7 +195,8 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
189
195
|
let execServerUsage: { inputTokens: number; outputTokens: number } | null = null;
|
|
190
196
|
|
|
191
197
|
try {
|
|
192
|
-
const r = await runViaAppServer(cwd, effectiveText, input.chatId, session, isFresh
|
|
198
|
+
const r = await runViaAppServer(cwd, effectiveText, input.chatId, session, isFresh,
|
|
199
|
+
async (t: string) => { try { await input.sendProgress?.(t); } catch {} });
|
|
193
200
|
response = r.response;
|
|
194
201
|
execServerUsage = r.usage;
|
|
195
202
|
} catch (appErr: any) {
|
|
@@ -199,7 +206,8 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
199
206
|
if (errMsg.includes('thread not found') || errMsg.includes('Thread not found')) {
|
|
200
207
|
try {
|
|
201
208
|
sessionAny.codexThreadId = undefined;
|
|
202
|
-
const r2 = await runViaAppServer(cwd, effectiveText, input.chatId, session, true
|
|
209
|
+
const r2 = await runViaAppServer(cwd, effectiveText, input.chatId, session, true,
|
|
210
|
+
async (t: string) => { try { await input.sendProgress?.(t); } catch {} });
|
|
203
211
|
response = r2.response;
|
|
204
212
|
execServerUsage = r2.usage;
|
|
205
213
|
console.error(`[CodexAdapter] app-server thread rebuilt successfully`);
|
|
@@ -213,6 +221,7 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
213
221
|
|
|
214
222
|
if (useExecFallback) {
|
|
215
223
|
getAppServerManager().removeClient(input.chatId);
|
|
224
|
+
input.sendProgress?.('⚙️ Processing... (CLI mode, no streaming progress)').catch(() => {});
|
|
216
225
|
if (isFresh || !sessionAny.codexThreadId) {
|
|
217
226
|
const r = await spawnCodexExec(cwd, effectiveText);
|
|
218
227
|
sessionAny.codexThreadId = r.threadId;
|
|
@@ -80,7 +80,7 @@ async function ocSendPrompt(
|
|
|
80
80
|
while (turn < MAX_TURNS) {
|
|
81
81
|
if (Date.now() - startTime > MAX_DURATION) {
|
|
82
82
|
console.error(`[OpenCodeAdapter] Task timed out (${MAX_DURATION / 60000}min)`);
|
|
83
|
-
if (onProgress) onProgress('⚠️
|
|
83
|
+
if (onProgress) onProgress('⚠️ Task timed out, stopped');
|
|
84
84
|
break;
|
|
85
85
|
}
|
|
86
86
|
turn++;
|
|
@@ -131,7 +131,7 @@ async function ocSendPrompt(
|
|
|
131
131
|
const summary = args.command || args.cmd || args.file_path || args.query
|
|
132
132
|
|| JSON.stringify(args).slice(0, 80);
|
|
133
133
|
allToolCalls.push({ name, summary });
|
|
134
|
-
if (onProgress) onProgress(`🔧
|
|
134
|
+
if (onProgress) onProgress(`🔧 Executing: ${name} — ${summary.slice(0, 60)}`);
|
|
135
135
|
console.log(`[OpenCodeAdapter] 🔧 turn ${turn}: ${name} ${summary.slice(0, 60)}`);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -139,7 +139,7 @@ async function ocSendPrompt(
|
|
|
139
139
|
// 有文本回复 → 任务完成(OpenCode 内部已完成多轮 agent loop)
|
|
140
140
|
if (hasText) {
|
|
141
141
|
console.log(`[OpenCodeAdapter] ✅ completed at turn ${turn}/${MAX_TURNS}`);
|
|
142
|
-
if (onProgress) onProgress(`✅
|
|
142
|
+
if (onProgress) onProgress(`✅ Turn ${turn} completed`);
|
|
143
143
|
break;
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -150,7 +150,7 @@ async function ocSendPrompt(
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
// 仅有 tool_call 无文本 → 推进下一轮
|
|
153
|
-
if (onProgress) onProgress(`⏳
|
|
153
|
+
if (onProgress) onProgress(`⏳ Turn ${turn}/${MAX_TURNS}, continuing...`);
|
|
154
154
|
promptText = 'Continue executing, complete remaining tasks';
|
|
155
155
|
}
|
|
156
156
|
|
|
@@ -7,20 +7,70 @@ import * as fs from 'fs';
|
|
|
7
7
|
import * as os from 'os';
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
|
|
10
|
+
export type InstallSource = 'npm' | 'brew' | 'manual' | 'unknown';
|
|
11
|
+
|
|
10
12
|
export interface BackendInfo {
|
|
11
13
|
type: 'claude' | 'codex' | 'opencode';
|
|
12
14
|
label: string;
|
|
13
15
|
installed: boolean;
|
|
14
16
|
version: string | null;
|
|
15
17
|
installHint: string;
|
|
18
|
+
installSource: InstallSource;
|
|
19
|
+
binaryPath: string | null;
|
|
20
|
+
upgradeCommand: string | null;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
const BACKEND_DEFS: Omit<BackendInfo, 'installed' | 'version'>[] = [
|
|
23
|
+
const BACKEND_DEFS: Omit<BackendInfo, 'installed' | 'version' | 'installSource' | 'binaryPath' | 'upgradeCommand'>[] = [
|
|
19
24
|
{ type: 'claude', label: 'Claude Code', installHint: 'npm install -g @anthropic-ai/claude-agent-sdk' },
|
|
20
25
|
{ type: 'codex', label: 'Codex', installHint: 'npm install -g @openai/codex' },
|
|
21
26
|
{ type: 'opencode', label: 'OpenCode', installHint: 'curl -fsSL https://opencode.ai/install | bash' },
|
|
22
27
|
];
|
|
23
28
|
|
|
29
|
+
// ================================================================
|
|
30
|
+
// 安装来源检测
|
|
31
|
+
// ================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 根据二进制路径判断安装来源
|
|
35
|
+
*/
|
|
36
|
+
export function detectInstallSource(binaryPath: string, type: string): InstallSource {
|
|
37
|
+
if (binaryPath.includes('/opt/homebrew/') || binaryPath.includes('/usr/local/Cellar/')) {
|
|
38
|
+
return 'brew';
|
|
39
|
+
}
|
|
40
|
+
if (binaryPath.includes('node_modules/')) {
|
|
41
|
+
return 'npm';
|
|
42
|
+
}
|
|
43
|
+
if (type === 'opencode' && binaryPath.includes('.opencode/')) {
|
|
44
|
+
return 'manual';
|
|
45
|
+
}
|
|
46
|
+
return 'unknown';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 根据安装来源和后端类型生成升级命令
|
|
51
|
+
*/
|
|
52
|
+
export function getUpgradeCommand(source: InstallSource, type: string): string | null {
|
|
53
|
+
switch (source) {
|
|
54
|
+
case 'brew':
|
|
55
|
+
const brewPkg: Record<string, string> = { claude: undefined, codex: 'codex', opencode: undefined };
|
|
56
|
+
if (brewPkg[type]) return `brew upgrade ${brewPkg[type]}`;
|
|
57
|
+
return null; // claude/opencode not on brew
|
|
58
|
+
case 'npm':
|
|
59
|
+
const npmPkg: Record<string, string> = {
|
|
60
|
+
claude: '@anthropic-ai/claude-agent-sdk',
|
|
61
|
+
codex: '@openai/codex',
|
|
62
|
+
opencode: 'opencode',
|
|
63
|
+
};
|
|
64
|
+
if (npmPkg[type]) return `npm update -g ${npmPkg[type]}`;
|
|
65
|
+
return null;
|
|
66
|
+
case 'manual':
|
|
67
|
+
if (type === 'opencode') return 'curl -fsSL https://opencode.ai/install | bash';
|
|
68
|
+
return null;
|
|
69
|
+
case 'unknown':
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
24
74
|
// ================================================================
|
|
25
75
|
// 获取 npm 全局 bin 目录
|
|
26
76
|
// 解决 PATH 未包含 npm global bin 时的检测失败问题
|
|
@@ -49,47 +99,75 @@ export function getNpmGlobalBin(): string | null {
|
|
|
49
99
|
}
|
|
50
100
|
}
|
|
51
101
|
|
|
52
|
-
|
|
102
|
+
/**
|
|
103
|
+
* 定位后端可执行文件(按优先级)
|
|
104
|
+
* 返回 { path, version } 或 null
|
|
105
|
+
*/
|
|
106
|
+
function findBackendBinary(type: string): { binaryPath: string; version: string } | null {
|
|
53
107
|
const versionCmd: Record<string, string> = {
|
|
54
108
|
claude: 'claude --version',
|
|
55
109
|
codex: 'codex --version',
|
|
56
110
|
opencode: 'opencode version',
|
|
57
111
|
};
|
|
58
112
|
|
|
59
|
-
//
|
|
113
|
+
// 1) PATH 中的命令(最常见)
|
|
60
114
|
try {
|
|
61
|
-
const version = execSync(versionCmd[
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
115
|
+
const version = execSync(versionCmd[type], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
116
|
+
// 解析真实路径(处理 symlink)
|
|
117
|
+
const binName = type;
|
|
118
|
+
const realPath = execSync(`command -v ${binName}`, { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
119
|
+
return { binaryPath: realPath, version };
|
|
120
|
+
} catch {}
|
|
66
121
|
|
|
67
|
-
//
|
|
122
|
+
// 2) npm global bin
|
|
68
123
|
const npmBin = getNpmGlobalBin();
|
|
69
124
|
if (npmBin) {
|
|
70
|
-
const binPath = path.join(npmBin,
|
|
125
|
+
const binPath = path.join(npmBin, type);
|
|
71
126
|
try {
|
|
72
127
|
if (fs.existsSync(binPath)) {
|
|
73
128
|
const version = execSync(`"${binPath}" --version`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
74
|
-
return {
|
|
129
|
+
return { binaryPath: binPath, version };
|
|
75
130
|
}
|
|
76
|
-
} catch {
|
|
77
|
-
// bin 存在但执行失败,视为未安装
|
|
78
|
-
}
|
|
131
|
+
} catch {}
|
|
79
132
|
}
|
|
80
133
|
|
|
81
|
-
//
|
|
82
|
-
if (
|
|
134
|
+
// 3) OpenCode custom install path
|
|
135
|
+
if (type === 'opencode') {
|
|
83
136
|
const opencodePath = path.join(os.homedir(), '.opencode', 'bin', 'opencode');
|
|
84
137
|
try {
|
|
85
138
|
if (fs.existsSync(opencodePath)) {
|
|
86
139
|
const version = execSync(`"${opencodePath}" version`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
87
|
-
return {
|
|
140
|
+
return { binaryPath: opencodePath, version };
|
|
88
141
|
}
|
|
89
142
|
} catch {}
|
|
90
143
|
}
|
|
91
144
|
|
|
92
|
-
return
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function checkOne(b: Omit<BackendInfo, 'installed' | 'version' | 'installSource' | 'binaryPath' | 'upgradeCommand'>): BackendInfo {
|
|
149
|
+
const found = findBackendBinary(b.type);
|
|
150
|
+
if (found) {
|
|
151
|
+
const installSource = detectInstallSource(found.binaryPath, b.type);
|
|
152
|
+
const upgradeCommand = getUpgradeCommand(installSource, b.type);
|
|
153
|
+
return {
|
|
154
|
+
...b,
|
|
155
|
+
installed: true,
|
|
156
|
+
version: found.version,
|
|
157
|
+
installSource,
|
|
158
|
+
binaryPath: found.binaryPath,
|
|
159
|
+
upgradeCommand,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...b,
|
|
165
|
+
installed: false,
|
|
166
|
+
version: null,
|
|
167
|
+
installSource: 'unknown',
|
|
168
|
+
binaryPath: null,
|
|
169
|
+
upgradeCommand: null,
|
|
170
|
+
};
|
|
93
171
|
}
|
|
94
172
|
|
|
95
173
|
export function checkAllBackends(): BackendInfo[] {
|
|
@@ -106,8 +184,9 @@ export function formatBackendStatus(backends: BackendInfo[]): string {
|
|
|
106
184
|
return backends.map(b => {
|
|
107
185
|
const icon = b.installed ? '✅' : '❌';
|
|
108
186
|
const ver = b.version ? ` v${b.version}` : '';
|
|
187
|
+
const source = b.installed && b.installSource !== 'unknown' ? ` (${b.installSource})` : '';
|
|
109
188
|
const hint = b.installed ? '' : ` → ${b.installHint}`;
|
|
110
|
-
return ` ${icon} ${b.label}${ver}${hint}`;
|
|
189
|
+
return ` ${icon} ${b.label}${ver}${source}${hint}`;
|
|
111
190
|
}).join('\n');
|
|
112
191
|
}
|
|
113
192
|
|
|
@@ -143,6 +222,90 @@ function ensurePathInConfig(configPath: string, binDir: string): void {
|
|
|
143
222
|
} catch {}
|
|
144
223
|
}
|
|
145
224
|
|
|
225
|
+
// ================================================================
|
|
226
|
+
// 升级后端 CLI
|
|
227
|
+
// ================================================================
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 升级已安装的后端到最新版
|
|
231
|
+
* 根据检测到的 installSource 自动选择正确的升级命令
|
|
232
|
+
*/
|
|
233
|
+
export async function upgradeBackend(
|
|
234
|
+
type: 'claude' | 'codex' | 'opencode',
|
|
235
|
+
): Promise<{ success: boolean; oldVer: string; newVer: string }> {
|
|
236
|
+
const b = BACKEND_DEFS.find((x) => x.type === type);
|
|
237
|
+
if (!b) {
|
|
238
|
+
return { success: false, oldVer: '', newVer: '' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const currentInfo = checkOne(b);
|
|
242
|
+
if (!currentInfo.installed) {
|
|
243
|
+
console.error(`❌ ${b.label} not installed. Use installBackend() instead.`);
|
|
244
|
+
return { success: false, oldVer: '', newVer: '' };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const upgradeCmd = currentInfo.upgradeCommand;
|
|
248
|
+
if (!upgradeCmd) {
|
|
249
|
+
console.error(`❌ Cannot determine upgrade method for ${b.label} (source: ${currentInfo.installSource})`);
|
|
250
|
+
console.error(` Please upgrade manually.`);
|
|
251
|
+
return { success: false, oldVer: currentInfo.version || '', newVer: '' };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const oldVer = currentInfo.version || '';
|
|
255
|
+
console.log(`\n📦 Upgrading ${b.label}...`);
|
|
256
|
+
console.log(` Current: ${oldVer}`);
|
|
257
|
+
console.log(` Source: ${currentInfo.installSource}`);
|
|
258
|
+
console.log(` Command: ${upgradeCmd}\n`);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const child = Bun.spawn(['zsh', '-ic', upgradeCmd], {
|
|
262
|
+
stdout: 'pipe',
|
|
263
|
+
stderr: 'pipe',
|
|
264
|
+
stdin: 'ignore',
|
|
265
|
+
env: { ...process.env },
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const decoder = new TextDecoder();
|
|
269
|
+
|
|
270
|
+
const stdoutReader = child.stdout.getReader();
|
|
271
|
+
while (true) {
|
|
272
|
+
const { done, value } = await stdoutReader.read();
|
|
273
|
+
if (done) break;
|
|
274
|
+
process.stdout.write(decoder.decode(value, { stream: true }));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const stderrReader = child.stderr.getReader();
|
|
278
|
+
while (true) {
|
|
279
|
+
const { done, value } = await stderrReader.read();
|
|
280
|
+
if (done) break;
|
|
281
|
+
process.stderr.write(decoder.decode(value, { stream: true }));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const exitCode = await child.exited;
|
|
285
|
+
if (exitCode !== 0) {
|
|
286
|
+
console.error(`\n❌ ${b.label} upgrade failed (exit code: ${exitCode})`);
|
|
287
|
+
return { success: false, oldVer, newVer: '' };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 验证新版本
|
|
291
|
+
const newInfo = checkOne(b);
|
|
292
|
+
const newVer = newInfo.version || '';
|
|
293
|
+
if (newInfo.installed && newVer !== oldVer) {
|
|
294
|
+
console.log(`\n✅ ${b.label} upgraded: ${oldVer} → ${newVer}`);
|
|
295
|
+
return { success: true, oldVer, newVer };
|
|
296
|
+
} else if (newVer === oldVer) {
|
|
297
|
+
console.log(`\n✅ ${b.label} is already at latest version (${newVer})`);
|
|
298
|
+
return { success: true, oldVer, newVer };
|
|
299
|
+
} else {
|
|
300
|
+
console.log(`\n✅ ${b.label} upgrade completed (new version: ${newVer})`);
|
|
301
|
+
return { success: true, oldVer, newVer };
|
|
302
|
+
}
|
|
303
|
+
} catch (e: any) {
|
|
304
|
+
console.error(`\n❌ Error upgrading ${b.label}: ${e.message || e}`);
|
|
305
|
+
return { success: false, oldVer, newVer: '' };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
146
309
|
// ================================================================
|
|
147
310
|
// 自动安装后端 CLI
|
|
148
311
|
// ================================================================
|