cortex-agents 2.3.0 → 3.4.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 (54) hide show
  1. package/.opencode/agents/{plan.md → architect.md} +104 -45
  2. package/.opencode/agents/audit.md +314 -0
  3. package/.opencode/agents/crosslayer.md +218 -0
  4. package/.opencode/agents/{debug.md → fix.md} +75 -46
  5. package/.opencode/agents/guard.md +202 -0
  6. package/.opencode/agents/{build.md → implement.md} +151 -107
  7. package/.opencode/agents/qa.md +265 -0
  8. package/.opencode/agents/ship.md +249 -0
  9. package/README.md +119 -31
  10. package/dist/cli.js +87 -16
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +215 -9
  13. package/dist/registry.d.ts +8 -3
  14. package/dist/registry.d.ts.map +1 -1
  15. package/dist/registry.js +16 -2
  16. package/dist/tools/cortex.d.ts +2 -2
  17. package/dist/tools/cortex.js +7 -7
  18. package/dist/tools/environment.d.ts +31 -0
  19. package/dist/tools/environment.d.ts.map +1 -0
  20. package/dist/tools/environment.js +93 -0
  21. package/dist/tools/github.d.ts +42 -0
  22. package/dist/tools/github.d.ts.map +1 -0
  23. package/dist/tools/github.js +200 -0
  24. package/dist/tools/repl.d.ts +50 -0
  25. package/dist/tools/repl.d.ts.map +1 -0
  26. package/dist/tools/repl.js +240 -0
  27. package/dist/tools/task.d.ts +2 -0
  28. package/dist/tools/task.d.ts.map +1 -1
  29. package/dist/tools/task.js +25 -30
  30. package/dist/tools/worktree.d.ts.map +1 -1
  31. package/dist/tools/worktree.js +22 -11
  32. package/dist/utils/github.d.ts +104 -0
  33. package/dist/utils/github.d.ts.map +1 -0
  34. package/dist/utils/github.js +243 -0
  35. package/dist/utils/ide.d.ts +76 -0
  36. package/dist/utils/ide.d.ts.map +1 -0
  37. package/dist/utils/ide.js +307 -0
  38. package/dist/utils/plan-extract.d.ts +7 -0
  39. package/dist/utils/plan-extract.d.ts.map +1 -1
  40. package/dist/utils/plan-extract.js +25 -1
  41. package/dist/utils/repl.d.ts +114 -0
  42. package/dist/utils/repl.d.ts.map +1 -0
  43. package/dist/utils/repl.js +434 -0
  44. package/dist/utils/terminal.d.ts +53 -1
  45. package/dist/utils/terminal.d.ts.map +1 -1
  46. package/dist/utils/terminal.js +642 -5
  47. package/package.json +1 -1
  48. package/.opencode/agents/devops.md +0 -176
  49. package/.opencode/agents/fullstack.md +0 -171
  50. package/.opencode/agents/security.md +0 -148
  51. package/.opencode/agents/testing.md +0 -132
  52. package/dist/plugin.d.ts +0 -1
  53. package/dist/plugin.d.ts.map +0 -1
  54. package/dist/plugin.js +0 -4
@@ -2,35 +2,12 @@ import { tool } from "@opencode-ai/plugin";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import { detectWorktreeInfo } from "../utils/worktree-detect.js";
5
- import { findPlanContent, extractPlanSections, buildPrBodyFromPlan, } from "../utils/plan-extract.js";
6
- import { git, gh, which } from "../utils/shell.js";
5
+ import { findPlanContent, extractPlanSections, extractIssueRefs, buildPrBodyFromPlan, } from "../utils/plan-extract.js";
6
+ import { git, gh } from "../utils/shell.js";
7
+ import { checkGhAvailability } from "../utils/github.js";
7
8
  const PROTECTED_BRANCHES = ["main", "master", "develop", "production", "staging"];
8
9
  const DOCS_DIR = "docs";
9
10
  // ─── Helpers ─────────────────────────────────────────────────────────────────
