mixdog 0.7.8 → 0.7.12

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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +5 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +40 -0
  4. package/README.md +198 -251
  5. package/bin/statusline-launcher.mjs +5 -1
  6. package/bin/statusline-lib.mjs +14 -6
  7. package/bin/statusline.mjs +14 -6
  8. package/hooks/lib/settings-loader.cjs +4 -3
  9. package/hooks/pre-tool-subagent.cjs +7 -2
  10. package/hooks/session-start.cjs +52 -24
  11. package/lib/mixdog-debug.cjs +163 -0
  12. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  13. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  14. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  15. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  16. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  17. package/package.json +1 -1
  18. package/scripts/builtin-utils-smoke.mjs +14 -8
  19. package/scripts/bump.mjs +80 -0
  20. package/scripts/doctor.mjs +8 -3
  21. package/scripts/mutation-io-smoke.mjs +17 -1
  22. package/scripts/openai-oauth-catalog-smoke.mjs +53 -0
  23. package/scripts/permission-eval-smoke.mjs +18 -1
  24. package/scripts/statusline-launcher-smoke.mjs +2 -2
  25. package/scripts/webhook-selfheal-smoke.mjs +1 -3
  26. package/server-main.mjs +57 -3
  27. package/setup/config-merge.mjs +0 -1
  28. package/setup/install.mjs +241 -51
  29. package/setup/mixdog-cli.mjs +30 -3
  30. package/setup/setup-server.mjs +21 -33
  31. package/setup/setup.html +46 -11
  32. package/setup/tui.mjs +35 -316
  33. package/src/agent/orchestrator/config.mjs +0 -1
  34. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +2 -5
  35. package/src/agent/orchestrator/providers/anthropic.mjs +243 -86
  36. package/src/agent/orchestrator/providers/gemini.mjs +386 -31
  37. package/src/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  38. package/src/agent/orchestrator/providers/model-catalog.mjs +146 -13
  39. package/src/agent/orchestrator/providers/openai-compat-stream.mjs +366 -0
  40. package/src/agent/orchestrator/providers/openai-compat.mjs +74 -30
  41. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -1
  42. package/src/agent/orchestrator/providers/openai-oauth.mjs +66 -13
  43. package/src/agent/orchestrator/providers/openai-ws.mjs +23 -0
  44. package/src/agent/orchestrator/session/manager.mjs +18 -4
  45. package/src/agent/orchestrator/stall-policy.mjs +6 -0
  46. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +29 -8
  47. package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
  48. package/src/agent/orchestrator/tools/patch-manifest.json +11 -11
  49. package/src/channels/index.mjs +27 -8
  50. package/src/channels/lib/event-queue.mjs +24 -1
  51. package/src/channels/lib/hook-pipe-server.mjs +21 -8
  52. package/src/channels/lib/webhook.mjs +142 -20
  53. package/src/memory/lib/memory-cycle1.mjs +7 -3
  54. package/src/memory/lib/memory-recall-store.mjs +27 -10
  55. package/src/search/lib/backends/openai-oauth.mjs +6 -2
  56. package/src/search/lib/cache.mjs +55 -7
  57. package/src/shared/config.mjs +1 -1
  58. package/src/shared/llm/cost.mjs +2 -2
  59. package/src/shared/open-url.mjs +37 -0
  60. package/src/shared/seed.mjs +20 -3
  61. package/src/shared/user-data-guard.mjs +3 -1
  62. package/scripts/test-config-rmw-restore.mjs +0 -122
  63. package/setup/wizard.mjs +0 -696
package/setup/tui.mjs CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Zero-dependency clack-style terminal UI helpers for the mixdog setup wizard.
3
- * Node built-ins only; assumes an interactive TTY (wizard guarantees this).
2
+ * Zero-dependency clack-style terminal UI helpers for mixdog setup/install.
3
+ * Node built-ins only; assumes an interactive TTY when prompts are used.
4
4
  */
5
5
  import { emitKeypressEvents } from 'node:readline';
6
- import { fileURLToPath } from 'node:url';
7
6
 
8
7
  const stdin = process.stdin;
9
8
  const stdout = process.stdout;
@@ -16,12 +15,14 @@ const ansi = {
16
15
  green: '\x1b[32m',
17
16
  red: '\x1b[31m',
18
17
  dim: '\x1b[2m',
18
+ bold: '\x1b[1m',
19
19
  inverse: '\x1b[7m',
20
20
  };
