imtoagent 0.3.4 → 0.3.6

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 (39) hide show
  1. package/README.md +97 -97
  2. package/bin/imtoagent-real +197 -153
  3. package/bin/imtoagent.cjs +13 -5
  4. package/index.ts +106 -106
  5. package/modules/agent/claude-adapter.ts +6 -6
  6. package/modules/agent/claude.ts +6 -6
  7. package/modules/agent/codex-adapter.ts +13 -13
  8. package/modules/agent/codex-exec-server.ts +11 -11
  9. package/modules/agent/codex.ts +29 -29
  10. package/modules/agent/opencode-adapter.ts +17 -17
  11. package/modules/agent/opencode.ts +10 -10
  12. package/modules/capabilities.ts +33 -33
  13. package/modules/cli/setup.ts +164 -164
  14. package/modules/core/config.ts +5 -5
  15. package/modules/core/error.ts +8 -8
  16. package/modules/core/runtime.ts +10 -10
  17. package/modules/core/session.ts +4 -4
  18. package/modules/core/stats.ts +14 -14
  19. package/modules/core/types.ts +7 -7
  20. package/modules/im/feishu.ts +56 -56
  21. package/modules/im/telegram.ts +23 -23
  22. package/modules/im/wechat.ts +54 -54
  23. package/modules/im/wecom.ts +50 -50
  24. package/modules/media/feishu-inbound-adapter.ts +4 -4
  25. package/modules/media/resolver.ts +11 -11
  26. package/modules/media/telegram-inbound-adapter.ts +8 -8
  27. package/modules/prompt-builder.ts +12 -12
  28. package/modules/proxy/anthropic-proxy.ts +31 -31
  29. package/modules/proxy/codex-proxy.ts +18 -18
  30. package/modules/utils/backend-check.ts +12 -12
  31. package/modules/utils/paths.ts +8 -8
  32. package/package.json +1 -1
  33. package/scripts/postinstall.cjs +10 -10
  34. package/scripts/postinstall.ts +13 -13
  35. package/templates/soul.template/identity.md +5 -5
  36. package/templates/soul.template/profile.md +7 -7
  37. package/templates/soul.template/rules.md +5 -5
  38. package/templates/soul.template/skills.md +2 -2
  39. package/templates/soul.template/workspace.md +3 -3
@@ -1,24 +1,26 @@
1
1
  #!/usr/bin/env bun
2
2
  // ================================================================
3
- // imtoagent CLI — 全局命令入口
3
+ // imtoagent CLI — global command entry point
4
4
  // ================================================================
5
- // npm install -g imtoagent 后可用:
6
- // imtoagent setup — 交互式配置向导
7
- // imtoagent start — 后台启动网关
8
- // imtoagent stop 停止网关
9
- // imtoagent status 查看运行状态
10
- // imtoagent restore 热重载恢复
11
- // imtoagent daemon 前台守护模式(自动重启 + 日志)
5
+ // Available after npm install -g imtoagent:
6
+ // imtoagent setup — interactive setup wizard
7
+ // imtoagent start — start gateway in background
8
+ // imtoagent run start gateway in foreground
9
+ // imtoagent stop stop gateway
10
+ // imtoagent status check running status
11
+ // imtoagent restore hot reload
12
+ // imtoagent daemon — foreground daemon (auto-restart, for launchd/systemd)
12
13
  // ================================================================
13
14
 
14
15
  import * as fs from 'fs';
15
16
  import * as path from 'path';
17
+ import { spawn, execSync } from 'child_process';
16
18
  import { getDataDir } from '../modules/utils/paths';
17
19
 
18
20
  const PID_FILE = '/tmp/imtoagent.pid';
19
21
 
20
22
  // ================================================================
21
- // 命令分发
23
+ // Command dispatch
22
24
  // ================================================================
23
25
  const command = process.argv[2];
24
26
 
