imtoagent 0.3.22 → 0.3.24
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 +555 -3
- package/index.ts +50 -12
- package/modules/cli/setup.ts +185 -54
- package/modules/utils/migrate-workspaces.ts +230 -0
- package/modules/utils/paths.ts +1 -1
- package/modules/utils/workspace-manager.ts +209 -0
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
import { parseToBlocks } from './modules/capabilities';
|
|
20
20
|
import { resolveCapabilities } from './modules/prompt-builder';
|
|
21
21
|
import { getDataDir } from './modules/utils/paths';
|
|
22
|
+
import { WorkspaceManager, createWorkspaceManager } from './modules/utils/workspace-manager';
|
|
23
|
+
import { migrateWorkspaces } from './modules/utils/migrate-workspaces';
|
|
22
24
|
import { FeishuIMModule } from './modules/im/feishu';
|
|
23
25
|
import { TelegramAdapter } from './modules/im/telegram';
|
|
24
26
|
import { WeComIMModule } from './modules/im/wecom';
|
|
@@ -109,15 +111,17 @@ interface ChatSession extends Session {
|
|
|
109
111
|
// ================================================================
|
|
110
112
|
class CustomSessionManager {
|
|
111
113
|
sessions: Map<string, ChatSession>;
|
|
112
|
-
|
|
114
|
+
botKey: string;
|
|
115
|
+
sessionsDir: string;
|
|
113
116
|
|
|
114
|
-
constructor(
|
|
115
|
-
this.
|
|
117
|
+
constructor(botKey: string, workspacePath: string, sessions: Map<string, ChatSession>) {
|
|
118
|
+
this.botKey = botKey;
|
|
116
119
|
this.sessions = sessions;
|
|
120
|
+
this.sessionsDir = path.join(workspacePath, 'sessions');
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
private _sessionPath(chatId: string): string {
|
|
120
|
-
return path.join(
|
|
124
|
+
return path.join(this.sessionsDir, `${chatId}.memory.json`);
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
async getOrCreate(chatId: string, userId: string): Promise<ChatSession> {
|
|
@@ -128,6 +132,10 @@ class CustomSessionManager {
|
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
// 从文件加载(兼容旧格式)
|
|
135
|
+
// 确保 sessions 目录存在
|
|
136
|
+
if (!fs.existsSync(this.sessionsDir)) {
|
|
137
|
+
fs.mkdirSync(this.sessionsDir, { recursive: true });
|
|
138
|
+
}
|
|
131
139
|
const fp = this._sessionPath(chatId);
|
|
132
140
|
let session: ChatSession;
|
|
133
141
|
|
|
@@ -300,6 +308,7 @@ class Bot {
|
|
|
300
308
|
client: Lark.Client;
|
|
301
309
|
im: IMModule;
|
|
302
310
|
config: any;
|
|
311
|
+
workspaceManager: WorkspaceManager;
|
|
303
312
|
|
|
304
313
|
// SDK
|
|
305
314
|
runtime: AgentRuntime;
|
|
@@ -310,7 +319,7 @@ class Bot {
|
|
|
310
319
|
/** 正在执行的任务的取消信号(chatId → AbortController) */
|
|
311
320
|
activeControllers: Map<string, AbortController> = new Map();
|
|
312
321
|
|
|
313
|
-
constructor(cfg: BotConfig, globalConfig: any) {
|
|
322
|
+
constructor(cfg: BotConfig, globalConfig: any, workspaceManager: WorkspaceManager) {
|
|
314
323
|
this.id = cfg.id || cfg.name; // 后向兼容:无 id 时用 name
|
|
315
324
|
this.name = cfg.name;
|
|
316
325
|
this.backend = cfg.backend;
|
|
@@ -318,6 +327,11 @@ class Bot {
|
|
|
318
327
|
this.appSecret = cfg.appSecret;
|
|
319
328
|
this.defaultCwd = cfg.cwd || globalConfig.system?.defaultProjectDir || path.join(os.homedir(), 'Projects');
|
|
320
329
|
this.config = globalConfig;
|
|
330
|
+
this.workspaceManager = workspaceManager;
|
|
331
|
+
|
|
332
|
+
// 确保工作空间目录存在
|
|
333
|
+
const botKey = this.id;
|
|
334
|
+
this.workspaceManager.ensureWorkspace(botKey);
|
|
321
335
|
|
|
322
336
|
// Bot 级模型配置
|
|
323
337
|
const botCfg = this._loadBotConfig();
|
|
@@ -352,7 +366,8 @@ class Bot {
|
|
|
352
366
|
this.im = imFactory.create(cfg);
|
|
353
367
|
|
|
354
368
|
// ===== SDK 集成 =====
|
|
355
|
-
|
|
369
|
+
const workspacePath = this.workspaceManager.getWorkspacePath(this.id);
|
|
370
|
+
this.sessionManager = new CustomSessionManager(this.id, workspacePath, this.sessions);
|
|
356
371
|
|
|
357
372
|
const adapterCtx = {
|
|
358
373
|
imModule: this.im,
|
|
@@ -387,7 +402,9 @@ class Bot {
|
|
|
387
402
|
}
|
|
388
403
|
|
|
389
404
|
// ===== 灵魂管理 =====
|
|
390
|
-
_soulDir() {
|
|
405
|
+
_soulDir() {
|
|
406
|
+
return this.workspaceManager.getSoulPath(this.id);
|
|
407
|
+
}
|
|
391
408
|
|
|
392
409
|
_initSoul() {
|
|
393
410
|
const dir = this._soulDir();
|
|
@@ -614,9 +631,19 @@ class Bot {
|
|
|
614
631
|
|
|
615
632
|
cmd('/dir', ({ args, session }) => {
|
|
616
633
|
const dir = args.trim();
|
|
617
|
-
|
|
618
|
-
if (
|
|
619
|
-
|
|
634
|
+
const currentCwd = session?.cwd || this.defaultCwd;
|
|
635
|
+
if (!dir) {
|
|
636
|
+
const mode = this.workspaceManager.getMode();
|
|
637
|
+
return `📁 ${currentCwd}\n🏷 Workspace mode: ${mode}`;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const resolved = this.workspaceManager.resolveAndValidatePath(this.id, dir, currentCwd);
|
|
641
|
+
if (resolved === null) {
|
|
642
|
+
return `❌ Path not allowed: ${dir}\n💡 In sandbox mode, you can only access paths within your workspace`;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (session) session.cwd = resolved;
|
|
646
|
+
return `📁 Switched: ${resolved}`;
|
|
620
647
|
});
|
|
621
648
|
|
|
622
649
|
cmd('/mode', ({ args, session }) => {
|
|
@@ -950,6 +977,16 @@ async function main() {
|
|
|
950
977
|
return;
|
|
951
978
|
}
|
|
952
979
|
|
|
980
|
+
// ===== Workspace Migration =====
|
|
981
|
+
// 老用户升级:自动迁移旧 sessions/ + soul/ 到新的 workspace 结构
|
|
982
|
+
const migrationResult = migrateWorkspaces();
|
|
983
|
+
if (migrationResult.botsMigrated.length > 0) {
|
|
984
|
+
console.log(` 🔄 Workspace migration: ${migrationResult.botsMigrated.length} bot(s) migrated`);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// 创建 WorkspaceManager(所有 Bot 共享)
|
|
988
|
+
const workspaceManager = createWorkspaceManager(config);
|
|
989
|
+
|
|
953
990
|
const bots: Bot[] = [];
|
|
954
991
|
for (const c of botCfgs) {
|
|
955
992
|
const appId = c.appId || c.feishu?.appId || '';
|
|
@@ -958,7 +995,7 @@ async function main() {
|
|
|
958
995
|
|
|
959
996
|
// wechat 不需要 appId/appSecret,首次启动会触发 QR 扫码绑定
|
|
960
997
|
if (imType === 'wechat') {
|
|
961
|
-
bots.push(new Bot({ ...c, appId: appId || 'wechat-bot', appSecret }, config));
|
|
998
|
+
bots.push(new Bot({ ...c, appId: appId || 'wechat-bot', appSecret }, config, workspaceManager));
|
|
962
999
|
continue;
|
|
963
1000
|
}
|
|
964
1001
|
|
|
@@ -968,7 +1005,7 @@ async function main() {
|
|
|
968
1005
|
console.log(`[Config] ⚠️ Bot "${c.name}" has placeholder credentials, skipping`);
|
|
969
1006
|
continue;
|
|
970
1007
|
}
|
|
971
|
-
bots.push(new Bot({ ...c, appId, appSecret }, config));
|
|
1008
|
+
bots.push(new Bot({ ...c, appId, appSecret }, config, workspaceManager));
|
|
972
1009
|
}
|
|
973
1010
|
|
|
974
1011
|
if (bots.length === 0) {
|
|
@@ -976,6 +1013,7 @@ async function main() {
|
|
|
976
1013
|
return;
|
|
977
1014
|
}
|
|
978
1015
|
|
|
1016
|
+
console.log(` Workspace: ${workspaceManager.getConfigSummary()}`);
|
|
979
1017
|
_allBots = bots;
|
|
980
1018
|
console.log(`\n🚀 CC Routing v4 — Multi-Bot Architecture (Full SDK Integration)`);
|
|
981
1019
|
console.log(` Anthropic: http://localhost:${proxyPort}`);
|
package/modules/cli/setup.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
-
import { getDataDir, getPkgDir, getTemplatePath, getSoulDir
|
|
15
|
+
import { getDataDir, getPkgDir, getTemplatePath, getSoulDir } from '../utils/paths';
|
|
16
16
|
import { randomUUID } from 'crypto';
|
|
17
17
|
import { checkAllBackends, formatBackendStatus } from '../utils/backend-check';
|
|
18
18
|
|
|
@@ -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();
|
|
@@ -290,7 +294,11 @@ const IM_FIELDS: Record<string, { key: string; label: string; required: boolean
|
|
|
290
294
|
// Main flow
|
|
291
295
|
// ================================================================
|
|
292
296
|
|
|
293
|
-
export
|
|
297
|
+
export interface SetupOptions {
|
|
298
|
+
quick?: boolean;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function runSetupWizard(options?: SetupOptions): Promise<void> {
|
|
294
302
|
// Guard: refuse to run in non-TTY environment
|
|
295
303
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
296
304
|
console.error('');
|
|
@@ -311,6 +319,11 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
311
319
|
console.log(`\nData directory: ${dataDir}`);
|
|
312
320
|
console.log(`Controls: ↑↓/Space navigate | Enter confirm | ESC go back\n`);
|
|
313
321
|
|
|
322
|
+
const isQuick = options?.quick || false;
|
|
323
|
+
if (isQuick) {
|
|
324
|
+
console.log('⚡ Quick mode: workspace defaults to sandbox, skip workspace step\n');
|
|
325
|
+
}
|
|
326
|
+
|
|
314
327
|
// ===== Step 1: Detect existing configuration =====
|
|
315
328
|
let existingConfig: any = null;
|
|
316
329
|
let mergeMode = false;
|
|
@@ -368,8 +381,18 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
368
381
|
// 3b: Auto-generate Bot name, customizable
|
|
369
382
|
const defaultName = IM_PLATFORMS[imIdx].label + 'Bot';
|
|
370
383
|
const nameInput = await promptText('Bot name', defaultName);
|
|
371
|
-
if (
|
|
372
|
-
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
|
+
}
|
|
373
396
|
|
|
374
397
|
// 3c: Select backend
|
|
375
398
|
const backendLabels = backendStatus.map(b =>
|
|
@@ -410,14 +433,24 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
410
433
|
|
|
411
434
|
for (const field of fields) {
|
|
412
435
|
const val = await promptText(field.label + (field.required ? '' : ' (optional)'));
|
|
413
|
-
if (
|
|
436
|
+
if (val === null) { credentials._escaped = 'true'; break; } // ESC
|
|
414
437
|
credentials[field.key] = val;
|
|
415
438
|
}
|
|
416
439
|
if (credentials._escaped) continue; // ESC go back and re-select backend
|
|
417
440
|
|
|
418
|
-
// 3e: Working directory
|
|
419
|
-
|
|
420
|
-
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);
|
|
421
454
|
|
|
422
455
|
// Generate unique ID (UUID, for directory isolation, renaming doesn't affect it)
|
|
423
456
|
const botId = randomUUID();
|
|
@@ -458,15 +491,20 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
458
491
|
bot.im = imType;
|
|
459
492
|
}
|
|
460
493
|
|
|
461
|
-
// Check for duplicate name
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
494
|
+
// Check for duplicate name — auto-rename with counter
|
|
495
|
+
let finalName = botName;
|
|
496
|
+
let counter = 1;
|
|
497
|
+
while (bots.find(b => b.name === finalName)) {
|
|
498
|
+
finalName = `${botName} (${counter})`;
|
|
499
|
+
counter++;
|
|
500
|
+
}
|
|
501
|
+
bot.name = finalName;
|
|
502
|
+
|
|
503
|
+
if (finalName !== botName) {
|
|
504
|
+
console.log(`⚠️ "${botName}" already exists, renamed to "${finalName}"`);
|
|
469
505
|
}
|
|
506
|
+
bots.push(bot);
|
|
507
|
+
console.log(`✅ Added: ${finalName}`);
|
|
470
508
|
|
|
471
509
|
// Whether to continue adding
|
|
472
510
|
const r = await confirm('Add another Bot?', true);
|
|
@@ -482,8 +520,51 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
482
520
|
return;
|
|
483
521
|
}
|
|
484
522
|
|
|
485
|
-
// ===== Step 4: Configure
|
|
486
|
-
|
|
523
|
+
// ===== Step 4: Configure workspace =====
|
|
524
|
+
let workspaceMode: 'sandbox' | 'global' = 'sandbox';
|
|
525
|
+
let workspaceGlobalPath: string | null = null;
|
|
526
|
+
|
|
527
|
+
if (!isQuick) {
|
|
528
|
+
console.log('\n📌 Step 4: Configure workspace\n');
|
|
529
|
+
console.log('Each Bot gets its own isolated file workspace:');
|
|
530
|
+
console.log(' sandbox — isolated per Bot, no cross-Bot file access (recommended)');
|
|
531
|
+
console.log(' global — shared root directory, multiple Bots collaborate on same codebase');
|
|
532
|
+
console.log('');
|
|
533
|
+
|
|
534
|
+
const wsLabels = ['Sandbox mode (isolated per Bot)', 'Global mode (shared root path)'];
|
|
535
|
+
const wsIdx = await selectMenu('Select workspace mode', wsLabels);
|
|
536
|
+
if (wsIdx === -1) { console.log('\n👋 Cancelled'); process.exit(0); }
|
|
537
|
+
workspaceMode = wsIdx === 1 ? 'global' : 'sandbox';
|
|
538
|
+
|
|
539
|
+
if (workspaceMode === 'global') {
|
|
540
|
+
const defaultGlobal = os.homedir() + '/imtoagent-workspace';
|
|
541
|
+
const gpInput = await promptText('Global workspace root path', defaultGlobal);
|
|
542
|
+
if (gpInput === null) { console.log('\n👋 Cancelled'); process.exit(0); }
|
|
543
|
+
let resolved = (gpInput || defaultGlobal).trim();
|
|
544
|
+
|
|
545
|
+
// Validate path
|
|
546
|
+
const badPaths = ['/dev/null', '/dev/zero', '/dev/random', '/etc', '/System', '/usr'];
|
|
547
|
+
if (badPaths.some(bp => resolved === bp || resolved.startsWith(bp + '/'))) {
|
|
548
|
+
console.log(`⚠️ Invalid path "${resolved}". Using default instead.`);
|
|
549
|
+
resolved = defaultGlobal;
|
|
550
|
+
}
|
|
551
|
+
workspaceGlobalPath = path.resolve(resolved);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const home = process.env.HOME || process.env.USERPROFILE?.replace(/\\/g, '/') || '';
|
|
555
|
+
console.log('');
|
|
556
|
+
console.log('Workspace layout:');
|
|
557
|
+
if (workspaceMode === 'sandbox') {
|
|
558
|
+
console.log(` ${home}/.imtoagent/workspaces/<UUID>/ (each Bot gets a unique directory)`);
|
|
559
|
+
} else {
|
|
560
|
+
console.log(` ${workspaceGlobalPath}/ (all Bots share this root)`);
|
|
561
|
+
console.log(` soul files in ${workspaceGlobalPath}/.imtoagent/soul/<botId>/`);
|
|
562
|
+
}
|
|
563
|
+
console.log('');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ===== Step 5: Configure model providers =====
|
|
567
|
+
console.log('\n📌 Step 5: Configure model providers\n');
|
|
487
568
|
|
|
488
569
|
const providers: Record<string, any> = {};
|
|
489
570
|
if (mergeMode && existingConfig?.providers) {
|
|
@@ -524,19 +605,27 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
524
605
|
console.log(` Format: ${preset.format}`);
|
|
525
606
|
console.log(` Models: ${preset.models.join(', ')}\n`);
|
|
526
607
|
|
|
527
|
-
// Confirm/edit short name
|
|
608
|
+
// Confirm/edit short name (validate — must be safe JSON key)
|
|
528
609
|
const nameEdit = await promptText('Provider name (leave blank to confirm)', provName);
|
|
529
|
-
if (
|
|
610
|
+
if (nameEdit === null) continue;
|
|
530
611
|
provName = nameEdit || provName;
|
|
612
|
+
// Sanitize provider name: only alphanumeric, hyphens, underscores
|
|
613
|
+
const sanitizedProv = provName.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
614
|
+
if (!sanitizedProv) {
|
|
615
|
+
console.log('⚠️ Invalid provider name. Using original.');
|
|
616
|
+
} else if (sanitizedProv !== provName) {
|
|
617
|
+
console.log(`⚠️ Provider name sanitized: "${provName}" → "${sanitizedProv}"`);
|
|
618
|
+
provName = sanitizedProv;
|
|
619
|
+
}
|
|
531
620
|
|
|
532
621
|
// Confirm/edit Base URL
|
|
533
622
|
const urlEdit = await promptText('Base URL', baseUrl);
|
|
534
|
-
if (
|
|
623
|
+
if (urlEdit === null) continue;
|
|
535
624
|
baseUrl = urlEdit || baseUrl;
|
|
536
625
|
|
|
537
626
|
// Confirm/edit model list
|
|
538
627
|
const modelsEdit = await promptText('Model list (comma-separated)', models.join(', '));
|
|
539
|
-
if (
|
|
628
|
+
if (modelsEdit === null) continue;
|
|
540
629
|
if (modelsEdit) models = modelsEdit.split(',').map(s => s.trim()).filter(Boolean);
|
|
541
630
|
|
|
542
631
|
if (providers[provName]) {
|
|
@@ -544,17 +633,18 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
544
633
|
}
|
|
545
634
|
} else {
|
|
546
635
|
// Custom
|
|
547
|
-
|
|
548
|
-
if (
|
|
636
|
+
let customName = await promptText('Provider name (e.g. deepseek, dashscope)');
|
|
637
|
+
if (customName === null) { addingProviders = false; continue; }
|
|
638
|
+
provName = customName.trim().toLowerCase().replace(/[^a-zA-Z0-9_-]/g, '');
|
|
549
639
|
if (!provName) { addingProviders = false; continue; }
|
|
550
640
|
if (providers[provName]) {
|
|
551
641
|
console.log(`⚠️ Provider "${provName}" already exists, will overwrite\n`);
|
|
552
642
|
}
|
|
553
643
|
|
|
554
644
|
baseUrl = await promptText('Base URL (e.g. https://api.deepseek.com/v1)');
|
|
555
|
-
if (
|
|
645
|
+
if (baseUrl === null) continue;
|
|
556
646
|
const modelsStr = await promptText('Model list (comma-separated)');
|
|
557
|
-
if (
|
|
647
|
+
if (modelsStr === null) continue;
|
|
558
648
|
models = (modelsStr || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
559
649
|
|
|
560
650
|
const formatIdx = await selectMenu('API format', ['openai', 'anthropic']);
|
|
@@ -564,14 +654,14 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
564
654
|
|
|
565
655
|
// API Key (required for all providers)
|
|
566
656
|
const apiKey = await promptText('API Key');
|
|
567
|
-
if (
|
|
657
|
+
if (apiKey === null) continue;
|
|
568
658
|
if (!apiKey) {
|
|
569
659
|
console.log('⚠️ API Key is empty, this provider will be temporarily unavailable\n');
|
|
570
660
|
}
|
|
571
661
|
|
|
572
662
|
// Pricing (optional)
|
|
573
663
|
const priceInput = await promptText('Pricing (in/out per million tokens, e.g. 0.55,2.19, leave blank to skip)');
|
|
574
|
-
if (
|
|
664
|
+
if (priceInput === null) continue;
|
|
575
665
|
|
|
576
666
|
const pricing: any = {};
|
|
577
667
|
if (priceInput) {
|
|
@@ -600,8 +690,8 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
600
690
|
step4Loop = false; // Has providers or user explicitly skipped
|
|
601
691
|
}
|
|
602
692
|
|
|
603
|
-
// ===== Step
|
|
604
|
-
console.log('\n📌 Step
|
|
693
|
+
// ===== Step 6: Select default model =====
|
|
694
|
+
console.log('\n📌 Step 6: Select default model\n');
|
|
605
695
|
|
|
606
696
|
const allModels: string[] = [];
|
|
607
697
|
for (const [provName, prov] of Object.entries(providers)) {
|
|
@@ -614,17 +704,29 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
614
704
|
if (allModels.length > 0) {
|
|
615
705
|
const existingDefault = existingConfig?.defaultModel || allModels[0];
|
|
616
706
|
const val = await promptText('Default model', existingDefault);
|
|
617
|
-
defaultModel =
|
|
707
|
+
defaultModel = val === null ? existingDefault : (val || existingDefault);
|
|
618
708
|
} else {
|
|
619
|
-
|
|
620
|
-
|
|
709
|
+
const val = await promptText('Default model (provider/model)');
|
|
710
|
+
defaultModel = val === null ? 'deepseek/deepseek-v4-pro' : (val || 'deepseek/deepseek-v4-pro');
|
|
621
711
|
}
|
|
622
712
|
|
|
623
|
-
// ===== Step
|
|
624
|
-
console.log('\n📌 Step
|
|
713
|
+
// ===== Step 7: Generate soul files =====
|
|
714
|
+
console.log('\n📌 Step 7: Generate soul files\n');
|
|
715
|
+
|
|
716
|
+
// Compute workspace soul dirs based on configured workspace mode
|
|
717
|
+
const home = process.env.HOME || process.env.USERPROFILE?.replace(/\\/g, '/') || '';
|
|
625
718
|
|
|
626
719
|
for (const bot of bots) {
|
|
627
|
-
|
|
720
|
+
let botSoulDir: string;
|
|
721
|
+
|
|
722
|
+
if (workspaceMode === 'sandbox') {
|
|
723
|
+
// Sandbox: soul in workspaces/<UUID>/soul/
|
|
724
|
+
botSoulDir = path.join(home, '.imtoagent', 'workspaces', bot.id, 'soul');
|
|
725
|
+
} else {
|
|
726
|
+
// Global: soul in <globalPath>/.imtoagent/soul/<botId>/
|
|
727
|
+
botSoulDir = path.join(workspaceGlobalPath || path.join(home, 'imtoagent-workspace'), '.imtoagent', 'soul', bot.id);
|
|
728
|
+
}
|
|
729
|
+
|
|
628
730
|
const templateSoulDir = path.join(getPkgDir(), 'templates', 'soul.template');
|
|
629
731
|
|
|
630
732
|
if (fs.existsSync(botSoulDir)) {
|
|
@@ -654,18 +756,23 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
654
756
|
}
|
|
655
757
|
console.log(`✅ ${bot.name}: soul files → ${botSoulDir}`);
|
|
656
758
|
}
|
|
657
|
-
|
|
658
|
-
// ===== Step 7: Write configuration files =====
|
|
659
|
-
console.log('\n📌 Step 7: Write configuration files\n');
|
|
759
|
+
console.log('\n📌 Step 8: Write configuration files\n');
|
|
660
760
|
|
|
661
761
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
662
762
|
|
|
763
|
+
// Atomic config write: write to temp file first, then rename.
|
|
764
|
+
// If the write fails, the original config (if any) is preserved.
|
|
663
765
|
const config: any = {
|
|
664
766
|
system: existingConfig?.system || {
|
|
665
767
|
defaultProjectDir: os.homedir(),
|
|
666
768
|
idleTimeoutMinutes: 30,
|
|
667
769
|
maxReplyLength: 140000,
|
|
668
770
|
},
|
|
771
|
+
workspace: {
|
|
772
|
+
mode: workspaceMode,
|
|
773
|
+
globalPath: workspaceGlobalPath,
|
|
774
|
+
botOverrides: {},
|
|
775
|
+
},
|
|
669
776
|
providers,
|
|
670
777
|
defaultModel,
|
|
671
778
|
modelAliases: existingConfig?.modelAliases || buildDefaultAliases(defaultModel),
|
|
@@ -694,19 +801,42 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
694
801
|
bots,
|
|
695
802
|
};
|
|
696
803
|
|
|
697
|
-
|
|
698
|
-
|
|
804
|
+
// Write to temp file first for atomicity
|
|
805
|
+
const configTmpPath = configPath + '.tmp';
|
|
806
|
+
const providersTmpPath = path.join(dataDir, 'providers.json.tmp');
|
|
807
|
+
let writeOk = true;
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
fs.writeFileSync(configTmpPath, JSON.stringify(config, null, 2) + '\n');
|
|
811
|
+
fs.renameSync(configTmpPath, configPath);
|
|
812
|
+
console.log(`✅ ${configPath}`);
|
|
813
|
+
} catch (e: any) {
|
|
814
|
+
console.error(`❌ Failed to write config.json: ${e.message}`);
|
|
815
|
+
writeOk = false;
|
|
816
|
+
}
|
|
699
817
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
818
|
+
if (writeOk) {
|
|
819
|
+
try {
|
|
820
|
+
const providersFile: any = { providers, defaultModel, modelAliases: config.modelAliases };
|
|
821
|
+
const providersPath = path.join(dataDir, 'providers.json');
|
|
822
|
+
fs.writeFileSync(providersTmpPath, JSON.stringify(providersFile, null, 2) + '\n');
|
|
823
|
+
fs.renameSync(providersTmpPath, providersPath);
|
|
824
|
+
console.log(`✅ ${providersPath}`);
|
|
825
|
+
} catch (e: any) {
|
|
826
|
+
console.error(`❌ Failed to write providers.json: ${e.message}`);
|
|
827
|
+
console.error('⚠️ config.json was written successfully, but providers.json failed.');
|
|
828
|
+
console.error(' Please re-run "imtoagent setup" to fix.');
|
|
829
|
+
writeOk = false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
704
832
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
833
|
+
if (writeOk) {
|
|
834
|
+
const opencodePath = path.join(dataDir, 'opencode.json');
|
|
835
|
+
const opencodeTemplate = getTemplatePath('opencode.template.json');
|
|
836
|
+
if (fs.existsSync(opencodeTemplate)) {
|
|
837
|
+
fs.writeFileSync(opencodePath, fs.readFileSync(opencodeTemplate, 'utf-8'));
|
|
838
|
+
console.log(`✅ ${opencodePath}`);
|
|
839
|
+
}
|
|
710
840
|
}
|
|
711
841
|
|
|
712
842
|
fs.mkdirSync(path.join(dataDir, 'sessions'), { recursive: true });
|
|
@@ -717,9 +847,10 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
717
847
|
console.log('\n╔══════════════════════════════════════════════╗');
|
|
718
848
|
console.log('║ ✅ Configuration complete! ║');
|
|
719
849
|
console.log('╚══════════════════════════════════════════════╝\n');
|
|
720
|
-
console.log(`
|
|
850
|
+
console.log(`Bots: ${bots.map(b => b.name).join(', ')}`);
|
|
721
851
|
console.log(`Default model: ${defaultModel}`);
|
|
722
852
|
console.log(`Providers: ${Object.keys(providers).join(', ') || 'None'}`);
|
|
853
|
+
console.log(`Workspace: ${workspaceMode === 'sandbox' ? 'sandbox (isolated)' : `global (${workspaceGlobalPath})`}`);
|
|
723
854
|
console.log(`\nNext steps:`);
|
|
724
855
|
console.log(` imtoagent start Start the gateway`);
|
|
725
856
|
console.log(` imtoagent status Check status\n`);
|