@spinabot/brigade 0.1.0 → 0.1.2

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/CHANGELOG.md CHANGED
@@ -1,10 +1,48 @@
1
- # Changelog
2
-
3
- ## 0.1.0 2026-05-02
4
-
5
- - Initial release.
6
- - Polished terminal TUI for Anthropic, OpenAI, Gemini, Groq, xAI, DeepSeek, Mistral, OpenRouter, Cerebras, Ollama, and any OpenAI-compatible endpoint.
7
- - Subcommands: `brigade chat`, `brigade gateway`, `brigade connect`, `brigade onboard`, `brigade doctor`, `brigade config`.
8
- - Provider onboarding wizard on first launch.
9
- - `/model`, `/provider`, `/thinking`, `/help` in-chat commands.
10
- - Local-only — no telemetry, no cloud component.
1
+ # Changelog
2
+
3
+ ## [0.1.2](https://github.com/Bhasvanth-Dev9380/brigade/compare/v0.1.1...v0.1.2) (2026-05-03)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **readme:** use absolute raw URL for banner so npm renders it ([975f5f2](https://github.com/Bhasvanth-Dev9380/brigade/commit/975f5f2884ef98af017720c0eaa499eb4c9f3572))
9
+
10
+ ## [0.1.1](https://github.com/Bhasvanth-Dev9380/brigade/compare/v0.1.0...v0.1.1) (2026-05-03)
11
+
12
+
13
+ ### Features
14
+
15
+ * add CI and release workflows, configure automated npm publishing, and enhance README with badges ([7cda18d](https://github.com/Bhasvanth-Dev9380/brigade/commit/7cda18dfecd320fb0cf70553a64baecf291175fb))
16
+ * add injected-content defence section to AGENTS.md and update image layout in README.md ([7180544](https://github.com/Bhasvanth-Dev9380/brigade/commit/718054476cc02687cbc84fc79b9e029ec894568e))
17
+ * **brand:** enhance layout handling for terminal width and add wordmark indentation ([0f68c83](https://github.com/Bhasvanth-Dev9380/brigade/commit/0f68c8357f27d966ce4b4cfa0bf6450cf999f7bc))
18
+ * **brand:** enhance layout handling for terminal widths and add scrollback-friendly rendering ([28b280b](https://github.com/Bhasvanth-Dev9380/brigade/commit/28b280bc3ffdcbd3b60e7937885eda8995ee3d9b))
19
+ * enhance README with new images for dark mode and adjust wordmark size; add demo GIF ([cd9c607](https://github.com/Bhasvanth-Dev9380/brigade/commit/cd9c6074fff29b793bb486cef4a5dc91e262b2c7))
20
+ * enhance README with responsive wordmark and add new image for dark mode ([5de16f7](https://github.com/Bhasvanth-Dev9380/brigade/commit/5de16f7118114c209f6eea46e078cec18ae5da75))
21
+ * implement Brigade-native auth error translation and guidance messages; add tests for auth-error handling ([1f57ba5](https://github.com/Bhasvanth-Dev9380/brigade/commit/1f57ba567701e77f83ee24ae50bf8e776a6cba2a))
22
+ * implement ephemeral system prompt feature with sanitization and context framing ([cd9c607](https://github.com/Bhasvanth-Dev9380/brigade/commit/cd9c6074fff29b793bb486cef4a5dc91e262b2c7))
23
+ * implement injected text sanitization and defensive framing for project context ([5de16f7](https://github.com/Bhasvanth-Dev9380/brigade/commit/5de16f7118114c209f6eea46e078cec18ae5da75))
24
+ * **tests:** add comprehensive CLI tests for argument parsing, dispatching, and command execution ([1c398c5](https://github.com/Bhasvanth-Dev9380/brigade/commit/1c398c5611349a5765022306478d7ba133a7613d))
25
+ * **tests:** add comprehensive tests for onboarding, provider commands, and error handling ([c1d5a77](https://github.com/Bhasvanth-Dev9380/brigade/commit/c1d5a77238bf5f02e8a20054671140b0169f7e63))
26
+ * update README and add brigade wordmark image ([55a2a1b](https://github.com/Bhasvanth-Dev9380/brigade/commit/55a2a1b9f7c1f8cae51307052e4603713865e3bd))
27
+ * update README with new image layout, add demo GIFs, and clean up package.json assets ([378529a](https://github.com/Bhasvanth-Dev9380/brigade/commit/378529a6b815f1aaab557bccca8a18e589c013c8))
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * adjust image layout and alignment in README ([a80268b](https://github.com/Bhasvanth-Dev9380/brigade/commit/a80268b1fb1d363306f27dcd93b1a290442ab3c4))
33
+ * **tests:** add connect-cmd footer note for /provider command visibility ([2367deb](https://github.com/Bhasvanth-Dev9380/brigade/commit/2367deb4b445740bd556ed22b3acac924fd2bd4a))
34
+ * update demo image link in README ([f688f01](https://github.com/Bhasvanth-Dev9380/brigade/commit/f688f01d92e56805aeaa78b8da6b7aa40d619c16))
35
+
36
+
37
+ ### Refactor
38
+
39
+ * **tests:** enhance brigade-config migration tests to prevent product name leakage ([2367deb](https://github.com/Bhasvanth-Dev9380/brigade/commit/2367deb4b445740bd556ed22b3acac924fd2bd4a))
40
+
41
+ ## 0.1.0 — 2026-05-02
42
+
43
+ - Initial release.
44
+ - Polished terminal TUI for Anthropic, OpenAI, Gemini, Groq, xAI, DeepSeek, Mistral, OpenRouter, Cerebras, Ollama, and any OpenAI-compatible endpoint.
45
+ - Subcommands: `brigade chat`, `brigade gateway`, `brigade connect`, `brigade onboard`, `brigade doctor`, `brigade config`.
46
+ - Provider onboarding wizard on first launch.
47
+ - `/model`, `/provider`, `/thinking`, `/help` in-chat commands.
48
+ - Local-only — no telemetry, no cloud component.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Brigade contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brigade contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
- <p>
2
- <img src="https://github.com/user-attachments/assets/44afe91e-bc06-4b85-a8a9-dcc608041e28#gh-dark-mode-only" alt="Brigade demo" width="180" align="right" />
3
- <img src="https://github.com/user-attachments/assets/3cc8ad09-259b-4eef-889a-23ded6eabab3#gh-light-mode-only" alt="Brigade demo" width="180" align="right" />
4
- <img src="./assets/brigade-wordmark.png#gh-dark-mode-only" alt="BRIGADE" width="600" />
5
- <img src="./assets/brigade-wordmark-on-black.png#gh-light-mode-only" alt="BRIGADE" width="600" />
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/Bhasvanth-Dev9380/brigade/main/assets/brigade-banner-on-black.gif" alt="BRIGADE — your personal AI crew" width="900" />
6
3
  </p>
7
4
 
8
- <br clear="all" />
5
+ <p>
6
+ <a href="https://github.com/Bhasvanth-Dev9380/brigade/actions/workflows/ci.yml"><img src="https://github.com/Bhasvanth-Dev9380/brigade/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
7
+ <a href="https://www.npmjs.com/package/@spinabot/brigade"><img src="https://img.shields.io/npm/v/@spinabot/brigade" alt="npm version" /></a>
8
+ <a href="https://www.npmjs.com/package/@spinabot/brigade"><img src="https://img.shields.io/npm/l/@spinabot/brigade" alt="license" /></a>
9
+ <a href="https://www.npmjs.com/package/@spinabot/brigade"><img src="https://img.shields.io/node/v/@spinabot/brigade" alt="node engine" /></a>
10
+ </p>
9
11
 
10
12
  Brigade is a polished terminal interface for working with the world's
11
13
  leading language models — Anthropic Claude, OpenAI GPT, Google Gemini,
@@ -10,16 +10,20 @@
10
10
  * → buildAgent → runChat) and surfaces a clean SIGINT chain so Ctrl+C aborts
11
11
  * a turn first, exits second.
12
12
  */
13
+ import * as fs from "node:fs/promises";
14
+ import * as path from "node:path";
13
15
  import process from "node:process";
14
16
  import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
15
17
  import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
16
18
  import chalk from "chalk";
17
19
  import { buildAgent } from "../core/agent.js";
18
- import { BRIGADE_DIR, loadConfig } from "../core/config.js";
20
+ import { BRIGADE_DIR, getBrigadeWorkspaceDir, loadConfig } from "../core/config.js";
19
21
  import { attachEventLogger, getTodayLogPath } from "../core/event-logger.js";
20
22
  import { findProvider } from "../providers/catalog.js";
23
+ import { EXIT_CONFIG_ERROR } from "../protocol.js";
21
24
  import { runChat } from "../ui/chat.js";
22
25
  import { runOnboarding } from "../ui/onboarding.js";
26
+ import { restoreTerminal } from "../ui/terminal-cleanup.js";
23
27
  /**
24
28
  * Boot the in-process chat TUI. Returns once the editor is ready; the chat
25
29
  * runs until SIGINT/Ctrl+D/`/exit`. Resolves with the chat handle in case
@@ -27,6 +31,17 @@ import { runOnboarding } from "../ui/onboarding.js";
27
31
  */
28
32
  export async function runChatCommand(opts = {}) {
29
33
  const cwd = opts.cwd ?? process.cwd();
34
+ // Refuse to boot in a non-TTY environment (CI, piped stdin, redirected
35
+ // stdout) — the onboarding wizard would block forever waiting for keystrokes
36
+ // that never arrive. Better to fail loudly with a fixable instruction than
37
+ // hang the parent process indefinitely.
38
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
39
+ console.error(chalk.red("brigade chat needs an interactive terminal."));
40
+ console.error(chalk.dim("For non-interactive use, set up your provider first via env vars or `brigade config set`, then call brigade in a TTY."));
41
+ // Sysexits 78: this is a configuration/usage error that won't fix on
42
+ // retry. Supervisors should STOP, not restart-loop.
43
+ process.exit(EXIT_CONFIG_ERROR);
44
+ }
30
45
  // Boot the TUI renderer FIRST. From this point on, addChild/requestRender
31
46
  // actually paint to the screen. Onboarding, splash, and chat all depend on it.
32
47
  const tui = new TUI(new ProcessTerminal());
@@ -46,6 +61,12 @@ export async function runChatCommand(opts = {}) {
46
61
  const wasRunning = chatHandle.abort();
47
62
  if (!wasRunning) {
48
63
  tui.stop();
64
+ // Belt-and-braces: tui.stop() runs Pi-TUI's cleanup which
65
+ // covers the kitty pop in the happy path. We follow it with
66
+ // an explicit restoreTerminal() so the broader safety net
67
+ // (focus reporting, mouse, alt-screen, modifyOtherKeys)
68
+ // also fires before we hand control back to the shell.
69
+ restoreTerminal();
49
70
  process.exit(0);
50
71
  }
51
72
  // A turn was aborted — re-arm so the next Ctrl+C is heard.
@@ -54,6 +75,7 @@ export async function runChatCommand(opts = {}) {
54
75
  }
55
76
  // During splash / onboarding (no chat yet): clean exit.
56
77
  tui.stop();
78
+ restoreTerminal();
57
79
  process.exit(130); // 128 + SIGINT
58
80
  };
59
81
  process.once("SIGINT", onSigint);
@@ -76,23 +98,44 @@ export async function runChatCommand(opts = {}) {
76
98
  }
77
99
  }
78
100
  }
101
+ // Snapshot whether BOOTSTRAP.md exists BEFORE we touch the workspace.
102
+ // `buildAgent` calls `seedDefaultPrompts` (idempotent — only writes files
103
+ // that don't exist), which means by the time the chat is wired BOOTSTRAP.md
104
+ // will exist regardless. Capturing the pre-boot state is the only honest
105
+ // way to tell "this is the user's very first run" from "the user previously
106
+ // completed bootstrap and explicitly deleted BOOTSTRAP.md to mark it done."
107
+ // We only fire the first-run kickoff in the former case.
108
+ const bootstrapPathBefore = path.join(getBrigadeWorkspaceDir(), "BOOTSTRAP.md");
109
+ let bootstrapExistedBeforeBoot = true;
110
+ try {
111
+ await fs.stat(bootstrapPathBefore);
112
+ }
113
+ catch {
114
+ bootstrapExistedBeforeBoot = false;
115
+ }
79
116
  // Onboard if needed.
117
+ let onboardingRan = false;
80
118
  if (!model || !provider || !modelId) {
81
119
  try {
82
- const result = await runOnboarding(tui, authStorage, modelRegistry);
120
+ const result = await runOnboarding(tui, authStorage, modelRegistry, {
121
+ noEnvDetect: opts.noEnvDetect,
122
+ });
83
123
  provider = result.provider;
84
124
  modelId = result.modelId;
85
125
  modelRegistry.refresh();
86
126
  model = modelRegistry.find(provider, modelId);
127
+ onboardingRan = true;
87
128
  }
88
129
  catch (_err) {
89
130
  tui.stop();
131
+ restoreTerminal();
90
132
  console.error(chalk.red("Onboarding cancelled."));
91
133
  process.exit(1);
92
134
  }
93
135
  }
94
136
  if (!model || !provider || !modelId) {
95
137
  tui.stop();
138
+ restoreTerminal();
96
139
  console.error(chalk.red("Brigade couldn't get ready. Please run setup again."));
97
140
  process.exit(1);
98
141
  }
@@ -111,10 +154,38 @@ export async function runChatCommand(opts = {}) {
111
154
  // switch confusion) the user can grep today's file.
112
155
  attachEventLogger(session);
113
156
  process.stderr.write(`\x1b[2mlogs: ${getTodayLogPath()}\x1b[0m\n`);
157
+ // First-run kickoff. ONLY fire when BOTH:
158
+ // 1. Onboarding actually ran on this boot (the user picked a provider +
159
+ // model + key just now — we don't kick off an existing user when
160
+ // `~/.brigade/config.json` already had a default).
161
+ // 2. BOOTSTRAP.md didn't exist before this boot — i.e. the workspace
162
+ // is truly fresh, never been here. If the user previously completed
163
+ // bootstrap and deleted BOOTSTRAP.md (the file's own instructions
164
+ // tell the agent to do so when done), we honour that "I'm an
165
+ // established user" signal and skip the kickoff even on re-onboard.
166
+ //
167
+ // The kickoff is a synthesised first user turn — same wording every time
168
+ // so the agent's BOOTSTRAP.md (loaded into the system prompt) reliably
169
+ // triggers the "Hey. I just came online. Who am I? Who are you?"
170
+ // name-discovery conversation it was designed to open.
171
+ const shouldKickoff = onboardingRan && !bootstrapExistedBeforeBoot;
172
+ const kickoffMessage = shouldKickoff ? "Wake up, my friend!" : undefined;
114
173
  // Hand off to the chat TUI. runChat returns a ChatHandle synchronously once
115
174
  // the UI is wired — the editor's onSubmit drives subsequent turns until
116
175
  // /exit, Ctrl+D, or SIGINT (already wired above) tears it down.
117
- chatHandle = await runChat({ session, tui, provider, modelId, authStorage, modelRegistry });
176
+ chatHandle = await runChat({
177
+ session,
178
+ tui,
179
+ provider,
180
+ modelId,
181
+ authStorage,
182
+ modelRegistry,
183
+ kickoffMessage,
184
+ // Same gate as the kickoff message — only render the slash-command
185
+ // discoverability tip on a fresh-workspace boot. Returning users with
186
+ // an established BOOTSTRAP.md never see it.
187
+ firstRun: shouldKickoff,
188
+ });
118
189
  return chatHandle;
119
190
  }
120
191
  //# sourceMappingURL=chat-cmd.js.map
@@ -13,7 +13,9 @@
13
13
  */
14
14
  import process from "node:process";
15
15
  import chalk from "chalk";
16
- import { loadConfig, saveConfig } from "../core/config.js";
16
+ import { BRIGADE_DIR, loadConfig, saveConfig } from "../core/config.js";
17
+ import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
18
+ import { findProvider } from "../providers/catalog.js";
17
19
  /** Whitelist of known keys. Keep in sync with BrigadeConfig in core/config.ts. */
18
20
  const KNOWN_KEYS = [
19
21
  "defaultProvider",
@@ -88,6 +90,43 @@ async function setConfig(key, value) {
88
90
  chalk.dim(`valid keys: ${KNOWN_KEYS.join(", ")}\n`));
89
91
  return { exitCode: 2 };
90
92
  }
93
+ // Guard model+provider writes: refuse to set a default for a provider that
94
+ // has no key, or a model id that isn't in the registry. Without these,
95
+ // `brigade config set defaultProvider anthropic` would happily save
96
+ // anthropic with no key, and the next `brigade chat` would auto-select
97
+ // it and only fail when the user typed their first message.
98
+ if (key === "defaultProvider") {
99
+ const provider = findProvider(value);
100
+ if (!provider) {
101
+ process.stderr.write(chalk.red(`unknown provider: ${value}\n`));
102
+ return { exitCode: 2 };
103
+ }
104
+ if (!provider.noAuth) {
105
+ const authStorage = AuthStorage.create(`${BRIGADE_DIR}/auth.json`);
106
+ const apiKey = await authStorage.getApiKey(value);
107
+ if (!apiKey) {
108
+ process.stderr.write(chalk.red(`${provider.name} has no API key configured. `) +
109
+ chalk.dim(`Run \`brigade onboard\` first, or set ${provider.envVar ?? "the env var"}.\n`));
110
+ return { exitCode: 2 };
111
+ }
112
+ }
113
+ }
114
+ if (key === "defaultModelId") {
115
+ const cfg = await loadConfig();
116
+ const provider = cfg.defaultProvider;
117
+ if (!provider) {
118
+ process.stderr.write(chalk.red("set defaultProvider first, then defaultModelId.\n"));
119
+ return { exitCode: 2 };
120
+ }
121
+ const authStorage = AuthStorage.create(`${BRIGADE_DIR}/auth.json`);
122
+ const registry = ModelRegistry.create(authStorage, `${BRIGADE_DIR}/models.json`);
123
+ const model = registry.find(provider, value);
124
+ if (!model) {
125
+ process.stderr.write(chalk.red(`${value} is not a registered model for ${provider}. `) +
126
+ chalk.dim(`Run \`brigade chat\` and use /model to see available ids.\n`));
127
+ return { exitCode: 2 };
128
+ }
129
+ }
91
130
  // saveConfig MERGES, so passing a single key preserves the rest. Cast through
92
131
  // Partial<BrigadeConfig> so unknown extra keys can't sneak in via this path.
93
132
  const partial = { [key]: value };
@@ -12,11 +12,20 @@
12
12
  * available from connect mode in v1 — onboarding writes to the gateway's
13
13
  * filesystem and is out-of-band; the user runs `brigade onboard` against
14
14
  * the gateway machine instead.
15
+ *
16
+ * MAINTAINER NOTE: The footer at `wireConnectUi` deliberately OMITS
17
+ * `/provider` from the slash-command list. Do NOT add it to the connect
18
+ * footer "for parity with chat" — connect cannot perform provider
19
+ * onboarding (no gateway-side filesystem access from the TUI client), and
20
+ * advertising a command we can't honour confuses users. The chat-mode
21
+ * footer (in `src/ui/chat.ts`) DOES list `/provider` because in-process
22
+ * chat owns the filesystem the wizard needs to write to.
15
23
  */
16
24
  import process from "node:process";
17
25
  import { ProcessTerminal, TUI, Text, Markdown, CancellableLoader, Editor } from "@mariozechner/pi-tui";
18
26
  import chalk from "chalk";
19
27
  import { renderBrandHeader } from "../ui/brand.js";
28
+ import { restoreTerminal } from "../ui/terminal-cleanup.js";
20
29
  import { brand, editorTheme, markdownTheme } from "../ui/theme.js";
21
30
  import { BrigadeClient } from "../tui/client.js";
22
31
  /**
@@ -40,6 +49,12 @@ export async function runConnectCommand(opts = {}) {
40
49
  if (!wasRunning) {
41
50
  void chatHandle.close().then(() => {
42
51
  tui.stop();
52
+ // tui.stop() already runs Pi-TUI's cleanup (kitty pop on
53
+ // the happy path); restoreTerminal() is the broader safety
54
+ // net (focus, mouse, alt-screen, modifyOtherKeys) plus an
55
+ // unconditional kitty pop in case Pi-TUI's flag tracker
56
+ // missed it.
57
+ restoreTerminal();
43
58
  process.exit(0);
44
59
  });
45
60
  return;
@@ -48,6 +63,7 @@ export async function runConnectCommand(opts = {}) {
48
63
  return;
49
64
  }
50
65
  tui.stop();
66
+ restoreTerminal();
51
67
  process.exit(130);
52
68
  };
53
69
  process.once("SIGINT", onSigint);
@@ -60,10 +76,15 @@ export async function runConnectCommand(opts = {}) {
60
76
  }
61
77
  catch (err) {
62
78
  tui.stop();
79
+ restoreTerminal();
63
80
  const msg = err instanceof Error ? err.message : String(err);
64
81
  const isRefused = /ECONNREFUSED|connect.*refused/i.test(msg);
65
82
  const isTimeout = /ETIMEDOUT|timed?\s*out/i.test(msg);
66
- console.error(chalk.red(`✗ Couldn't reach the Brigade gateway at ${url}: ${msg}`));
83
+ const isUnknownHost = /ENOTFOUND|EAI_AGAIN|getaddrinfo|temporary failure in name resolution/i.test(msg);
84
+ const isUnreachable = /EHOSTUNREACH|ENETUNREACH|network is unreachable|no route to host/i.test(msg);
85
+ // Headline: don't interpolate the raw `msg` (it can carry ENOTFOUND-style
86
+ // internal codes and getaddrinfo jargon). Just say what we tried.
87
+ console.error(chalk.red(`✗ Couldn't reach the Brigade gateway at ${url}.`));
67
88
  // Differentiate the most common failure modes so users don't waste
68
89
  // time debugging the wrong cause.
69
90
  if (isRefused) {
@@ -75,10 +96,20 @@ export async function runConnectCommand(opts = {}) {
75
96
  console.error(chalk.dim(` Likely cause: gateway is reachable but slow to handshake.`));
76
97
  console.error(chalk.dim(` Check that ${host} resolves and the port isn't blocked by a firewall.`));
77
98
  }
99
+ else if (isUnknownHost) {
100
+ console.error(chalk.dim(` Couldn't resolve "${host}" — check the hostname and your DNS.`));
101
+ }
102
+ else if (isUnreachable) {
103
+ console.error(chalk.dim(` No network route to ${host}. Check your VPN / firewall / network connection.`));
104
+ }
78
105
  else {
79
106
  console.error(chalk.dim(` Start the gateway first: brigade gateway --port ${port}`));
80
107
  console.error(chalk.dim(` Or check the host/port flags: brigade connect --host ${host} --port ${port}`));
81
108
  }
109
+ // Surface the raw message only when the operator opts into debug mode.
110
+ if (process.env.BRIGADE_DEBUG === "1") {
111
+ console.error(chalk.dim(` (debug: ${msg})`));
112
+ }
82
113
  process.exit(1);
83
114
  }
84
115
  chatHandle = await wireConnectUi(tui, client);
@@ -90,7 +121,10 @@ export async function runConnectCommand(opts = {}) {
90
121
  * parsing or process.exit.
91
122
  */
92
123
  export async function wireConnectUi(tui, client) {
93
- renderBrandHeader(tui);
124
+ // Static (last-frame) wordmark — `brigade connect` is the chat surface
125
+ // just like `brigade chat`, so we want the same still rendering here. The
126
+ // looping clip is reserved for onboarding's one-time wow moment.
127
+ renderBrandHeader(tui, { animate: false });
94
128
  const header = new Text("", 0, 0);
95
129
  tui.addChild(header);
96
130
  const divider = new Text(brand.dim("─".repeat(80)), 0, 0);
@@ -126,6 +160,11 @@ export async function wireConnectUi(tui, client) {
126
160
  const editor = new Editor(tui, editorTheme);
127
161
  tui.addChild(editor);
128
162
  tui.setFocus(editor);
163
+ // Connect mode cannot run the provider-onboarding wizard (it writes to the
164
+ // gateway machine's filesystem, which we don't have). We surface that gap
165
+ // inline above the footer so users who notice `/provider` is missing aren't
166
+ // left guessing — they get the exact escape hatch.
167
+ tui.addChild(new Text(brand.dim(" connect-mode commands: /model · /thinking · /compact · /help (use 'brigade chat' on the gateway machine for /provider)"), 0, 0));
129
168
  tui.addChild(new Text(brand.dim(" Enter to send · Ctrl+C abort · Ctrl+D quit · /model /thinking /compact /help"), 0, 0));
130
169
  const insertBeforeEditor = (component) => {
131
170
  const children = tui.children;
@@ -306,6 +345,7 @@ export async function wireConnectUi(tui, client) {
306
345
  if (trimmed === "/exit" || trimmed === "/quit") {
307
346
  client.close();
308
347
  tui.stop();
348
+ restoreTerminal();
309
349
  process.exit(0);
310
350
  }
311
351
  if (trimmed === "/help") {
@@ -21,7 +21,41 @@ import { BRIGADE_DIR, loadConfig } from "../core/config.js";
21
21
  import { getTodayLogPath } from "../core/event-logger.js";
22
22
  import { assembleSystemPrompt, getPromptDir, seedDefaultPrompts, } from "../core/system-prompt.js";
23
23
  import { findProvider, PROVIDERS } from "../providers/catalog.js";
24
+ import { formatAuthLabel, listConfiguredProviders } from "../core/auth-label.js";
25
+ import { getAppliedEnvKeys, loadBrigadeConfig } from "../core/brigade-config.js";
24
26
  import { DEFAULT_PORT } from "../protocol.js";
27
+ /**
28
+ * Translate raw Node `err.message` into a doctor-safe single line. Strips
29
+ * absolute paths from EACCES/ENOENT/EPERM/EBUSY/EISDIR messages (no
30
+ * `C:\\Users\\Bob\\.brigade\\auth.json` leaks into doctor output) and
31
+ * collapses common errno tokens to plain English. Falls back to the raw
32
+ * message when nothing matches — doctor's audience can read raw messages,
33
+ * we just don't want absolute-path leaks.
34
+ */
35
+ function doctorFriendlyError(err) {
36
+ const raw = err instanceof Error ? err.message : String(err);
37
+ if (!raw)
38
+ return "unknown error";
39
+ // Match `EACCES: permission denied, open 'C:\\Users\\…\\file.json'` shape.
40
+ const fileErr = raw.match(/^(EACCES|EPERM|ENOENT|EBUSY|EISDIR|ENOTDIR|EROFS):\s*[^,]+,\s*\w+\s*['"]([^'"]+)['"]/);
41
+ if (fileErr) {
42
+ const code = fileErr[1];
43
+ const fullPath = fileErr[2] ?? "";
44
+ const basename = fullPath.split(/[/\\]/).pop() ?? "";
45
+ const friendly = {
46
+ EACCES: "permission denied",
47
+ EPERM: "permission denied",
48
+ ENOENT: "missing",
49
+ EBUSY: "locked by another process",
50
+ EISDIR: "path is a directory, expected a file",
51
+ ENOTDIR: "path is a file, expected a directory",
52
+ EROFS: "filesystem is read-only",
53
+ };
54
+ return `${friendly[code] ?? code}: ${basename}`;
55
+ }
56
+ // Truncate runaway error messages to keep doctor output scannable.
57
+ return raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
58
+ }
25
59
  /**
26
60
  * Run the full set of checks. Prints a coloured summary to stderr (so the
27
61
  * stdout stream stays free for any future machine-readable mode) and returns
@@ -52,7 +86,7 @@ export async function runDoctorCommand(opts = {}) {
52
86
  checks.push({
53
87
  name: "brigade dir",
54
88
  status: "fail",
55
- detail: `${BRIGADE_DIR} — ${err instanceof Error ? err.message : String(err)}`,
89
+ detail: `${BRIGADE_DIR} — ${doctorFriendlyError(err)}`,
56
90
  });
57
91
  }
58
92
  // ── config.json parses ──────────────────────────────────────────────
@@ -72,7 +106,7 @@ export async function runDoctorCommand(opts = {}) {
72
106
  checks.push({
73
107
  name: "config.json",
74
108
  status: "warn",
75
- detail: `unparseable — ${err instanceof Error ? err.message : String(err)}`,
109
+ detail: `unparseable — ${doctorFriendlyError(err)}`,
76
110
  });
77
111
  }
78
112
  }
@@ -125,12 +159,38 @@ export async function runDoctorCommand(opts = {}) {
125
159
  /* missing key for this provider is normal — skip */
126
160
  }
127
161
  }
162
+ // Auth source per provider — surfaces WHERE each key came from
163
+ // (`brigade.json::env`, a shell env var, or unconfigured) so the
164
+ // user always knows what `rm -rf ~/.brigade` will and won't wipe.
165
+ try {
166
+ const cfg = await loadBrigadeConfig(BRIGADE_DIR);
167
+ const configured = listConfiguredProviders({
168
+ provider: "",
169
+ brigadeEnvBlock: cfg.env,
170
+ processEnv: process.env,
171
+ // Threading the boot-time applied set is what makes the
172
+ // "file" vs "env" distinction honest: if the shell had a
173
+ // key first, loadEnvIntoProcess skipped it, the key is NOT
174
+ // in this set, and we correctly label it "env".
175
+ appliedFromFile: getAppliedEnvKeys(),
176
+ });
177
+ for (const { provider, label } of configured) {
178
+ checks.push({
179
+ name: `auth source · ${provider}`,
180
+ status: "pass",
181
+ detail: formatAuthLabel(label),
182
+ });
183
+ }
184
+ }
185
+ catch {
186
+ /* non-fatal — auth-source reporting is informational */
187
+ }
128
188
  }
129
189
  catch (err) {
130
190
  checks.push({
131
191
  name: "providers",
132
192
  status: "fail",
133
- detail: err instanceof Error ? err.message : String(err),
193
+ detail: doctorFriendlyError(err),
134
194
  });
135
195
  }
136
196
  }
@@ -146,19 +206,29 @@ export async function runDoctorCommand(opts = {}) {
146
206
  checks.push({
147
207
  name: "log sink",
148
208
  status: "warn",
149
- detail: `${logPath} — ${err instanceof Error ? err.message : String(err)}`,
209
+ detail: `${logPath} — ${doctorFriendlyError(err)}`,
150
210
  });
151
211
  }
152
212
  }
153
- // ── prompt directory present + seeded ───────────────────────────────
154
- // Brigade ships 4 default prompt layers (soul / identity / instructions /
155
- // tools). seedDefaultPrompts is idempotent it never overwrites existing
156
- // files, so it's safe to call here too. We then count how many of the
157
- // expected layers exist; missing layers are "warn", not "fail" because
158
- // the assembler always falls back to embedded defaults.
213
+ // ── workspace dir present + persona layers seeded ───────────────────
214
+ // Brigade ships 7 default persona layers at the workspace root
215
+ // (SOUL / IDENTITY / AGENTS / TOOLS / USER / BOOTSTRAP / HEARTBEAT).
216
+ // seedDefaultPrompts is idempotent it never overwrites existing files,
217
+ // so it's safe to call here too. We then count how many of the expected
218
+ // layers exist; missing layers are "warn", not "fail" because the
219
+ // assembler always falls back to embedded defaults.
159
220
  if (brigadeDirOk) {
160
221
  const promptDir = getPromptDir();
161
- const expectedLayers = ["soul.md", "identity.md", "instructions.md", "tools.md"];
222
+ const expectedLayers = [
223
+ "SOUL.md",
224
+ "IDENTITY.md",
225
+ "AGENTS.md",
226
+ "TOOLS.md",
227
+ "USER.md",
228
+ "BOOTSTRAP.md",
229
+ "BOOT.md",
230
+ "HEARTBEAT.md",
231
+ ];
162
232
  try {
163
233
  await seedDefaultPrompts(promptDir);
164
234
  let presentCount = 0;
@@ -198,7 +268,7 @@ export async function runDoctorCommand(opts = {}) {
198
268
  checks.push({
199
269
  name: "prompts",
200
270
  status: "warn",
201
- detail: `${promptDir} — ${err instanceof Error ? err.message : String(err)}`,
271
+ detail: `${promptDir} — ${doctorFriendlyError(err)}`,
202
272
  });
203
273
  }
204
274
  }
@@ -220,7 +290,7 @@ export async function runDoctorCommand(opts = {}) {
220
290
  checks.push({
221
291
  name: "system prompt",
222
292
  status: "fail",
223
- detail: `assembly failed — ${err instanceof Error ? err.message : String(err)}`,
293
+ detail: `assembly failed — ${doctorFriendlyError(err)}`,
224
294
  });
225
295
  }
226
296
  }
@@ -287,7 +357,7 @@ async function probeGateway(url) {
287
357
  resolve({
288
358
  name: "gateway",
289
359
  status: "fail",
290
- detail: `${url} — ${err instanceof Error ? err.message : String(err)}`,
360
+ detail: `${url} — ${doctorFriendlyError(err)}`,
291
361
  });
292
362
  return;
293
363
  }
@@ -309,7 +379,7 @@ async function probeGateway(url) {
309
379
  resolve({
310
380
  name: "gateway",
311
381
  status: "fail",
312
- detail: `${url} — ${err instanceof Error ? err.message : String(err)}`,
382
+ detail: `${url} — ${doctorFriendlyError(err)}`,
313
383
  });
314
384
  });
315
385
  });