21
21
 
22
22
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
23
23
 
24
24
  const STRIP_ANSI_RE = /\x1b\[[0-9;]*m/g;
25
+ const STRIP_OSC_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
25
26
  const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
26
27
 
27
28
  function assertInteractiveTTY() {
@@ -30,13 +31,23 @@ function assertInteractiveTTY() {
30
31
  }
31
32
  }
32
33
 
33
- function rail(active = true) {
34
- return active ? `${ansi.dim}│${ansi.reset}` : `${ansi.dim}◇${ansi.reset}`;
34
+ function gutter() {
35
+ return `${ansi.dim}│${ansi.reset}`;
36
+ }
37
+
38
+ function railEnd() {
39
+ return gutter();
40
+ }
41
+
42
+ /** Global key legend pinned at the bottom of every prompt (outside the rail). */
43
+ const NAV_LEGEND = 'Enter confirm · Ctrl+C quit';
44
+ function navLegendRow() {
45
+ return ` ${ansi.dim}${NAV_LEGEND}${ansi.reset}`;
35
46
  }
36
47
 
37
48
  function prefixGlyph(done) {
38
49
  return done
39
- ? `${ansi.green}✔${ansi.reset}`
50
+ ? `${ansi.green}✓${ansi.reset}`
40
51
  : `${ansi.cyan}◆${ansi.reset}`;
41
52
  }
42
53
 
@@ -97,7 +108,7 @@ function graphemeClusterWidth(segment) {
97
108
  }
98
109
 
99
110
  function cellWidth(str) {
100
- const plain = str.replace(STRIP_ANSI_RE, '');
111
+ const plain = str.replace(STRIP_OSC_RE, '').replace(STRIP_ANSI_RE, '');
101
112
  let width = 0;
102
113
  for (const { segment } of GRAPHEME_SEGMENTER.segment(plain)) {
103
114
  width += graphemeClusterWidth(segment);
@@ -129,7 +140,8 @@ function redrawUp(lines) {
129
140
 
130
141
  function finishPrompt(lines, finalLine) {
131
142
  redrawUp(lines);
132
- stdout.write(`${ansi.green}✔${ansi.reset} ${finalLine}\n`);
143
+ stdout.write(`${ansi.green}✓${ansi.reset} ${finalLine}\n`);
144
+ stdout.write(`${gutter()}\n`);
133
145
  }
134
146
 
135
147
  /**
@@ -211,158 +223,6 @@ async function withRawPrompt(run) {
211
223
  }
212
224
  }
213
225
 
214
- /**
215
- * @param {string} message
216
- * @param {{ value: string, label: string, hint?: string }[]} options
217
- * @param {{ initial?: string }} [opts]
218
- */
219
- export async function select(message, options, { initial } = {}) {
220
- if (!options?.length) {
221
- throw new Error('select: options must be a non-empty array');
222
- }
223
- let index = 0;
224
- if (initial !== undefined) {
225
- const i = options.findIndex((o) => o.value === initial);
226
- if (i >= 0) index = i;
227
- }
228
-
229
- return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
230
- const draw = () => {
231
- const rows = [
232
- `${prefixGlyph(false)} ${message}`,
233
- rail(),
234
- ];
235
- for (let i = 0; i < options.length; i += 1) {
236
- const opt = options[i];
237
- const active = i === index;
238
- const cursor = active ? `${ansi.cyan}❯ ${ansi.reset}` : ' ';
239
- const label = active
240
- ? `${ansi.cyan}${ansi.inverse} ${opt.label} ${ansi.reset}`
241
- : opt.label;
242
- const hint = opt.hint ? ` ${ansi.dim}${opt.hint}${ansi.reset}` : '';
243
- rows.push(`${rail()} ${cursor}${label}${hint}`);
244
- }
245
- return rows.join('\n');
246
- };
247
-
248
- rerender(draw);
249
-
250
- return new Promise((resolve, reject) => {
251
- promptReject(reject);
252
- onKey((_str, key) => {
253
- const name = key.name;
254
- if (name === 'up' || name === 'k') {
255
- index = (index - 1 + options.length) % options.length;
256
- rerender(draw);
257
- return;
258
- }
259
- if (name === 'down' || name === 'j') {
260
- index = (index + 1) % options.length;
261
- rerender(draw);
262
- return;
263
- }
264
- if (name === 'return') {
265
- const chosen = options[index];
266
- finishPrompt(
267
- lineCount(draw()),
268
- `${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${chosen.label}${ansi.reset}`,
269
- );
270
- resolve(chosen.value);
271
- }
272
- });
273
- });
274
- });
275
- }
276
-
277
- /**
278
- * @param {string} message
279
- * @param {{ value: string, label: string, hint?: string }[]} options
280
- * @param {{ initial?: string[], min?: number }} [opts]
281
- */
282
- export async function multiselect(message, options, { initial = [], min = 0 } = {}) {
283
- if (!options?.length) {
284
- throw new Error('multiselect: options must be a non-empty array');
285
- }
286
- const selected = new Set(
287
- Array.isArray(initial) ? initial.filter((v) => options.some((o) => o.value === v)) : [],
288
- );
289
- let index = 0;
290
- let error = '';
291
-
292
- return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
293
- const draw = () => {
294
- const rows = [
295
- `${prefixGlyph(false)} ${message}`,
296
- rail(),
297
- ];
298
- for (let i = 0; i < options.length; i += 1) {
299
- const opt = options[i];
300
- const active = i === index;
301
- const checked = selected.has(opt.value);
302
- const box = checked ? '◉' : '◯';
303
- const cursor = active ? `${ansi.cyan}❯ ${ansi.reset}` : ' ';
304
- const label = active
305
- ? `${ansi.cyan}${ansi.inverse} ${opt.label} ${ansi.reset}`
306
- : opt.label;
307
- const hint = opt.hint ? ` ${ansi.dim}${opt.hint}${ansi.reset}` : '';
308
- rows.push(`${rail()} ${cursor}${box} ${label}${hint}`);
309
- }
310
- if (error) {
311
- rows.push(`${rail()} ${ansi.red}${error}${ansi.reset}`);
312
- }
313
- return rows.join('\n');
314
- };
315
-
316
- rerender(draw);
317
-
318
- return new Promise((resolve, reject) => {
319
- promptReject(reject);
320
- onKey((_str, key) => {
321
- const name = key.name;
322
- if (name === 'up' || name === 'k') {
323
- index = (index - 1 + options.length) % options.length;
324
- error = '';
325
- rerender(draw);
326
- return;
327
- }
328
- if (name === 'down' || name === 'j') {
329
- index = (index + 1) % options.length;
330
- error = '';
331
- rerender(draw);
332
- return;
333
- }
334
- if (name === 'space') {
335
- const val = options[index].value;
336
- if (selected.has(val)) selected.delete(val);
337
- else selected.add(val);
338
- error = '';
339
- rerender(draw);
340
- return;
341
- }
342
- if (name === 'return') {
343
- if (selected.size < min) {
344
- error = `Select at least ${min} option${min === 1 ? '' : 's'} (${selected.size}/${min})`;
345
- rerender(draw);
346
- return;
347
- }
348
- const values = options
349
- .filter((o) => selected.has(o.value))
350
- .map((o) => o.value);
351
- const labels = options
352
- .filter((o) => selected.has(o.value))
353
- .map((o) => o.label)
354
- .join(', ');
355
- finishPrompt(
356
- lineCount(draw()),
357
- `${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${labels || '(none)'}${ansi.reset}`,
358
- );
359
- resolve(values);
360
- }
361
- });
362
- });
363
- });
364
- }
365
-
366
226
  /**
367
227
  * @param {string} message
368
228
  * @param {{ initial?: boolean }} [opts]
@@ -380,9 +240,10 @@ export async function confirm(message, { initial = false } = {}) {
380
240
  : `${ansi.dim}No${ansi.reset}`;
381
241
  return [
382
242
  `${prefixGlyph(false)} ${message}`,
383
- rail(),
384
- `${rail()} ${yes} / ${no}`,
385
- `${rail()} ${ansi.dim}←/→ or y/n, Enter${ansi.reset}`,
243
+ gutter(),
244
+ `${gutter()} ${yes} / ${no}`,
245
+ railEnd(),
246
+ navLegendRow(),
386
247
  ].join('\n');
387
248
  };
388
249
 
@@ -392,12 +253,12 @@ export async function confirm(message, { initial = false } = {}) {
392
253
  promptReject(reject);
393
254
  onKey((str, key) => {
394
255
  const name = key.name;
395
- if (name === 'left' || name === 'y') {
256
+ if (name === 'y') {
396
257
  value = true;
397
258
  rerender(draw);
398
259
  return;
399
260
  }
400
- if (name === 'right' || name === 'n') {
261
+ if (name === 'n') {
401
262
  value = false;
402
263
  rerender(draw);
403
264
  return;
@@ -406,18 +267,10 @@ export async function confirm(message, { initial = false } = {}) {
406
267
  const label = value ? 'Yes' : 'No';
407
268
  finishPrompt(
408
269
  lineCount(draw()),
409
- `${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${label}${ansi.reset}`,
270
+ `${message} ${ansi.dim}· ${label}${ansi.reset}`,
410
271
  );
411
272
  resolve(value);
412
273
  }
413
- if (str === 'y' || str === 'Y') {
414
- value = true;
415
- rerender(draw);
416
- }
417
- if (str === 'n' || str === 'N') {
418
- value = false;
419
- rerender(draw);
420
- }
421
274
  });
422
275
  });
423
276
  });
@@ -425,128 +278,10 @@ export async function confirm(message, { initial = false } = {}) {
425
278
 
426
279
  /**
427
280
  * @param {string} message
428
- * @param {{ initial?: string, placeholder?: string }} [opts]
429
281
  */
430
- export async function text(message, { initial = '', placeholder = '' } = {}) {
431
- let value = String(initial ?? '');
432
-
433
- return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
434
- const draw = () => {
435
- const shown = value.length > 0
436
- ? value
437
- : (placeholder ? `${ansi.dim}${placeholder}${ansi.reset}` : '');
438
- return [
439
- `${prefixGlyph(false)} ${message}`,
440
- rail(),
441
- `${rail()} ${shown}${ansi.dim}▌${ansi.reset}`,
442
- ].join('\n');
443
- };
444
-
445
- rerender(draw);
446
-
447
- return new Promise((resolve, reject) => {
448
- promptReject(reject);
449
- onKey((str, key) => {
450
- const name = key.name;
451
- if (name === 'return') {
452
- const out = value.length > 0 ? value : String(initial ?? '');
453
- finishPrompt(
454
- lineCount(draw()),
455
- `${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${out || '(empty)'}${ansi.reset}`,
456
- );
457
- resolve(out);
458
- return;
459
- }
460
- if (name === 'backspace') {
461
- value = value.slice(0, -1);
462
- rerender(draw);
463
- return;
464
- }
465
- if (str && !key.ctrl && !key.meta && str >= ' ') {
466
- value += str;
467
- rerender(draw);
468
- }
469
- });
470
- });
471
- });
472
- }
473
-
474
- /**
475
- * @param {string} message
476
- */
477
- export async function password(message) {
478
- let value = '';
479
-
480
- return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
481
- const draw = () => {
482
- const masked = value.length > 0 ? '•'.repeat(value.length) : '';
483
- return [
484
- `${prefixGlyph(false)} ${message}`,
485
- rail(),
486
- `${rail()} ${masked}${ansi.dim}▌${ansi.reset}`,
487
- ].join('\n');
488
- };
489
-
490
- rerender(draw);
491
-
492
- return new Promise((resolve, reject) => {
493
- promptReject(reject);
494
- onKey((str, key) => {
495
- const name = key.name;
496
- if (name === 'return') {
497
- finishPrompt(
498
- lineCount(draw()),
499
- `${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${'•'.repeat(value.length)}${ansi.reset}`,
500
- );
501
- resolve(value);
502
- return;
503
- }
504
- if (name === 'backspace') {
505
- value = value.slice(0, -1);
506
- rerender(draw);
507
- return;
508
- }
509
- if (str && !key.ctrl && !key.meta && str >= ' ') {
510
- value += str;
511
- rerender(draw);
512
- }
513
- });
514
- });
515
- });
516
- }
517
-
518
- /**
519
- * @param {string} message
520
- * @param {{ total?: number, width?: number }} [opts]
521
- */
522
- export function createProgressBar(message, { total = 100, width = 24 } = {}) {
523
- const safeTotal = total > 0 ? total : 100;
524
- let lastDone = 0;
525
- let closed = false;
526
-
527
- const render = (done, tot) => {
528
- const clamped = Math.max(0, Math.min(tot, done));
529
- const pct = Math.min(100, Math.floor((clamped / tot) * 100));
530
- const filled = Math.min(width, Math.round((clamped / tot) * width));
531
- const bar = `${'█'.repeat(filled)}${'░'.repeat(width - filled)}`;
532
- stdout.write(`\r${message} [${bar}] ${pct}%`);
533
- lastDone = clamped;
534
- };
535
-
536
- return {
537
- update(done, totalOverride) {
538
- if (closed) return;
539
- const tot = totalOverride !== undefined && totalOverride > 0 ? totalOverride : safeTotal;
540
- render(done, tot);
541
- },
542
- done(finalMsg) {
543
- if (closed) return;
544
- closed = true;
545
- const tail = finalMsg ? ` ${finalMsg}` : '';
546
- stdout.write('\r\x1b[2K');
547
- stdout.write(`${ansi.green}✔${ansi.reset} ${message}${tail}\n`);
548
- },
549
- };
282
+ export function outro(message) {
283
+ writeBlock(gutter());
284
+ writeBlock(`${ansi.green}└${ansi.reset} ${message}`);
550
285
  }
551
286
 
552
287
  /**
@@ -578,29 +313,13 @@ export function createSpinner(message) {
578
313
  stopped = true;
579
314
  if (timer) clearInterval(timer);
580
315
  stdout.write('\r\x1b[2K');
581
- const mark = ok ? `${ansi.green}✔${ansi.reset}` : `${ansi.red}✖${ansi.reset}`;
582
316
  const tail = finalMsg ? ` ${finalMsg}` : '';
317
+ if (ok) {
318
+ stdout.write(`${ansi.green}✓${ansi.reset} ${current}${tail}\n`);
319
+ return;
320
+ }
321
+ const mark = `${ansi.red}✖${ansi.reset}`;
583
322
  stdout.write(`${mark} ${current}${tail}\n`);
584
323
  },
585
324
  };
586
- }
587
-
588
- if (process.argv[1] === fileURLToPath(import.meta.url) && process.env.TUI_SMOKE) {
589
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
590
- (async () => {
591
- const bar = createProgressBar('TUI smoke progress', { total: 100, width: 20 });
592
- for (let i = 0; i <= 100; i += 10) {
593
- bar.update(i);
594
- await sleep(40);
595
- }
596
- bar.done('complete');
597
- const spin = createSpinner('TUI smoke spinner');
598
- await sleep(400);
599
- spin.update('TUI smoke spinner (tick)');
600
- await sleep(400);
601
- spin.stop('stopped', true);
602
- })().catch((err) => {
603
- console.error(err);
604
- process.exit(1);
605
- });
606
325
  }
@@ -15,7 +15,6 @@ const ENV_KEY_MAP = {
15
15
  gemini: 'GEMINI_API_KEY',
16
16
  deepseek: 'DEEPSEEK_API_KEY',
17
17
  xai: 'XAI_API_KEY',
18
- nvidia: 'NVIDIA_API_KEY',
19
18
  };
20
19
  // Canonical maintenance defaults. Single source of truth — imported by
21
20
  // llm/index.mjs and setup-server.mjs so UI/runtime cannot drift from config.
@@ -1790,11 +1790,8 @@ export async function loginOAuth() {
1790
1790
  url.searchParams.set('code_challenge_method', 'S256');
1791
1791
  url.searchParams.set('state', state);
1792
1792
  process.stderr.write(`\n[anthropic-oauth] Open this URL to log in with Claude:\n${url.toString()}\n\n`);
1793
- try {
1794
- const { exec } = await import('child_process');
1795
- const opener = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
1796
- exec(`${opener} "${url.toString()}"`, { windowsHide: true });
1797
- } catch { /* user opens manually */ }
1793
+ const { openInBrowser } = await import('../../../shared/open-url.mjs');
1794
+ openInBrowser(url.toString());
1798
1795
 
1799
1796
  return new Promise((resolve) => {
1800
1797
  const timeout = setTimeout(() => { server.close(); resolve(null); }, OAUTH_LOGIN_TIMEOUT_MS);