@sideboard-ai/core 0.1.24 → 0.1.30

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.
@@ -1776,6 +1776,9 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
1776
1776
  parts.push("");
1777
1777
  parts.push(`## Attachment: ${att.name}`);
1778
1778
  parts.push(`Kind: ${att.kind}`);
1779
+ if (att.path) {
1780
+ parts.push(`Path in worktree: \`${att.path}\``);
1781
+ }
1779
1782
  parts.push("");
1780
1783
  parts.push(att.content);
1781
1784
  }
@@ -1818,6 +1821,234 @@ function expandComposerPrompt(worktreePath, prompt, opts) {
1818
1821
  };
1819
1822
  }
1820
1823
 
1824
+ // src/composer/stage-files.ts
1825
+ import { copyFileSync as copyFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync2 } from "fs";
1826
+ import { basename as basename2, extname, join as join6 } from "path";
1827
+ import { randomUUID } from "crypto";
1828
+ var IMAGE_EXTENSIONS2 = /* @__PURE__ */ new Set([
1829
+ "png",
1830
+ "jpg",
1831
+ "jpeg",
1832
+ "gif",
1833
+ "webp",
1834
+ "svg",
1835
+ "bmp",
1836
+ "ico"
1837
+ ]);
1838
+ var IMAGE_MIME_BY_EXT = {
1839
+ png: "image/png",
1840
+ jpg: "image/jpeg",
1841
+ jpeg: "image/jpeg",
1842
+ gif: "image/gif",
1843
+ webp: "image/webp",
1844
+ svg: "image/svg+xml",
1845
+ bmp: "image/bmp",
1846
+ ico: "image/x-icon"
1847
+ };
1848
+ var ATTACHMENTS_DIR = ".sideboard/attachments";
1849
+ var ATTACHMENTS_GITIGNORE = `# Sideboard review / composer attachments (local only)
1850
+ *
1851
+ !.gitignore
1852
+ `;
1853
+ var MAX_INLINE_BYTES = 4e5;
1854
+ var MAX_PREVIEW_BYTES = 5e6;
1855
+ function fileExtension(filePath) {
1856
+ const base = basename2(filePath).toLowerCase();
1857
+ return base.includes(".") ? base.split(".").pop() || "" : "";
1858
+ }
1859
+ function isImageFilePath(filePath) {
1860
+ return IMAGE_EXTENSIONS2.has(fileExtension(filePath));
1861
+ }
1862
+ function imageMimeType(filePath) {
1863
+ return IMAGE_MIME_BY_EXT[fileExtension(filePath)] || "image/png";
1864
+ }
1865
+ function ensureAttachmentsDir(worktreePath) {
1866
+ const dir = join6(worktreePath, ATTACHMENTS_DIR);
1867
+ mkdirSync3(dir, { recursive: true });
1868
+ const gi = join6(dir, ".gitignore");
1869
+ if (!existsSync6(gi)) {
1870
+ writeFileSync2(gi, ATTACHMENTS_GITIGNORE, "utf8");
1871
+ }
1872
+ return dir;
1873
+ }
1874
+ function uniqueAttachmentName(dir, originalName) {
1875
+ const safe = originalName.replace(/[/\\]/g, "_") || "file";
1876
+ if (!existsSync6(join6(dir, safe))) return safe;
1877
+ const ext = extname(safe);
1878
+ const stem = ext ? safe.slice(0, -ext.length) : safe;
1879
+ for (let i = 1; i < 1e4; i++) {
1880
+ const candidate = `${stem}-${i}${ext}`;
1881
+ if (!existsSync6(join6(dir, candidate))) return candidate;
1882
+ }
1883
+ return `${stem}-${randomUUID()}${ext}`;
1884
+ }
1885
+ function previewDataUrlFromBuf(filePath, buf) {
1886
+ if (!isImageFilePath(filePath)) return void 0;
1887
+ if (buf.length > MAX_PREVIEW_BYTES) return void 0;
1888
+ return `data:${imageMimeType(filePath)};base64,${buf.toString("base64")}`;
1889
+ }
1890
+ function attachmentFromBuffer(name, buf, opts) {
1891
+ const previewDataUrl = previewDataUrlFromBuf(name, buf);
1892
+ if (isImageFilePath(name)) {
1893
+ const pathHint = opts.path ? `\`${opts.path}\`` : opts.sourceLabel || name;
1894
+ return {
1895
+ id: randomUUID(),
1896
+ name,
1897
+ kind: "file",
1898
+ path: opts.path,
1899
+ previewDataUrl,
1900
+ content: [
1901
+ `Image attached: ${pathHint}`,
1902
+ opts.path ? `Use the Read tool on \`${opts.path}\` to view this image.` : "The image is shown in the composer; copy it into the worktree if you need to inspect pixels."
1903
+ ].join("\n")
1904
+ };
1905
+ }
1906
+ if (buf.length > MAX_INLINE_BYTES) {
1907
+ return {
1908
+ id: randomUUID(),
1909
+ name,
1910
+ kind: "file",
1911
+ path: opts.path,
1912
+ content: opts.path ? `(file too large to attach inline: \`${opts.path}\`, ${buf.length} bytes \u2014 use the Read tool)` : `(file too large to attach inline: ${opts.sourceLabel || name}, ${buf.length} bytes)`
1913
+ };
1914
+ }
1915
+ if (buf.includes(0)) {
1916
+ return {
1917
+ id: randomUUID(),
1918
+ name,
1919
+ kind: "file",
1920
+ path: opts.path,
1921
+ content: opts.path ? `(binary file at \`${opts.path}\` \u2014 use tools to inspect)` : `(binary file attached by path only: ${opts.sourceLabel || name})`
1922
+ };
1923
+ }
1924
+ return {
1925
+ id: randomUUID(),
1926
+ name,
1927
+ kind: "file",
1928
+ path: opts.path,
1929
+ content: buf.toString("utf8")
1930
+ };
1931
+ }
1932
+ function attachmentFromAbsolutePath(absolutePath) {
1933
+ const name = basename2(absolutePath);
1934
+ try {
1935
+ const st = statSync4(absolutePath);
1936
+ if (!st.isFile()) {
1937
+ return {
1938
+ id: randomUUID(),
1939
+ name,
1940
+ kind: "file",
1941
+ content: `(not a file: ${absolutePath})`
1942
+ };
1943
+ }
1944
+ const buf = readFileSync6(absolutePath);
1945
+ return attachmentFromBuffer(name, buf, { sourceLabel: absolutePath });
1946
+ } catch (err) {
1947
+ return {
1948
+ id: randomUUID(),
1949
+ name,
1950
+ kind: "file",
1951
+ content: `(could not read ${absolutePath}: ${err instanceof Error ? err.message : String(err)})`
1952
+ };
1953
+ }
1954
+ }
1955
+ function stageAbsolutePathsAsAttachments(worktreePath, absolutePaths) {
1956
+ if (absolutePaths.length === 0) return [];
1957
+ const dir = ensureAttachmentsDir(worktreePath);
1958
+ const out = [];
1959
+ for (const abs of absolutePaths) {
1960
+ const originalName = basename2(abs);
1961
+ try {
1962
+ const st = statSync4(abs);
1963
+ if (!st.isFile()) continue;
1964
+ const name = uniqueAttachmentName(dir, originalName);
1965
+ const destAbs = join6(dir, name);
1966
+ copyFileSync2(abs, destAbs);
1967
+ const rel = `${ATTACHMENTS_DIR}/${name}`;
1968
+ const buf = readFileSync6(destAbs);
1969
+ out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
1970
+ } catch (err) {
1971
+ out.push({
1972
+ id: randomUUID(),
1973
+ name: originalName,
1974
+ kind: "file",
1975
+ content: `(could not attach ${abs}: ${err instanceof Error ? err.message : String(err)})`
1976
+ });
1977
+ }
1978
+ }
1979
+ return out;
1980
+ }
1981
+ function stageBuffersAsAttachments(worktreePath, buffers) {
1982
+ if (buffers.length === 0) return [];
1983
+ const dir = ensureAttachmentsDir(worktreePath);
1984
+ const out = [];
1985
+ for (const item of buffers) {
1986
+ const originalName = (item.name || "file").replace(/[/\\]/g, "_") || "file";
1987
+ try {
1988
+ const buf = Buffer.from(item.dataBase64, "base64");
1989
+ const name = uniqueAttachmentName(dir, originalName);
1990
+ const destAbs = join6(dir, name);
1991
+ writeFileSync2(destAbs, buf);
1992
+ const rel = `${ATTACHMENTS_DIR}/${name}`;
1993
+ out.push(attachmentFromBuffer(name, buf, { path: rel }));
1994
+ } catch (err) {
1995
+ out.push({
1996
+ id: randomUUID(),
1997
+ name: originalName,
1998
+ kind: "file",
1999
+ content: `(could not attach ${originalName}: ${err instanceof Error ? err.message : String(err)})`
2000
+ });
2001
+ }
2002
+ }
2003
+ return out;
2004
+ }
2005
+ function attachmentsFromBuffers(buffers) {
2006
+ return buffers.map((item) => {
2007
+ const name = (item.name || "file").replace(/[/\\]/g, "_") || "file";
2008
+ try {
2009
+ const buf = Buffer.from(item.dataBase64, "base64");
2010
+ return attachmentFromBuffer(name, buf, { sourceLabel: name });
2011
+ } catch (err) {
2012
+ return {
2013
+ id: randomUUID(),
2014
+ name,
2015
+ kind: "file",
2016
+ content: `(could not attach ${name}: ${err instanceof Error ? err.message : String(err)})`
2017
+ };
2018
+ }
2019
+ });
2020
+ }
2021
+ function attachmentsFromWorktreePaths(worktreePath, relativePaths) {
2022
+ const out = [];
2023
+ for (const rel of relativePaths) {
2024
+ if (!rel || rel.includes("..") || rel.startsWith("/")) {
2025
+ out.push({
2026
+ id: randomUUID(),
2027
+ name: basename2(rel) || "file",
2028
+ kind: "file",
2029
+ content: `(invalid path: ${rel})`
2030
+ });
2031
+ continue;
2032
+ }
2033
+ const name = basename2(rel);
2034
+ try {
2035
+ const abs = join6(worktreePath, rel);
2036
+ const st = statSync4(abs);
2037
+ if (!st.isFile()) continue;
2038
+ const buf = readFileSync6(abs);
2039
+ out.push(attachmentFromBuffer(name, buf, { path: rel, sourceLabel: abs }));
2040
+ } catch (err) {
2041
+ out.push({
2042
+ id: randomUUID(),
2043
+ name,
2044
+ kind: "file",
2045
+ content: `(could not read ${rel}: ${err instanceof Error ? err.message : String(err)})`
2046
+ });
2047
+ }
2048
+ }
2049
+ return out;
2050
+ }
2051
+
1821
2052
  // src/composer/summarize.ts
