mixdog 0.7.7 → 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 +51 -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 -338
- 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/disable-claude-builtins.mjs +7 -4
- 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 +8 -2
- package/setup/wizard.mjs +0 -696
package/setup/mixdog-cli.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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; no args → install if plugin not registered else launch;
|
|
4
|
+
// `install` → always runInstall(); other args → launch passthrough.
|
|
4
5
|
|
|
5
6
|
import { spawn } from 'node:child_process';
|
|
6
7
|
import { realpathSync } from 'node:fs';
|
|
7
8
|
import { constants as osConstants } from 'node:os';
|
|
8
9
|
import { fileURLToPath } from 'node:url';
|
|
9
10
|
import { DEFAULT_MARKETPLACE, DEFAULT_PLUGIN } from '../src/shared/plugin-paths.mjs';
|
|
10
|
-
import { runInstall } from './install.mjs';
|
|
11
|
+
import { isPluginRegistered, runInstall } from './install.mjs';
|
|
11
12
|
import { resolveClaudeExecutable } from './locate-claude.mjs';
|
|
12
13
|
|
|
13
14
|
export { resolveClaudeExecutable } from './locate-claude.mjs';
|
|
@@ -21,7 +22,8 @@ export function buildClaudeLaunchArgv(passthrough = []) {
|
|
|
21
22
|
|
|
22
23
|
export async function dispatchMixdogCli(argv = process.argv.slice(2)) {
|
|
23
24
|
const [first] = argv;
|
|
24
|
-
|
|
25
|
+
|
|
26
|
+
if (first === 'install') {
|
|
25
27
|
if (process.env.MIXDOG_CLI_DRY_RUN === '1') {
|
|
26
28
|
process.stdout.write('mixdog-cli: route=setup\n');
|
|
27
29
|
return 0;
|
|
@@ -30,6 +32,31 @@ export async function dispatchMixdogCli(argv = process.argv.slice(2)) {
|
|
|
30
32
|
return 0;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
if (argv.length === 0) {
|
|
36
|
+
const registered = isPluginRegistered();
|
|
37
|
+
if (process.env.MIXDOG_CLI_DRY_RUN === '1') {
|
|
38
|
+
process.stdout.write(
|
|
39
|
+
registered
|
|
40
|
+
? `mixdog-cli: claude ${JSON.stringify(buildClaudeLaunchArgv([]))}\n`
|
|
41
|
+
: 'mixdog-cli: route=setup+launch\n',
|
|
42
|
+
);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
// First run (plugin not yet registered): walk through setup, then launch in
|
|
46
|
+
// the SAME invocation so `mixdog` is the one command that onboards AND opens
|
|
47
|
+
// Claude Code — no dead-end, no required second run.
|
|
48
|
+
if (!registered) await runInstall({ launchAfter: true });
|
|
49
|
+
const claudeArgs = buildClaudeLaunchArgv([]);
|
|
50
|
+
const claudePath = resolveClaudeExecutable();
|
|
51
|
+
if (!claudePath) {
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
'\n✗ `claude` was not found on PATH. Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code\n',
|
|
54
|
+
);
|
|
55
|
+
return 127;
|
|
56
|
+
}
|
|
57
|
+
return launchClaude(claudePath, claudeArgs);
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
const claudeArgs = buildClaudeLaunchArgv(argv);
|
|
34
61
|
if (process.env.MIXDOG_CLI_DRY_RUN === '1') {
|
|
35
62
|
process.stdout.write(`mixdog-cli: claude ${JSON.stringify(claudeArgs)}\n`);
|
package/setup/setup-server.mjs
CHANGED
|
@@ -91,26 +91,13 @@ function dropRuntimeModelCaches() {
|
|
|
91
91
|
}
|
|
92
92
|
dropRuntimeModelCaches();
|
|
93
93
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
markUserDataInitialized(DATA_DIR);
|
|
102
|
-
}
|
|
103
|
-
if (!existsSync(USER_WORKFLOW_MD_PATH) && shouldSeedMissingUserData(DATA_DIR, 'user-workflow.md')) {
|
|
104
|
-
mkdirSync(DATA_DIR, { recursive: true });
|
|
105
|
-
writeFileSync(USER_WORKFLOW_MD_PATH, DEFAULT_USER_WORKFLOW_MD);
|
|
106
|
-
markUserDataInitialized(DATA_DIR);
|
|
107
|
-
}
|
|
108
|
-
} catch {}
|
|
109
|
-
|
|
110
|
-
// Seed plugin-owned scaffolding files (memory-config.json, etc.).
|
|
111
|
-
// Idempotent — ensureDataSeeds skips existing files; fatal throw propagates
|
|
112
|
-
// anything that already exists, so the agent/index.mjs call and this one
|
|
113
|
-
// can both run without colliding.
|
|
94
|
+
// First-install seeding SSOT. ensureDataSeeds seeds mixdog-config.json AND
|
|
95
|
+
// user-workflow.json / user-workflow.md together as one fresh-install set, so
|
|
96
|
+
// the role→preset mapping always lands regardless of which boot path (this
|
|
97
|
+
// server, MCP boot, or agent index) runs first. It must NOT be split: seeding
|
|
98
|
+
// user-workflow here separately would set the init marker early and make
|
|
99
|
+
// ensureDataSeeds refuse the remaining first-time seeds. Idempotent — existing
|
|
100
|
+
// files are left untouched.
|
|
114
101
|
ensureDataSeeds(DATA_DIR);
|
|
115
102
|
|
|
116
103
|
// -- Helpers --
|
|
@@ -155,7 +142,7 @@ const SUPPORTED_PROVIDERS = [
|
|
|
155
142
|
'anthropic-oauth', 'openai-oauth', 'openai-api', 'gemini-api', 'xai-api', 'grok-oauth',
|
|
156
143
|
'tavily', 'firecrawl', 'exa',
|
|
157
144
|
];
|
|
158
|
-
const AGENT_KEY_PROVIDER_IDS = ['openai', 'anthropic', 'gemini', 'deepseek', 'xai'
|
|
145
|
+
const AGENT_KEY_PROVIDER_IDS = ['openai', 'anthropic', 'gemini', 'deepseek', 'xai'];
|
|
159
146
|
const SEARCH_KEY_PROVIDER_IDS = ['firecrawl', 'tavily', 'exa'];
|
|
160
147
|
const AGENT_PROVIDER_ENV = Object.freeze({
|
|
161
148
|
openai: 'OPENAI_API_KEY',
|
|
@@ -163,7 +150,6 @@ const AGENT_PROVIDER_ENV = Object.freeze({
|
|
|
163
150
|
gemini: 'GEMINI_API_KEY',
|
|
164
151
|
deepseek: 'DEEPSEEK_API_KEY',
|
|
165
152
|
xai: 'XAI_API_KEY',
|
|
166
|
-
nvidia: 'NVIDIA_API_KEY',
|
|
167
153
|
});
|
|
168
154
|
|
|
169
155
|
function envSecretPresent(account) {
|
|
@@ -420,11 +406,6 @@ async function validateAgentKey(provider, key) {
|
|
|
420
406
|
{ model: 'grok-3-mini-fast', messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 },
|
|
421
407
|
{ 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' });
|
|
422
408
|
return 'valid';
|
|
423
|
-
case 'nvidia':
|
|
424
|
-
await httpPostJson('https://integrate.api.nvidia.com/v1/chat/completions',
|
|
425
|
-
{ model: 'meta/llama-3.3-70b-instruct', messages: [{ role: 'user', content: 'hi' }], max_tokens: 1 },
|
|
426
|
-
{ 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' });
|
|
427
|
-
return 'valid';
|
|
428
409
|
default: return 'valid';
|
|
429
410
|
}
|
|
430
411
|
} catch { return 'invalid'; }
|
|
@@ -487,7 +468,7 @@ async function detectAuth(config = {}) {
|
|
|
487
468
|
for (const [name, envKey] of [
|
|
488
469
|
['openai', 'OPENAI_API_KEY'], ['anthropic', 'ANTHROPIC_API_KEY'],
|
|
489
470
|
['gemini', 'GEMINI_API_KEY'], ['deepseek', 'DEEPSEEK_API_KEY'],
|
|
490
|
-
['xai', 'XAI_API_KEY'],
|
|
471
|
+
['xai', 'XAI_API_KEY'],
|
|
491
472
|
]) { result.envKeys[name] = !!process.env[envKey]; }
|
|
492
473
|
// GROK_API_KEY is the last-resort xAI env alias honored by getAgentApiKey('xai')
|
|
493
474
|
// (shared/config.mjs is the SSOT); mirror it here so a GROK_API_KEY-only env
|
|
@@ -497,7 +478,7 @@ async function detectAuth(config = {}) {
|
|
|
497
478
|
// the secret value never leaves the server, so the UI can show "Set" without
|
|
498
479
|
// exposing the key.
|
|
499
480
|
result.keyStored = {};
|
|
500
|
-
for (const name of ['openai', 'anthropic', 'gemini', 'deepseek', 'xai'
|
|
481
|
+
for (const name of ['openai', 'anthropic', 'gemini', 'deepseek', 'xai']) {
|
|
501
482
|
result.keyStored[name] = hasStoredSecret(SECRET_ACCOUNTS.agentApiKey(name));
|
|
502
483
|
}
|
|
503
484
|
const ollamaUrl = config?.providers?.ollama?.baseURL || 'http://localhost:11434/v1';
|
|
@@ -537,7 +518,7 @@ async function detectAuth(config = {}) {
|
|
|
537
518
|
// in sync with src/agent/orchestrator/providers/registry.mjs.
|
|
538
519
|
const _RUNTIME_PROVIDER_NAMES = [
|
|
539
520
|
'anthropic', 'anthropic-oauth', 'openai', 'openai-oauth',
|
|
540
|
-
'gemini', 'deepseek', 'xai', 'grok-oauth',
|
|
521
|
+
'gemini', 'deepseek', 'xai', 'grok-oauth',
|
|
541
522
|
'ollama', 'lmstudio',
|
|
542
523
|
];
|
|
543
524
|
|
|
@@ -690,7 +671,6 @@ async function listProviderModels(providerId, cfg) {
|
|
|
690
671
|
openai: { url: 'https://api.openai.com/v1/models', auth: k => ({ 'Authorization': `Bearer ${k}` }) },
|
|
691
672
|
xai: { url: 'https://api.x.ai/v1/models', auth: k => ({ 'Authorization': `Bearer ${k}` }) },
|
|
692
673
|
deepseek: { url: 'https://api.deepseek.com/v1/models', auth: k => ({ 'Authorization': `Bearer ${k}` }) },
|
|
693
|
-
nvidia: { url: 'https://integrate.api.nvidia.com/v1/models', auth: k => ({ 'Authorization': `Bearer ${k}` }) },
|
|
694
674
|
};
|
|
695
675
|
const ep = KNOWN_ENDPOINTS[httpLookupId];
|
|
696
676
|
if (ep && pcfg.apiKey) {
|
package/setup/setup.html
CHANGED
|
@@ -2396,7 +2396,7 @@ function renderWebhookSection() {
|
|
|
2396
2396
|
'<a href="https://dashboard.ngrok.com/get-started/your-authtoken" target="_blank" style="color:var(--accent);text-decoration:none;font-size:11px;">Get Auth Token ↗</a>' +
|
|
2397
2397
|
'</div>';
|
|
2398
2398
|
document.getElementById('ch-webhook-authtoken').value = w.authtoken || '';
|
|
2399
|
-
document.getElementById('ch-webhook-domain').value = w.domain || '';
|
|
2399
|
+
document.getElementById('ch-webhook-domain').value = w.ngrokDomain || w.domain || '';
|
|
2400
2400
|
}
|
|
2401
2401
|
}
|
|
2402
2402
|
|
|
@@ -2507,7 +2507,7 @@ function buildChannelsData() {
|
|
|
2507
2507
|
|
|
2508
2508
|
const webhook = { respectQuiet: respectWebhook };
|
|
2509
2509
|
const webhookAuthtoken = document.getElementById('ch-webhook-authtoken');
|
|
2510
|
-
if (webhookAuthtoken) { webhook.enabled = document.getElementById('ch-webhook-enabled')?.classList.contains('on') || false; webhook.authtoken = webhookAuthtoken.value; webhook.
|
|
2510
|
+
if (webhookAuthtoken) { webhook.enabled = document.getElementById('ch-webhook-enabled')?.classList.contains('on') || false; webhook.authtoken = webhookAuthtoken.value; webhook.ngrokDomain = document.getElementById('ch-webhook-domain')?.value || undefined; webhook.port = parseInt(document.getElementById('ch-webhook-port')?.value) || undefined; webhook.batchInterval = parseInt(document.getElementById('ch-webhook-batch-interval')?.value) || undefined; }
|
|
2511
2511
|
|
|
2512
2512
|
return {
|
|
2513
2513
|
discord: { token: document.getElementById('ch-discord-token').value, applicationId: document.getElementById('ch-discord-appid').value },
|
|
@@ -2604,7 +2604,7 @@ const AG_API_PROVIDERS = [
|
|
|
2604
2604
|
{id:'gemini',name:'Gemini',env:'GEMINI_API_KEY',url:'https://aistudio.google.com/apikey'},
|
|
2605
2605
|
{id:'deepseek',name:'DeepSeek',env:'DEEPSEEK_API_KEY',url:'https://platform.deepseek.com/api_keys'},
|
|
2606
2606
|
{id:'xai',name:'xAI',env:'XAI_API_KEY',url:'https://console.x.ai'},
|
|
2607
|
-
{id:'
|
|
2607
|
+
{id:'opencode-go',name:'OpenCode Go',env:'OPENCODE_API_KEY',url:'https://opencode.ai'},
|
|
2608
2608
|
];
|
|
2609
2609
|
const AG_OAUTH_PROVIDERS = [
|
|
2610
2610
|
{id:'openai-oauth',name:'Codex',desc:'~/.codex/auth.json',login:true},
|
package/setup/tui.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Zero-dependency clack-style terminal UI helpers for
|
|
3
|
-
* Node built-ins only; assumes an interactive TTY
|
|
2
|
+
* Zero-dependency clack-style terminal UI helpers for mixdog setup/install.
|
|
3
|
+
* Node built-ins only; assumes an interactive TTY when prompts are used.
|
|
4
4
|
*/
|
|
5
5
|
import { emitKeypressEvents } from 'node:readline';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
6
|
|
|
8
7
|
const stdin = process.stdin;
|
|
9
8
|
const stdout = process.stdout;
|
|
@@ -16,12 +15,14 @@ const ansi = {
|
|
|
16
15
|
green: '\x1b[32m',
|
|
17
16
|
red: '\x1b[31m',
|
|
18
17
|
dim: '\x1b[2m',
|
|
18
|
+
bold: '\x1b[1m',
|
|
19
19
|
inverse: '\x1b[7m',
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
23
23
|
|
|
24
24
|
const STRIP_ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
25
|
+
const STRIP_OSC_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
25
26
|
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
26
27
|
|
|
27
28
|
function assertInteractiveTTY() {
|
|
@@ -30,13 +31,23 @@ function assertInteractiveTTY() {
|
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function
|
|
34
|
-
return
|
|
34
|
+
function gutter() {
|
|
35
|
+
return `${ansi.dim}│${ansi.reset}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function railEnd() {
|
|
39
|
+
return gutter();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Global key legend pinned at the bottom of every prompt (outside the rail). */
|
|
43
|
+
const NAV_LEGEND = 'Enter confirm · Ctrl+C quit';
|
|
44
|
+
function navLegendRow() {
|
|
45
|
+
return ` ${ansi.dim}${NAV_LEGEND}${ansi.reset}`;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
function prefixGlyph(done) {
|
|
38
49
|
return done
|
|
39
|
-
? `${ansi.green}
|
|
50
|
+
? `${ansi.green}✓${ansi.reset}`
|
|
40
51
|
: `${ansi.cyan}◆${ansi.reset}`;
|
|
41
52
|
}
|
|
42
53
|
|
|
@@ -97,7 +108,7 @@ function graphemeClusterWidth(segment) {
|
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
function cellWidth(str) {
|
|
100
|
-
const plain = str.replace(STRIP_ANSI_RE, '');
|
|
111
|
+
const plain = str.replace(STRIP_OSC_RE, '').replace(STRIP_ANSI_RE, '');
|
|
101
112
|
let width = 0;
|
|
102
113
|
for (const { segment } of GRAPHEME_SEGMENTER.segment(plain)) {
|
|
103
114
|
width += graphemeClusterWidth(segment);
|
|
@@ -129,7 +140,8 @@ function redrawUp(lines) {
|
|
|
129
140
|
|
|
130
141
|
function finishPrompt(lines, finalLine) {
|
|
131
142
|
redrawUp(lines);
|
|
132
|
-
stdout.write(`${ansi.green}
|
|
143
|
+
stdout.write(`${ansi.green}✓${ansi.reset} ${finalLine}\n`);
|
|
144
|
+
stdout.write(`${gutter()}\n`);
|
|
133
145
|
}
|
|
134
146
|
|
|
135
147
|
/**
|
|
@@ -211,158 +223,6 @@ async function withRawPrompt(run) {
|
|
|
211
223
|
}
|
|
212
224
|
}
|
|
213
225
|
|
|
214
|
-
/**
|
|
215
|
-
* @param {string} message
|
|
216
|
-
* @param {{ value: string, label: string, hint?: string }[]} options
|
|
217
|
-
* @param {{ initial?: string }} [opts]
|
|
218
|
-
*/
|
|
219
|
-
export async function select(message, options, { initial } = {}) {
|
|
220
|
-
if (!options?.length) {
|
|
221
|
-
throw new Error('select: options must be a non-empty array');
|
|
222
|
-
}
|
|
223
|
-
let index = 0;
|
|
224
|
-
if (initial !== undefined) {
|
|
225
|
-
const i = options.findIndex((o) => o.value === initial);
|
|
226
|
-
if (i >= 0) index = i;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
|
|
230
|
-
const draw = () => {
|
|
231
|
-
const rows = [
|
|
232
|
-
`${prefixGlyph(false)} ${message}`,
|
|
233
|
-
rail(),
|
|
234
|
-
];
|
|
235
|
-
for (let i = 0; i < options.length; i += 1) {
|
|
236
|
-
const opt = options[i];
|
|
237
|
-
const active = i === index;
|
|
238
|
-
const cursor = active ? `${ansi.cyan}❯ ${ansi.reset}` : ' ';
|
|
239
|
-
const label = active
|
|
240
|
-
? `${ansi.cyan}${ansi.inverse} ${opt.label} ${ansi.reset}`
|
|
241
|
-
: opt.label;
|
|
242
|
-
const hint = opt.hint ? ` ${ansi.dim}${opt.hint}${ansi.reset}` : '';
|
|
243
|
-
rows.push(`${rail()} ${cursor}${label}${hint}`);
|
|
244
|
-
}
|
|
245
|
-
return rows.join('\n');
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
rerender(draw);
|
|
249
|
-
|
|
250
|
-
return new Promise((resolve, reject) => {
|
|
251
|
-
promptReject(reject);
|
|
252
|
-
onKey((_str, key) => {
|
|
253
|
-
const name = key.name;
|
|
254
|
-
if (name === 'up' || name === 'k') {
|
|
255
|
-
index = (index - 1 + options.length) % options.length;
|
|
256
|
-
rerender(draw);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (name === 'down' || name === 'j') {
|
|
260
|
-
index = (index + 1) % options.length;
|
|
261
|
-
rerender(draw);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (name === 'return') {
|
|
265
|
-
const chosen = options[index];
|
|
266
|
-
finishPrompt(
|
|
267
|
-
lineCount(draw()),
|
|
268
|
-
`${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${chosen.label}${ansi.reset}`,
|
|
269
|
-
);
|
|
270
|
-
resolve(chosen.value);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* @param {string} message
|
|
279
|
-
* @param {{ value: string, label: string, hint?: string }[]} options
|
|
280
|
-
* @param {{ initial?: string[], min?: number }} [opts]
|
|
281
|
-
*/
|
|
282
|
-
export async function multiselect(message, options, { initial = [], min = 0 } = {}) {
|
|
283
|
-
if (!options?.length) {
|
|
284
|
-
throw new Error('multiselect: options must be a non-empty array');
|
|
285
|
-
}
|
|
286
|
-
const selected = new Set(
|
|
287
|
-
Array.isArray(initial) ? initial.filter((v) => options.some((o) => o.value === v)) : [],
|
|
288
|
-
);
|
|
289
|
-
let index = 0;
|
|
290
|
-
let error = '';
|
|
291
|
-
|
|
292
|
-
return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
|
|
293
|
-
const draw = () => {
|
|
294
|
-
const rows = [
|
|
295
|
-
`${prefixGlyph(false)} ${message}`,
|
|
296
|
-
rail(),
|
|
297
|
-
];
|
|
298
|
-
for (let i = 0; i < options.length; i += 1) {
|
|
299
|
-
const opt = options[i];
|
|
300
|
-
const active = i === index;
|
|
301
|
-
const checked = selected.has(opt.value);
|
|
302
|
-
const box = checked ? '◉' : '◯';
|
|
303
|
-
const cursor = active ? `${ansi.cyan}❯ ${ansi.reset}` : ' ';
|
|
304
|
-
const label = active
|
|
305
|
-
? `${ansi.cyan}${ansi.inverse} ${opt.label} ${ansi.reset}`
|
|
306
|
-
: opt.label;
|
|
307
|
-
const hint = opt.hint ? ` ${ansi.dim}${opt.hint}${ansi.reset}` : '';
|
|
308
|
-
rows.push(`${rail()} ${cursor}${box} ${label}${hint}`);
|
|
309
|
-
}
|
|
310
|
-
if (error) {
|
|
311
|
-
rows.push(`${rail()} ${ansi.red}${error}${ansi.reset}`);
|
|
312
|
-
}
|
|
313
|
-
return rows.join('\n');
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
rerender(draw);
|
|
317
|
-
|
|
318
|
-
return new Promise((resolve, reject) => {
|
|
319
|
-
promptReject(reject);
|
|
320
|
-
onKey((_str, key) => {
|
|
321
|
-
const name = key.name;
|
|
322
|
-
if (name === 'up' || name === 'k') {
|
|
323
|
-
index = (index - 1 + options.length) % options.length;
|
|
324
|
-
error = '';
|
|
325
|
-
rerender(draw);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
if (name === 'down' || name === 'j') {
|
|
329
|
-
index = (index + 1) % options.length;
|
|
330
|
-
error = '';
|
|
331
|
-
rerender(draw);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
if (name === 'space') {
|
|
335
|
-
const val = options[index].value;
|
|
336
|
-
if (selected.has(val)) selected.delete(val);
|
|
337
|
-
else selected.add(val);
|
|
338
|
-
error = '';
|
|
339
|
-
rerender(draw);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
if (name === 'return') {
|
|
343
|
-
if (selected.size < min) {
|
|
344
|
-
error = `Select at least ${min} option${min === 1 ? '' : 's'} (${selected.size}/${min})`;
|
|
345
|
-
rerender(draw);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const values = options
|
|
349
|
-
.filter((o) => selected.has(o.value))
|
|
350
|
-
.map((o) => o.value);
|
|
351
|
-
const labels = options
|
|
352
|
-
.filter((o) => selected.has(o.value))
|
|
353
|
-
.map((o) => o.label)
|
|
354
|
-
.join(', ');
|
|
355
|
-
finishPrompt(
|
|
356
|
-
lineCount(draw()),
|
|
357
|
-
`${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${labels || '(none)'}${ansi.reset}`,
|
|
358
|
-
);
|
|
359
|
-
resolve(values);
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
226
|
/**
|
|
367
227
|
* @param {string} message
|
|
368
228
|
* @param {{ initial?: boolean }} [opts]
|
|
@@ -380,9 +240,10 @@ export async function confirm(message, { initial = false } = {}) {
|
|
|
380
240
|
: `${ansi.dim}No${ansi.reset}`;
|
|
381
241
|
return [
|
|
382
242
|
`${prefixGlyph(false)} ${message}`,
|
|
383
|
-
|
|
384
|
-
`${
|
|
385
|
-
|
|
243
|
+
gutter(),
|
|
244
|
+
`${gutter()} ${yes} / ${no}`,
|
|
245
|
+
railEnd(),
|
|
246
|
+
navLegendRow(),
|
|
386
247
|
].join('\n');
|
|
387
248
|
};
|
|
388
249
|
|
|
@@ -392,12 +253,12 @@ export async function confirm(message, { initial = false } = {}) {
|
|
|
392
253
|
promptReject(reject);
|
|
393
254
|
onKey((str, key) => {
|
|
394
255
|
const name = key.name;
|
|
395
|
-
if (name === '
|
|
256
|
+
if (name === 'y') {
|
|
396
257
|
value = true;
|
|
397
258
|
rerender(draw);
|
|
398
259
|
return;
|
|
399
260
|
}
|
|
400
|
-
if (name === '
|
|
261
|
+
if (name === 'n') {
|
|
401
262
|
value = false;
|
|
402
263
|
rerender(draw);
|
|
403
264
|
return;
|
|
@@ -406,18 +267,10 @@ export async function confirm(message, { initial = false } = {}) {
|
|
|
406
267
|
const label = value ? 'Yes' : 'No';
|
|
407
268
|
finishPrompt(
|
|
408
269
|
lineCount(draw()),
|
|
409
|
-
`${message} ${ansi.dim}
|
|
270
|
+
`${message} ${ansi.dim}· ${label}${ansi.reset}`,
|
|
410
271
|
);
|
|
411
272
|
resolve(value);
|
|
412
273
|
}
|
|
413
|
-
if (str === 'y' || str === 'Y') {
|
|
414
|
-
value = true;
|
|
415
|
-
rerender(draw);
|
|
416
|
-
}
|
|
417
|
-
if (str === 'n' || str === 'N') {
|
|
418
|
-
value = false;
|
|
419
|
-
rerender(draw);
|
|
420
|
-
}
|
|
421
274
|
});
|
|
422
275
|
});
|
|
423
276
|
});
|
|
@@ -425,128 +278,10 @@ export async function confirm(message, { initial = false } = {}) {
|
|
|
425
278
|
|
|
426
279
|
/**
|
|
427
280
|
* @param {string} message
|
|
428
|
-
* @param {{ initial?: string, placeholder?: string }} [opts]
|
|
429
281
|
*/
|
|
430
|
-
export
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
|
|
434
|
-
const draw = () => {
|
|
435
|
-
const shown = value.length > 0
|
|
436
|
-
? value
|
|
437
|
-
: (placeholder ? `${ansi.dim}${placeholder}${ansi.reset}` : '');
|
|
438
|
-
return [
|
|
439
|
-
`${prefixGlyph(false)} ${message}`,
|
|
440
|
-
rail(),
|
|
441
|
-
`${rail()} ${shown}${ansi.dim}▌${ansi.reset}`,
|
|
442
|
-
].join('\n');
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
rerender(draw);
|
|
446
|
-
|
|
447
|
-
return new Promise((resolve, reject) => {
|
|
448
|
-
promptReject(reject);
|
|
449
|
-
onKey((str, key) => {
|
|
450
|
-
const name = key.name;
|
|
451
|
-
if (name === 'return') {
|
|
452
|
-
const out = value.length > 0 ? value : String(initial ?? '');
|
|
453
|
-
finishPrompt(
|
|
454
|
-
lineCount(draw()),
|
|
455
|
-
`${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${out || '(empty)'}${ansi.reset}`,
|
|
456
|
-
);
|
|
457
|
-
resolve(out);
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
if (name === 'backspace') {
|
|
461
|
-
value = value.slice(0, -1);
|
|
462
|
-
rerender(draw);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
if (str && !key.ctrl && !key.meta && str >= ' ') {
|
|
466
|
-
value += str;
|
|
467
|
-
rerender(draw);
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* @param {string} message
|
|
476
|
-
*/
|
|
477
|
-
export async function password(message) {
|
|
478
|
-
let value = '';
|
|
479
|
-
|
|
480
|
-
return withRawPrompt(async ({ rerender, onKey, promptReject }) => {
|
|
481
|
-
const draw = () => {
|
|
482
|
-
const masked = value.length > 0 ? '•'.repeat(value.length) : '';
|
|
483
|
-
return [
|
|
484
|
-
`${prefixGlyph(false)} ${message}`,
|
|
485
|
-
rail(),
|
|
486
|
-
`${rail()} ${masked}${ansi.dim}▌${ansi.reset}`,
|
|
487
|
-
].join('\n');
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
rerender(draw);
|
|
491
|
-
|
|
492
|
-
return new Promise((resolve, reject) => {
|
|
493
|
-
promptReject(reject);
|
|
494
|
-
onKey((str, key) => {
|
|
495
|
-
const name = key.name;
|
|
496
|
-
if (name === 'return') {
|
|
497
|
-
finishPrompt(
|
|
498
|
-
lineCount(draw()),
|
|
499
|
-
`${message} ${ansi.dim}·${ansi.reset} ${ansi.cyan}${'•'.repeat(value.length)}${ansi.reset}`,
|
|
500
|
-
);
|
|
501
|
-
resolve(value);
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
if (name === 'backspace') {
|
|
505
|
-
value = value.slice(0, -1);
|
|
506
|
-
rerender(draw);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
if (str && !key.ctrl && !key.meta && str >= ' ') {
|
|
510
|
-
value += str;
|
|
511
|
-
rerender(draw);
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
});
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* @param {string} message
|
|
520
|
-
* @param {{ total?: number, width?: number }} [opts]
|
|
521
|
-
*/
|
|
522
|
-
export function createProgressBar(message, { total = 100, width = 24 } = {}) {
|
|
523
|
-
const safeTotal = total > 0 ? total : 100;
|
|
524
|
-
let lastDone = 0;
|
|
525
|
-
let closed = false;
|
|
526
|
-
|
|
527
|
-
const render = (done, tot) => {
|
|
528
|
-
const clamped = Math.max(0, Math.min(tot, done));
|
|
529
|
-
const pct = Math.min(100, Math.floor((clamped / tot) * 100));
|
|
530
|
-
const filled = Math.min(width, Math.round((clamped / tot) * width));
|
|
531
|
-
const bar = `${'█'.repeat(filled)}${'░'.repeat(width - filled)}`;
|
|
532
|
-
stdout.write(`\r${message} [${bar}] ${pct}%`);
|
|
533
|
-
lastDone = clamped;
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
return {
|
|
537
|
-
update(done, totalOverride) {
|
|
538
|
-
if (closed) return;
|
|
539
|
-
const tot = totalOverride !== undefined && totalOverride > 0 ? totalOverride : safeTotal;
|
|
540
|
-
render(done, tot);
|
|
541
|
-
},
|
|
542
|
-
done(finalMsg) {
|
|
543
|
-
if (closed) return;
|
|
544
|
-
closed = true;
|
|
545
|
-
const tail = finalMsg ? ` ${finalMsg}` : '';
|
|
546
|
-
stdout.write('\r\x1b[2K');
|
|
547
|
-
stdout.write(`${ansi.green}✔${ansi.reset} ${message}${tail}\n`);
|
|
548
|
-
},
|
|
549
|
-
};
|
|
282
|
+
export function outro(message) {
|
|
283
|
+
writeBlock(gutter());
|
|
284
|
+
writeBlock(`${ansi.green}└${ansi.reset} ${message}`);
|
|
550
285
|
}
|
|
551
286
|
|
|
552
287
|
/**
|
|
@@ -578,29 +313,13 @@ export function createSpinner(message) {
|
|
|
578
313
|
stopped = true;
|
|
579
314
|
if (timer) clearInterval(timer);
|
|
580
315
|
stdout.write('\r\x1b[2K');
|
|
581
|
-
const mark = ok ? `${ansi.green}✔${ansi.reset}` : `${ansi.red}✖${ansi.reset}`;
|
|
582
316
|
const tail = finalMsg ? ` ${finalMsg}` : '';
|
|
317
|
+
if (ok) {
|
|
318
|
+
stdout.write(`${ansi.green}✓${ansi.reset} ${current}${tail}\n`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const mark = `${ansi.red}✖${ansi.reset}`;
|
|
583
322
|
stdout.write(`${mark} ${current}${tail}\n`);
|
|
584
323
|
},
|
|
585
324
|
};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (process.argv[1] === fileURLToPath(import.meta.url) && process.env.TUI_SMOKE) {
|
|
589
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
590
|
-
(async () => {
|
|
591
|
-
const bar = createProgressBar('TUI smoke progress', { total: 100, width: 20 });
|
|
592
|
-
for (let i = 0; i <= 100; i += 10) {
|
|
593
|
-
bar.update(i);
|
|
594
|
-
await sleep(40);
|
|
595
|
-
}
|
|
596
|
-
bar.done('complete');
|
|
597
|
-
const spin = createSpinner('TUI smoke spinner');
|
|
598
|
-
await sleep(400);
|
|
599
|
-
spin.update('TUI smoke spinner (tick)');
|
|
600
|
-
await sleep(400);
|
|
601
|
-
spin.stop('stopped', true);
|
|
602
|
-
})().catch((err) => {
|
|
603
|
-
console.error(err);
|
|
604
|
-
process.exit(1);
|
|
605
|
-
});
|
|
606
325
|
}
|
|
@@ -15,7 +15,6 @@ const ENV_KEY_MAP = {
|
|
|
15
15
|
gemini: 'GEMINI_API_KEY',
|
|
16
16
|
deepseek: 'DEEPSEEK_API_KEY',
|
|
17
17
|
xai: 'XAI_API_KEY',
|
|
18
|
-
nvidia: 'NVIDIA_API_KEY',
|
|
19
18
|
};
|
|
20
19
|
// Canonical maintenance defaults. Single source of truth — imported by
|
|
21
20
|
// llm/index.mjs and setup-server.mjs so UI/runtime cannot drift from config.
|
|
@@ -1790,11 +1790,8 @@ export async function loginOAuth() {
|
|
|
1790
1790
|
url.searchParams.set('code_challenge_method', 'S256');
|
|
1791
1791
|
url.searchParams.set('state', state);
|
|
1792
1792
|
process.stderr.write(`\n[anthropic-oauth] Open this URL to log in with Claude:\n${url.toString()}\n\n`);
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
const opener = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
1796
|
-
exec(`${opener} "${url.toString()}"`, { windowsHide: true });
|
|
1797
|
-
} catch { /* user opens manually */ }
|
|
1793
|
+
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
1794
|
+
openInBrowser(url.toString());
|
|
1798
1795
|
|
|
1799
1796
|
return new Promise((resolve) => {
|
|
1800
1797
|
const timeout = setTimeout(() => { server.close(); resolve(null); }, OAUTH_LOGIN_TIMEOUT_MS);
|