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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "Claude Code all-in-one agent plugin — autonomous agents, continuous memory, cost-aware sub-agents, and syntax-aware code editing.",
5
5
  "hooks": "./hooks/hooks.json",
6
6
  "mcpServers": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.7.4",
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
- io.say('\n── Step 2/7: Discord bot token ──');
180
- io.say('Paste your Discord bot token (hidden). Enter to skip.');
181
- const token = (await io.askSecret('Discord bot token: ')).trim();
182
- if (isSkippableAnswer(token)) {
183
- io.say('• Skipped Discord token.');
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) => mergeConfig(current, { discord: { token } }, secrets));
188
- io.say('• Discord token saved to keychain.');
189
- return true;
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/7: Voice transcription (음성 전사) ──');
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/7: Address form (호칭) ──');
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/7: Inbound webhooks (ngrok receiver) ──');
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/7: Provider API keys ──');
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/7: Agent presets ──');
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/7: Role → preset mapping ──');
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 4 first or configure presets in the Mixdog UI later.');
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}`);