grepper-dev 0.2.0

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 ADDED
@@ -0,0 +1,108 @@
1
+ # @grepper/cli
2
+
3
+ Local code review powered by AI. Review your code changes before pushing, right from your terminal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @grepper/cli
9
+ ```
10
+
11
+ Or use directly with npx:
12
+
13
+ ```bash
14
+ npx @grepper/cli review
15
+ ```
16
+
17
+ ## Setup
18
+
19
+ 1. Get your API key from the [Grepper dashboard](https://grepper.dev/dashboard/settings)
20
+ 2. Login with your key:
21
+
22
+ ```bash
23
+ grepper login
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Review staged changes (default)
29
+
30
+ ```bash
31
+ grepper review
32
+ ```
33
+
34
+ ### Review all uncommitted changes
35
+
36
+ ```bash
37
+ grepper review --all
38
+ ```
39
+
40
+ ### Review changes against a branch
41
+
42
+ ```bash
43
+ grepper review --base main
44
+ ```
45
+
46
+ ### Review specific files
47
+
48
+ ```bash
49
+ grepper review src/api.ts src/utils.ts
50
+ ```
51
+
52
+ ## Options
53
+
54
+ | Option | Description |
55
+ |--------|-------------|
56
+ | `-s, --staged` | Review staged changes only |
57
+ | `-a, --all` | Review all uncommitted changes |
58
+ | `-b, --base <branch>` | Compare against a branch |
59
+ | `-m, --mode <mode>` | Review mode: `full`, `quick`, or `security` |
60
+ | `-f, --format <format>` | Output: `pretty`, `json`, or `markdown` |
61
+ | `--fail-on-critical` | Exit with code 1 if critical issues found |
62
+
63
+ ## Review Modes
64
+
65
+ - **full** (default) - Comprehensive review covering all aspects
66
+ - **quick** - Fast review focusing on obvious issues
67
+ - **security** - Security-focused review
68
+
69
+ ## Output Formats
70
+
71
+ - **pretty** (default) - Colored terminal output
72
+ - **json** - Machine-readable JSON for CI/automation
73
+ - **markdown** - Markdown for documentation or PRs
74
+
75
+ ## CI Integration
76
+
77
+ Use in CI pipelines to catch issues before merge:
78
+
79
+ ```yaml
80
+ - name: Review code
81
+ run: npx @grepper/cli review --base main --format json --fail-on-critical
82
+ ```
83
+
84
+ ## Configuration
85
+
86
+ Create a `.grepper.json` in your project root:
87
+
88
+ ```json
89
+ {
90
+ "mode": "full",
91
+ "ignore": ["*.test.ts", "dist/**"]
92
+ }
93
+ ```
94
+
95
+ The CLI also reads `CLAUDE.md` and `agents.md` for project-specific context.
96
+
97
+ ## Commands
98
+
99
+ | Command | Description |
100
+ |---------|-------------|
101
+ | `grepper login` | Authenticate with your API key |
102
+ | `grepper logout` | Clear stored credentials |
103
+ | `grepper whoami` | Show current user and organization |
104
+ | `grepper review` | Review code changes |
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,725 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command, Option } from "commander";
5
+ import updateNotifier from "update-notifier";
6
+
7
+ // src/commands/login.ts
8
+ import chalk from "chalk";
9
+
10
+ // src/lib/auth.ts
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
+ import { homedir } from "os";
13
+ import { join } from "path";
14
+ var CONFIG_DIR = join(homedir(), ".grepper");
15
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
16
+ var DEFAULT_API_URL = "https://grepper.convex.site";
17
+ function ensureConfigDir() {
18
+ if (!existsSync(CONFIG_DIR)) {
19
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
20
+ }
21
+ }
22
+ function loadConfig() {
23
+ try {
24
+ if (existsSync(CONFIG_FILE)) {
25
+ const data = readFileSync(CONFIG_FILE, "utf-8");
26
+ return JSON.parse(data);
27
+ }
28
+ } catch {
29
+ }
30
+ return {};
31
+ }
32
+ function saveConfig(config) {
33
+ ensureConfigDir();
34
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
35
+ mode: 384
36
+ });
37
+ }
38
+ function getApiKey() {
39
+ const envKey = process.env.GREPPER_API_KEY;
40
+ if (envKey) {
41
+ return envKey;
42
+ }
43
+ const config = loadConfig();
44
+ return config.apiKey;
45
+ }
46
+ function getApiUrl() {
47
+ const config = loadConfig();
48
+ return config.apiUrl ?? process.env.GREPPER_API_URL ?? DEFAULT_API_URL;
49
+ }
50
+ function setApiKey(apiKey) {
51
+ const config = loadConfig();
52
+ config.apiKey = apiKey;
53
+ saveConfig(config);
54
+ }
55
+ function clearCredentials() {
56
+ const config = loadConfig();
57
+ config.apiKey = void 0;
58
+ saveConfig(config);
59
+ }
60
+ function isLoggedIn() {
61
+ return !!getApiKey();
62
+ }
63
+
64
+ // src/lib/api.ts
65
+ var REVIEW_TIMEOUT_MS = 12e4;
66
+ var DEFAULT_TIMEOUT_MS = 15e3;
67
+ var ApiError = class extends Error {
68
+ statusCode;
69
+ constructor(message, statusCode) {
70
+ super(message);
71
+ this.name = "ApiError";
72
+ this.statusCode = statusCode;
73
+ }
74
+ };
75
+ function getHeaders() {
76
+ const apiKey = getApiKey();
77
+ if (!apiKey) {
78
+ throw new ApiError("Not logged in. Run `grepper login` first.", 401);
79
+ }
80
+ return {
81
+ Authorization: `Bearer ${apiKey}`,
82
+ "Content-Type": "application/json"
83
+ };
84
+ }
85
+ async function whoami() {
86
+ const apiUrl = getApiUrl();
87
+ const response = await fetch(`${apiUrl}/cli/whoami`, {
88
+ method: "GET",
89
+ headers: getHeaders(),
90
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
91
+ });
92
+ if (!response.ok) {
93
+ const error = await response.json().catch(() => ({}));
94
+ throw new ApiError(
95
+ error.error ?? `API error: ${response.status}`,
96
+ response.status
97
+ );
98
+ }
99
+ return response.json();
100
+ }
101
+ async function runReview(options) {
102
+ const apiUrl = getApiUrl();
103
+ const response = await fetch(`${apiUrl}/cli/review`, {
104
+ method: "POST",
105
+ headers: getHeaders(),
106
+ body: JSON.stringify(options),
107
+ signal: AbortSignal.timeout(REVIEW_TIMEOUT_MS)
108
+ });
109
+ if (!response.ok) {
110
+ const error = await response.json().catch(() => ({}));
111
+ throw new ApiError(
112
+ error.error ?? `API error: ${response.status}`,
113
+ response.status
114
+ );
115
+ }
116
+ return response.json();
117
+ }
118
+
119
+ // src/commands/login.ts
120
+ function promptApiKey() {
121
+ return new Promise((resolve) => {
122
+ const { stdin, stdout } = process;
123
+ stdout.write("Enter your API key: ");
124
+ const wasRaw = stdin.isRaw;
125
+ if (stdin.isTTY) {
126
+ stdin.setRawMode(true);
127
+ }
128
+ stdin.resume();
129
+ stdin.setEncoding("utf-8");
130
+ let input = "";
131
+ const onData = (char) => {
132
+ if (char === "\n" || char === "\r" || char === "") {
133
+ stdout.write("\n");
134
+ stdin.removeListener("data", onData);
135
+ stdin.pause();
136
+ if (stdin.isTTY && wasRaw !== void 0) {
137
+ stdin.setRawMode(wasRaw);
138
+ }
139
+ resolve(input.trim());
140
+ return;
141
+ }
142
+ if (char === "\x7F" || char === "\b") {
143
+ if (input.length > 0) {
144
+ input = input.slice(0, -1);
145
+ stdout.write("\b \b");
146
+ }
147
+ return;
148
+ }
149
+ if (char === "") {
150
+ stdout.write("\n");
151
+ process.exit(130);
152
+ }
153
+ input += char;
154
+ stdout.write("*");
155
+ };
156
+ stdin.on("data", onData);
157
+ });
158
+ }
159
+ async function loginCommand() {
160
+ console.log(chalk.bold("\n\u{1F511} Grepper Login\n"));
161
+ console.log("Get your API key from the Grepper dashboard:");
162
+ console.log(chalk.dim("https://grepper.dev/dashboard/settings\n"));
163
+ const apiKey = await promptApiKey();
164
+ if (!apiKey) {
165
+ console.error(chalk.red("No API key provided."));
166
+ process.exit(1);
167
+ }
168
+ if (!apiKey.startsWith("grp_")) {
169
+ console.error(
170
+ chalk.red('Invalid API key format. Keys should start with "grp_".')
171
+ );
172
+ process.exit(1);
173
+ }
174
+ try {
175
+ setApiKey(apiKey);
176
+ const info = await whoami();
177
+ console.log(chalk.green("\n\u2713 Logged in successfully!\n"));
178
+ console.log(` User: ${info.user.name} (${info.user.email})`);
179
+ console.log(` Org: ${info.org.name} (${info.org.slug})`);
180
+ console.log(chalk.dim("\n Credentials saved to ~/.grepper/config.json"));
181
+ } catch (error) {
182
+ setApiKey("");
183
+ if (error instanceof TypeError || error instanceof Error && error.message.includes("fetch")) {
184
+ console.error(
185
+ chalk.red(
186
+ "\nNetwork error: Could not connect to Grepper. Check your internet connection."
187
+ )
188
+ );
189
+ process.exit(1);
190
+ }
191
+ console.error(
192
+ chalk.red(
193
+ `
194
+ Failed to validate API key: ${error instanceof Error ? error.message : "Unknown error"}`
195
+ )
196
+ );
197
+ process.exit(1);
198
+ }
199
+ }
200
+
201
+ // src/commands/logout.ts
202
+ import chalk2 from "chalk";
203
+ function logoutCommand() {
204
+ if (!isLoggedIn()) {
205
+ console.error(chalk2.yellow("You are not logged in."));
206
+ return;
207
+ }
208
+ clearCredentials();
209
+ console.log(chalk2.green("\u2713 Logged out successfully."));
210
+ console.log(chalk2.dim(" Credentials removed from ~/.grepper/config.json"));
211
+ }
212
+
213
+ // src/commands/review.ts
214
+ import chalk4 from "chalk";
215
+ import ora from "ora";
216
+
217
+ // src/lib/context.ts
218
+ import { execFileSync, execSync } from "child_process";
219
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
220
+ import { join as join2 } from "path";
221
+ var MAX_DIFF_SIZE = 100 * 1024;
222
+ var SSH_REMOTE_REGEX = /^git@[^:]+:([^/]+\/[^/]+?)(?:\.git)?$/;
223
+ var GIT_SUFFIX_REGEX = /\.git$/;
224
+ function parseRepoFullName(remoteUrl) {
225
+ const sshMatch = remoteUrl.match(SSH_REMOTE_REGEX);
226
+ if (sshMatch?.[1]) {
227
+ return sshMatch[1];
228
+ }
229
+ try {
230
+ const url = new URL(remoteUrl);
231
+ const parts = url.pathname.replace(GIT_SUFFIX_REGEX, "").split("/").filter(Boolean);
232
+ if (parts.length >= 2) {
233
+ return `${parts[0]}/${parts[1]}`;
234
+ }
235
+ } catch {
236
+ }
237
+ return null;
238
+ }
239
+ function getRepoFullName() {
240
+ try {
241
+ const remoteUrl = execFileSync("git", ["remote", "get-url", "origin"], {
242
+ encoding: "utf-8",
243
+ stdio: ["pipe", "pipe", "pipe"]
244
+ }).trim();
245
+ return parseRepoFullName(remoteUrl);
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+ function getProjectRoot() {
251
+ try {
252
+ const root = execSync("git rev-parse --show-toplevel", {
253
+ encoding: "utf-8",
254
+ stdio: ["pipe", "pipe", "pipe"]
255
+ }).trim();
256
+ return root;
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+ function hasStagedChanges() {
262
+ try {
263
+ execSync("git diff --cached --quiet", {
264
+ stdio: ["pipe", "pipe", "pipe"]
265
+ });
266
+ return false;
267
+ } catch {
268
+ return true;
269
+ }
270
+ }
271
+ function hasUnstagedChanges() {
272
+ try {
273
+ execSync("git diff --quiet", {
274
+ stdio: ["pipe", "pipe", "pipe"]
275
+ });
276
+ return false;
277
+ } catch {
278
+ return true;
279
+ }
280
+ }
281
+ function getGitDiff(options = {}) {
282
+ const args = ["diff"];
283
+ if (options.base) {
284
+ args.push(`${options.base}...HEAD`);
285
+ } else if (options.staged) {
286
+ args.push("--cached");
287
+ } else if (options.all) {
288
+ args.push("HEAD");
289
+ }
290
+ if (options.files?.length) {
291
+ args.push("--");
292
+ args.push(...options.files);
293
+ }
294
+ try {
295
+ const diff = execFileSync("git", args, {
296
+ encoding: "utf-8",
297
+ maxBuffer: 10 * 1024 * 1024,
298
+ stdio: ["pipe", "pipe", "pipe"]
299
+ });
300
+ if (diff.length > MAX_DIFF_SIZE) {
301
+ return {
302
+ diff: diff.slice(0, MAX_DIFF_SIZE),
303
+ truncated: true
304
+ };
305
+ }
306
+ return { diff, truncated: false };
307
+ } catch (error) {
308
+ if (error instanceof Error && "stdout" in error) {
309
+ const stdout = error.stdout ?? "";
310
+ if (stdout.length > MAX_DIFF_SIZE) {
311
+ return {
312
+ diff: stdout.slice(0, MAX_DIFF_SIZE),
313
+ truncated: true
314
+ };
315
+ }
316
+ return { diff: stdout, truncated: false };
317
+ }
318
+ return { diff: "", truncated: false };
319
+ }
320
+ }
321
+ function readClaudeMd(projectRoot) {
322
+ const claudeDir = join2(projectRoot, ".claude", "CLAUDE.md");
323
+ if (existsSync2(claudeDir)) {
324
+ return readFileSync2(claudeDir, "utf-8");
325
+ }
326
+ const claudeRoot = join2(projectRoot, "CLAUDE.md");
327
+ if (existsSync2(claudeRoot)) {
328
+ return readFileSync2(claudeRoot, "utf-8");
329
+ }
330
+ return void 0;
331
+ }
332
+ function readAgentsMd(projectRoot) {
333
+ const agentsPath = join2(projectRoot, "agents.md");
334
+ if (existsSync2(agentsPath)) {
335
+ return readFileSync2(agentsPath, "utf-8");
336
+ }
337
+ return void 0;
338
+ }
339
+ function readLocalConfig(projectRoot) {
340
+ const configPath = join2(projectRoot, ".grepper.json");
341
+ if (existsSync2(configPath)) {
342
+ try {
343
+ const data = readFileSync2(configPath, "utf-8");
344
+ return JSON.parse(data);
345
+ } catch {
346
+ }
347
+ }
348
+ return void 0;
349
+ }
350
+
351
+ // src/lib/output.ts
352
+ import chalk3 from "chalk";
353
+ var SEVERITY_ICONS = {
354
+ critical: "\u{1F534}",
355
+ warning: "\u{1F7E1}",
356
+ suggestion: "\u{1F535}",
357
+ nitpick: "\u26AA",
358
+ praise: "\u{1F7E2}"
359
+ };
360
+ var SEVERITY_COLORS = {
361
+ critical: chalk3.red,
362
+ warning: chalk3.yellow,
363
+ suggestion: chalk3.blue,
364
+ nitpick: chalk3.gray,
365
+ praise: chalk3.green
366
+ };
367
+ function formatFinding(finding) {
368
+ const icon = SEVERITY_ICONS[finding.severity];
369
+ const color = SEVERITY_COLORS[finding.severity];
370
+ const lines = [];
371
+ const lineRange = finding.endLine && finding.endLine !== finding.startLine ? `${finding.startLine}-${finding.endLine}` : String(finding.startLine);
372
+ lines.push(
373
+ `${icon} ${color.bold(finding.severity.charAt(0).toUpperCase() + finding.severity.slice(1))}: ${finding.title}`
374
+ );
375
+ lines.push(chalk3.dim(` ${finding.file}:${lineRange}`));
376
+ lines.push(` ${finding.message}`);
377
+ if (finding.suggestion) {
378
+ lines.push("");
379
+ lines.push(chalk3.dim(" Suggested fix:"));
380
+ const suggestionLines = finding.suggestion.split("\n");
381
+ for (const line of suggestionLines) {
382
+ if (line.startsWith("+")) {
383
+ lines.push(chalk3.green(` ${line}`));
384
+ } else if (line.startsWith("-")) {
385
+ lines.push(chalk3.red(` ${line}`));
386
+ } else {
387
+ lines.push(chalk3.dim(` ${line}`));
388
+ }
389
+ }
390
+ }
391
+ return lines.join("\n");
392
+ }
393
+ function formatPretty(output) {
394
+ const lines = [];
395
+ lines.push(
396
+ `
397
+ ${chalk3.bold("\u{1F4CB} Reviewed")} ${output.reviewedFiles.length} file${output.reviewedFiles.length === 1 ? "" : "s"}`
398
+ );
399
+ if (output.truncated) {
400
+ lines.push(chalk3.yellow("\u26A0\uFE0F Diff was truncated due to size limits"));
401
+ }
402
+ lines.push("");
403
+ const critical = output.findings.filter((f) => f.severity === "critical");
404
+ const warning = output.findings.filter((f) => f.severity === "warning");
405
+ const suggestion = output.findings.filter((f) => f.severity === "suggestion");
406
+ const nitpick = output.findings.filter((f) => f.severity === "nitpick");
407
+ const praise = output.findings.filter((f) => f.severity === "praise");
408
+ const allFindings = [
409
+ ...critical,
410
+ ...warning,
411
+ ...suggestion,
412
+ ...nitpick,
413
+ ...praise
414
+ ];
415
+ if (allFindings.length === 0) {
416
+ lines.push(chalk3.green("\u2728 No issues found!"));
417
+ } else {
418
+ for (const finding of allFindings) {
419
+ lines.push(formatFinding(finding));
420
+ lines.push("");
421
+ }
422
+ }
423
+ lines.push("\u2500".repeat(40));
424
+ const counts = [];
425
+ if (critical.length > 0) {
426
+ counts.push(chalk3.red(`${critical.length} critical`));
427
+ }
428
+ if (warning.length > 0) {
429
+ counts.push(chalk3.yellow(`${warning.length} warning`));
430
+ }
431
+ if (suggestion.length > 0) {
432
+ counts.push(chalk3.blue(`${suggestion.length} suggestion`));
433
+ }
434
+ if (nitpick.length > 0) {
435
+ counts.push(chalk3.gray(`${nitpick.length} nitpick`));
436
+ }
437
+ if (praise.length > 0) {
438
+ counts.push(chalk3.green(`${praise.length} praise`));
439
+ }
440
+ if (counts.length > 0) {
441
+ lines.push(`Summary: ${counts.join(", ")}`);
442
+ }
443
+ if (output.summary) {
444
+ lines.push(chalk3.dim(output.summary));
445
+ }
446
+ return lines.join("\n");
447
+ }
448
+ function formatJson(output) {
449
+ return JSON.stringify(output, null, 2);
450
+ }
451
+ function formatFindingMarkdown(finding) {
452
+ const lines = [];
453
+ const lineRange = finding.endLine && finding.endLine !== finding.startLine ? `${finding.startLine}-${finding.endLine}` : String(finding.startLine);
454
+ lines.push(`#### ${finding.title}`);
455
+ lines.push(`\u{1F4CD} \`${finding.file}:${lineRange}\`
456
+ `);
457
+ lines.push(`${finding.message}
458
+ `);
459
+ if (finding.suggestion) {
460
+ lines.push("**Suggested fix:**");
461
+ lines.push("```diff");
462
+ lines.push(finding.suggestion);
463
+ lines.push("```\n");
464
+ }
465
+ return lines;
466
+ }
467
+ function groupBySeverity(findings) {
468
+ const bySeverity = /* @__PURE__ */ new Map();
469
+ for (const finding of findings) {
470
+ const list = bySeverity.get(finding.severity) ?? [];
471
+ list.push(finding);
472
+ bySeverity.set(finding.severity, list);
473
+ }
474
+ return bySeverity;
475
+ }
476
+ var SEVERITY_ORDER = [
477
+ "critical",
478
+ "warning",
479
+ "suggestion",
480
+ "nitpick",
481
+ "praise"
482
+ ];
483
+ function formatMarkdown(output) {
484
+ const lines = [];
485
+ lines.push("## Code Review Results\n");
486
+ if (output.truncated) {
487
+ lines.push(
488
+ "> \u26A0\uFE0F **Note:** Diff was truncated due to size limits. Some files may not have been reviewed.\n"
489
+ );
490
+ }
491
+ if (output.findings.length === 0) {
492
+ lines.push("\u2728 No issues found!\n");
493
+ } else {
494
+ const bySeverity = groupBySeverity(output.findings);
495
+ for (const severity of SEVERITY_ORDER) {
496
+ const findings = bySeverity.get(severity);
497
+ if (!findings?.length) {
498
+ continue;
499
+ }
500
+ const icon = SEVERITY_ICONS[severity];
501
+ const title = severity.charAt(0).toUpperCase() + severity.slice(1);
502
+ lines.push(`### ${icon} ${title} (${findings.length})
503
+ `);
504
+ for (const finding of findings) {
505
+ lines.push(...formatFindingMarkdown(finding));
506
+ }
507
+ }
508
+ }
509
+ lines.push("---\n");
510
+ lines.push(`**Summary:** ${output.summary}`);
511
+ lines.push(`
512
+ **Files reviewed:** ${output.reviewedFiles.join(", ")}`);
513
+ return lines.join("\n");
514
+ }
515
+ function formatOutput(output, format) {
516
+ switch (format) {
517
+ case "json":
518
+ return formatJson(output);
519
+ case "markdown":
520
+ return formatMarkdown(output);
521
+ default:
522
+ return formatPretty(output);
523
+ }
524
+ }
525
+
526
+ // src/commands/review.ts
527
+ function determineDiffOptions(options) {
528
+ if (options.files?.length) {
529
+ return { files: options.files };
530
+ }
531
+ if (options.base) {
532
+ return { base: options.base };
533
+ }
534
+ if (options.staged) {
535
+ return { staged: true };
536
+ }
537
+ if (options.all) {
538
+ return { all: true };
539
+ }
540
+ if (hasStagedChanges()) {
541
+ console.error(chalk4.dim("Reviewing staged changes..."));
542
+ return { staged: true };
543
+ }
544
+ if (hasUnstagedChanges()) {
545
+ console.error(chalk4.dim("Reviewing unstaged changes..."));
546
+ return {};
547
+ }
548
+ return null;
549
+ }
550
+ function handleReviewError(error) {
551
+ if (error instanceof ApiError) {
552
+ console.error(chalk4.red(`API Error: ${error.message}`));
553
+ process.exit(2);
554
+ }
555
+ if (error instanceof TypeError || error instanceof Error && error.message.includes("fetch")) {
556
+ console.error(
557
+ chalk4.red(
558
+ "Network error: Could not connect to Grepper. Check your internet connection."
559
+ )
560
+ );
561
+ process.exit(2);
562
+ }
563
+ console.error(
564
+ chalk4.red(
565
+ `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`
566
+ )
567
+ );
568
+ process.exit(2);
569
+ }
570
+ function showContextInfo(context) {
571
+ console.error(chalk4.dim(`Mode: ${context.mode}`));
572
+ if (context.repoFullName) {
573
+ console.error(chalk4.dim(`Repository: ${context.repoFullName}`));
574
+ }
575
+ if (context.claudeMd) {
576
+ console.error(chalk4.dim("Found CLAUDE.md"));
577
+ }
578
+ if (context.agentsMd) {
579
+ console.error(chalk4.dim("Found agents.md"));
580
+ }
581
+ if (context.truncated) {
582
+ console.error(
583
+ chalk4.yellow("Diff truncated to 100KB. Some files may not be reviewed.")
584
+ );
585
+ }
586
+ }
587
+ function showRepoStatus(repoFullName, repoConnected, skillCount) {
588
+ if (repoConnected) {
589
+ console.error(
590
+ chalk4.dim(`Repository: ${repoFullName} (${skillCount ?? 0} skills)`)
591
+ );
592
+ } else {
593
+ console.error(
594
+ chalk4.dim(
595
+ "Tip: Connect this repo at grepper.dev/dashboard for repo-specific review rules"
596
+ )
597
+ );
598
+ }
599
+ }
600
+ async function reviewCommand(options) {
601
+ if (!isLoggedIn()) {
602
+ console.error(chalk4.yellow("Not logged in. Run `grepper login` first."));
603
+ process.exit(1);
604
+ }
605
+ const projectRoot = getProjectRoot();
606
+ if (!projectRoot) {
607
+ console.error(chalk4.red("Not in a git repository."));
608
+ process.exit(1);
609
+ }
610
+ const diffOptions = determineDiffOptions(options);
611
+ if (diffOptions === null) {
612
+ console.error(chalk4.yellow("No changes to review."));
613
+ console.error(
614
+ chalk4.dim(
615
+ "Stage some changes with `git add` or use `--base main` to compare against a branch."
616
+ )
617
+ );
618
+ process.exit(0);
619
+ }
620
+ const { diff, truncated } = getGitDiff(diffOptions);
621
+ if (!diff.trim()) {
622
+ console.error(chalk4.yellow("No changes to review."));
623
+ process.exit(0);
624
+ }
625
+ const repoFullName = getRepoFullName();
626
+ const claudeMd = readClaudeMd(projectRoot);
627
+ const agentsMd = readAgentsMd(projectRoot);
628
+ const localConfig = readLocalConfig(projectRoot);
629
+ const mode = options.mode ?? localConfig?.mode ?? "full";
630
+ const format = options.format ?? "pretty";
631
+ if (format === "pretty") {
632
+ showContextInfo({ mode, repoFullName, claudeMd, agentsMd, truncated });
633
+ }
634
+ const spinner = format === "pretty" ? ora("Analyzing diff...").start() : null;
635
+ try {
636
+ const output = await runReview({
637
+ diff,
638
+ mode,
639
+ claudeMd,
640
+ agentsMd,
641
+ repoFullName: repoFullName ?? void 0
642
+ });
643
+ if (truncated) {
644
+ output.truncated = true;
645
+ }
646
+ spinner?.stop();
647
+ console.log(formatOutput(output, format));
648
+ if (format === "pretty" && repoFullName) {
649
+ showRepoStatus(repoFullName, output.repoConnected, output.skillCount);
650
+ }
651
+ if (options.failOnCritical) {
652
+ const hasCritical = output.findings.some(
653
+ (f) => f.severity === "critical"
654
+ );
655
+ if (hasCritical) {
656
+ process.exit(1);
657
+ }
658
+ }
659
+ } catch (error) {
660
+ spinner?.stop();
661
+ handleReviewError(error);
662
+ }
663
+ }
664
+
665
+ // src/commands/whoami.ts
666
+ import chalk5 from "chalk";
667
+ async function whoamiCommand() {
668
+ if (!isLoggedIn()) {
669
+ console.error(chalk5.yellow("Not logged in. Run `grepper login` first."));
670
+ process.exit(1);
671
+ }
672
+ try {
673
+ const info = await whoami();
674
+ console.log(chalk5.bold("\n\u{1F4CB} Current Session\n"));
675
+ console.log(` User: ${info.user.name} (${info.user.email})`);
676
+ console.log(` Org: ${info.org.name} (${info.org.slug})`);
677
+ } catch (error) {
678
+ if (error instanceof TypeError || error instanceof Error && error.message.includes("fetch")) {
679
+ console.error(
680
+ chalk5.red(
681
+ "Network error: Could not connect to Grepper. Check your internet connection."
682
+ )
683
+ );
684
+ process.exit(1);
685
+ }
686
+ console.error(
687
+ chalk5.red(
688
+ `Failed to get user info: ${error instanceof Error ? error.message : "Unknown error"}`
689
+ )
690
+ );
691
+ process.exit(1);
692
+ }
693
+ }
694
+
695
+ // src/index.ts
696
+ updateNotifier({
697
+ pkg: { name: "grepper-dev", version: "0.2.0" }
698
+ }).notify();
699
+ var program = new Command();
700
+ program.name("grepper").description("Local code review powered by AI").version("0.2.0");
701
+ program.command("login").description("Log in with your Grepper API key").action(async () => {
702
+ await loginCommand();
703
+ });
704
+ program.command("logout").description("Log out and clear credentials").action(() => {
705
+ logoutCommand();
706
+ });
707
+ program.command("whoami").description("Show current user and organization").action(async () => {
708
+ await whoamiCommand();
709
+ });
710
+ program.command("review", { isDefault: true }).description("Review code changes").option("-s, --staged", "Review staged changes only").option("-a, --all", "Review all uncommitted changes (staged + unstaged)").option("-b, --base <branch>", "Review changes against a branch (e.g., main)").addOption(
711
+ new Option("-m, --mode <mode>", "Review mode").choices(["full", "quick", "security"]).default("full")
712
+ ).addOption(
713
+ new Option("-f, --format <format>", "Output format").choices(["pretty", "json", "markdown"]).default("pretty")
714
+ ).option("--fail-on-critical", "Exit with code 1 if critical issues found").argument("[files...]", "Specific files to review").action(async (files, opts) => {
715
+ await reviewCommand({
716
+ staged: opts.staged,
717
+ all: opts.all,
718
+ base: opts.base,
719
+ files: files.length > 0 ? files : void 0,
720
+ mode: opts.mode,
721
+ format: opts.format,
722
+ failOnCritical: opts.failOnCritical
723
+ });
724
+ });
725
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "grepper-dev",
3
+ "version": "0.2.0",
4
+ "description": "Grepper CLI - Local code review powered by AI",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "grepper": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/avr00/grepper.git",
16
+ "directory": "packages/cli"
17
+ },
18
+ "homepage": "https://grepper.dev",
19
+ "bugs": {
20
+ "url": "https://github.com/avr00/grepper/issues"
21
+ },
22
+ "keywords": [
23
+ "code-review",
24
+ "ai",
25
+ "cli",
26
+ "linter",
27
+ "static-analysis",
28
+ "developer-tools"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "dependencies": {
34
+ "chalk": "^5.4.1",
35
+ "commander": "^14.0.0",
36
+ "ora": "^9.3.0",
37
+ "update-notifier": "^7.3.1"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^24.3.0",
41
+ "@types/update-notifier": "^6.0.8",
42
+ "tsup": "^8.5.0",
43
+ "typescript": "^5",
44
+ "@grepper/config": "0.0.0"
45
+ },
46
+ "scripts": {
47
+ "build": "tsup",
48
+ "dev": "tsup --watch",
49
+ "typecheck": "tsc --noEmit"
50
+ }
51
+ }