@staff0rd/assist 0.219.0 → 0.220.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.
package/README.md CHANGED
@@ -88,7 +88,14 @@ After installation, the `assist` command will be available globally. You can als
88
88
  - `assist prs fixed <comment-id> <sha>` - Reply with commit link and resolve thread
89
89
  - `assist prs wontfix <comment-id> <reason>` - Reply with reason and resolve thread
90
90
  - `assist prs comment <path> <line> <body>` - Add a line comment to the pending review
91
- - `assist review [--no-prompt] [--submit] [--force] [--refine] [--apply] [--verbose]` - Run Claude and Codex in parallel to review the open PR for the current branch. The diff is fetched from GitHub (base SHA → head SHA via `gh pr diff`), so stale local base branches don't pollute the review; fails fast if no PR is open. By default, prompts before posting line-bound comments and then prompts again to submit the pending review (defaulting to no). Cached `claude.md` / `codex.md` / `synthesis.md` are reused when present; if any reviewer is re-run, the synthesis is invalidated. `--no-prompt` skips all confirmations. `--submit` defaults the submit prompt to yes (or auto-submits when combined with `--no-prompt`). `--force` clears all cached files and re-runs every phase. `--refine` skips posting and instead launches an interactive Claude session that walks through `synthesis.md` and edits it in place; a subsequent `assist review` (no flag) reuses the refined file and posts only the surviving findings. `--apply` skips posting and instead launches an interactive Claude session that walks through each remaining finding asking apply/skip; applied findings have their code fix made in the working tree (unstaged) and are removed from `synthesis.md`, while skipped findings stay so a subsequent `assist review` posts them. `--apply` cannot be combined with `--refine`. `--verbose` disables the stacked-spinner UI and falls back to per-line log output (per-tool lines, starting/done lines); non-TTY environments (CI) automatically fall back to verbose-style output
91
+ - `assist review [sha] [options]` - Run Claude and Codex in parallel to review the open PR for the current branch. The diff is fetched from GitHub (base SHA → head SHA via `gh pr diff`), so stale local base branches don't pollute the review; fails fast if no PR is open. By default, prompts before posting line-bound comments and then prompts again to submit the pending review (defaulting to no). Cached `claude.md` / `codex.md` / `synthesis.md` are reused when present; if any reviewer is re-run, the synthesis is invalidated.
92
+ - `[sha]` - Review that commit's diff (`sha^..sha`) instead of the open PR. Files land under `.assist/reviews/<shortSha>/`, no GitHub lookup or posting happens, and `--refine` / `--apply` / `--submit` are rejected
93
+ - `--no-prompt` - Skip all confirmations
94
+ - `--submit` - Default the submit prompt to yes (or auto-submit when combined with `--no-prompt`)
95
+ - `--force` - Clear all cached files and re-run every phase
96
+ - `--refine` - Skip posting; launch an interactive Claude session that walks through `synthesis.md` and edits it in place. A subsequent `assist review` reuses the refined file and posts only the surviving findings
97
+ - `--apply` - Skip posting; launch an interactive Claude session that walks through each finding asking apply/skip. Applied findings are fixed in the working tree (unstaged) and removed from `synthesis.md`; skipped findings stay so a subsequent `assist review` posts them. Cannot be combined with `--refine`
98
+ - `--verbose` - Disable the stacked-spinner UI and fall back to per-line log output. Non-TTY environments (CI) automatically use this mode
92
99
  - `assist news` - Start the news web UI showing latest RSS feed items (same as `news web`)
93
100
  - `assist news add [url]` - Add an RSS feed URL to the config
94
101
  - `assist news web [-p, --port <number>]` - Start a web view of the news feeds (default port 3001)
