@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/README.md
CHANGED
|
@@ -51,31 +51,32 @@ ona-code
|
|
|
51
51
|
|
|
52
52
|
### 3. Provider Setup
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
The CLI defaults to the primary Nehanda endpoint (`https://nehanda-ml.asoba.co/v1`). If an API key is required:
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
❯ /key
|
|
60
|
-
New Nehanda API key: <your-key>
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
#### Option B: Local LM Studio
|
|
64
|
-
|
|
65
|
-
Start LM Studio locally on port `1234` or `8000`, then start the CLI. Select or switch models via:
|
|
54
|
+
Use a single Goose-style wizard — provider, API key, host, and model in one flow. The CLI discovers available models from the provider (`GET /v1/models` with your key, or Ollama `/api/tags`).
|
|
66
55
|
|
|
67
56
|
```
|
|
68
57
|
❯ /model
|
|
69
|
-
```
|
|
70
58
|
|
|
71
|
-
|
|
59
|
+
◆ Which provider?
|
|
60
|
+
│ ● OpenRouter
|
|
61
|
+
└
|
|
62
|
+
◆ API key
|
|
63
|
+
│ sk-or-v1-…
|
|
64
|
+
└
|
|
65
|
+
◆ API host
|
|
66
|
+
│ https://openrouter.ai/api/v1
|
|
67
|
+
└
|
|
68
|
+
Fetching models from provider…
|
|
69
|
+
◆ Model
|
|
70
|
+
│ ● openai/gpt-4o
|
|
71
|
+
└
|
|
72
|
+
✓ Provider / host / model saved — session active
|
|
73
|
+
```
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
Shortcuts:
|
|
76
|
+
- `/key openrouter sk-…` — saves the key, then continues into the same host → discover → model steps
|
|
77
|
+
- `/config` — read-only status (provider, model, endpoint, which keys are set)
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
❯ /config base_url http://AsobaCorp-1.local:11434/v1
|
|
77
|
-
❯ /model ollama/deepseek-coder-v2:latest
|
|
78
|
-
```
|
|
79
|
+
Local Ollama / LM Studio: pick that provider in `/model`, accept or edit the host, then choose from the discovered catalog (or enter a custom name if discovery fails).
|
|
79
80
|
|
|
80
81
|
---
|
|
81
82
|
|
|
@@ -326,9 +327,9 @@ Place the script at `lib/scripts/my-new-tool.py`. It will receive arguments inte
|
|
|
326
327
|
| Command | Description |
|
|
327
328
|
|---|---|
|
|
328
329
|
| `/help` | Display available commands |
|
|
329
|
-
| `/model
|
|
330
|
-
| `/key` | Save
|
|
331
|
-
| `/config` |
|
|
330
|
+
| `/model` | Configure provider → API key → host → discover models → select (single wizard) |
|
|
331
|
+
| `/key [provider] [value]` | Save API key then continue the configure wizard (`openrouter`, `zhipu`, `openai`, or Nehanda default) |
|
|
332
|
+
| `/config` | Show provider, model, endpoint, and key status (read-only) |
|
|
332
333
|
| `/mcp` | Manage MCP server connections (see below) |
|
|
333
334
|
| `/clear` | Clear conversation history and reset transcript state |
|
|
334
335
|
| `/retry` | Resend the last failed request |
|
|
@@ -342,10 +343,10 @@ Place the script at `lib/scripts/my-new-tool.py`. It will receive arguments inte
|
|
|
342
343
|
| `/mcp list` | Connect to each server and enumerate available tools |
|
|
343
344
|
| `/mcp reload [server]` | Re-read `mcp.json` and bust the tool cache (optionally for one server) |
|
|
344
345
|
| `/mcp add <name> <command> [args…]` | Register a new server and save it to `~/.config/nehanda/mcp.json` |
|
|
345
|
-
| `/mcp env` | Show environment variables for all configured servers |
|
|
346
|
+
| `/mcp env` | Show environment variables for all configured servers (values masked) |
|
|
346
347
|
| `/mcp env <server>` | Show environment variables for a specific server |
|
|
347
|
-
| `/mcp env <server> <
|
|
348
|
-
| `/mcp env <server> <
|
|
348
|
+
| `/mcp env <server> <NAME> <VALUE>` | Set an environment variable (e.g. an API key) |
|
|
349
|
+
| `/mcp env unset <server> <NAME>` | Remove an environment variable (`clear` is also accepted) |
|
|
349
350
|
|
|
350
351
|
---
|
|
351
352
|
|
|
@@ -387,18 +388,18 @@ Many MCP servers require an API key or other secrets. Rather than editing `mcp.j
|
|
|
387
388
|
```
|
|
388
389
|
❯ /mcp env # show env vars for all servers
|
|
389
390
|
❯ /mcp env asoba # show env vars for 'asoba' server
|
|
390
|
-
❯ /mcp env asoba ASOBA_API_KEY sk-abc123... # set the key
|
|
391
|
+
❯ /mcp env asoba ASOBA_API_KEY sk-abc123... # set the key (NAME + VALUE)
|
|
391
392
|
✓ Set ASOBA_API_KEY = ******** on asoba
|
|
392
393
|
Config file: ~/.config/nehanda/mcp.json
|
|
393
394
|
```
|
|
394
395
|
|
|
395
396
|
This writes the value to the `mcp.json` file where the server is defined (project-local `./mcp.json` takes priority over global `~/.config/nehanda/mcp.json`), then automatically reloads the server so the change takes effect immediately.
|
|
396
397
|
|
|
397
|
-
To
|
|
398
|
+
To remove a key (explicit unset — omitting VALUE no longer clears):
|
|
398
399
|
|
|
399
400
|
```
|
|
400
|
-
❯ /mcp env asoba ASOBA_API_KEY
|
|
401
|
-
✓
|
|
401
|
+
❯ /mcp env unset asoba ASOBA_API_KEY
|
|
402
|
+
✓ Unset ASOBA_API_KEY on asoba
|
|
402
403
|
```
|
|
403
404
|
|
|
404
405
|
**Tip:** Keys are masked in `/mcp env` output. Only the first 6 and last 4 characters are shown.
|
|
@@ -497,6 +498,12 @@ node tests/unit/ui_legibility_behavioral.mjs
|
|
|
497
498
|
|
|
498
499
|
## Changelog
|
|
499
500
|
|
|
501
|
+
### 0.8.0
|
|
502
|
+
|
|
503
|
+
- **Full `/help` + explicit MCP env unset (`lib/slashCommands.mjs`, `bin/nehanda-ui.mjs`):** `/help` lists every command and subcommand (`/key` variants, full `/mcp` surface). Omitting a VALUE no longer silently clears env vars — use `/mcp env unset <server> <NAME>` (or `clear`). Autocomplete stays top-level unless you type `/mcp` or `/key`. Behavioral coverage in `tests/unit/help_commands_behavioral.mjs`. Closes #9.
|
|
504
|
+
|
|
505
|
+
- **Unified `/model` configure wizard with keyed discovery (`lib/configureProvider.mjs`, `lib/modelDiscovery.mjs`, `bin/nehanda-ui.mjs`):** Provider setup is a single Goose-style flow — provider → API key → host → live model catalog → select → persist. OpenRouter / OpenAI-compat / Zhipu / Nehanda / LM Studio discover via authenticated `GET {base}/models`; Ollama keeps `/api/tags`. Discovery failure falls back to custom model name. `/key <provider>` chains into the same wizard tail. `/config` is read-only status. Behavioral coverage in `tests/unit/configure_provider_behavioral.mjs`. Closes #8.
|
|
506
|
+
|
|
500
507
|
### 0.6.1
|
|
501
508
|
|
|
502
509
|
- **Fix: Laya provisioning smoke test (`lib/scripts/ensure-laya-env.sh`):** The `Router()` constructibility check was missing `import sys`, causing `npm install` to fail with `NameError: name 'sys' is not defined` even when all packages installed successfully. The marker file was never written, leaving the CLI in a permanently broken state despite a complete venv. Fixed.
|
package/bin/agent.mjs
CHANGED
|
@@ -20,7 +20,8 @@ import { getPhase, canTransition, setPhase } from '../lib/workflow.mjs'
|
|
|
20
20
|
import { executeBuiltinTool, invalidateMcpToolCache } from '../lib/tools.mjs'
|
|
21
21
|
import { appendEntry, makeUserPayload, makeToolResultPayload } from '../lib/transcript.mjs'
|
|
22
22
|
import { loadInstructions } from '../lib/instructions.mjs'
|
|
23
|
-
import {
|
|
23
|
+
import { discoverModelsForProvider, formatModelList } from '../lib/modelDiscovery.mjs'
|
|
24
|
+
import { parseMcpEnvParts } from '../lib/slashCommands.mjs'
|
|
24
25
|
import { compactConversation } from '../lib/compact.mjs'
|
|
25
26
|
import { createTeam, getTeam, listTeams, deleteTeam } from '../lib/team.mjs'
|
|
26
27
|
import { ensureLayaReady } from '../lib/laya_sidecar.mjs'
|
|
@@ -165,16 +166,19 @@ Tools:
|
|
|
165
166
|
/team Manage teams
|
|
166
167
|
|
|
167
168
|
Session:
|
|
168
|
-
/model
|
|
169
|
-
/models Discover available models
|
|
169
|
+
/model Configure provider → key → host → model
|
|
170
|
+
/models Discover available models for active provider
|
|
171
|
+
/key Provider API keys (see /help in nehanda TUI for variants)
|
|
170
172
|
/login Authenticate
|
|
171
173
|
/logout Clear credentials
|
|
172
174
|
/status Auth status
|
|
173
175
|
/config Settings (try /config set <key> <value>)
|
|
174
|
-
/mcp MCP
|
|
175
|
-
/mcp list List
|
|
176
|
-
/mcp status Show configured
|
|
177
|
-
/mcp env
|
|
176
|
+
/mcp MCP guide (list · status · reload · add · env)
|
|
177
|
+
/mcp list List tools from configured MCP servers
|
|
178
|
+
/mcp status Show configured servers
|
|
179
|
+
/mcp env Show env vars (masked)
|
|
180
|
+
/mcp env <server> <NAME> <VALUE> Set env var
|
|
181
|
+
/mcp env unset <server> <NAME> Remove env var
|
|
178
182
|
/clear New conversation
|
|
179
183
|
/exit Quit`)
|
|
180
184
|
return
|
|
@@ -259,25 +263,26 @@ Session:
|
|
|
259
263
|
app.addSystemMessage('No provider configured. Use /model to set a provider first.')
|
|
260
264
|
return
|
|
261
265
|
}
|
|
262
|
-
|
|
266
|
+
|
|
263
267
|
if (!supportsDiscovery(provider)) {
|
|
264
|
-
app.addSystemMessage(`Model discovery is not supported for provider: ${provider}\n\
|
|
268
|
+
app.addSystemMessage(`Model discovery is not supported for provider: ${provider}\n\nUse /model to configure a discoverable provider.`)
|
|
265
269
|
return
|
|
266
270
|
}
|
|
267
|
-
|
|
268
|
-
// Get base URL for the provider
|
|
271
|
+
|
|
269
272
|
const baseUrl = getBaseUrlForProvider(provider, settings)
|
|
270
|
-
|
|
273
|
+
const apiKey = settings.model_config?.api_key
|
|
274
|
+
|| (provider === 'zhipu' ? resolveZhipuApiKey() : resolveOpenAiCompatKey())
|
|
275
|
+
|
|
271
276
|
app.addSystemMessage(`Discovering models from ${baseUrl}...`)
|
|
272
|
-
|
|
277
|
+
|
|
273
278
|
try {
|
|
274
|
-
const result = await
|
|
275
|
-
|
|
279
|
+
const result = await discoverModelsForProvider(provider, { baseUrl, apiKey, timeout: 5000 })
|
|
280
|
+
|
|
276
281
|
if (!result.success) {
|
|
277
|
-
app.addSystemMessage(`\nDiscovery failed: ${result.error}\n\nTroubleshooting:\n •
|
|
282
|
+
app.addSystemMessage(`\nDiscovery failed: ${result.error}\n\nTroubleshooting:\n • Check the API key (/key)\n • Check the base URL in settings: /config\n • Verify network connectivity to ${baseUrl}`)
|
|
278
283
|
return
|
|
279
284
|
}
|
|
280
|
-
|
|
285
|
+
|
|
281
286
|
const formatted = formatModelList(result.models)
|
|
282
287
|
app.addSystemMessage(`\n${formatted}`)
|
|
283
288
|
} catch (error) {
|
|
@@ -530,11 +535,14 @@ Session:
|
|
|
530
535
|
if (!sub || sub === 'help') {
|
|
531
536
|
const lines = [
|
|
532
537
|
'MCP Server & Tool Commands:',
|
|
533
|
-
' /mcp list
|
|
534
|
-
' /mcp status
|
|
535
|
-
' /mcp reload [server]
|
|
536
|
-
' /mcp add <name> <
|
|
537
|
-
' /mcp env
|
|
538
|
+
' /mcp list List tools from configured MCP servers',
|
|
539
|
+
' /mcp status Show configured servers',
|
|
540
|
+
' /mcp reload [server] Reload mcp.json into this session',
|
|
541
|
+
' /mcp add <name> <command> [args…] Register a server',
|
|
542
|
+
' /mcp env Show env vars for every server (values masked)',
|
|
543
|
+
' /mcp env <server> Show env vars for one server',
|
|
544
|
+
' /mcp env <server> <NAME> <VALUE> Set env var e.g. /mcp env jupyter JUPYTER_TOKEN sk-…',
|
|
545
|
+
' /mcp env unset <server> <NAME> Remove an env var',
|
|
538
546
|
'',
|
|
539
547
|
`Configured servers: ${serverNames.length ? serverNames.join(', ') : 'none'}`,
|
|
540
548
|
'Tip: Run /mcp list to inspect tools injected into the assistant.',
|
|
@@ -618,10 +626,12 @@ Session:
|
|
|
618
626
|
}
|
|
619
627
|
|
|
620
628
|
if (sub === 'env') {
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
629
|
+
const parsed = parseMcpEnvParts(parts.slice(1))
|
|
630
|
+
if (parsed.action === 'usage_error') {
|
|
631
|
+
app.addSystemMessage(parsed.error)
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
if (parsed.action === 'list_all') {
|
|
625
635
|
const mcpServers = settings?.mcp_servers || {}
|
|
626
636
|
const names = Object.keys(mcpServers)
|
|
627
637
|
if (!names.length) { app.addSystemMessage('No MCP servers configured.'); return }
|
|
@@ -644,13 +654,15 @@ Session:
|
|
|
644
654
|
app.addSystemMessage(lines.join('\n'))
|
|
645
655
|
return
|
|
646
656
|
}
|
|
647
|
-
if (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (!cfg) { app.addSystemMessage(`Server "${srvName}" not found. Use /mcp status to see configured servers.`); return }
|
|
657
|
+
if (parsed.action === 'list_server') {
|
|
658
|
+
const cfg = (settings?.mcp_servers || {})[parsed.server]
|
|
659
|
+
if (!cfg) { app.addSystemMessage(`Server "${parsed.server}" not found. Use /mcp status to see configured servers.`); return }
|
|
651
660
|
const envKeys = Object.keys(cfg.env || {})
|
|
652
|
-
if (!envKeys.length) {
|
|
653
|
-
|
|
661
|
+
if (!envKeys.length) {
|
|
662
|
+
app.addSystemMessage(`${parsed.server}: no env vars set\nUse /mcp env ${parsed.server} <NAME> <VALUE> to set one.`)
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
const lines = [`${parsed.server} environment variables:`]
|
|
654
666
|
for (const k of envKeys) {
|
|
655
667
|
const v = cfg.env[k]
|
|
656
668
|
const masked = v ? v.slice(0, 6) + '…' + v.slice(-4) : '(empty)'
|
|
@@ -659,25 +671,25 @@ Session:
|
|
|
659
671
|
app.addSystemMessage(lines.join('\n'))
|
|
660
672
|
return
|
|
661
673
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
674
|
+
if (parsed.action === 'set' || parsed.action === 'unset') {
|
|
675
|
+
const value = parsed.action === 'unset' ? '' : parsed.value
|
|
676
|
+
const result = updateMcpServerEnv(parsed.server, parsed.name, value, cwd)
|
|
677
|
+
if (!result.ok) {
|
|
678
|
+
app.addSystemMessage(`✗ ${result.error}`)
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
settings.mcp_servers = {}
|
|
682
|
+
applyMcpConfig(settings, cwd)
|
|
683
|
+
invalidateMcpToolCache()
|
|
684
|
+
const action = parsed.action === 'set' ? 'Set' : 'Unset'
|
|
685
|
+
const display = parsed.action === 'set' ? ' = ********' : ''
|
|
686
|
+
const relPath = path.relative(os.homedir(), result.path).startsWith('..') ? result.path : '~/' + path.relative(os.homedir(), result.path)
|
|
687
|
+
app.addSystemMessage(`✓ ${action} ${parsed.name}${display} on ${parsed.server}\n Config file: ${relPath}`)
|
|
667
688
|
return
|
|
668
689
|
}
|
|
669
|
-
// Reload to pick up the change
|
|
670
|
-
settings.mcp_servers = {}
|
|
671
|
-
applyMcpConfig(settings, cwd)
|
|
672
|
-
invalidateMcpToolCache()
|
|
673
|
-
const action = result.set ? 'Set' : 'Cleared'
|
|
674
|
-
const display = result.set ? ' = ********' : ''
|
|
675
|
-
const relPath = path.relative(os.homedir(), result.path).startsWith('..') ? result.path : '~/' + path.relative(os.homedir(), result.path)
|
|
676
|
-
app.addSystemMessage(`✓ ${action} ${envKey}${display} on ${srvName}\n Config file: ${relPath}`)
|
|
677
|
-
return
|
|
678
690
|
}
|
|
679
691
|
|
|
680
|
-
app.addSystemMessage('/mcp sub-commands: status · list · reload
|
|
692
|
+
app.addSystemMessage('/mcp sub-commands: status · list · reload · add · env · env unset — type /help for full list')
|
|
681
693
|
return
|
|
682
694
|
}
|
|
683
695
|
|
|
@@ -842,32 +854,31 @@ async function mainPipe(opts) {
|
|
|
842
854
|
io.println(ui.colors.error(' No provider configured. Use /model to set a provider first.'))
|
|
843
855
|
continue
|
|
844
856
|
}
|
|
845
|
-
|
|
857
|
+
|
|
846
858
|
if (!supportsDiscovery(provider)) {
|
|
847
859
|
io.println(ui.colors.error(` Model discovery is not supported for provider: ${provider}`))
|
|
848
|
-
io.println(`\n ${ui.colors.
|
|
849
|
-
io.println(' ollama')
|
|
850
|
-
io.println(`\n ${ui.colors.dim('For other providers, use /model <provider>/<model-name> directly.')}`)
|
|
860
|
+
io.println(`\n ${ui.colors.dim('Use /model to configure a discoverable provider.')}`)
|
|
851
861
|
continue
|
|
852
862
|
}
|
|
853
|
-
|
|
854
|
-
// Get base URL for the provider
|
|
863
|
+
|
|
855
864
|
const baseUrl = getBaseUrlForProvider(provider, settings)
|
|
856
|
-
|
|
865
|
+
const apiKey = settings.model_config?.api_key
|
|
866
|
+
|| (provider === 'zhipu' ? resolveZhipuApiKey() : resolveOpenAiCompatKey())
|
|
867
|
+
|
|
857
868
|
io.println(`\n Discovering models from ${baseUrl}...\n`)
|
|
858
|
-
|
|
869
|
+
|
|
859
870
|
try {
|
|
860
|
-
const result = await
|
|
861
|
-
|
|
871
|
+
const result = await discoverModelsForProvider(provider, { baseUrl, apiKey, timeout: 5000 })
|
|
872
|
+
|
|
862
873
|
if (!result.success) {
|
|
863
874
|
io.println(ui.colors.error(` Discovery failed: ${result.error}`))
|
|
864
875
|
io.println(`\n ${ui.colors.header('Troubleshooting:')}`)
|
|
865
|
-
io.println(' •
|
|
876
|
+
io.println(' • Check the API key (/key)')
|
|
866
877
|
io.println(' • Check the base URL in settings: /config')
|
|
867
878
|
io.println(` • Verify network connectivity to ${baseUrl}\n`)
|
|
868
879
|
continue
|
|
869
880
|
}
|
|
870
|
-
|
|
881
|
+
|
|
871
882
|
const formatted = formatModelList(result.models)
|
|
872
883
|
io.println(formatted)
|
|
873
884
|
} catch (error) {
|