@kinqs/brainrouter-cli 0.3.6 → 0.3.7
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 +29 -52
- package/agents/architect.json +18 -0
- package/agents/explorer.json +18 -0
- package/agents/reviewer.json +18 -0
- package/agents/verifier.json +18 -0
- package/agents/worker.json +18 -0
- package/dist/agent/agent.d.ts +12 -1
- package/dist/agent/agent.js +134 -18
- package/dist/cli/banner.d.ts +20 -0
- package/dist/cli/banner.js +47 -14
- package/dist/cli/cliPrompt.d.ts +40 -3
- package/dist/cli/cliPrompt.js +52 -25
- package/dist/cli/commands/_context.d.ts +3 -1
- package/dist/cli/commands/_helpers.d.ts +1 -1
- package/dist/cli/commands/config.d.ts +46 -0
- package/dist/cli/commands/config.js +1042 -0
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.js +64 -0
- package/dist/cli/commands/login.d.ts +13 -0
- package/dist/cli/commands/login.js +179 -0
- package/dist/cli/commands/mcp.d.ts +13 -11
- package/dist/cli/commands/mcp.js +239 -74
- package/dist/cli/commands/orchestration.js +18 -0
- package/dist/cli/commands/ui.js +117 -58
- package/dist/cli/commands/workflow.d.ts +2 -0
- package/dist/cli/commands/workflow.js +54 -8
- package/dist/cli/ink/ChatApp.d.ts +206 -0
- package/dist/cli/ink/ChatApp.js +493 -0
- package/dist/cli/ink/Frame.d.ts +26 -0
- package/dist/cli/ink/Frame.js +5 -0
- package/dist/cli/ink/Picker.d.ts +65 -0
- package/dist/cli/ink/Picker.js +133 -0
- package/dist/cli/ink/SlashPalette.d.ts +51 -0
- package/dist/cli/ink/SlashPalette.js +136 -0
- package/dist/cli/ink/TextField.d.ts +34 -0
- package/dist/cli/ink/TextField.js +47 -0
- package/dist/cli/ink/WizardApp.d.ts +7 -0
- package/dist/cli/ink/WizardApp.js +422 -0
- package/dist/cli/ink/ambientChat.d.ts +34 -0
- package/dist/cli/ink/ambientChat.js +7 -0
- package/dist/cli/ink/consoleCapture.d.ts +11 -0
- package/dist/cli/ink/consoleCapture.js +33 -0
- package/dist/cli/ink/markdownRender.d.ts +41 -0
- package/dist/cli/ink/markdownRender.js +278 -0
- package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
- package/dist/cli/ink/renderWithResizeClear.js +33 -0
- package/dist/cli/ink/runChat.d.ts +34 -0
- package/dist/cli/ink/runChat.js +571 -0
- package/dist/cli/ink/runPicker.d.ts +31 -0
- package/dist/cli/ink/runPicker.js +139 -0
- package/dist/cli/ink/runSlashPalette.d.ts +23 -0
- package/dist/cli/ink/runSlashPalette.js +33 -0
- package/dist/cli/ink/runWizard.d.ts +22 -0
- package/dist/cli/ink/runWizard.js +133 -0
- package/dist/cli/ink/stdinHandoff.d.ts +51 -0
- package/dist/cli/ink/stdinHandoff.js +78 -0
- package/dist/cli/ink/toolFormat.d.ts +73 -0
- package/dist/cli/ink/toolFormat.js +180 -0
- package/dist/cli/ink/useTerminalSize.d.ts +35 -0
- package/dist/cli/ink/useTerminalSize.js +26 -0
- package/dist/cli/repl.d.ts +25 -3
- package/dist/cli/repl.js +43 -712
- package/dist/cli/slashSuggest.d.ts +32 -0
- package/dist/cli/slashSuggest.js +146 -0
- package/dist/cli/wizard/modelsApi.d.ts +72 -0
- package/dist/cli/wizard/modelsApi.js +166 -0
- package/dist/cli/wizard/picker.d.ts +202 -0
- package/dist/cli/wizard/picker.js +547 -0
- package/dist/cli/wizard/providers.d.ts +86 -0
- package/dist/cli/wizard/providers.js +190 -0
- package/dist/cli/wizard/runner.d.ts +13 -0
- package/dist/cli/wizard/runner.js +488 -0
- package/dist/cli/wizard/types.d.ts +122 -0
- package/dist/cli/wizard/types.js +109 -0
- package/dist/config/config.d.ts +12 -0
- package/dist/config/config.js +45 -3
- package/dist/index.js +148 -206
- package/dist/memory/briefing.d.ts +1 -1
- package/dist/memory/consolidation.d.ts +1 -1
- package/dist/orchestration/agentRegistry.d.ts +36 -0
- package/dist/orchestration/agentRegistry.js +64 -0
- package/dist/orchestration/orchestrator.d.ts +7 -0
- package/dist/orchestration/orchestrator.js +2 -0
- package/dist/orchestration/tools.d.ts +10 -1
- package/dist/orchestration/tools.js +48 -4
- package/dist/prompt/skillCatalog.d.ts +11 -0
- package/dist/prompt/skillCatalog.js +134 -0
- package/dist/prompt/skillRunner.d.ts +2 -2
- package/dist/prompt/skillRunner.js +2 -31
- package/dist/prompt/systemPrompt.js +5 -1
- package/dist/runtime/mcpClient.js +14 -11
- package/dist/runtime/mcpPool.d.ts +162 -0
- package/dist/runtime/mcpPool.js +423 -0
- package/dist/runtime/mcpUtils.d.ts +3 -1
- package/package.json +8 -2
- package/.env.example +0 -116
package/README.md
CHANGED
|
@@ -6,8 +6,6 @@ recall, skills, and capture.
|
|
|
6
6
|
|
|
7
7
|
Ships the `brainrouter` binary.
|
|
8
8
|
|
|
9
|
-
---
|
|
10
|
-
|
|
11
9
|
## Install
|
|
12
10
|
|
|
13
11
|
```bash
|
|
@@ -23,52 +21,44 @@ command not found`.
|
|
|
23
21
|
|
|
24
22
|
| How Node is installed | Use `sudo`? |
|
|
25
23
|
|---|---|
|
|
26
|
-
| Homebrew (`brew install node`) |
|
|
27
|
-
| nvm / asdf / fnm |
|
|
28
|
-
| System Node on macOS / Linux |
|
|
29
|
-
|
|
30
|
-
Check yours:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm config get prefix
|
|
34
|
-
```
|
|
24
|
+
| Homebrew (`brew install node`) | No — global prefix is user-writable |
|
|
25
|
+
| nvm / asdf / fnm | No — same reason |
|
|
26
|
+
| System Node on macOS / Linux | Yes — global prefix is `/usr/local/...` |
|
|
35
27
|
|
|
36
|
-
If the path is under `/Users/...`,
|
|
37
|
-
|
|
28
|
+
Check yours: `npm config get prefix`. If the path is under `/Users/...`,
|
|
29
|
+
`/opt/homebrew/...`, or your home dir — no sudo. If it's `/usr/local/...` — use sudo.
|
|
38
30
|
|
|
39
31
|
Verify the install:
|
|
40
32
|
|
|
41
33
|
```bash
|
|
42
34
|
which brainrouter # prints the path to the binary
|
|
43
|
-
brainrouter --version # prints 0.3.
|
|
35
|
+
brainrouter --version # prints 0.3.7
|
|
44
36
|
```
|
|
45
37
|
|
|
46
|
-
---
|
|
47
|
-
|
|
48
38
|
## Configure
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
### 1. The chat LLM and MCP server profile
|
|
40
|
+
Run `brainrouter` for the first time and the **setup wizard** starts
|
|
41
|
+
automatically:
|
|
53
42
|
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
brainrouter login # interactive — set MCP server URL + API key
|
|
43
|
+
```
|
|
44
|
+
Welcome → Theme → Provider → API key → Model → MCP → AGENT.md → Done
|
|
57
45
|
```
|
|
58
46
|
|
|
59
|
-
|
|
47
|
+
It writes everything to `~/.config/brainrouter/config.json` — no manual
|
|
48
|
+
file editing needed.
|
|
60
49
|
|
|
61
|
-
|
|
62
|
-
`http://localhost:1234/v1/chat/completions` or `http://localhost:11434/v1/chat/completions`.
|
|
50
|
+
To re-run the wizard later: type `/init` inside the REPL.
|
|
63
51
|
|
|
64
|
-
|
|
52
|
+
To change a single setting: use `/config <key> <value>` or the `/config`
|
|
53
|
+
home panel. To re-configure the MCP server connection: use `/login`.
|
|
65
54
|
|
|
66
|
-
|
|
67
|
-
or
|
|
68
|
-
this package for the full list. LLM credentials do **not** go here — they
|
|
69
|
-
live in `config.json`.
|
|
55
|
+
For local-model setups (LM Studio / Ollama), point the LLM endpoint at
|
|
56
|
+
`http://localhost:1234/v1/chat/completions` or `http://localhost:11434/v1/chat/completions`.
|
|
70
57
|
|
|
71
|
-
|
|
58
|
+
**Runtime knobs** (sandbox, trace log, web-search backend, tool-loop limits)
|
|
59
|
+
are set as shell environment variables. See
|
|
60
|
+
[`brainrouter-docs/configuration.md`](https://github.com/kinqsradiollc/BrainRouter/blob/main/brainrouter-docs/configuration.md)
|
|
61
|
+
for the full list.
|
|
72
62
|
|
|
73
63
|
## Run
|
|
74
64
|
|
|
@@ -83,22 +73,15 @@ Inside the REPL, type `/help` for the full slash-command list (60+
|
|
|
83
73
|
commands across session / memory / workflow / orchestration / observability
|
|
84
74
|
surfaces).
|
|
85
75
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
capture, and skills are disabled until the server is back. The startup
|
|
91
|
-
banner shows `⚠️ OFFLINE MODE` when this happens. Pass `--strict-mcp` to
|
|
76
|
+
**Offline mode** — if the MCP server isn't reachable, the CLI still boots
|
|
77
|
+
with only local tools (file edits, shell, web fetch, `spawn_agent`). Memory
|
|
78
|
+
recall, capture, and skills are disabled until the server is back. The
|
|
79
|
+
startup banner shows `offline` when this happens. Pass `--strict-mcp` to
|
|
92
80
|
make the CLI exit instead of degrading.
|
|
93
81
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
instead of running it separately, use `brainrouter config` → "Set Active
|
|
98
|
-
Server Profile" → `default` (the bundled stdio profile). You don't need
|
|
99
|
-
to run anything else — the CLI manages the server's lifecycle.
|
|
100
|
-
|
|
101
|
-
---
|
|
82
|
+
**Stdio mode** — to have the CLI spawn the MCP server as a child process
|
|
83
|
+
instead of running it separately: open `/config`, go to MCP settings, and
|
|
84
|
+
pick the bundled `stdio` profile. The CLI manages the server's lifecycle.
|
|
102
85
|
|
|
103
86
|
## Workspace detection
|
|
104
87
|
|
|
@@ -113,8 +96,6 @@ BRAINROUTER_WORKSPACE=/absolute/path/to/project brainrouter
|
|
|
113
96
|
|
|
114
97
|
Inside the REPL, run `/workspace` to confirm the active root and session key.
|
|
115
98
|
|
|
116
|
-
---
|
|
117
|
-
|
|
118
99
|
## What you also probably want
|
|
119
100
|
|
|
120
101
|
A BrainRouter MCP server for the cognitive memory. The CLI works without
|
|
@@ -127,9 +108,7 @@ $EDITOR ~/.config/brainrouter/server.env # set BRAINROUTER_LLM_API_KEY,
|
|
|
127
108
|
brainrouter-mcp --http --port 3747 # in a separate terminal
|
|
128
109
|
```
|
|
129
110
|
|
|
130
|
-
Then
|
|
131
|
-
|
|
132
|
-
---
|
|
111
|
+
Then run `/login` inside the REPL and point at `http://localhost:3747/mcp`.
|
|
133
112
|
|
|
134
113
|
## Docs
|
|
135
114
|
|
|
@@ -138,8 +117,6 @@ Then `brainrouter login` and point at `http://localhost:3747/mcp`.
|
|
|
138
117
|
- **Maintainer runbook**: [SETUP.md](https://github.com/kinqsradiollc/BrainRouter/blob/main/SETUP.md)
|
|
139
118
|
- **Bugs / requests**: <https://github.com/kinqsradiollc/BrainRouter/issues>
|
|
140
119
|
|
|
141
|
-
---
|
|
142
|
-
|
|
143
120
|
## License
|
|
144
121
|
|
|
145
122
|
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "architect",
|
|
3
|
+
"displayName": "Architect",
|
|
4
|
+
"whenToUse": "Design alternatives and tradeoffs for a feature or system change. No file writes.",
|
|
5
|
+
"prompt": "## Role: Architect\nYou design solutions; you do not write production code.\n\n### Memory-first opening (mandatory)\n- `memory_search` and `memory_graph_query` for the feature/domain — past architecture decisions often constrain new ones.\n- `memory_contradictions` (action: list) — if prior designs contradict the proposed change, flag it.\n- Cite any architecture_decision records you find with their recordId.\n\nAlways present at least two design alternatives with explicit tradeoffs (complexity, blast radius, reversibility, test cost).\nEnd with a clear recommendation and the smallest first vertical slice.",
|
|
6
|
+
"model": null,
|
|
7
|
+
"effort": null,
|
|
8
|
+
"defaultAccess": "read",
|
|
9
|
+
"toolScope": { "local": ["*"], "mcp": ["memory_*"] },
|
|
10
|
+
"disallowedTools": [],
|
|
11
|
+
"maxIterations": 30,
|
|
12
|
+
"timeoutMs": 120000,
|
|
13
|
+
"maxResultChars": 8000,
|
|
14
|
+
"subagents": [],
|
|
15
|
+
"delegateName": "delegate_architect",
|
|
16
|
+
"tier": "reasoning",
|
|
17
|
+
"outputContract": null
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "explorer",
|
|
3
|
+
"displayName": "Explorer",
|
|
4
|
+
"whenToUse": "Read-only codebase investigation. Returns concrete file paths, line ranges, and a short list of facts.",
|
|
5
|
+
"prompt": "## Role: Explorer\nYou are a read-only investigator. Do not edit files or run shell commands.\nGoal: map the relevant code, return concrete file paths with line ranges, and surface the few facts the parent needs to decide.\n\n### Memory-first opening (mandatory)\n- Step 1: `memory_search` for the topic of investigation. Past explorers may have mapped this already — do not re-discover what BrainRouter already knows.\n- Step 2: `memory_graph_query` with the dominant feature/entity name to surface related memories across 2 hops.\n- Step 3: `memory_file_history` for any file the parent specifically mentions.\n- Cite every recordId you build on. Your output begins with a `### Memory consulted` block listing the record IDs and what they told you.\n\nOutput structure: 1) Memory consulted, 2) Summary (3-5 bullets), 3) Key files with line ranges, 4) Open questions, 5) Suggested next probe.\nNever claim work is complete without naming actual files you read AND showing the memory you consulted.",
|
|
6
|
+
"model": null,
|
|
7
|
+
"effort": null,
|
|
8
|
+
"defaultAccess": "read",
|
|
9
|
+
"toolScope": { "local": ["*"], "mcp": ["memory_*"] },
|
|
10
|
+
"disallowedTools": [],
|
|
11
|
+
"maxIterations": 30,
|
|
12
|
+
"timeoutMs": 120000,
|
|
13
|
+
"maxResultChars": 8000,
|
|
14
|
+
"subagents": [],
|
|
15
|
+
"delegateName": "delegate_explorer",
|
|
16
|
+
"tier": "reasoning",
|
|
17
|
+
"outputContract": null
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "reviewer",
|
|
3
|
+
"displayName": "Reviewer",
|
|
4
|
+
"whenToUse": "Code review stance; findings first, severity-ordered. Read-only.",
|
|
5
|
+
"prompt": "## Role: Reviewer\nYou review changes critically. Findings first; severity-ordered (blocker, major, minor, nit).\n\n### Memory-first opening (mandatory)\n- `memory_search` for prior reviews on the same files or feature — never re-flag an issue another reviewer already decided is acceptable.\n- `memory_file_history` for each file in the diff — known regressions and prior bug fixes inform your verdict.\n- Cite related recordIds inline in each finding so the parent can see the precedent.\n\nFor each finding: file:line, what is wrong, why it matters, suggested fix.\nDo not make edits. The parent will decide what to apply.",
|
|
6
|
+
"model": null,
|
|
7
|
+
"effort": null,
|
|
8
|
+
"defaultAccess": "read",
|
|
9
|
+
"toolScope": { "local": ["*"], "mcp": ["memory_*"] },
|
|
10
|
+
"disallowedTools": [],
|
|
11
|
+
"maxIterations": 30,
|
|
12
|
+
"timeoutMs": 120000,
|
|
13
|
+
"maxResultChars": 8000,
|
|
14
|
+
"subagents": [],
|
|
15
|
+
"delegateName": "delegate_reviewer",
|
|
16
|
+
"tier": "reasoning",
|
|
17
|
+
"outputContract": null
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "verifier",
|
|
3
|
+
"displayName": "Verifier",
|
|
4
|
+
"whenToUse": "Runs tests and checks; reports pass/fail with evidence.",
|
|
5
|
+
"prompt": "## Role: Verifier\nYou verify that recent changes work. Run the smallest useful set of tests/typechecks.\n\n### Memory-first opening (mandatory)\n- `memory_search` for prior failure modes on these tests — flaky tests, environment caveats, and known-bad commands live in memory.\n- `memory_file_history` for any test file involved — past fixes for the same suite are highly relevant.\n\nReport: which command(s) you ran, exit codes, failing output (trimmed), and a clear PASS/FAIL verdict.\nNever claim PASS without actually executing a check. On failure, call `memory_task_update` with the blocker so the next worker can pick it up.",
|
|
6
|
+
"model": null,
|
|
7
|
+
"effort": null,
|
|
8
|
+
"defaultAccess": "shell",
|
|
9
|
+
"toolScope": { "local": ["*"], "mcp": ["memory_*"] },
|
|
10
|
+
"disallowedTools": [],
|
|
11
|
+
"maxIterations": 30,
|
|
12
|
+
"timeoutMs": 120000,
|
|
13
|
+
"maxResultChars": 8000,
|
|
14
|
+
"subagents": [],
|
|
15
|
+
"delegateName": "delegate_verifier",
|
|
16
|
+
"tier": "worker",
|
|
17
|
+
"outputContract": null
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "worker",
|
|
3
|
+
"displayName": "Worker",
|
|
4
|
+
"whenToUse": "Implementation-focused. May edit files when granted write access.",
|
|
5
|
+
"prompt": "## Role: Worker\nYou implement a single bounded task. Keep edits minimal and scoped.\n\n### Memory-first opening (mandatory)\n- `memory_recall` for the task topic — past instructions, conventions, and tool_preference records often dictate HOW to implement.\n- `memory_file_history` for the files you intend to touch — known fragility lives there.\n- If the parent gave you `seedRecordIds`, treat those as authoritative context.\n- `memory_task_state` if this looks like a continuation — pick up where prior work left off.\n\nRead before editing. Prefer edit_file over write_file when possible. Prefer apply_patch for multi-file edits.\nOn completion call `memory_task_update` with the outcome, then report exactly which files you changed and any follow-ups the verifier should run.",
|
|
6
|
+
"model": null,
|
|
7
|
+
"effort": null,
|
|
8
|
+
"defaultAccess": "write",
|
|
9
|
+
"toolScope": { "local": ["*"], "mcp": ["memory_*"] },
|
|
10
|
+
"disallowedTools": [],
|
|
11
|
+
"maxIterations": 30,
|
|
12
|
+
"timeoutMs": 120000,
|
|
13
|
+
"maxResultChars": 8000,
|
|
14
|
+
"subagents": [],
|
|
15
|
+
"delegateName": "delegate_worker",
|
|
16
|
+
"tier": "worker",
|
|
17
|
+
"outputContract": null
|
|
18
|
+
}
|
package/dist/agent/agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { McpClientWrapper } from '../runtime/
|
|
1
|
+
import type { McpClientPool as McpClientWrapper } from '../runtime/mcpPool.js';
|
|
2
2
|
import type { LLMConfig } from '../config/config.js';
|
|
3
3
|
import type { AccessMode } from '../orchestration/roles.js';
|
|
4
4
|
import { type RecalledRecord } from '../memory/briefing.js';
|
|
@@ -104,6 +104,10 @@ export interface AgentOptions {
|
|
|
104
104
|
*/
|
|
105
105
|
parentTraceId?: string;
|
|
106
106
|
parentSpanId?: string;
|
|
107
|
+
/** Agent tier — propagated from the definition so hierarchy checks work in grandchildren. */
|
|
108
|
+
tier?: 'chat' | 'reasoning' | 'worker';
|
|
109
|
+
/** Nesting depth in the spawn chain; 0 = direct child of the chat root (default). */
|
|
110
|
+
agentDepth?: number;
|
|
107
111
|
}
|
|
108
112
|
export declare const LOCAL_TOOLS: ({
|
|
109
113
|
name: string;
|
|
@@ -744,11 +748,18 @@ export declare class Agent {
|
|
|
744
748
|
readonly agentId: string;
|
|
745
749
|
/** agent_id of the parent (set by spawn_agent for children). */
|
|
746
750
|
private parentAgentId?;
|
|
751
|
+
/** Agent tier — forwarded to OrchestrationContext so grandchildren can inherit hierarchy checks. */
|
|
752
|
+
readonly tier?: 'chat' | 'reasoning' | 'worker';
|
|
753
|
+
/** Spawn-chain depth (0 = direct chat-root child). Forwarded to hierarchy checks. */
|
|
754
|
+
readonly agentDepth: number;
|
|
747
755
|
constructor(mcpClient: McpClientWrapper, llmConfig: LLMConfig, options: AgentOptions);
|
|
748
756
|
/** Expose for orchestration so spawn_agent can record the parent linkage. */
|
|
749
757
|
getAgentId(): string;
|
|
750
758
|
/** Internal — used by spawn_agent to record which parent dispatched us. */
|
|
751
759
|
setParentAgentId(id: string | undefined): void;
|
|
760
|
+
private isModelVisibleMcpTool;
|
|
761
|
+
private rawMcpToolName;
|
|
762
|
+
private serverIdFromMcpToolName;
|
|
752
763
|
private allowedToolsForAccess;
|
|
753
764
|
runTurn(prompt: string, callbacks: RunTurnCallbacks): Promise<string>;
|
|
754
765
|
/** Rough token estimate (1 token ≈ 4 characters of English / code). */
|
package/dist/agent/agent.js
CHANGED
|
@@ -23,6 +23,53 @@ import { renderCompactSystemMessage, runCompaction } from '../prompt/compactor.j
|
|
|
23
23
|
import { buildFanOutHint, shouldSuggestFanOut } from '../prompt/breadthHint.js';
|
|
24
24
|
const execPromise = promisify(exec);
|
|
25
25
|
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', '.DS_Store', '.next']);
|
|
26
|
+
function parseJsonObject(text) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(text);
|
|
29
|
+
return parsed && typeof parsed === 'object' ? parsed : undefined;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function collectChildIds(value) {
|
|
36
|
+
if (!value || typeof value !== 'object')
|
|
37
|
+
return [];
|
|
38
|
+
const ids = [];
|
|
39
|
+
const maybeRecord = value;
|
|
40
|
+
if (typeof maybeRecord.id === 'string')
|
|
41
|
+
ids.push(maybeRecord.id);
|
|
42
|
+
if (Array.isArray(maybeRecord.agents)) {
|
|
43
|
+
for (const entry of maybeRecord.agents) {
|
|
44
|
+
if (entry && typeof entry === 'object' && typeof entry.id === 'string') {
|
|
45
|
+
ids.push(entry.id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return [...new Set(ids)];
|
|
50
|
+
}
|
|
51
|
+
function trackChildObservation(toolName, args, resultText, spawned, waited) {
|
|
52
|
+
if (toolName === 'spawn_agent' || toolName === 'spawn_agents') {
|
|
53
|
+
const ids = collectChildIds(parseJsonObject(resultText));
|
|
54
|
+
for (const id of ids) {
|
|
55
|
+
spawned.add(id);
|
|
56
|
+
if (toolName === 'spawn_agent' && args?.wait)
|
|
57
|
+
waited.add(id);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (toolName === 'wait_agent') {
|
|
62
|
+
const id = typeof args?.id === 'string' ? args.id : undefined;
|
|
63
|
+
if (id)
|
|
64
|
+
waited.add(id);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (toolName === 'wait_agents') {
|
|
68
|
+
const ids = Array.isArray(args?.ids) ? args.ids.filter((id) => typeof id === 'string') : [];
|
|
69
|
+
for (const id of ids)
|
|
70
|
+
waited.add(id);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
26
73
|
export const LOCAL_TOOLS = [
|
|
27
74
|
{
|
|
28
75
|
name: 'read_file',
|
|
@@ -414,6 +461,10 @@ export class Agent {
|
|
|
414
461
|
agentId = `agent-${Math.random().toString(36).slice(2, 8)}`;
|
|
415
462
|
/** agent_id of the parent (set by spawn_agent for children). */
|
|
416
463
|
parentAgentId;
|
|
464
|
+
/** Agent tier — forwarded to OrchestrationContext so grandchildren can inherit hierarchy checks. */
|
|
465
|
+
tier;
|
|
466
|
+
/** Spawn-chain depth (0 = direct chat-root child). Forwarded to hierarchy checks. */
|
|
467
|
+
agentDepth;
|
|
417
468
|
constructor(mcpClient, llmConfig, options) {
|
|
418
469
|
this.mcpClient = mcpClient;
|
|
419
470
|
this.llmConfig = llmConfig;
|
|
@@ -437,6 +488,8 @@ export class Agent {
|
|
|
437
488
|
this.systemPromptOverride = options.systemPromptOverride;
|
|
438
489
|
this.parentTraceId = options.parentTraceId;
|
|
439
490
|
this.parentSpanId = options.parentSpanId;
|
|
491
|
+
this.tier = options.tier;
|
|
492
|
+
this.agentDepth = options.agentDepth ?? 0;
|
|
440
493
|
}
|
|
441
494
|
/** Expose for orchestration so spawn_agent can record the parent linkage. */
|
|
442
495
|
getAgentId() {
|
|
@@ -446,6 +499,47 @@ export class Agent {
|
|
|
446
499
|
setParentAgentId(id) {
|
|
447
500
|
this.parentAgentId = id;
|
|
448
501
|
}
|
|
502
|
+
isModelVisibleMcpTool(tool) {
|
|
503
|
+
const hiddenBrainrouterTools = new Set([
|
|
504
|
+
'memory_capture_turn',
|
|
505
|
+
'memory_mark_cited',
|
|
506
|
+
'memory_resolve_session',
|
|
507
|
+
'memory_register_skill_hints',
|
|
508
|
+
'memory_hook_register',
|
|
509
|
+
'memory_hook_status',
|
|
510
|
+
]);
|
|
511
|
+
const name = String(tool?.name ?? '');
|
|
512
|
+
const rawName = String(tool?.__rawName ?? this.rawMcpToolName(name));
|
|
513
|
+
if (!hiddenBrainrouterTools.has(rawName))
|
|
514
|
+
return true;
|
|
515
|
+
const serverId = typeof tool?.__serverId === 'string'
|
|
516
|
+
? tool.__serverId
|
|
517
|
+
: this.serverIdFromMcpToolName(name);
|
|
518
|
+
const status = serverId && typeof this.mcpClient.getStatus === 'function'
|
|
519
|
+
? this.mcpClient.getStatus(serverId)
|
|
520
|
+
: undefined;
|
|
521
|
+
// Hide only BrainRouter auto-pipeline/admin tools. Third-party MCP tools
|
|
522
|
+
// with coincidentally similar names stay visible.
|
|
523
|
+
return status?.identity !== 'brainrouter';
|
|
524
|
+
}
|
|
525
|
+
rawMcpToolName(name) {
|
|
526
|
+
const serverId = this.serverIdFromMcpToolName(name);
|
|
527
|
+
return serverId ? name.slice(`mcp__${serverId}__`.length) : name;
|
|
528
|
+
}
|
|
529
|
+
serverIdFromMcpToolName(name) {
|
|
530
|
+
if (!name.startsWith('mcp__'))
|
|
531
|
+
return undefined;
|
|
532
|
+
const rest = name.slice('mcp__'.length);
|
|
533
|
+
if (typeof this.mcpClient.getServerIds === 'function') {
|
|
534
|
+
const ids = this.mcpClient.getServerIds();
|
|
535
|
+
for (const id of ids.sort((a, b) => b.length - a.length)) {
|
|
536
|
+
if (rest.startsWith(`${id}__`))
|
|
537
|
+
return id;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const idx = rest.indexOf('__');
|
|
541
|
+
return idx >= 0 ? rest.slice(0, idx) : undefined;
|
|
542
|
+
}
|
|
449
543
|
allowedToolsForAccess() {
|
|
450
544
|
// Lifecycle / inspection tools are always available regardless of access
|
|
451
545
|
// mode — they don't touch the workspace and the agent needs them to end
|
|
@@ -504,27 +598,20 @@ export class Agent {
|
|
|
504
598
|
// whenever the inventory shape changed (online → offline or vice
|
|
505
599
|
// versa) so the next LLM call sees the correct system message.
|
|
506
600
|
const prevTools = this.lastKnownMcpTools?.map((t) => t.name).sort().join(',');
|
|
507
|
-
this.lastKnownMcpTools = mcpTools.map((t) => ({
|
|
601
|
+
this.lastKnownMcpTools = mcpTools.map((t) => ({
|
|
602
|
+
name: String(t?.__rawName ?? this.rawMcpToolName(String(t?.name ?? ''))),
|
|
603
|
+
}));
|
|
508
604
|
const newTools = this.lastKnownMcpTools.map((t) => t.name).sort().join(',');
|
|
509
605
|
if (prevTools !== newTools && this.chatHistory.length > 0 && this.chatHistory[0].role === 'system') {
|
|
510
606
|
this.chatHistory[0] = this.createSystemMessage();
|
|
511
607
|
}
|
|
512
608
|
const allowed = this.allowedToolsForAccess();
|
|
513
609
|
const filteredLocalTools = LOCAL_TOOLS.filter(t => allowed.has(t.name));
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
const HIDDEN_FROM_LLM = new Set([
|
|
520
|
-
'memory_capture_turn', // called automatically post-turn
|
|
521
|
-
'memory_mark_cited', // called automatically with real citation IDs
|
|
522
|
-
'memory_resolve_session', // called automatically at bootstrap
|
|
523
|
-
'memory_register_skill_hints', // boot-time, not turn-level
|
|
524
|
-
'memory_hook_register', // managed via /hooks
|
|
525
|
-
'memory_hook_status',
|
|
526
|
-
]);
|
|
527
|
-
const visibleMcpTools = mcpTools.filter((t) => !HIDDEN_FROM_LLM.has(t.name));
|
|
610
|
+
// Multi-MCP parity: expose every connected third-party MCP tool and the
|
|
611
|
+
// model-safe BrainRouter MCP tools in one turn, using the pool's
|
|
612
|
+
// `mcp__<serverId>__<tool>` namespaces. BrainRouter's auto-pipeline/admin
|
|
613
|
+
// tools stay hidden because the CLI owns those flows.
|
|
614
|
+
const visibleMcpTools = mcpTools.filter((t) => this.isModelVisibleMcpTool(t));
|
|
528
615
|
const allTools = [...filteredLocalTools, ...visibleMcpTools];
|
|
529
616
|
callbacks.onStatusUpdate(`Loaded ${filteredLocalTools.length} local tools and ${mcpTools.length} MCP tools.`);
|
|
530
617
|
// Auto-compact: if the chat history has grown past the configured token
|
|
@@ -612,6 +699,9 @@ export class Agent {
|
|
|
612
699
|
// signatures so we can interrupt the loop with corrective feedback.
|
|
613
700
|
const recentToolSignatures = [];
|
|
614
701
|
const REPEAT_GUARD_LIMIT = 3;
|
|
702
|
+
const spawnedChildIdsThisTurn = new Set();
|
|
703
|
+
const waitedChildIdsThisTurn = new Set();
|
|
704
|
+
let spawnWaitGuardInjected = false;
|
|
615
705
|
while (loopCount < maxLoops) {
|
|
616
706
|
loopCount++;
|
|
617
707
|
callbacks.onStatusUpdate(`Thinking (turn ${loopCount})...`);
|
|
@@ -639,6 +729,21 @@ export class Agent {
|
|
|
639
729
|
this.chatHistory.push(assistantMsg);
|
|
640
730
|
this.recordTranscript(assistantMsg);
|
|
641
731
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
732
|
+
const unobservedChildIds = [...spawnedChildIdsThisTurn].filter((id) => !waitedChildIdsThisTurn.has(id));
|
|
733
|
+
if (unobservedChildIds.length > 0 && !spawnWaitGuardInjected) {
|
|
734
|
+
spawnWaitGuardInjected = true;
|
|
735
|
+
const waitTool = unobservedChildIds.length === 1 ? 'wait_agent' : 'wait_agents';
|
|
736
|
+
const correction = [
|
|
737
|
+
`You spawned ${unobservedChildIds.length} child agent${unobservedChildIds.length === 1 ? '' : 's'} in this turn but have not waited for their outputs yet.`,
|
|
738
|
+
`Call \`${waitTool}\` now for: ${unobservedChildIds.join(', ')}.`,
|
|
739
|
+
'Do not tell the user you are waiting in prose; use the tool call, then synthesize the returned child output.',
|
|
740
|
+
].join(' ');
|
|
741
|
+
const guardMsg = { role: 'user', content: correction };
|
|
742
|
+
this.chatHistory.push(guardMsg);
|
|
743
|
+
this.recordTranscript(guardMsg);
|
|
744
|
+
callbacks.onStatusUpdate(`Waiting required for ${unobservedChildIds.length} child agent${unobservedChildIds.length === 1 ? '' : 's'}...`);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
642
747
|
finalAnswer = response.content;
|
|
643
748
|
exitedCleanly = true;
|
|
644
749
|
break;
|
|
@@ -758,6 +863,8 @@ export class Agent {
|
|
|
758
863
|
parentTraceId: turnSpan.traceId,
|
|
759
864
|
parentSpanId: turnSpan.spanId,
|
|
760
865
|
parentAgentId: this.agentId,
|
|
866
|
+
parentTier: this.tier,
|
|
867
|
+
depth: this.agentDepth,
|
|
761
868
|
mcpClient: this.mcpClient,
|
|
762
869
|
llmConfig: this.llmConfig,
|
|
763
870
|
launchCwd: this.launchCwd,
|
|
@@ -772,6 +879,7 @@ export class Agent {
|
|
|
772
879
|
},
|
|
773
880
|
});
|
|
774
881
|
summary = getToolSummary(name, args, resultText);
|
|
882
|
+
trackChildObservation(name, args, resultText, spawnedChildIdsThisTurn, waitedChildIdsThisTurn);
|
|
775
883
|
}
|
|
776
884
|
else if (isLocal) {
|
|
777
885
|
resultText = await this.executeLocalTool(name, args);
|
|
@@ -1104,7 +1212,7 @@ export class Agent {
|
|
|
1104
1212
|
try {
|
|
1105
1213
|
const res = await fetch(url, {
|
|
1106
1214
|
headers: {
|
|
1107
|
-
'User-Agent': 'Mozilla/5.0 (compatible; BrainRouterCLI/0.3.
|
|
1215
|
+
'User-Agent': 'Mozilla/5.0 (compatible; BrainRouterCLI/0.3.7)'
|
|
1108
1216
|
}
|
|
1109
1217
|
});
|
|
1110
1218
|
if (!res.ok) {
|
|
@@ -1712,7 +1820,7 @@ async function runWebSearch(query, maxResults) {
|
|
|
1712
1820
|
}
|
|
1713
1821
|
try {
|
|
1714
1822
|
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
|
|
1715
|
-
const res = await fetch(url, { headers: { 'User-Agent': 'BrainRouterCLI/0.3.
|
|
1823
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'BrainRouterCLI/0.3.7' } });
|
|
1716
1824
|
if (!res.ok) {
|
|
1717
1825
|
return `web_search failed: DuckDuckGo returned ${res.status} ${res.statusText}.`;
|
|
1718
1826
|
}
|
|
@@ -2267,7 +2375,15 @@ export function buildChatCompletionPayload(config, messages, tools, options = {}
|
|
|
2267
2375
|
return body;
|
|
2268
2376
|
}
|
|
2269
2377
|
export async function callOpenAI(config, messages, tools, options = {}) {
|
|
2270
|
-
|
|
2378
|
+
// Normalize the endpoint to a base URL (everything UP TO `/chat/completions`
|
|
2379
|
+
// exclusive). Earlier callers stored the full chat-completions URL in
|
|
2380
|
+
// `config.endpoint` (e.g. "https://api.openai.com/v1/chat/completions")
|
|
2381
|
+
// because the in-terminal wizard's provider catalog wrote the full path.
|
|
2382
|
+
// We then re-append `/chat/completions` below, producing a duplicate
|
|
2383
|
+
// `/chat/completions/chat/completions` and a 404. Strip the suffix
|
|
2384
|
+
// defensively so both shapes (full URL or base URL) work.
|
|
2385
|
+
const rawEndpoint = config.endpoint || 'https://api.openai.com/v1';
|
|
2386
|
+
const endpoint = rawEndpoint.replace(/\/+$/, '').replace(/\/chat\/completions$/, '');
|
|
2271
2387
|
let apiKey = config.apiKey || process.env.OPENAI_API_KEY || '';
|
|
2272
2388
|
const isLocal = endpoint.includes('localhost') || endpoint.includes('127.0.0.1');
|
|
2273
2389
|
if (!apiKey && !isLocal) {
|
package/dist/cli/banner.d.ts
CHANGED
|
@@ -39,6 +39,12 @@ export interface BannerInputs {
|
|
|
39
39
|
/** Version override (test fixture). */
|
|
40
40
|
version?: string;
|
|
41
41
|
}
|
|
42
|
+
export interface DisplayedMcpState {
|
|
43
|
+
profile: string;
|
|
44
|
+
transport: string;
|
|
45
|
+
online: boolean;
|
|
46
|
+
identity: 'brainrouter' | 'third-party' | 'unknown';
|
|
47
|
+
}
|
|
42
48
|
/**
|
|
43
49
|
* Pure renderer — returns the box as a single newline-joined string with
|
|
44
50
|
* ANSI sequences from `theme`. Caller appends the trailing newline.
|
|
@@ -57,4 +63,18 @@ export declare function buildBannerInputs(config: Config, agent: {
|
|
|
57
63
|
}, mcpClient: {
|
|
58
64
|
isConnected: () => boolean;
|
|
59
65
|
getIdentity?: () => 'brainrouter' | 'third-party' | 'unknown';
|
|
66
|
+
getStatus?: (serverId: string) => {
|
|
67
|
+
status: string;
|
|
68
|
+
identity: 'brainrouter' | 'third-party' | 'unknown';
|
|
69
|
+
} | undefined;
|
|
70
|
+
getActiveBrainrouterServerId?: () => string | undefined;
|
|
60
71
|
}): BannerInputs;
|
|
72
|
+
export declare function resolveDisplayedMcpState(config: Config, mcpClient: {
|
|
73
|
+
isConnected: () => boolean;
|
|
74
|
+
getIdentity?: () => 'brainrouter' | 'third-party' | 'unknown';
|
|
75
|
+
getStatus?: (serverId: string) => {
|
|
76
|
+
status: string;
|
|
77
|
+
identity: 'brainrouter' | 'third-party' | 'unknown';
|
|
78
|
+
} | undefined;
|
|
79
|
+
getActiveBrainrouterServerId?: () => string | undefined;
|
|
80
|
+
}): DisplayedMcpState;
|
package/dist/cli/banner.js
CHANGED
|
@@ -9,7 +9,7 @@ import { BOX } from './theme.js';
|
|
|
9
9
|
* (chalk title + workspace line + connecting-to line) with a single visually
|
|
10
10
|
* scannable block:
|
|
11
11
|
*
|
|
12
|
-
* ╭─ 🧠 BrainRouter CLI 0.3.
|
|
12
|
+
* ╭─ 🧠 BrainRouter CLI 0.3.7 ──────────────────────────────╮
|
|
13
13
|
* │ workspace BrainRouter · c5b8c12d │
|
|
14
14
|
* │ mcp local-http · http · online │
|
|
15
15
|
* │ workflow cli-shell-redesign (in-progress) │
|
|
@@ -26,10 +26,19 @@ import { BOX } from './theme.js';
|
|
|
26
26
|
* The function returns a single string with embedded ANSI; the caller prints
|
|
27
27
|
* it once. Pure-function so tests can assert against the rendered output.
|
|
28
28
|
*/
|
|
29
|
-
const VERSION = '0.3.
|
|
29
|
+
const VERSION = '0.3.7';
|
|
30
30
|
const TITLE = '🧠 BrainRouter CLI';
|
|
31
|
-
|
|
31
|
+
// Width floor for the BOXED banner. Below this we fall through to the
|
|
32
|
+
// `renderPlainBanner` plaintext format. Was 56 — that caused the box to
|
|
33
|
+
// overflow on terminals narrower than 58 cols (each row wrapped to
|
|
34
|
+
// multiple terminal rows with broken border alignment). 38 fits a
|
|
35
|
+
// 40-col terminal (the smallest realistic phone / split-pane width).
|
|
36
|
+
const MIN_BOX_WIDTH = 38;
|
|
32
37
|
const MAX_WIDTH = 100;
|
|
38
|
+
// Below this width we skip the box entirely and render the rows as
|
|
39
|
+
// "label: value" lines. The boxed format with horizontal borders +
|
|
40
|
+
// title is meaningless when each border row wraps.
|
|
41
|
+
const PLAIN_TEXT_THRESHOLD = 38;
|
|
33
42
|
function shortHash(absPath) {
|
|
34
43
|
return crypto.createHash('sha1').update(absPath).digest('hex').slice(0, 8);
|
|
35
44
|
}
|
|
@@ -123,8 +132,14 @@ export function renderBanner(inputs, theme) {
|
|
|
123
132
|
// Inner width is the widest "label + 2 spaces + value", clamped.
|
|
124
133
|
const naturalInner = rows.reduce((w, r) => Math.max(w, labelWidth + 2 + r.value.length), titleText.length + 4);
|
|
125
134
|
const targetCols = process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : MAX_WIDTH;
|
|
135
|
+
// Below the plaintext threshold the boxed layout is hostile (each
|
|
136
|
+
// border row wraps and looks chaotic). Fall back to a label:value
|
|
137
|
+
// text dump that the terminal can wrap naturally.
|
|
138
|
+
if (targetCols < PLAIN_TEXT_THRESHOLD) {
|
|
139
|
+
return renderPlainBanner(titleText, rows, theme);
|
|
140
|
+
}
|
|
126
141
|
// Reserve 2 columns for the side borders.
|
|
127
|
-
const innerWidth = Math.max(
|
|
142
|
+
const innerWidth = Math.max(MIN_BOX_WIDTH, Math.min(MAX_WIDTH, Math.min(naturalInner, targetCols - 2)));
|
|
128
143
|
const top = (() => {
|
|
129
144
|
// ╭─ <title> ──╮ — title sits inline at the top border.
|
|
130
145
|
const titlePiece = ` ${titleText} `;
|
|
@@ -140,6 +155,17 @@ export function renderBanner(inputs, theme) {
|
|
|
140
155
|
const bottom = theme.primary(BOX.bottomLeft + BOX.horizontal.repeat(innerWidth) + BOX.bottomRight);
|
|
141
156
|
return [top, ...bodyLines, bottom].join('\n');
|
|
142
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Compact label:value text banner — used on terminals narrower than
|
|
160
|
+
* PLAIN_TEXT_THRESHOLD cols where the boxed layout's border rows
|
|
161
|
+
* would wrap and look broken. Same information, no chrome.
|
|
162
|
+
*/
|
|
163
|
+
function renderPlainBanner(titleText, rows, theme) {
|
|
164
|
+
const labelWidth = rows.reduce((w, r) => Math.max(w, r.label.length), 0);
|
|
165
|
+
const headerLine = theme.primary(titleText);
|
|
166
|
+
const bodyLines = rows.map((row) => theme.muted(padRight(row.label, labelWidth) + ' ') + theme.plain(row.value));
|
|
167
|
+
return [headerLine, ...bodyLines].join('\n');
|
|
168
|
+
}
|
|
143
169
|
/**
|
|
144
170
|
* Convenience: assemble the inputs from live agent + config + workspace
|
|
145
171
|
* state. Pure read; no side effects. Anything that throws while reading the
|
|
@@ -147,12 +173,7 @@ export function renderBanner(inputs, theme) {
|
|
|
147
173
|
* workspace doesn't crash the banner.
|
|
148
174
|
*/
|
|
149
175
|
export function buildBannerInputs(config, agent, mcpClient) {
|
|
150
|
-
const
|
|
151
|
-
const server = config.servers[profile];
|
|
152
|
-
const transport = server?.type ?? 'unknown';
|
|
153
|
-
// 10c: identity comes from the live wrapper when present; fall back to
|
|
154
|
-
// the config field for callers that pass a thin stub.
|
|
155
|
-
const mcpIdentity = mcpClient.getIdentity ? mcpClient.getIdentity() : (server?.identity ?? 'unknown');
|
|
176
|
+
const displayedMcp = resolveDisplayedMcpState(config, mcpClient);
|
|
156
177
|
let workflow;
|
|
157
178
|
let lastUsedWorkflow;
|
|
158
179
|
try {
|
|
@@ -186,10 +207,10 @@ export function buildBannerInputs(config, agent, mcpClient) {
|
|
|
186
207
|
catch { /* ignore — no goal yet */ }
|
|
187
208
|
return {
|
|
188
209
|
workspaceRoot: agent.workspaceRoot,
|
|
189
|
-
mcpProfile: profile,
|
|
190
|
-
mcpTransport: transport,
|
|
191
|
-
mcpOnline:
|
|
192
|
-
mcpIdentity,
|
|
210
|
+
mcpProfile: displayedMcp.profile,
|
|
211
|
+
mcpTransport: displayedMcp.transport,
|
|
212
|
+
mcpOnline: displayedMcp.online,
|
|
213
|
+
mcpIdentity: displayedMcp.identity,
|
|
193
214
|
sessionKey: agent.sessionKey,
|
|
194
215
|
model: agent.getModel(),
|
|
195
216
|
workflow,
|
|
@@ -197,3 +218,15 @@ export function buildBannerInputs(config, agent, mcpClient) {
|
|
|
197
218
|
goal,
|
|
198
219
|
};
|
|
199
220
|
}
|
|
221
|
+
export function resolveDisplayedMcpState(config, mcpClient) {
|
|
222
|
+
const liveBrain = mcpClient.getActiveBrainrouterServerId?.();
|
|
223
|
+
const profile = liveBrain || config.activeServer;
|
|
224
|
+
const server = config.servers[profile];
|
|
225
|
+
const status = profile ? mcpClient.getStatus?.(profile) : undefined;
|
|
226
|
+
return {
|
|
227
|
+
profile,
|
|
228
|
+
transport: server?.type ?? 'unknown',
|
|
229
|
+
online: status ? status.status === 'connected' : mcpClient.isConnected(),
|
|
230
|
+
identity: status?.identity ?? server?.identity ?? mcpClient.getIdentity?.() ?? 'unknown',
|
|
231
|
+
};
|
|
232
|
+
}
|