@sideboard-ai/core 0.1.127 → 0.1.132

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.
@@ -1,3 +1,10 @@
1
+ import {
2
+ brightsyAccessTokenNeedsRefresh,
3
+ ensureBrightsyLocalConfigFresh,
4
+ loadBrightsyConfig,
5
+ refreshBrightsyAccessToken,
6
+ saveBrightsyConfig
7
+ } from "./chunk-AQESMTCZ.js";
1
8
  import {
2
9
  appDataDir
3
10
  } from "./chunk-QZQEXME2.js";
@@ -6,37 +13,12 @@ import {
6
13
  } from "./chunk-DVM4ID64.js";
7
14
 
8
15
  // src/brightsy/connected-teams.ts
9
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
10
- import { join as join2 } from "path";
11
-
12
- // src/brightsy/config.ts
13
- import { existsSync, readFileSync, writeFileSync } from "fs";
14
- import { homedir } from "os";
16
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
15
17
  import { join } from "path";
16
- function brightsyConfigPath() {
17
- return join(homedir(), ".brightsy", "config.json");
18
- }
19
- function loadBrightsyConfig() {
20
- const path = brightsyConfigPath();
21
- if (!existsSync(path)) {
22
- throw new Error("Brightsy not logged in \u2014 run `brightsy login` first");
23
- }
24
- const raw = JSON.parse(readFileSync(path, "utf8"));
25
- if (!raw.access_token || !raw.account_id) {
26
- throw new Error("Brightsy config incomplete \u2014 run `brightsy login`");
27
- }
28
- return raw;
29
- }
30
- function saveBrightsyConfig(cfg) {
31
- writeFileSync(brightsyConfigPath(), `${JSON.stringify(cfg, null, 2)}
32
- `, {
33
- mode: 384
34
- });
35
- }
36
18
 
37
19
  // src/brightsy/accounts.ts
38
20
  async function loadConnectedTeams() {
39
- const { listConnectedBrightsyTeams: listConnectedBrightsyTeams2 } = await import("./connected-teams-ZCOU6KCD.js");
21
+ const { listConnectedBrightsyTeams: listConnectedBrightsyTeams2 } = await import("./connected-teams-S6E3D5C7.js");
40
22
  return listConnectedBrightsyTeams2();
41
23
  }
