imtoagent 0.3.5 → 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.
@@ -5,14 +5,16 @@
5
5
  // Available after npm install -g imtoagent:
6
6
  // imtoagent setup — interactive setup wizard
7
7
  // imtoagent start — start gateway in background
8
+ // imtoagent run — start gateway in foreground
8
9
  // imtoagent stop — stop gateway
9
10
  // imtoagent status — check running status
10
11
  // imtoagent restore — hot reload
11
- // imtoagent daemon — foreground daemon (auto-restart + logs)
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';
@@ -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,14 +47,12 @@ switch (command) {
42
47
  await cmdDaemon();
43
48
  break;
44
49
  case undefined: {
45
- // No command → auto-enter setup if not configured, show help otherwise
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
- // If config still has YOUR_ placeholders, setup is incomplete
53
56
  needsSetup = /YOUR_[A-Z_]+/.test(raw);
54
57
  } catch { needsSetup = true; }
55
58
  }
@@ -81,11 +84,12 @@ imtoagent — IM ↔ Agent Unified Gateway
81
84
 
82
85
  Usage:
83
86
  imtoagent setup Interactive setup wizard
84
- imtoagent start Start gateway in background
87
+ imtoagent start Start gateway in background (returns immediately)
88
+ imtoagent run Start gateway in foreground (Ctrl+C to stop)
85
89
  imtoagent stop Stop gateway
86
90
  imtoagent status Check running status
87
91
  imtoagent restore Hot reload
88
- imtoagent daemon Foreground daemon (auto-restart + logs, for launchd/systemd)
92
+ imtoagent daemon Foreground daemon with auto-restart (for launchd/systemd)
89
93
 
90
94
  Data directory: ${getDataDir()}
91
95
  `);
@@ -100,7 +104,22 @@ async function cmdSetup() {
100
104
  }
101
105
 
102
106
  // ================================================================
103
- // start launch in background
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
@@ -112,12 +131,10 @@ async function cmdStart() {
112
131
  console.error(` Run "imtoagent stop" to stop first`);
113
132
  process.exit(1);
114
133
  } catch {
115
- // Stale PID file, clean up
116
134
  fs.unlinkSync(PID_FILE);
117
135
  }
118
136
  }
119
137
 
120
- // Check if config exists
121
138
  const dataDir = getDataDir();
122
139
  const configPath = path.join(dataDir, 'config.json');
123
140
  if (!fs.existsSync(configPath)) {
@@ -125,7 +142,7 @@ async function cmdStart() {
125
142
  process.exit(1);
126
143
  }
127
144
 
128
- // Check if configured backends are installed
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'));
@@ -140,25 +157,89 @@ async function cmdStart() {
140
157
  }
141
158
  if (missingBackends.length > 0) {
142
159
  console.error(`\n⚠️ The following backends are configured but not installed, messages will fail after gateway starts:`);
143
- for (const b of missingBackends) {
144
- console.error(` ❌ ${b}`);
145
- }
160
+ for (const b of missingBackends) console.error(` ❌ ${b}`);
146
161
  console.error(`\nPlease install the missing backends, or run "imtoagent setup" to reconfigure.\n`);
147
- // Don't force exit, let user start gateway and install backends later
148
162
  }
149
163
  } catch {
150
164
  // Check failure doesn't block startup
151
165
  }
152
166
 
153
- console.log('🚀 Starting imtoagent gateway...');
167
+ const logFile = ensureLogDir(dataDir);
168
+
169
+ console.log('🚀 Starting imtoagent gateway (background)...');
154
170
  console.log(` Data directory: ${dataDir}`);
155
- console.log(` Config file: ${configPath}`);
171
+ console.log(` Log file: ${logFile}`);
156
172
 
157
- // Launch in background using Bun.spawn
158
- const pkgDir = path.resolve(import.meta.dirname, '..');
159
- const indexFile = path.join(pkgDir, 'index.ts');
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`;
160
180
 
161
- const child = Bun.spawn([process.execPath, 'run', indexFile], {
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
+ }
235
+
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(`✅ Gateway started (PID=${child.pid})`);
170
250
 
171
- // Redirect background logs to 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
- // Start log collection
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,18 +266,24 @@ async function cmdStart() {
192
266
  }
193
267
  })().catch(() => {});
194
268
 
