mixdog 0.7.3 → 0.7.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/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/setup/wizard.mjs +251 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Claude Code all-in-one bridge plugin: role-based bridge workers, continuous memory, and syntax-aware code editing.",
|
|
5
5
|
"author": "mixdog contributors <dev@tribgames.com>",
|
|
6
6
|
"license": "MIT",
|
package/setup/wizard.mjs
CHANGED
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
mergeAgentConfig,
|
|
14
14
|
mergeMemoryConfig,
|
|
15
15
|
mergeConfig,
|
|
16
|
-
|
|
16
|
+
mergeSearchConfig,
|
|
17
17
|
} from './config-merge.mjs';
|
|
18
|
+
import { DEFAULT_MODELS } from '../src/search/lib/config.mjs';
|
|
18
19
|
|
|
19
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
21
|
const REPO_ROOT = join(__dirname, '..');
|
|
@@ -33,6 +34,22 @@ const AG_API_PROVIDERS = [
|
|
|
33
34
|
|
|
34
35
|
const WORKFLOW_ROLES = ['worker', 'reviewer', 'debugger', 'tester'];
|
|
35
36
|
|
|
37
|
+
/** Raw SERP API keys (setup.html SR_KEY_PROVIDERS) — independent of active provider. */
|
|
38
|
+
const SEARCH_RAW_KEY_PROVIDERS = [
|
|
39
|
+
{ id: 'firecrawl', name: 'Firecrawl' },
|
|
40
|
+
{ id: 'tavily', name: 'Tavily' },
|
|
41
|
+
{ id: 'exa', name: 'Exa' },
|
|
42
|
+
];
|
|
43
|
+
const SEARCH_OAUTH_PROVIDERS = new Set(['anthropic-oauth', 'openai-oauth', 'grok-oauth']);
|
|
44
|
+
const OPENAI_SEARCH_EFFORT_VALUES = new Set(['low', 'medium', 'high']);
|
|
45
|
+
const SEARCH_OAUTH_ALIASES = Object.freeze({
|
|
46
|
+
'anthropic-oauth': 'anthropic-oauth',
|
|
47
|
+
anthropic: 'anthropic-oauth',
|
|
48
|
+
'openai-oauth': 'openai-oauth',
|
|
49
|
+
openai: 'openai-oauth',
|
|
50
|
+
'grok-oauth': 'grok-oauth',
|
|
51
|
+
grok: 'grok-oauth',
|
|
52
|
+
});
|
|
36
53
|
function pluginDataDir() {
|
|
37
54
|
const dir = process.env.CLAUDE_PLUGIN_DATA;
|
|
38
55
|
if (!dir || typeof dir !== 'string' || !String(dir).trim()) {
|
|
@@ -122,11 +139,11 @@ function readHiddenLine(prompt) {
|
|
|
122
139
|
async function loadConfigModules() {
|
|
123
140
|
const { ensureDataSeeds } = await import('../src/shared/seed.mjs');
|
|
124
141
|
const { readSection, updateSection } = await import('../src/shared/config.mjs');
|
|
125
|
-
const { DEFAULT_PRESETS } = await import('../src/agent/orchestrator/config.mjs');
|
|
142
|
+
const { DEFAULT_PRESETS, DEFAULT_MAINTENANCE } = await import('../src/agent/orchestrator/config.mjs');
|
|
126
143
|
const dataDir = pluginDataDir();
|
|
127
144
|
mkdirSync(dataDir, { recursive: true });
|
|
128
145
|
ensureDataSeeds(dataDir);
|
|
129
|
-
return { readSection, updateSection, DEFAULT_PRESETS, dataDir };
|
|
146
|
+
return { readSection, updateSection, DEFAULT_PRESETS, DEFAULT_MAINTENANCE, dataDir };
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
function readUserWorkflow(dataDir) {
|
|
@@ -152,14 +169,6 @@ function readUserWorkflow(dataDir) {
|
|
|
152
169
|
}
|
|
153
170
|
}
|
|
154
171
|
|
|
155
|
-
function readJsonFile(path) {
|
|
156
|
-
try {
|
|
157
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
158
|
-
} catch {
|
|
159
|
-
return {};
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
172
|
function writeUserWorkflow(dataDir, data) {
|
|
164
173
|
const path = join(dataDir, 'user-workflow.json');
|
|
165
174
|
const roles = Array.isArray(data?.roles) ? data.roles.slice() : [];
|
|
@@ -184,18 +193,59 @@ function presetIdsFromAgent(agentSection) {
|
|
|
184
193
|
return presets.map((p) => p.id || p.name).filter(Boolean);
|
|
185
194
|
}
|
|
186
195
|
|
|
187
|
-
async function stepDiscordToken(io, { updateSection }) {
|
|
188
|
-
|
|
189
|
-
io.say('
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
export async function stepDiscordToken(io, { updateSection, readSection }) {
|
|
197
|
+
const { hasStoredSecret, SECRET_ACCOUNTS, getDiscordToken } = await import('../src/shared/config.mjs');
|
|
198
|
+
io.say('\n── Step 2/9: Discord ──');
|
|
199
|
+
io.say('Bot token (keychain), application ID, and optional main channel.');
|
|
200
|
+
|
|
201
|
+
const hadStoredToken = hasStoredSecret(SECRET_ACCOUNTS.discordToken);
|
|
202
|
+
const tokenPrompt = hadStoredToken
|
|
203
|
+
? 'Discord bot token (stored, Enter=keep): '
|
|
204
|
+
: 'Discord bot token [hidden] (Enter=skip whole step): ';
|
|
205
|
+
const token = (await io.askSecret(tokenPrompt)).trim();
|
|
206
|
+
const enteredToken = !isSkippableAnswer(token);
|
|
207
|
+
if (!enteredToken && !hadStoredToken) {
|
|
208
|
+
io.say('• Skipped Discord setup.');
|
|
193
209
|
return false;
|
|
194
210
|
}
|
|
211
|
+
|
|
212
|
+
const channels = readSection('channels') || {};
|
|
213
|
+
const curDiscord = channels.discord && typeof channels.discord === 'object' ? channels.discord : {};
|
|
214
|
+
const curAppId = String(curDiscord.applicationId || '').trim();
|
|
215
|
+
const appIdBase = 'Application ID';
|
|
216
|
+
const appIdPrompt = curAppId
|
|
217
|
+
? `${appIdBase} (current: ${curAppId}, Enter=keep): `
|
|
218
|
+
: `${appIdBase}: `;
|
|
219
|
+
const appIdRaw = await io.ask(appIdPrompt);
|
|
220
|
+
const appIdToSet = isSkippableAnswer(appIdRaw) ? '' : String(appIdRaw).trim();
|
|
221
|
+
// The main channel is conventionally named "main" with interactive mode;
|
|
222
|
+
// only the channel ID varies, so we ask just that. Extra channels and
|
|
223
|
+
// monitor mode are configured later in the UI.
|
|
224
|
+
const chIdRaw = await io.ask('Main channel ID (Enter=skip channel): ');
|
|
225
|
+
const channelId = isSkippableAnswer(chIdRaw) ? '' : String(chIdRaw).trim();
|
|
226
|
+
const channelName = 'main';
|
|
227
|
+
const mode = 'interactive';
|
|
195
228
|
const secrets = {};
|
|
196
|
-
updateSection('channels', (current) =>
|
|
197
|
-
|
|
198
|
-
|
|
229
|
+
updateSection('channels', (current) => {
|
|
230
|
+
const payload = {};
|
|
231
|
+
const discord = {};
|
|
232
|
+
if (enteredToken) discord.token = token;
|
|
233
|
+
if (appIdToSet) discord.applicationId = appIdToSet;
|
|
234
|
+
if (Object.keys(discord).length > 0) payload.discord = discord;
|
|
235
|
+
if (channelId) {
|
|
236
|
+
const existingCfg = current.channelsConfig && typeof current.channelsConfig === 'object'
|
|
237
|
+
? { ...current.channelsConfig }
|
|
238
|
+
: {};
|
|
239
|
+
existingCfg[channelName] = { channelId, mode };
|
|
240
|
+
payload.channelsConfig = existingCfg;
|
|
241
|
+
payload.mainChannel = channelName;
|
|
242
|
+
}
|
|
243
|
+
return mergeConfig(current, payload, secrets);
|
|
244
|
+
});
|
|
245
|
+
if (enteredToken) io.say('• Discord token saved to keychain.');
|
|
246
|
+
if (appIdToSet) io.say('• Application ID saved.');
|
|
247
|
+
if (channelId) io.say(`• Main channel "${channelName}" configured (${mode}).`);
|
|
248
|
+
return enteredToken || hadStoredToken || !!getDiscordToken();
|
|
199
249
|
}
|
|
200
250
|
|
|
201
251
|
function formatVoiceProgress(p) {
|
|
@@ -230,7 +280,7 @@ async function installVoiceRuntime(dataDir, io) {
|
|
|
230
280
|
/** Mirrors setup.html channels save: `voice` via POST /config → mergeConfig. */
|
|
231
281
|
async function stepVoiceTranscription(io, ctx, discordTokenSaved) {
|
|
232
282
|
if (!discordTokenSaved) return;
|
|
233
|
-
io.say('\n── Step 2a/
|
|
283
|
+
io.say('\n── Step 2a/9: Voice transcription (음성 전사) ──');
|
|
234
284
|
io.say('Install local Speech-to-text (whisper.cpp) for Discord voice messages.');
|
|
235
285
|
const raw = await io.ask('Enable voice transcription? [y/N] (Enter=skip): ');
|
|
236
286
|
if (isSkippableAnswer(raw)) {
|
|
@@ -256,7 +306,7 @@ async function stepVoiceTranscription(io, ctx, discordTokenSaved) {
|
|
|
256
306
|
}
|
|
257
307
|
|
|
258
308
|
async function stepAddressForm(io, { updateSection, readSection }) {
|
|
259
|
-
io.say('\n── Step 1/
|
|
309
|
+
io.say('\n── Step 1/9: Address form (호칭) ──');
|
|
260
310
|
const memory = readSection('memory');
|
|
261
311
|
const curTitle = memory?.user?.title || '';
|
|
262
312
|
const curName = memory?.user?.name || '';
|
|
@@ -276,10 +326,10 @@ async function stepAddressForm(io, { updateSection, readSection }) {
|
|
|
276
326
|
io.say('• Saved memory.user (title/name).');
|
|
277
327
|
}
|
|
278
328
|
|
|
279
|
-
async function
|
|
280
|
-
io.say('\n── Step 3/
|
|
281
|
-
io.say('
|
|
282
|
-
const enableRaw = await io.ask('
|
|
329
|
+
export async function stepWebhookReceiver(io, { updateSection, readSection }) {
|
|
330
|
+
io.say('\n── Step 3/9: Inbound webhooks (ngrok receiver) ──');
|
|
331
|
+
io.say('Global webhook tunnel for inbound HTTP (channels.webhook). Per-endpoint registration is configured later in the UI.');
|
|
332
|
+
const enableRaw = await io.ask('Enable inbound webhooks? [y/N]: ');
|
|
283
333
|
if (isSkippableAnswer(enableRaw)) {
|
|
284
334
|
io.say('• Skipped webhook setup.');
|
|
285
335
|
return;
|
|
@@ -289,31 +339,31 @@ async function stepWebhookRegister(io, { dataDir }) {
|
|
|
289
339
|
io.say('• Skipped webhook setup.');
|
|
290
340
|
return;
|
|
291
341
|
}
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
342
|
+
const { hasStoredSecret, SECRET_ACCOUNTS } = await import('../src/shared/config.mjs');
|
|
343
|
+
const channels = readSection('channels') || {};
|
|
344
|
+
const curWebhook = channels.webhook && typeof channels.webhook === 'object' ? channels.webhook : {};
|
|
345
|
+
const curDomain = String(curWebhook.domain || curWebhook.ngrokDomain || '').trim();
|
|
346
|
+
const domainBase =
|
|
347
|
+
'Domain (ngrok, e.g. your-name.ngrok-free.dev — get it at dashboard.ngrok.com/domains)';
|
|
348
|
+
const domainPrompt = curDomain
|
|
349
|
+
? `${domainBase} (current: ${curDomain}, Enter=keep): `
|
|
350
|
+
: `${domainBase}: `;
|
|
351
|
+
const domainRaw = await io.ask(domainPrompt);
|
|
352
|
+
const webhook = { enabled: true };
|
|
353
|
+
if (!isSkippableAnswer(domainRaw)) {
|
|
354
|
+
webhook.domain = String(domainRaw).trim();
|
|
297
355
|
}
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const incoming = {};
|
|
306
|
-
if (secret) incoming.secret = secret;
|
|
307
|
-
if (channel) incoming.channel = channel;
|
|
308
|
-
if (model) incoming.model = model;
|
|
309
|
-
const merged = mergeWebhookEndpointConfig(readJsonFile(configPath), incoming);
|
|
310
|
-
writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
311
|
-
writeFileSync(join(dir, 'instructions.md'), instructions, 'utf8');
|
|
312
|
-
io.say(`• Webhook endpoint "${name}" saved (role: ${merged.role}).`);
|
|
356
|
+
const authPrompt = hasStoredSecret(SECRET_ACCOUNTS.webhookAuth)
|
|
357
|
+
? 'Auth Token (stored, Enter=keep): '
|
|
358
|
+
: 'ngrok Auth Token [hidden]: ';
|
|
359
|
+
webhook.authtoken = (await io.askSecret(authPrompt)).trim();
|
|
360
|
+
const secrets = {};
|
|
361
|
+
updateSection('channels', (current) => mergeConfig(current, { webhook }, secrets));
|
|
362
|
+
io.say('• Inbound webhook receiver saved (channels.webhook enabled/domain; authtoken in keychain).');
|
|
313
363
|
}
|
|
314
364
|
|
|
315
365
|
async function stepProviderKeys(io, { updateSection }) {
|
|
316
|
-
io.say('\n── Step 4/
|
|
366
|
+
io.say('\n── Step 4/9: Provider API keys ──');
|
|
317
367
|
io.say('Optional API keys (hidden). Enter to skip a provider.');
|
|
318
368
|
const providers = {};
|
|
319
369
|
for (const p of AG_API_PROVIDERS) {
|
|
@@ -332,7 +382,7 @@ async function stepProviderKeys(io, { updateSection }) {
|
|
|
332
382
|
}
|
|
333
383
|
|
|
334
384
|
async function stepPresets(io, { readSection, updateSection, DEFAULT_PRESETS }) {
|
|
335
|
-
io.say('\n── Step 5/
|
|
385
|
+
io.say('\n── Step 5/9: Agent presets ──');
|
|
336
386
|
const agent = readSection('agent');
|
|
337
387
|
const existing = presetIdsFromAgent(agent);
|
|
338
388
|
if (existing.length > 0) {
|
|
@@ -359,11 +409,11 @@ async function stepPresets(io, { readSection, updateSection, DEFAULT_PRESETS })
|
|
|
359
409
|
}
|
|
360
410
|
|
|
361
411
|
async function stepRolePresetMapping(io, { readSection, dataDir }) {
|
|
362
|
-
io.say('\n── Step 6/
|
|
412
|
+
io.say('\n── Step 6/9: Role → preset mapping ──');
|
|
363
413
|
const agent = readSection('agent');
|
|
364
414
|
const presetIds = presetIdsFromAgent(agent);
|
|
365
415
|
if (presetIds.length === 0) {
|
|
366
|
-
io.say('No presets on disk — run step
|
|
416
|
+
io.say('No presets on disk — run step 5 first or configure presets in the Mixdog UI later.');
|
|
367
417
|
return;
|
|
368
418
|
}
|
|
369
419
|
io.say(`Available presets: ${presetIds.join(', ')}`);
|
|
@@ -393,6 +443,154 @@ async function stepRolePresetMapping(io, { readSection, dataDir }) {
|
|
|
393
443
|
io.say('• Role → preset mapping saved to user-workflow.json.');
|
|
394
444
|
}
|
|
395
445
|
|
|
446
|
+
function resolveSearchBackendInput(raw) {
|
|
447
|
+
if (isSkippableAnswer(raw)) return null;
|
|
448
|
+
const key = String(raw).trim().toLowerCase();
|
|
449
|
+
return SEARCH_OAUTH_ALIASES[key] || null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function parseYesNo(raw) {
|
|
453
|
+
if (isSkippableAnswer(raw)) return null;
|
|
454
|
+
const v = String(raw).trim().toLowerCase();
|
|
455
|
+
if (v === 'y' || v === 'yes' || v === 'true' || v === '1') return true;
|
|
456
|
+
if (v === 'n' || v === 'no' || v === 'false' || v === '0') return false;
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Mirrors POST /search/config → mergeSearchConfig. */
|
|
461
|
+
export async function stepSearchBackend(io, { updateSection, readSection }) {
|
|
462
|
+
io.say('\n── Step 7/9: Search backend ──');
|
|
463
|
+
io.say('Active provider: anthropic-oauth | openai-oauth | grok-oauth (OAuth — uses Agent credentials).');
|
|
464
|
+
const { hasStoredSecret, SECRET_ACCOUNTS, getSearchApiKey } = await import('../src/shared/config.mjs');
|
|
465
|
+
const search = readSection('search') || {};
|
|
466
|
+
const curProvider = String(search.provider || 'anthropic-oauth').trim() || 'anthropic-oauth';
|
|
467
|
+
const backendRaw = await io.ask(`Search provider [${curProvider}]: `);
|
|
468
|
+
let provider = curProvider;
|
|
469
|
+
if (!isSkippableAnswer(backendRaw)) {
|
|
470
|
+
const resolved = resolveSearchBackendInput(backendRaw);
|
|
471
|
+
if (!resolved) {
|
|
472
|
+
io.say(` ! Unknown provider "${String(backendRaw).trim()}" — keeping ${curProvider}.`);
|
|
473
|
+
} else if (!SEARCH_OAUTH_PROVIDERS.has(resolved)) {
|
|
474
|
+
io.say(` ! Provider "${resolved}" is not a supported OAuth backend — keeping ${curProvider}.`);
|
|
475
|
+
} else {
|
|
476
|
+
provider = resolved;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const payload = {};
|
|
481
|
+
if (provider !== curProvider) payload.provider = provider;
|
|
482
|
+
|
|
483
|
+
const searchProviders = {};
|
|
484
|
+
for (const p of SEARCH_RAW_KEY_PROVIDERS) {
|
|
485
|
+
const hadKey = hasStoredSecret(SECRET_ACCOUNTS.searchApiKey(p.id));
|
|
486
|
+
const keyPrompt = hadKey
|
|
487
|
+
? `${p.name} API key (stored, Enter=keep): `
|
|
488
|
+
: `${p.name} API key [hidden] (Enter=skip): `;
|
|
489
|
+
const key = (await io.askSecret(keyPrompt)).trim();
|
|
490
|
+
if (!isSkippableAnswer(key)) {
|
|
491
|
+
searchProviders[p.id] = key;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (Object.keys(searchProviders).length) payload.searchProviders = searchProviders;
|
|
495
|
+
|
|
496
|
+
if (provider === 'openai-oauth') {
|
|
497
|
+
const curModel = (search.models && search.models.openai)
|
|
498
|
+
|| DEFAULT_MODELS.openai
|
|
499
|
+
|| '';
|
|
500
|
+
const modelRaw = await io.ask(`OpenAI model [${curModel}] (Enter=keep): `);
|
|
501
|
+
if (!isSkippableAnswer(modelRaw)) {
|
|
502
|
+
const model = String(modelRaw).trim();
|
|
503
|
+
if (model) payload.models = { ...(payload.models || {}), openai: model };
|
|
504
|
+
}
|
|
505
|
+
const curEffort = String(search.modelOptions?.openai?.effort || 'medium').trim() || 'medium';
|
|
506
|
+
const effortRaw = await io.ask(`OpenAI effort (low/medium/high) [${curEffort}] (Enter=keep): `);
|
|
507
|
+
if (!isSkippableAnswer(effortRaw)) {
|
|
508
|
+
const effort = String(effortRaw).trim().toLowerCase();
|
|
509
|
+
if (!OPENAI_SEARCH_EFFORT_VALUES.has(effort)) {
|
|
510
|
+
io.say(` ! Unknown effort "${effortRaw.trim()}" — keeping ${curEffort}.`);
|
|
511
|
+
} else {
|
|
512
|
+
const openaiOpts = { ...(search.modelOptions?.openai || {}), effort };
|
|
513
|
+
payload.modelOptions = { ...(payload.modelOptions || {}), openai: openaiOpts };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const curFast = !!search.modelOptions?.openai?.fast;
|
|
517
|
+
const fastRaw = await io.ask(`OpenAI Fast mode [${curFast ? 'y' : 'N'}] (y/N, Enter=keep): `);
|
|
518
|
+
const fastParsed = parseYesNo(fastRaw);
|
|
519
|
+
if (fastParsed === undefined && !isSkippableAnswer(fastRaw)) {
|
|
520
|
+
io.say(' ! Fast mode: answer y or n — keeping current.');
|
|
521
|
+
} else if (fastParsed !== null) {
|
|
522
|
+
const base = payload.modelOptions?.openai || search.modelOptions?.openai || {};
|
|
523
|
+
const openaiOpts = { ...base };
|
|
524
|
+
if (fastParsed) openaiOpts.fast = true;
|
|
525
|
+
else delete openaiOpts.fast;
|
|
526
|
+
// Write even an empty object: mergeSearchConfig treats an empty per-family
|
|
527
|
+
// entry as "clear this family", which is how an explicit Fast=n drops a
|
|
528
|
+
// lone fast flag (no effort left to keep the object non-empty).
|
|
529
|
+
payload.modelOptions = { ...(payload.modelOptions || {}), openai: openaiOpts };
|
|
530
|
+
}
|
|
531
|
+
} else if (provider === 'grok-oauth') {
|
|
532
|
+
const curModel = (search.models && search.models.xai)
|
|
533
|
+
|| DEFAULT_MODELS.xai
|
|
534
|
+
|| '';
|
|
535
|
+
const modelRaw = await io.ask(`xAI model [${curModel}] (Enter=keep): `);
|
|
536
|
+
if (!isSkippableAnswer(modelRaw)) {
|
|
537
|
+
const model = String(modelRaw).trim();
|
|
538
|
+
if (model) payload.models = { ...(payload.models || {}), xai: model };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const secrets = {};
|
|
543
|
+
updateSection('search', (current) => mergeSearchConfig(current, payload, secrets));
|
|
544
|
+
const after = readSection('search') || {};
|
|
545
|
+
const savedProvider = after.provider || curProvider;
|
|
546
|
+
io.say(`• Search provider: ${savedProvider}.`);
|
|
547
|
+
for (const p of SEARCH_RAW_KEY_PROVIDERS) {
|
|
548
|
+
io.say(`• ${p.name} API key: ${getSearchApiKey(p.id) ? 'stored' : 'not set'}.`);
|
|
549
|
+
}
|
|
550
|
+
if (savedProvider === 'openai-oauth' && after.models?.openai) {
|
|
551
|
+
io.say(`• OpenAI model: ${after.models.openai}.`);
|
|
552
|
+
}
|
|
553
|
+
if (savedProvider === 'grok-oauth' && after.models?.xai) {
|
|
554
|
+
io.say(`• xAI model: ${after.models.xai}.`);
|
|
555
|
+
}
|
|
556
|
+
if (savedProvider === 'openai-oauth' && after.modelOptions?.openai?.effort) {
|
|
557
|
+
io.say(`• OpenAI effort: ${after.modelOptions.openai.effort}.`);
|
|
558
|
+
}
|
|
559
|
+
if (savedProvider === 'openai-oauth' && after.modelOptions?.openai?.fast) {
|
|
560
|
+
io.say('• OpenAI Fast mode: on.');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Mirrors POST /agent/maintenance for the explore slot. */
|
|
565
|
+
export async function stepExplorerPreset(io, { readSection, updateSection, DEFAULT_PRESETS, DEFAULT_MAINTENANCE }) {
|
|
566
|
+
io.say('\n── Step 8/9: Explorer model (explore tool) ──');
|
|
567
|
+
const agent = readSection('agent') || {};
|
|
568
|
+
const presetIds = presetIdsFromAgent(agent);
|
|
569
|
+
const validIds = new Set([
|
|
570
|
+
...presetIds,
|
|
571
|
+
...DEFAULT_PRESETS.map((p) => p.id).filter(Boolean),
|
|
572
|
+
]);
|
|
573
|
+
const curExplore = String(agent.maintenance?.explore || DEFAULT_MAINTENANCE.explore || 'haiku').trim() || 'haiku';
|
|
574
|
+
if (validIds.size > 0) {
|
|
575
|
+
io.say(`Available presets: ${[...validIds].join(', ')}`);
|
|
576
|
+
}
|
|
577
|
+
const raw = await io.ask(`Preset for explorer (explore tool) [${curExplore}]: `);
|
|
578
|
+
if (isSkippableAnswer(raw)) {
|
|
579
|
+
io.say(`• Explorer preset unchanged (${curExplore}).`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const preset = String(raw).trim();
|
|
583
|
+
if (!validIds.has(preset)) {
|
|
584
|
+
io.say(` ! Unknown preset "${preset}" — keeping ${curExplore}.`);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
updateSection('agent', (current) => ({
|
|
588
|
+
...current,
|
|
589
|
+
maintenance: { ...(current.maintenance || {}), explore: preset },
|
|
590
|
+
}));
|
|
591
|
+
io.say(`• Explorer maintenance preset: ${preset}.`);
|
|
592
|
+
}
|
|
593
|
+
|
|
396
594
|
/**
|
|
397
595
|
* @param {object} [ioOverride]
|
|
398
596
|
* @param {boolean} [ioOverride.interactive]
|
|
@@ -412,10 +610,12 @@ export async function runSetupWizard(ioOverride = null) {
|
|
|
412
610
|
await stepAddressForm(io, ctx);
|
|
413
611
|
const discordSaved = await stepDiscordToken(io, ctx);
|
|
414
612
|
await stepVoiceTranscription(io, ctx, discordSaved);
|
|
415
|
-
await
|
|
613
|
+
await stepWebhookReceiver(io, ctx);
|
|
416
614
|
await stepProviderKeys(io, ctx);
|
|
417
615
|
await stepPresets(io, ctx);
|
|
418
616
|
await stepRolePresetMapping(io, ctx);
|
|
617
|
+
await stepSearchBackend(io, ctx);
|
|
618
|
+
await stepExplorerPreset(io, ctx);
|
|
419
619
|
io.say('\n✓ Wizard complete. Restart Claude Code (or /reload-plugins) to load mixdog.');
|
|
420
620
|
} catch (err) {
|
|
421
621
|
io.say(`\n✗ Wizard error: ${err?.message || err}`);
|