package/claude/CLAUDE.md CHANGED
@@ -11,6 +11,4 @@ When using extract, name the destination file after the exported function (e.g.
11
11
 
12
12
  Do not modify `claude/settings.json` without asking the user first. Only read-only commands should be added to the allow list — write operations (add, remove, set, delete) must require confirmation.
13
13
 
14
- When the user mentions a Jira issue key (e.g. `BAD-671`, `PROJ-123`), use these commands to fetch context:
15
- - `assist jira view <issue-key>` — print the title and description
16
- - `assist jira ac <issue-key>` — print acceptance criteria
14
+ When the user mentions a Jira issue key (e.g. `BAD-671`, `PROJ-123`), use the Atlassian MCP to fetch context.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.219.0",
9
+ version: "0.220.1",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -6135,9 +6135,9 @@ import { existsSync as existsSync23, mkdirSync as mkdirSync6, readFileSync as re
6135
6135
  import { homedir as homedir6 } from "os";
6136
6136
  import { join as join22 } from "path";
6137
6137
 
6138
- // src/commands/permitCliReads/assertCliExists.ts
6138
+ // src/shared/checkCliAvailable.ts
6139
6139
  import { execSync as execSync17 } from "child_process";
6140
- function assertCliExists(cli) {
6140
+ function checkCliAvailable(cli) {
6141
6141
  const binary = cli.split(/\s+/)[0];
6142
6142
  const opts = {
6143
6143
  encoding: "utf-8",
@@ -6145,16 +6145,24 @@ function assertCliExists(cli) {
6145
6145
  };
6146
6146
  try {
6147
6147
  execSync17(`command -v ${binary}`, opts);
6148
+ return true;
6148
6149
  } catch {
6149
6150
  try {
6150
6151
  execSync17(`where ${binary}`, opts);
6152
+ return true;
6151
6153
  } catch {
6152
- console.error(`CLI "${cli}" not found in PATH`);
6153
- process.exit(1);
6154
+ return false;
6154
6155
  }
6155
6156
  }
6156
6157
  }
6157
6158
 
6159
+ // src/commands/permitCliReads/assertCliExists.ts
6160
+ function assertCliExists(cli) {
6161
+ if (checkCliAvailable(cli)) return;
6162
+ console.error(`CLI "${cli}" not found in PATH`);
6163
+ process.exit(1);
6164
+ }
6165
+
6158
6166
  // src/commands/permitCliReads/colorize.ts
6159
6167
  import chalk66 from "chalk";
6160
6168
  function colorize(plainOutput) {
@@ -11639,60 +11647,6 @@ function registerRefactor(program2) {
11639
11647
  registerRestructure(refactorCommand);
11640
11648
  }
11641
11649
 
11642
- // src/commands/review/buildRequest.ts
11643
- import { execSync as execSync38 } from "child_process";
11644
-
11645
- // src/commands/review/fetchPrDiffInfo.ts
11646
- import { execSync as execSync37 } from "child_process";
11647
- function getCurrentBranch2() {
11648
- return execSync37("git rev-parse --abbrev-ref HEAD", {
11649
- encoding: "utf-8"
11650
- }).trim();
11651
- }
11652
- function fetchPrDiffInfo() {
11653
- const { org, repo } = getRepoInfo();
11654
- const branch = getCurrentBranch2();
11655
- const fields = "number,baseRefName,baseRefOid,headRefName,headRefOid";
11656
- let raw;
11657
- try {
11658
- raw = execSync37(`gh pr view ${branch} --json ${fields} -R ${org}/${repo}`, {
11659
- encoding: "utf-8",
11660
- stdio: ["ignore", "pipe", "pipe"]
11661
- });
11662
- } catch (error) {
11663
- if (error instanceof Error && error.message.includes("no pull requests")) {
11664
- console.error(
11665
- `Error: No open pull request found for branch \`${branch}\`. Open a PR for this branch before running \`assist review\`.`
11666
- );
11667
- process.exit(1);
11668
- }
11669
- throw error;
11670
- }
11671
- const parsed = JSON.parse(raw);
11672
- return {
11673
- prNumber: parsed.number,
11674
- baseRef: parsed.baseRefName,
11675
- baseSha: parsed.baseRefOid,
11676
- headRef: parsed.headRefName,
11677
- headSha: parsed.headRefOid
11678
- };
11679
- }
11680
- function fetchPrChangedFiles(prNumber) {
11681
- const { org, repo } = getRepoInfo();
11682
- const out = execSync37(`gh pr diff ${prNumber} --name-only -R ${org}/${repo}`, {
11683
- encoding: "utf-8",
11684
- maxBuffer: 64 * 1024 * 1024
11685
- });
11686
- return out.trim().split("\n").filter(Boolean);
11687
- }
11688
- function fetchPrDiff(prNumber) {
11689
- const { org, repo } = getRepoInfo();
11690
- return execSync37(`gh pr diff ${prNumber} -R ${org}/${repo}`, {
11691
- encoding: "utf-8",
11692
- maxBuffer: 256 * 1024 * 1024
11693
- });
11694
- }
11695
-
11696
11650
  // src/commands/review/formatPriorComments.ts
11697
11651
  function threadKey(c, byId) {
11698
11652
  if (c.threadId) return c.threadId;
@@ -11743,30 +11697,6 @@ ${blocks.join("\n\n")}`;
11743
11697
  }
11744
11698
 
11745
11699
  // src/commands/review/buildRequest.ts
11746
- function gatherContext() {
11747
- const branch = execSync38("git rev-parse --abbrev-ref HEAD", {
11748
- encoding: "utf-8"
11749
- }).trim();
11750
- const sha = execSync38("git rev-parse HEAD", { encoding: "utf-8" }).trim();
11751
- const shortSha = execSync38("git rev-parse --short=7 HEAD", {
11752
- encoding: "utf-8"
11753
- }).trim();
11754
- const prInfo = fetchPrDiffInfo();
11755
- const changedFiles = fetchPrChangedFiles(prInfo.prNumber);
11756
- const diff2 = fetchPrDiff(prInfo.prNumber);
11757
- return {
11758
- branch,
11759
- sha,
11760
- shortSha,
11761
- prNumber: prInfo.prNumber,
11762
- baseRef: prInfo.baseRef,
11763
- baseSha: prInfo.baseSha,
11764
- headRef: prInfo.headRef,
11765
- headSha: prInfo.headSha,
11766
- changedFiles,
11767
- diff: diff2
11768
- };
11769
- }
11770
11700
  function formatFiles(files) {
11771
11701
  if (files.length === 0) return "(none)";
11772
11702
  return files.map((file) => `- ${file}`).join("\n");
@@ -11790,6 +11720,23 @@ ${formatFiles(context.changedFiles)}
11790
11720
  ${priorBlock}
11791
11721
  ## Diff (PR #${context.prNumber}: ${context.baseSha}..${context.headSha})
11792
11722
 
11723
+ \`\`\`diff
11724
+ ${context.diff.trimEnd()}
11725
+ \`\`\`
11726
+ `;
11727
+ }
11728
+ function buildShaRequest(context) {
11729
+ return `# Code review request
11730
+
11731
+ - Commit: \`${context.sha}\`
11732
+ - Parent: \`${context.parentSha}\`
11733
+
11734
+ ## Changed files
11735
+
11736
+ ${formatFiles(context.changedFiles)}
11737
+
11738
+ ## Diff (commit ${context.sha}: ${context.parentSha}..${context.sha})
11739
+
11793
11740
  \`\`\`diff
11794
11741
  ${context.diff.trimEnd()}
11795
11742
  \`\`\`
@@ -11798,13 +11745,8 @@ ${context.diff.trimEnd()}
11798
11745
 
11799
11746
  // src/commands/review/buildReviewPaths.ts
11800
11747
  import { join as join35 } from "path";
11801
- function buildReviewPaths(repoRoot, branch, shortSha) {
11802
- const reviewDir = join35(
11803
- repoRoot,
11804
- ".assist",
11805
- "reviews",
11806
- `${branch}-${shortSha}`
11807
- );
11748
+ function buildReviewPaths(repoRoot, key) {
11749
+ const reviewDir = join35(repoRoot, ".assist", "reviews", key);
11808
11750
  return {
11809
11751
  reviewDir,
11810
11752
  requestPath: join35(reviewDir, "request.md"),
@@ -11815,9 +11757,9 @@ function buildReviewPaths(repoRoot, branch, shortSha) {
11815
11757
  }
11816
11758
 
11817
11759
  // src/commands/review/fetchExistingComments.ts
11818
- import { execSync as execSync39 } from "child_process";
11760
+ import { execSync as execSync37 } from "child_process";
11819
11761
  function fetchRawComments(org, repo, prNumber) {
11820
- const out = execSync39(
11762
+ const out = execSync37(
11821
11763
  `gh api --paginate repos/${org}/${repo}/pulls/${prNumber}/comments`,
11822
11764
  { encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }
11823
11765
  );
@@ -11847,6 +11789,86 @@ function fetchExistingComments() {
11847
11789
  });
11848
11790
  }
11849
11791
 
11792
+ // src/commands/review/gatherContext.ts
11793
+ import { execSync as execSync39 } from "child_process";
11794
+
11795
+ // src/commands/review/fetchPrDiffInfo.ts
11796
+ import { execSync as execSync38 } from "child_process";
11797
+ function getCurrentBranch2() {
11798
+ return execSync38("git rev-parse --abbrev-ref HEAD", {
11799
+ encoding: "utf-8"
11800
+ }).trim();
11801
+ }
11802
+ function fetchPrDiffInfo() {
11803
+ const { org, repo } = getRepoInfo();
11804
+ const branch = getCurrentBranch2();
11805
+ const fields = "number,baseRefName,baseRefOid,headRefName,headRefOid";
11806
+ let raw;
11807
+ try {
11808
+ raw = execSync38(`gh pr view ${branch} --json ${fields} -R ${org}/${repo}`, {
11809
+ encoding: "utf-8",
11810
+ stdio: ["ignore", "pipe", "pipe"]
11811
+ });
11812
+ } catch (error) {
11813
+ if (error instanceof Error && error.message.includes("no pull requests")) {
11814
+ console.error(
11815
+ `Error: No open pull request found for branch \`${branch}\`. Open a PR for this branch before running \`assist review\`.`
11816
+ );
11817
+ process.exit(1);
11818
+ }
11819
+ throw error;
11820
+ }
11821
+ const parsed = JSON.parse(raw);
11822
+ return {
11823
+ prNumber: parsed.number,
11824
+ baseRef: parsed.baseRefName,
11825
+ baseSha: parsed.baseRefOid,
11826
+ headRef: parsed.headRefName,
11827
+ headSha: parsed.headRefOid
11828
+ };
11829
+ }
11830
+ function fetchPrChangedFiles(prNumber) {
11831
+ const { org, repo } = getRepoInfo();
11832
+ const out = execSync38(`gh pr diff ${prNumber} --name-only -R ${org}/${repo}`, {
11833
+ encoding: "utf-8",
11834
+ maxBuffer: 64 * 1024 * 1024
11835
+ });
11836
+ return out.trim().split("\n").filter(Boolean);
11837
+ }
11838
+ function fetchPrDiff(prNumber) {
11839
+ const { org, repo } = getRepoInfo();
11840
+ return execSync38(`gh pr diff ${prNumber} -R ${org}/${repo}`, {
11841
+ encoding: "utf-8",
11842
+ maxBuffer: 256 * 1024 * 1024
11843
+ });
11844
+ }
11845
+
11846
+ // src/commands/review/gatherContext.ts
11847
+ function gatherContext() {
11848
+ const branch = execSync39("git rev-parse --abbrev-ref HEAD", {
11849
+ encoding: "utf-8"
11850
+ }).trim();
11851
+ const sha = execSync39("git rev-parse HEAD", { encoding: "utf-8" }).trim();
11852
+ const shortSha = execSync39("git rev-parse --short=7 HEAD", {
11853
+ encoding: "utf-8"
11854
+ }).trim();
11855
+ const prInfo = fetchPrDiffInfo();
11856
+ const changedFiles = fetchPrChangedFiles(prInfo.prNumber);
11857
+ const diff2 = fetchPrDiff(prInfo.prNumber);
11858
+ return {
11859
+ branch,
11860
+ sha,
11861
+ shortSha,
11862
+ prNumber: prInfo.prNumber,
11863
+ baseRef: prInfo.baseRef,
11864
+ baseSha: prInfo.baseSha,
11865
+ headRef: prInfo.headRef,
11866
+ headSha: prInfo.headSha,
11867
+ changedFiles,
11868
+ diff: diff2
11869
+ };
11870
+ }
11871
+
11850
11872
  // src/commands/review/postReviewToPr.ts
11851
11873
  import { readFileSync as readFileSync29 } from "fs";
11852
11874
 
@@ -12202,68 +12224,18 @@ async function runApplySession(synthesisPath) {
12202
12224
  await done2;
12203
12225
  }
12204
12226
 
12205
- // src/commands/review/runReviewPipeline.ts
12206
- import { existsSync as existsSync35, unlinkSync as unlinkSync12 } from "fs";
12207
-
12208
- // src/commands/review/buildReviewerStdin.ts
12209
- var REVIEW_PROMPT = `You are acting as a reviewer for a proposed code change made by another engineer. The full review request \u2014 branch, base, changed files, and unified diff \u2014 is in request.md in the current working directory.
12210
-
12211
- Read request.md, then produce a thorough code review in Markdown.
12212
-
12213
- ## When to flag a finding
12214
-
12215
- A finding is worth raising only if all of the following hold:
12216
-
12217
- 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
12218
- 2. The issue is discrete and actionable \u2014 not a vague observation about the codebase or a tangle of several things.
12219
- 3. Fixing it does not demand more rigour than the rest of the codebase already shows (e.g. don't ask for exhaustive input validation in a repo of one-off scripts).
12220
- 4. The issue was introduced by this change. Do not flag pre-existing bugs.
12221
- 5. The original author would likely fix it if made aware.
12222
- 6. It does not rely on unstated assumptions about the codebase or author's intent.
12223
- 7. You can name the concretely affected code path. Speculation that a change *might* disrupt something elsewhere is not enough \u2014 identify the other code that is provably affected.
12224
- 8. It is clearly not an intentional change by the author.
12225
-
12226
- ## How to write the comment (Impact + Recommendation)
12227
-
12228
- 1. Make clear *why* the issue is a bug.
12229
- 2. Communicate severity accurately \u2014 do not inflate.
12230
- 3. Keep it brief: at most one paragraph of prose. Avoid line breaks inside the natural-language flow unless needed for a code fragment.
12231
- 4. Do not paste code chunks longer than 3 lines. Wrap short snippets in inline code or a fenced block.
12232
- 5. State explicitly the scenarios, environments, or inputs needed for the bug to manifest \u2014 and signal up front that severity depends on those factors.
12233
- 6. Tone is matter-of-fact: not accusatory, not gushing. Read as a helpful assistant, not a performative human reviewer.
12234
- 7. Write so the author grasps the point on first read.
12235
- 8. Avoid flattery and filler ("Great job\u2026", "Thanks for\u2026"). They are not useful to the author.
12236
-
12237
- Ignore trivial style unless it obscures meaning or violates a documented standard. One finding per distinct issue.
12238
-
12239
- ## How many findings to return
12240
-
12241
- List every finding that the original author would want to know about and fix. Do not stop at the first qualifying one. If nothing clears the bar above, return no findings \u2014 empty is better than padded.
12242
-
12243
- ## Output format
12244
-
12245
- For each finding include:
12246
- - Severity (blocker, major, minor, nit) \u2014 see rubric below
12247
- - File and line (e.g. \`src/foo.ts:42\`) when the finding is tied to a specific location
12248
- - Impact: what could go wrong, including the conditions under which it manifests
12249
- - Recommendation: a concrete change
12250
-
12251
- Severity rubric:
12252
- - **blocker** \u2014 ships broken behaviour: crash, data loss, security hole, breaks the build or existing tests, or violates a stated requirement.
12253
- - **major** \u2014 likely bug, missing error handling on a real failure mode, or a regression in existing behaviour. Not "this could be cleaner" or "this might be slow."
12254
- - **minor** \u2014 narrow correctness or clarity issue with limited blast radius; worth fixing but not urgent.
12255
- - **nit** \u2014 style, naming, micro-refactors, comment wording; reviewer would not block on it.
12256
-
12257
- Default to the lower tier when uncertain. Code-style preferences, refactor suggestions, and "I would have written it differently" belong in nit \u2014 not major. A finding is only major if you can name a concrete failure mode or regression.
12258
-
12259
- Group findings by severity. If you have no findings in a category, omit it. End with a short overall summary.
12260
-
12261
- Output only the review Markdown. Do not include any preamble or commentary about the process.`;
12262
- function buildReviewerStdin(requestPath) {
12263
- return `${REVIEW_PROMPT}
12264
-
12265
- The review request is at: ${requestPath}
12266
- `;
12227
+ // src/commands/review/cachedReviewerResult.ts
12228
+ import { statSync as statSync4 } from "fs";
12229
+ function cachedReviewerResult(name, outputPath) {
12230
+ let size;
12231
+ try {
12232
+ size = statSync4(outputPath).size;
12233
+ } catch {
12234
+ return null;
12235
+ }
12236
+ if (size === 0) return null;
12237
+ console.log(`[${name}] cached \u2192 ${outputPath} (${size} bytes)`);
12238
+ return { name, outputPath, exitCode: 0, stderr: "" };
12267
12239
  }
12268
12240
 
12269
12241
  // src/commands/review/MultiSpinner.ts
@@ -12368,18 +12340,114 @@ var MultiSpinner = class {
12368
12340
  }
12369
12341
  };
12370
12342
 
12371
- // src/commands/review/cachedReviewerResult.ts
12372
- import { statSync as statSync4 } from "fs";
12373
- function cachedReviewerResult(name, outputPath) {
12374
- let size;
12375
- try {
12376
- size = statSync4(outputPath).size;
12377
- } catch {
12378
- return null;
12343
+ // src/commands/review/ensureCodexAvailable.ts
12344
+ import { spawnSync as spawnSync3 } from "child_process";
12345
+ function runNpmInstall() {
12346
+ const result = spawnSync3("npm", ["install", "-g", "@openai/codex"], {
12347
+ stdio: "inherit",
12348
+ shell: true
12349
+ });
12350
+ return result.status === 0 && result.error === void 0;
12351
+ }
12352
+ async function ensureCodexAvailable() {
12353
+ if (checkCliAvailable("codex")) return "available";
12354
+ console.log("codex CLI was not found on PATH.");
12355
+ const install = await promptConfirm(
12356
+ "Install @openai/codex globally via npm install -g @openai/codex?",
12357
+ true
12358
+ );
12359
+ if (!install) {
12360
+ console.log("Skipping codex reviewer.");
12361
+ return "skipped";
12379
12362
  }
12380
- if (size === 0) return null;
12381
- console.log(`[${name}] cached \u2192 ${outputPath} (${size} bytes)`);
12382
- return { name, outputPath, exitCode: 0, stderr: "" };
12363
+ console.log("Installing @openai/codex...");
12364
+ if (!runNpmInstall()) {
12365
+ console.error(
12366
+ "npm install -g @openai/codex failed. Skipping codex reviewer."
12367
+ );
12368
+ return "skipped";
12369
+ }
12370
+ if (checkCliAvailable("codex")) return "available";
12371
+ console.error(
12372
+ "codex still not found on PATH after install. Skipping codex reviewer."
12373
+ );
12374
+ return "skipped";
12375
+ }
12376
+
12377
+ // src/commands/review/planCodexReviewer.ts
12378
+ async function planCodexReviewer(codexPath) {
12379
+ const cached = cachedReviewerResult("codex", codexPath);
12380
+ if (cached) return { kind: "cached", cached };
12381
+ const status2 = await ensureCodexAvailable();
12382
+ if (status2 === "available") return { kind: "run" };
12383
+ return { kind: "skipped" };
12384
+ }
12385
+ function skippedCodexResult(outputPath) {
12386
+ return { name: "codex", outputPath, exitCode: 0, stderr: "" };
12387
+ }
12388
+
12389
+ // src/commands/review/runAndSynthesise.ts
12390
+ import { existsSync as existsSync35, unlinkSync as unlinkSync12 } from "fs";
12391
+
12392
+ // src/commands/review/buildReviewerStdin.ts
12393
+ var REVIEW_PROMPT = `You are acting as a reviewer for a proposed code change made by another engineer. The full review request \u2014 branch, base, changed files, and unified diff \u2014 is in request.md in the current working directory.
12394
+
12395
+ Read request.md, then produce a thorough code review in Markdown.
12396
+
12397
+ ## When to flag a finding
12398
+
12399
+ A finding is worth raising only if all of the following hold:
12400
+
12401
+ 1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
12402
+ 2. The issue is discrete and actionable \u2014 not a vague observation about the codebase or a tangle of several things.
12403
+ 3. Fixing it does not demand more rigour than the rest of the codebase already shows (e.g. don't ask for exhaustive input validation in a repo of one-off scripts).
12404
+ 4. The issue was introduced by this change. Do not flag pre-existing bugs.
12405
+ 5. The original author would likely fix it if made aware.
12406
+ 6. It does not rely on unstated assumptions about the codebase or author's intent.
12407
+ 7. You can name the concretely affected code path. Speculation that a change *might* disrupt something elsewhere is not enough \u2014 identify the other code that is provably affected.
12408
+ 8. It is clearly not an intentional change by the author.
12409
+
12410
+ ## How to write the comment (Impact + Recommendation)
12411
+
12412
+ 1. Make clear *why* the issue is a bug.
12413
+ 2. Communicate severity accurately \u2014 do not inflate.
12414
+ 3. Keep it brief: at most one paragraph of prose. Avoid line breaks inside the natural-language flow unless needed for a code fragment.
12415
+ 4. Do not paste code chunks longer than 3 lines. Wrap short snippets in inline code or a fenced block.
12416
+ 5. State explicitly the scenarios, environments, or inputs needed for the bug to manifest \u2014 and signal up front that severity depends on those factors.
12417
+ 6. Tone is matter-of-fact: not accusatory, not gushing. Read as a helpful assistant, not a performative human reviewer.
12418
+ 7. Write so the author grasps the point on first read.
12419
+ 8. Avoid flattery and filler ("Great job\u2026", "Thanks for\u2026"). They are not useful to the author.
12420
+
12421
+ Ignore trivial style unless it obscures meaning or violates a documented standard. One finding per distinct issue.
12422
+
12423
+ ## How many findings to return
12424
+
12425
+ List every finding that the original author would want to know about and fix. Do not stop at the first qualifying one. If nothing clears the bar above, return no findings \u2014 empty is better than padded.
12426
+
12427
+ ## Output format
12428
+
12429
+ For each finding include:
12430
+ - Severity (blocker, major, minor, nit) \u2014 see rubric below
12431
+ - File and line (e.g. \`src/foo.ts:42\`) when the finding is tied to a specific location
12432
+ - Impact: what could go wrong, including the conditions under which it manifests
12433
+ - Recommendation: a concrete change
12434
+
12435
+ Severity rubric:
12436
+ - **blocker** \u2014 ships broken behaviour: crash, data loss, security hole, breaks the build or existing tests, or violates a stated requirement.
12437
+ - **major** \u2014 likely bug, missing error handling on a real failure mode, or a regression in existing behaviour. Not "this could be cleaner" or "this might be slow."
12438
+ - **minor** \u2014 narrow correctness or clarity issue with limited blast radius; worth fixing but not urgent.
12439
+ - **nit** \u2014 style, naming, micro-refactors, comment wording; reviewer would not block on it.
12440
+
12441
+ Default to the lower tier when uncertain. Code-style preferences, refactor suggestions, and "I would have written it differently" belong in nit \u2014 not major. A finding is only major if you can name a concrete failure mode or regression.
12442
+
12443
+ Group findings by severity. If you have no findings in a category, omit it. End with a short overall summary.
12444
+
12445
+ Output only the review Markdown. Do not include any preamble or commentary about the process.`;
12446
+ function buildReviewerStdin(requestPath) {
12447
+ return `${REVIEW_PROMPT}
12448
+
12449
+ The review request is at: ${requestPath}
12450
+ `;
12383
12451
  }
12384
12452
 
12385
12453
  // src/commands/review/printReviewerFailures.ts
@@ -12563,42 +12631,90 @@ function handleChildClose(args) {
12563
12631
  return { exitCode, elapsedMs };
12564
12632
  }
12565
12633
 
12634
+ // src/commands/review/handleSpawnError.ts
12635
+ function messageFor(err, command) {
12636
+ if (err.code === "ENOENT") return `command not found: ${command}`;
12637
+ return err.message || String(err);
12638
+ }
12639
+ function handleSpawnError(ctx, err) {
12640
+ const message = messageFor(err, ctx.command);
12641
+ const stderr = ctx.stderr ? `${ctx.stderr}
12642
+ ${message}` : message;
12643
+ if (!ctx.quiet) console.error(`[${ctx.name}] failed: ${message}`);
12644
+ return {
12645
+ exitCode: 127,
12646
+ stderr,
12647
+ elapsedMs: Date.now() - ctx.startedAt
12648
+ };
12649
+ }
12650
+
12651
+ // src/commands/review/waitForChildExit.ts
12652
+ function onErrorResult(ctx, err) {
12653
+ ctx.flushPending();
12654
+ return handleSpawnError(
12655
+ {
12656
+ command: ctx.command,
12657
+ name: ctx.name,
12658
+ stderr: ctx.stderr.value,
12659
+ startedAt: ctx.startedAt,
12660
+ quiet: ctx.quiet
12661
+ },
12662
+ err
12663
+ );
12664
+ }
12665
+ function onCloseResult(ctx, code) {
12666
+ ctx.flushPending();
12667
+ const closed = handleChildClose({
12668
+ code,
12669
+ startedAt: ctx.startedAt,
12670
+ name: ctx.name,
12671
+ stderr: ctx.stderr.value,
12672
+ quiet: ctx.quiet
12673
+ });
12674
+ return { ...closed, stderr: ctx.stderr.value };
12675
+ }
12676
+ function waitForChildExit(ctx) {
12677
+ return new Promise((resolve15) => {
12678
+ let settled = false;
12679
+ const settle = (result) => {
12680
+ if (settled) return;
12681
+ settled = true;
12682
+ resolve15(result);
12683
+ };
12684
+ ctx.child.on("error", (err) => settle(onErrorResult(ctx, err)));
12685
+ ctx.child.on("close", (code) => settle(onCloseResult(ctx, code)));
12686
+ });
12687
+ }
12688
+
12566
12689
  // src/commands/review/runStreamingChild.ts
12690
+ function writeStdinSafely(child, payload) {
12691
+ child.stdin?.on("error", () => {
12692
+ });
12693
+ try {
12694
+ child.stdin?.write(payload);
12695
+ child.stdin?.end();
12696
+ } catch {
12697
+ }
12698
+ }
12567
12699
  function startChild(spec) {
12568
12700
  const child = spawn6(spec.command, spec.args, {
12569
12701
  stdio: ["pipe", "pipe", "pipe"]
12570
12702
  });
12571
12703
  const flushPending = attachLineParser(child, spec.onLine);
12572
12704
  const stderr = attachStderrCollector(child);
12573
- child.stdin?.write(spec.stdin);
12574
- child.stdin?.end();
12705
+ writeStdinSafely(child, spec.stdin);
12575
12706
  return { child, flushPending, stderr };
12576
12707
  }
12577
- function waitForExit(ctx) {
12578
- return new Promise((resolve15, reject) => {
12579
- ctx.child.on("error", reject);
12580
- ctx.child.on("close", (code) => {
12581
- ctx.flushPending();
12582
- const closed = handleChildClose({
12583
- code,
12584
- startedAt: ctx.startedAt,
12585
- name: ctx.name,
12586
- stderr: ctx.stderr.value,
12587
- quiet: ctx.quiet
12588
- });
12589
- resolve15({ ...closed, stderr: ctx.stderr.value });
12590
- });
12591
- });
12592
- }
12593
12708
  function runStreamingChild(spec) {
12594
12709
  const startedAt = Date.now();
12595
12710
  if (!spec.quiet) console.log(`[${spec.name}] starting`);
12596
12711
  const { child, flushPending, stderr } = startChild(spec);
12597
- return waitForExit({
12712
+ return waitForChildExit({
12598
12713
  child,
12599
12714
  flushPending,
12600
12715
  stderr,
12601
12716
  name: spec.name,
12717
+ command: spec.command,
12602
12718
  startedAt,
12603
12719
  quiet: spec.quiet ?? false
12604
12720
  });
@@ -12636,6 +12752,19 @@ async function runClaudeReviewer(spec) {
12636
12752
  return finaliseReviewerRun(spec, spinner, result);
12637
12753
  }
12638
12754
 
12755
+ // src/commands/review/resolveClaude.ts
12756
+ function resolveClaude(args) {
12757
+ if (args.cached) return Promise.resolve(args.cached);
12758
+ const spinner = args.multi?.create("claude \u2014 starting");
12759
+ return runClaudeReviewer({
12760
+ name: "claude",
12761
+ reviewDir: args.reviewDir,
12762
+ stdin: args.stdin,
12763
+ outputPath: args.claudePath,
12764
+ spinner
12765
+ });
12766
+ }
12767
+
12639
12768
  // src/commands/review/runCodexReviewer.ts
12640
12769
  import { existsSync as existsSync34, unlinkSync as unlinkSync11 } from "fs";
12641
12770
 
@@ -12696,38 +12825,42 @@ async function runCodexReviewer(spec) {
12696
12825
  return finaliseReviewerRun(spec, spinner, result);
12697
12826
  }
12698
12827
 
12699
- // src/commands/review/runReviewers.ts
12700
- function spinnerFor(multi, name, cached) {
12701
- if (cached || !multi) return void 0;
12702
- return multi.create(`${name} \u2014 starting`);
12828
+ // src/commands/review/resolveCodex.ts
12829
+ function resolveCodex(args) {
12830
+ if (args.plan.kind === "cached") return Promise.resolve(args.plan.cached);
12831
+ if (args.plan.kind === "skipped") {
12832
+ return Promise.resolve(skippedCodexResult(args.codexPath));
12833
+ }
12834
+ const spinner = args.multi?.create("codex \u2014 starting");
12835
+ return runCodexReviewer({
12836
+ name: "codex",
12837
+ reviewDir: args.reviewDir,
12838
+ stdin: args.stdin,
12839
+ outputPath: args.codexPath,
12840
+ spinner
12841
+ });
12703
12842
  }
12843
+
12844
+ // src/commands/review/runReviewers.ts
12704
12845
  async function runReviewers(reviewDir, claudePath, codexPath, stdinPrompt, options2) {
12705
- const cachedClaude = cachedReviewerResult("claude", claudePath);
12706
- const cachedCodex = cachedReviewerResult("codex", codexPath);
12707
- if (cachedClaude && cachedCodex) {
12708
- return { results: [cachedClaude, cachedCodex], anyFresh: false };
12709
- }
12710
- const { multi } = options2;
12711
- const claudeSpinner = spinnerFor(multi, "claude", cachedClaude);
12712
- const codexSpinner = spinnerFor(multi, "codex", cachedCodex);
12713
- const results = await Promise.all([
12714
- cachedClaude ? Promise.resolve(cachedClaude) : runClaudeReviewer({
12715
- name: "claude",
12716
- reviewDir,
12717
- stdin: stdinPrompt,
12718
- outputPath: claudePath,
12719
- spinner: claudeSpinner
12720
- }),
12721
- cachedCodex ? Promise.resolve(cachedCodex) : runCodexReviewer({
12722
- name: "codex",
12723
- reviewDir,
12724
- stdin: stdinPrompt,
12725
- outputPath: codexPath,
12726
- spinner: codexSpinner
12727
- })
12728
- ]);
12729
- if (multi) printReviewerFailures(results);
12730
- return { results, anyFresh: true };
12846
+ const claudePromise = resolveClaude({
12847
+ reviewDir,
12848
+ claudePath,
12849
+ stdin: stdinPrompt,
12850
+ cached: options2.cachedClaude,
12851
+ multi: options2.multi
12852
+ });
12853
+ const codexPromise = resolveCodex({
12854
+ reviewDir,
12855
+ codexPath,
12856
+ stdin: stdinPrompt,
12857
+ plan: options2.codexPlan,
12858
+ multi: options2.multi
12859
+ });
12860
+ const results = await Promise.all([claudePromise, codexPromise]);
12861
+ if (options2.multi) printReviewerFailures(results);
12862
+ const anyFresh = options2.cachedClaude === null || options2.codexPlan.kind !== "cached";
12863
+ return { results, anyFresh };
12731
12864
  }
12732
12865
 
12733
12866
  // src/commands/review/synthesise.ts
@@ -12736,7 +12869,9 @@ import { readFileSync as readFileSync30 } from "fs";
12736
12869
  // src/commands/review/buildSynthesisStdin.ts
12737
12870
  var SYNTHESIS_PROMPT = `You are consolidating two independent code reviews of the same change. The original review request is in request.md. The two reviews are in claude.md and codex.md in the current working directory.
12738
12871
 
12739
- Read all three files, deduplicate findings, and produce a single consolidated review in Markdown with this exact structure:
12872
+ If codex.md does not exist on disk, the codex reviewer was skipped (CLI unavailable). In that case, work from claude.md alone; treat every finding as 'claude-only', do not mark anything 'confirmed' or 'codex-only', and add a note in the Summary that the codex reviewer was skipped.
12873
+
12874
+ Read all available review files, deduplicate findings, and produce a single consolidated review in Markdown with this exact structure:
12740
12875
 
12741
12876
  # Code review synthesis
12742
12877
 
@@ -12816,6 +12951,29 @@ async function synthesise(paths, options2) {
12816
12951
  return result;
12817
12952
  }
12818
12953
 
12954
+ // src/commands/review/runAndSynthesise.ts
12955
+ async function runAndSynthesise(args) {
12956
+ const { paths, multi } = args;
12957
+ const { results, anyFresh } = await runReviewers(
12958
+ paths.reviewDir,
12959
+ paths.claudePath,
12960
+ paths.codexPath,
12961
+ buildReviewerStdin(paths.requestPath),
12962
+ { multi, codexPlan: args.codexPlan, cachedClaude: args.cachedClaude }
12963
+ );
12964
+ if (results.every((r) => r.exitCode !== 0)) {
12965
+ console.error(
12966
+ "Both reviewers failed; skipping synthesis. See review folder for stderr details."
12967
+ );
12968
+ return false;
12969
+ }
12970
+ if (anyFresh && existsSync35(paths.synthesisPath)) {
12971
+ unlinkSync12(paths.synthesisPath);
12972
+ }
12973
+ const synthesisResult = await synthesise(paths, { multi });
12974
+ return synthesisResult.exitCode === 0;
12975
+ }
12976
+
12819
12977
  // src/commands/review/useSpinnerUi.ts
12820
12978
  function useSpinnerUi(verbose) {
12821
12979
  if (verbose) return false;
@@ -12837,27 +12995,16 @@ function finishUi(ui, ok) {
12837
12995
  else ui.elapsed.fail(label2);
12838
12996
  }
12839
12997
  async function runReviewPipeline(paths, options2) {
12998
+ const cachedClaude = cachedReviewerResult("claude", paths.claudePath);
12999
+ const codexPlan = await planCodexReviewer(paths.codexPath);
12840
13000
  const ui = createUi(useSpinnerUi(options2.verbose));
12841
13001
  try {
12842
- const { results, anyFresh } = await runReviewers(
12843
- paths.reviewDir,
12844
- paths.claudePath,
12845
- paths.codexPath,
12846
- buildReviewerStdin(paths.requestPath),
12847
- { multi: ui.multi }
12848
- );
12849
- if (results.every((r) => r.exitCode !== 0)) {
12850
- console.error(
12851
- "Both reviewers failed; skipping synthesis. See review folder for stderr details."
12852
- );
12853
- finishUi(ui, false);
12854
- return false;
12855
- }
12856
- if (anyFresh && existsSync35(paths.synthesisPath)) {
12857
- unlinkSync12(paths.synthesisPath);
12858
- }
12859
- const synthesisResult = await synthesise(paths, { multi: ui.multi });
12860
- const ok = synthesisResult.exitCode === 0;
13002
+ const ok = await runAndSynthesise({
13003
+ paths,
13004
+ cachedClaude,
13005
+ codexPlan,
13006
+ multi: ui.multi
13007
+ });
12861
13008
  finishUi(ui, ok);
12862
13009
  return ok;
12863
13010
  } catch (err) {
@@ -12866,19 +13013,7 @@ async function runReviewPipeline(paths, options2) {
12866
13013
  }
12867
13014
  }
12868
13015
 
12869
- // src/commands/review/review.ts
12870
- function resolveRepoRoot() {
12871
- const repoRoot = findRepoRoot(process.cwd());
12872
- if (repoRoot) return repoRoot;
12873
- console.error("Error: not inside a git repository.");
12874
- process.exit(1);
12875
- }
12876
- function validateOptions(options2) {
12877
- if (options2.apply && options2.refine) {
12878
- console.error("Error: --apply cannot be combined with --refine.");
12879
- process.exit(1);
12880
- }
12881
- }
13016
+ // src/commands/review/reviewPr.ts
12882
13017
  function logPriorComments(count) {
12883
13018
  if (count === 0) return;
12884
13019
  console.log(`Including ${count} prior review comment(s) in request.md.`);
@@ -12892,7 +13027,10 @@ function gatherChangedContext() {
12892
13027
  process.exit(1);
12893
13028
  }
12894
13029
  function setupReviewDir(repoRoot, context, force) {
12895
- const paths = buildReviewPaths(repoRoot, context.branch, context.shortSha);
13030
+ const paths = buildReviewPaths(
13031
+ repoRoot,
13032
+ `${context.branch}-${context.shortSha}`
13033
+ );
12896
13034
  const priorComments = fetchExistingComments();
12897
13035
  logPriorComments(priorComments?.length ?? 0);
12898
13036
  prepareReviewDir(paths, buildRequest(context, priorComments), force);
@@ -12910,9 +13048,7 @@ async function runPostSynthesis(synthesisPath, options2) {
12910
13048
  submit: options2.submit ?? false
12911
13049
  });
12912
13050
  }
12913
- async function review(options2 = {}) {
12914
- validateOptions(options2);
12915
- const repoRoot = resolveRepoRoot();
13051
+ async function reviewPr(repoRoot, options2) {
12916
13052
  const context = gatherChangedContext();
12917
13053
  const paths = setupReviewDir(repoRoot, context, options2.force ?? false);
12918
13054
  const synthesisOk = await runReviewPipeline(paths, {
@@ -12922,10 +13058,100 @@ async function review(options2 = {}) {
12922
13058
  console.log(`Done. Review folder: ${paths.reviewDir}`);
12923
13059
  }
12924
13060
 
13061
+ // src/commands/review/gatherShaContext.ts
13062
+ import { execSync as execSync40 } from "child_process";
13063
+ function resolveSha(ref, format2) {
13064
+ const flag = format2 === "short" ? "--short=7 " : "";
13065
+ try {
13066
+ return execSync40(`git rev-parse --verify ${flag}${ref}^{commit}`, {
13067
+ encoding: "utf-8",
13068
+ stdio: ["ignore", "pipe", "pipe"]
13069
+ }).trim();
13070
+ } catch {
13071
+ console.error(`Error: could not resolve commit \`${ref}\`.`);
13072
+ process.exit(1);
13073
+ }
13074
+ }
13075
+ function gatherShaContext(ref) {
13076
+ const sha = resolveSha(ref, "long");
13077
+ const shortSha = resolveSha(sha, "short");
13078
+ const parentSha = resolveSha(`${sha}^`, "long");
13079
+ const range = `${parentSha}..${sha}`;
13080
+ const changedFiles = execSync40(`git diff --name-only ${range}`, {
13081
+ encoding: "utf-8",
13082
+ maxBuffer: 64 * 1024 * 1024
13083
+ }).trim().split("\n").filter(Boolean);
13084
+ const diff2 = execSync40(`git diff ${range}`, {
13085
+ encoding: "utf-8",
13086
+ maxBuffer: 256 * 1024 * 1024
13087
+ });
13088
+ return { sha, shortSha, parentSha, changedFiles, diff: diff2 };
13089
+ }
13090
+
13091
+ // src/commands/review/reviewSha.ts
13092
+ function gatherShaChangedContext(ref) {
13093
+ const context = gatherShaContext(ref);
13094
+ if (context.changedFiles.length > 0) return context;
13095
+ console.error(
13096
+ `Error: commit ${context.sha} has no changed files \u2014 nothing to review.`
13097
+ );
13098
+ process.exit(1);
13099
+ }
13100
+ function setupShaReviewDir(repoRoot, context, force) {
13101
+ const paths = buildReviewPaths(repoRoot, context.shortSha);
13102
+ prepareReviewDir(paths, buildShaRequest(context), force);
13103
+ console.log(`Review folder: ${paths.reviewDir}`);
13104
+ return paths;
13105
+ }
13106
+ async function reviewSha(repoRoot, options2) {
13107
+ const context = gatherShaChangedContext(options2.sha);
13108
+ const paths = setupShaReviewDir(repoRoot, context, options2.force ?? false);
13109
+ await runReviewPipeline(paths, { verbose: options2.verbose ?? false });
13110
+ console.log(`Done. Review folder: ${paths.reviewDir}`);
13111
+ }
13112
+
13113
+ // src/commands/review/review.ts
13114
+ function resolveRepoRoot() {
13115
+ const repoRoot = findRepoRoot(process.cwd());
13116
+ if (repoRoot) return repoRoot;
13117
+ console.error("Error: not inside a git repository.");
13118
+ process.exit(1);
13119
+ }
13120
+ function rejectShaFlag(flag) {
13121
+ console.error(`Error: ${flag} cannot be combined with a SHA argument.`);
13122
+ process.exit(1);
13123
+ }
13124
+ function validateOptions(options2) {
13125
+ if (options2.apply && options2.refine) {
13126
+ console.error("Error: --apply cannot be combined with --refine.");
13127
+ process.exit(1);
13128
+ }
13129
+ if (!options2.sha) return;
13130
+ if (options2.refine) rejectShaFlag("--refine");
13131
+ if (options2.apply) rejectShaFlag("--apply");
13132
+ if (options2.submit) rejectShaFlag("--submit");
13133
+ }
13134
+ async function review(options2 = {}) {
13135
+ validateOptions(options2);
13136
+ const repoRoot = resolveRepoRoot();
13137
+ if (options2.sha) {
13138
+ await reviewSha(repoRoot, {
13139
+ sha: options2.sha,
13140
+ force: options2.force,
13141
+ verbose: options2.verbose
13142
+ });
13143
+ return;
13144
+ }
13145
+ await reviewPr(repoRoot, options2);
13146
+ }
13147
+
12925
13148
  // src/commands/registerReview.ts
12926
13149
  function registerReview(program2) {
12927
13150
  program2.command("review").description(
12928
- "Run Claude and Codex in parallel to review the current branch"
13151
+ "Run Claude and Codex in parallel to review the current branch, or a single commit when a SHA is given"
13152
+ ).argument(
13153
+ "[sha]",
13154
+ "Optional commit SHA to review (sha^..sha); when provided, no PR lookup or GitHub posting happens"
12929
13155
  ).option(
12930
13156
  "--no-prompt",
12931
13157
  "Skip confirmation prompts; use flag defaults non-interactively"
@@ -12944,7 +13170,9 @@ function registerReview(program2) {
12944
13170
  ).option(
12945
13171
  "--verbose",
12946
13172
  "Disable spinner UI and use per-line log output (per-tool lines, starting/done lines)"
12947
- ).action((options2) => review(options2));
13173
+ ).action(
13174
+ (sha, options2) => review({ ...options2, sha })
13175
+ );
12948
13176
  }
12949
13177
 
12950
13178
  // src/commands/seq/seqAuth.ts
@@ -14218,7 +14446,7 @@ function registerVerify(program2) {
14218
14446
  }
14219
14447
 
14220
14448
  // src/commands/voice/devices.ts
14221
- import { spawnSync as spawnSync3 } from "child_process";
14449
+ import { spawnSync as spawnSync4 } from "child_process";
14222
14450
  import { join as join43 } from "path";
14223
14451
 
14224
14452
  // src/commands/voice/shared.ts
@@ -14251,7 +14479,7 @@ function getLockFile() {
14251
14479
  // src/commands/voice/devices.ts
14252
14480
  function devices() {
14253
14481
  const script = join43(getPythonDir(), "list_devices.py");
14254
- spawnSync3(getVenvPython(), [script], { stdio: "inherit" });
14482
+ spawnSync4(getVenvPython(), [script], { stdio: "inherit" });
14255
14483
  }
14256
14484
 
14257
14485
  // src/commands/voice/logs.ts
@@ -14283,12 +14511,12 @@ function logs(options2) {
14283
14511
  }
14284
14512
 
14285
14513
  // src/commands/voice/setup.ts
14286
- import { spawnSync as spawnSync4 } from "child_process";
14514
+ import { spawnSync as spawnSync5 } from "child_process";
14287
14515
  import { mkdirSync as mkdirSync14 } from "fs";
14288
14516
  import { join as join45 } from "path";
14289
14517
 
14290
14518
  // src/commands/voice/checkLockFile.ts
14291
- import { execSync as execSync40 } from "child_process";
14519
+ import { execSync as execSync41 } from "child_process";
14292
14520
  import { existsSync as existsSync42, mkdirSync as mkdirSync13, readFileSync as readFileSync34, writeFileSync as writeFileSync29 } from "fs";
14293
14521
  import { join as join44 } from "path";
14294
14522
  function isProcessAlive2(pid) {
@@ -14317,7 +14545,7 @@ function bootstrapVenv() {
14317
14545
  if (existsSync42(getVenvPython())) return;
14318
14546
  console.log("Setting up Python environment...");
14319
14547
  const pythonDir = getPythonDir();
14320
- execSync40(
14548
+ execSync41(
14321
14549
  `uv sync --project "${pythonDir}" --extra runtime --no-install-project`,
14322
14550
  {
14323
14551
  stdio: "inherit",
@@ -14344,7 +14572,7 @@ function setup() {
14344
14572
  bootstrapVenv();
14345
14573
  console.log("\nDownloading models...\n");
14346
14574
  const script = join45(getPythonDir(), "setup_models.py");
14347
- const result = spawnSync4(getVenvPython(), [script], {
14575
+ const result = spawnSync5(getVenvPython(), [script], {
14348
14576
  stdio: "inherit",
14349
14577
  env: { ...process.env, VOICE_LOG_FILE: voicePaths.log }
14350
14578
  });
@@ -14484,11 +14712,11 @@ import { randomBytes } from "crypto";
14484
14712
  import chalk143 from "chalk";
14485
14713
 
14486
14714
  // src/lib/openBrowser.ts
14487
- import { execSync as execSync41 } from "child_process";
14715
+ import { execSync as execSync42 } from "child_process";
14488
14716
  function tryExec(commands) {
14489
14717
  for (const cmd of commands) {
14490
14718
  try {
14491
- execSync41(cmd);
14719
+ execSync42(cmd);
14492
14720
  return true;
14493
14721
  } catch {
14494
14722
  }
@@ -14830,11 +15058,11 @@ function resolveParams(params, cliArgs) {
14830
15058
  }
14831
15059
 
14832
15060
  // src/commands/run/runPreCommands.ts
14833
- import { execSync as execSync42 } from "child_process";
15061
+ import { execSync as execSync43 } from "child_process";
14834
15062
  function runPreCommands(pre, cwd) {
14835
15063
  for (const cmd of pre) {
14836
15064
  try {
14837
- execSync42(cmd, { stdio: "inherit", cwd });
15065
+ execSync43(cmd, { stdio: "inherit", cwd });
14838
15066
  } catch (err) {
14839
15067
  const code = err && typeof err === "object" && "status" in err ? err.status : 1;
14840
15068
  process.exit(code);
@@ -15097,7 +15325,7 @@ function registerRun(program2) {
15097
15325
  }
15098
15326
 
15099
15327
  // src/commands/screenshot/index.ts
15100
- import { execSync as execSync43 } from "child_process";
15328
+ import { execSync as execSync44 } from "child_process";
15101
15329
  import { existsSync as existsSync47, mkdirSync as mkdirSync17, unlinkSync as unlinkSync15, writeFileSync as writeFileSync32 } from "fs";
15102
15330
  import { tmpdir as tmpdir7 } from "os";
15103
15331
  import { join as join51, resolve as resolve13 } from "path";
@@ -15240,7 +15468,7 @@ function runPowerShellScript(processName, outputPath) {
15240
15468
  const scriptPath = join51(tmpdir7(), `assist-screenshot-${Date.now()}.ps1`);
15241
15469
  writeFileSync32(scriptPath, captureWindowPs1, "utf-8");
15242
15470
  try {
15243
- execSync43(
15471
+ execSync44(
15244
15472
  `powershell -NoProfile -ExecutionPolicy Bypass -File "${scriptPath}" -ProcessName "${processName}" -OutputPath "${outputPath}"`,
15245
15473
  { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }
15246
15474
  );
@@ -15641,7 +15869,7 @@ function syncCommands(claudeDir, targetBase) {
15641
15869
  }
15642
15870
 
15643
15871
  // src/commands/update.ts
15644
- import { execSync as execSync44 } from "child_process";
15872
+ import { execSync as execSync45 } from "child_process";
15645
15873
  import * as path51 from "path";
15646
15874
  function isGlobalNpmInstall(dir) {
15647
15875
  try {
@@ -15649,7 +15877,7 @@ function isGlobalNpmInstall(dir) {
15649
15877
  if (resolved.split(path51.sep).includes("node_modules")) {
15650
15878
  return true;
15651
15879
  }
15652
- const globalPrefix = execSync44("npm prefix -g", { stdio: "pipe" }).toString().trim();
15880
+ const globalPrefix = execSync45("npm prefix -g", { stdio: "pipe" }).toString().trim();
15653
15881
  return resolved.toLowerCase().startsWith(path51.resolve(globalPrefix).toLowerCase());
15654
15882
  } catch {
15655
15883
  return false;
@@ -15660,18 +15888,18 @@ async function update2() {
15660
15888
  console.log(`Assist is installed at: ${installDir}`);
15661
15889
  if (isGitRepo(installDir)) {
15662
15890
  console.log("Detected git repo installation, pulling latest...");
15663
- execSync44("git pull", { cwd: installDir, stdio: "inherit" });
15891
+ execSync45("git pull", { cwd: installDir, stdio: "inherit" });
15664
15892
  console.log("Installing dependencies...");
15665
- execSync44("npm i", { cwd: installDir, stdio: "inherit" });
15893
+ execSync45("npm i", { cwd: installDir, stdio: "inherit" });
15666
15894
  console.log("Building...");
15667
- execSync44("npm run build", { cwd: installDir, stdio: "inherit" });
15895
+ execSync45("npm run build", { cwd: installDir, stdio: "inherit" });
15668
15896
  console.log("Syncing commands...");
15669
- execSync44("assist sync", { stdio: "inherit" });
15897
+ execSync45("assist sync", { stdio: "inherit" });
15670
15898
  } else if (isGlobalNpmInstall(installDir)) {
15671
15899
  console.log("Detected global npm installation, updating...");
15672
- execSync44("npm i -g @staff0rd/assist@latest", { stdio: "inherit" });
15900
+ execSync45("npm i -g @staff0rd/assist@latest", { stdio: "inherit" });
15673
15901
  console.log("Syncing commands...");
15674
- execSync44("assist sync", { stdio: "inherit" });
15902
+ execSync45("assist sync", { stdio: "inherit" });
15675
15903
  } else {
15676
15904
  console.error(
15677
15905
  "Could not determine installation method. Expected a git repo or global npm install."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.219.0",
3
+ "version": "0.220.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {