mixdog 0.7.6 → 0.7.8
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/CHANGELOG.md +29 -0
- package/README.md +8 -4
- package/hooks/session-start.cjs +73 -2
- package/package.json +1 -1
- package/scripts/bootstrap.mjs +5 -59
- package/scripts/ensure-deps.mjs +259 -0
- package/scripts/resolve-bun.mjs +60 -0
- package/scripts/run-mcp.mjs +13 -168
- package/setup/install.mjs +227 -10
- package/setup/launch.mjs +0 -0
- package/setup/locate-claude.mjs +38 -0
- package/setup/mixdog-cli.mjs +6 -42
- package/setup/setup-server.mjs +50 -2
- package/setup/setup.html +26 -12
- package/setup/tui.mjs +606 -0
- package/setup/wizard.mjs +117 -128
- package/src/agent/bridge-stall-watchdog.mjs +2 -2
- package/src/agent/index.mjs +3 -3
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +139 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +96 -0
- package/src/agent/orchestrator/session/manager.mjs +5 -3
- package/src/agent/orchestrator/session/store.mjs +9 -1
- package/src/channels/lib/runtime-paths.mjs +112 -74
- package/src/memory/index.mjs +30 -7
- package/src/memory/lib/pg/supervisor.mjs +12 -12
- package/src/shared/atomic-file.mjs +16 -0
- package/src/shared/disable-claude-builtins.mjs +7 -4
- package/src/shared/user-data-guard.mjs +5 -1
- package/src/status/aggregator.mjs +3 -3
package/scripts/run-mcp.mjs
CHANGED
|
@@ -19,10 +19,14 @@ import { fileURLToPath } from 'url';
|
|
|
19
19
|
import { createRequire } from 'module';
|
|
20
20
|
import { dirname, join } from 'path';
|
|
21
21
|
import * as fs from 'fs';
|
|
22
|
-
import {
|
|
23
|
-
import { execSync, spawn
|
|
22
|
+
import { randomUUID } from 'crypto';
|
|
23
|
+
import { execSync, spawn } from 'child_process';
|
|
24
24
|
import * as os from 'os';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
ensureRuntimeDeps,
|
|
27
|
+
hasRequiredDeps,
|
|
28
|
+
renameWithRetrySync,
|
|
29
|
+
} from './ensure-deps.mjs';
|
|
26
30
|
|
|
27
31
|
// Stable per-terminal session id for this proxy supervisor's lifetime. The
|
|
28
32
|
// child server.mjs is respawned on crash / dev-sync restart, but THIS
|
|
@@ -34,29 +38,6 @@ import { assertSafeOwnedDir } from '../src/shared/user-data-guard.mjs';
|
|
|
34
38
|
// upstream-provided id if one already exists.
|
|
35
39
|
const STABLE_TERMINAL_SESSION_ID = process.env.MIXDOG_SESSION_ID || randomUUID();
|
|
36
40
|
|
|
37
|
-
const RENAME_RETRY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY', 'EEXIST']);
|
|
38
|
-
const RENAME_BACKOFFS_MS = Object.freeze([25, 50, 100, 200, 400, 800, 1200, 1600]);
|
|
39
|
-
function sleepSync(ms) {
|
|
40
|
-
try {
|
|
41
|
-
const buf = new SharedArrayBuffer(4);
|
|
42
|
-
Atomics.wait(new Int32Array(buf), 0, 0, Math.max(1, Number(ms) || 1));
|
|
43
|
-
} catch {}
|
|
44
|
-
}
|
|
45
|
-
function renameWithRetrySync(src, dst) {
|
|
46
|
-
let lastErr = null;
|
|
47
|
-
for (let attempt = 0; attempt <= RENAME_BACKOFFS_MS.length; attempt++) {
|
|
48
|
-
try {
|
|
49
|
-
fs.renameSync(src, dst);
|
|
50
|
-
return true;
|
|
51
|
-
} catch (err) {
|
|
52
|
-
lastErr = err;
|
|
53
|
-
if (!RENAME_RETRY_CODES.has(err?.code) || attempt >= RENAME_BACKOFFS_MS.length) break;
|
|
54
|
-
sleepSync(RENAME_BACKOFFS_MS[attempt] + Math.floor(Math.random() * 50));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
throw lastErr;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
41
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
61
42
|
const __localRoot = join(__dirname, '..');
|
|
62
43
|
|
|
@@ -87,7 +68,6 @@ if (pluginRoot !== __localRoot) {
|
|
|
87
68
|
}
|
|
88
69
|
const serverPath = join(pluginRoot, 'server.mjs');
|
|
89
70
|
const pluginPkg = join(pluginRoot, 'package.json');
|
|
90
|
-
const pluginLock = join(pluginRoot, 'bun.lock');
|
|
91
71
|
const pluginNm = join(pluginRoot, 'node_modules');
|
|
92
72
|
|
|
93
73
|
process.stderr.write(`[boot-time] tag=run-mcp-entry tMs=${Date.now()}\n`);
|
|
@@ -110,17 +90,6 @@ try {
|
|
|
110
90
|
// server.mjs reads MIXDOG_SUPERVISOR_PID + MIXDOG_SUPERVISOR_CACHE_DIR
|
|
111
91
|
// from its env (set in spawnChild below) to identify the supervisor.
|
|
112
92
|
|
|
113
|
-
const requiredDepNames = [
|
|
114
|
-
['@modelcontextprotocol', 'sdk', 'package.json'],
|
|
115
|
-
['zod', 'package.json'],
|
|
116
|
-
['zod-to-json-schema', 'package.json'],
|
|
117
|
-
['openai', 'package.json'],
|
|
118
|
-
];
|
|
119
|
-
|
|
120
|
-
function hasRequiredDeps(nmDir) {
|
|
121
|
-
return requiredDepNames.every((parts) => fs.existsSync(join(nmDir, ...parts)));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
93
|
// ── Lightweight JSON-RPC line scanner ────────────────────────────────────────
|
|
125
94
|
// Extracts `id` and `method` from a JSON-RPC line without a full JSON.parse.
|
|
126
95
|
// Returns { id, method } (each may be undefined), or null on scan failure.
|
|
@@ -256,50 +225,6 @@ function _scanIdMethod(line) {
|
|
|
256
225
|
}
|
|
257
226
|
}
|
|
258
227
|
|
|
259
|
-
const LOCK_POLL_MS = 250;
|
|
260
|
-
const LOCK_MAX_MS = 15 * 60 * 1000;
|
|
261
|
-
const LOCK_XHOST_MS = 10 * 60 * 1000;
|
|
262
|
-
|
|
263
|
-
function acquireLock(lockFile) {
|
|
264
|
-
const start = Date.now();
|
|
265
|
-
while (Date.now() - start < LOCK_MAX_MS) {
|
|
266
|
-
try {
|
|
267
|
-
const body = JSON.stringify({
|
|
268
|
-
pid: process.pid,
|
|
269
|
-
hostname: os.hostname(),
|
|
270
|
-
startedAt: Date.now(),
|
|
271
|
-
});
|
|
272
|
-
// 'wx' = O_CREAT | O_EXCL — fails atomically if file already exists.
|
|
273
|
-
fs.writeFileSync(lockFile, body, { flag: 'wx' });
|
|
274
|
-
return;
|
|
275
|
-
} catch (e) {
|
|
276
|
-
if (e.code !== 'EEXIST') throw e;
|
|
277
|
-
try {
|
|
278
|
-
const raw = fs.readFileSync(lockFile, 'utf8');
|
|
279
|
-
const body = JSON.parse(raw);
|
|
280
|
-
const st = fs.statSync(lockFile);
|
|
281
|
-
const sameHost = body.hostname === os.hostname();
|
|
282
|
-
let dead = false;
|
|
283
|
-
if (sameHost) {
|
|
284
|
-
try { process.kill(body.pid, 0); }
|
|
285
|
-
catch (ke) { if (ke.code === 'ESRCH') dead = true; }
|
|
286
|
-
} else {
|
|
287
|
-
if (Date.now() - st.mtimeMs > LOCK_XHOST_MS) dead = true;
|
|
288
|
-
}
|
|
289
|
-
if (dead) fs.unlinkSync(lockFile);
|
|
290
|
-
} catch { /* lock may have been released between read and stat — retry */ }
|
|
291
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_POLL_MS);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
throw new Error(
|
|
295
|
-
`timed out waiting for dependency install lock after ${LOCK_MAX_MS / 60000} minutes`
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function releaseLock(lockFile) {
|
|
300
|
-
try { fs.unlinkSync(lockFile); } catch {}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
228
|
function ensureNmSymlink(linkPath, targetPath) {
|
|
304
229
|
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
|
305
230
|
// EPERM/EBUSY here is almost always a transient AV / indexer lock on the
|
|
@@ -353,32 +278,6 @@ function ensureNmSymlink(linkPath, targetPath) {
|
|
|
353
278
|
trySymlink();
|
|
354
279
|
}
|
|
355
280
|
|
|
356
|
-
function sha256(buf) {
|
|
357
|
-
return createHash('sha256').update(buf).digest('hex');
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* SHA-256 hash that changes iff the resolved dep tree changes.
|
|
362
|
-
* Primary: bun.lock. Fallback: dep-key objects from package.json (so the very
|
|
363
|
-
* first install — before bun.lock exists — still hashes deterministically).
|
|
364
|
-
*/
|
|
365
|
-
function computeDepHash(pkgJsonPath, pkgLockPath) {
|
|
366
|
-
if (fs.existsSync(pkgLockPath)) {
|
|
367
|
-
return sha256(fs.readFileSync(pkgLockPath));
|
|
368
|
-
}
|
|
369
|
-
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
370
|
-
const depKeys = ['dependencies', 'optionalDependencies', 'peerDependencies'];
|
|
371
|
-
const depObj = {};
|
|
372
|
-
for (const k of depKeys) {
|
|
373
|
-
if (pkg[k]) {
|
|
374
|
-
depObj[k] = Object.fromEntries(
|
|
375
|
-
Object.entries(pkg[k]).sort(([a], [b]) => a.localeCompare(b))
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return sha256(Buffer.from(JSON.stringify(depObj)));
|
|
380
|
-
}
|
|
381
|
-
|
|
382
281
|
const require = createRequire(import.meta.url);
|
|
383
282
|
const { resolvePluginData } = require('../lib/plugin-paths.cjs');
|
|
384
283
|
const dataDir = resolvePluginData();
|
|
@@ -433,67 +332,13 @@ try {
|
|
|
433
332
|
// Install runtime deps into a DEDICATED <dataDir>/.deps/ subdir — NEVER the
|
|
434
333
|
// data root, which holds user data (mixdog-config.json, user-workflow.*,
|
|
435
334
|
// roles/). Running `bun install` with cwd=dataDir would wipe that state.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const stampTmp = join(depsDir, '.deps-stamp.tmp');
|
|
442
|
-
const lockFile = join(depsDir, '.install.lock');
|
|
443
|
-
|
|
444
|
-
const currentHash = computeDepHash(pluginPkg, pluginLock);
|
|
445
|
-
let storedHash = '';
|
|
446
|
-
try { storedHash = fs.readFileSync(stamp, 'utf8').trim(); } catch {}
|
|
447
|
-
|
|
448
|
-
const needsInstall = (currentHash !== storedHash) || !hasRequiredDeps(sharedNm);
|
|
449
|
-
|
|
450
|
-
if (needsInstall) {
|
|
451
|
-
// Hard guard: refuse to install anywhere that would clobber user data.
|
|
452
|
-
// assertSafeOwnedDir throws unless depsDir is an owned subdir (.deps).
|
|
453
|
-
assertSafeOwnedDir(depsDir, dataDir, 'bun install');
|
|
454
|
-
fs.mkdirSync(depsDir, { recursive: true });
|
|
455
|
-
acquireLock(lockFile);
|
|
456
|
-
try {
|
|
457
|
-
fs.copyFileSync(pluginPkg, sharedPkg);
|
|
458
|
-
if (fs.existsSync(pluginLock)) fs.copyFileSync(pluginLock, sharedLock);
|
|
459
|
-
|
|
460
|
-
const args = fs.existsSync(sharedLock)
|
|
461
|
-
? ['install', '--frozen-lockfile']
|
|
462
|
-
: ['install'];
|
|
463
|
-
process.stderr.write(`[run-mcp] installing shared deps: bun ${args.join(' ')}\n`);
|
|
335
|
+
ensureRuntimeDeps({
|
|
336
|
+
dataDir,
|
|
337
|
+
pluginRoot,
|
|
338
|
+
bunPath: process.env.BUN_EXEC_PATH,
|
|
339
|
+
});
|
|
464
340
|
|
|
465
|
-
|
|
466
|
-
// routinely exceeds 30s; too low a ceiling times out into an empty
|
|
467
|
-
// node_modules and aborts the very first boot. 3 minutes covers a cold
|
|
468
|
-
// network fetch while still bounding a genuinely stuck install.
|
|
469
|
-
const INSTALL_TIMEOUT_MS = 180_000;
|
|
470
|
-
const result = spawnSync(process.env.BUN_EXEC_PATH || process.execPath, args, {
|
|
471
|
-
cwd: depsDir,
|
|
472
|
-
stdio: 'inherit',
|
|
473
|
-
timeout: INSTALL_TIMEOUT_MS,
|
|
474
|
-
windowsHide: true,
|
|
475
|
-
});
|
|
476
|
-
if (result.error?.code === 'ETIMEDOUT' || result.signal === 'SIGTERM') {
|
|
477
|
-
process.stderr.write(
|
|
478
|
-
`[run-mcp] WARN: bun install timed out after ${INSTALL_TIMEOUT_MS}ms — ` +
|
|
479
|
-
`continuing with existing node_modules (stale lock removed)\n`
|
|
480
|
-
);
|
|
481
|
-
try { fs.unlinkSync(lockFile); } catch {}
|
|
482
|
-
} else if (result.status !== 0) {
|
|
483
|
-
const detail = result.status ?? result.signal ?? 'unknown';
|
|
484
|
-
process.stderr.write(
|
|
485
|
-
`[run-mcp] WARN: bun install exited with status ${detail} — ` +
|
|
486
|
-
`continuing with existing node_modules if available\n`
|
|
487
|
-
);
|
|
488
|
-
} else {
|
|
489
|
-
// Atomic stamp write: tmp + rename so a crash cannot leave it half-written.
|
|
490
|
-
fs.writeFileSync(stampTmp, currentHash);
|
|
491
|
-
renameWithRetrySync(stampTmp, stamp);
|
|
492
|
-
}
|
|
493
|
-
} finally {
|
|
494
|
-
releaseLock(lockFile);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
341
|
+
const sharedNm = join(dataDir, '.deps', 'node_modules');
|
|
497
342
|
|
|
498
343
|
ensureNmSymlink(pluginNm, sharedNm);
|
|
499
344
|
|
package/setup/install.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// install.mjs — one-shot bootstrapper that registers the mixdog plugin in the
|
|
3
3
|
// user's Claude Code settings so it auto-loads on the next session start.
|
|
4
4
|
//
|
|
5
|
-
// Run via: npx mixdog
|
|
5
|
+
// Run via: npx mixdog | mixdog install | mixdog-install (after publish)
|
|
6
6
|
// or: node setup/install.mjs (from a checkout)
|
|
7
7
|
//
|
|
8
8
|
// It merges two keys into the user-scope settings file (preserving everything
|
|
@@ -19,14 +19,18 @@ import {
|
|
|
19
19
|
existsSync,
|
|
20
20
|
mkdirSync,
|
|
21
21
|
copyFileSync,
|
|
22
|
+
mkdtempSync,
|
|
23
|
+
rmSync,
|
|
22
24
|
} from 'node:fs';
|
|
23
|
-
import { join } from 'node:path';
|
|
24
|
-
import { homedir } from 'node:os';
|
|
25
|
+
import { join, dirname } from 'node:path';
|
|
26
|
+
import { homedir, tmpdir } from 'node:os';
|
|
25
27
|
import { realpathSync } from 'node:fs';
|
|
26
28
|
import { fileURLToPath } from 'node:url';
|
|
27
29
|
import { createInterface } from 'node:readline';
|
|
28
|
-
import { spawn } from 'node:child_process';
|
|
30
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
29
31
|
import { DEFAULT_MARKETPLACE, DEFAULT_PLUGIN } from '../src/shared/plugin-paths.mjs';
|
|
32
|
+
import { resolveClaudeExecutable } from './locate-claude.mjs';
|
|
33
|
+
import { createSpinner } from './tui.mjs';
|
|
30
34
|
|
|
31
35
|
const MARKETPLACE = DEFAULT_MARKETPLACE;
|
|
32
36
|
const PLUGIN_REF = `${DEFAULT_PLUGIN}@${DEFAULT_MARKETPLACE}`;
|
|
@@ -64,7 +68,7 @@ function isPlainObject(value) {
|
|
|
64
68
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
function registerPluginInSettings() {
|
|
71
|
+
function registerPluginInSettings(dryRun = false) {
|
|
68
72
|
const dir = settingsDir();
|
|
69
73
|
const file = join(dir, 'settings.json');
|
|
70
74
|
|
|
@@ -90,6 +94,15 @@ function registerPluginInSettings() {
|
|
|
90
94
|
const already = settings.enabledPlugins[PLUGIN_REF] === true;
|
|
91
95
|
settings.enabledPlugins[PLUGIN_REF] = true;
|
|
92
96
|
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
console.log(`[dry-run] would register mixdog in ${file}`);
|
|
99
|
+
console.log(`[dry-run] marketplace "${MARKETPLACE}" → github:${REPO}`);
|
|
100
|
+
console.log(
|
|
101
|
+
`[dry-run] enabled plugin "${PLUGIN_REF}"${already ? ' (was already enabled)' : ''}`,
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
// Back up an existing file before the first write; create the dir otherwise.
|
|
94
107
|
if (existsSync(file)) {
|
|
95
108
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -107,19 +120,223 @@ function registerPluginInSettings() {
|
|
|
107
120
|
console.log(`\nNext: restart Claude Code (or run /reload-plugins). mixdog loads automatically.`);
|
|
108
121
|
}
|
|
109
122
|
|
|
123
|
+
const CLAUDE_SETUP_GUIDANCE =
|
|
124
|
+
'Install Claude Code first: https://code.claude.com/docs/en/setup';
|
|
125
|
+
|
|
126
|
+
function officialClaudeInstallerCommandDescription() {
|
|
127
|
+
if (process.platform === 'win32') {
|
|
128
|
+
return 'powershell -NoProfile -Command "irm https://claude.ai/install.ps1 | iex"';
|
|
129
|
+
}
|
|
130
|
+
return 'bash -c "curl -fsSL https://claude.ai/install.sh | bash"';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function runOfficialClaudeInstaller() {
|
|
134
|
+
if (process.platform === 'win32') {
|
|
135
|
+
spawnSync(
|
|
136
|
+
'powershell.exe',
|
|
137
|
+
['-NoProfile', '-Command', 'irm https://claude.ai/install.ps1 | iex'],
|
|
138
|
+
{ stdio: 'inherit', windowsHide: true },
|
|
139
|
+
);
|
|
140
|
+
} else {
|
|
141
|
+
spawnSync('bash', ['-c', 'curl -fsSL https://claude.ai/install.sh | bash'], {
|
|
142
|
+
stdio: 'inherit',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function askInstallClaude() {
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
150
|
+
rl.question(
|
|
151
|
+
'Claude Code CLI not found. Install it now with the official installer? (y/N) ',
|
|
152
|
+
(answer) => {
|
|
153
|
+
rl.close();
|
|
154
|
+
const a = answer.trim().toLowerCase();
|
|
155
|
+
resolve(a === 'y' || a === 'yes');
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function ensureClaudeInstalled(dryRun = false) {
|
|
162
|
+
const claudePath = resolveClaudeExecutable();
|
|
163
|
+
if (claudePath) {
|
|
164
|
+
if (dryRun) console.log(`[dry-run] Claude Code detected at ${claudePath}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (dryRun) {
|
|
169
|
+
const cmd = officialClaudeInstallerCommandDescription();
|
|
170
|
+
console.log(
|
|
171
|
+
`[dry-run] Claude Code NOT found — would prompt to install via the official installer (${cmd})`,
|
|
172
|
+
);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const interactive = process.stdin.isTTY && !process.env.CI;
|
|
177
|
+
|
|
178
|
+
if (!interactive) {
|
|
179
|
+
console.log(CLAUDE_SETUP_GUIDANCE);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const yes = await askInstallClaude();
|
|
184
|
+
if (yes) {
|
|
185
|
+
runOfficialClaudeInstaller();
|
|
186
|
+
if (resolveClaudeExecutable()) {
|
|
187
|
+
console.log('✓ Claude Code detected.');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
console.log(
|
|
191
|
+
"✓ Claude Code installed — open a NEW terminal so 'claude' is on PATH, then start Claude Code (mixdog is being registered now).",
|
|
192
|
+
);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(CLAUDE_SETUP_GUIDANCE);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const WIZARD_STEP_LABELS = [
|
|
200
|
+
'Address form',
|
|
201
|
+
'Discord bot token',
|
|
202
|
+
'Voice transcription',
|
|
203
|
+
'Webhook receiver',
|
|
204
|
+
'Provider API keys',
|
|
205
|
+
'Model presets',
|
|
206
|
+
'Role → preset mapping',
|
|
207
|
+
'Search backend',
|
|
208
|
+
'Explorer maintenance preset',
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
function logDryRunWizard() {
|
|
212
|
+
console.log('[dry-run] would run the setup wizard (9 steps):');
|
|
213
|
+
for (let i = 0; i < WIZARD_STEP_LABELS.length; i += 1) {
|
|
214
|
+
console.log(`[dry-run] ${i + 1}. ${WIZARD_STEP_LABELS[i]}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function runInstallDemo() {
|
|
219
|
+
const tmpRoot = mkdtempSync(join(tmpdir(), 'mixdog-demo-'));
|
|
220
|
+
const configDir = join(tmpRoot, 'config');
|
|
221
|
+
const dataDir = join(tmpRoot, 'data');
|
|
222
|
+
process.env.CLAUDE_CONFIG_DIR = configDir;
|
|
223
|
+
process.env.CLAUDE_PLUGIN_DATA = dataDir;
|
|
224
|
+
mkdirSync(configDir, { recursive: true });
|
|
225
|
+
mkdirSync(dataDir, { recursive: true });
|
|
226
|
+
|
|
227
|
+
process.on('exit', () => {
|
|
228
|
+
try {
|
|
229
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
230
|
+
} catch {}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
console.log('\n🎬 mixdog demo — real wizard UI, nothing saved (isolated temp, auto-cleaned).\n');
|
|
234
|
+
|
|
235
|
+
const claudePath = resolveClaudeExecutable();
|
|
236
|
+
if (claudePath) {
|
|
237
|
+
console.log(`[demo] Claude Code detected at ${claudePath}`);
|
|
238
|
+
} else {
|
|
239
|
+
console.log('[demo] Claude Code not detected');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log('[demo] skipping plugin registration');
|
|
243
|
+
|
|
244
|
+
const { runSetupWizard } = await import('./wizard.mjs');
|
|
245
|
+
await runSetupWizard();
|
|
246
|
+
|
|
247
|
+
console.log('[demo] skipping runtime dependency install');
|
|
248
|
+
|
|
249
|
+
console.log('\n✓ Demo complete — nothing was saved to your real config.');
|
|
250
|
+
try {
|
|
251
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
254
|
+
|
|
110
255
|
export async function runInstall() {
|
|
111
|
-
|
|
256
|
+
const demo =
|
|
257
|
+
process.argv.includes('--demo') || process.env.MIXDOG_SETUP_DEMO === '1';
|
|
258
|
+
if (demo) {
|
|
259
|
+
await runInstallDemo();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const dryRun =
|
|
264
|
+
process.argv.includes('--dry-run') || process.env.MIXDOG_SETUP_DRY_RUN === '1';
|
|
265
|
+
|
|
266
|
+
if (dryRun) {
|
|
267
|
+
console.log('[dry-run] mixdog install preview — no files, prompts, or installs');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await ensureClaudeInstalled(dryRun);
|
|
271
|
+
registerPluginInSettings(dryRun);
|
|
112
272
|
|
|
113
273
|
// npx / node setup/install.mjs runs outside Claude Code — config.mjs needs a data dir.
|
|
114
274
|
const pluginData = process.env.CLAUDE_PLUGIN_DATA;
|
|
115
|
-
|
|
275
|
+
const dataDir =
|
|
276
|
+
pluginData && String(pluginData).trim() ? String(pluginData).trim() : defaultPluginDataDir();
|
|
277
|
+
if (!dryRun && (!pluginData || !String(pluginData).trim())) {
|
|
116
278
|
process.env.CLAUDE_PLUGIN_DATA = defaultPluginDataDir();
|
|
117
279
|
}
|
|
118
280
|
|
|
119
|
-
|
|
120
|
-
|
|
281
|
+
if (dryRun) {
|
|
282
|
+
logDryRunWizard();
|
|
283
|
+
} else {
|
|
284
|
+
const { runSetupWizard } = await import('./wizard.mjs');
|
|
285
|
+
await runSetupWizard();
|
|
286
|
+
}
|
|
121
287
|
|
|
122
|
-
|
|
288
|
+
await prewarmRuntimeDepsBestEffort(dryRun, dataDir);
|
|
289
|
+
|
|
290
|
+
if (!dryRun) maybeStarNudge();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function prewarmRuntimeDepsBestEffort(dryRun = false, dataDirOverride = null) {
|
|
294
|
+
const dataDir = dataDirOverride || process.env.CLAUDE_PLUGIN_DATA || defaultPluginDataDir();
|
|
295
|
+
if (dryRun) {
|
|
296
|
+
console.log(
|
|
297
|
+
`[dry-run] would prewarm runtime deps (bun install into ${join(dataDir, '.deps')})`,
|
|
298
|
+
);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const spinner = createSpinner('Installing runtime dependencies…');
|
|
303
|
+
try {
|
|
304
|
+
const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
305
|
+
mkdirSync(dataDir, { recursive: true });
|
|
306
|
+
|
|
307
|
+
const { resolveBun, installBunViaNpm } = await import('../scripts/resolve-bun.mjs');
|
|
308
|
+
const { ensureRuntimeDeps, resolveNmWithRequiredDeps } = await import('../scripts/ensure-deps.mjs');
|
|
309
|
+
|
|
310
|
+
let bunPath = resolveBun(pluginRoot);
|
|
311
|
+
if (!bunPath) {
|
|
312
|
+
installBunViaNpm(pluginRoot, { fatal: false });
|
|
313
|
+
bunPath = resolveBun(pluginRoot);
|
|
314
|
+
}
|
|
315
|
+
if (!bunPath) {
|
|
316
|
+
spinner.stop('bun unavailable — will install on first launch', false);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const seedNm = resolveNmWithRequiredDeps(pluginRoot) || undefined;
|
|
320
|
+
|
|
321
|
+
const result = ensureRuntimeDeps({
|
|
322
|
+
dataDir,
|
|
323
|
+
pluginRoot,
|
|
324
|
+
bunPath,
|
|
325
|
+
seedNm,
|
|
326
|
+
logPrefix: '[setup]',
|
|
327
|
+
installStdio: 'pipe',
|
|
328
|
+
});
|
|
329
|
+
if (result?.satisfied) {
|
|
330
|
+
spinner.stop('prewarmed (first launch should skip bun install)', true);
|
|
331
|
+
} else {
|
|
332
|
+
spinner.stop(`will install on first launch (${result?.reason || 'prewarm did not complete'})`, false);
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
spinner.stop(
|
|
336
|
+
`prewarm skipped — ${err?.message || 'first MCP launch may run bun install'}`,
|
|
337
|
+
false,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
123
340
|
}
|
|
124
341
|
|
|
125
342
|
// Non-blocking, opt-in star nudge. Only on a real interactive terminal — never
|
package/setup/launch.mjs
CHANGED
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// locate-claude.mjs — shared PATH scan for the Claude Code CLI executable.
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export function resolveClaudeExecutable() {
|
|
8
|
+
const win32 = process.platform === 'win32';
|
|
9
|
+
try {
|
|
10
|
+
const cmd = win32 ? 'where' : 'which';
|
|
11
|
+
const out = execFileSync(cmd, ['claude'], { encoding: 'utf8', windowsHide: true }).trim();
|
|
12
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
|
|
13
|
+
if (first && existsSync(first)) return first;
|
|
14
|
+
} catch { /* PATH scan */ }
|
|
15
|
+
|
|
16
|
+
const pathSep = win32 ? ';' : ':';
|
|
17
|
+
const dirs = String(process.env.PATH || '').split(pathSep).filter(Boolean);
|
|
18
|
+
const pathext = win32
|
|
19
|
+
? String(process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';').map((e) => e.toLowerCase())
|
|
20
|
+
: [''];
|
|
21
|
+
const bases = win32 ? ['claude'] : ['claude'];
|
|
22
|
+
for (const dir of dirs) {
|
|
23
|
+
for (const base of bases) {
|
|
24
|
+
if (win32) {
|
|
25
|
+
for (const ext of pathext) {
|
|
26
|
+
const candidate = join(dir, base + ext);
|
|
27
|
+
if (existsSync(candidate)) return candidate;
|
|
28
|
+
}
|
|
29
|
+
const bare = join(dir, base);
|
|
30
|
+
if (existsSync(bare)) return bare;
|
|
31
|
+
} else {
|
|
32
|
+
const candidate = join(dir, base);
|
|
33
|
+
if (existsSync(candidate)) return candidate;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
package/setup/mixdog-cli.mjs
CHANGED
|
@@ -1,63 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// mixdog-cli.mjs — `mixdog` bin dispatcher: launch Claude Code with the dev
|
|
3
|
-
// plugin load flags
|
|
3
|
+
// plugin load flags for other args; no args or `install` → runInstall().
|
|
4
4
|
|
|
5
|
-
import { spawn
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
8
6
|
import { realpathSync } from 'node:fs';
|
|
9
7
|
import { constants as osConstants } from 'node:os';
|
|
10
8
|
import { fileURLToPath } from 'node:url';
|
|
11
9
|
import { DEFAULT_MARKETPLACE, DEFAULT_PLUGIN } from '../src/shared/plugin-paths.mjs';
|
|
12
10
|
import { runInstall } from './install.mjs';
|
|
11
|
+
import { resolveClaudeExecutable } from './locate-claude.mjs';
|
|
12
|
+
|
|
13
|
+
export { resolveClaudeExecutable } from './locate-claude.mjs';
|
|
13
14
|
|
|
14
15
|
const PLUGIN_LOAD_ARG = `plugin:${DEFAULT_PLUGIN}@${DEFAULT_MARKETPLACE}`;
|
|
15
16
|
const CLAUDE_PREFIX = ['--dangerously-load-development-channels', PLUGIN_LOAD_ARG];
|
|
16
17
|
|
|
17
|
-
function isSetupCommand(first) {
|
|
18
|
-
return first === 'setup' || first === 'install';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
18
|
export function buildClaudeLaunchArgv(passthrough = []) {
|
|
22
19
|
return [...CLAUDE_PREFIX, ...passthrough];
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
export function resolveClaudeExecutable() {
|
|
26
|
-
const win32 = process.platform === 'win32';
|
|
27
|
-
try {
|
|
28
|
-
const cmd = win32 ? 'where' : 'which';
|
|
29
|
-
const out = execFileSync(cmd, ['claude'], { encoding: 'utf8', windowsHide: true }).trim();
|
|
30
|
-
const first = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
|
|
31
|
-
if (first && existsSync(first)) return first;
|
|
32
|
-
} catch { /* PATH scan */ }
|
|
33
|
-
|
|
34
|
-
const pathSep = win32 ? ';' : ':';
|
|
35
|
-
const dirs = String(process.env.PATH || '').split(pathSep).filter(Boolean);
|
|
36
|
-
const pathext = win32
|
|
37
|
-
? String(process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';').map((e) => e.toLowerCase())
|
|
38
|
-
: [''];
|
|
39
|
-
const bases = win32 ? ['claude'] : ['claude'];
|
|
40
|
-
for (const dir of dirs) {
|
|
41
|
-
for (const base of bases) {
|
|
42
|
-
if (win32) {
|
|
43
|
-
for (const ext of pathext) {
|
|
44
|
-
const candidate = join(dir, base + ext);
|
|
45
|
-
if (existsSync(candidate)) return candidate;
|
|
46
|
-
}
|
|
47
|
-
const bare = join(dir, base);
|
|
48
|
-
if (existsSync(bare)) return bare;
|
|
49
|
-
} else {
|
|
50
|
-
const candidate = join(dir, base);
|
|
51
|
-
if (existsSync(candidate)) return candidate;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
22
|
export async function dispatchMixdogCli(argv = process.argv.slice(2)) {
|
|
59
23
|
const [first] = argv;
|
|
60
|
-
if (
|
|
24
|
+
if (argv.length === 0 || first === 'install') {
|
|
61
25
|
if (process.env.MIXDOG_CLI_DRY_RUN === '1') {
|
|
62
26
|
process.stdout.write('mixdog-cli: route=setup\n');
|
|
63
27
|
return 0;
|
package/setup/setup-server.mjs
CHANGED
|
@@ -7,8 +7,8 @@ import { fileURLToPath } from 'url';
|
|
|
7
7
|
import http from 'http';
|
|
8
8
|
import https from 'https';
|
|
9
9
|
import { DEFAULT_MAINTENANCE, MAINTENANCE_SLOTS, DEFAULT_PRESETS, getPluginData } from '../src/agent/orchestrator/config.mjs';
|
|
10
|
-
import { getOpenAIOAuthModelCatalogError, hasOpenAIOAuthCredentials } from '../src/agent/orchestrator/providers/openai-oauth.mjs';
|
|
11
|
-
import { hasAnthropicOAuthCredentials } from '../src/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
10
|
+
import { getOpenAIOAuthModelCatalogError, hasOpenAIOAuthCredentials, loginOAuth as loginOpenAIOAuth } from '../src/agent/orchestrator/providers/openai-oauth.mjs';
|
|
11
|
+
import { hasAnthropicOAuthCredentials, loginOAuth as loginAnthropicOAuth } from '../src/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
12
12
|
import { hasGrokOAuthCredentials, loginOAuth as loginGrokOAuth } from '../src/agent/orchestrator/providers/grok-oauth.mjs';
|
|
13
13
|
import { resolvePluginData } from '../src/shared/plugin-paths.mjs';
|
|
14
14
|
import { listSchedules } from '../src/shared/schedules-store.mjs';
|
|
@@ -2135,6 +2135,54 @@ async function handleRequest(req, res) {
|
|
|
2135
2135
|
return;
|
|
2136
2136
|
}
|
|
2137
2137
|
|
|
2138
|
+
if (req.method === 'POST' && path === '/agent/openai-oauth/login') {
|
|
2139
|
+
if (!isAllowedOrigin(req)) {
|
|
2140
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
2141
|
+
res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
try {
|
|
2145
|
+
const tokens = await loginOpenAIOAuth();
|
|
2146
|
+
if (!tokens?.access_token) {
|
|
2147
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2148
|
+
res.end(JSON.stringify({ ok: false, error: 'login cancelled or timed out' }));
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
dropRuntimeModelCaches();
|
|
2152
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2153
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2154
|
+
} catch (e) {
|
|
2155
|
+
process.stderr.write('[setup] /agent/openai-oauth/login failed: ' + (e?.stack || e?.message || String(e)) + '\n');
|
|
2156
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2157
|
+
res.end(JSON.stringify({ ok: false, error: e?.message || String(e) }));
|
|
2158
|
+
}
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
if (req.method === 'POST' && path === '/agent/anthropic-oauth/login') {
|
|
2163
|
+
if (!isAllowedOrigin(req)) {
|
|
2164
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
2165
|
+
res.end(JSON.stringify({ ok: false, error: 'forbidden: cross-origin' }));
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
try {
|
|
2169
|
+
const tokens = await loginAnthropicOAuth();
|
|
2170
|
+
if (!tokens?.accessToken) {
|
|
2171
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2172
|
+
res.end(JSON.stringify({ ok: false, error: 'login cancelled or timed out' }));
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
dropRuntimeModelCaches();
|
|
2176
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2177
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2178
|
+
} catch (e) {
|
|
2179
|
+
process.stderr.write('[setup] /agent/anthropic-oauth/login failed: ' + (e?.stack || e?.message || String(e)) + '\n');
|
|
2180
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2181
|
+
res.end(JSON.stringify({ ok: false, error: e?.message || String(e) }));
|
|
2182
|
+
}
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2138
2186
|
if (req.method === 'GET' && path === '/agent/presets') {
|
|
2139
2187
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2140
2188
|
res.end(JSON.stringify({ presets: readAgentPresets() }));
|