@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.
- package/README.md +1 -1
- package/dist/index.js +124 -53
- 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.
|
|
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
|
|
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
|
|
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
|
|
10881
|
+
return [];
|
|
10861
10882
|
}
|
|
10862
|
-
if (!parsed.references?.length) return
|
|
10863
|
-
|
|
10864
|
-
|
|
10865
|
-
const
|
|
10866
|
-
|
|
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
|
-
|
|
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 =
|
|
10901
|
+
const sourcePath = path36.resolve(file);
|
|
10874
10902
|
const tsConfigPath = findTsConfig(sourcePath);
|
|
10875
|
-
const project = new
|
|
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 =
|
|
10889
|
-
const destPath =
|
|
10916
|
+
const sourcePath = path37.resolve(file);
|
|
10917
|
+
const destPath = path37.resolve(destination);
|
|
10890
10918
|
const cwd = process.cwd();
|
|
10891
|
-
const relDest =
|
|
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
|
|
10966
|
+
import path38 from "path";
|
|
10939
10967
|
import chalk122 from "chalk";
|
|
10940
10968
|
async function rename(source, destination, options2 = {}) {
|
|
10941
|
-
const destPath =
|
|
10969
|
+
const destPath = path38.resolve(destination);
|
|
10942
10970
|
const cwd = process.cwd();
|
|
10943
|
-
const relSource =
|
|
10944
|
-
const relDest =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
12726
|
-
const repoRoot = resolveRepoRoot();
|
|
12785
|
+
function gatherChangedContext() {
|
|
12727
12786
|
const context = gatherContext();
|
|
12728
|
-
if (context.changedFiles.length
|
|
12729
|
-
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
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)"
|