imtoagent 0.3.3 → 0.3.5
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/README.md +97 -97
- package/bin/imtoagent-real +96 -96
- package/bin/imtoagent.cjs +1 -1
- package/index.ts +106 -106
- package/modules/agent/claude-adapter.ts +6 -6
- package/modules/agent/claude.ts +6 -6
- package/modules/agent/codex-adapter.ts +13 -13
- package/modules/agent/codex-exec-server.ts +11 -11
- package/modules/agent/codex.ts +29 -29
- package/modules/agent/opencode-adapter.ts +17 -17
- package/modules/agent/opencode.ts +10 -10
- package/modules/capabilities.ts +33 -33
- package/modules/cli/setup.ts +171 -163
- package/modules/core/config.ts +5 -5
- package/modules/core/error.ts +8 -8
- package/modules/core/runtime.ts +10 -10
- package/modules/core/session.ts +4 -4
- package/modules/core/stats.ts +14 -14
- package/modules/core/types.ts +7 -7
- package/modules/im/feishu.ts +56 -56
- package/modules/im/telegram.ts +23 -23
- package/modules/im/wechat.ts +54 -54
- package/modules/im/wecom.ts +50 -50
- package/modules/media/feishu-inbound-adapter.ts +4 -4
- package/modules/media/resolver.ts +11 -11
- package/modules/media/telegram-inbound-adapter.ts +8 -8
- package/modules/prompt-builder.ts +12 -12
- package/modules/proxy/anthropic-proxy.ts +39 -28
- package/modules/proxy/codex-proxy.ts +18 -18
- package/modules/utils/backend-check.ts +12 -12
- package/modules/utils/paths.ts +8 -8
- package/package.json +1 -1
- package/scripts/postinstall.cjs +10 -10
- package/scripts/postinstall.ts +13 -13
- package/templates/soul.template/identity.md +5 -5
- package/templates/soul.template/profile.md +7 -7
- package/templates/soul.template/rules.md +5 -5
- package/templates/soul.template/skills.md +2 -2
- package/templates/soul.template/workspace.md +3 -3
package/modules/cli/setup.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// ================================================================
|
|
2
|
-
// setup.ts —
|
|
2
|
+
// setup.ts — Interactive Setup Wizard (v2)
|
|
3
3
|
// ================================================================
|
|
4
|
-
//
|
|
5
|
-
// ↑↓
|
|
6
|
-
//
|
|
7
|
-
// ESC —
|
|
4
|
+
// Interaction:
|
|
5
|
+
// ↑↓ or Space — navigate options
|
|
6
|
+
// Enter — confirm selection
|
|
7
|
+
// ESC — go back
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// Supported IM platforms: Feishu / Telegram / WeCom / WeChat
|
|
10
10
|
// ================================================================
|
|
11
11
|
|
|
12
12
|
import * as fs from 'fs';
|
|
@@ -17,7 +17,7 @@ import { randomUUID } from 'crypto';
|
|
|
17
17
|
import { checkAllBackends, formatBackendStatus } from '../utils/backend-check';
|
|
18
18
|
|
|
19
19
|
// ================================================================
|
|
20
|
-
//
|
|
20
|
+
// Keyboard input (raw mode)
|
|
21
21
|
// ================================================================
|
|
22
22
|
|
|
23
23
|
const KEY = {
|
|
@@ -29,7 +29,7 @@ const KEY = {
|
|
|
29
29
|
BACKSPACE: '\x7f',
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/** Read a single keypress */
|
|
33
33
|
function readKey(): Promise<string> {
|
|
34
34
|
return new Promise((resolve) => {
|
|
35
35
|
const onData = (data: Buffer) => {
|
|
@@ -43,23 +43,22 @@ function readKey(): Promise<string> {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// ================================================================
|
|
46
|
-
//
|
|
46
|
+
// Menu selection (↑↓/Space navigate, Enter confirm)
|
|
47
47
|
// ================================================================
|
|
48
48
|
|
|
49
49
|
async function selectMenu(title: string, options: string[]): Promise<number> {
|
|
50
50
|
let idx = 0;
|
|
51
|
-
const linesAbove = options.length + 2;
|
|
52
51
|
|
|
53
52
|
function render() {
|
|
54
|
-
//
|
|
55
|
-
process.stdout.write('\x1B[0G'); //
|
|
53
|
+
// Clear previous output
|
|
54
|
+
process.stdout.write('\x1B[0G'); // Return to line start
|
|
56
55
|
options.forEach((opt, i) => {
|
|
57
56
|
const prefix = i === idx ? '▸ ' : ' ';
|
|
58
57
|
process.stdout.write(`\x1B[0G${prefix}${opt}\x1B[0K\n`);
|
|
59
58
|
});
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
//
|
|
61
|
+
// Show title
|
|
63
62
|
console.log(title);
|
|
64
63
|
render();
|
|
65
64
|
|
|
@@ -71,15 +70,15 @@ async function selectMenu(title: string, options: string[]): Promise<number> {
|
|
|
71
70
|
const key = await readKey();
|
|
72
71
|
|
|
73
72
|
if (key === KEY.UP || key === KEY.DOWN || key === KEY.SPACE) {
|
|
74
|
-
//
|
|
73
|
+
// Move cursor
|
|
75
74
|
idx = (idx + (key === KEY.UP ? -1 : 1) + options.length) % options.length;
|
|
76
|
-
//
|
|
77
|
-
process.stdout.write(`\x1B[${options.length}A`); //
|
|
75
|
+
// Redraw all options
|
|
76
|
+
process.stdout.write(`\x1B[${options.length}A`); // Move up N lines
|
|
78
77
|
render();
|
|
79
78
|
} else if (key === KEY.ENTER) {
|
|
80
79
|
break;
|
|
81
80
|
} else if (key === KEY.ESC) {
|
|
82
|
-
return -1; // ESC =
|
|
81
|
+
return -1; // ESC = go back
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
} finally {
|
|
@@ -92,7 +91,7 @@ async function selectMenu(title: string, options: string[]): Promise<number> {
|
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
// ================================================================
|
|
95
|
-
//
|
|
94
|
+
// Text input (Enter confirm, ESC returns -1)
|
|
96
95
|
// ================================================================
|
|
97
96
|
|
|
98
97
|
async function promptText(label: string, defaultValue = ''): Promise<string> {
|
|
@@ -111,19 +110,19 @@ async function promptText(label: string, defaultValue = ''): Promise<string> {
|
|
|
111
110
|
break;
|
|
112
111
|
} else if (key === KEY.ESC) {
|
|
113
112
|
process.stdout.write('\x1B[0K\n');
|
|
114
|
-
return -1 as unknown as string; //
|
|
113
|
+
return -1 as unknown as string; // Special return value for ESC
|
|
115
114
|
} else if (key === KEY.BACKSPACE) {
|
|
116
115
|
if (buf.length > 0) {
|
|
117
116
|
buf.pop();
|
|
118
|
-
process.stdout.write('\x1B[1D \x1B[1D'); //
|
|
117
|
+
process.stdout.write('\x1B[1D \x1B[1D'); // Backspace delete
|
|
119
118
|
}
|
|
120
119
|
} else if (key === KEY.UP || key === KEY.DOWN) {
|
|
121
|
-
//
|
|
120
|
+
// Ignore arrow keys
|
|
122
121
|
} else if (key === KEY.SPACE) {
|
|
123
122
|
buf.push(' ');
|
|
124
123
|
process.stdout.write(' ');
|
|
125
124
|
} else if (key.length >= 1 && !key.startsWith('\x1b')) {
|
|
126
|
-
//
|
|
125
|
+
// Normal characters / pasted multi-char blocks (text without escape sequences)
|
|
127
126
|
buf.push(key);
|
|
128
127
|
process.stdout.write(key);
|
|
129
128
|
}
|
|
@@ -139,7 +138,7 @@ async function promptText(label: string, defaultValue = ''): Promise<string> {
|
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
// ================================================================
|
|
142
|
-
//
|
|
141
|
+
// Confirmation (Y/N, Enter confirm, ESC returns -1)
|
|
143
142
|
// ================================================================
|
|
144
143
|
|
|
145
144
|
async function confirm(label: string, defaultYes = true): Promise<boolean | -1> {
|
|
@@ -172,7 +171,7 @@ async function confirm(label: string, defaultYes = true): Promise<boolean | -1>
|
|
|
172
171
|
}
|
|
173
172
|
|
|
174
173
|
// ================================================================
|
|
175
|
-
//
|
|
174
|
+
// Provider presets
|
|
176
175
|
// ================================================================
|
|
177
176
|
|
|
178
177
|
interface ProviderPreset {
|
|
@@ -180,12 +179,12 @@ interface ProviderPreset {
|
|
|
180
179
|
baseUrl: string;
|
|
181
180
|
format: 'openai' | 'anthropic';
|
|
182
181
|
models: string[];
|
|
183
|
-
hint?: string; //
|
|
182
|
+
hint?: string; // Additional note
|
|
184
183
|
}
|
|
185
184
|
|
|
186
185
|
const PROVIDER_PRESETS: ProviderPreset[] = [
|
|
187
186
|
{
|
|
188
|
-
name: 'DashScope
|
|
187
|
+
name: 'DashScope (Alibaba Bailian)',
|
|
189
188
|
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
190
189
|
format: 'openai',
|
|
191
190
|
models: ['qwen-max', 'qwen-plus', 'qwen-turbo'],
|
|
@@ -197,7 +196,7 @@ const PROVIDER_PRESETS: ProviderPreset[] = [
|
|
|
197
196
|
models: ['deepseek-chat', 'deepseek-reasoner'],
|
|
198
197
|
},
|
|
199
198
|
{
|
|
200
|
-
name: '
|
|
199
|
+
name: 'Zhipu AI (Zhipu)',
|
|
201
200
|
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
|
202
201
|
format: 'openai',
|
|
203
202
|
models: ['glm-4-plus', 'glm-4-flash', 'glm-4'],
|
|
@@ -209,13 +208,13 @@ const PROVIDER_PRESETS: ProviderPreset[] = [
|
|
|
209
208
|
models: ['MiniMax-M2.5', 'MiniMax-M1'],
|
|
210
209
|
},
|
|
211
210
|
{
|
|
212
|
-
name: '
|
|
211
|
+
name: 'SiliconFlow',
|
|
213
212
|
baseUrl: 'https://api.siliconflow.cn/v1',
|
|
214
213
|
format: 'openai',
|
|
215
214
|
models: ['Qwen/Qwen2.5-72B-Instruct', 'deepseek-ai/DeepSeek-V3'],
|
|
216
215
|
},
|
|
217
216
|
{
|
|
218
|
-
name: 'Moonshot
|
|
217
|
+
name: 'Moonshot (Moonshot AI)',
|
|
219
218
|
baseUrl: 'https://api.moonshot.cn/v1',
|
|
220
219
|
format: 'openai',
|
|
221
220
|
models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'],
|
|
@@ -225,31 +224,31 @@ const PROVIDER_PRESETS: ProviderPreset[] = [
|
|
|
225
224
|
baseUrl: 'https://api.openai.com/v1',
|
|
226
225
|
format: 'openai',
|
|
227
226
|
models: ['gpt-4o', 'gpt-4o-mini', 'o3', 'o4-mini'],
|
|
228
|
-
hint: '
|
|
227
|
+
hint: 'Proxy required to access',
|
|
229
228
|
},
|
|
230
229
|
{
|
|
231
230
|
name: 'Anthropic',
|
|
232
231
|
baseUrl: 'https://api.anthropic.com',
|
|
233
232
|
format: 'anthropic',
|
|
234
233
|
models: ['claude-sonnet-4-20250514', 'claude-haiku-4-20250514', 'claude-opus-4-20250514'],
|
|
235
|
-
hint: '
|
|
234
|
+
hint: 'Proxy required to access',
|
|
236
235
|
},
|
|
237
236
|
{
|
|
238
|
-
name: 'Gemini
|
|
237
|
+
name: 'Gemini (Google)',
|
|
239
238
|
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
240
239
|
format: 'openai',
|
|
241
240
|
models: ['gemini-2.5-pro', 'gemini-2.5-flash'],
|
|
242
|
-
hint: '
|
|
241
|
+
hint: 'Proxy required to access',
|
|
243
242
|
},
|
|
244
243
|
{
|
|
245
|
-
name: 'xAI
|
|
244
|
+
name: 'xAI (Grok)',
|
|
246
245
|
baseUrl: 'https://api.x.ai/v1',
|
|
247
246
|
format: 'openai',
|
|
248
247
|
models: ['grok-3', 'grok-3-mini'],
|
|
249
|
-
hint: '
|
|
248
|
+
hint: 'Requires proxy to access',
|
|
250
249
|
},
|
|
251
250
|
{
|
|
252
|
-
name: 'Ollama
|
|
251
|
+
name: 'Ollama (Local)',
|
|
253
252
|
baseUrl: 'http://localhost:11434/v1',
|
|
254
253
|
format: 'openai',
|
|
255
254
|
models: ['qwen2.5', 'llama3.2', 'deepseek-r1'],
|
|
@@ -257,38 +256,38 @@ const PROVIDER_PRESETS: ProviderPreset[] = [
|
|
|
257
256
|
];
|
|
258
257
|
|
|
259
258
|
// ================================================================
|
|
260
|
-
// IM
|
|
259
|
+
// IM platform configuration
|
|
261
260
|
// ================================================================
|
|
262
261
|
|
|
263
262
|
const IM_PLATFORMS = [
|
|
264
|
-
{ value: 'feishu', label: '
|
|
265
|
-
{ value: 'telegram', label: 'Telegram', desc: '
|
|
266
|
-
{ value: 'wecom', label: '
|
|
267
|
-
{ value: 'wechat', label: '
|
|
263
|
+
{ value: 'feishu', label: 'Feishu', desc: 'WebSocket long-lived connection' },
|
|
264
|
+
{ value: 'telegram', label: 'Telegram', desc: 'Long polling' },
|
|
265
|
+
{ value: 'wecom', label: 'WeCom', desc: 'QR scan binding + WebSocket' },
|
|
266
|
+
{ value: 'wechat', label: 'WeChat', desc: 'iLink + QR scan' },
|
|
268
267
|
];
|
|
269
268
|
|
|
270
|
-
/**
|
|
269
|
+
/** Credential fields required by each IM type */
|
|
271
270
|
const IM_FIELDS: Record<string, { key: string; label: string; required: boolean }[]> = {
|
|
272
271
|
feishu: [
|
|
273
|
-
{ key: 'appId', label: '
|
|
274
|
-
{ key: 'appSecret', label: '
|
|
272
|
+
{ key: 'appId', label: 'Feishu App ID (cli_...)', required: true },
|
|
273
|
+
{ key: 'appSecret', label: 'Feishu App Secret', required: true },
|
|
275
274
|
],
|
|
276
275
|
telegram: [
|
|
277
276
|
{ key: 'appId', label: 'Bot Token', required: true },
|
|
278
|
-
{ key: 'proxy', label: '
|
|
277
|
+
{ key: 'proxy', label: 'Proxy URL (leave blank to skip)', required: false },
|
|
279
278
|
],
|
|
280
279
|
wecom: [
|
|
281
|
-
//
|
|
280
|
+
// WeCom uses QR scan binding, no pre-filled credentials needed, QR scan triggered automatically on startup
|
|
282
281
|
],
|
|
283
282
|
wechat: [
|
|
284
|
-
//
|
|
285
|
-
{ key: 'botId', label: 'iLink Bot ID
|
|
286
|
-
{ key: 'botToken', label: 'iLink Bot Token
|
|
283
|
+
// WeChat authenticates via iLink + QR scan, no pre-filled credentials needed
|
|
284
|
+
{ key: 'botId', label: 'iLink Bot ID (leave blank for QR scan)', required: false },
|
|
285
|
+
{ key: 'botToken', label: 'iLink Bot Token (leave blank for QR scan)', required: false },
|
|
287
286
|
],
|
|
288
287
|
};
|
|
289
288
|
|
|
290
289
|
// ================================================================
|
|
291
|
-
//
|
|
290
|
+
// Main flow
|
|
292
291
|
// ================================================================
|
|
293
292
|
|
|
294
293
|
export async function runSetupWizard(): Promise<void> {
|
|
@@ -296,123 +295,123 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
296
295
|
const configPath = path.join(dataDir, 'config.json');
|
|
297
296
|
|
|
298
297
|
console.log('\n╔══════════════════════════════════════════════╗');
|
|
299
|
-
console.log('║ 🚀 imtoagent
|
|
298
|
+
console.log('║ 🚀 imtoagent Setup Wizard ║');
|
|
300
299
|
console.log('╚══════════════════════════════════════════════╝');
|
|
301
|
-
console.log(`\
|
|
302
|
-
console.log(
|
|
300
|
+
console.log(`\nData directory: ${dataDir}`);
|
|
301
|
+
console.log(`Controls: ↑↓/Space navigate | Enter confirm | ESC go back\n`);
|
|
303
302
|
|
|
304
|
-
// ===== Step 1:
|
|
303
|
+
// ===== Step 1: Detect existing configuration =====
|
|
305
304
|
let existingConfig: any = null;
|
|
306
305
|
let mergeMode = false;
|
|
307
306
|
|
|
308
307
|
if (fs.existsSync(configPath)) {
|
|
309
|
-
console.log('📋
|
|
310
|
-
const idx = await selectMenu('
|
|
308
|
+
console.log('📋 Existing configuration detected\n');
|
|
309
|
+
const idx = await selectMenu('Choose action', ['Overwrite existing config', 'Merge (keep existing Bots)', 'Exit']);
|
|
311
310
|
if (idx === -1) return; // ESC
|
|
312
|
-
if (idx === 2) { console.log('👋
|
|
311
|
+
if (idx === 2) { console.log('👋 Cancelled'); process.exit(0); }
|
|
313
312
|
if (idx === 1) mergeMode = true;
|
|
314
313
|
|
|
315
314
|
try {
|
|
316
315
|
existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
317
316
|
} catch {
|
|
318
|
-
console.log('⚠️
|
|
317
|
+
console.log('⚠️ Failed to parse existing config, will regenerate');
|
|
319
318
|
existingConfig = null;
|
|
320
319
|
mergeMode = false;
|
|
321
320
|
}
|
|
322
321
|
}
|
|
323
322
|
|
|
324
|
-
// ===== Step 2:
|
|
325
|
-
console.log('\n📌
|
|
323
|
+
// ===== Step 2: Detect backends =====
|
|
324
|
+
console.log('\n📌 Detecting backend agents...\n');
|
|
326
325
|
const backendStatus = checkAllBackends();
|
|
327
326
|
console.log(formatBackendStatus(backendStatus));
|
|
328
327
|
|
|
329
328
|
const installedBackends = backendStatus.filter(b => b.installed);
|
|
330
329
|
if (installedBackends.length === 0) {
|
|
331
|
-
console.log('\n⚠️
|
|
332
|
-
console.log('
|
|
330
|
+
console.log('\n⚠️ No backend agents detected.');
|
|
331
|
+
console.log('Recommended installs:');
|
|
333
332
|
console.log(' npm install -g @anthropic-ai/claude-agent-sdk # Claude Code');
|
|
334
333
|
console.log(' npm install -g @openai/codex # Codex');
|
|
335
334
|
console.log(' npm install -g opencode # OpenCode');
|
|
336
|
-
const r = await confirm('
|
|
337
|
-
if (r === false || r === -1) { console.log('\n👋
|
|
338
|
-
console.log('\n⚠️
|
|
335
|
+
const r = await confirm('Continue configuring? (Messaging will fail until backends are installed)', false);
|
|
336
|
+
if (r === false || r === -1) { console.log('\n👋 Run "imtoagent setup" again after installing backends'); process.exit(0); }
|
|
337
|
+
console.log('\n⚠️ Skipped, you can install backends later.\n');
|
|
339
338
|
} else {
|
|
340
|
-
console.log(`\n✅
|
|
339
|
+
console.log(`\n✅ Installed: ${installedBackends.map(b => b.label).join(', ')}\n`);
|
|
341
340
|
}
|
|
342
341
|
|
|
343
|
-
// ===== Step 3:
|
|
344
|
-
console.log('📌 Step 3:
|
|
342
|
+
// ===== Step 3: Configure Bots =====
|
|
343
|
+
console.log('📌 Step 3: Configure Bots\n');
|
|
345
344
|
|
|
346
345
|
const bots: any[] = (mergeMode && existingConfig?.bots) ? [...existingConfig.bots] : [];
|
|
347
346
|
|
|
348
347
|
let addingBots = true;
|
|
349
348
|
while (addingBots) {
|
|
350
|
-
console.log(`\n---
|
|
349
|
+
console.log(`\n--- Add New Bot (${bots.length} existing) ---\n`);
|
|
351
350
|
|
|
352
|
-
// 3a:
|
|
351
|
+
// 3a: Select IM platform
|
|
353
352
|
const imLabels = IM_PLATFORMS.map(p => `${p.label} ${p.desc}`);
|
|
354
|
-
const imIdx = await selectMenu('
|
|
353
|
+
const imIdx = await selectMenu('Select IM platform', imLabels);
|
|
355
354
|
if (imIdx === -1) { if (bots.length === 0) return; break; } // ESC
|
|
356
355
|
const imType = IM_PLATFORMS[imIdx].value;
|
|
357
356
|
|
|
358
|
-
// 3b:
|
|
357
|
+
// 3b: Auto-generate Bot name, customizable
|
|
359
358
|
const defaultName = IM_PLATFORMS[imIdx].label + 'Bot';
|
|
360
|
-
const nameInput = await promptText('Bot
|
|
359
|
+
const nameInput = await promptText('Bot name', defaultName);
|
|
361
360
|
if ((nameInput as any) === -1) { if (bots.length === 0) return; break; } // ESC
|
|
362
|
-
const botName = nameInput || defaultName; //
|
|
361
|
+
const botName = nameInput || defaultName; // Use default if empty
|
|
363
362
|
|
|
364
|
-
// 3c:
|
|
363
|
+
// 3c: Select backend
|
|
365
364
|
const backendLabels = backendStatus.map(b =>
|
|
366
|
-
b.installed ? `${b.label} (v${b.version})` : `${b.label} ⚠️
|
|
365
|
+
b.installed ? `${b.label} (v${b.version})` : `${b.label} ⚠️ Not installed`
|
|
367
366
|
);
|
|
368
|
-
const backendIdx = await selectMenu('
|
|
369
|
-
if (backendIdx === -1) continue; // ESC
|
|
367
|
+
const backendIdx = await selectMenu('Select backend', backendLabels);
|
|
368
|
+
if (backendIdx === -1) continue; // ESC go back to IM selection
|
|
370
369
|
const backend = backendStatus[backendIdx].type;
|
|
371
370
|
const isBackendInstalled = backendStatus[backendIdx].installed;
|
|
372
371
|
|
|
373
|
-
//
|
|
372
|
+
// Backend not installed → prompt for auto-install
|
|
374
373
|
if (!isBackendInstalled) {
|
|
375
374
|
const installCmd = backendStatus[backendIdx].installHint || '';
|
|
376
|
-
console.log(`\n⚠️ ${backend}
|
|
375
|
+
console.log(`\n⚠️ ${backend} not installed`);
|
|
377
376
|
console.log(` ${installCmd}\n`);
|
|
378
|
-
const r = await confirm('
|
|
377
|
+
const r = await confirm('Auto-install now?');
|
|
379
378
|
if (r === true) {
|
|
380
379
|
const { installBackend } = await import('../utils/backend-check');
|
|
381
380
|
const ok = await installBackend(backend as 'claude' | 'codex' | 'opencode');
|
|
382
381
|
if (ok) {
|
|
383
|
-
console.log(`✅ ${backend}
|
|
382
|
+
console.log(`✅ ${backend} installed\n`);
|
|
384
383
|
} else {
|
|
385
|
-
console.log(`⚠️
|
|
386
|
-
const r2 = await confirm('
|
|
384
|
+
console.log(`⚠️ Installation failed, run manually later: ${installCmd}\n`);
|
|
385
|
+
const r2 = await confirm('Continue configuring this Bot anyway?');
|
|
387
386
|
if (r2 === false) continue;
|
|
388
387
|
}
|
|
389
388
|
} else if (r === -1) {
|
|
390
|
-
continue; // ESC
|
|
389
|
+
continue; // ESC go back
|
|
391
390
|
} else {
|
|
392
|
-
console.log('
|
|
391
|
+
console.log('Skipping installation\n');
|
|
393
392
|
}
|
|
394
393
|
}
|
|
395
394
|
|
|
396
|
-
// 3d:
|
|
397
|
-
console.log(`\n--- ${IM_PLATFORMS.find(p => p.value === imType)?.label}
|
|
395
|
+
// 3d: Collect credentials based on IM type
|
|
396
|
+
console.log(`\n--- ${IM_PLATFORMS.find(p => p.value === imType)?.label} credentials ---`);
|
|
398
397
|
const fields = IM_FIELDS[imType] || [];
|
|
399
398
|
const credentials: Record<string, string> = {};
|
|
400
399
|
|
|
401
400
|
for (const field of fields) {
|
|
402
|
-
const val = await promptText(field.label + (field.required ? '' : '
|
|
401
|
+
const val = await promptText(field.label + (field.required ? '' : ' (optional)'));
|
|
403
402
|
if ((val as any) === -1) { credentials._escaped = 'true'; break; } // ESC
|
|
404
403
|
credentials[field.key] = val;
|
|
405
404
|
}
|
|
406
|
-
if (credentials._escaped) continue; // ESC
|
|
405
|
+
if (credentials._escaped) continue; // ESC go back and re-select backend
|
|
407
406
|
|
|
408
|
-
// 3e:
|
|
409
|
-
const cwd = await promptText('
|
|
407
|
+
// 3e: Working directory
|
|
408
|
+
const cwd = await promptText('Working directory', os.homedir());
|
|
410
409
|
if ((cwd as any) === -1) continue;
|
|
411
410
|
|
|
412
|
-
//
|
|
411
|
+
// Generate unique ID (UUID, for directory isolation, renaming doesn't affect it)
|
|
413
412
|
const botId = randomUUID();
|
|
414
413
|
|
|
415
|
-
//
|
|
414
|
+
// Build Bot configuration (different IM types need different fields)
|
|
416
415
|
const bot: any = {
|
|
417
416
|
id: botId,
|
|
418
417
|
name: botName,
|
|
@@ -420,141 +419,147 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
420
419
|
cwd: cwd || os.homedir(),
|
|
421
420
|
};
|
|
422
421
|
|
|
423
|
-
//
|
|
422
|
+
// Feishu needs appId + appSecret
|
|
424
423
|
if (imType === 'feishu') {
|
|
425
424
|
bot.appId = credentials.appId || '';
|
|
426
425
|
bot.appSecret = credentials.appSecret || '';
|
|
427
426
|
}
|
|
428
|
-
// Telegram
|
|
427
|
+
// Telegram needs appId (Bot Token), optional proxy
|
|
429
428
|
else if (imType === 'telegram') {
|
|
430
429
|
bot.appId = credentials.appId || '';
|
|
431
430
|
if (credentials.proxy) bot.proxy = credentials.proxy;
|
|
432
431
|
}
|
|
433
|
-
//
|
|
432
|
+
// WeCom: QR scan binding, no pre-filled credentials (optional botId/secret)
|
|
434
433
|
else if (imType === 'wecom') {
|
|
435
434
|
bot.im = 'wecom';
|
|
436
435
|
if (credentials.botId) bot.botId = credentials.botId;
|
|
437
436
|
if (credentials.secret) bot.secret = credentials.secret;
|
|
438
437
|
}
|
|
439
|
-
//
|
|
438
|
+
// WeChat: optional botId/botToken, QR scan if left blank
|
|
440
439
|
else if (imType === 'wechat') {
|
|
441
440
|
bot.im = 'wechat';
|
|
442
441
|
if (credentials.botId) bot.botId = credentials.botId;
|
|
443
442
|
if (credentials.botToken) bot.botToken = credentials.botToken;
|
|
444
443
|
if (credentials.ilinkUserId) bot.ilinkUserId = credentials.ilinkUserId;
|
|
445
444
|
}
|
|
446
|
-
//
|
|
445
|
+
// Default: non-Feishu platforms add im field
|
|
447
446
|
else {
|
|
448
447
|
bot.im = imType;
|
|
449
448
|
}
|
|
450
449
|
|
|
451
|
-
//
|
|
450
|
+
// Check for duplicate name
|
|
452
451
|
const existingIdx = bots.findIndex(b => b.name === botName);
|
|
453
452
|
if (existingIdx >= 0) {
|
|
454
453
|
bots[existingIdx] = bot;
|
|
455
|
-
console.log(`✅
|
|
454
|
+
console.log(`✅ Replaced: ${botName}`);
|
|
456
455
|
} else {
|
|
457
456
|
bots.push(bot);
|
|
458
|
-
console.log(`✅
|
|
457
|
+
console.log(`✅ Added: ${botName}`);
|
|
459
458
|
}
|
|
460
459
|
|
|
461
|
-
//
|
|
462
|
-
const r = await confirm('
|
|
463
|
-
if (r === -1) addingBots = false; // ESC =
|
|
460
|
+
// Whether to continue adding
|
|
461
|
+
const r = await confirm('Add another Bot?', true);
|
|
462
|
+
if (r === -1) addingBots = false; // ESC = done adding, proceed to next step
|
|
464
463
|
else addingBots = (r === true);
|
|
465
464
|
}
|
|
466
465
|
|
|
467
466
|
if (bots.length === 0) {
|
|
468
|
-
console.log('\n⚠️
|
|
469
|
-
const r = await confirm('
|
|
467
|
+
console.log('\n⚠️ No Bots configured.');
|
|
468
|
+
const r = await confirm('Configure at least one Bot?');
|
|
470
469
|
if (r === true) return runSetupWizard();
|
|
471
|
-
console.log('\n⚠️
|
|
470
|
+
console.log('\n⚠️ At least one Bot required, configuration cancelled');
|
|
472
471
|
return;
|
|
473
472
|
}
|
|
474
473
|
|
|
475
|
-
// ===== Step 4:
|
|
476
|
-
console.log('\n📌 Step 4:
|
|
474
|
+
// ===== Step 4: Configure model providers =====
|
|
475
|
+
console.log('\n📌 Step 4: Configure model providers\n');
|
|
477
476
|
|
|
478
477
|
const providers: Record<string, any> = {};
|
|
479
478
|
if (mergeMode && existingConfig?.providers) {
|
|
480
479
|
Object.assign(providers, existingConfig.providers);
|
|
481
|
-
console.log(`✅
|
|
480
|
+
console.log(`✅ Kept ${Object.keys(providers).length} existing provider(s)\n`);
|
|
482
481
|
}
|
|
483
482
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
483
|
+
// Step 4 outer loop: ensure at least one provider (exit when user explicitly skips)
|
|
484
|
+
let step4Loop = true;
|
|
485
|
+
while (step4Loop) {
|
|
486
|
+
let addingProviders = true;
|
|
487
|
+
while (addingProviders) {
|
|
488
|
+
console.log('--- Add new provider ---\n');
|
|
487
489
|
|
|
488
|
-
//
|
|
490
|
+
// Choose preset or custom
|
|
489
491
|
const presetOptions = PROVIDER_PRESETS.map(p => {
|
|
490
492
|
const tag = p.hint ? ` ${p.hint}` : '';
|
|
491
493
|
return `${p.name}${tag}`;
|
|
492
494
|
});
|
|
493
|
-
presetOptions.push('
|
|
495
|
+
presetOptions.push('Custom...');
|
|
494
496
|
|
|
495
|
-
const presetIdx = await selectMenu('
|
|
497
|
+
const presetIdx = await selectMenu('Select provider', presetOptions);
|
|
496
498
|
if (presetIdx === -1) { addingProviders = false; continue; }
|
|
497
499
|
|
|
498
500
|
let provName: string, baseUrl: string, format: 'openai' | 'anthropic', models: string[];
|
|
499
501
|
|
|
500
502
|
if (presetIdx < PROVIDER_PRESETS.length) {
|
|
501
|
-
//
|
|
503
|
+
// Use preset
|
|
502
504
|
const preset = PROVIDER_PRESETS[presetIdx];
|
|
503
|
-
provName = preset.name.split('
|
|
505
|
+
provName = preset.name.split('(')[0].trim().toLowerCase(); // Take short name
|
|
504
506
|
baseUrl = preset.baseUrl;
|
|
505
507
|
format = preset.format;
|
|
506
508
|
models = [...preset.models];
|
|
507
509
|
|
|
508
|
-
console.log(`\n✅
|
|
509
|
-
console.log(`
|
|
510
|
+
console.log(`\n✅ Preset loaded:`);
|
|
511
|
+
console.log(` Name: ${provName}`);
|
|
510
512
|
console.log(` URL: ${preset.baseUrl}`);
|
|
511
|
-
console.log(`
|
|
512
|
-
console.log(`
|
|
513
|
+
console.log(` Format: ${preset.format}`);
|
|
514
|
+
console.log(` Models: ${preset.models.join(', ')}\n`);
|
|
513
515
|
|
|
514
|
-
//
|
|
515
|
-
const nameEdit = await promptText('
|
|
516
|
+
// Confirm/edit short name
|
|
517
|
+
const nameEdit = await promptText('Provider name (leave blank to confirm)', provName);
|
|
516
518
|
if ((nameEdit as any) === -1) continue;
|
|
517
519
|
provName = nameEdit || provName;
|
|
518
520
|
|
|
519
|
-
//
|
|
521
|
+
// Confirm/edit Base URL
|
|
520
522
|
const urlEdit = await promptText('Base URL', baseUrl);
|
|
521
523
|
if ((urlEdit as any) === -1) continue;
|
|
522
524
|
baseUrl = urlEdit || baseUrl;
|
|
523
525
|
|
|
524
|
-
//
|
|
525
|
-
const modelsEdit = await promptText('
|
|
526
|
+
// Confirm/edit model list
|
|
527
|
+
const modelsEdit = await promptText('Model list (comma-separated)', models.join(', '));
|
|
526
528
|
if ((modelsEdit as any) === -1) continue;
|
|
527
529
|
if (modelsEdit) models = modelsEdit.split(',').map(s => s.trim()).filter(Boolean);
|
|
528
530
|
|
|
529
531
|
if (providers[provName]) {
|
|
530
|
-
console.log(`⚠️
|
|
532
|
+
console.log(`⚠️ Provider "${provName}" already exists, will overwrite\n`);
|
|
531
533
|
}
|
|
532
534
|
} else {
|
|
533
|
-
//
|
|
534
|
-
provName = await promptText('
|
|
535
|
+
// Custom
|
|
536
|
+
provName = await promptText('Provider name (e.g. deepseek, dashscope)');
|
|
535
537
|
if ((provName as any) === -1) { addingProviders = false; continue; }
|
|
536
538
|
if (!provName) { addingProviders = false; continue; }
|
|
537
539
|
if (providers[provName]) {
|
|
538
|
-
console.log(`⚠️
|
|
540
|
+
console.log(`⚠️ Provider "${provName}" already exists, will overwrite\n`);
|
|
539
541
|
}
|
|
540
542
|
|
|
541
|
-
baseUrl = await promptText('Base URL (
|
|
543
|
+
baseUrl = await promptText('Base URL (e.g. https://api.deepseek.com/v1)');
|
|
542
544
|
if ((baseUrl as any) === -1) continue;
|
|
543
|
-
const modelsStr = await promptText('
|
|
545
|
+
const modelsStr = await promptText('Model list (comma-separated)');
|
|
544
546
|
if ((modelsStr as any) === -1) continue;
|
|
545
547
|
models = (modelsStr || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
546
548
|
|
|
547
|
-
const formatIdx = await selectMenu('API
|
|
549
|
+
const formatIdx = await selectMenu('API format', ['openai', 'anthropic']);
|
|
548
550
|
if (formatIdx === -1) continue;
|
|
549
551
|
format = ['openai', 'anthropic'][formatIdx];
|
|
550
552
|
}
|
|
551
553
|
|
|
552
|
-
// API Key
|
|
554
|
+
// API Key (required for all providers)
|
|
553
555
|
const apiKey = await promptText('API Key');
|
|
554
556
|
if ((apiKey as any) === -1) continue;
|
|
557
|
+
if (!apiKey) {
|
|
558
|
+
console.log('⚠️ API Key is empty, this provider will be temporarily unavailable\n');
|
|
559
|
+
}
|
|
555
560
|
|
|
556
|
-
//
|
|
557
|
-
const priceInput = await promptText('
|
|
561
|
+
// Pricing (optional)
|
|
562
|
+
const priceInput = await promptText('Pricing (in/out per million tokens, e.g. 0.55,2.19, leave blank to skip)');
|
|
558
563
|
if ((priceInput as any) === -1) continue;
|
|
559
564
|
|
|
560
565
|
const pricing: any = {};
|
|
@@ -568,21 +573,24 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
568
573
|
}
|
|
569
574
|
|
|
570
575
|
providers[provName] = { baseUrl, apiKey, models, format, ...(Object.keys(pricing).length ? { pricing } : {}) };
|
|
571
|
-
console.log(`✅
|
|
576
|
+
console.log(`✅ Added: ${provName}\n`);
|
|
572
577
|
|
|
573
|
-
const r = await confirm('
|
|
578
|
+
const r = await confirm('Continue adding providers?', false);
|
|
574
579
|
if (r === -1) addingProviders = false;
|
|
575
580
|
else addingProviders = (r === true);
|
|
576
581
|
}
|
|
577
582
|
|
|
578
583
|
if (Object.keys(providers).length === 0) {
|
|
579
|
-
console.log('\n⚠️
|
|
580
|
-
const r = await confirm('
|
|
581
|
-
if (r === true)
|
|
584
|
+
console.log('\n⚠️ No providers configured.');
|
|
585
|
+
const r = await confirm('Configure at least one provider?');
|
|
586
|
+
if (r === true) continue; // Re-enter step4Loop
|
|
587
|
+
if (r === -1) { console.log('\n⚠️ Skipped, you can configure this later.\n'); }
|
|
582
588
|
}
|
|
589
|
+
step4Loop = false; // Has providers or user explicitly skipped
|
|
590
|
+
}
|
|
583
591
|
|
|
584
|
-
// ===== Step 5:
|
|
585
|
-
console.log('\n📌 Step 5:
|
|
592
|
+
// ===== Step 5: Select default model =====
|
|
593
|
+
console.log('\n📌 Step 5: Select default model\n');
|
|
586
594
|
|
|
587
595
|
const allModels: string[] = [];
|
|
588
596
|
for (const [provName, prov] of Object.entries(providers)) {
|
|
@@ -594,24 +602,24 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
594
602
|
let defaultModel = '';
|
|
595
603
|
if (allModels.length > 0) {
|
|
596
604
|
const existingDefault = existingConfig?.defaultModel || allModels[0];
|
|
597
|
-
const val = await promptText('
|
|
605
|
+
const val = await promptText('Default model', existingDefault);
|
|
598
606
|
defaultModel = (val as any) === -1 ? existingDefault : (val || existingDefault);
|
|
599
607
|
} else {
|
|
600
|
-
defaultModel = await promptText('
|
|
608
|
+
defaultModel = await promptText('Default model (provider/model)') || 'deepseek/deepseek-v4-pro';
|
|
601
609
|
if ((defaultModel as any) === -1) defaultModel = 'deepseek/deepseek-v4-pro';
|
|
602
610
|
}
|
|
603
611
|
|
|
604
|
-
// ===== Step 6:
|
|
605
|
-
console.log('\n📌 Step 6:
|
|
612
|
+
// ===== Step 6: Generate soul files =====
|
|
613
|
+
console.log('\n📌 Step 6: Generate soul files\n');
|
|
606
614
|
|
|
607
615
|
for (const bot of bots) {
|
|
608
616
|
const botSoulDir = getSoulDir(getBotKey(bot));
|
|
609
617
|
const templateSoulDir = path.join(getPkgDir(), 'templates', 'soul.template');
|
|
610
618
|
|
|
611
619
|
if (fs.existsSync(botSoulDir)) {
|
|
612
|
-
const r = await confirm(
|
|
620
|
+
const r = await confirm(`Soul files for ${bot.name} already exist, regenerate?`, false);
|
|
613
621
|
if (r === -1 || r === false) {
|
|
614
|
-
console.log(`⏭
|
|
622
|
+
console.log(`⏭ Skipped: ${bot.name}`);
|
|
615
623
|
continue;
|
|
616
624
|
}
|
|
617
625
|
}
|
|
@@ -633,11 +641,11 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
633
641
|
fs.writeFileSync(destPath, content);
|
|
634
642
|
}
|
|
635
643
|
}
|
|
636
|
-
console.log(`✅ ${bot.name}:
|
|
644
|
+
console.log(`✅ ${bot.name}: soul files → ${botSoulDir}`);
|
|
637
645
|
}
|
|
638
646
|
|
|
639
|
-
// ===== Step 7:
|
|
640
|
-
console.log('\n📌 Step 7:
|
|
647
|
+
// ===== Step 7: Write configuration files =====
|
|
648
|
+
console.log('\n📌 Step 7: Write configuration files\n');
|
|
641
649
|
|
|
642
650
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
643
651
|
|
|
@@ -692,22 +700,22 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
692
700
|
|
|
693
701
|
fs.mkdirSync(path.join(dataDir, 'sessions'), { recursive: true });
|
|
694
702
|
fs.mkdirSync(path.join(dataDir, 'logs'), { recursive: true });
|
|
695
|
-
console.log('✅
|
|
703
|
+
console.log('✅ Sub-directories created (sessions/, logs/)');
|
|
696
704
|
|
|
697
|
-
// =====
|
|
705
|
+
// ===== Done =====
|
|
698
706
|
console.log('\n╔══════════════════════════════════════════════╗');
|
|
699
|
-
console.log('║ ✅
|
|
707
|
+
console.log('║ ✅ Configuration complete! ║');
|
|
700
708
|
console.log('╚══════════════════════════════════════════════╝\n');
|
|
701
709
|
console.log(`Bot: ${bots.map(b => b.name).join(', ')}`);
|
|
702
|
-
console.log(
|
|
703
|
-
console.log(
|
|
704
|
-
console.log(`\
|
|
705
|
-
console.log(` imtoagent start
|
|
706
|
-
console.log(` imtoagent status
|
|
710
|
+
console.log(`Default model: ${defaultModel}`);
|
|
711
|
+
console.log(`Providers: ${Object.keys(providers).join(', ') || 'None'}`);
|
|
712
|
+
console.log(`\nNext steps:`);
|
|
713
|
+
console.log(` imtoagent start Start the gateway`);
|
|
714
|
+
console.log(` imtoagent status Check status\n`);
|
|
707
715
|
}
|
|
708
716
|
|
|
709
717
|
// ================================================================
|
|
710
|
-
//
|
|
718
|
+
// Utility functions
|
|
711
719
|
// ================================================================
|
|
712
720
|
|
|
713
721
|
function buildDefaultAliases(defaultModel: string): Record<string, string> {
|