imtoagent 0.3.24 → 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.
@@ -61,6 +61,12 @@ 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;
@@ -121,6 +127,13 @@ Usage:
121
127
  imtoagent uninstall Uninstall imtoagent (keep data by default)
122
128
  imtoagent uninstall --purge Uninstall and delete all data
123
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
124
137
  imtoagent autostart enable Enable auto-start on login (launchd)
125
138
  imtoagent autostart disable Disable auto-start
126
139
  imtoagent autostart status Check auto-start status
@@ -939,6 +952,177 @@ async function cmdHealth(): Promise<void> {
939
952
  console.log();
940
953
  }
941
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
+ }
942
1126
  // ================================================================
943
1127
  // autostart — launchd integration (macOS only)
944
1128
  // ================================================================
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;
@@ -0,0 +1,359 @@
1
+ // ================================================================
2
+ // config-manager.ts — 配置管理 CRUD
3
+ // ================================================================
4
+ // imtoagent config list — 列出所有 Bot
5
+ // imtoagent config show NAME — 显示某个 Bot 的完整配置
6
+ // imtoagent config add — 交互式添加 Bot
7
+ // imtoagent config remove NAME — 删除 Bot
8
+ // imtoagent config modify NAME — 修改 Bot 配置
9
+ // ================================================================
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import * as readline from 'readline';
14
+ import { getDataDir, getConfigPath, getProvidersPath } from './paths';
15
+ import crypto from 'crypto';
16
+
17
+ // ================================================================
18
+ // 类型
19
+ // ================================================================
20
+
21
+ const VALID_BACKENDS = ['claude', 'codex', 'opencode'] as const;
22
+ const VALID_IMS = ['feishu', 'telegram', 'wecom', 'wechat'] as const;
23
+
24
+ export type BackendType = typeof VALID_BACKENDS[number];
25
+ export type IMType = typeof VALID_IMS[number];
26
+
27
+ export interface BotEntry {
28
+ id?: string;
29
+ name: string;
30
+ im: string;
31
+ appId: string;
32
+ appSecret: string;
33
+ backend: string;
34
+ cwd?: string;
35
+ }
36
+
37
+ interface RawConfig {
38
+ system?: Record<string, any>;
39
+ providers?: Record<string, any>;
40
+ defaultModel?: string;
41
+ activeModel?: string;
42
+ modelAliases?: Record<string, string>;
43
+ bots?: BotEntry[];
44
+ execServer?: any;
45
+ codex?: any;
46
+ opencode?: any;
47
+ rateLimit?: any;
48
+ shutdown?: any;
49
+ [key: string]: any;
50
+ }
51
+
52
+ // ================================================================
53
+ // 加载/保存
54
+ // ================================================================
55
+
56
+ function loadConfig(): { config: RawConfig; configPath: string } {
57
+ const configPath = getConfigPath();
58
+ if (!fs.existsSync(configPath)) {
59
+ throw new Error(`config.json not found at ${configPath} — run "imtoagent setup" first`);
60
+ }
61
+ const raw = fs.readFileSync(configPath, 'utf-8');
62
+ const config = JSON.parse(raw) as RawConfig;
63
+ return { config, configPath };
64
+ }
65
+
66
+ function saveConfig(config: RawConfig, configPath: string): void {
67
+ const tmpPath = configPath + '.tmp';
68
+ try {
69
+ fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + '\n');
70
+ fs.renameSync(tmpPath, configPath);
71
+ } catch (e) {
72
+ // Clean up tmp on failure
73
+ try { fs.unlinkSync(tmpPath); } catch {}
74
+ throw e;
75
+ }
76
+ }
77
+
78
+ // ================================================================
79
+ // 交互式 prompt(复用 setup 的风格)
80
+ // ================================================================
81
+
82
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
83
+
84
+ function prompt(question: string): Promise<string> {
85
+ return new Promise((resolve) => {
86
+ rl.question(question, (answer) => resolve(answer.trim()));
87
+ });
88
+ }
89
+
90
+ async function confirm(question: string): Promise<boolean> {
91
+ const answer = await prompt(`${question} [y/N] `);
92
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
93
+ }
94
+
95
+ // ================================================================
96
+ // config list
97
+ // ================================================================
98
+
99
+ export async function cmdConfigList(): Promise<void> {
100
+ const { config } = loadConfig();
101
+ const bots = config.bots || [];
102
+
103
+ if (bots.length === 0) {
104
+ console.log(' No Bots configured. Run "imtoagent config add" to add one.');
105
+ return;
106
+ }
107
+
108
+ console.log(`\n📋 Configured Bots (${bots.length}):\n`);
109
+ for (const bot of bots) {
110
+ const botId = bot.id ? ` [${bot.id.slice(0, 8)}]` : '';
111
+ const adminTag = bot.isAdmin ? ' ⭐' : '';
112
+ const cwd = bot.cwd ? ` (cwd: ${bot.cwd})` : '';
113
+ console.log(` • ${bot.name}${adminTag}${botId}`);
114
+ console.log(` IM: ${bot.im || "(not set)"} | Backend: ${bot.backend}${cwd}`);
115
+ }
116
+ console.log();
117
+ }
118
+
119
+ // ================================================================
120
+ // config show NAME
121
+ // ================================================================
122
+
123
+ export async function cmdConfigShow(name: string): Promise<void> {
124
+ const { config } = loadConfig();
125
+ const bots = config.bots || [];
126
+ const bot = bots.find(b => b.name === name);
127
+
128
+ if (!bot) {
129
+ console.error(`❌ Bot "${name}" not found`);
130
+ process.exit(1);
131
+ }
132
+
133
+ console.log(`\n📋 Bot: ${bot.name}\n`);
134
+ console.log(` ID: ${bot.id || '(auto-generated)'}`);
135
+ console.log(` IM: ${bot.im || "(not set)"}`);
136
+ console.log(` Backend: ${bot.backend}`);
137
+ console.log(` App ID: ${maskSecret(bot.appId)}`);
138
+ console.log(` App Secret: ${maskSecret(bot.appSecret)}`);
139
+ console.log(` Admin: ${bot.isAdmin ? '✅ Yes' : '❌ No'}`);
140
+ if (bot.cwd) console.log(` CWD: ${bot.cwd}`);
141
+ console.log();
142
+ }
143
+
144
+ function maskSecret(s: string): string {
145
+ if (s.length <= 8) return '***';
146
+ return s.slice(0, 4) + '...' + s.slice(-4);
147
+ }
148
+
149
+ // ================================================================
150
+ // config add
151
+ // ================================================================
152
+
153
+ export async function cmdConfigAdd(): Promise<void> {
154
+ console.log('\n➕ Add a new Bot\n');
155
+
156
+ // 1. Bot name
157
+ let name: string;
158
+ while (true) {
159
+ name = await prompt('Bot name (e.g. MyAssistantBot): ');
160
+ if (!name) { console.log(' ⚠️ Name is required'); continue; }
161
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) { console.log(' ⚠️ Name can only contain letters, numbers, hyphens, underscores'); continue; }
162
+
163
+ const { config } = loadConfig();
164
+ if (config.bots?.find(b => b.name === name)) {
165
+ console.log(` ⚠️ Bot "${name}" already exists`);
166
+ continue;
167
+ }
168
+ break;
169
+ }
170
+
171
+ // 2. IM platform
172
+ console.log('\n IM platforms:');
173
+ console.log(' 1. feishu (飞书/Lark)');
174
+ console.log(' 2. telegram (Telegram)');
175
+ console.log(' 3. wecom (企业微信)');
176
+ console.log(' 4. wechat (个人微信)');
177
+ let im: string;
178
+ while (true) {
179
+ im = await prompt('\n IM platform (1-4 or name): ');
180
+ const numMap: Record<string, string> = { '1': 'feishu', '2': 'telegram', '3': 'wecom', '4': 'wechat' };
181
+ im = numMap[im] || im.toLowerCase();
182
+ if (VALID_IMS.includes(im as IMType)) break;
183
+ console.log(' ⚠️ Valid options: feishu, telegram, wecom, wechat');
184
+ }
185
+
186
+ // 3. IM credentials
187
+ let appId = await prompt(`\n App ID (飞书 App ID / Telegram Bot Token / etc): `);
188
+ if (!appId) { console.log(' ⚠️ App ID is required'); process.exit(1); }
189
+
190
+ let appSecret = await prompt(` App Secret (飞书 App Secret / leave blank for Telegram): `);
191
+
192
+ // 4. Backend
193
+ console.log('\n Backends:');
194
+ console.log(' 1. claude (Claude Code)');
195
+ console.log(' 2. codex (OpenAI Codex)');
196
+ console.log(' 3. opencode (OpenCode)');
197
+ let backend: string;
198
+ while (true) {
199
+ backend = await prompt('\n Backend (1-3 or name): ');
200
+ const numMap: Record<string, string> = { '1': 'claude', '2': 'codex', '3': 'opencode' };
201
+ backend = numMap[backend] || backend.toLowerCase();
202
+ if (VALID_BACKENDS.includes(backend as BackendType)) break;
203
+ console.log(' ⚠️ Valid options: claude, codex, opencode');
204
+ }
205
+
206
+ // 5. Working directory (optional)
207
+ const cwd = await prompt('\n Working directory (leave blank for default): ');
208
+
209
+ // Summary
210
+ console.log('\n── Summary ──');
211
+ console.log(` Name: ${name}`);
212
+ console.log(` IM: ${im}`);
213
+ console.log(` App ID: ${maskSecret(appId)}`);
214
+ console.log(` App Secret: ${appSecret ? maskSecret(appSecret) : '(not set)'}`);
215
+ console.log(` Backend: ${backend}`);
216
+ if (cwd) console.log(` CWD: ${cwd}`);
217
+ console.log();
218
+
219
+ const ok = await confirm('Create this Bot?');
220
+ if (!ok) { console.log(' Cancelled.'); rl.close(); return; }
221
+
222
+ // Apply
223
+ const { config, configPath } = loadConfig();
224
+ if (!config.bots) config.bots = [];
225
+
226
+ const newBot: BotEntry = {
227
+ id: crypto.randomUUID(),
228
+ name,
229
+ im,
230
+ appId,
231
+ appSecret: appSecret || '',
232
+ backend,
233
+ isAdmin: false, // 通过 CLI 添加的 Bot 默认非 admin
234
+ ...(cwd ? { cwd } : {}),
235
+ };
236
+
237
+ config.bots.push(newBot);
238
+ saveConfig(config, configPath);
239
+
240
+ console.log(`\n✅ Bot "${name}" created!`);
241
+ console.log(` Run "imtoagent restore" to hot-reload the gateway.\n`);
242
+ rl.close();
243
+ }
244
+
245
+ // ================================================================
246
+ // config remove NAME
247
+ // ================================================================
248
+
249
+ export async function cmdConfigRemove(name: string): Promise<void> {
250
+ const { config, configPath } = loadConfig();
251
+ const bots = config.bots || [];
252
+ const idx = bots.findIndex(b => b.name === name);
253
+
254
+ if (idx === -1) {
255
+ console.error(`❌ Bot "${name}" not found`);
256
+ process.exit(1);
257
+ }
258
+
259
+ const bot = bots[idx];
260
+ console.log(`\n🗑️ Remove Bot: ${name}`);
261
+ console.log(` IM: ${bot.im || "(not set)"} | Backend: ${bot.backend}\n`);
262
+
263
+ const ok = await confirm('Are you sure? This cannot be undone');
264
+ if (!ok) { console.log(' Cancelled.'); rl.close(); return; }
265
+
266
+ config.bots = bots.filter((_, i) => i !== idx);
267
+ saveConfig(config, configPath);
268
+
269
+ console.log(`\n✅ Bot "${name}" removed`);
270
+ console.log(` Run "imtoagent restore" to hot-reload the gateway.\n`);
271
+ rl.close();
272
+ }
273
+
274
+ // ================================================================
275
+ // config modify NAME
276
+ // ================================================================
277
+
278
+ export async function cmdConfigModify(name: string): Promise<void> {
279
+ const { config, configPath } = loadConfig();
280
+ const bot = config.bots?.find(b => b.name === name);
281
+
282
+ if (!bot) {
283
+ console.error(`❌ Bot "${name}" not found`);
284
+ process.exit(1);
285
+ }
286
+
287
+ console.log(`\n✏️ Modify Bot: ${name}\n`);
288
+ console.log(` Current settings:`);
289
+ console.log(` IM: ${bot.im || "(not set)"}`);
290
+ console.log(` App ID: ${maskSecret(bot.appId)}`);
291
+ console.log(` App Secret: ${bot.appSecret ? maskSecret(bot.appSecret) : '(not set)'}`);
292
+ console.log(` Backend: ${bot.backend}`);
293
+ console.log(` CWD: ${bot.cwd || '(default)'}`);
294
+ console.log();
295
+
296
+ console.log(' What do you want to change?');
297
+ console.log(' 1. IM platform');
298
+ console.log(' 2. App ID');
299
+ console.log(' 3. App Secret');
300
+ console.log(' 4. Backend');
301
+ console.log(' 5. Working directory');
302
+ console.log(` 6. Admin status (${bot.isAdmin ? 'ON' : 'OFF'})`);
303
+ console.log(' 7. Quit');
304
+
305
+ const choice = await prompt('\n Choice (1-7): ');
306
+
307
+ switch (choice) {
308
+ case '1': {
309
+ console.log('\n Valid: feishu, telegram, wecom, wechat');
310
+ const newIm = await prompt(` New IM platform (current: ${bot.im}): `);
311
+ if (newIm && VALID_IMS.includes(newIm as IMType)) bot.im = newIm;
312
+ break;
313
+ }
314
+ case '2': {
315
+ const newAppId = await prompt(` New App ID (current: ${maskSecret(bot.appId)}): `);
316
+ if (newAppId) bot.appId = newAppId;
317
+ break;
318
+ }
319
+ case '3': {
320
+ const newSecret = await prompt(` New App Secret: `);
321
+ if (newSecret) bot.appSecret = newSecret;
322
+ break;
323
+ }
324
+ case '4': {
325
+ console.log('\n Valid: claude, codex, opencode');
326
+ const newBackend = await prompt(` New backend (current: ${bot.backend}): `);
327
+ if (newBackend && VALID_BACKENDS.includes(newBackend as BackendType)) bot.backend = newBackend;
328
+ break;
329
+ }
330
+ case '5': {
331
+ const newCwd = await prompt(` New working directory (current: ${bot.cwd || 'default'}): `);
332
+ if (newCwd) bot.cwd = newCwd;
333
+ else if (newCwd === '') delete bot.cwd;
334
+ break;
335
+ }
336
+ case '6': {
337
+ const current = bot.isAdmin ? 'ON' : 'OFF';
338
+ const newVal = await prompt(` Toggle admin status (current: ${current}, yes/no): `);
339
+ if (newVal && (newVal.toLowerCase() === 'yes' || newVal.toLowerCase() === 'y')) {
340
+ bot.isAdmin = !bot.isAdmin;
341
+ console.log(` Admin status → ${bot.isAdmin ? 'ON' : 'OFF'}`);
342
+ }
343
+ break;
344
+ }
345
+ case '7':
346
+ console.log(' No changes.');
347
+ rl.close();
348
+ return;
349
+ default:
350
+ console.log(' Invalid choice.');
351
+ rl.close();
352
+ return;
353
+ }
354
+
355
+ saveConfig(config, configPath);
356
+ console.log(`\n✅ Bot "${name}" updated`);
357
+ console.log(` Run "imtoagent restore" to hot-reload the gateway.\n`);
358
+ rl.close();
359
+ }
@@ -0,0 +1,462 @@
1
+ // ================================================================
2
+ // doctor.ts — 配置诊断与自动修复
3
+ // ================================================================
4
+ // imtoagent doctor
5
+ // 检查 config.json, providers.json, 数据目录, 后端, 端口, API Key 格式等
6
+ // 对可修复问题,用户确认后自动修复
7
+ // ================================================================
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as os from 'os';
12
+ import { execSync } from 'child_process';
13
+ import { getDataDir, getConfigPath, getProvidersPath, getSessionsDir, getLogsDir, getOpencodeConfigPath } from './paths';
14
+ import { checkBackend, checkAllBackends } from './backend-check';
15
+
16
+ // ================================================================
17
+ // Issue 类型
18
+ // ================================================================
19
+
20
+ export type IssueSeverity = 'error' | 'warning' | 'info';
21
+
22
+ export interface DoctorIssue {
23
+ severity: IssueSeverity;
24
+ category: string;
25
+ message: string;
26
+ fixable: boolean;
27
+ fixDescription?: string;
28
+ fix?: () => Promise<boolean> | boolean;
29
+ }
30
+
31
+ // ================================================================
32
+ // API Key 格式验证
33
+ // ================================================================
34
+
35
+ const API_KEY_PATTERNS: Record<string, { prefix: string; minLength: number }> = {
36
+ // OpenAI 格式
37
+ 'openai': { prefix: 'sk-proj-', minLength: 20 },
38
+ // Anthropic 格式
39
+ 'anthropic': { prefix: 'sk-ant-', minLength: 20 },
40
+ // 百炼/DashScope 格式
41
+ 'dashscope': { prefix: 'sk-', minLength: 20 },
42
+ // 通用 sk- 格式
43
+ 'generic-sk': { prefix: 'sk-', minLength: 10 },
44
+ };
45
+
46
+ function validateApiKey(key: string): { valid: boolean; reason?: string } {
47
+ if (!key) return { valid: false, reason: 'empty' };
48
+ if (key.includes('YOUR_') || key.includes('xxx') || key.includes('PLACEHOLDER') || key.includes('placeholder')) {
49
+ return { valid: false, reason: 'placeholder value' };
50
+ }
51
+ // 太短的 key 大概率是无效的
52
+ if (key.length < 8) return { valid: false, reason: `too short (${key.length} chars)` };
53
+ return { valid: true };
54
+ }
55
+
56
+ // ================================================================
57
+ // 诊断检查
58
+ // ================================================================
59
+
60
+ export async function runDoctorChecks(): Promise<DoctorIssue[]> {
61
+ const issues: DoctorIssue[] = [];
62
+ const dataDir = getDataDir();
63
+ const configPath = getConfigPath();
64
+ const providersPath = getProvidersPath();
65
+ const sessionsDir = getSessionsDir();
66
+ const logsDir = getLogsDir();
67
+
68
+ // ---- 1. 数据目录结构 ----
69
+ const requiredDirs = [
70
+ { path: dataDir, name: 'Data directory (~/.imtoagent/)' },
71
+ { path: sessionsDir, name: 'Sessions directory' },
72
+ { path: logsDir, name: 'Logs directory' },
73
+ ];
74
+ for (const dir of requiredDirs) {
75
+ if (!fs.existsSync(dir.path)) {
76
+ const dirPath = dir.path;
77
+ issues.push({
78
+ severity: 'error',
79
+ category: 'Directory',
80
+ message: `${dir.name} not found: ${dirPath}`,
81
+ fixable: true,
82
+ fixDescription: `Create directory: ${dirPath}`,
83
+ fix: () => { fs.mkdirSync(dirPath, { recursive: true }); return true; },
84
+ });
85
+ }
86
+ }
87
+
88
+ // ---- 2. config.json ----
89
+ let configRaw: string | null = null;
90
+ let config: any = null;
91
+
92
+ if (!fs.existsSync(configPath)) {
93
+ issues.push({
94
+ severity: 'error',
95
+ category: 'Config',
96
+ message: 'config.json not found — run "imtoagent setup" to create it',
97
+ fixable: false,
98
+ });
99
+ return issues; // 没有配置文件,后续检查无意义
100
+ }
101
+
102
+ try {
103
+ configRaw = fs.readFileSync(configPath, 'utf-8');
104
+ config = JSON.parse(configRaw);
105
+ issues.push({ severity: 'info', category: 'Config', message: 'config.json parse OK', fixable: false });
106
+ } catch (e: any) {
107
+ // 尝试修复常见 JSON 语法错误
108
+ const fixed = tryFixJSON(configRaw);
109
+ if (fixed !== null) {
110
+ issues.push({
111
+ severity: 'error',
112
+ category: 'Config',
113
+ message: `config.json has syntax errors: ${e.message}`,
114
+ fixable: true,
115
+ fixDescription: 'Auto-fix common JSON syntax issues (trailing commas, comments)',
116
+ fix: () => {
117
+ fs.writeFileSync(configPath, fixed + '\n');
118
+ return true;
119
+ },
120
+ });
121
+ config = JSON.parse(fixed);
122
+ } else {
123
+ issues.push({
124
+ severity: 'error',
125
+ category: 'Config',
126
+ message: `config.json parse error: ${e.message}`,
127
+ fixable: false,
128
+ });
129
+ return issues;
130
+ }
131
+ }
132
+
133
+ // 检查必要字段
134
+ if (!config.bots || !Array.isArray(config.bots)) {
135
+ issues.push({
136
+ severity: 'error',
137
+ category: 'Config',
138
+ message: 'No "bots" array in config.json — no Bots configured',
139
+ fixable: false,
140
+ });
141
+ } else if (config.bots.length === 0) {
142
+ issues.push({
143
+ severity: 'warning',
144
+ category: 'Config',
145
+ message: '"bots" array is empty — no Bots configured',
146
+ fixable: false,
147
+ });
148
+ } else {
149
+ for (let i = 0; i < config.bots.length; i++) {
150
+ const bot = config.bots[i];
151
+ const botLabel = bot.name || `bots[${i}]`;
152
+ const botIssues: string[] = [];
153
+
154
+ if (!bot.name) botIssues.push('missing "name"');
155
+ if (!bot.appId) botIssues.push('missing "appId"');
156
+ if (!bot.appSecret) botIssues.push('missing "appSecret"');
157
+ if (!bot.backend) botIssues.push('missing "backend"');
158
+ if (bot.backend && !['claude', 'codex', 'opencode'].includes(bot.backend)) {
159
+ botIssues.push(`unknown backend "${bot.backend}" (expected: claude/codex/opencode)`);
160
+ }
161
+
162
+ if (botIssues.length > 0) {
163
+ issues.push({
164
+ severity: 'error',
165
+ category: 'Config',
166
+ message: `Bot "${botLabel}": ${botIssues.join(', ')}`,
167
+ fixable: false,
168
+ });
169
+ } else {
170
+ issues.push({ severity: 'info', category: 'Config', message: `Bot "${bot.name}" OK (${bot.backend})`, fixable: false });
171
+ }
172
+ }
173
+ }
174
+
175
+ // 检查 system 配置
176
+ if (!config.system) {
177
+ const systemPath = 'system';
178
+ issues.push({
179
+ severity: 'warning',
180
+ category: 'Config',
181
+ message: 'Missing "system" section in config.json',
182
+ fixable: true,
183
+ fixDescription: 'Add default system config (idleTimeoutMinutes: 30, etc.)',
184
+ fix: () => {
185
+ config.system = { defaultProjectDir: os.homedir(), idleTimeoutMinutes: 30 };
186
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
187
+ return true;
188
+ },
189
+ });
190
+ }
191
+
192
+ // ---- 3. providers.json ----
193
+ let providers: any = null;
194
+
195
+ if (!fs.existsSync(providersPath)) {
196
+ issues.push({
197
+ severity: 'warning',
198
+ category: 'Providers',
199
+ message: 'providers.json not found',
200
+ fixable: false,
201
+ });
202
+ } else {
203
+ try {
204
+ const provRaw = fs.readFileSync(providersPath, 'utf-8');
205
+ providers = JSON.parse(provRaw);
206
+ issues.push({ severity: 'info', category: 'Providers', message: 'providers.json parse OK', fixable: false });
207
+
208
+ // 检查 placeholder API keys
209
+ const provStr = JSON.stringify(providers);
210
+ if (provStr.includes('YOUR_') || provStr.includes('sk-xxx') || provStr.includes('PLACEHOLDER')) {
211
+ issues.push({
212
+ severity: 'warning',
213
+ category: 'Providers',
214
+ message: 'providers.json may contain placeholder API keys',
215
+ fixable: false,
216
+ });
217
+ }
218
+
219
+ // 验证每个 provider 的 API key
220
+ if (providers.providers) {
221
+ for (const [provName, provCfg] of Object.entries(providers.providers) as [string, any][]) {
222
+ if (provCfg.apiKey) {
223
+ const result = validateApiKey(provCfg.apiKey);
224
+ if (!result.valid) {
225
+ issues.push({
226
+ severity: 'warning',
227
+ category: 'Providers',
228
+ message: `Provider "${provName}" API key looks invalid (${result.reason})`,
229
+ fixable: false,
230
+ });
231
+ } else {
232
+ issues.push({ severity: 'info', category: 'Providers', message: `Provider "${provName}" API key format OK`, fixable: false });
233
+ }
234
+ }
235
+ }
236
+ }
237
+ } catch (e: any) {
238
+ const fixed = tryFixJSON(fs.readFileSync(providersPath, 'utf-8'));
239
+ if (fixed !== null) {
240
+ issues.push({
241
+ severity: 'error',
242
+ category: 'Providers',
243
+ message: `providers.json has syntax errors: ${e.message}`,
244
+ fixable: true,
245
+ fixDescription: 'Auto-fix common JSON syntax issues',
246
+ fix: () => {
247
+ fs.writeFileSync(providersPath, fixed + '\n');
248
+ return true;
249
+ },
250
+ });
251
+ } else {
252
+ issues.push({
253
+ severity: 'error',
254
+ category: 'Providers',
255
+ message: `providers.json parse error: ${e.message}`,
256
+ fixable: false,
257
+ });
258
+ }
259
+ }
260
+ }
261
+
262
+ // ---- 4. 后端检查 ----
263
+ if (config.bots && config.bots.length > 0) {
264
+ const checkedTypes = new Set<string>();
265
+ for (const bot of config.bots) {
266
+ if (bot.backend && ['claude', 'codex', 'opencode'].includes(bot.backend) && !checkedTypes.has(bot.backend)) {
267
+ checkedTypes.add(bot.backend);
268
+ const info = checkBackend(bot.backend as any);
269
+ if (info.installed) {
270
+ issues.push({ severity: 'info', category: 'Backend', message: `${info.label} v${info.version} (${info.installSource})`, fixable: false });
271
+ } else {
272
+ issues.push({
273
+ severity: 'error',
274
+ category: 'Backend',
275
+ message: `${info.label} not installed — Bot "${bot.name}" requires it`,
276
+ fixable: false,
277
+ });
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ // ---- 5. 端口检查 ----
284
+ try {
285
+ const net = await import('net');
286
+ const checkPort = (port: number): Promise<boolean> => {
287
+ return new Promise((resolve) => {
288
+ const socket = new net.Socket();
289
+ socket.setTimeout(2000);
290
+ socket.on('connect', () => { socket.destroy(); resolve(true); });
291
+ socket.on('error', () => resolve(false));
292
+ socket.on('timeout', () => { socket.destroy(); resolve(false); });
293
+ socket.connect(port, '127.0.0.1');
294
+ });
295
+ };
296
+ const reachable = await checkPort(18899);
297
+ if (reachable) {
298
+ // 检查是否是 imtoagent 自己的进程
299
+ try {
300
+ const lsofOut = execSync(`lsof -i :18899 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 }).trim();
301
+ if (lsofOut && lsofOut.includes('imtoagent') || lsofOut.includes('bun') || lsofOut.includes('node')) {
302
+ issues.push({ severity: 'info', category: 'Port', message: 'Port 18899 in use by imtoagent gateway', fixable: false });
303
+ } else {
304
+ issues.push({
305
+ severity: 'error',
306
+ category: 'Port',
307
+ message: `Port 18899 occupied by another process:\n${lsofOut.split('\n').slice(0, 3).join('\n')}`,
308
+ fixable: false,
309
+ });
310
+ }
311
+ } catch {
312
+ issues.push({ severity: 'info', category: 'Port', message: 'Port 18899 in use', fixable: false });
313
+ }
314
+ } else {
315
+ issues.push({ severity: 'info', category: 'Port', message: 'Port 18899 is free', fixable: false });
316
+ }
317
+ } catch {
318
+ issues.push({ severity: 'warning', category: 'Port', message: 'Port check failed (skipped)', fixable: false });
319
+ }
320
+
321
+ // ---- 6. Bot appId/appSecret 格式 ----
322
+ if (config.bots) {
323
+ for (const bot of config.bots) {
324
+ if (bot.appId && bot.appId.includes('YOUR_')) {
325
+ issues.push({
326
+ severity: 'warning',
327
+ category: 'Credentials',
328
+ message: `Bot "${bot.name}" appId is placeholder: ${bot.appId}`,
329
+ fixable: false,
330
+ });
331
+ }
332
+ if (bot.appSecret && bot.appSecret.includes('YOUR_')) {
333
+ issues.push({
334
+ severity: 'warning',
335
+ category: 'Credentials',
336
+ message: `Bot "${bot.name}" appSecret is placeholder`,
337
+ fixable: false,
338
+ });
339
+ }
340
+ }
341
+ }
342
+
343
+ // ---- 7. Soul 目录检查 ----
344
+ if (config.bots) {
345
+ for (const bot of config.bots) {
346
+ const botKey = bot.id || bot.name;
347
+ const soulDir = path.join(dataDir, 'soul', botKey);
348
+ if (fs.existsSync(soulDir)) {
349
+ const soulFiles = fs.readdirSync(soulDir);
350
+ if (soulFiles.length === 0) {
351
+ issues.push({
352
+ severity: 'warning',
353
+ category: 'Soul',
354
+ message: `Bot "${bot.name}" soul directory is empty: ${soulDir}`,
355
+ fixable: false,
356
+ });
357
+ } else {
358
+ issues.push({ severity: 'info', category: 'Soul', message: `Bot "${bot.name}" soul: ${soulFiles.length} file(s)`, fixable: false });
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ // ---- 8. opencode.json 检查 ----
365
+ const opencodePath = getOpencodeConfigPath();
366
+ if (fs.existsSync(opencodePath)) {
367
+ try {
368
+ const ocRaw = fs.readFileSync(opencodePath, 'utf-8');
369
+ JSON.parse(ocRaw);
370
+ issues.push({ severity: 'info', category: 'Config', message: 'opencode.json parse OK', fixable: false });
371
+ } catch (e: any) {
372
+ const fixed = tryFixJSON(fs.readFileSync(opencodePath, 'utf-8'));
373
+ if (fixed !== null) {
374
+ issues.push({
375
+ severity: 'error',
376
+ category: 'Config',
377
+ message: `opencode.json has syntax errors: ${e.message}`,
378
+ fixable: true,
379
+ fixDescription: 'Auto-fix common JSON syntax issues',
380
+ fix: () => {
381
+ fs.writeFileSync(opencodePath, fixed + '\n');
382
+ return true;
383
+ },
384
+ });
385
+ } else {
386
+ issues.push({
387
+ severity: 'error',
388
+ category: 'Config',
389
+ message: `opencode.json parse error: ${e.message}`,
390
+ fixable: false,
391
+ });
392
+ }
393
+ }
394
+ }
395
+
396
+ return issues;
397
+ }
398
+
399
+ // ================================================================
400
+ // JSON 修复 — 处理常见语法错误
401
+ // ================================================================
402
+
403
+ function tryFixJSON(raw: string): string | null {
404
+ // 尝试 1: 直接解析
405
+ try { JSON.parse(raw); return raw; } catch {}
406
+
407
+ // 尝试 2: 移除行尾逗号 (trailing commas)
408
+ let fixed = raw.replace(/,\s*([}\]])/g, '$1');
409
+ try { JSON.parse(fixed); return fixed; } catch {}
410
+
411
+ // 尝试 3: 移除单行注释 (// ...)
412
+ fixed = fixed.replace(/\/\/.*$/gm, '');
413
+ try { JSON.parse(fixed); return fixed; } catch {}
414
+
415
+ // 尝试 4: 移除多行注释 (/* ... */)
416
+ fixed = fixed.replace(/\/\*[\s\S]*?\*\//g, '');
417
+ try { JSON.parse(fixed); return fixed; } catch {}
418
+
419
+ // 尝试 5: 移除尾随逗号 + 注释组合
420
+ fixed = raw.replace(/\/\/.*$/gm, '').replace(/,\s*([}\]])/g, '$1');
421
+ try { JSON.parse(fixed); return fixed; } catch {}
422
+
423
+ return null;
424
+ }
425
+
426
+ // ================================================================
427
+ // 格式化输出
428
+ // ================================================================
429
+
430
+ export function formatIssues(issues: DoctorIssue[]): string {
431
+ const errors = issues.filter(i => i.severity === 'error');
432
+ const warnings = issues.filter(i => i.severity === 'warning');
433
+ const infos = issues.filter(i => i.severity === 'info');
434
+
435
+ let output = '';
436
+
437
+ if (errors.length > 0) {
438
+ output += `\n❌ Errors (${errors.length}):\n`;
439
+ for (const e of errors) {
440
+ output += ` ${e.message}\n`;
441
+ if (e.fixable && e.fixDescription) {
442
+ output += ` → Fix: ${e.fixDescription}\n`;
443
+ }
444
+ }
445
+ }
446
+
447
+ if (warnings.length > 0) {
448
+ output += `\n⚠️ Warnings (${warnings.length}):\n`;
449
+ for (const w of warnings) {
450
+ output += ` ${w.message}\n`;
451
+ }
452
+ }
453
+
454
+ if (infos.length > 0) {
455
+ output += `\n✅ OK (${infos.length}):\n`;
456
+ for (const i of infos) {
457
+ output += ` ${i.message}\n`;
458
+ }
459
+ }
460
+
461
+ return output;
462
+ }
@@ -145,19 +145,39 @@ export class WorkspaceManager {
145
145
  /**
146
146
  * 检查路径是否允许该 Bot 访问。
147
147
  *
148
+ * 所有模式:禁止访问 ~/.imtoagent/ 下的配置敏感文件(配置保护)。
148
149
  * 沙盒模式:路径必须在 Bot 的工作空间范围内(或子目录)。
149
- * 全局模式:不做限制,允许访问任意路径(信任用户配置的全局目录)。
150
+ * 全局模式:不做其他限制,允许访问任意路径(信任用户配置的全局目录)。
150
151
  *
151
152
  * 返回 true 表示允许,false 表示拒绝。
152
153
  */
153
154
  isPathAllowed(botKey: string, targetPath: string): boolean {
154
- // 全局模式:不做边界限制
155
+ const resolved = path.resolve(targetPath);
156
+
157
+ // ⛔ 配置保护:禁止访问 ~/.imtoagent/ 下的敏感配置文件
158
+ // 白名单:允许访问 workspaces/ 和 soul/ 目录
159
+ const dataDir = path.resolve(getDataDir());
160
+ if (resolved === dataDir || resolved.startsWith(dataDir + path.sep)) {
161
+ const wsDir = path.resolve(this.workspacesDir);
162
+ const soulGlob = path.resolve(dataDir, 'soul');
163
+ // 允许:workspaces/ 下的内容、全局模式下的 soul/
164
+ if (resolved === wsDir || resolved.startsWith(wsDir + path.sep)) {
165
+ // OK — workspace 路径在工作空间范围内(沙盒模式下还需额外检查)
166
+ } else if (this.config.mode === 'global' &&
167
+ (resolved === soulGlob || resolved.startsWith(soulGlob + path.sep))) {
168
+ // OK — 全局模式下的 soul 目录
169
+ } else {
170
+ // 其他 ~/.imtoagent/ 路径一律禁止(config.json、providers.json、bot-ids.json 等)
171
+ return false;
172
+ }
173
+ }
174
+
175
+ // 全局模式:配置保护已通过,不做其他边界限制
155
176
  if (this.config.mode === 'global') {
156
177
  return true;
157
178
  }
158
179
 
159
180
  // 沙盒模式:路径必须在工作空间内
160
- const resolved = path.resolve(targetPath);
161
181
  const wsPath = this.getWorkspacePath(botKey);
162
182
  const resolvedWs = path.resolve(wsPath);
163
183
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imtoagent",
3
- "version": "0.3.24",
3
+ "version": "0.3.25",
4
4
  "description": "IM ↔ Agent 统一网关 — 飞书/Telegram/微信/企业微信对接 Claude Code/Codex/OpenCode",
5
5
  "type": "module",
6
6
  "bin": {