brainstorming 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/dist/index.js +1495 -0
  4. package/package.json +48 -0
package/dist/index.js ADDED
@@ -0,0 +1,1495 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import React4 from "react";
5
+ import { render as render2 } from "ink";
6
+
7
+ // ../../packages/core/src/mentions.ts
8
+ var MENTION_RE = /@([a-z0-9][a-z0-9._:-]*)/gi;
9
+ var ALL_MENTION = "all";
10
+ function parseMentions(content, roster) {
11
+ const known = new Set(roster.map((n) => n.toLowerCase()));
12
+ const found = [];
13
+ for (const match of content.matchAll(MENTION_RE)) {
14
+ const name = match[1].toLowerCase();
15
+ if (name === ALL_MENTION) return [...roster];
16
+ if (known.has(name) && !found.includes(name)) found.push(name);
17
+ }
18
+ return found;
19
+ }
20
+
21
+ // ../../packages/core/src/transcript.ts
22
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
23
+ import { dirname } from "path";
24
+ var TranscriptStore = class _TranscriptStore {
25
+ #messages = [];
26
+ #filePath;
27
+ constructor(filePath) {
28
+ this.#filePath = filePath;
29
+ if (filePath) mkdirSync(dirname(filePath), { recursive: true });
30
+ }
31
+ static load(filePath) {
32
+ const store = new _TranscriptStore(filePath);
33
+ if (existsSync(filePath)) {
34
+ for (const line of readFileSync(filePath, "utf8").split("\n")) {
35
+ if (line.trim()) store.#messages.push(JSON.parse(line));
36
+ }
37
+ }
38
+ return store;
39
+ }
40
+ append(msg) {
41
+ this.#messages.push(msg);
42
+ if (this.#filePath) appendFileSync(this.#filePath, JSON.stringify(msg) + "\n");
43
+ }
44
+ all() {
45
+ return this.#messages;
46
+ }
47
+ get length() {
48
+ return this.#messages.length;
49
+ }
50
+ };
51
+
52
+ // ../../packages/core/src/router.ts
53
+ function resolveTargets(args2) {
54
+ if (args2.mentions.length > 0) return [...args2.mentions];
55
+ return [...args2.sticky];
56
+ }
57
+
58
+ // ../../packages/core/src/prompt.ts
59
+ function formatLine(msg) {
60
+ return `[${msg.author}]: ${msg.content}`;
61
+ }
62
+ function renderPrompt(input) {
63
+ const lines = [];
64
+ if (input.digest.length > 0) {
65
+ lines.push("[Chat since your last turn]");
66
+ for (const m of input.digest) lines.push(formatLine(m));
67
+ lines.push("---");
68
+ }
69
+ lines.push("[You are addressed]");
70
+ lines.push(formatLine(input.addressed));
71
+ return lines.join("\n");
72
+ }
73
+
74
+ // ../../packages/core/src/fake-adapter.ts
75
+ import { randomUUID } from "crypto";
76
+ import { setTimeout as sleep } from "timers/promises";
77
+ var FakeAdapter = class {
78
+ constructor(name, script = {}) {
79
+ this.name = name;
80
+ this.#script = { replies: [], defaultReply: "(no reply)", chunkDelayMs: 0, ...script };
81
+ }
82
+ name;
83
+ capabilities = { tools: false, steering: false, resume: false };
84
+ lastInput = null;
85
+ #script;
86
+ async start(_ctx) {
87
+ }
88
+ async interrupt() {
89
+ }
90
+ async stop() {
91
+ return void 0;
92
+ }
93
+ async *prompt(input, signal) {
94
+ this.lastInput = input;
95
+ const rule = this.#script.replies.find(
96
+ (r) => !r.match || r.match.test(input.addressed.content)
97
+ );
98
+ if (rule?.error) {
99
+ yield { type: "error", error: { message: rule.error, fatal: false } };
100
+ return;
101
+ }
102
+ let reply = rule?.reply ?? this.#script.defaultReply;
103
+ let denied = false;
104
+ if (rule?.permission) {
105
+ let resolveDecision;
106
+ const decision = new Promise((res) => resolveDecision = res);
107
+ yield {
108
+ type: "permission-request",
109
+ request: {
110
+ id: randomUUID(),
111
+ agent: this.name,
112
+ action: rule.permission.action,
113
+ preview: rule.permission.preview
114
+ },
115
+ respond: (d) => resolveDecision(d)
116
+ };
117
+ denied = await decision === "deny";
118
+ if (denied) reply = rule.permission.denyReply ?? "Understood, I won't.";
119
+ }
120
+ if (rule?.activity && !denied) yield { type: "activity", activity: rule.activity };
121
+ for (const chunk of reply.split(/(?<= )/)) {
122
+ if (signal.aborted) return;
123
+ if (this.#script.chunkDelayMs > 0) await sleep(this.#script.chunkDelayMs);
124
+ yield { type: "text-delta", text: chunk };
125
+ }
126
+ yield { type: "done", finalText: reply };
127
+ }
128
+ };
129
+
130
+ // ../../packages/core/src/persona.ts
131
+ function buildPersona(opts) {
132
+ const others = opts.roster.filter((n) => n !== opts.name);
133
+ const lines = [
134
+ `You are @${opts.name}, one of several AI agents collaborating with a human in a shared group chat over a codebase.`,
135
+ others.length > 0 ? `Other agents in the room: ${others.map((n) => "@" + n).join(", ")}. The human user is here too.` : `The human user is here too.`,
136
+ `Messages appear as "[author]: text". To address someone directly, write @their-name; write @all to ask everyone.`,
137
+ `Keep chat replies short and concrete. When you are asked to do real work, do it in the shared workspace.`,
138
+ `Only speak when you are addressed or can add clear value \u2014 avoid repeating what others already said.`
139
+ ];
140
+ const extra = opts.extra?.trim();
141
+ if (extra) lines.push(extra);
142
+ return lines.join("\n");
143
+ }
144
+
145
+ // ../../packages/core/src/room.ts
146
+ import { randomUUID as randomUUID2 } from "crypto";
147
+ var Room = class {
148
+ #transcript;
149
+ #adapters = /* @__PURE__ */ new Map();
150
+ #cursors = /* @__PURE__ */ new Map();
151
+ #sticky = [];
152
+ #listeners = /* @__PURE__ */ new Set();
153
+ #roundBudget;
154
+ #abort = null;
155
+ #pending = [];
156
+ constructor(opts) {
157
+ this.#transcript = opts.transcript;
158
+ for (const a of opts.adapters) this.#adapters.set(a.name, a);
159
+ this.#roundBudget = opts.roundBudget ?? 3;
160
+ }
161
+ get roster() {
162
+ return [...this.#adapters.keys()];
163
+ }
164
+ get stickyTargets() {
165
+ return [...this.#sticky];
166
+ }
167
+ get pendingMentions() {
168
+ return [...this.#pending];
169
+ }
170
+ get transcript() {
171
+ return this.#transcript;
172
+ }
173
+ setRoundBudget(n) {
174
+ this.#roundBudget = n;
175
+ }
176
+ /**
177
+ * Mark the entire current transcript as already seen by every agent.
178
+ * Used when resuming a room so agents receive only NEW messages as digests
179
+ * (their pre-resume context comes from their backend session or ctx.transcript).
180
+ */
181
+ markAllSeen() {
182
+ const n = this.#transcript.length;
183
+ for (const name of this.#adapters.keys()) this.#cursors.set(name, n);
184
+ }
185
+ on(fn) {
186
+ this.#listeners.add(fn);
187
+ return () => this.#listeners.delete(fn);
188
+ }
189
+ async start(ctx) {
190
+ await Promise.all([...this.#adapters.values()].map((a) => a.start(ctx)));
191
+ }
192
+ async sendUserMessage(content) {
193
+ const mentions = parseMentions(content, this.roster);
194
+ const targets = resolveTargets({ mentions, sticky: this.#sticky });
195
+ if (targets.length === 0) return { status: "needs-target" };
196
+ this.#sticky = targets;
197
+ const msg = this.#append("user", content, "chat", targets);
198
+ await this.#runCascade([{ to: targets, message: msg }]);
199
+ return { status: "sent" };
200
+ }
201
+ async continueCascade() {
202
+ if (this.#pending.length === 0) return;
203
+ const pending = this.#pending;
204
+ this.#pending = [];
205
+ await this.#runCascade(pending.map((p) => ({ to: [p.to], message: p.message })));
206
+ }
207
+ interrupt() {
208
+ this.#abort?.abort();
209
+ }
210
+ /** Append a system note to the transcript (e.g. a recorded decision). */
211
+ note(content) {
212
+ return this.#append("system", content, "system", []);
213
+ }
214
+ #emit(ev) {
215
+ for (const fn of this.#listeners) fn(ev);
216
+ }
217
+ #append(author, content, kind, mentions) {
218
+ const msg = { id: randomUUID2(), ts: Date.now(), author, content, mentions, kind };
219
+ this.#transcript.append(msg);
220
+ this.#emit({ type: "message", message: msg });
221
+ return msg;
222
+ }
223
+ #status(agent, status) {
224
+ this.#emit({ type: "agent-status", agent, status });
225
+ }
226
+ async #runCascade(firstWave) {
227
+ const ac = new AbortController();
228
+ this.#abort = ac;
229
+ let wave = firstWave;
230
+ let round = 0;
231
+ try {
232
+ while (wave.length > 0) {
233
+ const deliveries = wave.flatMap((d) => d.to.map((agent) => ({ agent, message: d.message })));
234
+ const replies = await Promise.all(
235
+ deliveries.map((d) => this.#deliver(d.agent, d.message, ac.signal))
236
+ );
237
+ if (ac.signal.aborted) {
238
+ this.#append("system", "cascade interrupted", "system", []);
239
+ return;
240
+ }
241
+ const next = [];
242
+ for (const reply of replies) {
243
+ if (!reply) continue;
244
+ const targets = reply.mentions.filter((t) => t !== reply.author);
245
+ if (targets.length > 0) next.push({ to: targets, message: reply });
246
+ }
247
+ if (next.length === 0) return;
248
+ round += 1;
249
+ if (round > this.#roundBudget) {
250
+ this.#pending = next.flatMap(
251
+ (d) => d.to.map((to) => ({ from: d.message.author, to, message: d.message }))
252
+ );
253
+ this.#append(
254
+ "system",
255
+ `round budget (${this.#roundBudget}) exhausted \u2014 pending: ` + this.#pending.map((p) => `${p.from}\u2192@${p.to}`).join(", ") + " \u2014 use /continue to let them proceed",
256
+ "system",
257
+ []
258
+ );
259
+ this.#emit({ type: "budget-exhausted", pending: this.pendingMentions });
260
+ return;
261
+ }
262
+ wave = next;
263
+ }
264
+ } finally {
265
+ this.#abort = null;
266
+ }
267
+ }
268
+ async #deliver(agentName, addressed, signal) {
269
+ const adapter = this.#adapters.get(agentName);
270
+ if (!adapter || signal.aborted) return null;
271
+ const snapshot = this.#transcript.all();
272
+ const cursor = this.#cursors.get(agentName) ?? 0;
273
+ const digest = snapshot.slice(cursor).filter(
274
+ (m) => m.id !== addressed.id && m.author !== agentName && m.kind !== "activity" && m.kind !== "permission"
275
+ );
276
+ this.#cursors.set(agentName, snapshot.length);
277
+ this.#status(agentName, "thinking");
278
+ let acc = "";
279
+ let final = null;
280
+ try {
281
+ for await (const ev of adapter.prompt({ digest, addressed }, signal)) {
282
+ switch (ev.type) {
283
+ case "text-delta":
284
+ acc += ev.text;
285
+ this.#emit({ type: "stream-delta", agent: agentName, text: ev.text });
286
+ break;
287
+ case "activity":
288
+ this.#append(agentName, `\u25B8 ${ev.activity.title}`, "activity", []);
289
+ break;
290
+ case "permission-request": {
291
+ this.#status(agentName, "awaiting-permission");
292
+ const respond = (d) => {
293
+ this.#status(agentName, "thinking");
294
+ ev.respond(d);
295
+ };
296
+ this.#emit({ type: "permission", agent: agentName, request: ev.request, respond });
297
+ break;
298
+ }
299
+ case "usage":
300
+ break;
301
+ // surfaced in a later phase
302
+ case "done":
303
+ final = ev.finalText;
304
+ break;
305
+ case "error":
306
+ this.#emit({ type: "agent-error", agent: agentName, error: ev.error });
307
+ this.#append("system", `@${agentName} failed: ${ev.error.message}`, "system", []);
308
+ return null;
309
+ }
310
+ }
311
+ } catch (err) {
312
+ if (!signal.aborted) {
313
+ const error = { message: String(err), fatal: false };
314
+ this.#emit({ type: "agent-error", agent: agentName, error });
315
+ this.#append("system", `@${agentName} failed: ${error.message}`, "system", []);
316
+ }
317
+ return null;
318
+ } finally {
319
+ this.#status(agentName, "idle");
320
+ }
321
+ if (signal.aborted) return null;
322
+ const text = (final ?? acc).trim();
323
+ if (!text) return null;
324
+ const mentions = parseMentions(text, this.roster).filter((n) => n !== agentName);
325
+ return this.#append(agentName, text, "chat", mentions);
326
+ }
327
+ };
328
+
329
+ // ../../packages/core/src/room-store.ts
330
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
331
+ import { join } from "path";
332
+ var RoomStore = class {
333
+ #dir;
334
+ constructor(workspaceDir) {
335
+ this.#dir = join(workspaceDir, ".brainstorming");
336
+ mkdirSync2(this.#dir, { recursive: true });
337
+ }
338
+ get dir() {
339
+ return this.#dir;
340
+ }
341
+ get transcriptPath() {
342
+ return join(this.#dir, "room.jsonl");
343
+ }
344
+ get metaPath() {
345
+ return join(this.#dir, "room.json");
346
+ }
347
+ get exists() {
348
+ return existsSync2(this.metaPath);
349
+ }
350
+ loadTranscript() {
351
+ return TranscriptStore.load(this.transcriptPath);
352
+ }
353
+ loadMeta() {
354
+ if (!existsSync2(this.metaPath)) return null;
355
+ try {
356
+ return JSON.parse(readFileSync2(this.metaPath, "utf8"));
357
+ } catch {
358
+ return null;
359
+ }
360
+ }
361
+ saveMeta(meta) {
362
+ writeFileSync(this.metaPath, JSON.stringify(meta, null, 2) + "\n");
363
+ }
364
+ };
365
+
366
+ // ../../packages/tui/src/app.tsx
367
+ import { useCallback, useState as useState3 } from "react";
368
+ import { Box as Box6, Text as Text6, useApp, useInput as useInput2 } from "ink";
369
+
370
+ // ../../packages/tui/src/use-room.ts
371
+ import { useEffect, useState } from "react";
372
+ function useRoom(room) {
373
+ const [messages, setMessages] = useState([...room.transcript.all()]);
374
+ const [live, setLive] = useState(/* @__PURE__ */ new Map());
375
+ const [statuses, setStatuses] = useState(
376
+ new Map(room.roster.map((n) => [n, "idle"]))
377
+ );
378
+ const [permissions, setPermissions] = useState([]);
379
+ const [notice, setNotice] = useState(null);
380
+ useEffect(() => {
381
+ return room.on((ev) => {
382
+ switch (ev.type) {
383
+ case "message":
384
+ setMessages([...room.transcript.all()]);
385
+ if (ev.message.author !== "user" && ev.message.author !== "system") {
386
+ setLive((prev) => {
387
+ const next = new Map(prev);
388
+ next.delete(ev.message.author);
389
+ return next;
390
+ });
391
+ }
392
+ break;
393
+ case "stream-delta":
394
+ setLive((prev) => {
395
+ const next = new Map(prev);
396
+ next.set(ev.agent, (next.get(ev.agent) ?? "") + ev.text);
397
+ return next;
398
+ });
399
+ break;
400
+ case "agent-status":
401
+ setStatuses((prev) => new Map(prev).set(ev.agent, ev.status));
402
+ if (ev.status === "idle") {
403
+ setLive((prev) => {
404
+ const next = new Map(prev);
405
+ next.delete(ev.agent);
406
+ return next;
407
+ });
408
+ }
409
+ break;
410
+ case "permission": {
411
+ const prompt = {
412
+ agent: ev.agent,
413
+ request: ev.request,
414
+ respond: ev.respond
415
+ };
416
+ setPermissions((prev) => [...prev, prompt]);
417
+ break;
418
+ }
419
+ case "budget-exhausted":
420
+ setNotice("Round budget exhausted \u2014 /continue to let agents proceed.");
421
+ break;
422
+ case "agent-error":
423
+ setNotice(`@${ev.agent} error: ${ev.error.message}`);
424
+ break;
425
+ }
426
+ });
427
+ }, [room]);
428
+ return { messages, live, statuses, permissions, notice };
429
+ }
430
+
431
+ // ../../packages/tui/src/components/transcript.tsx
432
+ import { Box, Static, Text } from "ink";
433
+
434
+ // ../../packages/tui/src/theme.ts
435
+ var PALETTE = ["yellow", "green", "blue", "magenta", "red", "white"];
436
+ function authorColor(name) {
437
+ if (name === "user") return "cyan";
438
+ if (name === "system") return "gray";
439
+ let hash = 0;
440
+ for (const ch of name) hash = hash * 31 + ch.charCodeAt(0) >>> 0;
441
+ return PALETTE[hash % PALETTE.length];
442
+ }
443
+
444
+ // ../../packages/tui/src/components/transcript.tsx
445
+ import { jsx, jsxs } from "react/jsx-runtime";
446
+ function Transcript({ messages }) {
447
+ return /* @__PURE__ */ jsx(Static, { items: messages, children: (m) => /* @__PURE__ */ jsx(Box, { marginBottom: m.kind === "chat" ? 1 : 0, children: m.kind === "chat" ? /* @__PURE__ */ jsxs(Text, { children: [
448
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: authorColor(m.author), children: [
449
+ "[",
450
+ m.author,
451
+ "]"
452
+ ] }),
453
+ /* @__PURE__ */ jsxs(Text, { children: [
454
+ " ",
455
+ m.content
456
+ ] })
457
+ ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, italic: m.kind === "system", children: m.kind === "system" ? `\u2014 ${m.content} \u2014` : m.content }) }, m.id) });
458
+ }
459
+
460
+ // ../../packages/tui/src/components/live-blocks.tsx
461
+ import { Box as Box2, Text as Text2 } from "ink";
462
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
463
+ function LiveBlocks({ live }) {
464
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: [...live.entries()].map(([agent, text]) => /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { children: [
465
+ /* @__PURE__ */ jsxs2(Text2, { bold: true, color: authorColor(agent), children: [
466
+ "[",
467
+ agent,
468
+ "]"
469
+ ] }),
470
+ /* @__PURE__ */ jsxs2(Text2, { children: [
471
+ " ",
472
+ text
473
+ ] }),
474
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u258B" })
475
+ ] }) }, agent)) });
476
+ }
477
+
478
+ // ../../packages/tui/src/components/status-bar.tsx
479
+ import { Box as Box3, Text as Text3 } from "ink";
480
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
481
+ var DOT = {
482
+ idle: "\u25CF",
483
+ thinking: "\u25D0",
484
+ "awaiting-permission": "\u25CD"
485
+ };
486
+ function StatusBar({
487
+ title,
488
+ statuses
489
+ }) {
490
+ return /* @__PURE__ */ jsxs3(Box3, { borderStyle: "round", paddingX: 1, justifyContent: "space-between", children: [
491
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: title }),
492
+ /* @__PURE__ */ jsx3(Text3, { children: [...statuses.entries()].map(([name, status]) => /* @__PURE__ */ jsxs3(Text3, { children: [
493
+ " ",
494
+ /* @__PURE__ */ jsx3(Text3, { color: status === "idle" ? "gray" : authorColor(name), children: DOT[status] }),
495
+ /* @__PURE__ */ jsxs3(Text3, { children: [
496
+ " ",
497
+ name
498
+ ] })
499
+ ] }, name)) })
500
+ ] });
501
+ }
502
+
503
+ // ../../packages/tui/src/components/chat-input.tsx
504
+ import { useRef, useState as useState2 } from "react";
505
+ import { Box as Box4, Text as Text4, useInput } from "ink";
506
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
507
+ function mentionPrefix(value) {
508
+ const m = /(?:^|\s)@([a-z0-9._:-]*)$/i.exec(value);
509
+ return m ? m[1].toLowerCase() : null;
510
+ }
511
+ function suggestions(value, roster) {
512
+ const prefix = mentionPrefix(value);
513
+ if (prefix === null) return [];
514
+ return ["all", ...roster].filter((n) => n.startsWith(prefix) && n !== prefix);
515
+ }
516
+ function ChatInput({
517
+ roster,
518
+ onSubmit,
519
+ disabled
520
+ }) {
521
+ const [value, setValue] = useState2("");
522
+ const valueRef = useRef("");
523
+ const setVal = (next) => {
524
+ valueRef.current = next;
525
+ setValue(next);
526
+ };
527
+ const hints = suggestions(value, roster);
528
+ useInput(
529
+ (input, key) => {
530
+ if (key.return) {
531
+ const line = valueRef.current.trim();
532
+ if (line) {
533
+ setVal("");
534
+ onSubmit(line);
535
+ }
536
+ return;
537
+ }
538
+ if (key.backspace || key.delete) {
539
+ setVal(valueRef.current.slice(0, -1));
540
+ return;
541
+ }
542
+ if (key.tab) {
543
+ const prefix = mentionPrefix(valueRef.current);
544
+ const options = prefix === null ? [] : suggestions(valueRef.current, roster);
545
+ if (prefix !== null && options.length > 0) {
546
+ setVal(valueRef.current.slice(0, valueRef.current.length - prefix.length) + options[0] + " ");
547
+ }
548
+ return;
549
+ }
550
+ if (input && !key.ctrl && !key.meta && !key.escape) setVal(valueRef.current + input);
551
+ },
552
+ { isActive: !disabled }
553
+ );
554
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
555
+ /* @__PURE__ */ jsx4(Box4, { borderStyle: "round", paddingX: 1, children: /* @__PURE__ */ jsxs4(Text4, { children: [
556
+ "> ",
557
+ value,
558
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, children: " " })
559
+ ] }) }),
560
+ hints.length > 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
561
+ "tab: ",
562
+ hints.map((h) => "@" + h).join(" ")
563
+ ] }) : null
564
+ ] });
565
+ }
566
+
567
+ // ../../packages/tui/src/components/permission-card.tsx
568
+ import { Box as Box5, Text as Text5 } from "ink";
569
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
570
+ function PermissionCard({ prompt }) {
571
+ return /* @__PURE__ */ jsxs5(Box5, { borderStyle: "double", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [
572
+ /* @__PURE__ */ jsxs5(Text5, { children: [
573
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, color: "yellow", children: [
574
+ "PERMISSION",
575
+ " "
576
+ ] }),
577
+ /* @__PURE__ */ jsxs5(Text5, { bold: true, color: authorColor(prompt.agent), children: [
578
+ "@",
579
+ prompt.agent
580
+ ] }),
581
+ /* @__PURE__ */ jsxs5(Text5, { children: [
582
+ " wants to ",
583
+ prompt.request.action
584
+ ] })
585
+ ] }),
586
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: prompt.request.preview }),
587
+ /* @__PURE__ */ jsxs5(Text5, { children: [
588
+ /* @__PURE__ */ jsx5(Text5, { color: "green", children: "[i]" }),
589
+ " allow once ",
590
+ /* @__PURE__ */ jsx5(Text5, { color: "green", children: "[o]" }),
591
+ " allow for session",
592
+ " ",
593
+ /* @__PURE__ */ jsx5(Text5, { color: "red", children: "[r]" }),
594
+ " deny"
595
+ ] })
596
+ ] });
597
+ }
598
+
599
+ // ../../packages/tui/src/commands.ts
600
+ function parseCommand(line) {
601
+ if (!line.startsWith("/")) return { kind: "message", content: line };
602
+ const [name, ...rest] = line.slice(1).split(/\s+/);
603
+ switch (name) {
604
+ case "quit":
605
+ return { kind: "quit" };
606
+ case "continue":
607
+ return { kind: "continue" };
608
+ case "help":
609
+ return { kind: "help" };
610
+ case "decide": {
611
+ const text = rest.join(" ").trim();
612
+ if (text) return { kind: "decide", text };
613
+ return { kind: "unknown", name: "decide" };
614
+ }
615
+ case "budget": {
616
+ const n = Number(rest[0]);
617
+ if (Number.isInteger(n) && n > 0) return { kind: "budget", n };
618
+ return { kind: "unknown", name: "budget" };
619
+ }
620
+ default:
621
+ return { kind: "unknown", name };
622
+ }
623
+ }
624
+ var HELP_TEXT = "commands: /decide <text> /continue /budget N /help /quit \u2014 mention with @name or @all; ESC interrupts";
625
+
626
+ // ../../packages/tui/src/app.tsx
627
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
628
+ function App({
629
+ room,
630
+ title = "brainstorming",
631
+ onDecide
632
+ }) {
633
+ const view = useRoom(room);
634
+ const { exit } = useApp();
635
+ const [localNotice, setLocalNotice] = useState3(null);
636
+ const [answered, setAnswered] = useState3([]);
637
+ const activeCard = view.permissions.find((p) => !answered.includes(p)) ?? null;
638
+ const answer = (card, decision) => {
639
+ setAnswered((prev) => [...prev, card]);
640
+ card.respond(decision);
641
+ };
642
+ useInput2((input, key) => {
643
+ if (key.escape) {
644
+ room.interrupt();
645
+ return;
646
+ }
647
+ if (activeCard) {
648
+ if (input === "i") answer(activeCard, "allow-once");
649
+ else if (input === "o") answer(activeCard, "allow-session");
650
+ else if (input === "r") answer(activeCard, "deny");
651
+ }
652
+ });
653
+ const handleSubmit = useCallback(
654
+ (line) => {
655
+ setLocalNotice(null);
656
+ const cmd = parseCommand(line);
657
+ switch (cmd.kind) {
658
+ case "quit":
659
+ exit();
660
+ return;
661
+ case "help":
662
+ setLocalNotice(HELP_TEXT);
663
+ return;
664
+ case "budget":
665
+ room.setRoundBudget(cmd.n);
666
+ setLocalNotice(`round budget set to ${cmd.n}`);
667
+ return;
668
+ case "continue":
669
+ void room.continueCascade();
670
+ return;
671
+ case "decide":
672
+ room.note(`DECISION: ${cmd.text}`);
673
+ onDecide?.(cmd.text);
674
+ setLocalNotice(`decision recorded: ${cmd.text}`);
675
+ return;
676
+ case "unknown":
677
+ setLocalNotice(`unknown command: /${cmd.name} \u2014 ${HELP_TEXT}`);
678
+ return;
679
+ case "message":
680
+ void room.sendUserMessage(cmd.content).then((res) => {
681
+ if (res.status === "needs-target") {
682
+ setLocalNotice("No target: mention someone, e.g. @claude or @all.");
683
+ }
684
+ });
685
+ }
686
+ },
687
+ [room, exit, onDecide]
688
+ );
689
+ const notice = localNotice ?? view.notice;
690
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
691
+ /* @__PURE__ */ jsx6(StatusBar, { title, statuses: view.statuses }),
692
+ /* @__PURE__ */ jsx6(Transcript, { messages: view.messages }),
693
+ /* @__PURE__ */ jsx6(LiveBlocks, { live: view.live }),
694
+ activeCard ? /* @__PURE__ */ jsx6(PermissionCard, { prompt: activeCard }) : null,
695
+ notice ? /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: notice }) : null,
696
+ /* @__PURE__ */ jsx6(ChatInput, { roster: room.roster, onSubmit: handleSubmit, disabled: activeCard !== null })
697
+ ] });
698
+ }
699
+
700
+ // src/demo.ts
701
+ function demoAdapters() {
702
+ return [
703
+ new FakeAdapter("claude", {
704
+ chunkDelayMs: 15,
705
+ replies: [
706
+ {
707
+ match: /rest|graphql|api/i,
708
+ reply: "I lean REST here \u2014 simpler caching. @codex do you agree?"
709
+ },
710
+ { match: /ping|pong/i, reply: "@codex pong" }
711
+ ],
712
+ defaultReply: "Interesting. What is the goal?"
713
+ }),
714
+ new FakeAdapter("codex", {
715
+ chunkDelayMs: 15,
716
+ replies: [
717
+ { match: /do you agree/i, reply: "Agreed \u2014 versioned REST plus OpenAPI. I can scaffold it." },
718
+ { match: /ping|pong/i, reply: "@claude ping" },
719
+ {
720
+ match: /test/i,
721
+ reply: "Ran the tests \u2014 all green.",
722
+ permission: {
723
+ action: "run command",
724
+ preview: "pnpm test",
725
+ denyReply: "Okay, skipping the test run."
726
+ },
727
+ activity: { kind: "command", title: "ran: pnpm test (12 passed)", status: "ok" }
728
+ }
729
+ ],
730
+ defaultReply: "Give me a concrete task and I will do it."
731
+ }),
732
+ new FakeAdapter("antigravity", {
733
+ chunkDelayMs: 15,
734
+ defaultReply: "Alternative view: consider the long-term client list before deciding."
735
+ }),
736
+ new FakeAdapter("ollama", {
737
+ chunkDelayMs: 15,
738
+ defaultReply: "Opinion: keep it boring and ship."
739
+ })
740
+ ];
741
+ }
742
+
743
+ // src/run.ts
744
+ import { execFileSync as execFileSync2 } from "child_process";
745
+ import { appendFileSync as appendFileSync2 } from "fs";
746
+ import { join as join3 } from "path";
747
+ import React3 from "react";
748
+ import { render } from "ink";
749
+
750
+ // src/agents.ts
751
+ import { execFileSync } from "child_process";
752
+
753
+ // ../../packages/adapters/src/antigravity.ts
754
+ import { spawn as nodeSpawn } from "child_process";
755
+ import readline from "readline";
756
+ var AntigravityAdapter = class {
757
+ constructor(name = "antigravity", opts = {}) {
758
+ this.name = name;
759
+ this.#bin = opts.binPath ?? process.env.AGY_BIN ?? "agy";
760
+ this.#model = opts.model;
761
+ this.#permissionMode = opts.permissionMode ?? "accept-edits";
762
+ this.#printTimeout = opts.printTimeout ?? "30m";
763
+ this.#spawn = opts.spawn ?? this.#defaultSpawn;
764
+ }
765
+ name;
766
+ capabilities = { tools: true, steering: false, resume: true };
767
+ #conversationId;
768
+ #cwd = process.cwd();
769
+ #persona = "";
770
+ #child = null;
771
+ #bin;
772
+ #model;
773
+ #permissionMode;
774
+ #printTimeout;
775
+ #spawn;
776
+ /** The captured conversation id, once a turn has run (persist to resume later). */
777
+ get sessionId() {
778
+ return this.#conversationId;
779
+ }
780
+ #defaultSpawn = (args2, opts) => {
781
+ const child = nodeSpawn(this.#bin, args2, {
782
+ cwd: opts.cwd,
783
+ stdio: ["ignore", "pipe", "pipe"],
784
+ env: process.env
785
+ });
786
+ if (!child.stdout || !child.stderr) throw new Error("agy: stdout/stderr pipes unavailable");
787
+ return {
788
+ stdout: child.stdout,
789
+ stderr: child.stderr,
790
+ on: (event, listener) => child.on(event, listener),
791
+ kill: (signal) => void child.kill(signal)
792
+ };
793
+ };
794
+ async start(ctx) {
795
+ this.#cwd = ctx.workspaceDir;
796
+ this.#persona = ctx.persona;
797
+ this.#conversationId = ctx.savedSessionId ?? this.#conversationId;
798
+ }
799
+ async interrupt() {
800
+ this.#child?.kill("SIGTERM");
801
+ }
802
+ async stop() {
803
+ this.#child?.kill("SIGTERM");
804
+ this.#child = null;
805
+ return this.#conversationId;
806
+ }
807
+ #buildArgs(promptText) {
808
+ const args2 = ["--output-format", "stream-json", "--print-timeout", this.#printTimeout];
809
+ if (this.#model) args2.push("--model", this.#model);
810
+ args2.push("--add-dir", this.#cwd);
811
+ if (this.#permissionMode === "sandbox-auto") args2.push("--sandbox", "--dangerously-skip-permissions");
812
+ else if (this.#permissionMode === "plan") args2.push("--mode", "plan");
813
+ else args2.push("--mode", "accept-edits");
814
+ if (this.#conversationId) args2.push("--conversation", this.#conversationId);
815
+ args2.push("-p", promptText);
816
+ return args2;
817
+ }
818
+ async *prompt(input, signal) {
819
+ const base = renderPrompt(input);
820
+ const promptText = this.#conversationId || !this.#persona ? base : `${this.#persona}
821
+
822
+ ${base}`;
823
+ const child = this.#spawn(this.#buildArgs(promptText), { cwd: this.#cwd });
824
+ this.#child = child;
825
+ const queue = [];
826
+ let wake = null;
827
+ let finished = false;
828
+ let closeCode = null;
829
+ let sawResult = false;
830
+ const push = (ev) => {
831
+ queue.push(ev);
832
+ wake?.();
833
+ wake = null;
834
+ };
835
+ const rl = readline.createInterface({ input: child.stdout });
836
+ rl.on("line", (line) => {
837
+ const trimmed = line.trim();
838
+ if (!trimmed) return;
839
+ let ev;
840
+ try {
841
+ ev = JSON.parse(trimmed);
842
+ } catch {
843
+ return;
844
+ }
845
+ switch (ev.event) {
846
+ case "init":
847
+ if (ev.conversation_id) this.#conversationId = ev.conversation_id;
848
+ break;
849
+ case "step_update": {
850
+ const s = ev.step_update ?? {};
851
+ const stepType = s.step_type;
852
+ if (stepType === "agent_response" && typeof s.text_delta === "string" && s.text_delta) {
853
+ push({ type: "text-delta", text: s.text_delta });
854
+ } else if (stepType === "tool_call") {
855
+ push({
856
+ type: "activity",
857
+ activity: {
858
+ kind: "tool",
859
+ title: s.tool_name ?? "tool call",
860
+ status: s.state === "DONE" ? "ok" : "running"
861
+ }
862
+ });
863
+ }
864
+ break;
865
+ }
866
+ case "result": {
867
+ const r = ev.result ?? {};
868
+ sawResult = true;
869
+ if (r.conversation_id) this.#conversationId = r.conversation_id;
870
+ if (r.status === "SUCCESS") {
871
+ push({ type: "done", finalText: r.response ?? "" });
872
+ } else {
873
+ push({
874
+ type: "error",
875
+ error: { message: r.error || r.status || "antigravity error", fatal: false }
876
+ });
877
+ }
878
+ break;
879
+ }
880
+ }
881
+ });
882
+ child.on("close", (code) => {
883
+ closeCode = code;
884
+ });
885
+ rl.on("close", () => {
886
+ finished = true;
887
+ wake?.();
888
+ wake = null;
889
+ });
890
+ const onAbort = () => child.kill("SIGTERM");
891
+ if (signal.aborted) child.kill("SIGTERM");
892
+ else signal.addEventListener("abort", onAbort, { once: true });
893
+ try {
894
+ while (true) {
895
+ while (queue.length) yield queue.shift();
896
+ if (finished) break;
897
+ await new Promise((resolve) => {
898
+ wake = resolve;
899
+ });
900
+ }
901
+ while (queue.length) yield queue.shift();
902
+ if (!sawResult && !signal.aborted) {
903
+ yield {
904
+ type: "error",
905
+ error: { message: `agy exited (code ${closeCode ?? "unknown"}) with no result`, fatal: false }
906
+ };
907
+ }
908
+ } finally {
909
+ signal.removeEventListener("abort", onAbort);
910
+ rl.close();
911
+ this.#child = null;
912
+ }
913
+ }
914
+ };
915
+
916
+ // ../../packages/adapters/src/claude.ts
917
+ import { randomUUID as randomUUID3 } from "crypto";
918
+ function preview(input) {
919
+ try {
920
+ return JSON.stringify(input).slice(0, 500);
921
+ } catch {
922
+ return String(input).slice(0, 500);
923
+ }
924
+ }
925
+ var ClaudeAdapter = class {
926
+ constructor(name = "claude", opts = {}) {
927
+ this.name = name;
928
+ this.#permissionMode = opts.permissionMode ?? "default";
929
+ this.#query = opts.queryFn ?? this.#defaultQuery;
930
+ }
931
+ name;
932
+ capabilities = { tools: true, steering: false, resume: true };
933
+ #sessionId;
934
+ #cwd = process.cwd();
935
+ #persona = "";
936
+ #permissionMode;
937
+ #query;
938
+ get sessionId() {
939
+ return this.#sessionId;
940
+ }
941
+ #defaultQuery = async function* (params) {
942
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
943
+ const q = query({ prompt: params.prompt, options: params.options });
944
+ for await (const message of q) yield message;
945
+ };
946
+ async start(ctx) {
947
+ this.#cwd = ctx.workspaceDir;
948
+ this.#persona = ctx.persona;
949
+ this.#sessionId = ctx.savedSessionId ?? this.#sessionId;
950
+ }
951
+ async interrupt() {
952
+ }
953
+ async stop() {
954
+ return this.#sessionId;
955
+ }
956
+ async *prompt(input, signal) {
957
+ const promptText = renderPrompt(input);
958
+ const queue = [];
959
+ let wake = null;
960
+ let finished = false;
961
+ let final = null;
962
+ let errored = false;
963
+ const push = (ev) => {
964
+ queue.push(ev);
965
+ wake?.();
966
+ wake = null;
967
+ };
968
+ const ac = new AbortController();
969
+ const onAbort = () => ac.abort();
970
+ if (signal.aborted) ac.abort();
971
+ else signal.addEventListener("abort", onAbort, { once: true });
972
+ const canUseTool = (toolName, toolInput) => {
973
+ if (ac.signal.aborted) return Promise.resolve({ behavior: "deny", message: "aborted" });
974
+ return new Promise((resolve) => {
975
+ push({
976
+ type: "permission-request",
977
+ request: { id: randomUUID3(), agent: this.name, action: `use ${toolName}`, preview: preview(toolInput) },
978
+ respond: (d) => resolve(
979
+ d === "deny" ? { behavior: "deny", message: "Denied by the user." } : { behavior: "allow", updatedInput: toolInput }
980
+ )
981
+ });
982
+ });
983
+ };
984
+ const run = async () => {
985
+ try {
986
+ const q = this.#query({
987
+ prompt: promptText,
988
+ options: {
989
+ resume: this.#sessionId,
990
+ cwd: this.#cwd,
991
+ // Persona rides on the first turn; the resumed session already has it.
992
+ systemPrompt: this.#sessionId ? void 0 : { type: "preset", preset: "claude_code", append: this.#persona },
993
+ permissionMode: this.#permissionMode,
994
+ canUseTool,
995
+ settingSources: ["project"],
996
+ abortController: ac
997
+ }
998
+ });
999
+ for await (const msg of q) {
1000
+ if (msg.type === "system" && msg.subtype === "init") {
1001
+ const id = msg.session_id;
1002
+ if (id) this.#sessionId = id;
1003
+ } else if (msg.type === "assistant") {
1004
+ const m = msg;
1005
+ if (m.session_id) this.#sessionId = m.session_id;
1006
+ for (const block of m.message?.content ?? []) {
1007
+ if (block.type === "text" && block.text) push({ type: "text-delta", text: block.text });
1008
+ else if (block.type === "tool_use")
1009
+ push({ type: "activity", activity: { kind: "tool", title: block.name ?? "tool", status: "running" } });
1010
+ }
1011
+ } else if (msg.type === "result") {
1012
+ const r = msg;
1013
+ if (r.session_id) this.#sessionId = r.session_id;
1014
+ if (r.is_error || r.subtype && r.subtype !== "success") {
1015
+ errored = true;
1016
+ if (!ac.signal.aborted)
1017
+ push({ type: "error", error: { message: r.result || r.subtype || "claude error", fatal: false } });
1018
+ } else if (typeof r.result === "string") {
1019
+ final = r.result;
1020
+ }
1021
+ }
1022
+ }
1023
+ } catch (err) {
1024
+ errored = true;
1025
+ if (!ac.signal.aborted) push({ type: "error", error: { message: String(err), fatal: false } });
1026
+ } finally {
1027
+ finished = true;
1028
+ wake?.();
1029
+ wake = null;
1030
+ }
1031
+ };
1032
+ const runPromise = run();
1033
+ try {
1034
+ while (true) {
1035
+ while (queue.length) yield queue.shift();
1036
+ if (finished) break;
1037
+ await new Promise((resolve) => {
1038
+ wake = resolve;
1039
+ });
1040
+ }
1041
+ while (queue.length) yield queue.shift();
1042
+ if (!ac.signal.aborted && !errored && final !== null) yield { type: "done", finalText: final };
1043
+ } finally {
1044
+ signal.removeEventListener("abort", onAbort);
1045
+ await runPromise.catch(() => {
1046
+ });
1047
+ }
1048
+ }
1049
+ };
1050
+
1051
+ // ../../packages/adapters/src/codex.ts
1052
+ import { spawn } from "child_process";
1053
+ import readline2 from "readline";
1054
+ var DECISION_MAP = {
1055
+ "allow-once": "accept",
1056
+ "allow-session": "acceptForSession",
1057
+ deny: "decline"
1058
+ };
1059
+ var CodexAdapter = class {
1060
+ constructor(name = "codex", opts = {}) {
1061
+ this.name = name;
1062
+ this.#bin = opts.binPath ?? process.env.CODEX_BIN ?? "codex";
1063
+ this.#model = opts.model;
1064
+ this.#approvalPolicy = opts.approvalPolicy ?? "on-request";
1065
+ this.#sandbox = opts.sandbox ?? "workspace-write";
1066
+ this.#connect = opts.connect ?? this.#defaultConnect;
1067
+ }
1068
+ name;
1069
+ capabilities = { tools: true, steering: true, resume: true };
1070
+ #cwd = process.cwd();
1071
+ #persona = "";
1072
+ #threadId;
1073
+ #transport = null;
1074
+ #nextId = 0;
1075
+ #pending = /* @__PURE__ */ new Map();
1076
+ #turn = null;
1077
+ #bin;
1078
+ #model;
1079
+ #approvalPolicy;
1080
+ #sandbox;
1081
+ #connect;
1082
+ get sessionId() {
1083
+ return this.#threadId;
1084
+ }
1085
+ #defaultConnect = ({ cwd }) => {
1086
+ const child = spawn(this.#bin, ["app-server"], { cwd, stdio: ["pipe", "pipe", "inherit"], env: process.env });
1087
+ const rl = readline2.createInterface({ input: child.stdout });
1088
+ return {
1089
+ send: (message) => child.stdin.write(JSON.stringify(message) + "\n"),
1090
+ onMessage: (cb) => rl.on("line", (line) => {
1091
+ const t = line.trim();
1092
+ if (!t) return;
1093
+ try {
1094
+ cb(JSON.parse(t));
1095
+ } catch {
1096
+ }
1097
+ }),
1098
+ close: () => child.kill("SIGTERM")
1099
+ };
1100
+ };
1101
+ #request(method, params) {
1102
+ const id = ++this.#nextId;
1103
+ this.#transport.send({ jsonrpc: "2.0", id, method, params });
1104
+ return new Promise((resolve, reject) => this.#pending.set(id, { resolve, reject }));
1105
+ }
1106
+ #notify(method, params) {
1107
+ this.#transport.send({ jsonrpc: "2.0", method, params });
1108
+ }
1109
+ #respond(id, result) {
1110
+ this.#transport.send({ jsonrpc: "2.0", id, result });
1111
+ }
1112
+ #handleMessage(msg) {
1113
+ const method = msg.method;
1114
+ const id = msg.id;
1115
+ if (method && id !== void 0) {
1116
+ this.#handleServerRequest(id, method, msg.params ?? {});
1117
+ } else if (id !== void 0) {
1118
+ const p = this.#pending.get(id);
1119
+ if (!p) return;
1120
+ this.#pending.delete(id);
1121
+ if (msg.error) p.reject(new Error(JSON.stringify(msg.error)));
1122
+ else p.resolve(msg.result ?? {});
1123
+ } else if (method) {
1124
+ this.#turn?.onNotification(method, msg.params ?? {});
1125
+ }
1126
+ }
1127
+ #handleServerRequest(id, method, params) {
1128
+ const isApproval = method.endsWith("requestApproval");
1129
+ if (isApproval && this.#turn) {
1130
+ const preview2 = method.includes("command") ? params.command ?? JSON.stringify(params).slice(0, 300) : JSON.stringify(params.changes ?? params).slice(0, 300);
1131
+ this.#turn.push({
1132
+ type: "permission-request",
1133
+ request: {
1134
+ id: String(id),
1135
+ agent: this.name,
1136
+ action: method.includes("command") ? "run command" : "edit files",
1137
+ preview: String(preview2)
1138
+ },
1139
+ respond: (d) => this.#respond(id, { decision: DECISION_MAP[d] })
1140
+ });
1141
+ } else {
1142
+ this.#respond(id, isApproval ? { decision: "decline" } : {});
1143
+ }
1144
+ }
1145
+ async start(ctx) {
1146
+ this.#cwd = ctx.workspaceDir;
1147
+ this.#persona = ctx.persona;
1148
+ this.#threadId = ctx.savedSessionId ?? this.#threadId;
1149
+ this.#transport = this.#connect({ cwd: this.#cwd });
1150
+ this.#transport.onMessage((msg) => this.#handleMessage(msg));
1151
+ await this.#request("initialize", {
1152
+ clientInfo: { name: "brainstorming", title: "brainstorming", version: "0.1.0" },
1153
+ capabilities: null
1154
+ });
1155
+ this.#notify("initialized", {});
1156
+ if (this.#threadId) {
1157
+ try {
1158
+ await this.#request("thread/resume", { threadId: this.#threadId });
1159
+ return;
1160
+ } catch {
1161
+ this.#threadId = void 0;
1162
+ }
1163
+ }
1164
+ const params = {
1165
+ cwd: this.#cwd,
1166
+ approvalPolicy: this.#approvalPolicy,
1167
+ sandbox: this.#sandbox,
1168
+ developerInstructions: this.#persona
1169
+ };
1170
+ if (this.#model) params.model = this.#model;
1171
+ const res = await this.#request("thread/start", params);
1172
+ this.#threadId = res.thread?.id;
1173
+ }
1174
+ async interrupt() {
1175
+ if (this.#threadId) await this.#request("turn/interrupt", { threadId: this.#threadId }).catch(() => {
1176
+ });
1177
+ }
1178
+ async stop() {
1179
+ this.#transport?.close();
1180
+ this.#transport = null;
1181
+ return this.#threadId;
1182
+ }
1183
+ async *prompt(input, signal) {
1184
+ const queue = [];
1185
+ let wake = null;
1186
+ let finished = false;
1187
+ let errored = false;
1188
+ let final = "";
1189
+ const push = (ev) => {
1190
+ queue.push(ev);
1191
+ wake?.();
1192
+ wake = null;
1193
+ };
1194
+ const onNotification = (method, params) => {
1195
+ switch (method) {
1196
+ case "item/agentMessage/delta":
1197
+ push({ type: "text-delta", text: String(params.delta ?? "") });
1198
+ break;
1199
+ case "item/completed": {
1200
+ const item = params.item;
1201
+ const type = item?.type;
1202
+ if (type === "agentMessage") final = String(item?.text ?? final);
1203
+ else if (type === "commandExecution")
1204
+ push({
1205
+ type: "activity",
1206
+ activity: {
1207
+ kind: "command",
1208
+ title: `ran: ${String(item?.command ?? "command")}`,
1209
+ status: item?.exitCode === 0 ? "ok" : item?.exitCode == null ? "running" : "failed"
1210
+ }
1211
+ });
1212
+ else if (type === "fileChange")
1213
+ push({ type: "activity", activity: { kind: "file-change", title: "edited files", status: "ok" } });
1214
+ break;
1215
+ }
1216
+ case "error":
1217
+ errored = true;
1218
+ push({ type: "error", error: { message: String(params.message ?? "codex error"), fatal: false } });
1219
+ finished = true;
1220
+ wake?.();
1221
+ break;
1222
+ case "turn/completed":
1223
+ finished = true;
1224
+ wake?.();
1225
+ break;
1226
+ }
1227
+ };
1228
+ this.#turn = { push, onNotification };
1229
+ const onAbort = () => void this.interrupt();
1230
+ if (signal.aborted) void this.interrupt();
1231
+ else signal.addEventListener("abort", onAbort, { once: true });
1232
+ const text = renderPrompt(input);
1233
+ this.#request("turn/start", {
1234
+ threadId: this.#threadId,
1235
+ input: [{ type: "text", text, text_elements: [] }]
1236
+ }).catch((err) => {
1237
+ errored = true;
1238
+ push({ type: "error", error: { message: err.message, fatal: false } });
1239
+ finished = true;
1240
+ wake?.();
1241
+ });
1242
+ try {
1243
+ while (true) {
1244
+ while (queue.length) yield queue.shift();
1245
+ if (finished) break;
1246
+ await new Promise((resolve) => {
1247
+ wake = resolve;
1248
+ });
1249
+ }
1250
+ while (queue.length) yield queue.shift();
1251
+ if (!signal.aborted && !errored) yield { type: "done", finalText: final };
1252
+ } finally {
1253
+ signal.removeEventListener("abort", onAbort);
1254
+ this.#turn = null;
1255
+ }
1256
+ }
1257
+ };
1258
+
1259
+ // ../../packages/adapters/src/ollama.ts
1260
+ var OllamaAdapter = class {
1261
+ constructor(name = "ollama", opts = { model: "" }) {
1262
+ this.name = name;
1263
+ this.#model = opts.model;
1264
+ this.#host = opts.host;
1265
+ this.#clientOverride = opts.client;
1266
+ }
1267
+ name;
1268
+ capabilities = { tools: false, steering: false, resume: true };
1269
+ #history = [];
1270
+ #model;
1271
+ #host;
1272
+ #clientOverride;
1273
+ #client = null;
1274
+ #lock = Promise.resolve();
1275
+ async start(ctx) {
1276
+ if (this.#clientOverride) {
1277
+ this.#client = this.#clientOverride;
1278
+ } else {
1279
+ const { Ollama } = await import("ollama");
1280
+ this.#client = new Ollama({ host: this.#host ?? "http://127.0.0.1:11434" });
1281
+ }
1282
+ this.#history = ctx.persona ? [{ role: "system", content: ctx.persona }] : [];
1283
+ for (const m of ctx.transcript ?? []) this.#history.push(this.#toMessage(m));
1284
+ }
1285
+ async interrupt() {
1286
+ this.#client?.abort();
1287
+ }
1288
+ async stop() {
1289
+ return void 0;
1290
+ }
1291
+ #toMessage(m) {
1292
+ return m.author === this.name ? { role: "assistant", content: m.content } : { role: "user", content: `[${m.author}]: ${m.content}` };
1293
+ }
1294
+ async *prompt(input, signal) {
1295
+ const previous = this.#lock;
1296
+ let release;
1297
+ this.#lock = new Promise((r) => release = r);
1298
+ await previous;
1299
+ try {
1300
+ if (!this.#client) throw new Error("OllamaAdapter.start was not called");
1301
+ for (const m of input.digest) this.#history.push(this.#toMessage(m));
1302
+ this.#history.push(this.#toMessage(input.addressed));
1303
+ const onAbort = () => this.#client?.abort();
1304
+ if (signal.aborted) return;
1305
+ signal.addEventListener("abort", onAbort, { once: true });
1306
+ let acc = "";
1307
+ try {
1308
+ const stream = await this.#client.chat({
1309
+ model: this.#model,
1310
+ messages: [...this.#history],
1311
+ // snapshot: history keeps growing after this call
1312
+ stream: true
1313
+ });
1314
+ for await (const chunk of stream) {
1315
+ if (signal.aborted) break;
1316
+ const text = chunk.message?.content ?? "";
1317
+ if (text) {
1318
+ acc += text;
1319
+ yield { type: "text-delta", text };
1320
+ }
1321
+ }
1322
+ } catch (err) {
1323
+ if (!signal.aborted) yield { type: "error", error: { message: String(err), fatal: false } };
1324
+ return;
1325
+ } finally {
1326
+ signal.removeEventListener("abort", onAbort);
1327
+ }
1328
+ if (signal.aborted) return;
1329
+ this.#history.push({ role: "assistant", content: acc });
1330
+ yield { type: "done", finalText: acc };
1331
+ } finally {
1332
+ release();
1333
+ }
1334
+ }
1335
+ };
1336
+
1337
+ // src/agents.ts
1338
+ function detectOllamaModel() {
1339
+ try {
1340
+ const out = execFileSync("ollama", ["list"], { encoding: "utf8" });
1341
+ const names = out.trim().split("\n").slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean);
1342
+ return names.find((m) => m.includes(":cloud")) ?? names.find((m) => !m.includes("embed")) ?? void 0;
1343
+ } catch {
1344
+ return void 0;
1345
+ }
1346
+ }
1347
+ function buildAgents(config, notes) {
1348
+ const adapters = [];
1349
+ const a = config.agents;
1350
+ if (a.claude.enabled) adapters.push(new ClaudeAdapter("claude"));
1351
+ if (a.codex.enabled) adapters.push(new CodexAdapter("codex", { model: a.codex.model }));
1352
+ if (a.antigravity.enabled) adapters.push(new AntigravityAdapter("antigravity", { model: a.antigravity.model }));
1353
+ if (a.ollama.enabled) {
1354
+ const model = a.ollama.model ?? detectOllamaModel();
1355
+ if (model) adapters.push(new OllamaAdapter("ollama", { model }));
1356
+ else notes.push("ollama disabled: no model configured and none detected (set agents.ollama.model in config).");
1357
+ }
1358
+ return adapters;
1359
+ }
1360
+
1361
+ // src/config.ts
1362
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
1363
+ import { homedir } from "os";
1364
+ import { dirname as dirname2, join as join2 } from "path";
1365
+ import { z } from "zod";
1366
+ var AgentConfigSchema = z.object({
1367
+ enabled: z.boolean().default(true),
1368
+ /** Exact model string for the backend; omit to use its default (or auto-detect for Ollama). */
1369
+ model: z.string().optional(),
1370
+ /** Extra persona guidance appended to the group-chat protocol. */
1371
+ personaExtra: z.string().optional()
1372
+ });
1373
+ var ConfigSchema = z.object({
1374
+ roundBudget: z.number().int().positive().default(3),
1375
+ agents: z.object({
1376
+ claude: AgentConfigSchema.default({ enabled: true }),
1377
+ codex: AgentConfigSchema.default({ enabled: true }),
1378
+ antigravity: AgentConfigSchema.default({ enabled: true }),
1379
+ ollama: AgentConfigSchema.default({ enabled: true })
1380
+ }).default({
1381
+ claude: { enabled: true },
1382
+ codex: { enabled: true },
1383
+ antigravity: { enabled: true },
1384
+ ollama: { enabled: true }
1385
+ })
1386
+ });
1387
+ function configPath() {
1388
+ const base = process.env.XDG_CONFIG_HOME ?? join2(homedir(), ".config");
1389
+ return join2(base, "brainstorming", "config.json");
1390
+ }
1391
+ function loadConfig() {
1392
+ const path = configPath();
1393
+ if (existsSync3(path)) {
1394
+ try {
1395
+ return ConfigSchema.parse(JSON.parse(readFileSync3(path, "utf8")));
1396
+ } catch {
1397
+ }
1398
+ }
1399
+ const defaults = ConfigSchema.parse({});
1400
+ mkdirSync3(dirname2(path), { recursive: true });
1401
+ writeFileSync2(path, JSON.stringify(defaults, null, 2) + "\n");
1402
+ return defaults;
1403
+ }
1404
+
1405
+ // src/run.ts
1406
+ function isGitRepo(dir) {
1407
+ try {
1408
+ execFileSync2("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
1409
+ return true;
1410
+ } catch {
1411
+ return false;
1412
+ }
1413
+ }
1414
+ async function runLive(workspace) {
1415
+ const config = loadConfig();
1416
+ const store = new RoomStore(workspace);
1417
+ const transcript = store.loadTranscript();
1418
+ const meta = store.loadMeta();
1419
+ const notes = [];
1420
+ let adapters = buildAgents(config, notes);
1421
+ if (!isGitRepo(workspace) && adapters.some((a) => a.name === "codex")) {
1422
+ adapters = adapters.filter((a) => a.name !== "codex");
1423
+ notes.push("codex disabled: workspace is not a git repository (run `git init` to enable it).");
1424
+ }
1425
+ if (adapters.length === 0) {
1426
+ console.error(`No agents available. Edit ${configPath()} to enable agents, then retry.`);
1427
+ process.exit(1);
1428
+ }
1429
+ const roundBudget = meta?.roundBudget ?? config.roundBudget;
1430
+ const room = new Room({ transcript, adapters, roundBudget });
1431
+ const resuming = transcript.length > 0;
1432
+ if (resuming) room.markAllSeen();
1433
+ const roster = adapters.map((a) => a.name);
1434
+ const savedSessions = new Map(
1435
+ (meta?.participants ?? []).map((p) => [p.name, p.sessionId])
1436
+ );
1437
+ const priorMessages = [...transcript.all()];
1438
+ await Promise.all(
1439
+ adapters.map(
1440
+ (adapter) => adapter.start({
1441
+ workspaceDir: workspace,
1442
+ persona: buildPersona({
1443
+ name: adapter.name,
1444
+ roster,
1445
+ extra: config.agents[adapter.name]?.personaExtra
1446
+ }),
1447
+ savedSessionId: savedSessions.get(adapter.name),
1448
+ transcript: priorMessages
1449
+ })
1450
+ )
1451
+ );
1452
+ for (const note of notes) room.note(note);
1453
+ if (resuming) room.note(`resumed room with ${transcript.length} prior messages`);
1454
+ const decisionsPath = join3(workspace, "DECISIONS.md");
1455
+ const onDecide = (text) => appendFileSync2(decisionsPath, `- ${(/* @__PURE__ */ new Date()).toISOString()} \u2014 ${text}
1456
+ `);
1457
+ const instance = render(
1458
+ React3.createElement(App, { room, title: `brainstorming \u2014 ${workspace}`, onDecide })
1459
+ );
1460
+ await instance.waitUntilExit();
1461
+ const participants = [];
1462
+ for (const adapter of adapters) {
1463
+ const sessionId = await adapter.stop().catch(() => void 0);
1464
+ participants.push({ name: adapter.name, sessionId });
1465
+ }
1466
+ store.saveMeta({ participants, roundBudget });
1467
+ }
1468
+
1469
+ // src/index.ts
1470
+ var args = process.argv.slice(2);
1471
+ function argValue(flag) {
1472
+ const i = args.indexOf(flag);
1473
+ return i >= 0 ? args[i + 1] : void 0;
1474
+ }
1475
+ async function main() {
1476
+ if (args.includes("--help") || args.includes("-h")) {
1477
+ console.log(
1478
+ "brainstorming \u2014 multi-agent collaborative dev chat\n\nUsage:\n brainstorming Live chat with real agents in the current directory\n brainstorming --demo Scripted demo, no AI quota used\n brainstorming --budget N Set the agent-to-agent round budget (demo)\n"
1479
+ );
1480
+ return;
1481
+ }
1482
+ if (args.includes("--demo")) {
1483
+ const budget = Number(argValue("--budget") ?? 3);
1484
+ const room = new Room({
1485
+ transcript: new TranscriptStore(),
1486
+ adapters: demoAdapters(),
1487
+ roundBudget: Number.isInteger(budget) && budget > 0 ? budget : 3
1488
+ });
1489
+ await room.start({ workspaceDir: process.cwd(), persona: "" });
1490
+ render2(React4.createElement(App, { room, title: "brainstorming (demo)" }));
1491
+ return;
1492
+ }
1493
+ await runLive(process.cwd());
1494
+ }
1495
+ void main();