@review-my-code/rmcode 0.1.4 → 0.1.6

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 +15 -7
  2. package/dist/cli.js +278 -23
  3. package/package.json +1 -6
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RMCode CLI
2
2
 
3
- Run Review My Code from your terminal.
3
+ Run RMCode from your terminal.
4
4
 
5
5
  ```bash
6
6
  npm install -g @review-my-code/rmcode
@@ -9,21 +9,29 @@ export RMC_API_KEY=rmc_...
9
9
  rmcode --staged
10
10
  ```
11
11
 
12
- The CLI sends the selected diff, file, or stdin content to RMCode for analysis.
13
- Anonymous CLI reviews are disabled by default, so set `RMC_API_KEY` before
14
- running a review.
12
+ The CLI sends the selected diff, file, or stdin content to RMCode for review.
13
+ Set `RMC_API_KEY` before running a review; anonymous CLI reviews are disabled.
15
14
 
16
15
  ## Common Commands
17
16
 
18
17
  ```bash
19
18
  rmcode # review branch diff from merge base
19
+ rmcode --pr 123 # review GitHub PR #123 for this repo
20
+ rmcode --pr https://github.com/owner/repo/pull/123
20
21
  rmcode --staged # review staged changes
21
22
  rmcode --unstaged # review unstaged changes
22
- rmcode --all # review uncommitted tracked changes and safe text files
23
+ rmcode --all # review all uncommitted changes, including safe text files
23
24
  rmcode src/auth.ts # review one file
24
25
  git diff main | rmcode # review a custom diff; language is detected from filenames
25
26
  rmcode --json --fail-on-findings
27
+ rmcode cache clear # clear reusable PR checkout cache
28
+ rmcode update # update to the latest CLI
26
29
  ```
27
30
 
28
- Use `RMC_API_URL` to point at a non-production API and `RMC_APP_URL` to point
29
- `rmcode login` at a non-production web app.
31
+ Review commands check for the latest published CLI before uploading code. If a
32
+ newer version is available, run `rmcode update` and retry.
33
+
34
+ `rmcode --pr` fetches PR metadata and diff from GitHub, caches a reusable base
35
+ checkout under `~/.cache/rmcode/pr-repos`, and runs the same pinned context
36
+ extractor used for local diff reviews. Use `GH_TOKEN`, `GITHUB_TOKEN`, or
37
+ `gh auth login` for private repositories.
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ const path_1 = require("path");
9
9
  const readline_1 = require("readline");
10
10
  const API_URL = process.env.RMC_API_URL || "https://review-my-code.com";
11
11
  const APP_URL = process.env.RMC_APP_URL || "https://review-my-code.com";
12
- const VERSION = "0.1.4";
12
+ const VERSION = "0.1.6";
13
13
  const FREE_PLAN_CREDITS_PER_MONTH = 30;
14
14
  const REQUEST_TIMEOUT_MS = 290_000;
15
15
  const POLL_TIMEOUT_MS = 10 * 60_000;
@@ -22,6 +22,7 @@ const MAX_UNTRACKED_FILE_BYTES = 512 * 1024;
22
22
  const EXTRACTOR_TIMEOUT_MS = 120_000;
23
23
  const EXTRACTOR_DOWNLOAD_TIMEOUT_MS = 60_000;
24
24
  const EXTRACTOR_INFO_TIMEOUT_MS = 15_000;
25
+ const GITHUB_API_URL = "https://api.github.com";
25
26
  // Mirrors the GitHub App workflow template's "Install extractor runtime
26
27
  // dependencies" step and REPOMAP_MAX_CHARS env.
27
28
  const EXTRACTOR_RUNTIME_DEPS = [
@@ -89,6 +90,13 @@ function gitQuiet(args) {
89
90
  stdio: ["ignore", "pipe", "ignore"],
90
91
  }).trim();
91
92
  }
