imtoagent 0.3.23 → 0.3.25

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.
@@ -14,6 +14,7 @@
14
14
 
15
15
  import * as fs from 'fs';
16
16
  import * as path from 'path';
17
+ import * as readline from 'readline';
17
18
  import { spawn, execSync } from 'child_process';
18
19
  import { getDataDir } from '../modules/utils/paths';
19
20
 
@@ -54,6 +55,21 @@ switch (command) {
54
55
  await cmdUpdateBackend(backendType);
55
56
  break;
56
57
  }
58
+ case 'uninstall':
59
+ await cmdUninstall();
60
+ break;
61
+ case 'health':
62
+ await cmdHealth();
63
+ break;
64
+ case 'doctor':
65
+ await cmdDoctor();
66
+ break;
67
+ case 'config':
68
+ await cmdConfig();
69
+ break;
70
+ case 'autostart':
71
+ await cmdAutostart();
72
+ break;
57
73
  case undefined: {
58
74
  const dataDir = getDataDir();
59
75
  const configPath = path.join(dataDir, 'config.json');
@@ -108,6 +124,19 @@ Usage:
108
124
  imtoagent update-system Upgrade imtoagent itself
109
125
  imtoagent update-backend Upgrade current Bot's backend
110
126
  imtoagent update-backend TYPE Upgrade specific backend (codex|claude|opencode)
127
+ imtoagent uninstall Uninstall imtoagent (keep data by default)
128
+ imtoagent uninstall --purge Uninstall and delete all data
129
+ imtoagent health Run comprehensive health check
130
+ imtoagent doctor Diagnose & fix configuration issues
131
+ imtoagent config Manage Bot configuration
132
+ imtoagent config list List all Bots
133
+ imtoagent config show NAME Show Bot details
134
+ imtoagent config add Add a new Bot
135
+ imtoagent config remove NAME Remove a Bot
136
+ imtoagent config modify NAME Modify Bot settings
137
+ imtoagent autostart enable Enable auto-start on login (launchd)
138
+ imtoagent autostart disable Disable auto-start
139
+ imtoagent autostart status Check auto-start status
111
140
 
112
141
  Data directory: ${getDataDir()}
113
142
  `);
@@ -223,6 +252,9 @@ echo $PID`;
223
252
  process.exit(1);
224
253
  }
225
254
 
255
+ // Non-blocking version check
256
+ checkForUpdates().then(printUpdateHint).catch(() => {});
257
+
226
258
  // Explicitly exit — Bun may keep event loop alive due to inherited stdio
227
259
  process.exit(0);
228
260
  }
@@ -616,3 +648,705 @@ async function cmdUpdateBackend(backendType?: 'claude' | 'codex' | 'opencode'):
616
648
  process.exit(1);
617
649
  }
618
650
  }
