cortex-agents 2.3.1 → 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.
- package/.opencode/agents/{plan.md → architect.md} +69 -15
- package/.opencode/agents/{fullstack.md → crosslayer.md} +2 -2
- package/.opencode/agents/{debug.md → fix.md} +11 -9
- package/.opencode/agents/{security.md → guard.md} +1 -1
- package/.opencode/agents/{build.md → implement.md} +121 -22
- package/.opencode/agents/{testing.md → qa.md} +1 -1
- package/.opencode/agents/{devops.md → ship.md} +1 -1
- package/README.md +107 -31
- package/dist/cli.js +87 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -7
- package/dist/registry.d.ts +8 -3
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +16 -2
- package/dist/tools/cortex.d.ts +2 -2
- package/dist/tools/cortex.js +7 -7
- package/dist/tools/environment.d.ts +31 -0
- package/dist/tools/environment.d.ts.map +1 -0
- package/dist/tools/environment.js +93 -0
- package/dist/tools/github.d.ts +42 -0
- package/dist/tools/github.d.ts.map +1 -0
- package/dist/tools/github.js +200 -0
- package/dist/tools/repl.d.ts +50 -0
- package/dist/tools/repl.d.ts.map +1 -0
- package/dist/tools/repl.js +240 -0
- package/dist/tools/task.d.ts +2 -0
- package/dist/tools/task.d.ts.map +1 -1
- package/dist/tools/task.js +25 -30
- package/dist/tools/worktree.d.ts.map +1 -1
- package/dist/tools/worktree.js +22 -11
- package/dist/utils/github.d.ts +104 -0
- package/dist/utils/github.d.ts.map +1 -0
- package/dist/utils/github.js +243 -0
- package/dist/utils/ide.d.ts +76 -0
- package/dist/utils/ide.d.ts.map +1 -0
- package/dist/utils/ide.js +307 -0
- package/dist/utils/plan-extract.d.ts +7 -0
- package/dist/utils/plan-extract.d.ts.map +1 -1
- package/dist/utils/plan-extract.js +25 -1
- package/dist/utils/repl.d.ts +114 -0
- package/dist/utils/repl.d.ts.map +1 -0
- package/dist/utils/repl.js +434 -0
- package/dist/utils/terminal.d.ts +53 -1
- package/dist/utils/terminal.d.ts.map +1 -1
- package/dist/utils/terminal.js +642 -5
- package/package.json +1 -1
- package/dist/plugin.d.ts +0 -1
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -4
- /package/.opencode/agents/{review.md → audit.md} +0 -0
|
@@ -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"}
|
|
@@ -0,0 +1,307 @@
|
|
|
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
|
+
import { shellEscape, which } from "./shell.js";
|
|
12
|
+
/**
|
|
13
|
+
* Detect the current IDE/editor environment.
|
|
14
|
+
* Checks environment variables, process hierarchy, and context clues.
|
|
15
|
+
*/
|
|
16
|
+
export function detectIDE() {
|
|
17
|
+
const env = process.env;
|
|
18
|
+
// VS Code detection
|
|
19
|
+
// VSCODE_PID is set when running in VS Code's integrated terminal
|
|
20
|
+
// VSCODE_CWD is set in some VS Code contexts
|
|
21
|
+
// TERM_PROGRAM=vscode is set by VS Code's terminal
|
|
22
|
+
if (env.VSCODE_PID || env.VSCODE_CWD || env.TERM_PROGRAM === "vscode") {
|
|
23
|
+
return {
|
|
24
|
+
type: "vscode",
|
|
25
|
+
name: "Visual Studio Code",
|
|
26
|
+
version: env.VSCODE_VERSION,
|
|
27
|
+
hasIntegratedTerminal: true,
|
|
28
|
+
canOpenInTerminal: true,
|
|
29
|
+
canOpenInWindow: true,
|
|
30
|
+
detectionSource: env.VSCODE_PID ? "VSCODE_PID" :
|
|
31
|
+
env.VSCODE_CWD ? "VSCODE_CWD" : "TERM_PROGRAM",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Cursor detection
|
|
35
|
+
// CURSOR_TRACE_ID is set by Cursor AI editor
|
|
36
|
+
// CURSOR_SHELL_VERSION indicates Cursor's shell integration
|
|
37
|
+
if (env.CURSOR_TRACE_ID || env.CURSOR_SHELL_VERSION) {
|
|
38
|
+
return {
|
|
39
|
+
type: "cursor",
|
|
40
|
+
name: "Cursor",
|
|
41
|
+
version: env.CURSOR_SHELL_VERSION,
|
|
42
|
+
hasIntegratedTerminal: true,
|
|
43
|
+
canOpenInTerminal: true,
|
|
44
|
+
canOpenInWindow: true,
|
|
45
|
+
detectionSource: env.CURSOR_TRACE_ID ? "CURSOR_TRACE_ID" : "CURSOR_SHELL_VERSION",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Windsurf detection
|
|
49
|
+
// WINDSURF_PARENT_PROCESS indicates Windsurf editor
|
|
50
|
+
// WINDSURF_EDITOR is set by Windsurf's terminal
|
|
51
|
+
if (env.WINDSURF_PARENT_PROCESS || env.WINDSURF_EDITOR) {
|
|
52
|
+
return {
|
|
53
|
+
type: "windsurf",
|
|
54
|
+
name: "Windsurf",
|
|
55
|
+
hasIntegratedTerminal: true,
|
|
56
|
+
canOpenInTerminal: true,
|
|
57
|
+
canOpenInWindow: true,
|
|
58
|
+
detectionSource: env.WINDSURF_PARENT_PROCESS ? "WINDSURF_PARENT_PROCESS" : "WINDSURF_EDITOR",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// JetBrains detection
|
|
62
|
+
// TERMINAL_EMULATOR contains "JetBrains" when in JB IDE terminal
|
|
63
|
+
// JETBRAINS_IDE is set by some JetBrains IDEs
|
|
64
|
+
if (env.TERMINAL_EMULATOR?.includes("JetBrains") || env.JETBRAINS_IDE) {
|
|
65
|
+
return {
|
|
66
|
+
type: "jetbrains",
|
|
67
|
+
name: env.JETBRAINS_IDE_NAME || "JetBrains IDE",
|
|
68
|
+
hasIntegratedTerminal: true,
|
|
69
|
+
canOpenInTerminal: true, // Can open in terminal, but no CLI for new window
|
|
70
|
+
canOpenInWindow: false, // JB IDEs don't have CLI window opening
|
|
71
|
+
detectionSource: env.JETBRAINS_IDE ? "JETBRAINS_IDE" : "TERMINAL_EMULATOR",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Zed detection
|
|
75
|
+
// ZED_TERM is set by Zed's integrated terminal
|
|
76
|
+
// TERM_PROGRAM=zed in some Zed configurations
|
|
77
|
+
if (env.ZED_TERM || env.TERM_PROGRAM === "zed") {
|
|
78
|
+
return {
|
|
79
|
+
type: "zed",
|
|
80
|
+
name: "Zed",
|
|
81
|
+
hasIntegratedTerminal: true,
|
|
82
|
+
canOpenInTerminal: true,
|
|
83
|
+
canOpenInWindow: true,
|
|
84
|
+
detectionSource: env.ZED_TERM ? "ZED_TERM" : "TERM_PROGRAM",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Terminal-only detection (fallback when no IDE detected)
|
|
88
|
+
// This means we're in a standalone terminal emulator
|
|
89
|
+
if (env.TERM_PROGRAM || env.TMUX || env.TERM) {
|
|
90
|
+
return {
|
|
91
|
+
type: "terminal",
|
|
92
|
+
name: env.TERM_PROGRAM || env.TERM || "Terminal",
|
|
93
|
+
hasIntegratedTerminal: false,
|
|
94
|
+
canOpenInTerminal: false,
|
|
95
|
+
canOpenInWindow: true,
|
|
96
|
+
detectionSource: env.TERM_PROGRAM ? "TERM_PROGRAM" :
|
|
97
|
+
env.TMUX ? "TMUX" : "TERM",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Unknown environment
|
|
101
|
+
return {
|
|
102
|
+
type: "unknown",
|
|
103
|
+
name: "Unknown",
|
|
104
|
+
hasIntegratedTerminal: false,
|
|
105
|
+
canOpenInTerminal: false,
|
|
106
|
+
canOpenInWindow: true,
|
|
107
|
+
detectionSource: "none",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get the command to open a worktree in the IDE's integrated terminal.
|
|
112
|
+
* Returns null if the IDE doesn't support CLI-based opening.
|
|
113
|
+
*
|
|
114
|
+
* Note: This function is currently unused but kept for future use.
|
|
115
|
+
* All inputs are escaped to prevent command injection.
|
|
116
|
+
*/
|
|
117
|
+
export function getIDEOpenCommand(ide, worktreePath) {
|
|
118
|
+
// Escape the path to prevent command injection
|
|
119
|
+
const safePath = shellEscape(worktreePath);
|
|
120
|
+
switch (ide.type) {
|
|
121
|
+
case "vscode":
|
|
122
|
+
// VS Code: use `code` CLI to open folder in new window
|
|
123
|
+
return `code --new-window "${safePath}"`;
|
|
124
|
+
case "cursor":
|
|
125
|
+
// Cursor: use `cursor` CLI
|
|
126
|
+
return `cursor --new-window "${safePath}"`;
|
|
127
|
+
case "windsurf":
|
|
128
|
+
// Windsurf: use `windsurf` CLI
|
|
129
|
+
return `windsurf "${safePath}"`;
|
|
130
|
+
case "zed":
|
|
131
|
+
// Zed: use `zed` CLI
|
|
132
|
+
return `zed "${safePath}"`;
|
|
133
|
+
case "jetbrains":
|
|
134
|
+
// JetBrains: requires manual opening or platform-specific CLI
|
|
135
|
+
// Common CLIs: idea, webstorm, pycharm, etc.
|
|
136
|
+
return null;
|
|
137
|
+
default:
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get the name of the CLI binary for the detected IDE.
|
|
143
|
+
* Returns null if no CLI is available or known.
|
|
144
|
+
*/
|
|
145
|
+
export function getIDECliBinary(ide) {
|
|
146
|
+
switch (ide.type) {
|
|
147
|
+
case "vscode":
|
|
148
|
+
return "code";
|
|
149
|
+
case "cursor":
|
|
150
|
+
return "cursor";
|
|
151
|
+
case "windsurf":
|
|
152
|
+
return "windsurf";
|
|
153
|
+
case "zed":
|
|
154
|
+
return "zed";
|
|
155
|
+
default:
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get a human-readable hint for installing an IDE's CLI binary.
|
|
161
|
+
* Each IDE has a different installation method.
|
|
162
|
+
*/
|
|
163
|
+
export function getInstallHint(type, binary) {
|
|
164
|
+
switch (type) {
|
|
165
|
+
case "vscode":
|
|
166
|
+
return "Cmd+Shift+P → 'Shell Command: Install code command in PATH'";
|
|
167
|
+
case "cursor":
|
|
168
|
+
return "Cmd+Shift+P → 'Shell Command: Install cursor command in PATH'";
|
|
169
|
+
case "windsurf":
|
|
170
|
+
return "Ensure Windsurf is installed and 'windsurf' is in PATH";
|
|
171
|
+
case "zed":
|
|
172
|
+
return "Ensure Zed is installed and 'zed' is in PATH";
|
|
173
|
+
default:
|
|
174
|
+
return `Ensure '${binary}' is in PATH`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Async version of detectIDE() that also checks whether the IDE's CLI binary
|
|
179
|
+
* is actually available in PATH. Use this in tools where you need to verify
|
|
180
|
+
* runtime availability before offering IDE launch options.
|
|
181
|
+
*
|
|
182
|
+
* The sync detectIDE() is preserved for non-async contexts.
|
|
183
|
+
*/
|
|
184
|
+
export async function detectIDEWithCLICheck() {
|
|
185
|
+
const ide = detectIDE();
|
|
186
|
+
const cliBinary = getIDECliBinary(ide);
|
|
187
|
+
if (cliBinary) {
|
|
188
|
+
const available = await which(cliBinary);
|
|
189
|
+
ide.cliAvailable = !!available;
|
|
190
|
+
ide.cliBinary = cliBinary;
|
|
191
|
+
if (!available) {
|
|
192
|
+
ide.cliInstallHint = getInstallHint(ide.type, cliBinary);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
ide.cliAvailable = false;
|
|
197
|
+
}
|
|
198
|
+
return ide;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Check if we're already inside an IDE terminal.
|
|
202
|
+
* This helps determine if we should offer "stay here" as primary option.
|
|
203
|
+
*/
|
|
204
|
+
export function isInIDETerminal() {
|
|
205
|
+
const ide = detectIDE();
|
|
206
|
+
return ide.hasIntegratedTerminal && !!process.env.TERM;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Generate contextual recommendations based on detected environment.
|
|
210
|
+
* Used by agents to offer appropriate launch options to users.
|
|
211
|
+
*/
|
|
212
|
+
export function generateEnvironmentRecommendations(ide) {
|
|
213
|
+
const recommendations = [];
|
|
214
|
+
if (ide.hasIntegratedTerminal && ide.canOpenInTerminal && ide.cliAvailable !== false) {
|
|
215
|
+
// Only offer IDE option when CLI is confirmed available or hasn't been checked yet
|
|
216
|
+
recommendations.push({
|
|
217
|
+
option: `Open in ${ide.name} (Recommended)`,
|
|
218
|
+
priority: "high",
|
|
219
|
+
reason: `${ide.name} integrated terminal maintains context and is familiar`,
|
|
220
|
+
mode: "ide",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else if (ide.hasIntegratedTerminal && ide.cliAvailable === false) {
|
|
224
|
+
// IDE detected but CLI missing — note it but don't recommend
|
|
225
|
+
recommendations.push({
|
|
226
|
+
option: `Open in ${ide.name} (CLI not installed)`,
|
|
227
|
+
priority: "low",
|
|
228
|
+
reason: `${ide.cliBinary || "CLI"} not in PATH. ${ide.cliInstallHint || "Install the CLI to enable this option."}`,
|
|
229
|
+
mode: "ide",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
recommendations.push({
|
|
233
|
+
option: "Open in new terminal tab",
|
|
234
|
+
priority: ide.cliAvailable === false ? "high" : "medium",
|
|
235
|
+
reason: "Open in your current terminal emulator as a new tab",
|
|
236
|
+
mode: "terminal",
|
|
237
|
+
});
|
|
238
|
+
recommendations.push({
|
|
239
|
+
option: "Open in-app PTY",
|
|
240
|
+
priority: "medium",
|
|
241
|
+
reason: "Embedded terminal within this OpenCode session",
|
|
242
|
+
mode: "pty",
|
|
243
|
+
});
|
|
244
|
+
recommendations.push({
|
|
245
|
+
option: "Run in background",
|
|
246
|
+
priority: "low",
|
|
247
|
+
reason: "AI implements headlessly while you keep working here",
|
|
248
|
+
mode: "background",
|
|
249
|
+
});
|
|
250
|
+
recommendations.push({
|
|
251
|
+
option: "Stay in current session",
|
|
252
|
+
priority: "low",
|
|
253
|
+
reason: "Continue working in this terminal session",
|
|
254
|
+
mode: "stay",
|
|
255
|
+
});
|
|
256
|
+
return recommendations;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Format environment detection as a human-readable report.
|
|
260
|
+
* Used by the detect_environment tool.
|
|
261
|
+
*/
|
|
262
|
+
export function formatEnvironmentReport(ide, terminalName) {
|
|
263
|
+
const lines = [
|
|
264
|
+
`## Environment Detection`,
|
|
265
|
+
``,
|
|
266
|
+
`**IDE/Editor:** ${ide.name}`,
|
|
267
|
+
`**Detection Method:** ${ide.detectionSource}`,
|
|
268
|
+
`**Terminal:** ${terminalName}`,
|
|
269
|
+
`**Platform:** ${process.platform}`,
|
|
270
|
+
``,
|
|
271
|
+
];
|
|
272
|
+
if (ide.version) {
|
|
273
|
+
lines.push(`**Version:** ${ide.version}`, ``);
|
|
274
|
+
}
|
|
275
|
+
lines.push(`### Capabilities`, ``);
|
|
276
|
+
if (ide.hasIntegratedTerminal) {
|
|
277
|
+
lines.push(`- ✓ Has integrated terminal`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
lines.push(`- ✗ No integrated terminal`);
|
|
281
|
+
}
|
|
282
|
+
if (ide.canOpenInWindow) {
|
|
283
|
+
if (ide.cliAvailable === false) {
|
|
284
|
+
lines.push(`- ⚠ Can open new window via CLI — but \`${ide.cliBinary || "cli"}\` NOT found in PATH`);
|
|
285
|
+
if (ide.cliInstallHint) {
|
|
286
|
+
lines.push(` Fix: ${ide.cliInstallHint}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else if (ide.cliAvailable === true) {
|
|
290
|
+
lines.push(`- ✓ Can open new window via CLI (\`${ide.cliBinary}\` available)`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
lines.push(`- ✓ Can open new window via CLI`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
lines.push(`- ✗ Cannot open new window via CLI`);
|
|
298
|
+
}
|
|
299
|
+
lines.push(``, `### Recommended Launch Options`, ``);
|
|
300
|
+
const recommendations = generateEnvironmentRecommendations(ide);
|
|
301
|
+
for (const rec of recommendations) {
|
|
302
|
+
const priorityBadge = rec.priority === "high" ? " (Recommended)" :
|
|
303
|
+
rec.priority === "medium" ? "" : "";
|
|
304
|
+
lines.push(`- **${rec.option}${priorityBadge}** — ${rec.reason}`);
|
|
305
|
+
}
|
|
306
|
+
return lines.join("\n");
|
|
307
|
+
}
|
|
@@ -13,6 +13,13 @@ export interface PlanSections {
|
|
|
13
13
|
/** The raw plan filename */
|
|
14
14
|
filename: string;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Extract GitHub issue references from plan frontmatter.
|
|
18
|
+
*
|
|
19
|
+
* Looks for `issues: [42, 51]` in YAML frontmatter.
|
|
20
|
+
* Returns an array of issue numbers, or an empty array if none found.
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractIssueRefs(planContent: string): number[];
|
|
16
23
|
/**
|
|
17
24
|
* Extract relevant sections from a plan markdown file for composing a PR body.
|
|
18
25
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-extract.d.ts","sourceRoot":"","sources":["../../src/utils/plan-extract.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAuCvF;AAmCD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAwBlE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"plan-extract.d.ts","sourceRoot":"","sources":["../../src/utils/plan-extract.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAY9D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAuCvF;AAmCD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAwBlE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,EACrB,UAAU,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA0C9C"}
|