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
@@ -0,0 +1,37 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ /**
4
+ * Open a URL in the user's default browser. Best-effort and non-blocking —
5
+ * the caller always prints the URL too, so a failure here is never fatal.
6
+ *
7
+ * Platform dispatch (deterministic, not heuristic):
8
+ * - Windows: `rundll32 url.dll,FileProtocolHandler <url>` passes the URL as a
9
+ * single argv token, so query-string `&` separators are NOT re-parsed by a
10
+ * shell. The old `start "<url>"` form is broken on Windows: cmd's `start`
11
+ * treats the first quoted string as the WINDOW TITLE, so the URL is dropped
12
+ * and nothing opens.
13
+ * - macOS: `open <url>`.
14
+ * - Linux/other: `xdg-open <url>`.
15
+ */
16
+ export function openInBrowser(url) {
17
+ const u = String(url);
18
+ let cmd;
19
+ let args;
20
+ if (process.platform === 'win32') {
21
+ cmd = 'rundll32';
22
+ args = ['url.dll,FileProtocolHandler', u];
23
+ } else if (process.platform === 'darwin') {
24
+ cmd = 'open';
25
+ args = [u];
26
+ } else {
27
+ cmd = 'xdg-open';
28
+ args = [u];
29
+ }
30
+ try {
31
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true });
32
+ child.on('error', () => { /* opener missing — user uses the printed URL */ });
33
+ child.unref();
34
+ } catch {
35
+ /* spawn threw synchronously — user opens the printed URL manually */
36
+ }
37
+ }
@@ -3,7 +3,7 @@ import { dirname, join } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { DEFAULT_PRESETS, DEFAULT_MAINTENANCE } from '../agent/orchestrator/config.mjs';
5
5
  import { writeFileAtomicSync, withFileLockSync } from './atomic-file.mjs';
6
- import { backupUserData, markUserDataInitialized, shouldSeedMissingUserData } from './user-data-guard.mjs';
6
+ import { backupUserData, hasUserDataInitMarker, markUserDataInitialized, shouldSeedMissingUserData } from './user-data-guard.mjs';
7
7
  import { disableClaudeBuiltinsOnFirstInstall } from './disable-claude-builtins.mjs';
8
8
 
9
9
  const DEFAULTS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'defaults');
@@ -49,12 +49,27 @@ const SEEDS = {
49
49
  };
50
50
  return JSON.stringify(composed, null, 2) + '\n';
51
51
  },
52
+ // Role→preset mapping consumed by Smart Bridge (loadResolvedRoles); without
53
+ // it on disk bridge roles fall back to the default preset. Baseline Lead
54
+ // workflow description ships alongside. Seeded HERE (not in setup-server)
55
+ // so this is the single first-install SSOT and the init marker is set once,
56
+ // after the whole default set lands.
57
+ 'user-workflow.json': () => readFileSync(join(DEFAULTS_DIR, 'user-workflow.json'), 'utf8'),
58
+ 'user-workflow.md': () => readFileSync(join(DEFAULTS_DIR, 'user-workflow.md'), 'utf8'),
52
59
  };
53
60
 
