@asobacloud/nehanda 0.7.0 → 0.9.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
@@ -1,6 +1,6 @@
1
1
  # Nehanda Command-Line Interface (`nehanda-cli`)
2
2
 
3
- An agentic terminal REPL and single-process engine built for energy systems research and software development. `nehanda-cli` connects directly to our flagship [Nehanda v3](https://huggingface.co/asoba/nehanda-v3-27b), normalizes multi-OEM energy telemetry through the ODSE protocol, and enforces a governed development workflow across the full research-to-production lifecycle. It also connects to local or cloud-based Ollama models, LM Studio instances, or any OpenAI-compatible API.
3
+ An agentic terminal REPL and single-process engine built for energy systems research and software development. `nehanda-cli` works with Ollama, LM Studio, OpenRouter, Anthropic Claude, ZhipuAI, or any OpenAI-compatible API — local or cloud. It normalizes multi-OEM energy telemetry through the ODSE protocol and enforces a governed development workflow across the full research-to-production lifecycle.
4
4
 
5
5
  Every conversation turn, tool call, phase transition, and permission check is stored in a local, queryable SQLite database you own, ensuring complete transcripts exist for auditing and debugging.
6
6
 
@@ -94,7 +94,7 @@ Local Ollama / LM Studio: pick that provider in `/model`, accept or edit the hos
94
94
 
95
95
  * **Dynamic Tool Rescue (`[TOOL_CALL]`):** Native support for endpoints that strip OpenAI tool schemas (such as `nehandaMlProxy`). The engine dynamically injects active tool schemas directly into system prompts as `[TOOL_CALL]` blocks, parsing and executing tools locally without server-side function-calling support. Uses `[TOOL_CALL]` delimiters instead of `<tool_call>` XML to prevent vLLM's `--tool-call-parser qwen3_xml` stop-token interception.
96
96
 
97
- * **Deterministic SDLC Workflow:** Enforces a 6-phase state machine (`idle` → `plan` → `implement` → `test` → `verify` → `done`) to prevent unapproved code changes, hallucinated test passes, or unverified implementations.
97
+ * **Deterministic SDLC Workflow:** Enforces a 7-phase state machine (`idle` → `explore` → `planning` → `test` → `implement` → `test` → `verify` → `done`) following the AGENTS.md TDD mandate. Transitions are gated: `planning → implement` is blocked until an approved plan exists and at least one RED behavioral test is recorded; `test → verify` requires all tests passing and safety checkers clean.
98
98
 
99
99
  * **Interactive Safety Interventions:** When execution checkers flag a command — opening local sockets, inspecting HIL testbed hardware paths, or writing outside the working directory — the engine raises a structured `{ status, reason, risk_score, proposed_remediation }` payload rather than crashing. In the TUI, blocked operations surface a `BlockConfirmMenu` (`[A] Approve for Session | [S] Run in Laya Sandbox | [C] Cancel`) wired directly into the Ink event loop. Session approvals are cached so the same operation is not re-prompted within a session. Headless pipe mode auto-denies without prompting.
100
100
 
@@ -102,7 +102,7 @@ Local Ollama / LM Studio: pick that provider in `/model`, accept or edit the hos
102
102
 
103
103
  * **Runtime Visibility:** `RuntimeHealthMonitor` probes active inference endpoints (1s HEAD request) and checks Laya sidecar liveness before each session. The TUI status header shows active model, endpoint health and latency, sidecar PID, and context usage percentage — so researchers have immediate visual confirmation of whether the target backend is an internal local host or an external cloud endpoint before uploading grid telemetry.
104
104
 
105
- * **Process Legibility:** Every tool call is visible while it runs. Tool-start lines show argument summaries (`⚙ Read(ui.mjs)`, `⚙ Bash(git status)`), a live spinner with plain-English verbs tracks activity between outputs (`⠋ Analyzing…`, `⠙ Reading ui.mjs…`), and tool results carry honest context suffixes (`✓ Read: … (N lines)`, `✓ Bash: … (240ms)`, `✓ Write: … (N chars)`) — never fabricated data. A dim `esc to cancel` hint stays available while a turn runs, and result previews are capped at 120 characters so raw output never floods the screen. Backed by `summarizeToolArgs`, `verbForTool`, `formatToolResultSuffix`, and `TOOL_VERBS` in `lib/ui.mjs`, with a live spinner channel (`bridge.updateSpinner`) in the Ink TUI.
105
+ * **Process Legibility:** Every tool call is visible while it runs. Tool-start lines show argument summaries (`⚙ Read(ui.mjs)`, `⚙ Bash(git status)`), a live spinner with plain-English verbs tracks activity between outputs (`⠋ Analyzing…`, `⠙ Reading ui.mjs…`), and tool results carry honest context suffixes (`✓ Read: … (N lines)`, `✓ Bash: … (240ms)`, `✓ Write: … (N chars)`) — never fabricated data. When a reasoning model (Qwen3, Gemma) is thinking, the spinner shows live token progress (`⠋ Thinking (42 tokens)…`) rather than a frozen screen. Pressing Esc immediately updates the spinner to `Cancelling…` before the in-flight request is torn down. A dim `esc to cancel` hint stays available while a turn runs, and result previews are capped at 120 characters so raw output never floods the screen. Backed by `summarizeToolArgs`, `verbForTool`, `formatToolResultSuffix`, and `TOOL_VERBS` in `lib/ui.mjs`, with a live spinner channel (`bridge.updateSpinner`) in the Ink TUI.
106
106
 
107
107
  * **Interactive TUI & Pipe Support:** Rich Ink-based TUI (`bin/nehanda-ui.mjs`) for interactive development sessions, with headless pipe-mode support (`bin/agent.mjs`) for acceptance testing and automation.
108
108
 
@@ -343,10 +343,10 @@ Place the script at `lib/scripts/my-new-tool.py`. It will receive arguments inte
343
343
  | `/mcp list` | Connect to each server and enumerate available tools |
344
344
  | `/mcp reload [server]` | Re-read `mcp.json` and bust the tool cache (optionally for one server) |
345
345
  | `/mcp add <name> <command> [args…]` | Register a new server and save it to `~/.config/nehanda/mcp.json` |
346
- | `/mcp env` | Show environment variables for all configured servers |
346
+ | `/mcp env` | Show environment variables for all configured servers (values masked) |
347
347
  | `/mcp env <server>` | Show environment variables for a specific server |
348
- | `/mcp env <server> <KEY> <value>` | Set an environment variable (e.g. an API key) |
349
- | `/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) |
350
350
 
351
351
  ---
352
352
 
@@ -388,18 +388,18 @@ Many MCP servers require an API key or other secrets. Rather than editing `mcp.j
388
388
  ```
389
389
  ❯ /mcp env # show env vars for all servers
390
390
  ❯ /mcp env asoba # show env vars for 'asoba' server
391
- ❯ /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)
392
392
  ✓ Set ASOBA_API_KEY = ******** on asoba
393
393
  Config file: ~/.config/nehanda/mcp.json
394
394
  ```
395
395
 
396
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.
397
397
 
398
- To clear a key:
398
+ To remove a key (explicit unset — omitting VALUE no longer clears):
399
399
 
400
400
  ```
401
- ❯ /mcp env asoba ASOBA_API_KEY
402
- ✓ Cleared ASOBA_API_KEY on asoba
401
+ ❯ /mcp env unset asoba ASOBA_API_KEY
402
+ ✓ Unset ASOBA_API_KEY on asoba
403
403
  ```
404
404
 
405
405
  **Tip:** Keys are masked in `/mcp env` output. Only the first 6 and last 4 characters are shown.
@@ -430,19 +430,23 @@ Numeric values are auto-converted. Changes persist in the local settings databas
430
430
 
431
431
  ## SDLC Workflow
432
432
 
433
- The engine enforces state transitions across six distinct phases:
433
+ The engine enforces state transitions across seven distinct phases, aligned with the AGENTS.md TDD mandate:
434
434
 
435
- 1. **`idle`**: Discovery and triage. Mutating file tools are physically masked out. Safety and analysis tools are available on-demand.
435
+ 1. **`idle`**: Discovery and triage. Mutating file tools are available. Safety and analysis tools are available on-demand.
436
436
 
437
- 2. **`plan`**: Model formulates success criteria and implementation steps (`EnterPlanMode`). No tools available.
437
+ 2. **`explore`**: Codebase exploration only. `lib/explore.mjs` drives this phase — the model uses Glob, Grep, and Read to aggregate file summaries before entering planning. Ends when the model outputs `Exploration complete.`
438
438
 
439
- 3. **`implement`**: Code changes applied using file editing and shell execution (`ExitPlanMode`).
439
+ 3. **`planning`**: Model formulates a plan using exploration summaries (`EnterPlanMode`). Mutating tools are blocked until a plan is approved. No code changes permitted.
440
440
 
441
- 4. **`test`**: Automated test generation and execution (`SubmitImplementation`). **After tests pass, all tools marked `mandatory_in: ["test"]` must be run.** The system prompt enforces this — the model cannot declare success until all mandatory safety checkers pass.
441
+ 4. **`test`**: Behavioral test creation and RED confirmation. The model writes tests and runs them to confirm they fail for the diagnosed reason. **Gated:** `planning → implement` is blocked until at least one RED test result is recorded here.
442
442
 
443
- 5. **`verify`**: Inspection of test outputs and coverage verification (`SubmitTest`).
443
+ 5. **`implement`**: Code changes applied using file editing and shell execution (`ExitPlanMode`). Only reachable after a confirmed RED test exists and a plan is approved.
444
444
 
445
- 6. **`done`**: Final sign-off and git commit creation.
445
+ 6. **`test`**: Automated test execution to verify GREEN (`SubmitImplementation`). **All tools marked `mandatory_in: ["test"]` must run and pass** before this phase can advance — the system prompt enforces this.
446
+
447
+ 7. **`verify`**: Inspection of test outputs and coverage verification (`SubmitTest`). **Gated:** requires all test results passing and at least one safety checker event recorded.
448
+
449
+ 8. **`done`**: Final sign-off and git commit creation.
446
450
 
447
451
  ---
448
452
 
@@ -494,11 +498,40 @@ node tests/unit/gap4_pinned_compaction_behavioral.mjs
494
498
  node tests/unit/ui_legibility_behavioral.mjs
495
499
  ```
496
500
 
501
+ Run the Ollama, spinner, and Laya behavioral suites:
502
+
503
+ ```bash
504
+ node tests/unit/test-ollama-llm-call-behavioral.mjs
505
+ node tests/unit/test-ollama-permission-prompt-behavioral.mjs
506
+ node tests/unit/laya_warm_load_behavioral.mjs
507
+ node tests/unit/issue22_query_context_behavioral.mjs
508
+ ```
509
+
497
510
  ---
498
511
 
499
512
  ## Changelog
500
513
 
501
- ### 0.7.0
514
+ ### 0.9.0
515
+
516
+ - **AGENTS.md SDLC workflow + local issue tracking (`lib/workflow.mjs`, `lib/issues.mjs`, `lib/explore.mjs`):** The orchestrator now enforces the full E-P-C-V-D workflow defined in AGENTS.md — Explore, Plan, Confirm, Code, Validate. A local `.issues/` directory tracks open and closed issues with structured metadata (observed behavior, diagnosis, acceptance criteria). `lib/explore.mjs` drives the EXPLORE phase, aggregating file summaries before entering PLANNING. Closes #15.
517
+
518
+ - **Generic agent config restored to `~/.config/nehanda` (`lib/settings.mjs`, `scripts/postinstall.mjs`):** Removed the `.aimee` dependency and all references to the legacy agent registry. Agent configuration now lives exclusively at `~/.config/nehanda/agents.json`, consistent with the documented config file locations. Closes #16.
519
+
520
+ - **Working directory in welcome banner and footer (`bin/nehanda-ui.mjs`):** The TUI banner displayed on startup and the status footer both now show the current working directory, giving researchers immediate confirmation of which project root is active before issuing any commands. Closes #17.
521
+
522
+ - **Spinner stabilization: AskPrompt, spitfire suppression, null-safe `askHuman` (`bin/nehanda-ui.mjs`):** Fixed three related spinner regressions: (1) the spinner was visible and cycling behind interactive `io.ask` permission prompts, obscuring the question; (2) rapid `bridge.updateSpinner` calls between tool results caused visible label flicker (spitfire); (3) `askHuman` threw on a null `io` reference in headless contexts. Closes #18.
523
+
524
+ - **Ollama LLM call path: behavioral coverage + documentation (`lib/orchestrate.mjs`, `tests/unit/test-ollama-llm-call-behavioral.mjs`, `docs/MAP.md`):** Full behavioral RED/GREEN test suite for the Ollama call path — `detectOllamaToolSupport` probe logic (capabilities, template, modelfile fields, HTTP error fail-open), native vs. manual tool injection dispatch, `Authorization: Bearer ollama` header, `OLLAMA_BASE_URL` env var resolution, and `buildSystemPrompt` injection format. `docs/MAP.md` documents the full call path contract. Closes #19.
525
+
526
+ - **Turn-start and probe spinner stabilization (`bin/nehanda-ui.mjs`, `lib/orchestrate.mjs`):** Prior to this fix, the spinner had no label during the gap between turn submission and the first model event — cycling generic `TOOL_VERBS` while Laya intent classification and the Ollama `detectOllamaToolSupport` probe (up to 3 s) ran silently. Fixed by pre-seeding `bridge.updateSpinner('Analyzing')` immediately on submit and `io.spinner.start('Analyzing')` at `runSdlcOpenAICompat` entry. Behavioral coverage in `tests/unit/test-ollama-permission-prompt-behavioral.mjs`. Closes #20.
527
+
528
+ - **Laya sidecar startup warming (`lib/laya_sidecar.mjs`):** `ensureLayaReady()` now calls `loadLaya()` (`method: 'load'`) instead of `pingLaya()` (`method: 'ping'`). The `ping` path responded immediately without loading model weights, leaving the first user-turn `classifyUserIntent` call to cold-load the 322M-parameter checkpoint mid-turn (200–400 ms stall). `load` invokes `_ensure_loaded()` in `laya_worker.py`, priming both `noul` and `choice` criteria into MPS/CPU RAM before the first turn. Behavioral coverage in `tests/unit/laya_warm_load_behavioral.mjs`. Closes #21.
529
+
530
+ - **Grounded query context, Ollama `think:false`, reasoning token streaming, instant Esc cancel (`lib/orchestrate.mjs`, `lib/openaiCompat.mjs`, `bin/nehanda-ui.mjs`):** Four improvements to reduce reasoning model overhead and improve cancellation UX: (1) `buildSystemPrompt` appends an explicit `[GROUNDED FACTS]` block (date, cwd, platform, shell) to every phase prompt so models never spend tokens debating their own awareness; (2) `streamOpenAIChatCompletion` now parses `delta.reasoning` and `delta.reasoning_content` SSE fields and updates the spinner live — `⠋ Thinking (42 tokens)…` — rather than sitting frozen during the thinking phase; (3) a `think: false` option is forwarded to the Ollama API request body when `model_config.thinking === false`, disabling chain-of-thought for supported models; (4) `detectOllamaToolSupport` now accepts `rt.signal` composed via `AbortSignal.any([signal, AbortSignal.timeout(3000)])` so Esc cancels the capability probe immediately rather than waiting the full 3 s timeout, and `bridge.abortCurrent` updates the spinner to `Cancelling…` before invoking `controller.abort()`. Behavioral coverage in `tests/unit/issue22_query_context_behavioral.mjs`. Closes #22.
531
+
532
+ ### 0.8.0
533
+
534
+ - **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.
502
535
 
503
536
  - **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.
504
537
 
@@ -539,7 +572,7 @@ node tests/unit/ui_legibility_behavioral.mjs
539
572
  ### 0.2.0
540
573
  - Jev-Mem memory architecture (System-1 Laya control plane)
541
574
  - Dynamic Tool Rescue (`[TOOL_CALL]` delimiter path)
542
- - Deterministic 6-phase SDLC workflow
575
+ - Deterministic SDLC workflow (expanded to 7 phases in 0.9.0)
543
576
  - Multi-provider switching
544
577
 
545
578
  ---
package/bin/agent.mjs CHANGED
@@ -21,6 +21,7 @@ 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
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
@@ -531,11 +535,14 @@ Session:
531
535
  if (!sub || sub === 'help') {
532
536
  const lines = [
533
537
  'MCP Server & Tool Commands:',
534
- ' /mcp list Discover and list all available MCP tools',
535
- ' /mcp status Show configured servers and command paths',
536
- ' /mcp reload [server] Reload mcp.json configs into session',
537
- ' /mcp add <name> <cmd> […] Register a new MCP server',
538
- ' /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',
539
546
  '',
540
547
  `Configured servers: ${serverNames.length ? serverNames.join(', ') : 'none'}`,
541
548
  'Tip: Run /mcp list to inspect tools injected into the assistant.',
@@ -619,10 +626,12 @@ Session:
619
626
  }
620
627
 
621
628
  if (sub === 'env') {
622
- const srvName = parts[2]
623
- const envKey = parts[3]
624
- if (!srvName) {
625
- // 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') {
626
635
  const mcpServers = settings?.mcp_servers || {}
627
636
  const names = Object.keys(mcpServers)
628
637
  if (!names.length) { app.addSystemMessage('No MCP servers configured.'); return }
@@ -645,13 +654,15 @@ Session:
645
654
  app.addSystemMessage(lines.join('\n'))
646
655
  return
647
656
  }
648
- if (!envKey) {
649
- // Show env for a specific server
650
- const cfg = (settings?.mcp_servers || {})[srvName]
651
- 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 }
652
660
  const envKeys = Object.keys(cfg.env || {})
653
- if (!envKeys.length) { app.addSystemMessage(`${srvName}: no env vars set\nUse /mcp env ${srvName} <KEY> <value> to set one.`); return }
654
- 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:`]
655
666
  for (const k of envKeys) {
656
667
  const v = cfg.env[k]
657
668
  const masked = v ? v.slice(0, 6) + '…' + v.slice(-4) : '(empty)'
@@ -660,25 +671,25 @@ Session:
660
671
  app.addSystemMessage(lines.join('\n'))
661
672
  return
662
673
  }
