imtoagent 0.3.24 → 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
 
@@ -61,9 +61,21 @@ switch (command) {
61
61
  case 'health':
62
62
  await cmdHealth();
63
63
  break;
64
+ case 'doctor':
65
+ await cmdDoctor();
66
+ break;
67
+ case 'config':
68
+ await cmdConfig();
69
+ break;
64
70
  case 'autostart':
65
71
  await cmdAutostart();
66
72
  break;
73
+ case 'logs':
74
+ await cmdLogs();
75
+ break;
76
+ case 'validate':
77
+ await cmdValidate();
78
+ break;
67
79
  case undefined: {
68
80
  const dataDir = getDataDir();
69
81
  const configPath = path.join(dataDir, 'config.json');
@@ -121,9 +133,20 @@ Usage:
121
133
  imtoagent uninstall Uninstall imtoagent (keep data by default)
122
134
  imtoagent uninstall --purge Uninstall and delete all data
123
135
  imtoagent health Run comprehensive health check
136
+ imtoagent doctor Diagnose & fix configuration issues
137
+ imtoagent config Manage Bot configuration
138
+ imtoagent config list List all Bots
139
+ imtoagent config show NAME Show Bot details
140
+ imtoagent config add Add a new Bot
141
+ imtoagent config remove NAME Remove a Bot
142
+ imtoagent config modify NAME Modify Bot settings
124
143
  imtoagent autostart enable Enable auto-start on login (launchd)
125
144
  imtoagent autostart disable Disable auto-start
126
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
127
150
 
128
151
  Data directory: ${getDataDir()}
129
152
  `);
@@ -939,6 +962,177 @@ async function cmdHealth(): Promise<void> {
939
962
  console.log();
940
963
  }
941
964
 
965
+ // ================================================================
966
+ // config — Bot 配置管理
967
+ // ================================================================
968
+ async function cmdConfig(): Promise<void> {
969
+ const subcommand = process.argv[3];
970
+
971
+ switch (subcommand) {
972
+ case 'list':
973
+ await cmdConfigList();
974
+ break;
975
+ case 'show': {
976
+ const name = process.argv[4];
977
+ if (!name) { console.error('Usage: imtoagent config show <name>'); process.exit(1); }
978
+ await cmdConfigShow(name);
979
+ break;
980
+ }
981
+ case 'add':
982
+ await cmdConfigAdd();
983
+ break;
984
+ case 'remove': {
985
+ const name = process.argv[4];
986
+ if (!name) { console.error('Usage: imtoagent config remove <name>'); process.exit(1); }
987
+ await cmdConfigRemove(name);
988
+ break;
989
+ }
990
+ case 'modify': {
991
+ const name = process.argv[4];
992
+ if (!name) { console.error('Usage: imtoagent config modify <name>'); process.exit(1); }
993
+ await cmdConfigModify(name);
994
+ break;
995
+ }
996
+ case undefined:
997
+ case 'help':
998
+ case '--help':
999
+ case '-h':
1000
+ console.log(`
1001
+ imtoagent config — Manage Bot configuration
1002
+
1003
+ Usage:
1004
+ imtoagent config list List all Bots
1005
+ imtoagent config show NAME Show Bot details
1006
+ imtoagent config add Add a new Bot (interactive)
1007
+ imtoagent config remove NAME Remove a Bot
1008
+ imtoagent config modify NAME Modify Bot settings
1009
+ `);
1010
+ break;
1011
+ default:
1012
+ console.error(`❌ Unknown config subcommand: ${subcommand}`);
1013
+ console.log(' Run "imtoagent config help" for usage.');
1014
+ process.exit(1);
1015
+ }
1016
+ }
1017
+
1018
+ // ---- Config subcommand wrappers ----
1019
+ async function cmdConfigList(): Promise<void> {
1020
+ const m = await import('../modules/utils/config-manager');
1021
+ await m.cmdConfigList();
1022
+ }
1023
+ async function cmdConfigShow(name: string): Promise<void> {
1024
+ const m = await import('../modules/utils/config-manager');
1025
+ await m.cmdConfigShow(name);
1026
+ }
1027
+ async function cmdConfigAdd(): Promise<void> {
1028
+ const m = await import('../modules/utils/config-manager');
1029
+ await m.cmdConfigAdd();
1030
+ }
1031
+ async function cmdConfigRemove(name: string): Promise<void> {
1032
+ const m = await import('../modules/utils/config-manager');
1033
+ await m.cmdConfigRemove(name);
1034
+ }
1035
+ async function cmdConfigModify(name: string): Promise<void> {
1036
+ const m = await import('../modules/utils/config-manager');
1037
+ await m.cmdConfigModify(name);
1038
+ }
1039
+
1040
+
1041
+ // ================================================================
1042
+ // doctor — 配置诊断与自动修复
1043
+ // ================================================================
1044
+ async function cmdDoctor(): Promise<void> {
1045
+ console.log(`\n🔧 imtoagent Doctor — Configuration Diagnosis\n`);
1046
+
1047
+ try {
1048
+ const { runDoctorChecks, formatIssues } = await import('../modules/utils/doctor');
1049
+ const issues = await runDoctorChecks();
1050
+
1051
+ // 分组
1052
+ const fixableIssues = issues.filter(i => i.fixable);
1053
+ const unfixableIssues = issues.filter(i => !i.fixable);
1054
+ const errors = issues.filter(i => i.severity === 'error');
1055
+ const warnings = issues.filter(i => i.severity === 'warning');
1056
+ const infos = issues.filter(i => i.severity === 'info');
1057
+
1058
+ // 打印所有问题
1059
+ if (issues.length === 0) {
1060
+ console.log(' ✅ All checks passed! Nothing to fix.\n');
1061
+ return;
1062
+ }
1063
+
1064
+ // 打印 errors
1065
+ if (errors.length > 0) {
1066
+ console.log('❌ Errors:');
1067
+ for (const e of errors) {
1068
+ console.log(` ${e.message}`);
1069
+ if (e.fixable && e.fixDescription) {
1070
+ console.log(` → 🔧 ${e.fixDescription}`);
1071
+ }
1072
+ }
1073
+ console.log();
1074
+ }
1075
+
1076
+ // 打印 warnings
1077
+ if (warnings.length > 0) {
1078
+ console.log('⚠️ Warnings:');
1079
+ for (const w of warnings) {
1080
+ console.log(` ${w.message}`);
1081
+ }
1082
+ console.log();
1083
+ }
1084
+
1085
+ // 打印 infos
1086
+ if (infos.length > 0) {
1087
+ console.log('✅ OK:');
1088
+ for (const i of infos) {
1089
+ console.log(` ${i.message}`);
1090
+ }
1091
+ console.log();
1092
+ }
1093
+
1094
+ // 尝试自动修复
1095
+ if (fixableIssues.length > 0) {
1096
+ console.log(`─── Auto-Fix ───`);
1097
+ let fixed = 0;
1098
+ for (const issue of fixableIssues) {
1099
+ if (!issue.fix) continue;
1100
+ try {
1101
+ console.log(`\n🔧 Fixing: ${issue.fixDescription}`);
1102
+ const success = await issue.fix();
1103
+ if (success) {
1104
+ console.log(` ✅ Fixed`);
1105
+ fixed++;
1106
+ } else {
1107
+ console.log(` ❌ Fix failed`);
1108
+ }
1109
+ } catch (e: any) {
1110
+ console.log(` ❌ Fix failed: ${e.message}`);
1111
+ }
1112
+ }
1113
+ console.log(`\n ${fixed}/${fixableIssues.length} issues fixed`);
1114
+ if (fixed > 0) {
1115
+ console.log(`\n💡 Run "imtoagent doctor" again to re-check after fixes.\n`);
1116
+ }
1117
+ }
1118
+
1119
+ // Summary
1120
+ console.log('── Summary ──');
1121
+ if (errors.length === 0) {
1122
+ console.log(' ✅ No errors found');
1123
+ } else {
1124
+ console.log(` ❌ ${errors.length} error(s) — some may be fixable`);
1125
+ }
1126
+ if (warnings.length > 0) {
1127
+ console.log(` ⚠️ ${warnings.length} warning(s) — review recommended`);
1128
+ }
1129
+ console.log();
1130
+
1131
+ } catch (e: any) {
1132
+ console.error(`❌ Doctor check failed: ${e.message}\n`);
1133
+ process.exit(1);
1134
+ }
1135
+ }
942
1136
  // ================================================================
943
1137
  // autostart — launchd integration (macOS only)
944
1138
  // ================================================================
@@ -1084,6 +1278,185 @@ async function cmdAutostartStatus(plistPath: string): Promise<void> {
1084
1278
  console.log();
1085
1279
  }
1086
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
+
1087
1460
  // ================================================================
1088
1461
  // version-check — non-blocking npm registry check
1089
1462
  // ================================================================
package/index.ts CHANGED
@@ -289,6 +289,7 @@ interface BotConfig {
289
289
  appSecret: string;
290
290
  backend: 'claude' | 'codex' | 'opencode';
291
291
  cwd?: string;
292
+ isAdmin?: boolean; // true = 可以修改网关配置,默认第一个 Bot 为 true
292
293
  }
293
294
 
294
295
  // ================================================================
@@ -307,6 +308,7 @@ class Bot {
307
308
  soul: string;
308
309
  client: Lark.Client;
309
310
  im: IMModule;
311
+ isAdmin: boolean;
310
312
  config: any;
311
313
  workspaceManager: WorkspaceManager;
312
314
 
@@ -327,6 +329,7 @@ class Bot {
327
329
  this.appSecret = cfg.appSecret;
328
330
  this.defaultCwd = cfg.cwd || globalConfig.system?.defaultProjectDir || path.join(os.homedir(), 'Projects');
329
331
  this.config = globalConfig;
332
+ this.isAdmin = cfg.isAdmin !== undefined ? cfg.isAdmin : true; // 默认 true(后向兼容老用户)
330
333
  this.workspaceManager = workspaceManager;
331
334
 
332
335
  // 确保工作空间目录存在
@@ -412,8 +415,13 @@ class Bot {
412
415
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
413
416
  const hasFiles = fs.readdirSync(dir).some((f: string) => f.endsWith('.md'));
414
417
  if (hasFiles) return;
418
+ // 根据 isAdmin 生成不同的 rules.md
419
+ const rulesMd = this.isAdmin
420
+ ? '# Bot Rules\n\nYou are an **admin Bot** with full gateway management privileges.\n\n- You can modify IMtoAgent configuration (config.json, providers, Bot management)\n- You have access to any directory (global workspace mode)\n- Sensitive information such as project keys, tokens, and passwords must not be leaked\n- Destructive commands must not be executed without explicit confirmation'
421
+ : '# Bot Rules\n\nYou are a **non-admin Bot** with restricted privileges.\n\n- ⛔ NEVER modify any files under ~/.imtoagent/ (config.json, providers.json, etc.)\n- ⛔ NEVER read or expose gateway configuration contents\n- ⛔ NEVER attempt to add, remove, or modify Bot configurations\n- Your working directory is limited to your assigned workspace\n- All file operations must stay within your workspace boundary\n- Sensitive information such as project keys, tokens, and passwords must not be leaked\n- Destructive commands must not be executed without explicit confirmation';
422
+
415
423
  const defaults: Record<string, string> = {
416
- 'rules.md': '# Hard Constraint Rules\n\nThe following rules cannot be overridden or modified:\n\n- Sensitive information such as project keys, tokens, and passwords must not be leaked\n- Destructive commands must not be executed',
424
+ 'rules.md': rulesMd,
417
425
  'identity.md': `# Identity\n\n- I am an AI programming assistant connected via IMtoAgent\n- I run on the ${this.backend === 'codex' ? 'Codex' : 'Claude Code'} backend\n- Reply in Chinese`,
418
426
  'profile.md': '# User Profile\n\nThis file can be modified by the Agent. When the user says "remember xxx" or "I prefer xxx", the Agent should update this file.\n\n## Modification Guide (Agent Only)\n\nRead this file → Add/delete/modify entries based on user requests → Save',
419
427
  'workspace.md': '# Project Environment\n\nAuto-generated by IMtoAgent.',
@@ -437,11 +445,30 @@ class Bot {
437
445
  for (const file of order) {
438
446
  const fp = dir + '/' + file;
439
447
  if (fs.existsSync(fp)) {
440
- const content = fs.readFileSync(fp, 'utf-8').trim();
441
- if (content) parts.push(content);
448
+ const c = fs.readFileSync(fp, 'utf-8').trim();
449
+ if (c) parts.push(c);
442
450
  }
443
451
  }
444
452
  } catch {}
453
+
454
+ // Inject config CLI reference — so the Agent knows how to manage Bots via natural language
455
+ const adminTag = this.isAdmin ? '' : ' (you are a non-admin Bot — you cannot use these commands, tell the user to use an admin Bot)';
456
+ const bt = String.fromCharCode(96); // backtick
457
+ const fence = bt + bt + bt;
458
+ const cliRef = '## IMtoAgent Config CLI' +
459
+ '\n\nYou can manage Bot configurations via these CLI commands:' + adminTag +
460
+ '\n\n' + fence + 'bash' +
461
+ '\nimtoagent config list # List all Bots' +
462
+ '\nimtoagent config show <BotName> # Show a Bot\'s details' +
463
+ '\nimtoagent config add # Add a new Bot (interactive)' +
464
+ '\nimtoagent config remove <BotName> # Remove a Bot' +
465
+ '\nimtoagent config modify <BotName> # Modify a Bot\'s settings' +
466
+ '\nimtoagent restore # Hot-reload after config changes' +
467
+ '\nimtoagent doctor # Diagnose config issues' +
468
+ '\n' + fence +
469
+ '\n\nWhen the user asks to manage Bots, use these commands. After changes, run ' + bt + 'imtoagent restore' + bt + '.';
470
+ parts.push(cliRef);
471
+
445
472
  return parts.join('\n\n');
446
473
  }
447
474
 
@@ -499,7 +526,7 @@ class Bot {
499
526
  : `⏸ ${this.backend} idle | ${this.activeModel}`);
500
527
 
501
528
  cmd('/info', ({ session }) =>
502
- `🤖 ${this.name} (${this.backend})\nModel: ${this.activeModel}\nDirectory: ${session?.cwd || this.defaultCwd}\nSessions: ${this.sessions.size}`);
529
+ `🤖 ${this.name} (${this.backend})${this.isAdmin ? ' ⭐' : ''}\nModel: ${this.activeModel}\nDirectory: ${session?.cwd || this.defaultCwd}\nSessions: ${this.sessions.size}`);
503
530
 
504
531
  cmd('/stats', ({ session }) => {
505
532
  if (!session || session.stats.calls === 0) return '📊 No calls yet';
@@ -461,6 +461,7 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
461
461
  name: botName,
462
462
  backend,
463
463
  cwd: cwd || os.homedir(),
464
+ isAdmin: bots.length === 0, // 第一个 Bot 为 admin,后续为 false
464
465
  };
465
466
 
466
467
  // Feishu needs appId + appSecret
@@ -242,6 +242,7 @@ export interface BotConfig {
242
242
  /** 唯一标识(UUID,用于目录/文件隔离,改名不影响) */
243
243
  id?: string;
244
244
  name: string;
245
+ im: string; // 'feishu' | 'telegram' | 'wecom' | 'wechat'
245
246
  backend: string; // 'claude' | 'codex' | 'opencode'
246
247
  appId: string;
247
248
  appSecret: string;