imtoagent 0.3.25 → 0.3.26

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.
@@ -16,7 +16,7 @@ import * as fs from 'fs';
16
16
  import * as path from 'path';
17
17
  import * as readline from 'readline';
18
18
  import { spawn, execSync } from 'child_process';
19
- import { getDataDir } from '../modules/utils/paths';
19
+ import { getDataDir, getLogsDir } from '../modules/utils/paths';
20
20
 
21
21
  const PID_FILE = '/tmp/imtoagent.pid';
22
22
 
@@ -70,6 +70,12 @@ switch (command) {
70
70
  case 'autostart':
71
71
  await cmdAutostart();
72
72
  break;
73
+ case 'logs':
74
+ await cmdLogs();
75
+ break;
76
+ case 'validate':
77
+ await cmdValidate();
78
+ break;
73
79
  case undefined: {
74
80
  const dataDir = getDataDir();
75
81
  const configPath = path.join(dataDir, 'config.json');
@@ -137,6 +143,10 @@ Usage:
137
143
  imtoagent autostart enable Enable auto-start on login (launchd)
138
144
  imtoagent autostart disable Disable auto-start
139
145
  imtoagent autostart status Check auto-start status
146
+ imtoagent logs Show last 50 lines of log
147
+ imtoagent logs -n N Show last N lines
148
+ imtoagent logs -f Follow log in real-time (tail -f)
149
+ imtoagent validate Validate config.json for errors
140
150
 
141
151
  Data directory: ${getDataDir()}
142
152
  `);
@@ -1268,6 +1278,185 @@ async function cmdAutostartStatus(plistPath: string): Promise<void> {
1268
1278
  console.log();
1269
1279
  }
1270
1280
 
1281
+ // ================================================================
1282
+ // logs — 查看日志
1283
+ // ================================================================
1284
+ async function cmdLogs(): Promise<void> {
1285
+ const argv = process.argv.slice(3);
1286
+ let n = 50;
1287
+ let follow = false;
1288
+
1289
+ for (let i = 0; i < argv.length; i++) {
1290
+ if (argv[i] === '-n' && argv[i + 1]) {
1291
+ n = parseInt(argv[++i], 10);
1292
+ if (isNaN(n) || n < 1) {
1293
+ console.error('❌ Invalid line count');
1294
+ process.exit(1);
1295
+ }
1296
+ } else if (argv[i] === '-f') {
1297
+ follow = true;
1298
+ }
1299
+ }
1300
+
1301
+ const logsDir = getLogsDir();
1302
+ const logFile = path.join(logsDir, 'imtoagent.log');
1303
+
1304
+ if (!fs.existsSync(logFile)) {
1305
+ console.log('ℹ️ No log file found.');
1306
+ console.log(` Expected: ${logFile}`);
1307
+ console.log(' Start the gateway first: imtoagent start');
1308
+ return;
1309
+ }
1310
+
1311
+ if (follow) {
1312
+ // tail -f mode — spawn tail process
1313
+ console.log(`📄 Following log: ${logFile} (Ctrl+C to stop)\n`);
1314
+ const child = spawn('tail', ['-f', '-n', String(n), logFile], {
1315
+ stdio: 'inherit',
1316
+ });
1317
+
1318
+ const cleanup = () => {
1319
+ try { child.kill('SIGTERM'); } catch {}
1320
+ };
1321
+ process.on('SIGINT', cleanup);
1322
+ process.on('SIGTERM', cleanup);
1323
+
1324
+ await new Promise<void>((resolve) => {
1325
+ child.on('exit', () => resolve());
1326
+ child.on('error', () => resolve());
1327
+ });
1328
+ } else {
1329
+ // Print last N lines
1330
+ try {
1331
+ const content = fs.readFileSync(logFile, 'utf-8');
1332
+ const lines = content.split('\n');
1333
+ const tail = lines.slice(-n).filter(l => l.length > 0);
1334
+ if (tail.length === 0) {
1335
+ console.log('ℹ️ Log file is empty.');
1336
+ } else {
1337
+ console.log(`📄 Last ${tail.length} lines of ${logFile}:\n`);
1338
+ for (const line of tail) {
1339
+ console.log(line);
1340
+ }
1341
+ }
1342
+ } catch (e: any) {
1343
+ console.error(`❌ Failed to read log: ${e.message}`);
1344
+ process.exit(1);
1345
+ }
1346
+ }
1347
+ }
1348
+
1349
+ // ================================================================
1350
+ // validate — 验证配置文件
1351
+ // ================================================================
1352
+ async function cmdValidate(): Promise<void> {
1353
+ const dataDir = getDataDir();
1354
+ const configPath = path.join(dataDir, 'config.json');
1355
+
1356
+ console.log(`\n🔍 imtoagent Config Validation\n`);
1357
+ console.log(` Config: ${configPath}\n`);
1358
+
1359
+ const issues: string[] = [];
1360
+ const okItems: string[] = [];
1361
+
1362
+ // ---- 1. Check file exists ----
1363
+ if (!fs.existsSync(configPath)) {
1364
+ console.error('❌ config.json not found');
1365
+ console.error(` Run "imtoagent setup" to create it.\n`);
1366
+ process.exit(1);
1367
+ }
1368
+ okItems.push('config.json exists');
1369
+
1370
+ // ---- 2. Parse JSON ----
1371
+ let raw: string;
1372
+ let config: any;
1373
+ try {
1374
+ raw = fs.readFileSync(configPath, 'utf-8');
1375
+ config = JSON.parse(raw);
1376
+ okItems.push('JSON format is valid');
1377
+ } catch (e: any) {
1378
+ console.error(`❌ Invalid JSON: ${e.message}`);
1379
+ console.error(` Please fix the syntax error and try again.\n`);
1380
+ process.exit(1);
1381
+ }
1382
+
1383
+ // ---- 3. Validate bots array ----
1384
+ const validIMs = ['feishu', 'telegram', 'wecom', 'wechat'];
1385
+ const validBackends = ['claude', 'codex', 'opencode'];
1386
+
1387
+ if (!config.bots || !Array.isArray(config.bots)) {
1388
+ issues.push('bots is missing or not an array');
1389
+ } else if (config.bots.length === 0) {
1390
+ issues.push('bots array is empty — no bots configured');
1391
+ } else {
1392
+ okItems.push(`bots array has ${config.bots.length} entry/entries`);
1393
+
1394
+ config.bots.forEach((bot: any, idx: number) => {
1395
+ const label = `bots[${idx}]`;
1396
+ const botName = bot.name || `(unnamed, index ${idx})`;
1397
+
1398
+ // Check required fields
1399
+ if (!bot.name) issues.push(`${label}: missing required field "name"`);
1400
+ if (!bot.im) issues.push(`${label}: missing required field "im"`);
1401
+ if (!bot.appId) issues.push(`${label}: missing required field "appId"`);
1402
+ if (!bot.backend) issues.push(`${label}: missing required field "backend"`);
1403
+
1404
+ // Validate IM platform
1405
+ if (bot.im && !validIMs.includes(bot.im)) {
1406
+ issues.push(`${label}: invalid im value "${bot.im}" — must be one of: ${validIMs.join(', ')}`);
1407
+ }
1408
+
1409
+ // Validate backend
1410
+ if (bot.backend && !validBackends.includes(bot.backend)) {
1411
+ issues.push(`${label}: invalid backend value "${bot.backend}" — must be one of: ${validBackends.join(', ')}`);
1412
+ }
1413
+ });
1414
+ }
1415
+
1416
+ // ---- 4. Validate workspace config ----
1417
+ const ws = config.workspace || {};
1418
+ if (ws.mode === 'global') {
1419
+ if (!ws.globalPath) {
1420
+ issues.push('workspace: mode is "global" but globalPath is not set');
1421
+ } else {
1422
+ if (!fs.existsSync(ws.globalPath)) {
1423
+ issues.push(`workspace: globalPath directory does not exist: ${ws.globalPath}`);
1424
+ } else {
1425
+ okItems.push(`workspace: globalPath exists (${ws.globalPath})`);
1426
+ }
1427
+ }
1428
+ } else if (ws.mode && ws.mode !== 'sandbox') {
1429
+ issues.push(`workspace: invalid mode "${ws.mode}" — must be "sandbox" or "global"`);
1430
+ } else {
1431
+ okItems.push(`workspace: mode is "${ws.mode || 'sandbox'}" (default)`);
1432
+ }
1433
+
1434
+ // ---- Print results ----
1435
+ if (okItems.length > 0) {
1436
+ console.log('✅ OK:');
1437
+ for (const item of okItems) {
1438
+ console.log(` ✓ ${item}`);
1439
+ }
1440
+ console.log();
1441
+ }
1442
+
1443
+ if (issues.length > 0) {
1444
+ console.log('❌ Issues:');
1445
+ for (const issue of issues) {
1446
+ console.log(` ✗ ${issue}`);
1447
+ }
1448
+ console.log();
1449
+ }
1450
+
1451
+ // Summary
1452
+ if (issues.length === 0) {
1453
+ console.log('✅ Configuration is valid!\n');
1454
+ } else {
1455
+ console.log(`❌ ${issues.length} issue(s) found. Please fix them.\n`);
1456
+ process.exit(1);
1457
+ }
1458
+ }
1459
+
1271
1460
  // ================================================================
1272
1461
  // version-check — non-blocking npm registry check
1273
1462
  // ================================================================
@@ -141,6 +141,7 @@ export async function cmdConfigShow(name: string): Promise<void> {
141
141
  console.log();
142
142
  }
143
143
 
144
+ /** @internal — exported for testing via __TEST_MASK_SECRET */
144
145
  function maskSecret(s: string): string {
145
146
  if (s.length <= 8) return '***';
146
147
  return s.slice(0, 4) + '...' + s.slice(-4);
@@ -357,3 +358,17 @@ export async function cmdConfigModify(name: string): Promise<void> {
357
358
  console.log(` Run "imtoagent restore" to hot-reload the gateway.\n`);
358
359
  rl.close();
359
360
  }
361
+
362
+ // ================================================================
363
+ // Test exports (NOT for production use)
364
+ // ================================================================
365
+
366
+ /**
367
+ * Export maskSecret for testing only.
368
+ */
369
+ export const __test_maskSecret = maskSecret;
370
+
371
+ /**
372
+ * Export constants for validate command / testing.
373
+ */
374
+ export { VALID_BACKENDS, VALID_IMS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imtoagent",
3
- "version": "0.3.25",
3
+ "version": "0.3.26",
4
4
  "description": "IM ↔ Agent 统一网关 — 飞书/Telegram/微信/企业微信对接 Claude Code/Codex/OpenCode",
5
5
  "type": "module",
6
6
  "bin": {