cortex-agents 2.2.0 → 2.3.1

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.
@@ -3,6 +3,7 @@ import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import { detectWorktreeInfo } from "../utils/worktree-detect.js";
5
5
  import { findPlanContent, extractPlanSections, buildPrBodyFromPlan, } from "../utils/plan-extract.js";
6
+ import { git, gh, which } from "../utils/shell.js";
6
7
  const PROTECTED_BRANCHES = ["main", "master", "develop", "production", "staging"];
7
8
  const DOCS_DIR = "docs";
8
9
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -11,10 +12,8 @@ const DOCS_DIR = "docs";
11
12
  */
12
13
  async function checkGhCli(cwd) {
13
14
  // Check if gh exists
14
- try {
15
- await Bun.$ `which gh`.quiet().text();
16
- }
17
- catch {
15
+ const ghPath = await which("gh");
16
+ if (!ghPath) {
18
17
  return {
19
18
  ok: false,
20
19
  error: "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/ and run `gh auth login`.",
@@ -22,7 +21,7 @@ async function checkGhCli(cwd) {
22
21
  }
23
22
  // Check if authenticated
24
23
  try {
25
- await Bun.$ `gh auth status`.cwd(cwd).quiet().text();
24
+ await gh(cwd, "auth", "status");
26
25
  }
27
26
  catch {
28
27
  return {
@@ -37,8 +36,8 @@ async function checkGhCli(cwd) {
37
36
  */
38
37
  async function checkRemote(cwd) {
39
38
  try {
40
- const remotes = await Bun.$ `git -C ${cwd} remote -v`.quiet().text();
41
- if (!remotes.includes("origin")) {
39
+ const { stdout } = await git(cwd, "remote", "-v");
40
+ if (!stdout.includes("origin")) {
42
41
  return {
43
42
  ok: false,
44
43
  error: "No 'origin' remote configured. Add one with: git remote add origin <url>",
@@ -74,14 +73,14 @@ function checkDocsExist(worktree) {
74
73
  */
75
74
  async function getCommitLog(cwd, baseBranch) {
76
75
  try {
77
- const log = await Bun.$ `git -C ${cwd} log ${baseBranch}..HEAD --oneline`.quiet().text();
78
- return log.trim();
76
+ const { stdout } = await git(cwd, "log", `${baseBranch}..HEAD`, "--oneline");
77
+ return stdout.trim();
79
78
  }
80
79
  catch {
81
80
  // If base branch doesn't exist locally, try origin/<base>
82
81
  try {
83
- const log = await Bun.$ `git -C ${cwd} log origin/${baseBranch}..HEAD --oneline`.quiet().text();
84
- return log.trim();
82
+ const { stdout } = await git(cwd, "log", `origin/${baseBranch}..HEAD`, "--oneline");
83
+ return stdout.trim();
85
84
  }
86
85
  catch {
87
86
  return "";
@@ -126,7 +125,7 @@ export const finalize = tool({
126
125
  const warnings = [];
127
126
  // ── 1. Validate: git repo ─────────────────────────────────
128
127
  try {
129
- await Bun.$ `git -C ${cwd} rev-parse --git-dir`.quiet().text();
128
+ await git(cwd, "rev-parse", "--git-dir");
130
129
  }
131
130
  catch {
132
131
  return "✗ Error: Not in a git repository.";
@@ -150,8 +149,8 @@ Create a feature/bugfix branch first with branch_create or worktree_create.`;
150
149
  else {
151
150
  // Try to detect default branch from origin
152
151
  try {
153
- const defaultRef = await Bun.$ `git -C ${cwd} symbolic-ref refs/remotes/origin/HEAD`.quiet().text();
154
- baseBranch = defaultRef.trim().replace("refs/remotes/origin/", "");
152
+ const { stdout } = await git(cwd, "symbolic-ref", "refs/remotes/origin/HEAD");
153
+ baseBranch = stdout.trim().replace("refs/remotes/origin/", "");
155
154
  }
156
155
  catch {
157
156
  baseBranch = "main"; // Sensible default
@@ -181,7 +180,7 @@ Create a feature/bugfix branch first with branch_create or worktree_create.`;
181
180
  }
182
181
  // ── 6. Stage all changes ──────────────────────────────────
183
182
  try {
184
- await Bun.$ `git -C ${cwd} add -A`.quiet();
183
+ await git(cwd, "add", "-A");
185
184
  }
186
185
  catch (error) {
187
186
  return `✗ Error staging changes: ${error.message || error}`;
@@ -191,15 +190,15 @@ Create a feature/bugfix branch first with branch_create or worktree_create.`;
191
190
  let commitSkipped = false;
192
191
  try {
193
192
  // Check if there's anything to commit
194
- const status = await Bun.$ `git -C ${cwd} status --porcelain`.quiet().text();
195
- if (!status.trim()) {
193
+ const { stdout: statusOut } = await git(cwd, "status", "--porcelain");
194
+ if (!statusOut.trim()) {
196
195
  commitSkipped = true;
197
196
  output.push("No new changes to commit (working tree clean)");
198
197
  }
199
198
  else {
200
- await Bun.$ `git -C ${cwd} commit -m ${commitMessage}`.quiet();
201
- const hash = await Bun.$ `git -C ${cwd} rev-parse --short HEAD`.quiet().text();
202
- commitHash = hash.trim();
199
+ await git(cwd, "commit", "-m", commitMessage);
200
+ const { stdout: hashOut } = await git(cwd, "rev-parse", "--short", "HEAD");
201
+ commitHash = hashOut.trim();
203
202
  output.push(`Committed: ${commitHash} — ${commitMessage}`);
204
203
  }
205
204
  }
@@ -208,7 +207,7 @@ Create a feature/bugfix branch first with branch_create or worktree_create.`;
208
207
  }
209
208
  // ── 8. Push to origin ─────────────────────────────────────
210
209
  try {
211
- await Bun.$ `git -C ${cwd} push -u origin ${branchName}`.quiet();
210
+ await git(cwd, "push", "-u", "origin", branchName);
212
211
  output.push(`Pushed to origin/${branchName}`);
213
212
  }
214
213
  catch (error) {
@@ -243,49 +242,29 @@ All previous steps succeeded (changes committed). Try pushing manually:
243
242
  let prUrl = "";
244
243
  try {
245
244
  // Check if PR already exists for this branch
246
- const existingPr = await Bun.$ `gh pr view ${branchName} --json url --jq .url`
247
- .cwd(cwd).quiet().nothrow().text();
245
+ const { stdout: existingPr } = await gh(cwd, "pr", "view", branchName, "--json", "url", "--jq", ".url");
248
246
  if (existingPr.trim()) {
249
247
  prUrl = existingPr.trim();
250
248
  output.push(`PR already exists: ${prUrl}`);
251
249
  }
252
250
  else {
253
- // Create new PR
254
- const draftFlag = draft ? "--draft" : "";
255
- // Use a heredoc-style approach via stdin to handle complex body content
256
- const bodyFile = path.join(cwd, ".cortex", ".pr-body-tmp.md");
257
- const cortexDir = path.join(cwd, ".cortex");
258
- if (!fs.existsSync(cortexDir)) {
259
- fs.mkdirSync(cortexDir, { recursive: true });
260
- }
261
- fs.writeFileSync(bodyFile, prBodyContent);
262
- try {
263
- const createArgs = [
264
- "gh", "pr", "create",
265
- "--base", baseBranch,
266
- "--title", finalPrTitle,
267
- "--body-file", bodyFile,
268
- ];
269
- if (draft)
270
- createArgs.push("--draft");
271
- const result = await Bun.$ `gh pr create --base ${baseBranch} --title ${finalPrTitle} --body-file ${bodyFile} ${draft ? "--draft" : ""}`.cwd(cwd).quiet().text();
272
- prUrl = result.trim();
273
- output.push(`PR created: ${prUrl}`);
274
- }
275
- finally {
276
- // Clean up temp file
277
- if (fs.existsSync(bodyFile)) {
278
- fs.unlinkSync(bodyFile);
279
- }
280
- }
251
+ prUrl = await createPr(cwd, baseBranch, finalPrTitle, prBodyContent, draft);
252
+ output.push(`PR created: ${prUrl}`);
281
253
  }
282
254
  }
283
- catch (error) {
284
- // PR creation failed but everything else succeeded
285
- output.push(`⚠ PR creation failed: ${error.message || error}`);
286
- output.push("");
287
- output.push("Changes are committed and pushed. Create the PR manually:");
288
- output.push(` gh pr create --base ${baseBranch} --title "${finalPrTitle}"`);
255
+ catch {
256
+ // PR doesn't exist yet, create it
257
+ try {
258
+ prUrl = await createPr(cwd, baseBranch, finalPrTitle, prBodyContent, draft);
259
+ output.push(`PR created: ${prUrl}`);
260
+ }
261
+ catch (error) {
262
+ // PR creation failed but everything else succeeded
263
+ output.push(`⚠ PR creation failed: ${error.message || error}`);
264
+ output.push("");
265
+ output.push("Changes are committed and pushed. Create the PR manually:");
266
+ output.push(` gh pr create --base ${baseBranch} --title "${finalPrTitle}"`);
267
+ }
289
268
  }
290
269
  // ── Build final output ────────────────────────────────────
291
270
  let finalOutput = `✓ Task finalized\n\n`;
@@ -300,3 +279,32 @@ All previous steps succeeded (changes committed). Try pushing manually:
300
279
  return finalOutput;
301
280
  },
302
281
  });
282
+ /**
283
+ * Create a PR using gh CLI with array-based args (no shell injection).
284
+ * Uses a temp body file to avoid shell escaping issues with PR body content.
285
+ */
286
+ async function createPr(cwd, baseBranch, title, body, draft) {
287
+ const bodyFile = path.join(cwd, ".cortex", ".pr-body-tmp.md");
288
+ const cortexDir = path.join(cwd, ".cortex");
289
+ if (!fs.existsSync(cortexDir)) {
290
+ fs.mkdirSync(cortexDir, { recursive: true });
291
+ }
292
+ fs.writeFileSync(bodyFile, body);
293
+ try {
294
+ const createArgs = [
295
+ "pr", "create",
296
+ "--base", baseBranch,
297
+ "--title", title,
298
+ "--body-file", bodyFile,
299
+ ];
300
+ if (draft)
301
+ createArgs.push("--draft");
302
+ const { stdout } = await gh(cwd, ...createArgs);
303
+ return stdout.trim();
304
+ }
305
+ finally {
306
+ if (fs.existsSync(bodyFile)) {
307
+ fs.unlinkSync(bodyFile);
308
+ }
309
+ }
310
+ }
@@ -1,7 +1,11 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
2
  type Client = PluginInput["client"];
3
3
  type Shell = PluginInput["$"];
4
- export declare const create: {
4
+ /**
5
+ * Factory function that creates the worktree_create tool with access
6
+ * to the OpenCode client for toast notifications.
7
+ */
8
+ export declare function createCreate(client: Client): {
5
9
  description: string;
6
10
  args: {
7
11
  name: import("zod").ZodString;
@@ -25,7 +29,11 @@ export declare const list: {
25
29
  args: {};
26
30
  execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
27
31
  };
28
- export declare const remove: {
32
+ /**
33
+ * Factory function that creates the worktree_remove tool with access
34
+ * to the OpenCode client for toast notifications and PTY cleanup.
35
+ */
36
+ export declare function createRemove(client: Client): {
29
37
  description: string;
30
38
  args: {
31
39
  name: import("zod").ZodString;
@@ -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;AAQvD,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AACpC,KAAK,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;AAE9B,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;CA8DjB,CAAC;AAEH,eAAO,MAAM,IAAI;;;;CAiCf,CAAC;AAEH,eAAO,MAAM,MAAM;;;;;;;;;;CA4DjB,CAAC;AAEH,eAAO,MAAM,IAAI;;;;;;;;CAgDf,CAAC;AA4VH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;;;;;;;;;EAoIxD"}
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"}