93
+ function gitQuietIn(cwd, args) {
94
+ return (0, child_process_1.execFileSync)("git", ["-C", cwd, ...args], {
95
+ encoding: "utf-8",
96
+ maxBuffer: 10 * 1024 * 1024,
97
+ stdio: ["ignore", "pipe", "ignore"],
98
+ }).trim();
99
+ }
92
100
  function isGitRepo() {
93
101
  try {
94
102
  git(["rev-parse", "--is-inside-work-tree"]);
@@ -200,13 +208,171 @@ function pseudoDiffForNewFile(file, worktreeRoot) {
200
208
  function gitRepoName() {
201
209
  try {
202
210
  const remote = gitQuiet(["remote", "get-url", "origin"]);
203
- const m = remote.match(/github\.com[:/]([^/]+\/[^/.]+)/);
204
- return m ? m[1] : null;
211
+ return ownerRepoFromRemoteUrl(remote);
205
212
  }
206
213
  catch {
207
214
  return null;
208
215
  }
209
216
  }
217
+ function ownerRepoFromRemoteUrl(remote) {
218
+ const m = remote.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/i);
219
+ return m ? m[1].replace(/\.git$/i, "") : null;
220
+ }
221
+ function parsePrReference(value) {
222
+ const trimmed = value.trim();
223
+ const numeric = /^\d+$/.test(trimmed)
224
+ ? Number.parseInt(trimmed, 10)
225
+ : undefined;
226
+ if (numeric != null && numeric > 0) {
227
+ const ownerRepo = gitRepoName();
228
+ if (!ownerRepo) {
229
+ throw new Error("Could not infer owner/repo from origin. Use: rmcode --pr https://github.com/owner/repo/pull/123");
230
+ }
231
+ return { ownerRepo, number: numeric };
232
+ }
233
+ const match = trimmed.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)(?:[/?#].*)?$/i);
234
+ if (!match) {
235
+ throw new Error("Invalid PR reference. Use `rmcode --pr 123` or `rmcode --pr https://github.com/owner/repo/pull/123`.");
236
+ }
237
+ return {
238
+ ownerRepo: match[1].replace(/\.git$/i, ""),
239
+ number: Number.parseInt(match[2], 10),
240
+ };
241
+ }
242
+ function githubToken() {
243
+ const envToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
244
+ if (envToken?.trim())
245
+ return envToken.trim();
246
+ try {
247
+ return (0, child_process_1.execFileSync)("gh", ["auth", "token"], {
248
+ encoding: "utf-8",
249
+ stdio: ["ignore", "pipe", "ignore"],
250
+ timeout: 5_000,
251
+ }).trim();
252
+ }
253
+ catch {
254
+ return undefined;
255
+ }
256
+ }
257
+ function githubHeaders(accept) {
258
+ const headers = {
259
+ Accept: accept,
260
+ "User-Agent": `rmcode/${VERSION}`,
261
+ };
262
+ const token = githubToken();
263
+ if (token)
264
+ headers.Authorization = `Bearer ${token}`;
265
+ return headers;
266
+ }
267
+ async function fetchGithubJson(path) {
268
+ const resp = await fetch(`${GITHUB_API_URL}${path}`, {
269
+ headers: githubHeaders("application/vnd.github+json"),
270
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
271
+ });
272
+ if (!resp.ok) {
273
+ const body = await resp.text().catch(() => "");
274
+ throw new Error(`GitHub API request failed (${resp.status}). ${body.includes("Not Found")
275
+ ? "Check the PR URL and GitHub authentication."
276
+ : body.slice(0, 180)}`.trim());
277
+ }
278
+ return (await resp.json());
279
+ }
280
+ async function fetchGithubText(path, accept) {
281
+ const resp = await fetch(`${GITHUB_API_URL}${path}`, {
282
+ headers: githubHeaders(accept),
283
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
284
+ });
285
+ if (!resp.ok) {
286
+ throw new Error(`GitHub diff request failed (${resp.status}).`);
287
+ }
288
+ return resp.text();
289
+ }
290
+ async function fetchPrMetadata(ref) {
291
+ const pr = await fetchGithubJson(`/repos/${ref.ownerRepo}/pulls/${ref.number}`);
292
+ const baseRepo = pr.base?.repo?.full_name;
293
+ const cloneUrl = pr.base?.repo?.clone_url;
294
+ const baseSha = pr.base?.sha;
295
+ const baseRef = pr.base?.ref;
296
+ const headSha = pr.head?.sha;
297
+ if (!baseRepo || !cloneUrl || !baseSha || !baseRef || !headSha) {
298
+ throw new Error("GitHub PR metadata is missing base/head information.");
299
+ }
300
+ return {
301
+ number: pr.number,
302
+ title: pr.title || `PR #${pr.number}`,
303
+ htmlUrl: pr.html_url || `https://github.com/${ref.ownerRepo}/pull/${ref.number}`,
304
+ baseRef,
305
+ baseSha,
306
+ baseRepo,
307
+ cloneUrl,
308
+ headSha,
309
+ };
310
+ }
311
+ async function fetchPrDiff(ref) {
312
+ return fetchGithubText(`/repos/${ref.ownerRepo}/pulls/${ref.number}`, "application/vnd.github.v3.diff");
313
+ }
314
+ function rmcodeCacheRoot() {
315
+ return process.env.RMC_CACHE_DIR || (0, path_1.join)((0, os_1.homedir)(), ".cache", "rmcode");
316
+ }
317
+ function prCacheRoot() {
318
+ return process.env.RMC_PR_CACHE_DIR || (0, path_1.join)(rmcodeCacheRoot(), "pr-repos");
319
+ }
320
+ function safeCacheName(ownerRepo) {
321
+ return ownerRepo.toLowerCase().replace(/[^a-z0-9._-]+/g, "__");
322
+ }
323
+ function ensurePrCacheDir(metadata) {
324
+ const root = prCacheRoot();
325
+ (0, fs_1.mkdirSync)(root, { recursive: true });
326
+ const cacheDir = (0, path_1.join)(root, safeCacheName(metadata.baseRepo));
327
+ if (!(0, fs_1.existsSync)((0, path_1.join)(cacheDir, ".git"))) {
328
+ const clone = (0, child_process_1.spawnSync)("git", ["clone", "--no-checkout", metadata.cloneUrl, cacheDir], { stdio: "ignore", timeout: REQUEST_TIMEOUT_MS });
329
+ if (clone.status !== 0) {
330
+ throw new Error(`Could not clone ${metadata.baseRepo} into rmcode cache.`);
331
+ }
332
+ }
333
+ else {
334
+ try {
335
+ gitQuietIn(cacheDir, ["remote", "set-url", "origin", metadata.cloneUrl]);
336
+ }
337
+ catch {
338
+ // Keep the existing remote; the next fetch will report any real issue.
339
+ }
340
+ }
341
+ try {
342
+ gitQuietIn(cacheDir, ["fetch", "--no-tags", "origin", metadata.baseSha]);
343
+ }
344
+ catch {
345
+ gitQuietIn(cacheDir, ["fetch", "--no-tags", "origin", metadata.baseRef]);
346
+ }
347
+ try {
348
+ gitQuietIn(cacheDir, ["rev-parse", "--verify", metadata.baseSha]);
349
+ }
350
+ catch {
351
+ throw new Error(`Could not fetch PR base commit ${metadata.baseSha} into the rmcode cache.`);
352
+ }
353
+ return cacheDir;
354
+ }
355
+ async function diffFromPullRequest(value) {
356
+ const ref = typeof value === "string" ? parsePrReference(value) : value;
357
+ const [metadata, diff] = await Promise.all([
358
+ fetchPrMetadata(ref),
359
+ fetchPrDiff(ref),
360
+ ]);
361
+ if (!diff.trim()) {
362
+ throw new Error(`GitHub returned an empty diff for ${metadata.htmlUrl}.`);
363
+ }
364
+ const cacheDir = ensurePrCacheDir(metadata);
365
+ return {
366
+ diff,
367
+ files: filesFromDiff(diff),
368
+ baseRef: metadata.baseSha,
369
+ metadata,
370
+ cacheDir,
371
+ };
372
+ }
373
+ function clearPrCache() {
374
+ (0, fs_1.rmSync)(prCacheRoot(), { recursive: true, force: true });
375
+ }
210
376
  function currentBranch() {
211
377
  try {
212
378
  return git(["branch", "--show-current"]);
@@ -310,8 +476,8 @@ async function fetchReviewJson(url, init) {
310
476
  clearTimeout(timeout);
311
477
  }
312
478
  }
313
- async function callAuthenticatedAPI(code, language, type, files, bundle, progress) {
314
- const repo = gitRepoName();
479
+ async function callAuthenticatedAPI(code, language, type, files, bundle, progress, repoOverride) {
480
+ const repo = repoOverride || gitRepoName();
315
481
  const payload = {
316
482
  type,
317
483
  value: code,
@@ -353,8 +519,8 @@ async function callAuthenticatedAPI(code, language, type, files, bundle, progres
353
519
  }
354
520
  return body;
355
521
  }
356
- async function callAnonymousAPI(code, language, type, files, progress) {
357
- const repo = gitRepoName();
522
+ async function callAnonymousAPI(code, language, type, files, progress, repoOverride) {
523
+ const repo = repoOverride || gitRepoName();
358
524
  const { status, body } = await fetchReviewJson(`${API_URL}/api/reviews/anonymous`, {
359
525
  method: "POST",
360
526
  headers: { "Content-Type": "application/json" },
@@ -398,9 +564,9 @@ async function pollForResults(url, initialMeta, headers, progress) {
398
564
  }
399
565
  return { success: false, error: "Review timed out after 10 minutes." };
400
566
  }
401
- function callAPI(code, language, type = "snippet", files = [], bundle, progress) {
567
+ function callAPI(code, language, type = "snippet", files = [], bundle, progress, repoOverride) {
402
568
  if (API_KEY)
403
- return callAuthenticatedAPI(code, language, type, files, bundle, progress);
569
+ return callAuthenticatedAPI(code, language, type, files, bundle, progress, repoOverride);
404
570
  return Promise.resolve({
405
571
  success: false,
406
572
  error: "API key required. Run `rmcode login`, then set RMC_API_KEY before sending code for review.",
@@ -496,7 +662,7 @@ async function ensureExtractor(info) {
496
662
  * diff's base state and return the ContextBundle, or null when extraction
497
663
  * is unavailable. Throws ExtractorIntegrityError on checksum mismatch.
498
664
  */
499
- async function extractContextBundle(diff, baseRef, contextOptions = {}) {
665
+ async function extractContextBundle(diff, baseRef, contextOptions = {}, repoRoot) {
500
666
  const info = await fetchExtractorInfo();
501
667
  if (!info)
502
668
  return null;
@@ -508,7 +674,12 @@ async function extractContextBundle(diff, baseRef, contextOptions = {}) {
508
674
  let worktreeAdded = false;
509
675
  try {
510
676
  try {
511
- gitQuiet(["worktree", "add", "--detach", baseDir, baseRef]);
677
+ if (repoRoot) {
678
+ gitQuietIn(repoRoot, ["worktree", "add", "--detach", baseDir, baseRef]);
679
+ }
680
+ else {
681
+ gitQuiet(["worktree", "add", "--detach", baseDir, baseRef]);
682
+ }
512
683
  worktreeAdded = true;
513
684
  }
514
685
  catch {
@@ -554,12 +725,22 @@ async function extractContextBundle(diff, baseRef, contextOptions = {}) {
554
725
  finally {
555
726
  if (worktreeAdded) {
556
727
  try {
557
- gitQuiet(["worktree", "remove", "--force", baseDir]);
728
+ if (repoRoot) {
729
+ gitQuietIn(repoRoot, ["worktree", "remove", "--force", baseDir]);
730
+ }
731
+ else {
732
+ gitQuiet(["worktree", "remove", "--force", baseDir]);
733
+ }
558
734
  }
559
735
  catch {
560
736
  (0, fs_1.rmSync)(baseDir, { recursive: true, force: true });
561
737
  try {
562
- gitQuiet(["worktree", "prune"]);
738
+ if (repoRoot) {
739
+ gitQuietIn(repoRoot, ["worktree", "prune"]);
740
+ }
741
+ else {
742
+ gitQuiet(["worktree", "prune"]);
743
+ }
563
744
  }
564
745
  catch {
565
746
  // Stale worktree metadata only; git prunes it on its own later.
@@ -575,11 +756,11 @@ async function extractContextBundle(diff, baseRef, contextOptions = {}) {
575
756
  * Reports progress on the shared review spinner so the run renders as one
576
757
  * phase-aware line. Exits the process on extractor checksum mismatch.
577
758
  */
578
- async function maybeExtractContext(diff, spinner, baseRef, contextOptions = {}) {
579
- if (!API_KEY || !baseRef || !isGitRepo())
759
+ async function maybeExtractContext(diff, spinner, baseRef, contextOptions = {}, repoRoot) {
760
+ if (!API_KEY || !baseRef || (!repoRoot && !isGitRepo()))
580
761
  return null;
581
762
  try {
582
- const bundle = await extractContextBundle(diff, baseRef, contextOptions);
763
+ const bundle = await extractContextBundle(diff, baseRef, contextOptions, repoRoot);
583
764
  if (bundle)
584
765
  return bundle;
585
766
  }
@@ -806,7 +987,7 @@ function createSpinner(msg) {
806
987
  };
807
988
  }
808
989
  // ── Review runner ─────────────────────────────────────────────────────
809
- async function runReview(content, files, label, jsonMode, failOnFindings, type = "diff", language, baseRef, contextOptions = {}) {
990
+ async function runReview(content, files, label, jsonMode, failOnFindings, type = "diff", language, baseRef, contextOptions = {}, contextRepoRoot, repoOverride) {
810
991
  const startedAt = Date.now();
811
992
  const lang = language || detectLanguage(files);
812
993
  const uploadMsg = `Uploading review request (${files.length} file${files.length !== 1 ? "s" : ""}, ${lang})`;
@@ -814,15 +995,18 @@ async function runReview(content, files, label, jsonMode, failOnFindings, type =
814
995
  // the diff's base state so the review sees the same repository context.
815
996
  // One spinner carries the whole run through its phases. The guard here
816
997
  // mirrors maybeExtractContext's own early return.
817
- const willExtract = type === "diff" && Boolean(API_KEY) && Boolean(baseRef) && isGitRepo();
998
+ const willExtract = type === "diff" &&
999
+ Boolean(API_KEY) &&
1000
+ Boolean(baseRef) &&
1001
+ (Boolean(contextRepoRoot) || isGitRepo());
818
1002
  const spinner = createSpinner(willExtract ? "Extracting repository context" : uploadMsg);
819
1003
  const bundle = willExtract
820
- ? await maybeExtractContext(content, spinner, baseRef, contextOptions)
1004
+ ? await maybeExtractContext(content, spinner, baseRef, contextOptions, contextRepoRoot)
821
1005
  : null;
822
1006
  if (willExtract)
823
1007
  spinner.update(uploadMsg);
824
1008
  try {
825
- const result = await callAPI(content, lang, type, files, bundle, spinner);
1009
+ const result = await callAPI(content, lang, type, files, bundle, spinner, repoOverride);
826
1010
  spinner.stop(`Reviewed ${label}`);
827
1011
  if (jsonMode) {
828
1012
  printJsonResponse(result);
@@ -848,6 +1032,38 @@ async function runReview(content, files, label, jsonMode, failOnFindings, type =
848
1032
  process.exit(1);
849
1033
  }
850
1034
  }
1035
+ async function runPullRequestReview(prValue, jsonMode, failOnFindings, options) {
1036
+ if (!API_KEY) {
1037
+ console.error(style(" API key required. Run `rmcode login`, then set RMC_API_KEY before reviewing a PR.", c.red));
1038
+ process.exit(1);
1039
+ }
1040
+ let ref;
1041
+ try {
1042
+ ref = parsePrReference(prValue);
1043
+ }
1044
+ catch (err) {
1045
+ console.error(style(` ${err.message}`, c.red));
1046
+ process.exit(1);
1047
+ }
1048
+ const spinner = createSpinner(`Fetching GitHub PR ${prValue}`);
1049
+ try {
1050
+ const source = await diffFromPullRequest(ref);
1051
+ spinner.stop(`Fetched ${source.metadata.baseRepo}#${source.metadata.number}`);
1052
+ const contextOptions = {
1053
+ prTitle: firstNonEmpty(options.title, process.env.RMC_PR_TITLE, source.metadata.title),
1054
+ prNumber: source.metadata.number,
1055
+ sourceRepo: firstNonEmpty(options.sourceRepo, process.env.RMC_SOURCE_REPO, source.metadata.baseRepo),
1056
+ };
1057
+ await runReview(source.diff, source.files, `${source.metadata.baseRepo}#${source.metadata.number}`, jsonMode, failOnFindings, "diff", undefined, source.baseRef, contextOptions, source.cacheDir, source.metadata.baseRepo);
1058
+ if (!jsonMode)
1059
+ await maybeShowUpdateNudge();
1060
+ }
1061
+ catch (err) {
1062
+ spinner.stop("PR fetch failed");
1063
+ console.error(style(` ${err.message}`, c.red));
1064
+ process.exit(1);
1065
+ }
1066
+ }
851
1067
  // ── Help ──────────────────────────────────────────────────────────────
852
1068
  function printHelp() {
853
1069
  const header = !noColor && !isCI
@@ -859,10 +1075,13 @@ ${header}
859
1075
  ${style("COMMANDS", c.bold)}
860
1076
 
861
1077
  ${style("rmcode", c.cyan)} Review changes from merge base (default)
1078
+ ${style("rmcode --pr 123", c.cyan)} Review GitHub PR #123 for this repo
1079
+ ${style("rmcode --pr <url>", c.cyan)} Review a specific GitHub PR URL
862
1080
  ${style("rmcode --staged", c.cyan)} Review staged changes only
863
1081
  ${style("rmcode --unstaged", c.cyan)} Review unstaged changes only
864
1082
  ${style("rmcode --all", c.cyan)} Review all uncommitted changes (staged + unstaged)
865
1083
  ${style("rmcode <file>", c.cyan)} Review a single local file
1084
+ ${style("rmcode cache clear", c.cyan)} Clear cached PR checkouts
866
1085
  ${style("rmcode login", c.cyan)} Set up your API key
867
1086
  ${style("rmcode install", c.cyan)} Install the GitHub App for automatic PR reviews
868
1087
  ${style("rmcode update", c.cyan)} Update rmcode to the latest version
@@ -872,6 +1091,7 @@ ${header}
872
1091
 
873
1092
  --json Output the full API response as JSON (for CI/agents)
874
1093
  --fail-on-findings Exit 2 when JSON output contains findings
1094
+ --pr <number|url> Review a GitHub PR by number or URL
875
1095
  --lang <language> Optional language hint for raw stdin snippets
876
1096
  --title <text> Optional PR title hint for repository-context reviews
877
1097
  --pr-number <n> Optional PR number hint for CI/benchmark parity
@@ -882,6 +1102,7 @@ ${header}
882
1102
  ${style("MODES", c.bold)}
883
1103
 
884
1104
  ${style("Default", c.white)} Diff from merge base to HEAD — what your PR would contain
1105
+ ${style("--pr", c.white)} Fetch a GitHub PR diff and cached base checkout for context extraction
885
1106
  ${style("--staged", c.white)} Only changes in the staging area (git add)
886
1107
  ${style("--unstaged", c.white)} Only working directory changes not yet staged
887
1108
  ${style("--all", c.white)} Everything not yet committed, including untracked text files
@@ -899,6 +1120,10 @@ ${header}
899
1120
  ${style("# Review a specific range", c.dim)}
900
1121
  git diff abc123..def456 | rmcode
901
1122
 
1123
+ ${style("# Review a GitHub PR without checking it out locally", c.dim)}
1124
+ rmcode --pr 123
1125
+ rmcode --pr https://github.com/owner/repo/pull/123
1126
+
902
1127
  ${style("# Review one file", c.dim)}
903
1128
  rmcode src/auth.ts
904
1129
 
@@ -911,8 +1136,7 @@ ${header}
911
1136
  ${style("ENVIRONMENT", c.bold)}
912
1137
 
913
1138
  RMC_API_KEY API key for authenticated reviews (get one: rmcode login)
914
- RMC_API_URL API endpoint (default: https://review-my-code.com)
915
- RMC_APP_URL App URL for login (default: https://review-my-code.com)
1139
+ RMC_PR_CACHE_DIR Optional directory for reusable GitHub PR cache clones
916
1140
  RMC_PR_TITLE Optional PR title hint for repository-context reviews
917
1141
  RMC_PR_NUMBER Optional PR number hint for CI/benchmark parity
918
1142
  RMC_SOURCE_REPO Optional owner/repo hint when no origin remote exists
@@ -1084,6 +1308,7 @@ function parseArgs(args) {
1084
1308
  const positional = [];
1085
1309
  const optionAliases = {
1086
1310
  "--lang": "lang",
1311
+ "--pr": "pr",
1087
1312
  "--title": "title",
1088
1313
  "--pr-title": "title",
1089
1314
  "--pr-number": "prNumber",
@@ -1098,10 +1323,13 @@ function parseArgs(args) {
1098
1323
  if (eq > 0) {
1099
1324
  options[optionName] = arg.slice(eq + 1);
1100
1325
  }
1101
- else {
1326
+ else if (args[i + 1] && !args[i + 1].startsWith("-")) {
1102
1327
  options[optionName] = args[i + 1];
1103
1328
  i++;
1104
1329
  }
1330
+ else {
1331
+ options[optionName] = undefined;
1332
+ }
1105
1333
  }
1106
1334
  else if (arg.startsWith("-")) {
1107
1335
  flags.add(arg);
@@ -1148,6 +1376,15 @@ async function main() {
1148
1376
  await runUpdate();
1149
1377
  return;
1150
1378
  }
1379
+ if (positional[0] === "cache") {
1380
+ if (positional[1] === "clear" && positional.length === 2) {
1381
+ clearPrCache();
1382
+ console.log(`\n ${style("✓", c.green)} Cleared rmcode PR cache.\n`);
1383
+ return;
1384
+ }
1385
+ console.error(style(`\n Unknown cache command. Run: rmcode cache clear\n`, c.red));
1386
+ process.exit(1);
1387
+ }
1151
1388
  if (positional[0] === "install") {
1152
1389
  console.log(`\n ${style("Install the GitHub App for automatic PR reviews:", c.bold)}\n`);
1153
1390
  console.log(` ${style("https://github.com/apps/rmcode-ai", c.cyan, c.bold)}\n`);
@@ -1179,6 +1416,7 @@ async function main() {
1179
1416
  const jsonMode = flags.has("--json");
1180
1417
  const failOnFindings = flags.has("--fail-on-findings");
1181
1418
  const contextOptions = contextOptionsFromCli(options);
1419
+ const hasPrOption = Object.prototype.hasOwnProperty.call(options, "pr");
1182
1420
  if (positional.length > 1) {
1183
1421
  console.error(style(`\n Unknown command: ${positional[0]}`, c.red));
1184
1422
  console.error(style(` Run: rmcode help\n`, c.dim));
@@ -1190,6 +1428,23 @@ async function main() {
1190
1428
  if (!jsonMode && !noColor && !isCI) {
1191
1429
  process.stderr.write(`\n${owlHeader()}\n\n`);
1192
1430
  }
1431
+ if (hasPrOption) {
1432
+ const prValue = options.pr?.trim();
1433
+ if (!prValue) {
1434
+ console.error(style("\n Missing value for --pr.\n", c.red));
1435
+ process.exit(1);
1436
+ }
1437
+ if (positional.length > 0) {
1438
+ console.error(style("\n --pr cannot be combined with a file path or command.\n", c.red));
1439
+ process.exit(1);
1440
+ }
1441
+ if (flags.has("--staged") || flags.has("--unstaged") || flags.has("--all")) {
1442
+ console.error(style("\n --pr cannot be combined with --staged, --unstaged, or --all.\n", c.red));
1443
+ process.exit(1);
1444
+ }
1445
+ await runPullRequestReview(prValue, jsonMode, failOnFindings, options);
1446
+ return;
1447
+ }
1193
1448
  // Piped stdin
1194
1449
  if (hasPipedStdin()) {
1195
1450
  const rl = (0, readline_1.createInterface)({ input: process.stdin });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@review-my-code/rmcode",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "AI code review from your terminal. Catches logic errors, null risks, security holes, and broken error handling.",
5
5
  "keywords": [
6
6
  "code-review",
@@ -38,11 +38,6 @@
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/review-my-code/review-my-code.git",
44
- "directory": "cli"
45
- },
46
41
  "homepage": "https://review-my-code.com",
47
42
  "author": "Alex Yang"
48
43
  }