@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -0
  4. package/SECURITY.md +208 -0
  5. package/assets/brigade-wordmark-on-black.png +0 -0
  6. package/assets/brigade-wordmark.png +0 -0
  7. package/brigade.mjs +96 -0
  8. package/dist/cli/chat-cmd.js +120 -0
  9. package/dist/cli/config-cmd.js +132 -0
  10. package/dist/cli/connect-cmd.js +447 -0
  11. package/dist/cli/doctor-cmd.js +317 -0
  12. package/dist/cli/gateway-cmd.js +92 -0
  13. package/dist/cli.js +287 -0
  14. package/dist/core/agent.js +1123 -0
  15. package/dist/core/config.js +80 -0
  16. package/dist/core/console-stream.js +188 -0
  17. package/dist/core/error-classifier.js +354 -0
  18. package/dist/core/event-logger.js +122 -0
  19. package/dist/core/model-caps.js +185 -0
  20. package/dist/core/provider-payload-mutators.js +517 -0
  21. package/dist/core/provider-quirks.js +285 -0
  22. package/dist/core/server.js +459 -0
  23. package/dist/core/smart-compaction.js +209 -0
  24. package/dist/core/system-prompt-defaults.js +88 -0
  25. package/dist/core/system-prompt-guidance.js +269 -0
  26. package/dist/core/system-prompt.js +884 -0
  27. package/dist/index.js +30 -0
  28. package/dist/integrations/ollama.js +140 -0
  29. package/dist/protocol.js +49 -0
  30. package/dist/providers/catalog.js +100 -0
  31. package/dist/providers/validate-key.js +197 -0
  32. package/dist/tui/client.js +263 -0
  33. package/dist/ui/brand-frames-cli.js +20 -0
  34. package/dist/ui/brand-frames.js +36 -0
  35. package/dist/ui/brand.js +402 -0
  36. package/dist/ui/chat.js +929 -0
  37. package/dist/ui/onboarding.js +400 -0
  38. package/dist/ui/theme.js +51 -0
  39. package/package.json +92 -0
