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.
- package/.claude-plugin/marketplace.json +5 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +40 -0
- package/README.md +198 -251
- package/bin/statusline-launcher.mjs +5 -1
- package/bin/statusline-lib.mjs +14 -6
- package/bin/statusline.mjs +14 -6
- package/hooks/lib/settings-loader.cjs +4 -3
- package/hooks/pre-tool-subagent.cjs +7 -2
- package/hooks/session-start.cjs +52 -24
- package/lib/mixdog-debug.cjs +163 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +1 -1
- package/scripts/builtin-utils-smoke.mjs +14 -8
- package/scripts/bump.mjs +80 -0
- package/scripts/doctor.mjs +8 -3
- package/scripts/mutation-io-smoke.mjs +17 -1
- package/scripts/openai-oauth-catalog-smoke.mjs +53 -0
- package/scripts/permission-eval-smoke.mjs +18 -1
- package/scripts/statusline-launcher-smoke.mjs +2 -2
- package/scripts/webhook-selfheal-smoke.mjs +1 -3
- package/server-main.mjs +57 -3
- package/setup/config-merge.mjs +0 -1
- package/setup/install.mjs +241 -51
- package/setup/mixdog-cli.mjs +30 -3
- package/setup/setup-server.mjs +21 -33
- package/setup/setup.html +46 -11
- package/setup/tui.mjs +35 -316
- package/src/agent/orchestrator/config.mjs +0 -1
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +2 -5
- package/src/agent/orchestrator/providers/anthropic.mjs +243 -86
- package/src/agent/orchestrator/providers/gemini.mjs +386 -31
- package/src/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/agent/orchestrator/providers/model-catalog.mjs +146 -13
- package/src/agent/orchestrator/providers/openai-compat-stream.mjs +366 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +74 -30
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -1
- package/src/agent/orchestrator/providers/openai-oauth.mjs +66 -13
- package/src/agent/orchestrator/providers/openai-ws.mjs +23 -0
- package/src/agent/orchestrator/session/manager.mjs +18 -4
- package/src/agent/orchestrator/stall-policy.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +29 -8
- package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/channels/index.mjs +27 -8
- package/src/channels/lib/event-queue.mjs +24 -1
- package/src/channels/lib/hook-pipe-server.mjs +21 -8
- package/src/channels/lib/webhook.mjs +142 -20
- package/src/memory/lib/memory-cycle1.mjs +7 -3
- package/src/memory/lib/memory-recall-store.mjs +27 -10
- package/src/search/lib/backends/openai-oauth.mjs +6 -2
- package/src/search/lib/cache.mjs +55 -7
- package/src/shared/config.mjs +1 -1
- package/src/shared/llm/cost.mjs +2 -2
- package/src/shared/open-url.mjs +37 -0
- package/src/shared/seed.mjs +20 -3
- package/src/shared/user-data-guard.mjs +3 -1
- package/scripts/test-config-rmw-restore.mjs +0 -122
- 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
|
+
}
|
package/src/shared/seed.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
});
|