mixdog 0.7.8 → 0.7.11
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 +40 -0
- package/README.md +13 -10
- package/package.json +1 -1
- package/scripts/openai-oauth-catalog-smoke.mjs +53 -0
- package/setup/config-merge.mjs +0 -1
- package/setup/install.mjs +574 -384
- package/setup/mixdog-cli.mjs +30 -3
- package/setup/setup-server.mjs +11 -31
- package/setup/setup.html +3 -3
- 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 +59 -13
- package/src/agent/orchestrator/session/manager.mjs +18 -4
- package/src/agent/orchestrator/stall-policy.mjs +6 -0
- 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/setup/wizard.mjs +0 -696
package/setup/install.mjs
CHANGED
|
@@ -1,384 +1,574 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// install.mjs — one-shot bootstrapper that registers the mixdog plugin in the
|
|
3
|
-
// user's Claude Code settings so it auto-loads on the next session start.
|
|
4
|
-
//
|
|
5
|
-
// Run via: npx mixdog | mixdog install | mixdog-install (after publish)
|
|
6
|
-
// or: node setup/install.mjs (from a checkout)
|
|
7
|
-
//
|
|
8
|
-
// It merges two keys into the user-scope settings file (preserving everything
|
|
9
|
-
// else that is already there):
|
|
10
|
-
// extraKnownMarketplaces["trib-plugin"] -> the GitHub marketplace source
|
|
11
|
-
// enabledPlugins["mixdog@trib-plugin"] -> true
|
|
12
|
-
//
|
|
13
|
-
// After it runs, the user restarts Claude Code (or runs /reload-plugins) and
|
|
14
|
-
// mixdog is already present — there is no separate /plugin install step.
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
readFileSync,
|
|
18
|
-
writeFileSync,
|
|
19
|
-
existsSync,
|
|
20
|
-
mkdirSync,
|
|
21
|
-
copyFileSync,
|
|
22
|
-
mkdtempSync,
|
|
23
|
-
rmSync,
|
|
24
|
-
} from 'node:fs';
|
|
25
|
-
import { join, dirname } from 'node:path';
|
|
26
|
-
import { homedir, tmpdir } from 'node:os';
|
|
27
|
-
import { realpathSync } from 'node:fs';
|
|
28
|
-
import { fileURLToPath } from 'node:url';
|
|
29
|
-
import { createInterface } from 'node:readline';
|
|
30
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
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';
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
settings
|
|
78
|
-
} catch
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.log(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
await
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// install.mjs — one-shot bootstrapper that registers the mixdog plugin in the
|
|
3
|
+
// user's Claude Code settings so it auto-loads on the next session start.
|
|
4
|
+
//
|
|
5
|
+
// Run via: npx mixdog | mixdog install | mixdog-install (after publish)
|
|
6
|
+
// or: node setup/install.mjs (from a checkout)
|
|
7
|
+
//
|
|
8
|
+
// It merges two keys into the user-scope settings file (preserving everything
|
|
9
|
+
// else that is already there):
|
|
10
|
+
// extraKnownMarketplaces["trib-plugin"] -> the GitHub marketplace source
|
|
11
|
+
// enabledPlugins["mixdog@trib-plugin"] -> true
|
|
12
|
+
//
|
|
13
|
+
// After it runs, the user restarts Claude Code (or runs /reload-plugins) and
|
|
14
|
+
// mixdog is already present — there is no separate /plugin install step.
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
readFileSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
copyFileSync,
|
|
22
|
+
mkdtempSync,
|
|
23
|
+
rmSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { join, dirname } from 'node:path';
|
|
26
|
+
import { homedir, tmpdir } from 'node:os';
|
|
27
|
+
import { realpathSync } from 'node:fs';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { createInterface } from 'node:readline';
|
|
30
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
31
|
+
import { DEFAULT_MARKETPLACE, DEFAULT_PLUGIN } from '../src/shared/plugin-paths.mjs';
|
|
32
|
+
import { resolveClaudeExecutable } from './locate-claude.mjs';
|
|
33
|
+
import { confirm, createSpinner, outro } from './tui.mjs';
|
|
34
|
+
import { openInBrowser } from '../src/shared/open-url.mjs';
|
|
35
|
+
|
|
36
|
+
const MARKETPLACE = DEFAULT_MARKETPLACE;
|
|
37
|
+
const PLUGIN_REF = `${DEFAULT_PLUGIN}@${DEFAULT_MARKETPLACE}`;
|
|
38
|
+
const REPO = 'trib-plugin/mixdog'; // github owner/repo
|
|
39
|
+
const REPO_URL = 'https://github.com/trib-plugin/mixdog';
|
|
40
|
+
|
|
41
|
+
/** Claude config root — matches Claude Code (settings + plugins tree). */
|
|
42
|
+
export function claudeConfigBaseDir() {
|
|
43
|
+
return process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Claude Code honours CLAUDE_CONFIG_DIR; otherwise the user scope is ~/.claude.
|
|
47
|
+
function settingsDir() {
|
|
48
|
+
return claudeConfigBaseDir();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function defaultPluginDataDir() {
|
|
52
|
+
return join(
|
|
53
|
+
claudeConfigBaseDir(),
|
|
54
|
+
'plugins',
|
|
55
|
+
'data',
|
|
56
|
+
`${DEFAULT_PLUGIN}-${MARKETPLACE}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadSettings(file) {
|
|
61
|
+
if (!existsSync(file)) return {};
|
|
62
|
+
const raw = readFileSync(file, 'utf8');
|
|
63
|
+
if (raw.trim() === '') return {};
|
|
64
|
+
// Invariant: never clobber a file we cannot parse — let the caller abort.
|
|
65
|
+
return JSON.parse(raw);
|
|
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
|
+
|
|
83
|
+
function isPlainObject(value) {
|
|
84
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function registerPluginInSettings(dryRun = false) {
|
|
88
|
+
const dir = settingsDir();
|
|
89
|
+
const file = join(dir, 'settings.json');
|
|
90
|
+
|
|
91
|
+
let settings;
|
|
92
|
+
try {
|
|
93
|
+
settings = loadSettings(file);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(`\n✗ ${file} is not valid JSON — refusing to overwrite it.`);
|
|
96
|
+
console.error(` Fix or remove the file, then re-run. (${err.message})`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
if (!isPlainObject(settings)) {
|
|
100
|
+
console.error(`\n✗ ${file} does not contain a JSON object — refusing to overwrite it.`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Merge marketplace + enable flag without touching any other keys.
|
|
105
|
+
if (!isPlainObject(settings.extraKnownMarketplaces)) settings.extraKnownMarketplaces = {};
|
|
106
|
+
settings.extraKnownMarketplaces[MARKETPLACE] = {
|
|
107
|
+
source: { source: 'github', repo: REPO },
|
|
108
|
+
};
|
|
109
|
+
if (!isPlainObject(settings.enabledPlugins)) settings.enabledPlugins = {};
|
|
110
|
+
const already = settings.enabledPlugins[PLUGIN_REF] === true;
|
|
111
|
+
settings.enabledPlugins[PLUGIN_REF] = true;
|
|
112
|
+
|
|
113
|
+
if (dryRun) {
|
|
114
|
+
console.log(`[dry-run] would register mixdog in ${file}`);
|
|
115
|
+
console.log(`[dry-run] marketplace "${MARKETPLACE}" → github:${REPO}`);
|
|
116
|
+
console.log(
|
|
117
|
+
`[dry-run] enabled plugin "${PLUGIN_REF}"${already ? ' (was already enabled)' : ''}`,
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Back up an existing file before the first write; create the dir otherwise.
|
|
123
|
+
if (existsSync(file)) {
|
|
124
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
125
|
+
const bak = `${file}.bak-${stamp}`;
|
|
126
|
+
copyFileSync(file, bak);
|
|
127
|
+
console.log(`• Backed up existing settings → ${bak}`);
|
|
128
|
+
} else {
|
|
129
|
+
mkdirSync(dir, { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
133
|
+
console.log(`✓ Registered mixdog in ${file}`);
|
|
134
|
+
console.log(` - marketplace "${MARKETPLACE}" → github:${REPO}`);
|
|
135
|
+
console.log(` - enabled plugin "${PLUGIN_REF}"${already ? ' (was already enabled)' : ''}`);
|
|
136
|
+
console.log(`\nNext: restart Claude Code (or run /reload-plugins). mixdog loads automatically.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const CLAUDE_SETUP_GUIDANCE =
|
|
140
|
+
'Install Claude Code first: https://code.claude.com/docs/en/setup';
|
|
141
|
+
|
|
142
|
+
function officialClaudeInstallerCommandDescription() {
|
|
143
|
+
if (process.platform === 'win32') {
|
|
144
|
+
return 'powershell -NoProfile -Command "irm https://claude.ai/install.ps1 | iex"';
|
|
145
|
+
}
|
|
146
|
+
return 'bash -c "curl -fsSL https://claude.ai/install.sh | bash"';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function runOfficialClaudeInstaller() {
|
|
150
|
+
if (process.platform === 'win32') {
|
|
151
|
+
spawnSync(
|
|
152
|
+
'powershell.exe',
|
|
153
|
+
['-NoProfile', '-Command', 'irm https://claude.ai/install.ps1 | iex'],
|
|
154
|
+
{ stdio: 'inherit', windowsHide: true },
|
|
155
|
+
);
|
|
156
|
+
} else {
|
|
157
|
+
spawnSync('bash', ['-c', 'curl -fsSL https://claude.ai/install.sh | bash'], {
|
|
158
|
+
stdio: 'inherit',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function askInstallClaude() {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
166
|
+
rl.question(
|
|
167
|
+
'Claude Code CLI not found. Install it now with the official installer? (y/N) ',
|
|
168
|
+
(answer) => {
|
|
169
|
+
rl.close();
|
|
170
|
+
const a = answer.trim().toLowerCase();
|
|
171
|
+
resolve(a === 'y' || a === 'yes');
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function ensureClaudeInstalled(dryRun = false) {
|
|
178
|
+
const claudePath = resolveClaudeExecutable();
|
|
179
|
+
if (claudePath) {
|
|
180
|
+
if (dryRun) console.log(`[dry-run] Claude Code detected at ${claudePath}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (dryRun) {
|
|
185
|
+
const cmd = officialClaudeInstallerCommandDescription();
|
|
186
|
+
console.log(
|
|
187
|
+
`[dry-run] Claude Code NOT found — would prompt to install via the official installer (${cmd})`,
|
|
188
|
+
);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const interactive = process.stdin.isTTY && !process.env.CI;
|
|
193
|
+
|
|
194
|
+
if (!interactive) {
|
|
195
|
+
console.log(CLAUDE_SETUP_GUIDANCE);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const yes = await askInstallClaude();
|
|
200
|
+
if (yes) {
|
|
201
|
+
runOfficialClaudeInstaller();
|
|
202
|
+
if (resolveClaudeExecutable()) {
|
|
203
|
+
console.log('✓ Claude Code detected.');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
console.log(
|
|
207
|
+
"✓ Claude Code installed — open a NEW terminal so 'claude' is on PATH, then start Claude Code (mixdog is being registered now).",
|
|
208
|
+
);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log(CLAUDE_SETUP_GUIDANCE);
|
|
213
|
+
}
|
|
214
|
+
|
|
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
|
+
},
|
|
254
|
+
];
|
|
255
|
+
|
|
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
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function runInstallDemo() {
|
|
299
|
+
process.env.MIXDOG_SETUP_QUIET = '1';
|
|
300
|
+
const tmpRoot = mkdtempSync(join(tmpdir(), 'mixdog-demo-'));
|
|
301
|
+
const configDir = join(tmpRoot, 'config');
|
|
302
|
+
const dataDir = join(tmpRoot, 'data');
|
|
303
|
+
process.env.CLAUDE_CONFIG_DIR = configDir;
|
|
304
|
+
process.env.CLAUDE_PLUGIN_DATA = dataDir;
|
|
305
|
+
mkdirSync(configDir, { recursive: true });
|
|
306
|
+
mkdirSync(dataDir, { recursive: true });
|
|
307
|
+
|
|
308
|
+
process.on('exit', () => {
|
|
309
|
+
try {
|
|
310
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
311
|
+
} catch {}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
console.log('\n🎬 mixdog demo — install preview, nothing saved (isolated temp, auto-cleaned).\n');
|
|
315
|
+
|
|
316
|
+
const claudePath = resolveClaudeExecutable();
|
|
317
|
+
if (claudePath) {
|
|
318
|
+
console.log(`[demo] Claude Code detected at ${claudePath}`);
|
|
319
|
+
} else {
|
|
320
|
+
console.log('[demo] Claude Code not detected');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.log('[demo] skipping plugin registration');
|
|
324
|
+
|
|
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
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log('[demo] skipping runtime dependency install');
|
|
335
|
+
|
|
336
|
+
await runFinale({ demo: true });
|
|
337
|
+
|
|
338
|
+
console.log('\n✓ Demo complete — nothing was saved to your real config.');
|
|
339
|
+
console.log('Run `mixdog install` to set up for real.');
|
|
340
|
+
try {
|
|
341
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function runInstall({ launchAfter = false } = {}) {
|
|
346
|
+
const demo =
|
|
347
|
+
process.argv.includes('--demo') || process.env.MIXDOG_SETUP_DEMO === '1';
|
|
348
|
+
if (demo) {
|
|
349
|
+
await runInstallDemo();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
process.env.MIXDOG_SETUP_QUIET = '1';
|
|
354
|
+
|
|
355
|
+
const dryRun =
|
|
356
|
+
process.argv.includes('--dry-run') || process.env.MIXDOG_SETUP_DRY_RUN === '1';
|
|
357
|
+
|
|
358
|
+
if (dryRun) {
|
|
359
|
+
console.log('[dry-run] mixdog install preview — no files, prompts, or installs');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
await ensureClaudeInstalled(dryRun);
|
|
363
|
+
registerPluginInSettings(dryRun);
|
|
364
|
+
|
|
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.
|
|
370
|
+
const pluginData = process.env.CLAUDE_PLUGIN_DATA;
|
|
371
|
+
const dataDir =
|
|
372
|
+
pluginData && String(pluginData).trim() ? String(pluginData).trim() : defaultPluginDataDir();
|
|
373
|
+
if (!pluginData || !String(pluginData).trim()) {
|
|
374
|
+
process.env.CLAUDE_PLUGIN_DATA = dataDir;
|
|
375
|
+
}
|
|
376
|
+
|
|
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);
|
|
384
|
+
|
|
385
|
+
await prewarmRuntimeDepsBestEffort(dryRun, dataDir);
|
|
386
|
+
|
|
387
|
+
if (!dryRun) await runFinale({ launchAfter });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function prewarmRuntimeDepsBestEffort(dryRun = false, dataDirOverride = null) {
|
|
391
|
+
const dataDir = dataDirOverride || process.env.CLAUDE_PLUGIN_DATA || defaultPluginDataDir();
|
|
392
|
+
if (dryRun) {
|
|
393
|
+
console.log(
|
|
394
|
+
`[dry-run] would prewarm runtime deps (bun install into ${join(dataDir, '.deps')})`,
|
|
395
|
+
);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const spinner = createSpinner('Installing runtime dependencies…');
|
|
400
|
+
try {
|
|
401
|
+
const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
402
|
+
mkdirSync(dataDir, { recursive: true });
|
|
403
|
+
|
|
404
|
+
const { resolveBun, installBunViaNpm } = await import('../scripts/resolve-bun.mjs');
|
|
405
|
+
const { ensureRuntimeDeps, resolveNmWithRequiredDeps } = await import('../scripts/ensure-deps.mjs');
|
|
406
|
+
|
|
407
|
+
let bunPath = resolveBun(pluginRoot);
|
|
408
|
+
if (!bunPath) {
|
|
409
|
+
installBunViaNpm(pluginRoot, { fatal: false });
|
|
410
|
+
bunPath = resolveBun(pluginRoot);
|
|
411
|
+
}
|
|
412
|
+
if (!bunPath) {
|
|
413
|
+
spinner.stop('bun unavailable — will install on first launch', false);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const seedNm = resolveNmWithRequiredDeps(pluginRoot) || undefined;
|
|
417
|
+
|
|
418
|
+
const result = ensureRuntimeDeps({
|
|
419
|
+
dataDir,
|
|
420
|
+
pluginRoot,
|
|
421
|
+
bunPath,
|
|
422
|
+
seedNm,
|
|
423
|
+
logPrefix: '[setup]',
|
|
424
|
+
installStdio: 'pipe',
|
|
425
|
+
});
|
|
426
|
+
if (result?.satisfied) {
|
|
427
|
+
spinner.stop('prewarmed (first launch should skip bun install)', true);
|
|
428
|
+
} else {
|
|
429
|
+
spinner.stop(`will install on first launch (${result?.reason || 'prewarm did not complete'})`, false);
|
|
430
|
+
}
|
|
431
|
+
} catch (err) {
|
|
432
|
+
spinner.stop(
|
|
433
|
+
`prewarm skipped — ${err?.message || 'first MCP launch may run bun install'}`,
|
|
434
|
+
false,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
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();
|
|
472
|
+
}
|
|
473
|
+
|
|
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() {
|
|
534
|
+
try {
|
|
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;
|
|
541
|
+
} catch {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
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
|
+
|
|
553
|
+
function isInstallerEntry() {
|
|
554
|
+
const entry = process.argv[1];
|
|
555
|
+
if (!entry) return false;
|
|
556
|
+
try {
|
|
557
|
+
const self = realpathSync(fileURLToPath(import.meta.url));
|
|
558
|
+
const invoked = realpathSync(entry);
|
|
559
|
+
return self === invoked;
|
|
560
|
+
} catch {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (isInstallerEntry()) {
|
|
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
|
+
});
|
|
574
|
+
}
|