@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 CHANGED
@@ -51,31 +51,32 @@ ona-code
51
51
 
52
52
  ### 3. Provider Setup
53
53
 
54
- #### Option A: Nehanda Cloud (Default)
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
- #### Option C: Remote Ollama over LAN
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
- To connect to an Ollama instance running on your network:
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 [name]` | Discover and switch active provider or model endpoint |
330
- | `/key` | Save Nehanda API key |
331
- | `/config` | View or set settings (e.g., `/config base_url <url>`) |
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> <KEY> <value>` | Set an environment variable (e.g. an API key) |
348
- | `/mcp env <server> <KEY>` | Clear an environment variable |
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 clear a key:
398
+ To remove a key (explicit unset — omitting VALUE no longer clears):
398
399
 
399
400
  ```
400
- ❯ /mcp env asoba ASOBA_API_KEY
401
- ✓ Cleared ASOBA_API_KEY on asoba
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 { discoverOllamaModels, formatModelList } from '../lib/modelDiscovery.mjs'
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 [name] Change 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 server management & guide
175
- /mcp list List all discovered tools & descriptions
176
- /mcp status Show configured MCP servers
177
- /mcp env Set MCP env vars (API keys)
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\nProviders with discovery support:\n ollama\n\nFor other providers, use /model <provider>/<model-name> directly.`)
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 discoverOllamaModels(baseUrl, { timeout: 5000 })
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 • Make sure Ollama is running: ollama serve\n • Check the base URL in settings: /config\n • Verify network connectivity to ${baseUrl}`)
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 Discover and list all available MCP tools',
534
- ' /mcp status Show configured servers and command paths',
535
- ' /mcp reload [server] Reload mcp.json configs into session',
536
- ' /mcp add <name> <cmd> […] Register a new MCP server',
537
- ' /mcp env [server] [K] [V] View or set server environment variables',
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 srvName = parts[2]
622
- const envKey = parts[3]
623
- if (!srvName) {
624
- // Show env for all servers
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 (!envKey) {
648
- // Show env for a specific server
649
- const cfg = (settings?.mcp_servers || {})[srvName]
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) { app.addSystemMessage(`${srvName}: no env vars set\nUse /mcp env ${srvName} <KEY> <value> to set one.`); return }
653
- const lines = [`${srvName} environment variables:`]
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
- // Set or clear an env var
663
- const value = parts.slice(4).join(' ')
664
- const result = updateMcpServerEnv(srvName, envKey, value, cwd)
665
- if (!result.ok) {
666
- app.addSystemMessage(`✗ ${result.error}`)
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 [server] · add <name> <command> [args…] · env [server] [KEY] [value]')
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.header('Providers with discovery support:')}`)
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 discoverOllamaModels(baseUrl, { timeout: 5000 })
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(' • Make sure Ollama is running: ollama serve')
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) {