cortex-agents 2.3.1 → 4.0.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} +104 -58
- package/.opencode/agents/audit.md +183 -0
- package/.opencode/agents/{fullstack.md → coder.md} +10 -54
- package/.opencode/agents/debug.md +76 -201
- package/.opencode/agents/devops.md +16 -123
- package/.opencode/agents/docs-writer.md +195 -0
- package/.opencode/agents/fix.md +207 -0
- package/.opencode/agents/implement.md +433 -0
- package/.opencode/agents/perf.md +151 -0
- package/.opencode/agents/refactor.md +163 -0
- package/.opencode/agents/security.md +20 -85
- package/.opencode/agents/testing.md +1 -151
- package/.opencode/skills/data-engineering/SKILL.md +221 -0
- package/.opencode/skills/monitoring-observability/SKILL.md +251 -0
- package/README.md +315 -224
- package/dist/cli.js +85 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -22
- package/dist/registry.d.ts +8 -3
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +16 -2
- package/dist/tools/branch.d.ts +2 -2
- package/dist/tools/cortex.d.ts +2 -2
- package/dist/tools/cortex.js +7 -7
- package/dist/tools/docs.d.ts +2 -2
- 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/plan.d.ts +28 -4
- package/dist/tools/plan.d.ts.map +1 -1
- package/dist/tools/plan.js +232 -4
- package/dist/tools/quality-gate.d.ts +28 -0
- package/dist/tools/quality-gate.d.ts.map +1 -0
- package/dist/tools/quality-gate.js +233 -0
- package/dist/tools/repl.d.ts +55 -0
- package/dist/tools/repl.d.ts.map +1 -0
- package/dist/tools/repl.js +291 -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 +5 -32
- package/dist/tools/worktree.d.ts.map +1 -1
- package/dist/tools/worktree.js +75 -447
- package/dist/utils/change-scope.d.ts +33 -0
- package/dist/utils/change-scope.d.ts.map +1 -0
- package/dist/utils/change-scope.js +198 -0
- 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 +28 -0
- package/dist/utils/plan-extract.d.ts.map +1 -1
- package/dist/utils/plan-extract.js +90 -1
- package/dist/utils/repl.d.ts +145 -0
- package/dist/utils/repl.d.ts.map +1 -0
- package/dist/utils/repl.js +547 -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/.opencode/agents/build.md +0 -294
- package/.opencode/agents/review.md +0 -314
- package/dist/plugin.d.ts +0 -1
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -4
package/dist/utils/terminal.js
CHANGED
|
@@ -36,7 +36,11 @@ export function readSession(worktreePath) {
|
|
|
36
36
|
}
|
|
37
37
|
// ─── Helper: build the shell command for the new tab ─────────────────────────
|
|
38
38
|
function buildTabCommand(opts) {
|
|
39
|
-
|
|
39
|
+
// Escape all user-controlled inputs to prevent command injection
|
|
40
|
+
const safePath = shellEscape(opts.worktreePath);
|
|
41
|
+
const safeBin = shellEscape(opts.opencodeBin);
|
|
42
|
+
const safeAgent = shellEscape(opts.agent);
|
|
43
|
+
return `cd "${safePath}" && "${safeBin}" --agent "${safeAgent}"`;
|
|
40
44
|
}
|
|
41
45
|
// ─── Helper: safe process kill ───────────────────────────────────────────────
|
|
42
46
|
function killPid(pid) {
|
|
@@ -51,7 +55,165 @@ function killPid(pid) {
|
|
|
51
55
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
52
56
|
// Driver Implementations
|
|
53
57
|
// ═════════════════════════════════════════════════════════════════════════════
|
|
54
|
-
// ───
|
|
58
|
+
// ─── IDE Drivers (highest priority — prefer integrated terminals) ────────────
|
|
59
|
+
/**
|
|
60
|
+
* Base class for IDE drivers that use CLI commands to open windows.
|
|
61
|
+
* IDEs don't support programmatic closing, so closeTab returns false.
|
|
62
|
+
*/
|
|
63
|
+
class IDEDriver {
|
|
64
|
+
async openTab(opts) {
|
|
65
|
+
const cliAvailable = await which(this.cliBinary);
|
|
66
|
+
if (!cliAvailable) {
|
|
67
|
+
throw new Error(`${this.name} CLI not found. Install the \`${this.cliBinary}\` command.`);
|
|
68
|
+
}
|
|
69
|
+
// Open the worktree in a new IDE window
|
|
70
|
+
// The integrated terminal will automatically be available
|
|
71
|
+
try {
|
|
72
|
+
await exec(this.cliBinary, ["--new-window", opts.worktreePath]);
|
|
73
|
+
return {
|
|
74
|
+
sessionId: `${this.name}-${opts.branchName}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Some IDEs don't support --new-window, try without
|
|
79
|
+
try {
|
|
80
|
+
await exec(this.cliBinary, [opts.worktreePath]);
|
|
81
|
+
return {
|
|
82
|
+
sessionId: `${this.name}-${opts.branchName}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new Error(`Failed to open ${this.name} window`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async closeTab(session) {
|
|
91
|
+
// IDEs don't support programmatic tab/window closing via CLI
|
|
92
|
+
// Return false to indicate we couldn't close it
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// ─── VS Code ─────────────────────────────────────────────────────────────────
|
|
97
|
+
class VSCodeDriver extends IDEDriver {
|
|
98
|
+
name = "vscode";
|
|
99
|
+
cliBinary = "code";
|
|
100
|
+
envVars = ["VSCODE_PID", "VSCODE_CWD"];
|
|
101
|
+
detect() {
|
|
102
|
+
return !!process.env.VSCODE_PID ||
|
|
103
|
+
!!process.env.VSCODE_CWD ||
|
|
104
|
+
process.env.TERM_PROGRAM === "vscode";
|
|
105
|
+
}
|
|
106
|
+
async openTab(opts) {
|
|
107
|
+
const cliAvailable = await which(this.cliBinary);
|
|
108
|
+
if (!cliAvailable) {
|
|
109
|
+
throw new Error("VS Code CLI not found. Install the `code` command via VS Code's Command Palette (Cmd+Shift+P → 'Shell Command: Install code command in PATH')");
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
// Open in new window with the worktree folder
|
|
113
|
+
await exec(this.cliBinary, ["--new-window", opts.worktreePath]);
|
|
114
|
+
return {
|
|
115
|
+
sessionId: `vscode-${opts.branchName}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new Error("Failed to open VS Code window");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ─── Cursor ──────────────────────────────────────────────────────────────────
|
|
124
|
+
class CursorDriver extends IDEDriver {
|
|
125
|
+
name = "cursor";
|
|
126
|
+
cliBinary = "cursor";
|
|
127
|
+
envVars = ["CURSOR_TRACE_ID", "CURSOR_SHELL_VERSION"];
|
|
128
|
+
detect() {
|
|
129
|
+
return !!process.env.CURSOR_TRACE_ID ||
|
|
130
|
+
!!process.env.CURSOR_SHELL_VERSION;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// ─── Windsurf ────────────────────────────────────────────────────────────────
|
|
134
|
+
class WindsurfDriver extends IDEDriver {
|
|
135
|
+
name = "windsurf";
|
|
136
|
+
cliBinary = "windsurf";
|
|
137
|
+
envVars = ["WINDSURF_PARENT_PROCESS", "WINDSURF_EDITOR"];
|
|
138
|
+
detect() {
|
|
139
|
+
return !!process.env.WINDSURF_PARENT_PROCESS ||
|
|
140
|
+
!!process.env.WINDSURF_EDITOR;
|
|
141
|
+
}
|
|
142
|
+
async openTab(opts) {
|
|
143
|
+
const cliAvailable = await which(this.cliBinary);
|
|
144
|
+
if (!cliAvailable) {
|
|
145
|
+
throw new Error("Windsurf CLI not found. Ensure Windsurf is installed and the `windsurf` command is in PATH.");
|
|
146
|
+
}
|
|
147
|
+
// Windsurf may not support --new-window, try direct path
|
|
148
|
+
try {
|
|
149
|
+
await exec(this.cliBinary, [opts.worktreePath]);
|
|
150
|
+
return {
|
|
151
|
+
sessionId: `windsurf-${opts.branchName}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
throw new Error("Failed to open Windsurf window");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ─── Zed ─────────────────────────────────────────────────────────────────────
|
|
160
|
+
class ZedDriver extends IDEDriver {
|
|
161
|
+
name = "zed";
|
|
162
|
+
cliBinary = "zed";
|
|
163
|
+
envVars = ["ZED_TERM"];
|
|
164
|
+
detect() {
|
|
165
|
+
return !!process.env.ZED_TERM || process.env.TERM_PROGRAM === "zed";
|
|
166
|
+
}
|
|
167
|
+
async openTab(opts) {
|
|
168
|
+
const cliAvailable = await which(this.cliBinary);
|
|
169
|
+
if (!cliAvailable) {
|
|
170
|
+
throw new Error("Zed CLI not found. Ensure Zed is installed and the `zed` command is in PATH.");
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
await exec(this.cliBinary, [opts.worktreePath]);
|
|
174
|
+
return {
|
|
175
|
+
sessionId: `zed-${opts.branchName}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
throw new Error("Failed to open Zed window");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// ─── JetBrains IDEs ──────────────────────────────────────────────────────────
|
|
184
|
+
class JetBrainsDriver {
|
|
185
|
+
name = "jetbrains";
|
|
186
|
+
detect() {
|
|
187
|
+
const env = process.env.TERMINAL_EMULATOR || "";
|
|
188
|
+
return env.includes("JetBrains") || !!process.env.JETBRAINS_IDE;
|
|
189
|
+
}
|
|
190
|
+
async openTab(opts) {
|
|
191
|
+
// JetBrains IDEs don't have a universal CLI for opening folders
|
|
192
|
+
// We'll try common JetBrains CLI tools
|
|
193
|
+
const jetbrainsClis = ["idea", "webstorm", "pycharm", "goland", "clion", "rustrover"];
|
|
194
|
+
for (const cli of jetbrainsClis) {
|
|
195
|
+
const cliAvailable = await which(cli);
|
|
196
|
+
if (cliAvailable) {
|
|
197
|
+
try {
|
|
198
|
+
await exec(cli, [opts.worktreePath]);
|
|
199
|
+
return {
|
|
200
|
+
sessionId: `jetbrains-${opts.branchName}`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw new Error("JetBrains IDE CLI not found. Open the worktree manually in your JetBrains IDE:\n" +
|
|
209
|
+
` File → Open → ${opts.worktreePath}`);
|
|
210
|
+
}
|
|
211
|
+
async closeTab(session) {
|
|
212
|
+
// JetBrains IDEs don't support programmatic closing
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// ─── tmux (multiplexer — highest priority among terminals) ───────────────────
|
|
55
217
|
class TmuxDriver {
|
|
56
218
|
name = "tmux";
|
|
57
219
|
detect() {
|
|
@@ -494,6 +656,159 @@ class GnomeTerminalDriver {
|
|
|
494
656
|
return false;
|
|
495
657
|
}
|
|
496
658
|
}
|
|
659
|
+
// ─── Ghostty (macOS/Linux) ───────────────────────────────────────────────────
|
|
660
|
+
class GhosttyDriver {
|
|
661
|
+
name = "ghostty";
|
|
662
|
+
detect() {
|
|
663
|
+
return !!process.env.GHOSTTY_RESOURCES_DIR ||
|
|
664
|
+
process.env.TERM_PROGRAM === "ghostty";
|
|
665
|
+
}
|
|
666
|
+
async openTab(opts) {
|
|
667
|
+
const safePath = shellEscape(opts.worktreePath);
|
|
668
|
+
const safeBin = shellEscape(opts.opencodeBin);
|
|
669
|
+
const safeAgent = shellEscape(opts.agent);
|
|
670
|
+
// macOS: use AppleScript to create a new tab in the running Ghostty instance
|
|
671
|
+
if (process.platform === "darwin") {
|
|
672
|
+
const script = `tell application "Ghostty"
|
|
673
|
+
activate
|
|
674
|
+
end tell
|
|
675
|
+
tell application "System Events"
|
|
676
|
+
tell process "Ghostty"
|
|
677
|
+
keystroke "t" using command down
|
|
678
|
+
delay 0.3
|
|
679
|
+
keystroke "cd \\"${safePath}\\" && \\"${safeBin}\\" --agent ${safeAgent}"
|
|
680
|
+
key code 36
|
|
681
|
+
end tell
|
|
682
|
+
end tell`;
|
|
683
|
+
try {
|
|
684
|
+
await exec("osascript", ["-e", script]);
|
|
685
|
+
return {};
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
// Fall through to spawn
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Linux / fallback: spawn a new ghostty window
|
|
692
|
+
const cmd = buildTabCommand(opts);
|
|
693
|
+
const ghosttyBin = await which("ghostty");
|
|
694
|
+
if (ghosttyBin) {
|
|
695
|
+
const child = spawn("ghostty", [
|
|
696
|
+
"-e",
|
|
697
|
+
"bash",
|
|
698
|
+
"-c",
|
|
699
|
+
cmd,
|
|
700
|
+
], { cwd: opts.worktreePath });
|
|
701
|
+
return { pid: child.pid ?? undefined };
|
|
702
|
+
}
|
|
703
|
+
throw new Error("Failed to open Ghostty tab");
|
|
704
|
+
}
|
|
705
|
+
async closeTab(session) {
|
|
706
|
+
if (session.pid)
|
|
707
|
+
return killPid(session.pid);
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// ─── Alacritty (Linux/macOS) ─────────────────────────────────────────────────
|
|
712
|
+
class AlacrittyDriver {
|
|
713
|
+
name = "alacritty";
|
|
714
|
+
detect() {
|
|
715
|
+
return !!process.env.ALACRITTY_WINDOW_ID ||
|
|
716
|
+
!!process.env.ALACRITTY_LOG ||
|
|
717
|
+
process.env.TERM_PROGRAM === "alacritty";
|
|
718
|
+
}
|
|
719
|
+
async openTab(opts) {
|
|
720
|
+
const cmd = buildTabCommand(opts);
|
|
721
|
+
// Alacritty doesn't support tabs natively — open a new window
|
|
722
|
+
const child = spawn("alacritty", [
|
|
723
|
+
"--working-directory",
|
|
724
|
+
opts.worktreePath,
|
|
725
|
+
"--title",
|
|
726
|
+
`Worktree: ${opts.branchName}`,
|
|
727
|
+
"-e",
|
|
728
|
+
"bash",
|
|
729
|
+
"-c",
|
|
730
|
+
cmd,
|
|
731
|
+
], { cwd: opts.worktreePath });
|
|
732
|
+
return { pid: child.pid ?? undefined };
|
|
733
|
+
}
|
|
734
|
+
async closeTab(session) {
|
|
735
|
+
if (session.pid)
|
|
736
|
+
return killPid(session.pid);
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
// ─── Hyper (Electron-based) ──────────────────────────────────────────────────
|
|
741
|
+
class HyperDriver {
|
|
742
|
+
name = "hyper";
|
|
743
|
+
detect() {
|
|
744
|
+
return process.env.TERM_PROGRAM === "Hyper";
|
|
745
|
+
}
|
|
746
|
+
async openTab(opts) {
|
|
747
|
+
const safePath = shellEscape(opts.worktreePath);
|
|
748
|
+
const safeBin = shellEscape(opts.opencodeBin);
|
|
749
|
+
const safeAgent = shellEscape(opts.agent);
|
|
750
|
+
// macOS: use AppleScript to open a new tab in Hyper
|
|
751
|
+
if (process.platform === "darwin") {
|
|
752
|
+
const script = `tell application "Hyper"
|
|
753
|
+
activate
|
|
754
|
+
end tell
|
|
755
|
+
tell application "System Events"
|
|
756
|
+
tell process "Hyper"
|
|
757
|
+
keystroke "t" using command down
|
|
758
|
+
delay 0.3
|
|
759
|
+
keystroke "cd \\"${safePath}\\" && \\"${safeBin}\\" --agent ${safeAgent}"
|
|
760
|
+
key code 36
|
|
761
|
+
end tell
|
|
762
|
+
end tell`;
|
|
763
|
+
try {
|
|
764
|
+
await exec("osascript", ["-e", script]);
|
|
765
|
+
return {};
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
// Fall through to spawn
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// Fallback: spawn via hyper CLI
|
|
772
|
+
const hyperBin = await which("hyper");
|
|
773
|
+
if (hyperBin) {
|
|
774
|
+
const child = spawn("hyper", [opts.worktreePath], { cwd: opts.worktreePath });
|
|
775
|
+
return { pid: child.pid ?? undefined };
|
|
776
|
+
}
|
|
777
|
+
throw new Error("Failed to open Hyper tab");
|
|
778
|
+
}
|
|
779
|
+
async closeTab(session) {
|
|
780
|
+
if (session.pid)
|
|
781
|
+
return killPid(session.pid);
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// ─── Rio (Rust-based terminal) ───────────────────────────────────────────────
|
|
786
|
+
class RioDriver {
|
|
787
|
+
name = "rio";
|
|
788
|
+
detect() {
|
|
789
|
+
return process.env.TERM_PROGRAM === "rio";
|
|
790
|
+
}
|
|
791
|
+
async openTab(opts) {
|
|
792
|
+
const cmd = buildTabCommand(opts);
|
|
793
|
+
// Rio supports tabs but has no IPC — spawn a new window
|
|
794
|
+
const rioBin = await which("rio");
|
|
795
|
+
if (rioBin) {
|
|
796
|
+
const child = spawn("rio", [
|
|
797
|
+
"-e",
|
|
798
|
+
"bash",
|
|
799
|
+
"-c",
|
|
800
|
+
cmd,
|
|
801
|
+
], { cwd: opts.worktreePath });
|
|
802
|
+
return { pid: child.pid ?? undefined };
|
|
803
|
+
}
|
|
804
|
+
throw new Error("Failed to open Rio terminal");
|
|
805
|
+
}
|
|
806
|
+
async closeTab(session) {
|
|
807
|
+
if (session.pid)
|
|
808
|
+
return killPid(session.pid);
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
497
812
|
// ─── Fallback (PID-based, always matches) ────────────────────────────────────
|
|
498
813
|
class FallbackDriver {
|
|
499
814
|
name = "fallback";
|
|
@@ -571,17 +886,32 @@ class FallbackDriver {
|
|
|
571
886
|
/**
|
|
572
887
|
* Ordered list of terminal drivers. Detection runs first-to-last.
|
|
573
888
|
*
|
|
574
|
-
* Priority:
|
|
575
|
-
*
|
|
889
|
+
* Priority: IDEs first (vscode, cursor, windsurf), then multiplexers (tmux),
|
|
890
|
+
* then terminal emulators, then fallback.
|
|
891
|
+
* This ensures that if the user is in VS Code's terminal, we offer to open
|
|
892
|
+
* in VS Code rather than a standalone terminal.
|
|
576
893
|
*/
|
|
577
894
|
const DRIVERS = [
|
|
895
|
+
// IDE drivers - highest priority (prefer integrated terminals)
|
|
896
|
+
new VSCodeDriver(),
|
|
897
|
+
new CursorDriver(),
|
|
898
|
+
new WindsurfDriver(),
|
|
899
|
+
new ZedDriver(),
|
|
900
|
+
new JetBrainsDriver(),
|
|
901
|
+
// Multiplexer
|
|
578
902
|
new TmuxDriver(),
|
|
903
|
+
// Terminal emulators
|
|
579
904
|
new ITerm2Driver(),
|
|
580
905
|
new TerminalAppDriver(),
|
|
581
906
|
new KittyDriver(),
|
|
582
907
|
new WeztermDriver(),
|
|
908
|
+
new GhosttyDriver(),
|
|
909
|
+
new AlacrittyDriver(),
|
|
910
|
+
new HyperDriver(),
|
|
911
|
+
new RioDriver(),
|
|
583
912
|
new KonsoleDriver(),
|
|
584
913
|
new GnomeTerminalDriver(),
|
|
914
|
+
// Fallback
|
|
585
915
|
new FallbackDriver(),
|
|
586
916
|
];
|
|
587
917
|
/** Map of driver name → driver instance for reverse lookup. */
|
|
@@ -612,7 +942,7 @@ export function getDriverByName(name) {
|
|
|
612
942
|
* This is the main entry point for worktree_remove cleanup.
|
|
613
943
|
*/
|
|
614
944
|
export async function closeSession(session) {
|
|
615
|
-
if (session.mode === "terminal") {
|
|
945
|
+
if (session.mode === "terminal" || session.mode === "ide") {
|
|
616
946
|
const driver = getDriverByName(session.terminal);
|
|
617
947
|
if (driver) {
|
|
618
948
|
const closed = await driver.closeTab(session);
|
|
@@ -625,3 +955,310 @@ export async function closeSession(session) {
|
|
|
625
955
|
return killPid(session.pid);
|
|
626
956
|
return false;
|
|
627
957
|
}
|
|
958
|
+
// ─── IDE Detection Helpers ───────────────────────────────────────────────────
|
|
959
|
+
/**
|
|
960
|
+
* Check if a given driver is an IDE driver (VS Code, Cursor, Windsurf, Zed, JetBrains).
|
|
961
|
+
* Used to determine whether to attempt a fallback when the IDE CLI is unavailable.
|
|
962
|
+
*/
|
|
963
|
+
export function isIDEDriver(driver) {
|
|
964
|
+
return driver instanceof IDEDriver || driver instanceof JetBrainsDriver;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Detect the first non-IDE terminal driver that matches the environment.
|
|
968
|
+
* Used as a fallback when the IDE CLI is not available (e.g., `code` not in PATH).
|
|
969
|
+
*
|
|
970
|
+
* Skips IDE drivers and JetBrains, tries tmux, iTerm2, Terminal.app, kitty, etc.
|
|
971
|
+
* Returns null if no terminal driver matches (only fallback driver left).
|
|
972
|
+
*/
|
|
973
|
+
export function detectFallbackDriver() {
|
|
974
|
+
for (const driver of DRIVERS) {
|
|
975
|
+
// Skip IDE drivers — we want a real terminal emulator
|
|
976
|
+
if (driver instanceof IDEDriver || driver instanceof JetBrainsDriver)
|
|
977
|
+
continue;
|
|
978
|
+
// Skip the generic fallback — prefer a specific driver
|
|
979
|
+
if (driver instanceof FallbackDriver)
|
|
980
|
+
continue;
|
|
981
|
+
if (driver.detect())
|
|
982
|
+
return driver;
|
|
983
|
+
}
|
|
984
|
+
// If no specific terminal detected, use the fallback driver
|
|
985
|
+
return new FallbackDriver();
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Check if we're currently inside an IDE's integrated terminal.
|
|
989
|
+
* Returns the IDE driver if detected, null otherwise.
|
|
990
|
+
*/
|
|
991
|
+
export function detectIDE() {
|
|
992
|
+
for (const driver of DRIVERS) {
|
|
993
|
+
// IDE drivers are VSCodeDriver, CursorDriver, WindsurfDriver, etc.
|
|
994
|
+
// They come before TmuxDriver in the array
|
|
995
|
+
if (driver instanceof IDEDriver && driver.detect()) {
|
|
996
|
+
return driver;
|
|
997
|
+
}
|
|
998
|
+
// Also check JetBrains separately (not an IDEDriver subclass)
|
|
999
|
+
if (driver instanceof JetBrainsDriver && driver.detect()) {
|
|
1000
|
+
return driver;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Check if a specific IDE's CLI is available on the system.
|
|
1007
|
+
*/
|
|
1008
|
+
export async function isIDECliAvailable(ideName) {
|
|
1009
|
+
const cliMap = {
|
|
1010
|
+
vscode: "code",
|
|
1011
|
+
cursor: "cursor",
|
|
1012
|
+
windsurf: "windsurf",
|
|
1013
|
+
zed: "zed",
|
|
1014
|
+
jetbrains: "idea", // or webstorm, pycharm, etc.
|
|
1015
|
+
};
|
|
1016
|
+
const cli = cliMap[ideName];
|
|
1017
|
+
if (!cli)
|
|
1018
|
+
return false;
|
|
1019
|
+
return !!(await which(cli));
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Get a list of all available IDE CLIs on the system.
|
|
1023
|
+
* Useful for offering launch options.
|
|
1024
|
+
*/
|
|
1025
|
+
export async function getAvailableIDEs() {
|
|
1026
|
+
const ides = ["vscode", "cursor", "windsurf", "zed"];
|
|
1027
|
+
const available = [];
|
|
1028
|
+
for (const ide of ides) {
|
|
1029
|
+
if (await isIDECliAvailable(ide)) {
|
|
1030
|
+
available.push(ide);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return available;
|
|
1034
|
+
}
|
|
1035
|
+
// ─── Strategy 1: Environment variable detection (skip IDE drivers) ───────────
|
|
1036
|
+
function detectTerminalFromEnv() {
|
|
1037
|
+
for (const driver of DRIVERS) {
|
|
1038
|
+
if (isIDEDriver(driver))
|
|
1039
|
+
continue;
|
|
1040
|
+
if (driver instanceof FallbackDriver)
|
|
1041
|
+
continue;
|
|
1042
|
+
if (driver.detect())
|
|
1043
|
+
return driver;
|
|
1044
|
+
}
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
// ─── Strategy 2: Process-tree detection ──────────────────────────────────────
|
|
1048
|
+
/**
|
|
1049
|
+
* Map of known process names (lowercase) to terminal driver names.
|
|
1050
|
+
* Used by process-tree detection to match parent processes.
|
|
1051
|
+
*/
|
|
1052
|
+
const PROCESS_NAME_MAP = {
|
|
1053
|
+
// macOS process names (from ps -o comm=)
|
|
1054
|
+
"iterm2": "iterm2",
|
|
1055
|
+
"terminal": "terminal.app",
|
|
1056
|
+
"kitty": "kitty",
|
|
1057
|
+
"ghostty": "ghostty",
|
|
1058
|
+
"alacritty": "alacritty",
|
|
1059
|
+
"wezterm-gui": "wezterm",
|
|
1060
|
+
"wezterm": "wezterm",
|
|
1061
|
+
"hyper": "hyper",
|
|
1062
|
+
"rio": "rio",
|
|
1063
|
+
// Linux process names
|
|
1064
|
+
"gnome-terminal-server": "gnome-terminal",
|
|
1065
|
+
"gnome-terminal-": "gnome-terminal",
|
|
1066
|
+
"konsole": "konsole",
|
|
1067
|
+
// Multiplexers
|
|
1068
|
+
"tmux: server": "tmux",
|
|
1069
|
+
"tmux": "tmux",
|
|
1070
|
+
};
|
|
1071
|
+
/**
|
|
1072
|
+
* Walk up the process parent chain to find a known terminal emulator.
|
|
1073
|
+
* Works on macOS (ps) and Linux (/proc). Stops after 15 ancestors or PID <= 1.
|
|
1074
|
+
*/
|
|
1075
|
+
async function detectFromProcessTree() {
|
|
1076
|
+
const MAX_DEPTH = 15;
|
|
1077
|
+
let pid = process.ppid || process.pid;
|
|
1078
|
+
const visited = new Set();
|
|
1079
|
+
if (process.platform === "darwin" || process.platform === "linux") {
|
|
1080
|
+
for (let depth = 0; depth < MAX_DEPTH && pid > 1 && !visited.has(pid); depth++) {
|
|
1081
|
+
visited.add(pid);
|
|
1082
|
+
try {
|
|
1083
|
+
let comm = "";
|
|
1084
|
+
let ppid = 0;
|
|
1085
|
+
if (process.platform === "linux") {
|
|
1086
|
+
// Linux: read /proc/<pid>/comm and /proc/<pid>/stat
|
|
1087
|
+
try {
|
|
1088
|
+
const commData = fs.readFileSync(`/proc/${pid}/comm`, "utf-8").trim();
|
|
1089
|
+
comm = commData;
|
|
1090
|
+
const statData = fs.readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
1091
|
+
// Format: pid (comm) state ppid ...
|
|
1092
|
+
const match = statData.match(/\)\s+\S+\s+(\d+)/);
|
|
1093
|
+
ppid = match ? parseInt(match[1], 10) : 0;
|
|
1094
|
+
}
|
|
1095
|
+
catch {
|
|
1096
|
+
break; // Can't read /proc — permission denied or process gone
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
// macOS: use ps to get parent PID and command name
|
|
1101
|
+
const result = await exec("ps", ["-o", "ppid=", "-o", "comm=", "-p", String(pid)], {
|
|
1102
|
+
timeout: 2000,
|
|
1103
|
+
nothrow: true,
|
|
1104
|
+
});
|
|
1105
|
+
const output = result.stdout.trim();
|
|
1106
|
+
if (!output)
|
|
1107
|
+
break;
|
|
1108
|
+
// Parse: " 1234 /Applications/Ghostty.app/Contents/MacOS/ghostty"
|
|
1109
|
+
const spaceIdx = output.trimStart().indexOf(" ");
|
|
1110
|
+
if (spaceIdx === -1)
|
|
1111
|
+
break;
|
|
1112
|
+
const trimmed = output.trimStart();
|
|
1113
|
+
ppid = parseInt(trimmed.substring(0, spaceIdx), 10);
|
|
1114
|
+
const fullPath = trimmed.substring(spaceIdx).trim();
|
|
1115
|
+
// Extract just the binary name from the full path
|
|
1116
|
+
comm = fullPath.split("/").pop() || fullPath;
|
|
1117
|
+
}
|
|
1118
|
+
if (!comm)
|
|
1119
|
+
break;
|
|
1120
|
+
// Match against known process names (case-insensitive)
|
|
1121
|
+
const commLower = comm.toLowerCase();
|
|
1122
|
+
for (const [processName, driverName] of Object.entries(PROCESS_NAME_MAP)) {
|
|
1123
|
+
if (commLower === processName || commLower.startsWith(processName)) {
|
|
1124
|
+
const driver = getDriverByName(driverName);
|
|
1125
|
+
if (driver) {
|
|
1126
|
+
return { driver, detail: `process "${comm}" at PID ${pid}` };
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
pid = ppid;
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
break; // ps failed or process tree unavailable
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
// ─── Strategy 3: Frontmost app detection (macOS only) ────────────────────────
|
|
1140
|
+
/**
|
|
1141
|
+
* Map of macOS bundle identifiers to terminal driver names.
|
|
1142
|
+
*/
|
|
1143
|
+
const BUNDLE_ID_MAP = {
|
|
1144
|
+
"com.googlecode.iterm2": "iterm2",
|
|
1145
|
+
"com.apple.terminal": "terminal.app",
|
|
1146
|
+
"net.kovidgoyal.kitty": "kitty",
|
|
1147
|
+
"com.mitchellh.ghostty": "ghostty",
|
|
1148
|
+
"org.alacritty": "alacritty",
|
|
1149
|
+
"com.github.wez.wezterm": "wezterm",
|
|
1150
|
+
"co.zeit.hyper": "hyper",
|
|
1151
|
+
"com.raphaelamorim.rio": "rio",
|
|
1152
|
+
};
|
|
1153
|
+
/**
|
|
1154
|
+
* Detect the frontmost application on macOS and match to a terminal driver.
|
|
1155
|
+
* Uses AppleScript to query System Events for the frontmost process's bundle ID.
|
|
1156
|
+
*
|
|
1157
|
+
* This catches cases where the process tree doesn't contain the terminal
|
|
1158
|
+
* (e.g., launched via Spotlight or a launcher).
|
|
1159
|
+
*/
|
|
1160
|
+
async function detectFromFrontmostApp() {
|
|
1161
|
+
if (process.platform !== "darwin")
|
|
1162
|
+
return null;
|
|
1163
|
+
try {
|
|
1164
|
+
const script = 'tell application "System Events" to get bundle identifier of first process whose frontmost is true';
|
|
1165
|
+
const result = await exec("osascript", ["-e", script], { timeout: 3000, nothrow: true });
|
|
1166
|
+
const bundleId = result.stdout.trim().toLowerCase();
|
|
1167
|
+
if (!bundleId)
|
|
1168
|
+
return null;
|
|
1169
|
+
// Direct lookup
|
|
1170
|
+
const driverName = BUNDLE_ID_MAP[bundleId];
|
|
1171
|
+
if (driverName) {
|
|
1172
|
+
const driver = getDriverByName(driverName);
|
|
1173
|
+
if (driver) {
|
|
1174
|
+
return { driver, detail: `frontmost app bundle "${bundleId}"` };
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
// Partial match (e.g., "com.apple.Terminal" vs "com.apple.terminal")
|
|
1178
|
+
for (const [knownBundle, driverName] of Object.entries(BUNDLE_ID_MAP)) {
|
|
1179
|
+
if (bundleId.includes(knownBundle) || knownBundle.includes(bundleId)) {
|
|
1180
|
+
const driver = getDriverByName(driverName);
|
|
1181
|
+
if (driver) {
|
|
1182
|
+
return { driver, detail: `frontmost app bundle "${bundleId}" (partial match)` };
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
// AppleScript failed — accessibility permissions may be missing
|
|
1189
|
+
}
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
// ─── Strategy 4: User preference from .cortex/config.json ───────────────────
|
|
1193
|
+
/**
|
|
1194
|
+
* Read the user's preferred terminal from .cortex/config.json.
|
|
1195
|
+
* Format: { "preferredTerminal": "ghostty" }
|
|
1196
|
+
*
|
|
1197
|
+
* Accepts any driver name registered in the DRIVERS array.
|
|
1198
|
+
* This is the escape hatch for unusual setups where auto-detection fails.
|
|
1199
|
+
*/
|
|
1200
|
+
function getPreferredTerminal(projectRoot) {
|
|
1201
|
+
const roots = [
|
|
1202
|
+
projectRoot,
|
|
1203
|
+
process.cwd(),
|
|
1204
|
+
process.env.HOME,
|
|
1205
|
+
].filter(Boolean);
|
|
1206
|
+
for (const root of roots) {
|
|
1207
|
+
const configPath = path.join(root, ".cortex", "config.json");
|
|
1208
|
+
if (!fs.existsSync(configPath))
|
|
1209
|
+
continue;
|
|
1210
|
+
try {
|
|
1211
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1212
|
+
if (config.preferredTerminal && typeof config.preferredTerminal === "string") {
|
|
1213
|
+
const driver = getDriverByName(config.preferredTerminal);
|
|
1214
|
+
if (driver && !isIDEDriver(driver)) {
|
|
1215
|
+
return driver;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
catch {
|
|
1220
|
+
// Invalid JSON — skip
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
// ─── Main entry point: detectTerminalDriver() ───────────────────────────────
|
|
1226
|
+
/**
|
|
1227
|
+
* Async multi-strategy detection of the user's terminal emulator.
|
|
1228
|
+
*
|
|
1229
|
+
* This is the PRIMARY function for "Open in terminal tab" — it NEVER returns
|
|
1230
|
+
* an IDE driver. Use `detectDriver()` for IDE-first detection (e.g., "Open in IDE").
|
|
1231
|
+
*
|
|
1232
|
+
* Strategy chain:
|
|
1233
|
+
* 1. Environment variables (fast, synchronous)
|
|
1234
|
+
* 2. Process-tree walk (macOS: ps, Linux: /proc)
|
|
1235
|
+
* 3. Frontmost application (macOS: AppleScript)
|
|
1236
|
+
* 4. User preference (.cortex/config.json)
|
|
1237
|
+
* 5. FallbackDriver (last resort)
|
|
1238
|
+
*
|
|
1239
|
+
* @param projectRoot — optional project root for reading .cortex/config.json
|
|
1240
|
+
*/
|
|
1241
|
+
export async function detectTerminalDriver(projectRoot) {
|
|
1242
|
+
// Strategy 1: Environment variables (fastest — no subprocess needed)
|
|
1243
|
+
const envDriver = detectTerminalFromEnv();
|
|
1244
|
+
if (envDriver) {
|
|
1245
|
+
return { driver: envDriver, strategy: "env", detail: `env match: ${envDriver.name}` };
|
|
1246
|
+
}
|
|
1247
|
+
// Strategy 2: Process-tree walk
|
|
1248
|
+
const treeResult = await detectFromProcessTree();
|
|
1249
|
+
if (treeResult) {
|
|
1250
|
+
return { driver: treeResult.driver, strategy: "process-tree", detail: treeResult.detail };
|
|
1251
|
+
}
|
|
1252
|
+
// Strategy 3: Frontmost app (macOS only)
|
|
1253
|
+
const frontmostResult = await detectFromFrontmostApp();
|
|
1254
|
+
if (frontmostResult) {
|
|
1255
|
+
return { driver: frontmostResult.driver, strategy: "frontmost-app", detail: frontmostResult.detail };
|
|
1256
|
+
}
|
|
1257
|
+
// Strategy 4: User preference
|
|
1258
|
+
const preferred = getPreferredTerminal(projectRoot);
|
|
1259
|
+
if (preferred) {
|
|
1260
|
+
return { driver: preferred, strategy: "user-config", detail: `config: ${preferred.name}` };
|
|
1261
|
+
}
|
|
1262
|
+
// Strategy 5: Fallback
|
|
1263
|
+
return { driver: new FallbackDriver(), strategy: "fallback", detail: "no terminal detected" };
|
|
1264
|
+
}
|