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.
- package/bin/imtoagent-real +734 -0
- package/index.ts +31 -4
- package/modules/cli/setup.ts +103 -36
- package/modules/core/types.ts +1 -0
- package/modules/utils/config-manager.ts +359 -0
- package/modules/utils/doctor.ts +462 -0
- package/modules/utils/workspace-manager.ts +23 -3
- package/package.json +1 -1
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':
|
|
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
|
|
441
|
-
if (
|
|
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';
|
package/modules/cli/setup.ts
CHANGED
|
@@ -91,10 +91,14 @@ async function selectMenu(title: string, options: string[]): Promise<number> {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
// ================================================================
|
|
94
|
-
// Text input (Enter confirm, ESC returns
|
|
94
|
+
// Text input (Enter confirm, ESC returns null)
|
|
95
95
|
// ================================================================
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Prompt the user for text input.
|
|
99
|
+
* @returns The entered string, or `null` if user pressed ESC.
|
|
100
|
+
*/
|
|
101
|
+
async function promptText(label: string, defaultValue = ''): Promise<string | null> {
|
|
98
102
|
const buf: string[] = [];
|
|
99
103
|
const defaultHint = defaultValue ? ` [${defaultValue}]` : '';
|
|
100
104
|
|
|
@@ -110,7 +114,7 @@ async function promptText(label: string, defaultValue = ''): Promise<string> {
|
|
|
110
114
|
break;
|
|
111
115
|
} else if (key === KEY.ESC) {
|
|
112
116
|
process.stdout.write('\x1B[0K\n');
|
|
113
|
-
return
|
|
117
|
+
return null;
|
|
114
118
|
} else if (key === KEY.BACKSPACE) {
|
|
115
119
|
if (buf.length > 0) {
|
|
116
120
|
buf.pop();
|
|
@@ -377,8 +381,18 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
377
381
|
// 3b: Auto-generate Bot name, customizable
|
|
378
382
|
const defaultName = IM_PLATFORMS[imIdx].label + 'Bot';
|
|
379
383
|
const nameInput = await promptText('Bot name', defaultName);
|
|
380
|
-
if (
|
|
381
|
-
const botName = nameInput || defaultName;
|
|
384
|
+
if (nameInput === null) { if (bots.length === 0) return; break; } // ESC
|
|
385
|
+
const botName = nameInput || defaultName;
|
|
386
|
+
|
|
387
|
+
// Validate Bot name — no whitespace-only, no dangerous characters
|
|
388
|
+
if (!botName || !/^\S/.test(botName)) {
|
|
389
|
+
console.log('⚠️ Bot name must not be empty or whitespace-only. Using default.');
|
|
390
|
+
}
|
|
391
|
+
// Sanitize: remove characters that would break directory names
|
|
392
|
+
const sanitized = botName.replace(/[^\w\s.-]/g, '');
|
|
393
|
+
if (sanitized !== botName) {
|
|
394
|
+
console.log(`⚠️ Bot name sanitized: "${botName}" → "${sanitized}"`);
|
|
395
|
+
}
|
|
382
396
|
|
|
383
397
|
// 3c: Select backend
|
|
384
398
|
const backendLabels = backendStatus.map(b =>
|
|
@@ -419,14 +433,24 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
419
433
|
|
|
420
434
|
for (const field of fields) {
|
|
421
435
|
const val = await promptText(field.label + (field.required ? '' : ' (optional)'));
|
|
422
|
-
if (
|
|
436
|
+
if (val === null) { credentials._escaped = 'true'; break; } // ESC
|
|
423
437
|
credentials[field.key] = val;
|
|
424
438
|
}
|
|
425
439
|
if (credentials._escaped) continue; // ESC go back and re-select backend
|
|
426
440
|
|
|
427
|
-
// 3e: Working directory
|
|
428
|
-
|
|
429
|
-
if (
|
|
441
|
+
// 3e: Working directory (validated)
|
|
442
|
+
let cwd = await promptText('Working directory', os.homedir());
|
|
443
|
+
if (cwd === null) continue;
|
|
444
|
+
cwd = cwd.trim() || os.homedir();
|
|
445
|
+
|
|
446
|
+
// Validate path — reject obviously invalid / dangerous paths
|
|
447
|
+
const badPaths = ['/dev/null', '/dev/zero', '/dev/random', '/etc/passwd', '/etc/shadow', '/System'];
|
|
448
|
+
if (badPaths.some(bp => cwd === bp || cwd.startsWith(bp + '/'))) {
|
|
449
|
+
console.log(`⚠️ Invalid path "${cwd}". Using home directory instead.`);
|
|
450
|
+
cwd = os.homedir();
|
|
451
|
+
}
|
|
452
|
+
// Resolve to absolute path
|
|
453
|
+
cwd = path.resolve(cwd);
|
|
430
454
|
|
|
431
455
|
// Generate unique ID (UUID, for directory isolation, renaming doesn't affect it)
|
|
432
456
|
const botId = randomUUID();
|
|
@@ -437,6 +461,7 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
437
461
|
name: botName,
|
|
438
462
|
backend,
|
|
439
463
|
cwd: cwd || os.homedir(),
|
|
464
|
+
isAdmin: bots.length === 0, // 第一个 Bot 为 admin,后续为 false
|
|
440
465
|
};
|
|
441
466
|
|
|
442
467
|
// Feishu needs appId + appSecret
|
|
@@ -509,14 +534,22 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
509
534
|
|
|
510
535
|
const wsLabels = ['Sandbox mode (isolated per Bot)', 'Global mode (shared root path)'];
|
|
511
536
|
const wsIdx = await selectMenu('Select workspace mode', wsLabels);
|
|
512
|
-
|
|
537
|
+
if (wsIdx === -1) { console.log('\n👋 Cancelled'); process.exit(0); }
|
|
513
538
|
workspaceMode = wsIdx === 1 ? 'global' : 'sandbox';
|
|
514
539
|
|
|
515
540
|
if (workspaceMode === 'global') {
|
|
516
541
|
const defaultGlobal = os.homedir() + '/imtoagent-workspace';
|
|
517
542
|
const gpInput = await promptText('Global workspace root path', defaultGlobal);
|
|
518
|
-
if (
|
|
519
|
-
|
|
543
|
+
if (gpInput === null) { console.log('\n👋 Cancelled'); process.exit(0); }
|
|
544
|
+
let resolved = (gpInput || defaultGlobal).trim();
|
|
545
|
+
|
|
546
|
+
// Validate path
|
|
547
|
+
const badPaths = ['/dev/null', '/dev/zero', '/dev/random', '/etc', '/System', '/usr'];
|
|
548
|
+
if (badPaths.some(bp => resolved === bp || resolved.startsWith(bp + '/'))) {
|
|
549
|
+
console.log(`⚠️ Invalid path "${resolved}". Using default instead.`);
|
|
550
|
+
resolved = defaultGlobal;
|
|
551
|
+
}
|
|
552
|
+
workspaceGlobalPath = path.resolve(resolved);
|
|
520
553
|
}
|
|
521
554
|
|
|
522
555
|
const home = process.env.HOME || process.env.USERPROFILE?.replace(/\\/g, '/') || '';
|
|
@@ -573,19 +606,27 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
573
606
|
console.log(` Format: ${preset.format}`);
|
|
574
607
|
console.log(` Models: ${preset.models.join(', ')}\n`);
|
|
575
608
|
|
|
576
|
-
// Confirm/edit short name
|
|
609
|
+
// Confirm/edit short name (validate — must be safe JSON key)
|
|
577
610
|
const nameEdit = await promptText('Provider name (leave blank to confirm)', provName);
|
|
578
|
-
if (
|
|
611
|
+
if (nameEdit === null) continue;
|
|
579
612
|
provName = nameEdit || provName;
|
|
613
|
+
// Sanitize provider name: only alphanumeric, hyphens, underscores
|
|
614
|
+
const sanitizedProv = provName.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
615
|
+
if (!sanitizedProv) {
|
|
616
|
+
console.log('⚠️ Invalid provider name. Using original.');
|
|
617
|
+
} else if (sanitizedProv !== provName) {
|
|
618
|
+
console.log(`⚠️ Provider name sanitized: "${provName}" → "${sanitizedProv}"`);
|
|
619
|
+
provName = sanitizedProv;
|
|
620
|
+
}
|
|
580
621
|
|
|
581
622
|
// Confirm/edit Base URL
|
|
582
623
|
const urlEdit = await promptText('Base URL', baseUrl);
|
|
583
|
-
if (
|
|
624
|
+
if (urlEdit === null) continue;
|
|
584
625
|
baseUrl = urlEdit || baseUrl;
|
|
585
626
|
|
|
586
627
|
// Confirm/edit model list
|
|
587
628
|
const modelsEdit = await promptText('Model list (comma-separated)', models.join(', '));
|
|
588
|
-
if (
|
|
629
|
+
if (modelsEdit === null) continue;
|
|
589
630
|
if (modelsEdit) models = modelsEdit.split(',').map(s => s.trim()).filter(Boolean);
|
|
590
631
|
|
|
591
632
|
if (providers[provName]) {
|
|
@@ -593,17 +634,18 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
593
634
|
}
|
|
594
635
|
} else {
|
|
595
636
|
// Custom
|
|
596
|
-
|
|
597
|
-
if (
|
|
637
|
+
let customName = await promptText('Provider name (e.g. deepseek, dashscope)');
|
|
638
|
+
if (customName === null) { addingProviders = false; continue; }
|
|
639
|
+
provName = customName.trim().toLowerCase().replace(/[^a-zA-Z0-9_-]/g, '');
|
|
598
640
|
if (!provName) { addingProviders = false; continue; }
|
|
599
641
|
if (providers[provName]) {
|
|
600
642
|
console.log(`⚠️ Provider "${provName}" already exists, will overwrite\n`);
|
|
601
643
|
}
|
|
602
644
|
|
|
603
645
|
baseUrl = await promptText('Base URL (e.g. https://api.deepseek.com/v1)');
|
|
604
|
-
if (
|
|
646
|
+
if (baseUrl === null) continue;
|
|
605
647
|
const modelsStr = await promptText('Model list (comma-separated)');
|
|
606
|
-
if (
|
|
648
|
+
if (modelsStr === null) continue;
|
|
607
649
|
models = (modelsStr || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
608
650
|
|
|
609
651
|
const formatIdx = await selectMenu('API format', ['openai', 'anthropic']);
|
|
@@ -613,14 +655,14 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
613
655
|
|
|
614
656
|
// API Key (required for all providers)
|
|
615
657
|
const apiKey = await promptText('API Key');
|
|
616
|
-
if (
|
|
658
|
+
if (apiKey === null) continue;
|
|
617
659
|
if (!apiKey) {
|
|
618
660
|
console.log('⚠️ API Key is empty, this provider will be temporarily unavailable\n');
|
|
619
661
|
}
|
|
620
662
|
|
|
621
663
|
// Pricing (optional)
|
|
622
664
|
const priceInput = await promptText('Pricing (in/out per million tokens, e.g. 0.55,2.19, leave blank to skip)');
|
|
623
|
-
if (
|
|
665
|
+
if (priceInput === null) continue;
|
|
624
666
|
|
|
625
667
|
const pricing: any = {};
|
|
626
668
|
if (priceInput) {
|
|
@@ -663,10 +705,10 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
663
705
|
if (allModels.length > 0) {
|
|
664
706
|
const existingDefault = existingConfig?.defaultModel || allModels[0];
|
|
665
707
|
const val = await promptText('Default model', existingDefault);
|
|
666
|
-
defaultModel =
|
|
708
|
+
defaultModel = val === null ? existingDefault : (val || existingDefault);
|
|
667
709
|
} else {
|
|
668
|
-
|
|
669
|
-
|
|
710
|
+
const val = await promptText('Default model (provider/model)');
|
|
711
|
+
defaultModel = val === null ? 'deepseek/deepseek-v4-pro' : (val || 'deepseek/deepseek-v4-pro');
|
|
670
712
|
}
|
|
671
713
|
|
|
672
714
|
// ===== Step 7: Generate soul files =====
|
|
@@ -719,6 +761,8 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
719
761
|
|
|
720
762
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
721
763
|
|
|
764
|
+
// Atomic config write: write to temp file first, then rename.
|
|
765
|
+
// If the write fails, the original config (if any) is preserved.
|
|
722
766
|
const config: any = {
|
|
723
767
|
system: existingConfig?.system || {
|
|
724
768
|
defaultProjectDir: os.homedir(),
|
|
@@ -758,19 +802,42 @@ export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
|
758
802
|
bots,
|
|
759
803
|
};
|
|
760
804
|
|
|
761
|
-
|
|
762
|
-
|
|
805
|
+
// Write to temp file first for atomicity
|
|
806
|
+
const configTmpPath = configPath + '.tmp';
|
|
807
|
+
const providersTmpPath = path.join(dataDir, 'providers.json.tmp');
|
|
808
|
+
let writeOk = true;
|
|
763
809
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
810
|
+
try {
|
|
811
|
+
fs.writeFileSync(configTmpPath, JSON.stringify(config, null, 2) + '\n');
|
|
812
|
+
fs.renameSync(configTmpPath, configPath);
|
|
813
|
+
console.log(`✅ ${configPath}`);
|
|
814
|
+
} catch (e: any) {
|
|
815
|
+
console.error(`❌ Failed to write config.json: ${e.message}`);
|
|
816
|
+
writeOk = false;
|
|
817
|
+
}
|
|
768
818
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
819
|
+
if (writeOk) {
|
|
820
|
+
try {
|
|
821
|
+
const providersFile: any = { providers, defaultModel, modelAliases: config.modelAliases };
|
|
822
|
+
const providersPath = path.join(dataDir, 'providers.json');
|
|
823
|
+
fs.writeFileSync(providersTmpPath, JSON.stringify(providersFile, null, 2) + '\n');
|
|
824
|
+
fs.renameSync(providersTmpPath, providersPath);
|
|
825
|
+
console.log(`✅ ${providersPath}`);
|
|
826
|
+
} catch (e: any) {
|
|
827
|
+
console.error(`❌ Failed to write providers.json: ${e.message}`);
|
|
828
|
+
console.error('⚠️ config.json was written successfully, but providers.json failed.');
|
|
829
|
+
console.error(' Please re-run "imtoagent setup" to fix.');
|
|
830
|
+
writeOk = false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (writeOk) {
|
|
835
|
+
const opencodePath = path.join(dataDir, 'opencode.json');
|
|
836
|
+
const opencodeTemplate = getTemplatePath('opencode.template.json');
|
|
837
|
+
if (fs.existsSync(opencodeTemplate)) {
|
|
838
|
+
fs.writeFileSync(opencodePath, fs.readFileSync(opencodeTemplate, 'utf-8'));
|
|
839
|
+
console.log(`✅ ${opencodePath}`);
|
|
840
|
+
}
|
|
774
841
|
}
|
|
775
842
|
|
|
776
843
|
fs.mkdirSync(path.join(dataDir, 'sessions'), { recursive: true });
|
package/modules/core/types.ts
CHANGED
|
@@ -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;
|