@@ -29,6 +31,9 @@ switch (command) {
29
31
  case 'start':
30
32
  await cmdStart();
31
33
  break;
34
+ case 'run':
35
+ await cmdRun();
36
+ break;
32
37
  case 'stop':
33
38
  await cmdStop();
34
39
  break;
@@ -42,19 +47,17 @@ switch (command) {
42
47
  await cmdDaemon();
43
48
  break;
44
49
  case undefined: {
45
- // 无命令 → 没完成 setup 自动进向导,已完成显示帮助
46
50
  const dataDir = getDataDir();
47
51
  const configPath = path.join(dataDir, 'config.json');
48
52
  let needsSetup = !fs.existsSync(configPath);
49
53
  if (!needsSetup) {
50
54
  try {
51
55
  const raw = fs.readFileSync(configPath, 'utf-8');
52
- // 如果配置里还有 YOUR_ 占位符,说明还没完成 setup
53
56
  needsSetup = /YOUR_[A-Z_]+/.test(raw);
54
57
  } catch { needsSetup = true; }
55
58
  }
56
59
  if (needsSetup) {
57
- console.log('👋 欢迎使用 imtoagent,请先完成初始配置\n');
60
+ console.log('👋 Welcome to imtoagent! Please run setup first.\n');
58
61
  await cmdSetup();
59
62
  } else {
60
63
  printHelp();
@@ -67,7 +70,7 @@ switch (command) {
67
70
  printHelp();
68
71
  break;
69
72
  default:
70
- console.error(`❌ 未知命令: ${command}`);
73
+ console.error(`❌ Unknown command: ${command}`);
71
74
  printHelp();
72
75
  process.exit(1);
73
76
  }
@@ -77,22 +80,23 @@ switch (command) {
77
80
  // ================================================================
78
81
  function printHelp() {
79
82
  console.log(`
80
- imtoagent — IM ↔ Agent 统一网关
81
-
82
- 用法:
83
- imtoagent setup 交互式配置向导
84
- imtoagent start 后台启动网关
85
- imtoagent stop 停止网关
86
- imtoagent status 查看运行状态
87
- imtoagent restore 热重载恢复
88
- imtoagent daemon 前台守护模式(自动重启 + 日志,适合 launchd/systemd 托管)
89
-
90
- 数据目录: ${getDataDir()}
83
+ imtoagent — IM ↔ Agent Unified Gateway
84
+
85
+ Usage:
86
+ imtoagent setup Interactive setup wizard
87
+ imtoagent start Start gateway in background (returns immediately)
88
+ imtoagent run Start gateway in foreground (Ctrl+C to stop)
89
+ imtoagent stop Stop gateway
90
+ imtoagent status Check running status
91
+ imtoagent restore Hot reload
92
+ imtoagent daemon Foreground daemon with auto-restart (for launchd/systemd)
93
+
94
+ Data directory: ${getDataDir()}
91
95
  `);
92
96
  }
93
97
 
94
98
  // ================================================================
95
- // setup — 交互式向导
99
+ // setup — interactive wizard
96
100
  // ================================================================
97
101
  async function cmdSetup() {
98
102
  const { runSetupWizard } = await import('../modules/cli/setup');
@@ -100,32 +104,45 @@ async function cmdSetup() {
100
104
  }
101
105
 
102
106
  // ================================================================
103
- // start 后台启动
107
+ // Shared: build gateway launch args
108
+ // ================================================================
109
+ function getGatewayArgs() {
110
+ const pkgDir = path.resolve(import.meta.dirname, '..');
111
+ const indexFile = path.join(pkgDir, 'index.ts');
112
+ return { execPath: process.execPath, args: ['run', indexFile] };
113
+ }
114
+
115
+ function ensureLogDir(dataDir: string) {
116
+ const logsDir = path.join(dataDir, 'logs');
117
+ if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
118
+ return path.join(logsDir, 'imtoagent.log');
119
+ }
120
+
121
+ // ================================================================
122
+ // start — background mode (spawn detached, log to file, return immediately)
104
123
  // ================================================================
105
124
  async function cmdStart() {
106
- // 检查是否已在运行
125
+ // Check if already running
107
126
  if (fs.existsSync(PID_FILE)) {
108
127
  const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
109
128
  try {
110
129
  process.kill(pid, 0);
111
- console.error(`❌ 网关已在运行 (PID=${pid})`);
112
- console.error(` 运行 "imtoagent stop" 先停止`);
130
+ console.error(`❌ Gateway already running (PID=${pid})`);
131
+ console.error(` Run "imtoagent stop" to stop first`);
113
132
  process.exit(1);
114
133
  } catch {
115
- // 旧 PID 文件残留,清理
116
134
  fs.unlinkSync(PID_FILE);
117
135
  }
118
136
  }
119
137
 
120
- // 检查配置是否存在
121
138
  const dataDir = getDataDir();
122
139
  const configPath = path.join(dataDir, 'config.json');
123
140
  if (!fs.existsSync(configPath)) {
124
- console.error('❌ 未找到配置文件,请先运行 "imtoagent setup"');
141
+ console.error('❌ No config file found. Please run "imtoagent setup" first');
125
142
  process.exit(1);
126
143
  }
127
144
 
128
- // 检查配置的后端是否已安装
145
+ // Backend check (non-blocking)
129
146
  try {
130
147
  const { checkBackend } = await import('../modules/utils/backend-check');
131
148
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
@@ -139,26 +156,90 @@ async function cmdStart() {
139
156
  }
140
157
  }
141
158
  if (missingBackends.length > 0) {
142
- console.error(`\n⚠️ 以下后端已配置但未安装,网关启动后发消息会报错:`);
143
- for (const b of missingBackends) {
144
- console.error(` ❌ ${b}`);
145
- }
146
- console.error(`\n请先安装缺失的后端,或运行 "imtoagent setup" 修改配置。\n`);
147
- // 不强制退出,允许用户先启动网关再慢慢装后端
159
+ console.error(`\n⚠️ The following backends are configured but not installed, messages will fail after gateway starts:`);
160
+ for (const b of missingBackends) console.error(` ❌ ${b}`);
161
+ console.error(`\nPlease install the missing backends, or run "imtoagent setup" to reconfigure.\n`);
148
162
  }
149
163
  } catch {
150
- // 检查失败不影响启动
164
+ // Check failure doesn't block startup
151
165
  }
152
166
 
153
- console.log('🚀 启动 imtoagent 网关...');
154
- console.log(` 数据目录: ${dataDir}`);
155
- console.log(` 配置文件: ${configPath}`);
167
+ const logFile = ensureLogDir(dataDir);
156
168
 
157
- // 使用 Bun.spawn 后台启动
158
- const pkgDir = path.resolve(import.meta.dirname, '..');
159
- const indexFile = path.join(pkgDir, 'index.ts');
169
+ console.log('🚀 Starting imtoagent gateway (background)...');
170
+ console.log(` Data directory: ${dataDir}`);
171
+ console.log(` Log file: ${logFile}`);
172
+
173
+ const { execPath, args } = getGatewayArgs();
174
+ const cmdLine = `"${execPath}" run "${path.resolve(import.meta.dirname, '..', 'index.ts')}"`;
175
+
176
+ // Use a shell to launch the gateway in background — avoids event-loop blockers
177
+ const shellCmd = `IMTOAGENT_HOME="${dataDir}" ${cmdLine} >> "${logFile}" 2>&1 &
178
+ PID=$!
179
+ echo $PID`;
180
+
181
+ const { execSync } = await import('child_process');
182
+ const pidStr = execSync(shellCmd, {
183
+ cwd: dataDir,
184
+ env: { ...process.env, IMTOAGENT_HOME: dataDir },
185
+ encoding: 'utf-8',
186
+ stdio: ['ignore', 'pipe', 'pipe'],
187
+ }).trim();
188
+ const gatewayPid = parseInt(pidStr.split('\n').pop()!);
189
+
190
+ fs.writeFileSync(PID_FILE, String(gatewayPid));
191
+ console.log(`✅ Gateway started (PID=${gatewayPid})`);
192
+
193
+ // Wait for startup verification
194
+ await new Promise(r => setTimeout(r, 3000));
195
+ try {
196
+ process.kill(gatewayPid, 0);
197
+ console.log('✅ Gateway is running');
198
+ } catch {
199
+ console.error('❌ Gateway failed to start, check logs:');
200
+ if (fs.existsSync(logFile)) {
201
+ console.log(fs.readFileSync(logFile, 'utf-8').slice(-2000));
202
+ }
203
+ fs.unlinkSync(PID_FILE);
204
+ process.exit(1);
205
+ }
206
+
207
+ // Explicitly exit — Bun may keep event loop alive due to inherited stdio
208
+ process.exit(0);
209
+ }
210
+
211
+ // ================================================================
212
+ // run — foreground mode (real-time logs, Ctrl+C to stop)
213
+ // ================================================================
214
+ async function cmdRun() {
215
+ const dataDir = getDataDir();
216
+ const configPath = path.join(dataDir, 'config.json');
217
+ if (!fs.existsSync(configPath)) {
218
+ console.error('❌ No config file found. Please run "imtoagent setup" first');
219
+ process.exit(1);
220
+ }
221
+
222
+ const logFile = ensureLogDir(dataDir);
223
+
224
+ // Warn if already running in background
225
+ if (fs.existsSync(PID_FILE)) {
226
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
227
+ try {
228
+ process.kill(pid, 0);
229
+ console.log(`⚠️ Gateway is already running in background (PID=${pid})`);
230
+ console.log(` Run "imtoagent stop" first, or this may conflict.\n`);
231
+ } catch {
232
+ fs.unlinkSync(PID_FILE);
233
+ }
234
+ }
160
235
 
161
- const child = Bun.spawn([process.execPath, 'run', indexFile], {
236
+ const { execPath, args } = getGatewayArgs();
237
+
238
+ console.log('🚀 Starting imtoagent gateway (foreground mode)...');
239
+ console.log(' Press Ctrl+C to stop');
240
+ console.log('');
241
+
242
+ const child = Bun.spawn([execPath, ...args], {
162
243
  cwd: dataDir,
163
244
  env: { ...process.env, IMTOAGENT_HOME: dataDir },
164
245
  stdout: 'pipe',
@@ -166,16 +247,10 @@ async function cmdStart() {
166
247
  });
167
248
 
168
249
  fs.writeFileSync(PID_FILE, String(child.pid));
169
- console.log(`✅ 网关已启动 (PID=${child.pid})`);
170
250
 
171
- // 后台日志重定向到 logs/
172
- const logsDir = path.join(dataDir, 'logs');
173
- if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
174
- const logFile = path.join(logsDir, 'imtoagent.log');
251
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
175
252
 
176
- // 启动日志收集
177
- (async () => {
178
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
253
+ const pumpOut = (async () => {
179
254
  for await (const chunk of child.stdout as any) {
180
255
  const line = new TextDecoder().decode(chunk);
181
256
  process.stdout.write(line);
@@ -183,8 +258,7 @@ async function cmdStart() {
183
258
  }
184
259
  })().catch(() => {});
185
260
 
186
- (async () => {
187
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
261
+ const pumpErr = (async () => {
188
262
  for await (const chunk of child.stderr as any) {
189
263
  const line = new TextDecoder().decode(chunk);
190
264
  process.stderr.write(line);
@@ -192,37 +266,42 @@ async function cmdStart() {
192
266
  }
193
267
  })().catch(() => {});
194
268
 
195
- // 等待启动验证(5 秒内检查 PID 是否存活)
196
- await new Promise(r => setTimeout(r, 3000));
197
- try {
198
- process.kill(child.pid, 0);
199
- console.log('✅ 网关运行正常');
200
- } catch {
201
- console.error(' 网关启动失败,查看日志:');
202
- if (fs.existsSync(logFile)) {
203
- console.log(fs.readFileSync(logFile, 'utf-8').slice(-2000));
204
- }
205
- fs.unlinkSync(PID_FILE);
206
- process.exit(1);
269
+ // Ctrl+C SIGTERM to child
270
+ const cleanup = () => {
271
+ console.log('\n🛑 Stopping gateway...');
272
+ try { process.kill(child.pid, 'SIGTERM'); } catch {}
273
+ };
274
+ process.on('SIGINT', cleanup);
275
+ process.on('SIGTERM', cleanup);
276
+
277
+ const exitCode = await child.exited;
278
+ await Promise.allSettled([pumpOut, pumpErr]);
279
+ logStream.end();
280
+
281
+ try { fs.unlinkSync(PID_FILE); } catch {}
282
+
283
+ if (exitCode === 0) {
284
+ console.log('✅ Gateway exited cleanly');
285
+ } else {
286
+ console.log(`⚠️ Gateway exited with code ${exitCode}`);
207
287
  }
208
288
  }
209
289
 
210
290
  // ================================================================
211
- // stop — 停止
291
+ // stop — stop gateway
212
292
  // ================================================================
213
293
  async function cmdStop() {
214
294
  if (!fs.existsSync(PID_FILE)) {
215
- console.log('ℹ️ 网关未运行');
295
+ console.log('ℹ️ Gateway is not running');
216
296
  return;
217
297
  }
218
298
 
219
299
  const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
220
300
  try {
221
301
  process.kill(pid, 0);
222
- console.log(`⏹ 正在停止网关 (PID=${pid})...`);
302
+ console.log(`⏹ Stopping gateway (PID=${pid})...`);
223
303
  process.kill(pid, 'SIGTERM');
224
304
 
225
- // 等待退出
226
305
  for (let i = 0; i < 20; i++) {
227
306
  try {
228
307
  process.kill(pid, 0);
@@ -232,145 +311,127 @@ async function cmdStop() {
232
311
  }
233
312
  }
234
313
 
235
- // 检查是否还在
236
314
  try {
237
315
  process.kill(pid, 0);
238
- console.log('⚠️ 进程未响应,强制终止...');
316
+ console.log('⚠️ Process not responding, force killing...');
239
317
  process.kill(pid, 'SIGKILL');
240
318
  } catch {
241
- console.log('✅ 网关已停止');
319
+ console.log('✅ Gateway stopped');
242
320
  }
243
321
  } catch {
244
- console.log('ℹ️ 网关未运行(PID 文件残留,已清理)');
322
+ console.log('ℹ️ Gateway not running (stale PID file cleaned up)');
245
323
  }
246
324
 
247
325
  try { fs.unlinkSync(PID_FILE); } catch {}
248
326
  }
249
327
 
250
328
  // ================================================================
251
- // status — 状态
329
+ // status — status check
252
330
  // ================================================================
253
331
  async function cmdStatus() {
254
332
  const dataDir = getDataDir();
255
333
 
256
- console.log(`\n📊 imtoagent 状态`);
257
- console.log(` 数据目录: ${dataDir}`);
334
+ console.log(`\n📊 imtoagent Status`);
335
+ console.log(` Data directory: ${dataDir}`);
258
336
 
259
- // 进程状态
260
337
  if (fs.existsSync(PID_FILE)) {
261
338
  const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
262
339
  try {
263
340
  process.kill(pid, 0);
264
- console.log(` 进程:运行中 (PID=${pid})`);
341
+ console.log(` Process:Running (PID=${pid})`);
265
342
  } catch {
266
- console.log(` 进程:已停止 (PID=${pid} 不存在)`);
343
+ console.log(` Process:Stopped (PID=${pid} does not exist)`);
267
344
  }
268
345
  } else {
269
- console.log(` 进程:未运行`);
346
+ console.log(` Process:Not running`);
270
347
  }
271
348
 
272
- // 配置文件
273
349
  const configPath = path.join(dataDir, 'config.json');
274
350
  if (fs.existsSync(configPath)) {
275
351
  try {
276
352
  const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
277
353
  const bots = cfg.bots || [];
278
- console.log(` 配置:已配置 (${bots.length} Bot)`);
354
+ console.log(` Config:Configured (${bots.length} Bot(s))`);
279
355
  for (const bot of bots) {
280
356
  console.log(` - ${bot.name} (${bot.backend})`);
281
357
  }
282
358
  } catch {
283
- console.log(` 配置:解析失败`);
359
+ console.log(` Config:Parse error`);
284
360
  }
285
361
  } else {
286
- console.log(` 配置:未找到 (运行 "imtoagent setup")`);
362
+ console.log(` Config:Not found (run "imtoagent setup")`);
287
363
  }
288
364
 
289
- // 日志
290
365
  const logFile = path.join(dataDir, 'logs', 'imtoagent.log');
291
366
  if (fs.existsSync(logFile)) {
292
367
  const stats = fs.statSync(logFile);
293
368
  const size = stats.size > 1024 * 1024
294
369
  ? (stats.size / (1024 * 1024)).toFixed(1) + ' MB'
295
370
  : (stats.size / 1024).toFixed(1) + ' KB';
296
- console.log(` 日志: ${size} (${logFile})`);
371
+ console.log(` Log: ${size} (${logFile})`);
297
372
  }
298
373
 
299
374
  console.log();
300
375
  }
301
376
 
302
377
  // ================================================================
303
- // restore — 热重载
378
+ // restore — hot reload
304
379
  // ================================================================
305
380
  async function cmdRestore() {
306
381
  if (!fs.existsSync(PID_FILE)) {
307
- console.error('❌ 网关未运行,无法热重载');
382
+ console.error('❌ Gateway is not running, cannot hot reload');
308
383
  process.exit(1);
309
384
  }
310
385
 
311
386
  const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
312
- console.log(`🔄 发送 SIGHUP 到网关 (PID=${pid})...`);
387
+ console.log(`🔄 Sending SIGHUP to gateway (PID=${pid})...`);
313
388
  try {
314
389
  process.kill(pid, 'SIGHUP');
315
- console.log('✅ 热重载信号已发送');
390
+ console.log('✅ Hot reload signal sent');
316
391
  } catch (e: any) {
317
- console.error(`❌ 发送失败: ${e.message}`);
392
+ console.error(`❌ Failed to send: ${e.message}`);
318
393
  process.exit(1);
319
394
  }
320
395
  }
321
396
 
322
397
  // ================================================================
323
- // daemon — 前台守护模式(自动重启 + 日志 + 优雅退出)
324
- // ================================================================
325
- // 设计用途:
326
- // - 前台运行,被 launchd / systemd 等进程管理器托管
327
- // - 崩溃时自动重启(指数退避,最长 30s)
328
- // - 收到 SIGTERM/SIGINT 时优雅关闭,不重启
329
- // - 日志写入 ~/.imtoagent/logs/imtoagent.log
398
+ // daemon — foreground daemon with auto-restart (for launchd/systemd)
330
399
  // ================================================================
331
400
  async function cmdDaemon(): Promise<void> {
332
401
  const dataDir = getDataDir();
333
402
  const configPath = path.join(dataDir, 'config.json');
334
403
 
335
404
  if (!fs.existsSync(configPath)) {
336
- console.error('❌ 未找到配置文件,请先运行 "imtoagent setup"');
405
+ console.error('❌ No config file found. Please run "imtoagent setup" first');
337
406
  process.exit(1);
338
407
  }
339
408
 
340
- const logsDir = path.join(dataDir, 'logs');
341
- if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
342
- const logFile = path.join(logsDir, 'imtoagent.log');
343
-
344
- const pkgDir = path.resolve(import.meta.dirname, '..');
345
- const indexFile = path.join(pkgDir, 'index.ts');
409
+ const logFile = ensureLogDir(dataDir);
410
+ const { execPath, args } = getGatewayArgs();
346
411
 
347
- console.log(`🛡 imtoagent 守护模式`);
348
- console.log(` 数据目录: ${dataDir}`);
349
- console.log(` 日志文件: ${logFile}`);
350
- console.log(` Ctrl+C 停止\n`);
412
+ console.log(`🛡 imtoagent Daemon Mode`);
413
+ console.log(` Data directory: ${dataDir}`);
414
+ console.log(` Log file: ${logFile}`);
415
+ console.log(` Press Ctrl+C to stop\n`);
351
416
 
352
- // 优雅退出标记
353
417
  let shuttingDown = false;
354
-
355
418
  const shutdown = () => {
356
419
  if (shuttingDown) return;
357
420
  shuttingDown = true;
358
- console.log('\n🛑 收到停止信号,正在关闭...');
421
+ console.log('\n🛑 Received stop signal, shutting down...');
359
422
  };
360
423
 
361
424
  process.on('SIGTERM', shutdown);
362
425
  process.on('SIGINT', shutdown);
363
426
 
364
427
  let retryDelay = 0;
365
- const MAX_RETRY_DELAY = 30_000; // 30s 上限
428
+ const MAX_RETRY_DELAY = 30_000;
366
429
 
367
430
  while (!shuttingDown) {
368
- // 首次无延迟,之后指数退避
369
431
  if (retryDelay > 0) {
370
- console.log(` 等待 ${retryDelay / 1000}s 后重启...`);
432
+ console.log(` Waiting ${retryDelay / 1000}s before restart...`);
371
433
  await new Promise<void>(resolve => {
372
434
  const timer = setTimeout(resolve, retryDelay);
373
- // 等待期间如果收到停止信号,立即退出
374
435
  const check = setInterval(() => {
375
436
  if (shuttingDown) {
376
437
  clearTimeout(timer);
@@ -382,56 +443,39 @@ async function cmdDaemon(): Promise<void> {
382
443
  if (shuttingDown) break;
383
444
  }
384
445
 
385
- const logStream = fs.createWriteStream(logFile, { flags: 'a' });
446
+ // Open log fd for child stdout/stderr
447
+ const logFd = fs.openSync(logFile, 'a');
386
448
 
387
- const child = Bun.spawn([process.execPath, 'run', indexFile], {
449
+ const child = spawn(execPath, args, {
388
450
  cwd: dataDir,
389
451
  env: { ...process.env, IMTOAGENT_HOME: dataDir },
390
- stdout: 'pipe',
391
- stderr: 'pipe',
452
+ detached: true,
453
+ stdio: ['ignore', logFd, logFd],
392
454
  });
393
455
 
394
456
  const childPid = child.pid;
395
457
  fs.writeFileSync(PID_FILE, String(childPid));
396
- console.log(`[${new Date().toISOString()}] 🚀 启动网关 (PID=${childPid})`);
397
-
398
- // 日志收集
399
- const pumpStdout = (async () => {
400
- for await (const chunk of child.stdout as any) {
401
- const line = new TextDecoder().decode(chunk);
402
- process.stdout.write(line);
403
- logStream.write(line);
404
- }
405
- })().catch(() => {});
406
-
407
- const pumpStderr = (async () => {
408
- for await (const chunk of child.stderr as any) {
409
- const line = new TextDecoder().decode(chunk);
410
- process.stderr.write(line);
411
- logStream.write(line);
412
- }
413
- })().catch(() => {});
458
+ console.log(`[${new Date().toISOString()}] 🚀 Starting gateway (PID=${childPid})`);
414
459
 
415
- // 等待子进程退出
416
- const exitCode = await child.exited;
417
- await Promise.allSettled([pumpStdout, pumpStderr]);
418
- logStream.end();
460
+ let childExitCode: number | null = null;
461
+ await new Promise<void>(resolve => {
462
+ child.on('exit', (code) => { childExitCode = code; resolve(); });
463
+ child.on('error', () => resolve());
464
+ });
419
465
 
466
+ fs.closeSync(logFd);
420
467
  try { fs.unlinkSync(PID_FILE); } catch {}
421
468
 
422
469
  if (shuttingDown) break;
423
470
 
424
- // 判断是否需要重启
425
- if (exitCode === 0) {
426
- console.log(`[${new Date().toISOString()}] ⏹ 网关正常退出 (code=0),不重启`);
471
+ if (childExitCode === 0) {
472
+ console.log(`[${new Date().toISOString()}] ⏹ Gateway exited cleanly (code=0), not restarting`);
427
473
  break;
428
474
  }
429
475
 
430
- // 崩溃 → 指数退避重启
431
476
  retryDelay = retryDelay === 0 ? 3_000 : Math.min(retryDelay * 2, MAX_RETRY_DELAY);
432
- console.log(`[${new Date().toISOString()}] ⚠️ 网关异常退出 (code=${exitCode}),${retryDelay / 1000}s 后重启`);
477
+ console.log(`[${new Date().toISOString()}] ⚠️ Gateway crashed (code=${childExitCode}), restarting in ${retryDelay / 1000}s`);
433
478
  }
434
479
 
435
- console.log('👋 守护进程已停止');
480
+ console.log('👋 Daemon stopped');
436
481
  }
437
-
package/bin/imtoagent.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  "use strict";
4
4
  var path = require("path");
5
5
  var fs = require("fs");
6
- var spawnSync = require("child_process").spawnSync;
6
+ var spawn = require("child_process").spawn;
7
7
 
8
8
  var candidates = [
9
9
  process.env.BUN_BIN,
@@ -12,7 +12,7 @@ var candidates = [
12
12
  "/opt/homebrew/bin/bun",
13
13
  ];
14
14
  try {
15
- var r = spawnSync("which", ["bun"]);
15
+ var r = require("child_process").spawnSync("which", ["bun"]);
16
16
  if (r.status === 0) candidates.unshift(r.stdout.toString().trim());
17
17
  } catch (e) {}
18
18
 
@@ -25,15 +25,23 @@ for (var i = 0; i < candidates.length; i++) {
25
25
  }
26
26
 
27
27
  if (!bunPath) {
28
- console.error("❌ bun 未找到,请先安装: https://bun.sh");
28
+ console.error("❌ bun not found, please install: https://bun.sh");
29
29
  console.error(" curl -fsSL https://bun.sh/install | bash");
30
30
  process.exit(1);
31
31
  }
32
32
 
33
33
  var pkgDir = path.resolve(__dirname, "..");
34
34
  var real = path.join(pkgDir, "bin", "imtoagent-real");
35
- var result = spawnSync(bunPath, [real].concat(process.argv.slice(2)), {
35
+ var child = spawn(bunPath, [real].concat(process.argv.slice(2)), {
36
36
  stdio: "inherit",
37
37
  env: Object.assign({}, process.env),
38
38
  });
39
- process.exit(result.status || 0);
39
+
40
+ child.on("exit", function (code) {
41
+ process.exit(code || 0);
42
+ });
43
+
44
+ child.on("error", function (err) {
45
+ console.error("❌ Failed to start imtoagent:", err.message);
46
+ process.exit(1);
47
+ });