@tarcisiopgs/lisa 0.5.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.
Files changed (3) hide show
  1. package/README.md +113 -0
  2. package/dist/index.js +1157 -0
  3. package/package.json +35 -0
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # lisa
2
+
3
+ Autonomous issue resolver — picks up issues from Linear or Trello, sends them to an AI coding agent (Claude Code, Gemini CLI, or OpenCode), and opens PRs via the GitHub API. No MCP servers required.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @tarcisiopgs/lisa
9
+ ```
10
+
11
+ ## Environment Variables
12
+
13
+ lisa calls external APIs directly. Set these in your shell profile (`~/.zshrc` or `~/.bashrc`):
14
+
15
+ ```bash
16
+ # Required (always)
17
+ export GITHUB_TOKEN=""
18
+
19
+ # Required when source = linear
20
+ export LINEAR_API_KEY=""
21
+
22
+ # Required when source = trello
23
+ export TRELLO_API_KEY=""
24
+ export TRELLO_TOKEN=""
25
+ ```
26
+
27
+ The CLI will warn you if any required variable is missing.
28
+
29
+ ## Quick Start
30
+
31
+ ```bash
32
+ # Interactive setup
33
+ lisa init
34
+
35
+ # Run continuously
36
+ lisa run
37
+
38
+ # Single issue
39
+ lisa run --once
40
+
41
+ # Preview without executing
42
+ lisa run --dry-run
43
+
44
+ # Override provider
45
+ lisa run --provider gemini --once
46
+ ```
47
+
48
+ ## Commands
49
+
50
+ | Command | Description |
51
+ |---------|-------------|
52
+ | `lisa run` | Run the agent loop |
53
+ | `lisa run --once` | Process a single issue |
54
+ | `lisa run --limit N` | Process up to N issues |
55
+ | `lisa run --dry-run` | Preview without executing |
56
+ | `lisa config` | Interactive config wizard |
57
+ | `lisa config --show` | Show current config |
58
+ | `lisa config --set key=value` | Set a config value |
59
+ | `lisa init` | Create `.lisa/config.yaml` |
60
+ | `lisa status` | Show session stats |
61
+
62
+ ## Providers
63
+
64
+ | Provider | CLI | Auto-approve Flag |
65
+ |----------|-----|-------------------|
66
+ | Claude Code | `claude` | `--dangerously-skip-permissions` |
67
+ | Gemini CLI | `gemini` | `--yolo` |
68
+ | OpenCode | `opencode` | implicit in `run` |
69
+
70
+ At least one provider must be installed and available in your PATH.
71
+
72
+ ## Configuration
73
+
74
+ Config lives in `.lisa/config.yaml`:
75
+
76
+ ```yaml
77
+ provider: claude
78
+
79
+ source: linear
80
+ source_config:
81
+ team: Internal
82
+ project: Zenixx
83
+ label: ready
84
+ status: Backlog
85
+
86
+ workspace: .
87
+ repos:
88
+ - name: app
89
+ path: ./app
90
+ match: "App:"
91
+
92
+ loop:
93
+ cooldown: 10
94
+ max_sessions: 0
95
+
96
+ logs:
97
+ dir: .lisa/logs
98
+ format: text
99
+ ```
100
+
101
+ CLI flags override config values:
102
+
103
+ ```bash
104
+ lisa run --provider gemini --label "urgent"
105
+ ```
106
+
107
+ ## How It Works
108
+
109
+ 1. **Fetch** — Calls the Linear GraphQL API or Trello REST API to get the next issue matching the configured label, team, and project. Issues are sorted by priority.
110
+ 2. **Implement** — Builds a prompt with the issue title, description, and repo context, then sends it to the AI coding agent. The agent creates a branch, implements the changes, and pushes to origin.
111
+ 3. **PR** — Creates a pull request via the GitHub API referencing the original issue.
112
+ 4. **Update** — Moves the issue status to "In Review" and removes the pickup label via the source API.
113
+ 5. **Loop** — Waits `cooldown` seconds, then picks the next issue. Repeats until no issues remain or the limit is reached.
package/dist/index.js ADDED
@@ -0,0 +1,1157 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync } from "fs";
5
+ import { join, resolve as resolvePath } from "path";
6
+ import { defineCommand, runMain } from "citty";
7
+ import * as clack from "@clack/prompts";
8
+ import pc2 from "picocolors";
9
+
10
+ // src/config.ts
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
+ import { resolve } from "path";
13
+ import { parse, stringify } from "yaml";
14
+ var CONFIG_DIR = ".lisa";
15
+ var CONFIG_FILE = "config.yaml";
16
+ var DEFAULT_CONFIG = {
17
+ provider: "",
18
+ source: "",
19
+ source_config: {
20
+ team: "",
21
+ project: "",
22
+ label: "",
23
+ status: ""
24
+ },
25
+ github: "cli",
26
+ workspace: "",
27
+ repos: [],
28
+ loop: {
29
+ cooldown: 0,
30
+ max_sessions: 0
31
+ },
32
+ logs: {
33
+ dir: "",
34
+ format: ""
35
+ }
36
+ };
37
+ function getConfigPath(cwd = process.cwd()) {
38
+ return resolve(cwd, CONFIG_DIR, CONFIG_FILE);
39
+ }
40
+ function configExists(cwd = process.cwd()) {
41
+ return existsSync(getConfigPath(cwd));
42
+ }
43
+ function loadConfig(cwd = process.cwd()) {
44
+ const configPath = getConfigPath(cwd);
45
+ if (!existsSync(configPath)) {
46
+ return { ...DEFAULT_CONFIG };
47
+ }
48
+ const raw = readFileSync(configPath, "utf-8");
49
+ const parsed = parse(raw);
50
+ return {
51
+ ...DEFAULT_CONFIG,
52
+ ...parsed,
53
+ source_config: { ...DEFAULT_CONFIG.source_config, ...parsed.source_config },
54
+ loop: { ...DEFAULT_CONFIG.loop, ...parsed.loop },
55
+ logs: { ...DEFAULT_CONFIG.logs, ...parsed.logs }
56
+ };
57
+ }
58
+ function saveConfig(config2, cwd = process.cwd()) {
59
+ const configPath = getConfigPath(cwd);
60
+ const dir = resolve(cwd, CONFIG_DIR);
61
+ if (!existsSync(dir)) {
62
+ mkdirSync(dir, { recursive: true });
63
+ }
64
+ writeFileSync(configPath, stringify(config2), "utf-8");
65
+ }
66
+ function mergeWithFlags(config2, flags) {
67
+ const merged = { ...config2 };
68
+ if (flags.provider) merged.provider = flags.provider;
69
+ if (flags.source) merged.source = flags.source;
70
+ if (flags.github) merged.github = flags.github;
71
+ if (flags.label) merged.source_config = { ...merged.source_config, label: flags.label };
72
+ return merged;
73
+ }
74
+
75
+ // src/logger.ts
76
+ import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
77
+ import { dirname } from "path";
78
+ import pc from "picocolors";
79
+ var logFilePath = null;
80
+ var outputMode = "default";
81
+ var jsonEvents = [];
82
+ function setOutputMode(mode) {
83
+ outputMode = mode;
84
+ }
85
+ function initLogFile(path) {
86
+ const dir = dirname(path);
87
+ if (!existsSync2(dir)) {
88
+ mkdirSync2(dir, { recursive: true });
89
+ }
90
+ logFilePath = path;
91
+ }
92
+ function timestamp() {
93
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
94
+ }
95
+ function writeToFile(level, message) {
96
+ if (logFilePath) {
97
+ appendFileSync(logFilePath, `[${timestamp()}] [${level}] ${message}
98
+ `);
99
+ }
100
+ }
101
+ function emitJson(level, message) {
102
+ const event = { time: timestamp(), level, message };
103
+ jsonEvents.push(event);
104
+ console.log(JSON.stringify(event));
105
+ }
106
+ function log(message) {
107
+ if (outputMode === "json") return emitJson("info", message);
108
+ if (outputMode !== "quiet") {
109
+ console.log(`${pc.cyan("[lisa]")} ${pc.dim(timestamp())} ${message}`);
110
+ }
111
+ writeToFile("info", message);
112
+ }
113
+ function warn(message) {
114
+ if (outputMode === "json") return emitJson("warn", message);
115
+ if (outputMode !== "quiet") {
116
+ console.error(`${pc.yellow("[lisa]")} ${pc.dim(timestamp())} ${message}`);
117
+ }
118
+ writeToFile("warn", message);
119
+ }
120
+ function error(message) {
121
+ if (outputMode === "json") return emitJson("error", message);
122
+ console.error(`${pc.red("[lisa]")} ${pc.dim(timestamp())} ${message}`);
123
+ writeToFile("error", message);
124
+ }
125
+ function ok(message) {
126
+ if (outputMode === "json") return emitJson("ok", message);
127
+ if (outputMode !== "quiet") {
128
+ console.log(`${pc.green("[lisa]")} ${pc.dim(timestamp())} ${message}`);
129
+ }
130
+ writeToFile("ok", message);
131
+ }
132
+ function divider(session) {
133
+ log(`${"\u2501".repeat(3)} Session ${session} ${"\u2501".repeat(3)}`);
134
+ }
135
+ function banner() {
136
+ if (outputMode !== "default") return;
137
+ console.log(
138
+ pc.cyan(`
139
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
140
+ \u2502 lisa \u2014 autonomous issue resolver \u2502
141
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
142
+ `)
143
+ );
144
+ }
145
+
146
+ // src/loop.ts
147
+ import { resolve as resolve3 } from "path";
148
+ import { appendFileSync as appendFileSync2 } from "fs";
149
+
150
+ // src/prompt.ts
151
+ import { resolve as resolve2 } from "path";
152
+ function buildImplementPrompt(issue, config2) {
153
+ const workspace = resolve2(config2.workspace);
154
+ const repoEntries = config2.repos.map((r) => ` - If it says "Repo: ${r.name}" or title starts with "${r.match}" \u2192 \`${resolve2(workspace, r.path)}\``).join("\n");
155
+ return `You are an autonomous implementation agent. Your job is to implement a single
156
+ issue, validate it, commit, and push the branch.
157
+
158
+ ## Issue
159
+
160
+ - **ID:** ${issue.id}
161
+ - **Title:** ${issue.title}
162
+ - **URL:** ${issue.url}
163
+
164
+ ### Description
165
+
166
+ ${issue.description}
167
+
168
+ ## Instructions
169
+
170
+ 1. **Identify the repo**: Look at the issue description for relevant files or repo references.
171
+ ${repoEntries}
172
+ - If it references multiple repos, pick the PRIMARY one (the one with the most files listed).
173
+
174
+ 2. **Create a branch**: From the repo's main branch, create a branch named after the issue
175
+ (e.g., \`feat/${issue.id.toLowerCase()}-short-description\`).
176
+
177
+ 3. **Implement**: Follow the issue description exactly:
178
+ - Read all relevant files listed in the description first (if present)
179
+ - Follow the implementation instructions exactly
180
+ - Verify each acceptance criteria (if present)
181
+ - Respect any stack or technical constraints (if present)
182
+
183
+ 4. **Validate**: Run the project's linter/typecheck/tests if available:
184
+ - Check \`package.json\` (or equivalent) for lint, typecheck, check, or test scripts.
185
+ - Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`).
186
+ - Fix any errors before proceeding.
187
+
188
+ 5. **Commit & Push**: Make atomic commits with conventional commit messages.
189
+ Push the branch to origin.
190
+ **IMPORTANT \u2014 Language rules:**
191
+ - All commit messages MUST be in English.
192
+ - Use conventional commits format: \`feat: ...\`, \`fix: ...\`, \`refactor: ...\`, \`chore: ...\`
193
+
194
+ ## Rules
195
+
196
+ - **ALL git commits, branch names MUST be in English.**
197
+ - The issue description may be in any language \u2014 read it for context but write all code artifacts in English.
198
+ - Do NOT modify files outside the target repo.
199
+ - Do NOT install new dependencies unless the issue explicitly requires it.
200
+ - If you get stuck or the issue is unclear, STOP and explain why.
201
+ - One issue only. Do not pick up additional issues.
202
+ - If the repo has a CLAUDE.md, read it first and follow its conventions.
203
+ - Do NOT create pull requests \u2014 the caller handles that.
204
+ - Do NOT update the issue tracker \u2014 the caller handles that.`;
205
+ }
206
+
207
+ // src/providers/claude.ts
208
+ import { execa } from "execa";
209
+ var ClaudeProvider = class {
210
+ name = "claude";
211
+ async isAvailable() {
212
+ try {
213
+ await execa("claude", ["--version"]);
214
+ return true;
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+ async run(prompt, opts) {
220
+ const start = Date.now();
221
+ try {
222
+ const proc = execa(
223
+ "claude",
224
+ ["--dangerously-skip-permissions", "-p", prompt, "--output-format", "text"],
225
+ {
226
+ cwd: opts.cwd,
227
+ timeout: 30 * 60 * 1e3,
228
+ reject: false
229
+ }
230
+ );
231
+ proc.stdout?.pipe(process.stdout);
232
+ proc.stderr?.pipe(process.stderr);
233
+ const result = await proc;
234
+ const output = result.stdout + (result.stderr ? `
235
+ ${result.stderr}` : "");
236
+ return {
237
+ success: result.exitCode === 0,
238
+ output,
239
+ duration: Date.now() - start
240
+ };
241
+ } catch (err) {
242
+ return {
243
+ success: false,
244
+ output: err instanceof Error ? err.message : String(err),
245
+ duration: Date.now() - start
246
+ };
247
+ }
248
+ }
249
+ };
250
+
251
+ // src/providers/gemini.ts
252
+ import { execa as execa2 } from "execa";
253
+ var GeminiProvider = class {
254
+ name = "gemini";
255
+ async isAvailable() {
256
+ try {
257
+ await execa2("gemini", ["--version"]);
258
+ return true;
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+ async run(prompt, opts) {
264
+ const start = Date.now();
265
+ try {
266
+ const proc = execa2(
267
+ "gemini",
268
+ ["--yolo", "-p", prompt],
269
+ {
270
+ cwd: opts.cwd,
271
+ timeout: 30 * 60 * 1e3,
272
+ reject: false
273
+ }
274
+ );
275
+ proc.stdout?.pipe(process.stdout);
276
+ proc.stderr?.pipe(process.stderr);
277
+ const result = await proc;
278
+ const output = result.stdout + (result.stderr ? `
279
+ ${result.stderr}` : "");
280
+ return {
281
+ success: result.exitCode === 0,
282
+ output,
283
+ duration: Date.now() - start
284
+ };
285
+ } catch (err) {
286
+ return {
287
+ success: false,
288
+ output: err instanceof Error ? err.message : String(err),
289
+ duration: Date.now() - start
290
+ };
291
+ }
292
+ }
293
+ };
294
+
295
+ // src/providers/opencode.ts
296
+ import { execa as execa3 } from "execa";
297
+ var OpenCodeProvider = class {
298
+ name = "opencode";
299
+ async isAvailable() {
300
+ try {
301
+ await execa3("opencode", ["--version"]);
302
+ return true;
303
+ } catch {
304
+ return false;
305
+ }
306
+ }
307
+ async run(prompt, opts) {
308
+ const start = Date.now();
309
+ try {
310
+ const proc = execa3(
311
+ "opencode",
312
+ ["run", prompt],
313
+ {
314
+ cwd: opts.cwd,
315
+ timeout: 30 * 60 * 1e3,
316
+ reject: false
317
+ }
318
+ );
319
+ proc.stdout?.pipe(process.stdout);
320
+ proc.stderr?.pipe(process.stderr);
321
+ const result = await proc;
322
+ const output = result.stdout + (result.stderr ? `
323
+ ${result.stderr}` : "");
324
+ return {
325
+ success: result.exitCode === 0,
326
+ output,
327
+ duration: Date.now() - start
328
+ };
329
+ } catch (err) {
330
+ return {
331
+ success: false,
332
+ output: err instanceof Error ? err.message : String(err),
333
+ duration: Date.now() - start
334
+ };
335
+ }
336
+ }
337
+ };
338
+
339
+ // src/providers/index.ts
340
+ var providers = {
341
+ claude: () => new ClaudeProvider(),
342
+ gemini: () => new GeminiProvider(),
343
+ opencode: () => new OpenCodeProvider()
344
+ };
345
+ async function getAvailableProviders() {
346
+ const all = Object.values(providers).map((f) => f());
347
+ const results = await Promise.all(
348
+ all.map(async (p) => ({ provider: p, available: await p.isAvailable() }))
349
+ );
350
+ return results.filter((r) => r.available).map((r) => r.provider);
351
+ }
352
+ function createProvider(name) {
353
+ const factory = providers[name];
354
+ if (!factory) {
355
+ throw new Error(`Unknown provider: ${name}. Available: ${Object.keys(providers).join(", ")}`);
356
+ }
357
+ return factory();
358
+ }
359
+
360
+ // src/sources/linear.ts
361
+ var API_URL = "https://api.linear.app/graphql";
362
+ var REQUEST_TIMEOUT_MS = 3e4;
363
+ function getApiKey() {
364
+ const key = process.env.LINEAR_API_KEY;
365
+ if (!key) throw new Error("LINEAR_API_KEY is not set");
366
+ return key;
367
+ }
368
+ async function gql(query, variables) {
369
+ const res = await fetch(API_URL, {
370
+ method: "POST",
371
+ headers: {
372
+ "Content-Type": "application/json",
373
+ Authorization: getApiKey()
374
+ },
375
+ body: JSON.stringify({ query, variables }),
376
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
377
+ });
378
+ if (!res.ok) {
379
+ const text2 = await res.text();
380
+ throw new Error(`Linear API error (${res.status}): ${text2}`);
381
+ }
382
+ const json = await res.json();
383
+ if (json.errors?.length) {
384
+ throw new Error(`Linear GraphQL error: ${json.errors.map((e) => e.message).join(", ")}`);
385
+ }
386
+ return json.data;
387
+ }
388
+ var LinearSource = class {
389
+ name = "linear";
390
+ async fetchNextIssue(config2) {
391
+ const data = await gql(
392
+ `query($teamName: String!, $projectName: String!, $labelName: String!, $statusName: String!) {
393
+ issues(
394
+ filter: {
395
+ team: { name: { eq: $teamName } }
396
+ project: { name: { eq: $projectName } }
397
+ labels: { name: { eq: $labelName } }
398
+ state: { name: { eq: $statusName } }
399
+ }
400
+ first: 20
401
+ ) {
402
+ nodes {
403
+ id
404
+ identifier
405
+ title
406
+ description
407
+ url
408
+ priority
409
+ }
410
+ }
411
+ }`,
412
+ {
413
+ teamName: config2.team,
414
+ projectName: config2.project,
415
+ labelName: config2.label,
416
+ statusName: config2.status
417
+ }
418
+ );
419
+ const issues = data.issues.nodes;
420
+ if (issues.length === 0) return null;
421
+ issues.sort((a, b) => {
422
+ const pa = a.priority === 0 ? 5 : a.priority;
423
+ const pb = b.priority === 0 ? 5 : b.priority;
424
+ return pa - pb;
425
+ });
426
+ const issue = issues[0];
427
+ return {
428
+ id: issue.identifier,
429
+ title: issue.title,
430
+ description: issue.description || "",
431
+ url: issue.url
432
+ };
433
+ }
434
+ async updateStatus(issueId, statusName) {
435
+ const issueData = await gql(
436
+ `query($identifier: String!) {
437
+ issue(id: $identifier) {
438
+ id
439
+ team { id }
440
+ }
441
+ }`,
442
+ { identifier: issueId }
443
+ );
444
+ const statesData = await gql(
445
+ `query($teamId: String!) {
446
+ workflowStates(filter: { team: { id: { eq: $teamId } } }) {
447
+ nodes { id name }
448
+ }
449
+ }`,
450
+ { teamId: issueData.issue.team.id }
451
+ );
452
+ const state = statesData.workflowStates.nodes.find(
453
+ (s) => s.name.toLowerCase() === statusName.toLowerCase()
454
+ );
455
+ if (!state) {
456
+ const available = statesData.workflowStates.nodes.map((s) => s.name).join(", ");
457
+ throw new Error(`Status "${statusName}" not found. Available: ${available}`);
458
+ }
459
+ await gql(
460
+ `mutation($issueId: String!, $stateId: String!) {
461
+ issueUpdate(id: $issueId, input: { stateId: $stateId }) {
462
+ success
463
+ }
464
+ }`,
465
+ { issueId: issueData.issue.id, stateId: state.id }
466
+ );
467
+ }
468
+ async removeLabel(issueId, labelName) {
469
+ const issueData = await gql(
470
+ `query($identifier: String!) {
471
+ issue(id: $identifier) {
472
+ id
473
+ labels { nodes { id name } }
474
+ }
475
+ }`,
476
+ { identifier: issueId }
477
+ );
478
+ const currentLabels = issueData.issue.labels.nodes;
479
+ const filtered = currentLabels.filter(
480
+ (l) => l.name.toLowerCase() !== labelName.toLowerCase()
481
+ );
482
+ if (filtered.length === currentLabels.length) return;
483
+ await gql(
484
+ `mutation($issueId: String!, $labelIds: [String!]!) {
485
+ issueUpdate(id: $issueId, input: { labelIds: $labelIds }) {
486
+ success
487
+ }
488
+ }`,
489
+ {
490
+ issueId: issueData.issue.id,
491
+ labelIds: filtered.map((l) => l.id)
492
+ }
493
+ );
494
+ }
495
+ };
496
+
497
+ // src/sources/trello.ts
498
+ var API_URL2 = "https://api.trello.com/1";
499
+ var REQUEST_TIMEOUT_MS2 = 3e4;
500
+ function getAuthHeaders() {
501
+ const key = process.env.TRELLO_API_KEY;
502
+ const token = process.env.TRELLO_TOKEN;
503
+ if (!key || !token) throw new Error("TRELLO_API_KEY and TRELLO_TOKEN must be set");
504
+ return {
505
+ Authorization: `OAuth oauth_consumer_key="${key}", oauth_token="${token}"`
506
+ };
507
+ }
508
+ async function trelloFetch(method, path, params = "") {
509
+ const sep = params ? "?" : "";
510
+ const url = `${API_URL2}${path}${sep}${params}`;
511
+ const res = await fetch(url, {
512
+ method,
513
+ headers: getAuthHeaders(),
514
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
515
+ });
516
+ if (!res.ok) {
517
+ const text2 = await res.text();
518
+ throw new Error(`Trello API error (${res.status}): ${text2}`);
519
+ }
520
+ if (method === "DELETE") return void 0;
521
+ return await res.json();
522
+ }
523
+ async function trelloGet(path, params = "") {
524
+ return trelloFetch("GET", path, params);
525
+ }
526
+ async function trelloPut(path, params = "") {
527
+ return trelloFetch("PUT", path, params);
528
+ }
529
+ async function trelloDelete(path) {
530
+ await trelloFetch("DELETE", path);
531
+ }
532
+ async function findBoardByName(name) {
533
+ const boards = await trelloGet("/members/me/boards", "fields=name");
534
+ const board = boards.find((b) => b.name.toLowerCase() === name.toLowerCase());
535
+ if (!board) throw new Error(`Trello board "${name}" not found`);
536
+ return board;
537
+ }
538
+ async function findListByName(boardId, name) {
539
+ const lists = await trelloGet(`/boards/${boardId}/lists`, "fields=name");
540
+ const list = lists.find((l) => l.name.toLowerCase() === name.toLowerCase());
541
+ if (!list) {
542
+ const available = lists.map((l) => l.name).join(", ");
543
+ throw new Error(`Trello list "${name}" not found. Available: ${available}`);
544
+ }
545
+ return list;
546
+ }
547
+ async function findLabelByName(boardId, name) {
548
+ const labels = await trelloGet(`/boards/${boardId}/labels`, "fields=name");
549
+ const label = labels.find((l) => l.name.toLowerCase() === name.toLowerCase());
550
+ if (!label) throw new Error(`Trello label "${name}" not found`);
551
+ return label;
552
+ }
553
+ var TrelloSource = class {
554
+ name = "trello";
555
+ async fetchNextIssue(config2) {
556
+ const board = await findBoardByName(config2.team);
557
+ const list = await findListByName(board.id, config2.project);
558
+ const label = await findLabelByName(board.id, config2.label);
559
+ const cards = await trelloGet(
560
+ `/lists/${list.id}/cards`,
561
+ "fields=name,desc,url,idLabels,idList"
562
+ );
563
+ const matching = cards.filter((c) => c.idLabels.includes(label.id));
564
+ if (matching.length === 0) return null;
565
+ const card = matching[0];
566
+ return {
567
+ id: card.id,
568
+ title: card.name,
569
+ description: card.desc || "",
570
+ url: card.url
571
+ };
572
+ }
573
+ async updateStatus(cardId, listName) {
574
+ const card = await trelloGet(`/cards/${cardId}`, "fields=idBoard");
575
+ const list = await findListByName(card.idBoard, listName);
576
+ await trelloPut(`/cards/${cardId}`, `idList=${list.id}`);
577
+ }
578
+ async removeLabel(cardId, labelName) {
579
+ const card = await trelloGet(
580
+ `/cards/${cardId}`,
581
+ "fields=idBoard,idLabels"
582
+ );
583
+ const label = await findLabelByName(card.idBoard, labelName);
584
+ if (!card.idLabels.includes(label.id)) return;
585
+ await trelloDelete(`/cards/${cardId}/idLabels/${label.id}`);
586
+ }
587
+ };
588
+
589
+ // src/sources/index.ts
590
+ var sources = {
591
+ linear: () => new LinearSource(),
592
+ trello: () => new TrelloSource()
593
+ };
594
+ function createSource(name) {
595
+ const factory = sources[name];
596
+ if (!factory) {
597
+ throw new Error(`Unknown source: ${name}. Available: ${Object.keys(sources).join(", ")}`);
598
+ }
599
+ return factory();
600
+ }
601
+
602
+ // src/github.ts
603
+ import { execa as execa4 } from "execa";
604
+ var API_URL3 = "https://api.github.com";
605
+ var REQUEST_TIMEOUT_MS3 = 3e4;
606
+ async function isGhCliAvailable() {
607
+ try {
608
+ await execa4("gh", ["auth", "status"]);
609
+ return true;
610
+ } catch {
611
+ return false;
612
+ }
613
+ }
614
+ function getToken() {
615
+ const token = process.env.GITHUB_TOKEN;
616
+ if (!token) throw new Error("GITHUB_TOKEN is not set");
617
+ return token;
618
+ }
619
+ async function createPullRequest(opts, method = "cli") {
620
+ if (method === "cli" && await isGhCliAvailable()) {
621
+ return createPullRequestWithGhCli(opts);
622
+ }
623
+ const res = await fetch(`${API_URL3}/repos/${opts.owner}/${opts.repo}/pulls`, {
624
+ method: "POST",
625
+ headers: {
626
+ Authorization: `Bearer ${getToken()}`,
627
+ Accept: "application/vnd.github+json",
628
+ "Content-Type": "application/json"
629
+ },
630
+ body: JSON.stringify({
631
+ title: opts.title,
632
+ body: opts.body,
633
+ head: opts.head,
634
+ base: opts.base
635
+ }),
636
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS3)
637
+ });
638
+ if (!res.ok) {
639
+ const text2 = await res.text();
640
+ throw new Error(`GitHub API error (${res.status}): ${text2}`);
641
+ }
642
+ const data = await res.json();
643
+ return { number: data.number, html_url: data.html_url };
644
+ }
645
+ async function createPullRequestWithGhCli(opts) {
646
+ const result = await execa4("gh", [
647
+ "pr",
648
+ "create",
649
+ "--repo",
650
+ `${opts.owner}/${opts.repo}`,
651
+ "--head",
652
+ opts.head,
653
+ "--base",
654
+ opts.base,
655
+ "--title",
656
+ opts.title,
657
+ "--body",
658
+ opts.body
659
+ ]);
660
+ const url = result.stdout.trim();
661
+ const prNumberMatch = url.match(/\/pull\/(\d+)/);
662
+ const number = prNumberMatch ? Number.parseInt(prNumberMatch[1], 10) : 0;
663
+ return { number, html_url: url };
664
+ }
665
+ async function getRepoInfo(cwd) {
666
+ const { stdout: remoteUrl } = await execa4("git", ["remote", "get-url", "origin"], { cwd });
667
+ let owner;
668
+ let repo;
669
+ const sshMatch = remoteUrl.match(/git@github\.com:(.+?)\/(.+?)(?:\.git)?$/);
670
+ const httpsMatch = remoteUrl.match(/github\.com\/(.+?)\/(.+?)(?:\.git)?$/);
671
+ if (sshMatch) {
672
+ owner = sshMatch[1];
673
+ repo = sshMatch[2];
674
+ } else if (httpsMatch) {
675
+ owner = httpsMatch[1];
676
+ repo = httpsMatch[2];
677
+ } else {
678
+ throw new Error(`Cannot parse GitHub owner/repo from remote URL: ${remoteUrl}`);
679
+ }
680
+ const { stdout: branch } = await execa4("git", ["branch", "--show-current"], { cwd });
681
+ const { stdout: defaultBranch } = await execa4(
682
+ "git",
683
+ ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"],
684
+ { cwd, reject: false }
685
+ ).then(
686
+ (r) => r,
687
+ () => ({ stdout: "origin/main" })
688
+ );
689
+ return {
690
+ owner,
691
+ repo,
692
+ branch: branch.trim(),
693
+ defaultBranch: defaultBranch.replace("origin/", "").trim()
694
+ };
695
+ }
696
+
697
+ // src/loop.ts
698
+ async function runLoop(config2, opts) {
699
+ const provider = createProvider(config2.provider);
700
+ const source = createSource(config2.source);
701
+ const available = await provider.isAvailable();
702
+ if (!available) {
703
+ error(`Provider "${config2.provider}" is not installed or not in PATH.`);
704
+ process.exit(1);
705
+ }
706
+ log(
707
+ `Starting loop (provider: ${config2.provider}, source: ${config2.source}, label: ${config2.source_config.label})`
708
+ );
709
+ let session = 0;
710
+ while (true) {
711
+ session++;
712
+ if (opts.limit > 0 && session > opts.limit) {
713
+ ok(`Reached limit of ${opts.limit} issues. Stopping.`);
714
+ break;
715
+ }
716
+ const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
717
+ const logFile = resolve3(config2.logs.dir, `session_${session}_${timestamp2}.log`);
718
+ divider(session);
719
+ log(`Fetching next '${config2.source_config.label}' issue from ${config2.source}...`);
720
+ if (opts.dryRun) {
721
+ log(`[dry-run] Would fetch issue from ${config2.source} (${config2.source_config.team}/${config2.source_config.project})`);
722
+ log("[dry-run] Then implement, push, create PR, and update issue status");
723
+ break;
724
+ }
725
+ let issue;
726
+ try {
727
+ issue = await source.fetchNextIssue(config2.source_config);
728
+ } catch (err) {
729
+ error(`Failed to fetch issues: ${err instanceof Error ? err.message : String(err)}`);
730
+ if (opts.once) break;
731
+ await sleep(config2.loop.cooldown * 1e3);
732
+ continue;
733
+ }
734
+ if (!issue) {
735
+ warn(
736
+ `No issues with label '${config2.source_config.label}' found. Sleeping ${config2.loop.cooldown}s...`
737
+ );
738
+ if (opts.once) break;
739
+ await sleep(config2.loop.cooldown * 1e3);
740
+ continue;
741
+ }
742
+ ok(`Picked up: ${issue.id} \u2014 ${issue.title}`);
743
+ const prompt = buildImplementPrompt(issue, config2);
744
+ log(`Implementing... (log: ${logFile})`);
745
+ initLogFile(logFile);
746
+ const workspace = resolve3(config2.workspace);
747
+ const result = await provider.run(prompt, {
748
+ logFile,
749
+ cwd: workspace
750
+ });
751
+ try {
752
+ appendFileSync2(logFile, `
753
+ ${"=".repeat(80)}
754
+ Full output:
755
+ ${result.output}
756
+ `);
757
+ } catch {
758
+ }
759
+ if (!result.success) {
760
+ error(`Session ${session} failed for ${issue.id}. Check ${logFile}`);
761
+ if (opts.once) break;
762
+ await sleep(config2.loop.cooldown * 1e3);
763
+ continue;
764
+ }
765
+ try {
766
+ const repoInfo = await getRepoInfo(workspace);
767
+ const pr = await createPullRequest({
768
+ owner: repoInfo.owner,
769
+ repo: repoInfo.repo,
770
+ head: repoInfo.branch,
771
+ base: repoInfo.defaultBranch,
772
+ title: issue.title,
773
+ body: `Closes ${issue.url}
774
+
775
+ Implemented by lisa.`
776
+ }, config2.github);
777
+ ok(`PR created: ${pr.html_url}`);
778
+ } catch (err) {
779
+ error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
780
+ }
781
+ try {
782
+ await source.updateStatus(issue.id, "In Review");
783
+ ok(`Updated ${issue.id} status to "In Review"`);
784
+ } catch (err) {
785
+ error(`Failed to update status: ${err instanceof Error ? err.message : String(err)}`);
786
+ }
787
+ try {
788
+ await source.removeLabel(issue.id, config2.source_config.label);
789
+ ok(`Removed label "${config2.source_config.label}" from ${issue.id}`);
790
+ } catch (err) {
791
+ error(`Failed to remove label: ${err instanceof Error ? err.message : String(err)}`);
792
+ }
793
+ ok(
794
+ `Session ${session} complete for ${issue.id} (${formatDuration(result.duration)})`
795
+ );
796
+ if (opts.once) {
797
+ log("Single iteration mode. Exiting.");
798
+ break;
799
+ }
800
+ log(`Cooling down ${config2.loop.cooldown}s before next issue...`);
801
+ await sleep(config2.loop.cooldown * 1e3);
802
+ }
803
+ ok(`lisa finished. ${session} session(s) run.`);
804
+ }
805
+ function sleep(ms) {
806
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
807
+ }
808
+ function formatDuration(ms) {
809
+ const seconds = Math.floor(ms / 1e3);
810
+ const minutes = Math.floor(seconds / 60);
811
+ const remaining = seconds % 60;
812
+ if (minutes > 0) return `${minutes}m ${remaining}s`;
813
+ return `${seconds}s`;
814
+ }
815
+
816
+ // src/cli.ts
817
+ var run = defineCommand({
818
+ meta: { name: "run", description: "Run the agent loop" },
819
+ args: {
820
+ once: { type: "boolean", description: "Run a single iteration", default: false },
821
+ limit: { type: "string", description: "Max number of issues to process", default: "0" },
822
+ "dry-run": { type: "boolean", description: "Preview without executing", default: false },
823
+ provider: { type: "string", description: "AI provider (claude, gemini, opencode)" },
824
+ source: { type: "string", description: "Issue source (linear, trello)" },
825
+ label: { type: "string", description: "Label to filter issues" },
826
+ github: { type: "string", description: "GitHub method: cli or token" },
827
+ json: { type: "boolean", description: "Output as JSON lines", default: false },
828
+ quiet: { type: "boolean", description: "Suppress non-essential output", default: false }
829
+ },
830
+ async run({ args }) {
831
+ if (args.json) setOutputMode("json");
832
+ else if (args.quiet) setOutputMode("quiet");
833
+ banner();
834
+ const config2 = loadConfig();
835
+ const merged = mergeWithFlags(config2, {
836
+ provider: args.provider,
837
+ source: args.source,
838
+ github: args.github,
839
+ label: args.label
840
+ });
841
+ const missingVars = await getMissingEnvVars(merged.source);
842
+ if (missingVars.length > 0) {
843
+ const shell = process.env.SHELL?.includes("zsh") ? "~/.zshrc" : "~/.bashrc";
844
+ console.error(pc2.red(`Missing required environment variables:
845
+ ${missingVars.map((v) => ` ${v}`).join("\n")}`));
846
+ console.error(pc2.dim(`
847
+ Add them to your ${shell} and run: source ${shell}`));
848
+ process.exit(1);
849
+ }
850
+ await runLoop(merged, {
851
+ once: args.once,
852
+ limit: Number.parseInt(args.limit, 10),
853
+ dryRun: args["dry-run"]
854
+ });
855
+ }
856
+ });
857
+ var config = defineCommand({
858
+ meta: { name: "config", description: "Manage configuration" },
859
+ args: {
860
+ show: { type: "boolean", description: "Show current config", default: false },
861
+ set: { type: "string", description: "Set a config value (key=value)" }
862
+ },
863
+ async run({ args }) {
864
+ if (args.show) {
865
+ const cfg = loadConfig();
866
+ console.log(pc2.cyan("\nCurrent configuration:\n"));
867
+ console.log(JSON.stringify(cfg, null, 2));
868
+ return;
869
+ }
870
+ if (args.set) {
871
+ const [key, value] = args.set.split("=");
872
+ if (!key || !value) {
873
+ console.error(pc2.red("Usage: lisa config --set key=value"));
874
+ process.exit(1);
875
+ }
876
+ const cfg = loadConfig();
877
+ cfg[key] = value;
878
+ saveConfig(cfg);
879
+ log(`Set ${key} = ${value}`);
880
+ return;
881
+ }
882
+ await runConfigWizard();
883
+ }
884
+ });
885
+ var init = defineCommand({
886
+ meta: { name: "init", description: "Initialize lisa configuration" },
887
+ async run() {
888
+ if (!process.stdin.isTTY) {
889
+ console.error(pc2.red("Interactive mode requires a TTY. Cannot run init in non-interactive environments."));
890
+ process.exit(1);
891
+ }
892
+ if (configExists()) {
893
+ const overwrite = await clack.confirm({
894
+ message: "Config already exists. Overwrite?"
895
+ });
896
+ if (clack.isCancel(overwrite) || !overwrite) {
897
+ log("Cancelled.");
898
+ return;
899
+ }
900
+ }
901
+ await runConfigWizard();
902
+ }
903
+ });
904
+ var status = defineCommand({
905
+ meta: { name: "status", description: "Show session status and stats" },
906
+ async run() {
907
+ banner();
908
+ const config2 = loadConfig();
909
+ console.log(pc2.cyan("Configuration:"));
910
+ console.log(` Provider: ${pc2.bold(config2.provider)}`);
911
+ console.log(` Source: ${pc2.bold(config2.source)}`);
912
+ console.log(` Label: ${pc2.bold(config2.source_config.label)}`);
913
+ console.log(` Team: ${pc2.bold(config2.source_config.team)}`);
914
+ console.log(` Project: ${pc2.bold(config2.source_config.project)}`);
915
+ console.log(` Logs: ${pc2.dim(config2.logs.dir)}`);
916
+ const { readdirSync: readdirSync2, existsSync: existsSync4 } = await import("fs");
917
+ if (existsSync4(config2.logs.dir)) {
918
+ const logs = readdirSync2(config2.logs.dir).filter((f) => f.endsWith(".log"));
919
+ console.log(`
920
+ ${pc2.cyan("Sessions:")} ${logs.length} log file(s) found`);
921
+ } else {
922
+ console.log(`
923
+ ${pc2.dim("No sessions yet.")}`);
924
+ }
925
+ }
926
+ });
927
+ function getVersion() {
928
+ try {
929
+ const pkgPath = resolvePath(new URL(".", import.meta.url).pathname, "../package.json");
930
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
931
+ return pkg.version;
932
+ } catch {
933
+ return "0.0.0";
934
+ }
935
+ }
936
+ var main = defineCommand({
937
+ meta: {
938
+ name: "lisa",
939
+ version: getVersion(),
940
+ description: "Autonomous issue resolver \u2014 AI agent loop for Linear/Trello"
941
+ },
942
+ subCommands: { run, config, init, status }
943
+ });
944
+ var LISA_ART = pc2.yellow(`
945
+ @@@@@@@
946
+ @@@%+=*@@@@
947
+ @@@@+-----#@@@
948
+ @@@@*--------+@@@ @@@@@
949
+ @@@*-----------=%@@ @@@@@@@@@@@
950
+ @@@@@@@@@@@@@@@@@@#---------------*@@@@@@@@@%#*+=-=@@
951
+ @@@#########%%%%%*=-----------------=%%%#*+=--------@@@
952
+ @@*-------------------------------------------------#@@
953
+ @@*-------------------------------------------------+@@
954
+ @@*--------------------------------------------------@@@
955
+ @@*--------------------------------------------------%@@
956
+ @@*--------------------------------------------------*@@@
957
+ @@*--------------------------------------------------=%@@@@@@@@
958
+ @@@+------------------------------------------------------=+#%@@@@@@
959
+ @@@@@*------------------------------------------------------------=+*%@@@
960
+ @@@@%*=------------------------------------------------------------------=@@
961
+ @@@@#+----------------------------------------------------------------------%@@
962
+ @@%=------------------------------------------------------------------------*@@
963
+ @@@+--------------=%*------=#*-----*+------+@*-----------------------------=@@@
964
+ @@@#-------------+@#-----=@@=-----@@-----=@@=-----------------------------@@@
965
+ @@@=----##-----=@@-----%@+------%@@@@@@@@@#*+-----=--------------------%@@
966
+ @@@*---*@%++*#@@@@@@@@@@*---=*@@@#+======+*%@%#%@%#------------------+@@@
967
+ @@@*--=@@@#+=:::::::-+%@#*@@#=::::::::::::-*@@+--------------------#@@
968
+ @@@@*%@#::::::::::::::*@@#::::::::::::::::::%@*-------------------+@@@
969
+ @@@@%:::::::::::::::+@%:::::::::::::::::::-@@--------------------+@@@
970
+ @@@@@@+:::-**-::::::::%@=:::::::::-=:::::::::@@*+==-----------------=%@@@
971
+ @@@@@+:::%@@*::::::::@@-::::::::+@@%:::::::-@@#%%#-------------------*@@@
972
+ @@#::::==:::::::::#@+::::::::-%@*:::::::%@+-------------------------#@@@
973
+ @@+::::::::-====+#@@=::::::::::::::::-%@*-------------------------=@@@
974
+ @@@+::::+%@%%%%%%#*%@%=:::::::::::::+@@+-------------------------#@@@
975
+ @@%#*#@#----------+%@@*+=--::::-*@@@=-----------------------=*@@@
976
+ @@@#*#@@--------------+#%@@@@@@@@@*+-------==--------------=%@@@@
977
+ @@@%=---@@=--------==--------===----------=%@@@@%*-----------%@@
978
+ @@%=-----=#@%*****#%@%---------------------%@+---+@@=---------*@@
979
+ @@#----------+*#%%%#*=------------------------=**+-*@#----------@@@
980
+ @@----------------------------------%@=------+@@%%=*@#----------*@@
981
+ @@*--------------------------------=#@@=-----=*=---@@+----------=@@
982
+ @@@@*=----------------------==+*#%@@@@@*----##+-=*@@+-------=++**@@@
983
+ @@@@@%%#*++====+++**##%@@@@@%#*+=-=@@----=@@@@@@#=---=*%@@@@@@@@@
984
+ @@@@@@@@@@@@@@@%%##*++=--------+-----*@#--------*@@@@@
985
+ @@#------------------------#@*--------%@@
986
+ @@@@#---------------------%@@@%#*+==+@@
987
+ @@@---------------------%@@@@@@@@@@@@
988
+ @@%---------------------%@@
989
+ @@@@@@***+--------------*#@@@@@@
990
+ @@@*=*@@%%%@@#%%%#+-=*%%%%@#=-:=@@
991
+ @@-:-@@-:::+@@****@@@%***@@=:::-@@
992
+ @@#=%@+::::#@*::::@@@-:::+@@#*#@@@
993
+ @@@@@@%%%@@@%*=*%@@@%+=*@@@@@@@
994
+ @@@@@@@@@%@@@ @%%%%@@
995
+ `);
996
+ async function runConfigWizard() {
997
+ console.log(LISA_ART);
998
+ clack.intro(pc2.cyan("lisa \u2014 autonomous issue resolver"));
999
+ const providerLabels = {
1000
+ claude: "Claude Code",
1001
+ gemini: "Gemini CLI",
1002
+ opencode: "OpenCode"
1003
+ };
1004
+ const available = await getAvailableProviders();
1005
+ if (available.length === 0) {
1006
+ clack.log.error("No compatible AI providers found.");
1007
+ clack.log.info(
1008
+ `Install at least one of the following providers to continue:
1009
+
1010
+ ${pc2.bold("Claude Code")} ${pc2.dim("npm i -g @anthropic-ai/claude-code")}
1011
+ ${pc2.bold("Gemini CLI")} ${pc2.dim("npm i -g @anthropic-ai/gemini-cli")}
1012
+ ${pc2.bold("OpenCode")} ${pc2.dim("npm i -g opencode")}
1013
+
1014
+ After installing, run ${pc2.cyan("lisa init")} again.`
1015
+ );
1016
+ return process.exit(1);
1017
+ }
1018
+ let providerName;
1019
+ if (available.length === 1) {
1020
+ providerName = available[0].name;
1021
+ clack.log.info(`Found provider: ${pc2.bold(providerLabels[providerName])}`);
1022
+ } else {
1023
+ const selected = await clack.select({
1024
+ message: "Which AI provider do you want to use?",
1025
+ options: available.map((p) => ({
1026
+ value: p.name,
1027
+ label: providerLabels[p.name]
1028
+ }))
1029
+ });
1030
+ if (clack.isCancel(selected)) return process.exit(0);
1031
+ providerName = selected;
1032
+ }
1033
+ const source = await clack.select({
1034
+ message: "Where do your issues live?",
1035
+ options: [
1036
+ { value: "linear", label: "Linear" },
1037
+ { value: "trello", label: "Trello" }
1038
+ ]
1039
+ });
1040
+ if (clack.isCancel(source)) return process.exit(0);
1041
+ const missing = await getMissingEnvVars(source);
1042
+ if (missing.length > 0) {
1043
+ const shell = process.env.SHELL?.includes("zsh") ? "~/.zshrc" : "~/.bashrc";
1044
+ clack.log.warning(
1045
+ `Missing environment variables:
1046
+ ${missing.map((v) => ` ${pc2.bold(v)}`).join("\n")}
1047
+
1048
+ Add them to your environment variables:
1049
+ ${missing.map((v) => ` export ${v}="your-key-here"`).join("\n")}
1050
+
1051
+ Then run: ${pc2.cyan(`source ${shell}`)}`
1052
+ );
1053
+ }
1054
+ const teamAnswer = await clack.text({
1055
+ message: source === "linear" ? "Linear team name?" : "Trello board name?",
1056
+ initialValue: "Internal"
1057
+ });
1058
+ if (clack.isCancel(teamAnswer)) return process.exit(0);
1059
+ const team = teamAnswer;
1060
+ const projectAnswer = await clack.text({
1061
+ message: source === "linear" ? "Project name?" : "Trello list name?",
1062
+ initialValue: "Zenixx"
1063
+ });
1064
+ if (clack.isCancel(projectAnswer)) return process.exit(0);
1065
+ const project = projectAnswer;
1066
+ const labelAnswer = await clack.text({
1067
+ message: "Label to pick up?",
1068
+ initialValue: "ready"
1069
+ });
1070
+ if (clack.isCancel(labelAnswer)) return process.exit(0);
1071
+ const label = labelAnswer;
1072
+ const githubMethod = await detectGitHubMethod();
1073
+ const repos = await detectGitRepos();
1074
+ const cfg = {
1075
+ provider: providerName,
1076
+ source,
1077
+ source_config: {
1078
+ team,
1079
+ project,
1080
+ label,
1081
+ status: "Backlog"
1082
+ },
1083
+ github: githubMethod,
1084
+ workspace: ".",
1085
+ repos,
1086
+ loop: { cooldown: 10, max_sessions: 0 },
1087
+ logs: { dir: ".lisa/logs", format: "text" }
1088
+ };
1089
+ saveConfig(cfg);
1090
+ clack.outro(pc2.green("Config saved to .lisa/config.yaml"));
1091
+ }
1092
+ async function detectGitHubMethod() {
1093
+ const hasToken = !!process.env.GITHUB_TOKEN;
1094
+ const hasCli = await isGhCliAvailable();
1095
+ if (hasToken && hasCli) {
1096
+ const selected = await clack.select({
1097
+ message: "Both GitHub CLI and GITHUB_TOKEN detected. Which do you want to use?",
1098
+ options: [
1099
+ { value: "cli", label: "GitHub CLI", hint: "gh" },
1100
+ { value: "token", label: "GitHub API", hint: "GITHUB_TOKEN" }
1101
+ ]
1102
+ });
1103
+ if (clack.isCancel(selected)) return process.exit(0);
1104
+ return selected;
1105
+ }
1106
+ if (hasCli) {
1107
+ clack.log.info("Using GitHub CLI for pull requests.");
1108
+ return "cli";
1109
+ }
1110
+ if (hasToken) {
1111
+ clack.log.info("Using GITHUB_TOKEN for pull requests.");
1112
+ return "token";
1113
+ }
1114
+ return "token";
1115
+ }
1116
+ async function detectGitRepos() {
1117
+ const cwd = process.cwd();
1118
+ if (existsSync3(join(cwd, ".git"))) {
1119
+ clack.log.info(`Detected git repository in current directory.`);
1120
+ return [];
1121
+ }
1122
+ const entries = readdirSync(cwd, { withFileTypes: true });
1123
+ const gitDirs = entries.filter((e) => e.isDirectory() && existsSync3(join(cwd, e.name, ".git"))).map((e) => e.name);
1124
+ if (gitDirs.length === 0) {
1125
+ return [];
1126
+ }
1127
+ const selected = await clack.multiselect({
1128
+ message: "Select the repos to include in the workspace:",
1129
+ options: gitDirs.map((dir) => ({ value: dir, label: dir }))
1130
+ });
1131
+ if (clack.isCancel(selected)) return process.exit(0);
1132
+ return selected.map((dir) => ({
1133
+ name: dir,
1134
+ path: `./${dir}`,
1135
+ match: ""
1136
+ }));
1137
+ }
1138
+ async function getMissingEnvVars(source) {
1139
+ const missing = [];
1140
+ if (!process.env.GITHUB_TOKEN) {
1141
+ const ghAvailable = await isGhCliAvailable();
1142
+ if (!ghAvailable) missing.push("GITHUB_TOKEN");
1143
+ }
1144
+ if (source === "linear") {
1145
+ if (!process.env.LINEAR_API_KEY) missing.push("LINEAR_API_KEY");
1146
+ } else if (source === "trello") {
1147
+ if (!process.env.TRELLO_API_KEY) missing.push("TRELLO_API_KEY");
1148
+ if (!process.env.TRELLO_TOKEN) missing.push("TRELLO_TOKEN");
1149
+ }
1150
+ return missing;
1151
+ }
1152
+ function runCli() {
1153
+ runMain(main);
1154
+ }
1155
+
1156
+ // src/index.ts
1157
+ runCli();
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@tarcisiopgs/lisa",
3
+ "version": "0.5.0",
4
+ "description": "Autonomous issue resolver — AI agent loop for Linear/Trello",
5
+ "type": "module",
6
+ "bin": {
7
+ "lisa": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup",
11
+ "dev": "npx tsx src/index.ts",
12
+ "check": "biome check src/",
13
+ "format": "biome format --write src/",
14
+ "lint": "biome lint src/"
15
+ },
16
+ "dependencies": {
17
+ "@clack/prompts": "^1.0.1",
18
+ "citty": "^0.2.1",
19
+ "execa": "^9.6.1",
20
+ "picocolors": "^1.1.1",
21
+ "yaml": "^2.8.2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.13.4",
25
+ "tsup": "^8.4.0",
26
+ "tsx": "^4.19.3",
27
+ "typescript": "^5.9.3"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ]
35
+ }