@tarcisiopgs/lisa 1.26.2 → 1.28.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.
@@ -3,27 +3,24 @@ import {
3
3
  notify
4
4
  } from "./chunk-72CYGBT4.js";
5
5
 
6
- // src/ui/state.ts
7
- import { EventEmitter } from "events";
8
- import { useEffect, useState } from "react";
9
-
10
- // src/sources/github-issues.ts
11
- import { execa } from "execa";
12
-
13
6
  // src/output/logger.ts
14
7
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
15
8
  import { dirname } from "path";
16
9
  import pc from "picocolors";
17
10
  var logFilePath = null;
18
11
  var outputMode = "default";
12
+ var logLevel = "default";
19
13
  function setOutputMode(mode) {
20
14
  outputMode = mode;
21
15
  }
22
16
  function getOutputMode() {
23
17
  return outputMode;
24
18
  }
19
+ function setLogLevel(level) {
20
+ logLevel = level;
21
+ }
25
22
  function shouldPrintToConsole() {
26
- return outputMode !== "tui";
23
+ return outputMode !== "tui" && logLevel !== "quiet";
27
24
  }
28
25
  function initLogFile(path) {
29
26
  const dir = dirname(path);
@@ -45,7 +42,7 @@ function writeToFile(level, message) {
45
42
  }
46
43
  function log(message) {
47
44
  if (shouldPrintToConsole()) {
48
- console.log(`${pc.cyan("[lisa]")} ${pc.dim(timestamp())} ${message}`);
45
+ console.error(`${pc.cyan("[lisa]")} ${pc.dim(timestamp())} ${message}`);
49
46
  }
50
47
  writeToFile("info", message);
51
48
  }
@@ -63,7 +60,7 @@ function error(message) {
63
60
  }
64
61
  function ok(message) {
65
62
  if (shouldPrintToConsole()) {
66
- console.log(`${pc.green("[lisa]")} ${pc.dim(timestamp())} ${message}`);
63
+ console.error(`${pc.green("[lisa]")} ${pc.dim(timestamp())} ${message}`);
67
64
  }
68
65
  writeToFile("ok", message);
69
66
  }
@@ -71,34 +68,90 @@ function divider(session) {
71
68
  log(`${"\u2501".repeat(3)} Session ${session} ${"\u2501".repeat(3)}`);
72
69
  }
73
70
  function banner() {
74
- if (outputMode !== "default") return;
71
+ if (outputMode !== "default" || logLevel === "quiet") return;
75
72
  const title = " lisa \u266A autonomous issue resolver ";
76
73
  const border = "\u2500".repeat(title.length);
77
- console.log(pc.yellow(`
74
+ console.error(pc.yellow(`
78
75
  \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
76
+ console.error(pc.yellow(` \u2502`) + pc.bold(pc.white(title)) + pc.yellow("\u2502"));
77
+ console.error(pc.yellow(` \u2514${border}\u2518
81
78
  `));
82
79
  }
83
80
  function updateNotice(update) {
84
- if (outputMode !== "default") return;
81
+ if (outputMode !== "default" || logLevel === "quiet") return;
85
82
  const msg = `Update available ${pc.dim(update.currentVersion)} \u2192 ${pc.green(pc.bold(update.latestVersion))}`;
86
83
  const cmd = `Run ${pc.cyan("npm i -g @tarcisiopgs/lisa")} to update`;
87
84
  const lines = [msg, cmd];
88
85
  const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
89
86
  const maxLen = Math.max(...lines.map((l) => strip(l).length));
90
87
  const pad = (s) => s + " ".repeat(maxLen - strip(s).length);
91
- console.log(pc.yellow(` \u250C${"\u2500".repeat(maxLen + 2)}\u2510`));
88
+ console.error(pc.yellow(` \u250C${"\u2500".repeat(maxLen + 2)}\u2510`));
92
89
  for (const line of lines) {
93
- console.log(pc.yellow(" \u2502 ") + pad(line) + pc.yellow(" \u2502"));
90
+ console.error(pc.yellow(" \u2502 ") + pad(line) + pc.yellow(" \u2502"));
94
91
  }
95
- console.log(pc.yellow(` \u2514${"\u2500".repeat(maxLen + 2)}\u2518
92
+ console.error(pc.yellow(` \u2514${"\u2500".repeat(maxLen + 2)}\u2518
96
93
  `));
97
94
  }
98
95
 
96
+ // src/ui/state.ts
97
+ import { EventEmitter } from "events";
98
+ import { useEffect, useState } from "react";
99
+
99
100
  // src/sources/github-issues.ts
100
- var API_URL = "https://api.github.com";
101
+ import { execa } from "execa";
102
+
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,17 +419,26 @@ 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 {
396
426
  }
397
427
  }
428
+ async createIssue(opts, config) {
429
+ const { owner, repo } = parseOwnerRepo(config.scope);
430
+ const labels = Array.isArray(opts.label) ? opts.label : [opts.label];
431
+ const issue = await api().post(`/repos/${owner}/${repo}/issues`, {
432
+ title: opts.title,
433
+ body: opts.description,
434
+ labels
435
+ });
436
+ return makeIssueId(owner, repo, issue.number);
437
+ }
398
438
  };
399
439
 
400
440
  // src/sources/gitlab-issues.ts
401
441
  var DEFAULT_BASE_URL = "https://gitlab.com";
402
- var REQUEST_TIMEOUT_MS2 = 3e4;
403
442
  var PRIORITY_LABELS2 = ["p1", "p2", "p3"];
404
443
  function getBaseUrl() {
405
444
  return (process.env.GITLAB_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
@@ -409,33 +448,12 @@ function getAuthHeaders2() {
409
448
  if (!token) throw new Error("GITLAB_TOKEN must be set");
410
449
  return { "PRIVATE-TOKEN": token };
411
450
  }
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}`);
451
+ var _api2;
452
+ function api2() {
453
+ if (!_api2) {
454
+ _api2 = createApiClient(`${getBaseUrl()}/api/v4`, getAuthHeaders2, "GitLab");
427
455
  }
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);
456
+ return _api2;
439
457
  }
440
458
  function parseGitLabMrUrl(url) {
441
459
  const match = url.match(/gitlab(?:\.com|[^/]*)\/(.+?)\/-\/merge_requests\/(\d+)/);
@@ -449,7 +467,7 @@ async function checkPrMerged2(prUrl) {
449
467
  if (!parsed) return false;
450
468
  try {
451
469
  const encodedProject = parseGitLabProject(parsed.project);
452
- const mr = await gitlabGet(
470
+ const mr = await api2().get(
453
471
  `/projects/${encodedProject}/merge_requests/${parsed.iid}`
454
472
  );
455
473
  return mr.state === "merged";
@@ -480,15 +498,15 @@ var GitLabIssuesSource = class {
480
498
  const project = parseGitLabProject(config.scope);
481
499
  const validStates = ["opened", "closed", "all"];
482
500
  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];
501
+ const filterLabels = isOrphanDetection ? [config.pick_from] : normalizeLabels(config);
484
502
  const label = filterLabels.map((l) => encodeURIComponent(l)).join(",");
485
503
  const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
486
- const issues = await gitlabGet(path);
504
+ const issues = await api2().get(path);
487
505
  if (issues.length === 0) return null;
488
506
  const unblocked = [];
489
507
  const blocked = [];
490
508
  for (const issue2 of issues) {
491
- const links = await gitlabGet(
509
+ const links = await api2().get(
492
510
  `/projects/${project}/issues/${issue2.iid}/links`
493
511
  );
494
512
  const activeBlockers = links.filter((link) => {
@@ -533,12 +551,13 @@ var GitLabIssuesSource = class {
533
551
  const ref = parseGitLabIssueRef(id);
534
552
  try {
535
553
  const project = parseGitLabProject(ref.project);
536
- const issue = await gitlabGet(`/projects/${project}/issues/${ref.iid}`);
554
+ const issue = await api2().get(`/projects/${project}/issues/${ref.iid}`);
537
555
  return {
538
556
  id: makeIssueId2(ref.project, issue.iid),
539
557
  title: issue.title,
540
558
  description: issue.description ?? "",
541
- url: issue.web_url
559
+ url: issue.web_url,
560
+ status: issue.state
542
561
  };
543
562
  } catch {
544
563
  return null;
@@ -547,15 +566,15 @@ var GitLabIssuesSource = class {
547
566
  async updateStatus(issueId, labelToAdd, config) {
548
567
  const { project, iid } = splitIssueId(issueId);
549
568
  const encodedProject = parseGitLabProject(project);
550
- const issue = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
569
+ const issue = await api2().get(`/projects/${encodedProject}/issues/${iid}`);
551
570
  if (config && config.in_progress !== config.pick_from) {
552
- const filterLabels = Array.isArray(config.label) ? config.label : [config.label];
571
+ const filterLabels = normalizeLabels(config);
553
572
  const isMovingToInProgress = labelToAdd === config.in_progress;
554
573
  if (isMovingToInProgress) {
555
574
  const updated2 = [.../* @__PURE__ */ new Set([...issue.labels, labelToAdd])].filter(
556
575
  (l) => !filterLabels.includes(l)
557
576
  );
558
- await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
577
+ await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
559
578
  labels: updated2.join(",")
560
579
  });
561
580
  return;
@@ -563,40 +582,40 @@ var GitLabIssuesSource = class {
563
582
  const updated = [.../* @__PURE__ */ new Set([...issue.labels, ...filterLabels])].filter(
564
583
  (l) => l !== config.in_progress
565
584
  );
566
- await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
585
+ await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
567
586
  labels: updated.join(",")
568
587
  });
569
588
  return;
570
589
  }
571
590
  const labels = [.../* @__PURE__ */ new Set([...issue.labels, labelToAdd])];
572
- await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, { labels: labels.join(",") });
591
+ await api2().put(`/projects/${encodedProject}/issues/${iid}`, { labels: labels.join(",") });
573
592
  }
574
593
  async attachPullRequest(issueId, prUrl) {
575
594
  const { project, iid } = splitIssueId(issueId);
576
595
  const encodedProject = parseGitLabProject(project);
577
- await gitlabPost(`/projects/${encodedProject}/issues/${iid}/notes`, {
596
+ await api2().post(`/projects/${encodedProject}/issues/${iid}/notes`, {
578
597
  body: `Pull request: ${prUrl}`
579
598
  });
580
599
  }
581
600
  async completeIssue(issueId, _status, labelToRemove, config) {
582
601
  const { project, iid } = splitIssueId(issueId);
583
602
  const encodedProject = parseGitLabProject(project);
584
- const issue = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
603
+ const issue = await api2().get(`/projects/${encodedProject}/issues/${iid}`);
585
604
  let labels = labelToRemove ? issue.labels.filter((l) => l.toLowerCase() !== labelToRemove.toLowerCase()) : issue.labels;
586
605
  if (config && config.in_progress !== config.pick_from) {
587
606
  labels = labels.filter((l) => l !== config.in_progress);
588
607
  }
589
- await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
608
+ await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
590
609
  state_event: "close",
591
610
  labels: labels.join(",")
592
611
  });
593
612
  }
594
613
  async listIssues(config) {
595
614
  const project = parseGitLabProject(config.scope);
596
- const labelsArr = Array.isArray(config.label) ? config.label : [config.label];
615
+ const labelsArr = normalizeLabels(config);
597
616
  const label = labelsArr.map((l) => encodeURIComponent(l)).join(",");
598
617
  const path = `/projects/${project}/issues?labels=${label}&state=opened&per_page=100`;
599
- const issues = await gitlabGet(path);
618
+ const issues = await api2().get(path);
600
619
  return issues.map((issue) => ({
601
620
  id: makeIssueId2(config.scope, issue.iid),
602
621
  title: issue.title,
@@ -609,7 +628,7 @@ var GitLabIssuesSource = class {
609
628
  const results = [];
610
629
  let page = 1;
611
630
  while (true) {
612
- const labels = await gitlabGet(
631
+ const labels = await api2().get(
613
632
  `/projects/${project}/labels?per_page=100&page=${page}`
614
633
  );
615
634
  for (const l of labels) {
@@ -626,13 +645,37 @@ var GitLabIssuesSource = class {
626
645
  async removeLabel(issueId, labelToRemove) {
627
646
  const { project, iid } = splitIssueId(issueId);
628
647
  const encodedProject = parseGitLabProject(project);
629
- const issue = await gitlabGet(`/projects/${encodedProject}/issues/${iid}`);
648
+ const issue = await api2().get(`/projects/${encodedProject}/issues/${iid}`);
630
649
  const filtered = issue.labels.filter((l) => l.toLowerCase() !== labelToRemove.toLowerCase());
631
650
  if (filtered.length === issue.labels.length) return;
632
- await gitlabPut(`/projects/${encodedProject}/issues/${iid}`, {
651
+ await api2().put(`/projects/${encodedProject}/issues/${iid}`, {
633
652
  labels: filtered.join(",")
634
653
  });
635
654
  }
655
+ async createIssue(opts, config) {
656
+ const encodedProject = parseGitLabProject(config.scope);
657
+ const labels = Array.isArray(opts.label) ? opts.label : [opts.label];
658
+ const issue = await api2().post(`/projects/${encodedProject}/issues`, {
659
+ title: opts.title,
660
+ description: opts.description,
661
+ labels: labels.join(","),
662
+ ...opts.order !== void 0 && { weight: opts.order }
663
+ });
664
+ return makeIssueId2(config.scope, issue.iid);
665
+ }
666
+ async linkDependency(issueId, dependsOnId) {
667
+ const source = splitIssueId(issueId);
668
+ const target = splitIssueId(dependsOnId);
669
+ const encodedProject = parseGitLabProject(source.project);
670
+ const projectInfo = await api2().get(
671
+ `/projects/${parseGitLabProject(target.project)}`
672
+ );
673
+ await api2().post(`/projects/${encodedProject}/issues/${source.iid}/links`, {
674
+ target_project_id: projectInfo.id,
675
+ target_issue_iid: Number(target.iid),
676
+ link_type: "is_blocked_by"
677
+ });
678
+ }
636
679
  };
637
680
  function parseGitLabProject(input) {
638
681
  if (/^\d+$/.test(input)) return input;
@@ -819,11 +862,18 @@ function useKanbanState(bellEnabled, initialCards = []) {
819
862
  setCards((prev) => prev.map((c) => c.id === issueId ? { ...c, logFile } : c));
820
863
  };
821
864
  const MAX_OUTPUT_SIZE = 2e5;
822
- const onOutput = (issueId, text) => {
865
+ const outputBuffer = /* @__PURE__ */ new Map();
866
+ let flushTimer = null;
867
+ const flushOutputBuffer = () => {
868
+ flushTimer = null;
869
+ if (outputBuffer.size === 0) return;
870
+ const buffered = new Map(outputBuffer);
871
+ outputBuffer.clear();
823
872
  setCards(
824
873
  (prev) => prev.map((c) => {
825
- if (c.id !== issueId) return c;
826
- let newLog = c.outputLog + text;
874
+ const chunk = buffered.get(c.id);
875
+ if (chunk === void 0) return c;
876
+ let newLog = c.outputLog + chunk;
827
877
  if (newLog.length > MAX_OUTPUT_SIZE) {
828
878
  const trimAt = newLog.indexOf("\n", newLog.length - MAX_OUTPUT_SIZE);
829
879
  newLog = trimAt !== -1 ? newLog.slice(trimAt + 1) : newLog.slice(-MAX_OUTPUT_SIZE);
@@ -832,7 +882,15 @@ function useKanbanState(bellEnabled, initialCards = []) {
832
882
  })
833
883
  );
834
884
  };
885
+ const onOutput = (issueId, text) => {
886
+ const existing = outputBuffer.get(issueId);
887
+ outputBuffer.set(issueId, existing !== void 0 ? existing + text : text);
888
+ if (flushTimer === null) {
889
+ flushTimer = setTimeout(flushOutputBuffer, 100);
890
+ }
891
+ };
835
892
  const onReconcileRemove = (issueId) => {
893
+ outputBuffer.delete(issueId);
836
894
  setCards((prev) => prev.filter((c) => c.id !== issueId));
837
895
  };
838
896
  kanbanEmitter.on("issue:queued", onQueued);
@@ -850,6 +908,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
850
908
  const onModelChanged = (model) => setModelInUse(model);
851
909
  kanbanEmitter.on("provider:model-changed", onModelChanged);
852
910
  const onEmpty = () => setIsEmpty(true);
911
+ const onResumed = () => setIsEmpty(false);
853
912
  const onComplete = (data) => setWorkComplete(data);
854
913
  const onWatching = () => setIsWatching(true);
855
914
  const onWatchResume = () => setIsWatching(false);
@@ -859,6 +918,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
859
918
  };
860
919
  const onWatchPromptResolved = () => setIsWatchPrompt(false);
861
920
  kanbanEmitter.on("work:empty", onEmpty);
921
+ kanbanEmitter.on("work:resumed", onResumed);
862
922
  kanbanEmitter.on("work:complete", onComplete);
863
923
  kanbanEmitter.on("work:watching", onWatching);
864
924
  kanbanEmitter.on("work:watch-resume", onWatchResume);
@@ -866,6 +926,11 @@ function useKanbanState(bellEnabled, initialCards = []) {
866
926
  kanbanEmitter.on("work:watch-prompt-resolved", onWatchPromptResolved);
867
927
  const cleanupBell = registerBellListeners(bellEnabled);
868
928
  return () => {
929
+ if (flushTimer !== null) {
930
+ clearTimeout(flushTimer);
931
+ flushTimer = null;
932
+ }
933
+ outputBuffer.clear();
869
934
  kanbanEmitter.off("issue:queued", onQueued);
870
935
  kanbanEmitter.off("issue:started", onStarted);
871
936
  kanbanEmitter.off("issue:done", onDone);
@@ -880,6 +945,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
880
945
  kanbanEmitter.off("issue:output", onOutput);
881
946
  kanbanEmitter.off("provider:model-changed", onModelChanged);
882
947
  kanbanEmitter.off("work:empty", onEmpty);
948
+ kanbanEmitter.off("work:resumed", onResumed);
883
949
  kanbanEmitter.off("work:complete", onComplete);
884
950
  kanbanEmitter.off("work:watching", onWatching);
885
951
  kanbanEmitter.off("work:watch-resume", onWatchResume);
@@ -906,6 +972,7 @@ function useKanbanState(bellEnabled, initialCards = []) {
906
972
  export {
907
973
  setOutputMode,
908
974
  getOutputMode,
975
+ setLogLevel,
909
976
  initLogFile,
910
977
  log,
911
978
  warn,
@@ -914,6 +981,9 @@ export {
914
981
  divider,
915
982
  banner,
916
983
  updateNotice,
984
+ REQUEST_TIMEOUT_MS,
985
+ normalizeLabels,
986
+ createApiClient,
917
987
  GitHubIssuesSource,
918
988
  GitLabIssuesSource,
919
989
  kanbanEmitter,