@sideboard-ai/core 0.1.45 → 0.1.49

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 (45) hide show
  1. package/dist/agents/cursor-runner.cjs +11 -4
  2. package/dist/agents/cursor-runner.js +11 -4
  3. package/dist/{agents-JSHCAZUZ.js → agents-3P4N6KHC.js} +12 -2
  4. package/dist/agents-LUB3Q773.js +82 -0
  5. package/dist/app-settings-7XVDQJ7F.js +80 -0
  6. package/dist/{app-settings-LYGVDGZY.js → app-settings-LZP632KI.js} +1 -1
  7. package/dist/{chunk-U3EQKJHA.js → chunk-3NAS4MWH.js} +1 -1
  8. package/dist/{chunk-ZNSM2DDD.js → chunk-5WYEJ3F3.js} +6 -2
  9. package/dist/chunk-7MV3RXSC.js +277 -0
  10. package/dist/chunk-DF5VQQKA.js +191 -0
  11. package/dist/chunk-E35APBHV.js +269 -0
  12. package/dist/chunk-EOYDCKQC.js +172 -0
  13. package/dist/chunk-GI7E5733.js +30 -0
  14. package/dist/chunk-GZXBYHJC.js +148 -0
  15. package/dist/chunk-HBJSHRY2.js +494 -0
  16. package/dist/chunk-ISY4EGF6.js +77 -0
  17. package/dist/chunk-KGZBYWZ3.js +23 -0
  18. package/dist/chunk-RQI4TOXK.js +2232 -0
  19. package/dist/chunk-S2LL5HA4.js +33 -0
  20. package/dist/{chunk-ANZ566Z5.js → chunk-SS34TVSY.js} +66 -7
  21. package/dist/{chunk-WYY3J7GR.js → chunk-T5QQVXK3.js} +6 -3
  22. package/dist/chunk-V4JRXYYU.js +122 -0
  23. package/dist/{chunk-6NAPN2N5.js → chunk-ZQCQIWIP.js} +1 -1
  24. package/dist/chunk-ZVV5EEQN.js +1892 -0
  25. package/dist/connected-teams-UBXHZGO4.js +24 -0
  26. package/dist/{coordinator-prompt-2XFYG3C5.js → coordinator-prompt-SPGDT7J5.js} +1 -1
  27. package/dist/coordinator-prompt-VG4BZ5JL.js +25 -0
  28. package/dist/cursor-recover-NNK7JPQM.js +44 -0
  29. package/dist/{global-workspace-YFOQUGWD.js → global-workspace-KSR3E63K.js} +3 -2
  30. package/dist/global-workspace-OJF4ENH4.js +40 -0
  31. package/dist/index.cjs +377 -30
  32. package/dist/index.d.cts +70 -11
  33. package/dist/index.d.ts +70 -11
  34. package/dist/index.js +5779 -193
  35. package/dist/mcp/run-stdio.cjs +331 -32
  36. package/dist/mcp/run-stdio.js +5618 -14
  37. package/dist/paths-2OFB7UJG.js +28 -0
  38. package/dist/run-HMRSRG3U.js +12 -0
  39. package/dist/thread-store-XICUWFNM.js +32 -0
  40. package/dist/title-4WAKWZRF.js +27 -0
  41. package/dist/workspaces-OZE7LRDO.js +24 -0
  42. package/dist/{workspaces-TIKLNDW3.js → workspaces-UJX7JIGH.js} +4 -3
  43. package/dist/worktree-XG5PCLZY.js +94 -0
  44. package/package.json +2 -2
  45. package/dist/chunk-I6QGZOOS.js +0 -5667
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ assertOrchestratorCapableAgent
5
+ } from "./chunk-S2LL5HA4.js";
6
+ import {
7
+ ensureGlobalCoordinatorCwd
8
+ } from "./chunk-GZXBYHJC.js";
9
+ import {
10
+ allocateTeamName,
11
+ takenSlugsFromThread,
12
+ teamSlugFromName
13
+ } from "./chunk-ZVV5EEQN.js";
14
+ import {
15
+ createEmptyThread,
16
+ listThreads,
17
+ updateThread,
18
+ writeThread
19
+ } from "./chunk-EOYDCKQC.js";
20
+ import {
21
+ globalAgentCwd
22
+ } from "./chunk-7MV3RXSC.js";
23
+
24
+ // src/brightsy/cloud-connect-constants.ts
25
+ var CLOUD_ORCHESTRATOR_GOAL = "Cloud-connected Sideboard orchestrator";
26
+ var SIDEBOARD_FORCE_STOP = "SIDEBOARD_FORCE_STOP";
27
+ var CLOUD_COORDINATOR_BUSY_REPLY = [
28
+ "Sideboard is busy with an in-progress client/desktop tool turn on the global coordinator.",
29
+ "I did not start a new orchestration turn.",
30
+ `To force-stop the in-progress turn, send another desktop request whose first line is exactly ${SIDEBOARD_FORCE_STOP}`,
31
+ "(optional follow-up request on later lines).",
32
+ "What do you want me to do? (retry later, wait until idle, force-stop, rephrase, or cancel)"
33
+ ].join(" ");
34
+ var CLOUD_COORDINATOR_STOPPED_REPLY = [
35
+ "Sideboard force-stopped the in-progress global coordinator turn.",
36
+ "No new orchestration request was started."
37
+ ].join(" ");
38
+ var CLOUD_COORDINATOR_TIMEOUT_REPLY = [
39
+ "Sideboard global coordinator timed out waiting for the local agent turn to finish.",
40
+ "I did not produce a full orchestration result.",
41
+ `To force-stop the in-progress turn, send another desktop request whose first line is exactly ${SIDEBOARD_FORCE_STOP}`,
42
+ "(optional follow-up request on later lines).",
43
+ "What do you want me to do? (retry later, wait, force-stop, rephrase, or cancel)"
44
+ ].join(" ");
45
+
46
+ // src/store/global-workspace.ts
47
+ var GLOBAL_WORKSPACE_ID = "__global__";
48
+ function isGlobalThread(thread) {
49
+ return Boolean(thread && thread.repoPath === GLOBAL_WORKSPACE_ID);
50
+ }
51
+ function isGlobalRepoPath(repoPath) {
52
+ return repoPath === GLOBAL_WORKSPACE_ID;
53
+ }
54
+ function isOrchestratorThread(thread) {
55
+ return Boolean(
56
+ thread && (thread.sourceType === "orchestration" || isGlobalThread(thread))
57
+ );
58
+ }
59
+ function orchestratorSessionPoisonedByBuiltins(thread) {
60
+ if (!thread?.messages?.length) return false;
61
+ let usedBuiltin = false;
62
+ let usedSideboardMcp = false;
63
+ for (const msg of thread.messages) {
64
+ for (const part of msg.parts ?? []) {
65
+ if (part.type !== "tool") continue;
66
+ if (part.name.startsWith("mcp__sideboard")) {
67
+ usedSideboardMcp = true;
68
+ } else if (part.name === "Bash" || part.name === "Edit" || part.name === "Write" || part.name === "Read" || part.name === "Glob" || part.name === "Grep") {
69
+ usedBuiltin = true;
70
+ }
71
+ }
72
+ }
73
+ return usedBuiltin && !usedSideboardMcp;
74
+ }
75
+ function isCloudCoordinatorThread(thread) {
76
+ if (thread.sourceRef === CLOUD_ORCHESTRATOR_GOAL) {
77
+ return isGlobalThread(thread) || thread.sourceType === "orchestration";
78
+ }
79
+ if (thread.title === CLOUD_ORCHESTRATOR_GOAL) {
80
+ return isGlobalThread(thread) || thread.sourceType === "orchestration";
81
+ }
82
+ return false;
83
+ }
84
+ function orchestrationTitleNeedsSoccerNickname(thread) {
85
+ if (!isOrchestratorThread(thread)) return false;
86
+ const title = thread.title?.trim() ?? "";
87
+ if (teamSlugFromName(title)) return false;
88
+ if (title === CLOUD_ORCHESTRATOR_GOAL) return true;
89
+ if (!title || title === "Untitled") return true;
90
+ if (thread.userSetTitle) return false;
91
+ if (thread.sourceRef?.trim() && title === thread.sourceRef.trim()) return true;
92
+ return false;
93
+ }
94
+ function takenTeamSlugsForOrchestration() {
95
+ const taken = /* @__PURE__ */ new Set(["global"]);
96
+ for (const thread of listThreads({ includeArchived: true })) {
97
+ if (thread.status === "archived") continue;
98
+ for (const slug of takenSlugsFromThread(thread)) {
99
+ taken.add(slug);
100
+ }
101
+ }
102
+ return [...taken];
103
+ }
104
+ function createGlobalChat(opts) {
105
+ ensureGlobalCoordinatorCwd();
106
+ const isCloud = opts.sourceRef === CLOUD_ORCHESTRATOR_GOAL || opts.title?.trim() === CLOUD_ORCHESTRATOR_GOAL;
107
+ const explicit = opts.title?.trim();
108
+ const title = explicit && explicit !== CLOUD_ORCHESTRATOR_GOAL ? explicit : allocateTeamName(takenTeamSlugsForOrchestration()).name;
109
+ const sourceRef = opts.sourceRef?.trim() || (isCloud ? CLOUD_ORCHESTRATOR_GOAL : title);
110
+ const agent = assertOrchestratorCapableAgent(opts.agent);
111
+ const thread = createEmptyThread({
112
+ title,
113
+ // Stick nicknames the same way chat tabs do (avoid later sync overwrites).
114
+ userSetTitle: true,
115
+ sourceType: "orchestration",
116
+ sourceRef,
117
+ branchName: "global",
118
+ worktreePath: globalAgentCwd(),
119
+ repoPath: GLOBAL_WORKSPACE_ID,
120
+ agent,
121
+ autonomy: opts.autonomy ?? "default",
122
+ model: opts.model ?? null,
123
+ effort: opts.effort ?? "high",
124
+ fast: Boolean(opts.fast),
125
+ planMode: Boolean(opts.planMode),
126
+ attachments: opts.attachments ?? [],
127
+ parentThreadId: opts.parentThreadId ?? null,
128
+ status: "idle"
129
+ });
130
+ writeThread(thread);
131
+ return thread;
132
+ }
133
+ function healOrchestrationSoccerTitles() {
134
+ let healed = 0;
135
+ const taken = new Set(takenTeamSlugsForOrchestration());
136
+ for (const thread of listThreads({ includeArchived: true })) {
137
+ if (thread.status === "archived") continue;
138
+ if (!orchestrationTitleNeedsSoccerNickname(thread)) continue;
139
+ const team = allocateTeamName(taken);
140
+ taken.add(team.slug);
141
+ updateThread(thread.id, { title: team.name, userSetTitle: true });
142
+ healed += 1;
143
+ }
144
+ return healed;
145
+ }
146
+ function listGlobalThreads(includeArchived = false) {
147
+ return listThreads({ includeArchived }).filter(
148
+ (t) => t.repoPath === GLOBAL_WORKSPACE_ID
149
+ );
150
+ }
151
+ function findCloudCoordinator() {
152
+ return listThreads({ includeArchived: true }).find(
153
+ (t) => t.status !== "archived" && isCloudCoordinatorThread(t)
154
+ );
155
+ }
156
+ function ensureCloudCoordinator(agent) {
157
+ const existing = findCloudCoordinator();
158
+ if (existing) {
159
+ if (existing.repoPath !== GLOBAL_WORKSPACE_ID) {
160
+ return updateThread(existing.id, {
161
+ repoPath: GLOBAL_WORKSPACE_ID,
162
+ worktreePath: globalAgentCwd(),
163
+ branchName: "global"
164
+ });
165
+ }
166
+ return existing;
167
+ }
168
+ const created = createGlobalChat({
169
+ sourceRef: CLOUD_ORCHESTRATOR_GOAL,
170
+ agent
171
+ });
172
+ const all = listThreads({ includeArchived: true }).filter(
173
+ (t) => t.status !== "archived" && isCloudCoordinatorThread(t)
174
+ ).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
175
+ return all[0] ?? created;
176
+ }
177
+
178
+ export {
179
+ GLOBAL_WORKSPACE_ID,
180
+ isGlobalThread,
181
+ isGlobalRepoPath,
182
+ isOrchestratorThread,
183
+ orchestratorSessionPoisonedByBuiltins,
184
+ isCloudCoordinatorThread,
185
+ orchestrationTitleNeedsSoccerNickname,
186
+ takenTeamSlugsForOrchestration,
187
+ createGlobalChat,
188
+ healOrchestrationSoccerTitles,
189
+ listGlobalThreads,
190
+ ensureCloudCoordinator
191
+ };
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ run
5
+ } from "./chunk-ISY4EGF6.js";
6
+ import {
7
+ appDataDir
8
+ } from "./chunk-7MV3RXSC.js";
9
+
10
+ // src/brightsy/connected-teams.ts
11
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
12
+ import { join as join2 } from "path";
13
+
14
+ // src/brightsy/config.ts
15
+ import { existsSync, readFileSync, writeFileSync } from "fs";
16
+ import { homedir } from "os";
17
+ import { join } from "path";
18
+ function brightsyConfigPath() {
19
+ return join(homedir(), ".brightsy", "config.json");
20
+ }
21
+ function loadBrightsyConfig() {
22
+ const path = brightsyConfigPath();
23
+ if (!existsSync(path)) {
24
+ throw new Error("Brightsy not logged in \u2014 run `brightsy login` first");
25
+ }
26
+ const raw = JSON.parse(readFileSync(path, "utf8"));
27
+ if (!raw.access_token || !raw.account_id) {
28
+ throw new Error("Brightsy config incomplete \u2014 run `brightsy login`");
29
+ }
30
+ return raw;
31
+ }
32
+ function saveBrightsyConfig(cfg) {
33
+ writeFileSync(brightsyConfigPath(), `${JSON.stringify(cfg, null, 2)}
34
+ `, {
35
+ mode: 384
36
+ });
37
+ }
38
+
39
+ // src/brightsy/accounts.ts
40
+ async function runBrightsyTeamsJson(args) {
41
+ const listed = await run("brightsy", ["teams", ...args, "--json"], {
42
+ reject: false
43
+ });
44
+ if (listed.exitCode !== 0) {
45
+ throw new Error(
46
+ listed.stderr.trim() || listed.stdout.trim() || "brightsy teams failed \u2014 is `@brightsy/cli` installed and logged in?"
47
+ );
48
+ }
49
+ const raw = listed.stdout.trim();
50
+ if (!raw) {
51
+ throw new Error("brightsy teams returned empty output");
52
+ }
53
+ try {
54
+ return JSON.parse(raw);
55
+ } catch {
56
+ throw new Error("brightsy teams returned invalid JSON");
57
+ }
58
+ }
59
+ async function listBrightsyAccounts() {
60
+ const data = await runBrightsyTeamsJson([]);
61
+ return Array.isArray(data.teams) ? data.teams : [];
62
+ }
63
+ async function performBrightsyAccountSwitch(accountIdOrSlug) {
64
+ const data = await runBrightsyTeamsJson([
65
+ "switch",
66
+ accountIdOrSlug
67
+ ]);
68
+ if (!data.active?.id) {
69
+ throw new Error(`Team switch failed for: ${accountIdOrSlug}`);
70
+ }
71
+ const cfg = loadBrightsyConfig();
72
+ if (data.active.slug && data.active.slug !== cfg.account_slug) {
73
+ cfg.account_slug = data.active.slug;
74
+ saveBrightsyConfig(cfg);
75
+ }
76
+ return { cfg, target: data.active };
77
+ }
78
+
79
+ // src/brightsy/connected-teams.ts
80
+ function storePath() {
81
+ return join2(appDataDir(), "brightsy-teams.json");
82
+ }
83
+ function readStore() {
84
+ const path = storePath();
85
+ if (!existsSync2(path)) return [];
86
+ try {
87
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
88
+ return Array.isArray(parsed.teams) ? parsed.teams : [];
89
+ } catch {
90
+ return [];
91
+ }
92
+ }
93
+ function writeStore(teams) {
94
+ mkdirSync(appDataDir(), { recursive: true });
95
+ const path = storePath();
96
+ writeFileSync2(path, `${JSON.stringify({ teams }, null, 2)}
97
+ `, {
98
+ mode: 384
99
+ });
100
+ return teams;
101
+ }
102
+ function listConnectedBrightsyTeams() {
103
+ return readStore().map(({ id, slug, name, expires_at }) => ({
104
+ id,
105
+ slug,
106
+ name,
107
+ expires_at
108
+ }));
109
+ }
110
+ function getConnectedBrightsyTeamsRaw() {
111
+ return readStore();
112
+ }
113
+ function applyConnectedTeamToCli(team) {
114
+ let base = {};
115
+ try {
116
+ base = loadBrightsyConfig();
117
+ } catch {
118
+ }
119
+ saveBrightsyConfig({
120
+ ...base,
121
+ access_token: team.access_token,
122
+ refresh_token: team.refresh_token ?? base.refresh_token,
123
+ expires_at: team.expires_at ?? base.expires_at,
124
+ account_id: team.id,
125
+ account_slug: team.slug,
126
+ endpoint: team.endpoint ?? base.endpoint ?? "https://brightsy.ai",
127
+ oauth_client_id: base.oauth_client_id
128
+ });
129
+ }
130
+ async function refreshTeamToken(team) {
131
+ if (!team.refresh_token) return team;
132
+ const endpoint = (team.endpoint || "https://brightsy.ai").replace(/\/$/, "");
133
+ const cfg = (() => {
134
+ try {
135
+ return loadBrightsyConfig();
136
+ } catch {
137
+ return null;
138
+ }
139
+ })();
140
+ const clientId = cfg?.oauth_client_id || "brightsy-cli";
141
+ const res = await fetch(`${endpoint}/oauth/token`, {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
144
+ body: new URLSearchParams({
145
+ grant_type: "refresh_token",
146
+ refresh_token: team.refresh_token,
147
+ client_id: clientId
148
+ })
149
+ });
150
+ if (!res.ok) return team;
151
+ const data = await res.json();
152
+ if (!data.access_token) return team;
153
+ return {
154
+ ...team,
155
+ access_token: data.access_token,
156
+ refresh_token: data.refresh_token || team.refresh_token,
157
+ expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : team.expires_at
158
+ };
159
+ }
160
+ async function ensureConnectedBrightsyTeamTokens() {
161
+ const teams = readStore();
162
+ if (teams.length === 0) return [];
163
+ const next = [];
164
+ let changed = false;
165
+ for (const team of teams) {
166
+ const expired = typeof team.expires_at === "number" && Date.now() >= team.expires_at - 6e4;
167
+ if (!expired) {
168
+ next.push(team);
169
+ continue;
170
+ }
171
+ const refreshed = await refreshTeamToken(team);
172
+ if (refreshed.access_token !== team.access_token) changed = true;
173
+ next.push(refreshed);
174
+ }
175
+ if (changed) writeStore(next);
176
+ try {
177
+ const cfg = loadBrightsyConfig();
178
+ const active = next.find((t) => t.id === cfg.account_id);
179
+ if (active && active.access_token !== cfg.access_token) {
180
+ applyConnectedTeamToCli(active);
181
+ }
182
+ } catch {
183
+ }
184
+ return next;
185
+ }
186
+ function ensureCliTeamTracked(meta) {
187
+ try {
188
+ const cfg = loadBrightsyConfig();
189
+ const existing = readStore();
190
+ if (existing.some((t) => t.id === cfg.account_id)) {
191
+ return listConnectedBrightsyTeams();
192
+ }
193
+ const team = {
194
+ id: cfg.account_id,
195
+ slug: meta?.slug || cfg.account_slug || cfg.account_id,
196
+ name: meta?.name || cfg.account_slug || cfg.account_id,
197
+ access_token: cfg.access_token,
198
+ refresh_token: cfg.refresh_token,
199
+ expires_at: cfg.expires_at,
200
+ endpoint: cfg.endpoint
201
+ };
202
+ writeStore([...existing, team]);
203
+ } catch {
204
+ }
205
+ return listConnectedBrightsyTeams();
206
+ }
207
+ async function connectBrightsyTeam(accountIdOrSlug) {
208
+ const accounts = await listBrightsyAccounts();
209
+ const target = accounts.find((a) => a.id === accountIdOrSlug) ?? accounts.find((a) => a.slug === accountIdOrSlug);
210
+ if (!target) {
211
+ throw new Error(`Brightsy team not found: ${accountIdOrSlug}`);
212
+ }
213
+ const existing = readStore();
214
+ const already = existing.find((t) => t.id === target.id);
215
+ if (already) {
216
+ applyConnectedTeamToCli(already);
217
+ return listConnectedBrightsyTeams();
218
+ }
219
+ const { cfg: minted } = await performBrightsyAccountSwitch(target.id);
220
+ const team = {
221
+ id: target.id,
222
+ slug: target.slug,
223
+ name: target.name,
224
+ access_token: minted.access_token,
225
+ refresh_token: minted.refresh_token,
226
+ expires_at: minted.expires_at,
227
+ endpoint: minted.endpoint
228
+ };
229
+ writeStore([...existing, team]);
230
+ return listConnectedBrightsyTeams();
231
+ }
232
+ async function disconnectBrightsyTeam(accountIdOrSlug) {
233
+ const before = readStore();
234
+ const removed = before.find(
235
+ (t) => t.id === accountIdOrSlug || t.slug === accountIdOrSlug
236
+ );
237
+ const teams = before.filter(
238
+ (t) => t.id !== accountIdOrSlug && t.slug !== accountIdOrSlug
239
+ );
240
+ writeStore(teams);
241
+ if (removed) {
242
+ try {
243
+ const cfg = loadBrightsyConfig();
244
+ if (cfg.account_id === removed.id) {
245
+ if (teams[0]) {
246
+ applyConnectedTeamToCli(teams[0]);
247
+ }
248
+ }
249
+ } catch {
250
+ }
251
+ }
252
+ return listConnectedBrightsyTeams();
253
+ }
254
+ function brightsyMcpServerName(slug) {
255
+ const cleaned = slug.replace(/[^A-Za-z0-9_-]/g, "_").replace(/^_+|_+$/g, "");
256
+ return `brightsy_${cleaned || "team"}`;
257
+ }
258
+
259
+ export {
260
+ loadBrightsyConfig,
261
+ listConnectedBrightsyTeams,
262
+ getConnectedBrightsyTeamsRaw,
263
+ applyConnectedTeamToCli,
264
+ ensureConnectedBrightsyTeamTokens,
265
+ ensureCliTeamTracked,
266
+ connectBrightsyTeam,
267
+ disconnectBrightsyTeam,
268
+ brightsyMcpServerName
269
+ };
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ normalizeThinkingEffort
5
+ } from "./chunk-KGZBYWZ3.js";
6
+ import {
7
+ threadFilePath,
8
+ threadLockPath,
9
+ threadsDir
10
+ } from "./chunk-7MV3RXSC.js";
11
+
12
+ // src/store/thread-store.ts
13
+ import { randomUUID } from "crypto";
14
+ import {
15
+ existsSync,
16
+ readFileSync,
17
+ renameSync,
18
+ unlinkSync,
19
+ writeFileSync,
20
+ readdirSync
21
+ } from "fs";
22
+ import lockfile from "proper-lockfile";
23
+ function nowIso() {
24
+ return (/* @__PURE__ */ new Date()).toISOString();
25
+ }
26
+ function resolveThreadEffort(raw) {
27
+ const fromField = normalizeThinkingEffort(raw.effort);
28
+ if (fromField) return fromField;
29
+ if (raw.fast) return "low";
30
+ return "high";
31
+ }
32
+ function normalizeThread(raw) {
33
+ return {
34
+ ...raw,
35
+ model: raw.model ?? null,
36
+ effort: resolveThreadEffort(raw),
37
+ fast: Boolean(raw.fast),
38
+ planMode: Boolean(raw.planMode),
39
+ autonomy: raw.autonomy ?? "default",
40
+ lastError: raw.lastError ?? null,
41
+ agentPid: raw.agentPid ?? null,
42
+ attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
43
+ prTitle: raw.prTitle ?? null,
44
+ userSetTitle: Boolean(raw.userSetTitle),
45
+ activeRuns: Array.isArray(raw.activeRuns) ? raw.activeRuns : [],
46
+ quotaResumeAt: raw.quotaResumeAt ?? null,
47
+ quotaContinuedFromId: raw.quotaContinuedFromId ?? null
48
+ };
49
+ }
50
+ function createEmptyThread(partial) {
51
+ const ts = nowIso();
52
+ return {
53
+ id: randomUUID(),
54
+ sessionId: partial.sessionId ?? null,
55
+ autonomy: partial.autonomy ?? "default",
56
+ model: partial.model ?? null,
57
+ effort: partial.effort ?? "high",
58
+ fast: partial.fast ?? false,
59
+ planMode: partial.planMode ?? false,
60
+ sourceIsFork: partial.sourceIsFork ?? false,
61
+ status: partial.status ?? "idle",
62
+ queue: partial.queue ?? [],
63
+ parentThreadId: partial.parentThreadId ?? null,
64
+ devPort: partial.devPort ?? null,
65
+ activeRuns: partial.activeRuns ?? [],
66
+ prUrl: partial.prUrl ?? null,
67
+ prTitle: partial.prTitle ?? null,
68
+ userSetTitle: partial.userSetTitle ?? false,
69
+ messages: partial.messages ?? [],
70
+ attachments: partial.attachments ?? [],
71
+ createdAt: ts,
72
+ updatedAt: ts,
73
+ title: partial.title,
74
+ sourceType: partial.sourceType,
75
+ sourceRef: partial.sourceRef,
76
+ branchName: partial.branchName,
77
+ worktreePath: partial.worktreePath,
78
+ repoPath: partial.repoPath,
79
+ agent: partial.agent,
80
+ lastError: null,
81
+ agentPid: null
82
+ };
83
+ }
84
+ async function withThreadLock(id, fn) {
85
+ const lockPath = threadLockPath(id);
86
+ writeFileSync(lockPath, "", { flag: "a" });
87
+ let release;
88
+ try {
89
+ release = await lockfile.lock(lockPath, {
90
+ retries: { retries: 10, minTimeout: 50, maxTimeout: 200 },
91
+ stale: 6e4
92
+ });
93
+ return await fn();
94
+ } finally {
95
+ if (release) await release();
96
+ }
97
+ }
98
+ function readThread(id) {
99
+ const path = threadFilePath(id);
100
+ if (!existsSync(path)) return null;
101
+ const raw = readFileSync(path, "utf8");
102
+ return normalizeThread(JSON.parse(raw));
103
+ }
104
+ function writeThread(thread) {
105
+ const path = threadFilePath(idPath(thread.id));
106
+ const tmp = `${path}.${process.pid}.tmp`;
107
+ const next = { ...thread, updatedAt: nowIso() };
108
+ writeFileSync(tmp, JSON.stringify(next, null, 2), "utf8");
109
+ renameSync(tmp, path);
110
+ }
111
+ function idPath(id) {
112
+ return id;
113
+ }
114
+ function listThreads(opts) {
115
+ const files = readdirSync(threadsDir()).filter((f) => f.endsWith(".json"));
116
+ const threads = files.map((f) => {
117
+ try {
118
+ return normalizeThread(
119
+ JSON.parse(readFileSync(threadFilePath(f.replace(/\.json$/, "")), "utf8"))
120
+ );
121
+ } catch {
122
+ return null;
123
+ }
124
+ }).filter((t) => t !== null).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
125
+ if (opts?.includeArchived) return threads;
126
+ return threads.filter((t) => t.status !== "archived");
127
+ }
128
+ function deleteThreadRecord(id) {
129
+ const path = threadFilePath(id);
130
+ if (existsSync(path)) unlinkSync(path);
131
+ const lock = threadLockPath(id);
132
+ if (existsSync(lock)) {
133
+ try {
134
+ unlinkSync(lock);
135
+ } catch {
136
+ }
137
+ }
138
+ }
139
+ function updateThread(id, patch) {
140
+ const current = readThread(id);
141
+ if (!current) throw new Error(`Thread not found: ${id}`);
142
+ const next = { ...current, ...patch, id: current.id, updatedAt: nowIso() };
143
+ writeThread(next);
144
+ return next;
145
+ }
146
+ function appendMessage(id, message) {
147
+ const current = readThread(id);
148
+ if (!current) throw new Error(`Thread not found: ${id}`);
149
+ return updateThread(id, { messages: [...current.messages, message] });
150
+ }
151
+ function setStatus(id, status, lastError) {
152
+ return updateThread(id, { status, lastError: lastError ?? null });
153
+ }
154
+ function findThreadByRef(ref) {
155
+ const all = listThreads({ includeArchived: true });
156
+ return all.find((t) => t.id === ref || t.id.startsWith(ref) || t.branchName === ref || t.title === ref) ?? null;
157
+ }
158
+
159
+ export {
160
+ resolveThreadEffort,
161
+ normalizeThread,
162
+ createEmptyThread,
163
+ withThreadLock,
164
+ readThread,
165
+ writeThread,
166
+ listThreads,
167
+ deleteThreadRecord,
168
+ updateThread,
169
+ appendMessage,
170
+ setStatus,
171
+ findThreadByRef
172
+ };
@@ -0,0 +1,30 @@
1
+ // src/agents/orchestrator-capable.ts
2
+ var ORCHESTRATOR_AGENT_KINDS = [
3
+ "claude",
4
+ "codex",
5
+ "opencode",
6
+ "cursor"
7
+ ];
8
+ function isOrchestratorCapableAgent(agent) {
9
+ return Boolean(
10
+ agent && ORCHESTRATOR_AGENT_KINDS.includes(agent)
11
+ );
12
+ }
13
+ function assertOrchestratorCapableAgent(agent, context = "orchestration") {
14
+ if (!isOrchestratorCapableAgent(agent)) {
15
+ throw new Error(
16
+ `${agent} cannot run ${context} \u2014 it does not support Sideboard MCP. Use Claude, Cursor, Codex, or OpenCode.`
17
+ );
18
+ }
19
+ return agent;
20
+ }
21
+ function coerceOrchestratorAgent(agent, fallback = "claude") {
22
+ return isOrchestratorCapableAgent(agent) ? agent : fallback;
23
+ }
24
+
25
+ export {
26
+ ORCHESTRATOR_AGENT_KINDS,
27
+ isOrchestratorCapableAgent,
28
+ assertOrchestratorCapableAgent,
29
+ coerceOrchestratorAgent
30
+ };