pika-review 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pika Review Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Pika Review 🦊
2
+ ### Enterprise-grade AI Architectural Sentinel
3
+
4
+ Pika Review is a high-performance CLI tool designed to perform surgical code reviews using Cloudflare Workers AI. It focuses on deep architectural debt, security vulnerabilities, and UI/UX anomalies that traditional linters miss.
5
+
6
+ ---
7
+
8
+ ## 🚀 Key Features
9
+ - **Provider Agnostic**: Seamlessly works with Cloudflare Workers AI, OpenAI, Grok, or local LLMs via OpenAI-compatible endpoints.
10
+ - **Mixture of Experts (MoE) Reasoning**: Leverages advanced LLMs for deep structural analysis.
11
+ - **Polyglot Heuristics**: Idiom-aware reviews across Python, JS/TS, Go, Rust, and React.
12
+ - **Interactive UI**: Claude-inspired terminal experience with real-time progress and multi-select discovery.
13
+ - **Enterprise-Ready**: Built-in token safety, JSON self-healing, and CI/CD integration.
14
+
15
+ ---
16
+
17
+ ## 📦 Installation
18
+ ```bash
19
+ # Via Bun (Recommended)
20
+ bun add -g pika-review
21
+
22
+ # Via NPM
23
+ npm install -g pika-review
24
+ ```
25
+
26
+ ## 🛠️ Setup
27
+ 1. Initialize your configuration:
28
+ ```bash
29
+ pika-review init
30
+ ```
31
+ 2. Open `~/.pika-review.yaml` and add your Cloudflare credentials:
32
+ ```yaml
33
+ ai:
34
+ accountId: "your-account-id"
35
+ apiKey: "your-scoped-api-token"
36
+ ```
37
+
38
+ ### 🌍 Custom AI Providers (OpenAI, Grok, etc.)
39
+ Pika Review works with any OpenAI-compatible API. To use a different provider, simply override the `baseURL` and `model` in your config:
40
+
41
+ ```yaml
42
+ ai:
43
+ apiKey: "your-openai-key"
44
+ model: "gpt-4o"
45
+ baseURL: "https://api.openai.com/v1"
46
+ ```
47
+
48
+ ## 🔍 Usage
49
+ Scan staged git changes:
50
+ ```bash
51
+ pika-review scan
52
+ ```
53
+
54
+ Scan specific files:
55
+ ```bash
56
+ pika-review scan src/index.ts src/utils/token.ts
57
+ ```
58
+
59
+ Scan unstaged changes:
60
+ ```bash
61
+ pika-review scan --unstaged
62
+ ```
63
+
64
+ ### CI/CD Mode
65
+ Use the `--ci` flag in GitHub Actions or other pipelines to fail if Critical or High severity issues are found:
66
+ ```bash
67
+ pika-review scan --ci
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 🛡️ Privacy & Security
73
+ Pika Review uses your local git diffs and sends them directly to your Cloudflare Workers AI instance. No code is stored by Pika Review.
74
+
75
+ ## 📄 License
76
+ MIT © Pika Review Contributors
@@ -0,0 +1,8 @@
1
+ import { initConfig } from "../utils/config.js";
2
+ /**
3
+ * CLI Command: init
4
+ * Initializes the global configuration file.
5
+ */
6
+ export async function initAction() {
7
+ initConfig();
8
+ }
@@ -0,0 +1,64 @@
1
+ import prompts from "prompts";
2
+ import chalk from "chalk";
3
+ import { runScan, getDiffFiles, listProjectFiles } from "../core/scanner.js";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * CLI Command: scan
7
+ * Executes the scanning logic and handles user interaction for reports.
8
+ */
9
+ export async function scanAction(files, options) {
10
+ const target = options.unstaged ? "unstaged" : "staged";
11
+ const isCI = !!options.ci;
12
+ let targets = files.length > 0 ? files : undefined;
13
+ // Interactive Discovery: If no files provided and no git changes, ask the user
14
+ if (!targets && !isCI) {
15
+ const gitChanges = getDiffFiles(target);
16
+ if (gitChanges.length === 0) {
17
+ logger.info("No git changes detected. Entering interactive selection mode...");
18
+ const allFiles = listProjectFiles();
19
+ if (allFiles.length > 0) {
20
+ const selection = await prompts({
21
+ type: "multiselect",
22
+ name: "files",
23
+ message: "Select files to analyze (Space to select, Enter to confirm):",
24
+ choices: allFiles.map(f => ({ title: f, value: f })),
25
+ hint: "- Space to select. Return to submit"
26
+ });
27
+ if (selection.files && selection.files.length > 0) {
28
+ targets = selection.files;
29
+ }
30
+ }
31
+ }
32
+ }
33
+ let reports = await runScan(target, isCI, targets);
34
+ if (!reports || reports.length === 0) {
35
+ if (!isCI) {
36
+ logger.success("Scan complete. No critical architectural anomalies detected.");
37
+ logger.dim("Try scanning with '--unstaged' if you have uncommitted changes.");
38
+ }
39
+ return;
40
+ }
41
+ if (!isCI) {
42
+ console.log(chalk.bold(`\n${"=".repeat(50)}`));
43
+ logger.warn(`ANALYTICAL REPORT READY`);
44
+ logger.dim(`${reports.length} file(s) require your immediate attention.\n`);
45
+ const response = await prompts({
46
+ type: "select",
47
+ name: "action",
48
+ message: "What is your next step?",
49
+ choices: [
50
+ { title: "📂 Open generated Markdown reports", value: "view" },
51
+ { title: "🛑 Exit and start refactoring", value: "exit" },
52
+ ],
53
+ initial: 0,
54
+ });
55
+ if (response.action === "view") {
56
+ logger.info("\nGenerated Artifacts:");
57
+ reports.forEach((r) => {
58
+ const fileName = r.split("/").pop();
59
+ console.log(` ${chalk.cyan("→")} ${chalk.bold(fileName)} ${chalk.dim(`(${r})`)}`);
60
+ });
61
+ console.log(chalk.dim("\nTip: Use 'cat' or a Markdown viewer to read the findings."));
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,146 @@
1
+ import OpenAI from "openai";
2
+ import { ReviewSchema } from "../utils/schema.js";
3
+ import { getConfig } from "../utils/config.js";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * Structural Extractor: Isolate the JSON object from AI chatter.
7
+ * MoE (Mixture of Experts) models often add preamble or postscript;
8
+ * this ensures we only parse the valid payload even if wrapped in markdown.
9
+ */
10
+ export function extractJSON(raw) {
11
+ // Strip common AI "chatter" markers and control characters that break JSON.parse
12
+ let cleaned = raw
13
+ .replace(/```json/g, "")
14
+ .replace(/```/g, "")
15
+ .replace(/[\u0000-\u001F\u007F-\u009F]/g, (match) => {
16
+ // Keep only safe whitespace: \n, \r, \t
17
+ return ["\n", "\r", "\t"].includes(match) ? match : "";
18
+ });
19
+ // Find the outermost JSON structure
20
+ const firstBrace = cleaned.indexOf("{");
21
+ const firstBracket = cleaned.indexOf("[");
22
+ // If it's a raw array (starts with [ before {), wrap it
23
+ if (firstBracket !== -1 && (firstBrace === -1 || firstBracket < firstBrace)) {
24
+ const lastBracket = cleaned.lastIndexOf("]");
25
+ if (lastBracket !== -1) {
26
+ return `{"reviews": ${cleaned.substring(firstBracket, lastBracket + 1)}}`;
27
+ }
28
+ }
29
+ const start = firstBrace;
30
+ const end = cleaned.lastIndexOf("}");
31
+ if (start === -1 || end === -1) {
32
+ return '{"reviews": []}';
33
+ }
34
+ let snippet = cleaned.substring(start, end + 1);
35
+ // Heuristic: MoE models sometimes miss commas between objects in an array
36
+ snippet = snippet.replace(/\}\s*\{/g, "},{");
37
+ // Heuristic Structural Recovery for Truncated Responses
38
+ const openBraces = (snippet.match(/\{/g) || []).length;
39
+ const closeBraces = (snippet.match(/\}/g) || []).length;
40
+ const openBrackets = (snippet.match(/\[/g) || []).length;
41
+ const closeBrackets = (snippet.match(/\]/g) || []).length;
42
+ // Close in reverse order of nesting (assumes standard structure)
43
+ if (openBrackets > closeBrackets) {
44
+ snippet += "]".repeat(openBrackets - closeBrackets);
45
+ }
46
+ if (openBraces > closeBraces) {
47
+ snippet += "}".repeat(openBraces - closeBraces);
48
+ }
49
+ return snippet;
50
+ }
51
+ /**
52
+ * Deep Analysis Engine: Communicates with Cloudflare Workers AI.
53
+ * Uses XML-structured prompts for better instruction following in smaller LLMs.
54
+ */
55
+ export async function analyzeDiff(diff) {
56
+ const config = getConfig();
57
+ if (!config.ai.apiKey || !config.ai.accountId) {
58
+ throw new Error("Missing Cloudflare credentials in ~/.pika-review.yaml");
59
+ }
60
+ // Neuron Safety: Truncate input to protect the 10,000 daily free limit.
61
+ // 30,000 chars is roughly 7,500 tokens, leaving room for a detailed response.
62
+ const MAX_CHARS = 30000;
63
+ const safeDiff = diff.length > MAX_CHARS
64
+ ? diff.substring(0, MAX_CHARS) + "\n... [Content truncated for token safety]"
65
+ : diff;
66
+ const baseURL = (config.ai.baseURL && config.ai.baseURL.trim())
67
+ ? config.ai.baseURL
68
+ : `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`;
69
+ const openai = new OpenAI({
70
+ apiKey: config.ai.apiKey,
71
+ baseURL,
72
+ });
73
+ const defaultPrompt = `
74
+ <role>
75
+ You are an Elite Senior Full-Stack Architect and Security Researcher.
76
+ Your task is to perform a surgical review of the provided code.
77
+ </role>
78
+
79
+ <task>
80
+ Identify high-impact issues related to:
81
+ 1. SYSTEM COMPROMISE: Security vulnerabilities, insecure data handling.
82
+ 2. COMPUTATIONAL INEFFICIENCY: Algorithmic bottlenecks, memory leaks.
83
+ 3. ARCHITECTURAL DEBT: Logic flaws, redundant complexity.
84
+ 4. UI/UX ANOMALIES: Layout shifts, accessibility (a11y) violations, and CSS performance issues.
85
+ </task>
86
+
87
+ <instructions>
88
+ - Be language-agnostic but idiom-aware (Polyglot reasoning).
89
+ - Only report issues with clear, tangible impact.
90
+ - Line numbers must be accurate relative to the provided snippet.
91
+ - Return ONLY a JSON object matching the requested schema.
92
+ - ENSURE the JSON is syntactically correct: check all commas, braces, and double-quotes.
93
+ - DO NOT include trailing commas in arrays or objects.
94
+ - BE SUBSTANTIVE: Explain each finding and its impact briefly (2-3 sentences); avoid vague one-liners.
95
+ </instructions>
96
+
97
+ <output_format>
98
+ {
99
+ "reviews": [
100
+ {
101
+ "line": number,
102
+ "severity": "Critical" | "High" | "Medium" | "Low",
103
+ "finding": "A brief explanation of the anomaly and why it is problematic.",
104
+ "impact": "The specific technical or business consequence if left unaddressed.",
105
+ "recommendation": "The specific code correction or refactor."
106
+ }
107
+ ]
108
+ }
109
+ </output_format>
110
+
111
+ <code_to_analyze>
112
+ ${safeDiff}
113
+ </code_to_analyze>
114
+ `;
115
+ const finalPrompt = (config.ai.prompt && config.ai.prompt.trim())
116
+ ? config.ai.prompt.replace("{{code}}", safeDiff)
117
+ : defaultPrompt;
118
+ try {
119
+ const response = await openai.chat.completions.create({
120
+ model: config.ai.model,
121
+ messages: [{ role: "user", content: finalPrompt }],
122
+ temperature: 0.1, // Low temperature for analytical consistency
123
+ max_tokens: 1500, // Sufficient for multiple architectural findings
124
+ });
125
+ const rawContent = response.choices[0]?.message?.content || "{}";
126
+ const cleanedContent = extractJSON(rawContent);
127
+ try {
128
+ const parsedData = JSON.parse(cleanedContent);
129
+ return ReviewSchema.parse(parsedData);
130
+ }
131
+ catch (e) {
132
+ logger.error(`Structural recovery failed. AI returned malformed payload.`);
133
+ logger.dim(`Snippet: ${cleanedContent.substring(0, 100)}...`);
134
+ return { reviews: [] };
135
+ }
136
+ }
137
+ catch (error) {
138
+ if (error.status === 429 || error.status === 4006) {
139
+ const e = new Error("RATE_LIMIT");
140
+ e.status = 429;
141
+ throw e;
142
+ }
143
+ logger.error(`AI analysis failed: ${error.message}`);
144
+ return { reviews: [] };
145
+ }
146
+ }
@@ -0,0 +1,48 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ /**
4
+ * Report Orchestrator: Handles persistence of AI findings.
5
+ */
6
+ export function setupReportDir() {
7
+ const reportDir = path.join(process.cwd(), ".pika-reports");
8
+ if (!fs.existsSync(reportDir))
9
+ fs.mkdirSync(reportDir);
10
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
11
+ if (fs.existsSync(gitignorePath)) {
12
+ const gitignore = fs.readFileSync(gitignorePath, "utf-8");
13
+ if (!gitignore.includes(".pika-reports")) {
14
+ fs.appendFileSync(gitignorePath, "\n.pika-reports/\n");
15
+ }
16
+ }
17
+ return reportDir;
18
+ }
19
+ /**
20
+ * Generates a professional Markdown report.
21
+ */
22
+ export function writeMarkdownReport(fileName, reviews, reportDir) {
23
+ const safeName = fileName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
24
+ const reportPath = path.join(reportDir, `${safeName}_review.md`);
25
+ const timestamp = new Date().toLocaleString();
26
+ let mdContent = `# 🔍 Pika Review: \`${fileName}\`\n\n`;
27
+ mdContent += `> **Generated on:** ${timestamp}\n`;
28
+ mdContent += `> **Total Issues Found:** ${reviews.length}\n\n`;
29
+ reviews.forEach((issue) => {
30
+ const iconMap = {
31
+ Critical: "🚨",
32
+ High: "🔥",
33
+ Medium: "⚠️",
34
+ Low: "📝",
35
+ };
36
+ const icon = iconMap[issue.severity] || "⚠️";
37
+ mdContent += `## ${icon} [${issue.severity}] Line ${issue.line || "N/A"}\n\n`;
38
+ mdContent += `### 💡 Finding\n${issue.finding}\n\n`;
39
+ mdContent += `### 💥 Impact\n${issue.impact}\n\n`;
40
+ mdContent += `### 🛠️ Recommendation\n`;
41
+ // Attempt to detect language for syntax highlighting
42
+ const ext = path.extname(fileName).slice(1) || "typescript";
43
+ mdContent += `\`\`\`${ext}\n${issue.recommendation}\n\`\`\`\n\n`;
44
+ mdContent += `---\n\n`;
45
+ });
46
+ fs.writeFileSync(reportPath, mdContent);
47
+ return reportPath;
48
+ }
@@ -0,0 +1,169 @@
1
+ import { execFileSync } from "child_process";
2
+ import fs from "fs";
3
+ import cliProgress from "cli-progress";
4
+ import pLimit from "p-limit";
5
+ import { analyzeDiff } from "./ai.js";
6
+ import { setupReportDir, writeMarkdownReport } from "./reporter.js";
7
+ import { getIgnoredFiles } from "../utils/config.js";
8
+ import { logger } from "../utils/logger.js";
9
+ import path from "path";
10
+ import { validateTokenLimit } from "../utils/token.js";
11
+ /**
12
+ * Project Discovery: List all relevant files for interactive selection.
13
+ */
14
+ export function listProjectFiles(dir = ".", depth = 0) {
15
+ if (depth > 5)
16
+ return []; // Limit depth for performance
17
+ const ignores = getIgnoredFiles();
18
+ const files = [];
19
+ try {
20
+ const items = fs.readdirSync(dir, { withFileTypes: true });
21
+ for (const item of items) {
22
+ const fullPath = path.join(dir, item.name);
23
+ const isIgnored = ignores.some(ignore => item.name.includes(ignore.replace("*", "")));
24
+ if (isIgnored || item.name.startsWith("."))
25
+ continue;
26
+ if (item.isDirectory()) {
27
+ files.push(...listProjectFiles(fullPath, depth + 1));
28
+ }
29
+ else {
30
+ files.push(fullPath);
31
+ }
32
+ }
33
+ }
34
+ catch (e) {
35
+ // Silent fail for inaccessible dirs
36
+ }
37
+ return files;
38
+ }
39
+ /**
40
+ * Git Utilities: Extract file lists from the repository.
41
+ */
42
+ export function getDiffFiles(type) {
43
+ try {
44
+ const args = type === "staged"
45
+ ? ["diff", "--cached", "--name-only", "--diff-filter=ACMR"] // Only Added, Copied, Modified, Renamed
46
+ : ["diff", "--name-only", "--diff-filter=ACMR"];
47
+ const output = execFileSync("git", args, {
48
+ encoding: "utf-8",
49
+ stdio: ["ignore", "pipe", "ignore"]
50
+ });
51
+ const ignores = getIgnoredFiles();
52
+ return output.trim().split("\n").filter((file) => {
53
+ const trimmedFile = file.trim();
54
+ if (!trimmedFile)
55
+ return false;
56
+ // Filter out ignored patterns and ensure file exists
57
+ const isIgnored = ignores.some(ignore => trimmedFile.includes(ignore.replace("*", "")));
58
+ return !isIgnored && fs.existsSync(trimmedFile);
59
+ });
60
+ }
61
+ catch (e) {
62
+ logger.error("Git operation failed. Ensure you are in a git repository.");
63
+ return [];
64
+ }
65
+ }
66
+ /**
67
+ * Scan Orchestrator: Manages the review lifecycle.
68
+ * Concurrency is limited to 3 to stay within the Cloudflare Workers AI free tier constraints.
69
+ */
70
+ export async function runScan(target, isCI, specificFiles) {
71
+ let files = [];
72
+ if (specificFiles && specificFiles.length > 0) {
73
+ specificFiles.forEach(file => {
74
+ if (!fs.existsSync(file)) {
75
+ logger.error(`File not found: ${file}`);
76
+ process.exit(1);
77
+ }
78
+ });
79
+ files = specificFiles;
80
+ }
81
+ else {
82
+ files = getDiffFiles(target);
83
+ }
84
+ if (files.length === 0) {
85
+ if (!isCI)
86
+ logger.success("No changes detected. Codebase is clean.");
87
+ return [];
88
+ }
89
+ const reportDir = setupReportDir();
90
+ if (!isCI)
91
+ logger.info(`Initializing Pika Review on ${files.length} file(s)...`);
92
+ const bar = isCI ? null : new cliProgress.SingleBar({
93
+ format: ' {bar} {percentage}% | ETA: {eta}s | Scanning: {file}',
94
+ barCompleteChar: '\u2588',
95
+ barIncompleteChar: '\u2591',
96
+ hideCursor: true,
97
+ });
98
+ if (bar)
99
+ bar.start(files.length, 0, { file: "Starting..." });
100
+ const generatedReports = [];
101
+ let criticalIssuesFound = false;
102
+ const limit = pLimit(3);
103
+ let completed = 0;
104
+ const tasks = files.map((file) => limit(async () => {
105
+ let contentToScan = "";
106
+ try {
107
+ if (specificFiles) {
108
+ // Safety: Check file size before reading to prevent memory exhaustion
109
+ const stats = fs.statSync(file);
110
+ if (stats.size > 1024 * 1024) { // 1MB limit for safety
111
+ logger.warn(`Skipping ${file}: File too large (>1MB).`);
112
+ return;
113
+ }
114
+ contentToScan = fs.readFileSync(file, "utf-8");
115
+ }
116
+ else {
117
+ try {
118
+ contentToScan = execFileSync("git", ["diff", target === "staged" ? "--cached" : "", "--", file], {
119
+ encoding: "utf-8",
120
+ stdio: ["ignore", "pipe", "ignore"] // Suppress stderr
121
+ });
122
+ }
123
+ catch (gitError) {
124
+ logger.error(`Git diff failed for ${file}. Skipping.`);
125
+ return;
126
+ }
127
+ }
128
+ if (!contentToScan || !contentToScan.trim())
129
+ return;
130
+ // Binary Detection Heuristic: Check for null bytes or control characters in the first 1KB
131
+ if (/[\x00-\x08\x0E-\x1F]/.test(contentToScan.substring(0, 1024))) {
132
+ logger.dim(`Skipping ${file}: Binary file detected.`);
133
+ return;
134
+ }
135
+ // Token Estimator Check
136
+ validateTokenLimit(contentToScan);
137
+ const result = await analyzeDiff(contentToScan);
138
+ if (result.reviews.length > 0) {
139
+ const hasCritical = result.reviews.some(r => r.severity === "Critical" || r.severity === "High");
140
+ if (hasCritical)
141
+ criticalIssuesFound = true;
142
+ const reportPath = writeMarkdownReport(file, result.reviews, reportDir);
143
+ generatedReports.push(reportPath);
144
+ }
145
+ }
146
+ catch (e) {
147
+ if (e.message === "RATE_LIMIT") {
148
+ if (bar)
149
+ bar.stop();
150
+ logger.critical("DAILY NEURON LIMIT REACHED (10,000). Scanning aborted.");
151
+ process.exit(1);
152
+ }
153
+ logger.error(`Error scanning ${file}: ${e.message}`);
154
+ }
155
+ finally {
156
+ completed++;
157
+ if (bar)
158
+ bar.update(completed, { file });
159
+ }
160
+ }));
161
+ await Promise.all(tasks);
162
+ if (bar)
163
+ bar.stop();
164
+ if (isCI && criticalIssuesFound) {
165
+ logger.critical("CI Pipeline Failed: Critical issues detected.");
166
+ process.exit(1);
167
+ }
168
+ return generatedReports;
169
+ }
package/dist/index.js ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import chalk from "chalk";
4
+ import { initAction } from "./cmd/init.js";
5
+ import { scanAction } from "./cmd/scan.js";
6
+ /**
7
+ * Pika Review: The Enterprise-Grade AI Code Reviewer.
8
+ * Modular entry point for CLI operations.
9
+ */
10
+ const program = new Command();
11
+ // Branding Header
12
+ const BRAND = `
13
+ ${chalk.bgCyan.black.bold(" PIKA REVIEW ")} ${chalk.dim("v2.0.0")}
14
+ ${chalk.italic.gray("Enterprise-grade AI Architectural Sentinel")}
15
+ `;
16
+ const HELPER_TEXT = `
17
+ ${chalk.bold.cyan("🚀 Quick Start:")}
18
+ $ pika-review scan ${chalk.dim("# Scan staged git changes (default)")}
19
+ $ pika-review scan -u ${chalk.dim("# Scan unstaged git changes")}
20
+ $ pika-review scan file.ts ${chalk.dim("# Scan a specific file")}
21
+ $ pika-review scan f1 f2 ${chalk.dim("# Scan multiple specific files")}
22
+
23
+ ${chalk.bold.cyan("⌨️ Shortcuts & Tips:")}
24
+ - Use ${chalk.yellow("--ci")} in GitHub Actions to fail on Critical/High issues.
25
+ - Create a ${chalk.yellow(".pikaignore")} file to skip specific directories.
26
+ - Review artifacts are stored in ${chalk.yellow(".pika-reports/")} automatically.
27
+
28
+ ${chalk.bold.cyan("📊 Progress & Concurrency:")}
29
+ - Pika uses ${chalk.green("p-limit(3)")} to scan files in parallel without hitting rate limits.
30
+ - Real-time progress bars will show you the exact status of each analysis.
31
+ `;
32
+ program
33
+ .name("pika-review")
34
+ .description("Enterprise-grade AI Code Reviewer for Cloudflare Workers AI")
35
+ .version("2.0.0")
36
+ .addHelpText("before", BRAND)
37
+ .addHelpText("after", HELPER_TEXT);
38
+ program
39
+ .command("init")
40
+ .description("Initialize global configuration (~/.pika-review.yaml)")
41
+ .action(async () => {
42
+ console.log(BRAND);
43
+ await initAction();
44
+ });
45
+ program
46
+ .command("scan [files...]", { isDefault: true }) // Set scan as the default command
47
+ .description("Scan git changes or specific files for architectural anomalies")
48
+ .option("-u, --unstaged", "Analyze unstaged changes instead of staged")
49
+ .option("--ci", "Headless CI/CD mode (fails on Critical/High issues, strips visuals)")
50
+ .action(async (files, options) => {
51
+ // Only show branding if not in CI mode and if files were explicitly passed or if it's the default
52
+ if (!options.ci)
53
+ console.log(BRAND);
54
+ await scanAction(files, options);
55
+ });
56
+ program.parse();
@@ -0,0 +1,63 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import yaml from "yaml";
5
+ import { logger } from "./logger.js";
6
+ const CONFIG_PATH = path.join(os.homedir(), ".pika-review.yaml");
7
+ const DEFAULT_CONFIG = {
8
+ ai: {
9
+ accountId: "", // Required for Cloudflare default
10
+ apiKey: "",
11
+ model: "@cf/meta/llama-3-8b-instruct",
12
+ prompt: "",
13
+ baseURL: "", // Leave empty for Cloudflare, or set for OpenAI/Grok/Local
14
+ },
15
+ };
16
+ /**
17
+ * Initialize global configuration with restrictive permissions.
18
+ * We use 0o600 because this file contains sensitive API keys.
19
+ */
20
+ export function initConfig() {
21
+ if (!fs.existsSync(CONFIG_PATH)) {
22
+ fs.writeFileSync(CONFIG_PATH, yaml.stringify(DEFAULT_CONFIG), {
23
+ mode: 0o600,
24
+ });
25
+ logger.success(`Configuration initialized at ${CONFIG_PATH}`);
26
+ logger.info("Please edit this file and add your Cloudflare Account ID and API Token.");
27
+ }
28
+ else {
29
+ logger.warn(`Configuration already exists at ${CONFIG_PATH}`);
30
+ }
31
+ }
32
+ /**
33
+ * Retrieve configuration, failing fast if not found.
34
+ */
35
+ export function getConfig() {
36
+ if (!fs.existsSync(CONFIG_PATH)) {
37
+ logger.error("Configuration not found. Run 'pika-review init' first.");
38
+ process.exit(1);
39
+ }
40
+ return yaml.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
41
+ }
42
+ /**
43
+ * Get files to ignore during scanning.
44
+ */
45
+ export function getIgnoredFiles() {
46
+ const ignorePath = path.join(process.cwd(), ".pikaignore");
47
+ if (!fs.existsSync(ignorePath)) {
48
+ return [
49
+ ".svg", ".lock", "package-lock.json", ".png", ".jpg", ".jpeg", ".ico",
50
+ "node_modules", ".git", "dist", "build", "out", ".next", "public",
51
+ ".pika-reports", ".env", ".DS_Store", "bun.lockb", "pnpm-lock.yaml",
52
+ "venv", ".venv", "__pycache__", ".pytest_cache",
53
+ "target", ".gradle", ".idea", ".vscode", "vendor",
54
+ "coverage", ".turbo", "tests", "__tests__", "spec", "specs",
55
+ "cypress", "playwright-report", "test-results", ".nyc_output"
56
+ ];
57
+ }
58
+ const content = fs.readFileSync(ignorePath, "utf-8");
59
+ return content
60
+ .split("\n")
61
+ .map((line) => line.trim())
62
+ .filter(Boolean);
63
+ }
@@ -0,0 +1,12 @@
1
+ import chalk from "chalk";
2
+ /**
3
+ * Senior Logger: Standardized output with clear visual hierarchy.
4
+ */
5
+ export const logger = {
6
+ info: (msg) => console.log(chalk.cyan(msg)),
7
+ success: (msg) => console.log(chalk.green(`✅ ${msg}`)),
8
+ warn: (msg) => console.log(chalk.yellow(`⚠️ ${msg}`)),
9
+ error: (msg) => console.error(chalk.red(`❌ ${msg}`)),
10
+ critical: (msg) => console.error(chalk.bgRed.white.bold(` ⚡ ${msg} `)),
11
+ dim: (msg) => console.log(chalk.gray(msg)),
12
+ };
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Enterprise Schema for AI Code Review Output.
4
+ * Ensures the AI returns structured, actionable data.
5
+ */
6
+ export const ReviewSchema = z.object({
7
+ reviews: z.array(z.object({
8
+ line: z.number().optional(),
9
+ severity: z.enum(["Critical", "High", "Medium", "Low"]),
10
+ finding: z.string(),
11
+ impact: z.string(),
12
+ recommendation: z.string(),
13
+ })),
14
+ });
@@ -0,0 +1,21 @@
1
+ import { logger } from "./logger.js";
2
+ /**
3
+ * Token Safety Estimator.
4
+ * Heuristic: 1 token is roughly 4 characters in English code.
5
+ * This helps users avoid exhausting their 10,000 free neuron daily limit.
6
+ */
7
+ export function estimateTokens(text) {
8
+ return Math.ceil(text.length / 4);
9
+ }
10
+ /**
11
+ * Warn the user if the file is excessively large.
12
+ */
13
+ export function validateTokenLimit(text, limit = 7500) {
14
+ const estimation = estimateTokens(text);
15
+ if (estimation > limit) {
16
+ logger.warn(`Heuristic Alert: This file is estimated at ${estimation} tokens.`);
17
+ logger.dim(`Submitting this to AI might consume ~${estimation} neurons of your 10,000 daily limit.`);
18
+ return false;
19
+ }
20
+ return true;
21
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "pika-review",
3
+ "version": "2.0.0",
4
+ "description": "Enterprise-grade AI Architectural Code Reviewer powered by Cloudflare Workers AI.",
5
+ "main": "./dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "pika-review": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "bun run src/index.ts",
18
+ "start": "node dist/index.js",
19
+ "test": "bun test"
20
+ },
21
+ "keywords": [
22
+ "ai",
23
+ "code-review",
24
+ "cloudflare",
25
+ "workers-ai",
26
+ "architectural-analysis",
27
+ "cli"
28
+ ],
29
+ "author": "Pika Review Contributors",
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "chalk": "^5.6.2",
33
+ "cli-progress": "^3.12.0",
34
+ "commander": "^14.0.3",
35
+ "openai": "^6.34.0",
36
+ "ora": "^9.4.0",
37
+ "p-limit": "^7.3.0",
38
+ "prompts": "^2.4.2",
39
+ "yaml": "^2.8.3",
40
+ "zod": "^4.3.6"
41
+ },
42
+ "devDependencies": {
43
+ "@types/cli-progress": "^3.11.6",
44
+ "@types/node": "^25.6.0",
45
+ "@types/prompts": "^2.4.9",
46
+ "bun-types": "^1.3.13",
47
+ "tsx": "^4.21.0",
48
+ "typescript": "^6.0.3"
49
+ }
50
+ }