195
- // Wait for startup verification (check PID survives 5s)
196
- await new Promise(r => setTimeout(r, 3000));
197
- try {
198
- process.kill(child.pid, 0);
199
- console.log('✅ Gateway is running');
200
- } catch {
201
- console.error('❌ Gateway failed to start, check logs:');
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
 
@@ -222,7 +302,6 @@ async function cmdStop() {
222
302
  console.log(`⏹ Stopping gateway (PID=${pid})...`);
223
303
  process.kill(pid, 'SIGTERM');
224
304
 
225
- // Wait for process to exit
226
305
  for (let i = 0; i < 20; i++) {
227
306
  try {
228
307
  process.kill(pid, 0);
@@ -232,7 +311,6 @@ async function cmdStop() {
232
311
  }
233
312
  }
234
313
 
235
- // Check if still running
236
314
  try {
237
315
  process.kill(pid, 0);
238
316
  console.log('⚠️ Process not responding, force killing...');
@@ -256,7 +334,6 @@ async function cmdStatus() {
256
334
  console.log(`\n📊 imtoagent Status`);
257
335
  console.log(` Data directory: ${dataDir}`);
258
336
 
259
- // Process status
260
337
  if (fs.existsSync(PID_FILE)) {
261
338
  const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
262
339
  try {
@@ -269,7 +346,6 @@ async function cmdStatus() {
269
346
  console.log(` Process: ⏸ Not running`);
270
347
  }
271
348
 
272
- // Config file
273
349
  const configPath = path.join(dataDir, 'config.json');
274
350
  if (fs.existsSync(configPath)) {
275
351
  try {
@@ -286,7 +362,6 @@ async function cmdStatus() {
286
362
  console.log(` Config: ❌ Not found (run "imtoagent setup")`);
287
363
  }
288
364
 
289
- // Log file
290
365
  const logFile = path.join(dataDir, 'logs', 'imtoagent.log');
291
366
  if (fs.existsSync(logFile)) {
292
367
  const stats = fs.statSync(logFile);
@@ -320,13 +395,7 @@ async function cmdRestore() {
320
395
  }
321
396
 
322
397
  // ================================================================
323
- // daemon — foreground daemon mode (auto-restart + logs + graceful shutdown)
324
- // ================================================================
325
- // Design:
326
- // - Runs in foreground, managed by launchd / systemd etc.
327
- // - Auto-restarts on crash (exponential backoff, max 30s)
328
- // - Graceful shutdown on SIGTERM/SIGINT, no restart
329
- // - Logs written to ~/.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();
@@ -337,21 +406,15 @@ async function cmdDaemon(): Promise<void> {
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
412
  console.log(`🛡 imtoagent Daemon Mode`);
348
413
  console.log(` Data directory: ${dataDir}`);
349
414
  console.log(` Log file: ${logFile}`);
350
415
  console.log(` Press Ctrl+C to stop\n`);
351
416
 
352
- // Graceful shutdown flag
353
417
  let shuttingDown = false;
354
-
355
418
  const shutdown = () => {
356
419
  if (shuttingDown) return;
357
420
  shuttingDown = true;
@@ -362,15 +425,13 @@ async function cmdDaemon(): Promise<void> {
362
425
  process.on('SIGINT', shutdown);
363
426
 
364
427
  let retryDelay = 0;
365
- const MAX_RETRY_DELAY = 30_000; // 30s cap
428
+ const MAX_RETRY_DELAY = 30_000;
366
429
 
367
430
  while (!shuttingDown) {
368
- // No delay on first run, exponential backoff after
369
431
  if (retryDelay > 0) {
370
432
  console.log(` Waiting ${retryDelay / 1000}s before restart...`);
371
433
  await new Promise<void>(resolve => {
372
434
  const timer = setTimeout(resolve, retryDelay);
373
- // Exit immediately if shutdown signal received during wait
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
458
  console.log(`[${new Date().toISOString()}] 🚀 Starting gateway (PID=${childPid})`);
397
459
 
398
- // Log collection
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(() => {});
414
-
415
- // Wait for child process to exit
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
- // Determine if restart needed
425
- if (exitCode === 0) {
471
+ if (childExitCode === 0) {
426
472
  console.log(`[${new Date().toISOString()}] ⏹ Gateway exited cleanly (code=0), not restarting`);
427
473
  break;
428
474
  }
429
475
 
430
- // Crash → exponential backoff restart
431
476
  retryDelay = retryDelay === 0 ? 3_000 : Math.min(retryDelay * 2, MAX_RETRY_DELAY);
432
- console.log(`[${new Date().toISOString()}] ⚠️ Gateway crashed (code=${exitCode}), restarting in ${retryDelay / 1000}s`);
477
+ console.log(`[${new Date().toISOString()}] ⚠️ Gateway crashed (code=${childExitCode}), restarting in ${retryDelay / 1000}s`);
433
478
  }
434
479
 
435
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
 
@@ -32,8 +32,16 @@ if (!bunPath) {
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
+ });
@@ -88,7 +88,7 @@ async function spawnCodexResume(cwd: string, threadId: string, prompt: string):
88
88
  try {
89
89
  [stdout, stderr] = await Promise.all([
90
90
  new Response(child.stdout).text().catch((e: any) => { throw new Error(`stdout read failed: ${e?.message || e}`); }),
91
- new Response(child.stderr).text().catch((e: any) => { throw new Error(`stderr read failed: ${e?.message || e}`; }),
91
+ new Response(child.stderr).text().catch((e: any) => { throw new Error(`stderr read failed: ${e?.message || e}`); }),
92
92
  ]);
93
93
  } catch (ioErr: any) {
94
94
  try { child.kill('SIGKILL'); } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imtoagent",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "IM ↔ Agent 统一网关 — 飞书/Telegram/微信/企业微信对接 Claude Code/Codex/OpenCode",
5
5
  "type": "module",
6
6
  "bin": {