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
|
@@ -70,7 +70,23 @@ async function warmRead(path, scope, offset = 0, limit = 20) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
async function rmTree(path) {
|
|
73
|
-
|
|
73
|
+
// Windows CI occasionally keeps a just-used temp file locked briefly after the
|
|
74
|
+
// owning JS/native subprocess exits (EACCES/EBUSY/EPERM). Cleanup is not part
|
|
75
|
+
// of the smoke invariant, so retry with backoff and make final teardown
|
|
76
|
+
// best-effort instead of turning a passed mutation suite red.
|
|
77
|
+
const delays = [50, 100, 200, 400, 800];
|
|
78
|
+
let lastErr = null;
|
|
79
|
+
for (let i = 0; i <= delays.length; i++) {
|
|
80
|
+
try {
|
|
81
|
+
await rm(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
82
|
+
return;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
lastErr = err;
|
|
85
|
+
if (!['EACCES', 'EBUSY', 'EPERM', 'ENOTEMPTY'].includes(err?.code)) break;
|
|
86
|
+
if (i < delays.length) await new Promise((resolve) => setTimeout(resolve, delays[i]));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
console.warn(`mutation-io smoke cleanup warning: failed to remove ${path}: ${lastErr?.code || ''} ${lastErr?.message || lastErr}`);
|
|
74
90
|
}
|
|
75
91
|
|
|
76
92
|
try {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const tmpData = mkdtempSync(join(tmpdir(), 'mixdog-openai-oauth-catalog-'));
|
|
7
|
+
process.env.CLAUDE_PLUGIN_DATA = tmpData;
|
|
8
|
+
|
|
9
|
+
globalThis.fetch = async (url) => {
|
|
10
|
+
const text = String(url);
|
|
11
|
+
if (text.includes('model_prices_and_context_window')) {
|
|
12
|
+
return {
|
|
13
|
+
ok: true,
|
|
14
|
+
json: async () => ({
|
|
15
|
+
'openai/gpt-5.5': {
|
|
16
|
+
litellm_provider: 'openai',
|
|
17
|
+
max_input_tokens: 1050000,
|
|
18
|
+
max_output_tokens: 128000,
|
|
19
|
+
input_cost_per_token: 1e-6,
|
|
20
|
+
output_cost_per_token: 2e-6,
|
|
21
|
+
supports_prompt_caching: true,
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (text.includes('models.dev')) {
|
|
27
|
+
return { ok: true, json: async () => ({}) };
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`unexpected fetch: ${text}`);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const { enrichModels } = await import('../src/agent/orchestrator/providers/model-catalog.mjs');
|
|
34
|
+
const [model] = await enrichModels([{
|
|
35
|
+
id: 'gpt-5.5',
|
|
36
|
+
provider: 'openai-oauth',
|
|
37
|
+
contextWindow: 272000,
|
|
38
|
+
outputTokens: 32768,
|
|
39
|
+
serviceTiers: [{ id: 'priority', name: 'Fast', description: '1.5x speed' }],
|
|
40
|
+
additionalSpeedTiers: ['fast'],
|
|
41
|
+
}]);
|
|
42
|
+
|
|
43
|
+
assert.equal(model.contextWindow, 272000);
|
|
44
|
+
assert.equal(model.outputTokens, 32768);
|
|
45
|
+
assert.deepEqual(model.serviceTiers, [{ id: 'priority', name: 'Fast', description: '1.5x speed' }]);
|
|
46
|
+
assert.deepEqual(model.additionalSpeedTiers, ['fast']);
|
|
47
|
+
assert.equal(model.inputCostPerM, 1);
|
|
48
|
+
assert.equal(model.outputCostPerM, 2);
|
|
49
|
+
|
|
50
|
+
console.log('openai-oauth catalog smoke: ok');
|
|
51
|
+
} finally {
|
|
52
|
+
rmSync(tmpData, { recursive: true, force: true });
|
|
53
|
+
}
|
|
@@ -10,6 +10,23 @@ import path from 'path';
|
|
|
10
10
|
import { createRequire } from 'module';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
|
|
13
|
+
// The evaluator intentionally merges user-global Claude settings. Local dev
|
|
14
|
+
// machines commonly have defaultMode=bypassPermissions, which made this smoke
|
|
15
|
+
// pass locally while CI's empty user config exercised stricter defaults. Pin
|
|
16
|
+
// the user tier to a throwaway Claude config dir so the smoke is hermetic and
|
|
17
|
+
// still verifies the intended invariant: hard-deny and explicit list rules win
|
|
18
|
+
// before bypass mode.
|
|
19
|
+
const smokeHome = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'perm-smoke-home-')));
|
|
20
|
+
const smokeConfigDir = path.join(smokeHome, '.claude');
|
|
21
|
+
fs.mkdirSync(smokeConfigDir, { recursive: true });
|
|
22
|
+
fs.writeFileSync(
|
|
23
|
+
path.join(smokeConfigDir, 'settings.json'),
|
|
24
|
+
JSON.stringify({ permissions: { defaultMode: 'bypassPermissions' } }),
|
|
25
|
+
);
|
|
26
|
+
process.env.HOME = smokeHome;
|
|
27
|
+
process.env.USERPROFILE = smokeHome;
|
|
28
|
+
process.env.CLAUDE_CONFIG_DIR = smokeConfigDir;
|
|
29
|
+
|
|
13
30
|
const _require = createRequire(import.meta.url);
|
|
14
31
|
const evalPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../hooks/lib/permission-evaluator.cjs');
|
|
15
32
|
const { evaluatePermission } = _require(evalPath);
|
|
@@ -21,7 +38,7 @@ const { loadPermissions, clearSettingsCache } = _require(loaderPath);
|
|
|
21
38
|
let pass = 0, fail = 0;
|
|
22
39
|
|
|
23
40
|
function makeProject(settings = {}) {
|
|
24
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'perm-smoke-'));
|
|
41
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'perm-smoke-')));
|
|
25
42
|
fs.mkdirSync(path.join(dir, '.claude'), { recursive: true });
|
|
26
43
|
if (Object.keys(settings).length) {
|
|
27
44
|
fs.writeFileSync(
|
|
@@ -27,7 +27,7 @@ const tmp = mkdtempSync(join(tmpdir(), 'mixdog-sl-launcher-'));
|
|
|
27
27
|
function run(homeDir) {
|
|
28
28
|
return spawnSync(process.execPath, [LAUNCHER], {
|
|
29
29
|
input: SAMPLE,
|
|
30
|
-
env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir },
|
|
30
|
+
env: { ...process.env, HOME: homeDir, USERPROFILE: homeDir, CLAUDE_CONFIG_DIR: join(homeDir, '.claude') },
|
|
31
31
|
encoding: 'utf8',
|
|
32
32
|
});
|
|
33
33
|
}
|
|
@@ -60,7 +60,7 @@ try {
|
|
|
60
60
|
+ "process.stdout.write(await renderStatusLine(i));\n"
|
|
61
61
|
);
|
|
62
62
|
const directRun = spawnSync(process.execPath, [driver],
|
|
63
|
-
{ input: SAMPLE, env: { ...process.env, HOME: tmp, USERPROFILE: tmp }, encoding: 'utf8' });
|
|
63
|
+
{ input: SAMPLE, env: { ...process.env, HOME: tmp, USERPROFILE: tmp, CLAUDE_CONFIG_DIR: join(tmp, '.claude') }, encoding: 'utf8' });
|
|
64
64
|
const direct = directRun.stdout || '';
|
|
65
65
|
|
|
66
66
|
const ok = run(tmp);
|
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
process.env.CLAUDE_PLUGIN_DATA = join(tmpdir(), `mixdog-webhook-selfheal-smoke-${process.pid}`);
|
|
10
|
-
}
|
|
8
|
+
process.env.CLAUDE_PLUGIN_DATA = join(tmpdir(), `mixdog-webhook-selfheal-smoke-${process.pid}`);
|
|
11
9
|
|
|
12
10
|
const { decidePortReclaimAction } = await import('../src/channels/lib/webhook.mjs');
|
|
13
11
|
|
package/server-main.mjs
CHANGED
|
@@ -1040,10 +1040,62 @@ async function _getBridgeLlmFactory() {
|
|
|
1040
1040
|
// completion after the worker stopped waiting for the result.
|
|
1041
1041
|
const AGENT_IPC_MAX_CONCURRENT = 2
|
|
1042
1042
|
const _agentIpcInflight = new Map()
|
|
1043
|
-
/** @type {Array<{ msg: object, worker: string, proc: import('child_process').ChildProcess }>} */
|
|
1043
|
+
/** @type {Array<{ msg: object, worker: string, proc: import('child_process').ChildProcess, lane?: string }>} */
|
|
1044
1044
|
const _agentIpcQueue = []
|
|
1045
1045
|
let _agentIpcRunning = 0
|
|
1046
1046
|
|
|
1047
|
+
const AGENT_IPC_LANE_USER = 'user'
|
|
1048
|
+
const AGENT_IPC_LANE_MAINTENANCE = 'maintenance'
|
|
1049
|
+
/** @type {Set<string>} */
|
|
1050
|
+
const AGENT_IPC_MAINTENANCE_WORKERS = new Set(['memory'])
|
|
1051
|
+
|
|
1052
|
+
function _normalizeAgentIpcLaneToken(v) {
|
|
1053
|
+
return typeof v === 'string' ? v.trim().toLowerCase() : ''
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function _isMaintenanceBridgeRole(role) {
|
|
1057
|
+
const r = _normalizeAgentIpcLaneToken(role)
|
|
1058
|
+
if (!r) return false
|
|
1059
|
+
if (r === 'maintenance' || r === 'scheduler-task' || r === 'webhook-handler' || r === 'memory-classification') {
|
|
1060
|
+
return true
|
|
1061
|
+
}
|
|
1062
|
+
if (r.startsWith('cycle')) return true
|
|
1063
|
+
return false
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/** @returns {'user'|'maintenance'} */
|
|
1067
|
+
function _classifyAgentIpcJobLane(msg, worker) {
|
|
1068
|
+
const lane = _normalizeAgentIpcLaneToken(msg?.lane)
|
|
1069
|
+
if (lane === AGENT_IPC_LANE_MAINTENANCE || lane === 'maint') return AGENT_IPC_LANE_MAINTENANCE
|
|
1070
|
+
if (lane === AGENT_IPC_LANE_USER) return AGENT_IPC_LANE_USER
|
|
1071
|
+
|
|
1072
|
+
const priority = _normalizeAgentIpcLaneToken(msg?.priority)
|
|
1073
|
+
if (priority === AGENT_IPC_LANE_MAINTENANCE || priority === 'maint') return AGENT_IPC_LANE_MAINTENANCE
|
|
1074
|
+
if (priority === AGENT_IPC_LANE_USER) return AGENT_IPC_LANE_USER
|
|
1075
|
+
|
|
1076
|
+
if (msg?.maintenance === true || msg?.isMaintenance === true) return AGENT_IPC_LANE_MAINTENANCE
|
|
1077
|
+
|
|
1078
|
+
const params = msg?.params && typeof msg.params === 'object' ? msg.params : {}
|
|
1079
|
+
if (_isMaintenanceBridgeRole(params.role)) return AGENT_IPC_LANE_MAINTENANCE
|
|
1080
|
+
|
|
1081
|
+
const w = _normalizeAgentIpcLaneToken(worker)
|
|
1082
|
+
if (AGENT_IPC_MAINTENANCE_WORKERS.has(w)) return AGENT_IPC_LANE_MAINTENANCE
|
|
1083
|
+
|
|
1084
|
+
return AGENT_IPC_LANE_USER
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function _dequeueHighestPriorityAgentIpcJob() {
|
|
1088
|
+
if (_agentIpcQueue.length === 0) return null
|
|
1089
|
+
for (let i = 0; i < _agentIpcQueue.length; i++) {
|
|
1090
|
+
const job = _agentIpcQueue[i]
|
|
1091
|
+
const lane = job.lane || _classifyAgentIpcJobLane(job.msg, job.worker)
|
|
1092
|
+
if (lane !== AGENT_IPC_LANE_MAINTENANCE) {
|
|
1093
|
+
return _agentIpcQueue.splice(i, 1)[0]
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return _agentIpcQueue.shift()
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1047
1099
|
function _sendAgentIpcResponse(proc, callId, body) {
|
|
1048
1100
|
try { proc.send({ type: 'agent_ipc_response', callId, ...body }) } catch {}
|
|
1049
1101
|
}
|
|
@@ -1066,14 +1118,16 @@ function _startAgentIpcJob(job) {
|
|
|
1066
1118
|
|
|
1067
1119
|
function _drainAgentIpcQueue() {
|
|
1068
1120
|
while (_agentIpcRunning < AGENT_IPC_MAX_CONCURRENT && _agentIpcQueue.length > 0) {
|
|
1069
|
-
const job =
|
|
1121
|
+
const job = _dequeueHighestPriorityAgentIpcJob()
|
|
1122
|
+
if (!job) break
|
|
1070
1123
|
_agentIpcRunning++
|
|
1071
1124
|
_startAgentIpcJob(job)
|
|
1072
1125
|
}
|
|
1073
1126
|
}
|
|
1074
1127
|
|
|
1075
1128
|
function _enqueueAgentIpcRequest(msg, worker, proc) {
|
|
1076
|
-
|
|
1129
|
+
const lane = _classifyAgentIpcJobLane(msg, worker)
|
|
1130
|
+
_agentIpcQueue.push({ msg, worker, proc, lane })
|
|
1077
1131
|
_drainAgentIpcQueue()
|
|
1078
1132
|
}
|
|
1079
1133
|
|
package/setup/config-merge.mjs
CHANGED
package/setup/install.mjs
CHANGED
|
@@ -30,7 +30,8 @@ import { createInterface } from 'node:readline';
|
|
|
30
30
|
import { spawn, spawnSync } from 'node:child_process';
|
|
31
31
|
import { DEFAULT_MARKETPLACE, DEFAULT_PLUGIN } from '../src/shared/plugin-paths.mjs';
|
|
32
32
|
import { resolveClaudeExecutable } from './locate-claude.mjs';
|
|
33
|
-
import { createSpinner } from './tui.mjs';
|
|
33
|
+
import { confirm, createSpinner, outro } from './tui.mjs';
|
|
34
|
+
import { openInBrowser } from '../src/shared/open-url.mjs';
|
|
34
35
|
|
|
35
36
|
const MARKETPLACE = DEFAULT_MARKETPLACE;
|
|
36
37
|
const PLUGIN_REF = `${DEFAULT_PLUGIN}@${DEFAULT_MARKETPLACE}`;
|
|
@@ -64,6 +65,21 @@ function loadSettings(file) {
|
|
|
64
65
|
return JSON.parse(raw);
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/** True when user settings already enable the published plugin ref (deterministic read). */
|
|
69
|
+
export function isPluginRegistered() {
|
|
70
|
+
const file = join(claudeConfigBaseDir(), 'settings.json');
|
|
71
|
+
if (!existsSync(file)) return false;
|
|
72
|
+
try {
|
|
73
|
+
const raw = readFileSync(file, 'utf8');
|
|
74
|
+
if (raw.trim() === '') return false;
|
|
75
|
+
const settings = JSON.parse(raw);
|
|
76
|
+
if (!isPlainObject(settings)) return false;
|
|
77
|
+
return settings?.enabledPlugins?.[PLUGIN_REF] === true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
function isPlainObject(value) {
|
|
68
84
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
69
85
|
}
|
|
@@ -196,26 +212,91 @@ async function ensureClaudeInstalled(dryRun = false) {
|
|
|
196
212
|
console.log(CLAUDE_SETUP_GUIDANCE);
|
|
197
213
|
}
|
|
198
214
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
215
|
+
// OAuth logins for the agent runtime. Claude (anthropic-oauth) powers every
|
|
216
|
+
// default preset so it is recommended; Codex (openai-oauth) is an optional
|
|
217
|
+
// extra. Both complete a browser-PKCE login via a localhost callback, so an
|
|
218
|
+
// inline login works (`offerLogin: true`).
|
|
219
|
+
//
|
|
220
|
+
// Grok Build (grok-oauth) is DETECT-ONLY (`offerLogin: false`): xAI's consent
|
|
221
|
+
// page shows a code to copy instead of redirecting to our localhost callback,
|
|
222
|
+
// so an inline login here would dead-end (the user gets a key with nowhere to
|
|
223
|
+
// put it). Its real login is the official `grok` CLI, which handles that paste
|
|
224
|
+
// and writes ~/.grok/auth.json — mixdog reads that file (and its own store),
|
|
225
|
+
// so we only DETECT Grok and point at the CLI when it is missing.
|
|
226
|
+
//
|
|
227
|
+
// Each provider module exposes the same shape — a `has…Credentials` detector
|
|
228
|
+
// and (for offerLogin providers) a browser-PKCE `loginOAuth` resolving a truthy
|
|
229
|
+
// token object on success or null on failure. Already authenticated →
|
|
230
|
+
// auto-skip. All branches are invariant-gated (creds present? · offerLogin? ·
|
|
231
|
+
// TTY? · login result?) — no heuristic fallback.
|
|
232
|
+
const OAUTH_PROVIDERS = [
|
|
233
|
+
{
|
|
234
|
+
label: 'Claude',
|
|
235
|
+
module: '../src/agent/orchestrator/providers/anthropic-oauth.mjs',
|
|
236
|
+
has: 'hasAnthropicOAuthCredentials',
|
|
237
|
+
offerLogin: true,
|
|
238
|
+
recommended: true,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
label: 'Codex (ChatGPT)',
|
|
242
|
+
module: '../src/agent/orchestrator/providers/openai-oauth.mjs',
|
|
243
|
+
has: 'hasOpenAIOAuthCredentials',
|
|
244
|
+
offerLogin: true,
|
|
245
|
+
recommended: false,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
label: 'Grok Build',
|
|
249
|
+
module: '../src/agent/orchestrator/providers/grok-oauth.mjs',
|
|
250
|
+
has: 'hasGrokOAuthCredentials',
|
|
251
|
+
offerLogin: false,
|
|
252
|
+
hint: 'log in with the `grok` CLI — mixdog reads ~/.grok/auth.json automatically',
|
|
253
|
+
},
|
|
209
254
|
];
|
|
210
255
|
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
for (
|
|
214
|
-
|
|
256
|
+
async function ensureOAuthLogins(dryRun = false) {
|
|
257
|
+
const interactive = process.stdin.isTTY && !process.env.CI;
|
|
258
|
+
for (const p of OAUTH_PROVIDERS) {
|
|
259
|
+
const mod = await import(p.module);
|
|
260
|
+
if (mod[p.has]()) {
|
|
261
|
+
console.log(dryRun ? `[dry-run] ${p.label} login detected` : `✓ ${p.label} login detected.`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
// Detect-only providers (Grok): never open an inline login — point at the
|
|
265
|
+
// provider's own CLI, which mixdog auto-detects via its credential file.
|
|
266
|
+
if (!p.offerLogin) {
|
|
267
|
+
console.log(
|
|
268
|
+
dryRun
|
|
269
|
+
? `[dry-run] no ${p.label} login — would point to: ${p.hint}`
|
|
270
|
+
: `${p.label}: not logged in — ${p.hint}.`,
|
|
271
|
+
);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (dryRun) {
|
|
275
|
+
console.log(`[dry-run] no ${p.label} login — would offer to log in via the browser`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (!interactive) {
|
|
279
|
+
console.log(`${p.label}: not logged in — log in anytime via /setup.`);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const yes = await confirm(`Log in to ${p.label} now? (opens your browser)`, {
|
|
283
|
+
initial: p.recommended,
|
|
284
|
+
});
|
|
285
|
+
if (!yes) {
|
|
286
|
+
console.log(`Skipped ${p.label} — log in anytime via /setup.`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const result = await mod.loginOAuth();
|
|
290
|
+
console.log(
|
|
291
|
+
result
|
|
292
|
+
? `✓ ${p.label} login complete.`
|
|
293
|
+
: `${p.label} login did not complete — retry anytime via /setup.`,
|
|
294
|
+
);
|
|
215
295
|
}
|
|
216
296
|
}
|
|
217
297
|
|
|
218
298
|
async function runInstallDemo() {
|
|
299
|
+
process.env.MIXDOG_SETUP_QUIET = '1';
|
|
219
300
|
const tmpRoot = mkdtempSync(join(tmpdir(), 'mixdog-demo-'));
|
|
220
301
|
const configDir = join(tmpRoot, 'config');
|
|
221
302
|
const dataDir = join(tmpRoot, 'data');
|
|
@@ -230,7 +311,7 @@ async function runInstallDemo() {
|
|
|
230
311
|
} catch {}
|
|
231
312
|
});
|
|
232
313
|
|
|
233
|
-
console.log('\n🎬 mixdog demo —
|
|
314
|
+
console.log('\n🎬 mixdog demo — install preview, nothing saved (isolated temp, auto-cleaned).\n');
|
|
234
315
|
|
|
235
316
|
const claudePath = resolveClaudeExecutable();
|
|
236
317
|
if (claudePath) {
|
|
@@ -241,18 +322,27 @@ async function runInstallDemo() {
|
|
|
241
322
|
|
|
242
323
|
console.log('[demo] skipping plugin registration');
|
|
243
324
|
|
|
244
|
-
const
|
|
245
|
-
|
|
325
|
+
for (const p of OAUTH_PROVIDERS) {
|
|
326
|
+
const mod = await import(p.module);
|
|
327
|
+
console.log(
|
|
328
|
+
mod[p.has]()
|
|
329
|
+
? `[demo] ${p.label} login detected`
|
|
330
|
+
: `[demo] no ${p.label} login — ${p.offerLogin ? 'would offer a browser login' : `would point to: ${p.hint}`}`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
246
333
|
|
|
247
334
|
console.log('[demo] skipping runtime dependency install');
|
|
248
335
|
|
|
336
|
+
await runFinale({ demo: true });
|
|
337
|
+
|
|
249
338
|
console.log('\n✓ Demo complete — nothing was saved to your real config.');
|
|
339
|
+
console.log('Run `mixdog install` to set up for real.');
|
|
250
340
|
try {
|
|
251
341
|
rmSync(tmpRoot, { recursive: true, force: true });
|
|
252
342
|
} catch {}
|
|
253
343
|
}
|
|
254
344
|
|
|
255
|
-
export async function runInstall() {
|
|
345
|
+
export async function runInstall({ launchAfter = false } = {}) {
|
|
256
346
|
const demo =
|
|
257
347
|
process.argv.includes('--demo') || process.env.MIXDOG_SETUP_DEMO === '1';
|
|
258
348
|
if (demo) {
|
|
@@ -260,6 +350,8 @@ export async function runInstall() {
|
|
|
260
350
|
return;
|
|
261
351
|
}
|
|
262
352
|
|
|
353
|
+
process.env.MIXDOG_SETUP_QUIET = '1';
|
|
354
|
+
|
|
263
355
|
const dryRun =
|
|
264
356
|
process.argv.includes('--dry-run') || process.env.MIXDOG_SETUP_DRY_RUN === '1';
|
|
265
357
|
|
|
@@ -270,24 +362,29 @@ export async function runInstall() {
|
|
|
270
362
|
await ensureClaudeInstalled(dryRun);
|
|
271
363
|
registerPluginInSettings(dryRun);
|
|
272
364
|
|
|
273
|
-
// npx / node setup/install.mjs runs outside Claude Code
|
|
365
|
+
// npx / node setup/install.mjs runs outside Claude Code. The provider modules
|
|
366
|
+
// imported by ensureOAuthLogins (below) pull in config.mjs, which resolves
|
|
367
|
+
// plugin-data AT IMPORT TIME and throws if neither CLAUDE_PLUGIN_DATA nor
|
|
368
|
+
// CLAUDE_PLUGIN_ROOT is set. Export it before any provider import — in
|
|
369
|
+
// --dry-run too: this only sets a process env var, no files are written.
|
|
274
370
|
const pluginData = process.env.CLAUDE_PLUGIN_DATA;
|
|
275
371
|
const dataDir =
|
|
276
372
|
pluginData && String(pluginData).trim() ? String(pluginData).trim() : defaultPluginDataDir();
|
|
277
|
-
if (!
|
|
278
|
-
process.env.CLAUDE_PLUGIN_DATA =
|
|
373
|
+
if (!pluginData || !String(pluginData).trim()) {
|
|
374
|
+
process.env.CLAUDE_PLUGIN_DATA = dataDir;
|
|
279
375
|
}
|
|
280
376
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
377
|
+
// No interactive wizard. The full default config (presets, role→preset
|
|
378
|
+
// mapping, search backend, maintenance) is seeded on first launch by the
|
|
379
|
+
// agent boot + setup server; per-feature config (Discord, voice, webhooks,
|
|
380
|
+
// address, provider keys) lives in the /setup UI. Install only secures the
|
|
381
|
+
// OAuth logins (Claude / Codex / Grok Build) so agents work the moment
|
|
382
|
+
// mixdog launches.
|
|
383
|
+
await ensureOAuthLogins(dryRun);
|
|
287
384
|
|
|
288
385
|
await prewarmRuntimeDepsBestEffort(dryRun, dataDir);
|
|
289
386
|
|
|
290
|
-
if (!dryRun)
|
|
387
|
+
if (!dryRun) await runFinale({ launchAfter });
|
|
291
388
|
}
|
|
292
389
|
|
|
293
390
|
async function prewarmRuntimeDepsBestEffort(dryRun = false, dataDirOverride = null) {
|
|
@@ -339,31 +436,120 @@ async function prewarmRuntimeDepsBestEffort(dryRun = false, dataDirOverride = nu
|
|
|
339
436
|
}
|
|
340
437
|
}
|
|
341
438
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
439
|
+
async function runFinale({ demo = false, launchAfter = false } = {}) {
|
|
440
|
+
console.log('\n── All set ──');
|
|
441
|
+
console.log('🎉 mixdog is configured.');
|
|
442
|
+
|
|
443
|
+
const interactive = process.stdin.isTTY && !process.env.CI;
|
|
444
|
+
if (interactive) {
|
|
445
|
+
const star = await confirm('Star mixdog on GitHub?', { initial: true });
|
|
446
|
+
if (star) {
|
|
447
|
+
if (demo) {
|
|
448
|
+
console.log(`[demo] would star ${REPO} via GH_TOKEN/gh (or open ${REPO_URL} as a fallback)`);
|
|
449
|
+
} else if (await starRepo()) {
|
|
450
|
+
console.log(`⭐ Starred ${REPO} — thank you! 🙏`);
|
|
451
|
+
} else {
|
|
452
|
+
// No usable local GitHub credential (env token / gh) — hand off to the
|
|
453
|
+
// browser, where the user is already signed in to github.com.
|
|
454
|
+
openRepo();
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
console.log(`No problem — you can star it anytime at ${REPO_URL}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (demo) {
|
|
462
|
+
outro('Demo finished — no changes were saved.');
|
|
463
|
+
releaseInstallerStdin();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
outro(
|
|
467
|
+
launchAfter
|
|
468
|
+
? 'Setup complete — launching Claude Code with mixdog…'
|
|
469
|
+
: 'Setup complete — run `mixdog` to launch Claude Code with mixdog (or restart it if already open).',
|
|
470
|
+
);
|
|
471
|
+
releaseInstallerStdin();
|
|
353
472
|
}
|
|
354
473
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
474
|
+
/** Belt-and-suspenders after TUI prompts so direct `node setup/install.mjs` can exit. */
|
|
475
|
+
function releaseInstallerStdin() {
|
|
476
|
+
const stdin = process.stdin;
|
|
477
|
+
try {
|
|
478
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
479
|
+
} catch {
|
|
480
|
+
/* ignore */
|
|
481
|
+
}
|
|
482
|
+
stdin.removeAllListeners('keypress');
|
|
483
|
+
stdin.removeAllListeners('data');
|
|
484
|
+
stdin.pause();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Star the repo without a browser when a local GitHub credential is available,
|
|
489
|
+
* following the standard CLI pattern (gh primary, GITHUB_TOKEN secondary —
|
|
490
|
+
* confirmed against GitHub docs / gh manual). Order: an explicitly-set env
|
|
491
|
+
* token first (deliberate, e.g. CI), then the authenticated `gh` CLI. Returns
|
|
492
|
+
* true once one succeeds; false when none is usable, so the caller opens the
|
|
493
|
+
* browser. Starring needs the user's GitHub identity — there is no
|
|
494
|
+
* unauthenticated path.
|
|
495
|
+
*/
|
|
496
|
+
async function starRepo() {
|
|
497
|
+
const envToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
498
|
+
if (envToken && (await starViaToken(envToken))) return true;
|
|
499
|
+
return starRepoViaGh();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Star via the REST API with a bearer token (PUT /user/starred/:owner/:repo →
|
|
504
|
+
* HTTP 204). `public_repo` scope suffices for a public repo, `repo` for a
|
|
505
|
+
* private one. Returns true on 204, false on any non-204 / network error.
|
|
506
|
+
*/
|
|
507
|
+
async function starViaToken(token) {
|
|
508
|
+
try {
|
|
509
|
+
const res = await fetch(`https://api.github.com/user/starred/${REPO}`, {
|
|
510
|
+
method: 'PUT',
|
|
511
|
+
headers: {
|
|
512
|
+
Authorization: `Bearer ${token}`,
|
|
513
|
+
Accept: 'application/vnd.github+json',
|
|
514
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
515
|
+
'Content-Length': '0',
|
|
516
|
+
'User-Agent': 'mixdog-installer',
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
return res.status === 204;
|
|
520
|
+
} catch {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Star the repo directly via the authenticated `gh` CLI
|
|
527
|
+
* (PUT /user/starred/:owner/:repo → HTTP 204). Returns true on success.
|
|
528
|
+
* Returns false when gh is absent / not logged in / the call fails, so the
|
|
529
|
+
* caller falls back to opening the browser — starring requires the user's
|
|
530
|
+
* GitHub identity, which only gh provides here. Path has no leading slash so
|
|
531
|
+
* a shell never rewrites it; shell:true lets Windows resolve gh.exe on PATH.
|
|
532
|
+
*/
|
|
533
|
+
function starRepoViaGh() {
|
|
359
534
|
try {
|
|
360
|
-
|
|
361
|
-
|
|
535
|
+
const r = spawnSync(
|
|
536
|
+
'gh',
|
|
537
|
+
['api', '--method', 'PUT', `user/starred/${REPO}`, '--silent'],
|
|
538
|
+
{ stdio: 'ignore', timeout: 15000, shell: true },
|
|
539
|
+
);
|
|
540
|
+
return r.status === 0;
|
|
362
541
|
} catch {
|
|
363
|
-
|
|
542
|
+
return false;
|
|
364
543
|
}
|
|
365
544
|
}
|
|
366
545
|
|
|
546
|
+
function openRepo() {
|
|
547
|
+
// Shared opener: rundll32 on Windows (the old `cmd start "" url` worked only
|
|
548
|
+
// because REPO_URL has no query string; the helper is correct regardless).
|
|
549
|
+
openInBrowser(REPO_URL);
|
|
550
|
+
console.log(`Opening the repo in your browser — thank you! 🙏 (if it didn't open: ${REPO_URL})`);
|
|
551
|
+
}
|
|
552
|
+
|
|
367
553
|
function isInstallerEntry() {
|
|
368
554
|
const entry = process.argv[1];
|
|
369
555
|
if (!entry) return false;
|
|
@@ -377,8 +563,12 @@ function isInstallerEntry() {
|
|
|
377
563
|
}
|
|
378
564
|
|
|
379
565
|
if (isInstallerEntry()) {
|
|
380
|
-
runInstall()
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
566
|
+
runInstall()
|
|
567
|
+
.then(() => {
|
|
568
|
+
process.stdout.write('', () => process.exit(0));
|
|
569
|
+
})
|
|
570
|
+
.catch((err) => {
|
|
571
|
+
console.error(err?.stack || err?.message || String(err));
|
|
572
|
+
process.exit(1);
|
|
573
|
+
});
|
|
384
574
|
}
|