@spinabot/brigade 0.1.0 → 0.1.1
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 +41 -10
- package/LICENSE +21 -21
- package/README.md +11 -2
- package/dist/cli/chat-cmd.js +74 -3
- package/dist/cli/config-cmd.js +40 -1
- package/dist/cli/connect-cmd.js +42 -2
- package/dist/cli/doctor-cmd.js +85 -15
- package/dist/cli/gateway-cmd.js +69 -6
- package/dist/cli.js +53 -12
- package/dist/core/agent.js +3 -3
- package/dist/core/auth-error.js +111 -0
- package/dist/core/auth-label.js +147 -0
- package/dist/core/brigade-config.js +730 -0
- package/dist/core/cli-error.js +94 -0
- package/dist/core/config.js +182 -7
- package/dist/core/exec-approvals.js +337 -0
- package/dist/core/server.js +36 -0
- package/dist/core/system-prompt-defaults.js +221 -45
- package/dist/core/system-prompt-guidance.js +2 -0
- package/dist/core/system-prompt.js +842 -102
- package/dist/core/version.js +14 -0
- package/dist/index.js +8 -6
- package/dist/protocol.js +15 -0
- package/dist/ui/brand.js +31 -15
- package/dist/ui/chat.js +90 -11
- package/dist/ui/onboarding.js +218 -15
- package/dist/ui/terminal-cleanup.js +132 -0
- package/package.json +2 -3
- package/assets/brigade-wordmark-on-black.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.1.0
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1](https://github.com/Bhasvanth-Dev9380/brigade/compare/v0.1.0...v0.1.1) (2026-05-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add CI and release workflows, configure automated npm publishing, and enhance README with badges ([7cda18d](https://github.com/Bhasvanth-Dev9380/brigade/commit/7cda18dfecd320fb0cf70553a64baecf291175fb))
|
|
9
|
+
* add injected-content defence section to AGENTS.md and update image layout in README.md ([7180544](https://github.com/Bhasvanth-Dev9380/brigade/commit/718054476cc02687cbc84fc79b9e029ec894568e))
|
|
10
|
+
* **brand:** enhance layout handling for terminal width and add wordmark indentation ([0f68c83](https://github.com/Bhasvanth-Dev9380/brigade/commit/0f68c8357f27d966ce4b4cfa0bf6450cf999f7bc))
|
|
11
|
+
* **brand:** enhance layout handling for terminal widths and add scrollback-friendly rendering ([28b280b](https://github.com/Bhasvanth-Dev9380/brigade/commit/28b280bc3ffdcbd3b60e7937885eda8995ee3d9b))
|
|
12
|
+
* enhance README with new images for dark mode and adjust wordmark size; add demo GIF ([cd9c607](https://github.com/Bhasvanth-Dev9380/brigade/commit/cd9c6074fff29b793bb486cef4a5dc91e262b2c7))
|
|
13
|
+
* enhance README with responsive wordmark and add new image for dark mode ([5de16f7](https://github.com/Bhasvanth-Dev9380/brigade/commit/5de16f7118114c209f6eea46e078cec18ae5da75))
|
|
14
|
+
* implement Brigade-native auth error translation and guidance messages; add tests for auth-error handling ([1f57ba5](https://github.com/Bhasvanth-Dev9380/brigade/commit/1f57ba567701e77f83ee24ae50bf8e776a6cba2a))
|
|
15
|
+
* implement ephemeral system prompt feature with sanitization and context framing ([cd9c607](https://github.com/Bhasvanth-Dev9380/brigade/commit/cd9c6074fff29b793bb486cef4a5dc91e262b2c7))
|
|
16
|
+
* implement injected text sanitization and defensive framing for project context ([5de16f7](https://github.com/Bhasvanth-Dev9380/brigade/commit/5de16f7118114c209f6eea46e078cec18ae5da75))
|
|
17
|
+
* **tests:** add comprehensive CLI tests for argument parsing, dispatching, and command execution ([1c398c5](https://github.com/Bhasvanth-Dev9380/brigade/commit/1c398c5611349a5765022306478d7ba133a7613d))
|
|
18
|
+
* **tests:** add comprehensive tests for onboarding, provider commands, and error handling ([c1d5a77](https://github.com/Bhasvanth-Dev9380/brigade/commit/c1d5a77238bf5f02e8a20054671140b0169f7e63))
|
|
19
|
+
* update README and add brigade wordmark image ([55a2a1b](https://github.com/Bhasvanth-Dev9380/brigade/commit/55a2a1b9f7c1f8cae51307052e4603713865e3bd))
|
|
20
|
+
* update README with new image layout, add demo GIFs, and clean up package.json assets ([378529a](https://github.com/Bhasvanth-Dev9380/brigade/commit/378529a6b815f1aaab557bccca8a18e589c013c8))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* adjust image layout and alignment in README ([a80268b](https://github.com/Bhasvanth-Dev9380/brigade/commit/a80268b1fb1d363306f27dcd93b1a290442ab3c4))
|
|
26
|
+
* **tests:** add connect-cmd footer note for /provider command visibility ([2367deb](https://github.com/Bhasvanth-Dev9380/brigade/commit/2367deb4b445740bd556ed22b3acac924fd2bd4a))
|
|
27
|
+
* update demo image link in README ([f688f01](https://github.com/Bhasvanth-Dev9380/brigade/commit/f688f01d92e56805aeaa78b8da6b7aa40d619c16))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Refactor
|
|
31
|
+
|
|
32
|
+
* **tests:** enhance brigade-config migration tests to prevent product name leakage ([2367deb](https://github.com/Bhasvanth-Dev9380/brigade/commit/2367deb4b445740bd556ed22b3acac924fd2bd4a))
|
|
33
|
+
|
|
34
|
+
## 0.1.0 — 2026-05-02
|
|
35
|
+
|
|
36
|
+
- Initial release.
|
|
37
|
+
- Polished terminal TUI for Anthropic, OpenAI, Gemini, Groq, xAI, DeepSeek, Mistral, OpenRouter, Cerebras, Ollama, and any OpenAI-compatible endpoint.
|
|
38
|
+
- Subcommands: `brigade chat`, `brigade gateway`, `brigade connect`, `brigade onboard`, `brigade doctor`, `brigade config`.
|
|
39
|
+
- Provider onboarding wizard on first launch.
|
|
40
|
+
- `/model`, `/provider`, `/thinking`, `/help` in-chat commands.
|
|
41
|
+
- 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,12 +1,21 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://github.com/user-attachments/assets/e739669f-1e77-4093-bb72-6b5c9d4ca3db#gh-light-mode-only" alt="BRIGADE — your personal AI crew" width="900" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
<p>
|
|
2
6
|
<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
7
|
<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" />
|
|
6
8
|
</p>
|
|
7
9
|
|
|
8
10
|
<br clear="all" />
|
|
9
11
|
|
|
12
|
+
<p>
|
|
13
|
+
<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>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@spinabot/brigade"><img src="https://img.shields.io/npm/v/@spinabot/brigade" alt="npm version" /></a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/@spinabot/brigade"><img src="https://img.shields.io/npm/l/@spinabot/brigade" alt="license" /></a>
|
|
16
|
+
<a href="https://www.npmjs.com/package/@spinabot/brigade"><img src="https://img.shields.io/node/v/@spinabot/brigade" alt="node engine" /></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
10
19
|
Brigade is a polished terminal interface for working with the world's
|
|
11
20
|
leading language models — Anthropic Claude, OpenAI GPT, Google Gemini,
|
|
12
21
|
Groq, xAI, DeepSeek, Mistral, OpenRouter, Cerebras — and your own local
|
package/dist/cli/chat-cmd.js
CHANGED
|
@@ -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({
|
|
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
|
package/dist/cli/config-cmd.js
CHANGED
|
@@ -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 };
|
package/dist/cli/connect-cmd.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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") {
|
package/dist/cli/doctor-cmd.js
CHANGED
|
@@ -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} — ${
|
|
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 — ${
|
|
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:
|
|
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} — ${
|
|
209
|
+
detail: `${logPath} — ${doctorFriendlyError(err)}`,
|
|
150
210
|
});
|
|
151
211
|
}
|
|
152
212
|
}
|
|
153
|
-
// ──
|
|
154
|
-
// Brigade ships
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
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 = [
|
|
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} — ${
|
|
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 — ${
|
|
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} — ${
|
|
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} — ${
|
|
382
|
+
detail: `${url} — ${doctorFriendlyError(err)}`,
|
|
313
383
|
});
|
|
314
384
|
});
|
|
315
385
|
});
|