@sideboard-ai/core 0.1.9

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.
@@ -0,0 +1,1413 @@
1
+ import {
2
+ applyConnectedTeamToCli,
3
+ brightsyMcpServerName,
4
+ ensureCliTeamTracked,
5
+ ensureConnectedBrightsyTeamTokens,
6
+ loadBrightsyConfig
7
+ } from "./chunk-ILQK4P5R.js";
8
+ import {
9
+ parseCursorRunnerLine
10
+ } from "./chunk-3DKGI32Q.js";
11
+ import {
12
+ claudeChromeEnabled,
13
+ loadAppSettings,
14
+ resolveClaudeExecutable
15
+ } from "./chunk-3WF3X46L.js";
16
+ import {
17
+ run
18
+ } from "./chunk-AJ6ROGD7.js";
19
+
20
+ // src/agents/brightsy-targets.ts
21
+ function encodeBrightsyTarget(type, id, accountId) {
22
+ if (accountId) return `team:${accountId}:${type}:${id}`;
23
+ return `${type}:${id}`;
24
+ }
25
+ function decodeBrightsyTarget(model) {
26
+ const raw = model?.trim();
27
+ if (!raw || raw === "default" || raw === "agent:default") {
28
+ return { type: "agent", id: "default" };
29
+ }
30
+ const teamMatch = raw.match(/^team:([^:]+):(agent|model):(.+)$/);
31
+ if (teamMatch) {
32
+ return {
33
+ type: teamMatch[2],
34
+ id: teamMatch[3] || "default",
35
+ accountId: teamMatch[1]
36
+ };
37
+ }
38
+ if (raw.startsWith("agent:")) {
39
+ return { type: "agent", id: raw.slice("agent:".length) || "default" };
40
+ }
41
+ if (raw.startsWith("model:")) {
42
+ return { type: "model", id: raw.slice("model:".length) };
43
+ }
44
+ if (/^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(raw) || raw === "default") {
45
+ return { type: "agent", id: raw };
46
+ }
47
+ return { type: "model", id: raw };
48
+ }
49
+
50
+ // src/agents/turn-input.ts
51
+ function normalizeTurnInput(input) {
52
+ if (typeof input === "string") return { prompt: input };
53
+ return { prompt: input.prompt, cachedPrefix: input.cachedPrefix?.trim() || void 0 };
54
+ }
55
+ function flattenTurnInput(input) {
56
+ const { cachedPrefix, prompt } = normalizeTurnInput(input);
57
+ if (!cachedPrefix) return prompt;
58
+ return `${cachedPrefix}
59
+
60
+ ---
61
+
62
+ Current request:
63
+ ${prompt}`;
64
+ }
65
+ function buildCachedUserContent(input) {
66
+ return [{ type: "text", text: flattenTurnInput(input) }];
67
+ }
68
+ function buildClaudeStreamJsonUserMessage(input) {
69
+ return `${JSON.stringify({
70
+ type: "user",
71
+ message: {
72
+ role: "user",
73
+ content: buildCachedUserContent(input)
74
+ }
75
+ })}
76
+ `;
77
+ }
78
+ function walkCacheControls(blocks, saw5m) {
79
+ if (!blocks?.length) return { saw5m, invalid: null };
80
+ for (let i = 0; i < blocks.length; i++) {
81
+ const block = blocks[i];
82
+ const ttl = block.cache_control?.ttl ?? (block.cache_control ? "5m" : null);
83
+ if (ttl === "5m") saw5m = true;
84
+ if (ttl === "1h" && saw5m) return { saw5m, invalid: { index: i, ttl: "1h" } };
85
+ if (Array.isArray(block.content)) {
86
+ const nested = walkCacheControls(block.content, saw5m);
87
+ if (nested.invalid) return { saw5m: nested.saw5m, invalid: { index: i, ttl: "1h" } };
88
+ saw5m = nested.saw5m;
89
+ }
90
+ }
91
+ return { saw5m, invalid: null };
92
+ }
93
+ function findInvalidCacheControlTtlOrder(blocks) {
94
+ return walkCacheControls(blocks, false).invalid;
95
+ }
96
+ function countCacheControlBlocks(blocks) {
97
+ if (!blocks?.length) return 0;
98
+ let count = 0;
99
+ for (const block of blocks) {
100
+ if (block.cache_control) count++;
101
+ if (Array.isArray(block.content)) {
102
+ count += countCacheControlBlocks(block.content);
103
+ }
104
+ }
105
+ return count;
106
+ }
107
+ var MAX_ANTHROPIC_CACHE_CONTROL_BLOCKS = 4;
108
+
109
+ // src/agents/brightsy.ts
110
+ function usageFromBrightsy(usage) {
111
+ if (!usage) return null;
112
+ const inputTokens = Number(usage.prompt_tokens ?? 0);
113
+ const outputTokens = Number(usage.completion_tokens ?? 0);
114
+ if (!inputTokens && !outputTokens) return null;
115
+ const cached = Number(usage.prompt_tokens_details?.cached_tokens ?? 0);
116
+ return {
117
+ inputTokens,
118
+ outputTokens,
119
+ cacheReadTokens: cached || void 0
120
+ };
121
+ }
122
+ function parseBrightsyCliLine(line) {
123
+ const trimmed = line.trim();
124
+ if (!trimmed) return null;
125
+ try {
126
+ const obj = JSON.parse(trimmed);
127
+ if (obj.type === "text" && typeof obj.text === "string") {
128
+ return { type: "stdout", data: obj.text };
129
+ }
130
+ if (obj.type === "thinking" && typeof obj.text === "string") {
131
+ return { type: "thinking", data: obj.text };
132
+ }
133
+ if (obj.type === "error") {
134
+ const msg = String(obj.error ?? trimmed);
135
+ return [
136
+ { type: "stderr", data: msg },
137
+ { type: "stdout", data: `Error: ${msg}` }
138
+ ];
139
+ }
140
+ if (obj.type === "usage") {
141
+ const usage = usageFromBrightsy(obj.usage);
142
+ return usage ? { type: "usage", data: usage } : null;
143
+ }
144
+ if (obj.type === "tool_use") {
145
+ const id = typeof obj.id === "string" && obj.id || `brightsy-tool-${Date.now()}`;
146
+ const name = typeof obj.name === "string" && obj.name || "brightsy_tool";
147
+ const input = obj.input && typeof obj.input === "object" && !Array.isArray(obj.input) ? obj.input : void 0;
148
+ return { type: "tool_use", id, name, input };
149
+ }
150
+ if (obj.type === "tool_result" || obj.type === "tool") {
151
+ const id = typeof obj.id === "string" && obj.id || typeof obj.tool_call_id === "string" && obj.tool_call_id || `brightsy-tool-${Date.now()}`;
152
+ const content = typeof obj.content === "string" ? obj.content : obj.content != null ? JSON.stringify(obj.content) : void 0;
153
+ return {
154
+ type: "tool_result",
155
+ id,
156
+ content,
157
+ isError: obj.isError === true
158
+ };
159
+ }
160
+ if (obj.type === "done") return null;
161
+ if (typeof obj.type === "string" && ["tool_use", "tool_result", "tool", "thinking", "usage", "error", "done", "text"].includes(
162
+ obj.type
163
+ )) {
164
+ return null;
165
+ }
166
+ return { type: "stdout", data: trimmed };
167
+ } catch {
168
+ if (trimmed.startsWith("{") && /"type"\s*:\s*"(tool_use|tool_result|tool|text|thinking|usage|done|error)"/.test(trimmed)) {
169
+ return null;
170
+ }
171
+ return { type: "stdout", data: line };
172
+ }
173
+ }
174
+ async function fetchTeamChatTargets(team) {
175
+ const endpoint = (team.endpoint || "https://brightsy.ai").replace(/\/$/, "");
176
+ let agents = [
177
+ {
178
+ type: "agent",
179
+ id: "default",
180
+ name: "Default Agent",
181
+ description: "Account default agent (tools + memory)",
182
+ accountId: team.id,
183
+ accountSlug: team.slug,
184
+ accountName: team.name
185
+ }
186
+ ];
187
+ let models = [];
188
+ try {
189
+ const res = await fetch(`${endpoint}/api/v1beta/${team.id}/agents`, {
190
+ headers: { Authorization: `Bearer ${team.access_token}` }
191
+ });
192
+ if (res.ok) {
193
+ const json = await res.json();
194
+ const listed = (json.data || []).filter(
195
+ (a) => Boolean(a?.id && a?.name)
196
+ ).filter((a) => a.id !== "default").map((a) => ({
197
+ type: "agent",
198
+ id: a.id,
199
+ name: a.name,
200
+ description: a.description,
201
+ accountId: team.id,
202
+ accountSlug: team.slug,
203
+ accountName: team.name
204
+ }));
205
+ agents = [...agents, ...listed];
206
+ if (Array.isArray(json.models)) {
207
+ models = json.models.filter(
208
+ (m) => Boolean(m?.id && m?.name)
209
+ ).slice(0, 12).map((m) => ({
210
+ type: "model",
211
+ id: m.id,
212
+ name: m.name,
213
+ description: m.description,
214
+ accountId: team.id,
215
+ accountSlug: team.slug,
216
+ accountName: team.name
217
+ }));
218
+ }
219
+ }
220
+ } catch {
221
+ }
222
+ return {
223
+ accountId: team.id,
224
+ accountSlug: team.slug,
225
+ accountName: team.name,
226
+ agents,
227
+ models
228
+ };
229
+ }
230
+ async function listBrightsyChatTargetsViaCli() {
231
+ const listed = await run("brightsy", ["chat", "--list-targets", "--json"], {
232
+ reject: false
233
+ });
234
+ if (listed.exitCode !== 0 || !listed.stdout.trim()) {
235
+ throw new Error(
236
+ listed.stderr.trim() || "Failed to list Brightsy chat targets \u2014 is `brightsy` installed and logged in?"
237
+ );
238
+ }
239
+ try {
240
+ const parsed = JSON.parse(listed.stdout);
241
+ let accountId = null;
242
+ let accountSlug = "team";
243
+ let accountName = "Brightsy";
244
+ try {
245
+ const cfg = loadBrightsyConfig();
246
+ accountId = cfg.account_id;
247
+ accountSlug = cfg.account_slug || accountSlug;
248
+ accountName = cfg.account_slug || accountName;
249
+ } catch {
250
+ }
251
+ const agents = (Array.isArray(parsed.agents) ? parsed.agents : []).map((a) => ({
252
+ ...a,
253
+ accountId: accountId ?? void 0,
254
+ accountSlug,
255
+ accountName
256
+ }));
257
+ const models = (Array.isArray(parsed.models) ? parsed.models : []).map((m) => ({
258
+ ...m,
259
+ accountId: accountId ?? void 0,
260
+ accountSlug,
261
+ accountName
262
+ }));
263
+ const teams = accountId ? [
264
+ {
265
+ accountId,
266
+ accountSlug,
267
+ accountName,
268
+ agents,
269
+ models
270
+ }
271
+ ] : [];
272
+ return { teams, agents, models, activeAccountId: accountId };
273
+ } catch {
274
+ throw new Error("Brightsy --list-targets returned invalid JSON");
275
+ }
276
+ }
277
+ async function listBrightsyChatTargets() {
278
+ ensureCliTeamTracked();
279
+ const teamsRaw = await ensureConnectedBrightsyTeamTokens();
280
+ if (teamsRaw.length === 0) {
281
+ return listBrightsyChatTargetsViaCli();
282
+ }
283
+ let activeAccountId = null;
284
+ try {
285
+ activeAccountId = loadBrightsyConfig().account_id;
286
+ } catch {
287
+ activeAccountId = teamsRaw[0]?.id ?? null;
288
+ }
289
+ const teams = await Promise.all(teamsRaw.map((t) => fetchTeamChatTargets(t)));
290
+ teams.sort((a, b) => {
291
+ if (a.accountId === activeAccountId) return -1;
292
+ if (b.accountId === activeAccountId) return 1;
293
+ return a.accountSlug.localeCompare(b.accountSlug);
294
+ });
295
+ const active = teams.find((t) => t.accountId === activeAccountId) ?? teams[0] ?? null;
296
+ return {
297
+ teams,
298
+ agents: active?.agents ?? [],
299
+ models: active?.models ?? [],
300
+ activeAccountId
301
+ };
302
+ }
303
+ async function syncCliForTarget(accountId) {
304
+ if (!accountId) return;
305
+ const teams = await ensureConnectedBrightsyTeamTokens();
306
+ const team = teams.find((t) => t.id === accountId);
307
+ if (!team) return;
308
+ try {
309
+ const cfg = loadBrightsyConfig();
310
+ if (cfg.account_id === team.id && cfg.access_token === team.access_token) {
311
+ return;
312
+ }
313
+ } catch {
314
+ }
315
+ applyConnectedTeamToCli(team);
316
+ }
317
+ var brightsyAdapter = {
318
+ kind: "brightsy",
319
+ async detect() {
320
+ const which = await run("which", ["brightsy"], { reject: false });
321
+ if (which.exitCode !== 0) {
322
+ return {
323
+ agent: "brightsy",
324
+ installed: false,
325
+ authenticated: false,
326
+ linearMcp: false,
327
+ warnings: [],
328
+ reason: "brightsy CLI not found on PATH \u2014 npm i -g @brightsy/cli"
329
+ };
330
+ }
331
+ const who = await run("brightsy", ["whoami"], { reject: false });
332
+ const authenticated = who.exitCode === 0 && /logged in as/i.test(who.stdout);
333
+ return {
334
+ agent: "brightsy",
335
+ installed: true,
336
+ authenticated,
337
+ linearMcp: false,
338
+ warnings: [],
339
+ reason: authenticated ? void 0 : "not logged in \u2014 run `brightsy login`"
340
+ };
341
+ },
342
+ async buildTurn(thread, input) {
343
+ const prompt = flattenTurnInput(input);
344
+ const target = decodeBrightsyTarget(thread.model);
345
+ await syncCliForTarget(target.accountId);
346
+ const mode = thread.planMode ? "plan" : target.type === "model" ? "ask" : "agent";
347
+ const args = [
348
+ "chat",
349
+ "--json",
350
+ "--mode",
351
+ mode,
352
+ target.type === "model" ? "--model" : "--agent",
353
+ target.id
354
+ ];
355
+ return {
356
+ file: "brightsy",
357
+ args,
358
+ cwd: thread.worktreePath,
359
+ // Piped stdin becomes the message body (avoids ARG_MAX for long seeds).
360
+ stdin: prompt
361
+ };
362
+ },
363
+ parseEvent(line) {
364
+ return parseBrightsyCliLine(line);
365
+ },
366
+ async resolveSessionId() {
367
+ return null;
368
+ },
369
+ async buildAttach(thread) {
370
+ const target = decodeBrightsyTarget(thread.model);
371
+ await syncCliForTarget(target.accountId);
372
+ const args = target.type === "model" ? ["chat", "--model", target.id] : ["chat", "--agent", target.id];
373
+ return { file: "brightsy", args, cwd: thread.worktreePath };
374
+ }
375
+ };
376
+
377
+ // src/agents/claude.ts
378
+ import { existsSync as existsSync2 } from "fs";
379
+
380
+ // src/agents/claude-mcp.ts
381
+ function parseMcpList(text) {
382
+ const servers = [];
383
+ const seen = /* @__PURE__ */ new Set();
384
+ for (const raw of text.split("\n")) {
385
+ const line = raw.trim();
386
+ if (!line || /^Checking MCP/i.test(line)) continue;
387
+ const m = line.match(/^(.+?):\s+\S+/);
388
+ if (!m) continue;
389
+ const name = m[1].trim();
390
+ if (!name || seen.has(name)) continue;
391
+ const needsAuth = /Needs authentication/i.test(line);
392
+ const connected = !needsAuth && /Connected/i.test(line);
393
+ if (!needsAuth && !connected) continue;
394
+ seen.add(name);
395
+ servers.push({ name, connected, needsAuth });
396
+ }
397
+ return servers;
398
+ }
399
+ function sanitizeMcpServerName(name) {
400
+ return name.replace(/[^A-Za-z0-9_-]/g, "_");
401
+ }
402
+ function mcpAllowTools(servers) {
403
+ const out = [];
404
+ for (const s of servers) {
405
+ if (!s.connected) continue;
406
+ const id = sanitizeMcpServerName(s.name);
407
+ if (!id) continue;
408
+ out.push(`mcp__${id}`);
409
+ out.push(`mcp__${id}__*`);
410
+ }
411
+ return out;
412
+ }
413
+ function mcpAuthWarnings(servers) {
414
+ const needing = servers.filter((s) => s.needsAuth).map((s) => s.name);
415
+ if (needing.length === 0) return [];
416
+ return [
417
+ `MCP needs login: ${needing.join(", ")}. Run: claude mcp login "<name>"`
418
+ ];
419
+ }
420
+
421
+ // src/agents/injected-mcp.ts
422
+ import { existsSync, mkdtempSync, writeFileSync } from "fs";
423
+ import { createRequire } from "module";
424
+ import { tmpdir } from "os";
425
+ import { dirname, join } from "path";
426
+ import { fileURLToPath } from "url";
427
+ var SIDEBOARD_MCP_ALLOWED_TOOLS = [
428
+ "mcp__sideboard",
429
+ "mcp__sideboard__*"
430
+ ];
431
+ var BRIGHTSY_MCP_ALLOWED_TOOLS = [
432
+ "mcp__brightsy",
433
+ "mcp__brightsy__*"
434
+ ];
435
+ var brightsyMcpCommandCache = null;
436
+ async function resolveBrightsyMcpCommand() {
437
+ const now = Date.now();
438
+ if (brightsyMcpCommandCache && now - brightsyMcpCommandCache.at < 6e4 && brightsyMcpCommandCache.command) {
439
+ return brightsyMcpCommandCache.command;
440
+ }
441
+ const which = await run("which", ["brightsy-mcp"], { reject: false });
442
+ const command = which.exitCode === 0 && which.stdout.trim() ? "brightsy-mcp" : "npx";
443
+ brightsyMcpCommandCache = { at: now, command };
444
+ return command;
445
+ }
446
+ function isBrightsyConnected() {
447
+ try {
448
+ loadBrightsyConfig();
449
+ return true;
450
+ } catch {
451
+ return false;
452
+ }
453
+ }
454
+ function mcpLaunch(cmd, name, env) {
455
+ if (cmd === "brightsy-mcp") {
456
+ return { name, command: "brightsy-mcp", ...env ? { env } : {} };
457
+ }
458
+ return {
459
+ name,
460
+ command: "npx",
461
+ args: ["-y", "@brightsy/mcp-server"],
462
+ ...env ? { env } : {}
463
+ };
464
+ }
465
+ function teamEnv(team) {
466
+ const endpoint = (team.endpoint || "https://brightsy.ai").replace(/\/$/, "");
467
+ return {
468
+ BRIGHTSY_API_TOKEN: team.access_token,
469
+ BRIGHTSY_ACCOUNT_ID: team.id,
470
+ BRIGHTSY_API_URL: endpoint
471
+ };
472
+ }
473
+ function brightsyMcpAllowedTools(serverNames) {
474
+ const out = [];
475
+ for (const name of serverNames) {
476
+ out.push(`mcp__${name}`, `mcp__${name}__*`);
477
+ }
478
+ return out;
479
+ }
480
+ function corePackageDir() {
481
+ const cjsDir = typeof __dirname !== "undefined" ? __dirname : "";
482
+ if (cjsDir) return cjsDir;
483
+ try {
484
+ const url = import.meta.url;
485
+ if (typeof url === "string" && url.length > 0) {
486
+ return dirname(fileURLToPath(url));
487
+ }
488
+ } catch {
489
+ }
490
+ try {
491
+ const req = createRequire(join(process.cwd(), "package.json"));
492
+ return dirname(req.resolve("@sideboard-ai/core"));
493
+ } catch {
494
+ return process.cwd();
495
+ }
496
+ }
497
+ function findSideboardMcpJsEntry() {
498
+ const override = process.env.SIDEBOARD_MCP_ENTRY?.trim() || process.env.SIDEBOARD_CLI?.trim();
499
+ if (override && existsSync(override)) return override;
500
+ let dir = corePackageDir();
501
+ for (let i = 0; i < 10; i++) {
502
+ const candidates = [
503
+ join(dir, "mcp/run-stdio.js"),
504
+ join(dir, "mcp/run-stdio.cjs"),
505
+ join(dir, "dist/mcp/run-stdio.js"),
506
+ join(dir, "dist/mcp/run-stdio.cjs"),
507
+ join(dir, "packages/core/dist/mcp/run-stdio.js"),
508
+ join(dir, "packages/cli/dist/index.js"),
509
+ join(dir, "cli/dist/index.js")
510
+ ];
511
+ for (const p of candidates) {
512
+ if (existsSync(p)) return p;
513
+ }
514
+ const parent = dirname(dir);
515
+ if (parent === dir) break;
516
+ dir = parent;
517
+ }
518
+ return null;
519
+ }
520
+ async function resolveSideboardMcpServer() {
521
+ const which = await run("which", ["sideboard"], { reject: false });
522
+ if (which.exitCode === 0 && which.stdout.trim()) {
523
+ return { name: "sideboard", command: which.stdout.trim(), args: ["mcp"] };
524
+ }
525
+ const entry = findSideboardMcpJsEntry();
526
+ if (entry) {
527
+ const isCli = /[/\\]cli[/\\]dist[/\\]index\.js$/.test(entry);
528
+ return {
529
+ name: "sideboard",
530
+ // Use `node` (not process.execPath) — under Electron execPath is Electron itself.
531
+ command: "node",
532
+ args: isCli ? [entry, "mcp"] : [entry]
533
+ };
534
+ }
535
+ return { name: "sideboard", command: "sideboard", args: ["mcp"] };
536
+ }
537
+ async function buildInjectedMcpServers(opts) {
538
+ const servers = [];
539
+ if (opts.includeSideboard) {
540
+ servers.push(await resolveSideboardMcpServer());
541
+ }
542
+ if (opts.includeBrightsy && isBrightsyConnected()) {
543
+ const cmd = await resolveBrightsyMcpCommand();
544
+ const teams = await ensureConnectedBrightsyTeamTokens();
545
+ if (teams.length > 0) {
546
+ const used = /* @__PURE__ */ new Set();
547
+ for (const team of teams) {
548
+ let name = brightsyMcpServerName(team.slug);
549
+ if (used.has(name)) name = `${name}_${team.id.slice(0, 8)}`;
550
+ used.add(name);
551
+ servers.push(mcpLaunch(cmd, name, teamEnv(team)));
552
+ }
553
+ } else {
554
+ servers.push(mcpLaunch(cmd, "brightsy"));
555
+ }
556
+ }
557
+ return servers;
558
+ }
559
+ function writeMcpServersConfig(servers) {
560
+ if (servers.length === 0) return null;
561
+ const mcpServers = {};
562
+ for (const s of servers) {
563
+ mcpServers[s.name] = {
564
+ command: s.command,
565
+ ...s.args ? { args: s.args } : {},
566
+ ...s.env ? { env: s.env } : {}
567
+ };
568
+ }
569
+ const dir = mkdtempSync(join(tmpdir(), "sideboard-mcp-"));
570
+ const cfgPath = join(dir, "mcp.json");
571
+ writeFileSync(cfgPath, JSON.stringify({ mcpServers }, null, 2));
572
+ return cfgPath;
573
+ }
574
+ async function writeInjectedMcpConfig(opts) {
575
+ return writeMcpServersConfig(await buildInjectedMcpServers(opts));
576
+ }
577
+
578
+ // src/agents/types.ts
579
+ var PLAN_MODE_INSTRUCTION = "Plan mode is active and must remain active until the user turns Plan mode off in the UI (or explicitly asks you to implement). Analyze the codebase, search and read files as needed, and produce or refine a clear implementation plan. Do not modify, create, or delete any files. Do not exit plan mode on your own.";
580
+ function permissionMode(thread) {
581
+ if (thread.planMode) {
582
+ return {
583
+ claude: "plan",
584
+ opencodePermission: JSON.stringify({
585
+ edit: "deny",
586
+ write: "deny",
587
+ bash: { "*": "deny" }
588
+ }),
589
+ codexSandbox: "read-only"
590
+ };
591
+ }
592
+ if (thread.autonomy === "full") {
593
+ return {
594
+ claude: "bypassPermissions",
595
+ opencodePermission: JSON.stringify({ "*": "allow" }),
596
+ codexSandbox: "workspace-write"
597
+ };
598
+ }
599
+ return {
600
+ claude: "acceptEdits",
601
+ opencodePermission: JSON.stringify({
602
+ edit: "allow",
603
+ bash: { "*": "allow", "rm -rf *": "deny" }
604
+ }),
605
+ codexSandbox: "workspace-write"
606
+ };
607
+ }
608
+
609
+ // src/agents/claude.ts
610
+ var BASE_ALLOWED_TOOLS = ["Edit", "Write", "Bash", "Read", "Glob", "Grep"];
611
+ var CLAUDE_PROMPT_ARG_MAX = 2e5;
612
+ async function loadMcpServers() {
613
+ const claude = resolveClaudeExecutable();
614
+ const mcpText = await run(claude, ["mcp", "list"], { reject: false });
615
+ return parseMcpList(`${mcpText.stdout}
616
+ ${mcpText.stderr}`);
617
+ }
618
+ function usageFromClaude(usage) {
619
+ if (!usage) return null;
620
+ const inputTokens = Number(usage.input_tokens ?? 0);
621
+ const outputTokens = Number(usage.output_tokens ?? 0);
622
+ if (!inputTokens && !outputTokens) return null;
623
+ return {
624
+ inputTokens,
625
+ outputTokens,
626
+ cacheReadTokens: usage.cache_read_input_tokens ? Number(usage.cache_read_input_tokens) : void 0,
627
+ cacheWriteTokens: usage.cache_creation_input_tokens ? Number(usage.cache_creation_input_tokens) : void 0
628
+ };
629
+ }
630
+ function eventsFromContentBlocks(blocks) {
631
+ if (!blocks?.length) return [];
632
+ const out = [];
633
+ for (const block of blocks) {
634
+ if (!block?.type) continue;
635
+ if (block.type === "text" && block.text) {
636
+ out.push({ type: "stdout", data: block.text });
637
+ continue;
638
+ }
639
+ if ((block.type === "thinking" || block.type === "redacted_thinking") && block.thinking) {
640
+ out.push({ type: "thinking", data: block.thinking });
641
+ continue;
642
+ }
643
+ if (block.type === "tool_use" && block.id && block.name) {
644
+ out.push({
645
+ type: "tool_use",
646
+ id: block.id,
647
+ name: block.name,
648
+ input: block.input
649
+ });
650
+ continue;
651
+ }
652
+ if (block.type === "tool_result" && block.tool_use_id) {
653
+ const content = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
654
+ (c) => typeof c === "string" ? c : c && typeof c === "object" && "text" in c ? String(c.text ?? "") : ""
655
+ ).join("") : block.content != null ? JSON.stringify(block.content) : void 0;
656
+ out.push({
657
+ type: "tool_result",
658
+ id: block.tool_use_id,
659
+ content,
660
+ isError: Boolean(block.is_error)
661
+ });
662
+ }
663
+ }
664
+ return out;
665
+ }
666
+ var claudeAdapter = {
667
+ kind: "claude",
668
+ async detect() {
669
+ const claude = resolveClaudeExecutable();
670
+ if (claude !== "claude") {
671
+ if (!existsSync2(claude)) {
672
+ return {
673
+ agent: "claude",
674
+ installed: false,
675
+ authenticated: false,
676
+ linearMcp: false,
677
+ warnings: [],
678
+ reason: `Claude Code executable not found: ${claude}`
679
+ };
680
+ }
681
+ } else {
682
+ const which = await run("which", ["claude"], { reject: false });
683
+ if (which.exitCode !== 0) {
684
+ return {
685
+ agent: "claude",
686
+ installed: false,
687
+ authenticated: false,
688
+ linearMcp: false,
689
+ warnings: [],
690
+ reason: "claude CLI not found on PATH"
691
+ };
692
+ }
693
+ }
694
+ const auth = await run(claude, ["auth", "status"], { reject: false });
695
+ const authenticated = auth.exitCode === 0;
696
+ const servers = await loadMcpServers();
697
+ const linearMcp = servers.some(
698
+ (s) => s.connected && /linear/i.test(s.name)
699
+ );
700
+ return {
701
+ agent: "claude",
702
+ installed: true,
703
+ authenticated,
704
+ linearMcp,
705
+ warnings: mcpAuthWarnings(servers),
706
+ reason: authenticated ? void 0 : "claude auth status failed \u2014 run `claude auth login`"
707
+ };
708
+ },
709
+ async buildTurn(thread, input) {
710
+ const claude = resolveClaudeExecutable();
711
+ const turn = normalizeTurnInput(input);
712
+ const sessionId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
713
+ const effective = sessionId ? { prompt: turn.prompt } : turn;
714
+ const promptText = flattenTurnInput(effective);
715
+ const useStdin = promptText.length > CLAUDE_PROMPT_ARG_MAX;
716
+ if (process.env.SIDEBOARD_DEBUG_CLAUDE_TURN === "1") {
717
+ console.error(
718
+ `[sideboard/claude] promptChars=${promptText.length} resumed=${Boolean(sessionId)} stdin=${useStdin}`
719
+ );
720
+ }
721
+ const mode = permissionMode(thread);
722
+ const { isOrchestratorThread } = await import("./global-workspace-R44HGBU6.js");
723
+ const isOrchestrator = isOrchestratorThread(thread);
724
+ const injectedServers = await buildInjectedMcpServers({
725
+ includeSideboard: isOrchestrator,
726
+ includeBrightsy: isBrightsyConnected()
727
+ });
728
+ const injectedBrightsyNames = injectedServers.filter((s) => s.name === "brightsy" || s.name.startsWith("brightsy_")).map((s) => s.name);
729
+ let allowedTools;
730
+ if (isOrchestrator) {
731
+ allowedTools = [
732
+ ...BASE_ALLOWED_TOOLS,
733
+ ...SIDEBOARD_MCP_ALLOWED_TOOLS,
734
+ ...brightsyMcpAllowedTools(injectedBrightsyNames)
735
+ ];
736
+ } else {
737
+ const servers = await loadMcpServers();
738
+ allowedTools = [
739
+ ...BASE_ALLOWED_TOOLS,
740
+ ...mcpAllowTools(servers),
741
+ ...brightsyMcpAllowedTools(injectedBrightsyNames)
742
+ ];
743
+ }
744
+ const args = [
745
+ "-p",
746
+ ...useStdin ? [] : [promptText],
747
+ "--output-format",
748
+ "stream-json",
749
+ "--verbose",
750
+ "--include-partial-messages",
751
+ "--permission-mode",
752
+ mode.claude
753
+ ];
754
+ const mcpConfigPath = writeMcpServersConfig(injectedServers);
755
+ if (mcpConfigPath) {
756
+ args.push("--mcp-config", mcpConfigPath);
757
+ }
758
+ if (claudeChromeEnabled()) {
759
+ args.push("--chrome");
760
+ }
761
+ if (useStdin) {
762
+ args.push("--input-format", "text");
763
+ }
764
+ for (const tool of allowedTools) {
765
+ args.push("--allowedTools", tool);
766
+ }
767
+ if (thread.model) {
768
+ args.push("--model", thread.model);
769
+ }
770
+ if (thread.fast) {
771
+ args.push("--effort", "low");
772
+ }
773
+ if (sessionId) {
774
+ args.push("--resume", sessionId);
775
+ }
776
+ return {
777
+ file: claude,
778
+ args,
779
+ cwd: thread.worktreePath,
780
+ stdin: useStdin ? `${promptText}
781
+ ` : void 0
782
+ };
783
+ },
784
+ parseEvent(line) {
785
+ const trimmed = line.trim();
786
+ if (!trimmed) return null;
787
+ try {
788
+ const obj = JSON.parse(trimmed);
789
+ if (obj.type === "system" && obj.subtype === "init") {
790
+ const sid = obj.session_id;
791
+ if (typeof sid === "string") return { type: "session_id", data: sid };
792
+ return null;
793
+ }
794
+ if (obj.type === "system" && typeof obj.session_id === "string") {
795
+ return { type: "session_id", data: obj.session_id };
796
+ }
797
+ if (obj.type === "assistant" || obj.type === "user") {
798
+ const content = obj.message?.content;
799
+ const events = eventsFromContentBlocks(content);
800
+ if (events.length === 0) return null;
801
+ return events.length === 1 ? events[0] : events;
802
+ }
803
+ if (obj.type === "stream_event") {
804
+ const event = obj.event;
805
+ if (!event) return null;
806
+ if (event.type === "content_block_start" && event.content_block) {
807
+ const block = event.content_block;
808
+ if (block.type === "tool_use" && block.id && block.name) {
809
+ return {
810
+ type: "tool_use",
811
+ id: block.id,
812
+ name: block.name,
813
+ input: block.input
814
+ };
815
+ }
816
+ if (block.type === "thinking" && block.thinking) {
817
+ return { type: "thinking", data: block.thinking };
818
+ }
819
+ return null;
820
+ }
821
+ if (event.type === "content_block_delta" && event.delta) {
822
+ if (event.delta.text) return { type: "stdout", data: event.delta.text };
823
+ if (event.delta.thinking) return { type: "thinking", data: event.delta.thinking };
824
+ return null;
825
+ }
826
+ return null;
827
+ }
828
+ if (obj.type === "content_block_delta") {
829
+ const delta = obj.delta;
830
+ if (delta?.text) return { type: "stdout", data: delta.text };
831
+ if (delta?.thinking) return { type: "thinking", data: delta.thinking };
832
+ return null;
833
+ }
834
+ if (obj.type === "result") {
835
+ const events = [];
836
+ const text = obj.result;
837
+ if (typeof text === "string" && text) events.push({ type: "stdout", data: text });
838
+ const usage = usageFromClaude(obj.usage);
839
+ if (usage) events.push({ type: "usage", data: usage });
840
+ if (events.length === 0) return null;
841
+ return events.length === 1 ? events[0] : events;
842
+ }
843
+ return null;
844
+ } catch {
845
+ return { type: "stdout", data: line };
846
+ }
847
+ },
848
+ async resolveSessionId(_worktreePath, cached) {
849
+ return cached;
850
+ },
851
+ async buildAttach(thread) {
852
+ const claude = resolveClaudeExecutable();
853
+ const sessionId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
854
+ const args = sessionId ? ["--resume", sessionId] : [];
855
+ if (claudeChromeEnabled()) {
856
+ args.push("--chrome");
857
+ }
858
+ return { file: claude, args, cwd: thread.worktreePath };
859
+ },
860
+ async listLinearIssues(_repoPath) {
861
+ const claude = resolveClaudeExecutable();
862
+ const prompt = "List my assigned Linear issues as JSON array only, no markdown. Each item: id, identifier, title, url, labels (string[]).";
863
+ const { stdout, exitCode } = await run(
864
+ claude,
865
+ [
866
+ "-p",
867
+ prompt,
868
+ "--output-format",
869
+ "json",
870
+ "--permission-mode",
871
+ "bypassPermissions",
872
+ "--allowedTools",
873
+ "mcp__linear__*"
874
+ ],
875
+ { reject: false }
876
+ );
877
+ if (exitCode !== 0) return [];
878
+ return parseIssuesJson(stdout);
879
+ }
880
+ };
881
+ function parseIssuesJson(raw) {
882
+ const text = raw.trim();
883
+ const candidates = [text];
884
+ const match = text.match(/\[[\s\S]*\]/);
885
+ if (match) candidates.push(match[0]);
886
+ for (const c of candidates) {
887
+ try {
888
+ const parsed = JSON.parse(c);
889
+ if (Array.isArray(parsed)) {
890
+ return parsed.map((item) => ({
891
+ id: String(item.id ?? item.identifier ?? ""),
892
+ identifier: String(item.identifier ?? item.id ?? ""),
893
+ title: String(item.title ?? ""),
894
+ url: String(item.url ?? ""),
895
+ labels: Array.isArray(item.labels) ? item.labels.map(String) : []
896
+ }));
897
+ }
898
+ if (parsed && typeof parsed === "object" && typeof parsed.result === "string") {
899
+ return parseIssuesJson(parsed.result);
900
+ }
901
+ } catch {
902
+ }
903
+ }
904
+ return [];
905
+ }
906
+
907
+ // src/agents/codex.ts
908
+ import { existsSync as existsSync3, readFileSync } from "fs";
909
+ import { homedir } from "os";
910
+ import { join as join2 } from "path";
911
+ var CODEX_PROMPT_ARG_MAX = 2e5;
912
+ function usageFromCodex(usage) {
913
+ if (!usage) return null;
914
+ const inputTokens = Number(usage.input_tokens ?? 0);
915
+ const outputTokens = Number(usage.output_tokens ?? 0) + Number(usage.reasoning_output_tokens ?? 0);
916
+ if (!inputTokens && !outputTokens) return null;
917
+ return {
918
+ inputTokens,
919
+ outputTokens,
920
+ cacheReadTokens: usage.cached_input_tokens ? Number(usage.cached_input_tokens) : void 0
921
+ };
922
+ }
923
+ function codexConfigHasNetworkAccess() {
924
+ const candidates = [
925
+ join2(homedir(), ".codex", "config.toml"),
926
+ join2(homedir(), ".config", "codex", "config.toml")
927
+ ];
928
+ for (const path of candidates) {
929
+ if (!existsSync3(path)) continue;
930
+ const text = readFileSync(path, "utf8");
931
+ if (/network_access\s*=\s*true/.test(text)) return true;
932
+ }
933
+ return false;
934
+ }
935
+ var codexAdapter = {
936
+ kind: "codex",
937
+ async detect() {
938
+ const which = await run("which", ["codex"], { reject: false });
939
+ if (which.exitCode !== 0) {
940
+ return {
941
+ agent: "codex",
942
+ installed: false,
943
+ authenticated: false,
944
+ linearMcp: false,
945
+ warnings: [],
946
+ reason: "codex CLI not found on PATH"
947
+ };
948
+ }
949
+ const auth = await run("codex", ["login", "status"], { reject: false });
950
+ const authenticated = auth.exitCode === 0;
951
+ const mcp = await run("codex", ["mcp", "list", "--json"], { reject: false });
952
+ const linearMcp = /linear/i.test(mcp.stdout + mcp.stderr);
953
+ const warnings = [];
954
+ if (!codexConfigHasNetworkAccess()) {
955
+ warnings.push(
956
+ "Codex workspace-write blocks network by default \u2014 set [sandbox_workspace_write] network_access = true in ~/.codex/config.toml if agents need npm install etc."
957
+ );
958
+ }
959
+ return {
960
+ agent: "codex",
961
+ installed: true,
962
+ authenticated,
963
+ linearMcp,
964
+ warnings,
965
+ reason: authenticated ? void 0 : "codex login status failed \u2014 run `codex login`"
966
+ };
967
+ },
968
+ async buildTurn(thread, input) {
969
+ const prompt = flattenTurnInput(input);
970
+ const sessionId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
971
+ const useStdin = prompt.length > CODEX_PROMPT_ARG_MAX;
972
+ const promptArg = useStdin ? "-" : prompt;
973
+ if (process.env.SIDEBOARD_DEBUG_CODEX_TURN === "1") {
974
+ const { cachedPrefix } = normalizeTurnInput(input);
975
+ console.error(
976
+ `[sideboard/codex] promptChars=${prompt.length} resumed=${Boolean(sessionId)} stdin=${useStdin} hasPrefix=${Boolean(cachedPrefix)}`
977
+ );
978
+ }
979
+ const mode = permissionMode(thread);
980
+ const args = [
981
+ "exec",
982
+ ...sessionId ? ["resume", sessionId] : [],
983
+ promptArg,
984
+ "--cd",
985
+ thread.worktreePath,
986
+ "--json",
987
+ "--sandbox",
988
+ mode.codexSandbox,
989
+ "--ask-for-approval",
990
+ "never"
991
+ ];
992
+ return {
993
+ file: "codex",
994
+ args,
995
+ cwd: thread.worktreePath,
996
+ stdin: useStdin ? `${prompt}
997
+ ` : void 0
998
+ };
999
+ },
1000
+ parseEvent(line) {
1001
+ const trimmed = line.trim();
1002
+ if (!trimmed) return null;
1003
+ try {
1004
+ const obj = JSON.parse(trimmed);
1005
+ const sid = typeof obj.session_id === "string" && obj.session_id || typeof obj.thread_id === "string" && obj.thread_id || typeof obj.session?.id === "string" && obj.session.id;
1006
+ if (sid) return { type: "session_id", data: sid };
1007
+ if (obj.type === "turn.completed" || obj.type === "turn_completed") {
1008
+ const usage = usageFromCodex(obj.usage);
1009
+ return usage ? { type: "usage", data: usage } : null;
1010
+ }
1011
+ if (typeof obj.item === "object" && obj.item !== null) {
1012
+ const item = obj.item;
1013
+ if (item.type === "agent_message" && item.text) {
1014
+ return { type: "stdout", data: item.text };
1015
+ }
1016
+ }
1017
+ if (typeof obj.content === "string") {
1018
+ return { type: "stdout", data: obj.content };
1019
+ }
1020
+ return { type: "stdout", data: trimmed };
1021
+ } catch {
1022
+ return { type: "stdout", data: line };
1023
+ }
1024
+ },
1025
+ async resolveSessionId(_worktreePath, cached) {
1026
+ return cached;
1027
+ },
1028
+ async buildAttach(thread) {
1029
+ const sessionId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
1030
+ if (sessionId) {
1031
+ return {
1032
+ file: "codex",
1033
+ args: ["exec", "resume", sessionId, "--cd", thread.worktreePath],
1034
+ cwd: thread.worktreePath
1035
+ };
1036
+ }
1037
+ return {
1038
+ file: "codex",
1039
+ args: ["--cd", thread.worktreePath],
1040
+ cwd: thread.worktreePath
1041
+ };
1042
+ },
1043
+ async listLinearIssues(_repoPath) {
1044
+ const prompt = "List my assigned Linear issues as JSON array only: id, identifier, title, url, labels.";
1045
+ const { stdout, exitCode } = await run(
1046
+ "codex",
1047
+ [
1048
+ "exec",
1049
+ prompt,
1050
+ "--json",
1051
+ "--sandbox",
1052
+ "read-only",
1053
+ "--ask-for-approval",
1054
+ "never"
1055
+ ],
1056
+ { reject: false }
1057
+ );
1058
+ if (exitCode !== 0) return [];
1059
+ const match = stdout.match(/\[[\s\S]*\]/);
1060
+ if (!match) return [];
1061
+ try {
1062
+ const parsed = JSON.parse(match[0]);
1063
+ return Array.isArray(parsed) ? parsed : [];
1064
+ } catch {
1065
+ return [];
1066
+ }
1067
+ }
1068
+ };
1069
+
1070
+ // src/agents/cursor.ts
1071
+ import { existsSync as existsSync4 } from "fs";
1072
+ import { createRequire as createRequire2 } from "module";
1073
+ import { dirname as dirname2, join as join3 } from "path";
1074
+ import { fileURLToPath as fileURLToPath2 } from "url";
1075
+ import { Cursor } from "@cursor/sdk";
1076
+ function resolveCursorApiKey() {
1077
+ const fromEnv = (process.env.CURSOR_API_KEY || "").trim();
1078
+ if (fromEnv) return fromEnv;
1079
+ return (loadAppSettings().environment.CURSOR_API_KEY || "").trim();
1080
+ }
1081
+ function entryDir() {
1082
+ const cjsDir = typeof __dirname !== "undefined" ? __dirname : "";
1083
+ if (cjsDir) return cjsDir;
1084
+ try {
1085
+ return dirname2(fileURLToPath2(import.meta.url));
1086
+ } catch {
1087
+ try {
1088
+ const req = createRequire2(process.cwd() + "/");
1089
+ return dirname2(req.resolve("@sideboard-ai/core"));
1090
+ } catch {
1091
+ return process.cwd();
1092
+ }
1093
+ }
1094
+ }
1095
+ function cursorRunnerPath() {
1096
+ const root = entryDir();
1097
+ const candidates = [
1098
+ join3(root, "agents", "cursor-runner.js"),
1099
+ join3(root, "agents", "cursor-runner.cjs"),
1100
+ // If somehow resolved from package root instead of dist/
1101
+ join3(root, "dist", "agents", "cursor-runner.js"),
1102
+ join3(root, "dist", "agents", "cursor-runner.cjs"),
1103
+ // Source tree (dev): packages/core/src/agents/cursor-runner.ts
1104
+ join3(root, "cursor-runner.ts"),
1105
+ join3(root, "src", "agents", "cursor-runner.ts")
1106
+ ];
1107
+ for (const candidate of candidates) {
1108
+ if (existsSync4(candidate)) return candidate;
1109
+ }
1110
+ return candidates[0];
1111
+ }
1112
+ var cursorAdapter = {
1113
+ kind: "cursor",
1114
+ async detect() {
1115
+ const apiKey = resolveCursorApiKey();
1116
+ if (!apiKey) {
1117
+ return {
1118
+ agent: "cursor",
1119
+ installed: true,
1120
+ authenticated: false,
1121
+ linearMcp: false,
1122
+ warnings: [],
1123
+ reason: "CURSOR_API_KEY not set \u2014 add it in Settings \u2192 Agents \u2192 Cursor, or Settings \u2192 Environment"
1124
+ };
1125
+ }
1126
+ try {
1127
+ await Cursor.models.list({ apiKey });
1128
+ return {
1129
+ agent: "cursor",
1130
+ installed: true,
1131
+ authenticated: true,
1132
+ linearMcp: false,
1133
+ warnings: []
1134
+ };
1135
+ } catch (err) {
1136
+ const message = err instanceof Error ? err.message : String(err);
1137
+ return {
1138
+ agent: "cursor",
1139
+ installed: true,
1140
+ authenticated: false,
1141
+ linearMcp: false,
1142
+ warnings: [],
1143
+ reason: `Cursor API auth failed: ${message}`
1144
+ };
1145
+ }
1146
+ },
1147
+ async buildTurn(thread, input) {
1148
+ const prompt = flattenTurnInput(input);
1149
+ const agentId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
1150
+ const apiKey = resolveCursorApiKey() || void 0;
1151
+ const req = {
1152
+ prompt,
1153
+ cwd: thread.worktreePath,
1154
+ agentId,
1155
+ model: thread.model,
1156
+ fast: thread.fast,
1157
+ planMode: thread.planMode,
1158
+ apiKey
1159
+ };
1160
+ const runner = cursorRunnerPath();
1161
+ const isTs = runner.endsWith(".ts");
1162
+ return {
1163
+ file: process.execPath,
1164
+ args: isTs ? ["--import", "tsx", runner] : [runner],
1165
+ cwd: thread.worktreePath,
1166
+ stdin: JSON.stringify(req),
1167
+ env: {
1168
+ ...apiKey ? { CURSOR_API_KEY: apiKey } : {}
1169
+ }
1170
+ };
1171
+ },
1172
+ parseEvent(line) {
1173
+ return parseCursorRunnerLine(line);
1174
+ },
1175
+ async resolveSessionId(_worktreePath, cached) {
1176
+ return cached;
1177
+ },
1178
+ async buildAttach(thread) {
1179
+ const which = await run("which", ["cursor"], { reject: false });
1180
+ if (which.exitCode === 0) {
1181
+ return { file: "cursor", args: [thread.worktreePath], cwd: thread.worktreePath };
1182
+ }
1183
+ throw new Error(
1184
+ "Cursor agents have no interactive CLI attach. Install the Cursor shell command (`cursor`) to open the worktree, or continue the thread in Sideboard."
1185
+ );
1186
+ }
1187
+ };
1188
+
1189
+ // src/agents/opencode.ts
1190
+ function usageFromOpencode(tokens) {
1191
+ if (!tokens) return null;
1192
+ const inputTokens = Number(tokens.input ?? 0);
1193
+ const outputTokens = Number(tokens.output ?? 0) + Number(tokens.reasoning ?? 0);
1194
+ if (!inputTokens && !outputTokens) return null;
1195
+ return {
1196
+ inputTokens,
1197
+ outputTokens,
1198
+ cacheReadTokens: tokens.cache?.read ? Number(tokens.cache.read) : void 0,
1199
+ cacheWriteTokens: tokens.cache?.write ? Number(tokens.cache.write) : void 0
1200
+ };
1201
+ }
1202
+ var opencodeAdapter = {
1203
+ kind: "opencode",
1204
+ async detect() {
1205
+ const which = await run("which", ["opencode"], { reject: false });
1206
+ if (which.exitCode !== 0) {
1207
+ return {
1208
+ agent: "opencode",
1209
+ installed: false,
1210
+ authenticated: false,
1211
+ linearMcp: false,
1212
+ warnings: [],
1213
+ reason: "opencode CLI not found on PATH"
1214
+ };
1215
+ }
1216
+ const auth = await run("opencode", ["auth", "list"], { reject: false });
1217
+ const authenticated = auth.exitCode === 0 && auth.stdout.trim().length > 0;
1218
+ const mcp = await run("opencode", ["mcp", "list"], { reject: false });
1219
+ const linearMcp = /linear/i.test(mcp.stdout + mcp.stderr);
1220
+ return {
1221
+ agent: "opencode",
1222
+ installed: true,
1223
+ authenticated,
1224
+ linearMcp,
1225
+ warnings: [],
1226
+ reason: authenticated ? void 0 : "opencode auth list empty \u2014 run `opencode auth login`"
1227
+ };
1228
+ },
1229
+ async buildTurn(thread, input) {
1230
+ const prompt = flattenTurnInput(input);
1231
+ const sessionId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
1232
+ const mode = permissionMode(thread);
1233
+ const args = [
1234
+ "run",
1235
+ "--dir",
1236
+ thread.worktreePath,
1237
+ "--format",
1238
+ "json"
1239
+ ];
1240
+ if (sessionId) {
1241
+ args.push("--session", sessionId);
1242
+ }
1243
+ return {
1244
+ file: "opencode",
1245
+ args,
1246
+ cwd: thread.worktreePath,
1247
+ // `opencode run` treats non-TTY stdin as the message body when no positional
1248
+ // message is given (see resolveRunInput in opencode's run.ts).
1249
+ stdin: prompt,
1250
+ env: {
1251
+ OPENCODE_PERMISSION: mode.opencodePermission
1252
+ }
1253
+ };
1254
+ },
1255
+ parseEvent(line) {
1256
+ const trimmed = line.trim();
1257
+ if (!trimmed) return null;
1258
+ try {
1259
+ const obj = JSON.parse(trimmed);
1260
+ const sid = typeof obj.sessionID === "string" && obj.sessionID || typeof obj.sessionId === "string" && obj.sessionId || typeof obj.session_id === "string" && obj.session_id;
1261
+ if (sid) return { type: "session_id", data: sid };
1262
+ if (obj.type === "text") {
1263
+ const text = obj.part?.text ?? obj.text;
1264
+ if (text) return { type: "stdout", data: text };
1265
+ }
1266
+ if (obj.type === "tool_use") {
1267
+ const part = obj.part;
1268
+ const id = part?.id ?? obj.id ?? `tool-${Date.now()}`;
1269
+ const name = part?.name ?? part?.tool ?? obj.name ?? obj.tool ?? "tool";
1270
+ const input = part?.input ?? obj.input;
1271
+ return { type: "tool_use", id, name, input };
1272
+ }
1273
+ if (obj.type === "tool_result") {
1274
+ const part = obj.part;
1275
+ const id = part?.tool_use_id ?? part?.id ?? obj.tool_use_id ?? obj.id;
1276
+ if (!id) return null;
1277
+ return {
1278
+ type: "tool_result",
1279
+ id,
1280
+ content: part?.output ?? part?.content ?? obj.output ?? obj.content
1281
+ };
1282
+ }
1283
+ if (obj.type === "error") {
1284
+ return {
1285
+ type: "stderr",
1286
+ data: String(obj.error ?? trimmed)
1287
+ };
1288
+ }
1289
+ if (obj.type === "step_finish" || obj.type === "step-finish") {
1290
+ const part = obj.part;
1291
+ const usage = usageFromOpencode(
1292
+ part?.tokens ?? obj.tokens
1293
+ );
1294
+ return usage ? { type: "usage", data: usage } : null;
1295
+ }
1296
+ return { type: "stdout", data: trimmed };
1297
+ } catch {
1298
+ return { type: "stdout", data: line };
1299
+ }
1300
+ },
1301
+ async resolveSessionId(worktreePath, cached) {
1302
+ const listed = await run(
1303
+ "opencode",
1304
+ ["session", "list", "--format", "json"],
1305
+ { cwd: worktreePath, reject: false }
1306
+ );
1307
+ if (listed.exitCode === 0 && listed.stdout.trim()) {
1308
+ try {
1309
+ const sessions = JSON.parse(listed.stdout);
1310
+ if (Array.isArray(sessions) && sessions.length > 0) {
1311
+ const norm = (p) => p.replace(/\/+$/, "");
1312
+ const wt = norm(worktreePath);
1313
+ const match = sessions.find(
1314
+ (s) => s.directory && norm(s.directory) === wt || s.path && norm(s.path) === wt
1315
+ );
1316
+ if (match?.id) return match.id;
1317
+ }
1318
+ } catch {
1319
+ }
1320
+ }
1321
+ return cached;
1322
+ },
1323
+ async buildAttach(thread) {
1324
+ const sessionId = await this.resolveSessionId(thread.worktreePath, thread.sessionId);
1325
+ const args = ["--dir", thread.worktreePath];
1326
+ if (sessionId) args.push("--session", sessionId);
1327
+ return {
1328
+ file: "opencode",
1329
+ args,
1330
+ cwd: thread.worktreePath,
1331
+ env: {
1332
+ OPENCODE_PERMISSION: permissionMode(thread).opencodePermission
1333
+ }
1334
+ };
1335
+ },
1336
+ async listLinearIssues(_repoPath) {
1337
+ const prompt = "List my assigned Linear issues as JSON array only: id, identifier, title, url, labels.";
1338
+ const { stdout, exitCode } = await run(
1339
+ "opencode",
1340
+ ["run", prompt, "--format", "json"],
1341
+ {
1342
+ reject: false,
1343
+ env: { OPENCODE_PERMISSION: JSON.stringify({ "*": "allow" }) }
1344
+ }
1345
+ );
1346
+ if (exitCode !== 0) return [];
1347
+ const texts = [];
1348
+ for (const line of stdout.split("\n")) {
1349
+ try {
1350
+ const obj = JSON.parse(line);
1351
+ if (obj.type === "text") {
1352
+ texts.push(obj.part?.text ?? obj.text ?? "");
1353
+ }
1354
+ } catch {
1355
+ }
1356
+ }
1357
+ const joined = texts.join("");
1358
+ const match = joined.match(/\[[\s\S]*\]/);
1359
+ if (!match) return [];
1360
+ try {
1361
+ return JSON.parse(match[0]);
1362
+ } catch {
1363
+ return [];
1364
+ }
1365
+ }
1366
+ };
1367
+
1368
+ // src/agents/index.ts
1369
+ var adapters = {
1370
+ claude: claudeAdapter,
1371
+ codex: codexAdapter,
1372
+ opencode: opencodeAdapter,
1373
+ brightsy: brightsyAdapter,
1374
+ cursor: cursorAdapter
1375
+ };
1376
+ function getAdapter(kind) {
1377
+ return adapters[kind];
1378
+ }
1379
+ function allAdapters() {
1380
+ return Object.values(adapters);
1381
+ }
1382
+
1383
+ export {
1384
+ encodeBrightsyTarget,
1385
+ decodeBrightsyTarget,
1386
+ normalizeTurnInput,
1387
+ flattenTurnInput,
1388
+ buildCachedUserContent,
1389
+ buildClaudeStreamJsonUserMessage,
1390
+ findInvalidCacheControlTtlOrder,
1391
+ countCacheControlBlocks,
1392
+ MAX_ANTHROPIC_CACHE_CONTROL_BLOCKS,
1393
+ parseBrightsyCliLine,
1394
+ listBrightsyChatTargets,
1395
+ brightsyAdapter,
1396
+ parseMcpList,
1397
+ sanitizeMcpServerName,
1398
+ mcpAllowTools,
1399
+ mcpAuthWarnings,
1400
+ SIDEBOARD_MCP_ALLOWED_TOOLS,
1401
+ BRIGHTSY_MCP_ALLOWED_TOOLS,
1402
+ isBrightsyConnected,
1403
+ brightsyMcpAllowedTools,
1404
+ writeInjectedMcpConfig,
1405
+ PLAN_MODE_INSTRUCTION,
1406
+ permissionMode,
1407
+ claudeAdapter,
1408
+ codexAdapter,
1409
+ cursorAdapter,
1410
+ opencodeAdapter,
1411
+ getAdapter,
1412
+ allAdapters
1413
+ };