663
- // Set or clear an env var
664
- const value = parts.slice(4).join(' ')
665
- const result = updateMcpServerEnv(srvName, envKey, value, cwd)
666
- if (!result.ok) {
667
- 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}`)
668
688
  return
669
689
  }
670
- // Reload to pick up the change
671
- settings.mcp_servers = {}
672
- applyMcpConfig(settings, cwd)
673
- invalidateMcpToolCache()
674
- const action = result.set ? 'Set' : 'Cleared'
675
- const display = result.set ? ' = ********' : ''
676
- const relPath = path.relative(os.homedir(), result.path).startsWith('..') ? result.path : '~/' + path.relative(os.homedir(), result.path)
677
- app.addSystemMessage(`✓ ${action} ${envKey}${display} on ${srvName}\n Config file: ${relPath}`)
678
- return
679
690
  }
680
691
 
681
- 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')
682
693
  return
683
694
  }
684
695
 
@@ -1070,7 +1081,10 @@ function resolveModelArg(arg) {
1070
1081
 
1071
1082
  // First, try exact match in hardcoded WIRE map (backward compatibility)
1072
1083
  if (arg.includes('/')) {
1073
- const [p, m] = arg.split('/', 2)
1084
+ // Split on the FIRST "/" only — the model name may itself contain slashes (#12)
1085
+ const slashIdx = arg.indexOf('/')
1086
+ const p = arg.slice(0, slashIdx)
1087
+ const m = arg.slice(slashIdx + 1)
1074
1088
 
1075
1089
  // Validate provider and model_id are non-empty
1076
1090
  if (!p || !p.trim()) {