42
24
  async function runBrightsyTeamsJson(args) {
@@ -64,11 +46,15 @@ async function listBrightsyAccounts() {
64
46
  }
65
47
  async function getBrightsySession() {
66
48
  try {
49
+ const { ensureBrightsyLocalConfigFresh: ensureBrightsyLocalConfigFresh2 } = await import("./oauth-FT7EL2PY.js");
50
+ const { ensureConnectedBrightsyTeamTokens: ensureConnectedBrightsyTeamTokens2 } = await import("./connected-teams-S6E3D5C7.js");
51
+ await ensureBrightsyLocalConfigFresh2();
52
+ await ensureConnectedBrightsyTeamTokens2();
67
53
  loadBrightsyConfig();
68
54
  const data = await runBrightsyTeamsJson([]);
69
55
  const accounts = Array.isArray(data.teams) ? data.teams : [];
70
56
  const active = data.active ?? accounts.find((a) => a.active) ?? null;
71
- const { ensureCliTeamTracked: ensureCliTeamTracked2 } = await import("./connected-teams-ZCOU6KCD.js");
57
+ const { ensureCliTeamTracked: ensureCliTeamTracked2 } = await import("./connected-teams-S6E3D5C7.js");
72
58
  const connectedTeams = ensureCliTeamTracked2(
73
59
  active ? { id: active.id, slug: active.slug, name: active.name } : void 0
74
60
  );
@@ -108,20 +94,20 @@ async function performBrightsyAccountSwitch(accountIdOrSlug) {
108
94
  return { cfg, target: data.active };
109
95
  }
110
96
  async function switchBrightsyAccount(accountIdOrSlug) {
111
- const { connectBrightsyTeam: connectBrightsyTeam2 } = await import("./connected-teams-ZCOU6KCD.js");
97
+ const { connectBrightsyTeam: connectBrightsyTeam2 } = await import("./connected-teams-S6E3D5C7.js");
112
98
  await connectBrightsyTeam2(accountIdOrSlug);
113
99
  return getBrightsySession();
114
100
  }
115
101
 
116
102
  // src/brightsy/connected-teams.ts
117
103
  function storePath() {
118
- return join2(appDataDir(), "brightsy-teams.json");
104
+ return join(appDataDir(), "brightsy-teams.json");
119
105
  }
120
106
  function readStore() {
121
107
  const path = storePath();
122
- if (!existsSync2(path)) return [];
108
+ if (!existsSync(path)) return [];
123
109
  try {
124
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
110
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
125
111
  return Array.isArray(parsed.teams) ? parsed.teams : [];
126
112
  } catch {
127
113
  return [];
@@ -130,7 +116,7 @@ function readStore() {
130
116
  function writeStore(teams) {
131
117
  mkdirSync(appDataDir(), { recursive: true });
132
118
  const path = storePath();
133
- writeFileSync2(path, `${JSON.stringify({ teams }, null, 2)}
119
+ writeFileSync(path, `${JSON.stringify({ teams }, null, 2)}
134
120
  `, {
135
121
  mode: 384
136
122
  });
@@ -166,7 +152,6 @@ function applyConnectedTeamToCli(team) {
166
152
  }
167
153
  async function refreshTeamToken(team) {
168
154
  if (!team.refresh_token) return team;
169
- const endpoint = (team.endpoint || "https://brightsy.ai").replace(/\/$/, "");
170
155
  const cfg = (() => {
171
156
  try {
172
157
  return loadBrightsyConfig();
@@ -174,39 +159,34 @@ async function refreshTeamToken(team) {
174
159
  return null;
175
160
  }
176
161
  })();
177
- const clientId = cfg?.oauth_client_id || "brightsy-cli";
178
- const res = await fetch(`${endpoint}/oauth/token`, {
179
- method: "POST",
180
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
181
- body: new URLSearchParams({
182
- grant_type: "refresh_token",
183
- refresh_token: team.refresh_token,
184
- client_id: clientId
185
- })
162
+ const grant = await refreshBrightsyAccessToken({
163
+ endpoint: team.endpoint || "https://brightsy.ai",
164
+ refreshToken: team.refresh_token,
165
+ clientId: cfg?.oauth_client_id
186
166
  });
187
- if (!res.ok) return team;
188
- const data = await res.json();
189
- if (!data.access_token) return team;
167
+ if (!grant) return team;
190
168
  return {
191
169
  ...team,
192
- access_token: data.access_token,
193
- refresh_token: data.refresh_token || team.refresh_token,
194
- expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : team.expires_at
170
+ access_token: grant.access_token,
171
+ refresh_token: grant.refresh_token || team.refresh_token,
172
+ expires_at: grant.expires_at ?? team.expires_at
195
173
  };
196
174
  }
197
175
  async function ensureConnectedBrightsyTeamTokens() {
176
+ await ensureBrightsyLocalConfigFresh().catch(() => null);
198
177
  const teams = readStore();
199
178
  if (teams.length === 0) return [];
200
179
  const next = [];
201
180
  let changed = false;
202
181
  for (const team of teams) {
203
- const expired = typeof team.expires_at === "number" && Date.now() >= team.expires_at - 6e4;
204
- if (!expired) {
182
+ if (!team.refresh_token || !brightsyAccessTokenNeedsRefresh(team.expires_at)) {
205
183
  next.push(team);
206
184
  continue;
207
185
  }
208
186
  const refreshed = await refreshTeamToken(team);
209
- if (refreshed.access_token !== team.access_token) changed = true;
187
+ if (refreshed.access_token !== team.access_token || refreshed.refresh_token !== team.refresh_token || refreshed.expires_at !== team.expires_at) {
188
+ changed = true;
189
+ }
210
190
  next.push(refreshed);
211
191
  }
212
192
  if (changed) writeStore(next);
@@ -294,9 +274,6 @@ function brightsyMcpServerName(slug) {
294
274
  }
295
275
 
296
276
  export {
297
- brightsyConfigPath,
298
- loadBrightsyConfig,
299
- saveBrightsyConfig,
300
277
  listBrightsyAccounts,
301
278
  getBrightsySession,
302
279
  switchBrightsyAccount,
@@ -8,6 +8,7 @@ import {
8
8
  contextTokens,
9
9
  fallbackTurnFailDetail,
10
10
  finalizeParts,
11
+ formatAgentErrorContinuePrompt,
11
12
  formatTurnExitError,
12
13
  getAdapter,
13
14
  isBrightsyNdjsonLine,
@@ -23,13 +24,14 @@ import {
23
24
  partsToAssistantText,
24
25
  pushTurnStderr,
25
26
  resolveQuotaFallbackAgent,
27
+ shouldFeedErrorBackToAgent,
26
28
  shouldRetryFailedAgentTurn,
27
29
  stripBrightsyNdjsonNoise,
28
30
  sumUsageList,
29
31
  summarizeTurnStderr,
30
32
  toolDescription,
31
33
  turnFailChatText
32
- } from "./chunk-ZMUOPTKZ.js";
34
+ } from "./chunk-PK4PQUPU.js";
33
35
  import {
34
36
  addWorkspace,
35
37
  ensureWorkspace,
@@ -380,7 +382,7 @@ async function continueSourceThread(threadId, prompt) {
380
382
  await continueOnReply(threadId, prompt);
381
383
  return;
382
384
  }
383
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-3JLPPTCF.js");
385
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-JA65XKB6.js");
384
386
  await getOrchestrator2().send(threadId, prompt);
385
387
  } catch {
386
388
  }
@@ -2288,7 +2290,49 @@ function isMergedPrState(prUrl, prState) {
2288
2290
  if (!prUrl?.trim()) return false;
2289
2291
  return (prState ?? "").trim().toUpperCase() === "MERGED";
2290
2292
  }
2291
- function groupHomeBoardWorktrees(threads) {
2293
+ var DEFAULT_WORKTREE_SORT = "created";
2294
+ function groupCreatedAt(group) {
2295
+ let min = group[0]?.createdAt ?? "";
2296
+ for (const t of group) {
2297
+ if (t.createdAt && t.createdAt < min) min = t.createdAt;
2298
+ }
2299
+ return min;
2300
+ }
2301
+ function groupUpdatedAt(group) {
2302
+ let max = group[0]?.updatedAt ?? "";
2303
+ for (const t of group) {
2304
+ if (t.updatedAt && t.updatedAt > max) max = t.updatedAt;
2305
+ }
2306
+ return max;
2307
+ }
2308
+ function sortThreadsInWorktree(list) {
2309
+ return [...list].sort((a, b) => {
2310
+ const created = a.createdAt.localeCompare(b.createdAt);
2311
+ if (created !== 0) return created;
2312
+ return a.id.localeCompare(b.id);
2313
+ });
2314
+ }
2315
+ function compareWorktreeGroups(a, b, mode, labelOf) {
2316
+ if (mode === "name") {
2317
+ const named = (labelOf?.(a) ?? a[0]?.id ?? "").localeCompare(
2318
+ labelOf?.(b) ?? b[0]?.id ?? "",
2319
+ void 0,
2320
+ { sensitivity: "base" }
2321
+ );
2322
+ if (named !== 0) return named;
2323
+ } else if (mode === "activity") {
2324
+ const activity = groupUpdatedAt(b).localeCompare(groupUpdatedAt(a));
2325
+ if (activity !== 0) return activity;
2326
+ } else {
2327
+ const created = groupCreatedAt(b).localeCompare(groupCreatedAt(a));
2328
+ if (created !== 0) return created;
2329
+ }
2330
+ return (a[0]?.id ?? "").localeCompare(b[0]?.id ?? "");
2331
+ }
2332
+ function defaultWorktreeGroupLabel(group) {
2333
+ return worktreeDisplayLabelForGroup(group);
2334
+ }
2335
+ function groupHomeBoardWorktrees(threads, sort = DEFAULT_WORKTREE_SORT, labelOf = defaultWorktreeGroupLabel) {
2292
2336
  const map = /* @__PURE__ */ new Map();
2293
2337
  for (const t of threads) {
2294
2338
  const key = normalizeWorktreePath(t.worktreePath);
@@ -2296,7 +2340,7 @@ function groupHomeBoardWorktrees(threads) {
2296
2340
  list.push(t);
2297
2341
  map.set(key, list);
2298
2342
  }
2299
- return [...map.values()].map((list) => list.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))).sort((a, b) => (b[0]?.updatedAt ?? "").localeCompare(a[0]?.updatedAt ?? ""));
2343
+ return [...map.values()].map((list) => sortThreadsInWorktree(list)).sort((a, b) => compareWorktreeGroups(a, b, sort, labelOf));
2300
2344
  }
2301
2345
  function classifyWorktreeColumn(group) {
2302
2346
  if (group.some((t) => isMergedPrState(t.prUrl, t.prState))) return "done";
@@ -2597,8 +2641,7 @@ function assembleHomeBoard(input) {
2597
2641
  const kind = input.kind ?? "all";
2598
2642
  const limit = Math.max(1, input.limit ?? BOARD_PAGE_SIZE);
2599
2643
  const wsName = input.workspaceName ?? (() => "");
2600
- const byUpdated = (a, b) => b.updatedAt.localeCompare(a.updatedAt);
2601
- const live = input.threads.filter((t) => t.status !== "archived" && isHomeBoardThread(t)).sort(byUpdated);
2644
+ const live = input.threads.filter((t) => t.status !== "archived" && isHomeBoardThread(t));
2602
2645
  const columns = emptyColumns();
2603
2646
  const hidden = emptyHidden();
2604
2647
  const totals = {
@@ -2796,7 +2839,7 @@ async function createThread(input, _onSetupLine) {
2796
2839
  return readThread(thread.id) ?? thread;
2797
2840
  }
2798
2841
  async function listLinearIssues(agent, repoPath) {
2799
- const { getAdapter: getAdapter2 } = await import("./agents-7BUQY2WX.js");
2842
+ const { getAdapter: getAdapter2 } = await import("./agents-LGJLZMM3.js");
2800
2843
  await requireAgent(agent, { requireLinear: true });
2801
2844
  const adapter = getAdapter2(agent);
2802
2845
  if (!adapter.listLinearIssues) {
@@ -5054,7 +5097,8 @@ function formatArtifactDirective() {
5054
5097
  "```html",
5055
5098
  "<!DOCTYPE html><html><head><title>Demo</title></head><body><h1>Hi</h1></body></html>",
5056
5099
  "```",
5057
- "2) Or call Sideboard MCP `present_artifact` with title, type (html|svg|markdown), and content \u2014 not both a fence and this tool for the same body.",
5100
+ "2) Or call Sideboard MCP `present_artifact` with title, type (html|svg|markdown|react|log), and content \u2014 not both a fence and this tool for the same body.",
5101
+ " type=log is append-only: same `artifact_id`, `content` = new lines only (plus optional status/phase). Do not resend the full log or wrap it in HTML.",
5058
5102
  "CMS / JSON Schema forms & tables (Brightsy or any schema+schemaUi source):",
5059
5103
  "3) Call Sideboard MCP `present_schema` when the user needs to filter, edit, publish, or persist rows \u2014 including after you already showed a markdown table, if they then ask for an editable table. If they only need to read the data, a markdown table is enough. When you do call it, pass title, mode (table|form), and either:",
5060
5104
  " - datasource=brightsy + resource_id (record type UUID) after fetching types via Brightsy MCP, or",
@@ -5069,7 +5113,7 @@ function formatArtifactDirective() {
5069
5113
  ].join("\n");
5070
5114
  }
5071
5115
  function formatUiReminder() {
5072
- return "Sideboard UI: markdown table is enough to read data; present_schema if they ask to edit/filter (even after markdown); present_files for the file manager. html fence or present_artifact, not both for the same document. ask_user only for a real multiple-choice (not hellos or \u201Cwhat next?\u201D) \u2014 reply in chat. Do not say artifacts/CMS UI are unavailable.";
5116
+ return "Sideboard UI: markdown table is enough to read data; present_schema if they ask to edit/filter (even after markdown); present_files for the file manager. html fence or present_artifact, not both for the same document. type=log appends (same artifact_id, new lines only). ask_user only for a real multiple-choice (not hellos or \u201Cwhat next?\u201D) \u2014 reply in chat. Do not say artifacts/CMS UI are unavailable.";
5073
5117
  }
5074
5118
 
5075
5119
  // src/threads/sync-branch.ts
@@ -5340,7 +5384,7 @@ function formatScheduledPrompt(name, prompt) {
5340
5384
  ${prompt}`;
5341
5385
  }
5342
5386
  async function defaultDeps() {
5343
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-3JLPPTCF.js");
5387
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-JA65XKB6.js");
5344
5388
  const orch = getOrchestrator2();
5345
5389
  return {
5346
5390
  findThread: (id) => findThreadByRef(id),
@@ -5491,6 +5535,11 @@ var Orchestrator = class {
5491
5535
  turnBaselines = /* @__PURE__ */ new Map();
5492
5536
  /** Timers for orchestration session-quota auto-resume. */
5493
5537
  quotaResumeTimers = /* @__PURE__ */ new Map();
5538
+ /**
5539
+ * Threads that already got a crash-continue turn. Cleared on a successful
5540
+ * finish or a user send so a later crash can recover again.
5541
+ */
5542
+ crashContinued = /* @__PURE__ */ new Set();
5494
5543
  maxConcurrent;
5495
5544
  runningCount = 0;
5496
5545
  constructor(opts) {
@@ -5704,6 +5753,29 @@ var Orchestrator = class {
5704
5753
  );
5705
5754
  }
5706
5755
  }
5756
+ /**
5757
+ * Cursor-style recovery: after a runner crash, feed the error back as the
5758
+ * next prompt so the same agent can continue instead of sitting on lastError.
5759
+ */
5760
+ maybeEnqueueCrashContinue(threadId, opts) {
5761
+ if (this.crashContinued.has(threadId)) return;
5762
+ if (this.haltDrain.has(threadId)) return;
5763
+ if (!shouldFeedErrorBackToAgent({
5764
+ detail: opts.detail,
5765
+ assistantText: opts.assistantText,
5766
+ partsCount: opts.partsCount
5767
+ })) {
5768
+ return;
5769
+ }
5770
+ const thread = readThread(threadId);
5771
+ if (!thread || thread.status === "archived") return;
5772
+ this.crashContinued.add(threadId);
5773
+ const prompt = formatAgentErrorContinuePrompt(opts.detail);
5774
+ const queue = [prompt, ...thread.queue];
5775
+ updateThread(threadId, { queue });
5776
+ this.emit({ type: "queue_changed", threadId, queue });
5777
+ this.haltDrain.delete(threadId);
5778
+ }
5707
5779
  getThreads(includeArchived = false) {
5708
5780
  return listThreads({ includeArchived });
5709
5781
  }
@@ -5803,15 +5875,20 @@ var Orchestrator = class {
5803
5875
  throw new Error(`Thread is archived: ${thread.id}`);
5804
5876
  }
5805
5877
  const queue = [...current.queue, prompt];
5878
+ this.crashContinued.delete(thread.id);
5806
5879
  this.haltDrain.delete(thread.id);
5807
- const patch = { queue, status: "queued" };
5880
+ const inFlight = this.activeTurns.has(thread.id) || this.startingTurns.has(thread.id) || current.status === "running";
5881
+ const patch = { queue };
5882
+ if (!inFlight) patch.status = "queued";
5808
5883
  const pid = current.agentPid;
5809
5884
  if (typeof pid === "number" && pid > 0 && !isPidAlive(pid)) {
5810
5885
  patch.agentPid = null;
5811
5886
  }
5812
5887
  updateThread(thread.id, patch);
5813
5888
  this.emit({ type: "queue_changed", threadId: thread.id, queue });
5814
- this.emit({ type: "status_changed", threadId: thread.id, status: "queued" });
5889
+ if (!inFlight) {
5890
+ this.emit({ type: "status_changed", threadId: thread.id, status: "queued" });
5891
+ }
5815
5892
  if (thisProcessShouldDrainAgentQueues()) {
5816
5893
  void this.drainQueue(thread.id);
5817
5894
  }
@@ -6132,7 +6209,7 @@ var Orchestrator = class {
6132
6209
  let parts = result.parts;
6133
6210
  let usage = result.usage ?? void 0;
6134
6211
  let exitCode = result.exitCode;
6135
- if (this.requireThread(threadId).agent === "cursor" && exitCode !== 0 && !assistantText && parts.length === 0) {
6212
+ if (!this.stoppedTurns.has(threadId) && this.requireThread(threadId).agent === "cursor" && exitCode !== 0 && !assistantText && parts.length === 0) {
6136
6213
  const sessionId = result.sessionId || this.requireThread(threadId).sessionId || "";
6137
6214
  if (sessionId) {
6138
6215
  const { recoverFinishedCursorRun } = await import("./cursor-recover-NOQYOFRF.js");
@@ -6153,7 +6230,7 @@ var Orchestrator = class {
6153
6230
  }
6154
6231
  let lastStderr = summarizeTurnStderr(stderrTail);
6155
6232
  let detail = lastStderr || (exitCode !== 0 ? fallbackTurnFailDetail(assistantText) : "");
6156
- if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && this.requireThread(threadId).agent !== "brightsy" && shouldRetryFailedAgentTurn(detail, {
6233
+ if (exitCode !== 0 && !assistantText && parts.length === 0 && !this.stoppedTurns.has(threadId) && shouldRetryFailedAgentTurn(detail, {
6157
6234
  hasSession: Boolean(this.requireThread(threadId).sessionId)
6158
6235
  })) {
6159
6236
  updateThread(threadId, { sessionId: null });
@@ -6249,7 +6326,11 @@ var Orchestrator = class {
6249
6326
  )) {
6250
6327
  updateThread(threadId, { sessionId: null });
6251
6328
  }
6252
- await syncThreadBranchFromGit(threadId);
6329
+ if (this.stoppedTurns.has(threadId)) {
6330
+ void syncThreadBranchFromGit(threadId).catch(() => void 0);
6331
+ } else {
6332
+ await syncThreadBranchFromGit(threadId);
6333
+ }
6253
6334
  if (this.stoppedTurns.has(threadId)) {
6254
6335
  const stopped = writeLiveStatus(threadId, "stopped");
6255
6336
  if (stopped?.status === "stopped") {
@@ -6273,9 +6354,16 @@ var Orchestrator = class {
6273
6354
  });
6274
6355
  }
6275
6356
  this.emit({ type: "turn_finished", threadId, exitCode });
6276
- if (exitCode !== 0) {
6357
+ if (exitCode === 0) {
6358
+ this.crashContinued.delete(threadId);
6359
+ } else {
6277
6360
  const blob = [chatText, detail].filter(Boolean).join("\n");
6278
6361
  void this.maybeHandleOrchestrationQuotaFailover(threadId, blob);
6362
+ this.maybeEnqueueCrashContinue(threadId, {
6363
+ detail: detail || failDetail,
6364
+ assistantText: chatText,
6365
+ partsCount: parts.length
6366
+ });
6279
6367
  }
6280
6368
  }
6281
6369
  } catch (err) {
@@ -6295,6 +6383,11 @@ var Orchestrator = class {
6295
6383
  }
6296
6384
  this.emit({ type: "turn_finished", threadId, exitCode: 1 });
6297
6385
  void this.maybeHandleOrchestrationQuotaFailover(threadId, message);
6386
+ this.maybeEnqueueCrashContinue(threadId, {
6387
+ detail: message,
6388
+ assistantText: "",
6389
+ partsCount: 0
6390
+ });
6298
6391
  }
6299
6392
  } finally {
6300
6393
  this.startingTurns.delete(threadId);