@tarcisiopgs/lisa 1.17.2 → 1.18.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.
package/README.md CHANGED
@@ -4,10 +4,6 @@
4
4
  <strong>Label an issue. Walk away. Come back to a PR.</strong>
5
5
  </p>
6
6
 
7
- <p align="center">
8
- <img src="assets/demo.gif?v=2" alt="Lisa demo" width="800" />
9
- </p>
10
-
11
7
  <p align="center">
12
8
  <a href="https://www.npmjs.com/package/@tarcisiopgs/lisa"><img src="https://img.shields.io/npm/v/@tarcisiopgs/lisa.svg" alt="npm version" /></a>
13
9
  <a href="https://www.npmjs.com/package/@tarcisiopgs/lisa"><img src="https://img.shields.io/npm/dm/@tarcisiopgs/lisa.svg" alt="npm downloads" /></a>
@@ -15,6 +11,10 @@
15
11
  <img src="https://img.shields.io/node/v/%40tarcisiopgs%2Flisa" alt="Node.js version" />
16
12
  </p>
17
13
 
14
+ <p align="center">
15
+ <img src="assets/demo.gif?v=2" alt="Lisa demo" width="800" />
16
+ </p>
17
+
18
18
  ---
19
19
 
20
20
  ## Quickstart
@@ -7,6 +7,9 @@ import {
7
7
  import { EventEmitter } from "events";
8
8
  import { useEffect, useState } from "react";
9
9
 
10
+ // src/sources/github-issues.ts
11
+ import { execa } from "execa";
12
+
10
13
  // src/output/logger.ts
11
14
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
12
15
  import { dirname } from "path";
@@ -83,9 +86,17 @@ var API_URL = "https://api.github.com";
83
86
  var REQUEST_TIMEOUT_MS = 3e4;
84
87
  var PRIORITY_LABELS = ["p1", "p2", "p3"];
85
88
  var DEPENDENCY_PATTERN = /(?:depends\s+on|blocked\s+by)\s+#(\d+)/gi;
86
- function getAuthHeaders() {
87
- const token = process.env.GITHUB_TOKEN;
88
- if (!token) throw new Error("GITHUB_TOKEN must be set");
89
+ async function getToken() {
90
+ if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
91
+ try {
92
+ const { stdout } = await execa("gh", ["auth", "token"]);
93
+ if (stdout.trim()) return stdout.trim();
94
+ } catch {
95
+ }
96
+ throw new Error("GitHub authentication required: set GITHUB_TOKEN or run `gh auth login`");
97
+ }
98
+ async function getAuthHeaders() {
99
+ const token = await getToken();
89
100
  return {
90
101
  Authorization: `Bearer ${token}`,
91
102
  Accept: "application/vnd.github+json",
@@ -95,7 +106,7 @@ function getAuthHeaders() {
95
106
  async function githubFetch(method, path, body) {
96
107
  const url = `${API_URL}${path}`;
97
108
  const headers = {
98
- ...getAuthHeaders(),
109
+ ...await getAuthHeaders(),
99
110
  "Content-Type": "application/json"
100
111
  };
101
112
  const res = await fetch(url, {
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  ok,
33
33
  setOutputMode,
34
34
  warn
35
- } from "./chunk-WDGLMZB7.js";
35
+ } from "./chunk-KDKCASVH.js";
36
36
  import {
37
37
  notify,
38
38
  resetTitle,
@@ -257,10 +257,103 @@ import { resolve as resolvePath2 } from "path";
257
257
  import * as clack2 from "@clack/prompts";
258
258
  import pc from "picocolors";
259
259
 
260
+ // src/git/github.ts
261
+ import { execa } from "execa";
262
+
263
+ // src/git/pr-body.ts
264
+ var PROVIDER_ATTRIBUTION_RE = /claude\.ai|claude\s+code|gemini\s+cli|openai\s+codex|\bgoose\b|\baider\b|github\s+copilot|cursor\s+agent|\bopencode\b/i;
265
+ var AI_COAUTHOR_RE = /co-authored-by:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)/i;
266
+ function stripProviderAttribution(body) {
267
+ let result = body;
268
+ while (true) {
269
+ const sepIndex = result.lastIndexOf("\n---");
270
+ if (sepIndex === -1) break;
271
+ const section = result.slice(sepIndex);
272
+ if (PROVIDER_ATTRIBUTION_RE.test(section) || AI_COAUTHOR_RE.test(section)) {
273
+ result = result.slice(0, sepIndex).trimEnd();
274
+ } else {
275
+ break;
276
+ }
277
+ }
278
+ result = result.replace(
279
+ /\n+Co-Authored-By:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)[^\n]*/gi,
280
+ ""
281
+ );
282
+ return result.trimEnd();
283
+ }
284
+
285
+ // src/git/github.ts
286
+ async function isGhCliAvailable() {
287
+ try {
288
+ await execa("gh", ["auth", "status"]);
289
+ return true;
290
+ } catch {
291
+ return false;
292
+ }
293
+ }
294
+ var PROVIDER_DISPLAY_NAMES = {
295
+ claude: "Claude Code",
296
+ gemini: "Gemini CLI",
297
+ opencode: "OpenCode",
298
+ copilot: "GitHub Copilot CLI",
299
+ cursor: "Cursor Agent",
300
+ goose: "Goose",
301
+ aider: "Aider",
302
+ codex: "OpenAI Codex"
303
+ };
304
+ function formatProviderName(providerUsed) {
305
+ const providerKey = providerUsed.split("/")[0] ?? providerUsed;
306
+ return PROVIDER_DISPLAY_NAMES[providerKey] ?? providerKey;
307
+ }
308
+ async function deleteProviderComments(prUrl) {
309
+ try {
310
+ const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
311
+ if (!match) return;
312
+ const [, owner, repo, prNumber] = match;
313
+ const { stdout } = await execa("gh", [
314
+ "api",
315
+ "--paginate",
316
+ "--jq",
317
+ ".[]",
318
+ `/repos/${owner}/${repo}/issues/${prNumber}/comments`
319
+ ]);
320
+ const comments = stdout.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
321
+ for (const comment of comments) {
322
+ if (PROVIDER_ATTRIBUTION_RE.test(comment.body)) {
323
+ try {
324
+ await execa("gh", [
325
+ "api",
326
+ "--method",
327
+ "DELETE",
328
+ `/repos/${owner}/${repo}/issues/comments/${comment.id}`
329
+ ]);
330
+ } catch {
331
+ }
332
+ }
333
+ }
334
+ } catch {
335
+ }
336
+ }
337
+ async function appendPrAttribution(prUrl, providerUsed) {
338
+ await deleteProviderComments(prUrl);
339
+ try {
340
+ const { stdout: bodyJson } = await execa("gh", ["pr", "view", prUrl, "--json", "body"]);
341
+ const { body } = JSON.parse(bodyJson);
342
+ const providerName = formatProviderName(providerUsed);
343
+ const attribution = `
344
+
345
+ ---
346
+ \u{1F916} Resolved by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerName}**`;
347
+ const newBody = stripProviderAttribution(body ?? "") + attribution;
348
+ await execa("gh", ["pr", "edit", prUrl, "--body", newBody]);
349
+ } catch {
350
+ }
351
+ }
352
+
260
353
  // src/git/worktree.ts
261
354
  import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
262
355
  import { join, resolve as resolve2 } from "path";
263
- import { execa } from "execa";
356
+ import { execa as execa2 } from "execa";
264
357
  var WORKTREES_DIR = ".worktrees";
265
358
  function generateBranchName(issueId, title) {
266
359
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").substring(0, 40).replace(/^-|-$/g, "");
@@ -268,7 +361,7 @@ function generateBranchName(issueId, title) {
268
361
  return `feat/${safeId}-${slug}`;
269
362
  }
270
363
  async function cleanupOrphanedWorktree(repoRoot, branchName) {
271
- const { stdout: branchList } = await execa("git", ["branch", "--list", branchName], {
364
+ const { stdout: branchList } = await execa2("git", ["branch", "--list", branchName], {
272
365
  cwd: repoRoot,
273
366
  reject: false
274
367
  });
@@ -276,41 +369,41 @@ async function cleanupOrphanedWorktree(repoRoot, branchName) {
276
369
  return false;
277
370
  }
278
371
  const worktreePath = join(repoRoot, WORKTREES_DIR, branchName);
279
- const { stdout: worktreeList } = await execa("git", ["worktree", "list", "--porcelain"], {
372
+ const { stdout: worktreeList } = await execa2("git", ["worktree", "list", "--porcelain"], {
280
373
  cwd: repoRoot,
281
374
  reject: false
282
375
  });
283
376
  if (worktreeList.includes(worktreePath)) {
284
- await execa("git", ["worktree", "remove", worktreePath, "--force"], { cwd: repoRoot });
285
- await execa("git", ["worktree", "prune"], { cwd: repoRoot });
377
+ await execa2("git", ["worktree", "remove", worktreePath, "--force"], { cwd: repoRoot });
378
+ await execa2("git", ["worktree", "prune"], { cwd: repoRoot });
286
379
  }
287
- await execa("git", ["branch", "-D", branchName], { cwd: repoRoot });
380
+ await execa2("git", ["branch", "-D", branchName], { cwd: repoRoot });
288
381
  return true;
289
382
  }
290
383
  async function createWorktree(repoRoot, branchName, baseBranch) {
291
384
  const worktreePath = join(repoRoot, WORKTREES_DIR, branchName);
292
385
  await cleanupOrphanedWorktree(repoRoot, branchName);
293
386
  if (existsSync2(worktreePath)) {
294
- await execa("git", ["worktree", "remove", worktreePath, "--force"], {
387
+ await execa2("git", ["worktree", "remove", worktreePath, "--force"], {
295
388
  cwd: repoRoot,
296
389
  reject: false
297
390
  });
298
- await execa("git", ["worktree", "prune"], { cwd: repoRoot, reject: false });
391
+ await execa2("git", ["worktree", "prune"], { cwd: repoRoot, reject: false });
299
392
  if (existsSync2(worktreePath)) {
300
393
  rmSync(worktreePath, { recursive: true, force: true });
301
394
  }
302
395
  }
303
- await execa("git", ["fetch", "origin", baseBranch], { cwd: repoRoot });
304
- await execa("git", ["worktree", "add", "-b", branchName, worktreePath, `origin/${baseBranch}`], {
396
+ await execa2("git", ["fetch", "origin", baseBranch], { cwd: repoRoot });
397
+ await execa2("git", ["worktree", "add", "-b", branchName, worktreePath, `origin/${baseBranch}`], {
305
398
  cwd: repoRoot
306
399
  });
307
400
  return worktreePath;
308
401
  }
309
402
  async function removeWorktree(repoRoot, worktreePath) {
310
- await execa("git", ["worktree", "remove", worktreePath, "--force"], {
403
+ await execa2("git", ["worktree", "remove", worktreePath, "--force"], {
311
404
  cwd: repoRoot
312
405
  });
313
- await execa("git", ["worktree", "prune"], { cwd: repoRoot });
406
+ await execa2("git", ["worktree", "prune"], { cwd: repoRoot });
314
407
  }
315
408
  function ensureWorktreeGitignore(repoRoot) {
316
409
  const gitignorePath = join(repoRoot, ".gitignore");
@@ -328,21 +421,21 @@ function ensureWorktreeGitignore(repoRoot) {
328
421
  }
329
422
  async function findBranchByIssueId(repoRoot, issueId) {
330
423
  const needle = issueId.toLowerCase();
331
- const { stdout: local } = await execa(
424
+ const { stdout: local } = await execa2(
332
425
  "git",
333
426
  ["for-each-ref", "--sort=-committerdate", "--format=%(refname:short)", "refs/heads/"],
334
427
  { cwd: repoRoot }
335
428
  );
336
429
  const localMatch = local.split("\n").map((b) => b.trim()).filter(Boolean).find((b) => b.toLowerCase().includes(needle));
337
430
  if (localMatch) return localMatch;
338
- const { stdout: remote } = await execa(
431
+ const { stdout: remote } = await execa2(
339
432
  "git",
340
433
  ["for-each-ref", "--sort=-committerdate", "--format=%(refname:short)", "refs/remotes/origin/"],
341
434
  { cwd: repoRoot }
342
435
  );
343
436
  const remoteMatch = remote.split("\n").map((b) => b.trim()).filter(Boolean).find((b) => b.toLowerCase().includes(needle));
344
437
  if (remoteMatch) return remoteMatch.replace("origin/", "");
345
- const { stdout: lsRemote } = await execa("git", ["ls-remote", "--heads", "origin"], {
438
+ const { stdout: lsRemote } = await execa2("git", ["ls-remote", "--heads", "origin"], {
346
439
  cwd: repoRoot
347
440
  });
348
441
  const lsMatch = lsRemote.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => l.split(" ")[1]?.replace("refs/heads/", "") ?? "").find((b) => b.toLowerCase().includes(needle));
@@ -365,7 +458,7 @@ function determineRepoPath(repos, issue2, workspace) {
365
458
  }
366
459
  async function hasCodeChanges(repoPath, baseBranch) {
367
460
  try {
368
- const { stdout } = await execa("git", ["diff", "--stat", `${baseBranch}..HEAD`], {
461
+ const { stdout } = await execa2("git", ["diff", "--stat", `${baseBranch}..HEAD`], {
369
462
  cwd: repoPath,
370
463
  reject: false
371
464
  });
@@ -1624,101 +1717,6 @@ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 }
1624
1717
  import { tmpdir as tmpdir9 } from "os";
1625
1718
  import { join as join10, resolve as resolvePath } from "path";
1626
1719
  import * as clack from "@clack/prompts";
1627
-
1628
- // src/git/github.ts
1629
- import { execa as execa2 } from "execa";
1630
-
1631
- // src/git/pr-body.ts
1632
- var PROVIDER_ATTRIBUTION_RE = /claude\.ai|claude\s+code|gemini\s+cli|openai\s+codex|\bgoose\b|\baider\b|github\s+copilot|cursor\s+agent|\bopencode\b/i;
1633
- var AI_COAUTHOR_RE = /co-authored-by:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)/i;
1634
- function stripProviderAttribution(body) {
1635
- let result = body;
1636
- while (true) {
1637
- const sepIndex = result.lastIndexOf("\n---");
1638
- if (sepIndex === -1) break;
1639
- const section = result.slice(sepIndex);
1640
- if (PROVIDER_ATTRIBUTION_RE.test(section) || AI_COAUTHOR_RE.test(section)) {
1641
- result = result.slice(0, sepIndex).trimEnd();
1642
- } else {
1643
- break;
1644
- }
1645
- }
1646
- result = result.replace(
1647
- /\n+Co-Authored-By:[^\n]*(anthropic|claude|gemini|openai|codex|goose|aider|copilot|cursor|google)[^\n]*/gi,
1648
- ""
1649
- );
1650
- return result.trimEnd();
1651
- }
1652
-
1653
- // src/git/github.ts
1654
- async function isGhCliAvailable() {
1655
- try {
1656
- await execa2("gh", ["auth", "status"]);
1657
- return true;
1658
- } catch {
1659
- return false;
1660
- }
1661
- }
1662
- var PROVIDER_DISPLAY_NAMES = {
1663
- claude: "Claude Code",
1664
- gemini: "Gemini CLI",
1665
- opencode: "OpenCode",
1666
- copilot: "GitHub Copilot CLI",
1667
- cursor: "Cursor Agent",
1668
- goose: "Goose",
1669
- aider: "Aider",
1670
- codex: "OpenAI Codex"
1671
- };
1672
- function formatProviderName(providerUsed) {
1673
- const providerKey = providerUsed.split("/")[0] ?? providerUsed;
1674
- return PROVIDER_DISPLAY_NAMES[providerKey] ?? providerKey;
1675
- }
1676
- async function deleteProviderComments(prUrl) {
1677
- try {
1678
- const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
1679
- if (!match) return;
1680
- const [, owner, repo, prNumber] = match;
1681
- const { stdout } = await execa2("gh", [
1682
- "api",
1683
- "--paginate",
1684
- "--jq",
1685
- ".[]",
1686
- `/repos/${owner}/${repo}/issues/${prNumber}/comments`
1687
- ]);
1688
- const comments = stdout.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
1689
- for (const comment of comments) {
1690
- if (PROVIDER_ATTRIBUTION_RE.test(comment.body)) {
1691
- try {
1692
- await execa2("gh", [
1693
- "api",
1694
- "--method",
1695
- "DELETE",
1696
- `/repos/${owner}/${repo}/issues/comments/${comment.id}`
1697
- ]);
1698
- } catch {
1699
- }
1700
- }
1701
- }
1702
- } catch {
1703
- }
1704
- }
1705
- async function appendPrAttribution(prUrl, providerUsed) {
1706
- await deleteProviderComments(prUrl);
1707
- try {
1708
- const { stdout: bodyJson } = await execa2("gh", ["pr", "view", prUrl, "--json", "body"]);
1709
- const { body } = JSON.parse(bodyJson);
1710
- const providerName = formatProviderName(providerUsed);
1711
- const attribution = `
1712
-
1713
- ---
1714
- \u{1F916} Resolved by [lisa](https://github.com/tarcisiopgs/lisa) using **${providerName}**`;
1715
- const newBody = stripProviderAttribution(body ?? "") + attribution;
1716
- await execa2("gh", ["pr", "edit", prUrl, "--body", newBody]);
1717
- } catch {
1718
- }
1719
- }
1720
-
1721
- // src/cli/detection.ts
1722
1720
  function getVersion() {
1723
1721
  try {
1724
1722
  const pkgPath = resolvePath(new URL(".", import.meta.url).pathname, "../package.json");
@@ -2118,48 +2116,70 @@ remove them or set the file to ${pc.cyan("{}")} \u2014 MCP tools can cause OpenC
2118
2116
  if (clack2.isCancel(modelSelection)) return process.exit(0);
2119
2117
  selectedModels = modelSelection ?? [];
2120
2118
  }
2119
+ const ghCliAvailable = await isGhCliAvailable();
2121
2120
  const source = await clack2.select({
2122
2121
  message: "Where do your issues come from?",
2123
2122
  initialValue: initial?.source,
2124
2123
  options: [
2125
- { value: "linear", label: "Linear", apiHint: "GraphQL API", envVars: ["LINEAR_API_KEY"] },
2124
+ {
2125
+ value: "linear",
2126
+ label: "Linear",
2127
+ apiHint: "GraphQL API",
2128
+ envVars: ["LINEAR_API_KEY"],
2129
+ ghCliFallback: false
2130
+ },
2126
2131
  {
2127
2132
  value: "trello",
2128
2133
  label: "Trello",
2129
2134
  apiHint: "REST API",
2130
- envVars: ["TRELLO_API_KEY", "TRELLO_TOKEN"]
2135
+ envVars: ["TRELLO_API_KEY", "TRELLO_TOKEN"],
2136
+ ghCliFallback: false
2131
2137
  },
2132
2138
  {
2133
2139
  value: "github-issues",
2134
2140
  label: "GitHub Issues",
2135
2141
  apiHint: "REST API",
2136
- envVars: ["GITHUB_TOKEN"]
2142
+ envVars: ["GITHUB_TOKEN"],
2143
+ ghCliFallback: true
2137
2144
  },
2138
2145
  {
2139
2146
  value: "gitlab-issues",
2140
2147
  label: "GitLab Issues",
2141
2148
  apiHint: "REST API",
2142
- envVars: ["GITLAB_TOKEN"]
2149
+ envVars: ["GITLAB_TOKEN"],
2150
+ ghCliFallback: false
2151
+ },
2152
+ {
2153
+ value: "plane",
2154
+ label: "Plane",
2155
+ apiHint: "REST API",
2156
+ envVars: ["PLANE_API_TOKEN"],
2157
+ ghCliFallback: false
2143
2158
  },
2144
- { value: "plane", label: "Plane", apiHint: "REST API", envVars: ["PLANE_API_TOKEN"] },
2145
2159
  {
2146
2160
  value: "shortcut",
2147
2161
  label: "Shortcut",
2148
2162
  apiHint: "REST API",
2149
- envVars: ["SHORTCUT_API_TOKEN"]
2163
+ envVars: ["SHORTCUT_API_TOKEN"],
2164
+ ghCliFallback: false
2150
2165
  },
2151
2166
  {
2152
2167
  value: "jira",
2153
2168
  label: "Jira",
2154
2169
  apiHint: "REST API",
2155
- envVars: ["JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_API_TOKEN"]
2170
+ envVars: ["JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_API_TOKEN"],
2171
+ ghCliFallback: false
2172
+ }
2173
+ ].map(({ value, label: label2, apiHint, envVars, ghCliFallback }) => {
2174
+ let missing2 = envVars.filter((v) => !process.env[v]);
2175
+ if (ghCliFallback && ghCliAvailable) {
2176
+ missing2 = missing2.filter((v) => v !== "GITHUB_TOKEN");
2156
2177
  }
2157
- ].map(({ value, label: label2, apiHint, envVars }) => {
2158
- const missing2 = envVars.filter((v) => !process.env[v]);
2178
+ const usingGhCli = ghCliFallback && ghCliAvailable && !process.env.GITHUB_TOKEN;
2159
2179
  return {
2160
2180
  value,
2161
2181
  label: label2,
2162
- hint: missing2.length > 0 ? `missing: ${missing2.join(", ")}` : apiHint,
2182
+ hint: missing2.length > 0 ? `missing: ${missing2.join(", ")}` : usingGhCli ? "via gh CLI" : apiHint,
2163
2183
  disabled: missing2.length > 0
2164
2184
  };
2165
2185
  })
@@ -7248,7 +7268,7 @@ var run = defineCommand5({
7248
7268
  if (isTTY) {
7249
7269
  const { render } = await import("ink");
7250
7270
  const { createElement } = await import("react");
7251
- const { KanbanApp } = await import("./kanban-LG26AUFK.js");
7271
+ const { KanbanApp } = await import("./kanban-PTPW3HO2.js");
7252
7272
  const demoConfig = {
7253
7273
  provider: "claude",
7254
7274
  source: "linear",
@@ -7323,7 +7343,7 @@ Add them to your ${shell} and run: source ${shell}`));
7323
7343
  if (isTTY) {
7324
7344
  const { render } = await import("ink");
7325
7345
  const { createElement } = await import("react");
7326
- const { KanbanApp } = await import("./kanban-LG26AUFK.js");
7346
+ const { KanbanApp } = await import("./kanban-PTPW3HO2.js");
7327
7347
  render(createElement(KanbanApp, { config: merged }), { exitOnCtrlC: false });
7328
7348
  }
7329
7349
  await runLoop(merged, {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  kanbanEmitter,
4
4
  useKanbanState
5
- } from "./chunk-WDGLMZB7.js";
5
+ } from "./chunk-KDKCASVH.js";
6
6
  import {
7
7
  resetTitle,
8
8
  startSpinner,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarcisiopgs/lisa",
3
- "version": "1.17.2",
3
+ "version": "1.18.0",
4
4
  "description": "Autonomous issue resolver",
5
5
  "keywords": [
6
6
  "loop",