54
61
  export function ensureDataSeeds(dataDir) {
55
62
  if (!dataDir) return { created: [], skipped: [] };
56
63
  const created = [];
57
64
  const skipped = [];
65
+ // Capture fresh-install state ONCE, before the loop. The per-file
66
+ // markUserDataInitialized() below sets the marker as soon as the first seed
67
+ // lands; if we re-consulted the guard per file, every SUBSEQUENT first-time
68
+ // seed in this same pass would be refused (treated as a post-init deletion),
69
+ // which is exactly how user-workflow.json could end up permanently missing.
70
+ // On a fresh dir we seed the whole default set; once initialized, the guard
71
+ // governs (never recreate a file the user deleted on purpose).
72
+ const freshInstall = !hasUserDataInitMarker(dataDir);
58
73
  for (const [rel, bodyFn] of Object.entries(SEEDS)) {
59
74
  const full = join(dataDir, rel);
60
75
  if (existsSync(full)) {
@@ -62,7 +77,7 @@ export function ensureDataSeeds(dataDir) {
62
77
  skipped.push(rel);
63
78
  continue;
64
79
  }
65
- if (!shouldSeedMissingUserData(dataDir, rel)) {
80
+ if (!freshInstall && !shouldSeedMissingUserData(dataDir, rel)) {
66
81
  skipped.push(rel);
67
82
  continue;
68
83
  }
@@ -112,7 +127,9 @@ export function ensureDataSeeds(dataDir) {
112
127
  }
113
128
  }
114
129
  if (created.length > 0) {
115
- process.stderr.write(`[seed] created ${created.length} file(s): ${created.join(', ')}\n`);
130
+ if (process.env.MIXDOG_SETUP_QUIET !== '1') {
131
+ process.stderr.write(`[seed] created ${created.length} file(s): ${created.join(', ')}\n`);
132
+ }
116
133
  try { backupUserData(dataDir, 'post-seed'); } catch {}
117
134
  }
118
135
  return { created, skipped };
@@ -153,7 +153,9 @@ export function backupUserData(dataDir, reason = 'snapshot') {
153
153
  if (copied.length > 0) {
154
154
  markUserDataInitialized(dataDir);
155
155
  pruneBackups();
156
- process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}\n`);
156
+ if (process.env.MIXDOG_SETUP_QUIET !== '1') {
157
+ process.stderr.write(`[user-data-backup] ${reason}: copied ${copied.length} file(s) to ${backupDir}\n`);
158
+ }
157
159
  }
158
160
  return { dir: copied.length > 0 ? backupDir : null, copied };
159
161
  }
@@ -1,122 +0,0 @@
1
- /**
2
- * Repro: malformed mixdog-config.json + writeSection('search', …) must not
3
- * wipe channels/memory/agent; restores from backup or throws.
4
- */
5
- import assert from 'node:assert/strict';
6
- import {
7
- mkdtempSync,
8
- mkdirSync,
9
- writeFileSync,
10
- readFileSync,
11
- rmSync,
12
- } from 'fs';
13
- import { tmpdir } from 'os';
14
- import { join, dirname } from 'path';
15
- import { fileURLToPath } from 'url';
16
-
17
- const __dirname = dirname(fileURLToPath(import.meta.url));
18
-
19
- async function loadConfigModule(dataDir, backupRoot) {
20
- process.env.CLAUDE_PLUGIN_DATA = dataDir;
21
- process.env.MIXDOG_USER_DATA_BACKUP_ROOT = backupRoot;
22
- process.env.MIXDOG_SKIP_USER_DATA_BACKUP = '1';
23
- const url = new URL(`../src/shared/config.mjs?run=${Date.now()}`, import.meta.url).href;
24
- return import(url);
25
- }
26
-
27
- function writeConfig(dataDir, obj) {
28
- writeFileSync(
29
- join(dataDir, 'mixdog-config.json'),
30
- JSON.stringify(obj, null, 2) + '\n',
31
- 'utf8',
32
- );
33
- }
34
-
35
- async function main() {
36
- const dataDir = mkdtempSync(join(tmpdir(), 'mixdog-config-rmw-'));
37
- const backupRoot = mkdtempSync(join(tmpdir(), 'mixdog-config-backup-'));
38
-
39
- const prior = {
40
- channels: { guild: '111' },
41
- memory: { enabled: true },
42
- agent: { presets: { default: { model: 'x' } } },
43
- };
44
- writeConfig(dataDir, prior);
45
-
46
- process.env.CLAUDE_PLUGIN_DATA = dataDir;
47
- process.env.MIXDOG_USER_DATA_BACKUP_ROOT = backupRoot;
48
- const guardUrl = new URL(`../src/shared/user-data-guard.mjs?t=${Date.now()}`, import.meta.url).href;
49
- const { backupUserData, markUserDataInitialized } = await import(guardUrl);
50
- const snap = backupUserData(dataDir, 'test-fixture');
51
- assert.ok(snap.dir, 'backup fixture should copy mixdog-config.json');
52
-
53
- writeFileSync(join(dataDir, 'mixdog-config.json'), '{ not valid json\n', 'utf8');
54
-
55
- const { writeSection } = await loadConfigModule(dataDir, backupRoot);
56
- writeSection('search', { provider: 'brave' });
57
-
58
- const onDisk = JSON.parse(readFileSync(join(dataDir, 'mixdog-config.json'), 'utf8'));
59
- assert.deepEqual(onDisk.channels, prior.channels);
60
- assert.deepEqual(onDisk.memory, prior.memory);
61
- assert.deepEqual(onDisk.agent, prior.agent);
62
- assert.deepEqual(onDisk.search, { provider: 'brave' });
63
-
64
- const freshDir = mkdtempSync(join(tmpdir(), 'mixdog-config-fresh-'));
65
- const { writeSection: writeFresh } = await loadConfigModule(freshDir, backupRoot);
66
- writeFresh('search', { only: true });
67
- const freshDisk = JSON.parse(readFileSync(join(freshDir, 'mixdog-config.json'), 'utf8'));
68
- assert.deepEqual(freshDisk, { search: { only: true } });
69
-
70
- const noBackupDir = mkdtempSync(join(tmpdir(), 'mixdog-config-noback-'));
71
- markUserDataInitialized(noBackupDir);
72
- writeFileSync(join(noBackupDir, 'mixdog-config.json'), '[]', 'utf8');
73
- const { writeSection: writeNoBackup } = await loadConfigModule(
74
- noBackupDir,
75
- mkdtempSync(join(tmpdir(), 'empty-backup-')),
76
- );
77
- let threw = false;
78
- try {
79
- writeNoBackup('search', { x: 1 });
80
- } catch (err) {
81
- threw = true;
82
- assert.match(String(err.message), /refusing section write/);
83
- }
84
- assert.equal(threw, true, 'malformed config with init marker and no backup must throw');
85
-
86
- const pickRoot = mkdtempSync(join(tmpdir(), 'mixdog-config-pick-'));
87
- const fullCfg = {
88
- channels: { guild: '222' },
89
- memory: { enabled: false },
90
- agent: { presets: {} },
91
- };
92
- const oldDir = join(pickRoot, '2026-06-03T19-00-00-000Z-old-full');
93
- const newDir = join(pickRoot, '2026-06-03T21-00-00-000Z-new-degenerate');
94
- mkdirSync(oldDir, { recursive: true });
95
- mkdirSync(newDir, { recursive: true });
96
- writeFileSync(join(oldDir, 'mixdog-config.json'), JSON.stringify(fullCfg) + '\n', 'utf8');
97
- writeFileSync(
98
- join(newDir, 'mixdog-config.json'),
99
- JSON.stringify({ search: { provider: 'tavily' } }) + '\n',
100
- 'utf8',
101
- );
102
- process.env.MIXDOG_USER_DATA_BACKUP_ROOT = pickRoot;
103
- const pickUrl = new URL(`../src/shared/user-data-guard.mjs?pick=${Date.now()}`, import.meta.url).href;
104
- const { loadLatestMixdogConfigFromBackup } = await import(pickUrl);
105
- const picked = loadLatestMixdogConfigFromBackup(null);
106
- assert.deepEqual(picked?.channels, fullCfg.channels);
107
- assert.deepEqual(picked?.agent, fullCfg.agent);
108
- assert.equal(picked?.search, undefined, 'must not restore newest search-only snapshot');
109
- rmSync(pickRoot, { recursive: true, force: true });
110
-
111
- rmSync(dataDir, { recursive: true, force: true });
112
- rmSync(backupRoot, { recursive: true, force: true });
113
- rmSync(freshDir, { recursive: true, force: true });
114
- rmSync(noBackupDir, { recursive: true, force: true });
115
-
116
- console.log('test-config-rmw-restore: ok');
117
- }
118
-
119
- main().catch((err) => {
120
- console.error(err);
121
- process.exit(1);
122
- });