@spinabot/brigade 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/SECURITY.md +208 -0
- package/assets/brigade-wordmark-on-black.png +0 -0
- package/assets/brigade-wordmark.png +0 -0
- package/brigade.mjs +96 -0
- package/dist/cli/chat-cmd.js +120 -0
- package/dist/cli/config-cmd.js +132 -0
- package/dist/cli/connect-cmd.js +447 -0
- package/dist/cli/doctor-cmd.js +317 -0
- package/dist/cli/gateway-cmd.js +92 -0
- package/dist/cli.js +287 -0
- package/dist/core/agent.js +1123 -0
- package/dist/core/config.js +80 -0
- package/dist/core/console-stream.js +188 -0
- package/dist/core/error-classifier.js +354 -0
- package/dist/core/event-logger.js +122 -0
- package/dist/core/model-caps.js +185 -0
- package/dist/core/provider-payload-mutators.js +517 -0
- package/dist/core/provider-quirks.js +285 -0
- package/dist/core/server.js +459 -0
- package/dist/core/smart-compaction.js +209 -0
- package/dist/core/system-prompt-defaults.js +88 -0
- package/dist/core/system-prompt-guidance.js +269 -0
- package/dist/core/system-prompt.js +884 -0
- package/dist/index.js +30 -0
- package/dist/integrations/ollama.js +140 -0
- package/dist/protocol.js +49 -0
- package/dist/providers/catalog.js +100 -0
- package/dist/providers/validate-key.js +197 -0
- package/dist/tui/client.js +263 -0
- package/dist/ui/brand-frames-cli.js +20 -0
- package/dist/ui/brand-frames.js +36 -0
- package/dist/ui/brand.js +402 -0
- package/dist/ui/chat.js +929 -0
- package/dist/ui/onboarding.js +400 -0
- package/dist/ui/theme.js +51 -0
- package/package.json +92 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Brigade — your personal AI crew.
|
|
4
|
+
*
|
|
5
|
+
* Single-binary CLI with subcommands, like `git`.
|
|
6
|
+
*
|
|
7
|
+
* brigade # = brigade chat (default)
|
|
8
|
+
* brigade chat # in-process TUI + Pi session
|
|
9
|
+
* brigade gateway # WebSocket gateway server, no TUI
|
|
10
|
+
* brigade connect # TUI that talks to a running gateway
|
|
11
|
+
* brigade onboard # provider/model setup wizard
|
|
12
|
+
* brigade doctor # local health check
|
|
13
|
+
* brigade config list|get|set|unset # ~/.brigade/config.json
|
|
14
|
+
* brigade --version
|
|
15
|
+
* brigade --help
|
|
16
|
+
*
|
|
17
|
+
* All subcommands forward unrecognised flags through to the subcommand
|
|
18
|
+
* handler — the dispatcher itself only knows about top-level flags.
|
|
19
|
+
*
|
|
20
|
+
* IMPORTANT: do not add an external CLI library. The argv shape is small
|
|
21
|
+
* enough for hand-parsing. Adding a dependency would inflate the install
|
|
22
|
+
* and slow `brigade --version`.
|
|
23
|
+
*/
|
|
24
|
+
import process from "node:process";
|
|
25
|
+
import { pathToFileURL } from "node:url";
|
|
26
|
+
import chalk from "chalk";
|
|
27
|
+
import { runChatCommand } from "./cli/chat-cmd.js";
|
|
28
|
+
import { runConfigCommand } from "./cli/config-cmd.js";
|
|
29
|
+
import { runConnectCommand } from "./cli/connect-cmd.js";
|
|
30
|
+
import { runDoctorCommand } from "./cli/doctor-cmd.js";
|
|
31
|
+
import { runGatewayCommand } from "./cli/gateway-cmd.js";
|
|
32
|
+
import { DEFAULT_PORT } from "./protocol.js";
|
|
33
|
+
const VERSION = "0.1.0";
|
|
34
|
+
const HELP = `${chalk.bold("brigade")} — your personal AI crew
|
|
35
|
+
|
|
36
|
+
${chalk.dim("usage:")}
|
|
37
|
+
brigade [subcommand] [flags]
|
|
38
|
+
|
|
39
|
+
${chalk.dim("subcommands:")}
|
|
40
|
+
${chalk.bold("chat")} start the in-process TUI (default if no subcommand)
|
|
41
|
+
${chalk.bold("gateway")} run the WebSocket gateway server, no TUI
|
|
42
|
+
${chalk.bold("connect")} TUI that talks to a running gateway
|
|
43
|
+
${chalk.bold("onboard")} run the provider/model setup wizard
|
|
44
|
+
${chalk.bold("doctor")} check the local environment is healthy
|
|
45
|
+
${chalk.bold("config")} read & write ~/.brigade/config.json
|
|
46
|
+
|
|
47
|
+
${chalk.dim("flags (all subcommands respect):")}
|
|
48
|
+
--help show help (per-subcommand if applicable)
|
|
49
|
+
--version print version
|
|
50
|
+
|
|
51
|
+
${chalk.dim("flags (gateway):")}
|
|
52
|
+
--port <n> listen port (default: ${DEFAULT_PORT}; env: BRIGADE_PORT)
|
|
53
|
+
--host <addr> bind address (default: 127.0.0.1)
|
|
54
|
+
--verbose bump log level to debug (token deltas + intra-message events)
|
|
55
|
+
--quiet disable the live console stream (JSONL log only)
|
|
56
|
+
--log-level <l> error | warn | info | debug (default: info — overrides --verbose)
|
|
57
|
+
|
|
58
|
+
${chalk.dim("flags (connect):")}
|
|
59
|
+
--host <addr> gateway host (default: 127.0.0.1)
|
|
60
|
+
--port <n> gateway port (default: ${DEFAULT_PORT})
|
|
61
|
+
--timeout <ms> per-request timeout (default: 60000)
|
|
62
|
+
|
|
63
|
+
${chalk.dim("flags (doctor):")}
|
|
64
|
+
--gateway <url> probe a running gateway URL too
|
|
65
|
+
|
|
66
|
+
${chalk.dim("examples:")}
|
|
67
|
+
brigade # start chatting
|
|
68
|
+
brigade gateway --port 7777 # daemon mode, terminal #1
|
|
69
|
+
brigade connect --port 7777 # client mode, terminal #2
|
|
70
|
+
brigade doctor --gateway ws://127.0.0.1:7777
|
|
71
|
+
brigade config set defaultModelId claude-sonnet-4-6
|
|
72
|
+
`;
|
|
73
|
+
/** Flags that NEVER take a value — listing them here prevents the next-token
|
|
74
|
+
* consumption trap. Add new boolean flags here when introducing them. */
|
|
75
|
+
export const BOOLEAN_FLAGS = new Set([
|
|
76
|
+
"help",
|
|
77
|
+
"h",
|
|
78
|
+
"version",
|
|
79
|
+
"v",
|
|
80
|
+
"verbose",
|
|
81
|
+
"quiet",
|
|
82
|
+
"force",
|
|
83
|
+
]);
|
|
84
|
+
export function parseArgs(argv) {
|
|
85
|
+
const positional = [];
|
|
86
|
+
const flags = {};
|
|
87
|
+
let sawSeparator = false;
|
|
88
|
+
for (let i = 0; i < argv.length; i++) {
|
|
89
|
+
const tok = argv[i];
|
|
90
|
+
if (sawSeparator) {
|
|
91
|
+
positional.push(tok);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (tok === "--") {
|
|
95
|
+
sawSeparator = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (tok.startsWith("--")) {
|
|
99
|
+
const body = tok.slice(2);
|
|
100
|
+
const eq = body.indexOf("=");
|
|
101
|
+
if (eq >= 0) {
|
|
102
|
+
// --flag=value — explicit, never consumes the next token
|
|
103
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const key = body;
|
|
107
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
108
|
+
flags[key] = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// Value flag: consume next token UNLESS it's another flag or
|
|
112
|
+
// missing entirely. A flag with no value is treated as `true`
|
|
113
|
+
// so callers that didn't expect it can still detect it.
|
|
114
|
+
const next = argv[i + 1];
|
|
115
|
+
if (next !== undefined && !next.startsWith("--") && next !== "--") {
|
|
116
|
+
flags[key] = next;
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
flags[key] = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
positional.push(tok);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { positional, flags };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The subcommand registry. Adding a new subcommand:
|
|
131
|
+
* 1. Build src/cli/<name>-cmd.ts exporting an async runXxxCommand(opts)
|
|
132
|
+
* 2. Add an entry here that maps the literal to a handler
|
|
133
|
+
* 3. Mention it in HELP above
|
|
134
|
+
*/
|
|
135
|
+
const SUBCOMMANDS = {
|
|
136
|
+
chat: async () => {
|
|
137
|
+
await runChatCommand();
|
|
138
|
+
},
|
|
139
|
+
gateway: async ({ flags }) => {
|
|
140
|
+
const port = typeof flags.port === "string"
|
|
141
|
+
? Number(flags.port)
|
|
142
|
+
: undefined;
|
|
143
|
+
const host = typeof flags.host === "string" ? flags.host : undefined;
|
|
144
|
+
const verbose = flags.verbose === true;
|
|
145
|
+
const quiet = flags.quiet === true;
|
|
146
|
+
const logLevelRaw = typeof flags["log-level"] === "string" ? flags["log-level"] : undefined;
|
|
147
|
+
if (port !== undefined && (!Number.isFinite(port) || port < 1 || port > 65_535)) {
|
|
148
|
+
process.stderr.write(chalk.red(`invalid --port value: ${flags.port}\n`));
|
|
149
|
+
return 2;
|
|
150
|
+
}
|
|
151
|
+
if (verbose && quiet) {
|
|
152
|
+
process.stderr.write(chalk.red(`--verbose and --quiet are mutually exclusive\n`));
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
const validLevels = ["error", "warn", "info", "debug"];
|
|
156
|
+
if (logLevelRaw && !validLevels.includes(logLevelRaw)) {
|
|
157
|
+
process.stderr.write(chalk.red(`invalid --log-level value: ${logLevelRaw} (use error|warn|info|debug)\n`));
|
|
158
|
+
return 2;
|
|
159
|
+
}
|
|
160
|
+
const logLevel = logLevelRaw;
|
|
161
|
+
await runGatewayCommand({ port, host, verbose, quiet, logLevel });
|
|
162
|
+
// runGatewayCommand resolves once the server is listening; the process
|
|
163
|
+
// stays alive on its own (the WebSocketServer + http.Server hold the
|
|
164
|
+
// event loop). Don't return — let the process keep running until SIGINT.
|
|
165
|
+
await new Promise(() => { });
|
|
166
|
+
},
|
|
167
|
+
connect: async ({ flags }) => {
|
|
168
|
+
const host = typeof flags.host === "string" ? flags.host : undefined;
|
|
169
|
+
const port = typeof flags.port === "string"
|
|
170
|
+
? Number(flags.port)
|
|
171
|
+
: undefined;
|
|
172
|
+
const timeout = typeof flags.timeout === "string"
|
|
173
|
+
? Number(flags.timeout)
|
|
174
|
+
: undefined;
|
|
175
|
+
if (port !== undefined && (!Number.isFinite(port) || port < 1 || port > 65_535)) {
|
|
176
|
+
process.stderr.write(chalk.red(`invalid --port value: ${flags.port}\n`));
|
|
177
|
+
return 2;
|
|
178
|
+
}
|
|
179
|
+
await runConnectCommand({ host, port, requestTimeoutMs: timeout });
|
|
180
|
+
// Same as gateway — the TUI's stdin handler keeps the process alive.
|
|
181
|
+
await new Promise(() => { });
|
|
182
|
+
},
|
|
183
|
+
onboard: async () => {
|
|
184
|
+
// Onboarding lives inside chat-cmd's boot path today (it runs when no
|
|
185
|
+
// saved config is present). For an explicit `brigade onboard`, force
|
|
186
|
+
// the wizard by deleting the saved default before booting chat — the
|
|
187
|
+
// wizard re-runs and writes a fresh default.
|
|
188
|
+
//
|
|
189
|
+
// Implemented as "chat with forced re-onboarding" instead of duplicating
|
|
190
|
+
// the wizard call site so future onboarding changes (e.g., MCP setup)
|
|
191
|
+
// stay in one place.
|
|
192
|
+
const fs = await import("node:fs/promises");
|
|
193
|
+
const path = await import("node:path");
|
|
194
|
+
const os = await import("node:os");
|
|
195
|
+
const cfgPath = path.join(os.homedir(), ".brigade", "config.json");
|
|
196
|
+
try {
|
|
197
|
+
const existing = JSON.parse(await fs.readFile(cfgPath, "utf8"));
|
|
198
|
+
delete existing.defaultProvider;
|
|
199
|
+
delete existing.defaultModelId;
|
|
200
|
+
await fs.writeFile(cfgPath, JSON.stringify(existing, null, 2), "utf8");
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
/* missing config is fine — wizard will run regardless */
|
|
204
|
+
}
|
|
205
|
+
await runChatCommand();
|
|
206
|
+
},
|
|
207
|
+
doctor: async ({ flags }) => {
|
|
208
|
+
const gatewayUrl = typeof flags.gateway === "string" ? flags.gateway : undefined;
|
|
209
|
+
await runDoctorCommand({ gatewayUrl });
|
|
210
|
+
// runDoctorCommand calls process.exit on its own.
|
|
211
|
+
},
|
|
212
|
+
config: async ({ positional }) => {
|
|
213
|
+
// positional[0] is the subcommand (e.g. "set"); the rest are its args.
|
|
214
|
+
const result = await runConfigCommand(positional);
|
|
215
|
+
return result.exitCode;
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Top-level dispatch. Exported so tests can call directly without spawning
|
|
220
|
+
* a subprocess.
|
|
221
|
+
*/
|
|
222
|
+
export async function runCli(argv) {
|
|
223
|
+
const parsed = parseArgs(argv);
|
|
224
|
+
// Top-level flags first.
|
|
225
|
+
if (parsed.flags.version === true || parsed.flags.v === true) {
|
|
226
|
+
process.stdout.write(`brigade ${VERSION}\n`);
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
if (parsed.flags.help === true || parsed.flags.h === true) {
|
|
230
|
+
process.stdout.write(HELP);
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
// Default subcommand: chat
|
|
234
|
+
const sub = parsed.positional[0] ?? "chat";
|
|
235
|
+
// Help on a specific subcommand: `brigade <sub> --help` falls through to
|
|
236
|
+
// the per-subcommand handler so it can print its own usage if it wants
|
|
237
|
+
// to. For now they don't have rich per-subcommand help — surface the
|
|
238
|
+
// global one as a fallback.
|
|
239
|
+
if (!(sub in SUBCOMMANDS)) {
|
|
240
|
+
process.stderr.write(chalk.red(`unknown subcommand: ${sub}\n\n`));
|
|
241
|
+
process.stderr.write(HELP);
|
|
242
|
+
return 2;
|
|
243
|
+
}
|
|
244
|
+
// Strip the subcommand from positional before forwarding.
|
|
245
|
+
const handler = SUBCOMMANDS[sub];
|
|
246
|
+
const subArgs = {
|
|
247
|
+
positional: parsed.positional.slice(1),
|
|
248
|
+
flags: parsed.flags,
|
|
249
|
+
};
|
|
250
|
+
try {
|
|
251
|
+
const code = await handler(subArgs);
|
|
252
|
+
return typeof code === "number" ? code : 0;
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
256
|
+
process.stderr.write(chalk.red(`brigade ${sub}: ${msg}\n`));
|
|
257
|
+
if (err instanceof Error && err.stack) {
|
|
258
|
+
process.stderr.write(chalk.dim(`${err.stack}\n`));
|
|
259
|
+
}
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Entry point — only runs when this file is invoked directly (not when
|
|
265
|
+
* imported by tests). Mirrors the canonical Node pattern.
|
|
266
|
+
*/
|
|
267
|
+
const entry = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
|
|
268
|
+
if (import.meta.url === entry) {
|
|
269
|
+
runCli(process.argv.slice(2))
|
|
270
|
+
.then((code) => {
|
|
271
|
+
// Most subcommands keep the loop alive (gateway, connect, chat).
|
|
272
|
+
// Only short-lived ones (config, doctor, --help) reach this point.
|
|
273
|
+
if (typeof code === "number")
|
|
274
|
+
process.exit(code);
|
|
275
|
+
})
|
|
276
|
+
.catch((err) => {
|
|
277
|
+
try {
|
|
278
|
+
process.stdout.write("\x1b[?25h\x1b[?1049l");
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
/* terminal already gone */
|
|
282
|
+
}
|
|
283
|
+
console.error(chalk.red(`Brigade crashed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=cli.js.map
|