@@ -0,0 +1,400 @@
1
+ /**
2
+ * First-run onboarding wizard.
3
+ *
4
+ * Flow:
5
+ * 1. Welcome screen
6
+ * 2. Provider picker (SelectList of curated providers)
7
+ * 3. API key entry (or detect existing env var)
8
+ * 4. Model picker (SelectList of models for that provider)
9
+ * 5. Persist to AuthStorage + Brigade config
10
+ * 6. Return chosen { provider, modelId }
11
+ *
12
+ * Uses Pi-TUI components — same components the chat UI uses, so the visual
13
+ * language is consistent across the app.
14
+ */
15
+ import * as path from "node:path";
16
+ import { getEnvApiKey, getModels } from "@mariozechner/pi-ai";
17
+ import { CancellableLoader, Input, SelectList, Text } from "@mariozechner/pi-tui";
18
+ import { BRIGADE_DIR, saveConfig } from "../core/config.js";
19
+ import { discoverOllamaModels, writeOllamaToModelsJson } from "../integrations/ollama.js";
20
+ import { findProvider, PROVIDERS } from "../providers/catalog.js";
21
+ import { validateApiKeyOnline } from "../providers/validate-key.js";
22
+ import { renderBrandHeader } from "./brand.js";
23
+ import { brand, selectListTheme } from "./theme.js";
24
+ /* ────────────────────────────── public API ────────────────────────────── */
25
+ /**
26
+ * Resolve the model list for a provider. Pi's static `getModels()` only knows
27
+ * built-in catalogs (Anthropic, OpenAI, Google, etc.); custom providers we
28
+ * register at runtime via models.json (Ollama) are exposed through
29
+ * `modelRegistry.getAll()`. Try the static catalog first, then fall back to
30
+ * the dynamic registry — that way both code paths produce the same `Model<any>[]`
31
+ * shape that the picker expects.
32
+ */
33
+ function getProviderModels(modelRegistry, providerId) {
34
+ try {
35
+ const fromCatalog = getModels(providerId);
36
+ if (fromCatalog && fromCatalog.length > 0)
37
+ return fromCatalog;
38
+ }
39
+ catch {
40
+ /* unknown provider — fall through */
41
+ }
42
+ return modelRegistry.getAll().filter((m) => m.provider === providerId);
43
+ }
44
+ export async function runOnboarding(tui, authStorage, modelRegistry) {
45
+ // Tiny step state machine so each step can resolve to "ok" or "back".
46
+ // Esc on provider picker exits onboarding entirely; Esc on later steps
47
+ // just rewinds to the previous step. This is what makes invalid keys
48
+ // recoverable instead of a one-shot abort.
49
+ let step = "provider";
50
+ let provider = "";
51
+ let modelId = "";
52
+ while (true) {
53
+ if (step === "provider") {
54
+ renderScreen(tui, "Step 1 of 3 · Pick a provider");
55
+ provider = await pickProvider(tui); // throws "onboarding-cancelled" on Esc
56
+ step = "key";
57
+ continue;
58
+ }
59
+ if (step === "key") {
60
+ // Local providers (Ollama) skip API-key entry entirely. Instead we
61
+ // validate the local server is up, discover its model list, and
62
+ // register the provider in Pi's models.json so step 3 picks it up.
63
+ const providerInfo = findProvider(provider);
64
+ if (providerInfo?.local && providerInfo.id === "ollama") {
65
+ const result = await ensureLocalOllama(tui, modelRegistry, providerInfo.baseUrl ?? "http://localhost:11434");
66
+ if (result === "back") {
67
+ step = "provider";
68
+ continue;
69
+ }
70
+ step = "model";
71
+ continue;
72
+ }
73
+ const result = await ensureApiKey(tui, authStorage, provider);
74
+ if (result === "back") {
75
+ step = "provider";
76
+ continue;
77
+ }
78
+ modelRegistry.refresh(); // re-evaluate available models with the new key
79
+ step = "model";
80
+ continue;
81
+ }
82
+ // step === "model"
83
+ renderScreen(tui, "Step 3 of 3 · Default model");
84
+ const result = await pickModel(tui, modelRegistry, provider);
85
+ if (result === "back") {
86
+ step = "provider"; // go all the way back so they can change provider too
87
+ continue;
88
+ }
89
+ modelId = result.modelId;
90
+ break;
91
+ }
92
+ await saveConfig({ defaultProvider: provider, defaultModelId: modelId });
93
+ renderScreen(tui, ""); // brand-only frame for the "Ready." moment
94
+ renderDone(tui, provider, modelId);
95
+ await delay(900);
96
+ clear(tui);
97
+ return { provider, modelId };
98
+ }
99
+ /* ────────────────────────── screen scaffolding ────────────────────────── */
100
+ /** Wipe everything, render the chunky brand header, then a sub-header line. */
101
+ function renderScreen(tui, subheader) {
102
+ clear(tui);
103
+ renderBrandHeader(tui);
104
+ if (subheader) {
105
+ tui.addChild(new Text(brand.dim(" " + "─".repeat(54)), 0, 0));
106
+ tui.addChild(new Text("", 0, 0));
107
+ tui.addChild(new Text(` ${brand.amber(subheader)}`, 0, 0));
108
+ tui.addChild(new Text("", 0, 0));
109
+ tui.requestRender();
110
+ }
111
+ }
112
+ /* ────────────────────────────── steps ─────────────────────────────────── */
113
+ async function pickProvider(tui) {
114
+ const items = PROVIDERS.map((p) => ({
115
+ value: p.id,
116
+ label: p.name,
117
+ description: p.description,
118
+ }));
119
+ const list = new SelectList(items, Math.min(items.length, 9), selectListTheme, {
120
+ minPrimaryColumnWidth: 18,
121
+ maxPrimaryColumnWidth: 22,
122
+ });
123
+ tui.addChild(list);
124
+ tui.setFocus(list);
125
+ tui.requestRender();
126
+ const chosen = await new Promise((resolve, reject) => {
127
+ list.onSelect = (item) => resolve(item);
128
+ list.onCancel = () => reject(new Error("onboarding-cancelled"));
129
+ });
130
+ return chosen.value;
131
+ }
132
+ /**
133
+ * Prompt for the API key, validate it (locally + online), persist on success.
134
+ * Returns:
135
+ * - "ok" → key is valid and saved (or already existed)
136
+ * - "back" → user pressed Esc; caller should rewind to the previous step
137
+ *
138
+ * Loops on validation failure: the user sees "Wrong key — <reason>" inline
139
+ * and re-enters the field with their previous attempt pre-filled. Esc on
140
+ * the input bails out of the loop with "back" instead of throwing.
141
+ */
142
+ async function ensureApiKey(tui, authStorage, providerId) {
143
+ const provider = findProvider(providerId);
144
+ if (!provider)
145
+ throw new Error(`Unknown provider: ${providerId}`);
146
+ // Already configured? (env var OR previously stored)
147
+ const existing = await authStorage.getApiKey(providerId);
148
+ if (existing) {
149
+ renderScreen(tui, `Step 2 of 3 · ${provider.name}`);
150
+ const fromEnv = !!getEnvApiKey(providerId);
151
+ const source = fromEnv ? "your environment" : "your saved credentials";
152
+ tui.addChild(new Text(` ${brand.amber("✓")} ${provider.name} is already connected (using ${brand.white(source)}).`, 0, 0));
153
+ tui.requestRender();
154
+ await delay(600);
155
+ return "ok";
156
+ }
157
+ // Retry loop. Each iteration re-renders the screen so old error lines and
158
+ // stale loader frames don't pile up vertically.
159
+ let lastError = null;
160
+ while (true) {
161
+ renderScreen(tui, `Step 2 of 3 · Connect ${provider.name}`);
162
+ if (lastError) {
163
+ tui.addChild(new Text(` ${brand.error("✗")} ${brand.error(lastError)}`, 0, 0));
164
+ tui.addChild(new Text(brand.dim(" Press Enter to try again, or Esc to choose a different provider."), 0, 0));
165
+ tui.addChild(new Text("", 0, 0));
166
+ }
167
+ tui.addChild(new Text(` Paste your ${provider.name} API key.`, 0, 0));
168
+ tui.addChild(new Text(brand.dim(` We'll keep it private to this device. Need a key? ${provider.keyUrl}`), 0, 0));
169
+ tui.addChild(new Text(brand.dim(" Enter to continue · Esc to go back"), 0, 0));
170
+ tui.addChild(new Text("", 0, 0));
171
+ const input = new Input();
172
+ tui.addChild(input);
173
+ tui.setFocus(input);
174
+ tui.requestRender();
175
+ let key;
176
+ try {
177
+ key = await new Promise((resolve, reject) => {
178
+ input.onSubmit = (value) => resolve(value.trim());
179
+ input.onEscape = () => reject(new Error("back"));
180
+ });
181
+ }
182
+ catch {
183
+ return "back"; // user pressed Esc — caller rewinds to provider picker
184
+ }
185
+ // Step 1: cheap local format check (length, whitespace, prefix).
186
+ const localCheck = validateApiKey(providerId, key);
187
+ if (!localCheck.ok) {
188
+ lastError = localCheck.reason;
189
+ continue;
190
+ }
191
+ // Step 2: live online validation against the provider's /v1/models endpoint.
192
+ // This catches revoked / wrong-provider / typo'd keys BEFORE we persist.
193
+ tui.addChild(new Text("", 0, 0));
194
+ const loader = new CancellableLoader(tui, (s) => brand.amber(s), (s) => brand.dim(s), `Verifying with ${provider.name}…`);
195
+ tui.addChild(loader);
196
+ tui.requestRender();
197
+ const onlineCheck = await validateApiKeyOnline(providerId, key);
198
+ tui.removeChild(loader);
199
+ if (!onlineCheck.ok) {
200
+ lastError = onlineCheck.reason;
201
+ continue;
202
+ }
203
+ // Step 3: only persist after both checks pass.
204
+ authStorage.set(providerId, { type: "api_key", key });
205
+ authStorage.reload();
206
+ const successLine = onlineCheck.modelCount
207
+ ? `${provider.name} connected · ${onlineCheck.modelCount} model${onlineCheck.modelCount === 1 ? "" : "s"} available`
208
+ : `${provider.name} connected`;
209
+ tui.addChild(new Text(` ${brand.amber("✓")} ${successLine}.`, 0, 0));
210
+ if (onlineCheck.warning) {
211
+ tui.addChild(new Text(` ${brand.dim("Note: " + onlineCheck.warning)}`, 0, 0));
212
+ }
213
+ tui.requestRender();
214
+ await delay(500);
215
+ return "ok";
216
+ }
217
+ }
218
+ /**
219
+ * Cheap, provider-aware sanity check on the pasted key.
220
+ * Catches obvious mistakes (empty, accidental newline, wrong provider's key)
221
+ * BEFORE we persist garbage to disk. Doesn't validate against the real API —
222
+ * that happens implicitly on the first model call.
223
+ */
224
+ function validateApiKey(providerId, key) {
225
+ if (!key)
226
+ return { ok: false, reason: "Please enter an API key." };
227
+ if (key.length < 16)
228
+ return { ok: false, reason: `That looks incomplete (only ${key.length} characters). Try copying the key again.` };
229
+ if (/\s/.test(key))
230
+ return { ok: false, reason: "The key has extra spaces or line breaks. Copy just the key value." };
231
+ // Provider-specific prefix hints. Hard reject only when we have a stable, well-known prefix
232
+ // for that provider — we never want to block someone with a freshly-rotated format.
233
+ // For providers without a stable prefix (cerebras, mistral) we fall through to length+whitespace
234
+ // only, which is intentional.
235
+ const prefixHints = {
236
+ anthropic: "sk-ant-",
237
+ openai: "sk-",
238
+ google: "AIza", // Google API keys (Gemini Studio) all start with AIza
239
+ groq: "gsk_",
240
+ openrouter: "sk-or-",
241
+ xai: "xai-",
242
+ deepseek: "sk-", // DeepSeek mirrors OpenAI's prefix convention
243
+ };
244
+ const expected = prefixHints[providerId];
245
+ const providerName = findProvider(providerId)?.name ?? providerId;
246
+ if (expected && !key.startsWith(expected)) {
247
+ return {
248
+ ok: false,
249
+ reason: `That doesn't look like a ${providerName} key (${providerName} keys start with "${expected}"). Make sure you picked the right provider.`,
250
+ };
251
+ }
252
+ return { ok: true };
253
+ }
254
+ /**
255
+ * Local-provider variant of `ensureApiKey`. For Ollama:
256
+ * 1. Ping `/api/tags` to confirm the daemon is running.
257
+ * 2. List the user's locally-pulled models.
258
+ * 3. Write the provider config (and inferred model definitions) into
259
+ * `~/.brigade/models.json` so Pi's `ModelRegistry` exposes them.
260
+ * 4. Refresh the registry so the next step's model picker can see them.
261
+ *
262
+ * If discovery fails, show a friendly error with the next step the user
263
+ * needs to take (start the daemon, pull a model). Esc returns "back".
264
+ */
265
+ async function ensureLocalOllama(tui, modelRegistry, baseUrl) {
266
+ let lastError = null;
267
+ while (true) {
268
+ renderScreen(tui, "Step 2 of 3 · Connect Ollama");
269
+ if (lastError) {
270
+ tui.addChild(new Text(` ${brand.error("✗")} ${brand.error(lastError)}`, 0, 0));
271
+ tui.addChild(new Text(brand.dim(" Press Enter to try again, or Esc to choose a different provider."), 0, 0));
272
+ tui.addChild(new Text("", 0, 0));
273
+ }
274
+ tui.addChild(new Text(` Brigade will scan your local Ollama for available models.`, 0, 0));
275
+ tui.addChild(new Text(brand.dim(` Don't have Ollama yet? Get it at https://ollama.com/download`), 0, 0));
276
+ tui.addChild(new Text(brand.dim(" Enter to scan · Esc to go back"), 0, 0));
277
+ // Use an Input as a confirm-prompt — user just hits Enter (or Esc).
278
+ const confirm = new Input();
279
+ tui.addChild(confirm);
280
+ tui.setFocus(confirm);
281
+ tui.requestRender();
282
+ try {
283
+ await new Promise((resolve, reject) => {
284
+ confirm.onSubmit = () => resolve();
285
+ confirm.onEscape = () => reject(new Error("back"));
286
+ });
287
+ }
288
+ catch {
289
+ return "back";
290
+ }
291
+ // Discover models with a loader spinner.
292
+ const loader = new CancellableLoader(tui, (s) => brand.amber(s), (s) => brand.dim(s), "Scanning Ollama…");
293
+ tui.addChild(loader);
294
+ tui.requestRender();
295
+ let discovered;
296
+ try {
297
+ discovered = await discoverOllamaModels(baseUrl);
298
+ }
299
+ catch (err) {
300
+ tui.removeChild(loader);
301
+ lastError = err instanceof Error ? err.message : String(err);
302
+ continue;
303
+ }
304
+ // Persist the provider entry so Pi sees the models from now on.
305
+ const modelsJsonPath = path.join(BRIGADE_DIR, "models.json");
306
+ try {
307
+ await writeOllamaToModelsJson(modelsJsonPath, baseUrl, discovered);
308
+ modelRegistry.refresh();
309
+ }
310
+ catch (err) {
311
+ tui.removeChild(loader);
312
+ lastError = `Couldn't save the connection: ${err instanceof Error ? err.message : String(err)}`;
313
+ continue;
314
+ }
315
+ tui.removeChild(loader);
316
+ tui.addChild(new Text(` ${brand.amber("✓")} Ollama connected · ${brand.white(String(discovered.length))} model${discovered.length === 1 ? "" : "s"} available.`, 0, 0));
317
+ tui.requestRender();
318
+ await delay(500);
319
+ return "ok";
320
+ }
321
+ }
322
+ async function pickModel(tui, modelRegistry, providerId) {
323
+ const models = getProviderModels(modelRegistry, providerId);
324
+ if (models.length === 0) {
325
+ tui.addChild(new Text(brand.dim(" Type the model name you'd like to use, then press Enter. Esc to go back."), 0, 0));
326
+ const input = new Input();
327
+ tui.addChild(input);
328
+ tui.setFocus(input);
329
+ tui.requestRender();
330
+ try {
331
+ const id = await new Promise((resolve, reject) => {
332
+ input.onSubmit = (value) => resolve(value.trim());
333
+ input.onEscape = () => reject(new Error("back"));
334
+ });
335
+ return { modelId: id };
336
+ }
337
+ catch {
338
+ return "back";
339
+ }
340
+ }
341
+ const items = models.map((m) => ({
342
+ value: m.id,
343
+ label: m.id,
344
+ description: describeModel(m),
345
+ }));
346
+ // Default-first ordering: reasoning > non-reasoning, then larger context first.
347
+ items.sort((a, b) => {
348
+ const ma = models.find((m) => m.id === a.value);
349
+ const mb = models.find((m) => m.id === b.value);
350
+ if (!ma || !mb)
351
+ return 0;
352
+ if (!!ma.reasoning !== !!mb.reasoning)
353
+ return ma.reasoning ? -1 : 1;
354
+ return (mb.contextWindow ?? 0) - (ma.contextWindow ?? 0);
355
+ });
356
+ const list = new SelectList(items, Math.min(items.length, 12), selectListTheme, {
357
+ minPrimaryColumnWidth: 26,
358
+ maxPrimaryColumnWidth: 38,
359
+ });
360
+ tui.addChild(list);
361
+ tui.setFocus(list);
362
+ tui.requestRender();
363
+ try {
364
+ const chosen = await new Promise((resolve, reject) => {
365
+ list.onSelect = (item) => resolve(item);
366
+ list.onCancel = () => reject(new Error("back"));
367
+ });
368
+ return { modelId: chosen.value };
369
+ }
370
+ catch {
371
+ return "back";
372
+ }
373
+ }
374
+ function renderDone(tui, provider, modelId) {
375
+ const p = findProvider(provider)?.name ?? provider;
376
+ tui.addChild(new Text("", 0, 0));
377
+ tui.addChild(new Text(` ${brand.amber("●")} ${brand.white("Ready.")} ${brand.dim(`${p} · ${modelId}`)}`, 0, 0));
378
+ tui.addChild(new Text("", 0, 0));
379
+ tui.requestRender();
380
+ }
381
+ /* ────────────────────────────── helpers ───────────────────────────────── */
382
+ function clear(tui) {
383
+ for (const child of [...tui.children])
384
+ tui.removeChild(child);
385
+ tui.requestRender();
386
+ }
387
+ function describeModel(m) {
388
+ const parts = [];
389
+ if (m.reasoning)
390
+ parts.push("reasoning");
391
+ if (m.contextWindow)
392
+ parts.push(`${Math.round(m.contextWindow / 1000)}k ctx`);
393
+ if (m.cost?.input)
394
+ parts.push(`$${m.cost.input.toFixed(2)}/Mtok in`);
395
+ return parts.join(" · ");
396
+ }
397
+ function delay(ms) {
398
+ return new Promise((r) => setTimeout(r, ms));
399
+ }
400
+ //# sourceMappingURL=onboarding.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shared Pi-TUI themes for Brigade.
3
+ *
4
+ * One canonical place so onboarding + chat feel like the same app.
5
+ */
6
+ import chalk from "chalk";
7
+ export const markdownTheme = {
8
+ heading: (s) => chalk.bold.hex("#fbbf24")(s), // amber
9
+ link: (s) => chalk.hex("#60a5fa")(s),
10
+ linkUrl: (s) => chalk.dim(s),
11
+ code: (s) => chalk.hex("#fde7c4")(s),
12
+ codeBlock: (s) => chalk.hex("#a7f3d0")(s),
13
+ codeBlockBorder: (s) => chalk.dim.hex("#92400e")(s),
14
+ quote: (s) => chalk.italic.hex("#fde7c4")(s),
15
+ quoteBorder: (s) => chalk.dim.hex("#92400e")(s),
16
+ hr: (s) => chalk.dim(s),
17
+ listBullet: (s) => chalk.hex("#fbbf24")(s),
18
+ bold: (s) => chalk.bold(s),
19
+ italic: (s) => chalk.italic(s),
20
+ strikethrough: (s) => chalk.strikethrough(s),
21
+ underline: (s) => chalk.underline(s),
22
+ };
23
+ export const editorTheme = {
24
+ borderColor: (s) => chalk.dim(s),
25
+ selectList: {
26
+ selectedPrefix: (s) => chalk.hex("#fbbf24")(s),
27
+ selectedText: (s) => chalk.bold(s),
28
+ description: (s) => chalk.dim(s),
29
+ scrollInfo: (s) => chalk.dim(s),
30
+ noMatch: (s) => chalk.dim(s),
31
+ },
32
+ };
33
+ export const selectListTheme = {
34
+ selectedPrefix: (s) => chalk.hex("#fbbf24")(s),
35
+ selectedText: (s) => chalk.bold(s),
36
+ description: (s) => chalk.dim(s),
37
+ scrollInfo: (s) => chalk.dim(s),
38
+ noMatch: (s) => chalk.dim(" No matching option"),
39
+ };
40
+ /** Brand color helpers (used for status bars, dividers, accents). */
41
+ export const brand = {
42
+ amber: (s) => chalk.hex("#fbbf24")(s),
43
+ amberDeep: (s) => chalk.hex("#92400e")(s),
44
+ dim: (s) => chalk.dim(s),
45
+ white: (s) => chalk.white(s),
46
+ user: (s) => chalk.bold.hex("#60a5fa")(s),
47
+ agent: (s) => chalk.bold.hex("#fbbf24")(s),
48
+ tool: (s) => chalk.hex("#a7f3d0")(s),
49
+ error: (s) => chalk.bold.hex("#fca5a5")(s),
50
+ };
51
+ //# sourceMappingURL=theme.js.map
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@spinabot/brigade",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Brigade — your personal AI crew. Polished terminal CLI for Anthropic, OpenAI, Gemini, Groq, xAI, DeepSeek, Mistral, OpenRouter, Cerebras, Ollama and any OpenAI-compatible endpoint. Built on the Pi SDK.",
6
+ "license": "MIT",
7
+ "author": "Bhasvanth Dev",
8
+ "homepage": "https://github.com/Bhasvanth-Dev9380/brigade#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/Bhasvanth-Dev9380/brigade/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/Bhasvanth-Dev9380/brigade.git"
15
+ },
16
+ "engines": {
17
+ "node": ">=20.0.0"
18
+ },
19
+ "bin": {
20
+ "brigade": "brigade.mjs"
21
+ },
22
+ "main": "./dist/index.js",
23
+ "files": [
24
+ "brigade.mjs",
25
+ "dist",
26
+ "!dist/**/*.map",
27
+ "README.md",
28
+ "LICENSE",
29
+ "CHANGELOG.md",
30
+ "SECURITY.md",
31
+ "assets/brigade-wordmark.png",
32
+ "assets/brigade-wordmark-on-black.png"
33
+ ],
34
+ "keywords": [
35
+ "ai",
36
+ "cli",
37
+ "tui",
38
+ "llm",
39
+ "chat",
40
+ "agent",
41
+ "anthropic",
42
+ "claude",
43
+ "openai",
44
+ "gpt",
45
+ "gemini",
46
+ "ollama",
47
+ "openrouter",
48
+ "groq",
49
+ "xai",
50
+ "deepseek",
51
+ "mistral",
52
+ "cerebras",
53
+ "pi-sdk"
54
+ ],
55
+ "scripts": {
56
+ "dev": "tsx src/cli.ts",
57
+ "chat": "tsx src/cli.ts chat",
58
+ "gateway": "tsx src/cli.ts gateway",
59
+ "connect": "tsx src/cli.ts connect",
60
+ "doctor": "tsx src/cli.ts doctor",
61
+ "onboard": "tsx src/cli.ts onboard",
62
+ "build": "tsc -p tsconfig.json",
63
+ "start": "node dist/cli.js",
64
+ "typecheck": "tsc --noEmit",
65
+ "test": "vitest run",
66
+ "test:watch": "vitest",
67
+ "format": "biome format --write .",
68
+ "gen:wordmark": "node scripts/gen-wordmark-svg.mjs",
69
+ "prepublishOnly": "npm run build",
70
+ "prepack": "node scripts/prepack.mjs"
71
+ },
72
+ "publishConfig": {
73
+ "access": "public"
74
+ },
75
+ "dependencies": {
76
+ "@mariozechner/pi-agent-core": "0.70.6",
77
+ "@mariozechner/pi-ai": "0.70.6",
78
+ "@mariozechner/pi-coding-agent": "0.70.6",
79
+ "@mariozechner/pi-tui": "0.70.6",
80
+ "@types/ws": "^8.18.1",
81
+ "cfonts": "^3.3.1",
82
+ "chalk": "^5.3.0",
83
+ "ws": "^8.20.0"
84
+ },
85
+ "devDependencies": {
86
+ "@biomejs/biome": "^1.9.4",
87
+ "@types/node": "^22.7.0",
88
+ "tsx": "^4.19.0",
89
+ "typescript": "^5.6.0",
90
+ "vitest": "^2.1.0"
91
+ }
92
+ }