agent-board-runner 0.1.17 → 0.1.18

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 (3) hide show
  1. package/dist/cli.js +135 -22
  2. package/dist/index.js +135 -22
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2103,7 +2103,22 @@ function parseGitDiffNameStatus(output) {
2103
2103
  const columns = line.split(" ");
2104
2104
  const status = columns[0].slice(0, 1);
2105
2105
  const path = columns[columns.length - 1];
2106
- return { path, status };
2106
+ return { path, status, additions: null, deletions: null, binary: false };
2107
+ });
2108
+ }
2109
+ function parseGitDiffNumstat(output) {
2110
+ return output.split("\n").filter((line) => line.length > 0).map((line) => {
2111
+ const [added, deleted, path] = line.split(" ");
2112
+ const binary = added === "-" && deleted === "-";
2113
+ return { path, additions: binary ? null : Number(added), deletions: binary ? null : Number(deleted), binary };
2114
+ });
2115
+ }
2116
+ function mergeNumstat(files, numstat) {
2117
+ const byPath = new Map(numstat.map((entry) => [entry.path, entry]));
2118
+ return files.map((file) => {
2119
+ const stat = byPath.get(file.path);
2120
+ if (!stat) return file;
2121
+ return { ...file, additions: stat.additions, deletions: stat.deletions, binary: stat.binary };
2107
2122
  });
2108
2123
  }
