cortex-agents 2.1.0 → 2.3.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 (41) hide show
  1. package/.opencode/agents/build.md +179 -21
  2. package/.opencode/agents/debug.md +97 -11
  3. package/.opencode/agents/devops.md +75 -7
  4. package/.opencode/agents/fullstack.md +89 -1
  5. package/.opencode/agents/plan.md +83 -6
  6. package/.opencode/agents/security.md +60 -1
  7. package/.opencode/agents/testing.md +45 -1
  8. package/README.md +292 -356
  9. package/dist/cli.js +230 -65
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +10 -5
  12. package/dist/tools/branch.d.ts +7 -1
  13. package/dist/tools/branch.d.ts.map +1 -1
  14. package/dist/tools/branch.js +88 -53
  15. package/dist/tools/cortex.d.ts +19 -0
  16. package/dist/tools/cortex.d.ts.map +1 -1
  17. package/dist/tools/cortex.js +110 -1
  18. package/dist/tools/session.d.ts.map +1 -1
  19. package/dist/tools/session.js +3 -1
  20. package/dist/tools/task.d.ts +20 -0
  21. package/dist/tools/task.d.ts.map +1 -0
  22. package/dist/tools/task.js +310 -0
  23. package/dist/tools/worktree.d.ts +42 -2
  24. package/dist/tools/worktree.d.ts.map +1 -1
  25. package/dist/tools/worktree.js +573 -98
  26. package/dist/utils/plan-extract.d.ts +37 -0
  27. package/dist/utils/plan-extract.d.ts.map +1 -0
  28. package/dist/utils/plan-extract.js +137 -0
  29. package/dist/utils/propagate.d.ts +22 -0
  30. package/dist/utils/propagate.d.ts.map +1 -0
  31. package/dist/utils/propagate.js +64 -0
  32. package/dist/utils/shell.d.ts +53 -0
  33. package/dist/utils/shell.d.ts.map +1 -0
  34. package/dist/utils/shell.js +118 -0
  35. package/dist/utils/terminal.d.ts +66 -0
  36. package/dist/utils/terminal.d.ts.map +1 -0
  37. package/dist/utils/terminal.js +627 -0
  38. package/dist/utils/worktree-detect.d.ts +20 -0
  39. package/dist/utils/worktree-detect.d.ts.map +1 -0
  40. package/dist/utils/worktree-detect.js +43 -0
  41. package/package.json +13 -9
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Sections extracted from a plan for use in a PR body.
3
+ */
4
+ export interface PlanSections {
5
+ /** Plan title from frontmatter or first heading */
6
+ title: string;
7
+ /** Summary paragraph(s) */
8
+ summary: string;
9
+ /** Task list in markdown checkbox format */
10
+ tasks: string;
11
+ /** Key decisions section */
12
+ decisions: string;
13
+ /** The raw plan filename */
14
+ filename: string;
15
+ }
16
+ /**
17
+ * Extract relevant sections from a plan markdown file for composing a PR body.
18
+ *
19
+ * Parses the plan looking for ## Summary, ## Tasks, and ## Key Decisions sections.
20
+ * Falls back gracefully if sections are missing.
21
+ */
22
+ export declare function extractPlanSections(planContent: string, filename: string): PlanSections;
23
+ /**
24
+ * Build a PR body from extracted plan sections.
25
+ */
26
+ export declare function buildPrBodyFromPlan(sections: PlanSections): string;
27
+ /**
28
+ * Find and read a plan file from .cortex/plans/.
29
+ *
30
+ * If a specific filename is given, reads that file.
31
+ * Otherwise, finds the most recent plan matching the branch type prefix.
32
+ */
33
+ export declare function findPlanContent(worktree: string, planFilename?: string, branchName?: string): {
34
+ content: string;
35
+ filename: string;
36
+ } | null;
37
+ //# sourceMappingURL=plan-extract.d.ts.map
@@ -0,0 +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,CAqC9C"}
@@ -0,0 +1,137 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ const CORTEX_DIR = ".cortex";
4
+ const PLANS_DIR = "plans";
5
+ /**
6
+ * Extract relevant sections from a plan markdown file for composing a PR body.
7
+ *
8
+ * Parses the plan looking for ## Summary, ## Tasks, and ## Key Decisions sections.
9
+ * Falls back gracefully if sections are missing.
10
+ */
11
+ export function extractPlanSections(planContent, filename) {
12
+ const result = {
13
+ title: "",
14
+ summary: "",
15
+ tasks: "",
16
+ decisions: "",
17
+ filename,
18
+ };
19
+ // Extract title from frontmatter
20
+ const frontmatterMatch = planContent.match(/^---\n([\s\S]*?)\n---/);
21
+ if (frontmatterMatch) {
22
+ const titleMatch = frontmatterMatch[1].match(/title:\s*"?([^"\n]+)"?/);
23
+ if (titleMatch)
24
+ result.title = titleMatch[1].trim();
25
+ }
26
+ // Fallback: extract title from first # heading
27
+ if (!result.title) {
28
+ const headingMatch = planContent.match(/^#\s+(.+)$/m);
29
+ if (headingMatch)
30
+ result.title = headingMatch[1].trim();
31
+ }
32
+ // Extract sections by heading
33
+ // Split on ## headings and capture each section
34
+ const sections = splitByHeadings(planContent);
35
+ for (const [heading, content] of sections) {
36
+ const h = heading.toLowerCase();
37
+ if (h.includes("summary")) {
38
+ result.summary = content.trim();
39
+ }
40
+ else if (h.includes("task")) {
41
+ result.tasks = content.trim();
42
+ }
43
+ else if (h.includes("decision") || h.includes("key decision")) {
44
+ result.decisions = content.trim();
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+ /**
50
+ * Split markdown content into sections based on ## headings.
51
+ * Returns an array of [heading, content] tuples.
52
+ */
53
+ function splitByHeadings(content) {
54
+ const sections = [];
55
+ const lines = content.split("\n");
56
+ let currentHeading = "";
57
+ let currentContent = [];
58
+ for (const line of lines) {
59
+ const headingMatch = line.match(/^##\s+(.+)$/);
60
+ if (headingMatch) {
61
+ // Save previous section
62
+ if (currentHeading) {
63
+ sections.push([currentHeading, currentContent.join("\n")]);
64
+ }
65
+ currentHeading = headingMatch[1];
66
+ currentContent = [];
67
+ }
68
+ else if (currentHeading) {
69
+ currentContent.push(line);
70
+ }
71
+ }
72
+ // Save last section
73
+ if (currentHeading) {
74
+ sections.push([currentHeading, currentContent.join("\n")]);
75
+ }
76
+ return sections;
77
+ }
78
+ /**
79
+ * Build a PR body from extracted plan sections.
80
+ */
81
+ export function buildPrBodyFromPlan(sections) {
82
+ const parts = [];
83
+ if (sections.summary) {
84
+ parts.push(`## Summary\n\n${sections.summary}`);
85
+ }
86
+ if (sections.tasks) {
87
+ parts.push(`## Tasks\n\n${sections.tasks}`);
88
+ }
89
+ if (sections.decisions) {
90
+ parts.push(`## Key Decisions\n\n${sections.decisions}`);
91
+ }
92
+ if (parts.length === 0) {
93
+ return `Implementation based on plan: \`${sections.filename}\``;
94
+ }
95
+ parts.push(`---\n*Auto-generated by cortex-agents from plan: \`${sections.filename}\`*`);
96
+ return parts.join("\n\n");
97
+ }
98
+ /**
99
+ * Find and read a plan file from .cortex/plans/.
100
+ *
101
+ * If a specific filename is given, reads that file.
102
+ * Otherwise, finds the most recent plan matching the branch type prefix.
103
+ */
104
+ export function findPlanContent(worktree, planFilename, branchName) {
105
+ const plansDir = path.join(worktree, CORTEX_DIR, PLANS_DIR);
106
+ if (!fs.existsSync(plansDir))
107
+ return null;
108
+ if (planFilename) {
109
+ const filepath = path.join(plansDir, planFilename);
110
+ if (fs.existsSync(filepath)) {
111
+ return { content: fs.readFileSync(filepath, "utf-8"), filename: planFilename };
112
+ }
113
+ return null;
114
+ }
115
+ // Try to find a matching plan by branch type
116
+ // e.g., branch "feature/auth" → look for plans with "feature" type
117
+ const planFiles = fs
118
+ .readdirSync(plansDir)
119
+ .filter((f) => f.endsWith(".md") && f !== ".gitkeep")
120
+ .sort()
121
+ .reverse(); // Most recent first
122
+ if (planFiles.length === 0)
123
+ return null;
124
+ // If branch name provided, try to match by type
125
+ if (branchName) {
126
+ const branchType = branchName.split("/")[0]; // "feature", "bugfix", etc.
127
+ const matched = planFiles.find((f) => f.includes(branchType));
128
+ if (matched) {
129
+ const filepath = path.join(plansDir, matched);
130
+ return { content: fs.readFileSync(filepath, "utf-8"), filename: matched };
131
+ }
132
+ }
133
+ // Fall back to most recent plan
134
+ const mostRecent = planFiles[0];
135
+ const filepath = path.join(plansDir, mostRecent);
136
+ return { content: fs.readFileSync(filepath, "utf-8"), filename: mostRecent };
137
+ }
@@ -0,0 +1,22 @@
1
+ interface PropagateResult {
2
+ /** Plans that were copied */
3
+ copied: string[];
4
+ /** Whether .cortex was newly initialized in the target */
5
+ initialized: boolean;
6
+ }
7
+ /**
8
+ * Propagate plans from the main project into a worktree's .cortex/plans/ directory.
9
+ *
10
+ * Ensures the worktree is self-contained with its own copy of the plans,
11
+ * so the new OpenCode session has full context without referencing the parent.
12
+ */
13
+ export declare function propagatePlan(opts: {
14
+ /** Main project root (source) */
15
+ sourceWorktree: string;
16
+ /** Worktree path (target) */
17
+ targetWorktree: string;
18
+ /** Specific plan filename to copy — copies all plans if omitted */
19
+ planFilename?: string;
20
+ }): PropagateResult;
21
+ export {};
22
+ //# sourceMappingURL=propagate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"propagate.d.ts","sourceRoot":"","sources":["../../src/utils/propagate.ts"],"names":[],"mappings":"AAMA,UAAU,eAAe;IACvB,6BAA6B;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0DAA0D;IAC1D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,iCAAiC;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,6BAA6B;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,eAAe,CA6DlB"}
@@ -0,0 +1,64 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ const CORTEX_DIR = ".cortex";
4
+ const PLANS_DIR = "plans";
5
+ /**
6
+ * Propagate plans from the main project into a worktree's .cortex/plans/ directory.
7
+ *
8
+ * Ensures the worktree is self-contained with its own copy of the plans,
9
+ * so the new OpenCode session has full context without referencing the parent.
10
+ */
11
+ export function propagatePlan(opts) {
12
+ const { sourceWorktree, targetWorktree, planFilename } = opts;
13
+ const sourcePlansDir = path.join(sourceWorktree, CORTEX_DIR, PLANS_DIR);
14
+ const targetCortexDir = path.join(targetWorktree, CORTEX_DIR);
15
+ const targetPlansDir = path.join(targetCortexDir, PLANS_DIR);
16
+ const result = { copied: [], initialized: false };
17
+ // Check source has plans
18
+ if (!fs.existsSync(sourcePlansDir)) {
19
+ return result;
20
+ }
21
+ // Initialize target .cortex if needed
22
+ if (!fs.existsSync(targetCortexDir)) {
23
+ fs.mkdirSync(targetCortexDir, { recursive: true });
24
+ result.initialized = true;
25
+ }
26
+ if (!fs.existsSync(targetPlansDir)) {
27
+ fs.mkdirSync(targetPlansDir, { recursive: true });
28
+ }
29
+ // Copy config.json if it exists (for consistent configuration)
30
+ const sourceConfig = path.join(sourceWorktree, CORTEX_DIR, "config.json");
31
+ if (fs.existsSync(sourceConfig)) {
32
+ const targetConfig = path.join(targetCortexDir, "config.json");
33
+ if (!fs.existsSync(targetConfig)) {
34
+ fs.copyFileSync(sourceConfig, targetConfig);
35
+ }
36
+ }
37
+ // Create sessions dir in target (tools expect it)
38
+ const targetSessionsDir = path.join(targetCortexDir, "sessions");
39
+ if (!fs.existsSync(targetSessionsDir)) {
40
+ fs.mkdirSync(targetSessionsDir, { recursive: true });
41
+ }
42
+ if (planFilename) {
43
+ // Copy specific plan
44
+ const sourcePlan = path.join(sourcePlansDir, planFilename);
45
+ if (fs.existsSync(sourcePlan)) {
46
+ const targetPlan = path.join(targetPlansDir, planFilename);
47
+ fs.copyFileSync(sourcePlan, targetPlan);
48
+ result.copied.push(planFilename);
49
+ }
50
+ }
51
+ else {
52
+ // Copy all plans
53
+ const planFiles = fs
54
+ .readdirSync(sourcePlansDir)
55
+ .filter((f) => f.endsWith(".md") && f !== ".gitkeep");
56
+ for (const file of planFiles) {
57
+ const sourcePlan = path.join(sourcePlansDir, file);
58
+ const targetPlan = path.join(targetPlansDir, file);
59
+ fs.copyFileSync(sourcePlan, targetPlan);
60
+ result.copied.push(file);
61
+ }
62
+ }
63
+ return result;
64
+ }
@@ -0,0 +1,53 @@
1
+ export interface ExecResult {
2
+ stdout: string;
3
+ stderr: string;
4
+ exitCode: number;
5
+ }
6
+ export interface ExecOptions {
7
+ cwd?: string;
8
+ env?: NodeJS.ProcessEnv;
9
+ timeout?: number;
10
+ /** If true, resolve even on non-zero exit codes instead of rejecting. */
11
+ nothrow?: boolean;
12
+ }
13
+ /**
14
+ * Execute a command with array-based arguments (no shell invocation).
15
+ * Uses child_process.execFile which never spawns a shell, preventing injection.
16
+ *
17
+ * By default, rejects on non-zero exit codes. Pass `nothrow: true` to always resolve.
18
+ */
19
+ export declare function exec(cmd: string, args: string[], opts?: ExecOptions): Promise<ExecResult>;
20
+ /**
21
+ * Fire-and-forget spawn. Returns the ChildProcess for PID tracking.
22
+ * Uses child_process.spawn with shell: false (default).
23
+ */
24
+ export declare function spawn(cmd: string, args: string[], opts?: ExecOptions & {
25
+ stdio?: "ignore" | "pipe";
26
+ }): import("child_process").ChildProcess;
27
+ /**
28
+ * Shorthand for running a git command.
29
+ */
30
+ export declare function git(cwd: string, ...args: string[]): Promise<ExecResult>;
31
+ /**
32
+ * Shorthand for running a gh (GitHub CLI) command.
33
+ */
34
+ export declare function gh(cwd: string, ...args: string[]): Promise<ExecResult>;
35
+ /**
36
+ * Check if a binary exists in PATH.
37
+ */
38
+ export declare function which(bin: string): Promise<string | null>;
39
+ /**
40
+ * Send a signal to a process. Returns true if signal was sent, false if
41
+ * the process doesn't exist or we lack permissions.
42
+ */
43
+ export declare function kill(pid: number, signal?: NodeJS.Signals): boolean;
44
+ /**
45
+ * Check if a process is alive (sends signal 0 — no actual signal).
46
+ */
47
+ export declare function isAlive(pid: number): boolean;
48
+ /**
49
+ * Escape a string for safe inclusion in a shell command.
50
+ * Use only when array-based argument passing is impossible (e.g., osascript).
51
+ */
52
+ export declare function shellEscape(str: string): string;
53
+ //# sourceMappingURL=shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAClB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,UAAU,CAAC,CAqCrB;AAED;;;GAGG;AACH,wBAAgB,KAAK,CACnB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,WAAW,GAAG;IAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAA;CAAO,wCAYvD;AAED;;GAEG;AACH,wBAAsB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAE7E;AAED;;GAEG;AACH,wBAAsB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAE5E;AAED;;GAEG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQ/D;AAED;;;GAGG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,OAAmB,GAAG,OAAO,CAO7E;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAO5C;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAS/C"}
@@ -0,0 +1,118 @@
1
+ import { execFile, spawn as cpSpawn } from "child_process";
2
+ /**
3
+ * Execute a command with array-based arguments (no shell invocation).
4
+ * Uses child_process.execFile which never spawns a shell, preventing injection.
5
+ *
6
+ * By default, rejects on non-zero exit codes. Pass `nothrow: true` to always resolve.
7
+ */
8
+ export function exec(cmd, args, opts = {}) {
9
+ return new Promise((resolve, reject) => {
10
+ execFile(cmd, args, {
11
+ cwd: opts.cwd,
12
+ env: opts.env ?? process.env,
13
+ timeout: opts.timeout ?? 30_000,
14
+ maxBuffer: 10 * 1024 * 1024,
15
+ }, (error, stdout, stderr) => {
16
+ const result = {
17
+ stdout: String(stdout ?? ""),
18
+ stderr: String(stderr ?? ""),
19
+ exitCode: error ? error.code ?? 1 : 0,
20
+ };
21
+ if (error) {
22
+ if (error.code === "ENOENT") {
23
+ reject(new Error(`Command not found: ${cmd}`));
24
+ return;
25
+ }
26
+ if (!opts.nothrow) {
27
+ const err = new Error(result.stderr || error.message);
28
+ err.exitCode = result.exitCode;
29
+ err.stdout = result.stdout;
30
+ err.stderr = result.stderr;
31
+ reject(err);
32
+ return;
33
+ }
34
+ }
35
+ resolve(result);
36
+ });
37
+ });
38
+ }
39
+ /**
40
+ * Fire-and-forget spawn. Returns the ChildProcess for PID tracking.
41
+ * Uses child_process.spawn with shell: false (default).
42
+ */
43
+ export function spawn(cmd, args, opts = {}) {
44
+ const stdio = opts.stdio ?? "ignore";
45
+ const child = cpSpawn(cmd, args, {
46
+ cwd: opts.cwd,
47
+ env: opts.env ?? process.env,
48
+ stdio: stdio === "pipe" ? ["ignore", "pipe", "pipe"] : "ignore",
49
+ detached: false,
50
+ });
51
+ // Prevent unhandled error crashes on spawn failure
52
+ child.on("error", () => { });
53
+ return child;
54
+ }
55
+ /**
56
+ * Shorthand for running a git command.
57
+ */
58
+ export async function git(cwd, ...args) {
59
+ return exec("git", ["-C", cwd, ...args]);
60
+ }
61
+ /**
62
+ * Shorthand for running a gh (GitHub CLI) command.
63
+ */
64
+ export async function gh(cwd, ...args) {
65
+ return exec("gh", args, { cwd });
66
+ }
67
+ /**
68
+ * Check if a binary exists in PATH.
69
+ */
70
+ export async function which(bin) {
71
+ try {
72
+ const result = await exec(process.platform === "win32" ? "where" : "which", [bin]);
73
+ const p = result.stdout.trim().split("\n")[0];
74
+ return p || null;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ /**
81
+ * Send a signal to a process. Returns true if signal was sent, false if
82
+ * the process doesn't exist or we lack permissions.
83
+ */
84
+ export function kill(pid, signal = "SIGTERM") {
85
+ try {
86
+ process.kill(pid, signal);
87
+ return true;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ /**
94
+ * Check if a process is alive (sends signal 0 — no actual signal).
95
+ */
96
+ export function isAlive(pid) {
97
+ try {
98
+ process.kill(pid, 0);
99
+ return true;
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ /**
106
+ * Escape a string for safe inclusion in a shell command.
107
+ * Use only when array-based argument passing is impossible (e.g., osascript).
108
+ */
109
+ export function shellEscape(str) {
110
+ // Replace backslashes first, then other dangerous characters
111
+ return str
112
+ .replace(/\\/g, "\\\\")
113
+ .replace(/"/g, '\\"')
114
+ .replace(/'/g, "'\\''")
115
+ .replace(/`/g, "\\`")
116
+ .replace(/\$/g, "\\$")
117
+ .replace(/!/g, "\\!");
118
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Terminal Driver System — Strategy pattern for cross-platform terminal tab management.
3
+ *
4
+ * Each supported terminal emulator implements the TerminalDriver interface for:
5
+ * - detect() — check if this is the active terminal
6
+ * - openTab() — open a new tab and return identifiers
7
+ * - closeTab() — close a tab by its identifiers (idempotent, never throws)
8
+ *
9
+ * Session data is persisted to `.cortex/.terminal-session` inside each worktree
10
+ * so tabs can be closed when the worktree is removed.
11
+ */
12
+ /** Persisted session data for an opened terminal tab. */
13
+ export interface TerminalSession {
14
+ /** Driver name (e.g., "iterm2", "kitty", "tmux", "pty") */
15
+ terminal: string;
16
+ platform: NodeJS.Platform;
17
+ sessionId?: string;
18
+ windowId?: string;
19
+ tabId?: string;
20
+ paneId?: string;
21
+ dbusPath?: string;
22
+ pid?: number;
23
+ ptyId?: string;
24
+ mode: "terminal" | "pty" | "background";
25
+ branch: string;
26
+ agent: string;
27
+ worktreePath: string;
28
+ startedAt: string;
29
+ }
30
+ /** Options passed to a driver's openTab() method. */
31
+ export interface TabOpenOptions {
32
+ worktreePath: string;
33
+ opencodeBin: string;
34
+ agent: string;
35
+ prompt: string;
36
+ branchName: string;
37
+ }
38
+ /** Common interface for all terminal drivers. */
39
+ export interface TerminalDriver {
40
+ readonly name: string;
41
+ detect(): boolean;
42
+ openTab(opts: TabOpenOptions): Promise<Partial<TerminalSession>>;
43
+ closeTab(session: TerminalSession): Promise<boolean>;
44
+ }
45
+ /** Persist terminal session data to the worktree's .cortex directory. */
46
+ export declare function writeSession(worktreePath: string, session: TerminalSession): void;
47
+ /** Read terminal session data from the worktree's .cortex directory. */
48
+ export declare function readSession(worktreePath: string): TerminalSession | null;
49
+ /**
50
+ * Detect the active terminal emulator and return the matching driver.
51
+ * Falls back to FallbackDriver if no specific terminal is detected.
52
+ */
53
+ export declare function detectDriver(): TerminalDriver;
54
+ /**
55
+ * Get a driver by name (used when closing a tab from a persisted session).
56
+ * Returns null if the driver name is unknown.
57
+ */
58
+ export declare function getDriverByName(name: string): TerminalDriver | null;
59
+ /**
60
+ * Close a terminal session using the appropriate driver.
61
+ * Handles all modes (terminal, pty, background) with PID fallback.
62
+ *
63
+ * This is the main entry point for worktree_remove cleanup.
64
+ */
65
+ export declare function closeSession(session: TerminalSession): Promise<boolean>;
66
+ //# sourceMappingURL=terminal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../../src/utils/terminal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAG1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,IAAI,EAAE,UAAU,GAAG,KAAK,GAAG,YAAY,CAAC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,MAAM,IAAI,OAAO,CAAC;IAClB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC;IACjE,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtD;AAMD,yEAAyE;AACzE,wBAAgB,YAAY,CAC1B,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,IAAI,CASN;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CACzB,YAAY,EAAE,MAAM,GACnB,eAAe,GAAG,IAAI,CAQxB;AA6kBD;;;GAGG;AACH,wBAAgB,YAAY,IAAI,cAAc,CAM7C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAEnE;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAY7E"}