@staff0rd/assist 0.214.1 → 0.215.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +124 -53
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -88,7 +88,7 @@ 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] [--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. `--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 [--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
92
92
  - `assist news` - Start the news web UI showing latest RSS feed items (same as `news web`)
93
93
  - `assist news add [url]` - Add an RSS feed URL to the config
94
94
  - `assist news web [-p, --port <number>]` - Start a web view of the news feeds (default port 3001)
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.214.1",
9
+ version: "0.215.1",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -10247,7 +10247,7 @@ async function check(pattern2, options2) {
10247
10247
  }
10248
10248
 
10249
10249
  // src/commands/refactor/extract/index.ts
10250
- import path36 from "path";
10250
+ import path37 from "path";
10251
10251
  import chalk120 from "chalk";
10252
10252
 
10253
10253
  // src/commands/refactor/extract/applyExtraction.ts
@@ -10844,35 +10844,63 @@ function displayPlan(functionName, relDest, plan2, cwd) {
10844
10844
  }
10845
10845
 
10846
10846
  // src/commands/refactor/extract/loadProjectFile.ts
10847
+ import path36 from "path";
10848
+ import chalk119 from "chalk";
10849
+ import { Project as Project3 } from "ts-morph";
10850
+
10851
+ // src/commands/refactor/extract/findTsConfig.ts
10847
10852
  import fs19 from "fs";
10848
10853
  import path35 from "path";
10849
- import chalk119 from "chalk";
10850
10854
  import { Project as Project2 } from "ts-morph";
10851
10855
  function findTsConfig(sourcePath) {
10852
10856
  const rootConfig = path35.resolve("tsconfig.json");
10853
10857
  if (!fs19.existsSync(rootConfig)) return rootConfig;
10854
- const raw = fs19.readFileSync(rootConfig, "utf-8");
10858
+ const tried = /* @__PURE__ */ new Set();
10859
+ const candidates = [rootConfig, ...readReferences(rootConfig)];
10860
+ for (const candidate of candidates) {
10861
+ if (tried.has(candidate)) continue;
10862
+ tried.add(candidate);
10863
+ if (projectIncludes(candidate, sourcePath)) return candidate;
10864
+ }
10865
+ const siblings = fs19.readdirSync(path35.dirname(rootConfig)).filter((f) => /^tsconfig.*\.json$/.test(f)).map((f) => path35.resolve(path35.dirname(rootConfig), f));
10866
+ for (const sibling of siblings) {
10867
+ if (tried.has(sibling)) continue;
10868
+ tried.add(sibling);
10869
+ if (projectIncludes(sibling, sourcePath)) return sibling;
10870
+ }
10871
+ return rootConfig;
10872
+ }
10873
+ function readReferences(configPath) {
10874
+ if (!fs19.existsSync(configPath)) return [];
10875
+ const raw = fs19.readFileSync(configPath, "utf-8");
10855
10876
  const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
10856
10877
  let parsed;
10857
10878
  try {
10858
10879
  parsed = JSON.parse(stripped);
10859
10880
  } catch {
10860
- return rootConfig;
10881
+ return [];
10861
10882
  }
10862
- if (!parsed.references?.length) return rootConfig;
10863
- for (const ref of parsed.references) {
10864
- const refPath = path35.resolve(ref.path);
10865
- const configPath = fs19.statSync(refPath, { throwIfNoEntry: false })?.isDirectory() ? path35.join(refPath, "tsconfig.json") : refPath;
10866
- if (!fs19.existsSync(configPath)) continue;
10883
+ if (!parsed.references?.length) return [];
10884
+ const cwd = path35.dirname(configPath);
10885
+ return parsed.references.map((ref) => {
10886
+ const refPath = path35.resolve(cwd, ref.path);
10887
+ return fs19.statSync(refPath, { throwIfNoEntry: false })?.isDirectory() ? path35.join(refPath, "tsconfig.json") : refPath;
10888
+ }).filter((p) => fs19.existsSync(p));
10889
+ }
10890
+ function projectIncludes(configPath, sourcePath) {
10891
+ try {
10867
10892
  const project = new Project2({ tsConfigFilePath: configPath });
10868
- if (project.getSourceFile(sourcePath)) return configPath;
10893
+ return !!project.getSourceFile(sourcePath);
10894
+ } catch {
10895
+ return false;
10869
10896
  }
10870
- return rootConfig;
10871
10897
  }
10898
+
10899
+ // src/commands/refactor/extract/loadProjectFile.ts
10872
10900
  function loadProjectFile(file) {
10873
- const sourcePath = path35.resolve(file);
10901
+ const sourcePath = path36.resolve(file);
10874
10902
  const tsConfigPath = findTsConfig(sourcePath);
10875
- const project = new Project2({
10903
+ const project = new Project3({
10876
10904
  tsConfigFilePath: tsConfigPath
10877
10905
  });
10878
10906
  const sourceFile = project.getSourceFile(sourcePath);
@@ -10885,10 +10913,10 @@ function loadProjectFile(file) {
10885
10913
 
10886
10914
  // src/commands/refactor/extract/index.ts
10887
10915
  async function extract(file, functionName, destination, options2 = {}) {
10888
- const sourcePath = path36.resolve(file);
10889
- const destPath = path36.resolve(destination);
10916
+ const sourcePath = path37.resolve(file);
10917
+ const destPath = path37.resolve(destination);
10890
10918
  const cwd = process.cwd();
10891
- const relDest = path36.relative(cwd, destPath);
10919
+ const relDest = path37.relative(cwd, destPath);
10892
10920
  const { project, sourceFile } = loadProjectFile(file);
10893
10921
  const plan2 = buildPlan(
10894
10922
  functionName,
@@ -10935,13 +10963,13 @@ function ignore(file) {
10935
10963
  }
10936
10964
 
10937
10965
  // src/commands/refactor/rename/index.ts
10938
- import path37 from "path";
10966
+ import path38 from "path";
10939
10967
  import chalk122 from "chalk";
10940
10968
  async function rename(source, destination, options2 = {}) {
10941
- const destPath = path37.resolve(destination);
10969
+ const destPath = path38.resolve(destination);
10942
10970
  const cwd = process.cwd();
10943
- const relSource = path37.relative(cwd, path37.resolve(source));
10944
- const relDest = path37.relative(cwd, destPath);
10971
+ const relSource = path38.relative(cwd, path38.resolve(source));
10972
+ const relDest = path38.relative(cwd, destPath);
10945
10973
  const { project, sourceFile } = loadProjectFile(source);
10946
10974
  console.log(chalk122.bold(`Rename: ${relSource} \u2192 ${relDest}`));
10947
10975
  if (options2.apply) {
@@ -10954,9 +10982,7 @@ async function rename(source, destination, options2 = {}) {
10954
10982
  }
10955
10983
 
10956
10984
  // src/commands/refactor/renameSymbol/index.ts
10957
- import path39 from "path";
10958
10985
  import chalk123 from "chalk";
10959
- import { Project as Project3 } from "ts-morph";
10960
10986
 
10961
10987
  // src/commands/refactor/renameSymbol/findSymbol.ts
10962
10988
  import { SyntaxKind as SyntaxKind13 } from "ts-morph";
@@ -10983,12 +11009,12 @@ function findSymbol(sourceFile, symbolName) {
10983
11009
  }
10984
11010
 
10985
11011
  // src/commands/refactor/renameSymbol/groupReferences.ts
10986
- import path38 from "path";
11012
+ import path39 from "path";
10987
11013
  function groupReferences(symbol, cwd) {
10988
11014
  const refs = symbol.findReferencesAsNodes();
10989
11015
  const grouped = /* @__PURE__ */ new Map();
10990
11016
  for (const ref of refs) {
10991
- const refFile = path38.relative(cwd, ref.getSourceFile().getFilePath());
11017
+ const refFile = path39.relative(cwd, ref.getSourceFile().getFilePath());
10992
11018
  const lines = grouped.get(refFile) ?? [];
10993
11019
  if (!grouped.has(refFile)) grouped.set(refFile, lines);
10994
11020
  lines.push(ref.getStartLineNumber());
@@ -10998,15 +11024,8 @@ function groupReferences(symbol, cwd) {
10998
11024
 
10999
11025
  // src/commands/refactor/renameSymbol/index.ts
11000
11026
  async function renameSymbol(file, oldName, newName, options2 = {}) {
11001
- const filePath = path39.resolve(file);
11002
- const tsConfigPath = path39.resolve("tsconfig.json");
11003
11027
  const cwd = process.cwd();
11004
- const project = new Project3({ tsConfigFilePath: tsConfigPath });
11005
- const sourceFile = project.getSourceFile(filePath);
11006
- if (!sourceFile) {
11007
- console.log(chalk123.red(`File not found in project: ${file}`));
11008
- process.exit(1);
11009
- }
11028
+ const { project, sourceFile } = loadProjectFile(file);
11010
11029
  const symbol = findSymbol(sourceFile, oldName);
11011
11030
  if (!symbol) {
11012
11031
  console.log(chalk123.red(`Symbol "${oldName}" not found in ${file}`));
@@ -12081,6 +12100,41 @@ function prepareReviewDir(paths, requestBody, force) {
12081
12100
  writeFileSync26(paths.requestPath, requestBody);
12082
12101
  }
12083
12102
 
12103
+ // src/commands/review/runApplySession.ts
12104
+ function buildApplyPrompt(synthesisPath) {
12105
+ return `You are helping the user apply fixes for a code review with each finding decided one at a time.
12106
+
12107
+ Read ${synthesisPath}. It contains a list of findings under the \`## Findings\` heading. Each finding is a block in this exact format:
12108
+
12109
+ ### Finding: <short title>
12110
+ - Severity: blocker | major | minor | nit
12111
+ - Source: confirmed | disputed | claude-only | codex-only | already-raised
12112
+ - Location: \`path/to/file.ext:LINE\` or \`n/a\` when not tied to a specific line
12113
+ - Impact: one sentence on what could go wrong
12114
+ - Recommendation: one or two sentences with a concrete change
12115
+
12116
+ For every finding whose Source is NOT \`already-raised\`, walk through it with the user one at a time:
12117
+ 1. Read the referenced file/lines (use the Location field) and any nearby code needed to understand the impact.
12118
+ 2. Present a short assessment (one or two sentences) of whether the finding is real and what the right fix is.
12119
+ 3. Ask the user: apply or skip?
12120
+ - On 'apply': edit the relevant file(s) in place to fix the issue, then remove that finding's entire \`### Finding:\` block from ${synthesisPath} (including any blank line that separated it from the next block).
12121
+ - On 'skip': leave the finding block in ${synthesisPath} unchanged and move on.
12122
+
12123
+ Skip findings whose Source is \`already-raised\` entirely \u2014 do not present them and do not remove them from ${synthesisPath}.
12124
+
12125
+ Important constraints:
12126
+ - Do not stage, commit, or push any changes. Leave all code edits unstaged in the working tree.
12127
+ - Do not post anything to a PR.
12128
+ - Only modify ${synthesisPath} by removing the blocks for findings the user chose to apply. Do not edit the wording of any remaining finding block.
12129
+ - When every non-already-raised finding has been decided, briefly summarise what was applied vs skipped and exit.`;
12130
+ }
12131
+ async function runApplySession(synthesisPath) {
12132
+ const { done: done2 } = spawnClaude(buildApplyPrompt(synthesisPath), {
12133
+ allowEdits: true
12134
+ });
12135
+ await done2;
12136
+ }
12137
+
12084
12138
  // src/commands/review/runReviewPipeline.ts