2109
2124
  async function getRepoDiffForTarget(label, cwd) {
@@ -2137,14 +2152,84 @@ async function getRepoDiffForTarget(label, cwd) {
2137
2152
  files = parseGitDiffNameStatus(stdout);
2138
2153
  } catch (err) {
2139
2154
  const message = err instanceof Error ? err.message : String(err);
2140
- return { label, files: [], diff: "", error: message };
2155
+ return { label, files: [], diff: "", branch, error: message };
2156
+ }
2157
+ if (files.length === 0) return { label, files, diff: "", branch };
2158
+ try {
2159
+ const { stdout: numstatOut } = await execFileAsync2("git", ["diff", "--numstat", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
2160
+ files = mergeNumstat(files, parseGitDiffNumstat(numstatOut));
2161
+ } catch {
2141
2162
  }
2142
- if (files.length === 0) return { label, files, diff: "" };
2143
2163
  try {
2144
2164
  const { stdout } = await execFileAsync2("git", ["diff", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
2145
- return { label, files, diff: stdout };
2165
+ return { label, files, diff: stdout, branch };
2166
+ } catch {
2167
+ return { label, files, diff: "", branch };
2168
+ }
2169
+ }
2170
+ async function resolveDiffTargetCwd(entry, projectId, label) {
2171
+ if (!label) return entry.repoRoot;
2172
+ if (!statuslineSupabase) throw new HttpError(400, "Reposit\xF3rio filho n\xE3o dispon\xEDvel.");
2173
+ const children = await getProjectRepos(statuslineSupabase, projectId);
2174
+ const child2 = children.find((candidate) => candidate.subpath === label);
2175
+ if (!child2) throw new HttpError(400, "Reposit\xF3rio filho desconhecido.");
2176
+ return join9(entry.repoRoot, child2.subpath);
2177
+ }
2178
+ async function readFileAtRevision(cwd, ref, relPath) {
2179
+ try {
2180
+ const { stdout } = await execFileAsync2("git", ["show", `${ref}:${relPath}`], {
2181
+ cwd,
2182
+ maxBuffer: DIFF_EXEC_OPTIONS.maxBuffer,
2183
+ encoding: "buffer"
2184
+ });
2185
+ return stdout;
2146
2186
  } catch {
2147
- return { label, files, diff: "" };
2187
+ return null;
2188
+ }
2189
+ }
2190
+ function toFileRevisionJson(buffer, imageContentType) {
2191
+ if (buffer === null) return { exists: false, content: null, binary: false };
2192
+ if (looksBinary(buffer)) {
2193
+ return {
2194
+ exists: true,
2195
+ content: null,
2196
+ binary: true,
2197
+ ...imageContentType ? { imageBase64: buffer.toString("base64"), imageContentType } : {}
2198
+ };
2199
+ }
2200
+ return { exists: true, content: buffer.toString("utf8"), binary: false };
2201
+ }
2202
+ async function handleDiffFileRequest(entry, res, projectId, relPath, label) {
2203
+ if (!relPath) {
2204
+ sendJson(res, 400, { error: "Par\xE2metro path \xE9 obrigat\xF3rio." });
2205
+ return;
2206
+ }
2207
+ try {
2208
+ const cwd = await resolveDiffTargetCwd(entry, projectId, label);
2209
+ resolveWithinRepo(cwd, relPath);
2210
+ const { stdout: branchOut } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
2211
+ const branch = branchOut.trim();
2212
+ if (!branch) {
2213
+ sendJson(res, 400, { error: "HEAD destacado: sem branch local para comparar com a origin." });
2214
+ return;
2215
+ }
2216
+ const [originalBuffer, modifiedBuffer] = await Promise.all([
2217
+ readFileAtRevision(cwd, `origin/${branch}`, relPath),
2218
+ readFileAtRevision(cwd, "HEAD", relPath)
2219
+ ]);
2220
+ const ext = relPath.split(".").pop()?.toLowerCase() ?? "";
2221
+ const imageContentType = MIME_TYPES_BY_EXT[ext];
2222
+ sendJson(res, 200, {
2223
+ original: toFileRevisionJson(originalBuffer, imageContentType),
2224
+ modified: toFileRevisionJson(modifiedBuffer, imageContentType)
2225
+ });
2226
+ } catch (err) {
2227
+ if (err instanceof HttpError) {
2228
+ sendJson(res, err.status, { error: err.message });
2229
+ return;
2230
+ }
2231
+ logger.runner.error("FILES", "Unexpected error handling a diff-file request:", err);
2232
+ sendJson(res, 500, { error: "Erro interno do runner." });
2148
2233
  }
2149
2234
  }
2150
2235
  async function handleDiffRequest(entry, res, projectId) {
@@ -2329,6 +2414,12 @@ function handleRequest(req, res) {
2329
2414
  void handleDiffRequest(entry, res, projectId);
2330
2415
  return;
2331
2416
  }
2417
+ if (pathname === "/diff-file") {
2418
+ const projectId = typeof query.projectId === "string" ? query.projectId : "";
2419
+ const label = typeof query.label === "string" ? query.label : null;
2420
+ void handleDiffFileRequest(entry, res, projectId, relPath, label);
2421
+ return;
2422
+ }
2332
2423
  try {
2333
2424
  if (pathname === "/tree") {
2334
2425
  sendJson(res, 200, { entries: listDirectory(entry.repoRoot, relPath) });
@@ -2577,13 +2668,49 @@ function toSessionName(title) {
2577
2668
  // src/agentSession.ts
2578
2669
  var TUI_IDLE_MS = 1500;
2579
2670
  var TUI_READY_TIMEOUT_MS = 6e4;
2580
- var SUBMIT_DELAY_MS = 150;
2671
+ var PASTE_SETTLE_MS = 200;
2672
+ var PASTE_SETTLE_TIMEOUT_MS = 5e3;
2581
2673
  var PASTE_START = "\x1B[200~";
2582
2674
  var PASTE_END = "\x1B[201~";
2583
2675
  var SHIFT_TAB = "\x1B[Z";
2584
2676
  var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2585
2677
  var SHIFT_TAB_DELAY_MS = 400;
2586
2678
  var PLAN_MODE_INDICATOR = /plan mode on/i;
2679
+ function writePasteAndSubmit(pty, cardId, message, context) {
2680
+ pty.write(`${PASTE_START}${message}${PASTE_END}`);
2681
+ let settleTimer;
2682
+ let ceilingTimer;
2683
+ let dataSubscription;
2684
+ let exitSubscription;
2685
+ let submitted = false;
2686
+ const submit = () => {
2687
+ if (submitted) return;
2688
+ submitted = true;
2689
+ clearTimeout(settleTimer);
2690
+ clearTimeout(ceilingTimer);
2691
+ dataSubscription?.dispose();
2692
+ exitSubscription?.dispose();
2693
+ try {
2694
+ pty.write("\r");
2695
+ } catch (err) {
2696
+ logger.card.error(cardId, `Failed to submit ${context} to the agent:`, err);
2697
+ }
2698
+ };
2699
+ const cancel = () => {
2700
+ submitted = true;
2701
+ clearTimeout(settleTimer);
2702
+ clearTimeout(ceilingTimer);
2703
+ dataSubscription?.dispose();
2704
+ exitSubscription?.dispose();
2705
+ };
2706
+ dataSubscription = pty.onData(() => {
2707
+ clearTimeout(settleTimer);
2708
+ settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
2709
+ });
2710
+ settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
2711
+ ceilingTimer = setTimeout(submit, PASTE_SETTLE_TIMEOUT_MS);
2712
+ exitSubscription = pty.onExit(cancel);
2713
+ }
2587
2714
  function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
2588
2715
  ensureDirTrusted(repoPath);
2589
2716
  ensureMcpServersTrusted(repoPath);
@@ -2627,14 +2754,7 @@ function sendMessageWhenIdle(pty, cardId, message, planFirst) {
2627
2754
  if (!stopWaiting()) return;
2628
2755
  if (planFirst) await enterPlanMode(pty, cardId);
2629
2756
  try {
2630
- pty.write(`${PASTE_START}${message}${PASTE_END}`);
2631
- setTimeout(() => {
2632
- try {
2633
- pty.write("\r");
2634
- } catch (err) {
2635
- logger.card.error(cardId, "Failed to submit the card instructions to the agent:", err);
2636
- }
2637
- }, SUBMIT_DELAY_MS);
2757
+ writePasteAndSubmit(pty, cardId, message, "the card instructions");
2638
2758
  } catch (err) {
2639
2759
  logger.card.error(cardId, "Failed to send the card instructions to the agent:", err);
2640
2760
  }
@@ -2670,14 +2790,7 @@ async function enterPlanMode(pty, cardId) {
2670
2790
  }
2671
2791
  function sendMessageToIdleSession(pty, cardId, message) {
2672
2792
  try {
2673
- pty.write(`${PASTE_START}${message}${PASTE_END}`);
2674
- setTimeout(() => {
2675
- try {
2676
- pty.write("\r");
2677
- } catch (err) {
2678
- logger.card.error(cardId, "Failed to submit a message to the idle session:", err);
2679
- }
2680
- }, SUBMIT_DELAY_MS);
2793
+ writePasteAndSubmit(pty, cardId, message, "a message");
2681
2794
  } catch (err) {
2682
2795
  logger.card.error(cardId, "Failed to send a message to the idle session:", err);
2683
2796
  }
package/dist/index.js CHANGED
@@ -1823,7 +1823,22 @@ function parseGitDiffNameStatus(output) {
1823
1823
  const columns = line.split(" ");
1824
1824
  const status = columns[0].slice(0, 1);
1825
1825
  const path = columns[columns.length - 1];
1826
- return { path, status };
1826
+ return { path, status, additions: null, deletions: null, binary: false };
1827
+ });
1828
+ }
1829
+ function parseGitDiffNumstat(output) {
1830
+ return output.split("\n").filter((line) => line.length > 0).map((line) => {
1831
+ const [added, deleted, path] = line.split(" ");
1832
+ const binary = added === "-" && deleted === "-";
1833
+ return { path, additions: binary ? null : Number(added), deletions: binary ? null : Number(deleted), binary };
1834
+ });
1835
+ }
1836
+ function mergeNumstat(files, numstat) {
1837
+ const byPath = new Map(numstat.map((entry) => [entry.path, entry]));
1838
+ return files.map((file) => {
1839
+ const stat = byPath.get(file.path);
1840
+ if (!stat) return file;
1841
+ return { ...file, additions: stat.additions, deletions: stat.deletions, binary: stat.binary };
1827
1842
  });
1828
1843
  }
1829
1844
  async function getRepoDiffForTarget(label, cwd) {
@@ -1857,14 +1872,84 @@ async function getRepoDiffForTarget(label, cwd) {
1857
1872
  files = parseGitDiffNameStatus(stdout);
1858
1873
  } catch (err) {
1859
1874
  const message = err instanceof Error ? err.message : String(err);
1860
- return { label, files: [], diff: "", error: message };
1875
+ return { label, files: [], diff: "", branch, error: message };
1876
+ }
1877
+ if (files.length === 0) return { label, files, diff: "", branch };
1878
+ try {
1879
+ const { stdout: numstatOut } = await execFileAsync2("git", ["diff", "--numstat", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
1880
+ files = mergeNumstat(files, parseGitDiffNumstat(numstatOut));
1881
+ } catch {
1861
1882
  }
1862
- if (files.length === 0) return { label, files, diff: "" };
1863
1883
  try {
1864
1884
  const { stdout } = await execFileAsync2("git", ["diff", remoteRef, "HEAD"], { ...DIFF_EXEC_OPTIONS, cwd });
1865
- return { label, files, diff: stdout };
1885
+ return { label, files, diff: stdout, branch };
1886
+ } catch {
1887
+ return { label, files, diff: "", branch };
1888
+ }
1889
+ }
1890
+ async function resolveDiffTargetCwd(entry, projectId, label) {
1891
+ if (!label) return entry.repoRoot;
1892
+ if (!statuslineSupabase) throw new HttpError(400, "Reposit\xF3rio filho n\xE3o dispon\xEDvel.");
1893
+ const children = await getProjectRepos(statuslineSupabase, projectId);
1894
+ const child2 = children.find((candidate) => candidate.subpath === label);
1895
+ if (!child2) throw new HttpError(400, "Reposit\xF3rio filho desconhecido.");
1896
+ return join7(entry.repoRoot, child2.subpath);
1897
+ }
1898
+ async function readFileAtRevision(cwd, ref, relPath) {
1899
+ try {
1900
+ const { stdout } = await execFileAsync2("git", ["show", `${ref}:${relPath}`], {
1901
+ cwd,
1902
+ maxBuffer: DIFF_EXEC_OPTIONS.maxBuffer,
1903
+ encoding: "buffer"
1904
+ });
1905
+ return stdout;
1866
1906
  } catch {
1867
- return { label, files, diff: "" };
1907
+ return null;
1908
+ }
1909
+ }
1910
+ function toFileRevisionJson(buffer, imageContentType) {
1911
+ if (buffer === null) return { exists: false, content: null, binary: false };
1912
+ if (looksBinary(buffer)) {
1913
+ return {
1914
+ exists: true,
1915
+ content: null,
1916
+ binary: true,
1917
+ ...imageContentType ? { imageBase64: buffer.toString("base64"), imageContentType } : {}
1918
+ };
1919
+ }
1920
+ return { exists: true, content: buffer.toString("utf8"), binary: false };
1921
+ }
1922
+ async function handleDiffFileRequest(entry, res, projectId, relPath, label) {
1923
+ if (!relPath) {
1924
+ sendJson(res, 400, { error: "Par\xE2metro path \xE9 obrigat\xF3rio." });
1925
+ return;
1926
+ }
1927
+ try {
1928
+ const cwd = await resolveDiffTargetCwd(entry, projectId, label);
1929
+ resolveWithinRepo(cwd, relPath);
1930
+ const { stdout: branchOut } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
1931
+ const branch = branchOut.trim();
1932
+ if (!branch) {
1933
+ sendJson(res, 400, { error: "HEAD destacado: sem branch local para comparar com a origin." });
1934
+ return;
1935
+ }
1936
+ const [originalBuffer, modifiedBuffer] = await Promise.all([
1937
+ readFileAtRevision(cwd, `origin/${branch}`, relPath),
1938
+ readFileAtRevision(cwd, "HEAD", relPath)
1939
+ ]);
1940
+ const ext = relPath.split(".").pop()?.toLowerCase() ?? "";
1941
+ const imageContentType = MIME_TYPES_BY_EXT[ext];
1942
+ sendJson(res, 200, {
1943
+ original: toFileRevisionJson(originalBuffer, imageContentType),
1944
+ modified: toFileRevisionJson(modifiedBuffer, imageContentType)
1945
+ });
1946
+ } catch (err) {
1947
+ if (err instanceof HttpError) {
1948
+ sendJson(res, err.status, { error: err.message });
1949
+ return;
1950
+ }
1951
+ logger.runner.error("FILES", "Unexpected error handling a diff-file request:", err);
1952
+ sendJson(res, 500, { error: "Erro interno do runner." });
1868
1953
  }
1869
1954
  }
1870
1955
  async function handleDiffRequest(entry, res, projectId) {
@@ -2049,6 +2134,12 @@ function handleRequest(req, res) {
2049
2134
  void handleDiffRequest(entry, res, projectId);
2050
2135
  return;
2051
2136
  }
2137
+ if (pathname === "/diff-file") {
2138
+ const projectId = typeof query.projectId === "string" ? query.projectId : "";
2139
+ const label = typeof query.label === "string" ? query.label : null;
2140
+ void handleDiffFileRequest(entry, res, projectId, relPath, label);
2141
+ return;
2142
+ }
2052
2143
  try {
2053
2144
  if (pathname === "/tree") {
2054
2145
  sendJson(res, 200, { entries: listDirectory(entry.repoRoot, relPath) });
@@ -2465,13 +2556,49 @@ function toSessionName(title) {
2465
2556
  // src/agentSession.ts
2466
2557
  var TUI_IDLE_MS = 1500;
2467
2558
  var TUI_READY_TIMEOUT_MS = 6e4;
2468
- var SUBMIT_DELAY_MS = 150;
2559
+ var PASTE_SETTLE_MS = 200;
2560
+ var PASTE_SETTLE_TIMEOUT_MS = 5e3;
2469
2561
  var PASTE_START = "\x1B[200~";
2470
2562
  var PASTE_END = "\x1B[201~";
2471
2563
  var SHIFT_TAB = "\x1B[Z";
2472
2564
  var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
2473
2565
  var SHIFT_TAB_DELAY_MS = 400;
2474
2566
  var PLAN_MODE_INDICATOR = /plan mode on/i;
2567
+ function writePasteAndSubmit(pty, cardId, message, context) {
2568
+ pty.write(`${PASTE_START}${message}${PASTE_END}`);
2569
+ let settleTimer;
2570
+ let ceilingTimer;
2571
+ let dataSubscription;
2572
+ let exitSubscription;
2573
+ let submitted = false;
2574
+ const submit = () => {
2575
+ if (submitted) return;
2576
+ submitted = true;
2577
+ clearTimeout(settleTimer);
2578
+ clearTimeout(ceilingTimer);
2579
+ dataSubscription?.dispose();
2580
+ exitSubscription?.dispose();
2581
+ try {
2582
+ pty.write("\r");
2583
+ } catch (err) {
2584
+ logger.card.error(cardId, `Failed to submit ${context} to the agent:`, err);
2585
+ }
2586
+ };
2587
+ const cancel = () => {
2588
+ submitted = true;
2589
+ clearTimeout(settleTimer);
2590
+ clearTimeout(ceilingTimer);
2591
+ dataSubscription?.dispose();
2592
+ exitSubscription?.dispose();
2593
+ };
2594
+ dataSubscription = pty.onData(() => {
2595
+ clearTimeout(settleTimer);
2596
+ settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
2597
+ });
2598
+ settleTimer = setTimeout(submit, PASTE_SETTLE_MS);
2599
+ ceilingTimer = setTimeout(submit, PASTE_SETTLE_TIMEOUT_MS);
2600
+ exitSubscription = pty.onExit(cancel);
2601
+ }
2475
2602
  function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
2476
2603
  ensureDirTrusted(repoPath);
2477
2604
  ensureMcpServersTrusted(repoPath);
@@ -2515,14 +2642,7 @@ function sendMessageWhenIdle(pty, cardId, message, planFirst) {
2515
2642
  if (!stopWaiting()) return;
2516
2643
  if (planFirst) await enterPlanMode(pty, cardId);
2517
2644
  try {
2518
- pty.write(`${PASTE_START}${message}${PASTE_END}`);
2519
- setTimeout(() => {
2520
- try {
2521
- pty.write("\r");
2522
- } catch (err) {
2523
- logger.card.error(cardId, "Failed to submit the card instructions to the agent:", err);
2524
- }
2525
- }, SUBMIT_DELAY_MS);
2645
+ writePasteAndSubmit(pty, cardId, message, "the card instructions");
2526
2646
  } catch (err) {
2527
2647
  logger.card.error(cardId, "Failed to send the card instructions to the agent:", err);
2528
2648
  }
@@ -2558,14 +2678,7 @@ async function enterPlanMode(pty, cardId) {
2558
2678
  }
2559
2679
  function sendMessageToIdleSession(pty, cardId, message) {
2560
2680
  try {
2561
- pty.write(`${PASTE_START}${message}${PASTE_END}`);
2562
- setTimeout(() => {
2563
- try {
2564
- pty.write("\r");
2565
- } catch (err) {
2566
- logger.card.error(cardId, "Failed to submit a message to the idle session:", err);
2567
- }
2568
- }, SUBMIT_DELAY_MS);
2681
+ writePasteAndSubmit(pty, cardId, message, "a message");
2569
2682
  } catch (err) {
2570
2683
  logger.card.error(cardId, "Failed to send a message to the idle session:", err);
2571
2684
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-board-runner",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Local runner for Agent Board — signs in with your own Supabase account (no shared secrets) and drives Claude Code against repos under your local repos root.",
5
5
  "type": "module",
6
6
  "bin": {