mixdog 0.7.4 → 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 +227 -18
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,7 +13,9 @@ import {
|
|
|
13
13
|
mergeAgentConfig,
|
|
14
14
|
mergeMemoryConfig,
|
|
15
15
|
mergeConfig,
|
|
16
|
+
mergeSearchConfig,
|
|
16
17
|
} from './config-merge.mjs';
|
|
18
|
+
import { DEFAULT_MODELS } from '../src/search/lib/config.mjs';
|
|
17
19
|
|
|
18
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
21
|
const REPO_ROOT = join(__dirname, '..');
|
|
@@ -32,6 +34,22 @@ const AG_API_PROVIDERS = [
|
|
|
32
34
|
|
|
33
35
|
const WORKFLOW_ROLES = ['worker', 'reviewer', 'debugger', 'tester'];
|
|
34
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
|
+
});
|
|
35
53
|
function pluginDataDir() {
|
|
36
54
|
const dir = process.env.CLAUDE_PLUGIN_DATA;
|
|
37
55
|
if (!dir || typeof dir !== 'string' || !String(dir).trim()) {
|
|
@@ -121,11 +139,11 @@ function readHiddenLine(prompt) {
|
|
|
121
139
|
async function loadConfigModules() {
|
|
122
140
|
const { ensureDataSeeds } = await import('../src/shared/seed.mjs');
|
|
123
141
|
const { readSection, updateSection } = await import('../src/shared/config.mjs');
|
|
124
|
-
const { DEFAULT_PRESETS } = await import('../src/agent/orchestrator/config.mjs');
|
|
142
|
+
const { DEFAULT_PRESETS, DEFAULT_MAINTENANCE } = await import('../src/agent/orchestrator/config.mjs');
|
|
125
143
|
const dataDir = pluginDataDir();
|
|
126
144
|
mkdirSync(dataDir, { recursive: true });
|
|
127
145
|
ensureDataSeeds(dataDir);
|
|
128
|
-
return { readSection, updateSection, DEFAULT_PRESETS, dataDir };
|
|
146
|
+
return { readSection, updateSection, DEFAULT_PRESETS, DEFAULT_MAINTENANCE, dataDir };
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
function readUserWorkflow(dataDir) {
|
|
@@ -175,18 +193,59 @@ function presetIdsFromAgent(agentSection) {
|
|
|
175
193
|
return presets.map((p) => p.id || p.name).filter(Boolean);
|
|
176
194
|
}
|
|
177
195
|
|
|
178
|
-
async function stepDiscordToken(io, { updateSection }) {
|
|
179
|
-
|
|
180
|
-
io.say('
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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.');
|
|
184
209
|
return false;
|
|
185
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';
|
|
186
228
|
const secrets = {};
|
|
187
|
-
updateSection('channels', (current) =>
|
|
188
|
-
|
|
189
|
-
|
|
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();
|
|
190
249
|
}
|
|
191
250
|
|
|
192
251
|
function formatVoiceProgress(p) {
|
|
@@ -221,7 +280,7 @@ async function installVoiceRuntime(dataDir, io) {
|
|
|
221
280
|
/** Mirrors setup.html channels save: `voice` via POST /config → mergeConfig. */
|
|
222
281
|
async function stepVoiceTranscription(io, ctx, discordTokenSaved) {
|
|
223
282
|
if (!discordTokenSaved) return;
|
|
224
|
-
io.say('\n── Step 2a/
|
|
283
|
+
io.say('\n── Step 2a/9: Voice transcription (음성 전사) ──');
|
|
225
284
|
io.say('Install local Speech-to-text (whisper.cpp) for Discord voice messages.');
|
|
226
285
|
const raw = await io.ask('Enable voice transcription? [y/N] (Enter=skip): ');
|
|
227
286
|
if (isSkippableAnswer(raw)) {
|
|
@@ -247,7 +306,7 @@ async function stepVoiceTranscription(io, ctx, discordTokenSaved) {
|
|
|
247
306
|
}
|
|
248
307
|
|
|
249
308
|
async function stepAddressForm(io, { updateSection, readSection }) {
|
|
250
|
-
io.say('\n── Step 1/
|
|
309
|
+
io.say('\n── Step 1/9: Address form (호칭) ──');
|
|
251
310
|
const memory = readSection('memory');
|
|
252
311
|
const curTitle = memory?.user?.title || '';
|
|
253
312
|
const curName = memory?.user?.name || '';
|
|
@@ -268,7 +327,7 @@ async function stepAddressForm(io, { updateSection, readSection }) {
|
|
|
268
327
|
}
|
|
269
328
|
|
|
270
329
|
export async function stepWebhookReceiver(io, { updateSection, readSection }) {
|
|
271
|
-
io.say('\n── Step 3/
|
|
330
|
+
io.say('\n── Step 3/9: Inbound webhooks (ngrok receiver) ──');
|
|
272
331
|
io.say('Global webhook tunnel for inbound HTTP (channels.webhook). Per-endpoint registration is configured later in the UI.');
|
|
273
332
|
const enableRaw = await io.ask('Enable inbound webhooks? [y/N]: ');
|
|
274
333
|
if (isSkippableAnswer(enableRaw)) {
|
|
@@ -304,7 +363,7 @@ export async function stepWebhookReceiver(io, { updateSection, readSection }) {
|
|
|
304
363
|
}
|
|
305
364
|
|
|
306
365
|
async function stepProviderKeys(io, { updateSection }) {
|
|
307
|
-
io.say('\n── Step 4/
|
|
366
|
+
io.say('\n── Step 4/9: Provider API keys ──');
|
|
308
367
|
io.say('Optional API keys (hidden). Enter to skip a provider.');
|
|
309
368
|
const providers = {};
|
|
310
369
|
for (const p of AG_API_PROVIDERS) {
|
|
@@ -323,7 +382,7 @@ async function stepProviderKeys(io, { updateSection }) {
|
|
|
323
382
|
}
|
|
324
383
|
|
|
325
384
|
async function stepPresets(io, { readSection, updateSection, DEFAULT_PRESETS }) {
|
|
326
|
-
io.say('\n── Step 5/
|
|
385
|
+
io.say('\n── Step 5/9: Agent presets ──');
|
|
327
386
|
const agent = readSection('agent');
|
|
328
387
|
const existing = presetIdsFromAgent(agent);
|
|
329
388
|
if (existing.length > 0) {
|
|
@@ -350,11 +409,11 @@ async function stepPresets(io, { readSection, updateSection, DEFAULT_PRESETS })
|
|
|
350
409
|
}
|
|
351
410
|
|
|
352
411
|
async function stepRolePresetMapping(io, { readSection, dataDir }) {
|
|
353
|
-
io.say('\n── Step 6/
|
|
412
|
+
io.say('\n── Step 6/9: Role → preset mapping ──');
|
|
354
413
|
const agent = readSection('agent');
|
|
355
414
|
const presetIds = presetIdsFromAgent(agent);
|
|
356
415
|
if (presetIds.length === 0) {
|
|
357
|
-
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.');
|
|
358
417
|
return;
|
|
359
418
|
}
|
|
360
419
|
io.say(`Available presets: ${presetIds.join(', ')}`);
|
|
@@ -384,6 +443,154 @@ async function stepRolePresetMapping(io, { readSection, dataDir }) {
|
|
|
384
443
|
io.say('• Role → preset mapping saved to user-workflow.json.');
|
|
385
444
|
}
|
|
386
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
|
+
|
|
387
594
|
/**
|
|
388
595
|
* @param {object} [ioOverride]
|
|
389
596
|
* @param {boolean} [ioOverride.interactive]
|
|
@@ -407,6 +614,8 @@ export async function runSetupWizard(ioOverride = null) {
|
|
|
407
614
|
await stepProviderKeys(io, ctx);
|
|
408
615
|
await stepPresets(io, ctx);
|
|
409
616
|
await stepRolePresetMapping(io, ctx);
|
|
617
|
+
await stepSearchBackend(io, ctx);
|
|
618
|
+
await stepExplorerPreset(io, ctx);
|
|
410
619
|
io.say('\n✓ Wizard complete. Restart Claude Code (or /reload-plugins) to load mixdog.');
|
|
411
620
|
} catch (err) {
|
|
412
621
|
io.say(`\n✗ Wizard error: ${err?.message || err}`);
|