1822
2053
  var MAX_TRANSCRIPT_CHARS = 12e4;
1823
2054
  var MAX_SUMMARY_CHARS = 6e3;
@@ -2249,11 +2480,11 @@ async function confirmLand(thread, opts) {
2249
2480
  }
2250
2481
 
2251
2482
  // src/threads/create.ts
2252
- import { existsSync as existsSync6 } from "fs";
2483
+ import { existsSync as existsSync7 } from "fs";
2253
2484
  async function createThread(input, onSetupLine) {
2254
2485
  await requireAgent(input.agent);
2255
2486
  const repoPath = await resolveRepoRoot(input.repoPath);
2256
- if (!existsSync6(repoPath)) {
2487
+ if (!existsSync7(repoPath)) {
2257
2488
  throw new Error(`Repo not found: ${repoPath}`);
2258
2489
  }
2259
2490
  let sourceRef = input.sourceRef;
@@ -2336,7 +2567,7 @@ async function listLinearIssues(agent, repoPath) {
2336
2567
  }
2337
2568
 
2338
2569
  // src/threads/chat-tabs.ts
2339
- import { randomUUID } from "crypto";
2570
+ import { randomUUID as randomUUID2 } from "crypto";
2340
2571
  function sameWorktreePath(a, b) {
2341
2572
  return normalizeWorktreePath(a) === normalizeWorktreePath(b);
2342
2573
  }
@@ -2387,7 +2618,7 @@ function forkMessageSlice(from, throughIndex) {
2387
2618
  function buildForkTranscriptAttachment(baseTitle, messages) {
2388
2619
  const title = baseTitle || "Chat";
2389
2620
  return {
2390
- id: randomUUID(),
2621
+ id: randomUUID2(),
2391
2622
  name: `Transcript of ${title}.md`,
2392
2623
  kind: "transcript",
2393
2624
  content: formatTranscriptMarkdown(title, messages)
@@ -2445,35 +2676,39 @@ async function forkThreadWorktree(input, onSetupLine) {
2445
2676
  repoPath: from.repoPath,
2446
2677
  agent: input.agent ?? from.agent,
2447
2678
  autonomy: from.autonomy,
2679
+ model: from.model,
2680
+ fast: from.fast,
2681
+ planMode: from.planMode,
2448
2682
  title: input.title?.trim() || void 0,
2449
- parentThreadId: from.id
2683
+ parentThreadId: from.id,
2684
+ attachments: [attachment]
2450
2685
  },
2451
2686
  onSetupLine
2452
2687
  );
2453
- return updateThread(thread.id, { attachments: [attachment] });
2688
+ return thread;
2454
2689
  }
2455
2690
 
2456
2691
  // src/threads/adopt.ts
2457
2692
  import { execFileSync } from "child_process";
2458
2693
  import {
2459
- copyFileSync as copyFileSync2,
2460
- existsSync as existsSync7,
2694
+ copyFileSync as copyFileSync3,
2695
+ existsSync as existsSync8,
2461
2696
  mkdtempSync,
2462
2697
  readdirSync as readdirSync3,
2463
- readFileSync as readFileSync6,
2698
+ readFileSync as readFileSync7,
2464
2699
  rmSync
2465
2700
  } from "fs";
2466
2701
  import { tmpdir } from "os";
2467
- import { join as join6 } from "path";
2702
+ import { join as join7 } from "path";
2468
2703
  import Database from "better-sqlite3";
2469
- var CONDUCTOR_APP_SUPPORT = join6(
2704
+ var CONDUCTOR_APP_SUPPORT = join7(
2470
2705
  process.env.HOME ?? "",
2471
2706
  "Library",
2472
2707
  "Application Support",
2473
2708
  "com.conductor.app"
2474
2709
  );
2475
- var CONDUCTOR_DB = join6(CONDUCTOR_APP_SUPPORT, "conductor.db");
2476
- var CURSOR_SDK_STORE = join6(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
2710
+ var CONDUCTOR_DB = join7(CONDUCTOR_APP_SUPPORT, "conductor.db");
2711
+ var CURSOR_SDK_STORE = join7(CONDUCTOR_APP_SUPPORT, "cursor-sdk-store");
2477
2712
  function mapAgentType(raw) {
2478
2713
  if (!raw) return null;
2479
2714
  const v = raw.toLowerCase();
@@ -2485,7 +2720,7 @@ function mapAgentType(raw) {
2485
2720
  return null;
2486
2721
  }
2487
2722
  function resolveConductorCursorAgentId(workspacePath) {
2488
- if (!workspacePath || !existsSync7(CURSOR_SDK_STORE)) return null;
2723
+ if (!workspacePath || !existsSync8(CURSOR_SDK_STORE)) return null;
2489
2724
  const normalized = workspacePath.replace(/\/$/, "");
2490
2725
  let best = null;
2491
2726
  let hashes;
@@ -2495,11 +2730,11 @@ function resolveConductorCursorAgentId(workspacePath) {
2495
2730
  return null;
2496
2731
  }
2497
2732
  for (const hash of hashes) {
2498
- const agentsFile = join6(CURSOR_SDK_STORE, hash, "agents.ndjson");
2499
- if (!existsSync7(agentsFile)) continue;
2733
+ const agentsFile = join7(CURSOR_SDK_STORE, hash, "agents.ndjson");
2734
+ if (!existsSync8(agentsFile)) continue;
2500
2735
  let text;
2501
2736
  try {
2502
- text = readFileSync6(agentsFile, "utf8");
2737
+ text = readFileSync7(agentsFile, "utf8");
2503
2738
  } catch {
2504
2739
  continue;
2505
2740
  }
@@ -2523,7 +2758,7 @@ function resolveConductorCursorAgentId(workspacePath) {
2523
2758
  return best?.agentId ?? null;
2524
2759
  }
2525
2760
  async function adoptThread(input) {
2526
- if (!existsSync7(input.worktreePath)) {
2761
+ if (!existsSync8(input.worktreePath)) {
2527
2762
  throw new Error(`Worktree not found: ${input.worktreePath}`);
2528
2763
  }
2529
2764
  const repoPath = await resolveRepoRoot(input.worktreePath);
@@ -2550,18 +2785,18 @@ function conductorDbPath() {
2550
2785
  return CONDUCTOR_DB;
2551
2786
  }
2552
2787
  function listConductorWorkspaces() {
2553
- if (!existsSync7(CONDUCTOR_DB)) {
2788
+ if (!existsSync8(CONDUCTOR_DB)) {
2554
2789
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
2555
2790
  }
2556
- const tmp = mkdtempSync(join6(tmpdir(), "sideboard-conductor-"));
2557
- const snapshot = join6(tmp, "conductor.db");
2791
+ const tmp = mkdtempSync(join7(tmpdir(), "sideboard-conductor-"));
2792
+ const snapshot = join7(tmp, "conductor.db");
2558
2793
  try {
2559
- copyFileSync2(CONDUCTOR_DB, snapshot);
2794
+ copyFileSync3(CONDUCTOR_DB, snapshot);
2560
2795
  for (const suffix of ["-wal", "-shm"]) {
2561
2796
  const src = `${CONDUCTOR_DB}${suffix}`;
2562
- if (existsSync7(src)) {
2797
+ if (existsSync8(src)) {
2563
2798
  try {
2564
- copyFileSync2(src, `${snapshot}${suffix}`);
2799
+ copyFileSync3(src, `${snapshot}${suffix}`);
2565
2800
  } catch {
2566
2801
  }
2567
2802
  }
@@ -2641,18 +2876,18 @@ function listConductorWorkspaces() {
2641
2876
  }
2642
2877
  }
2643
2878
  function importConductorWorkspace(workspaceId) {
2644
- if (!existsSync7(CONDUCTOR_DB)) {
2879
+ if (!existsSync8(CONDUCTOR_DB)) {
2645
2880
  throw new Error(`Conductor DB not found at ${CONDUCTOR_DB}`);
2646
2881
  }
2647
- const tmp = mkdtempSync(join6(tmpdir(), "sideboard-conductor-"));
2648
- const snapshot = join6(tmp, "conductor.db");
2882
+ const tmp = mkdtempSync(join7(tmpdir(), "sideboard-conductor-"));
2883
+ const snapshot = join7(tmp, "conductor.db");
2649
2884
  try {
2650
- copyFileSync2(CONDUCTOR_DB, snapshot);
2885
+ copyFileSync3(CONDUCTOR_DB, snapshot);
2651
2886
  for (const suffix of ["-wal", "-shm"]) {
2652
2887
  const src = `${CONDUCTOR_DB}${suffix}`;
2653
- if (existsSync7(src)) {
2888
+ if (existsSync8(src)) {
2654
2889
  try {
2655
- copyFileSync2(src, `${snapshot}${suffix}`);
2890
+ copyFileSync3(src, `${snapshot}${suffix}`);
2656
2891
  } catch {
2657
2892
  }
2658
2893
  }
@@ -2670,7 +2905,7 @@ function importConductorWorkspace(workspaceId) {
2670
2905
  ).get(workspaceId);
2671
2906
  if (!row) throw new Error(`Conductor workspace not found: ${workspaceId}`);
2672
2907
  const worktreePath = String(row.workspacePath);
2673
- if (!existsSync7(worktreePath)) {
2908
+ if (!existsSync8(worktreePath)) {
2674
2909
  throw new Error(`Conductor worktree missing on disk: ${worktreePath}`);
2675
2910
  }
2676
2911
  let sessionId = null;
@@ -2741,8 +2976,8 @@ async function importConductorWorkspaceAsync(workspaceId) {
2741
2976
  }
2742
2977
 
2743
2978
  // src/git/orphan-cleanup.ts
2744
- import { existsSync as existsSync8, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2745
- import { join as join7 } from "path";
2979
+ import { existsSync as existsSync9, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
2980
+ import { join as join8 } from "path";
2746
2981
  function isSideboardWorktreePath(path) {
2747
2982
  return path.includes("/.sideboard/worktrees/") || path.includes("/sideboard/workspaces/");
2748
2983
  }
@@ -2753,7 +2988,7 @@ async function findOrphanWorktrees(repoPaths) {
2753
2988
  repoPaths?.length ? repoPaths : threads.map((t) => t.repoPath).filter(Boolean)
2754
2989
  );
2755
2990
  const homeRoot = sideboardWorkspacesDir();
2756
- if (existsSync8(homeRoot)) {
2991
+ if (existsSync9(homeRoot)) {
2757
2992
  try {
2758
2993
  for (const entry of readdirSync4(homeRoot, { withFileTypes: true })) {
2759
2994
  if (!entry.isDirectory()) continue;
@@ -2765,7 +3000,7 @@ async function findOrphanWorktrees(repoPaths) {
2765
3000
  const orphans = [];
2766
3001
  const seen = /* @__PURE__ */ new Set();
2767
3002
  for (const repoPath of repos) {
2768
- if (!repoPath || !existsSync8(repoPath)) continue;
3003
+ if (!repoPath || !existsSync9(repoPath)) continue;
2769
3004
  try {
2770
3005
  const wts = await listWorktrees(repoPath);
2771
3006
  for (const wt of wts) {
@@ -2776,7 +3011,7 @@ async function findOrphanWorktrees(repoPaths) {
2776
3011
  seen.add(path);
2777
3012
  let mtimeMs = 0;
2778
3013
  try {
2779
- mtimeMs = statSync4(path).mtimeMs;
3014
+ mtimeMs = statSync5(path).mtimeMs;
2780
3015
  } catch {
2781
3016
  mtimeMs = 0;
2782
3017
  }
@@ -2786,16 +3021,16 @@ async function findOrphanWorktrees(repoPaths) {
2786
3021
  }
2787
3022
  try {
2788
3023
  const root = worktreesRoot(repoPath);
2789
- if (existsSync8(root)) {
3024
+ if (existsSync9(root)) {
2790
3025
  for (const entry of readdirSync4(root, { withFileTypes: true })) {
2791
3026
  if (!entry.isDirectory()) continue;
2792
- const path = join7(root, entry.name).replace(/\/$/, "");
3027
+ const path = join8(root, entry.name).replace(/\/$/, "");
2793
3028
  if (known.has(path) || seen.has(path)) continue;
2794
- if (!existsSync8(join7(path, ".git"))) continue;
3029
+ if (!existsSync9(join8(path, ".git"))) continue;
2795
3030
  seen.add(path);
2796
3031
  let mtimeMs = 0;
2797
3032
  try {
2798
- mtimeMs = statSync4(path).mtimeMs;
3033
+ mtimeMs = statSync5(path).mtimeMs;
2799
3034
  } catch {
2800
3035
  mtimeMs = Date.now();
2801
3036
  }
@@ -2940,20 +3175,20 @@ async function applyThreadIntoMain(thread, opts) {
2940
3175
  }
2941
3176
 
2942
3177
  // src/git/clone-repo.ts
2943
- import { existsSync as existsSync9 } from "fs";
2944
- import { basename as basename2, join as join8 } from "path";
3178
+ import { existsSync as existsSync10 } from "fs";
3179
+ import { basename as basename3, join as join9 } from "path";
2945
3180
  import { execa as execa5 } from "execa";
2946
3181
  async function cloneRepoIntoSideboard(opts) {
2947
3182
  const url = opts.url.trim();
2948
3183
  if (!url) throw new Error("Clone URL is required");
2949
3184
  let name = opts.name?.trim();
2950
3185
  if (!name) {
2951
- const leaf = basename2(url.replace(/\/$/, "").replace(/\.git$/, ""));
3186
+ const leaf = basename3(url.replace(/\/$/, "").replace(/\.git$/, ""));
2952
3187
  name = leaf || "repo";
2953
3188
  }
2954
3189
  name = name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
2955
- const dest = join8(sideboardReposDir(), name);
2956
- if (existsSync9(dest)) {
3190
+ const dest = join9(sideboardReposDir(), name);
3191
+ if (existsSync10(dest)) {
2957
3192
  const repoPath2 = await resolveRepoRoot(dest);
2958
3193
  const workspace2 = await ensureWorkspace(repoPath2);
2959
3194
  return { repoPath: repoPath2, workspace: workspace2 };
@@ -2971,7 +3206,7 @@ async function cloneRepoIntoSideboard(opts) {
2971
3206
 
2972
3207
  // src/orchestrator/orchestrator.ts
2973
3208
  import { EventEmitter } from "events";
2974
- import { existsSync as existsSync10 } from "fs";
3209
+ import { existsSync as existsSync11 } from "fs";
2975
3210
 
2976
3211
  // src/threads/sync-branch.ts
2977
3212
  async function syncThreadBranchFromGit(threadId) {
@@ -3058,7 +3293,7 @@ var Orchestrator = class {
3058
3293
  }
3059
3294
  continue;
3060
3295
  }
3061
- if (!existsSync10(thread.worktreePath)) {
3296
+ if (!existsSync11(thread.worktreePath)) {
3062
3297
  setStatus(thread.id, "broken", "Worktree missing on disk");
3063
3298
  this.emit({ type: "status_changed", threadId: thread.id, status: "broken" });
3064
3299
  continue;
@@ -3870,6 +4105,23 @@ var Orchestrator = class {
3870
4105
  setAttachments(threadRef, attachments) {
3871
4106
  return updateThread(this.requireThread(threadRef).id, { attachments });
3872
4107
  }
4108
+ /**
4109
+ * Stage OS / worktree files into composer attachments (copies external files
4110
+ * into `.sideboard/attachments/` so agents can Read images and binaries).
4111
+ */
4112
+ attachComposerFiles(threadRef, opts) {
4113
+ const thread = this.requireThread(threadRef);
4114
+ const fromAbs = stageAbsolutePathsAsAttachments(
4115
+ thread.worktreePath,
4116
+ opts.absolutePaths ?? []
4117
+ );
4118
+ const fromRel = attachmentsFromWorktreePaths(
4119
+ thread.worktreePath,
4120
+ opts.relativePaths ?? []
4121
+ );
4122
+ const fromBuf = stageBuffersAsAttachments(thread.worktreePath, opts.buffers ?? []);
4123
+ return [...fromAbs, ...fromRel, ...fromBuf];
4124
+ }
3873
4125
  listWorktreeChats(threadRef) {
3874
4126
  const thread = this.requireThread(threadRef);
3875
4127
  return threadsSharingWorktree(thread.worktreePath);
@@ -3929,7 +4181,7 @@ var Orchestrator = class {
3929
4181
  updateThread(thread.id, { worktreePath: globalAgentCwd() });
3930
4182
  return setStatus(thread.id, "idle");
3931
4183
  }
3932
- if (!existsSync10(thread.worktreePath)) {
4184
+ if (!existsSync11(thread.worktreePath)) {
3933
4185
  const { createThreadWorktree: createThreadWorktree2 } = await import("./worktree-RYTBDHHP.js");
3934
4186
  const { execa: execa6 } = await import("execa");
3935
4187
  const slug = thread.worktreePath.split("/").pop();
@@ -4028,7 +4280,7 @@ async function startOrchestration(opts) {
4028
4280
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4029
4281
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4030
4282
  import { z } from "zod";
4031
- import { basename as basename3 } from "path";
4283
+ import { basename as basename4 } from "path";
4032
4284
 
4033
4285
  // src/mcp/archive-guard.ts
4034
4286
  function mcpArchiveBlockedReason(thread) {
@@ -4073,7 +4325,7 @@ async function startMcpServer() {
4073
4325
  async () => {
4074
4326
  const threads = orch.getThreads(true);
4075
4327
  const lines = threads.map((t) => {
4076
- const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : basename3(t.repoPath) || t.repoPath;
4328
+ const repo = t.repoPath === GLOBAL_WORKSPACE_ID ? "Orchestration" : basename4(t.repoPath) || t.repoPath;
4077
4329
  return `${t.id.slice(0, 8)} ${t.status.padEnd(9)} ${t.agent.padEnd(8)} ${repo} ${t.sourceType}:${t.sourceRef} ${t.title} sideboard://thread/${t.id}${t.devPort ? ` http://localhost:${t.devPort}` : ""}`;
4078
4330
  });
4079
4331
  return {
@@ -4647,6 +4899,12 @@ export {
4647
4899
  discoverSkills,
4648
4900
  readSkillBody,
4649
4901
  expandComposerPrompt,
4902
+ isImageFilePath,
4903
+ attachmentFromAbsolutePath,
4904
+ stageAbsolutePathsAsAttachments,
4905
+ stageBuffersAsAttachments,
4906
+ attachmentsFromBuffers,
4907
+ attachmentsFromWorktreePaths,
4650
4908
  summarizeConversation,
4651
4909
  extractiveSummary,
4652
4910
  CONTEXT_COMPACT_CHARS,