@tarcisiopgs/lisa 1.26.2 → 1.27.0

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.
@@ -16,14 +16,18 @@ import { dirname } from "path";
16
16
  import pc from "picocolors";
17
17
  var logFilePath = null;
18
18
  var outputMode = "default";
19
+ var logLevel = "default";
19
20
  function setOutputMode(mode) {
20
21
  outputMode = mode;
21
22
  }
22
23
  function getOutputMode() {
23
24
  return outputMode;
24
25
  }
26
+ function setLogLevel(level) {
27
+ logLevel = level;
28
+ }
25
29
  function shouldPrintToConsole() {
26
- return outputMode !== "tui";
30
+ return outputMode !== "tui" && logLevel !== "quiet";
27
31
  }
28
32
  function initLogFile(path) {
29
33
  const dir = dirname(path);
@@ -45,7 +49,7 @@ function writeToFile(level, message) {
45
49
  }
46
50
  function log(message) {
47
51
  if (shouldPrintToConsole()) {
48
- console.log(`${pc.cyan("[lisa]")} ${pc.dim(timestamp())} ${message}`);
52
+ console.error(`${pc.cyan("[lisa]")} ${pc.dim(timestamp())} ${message}`);
49
53
  }
50
54
  writeToFile("info", message);
51
55
  }
@@ -63,7 +67,7 @@ function error(message) {
63
67
  }
64
68
  function ok(message) {
65
69
  if (shouldPrintToConsole()) {
66
- console.log(`${pc.green("[lisa]")} ${pc.dim(timestamp())} ${message}`);
70
+ console.error(`${pc.green("[lisa]")} ${pc.dim(timestamp())} ${message}`);
67
71
  }
68
72
  writeToFile("ok", message);
69
73
  }
@@ -71,34 +75,83 @@ function divider(session) {
71
75
  log(`${"\u2501".repeat(3)} Session ${session} ${"\u2501".repeat(3)}`);
72
76
  }
73
77
  function banner() {
74
- if (outputMode !== "default") return;
78
+ if (outputMode !== "default" || logLevel === "quiet") return;
75
79
  const title = " lisa \u266A autonomous issue resolver ";
76
80
  const border = "\u2500".repeat(title.length);
77
- console.log(pc.yellow(`
81
+ console.error(pc.yellow(`
78
82
  \u250C${border}\u2510`));
79
- console.log(pc.yellow(` \u2502`) + pc.bold(pc.white(title)) + pc.yellow("\u2502"));
80
- console.log(pc.yellow(` \u2514${border}\u2518
83
+ console.error(pc.yellow(` \u2502`) + pc.bold(pc.white(title)) + pc.yellow("\u2502"));
84
+ console.error(pc.yellow(` \u2514${border}\u2518
81
85
  `));
82
86
  }
83
87
  function updateNotice(update) {
84
- if (outputMode !== "default") return;
88
+ if (outputMode !== "default" || logLevel === "quiet") return;
85
89
  const msg = `Update available ${pc.dim(update.currentVersion)} \u2192 ${pc.green(pc.bold(update.latestVersion))}`;
86
90
  const cmd = `Run ${pc.cyan("npm i -g @tarcisiopgs/lisa")} to update`;
87
91
  const lines = [msg, cmd];
88
92
  const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
89
93
  const maxLen = Math.max(...lines.map((l) => strip(l).length));
90
94
  const pad = (s) => s + " ".repeat(maxLen - strip(s).length);
91
- console.log(pc.yellow(` \u250C${"\u2500".repeat(maxLen + 2)}\u2510`));
95
+ console.error(pc.yellow(` \u250C${"\u2500".repeat(maxLen + 2)}\u2510`));
92
96
  for (const line of lines) {
93
- console.log(pc.yellow(" \u2502 ") + pad(line) + pc.yellow(" \u2502"));
97
+ console.error(pc.yellow(" \u2502 ") + pad(line) + pc.yellow(" \u2502"));
94
98
  }
95
- console.log(pc.yellow(` \u2514${"\u2500".repeat(maxLen + 2)}\u2518
99
+ console.error(pc.yellow(` \u2514${"\u2500".repeat(maxLen + 2)}\u2518
96
100
  `));
97
101
  }
98
102
 
99
- // src/sources/github-issues.ts
100
- var API_URL = "https://api.github.com";
103
+ // src/sources/base.ts
101
104
  var REQUEST_TIMEOUT_MS = 3e4;
105
+ function normalizeLabels(config) {
106
+ return Array.isArray(config.label) ? config.label : config.label ? [config.label] : [];
107
+ }
108
+ function createApiClient(baseUrl, getHeaders, name) {
109
+ async function request(method, path, body) {
110
+ const url = `${baseUrl}${path}`;
111
+ const headers = {
112
+ ...await getHeaders(),
113
+ "Content-Type": "application/json"
114
+ };
115
+ const res = await fetch(url, {
116
+ method,
117
+ headers,
118
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
119
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
120
+ });
121
+ if (!res.ok) {
122
+ const text = await res.text();
123
+ throw new Error(`${name} API error (${res.status}): ${text}`);
124
+ }
125
+ if (method === "DELETE" || res.status === 204) return void 0;
126
+ return await res.json();
127
+ }
128
+ return {
129
+ get: (path) => request("GET", path),
130
+ post: (path, body) => request("POST", path, body),
131
+ put: (path, body) => request("PUT", path, body),
132
+ patch: (path, body) => request("PATCH", path, body),
133
+ delete: (path) => request("DELETE", path),
134
+ /** Raw request for non-JSON bodies (e.g. Trello form-encoded). */
135
+ raw: async (method, path, init) => {
136
+ const url = `${baseUrl}${path}`;
137
+ const headers = await getHeaders();
138
+ const res = await fetch(url, {
139
+ method,
140
+ headers: { ...headers, ...init?.headers },
141
+ body: init?.body,
142
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
143
+ });
144
+ if (!res.ok) {
145
+ const text = await res.text();
146
+ throw new Error(`${name} API error (${res.status}): ${text}`);
147
+ }
148
+ if (method === "DELETE" || res.status === 204) return void 0;
149
+ return await res.json();
150
+ }
151
+ };
152
+ }
153
+
154
+ // src/sources/github-issues.ts
102
155
  var PRIORITY_LABELS = ["p1", "p2", "p3"];
103
156
  var DEPENDENCY_PATTERN = /(?:depends\s+on|blocked\s+by)\s+#(\d+)/gi;
104
157
  async function getToken() {
@@ -118,36 +171,12 @@ async function getAuthHeaders() {
118
171
  "X-GitHub-Api-Version": "2022-11-28"
119
172
  };
120
173
  }
121
- async function githubFetch(method, path, body) {
122
- const url = `${API_URL}${path}`;
123
- const headers = {
124
- ...await getAuthHeaders(),
125
- "Content-Type": "application/json"
126
- };
127
- const res = await fetch(url, {
128
- method,
129
- headers,
130
- body: body !== void 0 ? JSON.stringify(body) : void 0,
131
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
132
- });
133
- if (!res.ok) {
134
- const text = await res.text();
135
- throw new Error(`GitHub API error (${res.status}): ${text}`);
174
+ var _api;
175
+ function api() {
176
+ if (!_api) {
177
+ _api = createApiClient("https://api.github.com", getAuthHeaders, "GitHub");
136
178
  }
137
- if (method === "DELETE" || res.status === 204) return void 0;
138
- return await res.json();
139
- }
140
- async function githubGet(path) {
141
- return githubFetch("GET", path);
142
- }
143
- async function githubPost(path, body) {
144
- return githubFetch("POST", path, body);
145
- }
146
- async function githubPatch(path, body) {
147
- return githubFetch("PATCH", path, body);
148
- }
149
- async function githubDelete(path) {
150
- await githubFetch("DELETE", path);
179
+ return _api;
151
180
  }
152
181
  function parseGitHubPrUrl(url) {
153
182
  const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
@@ -160,7 +189,7 @@ async function checkPrMerged(prUrl) {
160
189
  const parsed = parseGitHubPrUrl(prUrl);
161
190
  if (!parsed) return false;
162
191
  try {
163
- const pr = await githubGet(
192
+ const pr = await api().get(
164
193
  `/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`
165
194
  );
166
195
  return pr.merged === true;
@@ -216,10 +245,10 @@ var GitHubIssuesSource = class {
216
245
  const { owner, repo } = parseOwnerRepo(config.scope);
217
246
  const validStates = ["open", "closed", "all"];
218
247
  const isOrphanDetection = !!config.pick_from && !validStates.includes(config.pick_from);
219
- const filterLabels = isOrphanDetection ? [config.pick_from] : Array.isArray(config.label) ? config.label : [config.label];
248
+ const filterLabels = isOrphanDetection ? [config.pick_from] : normalizeLabels(config);
220
249
  const label = filterLabels.map((l) => encodeURIComponent(l)).join(",");
221
250
  const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
222
- const issues = (await githubGet(path)).filter((i) => !i.pull_request);
251
+ const issues = (await api().get(path)).filter((i) => !i.pull_request);
223
252
  if (issues.length === 0) return null;
224
253
  const unblocked = [];
225
254
  const blocked = [];
@@ -234,7 +263,7 @@ var GitHubIssuesSource = class {
234
263
  const closedBlockers = [];
235
264
  for (const depNum of depNumbers) {
236
265
  try {
237
- const dep = await githubGet(`/repos/${owner}/${repo}/issues/${depNum}`);
266
+ const dep = await api().get(`/repos/${owner}/${repo}/issues/${depNum}`);
238
267
  if (!dep.state || dep.state === "open") {
239
268
  activeBlockers.push(depNum);
240
269
  } else {
@@ -284,14 +313,15 @@ var GitHubIssuesSource = class {
284
313
  async fetchIssueById(id) {
285
314
  const ref = parseGitHubIssueNumber(id);
286
315
  try {
287
- const issue = await githubGet(
316
+ const issue = await api().get(
288
317
  `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`
289
318
  );
290
319
  return {
291
320
  id: makeIssueId(ref.owner, ref.repo, issue.number),
292
321
  title: issue.title,
293
322
  description: issue.body ?? "",
294
- url: issue.html_url
323
+ url: issue.html_url,
324
+ status: issue.state
295
325
  };
296
326
  } catch {
297
327
  return null;
@@ -300,15 +330,15 @@ var GitHubIssuesSource = class {
300
330
  async updateStatus(issueId, labelToAdd, config) {
301
331
  const ref = parseGitHubIssueNumber(issueId);
302
332
  if (config && config.in_progress !== config.pick_from) {
303
- const filterLabels = Array.isArray(config.label) ? config.label : [config.label];
333
+ const filterLabels = normalizeLabels(config);
304
334
  const isMovingToInProgress = labelToAdd === config.in_progress;
305
335
  if (isMovingToInProgress) {
306
- await githubPost(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
336
+ await api().post(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
307
337
  labels: [labelToAdd]
308
338
  });
309
339
  for (const label of filterLabels) {
310
340
  try {
311
- await githubDelete(
341
+ await api().delete(
312
342
  `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(label)}`
313
343
  );
314
344
  } catch {
@@ -316,30 +346,30 @@ var GitHubIssuesSource = class {
316
346
  }
317
347
  return;
318
348
  }
319
- await githubPost(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
349
+ await api().post(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
320
350
  labels: filterLabels
321
351
  });
322
352
  try {
323
- await githubDelete(
353
+ await api().delete(
324
354
  `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(config.in_progress)}`
325
355
  );
326
356
  } catch {
327
357
  }
328
358
  return;
329
359
  }
330
- await githubPost(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
360
+ await api().post(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels`, {
331
361
  labels: [labelToAdd]
332
362
  });
333
363
  }
334
364
  async attachPullRequest(issueId, prUrl) {
335
365
  const ref = parseGitHubIssueNumber(issueId);
336
- await githubPost(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/comments`, {
366
+ await api().post(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/comments`, {
337
367
  body: `Pull request: ${prUrl}`
338
368
  });
339
369
  }
340
370
  async completeIssue(issueId, _status, labelToRemove, config) {
341
371
  const ref = parseGitHubIssueNumber(issueId);
342
- await githubPatch(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`, {
372
+ await api().patch(`/repos/${ref.owner}/${ref.repo}/issues/${ref.number}`, {
343
373
  state: "closed"
344
374
  });
345
375
  if (labelToRemove) {
@@ -347,7 +377,7 @@ var GitHubIssuesSource = class {
347
377
  }
348
378
  if (config && config.in_progress !== config.pick_from) {
349
379
  try {
350
- await githubDelete(
380
+ await api().delete(
351
381
  `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(config.in_progress)}`
352
382
  );
353
383
  } catch {
@@ -356,10 +386,10 @@ var GitHubIssuesSource = class {
356
386
  }
357
387
  async listIssues(config) {
358
388
  const { owner, repo } = parseOwnerRepo(config.scope);
359
- const labels = Array.isArray(config.label) ? config.label : [config.label];
389
+ const labels = normalizeLabels(config);
360
390
  const label = labels.map((l) => encodeURIComponent(l)).join(",");
361
391
  const path = `/repos/${owner}/${repo}/issues?labels=${label}&state=open&sort=created&direction=asc&per_page=100`;
362
- const issues = (await githubGet(path)).filter((i) => !i.pull_request);
392
+ const issues = (await api().get(path)).filter((i) => !i.pull_request);
363
393
  return issues.map((issue) => ({
364
394
  id: makeIssueId(owner, repo, issue.number),
365
395
  title: issue.title,
@@ -372,7 +402,7 @@ var GitHubIssuesSource = class {
372
402
  const results = [];
373
403
  let page = 1;
374
404
  while (true) {
375
- const labels = await githubGet(
405
+ const labels = await api().get(
376
406
  `/repos/${owner}/${repo}/labels?per_page=100&page=${page}`
377
407
  );
378
408
  for (const l of labels) {
@@ -389,7 +419,7 @@ var GitHubIssuesSource = class {
389
419
  async removeLabel(issueId, labelToRemove) {
390
420
  const ref = parseGitHubIssueNumber(issueId);
391
421
  try {
392
- await githubDelete(
422
+ await api().delete(
393
423
  `/repos/${ref.owner}/${ref.repo}/issues/${ref.number}/labels/${encodeURIComponent(labelToRemove)}`
394
424
  );
395
425
  } catch {
@@ -399,7 +429,6 @@ var GitHubIssuesSource = class {
399
429
 
400
430
  // src/sources/gitlab-issues.ts
401
431
  var DEFAULT_BASE_URL = "https://gitlab.com";
402
- var REQUEST_TIMEOUT_MS2 = 3e4;
403
432
  var PRIORITY_LABELS2 = ["p1", "p2", "p3"];
404
433
  function getBaseUrl() {
405
434
  return (process.env.GITLAB_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
@@ -409,33 +438,12 @@ function getAuthHeaders2() {
409
438
  if (!token) throw new Error("GITLAB_TOKEN must be set");
410
439
  return { "PRIVATE-TOKEN": token };
411
440
  }
412
- async function gitlabFetch(method, path, body) {
413
- const url = `${getBaseUrl()}/api/v4${path}`;
414
- const headers = {
415
- ...getAuthHeaders2(),
416
- "Content-Type": "application/json"
417
- };
418
- const res = await fetch(url, {
419
- method,
420
- headers,
421
- body: body !== void 0 ? JSON.stringify(body) : void 0,
422
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
423
- });
424
- if (!res.ok) {
425
- const text = await res.text();
426
- throw new Error(`GitLab API error (${res.status}): ${text}`);
441
+ var _api2;
442
+ function api2() {
443
+ if (!_api2) {
444
+ _api2 = createApiClient(`${getBaseUrl()}/api/v4`, getAuthHeaders2, "GitLab");
427
445
  }
428
- if (method === "DELETE" || res.status === 204) return void 0;
429
- return await res.json();
430
- }
431
- async function gitlabGet(path) {
432
- return gitlabFetch("GET", path);
433
- }
434
- async function gitlabPost(path, body) {
435
- return gitlabFetch("POST", path, body);
436
- }
437
- async function gitlabPut(path, body) {
438
- return gitlabFetch("PUT", path, body);
446
+ return _api2;
439
447
  }
440
448
  function parseGitLabMrUrl(url) {
441
449
  const match = url.match(/gitlab(?:\.com|[^/]*)\/(.+?)\/-\/merge_requests\/(\d+)/);
@@ -449,7 +457,7 @@ async function checkPrMerged2(prUrl) {
449
457
  if (!parsed) return false;
450
458
  try {
451
459
  const encodedProject = parseGitLabProject(parsed.project);
452
- const mr = await gitlabGet(
460
+ const mr = await api2().get(
453
461
  `/projects/${encodedProject}/merge_requests/${parsed.iid}`
454
462
  );
455
463
  return mr.state === "merged";
@@ -480,15 +488,15 @@ var GitLabIssuesSource = class {
480
488
  const project = parseGitLabProject(config.scope);
481
489
  const validStates = ["opened", "closed", "all"];
482
490
  const isOrphanDetection = !!config.pick_from && !validStates.includes(config.pick_from);
483
- const filterLabels = isOrphanDetection ? [config.pick_from] : Array.isArray(config.label) ? config.label : [config.label];
491
+ const filterLabels = isOrphanDetection ? [config.pick_from] : normalizeLabels(config);
484
492
  const label = filterLabels.map((l) => encodeURIComponent(l)).join(",");
485
493
  const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
486
- const issues = await gitlabGet(path);
494
+ const issues = await api2().get(path);
487
495
  if (issues.length === 0) return null;
488
496
  const unblocked = [];
489
497
  const blocked = [];
490
498
  for (const issue2 of issues) {
491
- const links = await gitlabGet(
499
+ const links = await api2().get(
492
500
  `/projects/${project}/issues/${issue2.iid}/links`
493
501
  );
494
502
  const activeBlockers = links.filter((link) => {
@@ -533,12 +541,13 @@ var GitLabIssuesSource = class {
533
541
  const ref = parseGitLabIssueRef(id);
534
542
  try {
535
543
  const project = parseGitLabProject(ref.project);
536
- const issue = await gitlabGet(`/projects/${project}/issues/${ref.iid}`);
544
+ const issue = await api2().get(`/projects/${project}/issues/${ref.iid}`);
537
545
  return {
538
546
  id: makeIssueId2(ref.project, issue.iid),
539
547
  title: issue.title,
540
548
  description: issue.description ?? "",
541
- url: issue.web_url
549
+ url: issue.web_url,
550
+ status: issue.state
542
551
  };
543
552
  } catch {
544
553
  return null;
@@ -547,15 +556,15 @@ var GitLabIssuesSource = class {
547
556
  async updateStatus(issueId, labelToAdd, config) {
548
557
  const { project, iid } = splitIssueId(issueId);
549
558
  const encodedProject = parseGitLabProject(project);
550
- const issue = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
559
+ const issue = await api2().get(`/projects/${encodedProject}/issues/${iid}`);
551
560
  if (config && config.in_progress !== config.pick_from) {
552
- const filterLabels = Array.isArray(config.label) ? config.label : [config.label];
561
+ const filterLabels = normalizeLabels(config);
553
562
  const isMovingToInProgress = labelToAdd === config.in_progress;
554
563
  if (isMovingToInProgress) {
555
564
  const updated2 = [.../* @__PURE__ */ new Set([...issue.labels, labelToAdd])].filter(
556
565
  (l) => !filterLabels.includes(l)
557
566
  );
558
- await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
567
+ await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
559
568
  labels: updated2.join(",")
560
569
  });
561
570
  return;
@@ -563,40 +572,40 @@ var GitLabIssuesSource = class {
563
572
  const updated = [.../* @__PURE__ */ new Set([...issue.labels, ...filterLabels])].filter(
564
573
  (l) => l !== config.in_progress
565
574
  );
566
- await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
575
+ await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
567
576
  labels: updated.join(",")
568
577
  });
569
578
  return;
570
579
  }
571
580
  const labels = [.../* @__PURE__ */ new Set([...issue.labels, labelToAdd])];
572
- await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, { labels: labels.join(",") });
581
+ await api2().put(`/projects/${encodedProject}/issues/${iid}`, { labels: labels.join(",") });
573
582
  }
574
583
  async attachPullRequest(issueId, prUrl) {
575
584
  const { project, iid } = splitIssueId(issueId);
576
585
  const encodedProject = parseGitLabProject(project);
577
- await gitlabPost(`/projects/${encodedProject}/issues/${iid}/notes`, {
586
+ await api2().post(`/projects/${encodedProject}/issues/${iid}/notes`, {
578
587
  body: `Pull request: ${prUrl}`
579
588
  });
580
589
  }
581
590
  async completeIssue(issueId, _status, labelToRemove, config) {
582
591
  const { project, iid } = splitIssueId(issueId);
583
592
  const encodedProject = parseGitLabProject(project);
584
- const issue = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
593
+ const issue = await api2().get(`/projects/${encodedProject}/issues/${iid}`);
585
594
  let labels = labelToRemove ? issue.labels.filter((l) => l.toLowerCase() !== labelToRemove.toLowerCase()) : issue.labels;
586
595
  if (config && config.in_progress !== config.pick_from) {
587
596
  labels = labels.filter((l) => l !== config.in_progress);
588
597
  }
589
- await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
598
+ await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
590
599
  state_event: "close",
591
600
  labels: labels.join(",")
592
601
  });
593
602
  }
594
603
  async listIssues(config) {
595
604
  const project = parseGitLabProject(config.scope);
596
- const labelsArr = Array.isArray(config.label) ? config.label : [config.label];
605
+ const labelsArr = normalizeLabels(config);
597
606
  const label = labelsArr.map((l) => encodeURIComponent(l)).join(",");
598
607
  const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
599
- const issues = await gitlabGet(path);
608
+ const issues = await api2().get(path);
600
609
  return issues.map((issue) => ({
601
610
  id: makeIssueId2(config.scope, issue.iid),
602
611
  title: issue.title,
@@ -609,7 +618,7 @@ var GitLabIssuesSource = class {
609
618
  const results = [];
610
619
  let page = 1;
611
620
  while (true) {
612
- const labels = await gitlabGet(
621
+ const labels = await api2().get(
613
622
  `/projects/${project}/labels?per_page=100&page=${page}`
614
623
  );
615
624
  for (const l of labels) {
@@ -626,10 +635,10 @@ var GitLabIssuesSource = class {
626
635
  async removeLabel(issueId, labelToRemove) {
627
636
  const { project, iid } = splitIssueId(issueId);
628
637
  const encodedProject = parseGitLabProject(project);
629
- const issue = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
638
+ const issue = await api2().get(`/projects/${encodedProject}/issues/${iid}`);
630
639
  const filtered = issue.labels.filter((l) => l.toLowerCase() !== labelToRemove.toLowerCase());
631
640
  if (filtered.length === issue.labels.length) return;
632
- await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
641
+ await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
633
642
  labels: filtered.join(",")
634
643
  });
635
644
  }
@@ -819,11 +828,18 @@ function useKanbanState(bellEnabled, initialCards = []) {
819
828
  setCards((prev) => prev.map((c) => c.id === issueId ? { ...c, logFile } : c));
820
829
  };
821
830
  const MAX_OUTPUT_SIZE = 2e5;
822
- const onOutput = (issueId, text) => {
831
+ const outputBuffer = /* @__PURE__ */ new Map();
832
+ let flushTimer = null;
833
+ const flushOutputBuffer = () => {
834
+ flushTimer = null;
835
+ if (outputBuffer.size === 0) return;
836
+ const buffered = new Map(outputBuffer);
837
+ outputBuffer.clear();
823
838
  setCards(
824
839
  (prev) => prev.map((c) => {
825
- if (c.id !== issueId) return c;
826
- let newLog = c.outputLog + text;
840
+ const chunk = buffered.get(c.id);
841
+ if (chunk === void 0) return c;
842
+ let newLog = c.outputLog + chunk;
827
843
  if (newLog.length > MAX_OUTPUT_SIZE) {
828
844
  const trimAt = newLog.indexOf("\n", newLog.length - MAX_OUTPUT_SIZE);
829
845
  newLog = trimAt !== -1 ? newLog.slice(trimAt + 1) : newLog.slice(-MAX_OUTPUT_SIZE);
@@ -832,7 +848,15 @@ function useKanbanState(bellEnabled, initialCards = []) {
832
848
  })
833
849
  );
834
850
  };
851
+ const onOutput = (issueId, text) => {
852
+ const existing = outputBuffer.get(issueId);
853
+ outputBuffer.set(issueId, existing !== void 0 ? existing + text : text);
854
+ if (flushTimer === null) {
855
+ flushTimer = setTimeout(flushOutputBuffer, 100);
856
+ }
857
+ };
835
858
  const onReconcileRemove = (issueId) => {
859
+ outputBuffer.delete(issueId);
836
860
  setCards((prev) => prev.filter((c) => c.id !== issueId));
837
861
  };
838
862
  kanbanEmitter.on("issue:queued", onQueued);
@@ -866,6 +890,11 @@ function useKanbanState(bellEnabled, initialCards = []) {
866
890
  kanbanEmitter.on("work:watch-prompt-resolved", onWatchPromptResolved);
867
891
  const cleanupBell = registerBellListeners(bellEnabled);
868
892
  return () => {
893
+ if (flushTimer !== null) {
894
+ clearTimeout(flushTimer);
895
+ flushTimer = null;
896
+ }
897
+ outputBuffer.clear();
869
898
  kanbanEmitter.off("issue:queued", onQueued);
870
899
  kanbanEmitter.off("issue:started", onStarted);
871
900
  kanbanEmitter.off("issue:done", onDone);
@@ -906,6 +935,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
906
935
  export {
907
936
  setOutputMode,
908
937
  getOutputMode,
938
+ setLogLevel,
909
939
  initLogFile,
910
940
  log,
911
941
  warn,
@@ -914,6 +944,9 @@ export {
914
944
  divider,
915
945
  banner,
916
946
  updateNotice,
947
+ REQUEST_TIMEOUT_MS,
948
+ normalizeLabels,
949
+ createApiClient,
917
950
  GitHubIssuesSource,
918
951
  GitLabIssuesSource,
919
952
  kanbanEmitter,
@@ -12,7 +12,7 @@ import {
12
12
  getVersion,
13
13
  isCursorFreePlan,
14
14
  verifyPlatformCredential
15
- } from "./chunk-AGYOJQBR.js";
15
+ } from "./chunk-JRCAT5PI.js";
16
16
  export {
17
17
  detectDefaultBranch,
18
18
  detectGitRepos,
@@ -10,7 +10,7 @@ import {
10
10
  guardrailsPath,
11
11
  migrateGuardrails,
12
12
  readGuardrails
13
- } from "./chunk-CDI22S63.js";
13
+ } from "./chunk-3EOEDL3T.js";
14
14
  import "./chunk-7OCDGYDM.js";
15
15
  export {
16
16
  appendEntry,