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 CHANGED
@@ -1,4 +1,4 @@
1
- # imtoagent — IM ↔ Agent Unified Gateway
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
 
@@ -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 Interactive setup wizard
93
- imtoagent start Start gateway in background (returns immediately)
94
- imtoagent run Start gateway in foreground (Ctrl+C to stop)
95
- imtoagent stop Stop gateway
96
- imtoagent status Check running status
97
- imtoagent restore Hot reload
98
- imtoagent daemon Foreground daemon with auto-restart (for launchd/systemd)
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
- break; // tool 日志由 Runtime 层处理
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(`🔧 正在执行: ${name} — ${summary.slice(0, 60)}`);
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(`✅ ${turn} 轮处理完成`);
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(`⏳ ${turn}/${MAX_TURNS} 轮,继续推进...`);
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
- function checkOne(b: Omit<BackendInfo, 'installed' | 'version'>): BackendInfo {
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
- // 先尝试 PATH 中的命令
113
+ // 1) PATH 中的命令(最常见)
60
114
  try {
61
- const version = execSync(versionCmd[b.type], { encoding: 'utf-8', timeout: 5000 }).trim();
62
- return { ...b, installed: true, version };
63
- } catch {
64
- // PATH 中找不到,继续尝试 fallback
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
- // fallback 1: npm global bin
122
+ // 2) npm global bin
68
123
  const npmBin = getNpmGlobalBin();
69
124
  if (npmBin) {
70
- const binPath = path.join(npmBin, b.type);
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 { ...b, installed: true, version };
129
+ return { binaryPath: binPath, version };
75
130
  }
76
- } catch {
77
- // bin 存在但执行失败,视为未安装
78
- }
131
+ } catch {}
79
132
  }
80
133
 
81
- // fallback 2: OpenCode custom install path
82
- if (b.type === 'opencode') {
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 { ...b, installed: true, version };
140
+ return { binaryPath: opencodePath, version };
88
141
  }
89
142
  } catch {}
90
143
  }
91
144
 
92
- return { ...b, installed: false, version: null };
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
  // ================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imtoagent",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "IM ↔ Agent 统一网关 — 飞书/Telegram/微信/企业微信对接 Claude Code/Codex/OpenCode",
5
5
  "type": "module",
6
6
  "bin": {