10
- /**
11
- * Check if `gh` CLI is installed and authenticated.
12
- */
13
- async function checkGhCli(cwd) {
14
- // Check if gh exists
15
- const ghPath = await which("gh");
16
- if (!ghPath) {
17
- return {
18
- ok: false,
19
- error: "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/ and run `gh auth login`.",
20
- };
21
- }
22
- // Check if authenticated
23
- try {
24
- await gh(cwd, "auth", "status");
25
- }
26
- catch {
27
- return {
28
- ok: false,
29
- error: "GitHub CLI is not authenticated. Run `gh auth login` to authenticate.",
30
- };
31
- }
32
- return { ok: true };
33
- }
34
11
  /**
35
12
  * Check if a remote named "origin" is configured.
36
13
  */
@@ -117,9 +94,13 @@ export const finalize = tool({
117
94
  .boolean()
118
95
  .optional()
119
96
  .describe("Create as draft PR (default: false)"),
97
+ issueRefs: tool.schema
98
+ .array(tool.schema.number())
99
+ .optional()
100
+ .describe("GitHub issue numbers to link in PR body (adds 'Closes #N' for each)"),
120
101
  },
121
102
  async execute(args, context) {
122
- const { commitMessage, prTitle, prBody: customPrBody, baseBranch: customBaseBranch, planFilename, draft = false, } = args;
103
+ const { commitMessage, prTitle, prBody: customPrBody, baseBranch: customBaseBranch, planFilename, draft = false, issueRefs: explicitIssueRefs, } = args;
123
104
  const cwd = context.worktree;
124
105
  const output = [];
125
106
  const warnings = [];
@@ -162,9 +143,12 @@ Create a feature/bugfix branch first with branch_create or worktree_create.`;
162
143
  output.push(`Worktree detected (main tree: ${wtInfo.mainWorktreePath})`);
163
144
  }
164
145
  // ── 4. Check prerequisites ────────────────────────────────
165
- const ghCheck = await checkGhCli(cwd);
166
- if (!ghCheck.ok) {
167
- return `✗ ${ghCheck.error}`;
146
+ const ghStatus = await checkGhAvailability(cwd);
147
+ if (!ghStatus.installed) {
148
+ return "✗ GitHub CLI (gh) is not installed. Install it from https://cli.github.com/ and run `gh auth login`.";
149
+ }
150
+ if (!ghStatus.authenticated) {
151
+ return "✗ GitHub CLI is not authenticated. Run `gh auth login` to authenticate.";
168
152
  }
169
153
  const remoteCheck = await checkRemote(cwd);
170
154
  if (!remoteCheck.ok) {
@@ -218,6 +202,7 @@ All previous steps succeeded (changes committed). Try pushing manually:
218
202
  }
219
203
  // ── 9. Build PR body ──────────────────────────────────────
220
204
  let prBodyContent = customPrBody || "";
205
+ let issueRefs = explicitIssueRefs ?? [];
221
206
  if (!prBodyContent) {
222
207
  // Try to build from plan
223
208
  const plan = findPlanContent(cwd, planFilename, branchName);
@@ -225,6 +210,10 @@ All previous steps succeeded (changes committed). Try pushing manually:
225
210
  const sections = extractPlanSections(plan.content, plan.filename);
226
211
  prBodyContent = buildPrBodyFromPlan(sections);
227
212
  output.push(`PR body generated from plan: ${plan.filename}`);
213
+ // Extract issue refs from plan frontmatter if not explicitly provided
214
+ if (issueRefs.length === 0) {
215
+ issueRefs = extractIssueRefs(plan.content);
216
+ }
228
217
  }
229
218
  else {
230
219
  // Fall back to commit log
@@ -237,6 +226,12 @@ All previous steps succeeded (changes committed). Try pushing manually:
237
226
  }
238
227
  }
239
228
  }
229
+ // Append issue closing references to PR body
230
+ if (issueRefs.length > 0) {
231
+ const closingRefs = issueRefs.map((n) => `Closes #${n}`).join("\n");
232
+ prBodyContent += `\n\n## Linked Issues\n\n${closingRefs}`;
233
+ output.push(`Linked issues: ${issueRefs.map((n) => `#${n}`).join(", ")}`);
234
+ }
240
235
  // ── 10. Create PR via gh ──────────────────────────────────
241
236
  const finalPrTitle = prTitle || commitMessage;
242
237
  let prUrl = "";
@@ -1 +1 @@
1
- {"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../src/tools/worktree.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAiBvD,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AACpC,KAAK,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;AAE9B;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;EAuF1C;AAED,eAAO,MAAM,IAAI;;;;CAiCf,CAAC;AAEH;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;EAkI1C;AAED,eAAO,MAAM,IAAI;;;;;;;;CAgDf,CAAC;AAkUH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;;;;;;;;;EAsIxD"}
1
+ {"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../src/tools/worktree.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAiBvD,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AACpC,KAAK,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;AAE9B;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;EAuF1C;AAED,eAAO,MAAM,IAAI;;;;CAiCf,CAAC;AAEH;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;EAkI1C;AAED,eAAO,MAAM,IAAI;;;;;;;;CAgDf,CAAC;AA8UH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;;;;;;;;;EAsIxD"}
@@ -2,8 +2,8 @@ import { tool } from "@opencode-ai/plugin";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import { propagatePlan } from "../utils/propagate.js";
5
- import { git, which, kill, spawn as shellSpawn } from "../utils/shell.js";
6
- import { detectDriver, closeSession, writeSession, readSession, } from "../utils/terminal.js";
5
+ import { git, which, shellEscape, kill, spawn as shellSpawn } from "../utils/shell.js";
6
+ import { detectTerminalDriver, closeSession, writeSession, readSession, } from "../utils/terminal.js";
7
7
  const WORKTREE_ROOT = ".worktrees";
8
8
  /**
9
9
  * Factory function that creates the worktree_create tool with access
@@ -171,8 +171,8 @@ Use worktree_list to see existing worktrees.`;
171
171
  // PTY may already be closed
172
172
  }
173
173
  }
174
- else if (session.mode === "terminal") {
175
- // Close terminal tab via driver
174
+ else if (session.mode === "terminal" || session.mode === "ide") {
175
+ // Close terminal/IDE tab via driver
176
176
  closedSession = await closeSession(session);
177
177
  }
178
178
  else if (session.mode === "background" && session.pid) {
@@ -351,7 +351,11 @@ async function launchTerminalTab(client, worktreePath, branchName, opencodeBin,
351
351
  // Toast failure is non-fatal
352
352
  }
353
353
  };
354
- const driver = detectDriver();
354
+ // KEY FIX: Use multi-strategy terminal detection that NEVER returns IDE drivers.
355
+ // This ensures "Open in terminal tab" always opens in the user's actual terminal
356
+ // emulator (iTerm2, Ghostty, kitty, etc.), not in a new IDE window.
357
+ const detection = await detectTerminalDriver(worktreePath);
358
+ const driver = detection.driver;
355
359
  const opts = {
356
360
  worktreePath,
357
361
  opencodeBin,
@@ -373,13 +377,20 @@ async function launchTerminalTab(client, worktreePath, branchName, opencodeBin,
373
377
  ...result,
374
378
  };
375
379
  writeSession(worktreePath, session);
376
- await notify(`Opened ${driver.name} tab with agent '${agent}'`);
377
- return `✓ Opened ${driver.name} tab in worktree\n\nBranch: ${branchName}\nTerminal: ${driver.name}`;
380
+ const strategyNote = detection.strategy !== "env"
381
+ ? ` (detected via ${detection.strategy})`
382
+ : "";
383
+ await notify(`Opened ${driver.name} tab with agent '${agent}'${strategyNote}`);
384
+ return `✓ Opened ${driver.name} tab in worktree\n\nBranch: ${branchName}\nTerminal: ${driver.name}\nDetection: ${detection.strategy}${detection.detail ? ` — ${detection.detail}` : ""}`;
378
385
  }
379
386
  catch (error) {
380
387
  await notify(`Could not open terminal: ${error.message || error}`, "error");
381
- const innerCmd = `cd "${worktreePath}" && "${opencodeBin}" --agent ${agent}`;
382
- return `✗ Could not open terminal (${driver.name}). Manual command:\n ${innerCmd}`;
388
+ // Escape all user-controlled inputs in the manual command to prevent injection
389
+ const safePath = shellEscape(worktreePath);
390
+ const safeBin = shellEscape(opencodeBin);
391
+ const safeAgent = shellEscape(agent);
392
+ const innerCmd = `cd "${safePath}" && "${safeBin}" --agent "${safeAgent}"`;
393
+ return `✗ Could not open terminal (${driver.name}, detected via ${detection.strategy}). Manual command:\n ${innerCmd}`;
383
394
  }
384
395
  }
385
396
  // ─── Mode B: In-App PTY ─────────────────────────────────────────────────────
@@ -597,14 +608,14 @@ export function createLaunch(client, shell) {
597
608
  agent: tool.schema
598
609
  .string()
599
610
  .optional()
600
- .describe("Agent to use in the new session (default: 'build')"),
611
+ .describe("Agent to use in the new session (default: 'implement')"),
601
612
  prompt: tool.schema
602
613
  .string()
603
614
  .optional()
604
615
  .describe("Custom prompt for the new session (auto-generated from plan if omitted)"),
605
616
  },
606
617
  async execute(args, context) {
607
- const { name, mode, plan: planFilename, agent = "build", prompt: customPrompt, } = args;
618
+ const { name, mode, plan: planFilename, agent = "implement", prompt: customPrompt, } = args;
608
619
  const worktreePath = path.join(context.worktree, WORKTREE_ROOT, name);
609
620
  const absoluteWorktreePath = path.resolve(worktreePath);
610
621
  // ── Validate worktree exists ───────────────────────────────
@@ -0,0 +1,104 @@
1
+ export interface GhStatus {
2
+ installed: boolean;
3
+ authenticated: boolean;
4
+ hasRemote: boolean;
5
+ repoOwner?: string;
6
+ repoName?: string;
7
+ projects: {
8
+ id: string;
9
+ number: number;
10
+ title: string;
11
+ }[];
12
+ }
13
+ export interface GitHubIssue {
14
+ number: number;
15
+ title: string;
16
+ state: string;
17
+ labels: string[];
18
+ assignees: string[];
19
+ milestone?: string;
20
+ body: string;
21
+ url: string;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ }
25
+ export interface GitHubProjectItem {
26
+ id: string;
27
+ title: string;
28
+ type: "ISSUE" | "PULL_REQUEST" | "DRAFT_ISSUE";
29
+ status?: string;
30
+ assignees: string[];
31
+ labels: string[];
32
+ issueNumber?: number;
33
+ url?: string;
34
+ body?: string;
35
+ }
36
+ /**
37
+ * Check full GitHub CLI availability and repo context.
38
+ * Returns a status object with installation, authentication, and repo info.
39
+ */
40
+ export declare function checkGhAvailability(cwd: string): Promise<GhStatus>;
41
+ /**
42
+ * Parse a git remote URL to extract owner and repo name.
43
+ * Handles HTTPS, SSH, and GitHub CLI formats.
44
+ * Supports both github.com and GitHub Enterprise Server URLs (e.g., github.mycompany.com).
45
+ *
46
+ * The regex requires "github" to appear at a hostname boundary (after `//` or `@`)
47
+ * to prevent false positives like "notgithub.com" or "fakegithub.evil.com".
48
+ */
49
+ export declare function parseRepoUrl(url: string): {
50
+ owner: string;
51
+ name: string;
52
+ } | null;
53
+ /**
54
+ * Truncate a string to the given length, appending "..." if truncated.
55
+ */
56
+ export declare function truncate(str: string, maxLen: number): string;
57
+ /**
58
+ * Format a single GitHub issue into a compact list entry.
59
+ * Used when presenting multiple issues for selection.
60
+ */
61
+ export declare function formatIssueListEntry(issue: GitHubIssue): string;
62
+ /**
63
+ * Format multiple issues into a numbered selection list.
64
+ */
65
+ export declare function formatIssueList(issues: GitHubIssue[]): string;
66
+ /**
67
+ * Format a GitHub issue into a planning-friendly markdown block.
68
+ * Used by the architect agent to seed plan content from selected issues.
69
+ */
70
+ export declare function formatIssueForPlan(issue: GitHubIssue): string;
71
+ /**
72
+ * Format a single GitHub Project item into a compact list entry.
73
+ */
74
+ export declare function formatProjectItemEntry(item: GitHubProjectItem): string;
75
+ /**
76
+ * Format multiple project items into a list.
77
+ */
78
+ export declare function formatProjectItemList(items: GitHubProjectItem[]): string;
79
+ /**
80
+ * Fetch GitHub projects associated with the repo owner.
81
+ */
82
+ export declare function fetchProjects(cwd: string, owner: string): Promise<{
83
+ id: string;
84
+ number: number;
85
+ title: string;
86
+ }[]>;
87
+ /**
88
+ * Fetch issues from the current repository using gh CLI.
89
+ */
90
+ export declare function fetchIssues(cwd: string, options?: {
91
+ state?: string;
92
+ labels?: string;
93
+ milestone?: string;
94
+ assignee?: string;
95
+ limit?: number;
96
+ }): Promise<GitHubIssue[]>;
97
+ /**
98
+ * Fetch project items from a specific GitHub Project.
99
+ */
100
+ export declare function fetchProjectItems(cwd: string, owner: string, projectNumber: number, options?: {
101
+ status?: string;
102
+ limit?: number;
103
+ }): Promise<GitHubProjectItem[]>;
104
+ //# sourceMappingURL=github.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../src/utils/github.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,243 @@
1
+ import { gh, which, git } from "./shell.js";
2
+ // ─── Constants ───────────────────────────────────────────────────────────────
3
+ /** Maximum character length for issue body in list views to avoid overwhelming context. */
4
+ const BODY_TRUNCATE_LENGTH = 200;
5
+ /** Maximum character length for issue body in detail views (plan formatting). */
6
+ const BODY_DETAIL_LENGTH = 2000;
7
+ // ─── GitHub CLI Availability ─────────────────────────────────────────────────
8
+ /**
9
+ * Check full GitHub CLI availability and repo context.
10
+ * Returns a status object with installation, authentication, and repo info.
11
+ */
12
+ export async function checkGhAvailability(cwd) {
13
+ const status = {
14
+ installed: false,
15
+ authenticated: false,
16
+ hasRemote: false,
17
+ projects: [],
18
+ };
19
+ // 1. Check if gh is installed
20
+ const ghPath = await which("gh");
21
+ if (!ghPath)
22
+ return status;
23
+ status.installed = true;
24
+ // 2. Check if authenticated
25
+ try {
26
+ await gh(cwd, "auth", "status");
27
+ status.authenticated = true;
28
+ }
29
+ catch {
30
+ return status;
31
+ }
32
+ // 3. Extract repo owner/name from remote
33
+ try {
34
+ const { stdout } = await git(cwd, "remote", "get-url", "origin");
35
+ const parsed = parseRepoUrl(stdout.trim());
36
+ if (parsed) {
37
+ status.hasRemote = true;
38
+ status.repoOwner = parsed.owner;
39
+ status.repoName = parsed.name;
40
+ }
41
+ }
42
+ catch {
43
+ // No remote configured — hasRemote stays false
44
+ }
45
+ return status;
46
+ }
47
+ /**
48
+ * Parse a git remote URL to extract owner and repo name.
49
+ * Handles HTTPS, SSH, and GitHub CLI formats.
50
+ * Supports both github.com and GitHub Enterprise Server URLs (e.g., github.mycompany.com).
51
+ *
52
+ * The regex requires "github" to appear at a hostname boundary (after `//` or `@`)
53
+ * to prevent false positives like "notgithub.com" or "fakegithub.evil.com".
54
+ */
55
+ export function parseRepoUrl(url) {
56
+ // HTTPS: https://github.com/owner/repo.git OR https://github.mycompany.com/owner/repo.git
57
+ // The `//` anchor ensures "github" is at the start of the hostname, not mid-word.
58
+ const httpsMatch = url.match(/\/\/github[^/]*\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
59
+ if (httpsMatch) {
60
+ return { owner: httpsMatch[1], name: httpsMatch[2] };
61
+ }
62
+ // SSH: git@github.com:owner/repo.git OR git@github.mycompany.com:owner/repo.git
63
+ // The `git@` prefix already anchors to the hostname start.
64
+ const sshMatch = url.match(/git@github[^:]*:([^/]+)\/([^/.]+?)(?:\.git)?$/);
65
+ if (sshMatch) {
66
+ return { owner: sshMatch[1], name: sshMatch[2] };
67
+ }
68
+ return null;
69
+ }
70
+ // ─── Issue Formatting ────────────────────────────────────────────────────────
71
+ /**
72
+ * Truncate a string to the given length, appending "..." if truncated.
73
+ */
74
+ export function truncate(str, maxLen) {
75
+ if (!str)
76
+ return "";
77
+ if (str.length <= maxLen)
78
+ return str;
79
+ return str.substring(0, maxLen).trimEnd() + "...";
80
+ }
81
+ /**
82
+ * Format a single GitHub issue into a compact list entry.
83
+ * Used when presenting multiple issues for selection.
84
+ */
85
+ export function formatIssueListEntry(issue) {
86
+ const labels = issue.labels.length > 0 ? ` [${issue.labels.join(", ")}]` : "";
87
+ const assignees = issue.assignees.length > 0 ? ` → ${issue.assignees.join(", ")}` : "";
88
+ const body = issue.body ? `\n ${truncate(issue.body.replace(/\n/g, " "), BODY_TRUNCATE_LENGTH)}` : "";
89
+ return `#${issue.number}: ${issue.title}${labels}${assignees}${body}`;
90
+ }
91
+ /**
92
+ * Format multiple issues into a numbered selection list.
93
+ */
94
+ export function formatIssueList(issues) {
95
+ if (issues.length === 0)
96
+ return "No issues found.";
97
+ return issues.map((issue) => formatIssueListEntry(issue)).join("\n\n");
98
+ }
99
+ /**
100
+ * Format a GitHub issue into a planning-friendly markdown block.
101
+ * Used by the architect agent to seed plan content from selected issues.
102
+ */
103
+ export function formatIssueForPlan(issue) {
104
+ const parts = [];
105
+ parts.push(`### Issue #${issue.number}: ${issue.title}`);
106
+ parts.push("");
107
+ if (issue.labels.length > 0) {
108
+ parts.push(`**Labels:** ${issue.labels.join(", ")}`);
109
+ }
110
+ if (issue.assignees.length > 0) {
111
+ parts.push(`**Assignees:** ${issue.assignees.join(", ")}`);
112
+ }
113
+ if (issue.milestone) {
114
+ parts.push(`**Milestone:** ${issue.milestone}`);
115
+ }
116
+ parts.push(`**URL:** ${issue.url}`);
117
+ parts.push("");
118
+ if (issue.body) {
119
+ parts.push("**Description:**");
120
+ parts.push("");
121
+ parts.push(truncate(issue.body, BODY_DETAIL_LENGTH));
122
+ }
123
+ return parts.join("\n");
124
+ }
125
+ // ─── Project Item Formatting ─────────────────────────────────────────────────
126
+ /**
127
+ * Format a single GitHub Project item into a compact list entry.
128
+ */
129
+ export function formatProjectItemEntry(item) {
130
+ const status = item.status ? ` (${item.status})` : "";
131
+ const type = item.type === "DRAFT_ISSUE" ? " [Draft]" : item.type === "PULL_REQUEST" ? " [PR]" : "";
132
+ const labels = item.labels.length > 0 ? ` [${item.labels.join(", ")}]` : "";
133
+ const assignees = item.assignees.length > 0 ? ` → ${item.assignees.join(", ")}` : "";
134
+ const ref = item.issueNumber ? `#${item.issueNumber}: ` : "";
135
+ return `${ref}${item.title}${type}${status}${labels}${assignees}`;
136
+ }
137
+ /**
138
+ * Format multiple project items into a list.
139
+ */
140
+ export function formatProjectItemList(items) {
141
+ if (items.length === 0)
142
+ return "No project items found.";
143
+ return items.map((item) => formatProjectItemEntry(item)).join("\n");
144
+ }
145
+ // ─── GitHub CLI Data Fetching ────────────────────────────────────────────────
146
+ /**
147
+ * Fetch GitHub projects associated with the repo owner.
148
+ */
149
+ export async function fetchProjects(cwd, owner) {
150
+ try {
151
+ const { stdout } = await gh(cwd, "project", "list", "--owner", owner, "--format", "json");
152
+ const parsed = JSON.parse(stdout);
153
+ // gh project list --format json returns { projects: [...] }
154
+ const projects = parsed.projects ?? parsed ?? [];
155
+ return projects.map((p) => ({
156
+ id: String(p.id ?? ""),
157
+ number: Number(p.number ?? 0),
158
+ title: String(p.title ?? "Untitled"),
159
+ }));
160
+ }
161
+ catch (error) {
162
+ // Surface auth errors — these are actionable for the user.
163
+ // Other errors (no projects, API format changes) are non-critical.
164
+ const msg = error?.message ?? String(error);
165
+ if (msg.includes("auth") || msg.includes("401") || msg.includes("403")) {
166
+ throw new Error(`GitHub authentication error while fetching projects: ${msg}`);
167
+ }
168
+ return [];
169
+ }
170
+ }
171
+ /**
172
+ * Fetch issues from the current repository using gh CLI.
173
+ */
174
+ export async function fetchIssues(cwd, options = {}) {
175
+ const args = [
176
+ "issue", "list",
177
+ "--json", "number,title,state,labels,assignees,milestone,body,url,createdAt,updatedAt",
178
+ "--limit", String(options.limit ?? 20),
179
+ ];
180
+ if (options.state)
181
+ args.push("--state", options.state);
182
+ if (options.labels)
183
+ args.push("--label", options.labels);
184
+ if (options.milestone)
185
+ args.push("--milestone", options.milestone);
186
+ if (options.assignee)
187
+ args.push("--assignee", options.assignee);
188
+ const { stdout } = await gh(cwd, ...args);
189
+ const raw = JSON.parse(stdout);
190
+ return raw.map((item) => ({
191
+ number: item.number,
192
+ title: item.title ?? "",
193
+ state: item.state ?? "open",
194
+ labels: (item.labels ?? []).map((l) => (typeof l === "string" ? l : l.name ?? "")),
195
+ assignees: (item.assignees ?? []).map((a) => (typeof a === "string" ? a : a.login ?? "")),
196
+ milestone: item.milestone?.title ?? undefined,
197
+ body: item.body ?? "",
198
+ url: item.url ?? "",
199
+ createdAt: item.createdAt ?? "",
200
+ updatedAt: item.updatedAt ?? "",
201
+ }));
202
+ }
203
+ /**
204
+ * Fetch project items from a specific GitHub Project.
205
+ */
206
+ export async function fetchProjectItems(cwd, owner, projectNumber, options = {}) {
207
+ const { stdout } = await gh(cwd, "project", "item-list", String(projectNumber), "--owner", owner, "--format", "json", "--limit", String(options.limit ?? 30));
208
+ const parsed = JSON.parse(stdout);
209
+ // gh project item-list --format json returns { items: [...] }
210
+ const raw = parsed.items ?? parsed ?? [];
211
+ const items = raw.map((item) => ({
212
+ id: String(item.id ?? ""),
213
+ title: item.title ?? "",
214
+ type: normalizeItemType(item.type),
215
+ status: item.status ?? undefined,
216
+ assignees: Array.isArray(item.assignees)
217
+ ? item.assignees.map((a) => (typeof a === "string" ? a : a.login ?? ""))
218
+ : [],
219
+ labels: Array.isArray(item.labels)
220
+ ? item.labels.map((l) => (typeof l === "string" ? l : l.name ?? ""))
221
+ : [],
222
+ issueNumber: item.content?.number ?? undefined,
223
+ url: item.content?.url ?? undefined,
224
+ body: item.content?.body ?? undefined,
225
+ }));
226
+ // Apply status filter client-side if provided
227
+ if (options.status) {
228
+ const filterStatus = options.status.toLowerCase();
229
+ return items.filter((item) => item.status?.toLowerCase().includes(filterStatus));
230
+ }
231
+ return items;
232
+ }
233
+ /**
234
+ * Normalize the item type string from GitHub's API into our union type.
235
+ */
236
+ function normalizeItemType(type) {
237
+ const t = String(type ?? "").toUpperCase();
238
+ if (t.includes("PULL") || t === "PULL_REQUEST")
239
+ return "PULL_REQUEST";
240
+ if (t.includes("DRAFT") || t === "DRAFT_ISSUE")
241
+ return "DRAFT_ISSUE";
242
+ return "ISSUE";
243
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * IDE Detection System
3
+ *
4
+ * Detects the current Integrated Development Environment or editor context.
5
+ * This is used to offer appropriate worktree launch options that match
6
+ * the user's current workflow.
7
+ *
8
+ * Detection is based on environment variables set by various IDEs when
9
+ * running terminal sessions within them.
10
+ */
11
+ export type IDEType = "vscode" | "cursor" | "windsurf" | "jetbrains" | "zed" | "terminal" | "unknown";
12
+ export interface IDEDetection {
13
+ type: IDEType;
14
+ name: string;
15
+ version?: string;
16
+ hasIntegratedTerminal: boolean;
17
+ canOpenInTerminal: boolean;
18
+ canOpenInWindow: boolean;
19
+ cliAvailable?: boolean;
20
+ cliBinary?: string;
21
+ cliInstallHint?: string;
22
+ detectionSource: string;
23
+ }
24
+ export interface EnvironmentRecommendation {
25
+ option: string;
26
+ priority: "high" | "medium" | "low";
27
+ reason: string;
28
+ mode?: "ide" | "terminal" | "pty" | "background" | "stay";
29
+ }
30
+ /**
31
+ * Detect the current IDE/editor environment.
32
+ * Checks environment variables, process hierarchy, and context clues.
33
+ */
34
+ export declare function detectIDE(): IDEDetection;
35
+ /**
36
+ * Get the command to open a worktree in the IDE's integrated terminal.
37
+ * Returns null if the IDE doesn't support CLI-based opening.
38
+ *
39
+ * Note: This function is currently unused but kept for future use.
40
+ * All inputs are escaped to prevent command injection.
41
+ */
42
+ export declare function getIDEOpenCommand(ide: IDEDetection, worktreePath: string): string | null;
43
+ /**
44
+ * Get the name of the CLI binary for the detected IDE.
45
+ * Returns null if no CLI is available or known.
46
+ */
47
+ export declare function getIDECliBinary(ide: IDEDetection): string | null;
48
+ /**
49
+ * Get a human-readable hint for installing an IDE's CLI binary.
50
+ * Each IDE has a different installation method.
51
+ */
52
+ export declare function getInstallHint(type: IDEType, binary: string): string;
53
+ /**
54
+ * Async version of detectIDE() that also checks whether the IDE's CLI binary
55
+ * is actually available in PATH. Use this in tools where you need to verify
56
+ * runtime availability before offering IDE launch options.
57
+ *
58
+ * The sync detectIDE() is preserved for non-async contexts.
59
+ */
60
+ export declare function detectIDEWithCLICheck(): Promise<IDEDetection>;
61
+ /**
62
+ * Check if we're already inside an IDE terminal.
63
+ * This helps determine if we should offer "stay here" as primary option.
64
+ */
65
+ export declare function isInIDETerminal(): boolean;
66
+ /**
67
+ * Generate contextual recommendations based on detected environment.
68
+ * Used by agents to offer appropriate launch options to users.
69
+ */
70
+ export declare function generateEnvironmentRecommendations(ide: IDEDetection): EnvironmentRecommendation[];
71
+ /**
72
+ * Format environment detection as a human-readable report.
73
+ * Used by the detect_environment tool.
74
+ */
75
+ export declare function formatEnvironmentReport(ide: IDEDetection, terminalName: string): string;
76
+ //# sourceMappingURL=ide.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ide.d.ts","sourceRoot":"","sources":["../../src/utils/ide.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,MAAM,OAAO,GACf,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,WAAW,GACX,KAAK,GACL,UAAU,GACV,SAAS,CAAC;AAEd,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,KAAK,GAAG,UAAU,GAAG,KAAK,GAAG,YAAY,GAAG,MAAM,CAAC;CAC3D;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,YAAY,CAoGxC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6BxF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,CAahE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAapE;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,YAAY,CAAC,CAgBnE;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAGzC;AAED;;;GAGG;AACH,wBAAgB,kCAAkC,CAAC,GAAG,EAAE,YAAY,GAAG,yBAAyB,EAAE,CAkDjG;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAgDvF"}