651
+
652
+ // ================================================================
653
+ // uninstall — remove imtoagent
654
+ // ================================================================
655
+ async function cmdUninstall(): Promise<void> {
656
+ const dataDir = getDataDir();
657
+ const purge = process.argv.includes('--purge');
658
+ const force = process.argv.includes('--force') || process.argv.includes('-f');
659
+
660
+ console.log('\n🗑️ imtoagent Uninstall');
661
+ console.log(` Data directory: ${dataDir}`);
662
+
663
+ if (!purge && !force) {
664
+ console.log('\n ⚠️ This will uninstall the imtoagent npm package but KEEP your data.');
665
+ console.log(` Your data in ${dataDir} will be preserved.`);
666
+ console.log('');
667
+ console.log(' To also delete all data, use: imtoagent uninstall --purge');
668
+ console.log('');
669
+ console.log(' Continue? [y/N]');
670
+ } else if (purge && !force) {
671
+ console.log('\n 🔴 This will DELETE all imtoagent data and the npm package.');
672
+ console.log(` The following will be permanently removed:`);
673
+ console.log(` - Data directory: ${dataDir}`);
674
+ console.log(' - npm global package: imtoagent');
675
+ console.log('');
676
+ console.log(' Continue? [y/N]');
677
+ } else {
678
+ console.log(' --force: skipping confirmation\n');
679
+ }
680
+
681
+ // Confirmation
682
+ if (!force) {
683
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
684
+ const answer = await new Promise<string>((resolve) => {
685
+ rl.question(' > ', resolve);
686
+ }).finally(() => rl.close());
687
+ if (answer.trim().toLowerCase() !== 'y' && answer.trim().toLowerCase() !== 'yes') {
688
+ console.log('❌ Uninstall cancelled.');
689
+ return;
690
+ }
691
+ }
692
+
693
+ // Step 1: Stop gateway if running
694
+ if (fs.existsSync(PID_FILE)) {
695
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
696
+ try {
697
+ process.kill(pid, 0);
698
+ console.log(`⏹ Stopping gateway (PID=${pid})...`);
699
+ process.kill(pid, 'SIGTERM');
700
+ for (let i = 0; i < 10; i++) {
701
+ try { process.kill(pid, 0); await new Promise(r => setTimeout(r, 500)); } catch { break; }
702
+ }
703
+ try { process.kill(pid, 0); process.kill(pid, 'SIGKILL'); } catch {}
704
+ console.log('✅ Gateway stopped');
705
+ } catch {}
706
+ try { fs.unlinkSync(PID_FILE); } catch {}
707
+ } else {
708
+ console.log('ℹ️ Gateway is not running');
709
+ }
710
+
711
+ // Step 2: Remove launchd plist
712
+ const plistPath = path.join(process.env.HOME || '', 'Library', 'LaunchAgents', 'com.imtoagent.gateway.plist');
713
+ if (fs.existsSync(plistPath)) {
714
+ try {
715
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' });
716
+ console.log('⏹ Launchd service unloaded');
717
+ } catch {}
718
+ try {
719
+ fs.unlinkSync(plistPath);
720
+ console.log('🗑️ Launchd plist removed');
721
+ } catch {}
722
+ } else {
723
+ console.log('ℹ️ No launchd service configured');
724
+ }
725
+
726
+ // Step 3: Uninstall npm package
727
+ console.log('\n📦 Uninstalling npm package...');
728
+ try {
729
+ execSync('npm uninstall -g imtoagent', {
730
+ encoding: 'utf-8',
731
+ stdio: 'inherit',
732
+ timeout: 30000,
733
+ });
734
+ console.log('✅ npm package uninstalled');
735
+ } catch (e: any) {
736
+ console.error(`⚠️ npm uninstall may have failed: ${e.message}`);
737
+ }
738
+
739
+ // Step 4: Purge data if requested
740
+ if (purge) {
741
+ console.log(`\n🔥 Deleting data directory: ${dataDir}`);
742
+ try {
743
+ execSync(`rm -rf "${dataDir}"`, { encoding: 'utf-8', timeout: 10000 });
744
+ console.log('✅ Data directory deleted');
745
+ } catch (e: any) {
746
+ console.error(`⚠️ Failed to delete data directory: ${e.message}`);
747
+ console.error(` Please remove manually: rm -rf "${dataDir}"`);
748
+ }
749
+ } else {
750
+ console.log(`\n💾 Data preserved at: ${dataDir}`);
751
+ console.log(' To remove it later: rm -rf ~/.imtoagent');
752
+ }
753
+
754
+ console.log('\n✅ imtoagent uninstalled');
755
+ }
756
+
757
+ // ================================================================
758
+ // health — comprehensive health check
759
+ // ================================================================
760
+ async function cmdHealth(): Promise<void> {
761
+ const dataDir = getDataDir();
762
+ const configPath = path.join(dataDir, 'config.json');
763
+ const logFile = path.join(dataDir, 'logs', 'imtoagent.log');
764
+
765
+ console.log(`\n🔍 imtoagent Health Check`);
766
+ console.log(` Data directory: ${dataDir}`);
767
+ console.log(` Time: ${new Date().toISOString()}\n`);
768
+
769
+ let errorCount = 0;
770
+ let warnCount = 0;
771
+
772
+ // ---- 1. Gateway Process ----
773
+ console.log('── Gateway Process ──');
774
+ if (fs.existsSync(PID_FILE)) {
775
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
776
+ try {
777
+ process.kill(pid, 0);
778
+ console.log(` ✅ Running (PID=${pid})`);
779
+ } catch {
780
+ console.log(` ❌ PID file exists but process ${pid} is gone (stale)`);
781
+ errorCount++;
782
+ }
783
+ } else {
784
+ console.log(` ⏸ Not running`);
785
+ }
786
+
787
+ // ---- 2. Configuration ----
788
+ console.log('\n── Configuration ──');
789
+ if (fs.existsSync(configPath)) {
790
+ try {
791
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
792
+ console.log(` ✅ config.json parse OK`);
793
+
794
+ // Check required fields
795
+ if (!cfg.bots || !Array.isArray(cfg.bots) || cfg.bots.length === 0) {
796
+ console.log(` ⚠️ No bots configured`);
797
+ warnCount++;
798
+ } else {
799
+ console.log(` ✅ ${cfg.bots.length} Bot(s) configured`);
800
+ for (const bot of cfg.bots) {
801
+ const issues: string[] = [];
802
+ if (!bot.name) issues.push('missing name');
803
+ if (!bot.im) issues.push('missing IM platform');
804
+ if (!bot.backend) issues.push('missing backend');
805
+ if (issues.length > 0) {
806
+ console.log(` ⚠️ Bot "${bot.name || '?'}": ${issues.join(', ')}`);
807
+ warnCount++;
808
+ } else {
809
+ console.log(` ✅ ${bot.name} (${bot.im} + ${bot.backend})`);
810
+ }
811
+ }
812
+ }
813
+
814
+ // Check providers.json
815
+ const providersPath = path.join(dataDir, 'providers.json');
816
+ if (fs.existsSync(providersPath)) {
817
+ try {
818
+ const prov = JSON.parse(fs.readFileSync(providersPath, 'utf-8'));
819
+ console.log(` ✅ providers.json parse OK`);
820
+ // Check for placeholder API keys
821
+ const provStr = JSON.stringify(prov);
822
+ if (provStr.includes('YOUR_') || provStr.includes('sk-xxx') || provStr.includes('placehold')) {
823
+ console.log(` ⚠️ providers.json may contain placeholder API keys`);
824
+ warnCount++;
825
+ }
826
+ } catch {
827
+ console.log(` ❌ providers.json parse error`);
828
+ errorCount++;
829
+ }
830
+ } else {
831
+ console.log(` ⚠️ providers.json not found`);
832
+ warnCount++;
833
+ }
834
+ } catch {
835
+ console.log(` ❌ config.json parse error`);
836
+ errorCount++;
837
+ }
838
+ } else {
839
+ console.log(` ❌ config.json not found (run "imtoagent setup")`);
840
+ errorCount++;
841
+ }
842
+
843
+ // ---- 3. Backend Status ----
844
+ console.log('\n── Backend Status ──');
845
+ try {
846
+ const { checkBackend } = await import('../modules/utils/backend-check');
847
+ if (fs.existsSync(configPath)) {
848
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
849
+ const checkedTypes = new Set<string>();
850
+ for (const bot of cfg.bots || []) {
851
+ if (bot.backend && ['claude', 'codex', 'opencode'].includes(bot.backend) && !checkedTypes.has(bot.backend)) {
852
+ checkedTypes.add(bot.backend);
853
+ const info = checkBackend(bot.backend as any);
854
+ if (info.installed) {
855
+ console.log(` ✅ ${info.label} v${info.version} (${info.installSource})`);
856
+ } else {
857
+ console.log(` ❌ ${info.label} not installed → ${info.installHint}`);
858
+ errorCount++;
859
+ }
860
+ }
861
+ }
862
+ if (checkedTypes.size === 0) {
863
+ console.log(` ℹ️ No backends to check`);
864
+ }
865
+ }
866
+ } catch {
867
+ console.log(` ⚠️ Backend check skipped`);
868
+ warnCount++;
869
+ }
870
+
871
+ // ---- 4. Proxy Port ----
872
+ console.log('\n── Proxy Port (:18899) ──');
873
+ try {
874
+ const net = await import('net');
875
+ const checkPort = (port: number): Promise<boolean> => {
876
+ return new Promise((resolve) => {
877
+ const socket = new net.Socket();
878
+ socket.setTimeout(2000);
879
+ socket.on('connect', () => { socket.destroy(); resolve(true); });
880
+ socket.on('error', () => resolve(false));
881
+ socket.on('timeout', () => { socket.destroy(); resolve(false); });
882
+ socket.connect(port, '127.0.0.1');
883
+ });
884
+ };
885
+ const reachable = await checkPort(18899);
886
+ if (reachable) {
887
+ console.log(` ✅ Port 18899 is reachable`);
888
+ } else {
889
+ // Check if port is in use by another process
890
+ try {
891
+ const lsofOut = execSync(`lsof -i :18899 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 }).trim();
892
+ if (lsofOut) {
893
+ console.log(` ❌ Port 18899 is occupied by another process:`);
894
+ lsofOut.split('\n').forEach(line => console.log(` ${line}`));
895
+ } else {
896
+ console.log(` ⏸ Port 18899 is free (gateway not running)`);
897
+ }
898
+ } catch {
899
+ console.log(` ⏸ Port 18899 is free (gateway not running)`);
900
+ }
901
+ }
902
+ } catch {
903
+ console.log(` ⚠️ Port check failed`);
904
+ warnCount++;
905
+ }
906
+
907
+ // ---- 5. Recent Log Errors ----
908
+ console.log('\n── Recent Log Errors ──');
909
+ if (fs.existsSync(logFile)) {
910
+ const stats = fs.statSync(logFile);
911
+ const size = stats.size > 1024 * 1024
912
+ ? (stats.size / (1024 * 1024)).toFixed(1) + ' MB'
913
+ : (stats.size / 1024).toFixed(1) + ' KB';
914
+ console.log(` Log: ${size}`);
915
+
916
+ // Read last 50 lines looking for ERROR/WARN
917
+ try {
918
+ const content = fs.readFileSync(logFile, 'utf-8');
919
+ const lines = content.split('\n');
920
+ const recent = lines.slice(-50);
921
+ const errors = recent.filter(l => l.includes('ERROR') || l.includes('[error]'));
922
+ const warns = recent.filter(l => l.includes('WARN') || l.includes('[warn]'));
923
+
924
+ if (errors.length > 0) {
925
+ console.log(` ❌ ${errors.length} recent ERROR(s):`);
926
+ for (const line of errors.slice(-3)) {
927
+ console.log(` ${line.trim().slice(0, 120)}`);
928
+ }
929
+ errorCount++;
930
+ } else {
931
+ console.log(` ✅ No recent errors`);
932
+ }
933
+ if (warns.length > 0 && errors.length === 0) {
934
+ console.log(` ℹ️ ${warns.length} recent warning(s) (non-critical)`);
935
+ }
936
+ } catch {
937
+ console.log(` ⚠️ Could not read log file`);
938
+ warnCount++;
939
+ }
940
+ } else {
941
+ console.log(` ℹ️ No log file yet`);
942
+ }
943
+
944
+ // ---- Summary ----
945
+ console.log('\n── Summary ──');
946
+ if (errorCount === 0 && warnCount === 0) {
947
+ console.log(` ✅ All checks passed!`);
948
+ } else {
949
+ if (errorCount > 0) console.log(` ❌ ${errorCount} error(s)`);
950
+ if (warnCount > 0) console.log(` ⚠️ ${warnCount} warning(s)`);
951
+ }
952
+ console.log();
953
+ }
954
+
955
+ // ================================================================
956
+ // config — Bot 配置管理
957
+ // ================================================================
958
+ async function cmdConfig(): Promise<void> {
959
+ const subcommand = process.argv[3];
960
+
961
+ switch (subcommand) {
962
+ case 'list':
963
+ await cmdConfigList();
964
+ break;
965
+ case 'show': {
966
+ const name = process.argv[4];
967
+ if (!name) { console.error('Usage: imtoagent config show <name>'); process.exit(1); }
968
+ await cmdConfigShow(name);
969
+ break;
970
+ }
971
+ case 'add':
972
+ await cmdConfigAdd();
973
+ break;
974
+ case 'remove': {
975
+ const name = process.argv[4];
976
+ if (!name) { console.error('Usage: imtoagent config remove <name>'); process.exit(1); }
977
+ await cmdConfigRemove(name);
978
+ break;
979
+ }
980
+ case 'modify': {
981
+ const name = process.argv[4];
982
+ if (!name) { console.error('Usage: imtoagent config modify <name>'); process.exit(1); }
983
+ await cmdConfigModify(name);
984
+ break;
985
+ }
986
+ case undefined:
987
+ case 'help':
988
+ case '--help':
989
+ case '-h':
990
+ console.log(`
991
+ imtoagent config — Manage Bot configuration
992
+
993
+ Usage:
994
+ imtoagent config list List all Bots
995
+ imtoagent config show NAME Show Bot details
996
+ imtoagent config add Add a new Bot (interactive)
997
+ imtoagent config remove NAME Remove a Bot
998
+ imtoagent config modify NAME Modify Bot settings
999
+ `);
1000
+ break;
1001
+ default:
1002
+ console.error(`❌ Unknown config subcommand: ${subcommand}`);
1003
+ console.log(' Run "imtoagent config help" for usage.');
1004
+ process.exit(1);
1005
+ }
1006
+ }
1007
+
1008
+ // ---- Config subcommand wrappers ----
1009
+ async function cmdConfigList(): Promise<void> {
1010
+ const m = await import('../modules/utils/config-manager');
1011
+ await m.cmdConfigList();
1012
+ }
1013
+ async function cmdConfigShow(name: string): Promise<void> {
1014
+ const m = await import('../modules/utils/config-manager');
1015
+ await m.cmdConfigShow(name);
1016
+ }
1017
+ async function cmdConfigAdd(): Promise<void> {
1018
+ const m = await import('../modules/utils/config-manager');
1019
+ await m.cmdConfigAdd();
1020
+ }
1021
+ async function cmdConfigRemove(name: string): Promise<void> {
1022
+ const m = await import('../modules/utils/config-manager');
1023
+ await m.cmdConfigRemove(name);
1024
+ }
1025
+ async function cmdConfigModify(name: string): Promise<void> {
1026
+ const m = await import('../modules/utils/config-manager');
1027
+ await m.cmdConfigModify(name);
1028
+ }
1029
+
1030
+
1031
+ // ================================================================
1032
+ // doctor — 配置诊断与自动修复
1033
+ // ================================================================
1034
+ async function cmdDoctor(): Promise<void> {
1035
+ console.log(`\n🔧 imtoagent Doctor — Configuration Diagnosis\n`);
1036
+
1037
+ try {
1038
+ const { runDoctorChecks, formatIssues } = await import('../modules/utils/doctor');
1039
+ const issues = await runDoctorChecks();
1040
+
1041
+ // 分组
1042
+ const fixableIssues = issues.filter(i => i.fixable);
1043
+ const unfixableIssues = issues.filter(i => !i.fixable);
1044
+ const errors = issues.filter(i => i.severity === 'error');
1045
+ const warnings = issues.filter(i => i.severity === 'warning');
1046
+ const infos = issues.filter(i => i.severity === 'info');
1047
+
1048
+ // 打印所有问题
1049
+ if (issues.length === 0) {
1050
+ console.log(' ✅ All checks passed! Nothing to fix.\n');
1051
+ return;
1052
+ }
1053
+
1054
+ // 打印 errors
1055
+ if (errors.length > 0) {
1056
+ console.log('❌ Errors:');
1057
+ for (const e of errors) {
1058
+ console.log(` ${e.message}`);
1059
+ if (e.fixable && e.fixDescription) {
1060
+ console.log(` → 🔧 ${e.fixDescription}`);
1061
+ }
1062
+ }
1063
+ console.log();
1064
+ }
1065
+
1066
+ // 打印 warnings
1067
+ if (warnings.length > 0) {
1068
+ console.log('⚠️ Warnings:');
1069
+ for (const w of warnings) {
1070
+ console.log(` ${w.message}`);
1071
+ }
1072
+ console.log();
1073
+ }
1074
+
1075
+ // 打印 infos
1076
+ if (infos.length > 0) {
1077
+ console.log('✅ OK:');
1078
+ for (const i of infos) {
1079
+ console.log(` ${i.message}`);
1080
+ }
1081
+ console.log();
1082
+ }
1083
+
1084
+ // 尝试自动修复
1085
+ if (fixableIssues.length > 0) {
1086
+ console.log(`─── Auto-Fix ───`);
1087
+ let fixed = 0;
1088
+ for (const issue of fixableIssues) {
1089
+ if (!issue.fix) continue;
1090
+ try {
1091
+ console.log(`\n🔧 Fixing: ${issue.fixDescription}`);
1092
+ const success = await issue.fix();
1093
+ if (success) {
1094
+ console.log(` ✅ Fixed`);
1095
+ fixed++;
1096
+ } else {
1097
+ console.log(` ❌ Fix failed`);
1098
+ }
1099
+ } catch (e: any) {
1100
+ console.log(` ❌ Fix failed: ${e.message}`);
1101
+ }
1102
+ }
1103
+ console.log(`\n ${fixed}/${fixableIssues.length} issues fixed`);
1104
+ if (fixed > 0) {
1105
+ console.log(`\n💡 Run "imtoagent doctor" again to re-check after fixes.\n`);
1106
+ }
1107
+ }
1108
+
1109
+ // Summary
1110
+ console.log('── Summary ──');
1111
+ if (errors.length === 0) {
1112
+ console.log(' ✅ No errors found');
1113
+ } else {
1114
+ console.log(` ❌ ${errors.length} error(s) — some may be fixable`);
1115
+ }
1116
+ if (warnings.length > 0) {
1117
+ console.log(` ⚠️ ${warnings.length} warning(s) — review recommended`);
1118
+ }
1119
+ console.log();
1120
+
1121
+ } catch (e: any) {
1122
+ console.error(`❌ Doctor check failed: ${e.message}\n`);
1123
+ process.exit(1);
1124
+ }
1125
+ }
1126
+ // ================================================================
1127
+ // autostart — launchd integration (macOS only)
1128
+ // ================================================================
1129
+ async function cmdAutostart(): Promise<void> {
1130
+ const subcommand = process.argv[3];
1131
+ const home = process.env.HOME || '';
1132
+ const plistDir = path.join(home, 'Library', 'LaunchAgents');
1133
+ const plistPath = path.join(plistDir, 'com.imtoagent.gateway.plist');
1134
+
1135
+ if (process.platform !== 'darwin') {
1136
+ console.error('❌ autostart is only supported on macOS (launchd)');
1137
+ process.exit(1);
1138
+ }
1139
+
1140
+ switch (subcommand) {
1141
+ case 'enable':
1142
+ await cmdAutostartEnable(plistPath, plistDir, home);
1143
+ break;
1144
+ case 'disable':
1145
+ await cmdAutostartDisable(plistPath);
1146
+ break;
1147
+ case 'status':
1148
+ await cmdAutostartStatus(plistPath);
1149
+ break;
1150
+ default:
1151
+ console.log('Usage:');
1152
+ console.log(' imtoagent autostart enable Enable auto-start on login');
1153
+ console.log(' imtoagent autostart disable Disable auto-start');
1154
+ console.log(' imtoagent autostart status Check auto-start status');
1155
+ }
1156
+ }
1157
+
1158
+ async function cmdAutostartEnable(plistPath: string, plistDir: string, home: string): Promise<void> {
1159
+ const dataDir = getDataDir();
1160
+ const binPath = execSync('command -v imtoagent', { encoding: 'utf-8', timeout: 3000 }).trim();
1161
+
1162
+ if (!binPath) {
1163
+ console.error('❌ Cannot find imtoagent binary');
1164
+ process.exit(1);
1165
+ }
1166
+
1167
+ // Find node and bun paths for launchd (which has minimal PATH)
1168
+ const nodePath = execSync('which node', { encoding: 'utf-8', timeout: 3000 }).trim();
1169
+ const bunPath = execSync('which bun', { encoding: 'utf-8', timeout: 3000 }).trim();
1170
+ const launchdPATH = `${path.dirname(nodePath)}:${path.dirname(bunPath)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin`;
1171
+
1172
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
1173
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1174
+ <plist version="1.0">
1175
+ <dict>
1176
+ <key>Label</key>
1177
+ <string>com.imtoagent.gateway</string>
1178
+ <key>ProgramArguments</key>
1179
+ <array>
1180
+ <string>${binPath}</string>
1181
+ <string>daemon</string>
1182
+ </array>
1183
+ <key>WorkingDirectory</key>
1184
+ <string>${dataDir}</string>
1185
+ <key>EnvironmentVariables</key>
1186
+ <dict>
1187
+ <key>IMTOAGENT_HOME</key>
1188
+ <string>${dataDir}</string>
1189
+ <key>PATH</key>
1190
+ <string>${launchdPATH}</string>
1191
+ </dict>
1192
+ <key>StandardOutPath</key>
1193
+ <string>${path.join(dataDir, 'logs', 'launchd.log')}</string>
1194
+ <key>StandardErrorPath</key>
1195
+ <string>${path.join(dataDir, 'logs', 'launchd-error.log')}</string>
1196
+ <key>RunAtLoad</key>
1197
+ <true/>
1198
+ <key>KeepAlive</key>
1199
+ <dict>
1200
+ <key>SuccessfulExit</key>
1201
+ <false/>
1202
+ </dict>
1203
+ <key>ThrottleInterval</key>
1204
+ <integer>30</integer>
1205
+ <key>ProcessType</key>
1206
+ <string>Background</string>
1207
+ </dict>
1208
+ </plist>
1209
+ `;
1210
+
1211
+ fs.mkdirSync(plistDir, { recursive: true });
1212
+ fs.writeFileSync(plistPath, plist);
1213
+ console.log(`✅ Plist written: ${plistPath}`);
1214
+
1215
+ // Unload existing (if any) then reload
1216
+ try { execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8' }); } catch {}
1217
+
1218
+ try {
1219
+ execSync(`launchctl load "${plistPath}"`, { encoding: 'utf-8', timeout: 5000 });
1220
+ console.log('✅ launchd service loaded');
1221
+ } catch (e: any) {
1222
+ console.error(`⚠️ launchctl load failed: ${e.message}`);
1223
+ console.error(' Plist written. Manual: launchctl load ' + plistPath);
1224
+ }
1225
+
1226
+ console.log(`\n📌 Auto-start enabled. Gateway starts on login.`);
1227
+ console.log(` Logs: ${path.join(dataDir, 'logs', 'launchd.log')}`);
1228
+ }
1229
+
1230
+ async function cmdAutostartDisable(plistPath: string): Promise<void> {
1231
+ if (!fs.existsSync(plistPath)) {
1232
+ console.log('ℹ️ No autostart configured');
1233
+ return;
1234
+ }
1235
+
1236
+ try {
1237
+ execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { encoding: 'utf-8', timeout: 5000 });
1238
+ console.log('✅ launchd service unloaded');
1239
+ } catch {
1240
+ console.log('ℹ️ Service was not loaded');
1241
+ }
1242
+
1243
+ try {
1244
+ fs.unlinkSync(plistPath);
1245
+ console.log('✅ Plist removed');
1246
+ } catch (e: any) {
1247
+ console.error(`⚠️ Failed to remove plist: ${e.message}`);
1248
+ }
1249
+
1250
+ console.log('\n📌 Auto-start disabled');
1251
+ }
1252
+
1253
+ async function cmdAutostartStatus(plistPath: string): Promise<void> {
1254
+ const exists = fs.existsSync(plistPath);
1255
+ console.log(`\n📌 Autostart Status`);
1256
+ console.log(` Plist file: ${plistPath}`);
1257
+ console.log(` Installed: ${exists ? '✅ Yes' : '❌ No'}`);
1258
+
1259
+ if (exists) {
1260
+ try {
1261
+ const out = execSync(`launchctl list | grep com.imtoagent`, { encoding: 'utf-8', timeout: 3000 }).trim();
1262
+ console.log(` Running: ${out ? '✅ Yes' : '❌ No (plist exists but not loaded)'}`);
1263
+ if (out) console.log(` ${out}`);
1264
+ } catch {
1265
+ console.log(` Running: ❌ No`);
1266
+ }
1267
+ }
1268
+ console.log();
1269
+ }
1270
+
1271
+ // ================================================================
1272
+ // version-check — non-blocking npm registry check
1273
+ // ================================================================
1274
+
1275
+ /**
1276
+ * Check if a newer version is available on npm.
1277
+ * Returns null if check fails or no update available.
1278
+ * Caches result for 24 hours.
1279
+ */
1280
+ async function checkForUpdates(): Promise<string | null> {
1281
+ const dataDir = getDataDir();
1282
+ const cacheFile = path.join(dataDir, '.last-version-check');
1283
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
1284
+ const currentVer = pkg.version;
1285
+
1286
+ // Check cache (24h TTL)
1287
+ try {
1288
+ if (fs.existsSync(cacheFile)) {
1289
+ const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'));
1290
+ const ageMs = Date.now() - cached.timestamp;
1291
+ if (ageMs < 24 * 60 * 60 * 1000) {
1292
+ // Cache hit — return stored result
1293
+ if (cached.latest && cached.latest !== currentVer) {
1294
+ return cached.latest;
1295
+ }
1296
+ return null;
1297
+ }
1298
+ }
1299
+ } catch {}
1300
+
1301
+ // Fetch from npm registry (3s timeout, npmmirror for China)
1302
+ try {
1303
+ const http = await import('http');
1304
+ const https = await import('https');
1305
+
1306
+ const fetchJson = (url: string): Promise<any> => {
1307
+ return new Promise((resolve, reject) => {
1308
+ const client = url.startsWith('https') ? https : http;
1309
+ const req = client.get(url, { timeout: 3000 }, (res) => {
1310
+ let data = '';
1311
+ res.on('data', chunk => data += chunk);
1312
+ res.on('end', () => {
1313
+ try { resolve(JSON.parse(data)); } catch { reject(new Error('parse error')); }
1314
+ });
1315
+ });
1316
+ req.on('error', reject);
1317
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
1318
+ });
1319
+ };
1320
+
1321
+ // Try npmmirror first (China-friendly), fallback to npmjs
1322
+ let result: any;
1323
+ try {
1324
+ result = await fetchJson('https://registry.npmmirror.com/imtoagent');
1325
+ } catch {
1326
+ result = await fetchJson('https://registry.npmjs.org/imtoagent');
1327
+ }
1328
+
1329
+ const latest = result['dist-tags']?.latest;
1330
+ if (latest && latest !== currentVer) {
1331
+ // Cache the result
1332
+ fs.writeFileSync(cacheFile, JSON.stringify({ latest, timestamp: Date.now() }));
1333
+ return latest;
1334
+ }
1335
+
1336
+ // Cache "no update" result too
1337
+ fs.writeFileSync(cacheFile, JSON.stringify({ latest: currentVer, timestamp: Date.now() }));
1338
+ return null;
1339
+ } catch {
1340
+ // Silently fail — non-blocking
1341
+ return null;
1342
+ }
1343
+ }
1344
+
1345
+ /** Print version update hint if available */
1346
+ function printUpdateHint(latestVer: string | null): void {
1347
+ if (latestVer) {
1348
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
1349
+ console.log(`\n⬆️ New version available: ${pkg.version} → ${latestVer}`);
1350
+ console.log(` Upgrade: imtoagent update-system`);
1351
+ }
1352
+ }