12085
12139
  import { existsSync as existsSync35, unlinkSync as unlinkSync12 } from "fs";
12086
12140
 
@@ -12718,38 +12772,52 @@ function resolveRepoRoot() {
12718
12772
  console.error("Error: not inside a git repository.");
12719
12773
  process.exit(1);
12720
12774
  }
12775
+ function validateOptions(options2) {
12776
+ if (options2.apply && options2.refine) {
12777
+ console.error("Error: --apply cannot be combined with --refine.");
12778
+ process.exit(1);
12779
+ }
12780
+ }
12721
12781
  function logPriorComments(count) {
12722
12782
  if (count === 0) return;
12723
12783
  console.log(`Including ${count} prior review comment(s) in request.md.`);
12724
12784
  }
12725
- async function review(options2 = {}) {
12726
- const repoRoot = resolveRepoRoot();
12785
+ function gatherChangedContext() {
12727
12786
  const context = gatherContext();
12728
- if (context.changedFiles.length === 0) {
12729
- console.error(
12730
- `Error: PR #${context.prNumber} has no changed files \u2014 nothing to review.`
12731
- );
12732
- process.exit(1);
12733
- }
12787
+ if (context.changedFiles.length > 0) return context;
12788
+ console.error(
12789
+ `Error: PR #${context.prNumber} has no changed files \u2014 nothing to review.`
12790
+ );
12791
+ process.exit(1);
12792
+ }
12793
+ function setupReviewDir(repoRoot, context, force) {
12734
12794
  const paths = buildReviewPaths(repoRoot, context.branch, context.shortSha);
12735
12795
  const priorComments = fetchExistingComments();
12736
12796
  logPriorComments(priorComments?.length ?? 0);
12737
- prepareReviewDir(
12738
- paths,
12739
- buildRequest(context, priorComments),
12740
- options2.force ?? false
12741
- );
12797
+ prepareReviewDir(paths, buildRequest(context, priorComments), force);
12742
12798
  console.log(`Review folder: ${paths.reviewDir}`);
12799
+ return paths;
12800
+ }
12801
+ async function runPostSynthesis(synthesisPath, options2) {
12802
+ if (options2.apply) {
12803
+ await runApplySession(synthesisPath);
12804
+ return;
12805
+ }
12806
+ await handlePostSynthesis(synthesisPath, {
12807
+ refine: options2.refine ?? false,
12808
+ prompt: options2.prompt ?? true,
12809
+ submit: options2.submit ?? false
12810
+ });
12811
+ }
12812
+ async function review(options2 = {}) {
12813
+ validateOptions(options2);
12814
+ const repoRoot = resolveRepoRoot();
12815
+ const context = gatherChangedContext();
12816
+ const paths = setupReviewDir(repoRoot, context, options2.force ?? false);
12743
12817
  const synthesisOk = await runReviewPipeline(paths, {
12744
12818
  verbose: options2.verbose ?? false
12745
12819
  });
12746
- if (synthesisOk) {
12747
- await handlePostSynthesis(paths.synthesisPath, {
12748
- refine: options2.refine ?? false,
12749
- prompt: options2.prompt ?? true,
12750
- submit: options2.submit ?? false
12751
- });
12752
- }
12820
+ if (synthesisOk) await runPostSynthesis(paths.synthesisPath, options2);
12753
12821
  console.log(`Done. Review folder: ${paths.reviewDir}`);
12754
12822
  }
12755
12823
 
@@ -12769,6 +12837,9 @@ function registerReview(program2) {
12769
12837
  ).option(
12770
12838
  "--refine",
12771
12839
  "After synthesis, launch an interactive Claude session to walk through findings instead of posting"
12840
+ ).option(
12841
+ "--apply",
12842
+ "After synthesis, launch an interactive Claude session to apply fixes for each finding; applied findings are removed from synthesis, skipped ones remain for a later post"
12772
12843
  ).option(
12773
12844
  "--verbose",
12774
12845
  "Disable spinner UI and use per-line log output (per-tool lines, starting/done lines)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.214.1",
3
+ "version": "0.215.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {