@asobacloud/nehanda 0.6.1 → 0.8.0
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/README.md +36 -29
- package/bin/agent.mjs +70 -59
- package/bin/nehanda-ui.mjs +147 -143
- package/lib/configureProvider.mjs +254 -0
- package/lib/modelConfig.mjs +20 -10
- package/lib/modelDiscovery.mjs +125 -1
- package/lib/openaiCompat.mjs +11 -1
- package/lib/orchestrate.mjs +32 -9
- package/lib/slashCommands.mjs +128 -0
- package/lib/ui.mjs +15 -10
- package/package.json +1 -1
- package/lib/scripts/__pycache__/laya_worker.cpython-314.pyc +0 -0
package/bin/nehanda-ui.mjs
CHANGED
|
@@ -18,6 +18,12 @@ import { fileURLToPath } from 'node:url'
|
|
|
18
18
|
import { runUserTurn } from '../lib/orchestrate.mjs'
|
|
19
19
|
import { loadInstructions } from '../lib/instructions.mjs'
|
|
20
20
|
import { modelConfigFromAgent } from '../lib/modelConfig.mjs'
|
|
21
|
+
import { runConfigureWizard } from '../lib/configureProvider.mjs'
|
|
22
|
+
import {
|
|
23
|
+
TUI_SLASH_COMMANDS,
|
|
24
|
+
filterSlashAutocomplete,
|
|
25
|
+
parseMcpEnvParts,
|
|
26
|
+
} from '../lib/slashCommands.mjs'
|
|
21
27
|
import { applyMcpConfig, loadMcpConfig, writeGlobalMcpConfig, readGlobalMcpConfig, globalMcpConfigPath, updateMcpServerEnv } from '../lib/mcp-config.mjs'
|
|
22
28
|
import { getMcpServer, closeAllMcpServers, listMcpServers, mcpListResources } from '../lib/mcp.mjs'
|
|
23
29
|
import { invalidateMcpToolCache } from '../lib/tools.mjs'
|
|
@@ -136,51 +142,7 @@ function renderMd(text) {
|
|
|
136
142
|
|
|
137
143
|
// ── Constants ────────────────────────────────────────────────
|
|
138
144
|
const SPINNER_FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏']
|
|
139
|
-
const SLASH_COMMANDS =
|
|
140
|
-
{ name: '/model', desc: 'Change model endpoint' },
|
|
141
|
-
{ name: '/key', desc: 'Set API key (/key, /key openrouter, /key zhipu, /key openai)' },
|
|
142
|
-
{ name: '/config', desc: 'Show current config' },
|
|
143
|
-
{ name: '/mcp', desc: 'MCP server & tool management (/mcp list)' },
|
|
144
|
-
{ name: '/clear', desc: 'New conversation' },
|
|
145
|
-
{ name: '/retry', desc: 'Resend the last failed message' },
|
|
146
|
-
{ name: '/help', desc: 'Show commands' },
|
|
147
|
-
{ name: '/exit', desc: 'Quit' },
|
|
148
|
-
]
|
|
149
|
-
|
|
150
|
-
// ── Model registry ────────────────────────────────────────────
|
|
151
|
-
const DEFAULT_REGISTRY = {
|
|
152
|
-
ollama_hosts: ['http://AsobaCorp-1.local:11434'],
|
|
153
|
-
models: [
|
|
154
|
-
{ name: 'nehanda-rag-synthesis-27b', endpoint: 'https://nehanda-ml.asoba.co/v1', label: 'Nehanda 27B (EC2)', desc: 'Primary — fine-tuned Qwen3.6 27B, vLLM, af-south-1' },
|
|
155
|
-
{ name: 'deepseek-coder-v2:latest', endpoint: 'http://AsobaCorp-1.local:11434/v1', label: 'DeepSeek Coder V2 (LAN)', desc: 'Windows LAN delegate — coding specialist' },
|
|
156
|
-
{ name: 'qwen2.5:14b', endpoint: 'http://AsobaCorp-1.local:11434/v1', label: 'Qwen 2.5 14B (LAN)', desc: 'Windows LAN delegate — general reasoning' },
|
|
157
|
-
],
|
|
158
|
-
}
|
|
159
|
-
function readRegistry() {
|
|
160
|
-
try { return JSON.parse(fs.readFileSync(path.join(AIMEE_DIR, 'model-registry.json'), 'utf8')) }
|
|
161
|
-
catch { return DEFAULT_REGISTRY }
|
|
162
|
-
}
|
|
163
|
-
async function discoverOllamaModels(hosts) {
|
|
164
|
-
const out = []
|
|
165
|
-
await Promise.all(hosts.map(async host => {
|
|
166
|
-
const base = host.replace(/\/$/, '')
|
|
167
|
-
try {
|
|
168
|
-
const res = await fetch(`${base}/api/tags`, { signal: AbortSignal.timeout(2000) })
|
|
169
|
-
if (!res.ok) return
|
|
170
|
-
const data = await res.json()
|
|
171
|
-
for (const m of (data.models || []))
|
|
172
|
-
out.push({ name: m.name, endpoint: `${base}/v1`, label: `${m.name} (${new URL(base).hostname})`, desc: `Discovered — ${m.name}`, discovered: true })
|
|
173
|
-
} catch {}
|
|
174
|
-
}))
|
|
175
|
-
return out
|
|
176
|
-
}
|
|
177
|
-
async function buildModelList() {
|
|
178
|
-
const reg = readRegistry()
|
|
179
|
-
const base = reg.models || []
|
|
180
|
-
const disc = await discoverOllamaModels(reg.ollama_hosts || [])
|
|
181
|
-
const keys = new Set(base.map(m => `${m.name}|${m.endpoint}`))
|
|
182
|
-
return [...base, ...disc.filter(m => !keys.has(`${m.name}|${m.endpoint}`))]
|
|
183
|
-
}
|
|
145
|
+
const SLASH_COMMANDS = TUI_SLASH_COMMANDS
|
|
184
146
|
|
|
185
147
|
// ── Asoba lock-up (compact) ───────────────────────────────────
|
|
186
148
|
function AsobaMark() {
|
|
@@ -302,15 +264,20 @@ function Spinner({ label }) {
|
|
|
302
264
|
return e(Box, { marginLeft: 1 }, e(Text, { color: BRAND.accent }, SPINNER_FRAMES[frame % SPINNER_FRAMES.length] + ' '), e(Text, { dimColor: true }, verb + '…'))
|
|
303
265
|
}
|
|
304
266
|
function SlashMenu({ filter }) {
|
|
305
|
-
const matches =
|
|
267
|
+
const matches = filterSlashAutocomplete(filter)
|
|
306
268
|
if (!matches.length) return null
|
|
269
|
+
const pad = Math.min(36, Math.max(...matches.map(c => c.name.length), 12) + 1)
|
|
307
270
|
return e(Box, { flexDirection: 'column', marginLeft: 2 },
|
|
308
271
|
...matches.map((c, i) => e(Box, { key: String(i) },
|
|
309
|
-
e(Text, { color: BRAND.accent }, c.name.padEnd(
|
|
272
|
+
e(Text, { color: BRAND.accent }, c.name.padEnd(pad)), e(Text, { dimColor: true }, c.desc))))
|
|
310
273
|
}
|
|
311
274
|
function InputArea({ onSubmit, isLoading }) {
|
|
312
275
|
const [value, setValue] = React.useState('')
|
|
313
|
-
|
|
276
|
+
// Show slash menu for top-level (`/m`) and while typing known parents (`/mcp `, `/key o`)
|
|
277
|
+
const showMenu = value.startsWith('/') && (
|
|
278
|
+
!value.includes(' ')
|
|
279
|
+
|| /^\/(mcp|key)(\s|$)/i.test(value)
|
|
280
|
+
)
|
|
314
281
|
const handleSubmit = React.useCallback(v => { if (!v.trim()) return; setValue(''); onSubmit(v.trim()) }, [onSubmit])
|
|
315
282
|
if (isLoading) return e(Box, { paddingX: 1 }, e(Text, { dimColor: true }, 'esc to cancel'))
|
|
316
283
|
return e(Box, { flexDirection: 'column' },
|
|
@@ -390,6 +357,7 @@ function BlockConfirmMenu({ payload, onDecide }) {
|
|
|
390
357
|
}
|
|
391
358
|
|
|
392
359
|
function TokenFooter({ calls, model }) {
|
|
360
|
+
const cols = process.stdout.columns || 80
|
|
393
361
|
return e(Box, { flexDirection: 'column' },
|
|
394
362
|
e(Text, { dimColor: true }, '─'.repeat(cols)),
|
|
395
363
|
e(Box, { flexDirection: 'row', paddingX: 1, justifyContent: 'space-between' },
|
|
@@ -517,26 +485,84 @@ function stripThink(text) {
|
|
|
517
485
|
.trim()
|
|
518
486
|
}
|
|
519
487
|
|
|
488
|
+
/**
|
|
489
|
+
* UI adapter for Goose-style configure wizard (#8).
|
|
490
|
+
* Persists agents.json + runtimeSettings.model_config atomically at the end.
|
|
491
|
+
*/
|
|
492
|
+
async function runUiConfigureWizard(bridge, opts = {}) {
|
|
493
|
+
try {
|
|
494
|
+
const result = await runConfigureWizard({
|
|
495
|
+
ask: q => bridge.ask(q),
|
|
496
|
+
select: (title, items) => bridge.select(title, items),
|
|
497
|
+
notify: msg => bridge.addMessage({ role: 'system', text: ` ${msg}` }),
|
|
498
|
+
resolveExistingKey: async (preset) => {
|
|
499
|
+
if (preset.keySlot === 'nehanda') return readApiKey()
|
|
500
|
+
if (preset.keySlot === 'zhipu') return resolveZhipuApiKey()
|
|
501
|
+
if (preset.keySlot === 'openai_compat') return resolveOpenAiCompatKey()
|
|
502
|
+
return ''
|
|
503
|
+
},
|
|
504
|
+
saveKey: async ({ slot, key }) => {
|
|
505
|
+
if (slot === 'nehanda') writeApiKey(key)
|
|
506
|
+
else if (slot === 'zhipu') saveSecureCredentials({ zhipuApiKey: key })
|
|
507
|
+
else if (slot === 'openai_compat') saveOpenAiCompatKey(key)
|
|
508
|
+
},
|
|
509
|
+
persist: async (cfg) => {
|
|
510
|
+
const data = readAgents()
|
|
511
|
+
const ag = data.agents?.find(a => a.name === data.default_agent) || data.agents?.[0]
|
|
512
|
+
if (ag) {
|
|
513
|
+
ag.model = cfg.model
|
|
514
|
+
ag.endpoint = cfg.endpoint
|
|
515
|
+
if (cfg.provider === 'nehanda' && cfg.apiKey) ag.api_key = cfg.apiKey
|
|
516
|
+
}
|
|
517
|
+
fs.writeFileSync(AGENTS_JSON, JSON.stringify(data, null, 2), 'utf8')
|
|
518
|
+
runtimeSettings.model_config = cfg.model_config || modelConfigFromAgent(
|
|
519
|
+
{ model: cfg.model, endpoint: cfg.endpoint },
|
|
520
|
+
{ apiKey: cfg.apiKey || readApiKey() },
|
|
521
|
+
)
|
|
522
|
+
},
|
|
523
|
+
}, opts)
|
|
524
|
+
|
|
525
|
+
if (!result) {
|
|
526
|
+
bridge.addMessage({ role: 'system', text: ' Configure cancelled.' })
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
bridge.refreshModel()
|
|
530
|
+
bridge.addMessage({ role: 'system', text: [
|
|
531
|
+
` ${chalk.green('✓')} Provider → ${chalk.bold(result.provider)}`,
|
|
532
|
+
` ${chalk.green('✓')} Host → ${chalk.dim(result.endpoint)}`,
|
|
533
|
+
` ${chalk.green('✓')} Model → ${chalk.bold(result.model)}`,
|
|
534
|
+
].join('\n') })
|
|
535
|
+
} catch (err) {
|
|
536
|
+
bridge.addMessage({ role: 'error', text: 'Configure failed: ' + err.message })
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
520
540
|
// ── Command handler ───────────────────────────────────────────
|
|
521
541
|
async function handleCommand(cmd, bridge) {
|
|
522
542
|
const [name] = cmd.split(/\s+/)
|
|
523
543
|
|
|
524
544
|
if (name === '/help') {
|
|
525
|
-
|
|
526
|
-
|
|
545
|
+
const pad = Math.max(...TUI_SLASH_COMMANDS.map(c => c.name.length), 12) + 2
|
|
546
|
+
const body = TUI_SLASH_COMMANDS.map(c =>
|
|
547
|
+
` ${chalk.hex(BRAND.accent)(c.name.padEnd(pad))}${chalk.dim(c.desc)}`
|
|
548
|
+
).join('\n')
|
|
549
|
+
bridge.addMessage({ role: 'system', text: `\nCommands:\n\n${body}\n` })
|
|
527
550
|
return
|
|
528
551
|
}
|
|
529
552
|
if (name === '/config') {
|
|
530
553
|
const data = readAgents()
|
|
531
554
|
const ag = data.agents?.find(a => a.name === data.default_agent) || data.agents?.[0]
|
|
555
|
+
const mc = runtimeSettings?.model_config
|
|
532
556
|
const compatKeySet = !!resolveOpenAiCompatKey()
|
|
533
557
|
const zhipuKeySet = !!resolveZhipuApiKey()
|
|
534
558
|
bridge.addMessage({ role: 'system', text: [
|
|
535
|
-
'', `
|
|
536
|
-
`
|
|
559
|
+
'', ` Provider: ${chalk.hex(BRAND.accent)(mc?.provider || '—')}`,
|
|
560
|
+
` Model: ${chalk.hex(BRAND.accent)(ag?.model || mc?.custom_model_name || '—')}`,
|
|
561
|
+
` Endpoint: ${chalk.dim(ag?.endpoint || mc?.base_url || '—')}`,
|
|
537
562
|
` Nehanda key: ${readApiKey() ? chalk.green('set') : chalk.hex(BRAND.red)('not set')}`,
|
|
538
563
|
` OpenAI-compat key: ${compatKeySet ? chalk.green('set') : chalk.dim('not set')}`,
|
|
539
|
-
` Zhipu key: ${zhipuKeySet ? chalk.green('set') : chalk.dim('not set')}`,
|
|
564
|
+
` Zhipu key: ${zhipuKeySet ? chalk.green('set') : chalk.dim('not set')}`,
|
|
565
|
+
` ${chalk.dim('Use /model to configure provider, key, host, and model in one flow.')}`, '',
|
|
540
566
|
].join('\n') })
|
|
541
567
|
return
|
|
542
568
|
}
|
|
@@ -552,74 +578,43 @@ async function handleCommand(cmd, bridge) {
|
|
|
552
578
|
}
|
|
553
579
|
if (name === '/exit' || name === '/quit') { bridge.exit(); process.exit(0) }
|
|
554
580
|
if (name === '/key') {
|
|
555
|
-
//
|
|
581
|
+
// /key [openrouter|zhipu|openai|nehanda] [value] — save key then continue configure wizard
|
|
556
582
|
const parts = cmd.trim().split(/\s+/)
|
|
557
|
-
const sub = parts[1] || ''
|
|
558
|
-
const
|
|
583
|
+
const sub = (parts[1] || '').toLowerCase()
|
|
584
|
+
const KEY_TO_PRESET = {
|
|
585
|
+
openrouter: 'openrouter',
|
|
586
|
+
openai: 'openai_compatible',
|
|
587
|
+
zhipu: 'zhipu',
|
|
588
|
+
nehanda: 'nehanda',
|
|
589
|
+
}
|
|
559
590
|
|
|
560
|
-
if (!sub || !
|
|
561
|
-
// Default: Nehanda API key (existing behaviour, unchanged)
|
|
591
|
+
if (!sub || !KEY_TO_PRESET[sub]) {
|
|
562
592
|
const answer = await bridge.ask('New Nehanda API key:')
|
|
563
593
|
if (!answer) return
|
|
564
|
-
try {
|
|
565
|
-
|
|
594
|
+
try {
|
|
595
|
+
writeApiKey(answer)
|
|
596
|
+
bridge.addMessage({ role: 'system', text: ` ${chalk.green('✓')} Nehanda API key saved — continuing configure…` })
|
|
597
|
+
await runUiConfigureWizard(bridge, { presetId: 'nehanda', prefilledKey: answer })
|
|
598
|
+
} catch (err) { bridge.addMessage({ role: 'error', text: 'Failed: ' + err.message }) }
|
|
566
599
|
return
|
|
567
600
|
}
|
|
568
601
|
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
const answer = inlineValue || await bridge.ask(`New ${provider} API key:`)
|
|
602
|
+
const presetId = KEY_TO_PRESET[sub]
|
|
603
|
+
const inlineValue = parts.slice(2).join(' ').trim()
|
|
604
|
+
const answer = inlineValue || await bridge.ask(`New ${sub} API key:`)
|
|
573
605
|
if (!answer) return
|
|
574
606
|
|
|
575
607
|
try {
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
saveOpenAiCompatKey(answer)
|
|
582
|
-
if (provider === 'openrouter') {
|
|
583
|
-
// Auto-configure base_url and provider for OpenRouter
|
|
584
|
-
runtimeSettings.model_config = {
|
|
585
|
-
...runtimeSettings.model_config,
|
|
586
|
-
base_url: 'https://openrouter.ai/api/v1',
|
|
587
|
-
provider: 'openai_compatible',
|
|
588
|
-
}
|
|
589
|
-
bridge.addMessage({ role: 'system', text: [
|
|
590
|
-
` ${chalk.green('✓')} OpenRouter API key saved to secure storage.`,
|
|
591
|
-
` ${chalk.green('✓')} Base URL set to ${chalk.dim('https://openrouter.ai/api/v1')}`,
|
|
592
|
-
` ${chalk.dim('Use /model to select an OpenRouter model (e.g. openai_compatible/anthropic/claude-3.5-sonnet)')}`,
|
|
593
|
-
].join('\n') })
|
|
594
|
-
} else {
|
|
595
|
-
bridge.addMessage({ role: 'system', text: ` ${chalk.green('✓')} OpenAI-compat API key saved to secure storage.` })
|
|
596
|
-
}
|
|
597
|
-
}
|
|
608
|
+
if (sub === 'zhipu') saveSecureCredentials({ zhipuApiKey: answer })
|
|
609
|
+
else if (sub === 'nehanda') writeApiKey(answer)
|
|
610
|
+
else saveOpenAiCompatKey(answer)
|
|
611
|
+
bridge.addMessage({ role: 'system', text: ` ${chalk.green('✓')} ${sub} API key saved — continuing configure…` })
|
|
612
|
+
await runUiConfigureWizard(bridge, { presetId, prefilledKey: answer })
|
|
598
613
|
} catch (err) { bridge.addMessage({ role: 'error', text: 'Failed: ' + err.message }) }
|
|
599
614
|
return
|
|
600
615
|
}
|
|
601
616
|
if (name === '/model') {
|
|
602
|
-
bridge
|
|
603
|
-
const all = await buildModelList()
|
|
604
|
-
const data0 = readAgents()
|
|
605
|
-
const curAg = data0.agents?.find(a => a.name === data0.default_agent) || data0.agents?.[0]
|
|
606
|
-
const cur = curAg?.model || ''
|
|
607
|
-
const items = all.map(m => ({ ...m, label: m.name === cur ? `${m.label} ✓` : m.label }))
|
|
608
|
-
const sel = await bridge.select('Select model', items)
|
|
609
|
-
if (!sel) return
|
|
610
|
-
try {
|
|
611
|
-
const data = readAgents()
|
|
612
|
-
const ag = data.agents?.find(a => a.name === data.default_agent) || data.agents?.[0]
|
|
613
|
-
if (ag) { ag.model = sel.name; ag.endpoint = sel.endpoint }
|
|
614
|
-
fs.writeFileSync(AGENTS_JSON, JSON.stringify(data, null, 2), 'utf8')
|
|
615
|
-
// Keep in-session model_config in sync so the next turn uses the selected wire name
|
|
616
|
-
runtimeSettings.model_config = modelConfigFromAgent(
|
|
617
|
-
ag || { model: sel.name, endpoint: sel.endpoint },
|
|
618
|
-
{ apiKey: readApiKey() },
|
|
619
|
-
)
|
|
620
|
-
bridge.refreshModel()
|
|
621
|
-
bridge.addMessage({ role: 'system', text: ` ${chalk.green('✓')} Model → ${chalk.bold(sel.name)}` })
|
|
622
|
-
} catch (err) { bridge.addMessage({ role: 'error', text: 'Failed: ' + err.message }) }
|
|
617
|
+
await runUiConfigureWizard(bridge)
|
|
623
618
|
return
|
|
624
619
|
}
|
|
625
620
|
if (name === '/mcp') {
|
|
@@ -629,14 +624,19 @@ async function handleCommand(cmd, bridge) {
|
|
|
629
624
|
const serverNames = Object.keys(servers)
|
|
630
625
|
|
|
631
626
|
if (!sub || sub === 'help') {
|
|
627
|
+
const pad = 40
|
|
628
|
+
const row = (n, d) => ` ${chalk.hex(BRAND.accent)(n.padEnd(pad))}${chalk.dim(d)}`
|
|
632
629
|
const lines = [
|
|
633
630
|
'',
|
|
634
631
|
` ${chalk.bold('MCP Server & Tool Commands:')}`,
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
632
|
+
row('/mcp list', 'List tools from configured MCP servers'),
|
|
633
|
+
row('/mcp status', 'Show configured servers'),
|
|
634
|
+
row('/mcp reload [server]', 'Reload mcp.json into this session'),
|
|
635
|
+
row('/mcp add <name> <command> [args…]', 'Register a server'),
|
|
636
|
+
row('/mcp env', 'Show env vars for every server (values masked)'),
|
|
637
|
+
row('/mcp env <server>', 'Show env vars for one server'),
|
|
638
|
+
row('/mcp env <server> <NAME> <VALUE>', 'Set env var e.g. /mcp env jupyter JUPYTER_TOKEN sk-…'),
|
|
639
|
+
row('/mcp env unset <server> <NAME>', 'Remove an env var'),
|
|
640
640
|
'',
|
|
641
641
|
` ${chalk.dim(`Configured servers: ${serverNames.length ? serverNames.join(', ') : 'none'}`)}`,
|
|
642
642
|
` ${chalk.dim('Tip: Run /mcp list to inspect tools injected into the assistant.')}`,
|
|
@@ -726,24 +726,26 @@ async function handleCommand(cmd, bridge) {
|
|
|
726
726
|
}
|
|
727
727
|
|
|
728
728
|
if (sub === 'env') {
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
729
|
+
const parsed = parseMcpEnvParts(parts.slice(1))
|
|
730
|
+
|
|
731
|
+
if (parsed.action === 'usage_error') {
|
|
732
|
+
bridge.addMessage({ role: 'system', text: ' ' + parsed.error.replace(/\n/g, '\n ') })
|
|
733
|
+
return
|
|
734
|
+
}
|
|
732
735
|
|
|
733
|
-
if (
|
|
734
|
-
// Show env for all servers
|
|
736
|
+
if (parsed.action === 'list_all') {
|
|
735
737
|
if (!serverNames.length) {
|
|
736
738
|
bridge.addMessage({ role: 'system', text: ' No MCP servers configured.' })
|
|
737
739
|
return
|
|
738
740
|
}
|
|
739
741
|
const lines = ['\n MCP Server Environment Variables:']
|
|
740
|
-
for (const
|
|
741
|
-
const cfg = servers[
|
|
742
|
+
for (const srv of serverNames) {
|
|
743
|
+
const cfg = servers[srv]
|
|
742
744
|
const envKeys = Object.keys(cfg.env || {})
|
|
743
745
|
if (!envKeys.length) {
|
|
744
|
-
lines.push(` ${chalk.hex(BRAND.accent)(
|
|
746
|
+
lines.push(` ${chalk.hex(BRAND.accent)(srv)}: (none)`)
|
|
745
747
|
} else {
|
|
746
|
-
lines.push(` ${chalk.hex(BRAND.accent)(
|
|
748
|
+
lines.push(` ${chalk.hex(BRAND.accent)(srv)}:`)
|
|
747
749
|
for (const k of envKeys) {
|
|
748
750
|
const v = cfg.env[k]
|
|
749
751
|
const masked = v.length > 10 ? v.slice(0, 6) + '...' + v.slice(-4) : '********'
|
|
@@ -755,18 +757,17 @@ async function handleCommand(cmd, bridge) {
|
|
|
755
757
|
return
|
|
756
758
|
}
|
|
757
759
|
|
|
758
|
-
if (
|
|
759
|
-
|
|
760
|
-
const cfg = servers[targetServer]
|
|
760
|
+
if (parsed.action === 'list_server') {
|
|
761
|
+
const cfg = servers[parsed.server]
|
|
761
762
|
if (!cfg) {
|
|
762
|
-
bridge.addMessage({ role: 'system', text: ` Server "${
|
|
763
|
+
bridge.addMessage({ role: 'system', text: ` Server "${parsed.server}" not found.` })
|
|
763
764
|
return
|
|
764
765
|
}
|
|
765
766
|
const envKeys = Object.keys(cfg.env || {})
|
|
766
767
|
if (!envKeys.length) {
|
|
767
|
-
bridge.addMessage({ role: 'system', text: ` ${chalk.hex(BRAND.accent)(
|
|
768
|
+
bridge.addMessage({ role: 'system', text: ` ${chalk.hex(BRAND.accent)(parsed.server)}: (no env vars set)\n Set one with: /mcp env ${parsed.server} <NAME> <VALUE>` })
|
|
768
769
|
} else {
|
|
769
|
-
const lines = [` ${chalk.hex(BRAND.accent)(
|
|
770
|
+
const lines = [` ${chalk.hex(BRAND.accent)(parsed.server)}:`]
|
|
770
771
|
for (const k of envKeys) {
|
|
771
772
|
const v = cfg.env[k]
|
|
772
773
|
const masked = v.length > 10 ? v.slice(0, 6) + '...' + v.slice(-4) : '********'
|
|
@@ -777,25 +778,26 @@ async function handleCommand(cmd, bridge) {
|
|
|
777
778
|
return
|
|
778
779
|
}
|
|
779
780
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
781
|
+
if (parsed.action === 'set' || parsed.action === 'unset') {
|
|
782
|
+
const value = parsed.action === 'unset' ? '' : parsed.value
|
|
783
|
+
const result = updateMcpServerEnv(parsed.server, parsed.name, value, process.cwd())
|
|
784
|
+
if (!result.ok) {
|
|
785
|
+
bridge.addMessage({ role: 'system', text: ` ${chalk.hex(BRAND.red)('✗')} ${result.error}` })
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
runtimeSettings.mcp_servers = {}
|
|
789
|
+
applyMcpConfig(runtimeSettings, process.cwd())
|
|
790
|
+
invalidateMcpToolCache()
|
|
791
|
+
if (parsed.action === 'set') {
|
|
792
|
+
bridge.addMessage({ role: 'system', text: ` ${chalk.green('✓')} Set ${chalk.bold(parsed.name)} = ******** on ${chalk.bold(parsed.server)}\n Config file: ${result.path}` })
|
|
793
|
+
} else {
|
|
794
|
+
bridge.addMessage({ role: 'system', text: ` ${chalk.green('✓')} Unset ${chalk.bold(parsed.name)} on ${chalk.bold(parsed.server)}\n Config file: ${result.path}` })
|
|
795
|
+
}
|
|
784
796
|
return
|
|
785
797
|
}
|
|
786
|
-
// Reload runtime settings and bust cache
|
|
787
|
-
runtimeSettings.mcp_servers = {}
|
|
788
|
-
applyMcpConfig(runtimeSettings, process.cwd())
|
|
789
|
-
invalidateMcpToolCache()
|
|
790
|
-
if (result.set) {
|
|
791
|
-
bridge.addMessage({ role: 'system', text: ` ${chalk.green('✓')} Set ${chalk.bold(envKey)} = ${'********'} on ${chalk.bold(targetServer)}\n Config file: ${result.path}` })
|
|
792
|
-
} else {
|
|
793
|
-
bridge.addMessage({ role: 'system', text: ` ${chalk.green('✓')} Cleared ${chalk.bold(envKey)} on ${chalk.bold(targetServer)}\n Config file: ${result.path}` })
|
|
794
|
-
}
|
|
795
|
-
return
|
|
796
798
|
}
|
|
797
799
|
|
|
798
|
-
bridge.addMessage({ role: 'system', text: ' /mcp sub-commands: status · list · reload
|
|
800
|
+
bridge.addMessage({ role: 'system', text: ' /mcp sub-commands: status · list · reload · add · env · env unset — type /help for full list' })
|
|
799
801
|
return
|
|
800
802
|
}
|
|
801
803
|
|
|
@@ -862,6 +864,8 @@ bridge.onSubmit = async (text) => {
|
|
|
862
864
|
// field belongs to aimee and was silently reintroducing conflicting tool-call
|
|
863
865
|
// syntax (<tool_call><name>...) that fights orchestrate.mjs's [TOOL_CALL] format.
|
|
864
866
|
onaInstructions: onaInstructionsContent || null,
|
|
867
|
+
// Esc/Ctrl+C abort — must reach fetch / Anthropic stream (issue #7).
|
|
868
|
+
signal: controller.signal,
|
|
865
869
|
settings: {
|
|
866
870
|
...runtimeSettings,
|
|
867
871
|
model_config: modelConfig,
|
|
@@ -914,7 +918,7 @@ bridge.onSubmit = async (text) => {
|
|
|
914
918
|
bridge.lastFailedText = null
|
|
915
919
|
|
|
916
920
|
} catch (err) {
|
|
917
|
-
if (err.name === 'AbortError') {
|
|
921
|
+
if (err.name === 'AbortError' || err.name === 'APIUserAbortError') {
|
|
918
922
|
bridge.endAssistant({ role: 'assistant', text: lastPartial, cancelled: true })
|
|
919
923
|
} else {
|
|
920
924
|
bridge.discardAssistant()
|