pika-review 2.0.1 → 2.1.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 CHANGED
@@ -1,22 +1,27 @@
1
1
  # Pika Review 🦊
2
- ### Enterprise-grade AI Architectural Sentinel
3
2
 
4
- Pika Review is a high-performance CLI tool designed to perform surgical code reviews using AI. It focuses on deep architectural debt, security vulnerabilities, and UI/UX anomalies that traditional linters miss.
3
+ ### Enterprise-grade AI Architectural Sentinel & Compliance Engine
4
+
5
+ Pika Review is a high-performance CLI tool designed to perform surgical code reviews using AI. It focuses on deep architectural debt, security vulnerabilities, and project-specific compliance that traditional linters miss.
5
6
 
6
7
  **GitHub**: [HackX-IN/pika-review](https://github.com/HackX-IN/pika-review)
7
8
 
8
9
  ---
9
10
 
10
11
  ## 🚀 Key Features
11
- - **Provider Agnostic**: Seamlessly works with Cloudflare Workers AI, OpenAI, Grok, or local LLMs via OpenAI-compatible endpoints.
12
- - **Mixture of Experts (MoE) Reasoning**: Leverages advanced LLMs for deep structural analysis.
13
- - **Polyglot Heuristics**: Idiom-aware reviews across Python, JS/TS, Go, Rust, and React.
14
- - **Interactive UI**: Claude-inspired terminal experience with real-time progress and multi-select discovery.
15
- - **Enterprise-Ready**: Built-in token safety, JSON self-healing, and CI/CD integration.
12
+
13
+ - **🧠 Architecture Rules Engine**: Enforce project-specific standards via `.pika-rules.md`.
14
+ - **📊 Health Dashboard**: Track architectural health trends over time with `pika-review stats`.
15
+ - **🎨 Premium Interactive Reports**: Immersive, dark-mode HTML reports for deep triage.
16
+ - **🔍 Smart Context Scanning**: Uses `-U10` context windows for higher AI reasoning accuracy.
17
+ - **🛡️ "Pika-Ignore" support**: Suppress specific lines using `// pika-ignore` comments.
18
+ - **⚡ Parallel Orchestration**: Scans multiple files concurrently with `p-limit`.
19
+ - **🌍 Provider Agnostic**: Works with OpenAI, Claude, Grok, or any OpenAI-compatible endpoint.
16
20
 
17
21
  ---
18
22
 
19
23
  ## 📦 Installation
24
+
20
25
  ```bash
21
26
  # Via Bun (Recommended)
22
27
  bun add -g pika-review
@@ -26,53 +31,69 @@ npm install -g pika-review
26
31
  ```
27
32
 
28
33
  ## 🛠️ Setup
34
+
29
35
  1. Initialize your configuration:
30
36
  ```bash
31
37
  pika-review init
32
38
  ```
33
- 2. Open `~/.pika-review.yaml` and add your Cloudflare credentials:
39
+ 2. Configure your AI provider in `~/.pika-review.yaml`:
34
40
  ```yaml
35
41
  ai:
36
- accountId: "your-account-id"
37
- apiKey: "your-scoped-api-token"
42
+ apiKey: "your-api-token"
43
+ model: "gpt-4o" # or your preferred model
44
+ baseURL: "https://api.openai.com/v1"
38
45
  ```
39
46
 
40
- ### 🌍 Custom AI Providers (OpenAI, Grok, etc.)
41
- Pika Review works with any OpenAI-compatible API. To use a different provider, simply override the `baseURL` and `model` in your config:
47
+ ---
42
48
 
43
- ```yaml
44
- ai:
45
- apiKey: "your-openai-key"
46
- model: "gpt-4o"
47
- baseURL: "https://api.openai.com/v1"
48
- ```
49
+ ## 🏗️ Enterprise Features
49
50
 
50
- ## 🔍 Usage
51
- Scan staged git changes:
52
- ```bash
53
- pika-review scan
54
- ```
51
+ ### 1. Architecture Rules Engine
55
52
 
56
- Scan specific files:
57
- ```bash
58
- pika-review scan src/index.ts src/utils/token.ts
53
+ Create a `.pika-rules.md` file in your repository root to guide the AI with project-specific context:
54
+
55
+ ```markdown
56
+ # Architectural Rules
57
+
58
+ - Use Functional Components with Hooks, never Class Components.
59
+ - All service calls must go through `src/api/client.ts`.
60
+ - Database queries are restricted to the Repository layer.
59
61
  ```
60
62
 
61
- Scan unstaged changes:
62
- ```bash
63
- pika-review scan --unstaged
63
+ ### 2. Pika-Ignore
64
+
65
+ Suppress false positives or acknowledged risks directly in your code:
66
+
67
+ ```typescript
68
+ const secret = "12345"; // pika-ignore (intentional for testing)
64
69
  ```
65
70
 
66
- ### CI/CD Mode
67
- Use the `--ci` flag in GitHub Actions or other pipelines to fail if Critical or High severity issues are found:
71
+ ### 3. Health Stats
72
+
73
+ Monitor your codebase's architectural debt over time:
74
+
68
75
  ```bash
69
- pika-review scan --ci
76
+ pika-review stats
70
77
  ```
71
78
 
72
79
  ---
73
80
 
81
+ ## 🔍 Usage
82
+
83
+ | Command | Description |
84
+ | :---------------------- | :------------------------------------ |
85
+ | `pika-review scan` | Scan staged git changes (Default) |
86
+ | `pika-review scan -i` | Interactive file selection mode |
87
+ | `pika-review view` | Open the latest interactive report |
88
+ | `pika-review stats` | View architectural health dashboard |
89
+ | `pika-review scan --ci` | Fail pipeline on Critical/High issues |
90
+
91
+ ---
92
+
74
93
  ## 🛡️ Privacy & Security
75
- Pika Review uses your local git diffs and sends them directly to your Cloudflare Workers AI instance. No code is stored by Pika Review.
94
+
95
+ Pika Review processes your local git diffs and transmits them directly to your configured AI provider via SSL. No code is stored or cached by the Pika Review engine.
76
96
 
77
97
  ## 📄 License
98
+
78
99
  MIT © Pika Review Contributors
package/dist/cmd/scan.js CHANGED
@@ -10,23 +10,25 @@ export async function scanAction(files, options) {
10
10
  const target = options.unstaged ? "unstaged" : "staged";
11
11
  const isCI = !!options.ci;
12
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
- }
13
+ // Interactive Discovery: If requested or if no files provided and no git changes
14
+ if (!isCI && (options.interactive || (!targets && getDiffFiles(target).length === 0))) {
15
+ logger.info("Entering interactive selection mode...");
16
+ const allFiles = listProjectFiles();
17
+ if (allFiles.length > 0) {
18
+ const selection = await prompts({
19
+ type: "multiselect",
20
+ name: "files",
21
+ message: "Select files to analyze (Space to select, Enter to confirm):",
22
+ choices: allFiles.map(f => ({ title: f, value: f })),
23
+ hint: "- Space to select. Return to submit",
24
+ instructions: false
25
+ });
26
+ if (selection.files && selection.files.length > 0) {
27
+ targets = selection.files;
28
+ }
29
+ else if (options.interactive) {
30
+ logger.warn("No files selected. Exiting.");
31
+ return;
30
32
  }
31
33
  }
32
34
  }
@@ -0,0 +1,55 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * Stats Action: Displays architectural health trends.
7
+ */
8
+ export async function statsAction() {
9
+ const historyPath = path.join(process.cwd(), ".pika-reports", "history.json");
10
+ if (!fs.existsSync(historyPath)) {
11
+ logger.error("No scan history found. Run some scans first!");
12
+ return;
13
+ }
14
+ try {
15
+ const history = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
16
+ if (history.length === 0) {
17
+ logger.error("Scan history is empty.");
18
+ return;
19
+ }
20
+ console.log(`\n${chalk.bgBlue.white.bold(" PIKA ARCHITECTURAL HEALTH DASHBOARD ")}\n`);
21
+ console.log(`${chalk.dim("Tracking trends across the last " + history.length + " scans")}\n`);
22
+ // Table Header
23
+ console.log(chalk.bold("Date Files Issues Critical High Medium Low"));
24
+ console.log(chalk.dim("-".repeat(75)));
25
+ history.slice(-10).forEach((run) => {
26
+ const date = new Date(run.timestamp).toLocaleString().padEnd(20);
27
+ const files = String(run.totalFiles).padEnd(8);
28
+ const issues = String(run.totalIssues).padEnd(9);
29
+ const crit = chalk.red(String(run.severityCounts.Critical || 0).padEnd(11));
30
+ const high = chalk.hex("#fb923c")(String(run.severityCounts.High || 0).padEnd(7));
31
+ const med = chalk.yellow(String(run.severityCounts.Medium || 0).padEnd(9));
32
+ const low = chalk.green(String(run.severityCounts.Low || 0).padEnd(6));
33
+ console.log(`${date}${files}${issues}${crit}${high}${med}${low}`);
34
+ });
35
+ // Summary Insights
36
+ const latest = history[history.length - 1];
37
+ const previous = history.length > 1 ? history[history.length - 2] : null;
38
+ console.log(`\n${chalk.bold("Latest Insight:")}`);
39
+ if (previous) {
40
+ const diff = latest.totalIssues - previous.totalIssues;
41
+ const direction = diff > 0 ? chalk.red("increased") : diff < 0 ? chalk.green("decreased") : "remained stable";
42
+ console.log(`- Issue count ${direction} by ${Math.abs(diff)} since the last scan.`);
43
+ }
44
+ const critTotal = latest.severityCounts.Critical || 0;
45
+ if (critTotal > 0) {
46
+ console.log(`- ${chalk.red("🚨 Critical Action Required:")} ${critTotal} architectural risks detected.`);
47
+ }
48
+ else {
49
+ console.log(`- ${chalk.green("✅ Zero Critical Issues:")} Maintain this standard!`);
50
+ }
51
+ }
52
+ catch (e) {
53
+ logger.error("Failed to parse history data.");
54
+ }
55
+ }
@@ -0,0 +1,55 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { execSync } from "child_process";
4
+ import chalk from "chalk";
5
+ import { logger } from "../utils/logger.js";
6
+ /**
7
+ * View Action: Finds and opens the latest HTML report.
8
+ */
9
+ export async function viewAction() {
10
+ const rootReportDir = path.join(process.cwd(), ".pika-reports");
11
+ if (!fs.existsSync(rootReportDir)) {
12
+ logger.error("No reports found. Run a scan first!");
13
+ return;
14
+ }
15
+ // Get project directories (e.g. .pika-reports/pika-review/)
16
+ const projectDirs = fs.readdirSync(rootReportDir, { withFileTypes: true })
17
+ .filter(dirent => dirent.isDirectory())
18
+ .map(dirent => dirent.name);
19
+ if (projectDirs.length === 0) {
20
+ logger.error("No reports found. Run a scan first!");
21
+ return;
22
+ }
23
+ // Find all report.html files across all projects and timestamps
24
+ const allReports = [];
25
+ for (const project of projectDirs) {
26
+ const projectPath = path.join(rootReportDir, project);
27
+ const timestamps = fs.readdirSync(projectPath, { withFileTypes: true })
28
+ .filter(dirent => dirent.isDirectory())
29
+ .map(dirent => dirent.name);
30
+ for (const ts of timestamps) {
31
+ const reportPath = path.join(projectPath, ts, "report.html");
32
+ if (fs.existsSync(reportPath)) {
33
+ allReports.push({
34
+ path: reportPath,
35
+ mtime: fs.statSync(reportPath).mtimeMs
36
+ });
37
+ }
38
+ }
39
+ }
40
+ if (allReports.length === 0) {
41
+ logger.error("No HTML reports found. Run a scan first!");
42
+ return;
43
+ }
44
+ // Sort by modification time (latest first)
45
+ allReports.sort((a, b) => b.mtime - a.mtime);
46
+ const latestReport = allReports[0].path;
47
+ logger.info(`Opening latest report: ${chalk.cyan(latestReport)}`);
48
+ try {
49
+ const command = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
50
+ execSync(`${command} "${latestReport}"`);
51
+ }
52
+ catch (e) {
53
+ logger.error("Failed to open the report automatically. Please open it manually.");
54
+ }
55
+ }
package/dist/core/ai.js CHANGED
@@ -2,6 +2,8 @@ import OpenAI from "openai";
2
2
  import { ReviewSchema } from "../utils/schema.js";
3
3
  import { getConfig } from "../utils/config.js";
4
4
  import { logger } from "../utils/logger.js";
5
+ import path from "path";
6
+ import fs from "fs";
5
7
  /**
6
8
  * Structural Extractor: Isolate the JSON object from AI chatter.
7
9
  * MoE (Mixture of Experts) models often add preamble or postscript;
@@ -49,32 +51,49 @@ export function extractJSON(raw) {
49
51
  return snippet;
50
52
  }
51
53
  /**
52
- * Deep Analysis Engine: Communicates with Cloudflare Workers AI.
54
+ * Deep Analysis Engine: Communicates with the AI provider.
53
55
  * Uses XML-structured prompts for better instruction following in smaller LLMs.
54
56
  */
55
- export async function analyzeDiff(diff) {
57
+ export async function analyzeDiff(diff, fileName) {
56
58
  const config = getConfig();
57
59
  if (!config.ai.apiKey || !config.ai.accountId) {
58
- throw new Error("Missing Cloudflare credentials in ~/.pika-review.yaml");
60
+ throw new Error("Missing AI provider credentials in ~/.pika-review.yaml");
59
61
  }
60
- // Neuron Safety: Truncate input to protect the 10,000 daily free limit.
62
+ // Rate Limit Safety: Truncate input to protect the daily free limit.
61
63
  // 30,000 chars is roughly 7,500 tokens, leaving room for a detailed response.
62
64
  const MAX_CHARS = 30000;
63
65
  const safeDiff = diff.length > MAX_CHARS
64
- ? diff.substring(0, MAX_CHARS) + "\n... [Content truncated for token safety]"
66
+ ? diff.substring(0, MAX_CHARS) +
67
+ "\n... [Content truncated for token safety]"
65
68
  : diff;
66
- const baseURL = (config.ai.baseURL && config.ai.baseURL.trim())
69
+ const baseURL = config.ai.baseURL && config.ai.baseURL.trim()
67
70
  ? config.ai.baseURL
68
71
  : `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`;
69
72
  const openai = new OpenAI({
70
73
  apiKey: config.ai.apiKey,
71
74
  baseURL,
72
75
  });
76
+ // Load custom architecture rules if they exist
77
+ let complianceSection = "";
78
+ try {
79
+ const rulesPath = path.join(process.cwd(), ".pika-rules.md");
80
+ if (fs.existsSync(rulesPath)) {
81
+ complianceSection = `
82
+ <compliance_standards>
83
+ ${fs.readFileSync(rulesPath, "utf-8")}
84
+ </compliance_standards>`;
85
+ }
86
+ }
87
+ catch (e) { }
73
88
  const defaultPrompt = `
74
89
  <role>
75
90
  You are an Elite Senior Full-Stack Architect and Security Researcher.
76
91
  Your task is to perform a surgical review of the provided code.
77
- </role>
92
+ </role>${complianceSection}
93
+
94
+ <context>
95
+ Reviewing File: ${fileName}
96
+ </context>
78
97
 
79
98
  <task>
80
99
  Identify high-impact issues related to:
@@ -112,7 +131,7 @@ export async function analyzeDiff(diff) {
112
131
  ${safeDiff}
113
132
  </code_to_analyze>
114
133
  `;
115
- const finalPrompt = (config.ai.prompt && config.ai.prompt.trim())
134
+ const finalPrompt = config.ai.prompt && config.ai.prompt.trim()
116
135
  ? config.ai.prompt.replace("{{code}}", safeDiff)
117
136
  : defaultPrompt;
118
137
  try {
@@ -1,12 +1,38 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  /**
4
- * Report Orchestrator: Handles persistence of AI findings.
4
+ * Helper to get a sanitized project name.
5
+ */
6
+ function getProjectName() {
7
+ try {
8
+ const pkgPath = path.join(process.cwd(), "package.json");
9
+ if (fs.existsSync(pkgPath)) {
10
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
11
+ if (pkg.name)
12
+ return pkg.name.replace(/\//g, "-");
13
+ }
14
+ }
15
+ catch (e) { }
16
+ return path.basename(process.cwd());
17
+ }
18
+ /**
19
+ * Helper to generate a safe filename from a path.
20
+ */
21
+ function getSafeFileName(fileName) {
22
+ return fileName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
23
+ }
24
+ /**
25
+ * Report Orchestrator: Handles persistence of AI findings in a structured way.
26
+ * Stores reports in .pika-reports/<project-name>/<timestamp>/
5
27
  */
6
28
  export function setupReportDir() {
7
- const reportDir = path.join(process.cwd(), ".pika-reports");
8
- if (!fs.existsSync(reportDir))
9
- fs.mkdirSync(reportDir);
29
+ const rootReportDir = path.join(process.cwd(), ".pika-reports");
30
+ const projectName = getProjectName();
31
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
32
+ const sessionDir = path.join(rootReportDir, projectName, timestamp);
33
+ if (!fs.existsSync(sessionDir)) {
34
+ fs.mkdirSync(sessionDir, { recursive: true });
35
+ }
10
36
  const gitignorePath = path.join(process.cwd(), ".gitignore");
11
37
  if (fs.existsSync(gitignorePath)) {
12
38
  const gitignore = fs.readFileSync(gitignorePath, "utf-8");
@@ -14,13 +40,13 @@ export function setupReportDir() {
14
40
  fs.appendFileSync(gitignorePath, "\n.pika-reports/\n");
15
41
  }
16
42
  }
17
- return reportDir;
43
+ return sessionDir;
18
44
  }
19
45
  /**
20
46
  * Generates a professional Markdown report.
21
47
  */
22
48
  export function writeMarkdownReport(fileName, reviews, reportDir) {
23
- const safeName = fileName.replace(/[^a-z0-9]/gi, "_").toLowerCase();
49
+ const safeName = getSafeFileName(fileName);
24
50
  const reportPath = path.join(reportDir, `${safeName}_review.md`);
25
51
  const timestamp = new Date().toLocaleString();
26
52
  let mdContent = `# 🔍 Pika Review: \`${fileName}\`\n\n`;
@@ -46,3 +72,414 @@ export function writeMarkdownReport(fileName, reviews, reportDir) {
46
72
  fs.writeFileSync(reportPath, mdContent);
47
73
  return reportPath;
48
74
  }
75
+ /**
76
+ * Generates a premium Interactive HTML report.
77
+ */
78
+ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
79
+ const reportPath = path.join(sessionDir, `report.html`);
80
+ const projectName = getProjectName();
81
+ const timestamp = new Date().toLocaleString();
82
+ const data = JSON.stringify(allFindings);
83
+ const htmlContent = `
84
+ <!DOCTYPE html>
85
+ <html lang="en">
86
+ <head>
87
+ <meta charset="UTF-8">
88
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
89
+ <title>Pika Review | ${projectName}</title>
90
+ <link rel="preconnect" href="https://fonts.googleapis.com">
91
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
92
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
93
+ <style>
94
+ :root {
95
+ --bg: #0b0f1a;
96
+ --sidebar-bg: #151b2d;
97
+ --card-bg: #1e253a;
98
+ --text: #f1f5f9;
99
+ --text-dim: #94a3b8;
100
+ --accent: #38bdf8;
101
+ --critical: #f43f5e;
102
+ --high: #fb923c;
103
+ --medium: #fbbf24;
104
+ --low: #4ade80;
105
+ --border: rgba(255,255,255,0.08);
106
+ }
107
+
108
+ * { margin: 0; padding: 0; box-sizing: border-box; }
109
+ body {
110
+ font-family: 'Inter', sans-serif;
111
+ background: var(--bg);
112
+ color: var(--text);
113
+ display: flex;
114
+ height: 100vh;
115
+ overflow: hidden;
116
+ }
117
+
118
+ /* Sidebar */
119
+ aside {
120
+ width: 350px;
121
+ background: var(--sidebar-bg);
122
+ border-right: 1px solid var(--border);
123
+ display: flex;
124
+ flex-direction: column;
125
+ box-shadow: 10px 0 30px rgba(0,0,0,0.3);
126
+ z-index: 10;
127
+ }
128
+
129
+ .brand {
130
+ padding: 32px 24px;
131
+ border-bottom: 1px solid var(--border);
132
+ background: linear-gradient(135deg, rgba(56, 189, 248, 0.1), transparent);
133
+ }
134
+
135
+ .brand h1 { font-size: 1.5rem; font-weight: 800; letter-spacing: -0.02em; color: var(--accent); display: flex; align-items: center; gap: 10px; }
136
+ .brand p { font-size: 0.8rem; color: var(--text-dim); margin-top: 8px; font-weight: 500; }
137
+
138
+ .sidebar-stats {
139
+ padding: 20px 24px;
140
+ display: grid;
141
+ grid-template-columns: 1fr 1fr;
142
+ gap: 12px;
143
+ border-bottom: 1px solid var(--border);
144
+ }
145
+
146
+ .stat-box {
147
+ background: rgba(255,255,255,0.03);
148
+ padding: 12px;
149
+ border-radius: 10px;
150
+ text-align: center;
151
+ border: 1px solid var(--border);
152
+ }
153
+
154
+ .stat-val { display: block; font-size: 1.25rem; font-weight: 700; color: var(--accent); }
155
+ .stat-lab { display: block; font-size: 0.65rem; text-transform: uppercase; color: var(--text-dim); margin-top: 4px; letter-spacing: 0.05em; }
156
+
157
+ .file-list {
158
+ flex: 1;
159
+ overflow-y: auto;
160
+ padding: 16px;
161
+ }
162
+
163
+ .file-item {
164
+ padding: 14px 16px;
165
+ border-radius: 12px;
166
+ cursor: pointer;
167
+ margin-bottom: 8px;
168
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
169
+ display: flex;
170
+ flex-direction: column;
171
+ gap: 6px;
172
+ border: 1px solid transparent;
173
+ }
174
+
175
+ .file-item:hover { background: rgba(255,255,255,0.04); border-color: var(--border); }
176
+ .file-item.active { background: var(--accent); color: #000; box-shadow: 0 4px 15px rgba(56, 189, 248, 0.3); }
177
+ .file-item .name { font-size: 0.85rem; font-family: 'JetBrains Mono', monospace; font-weight: 500; word-break: break-all; }
178
+
179
+ .severity-pills { display: flex; gap: 4px; }
180
+ .pill { font-size: 0.65rem; padding: 2px 6px; border-radius: 4px; background: rgba(0,0,0,0.2); font-weight: 700; }
181
+ .file-item.active .pill { background: rgba(0,0,0,0.1); color: #000; }
182
+
183
+ /* Main Content */
184
+ main {
185
+ flex: 1;
186
+ overflow-y: auto;
187
+ padding: 60px;
188
+ background: radial-gradient(circle at top right, rgba(56, 189, 248, 0.05), transparent 40%);
189
+ }
190
+
191
+ .content-container { max-width: 900px; margin: 0 auto; }
192
+
193
+ .page-header {
194
+ margin-bottom: 50px;
195
+ animation: fadeInDown 0.5s ease-out;
196
+ }
197
+
198
+ @keyframes fadeInDown {
199
+ from { opacity: 0; transform: translateY(-20px); }
200
+ to { opacity: 1; transform: translateY(0); }
201
+ }
202
+
203
+ .page-header h2 { font-size: 2rem; font-family: 'JetBrains Mono', monospace; margin-bottom: 12px; color: #fff; }
204
+ .page-header p { color: var(--text-dim); font-size: 1rem; }
205
+
206
+ .issue-card {
207
+ background: var(--card-bg);
208
+ border: 1px solid var(--border);
209
+ border-radius: 20px;
210
+ padding: 32px;
211
+ margin-bottom: 30px;
212
+ position: relative;
213
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
214
+ transition: transform 0.3s;
215
+ }
216
+
217
+ .issue-card:hover { transform: translateY(-4px); }
218
+
219
+ .card-accent {
220
+ position: absolute;
221
+ left: 0; top: 32px; bottom: 32px;
222
+ width: 5px;
223
+ border-radius: 0 4px 4px 0;
224
+ }
225
+
226
+ .issue-card.Critical .card-accent { background: var(--critical); box-shadow: 0 0 15px var(--critical); }
227
+ .issue-card.High .card-accent { background: var(--high); box-shadow: 0 0 15px var(--high); }
228
+ .issue-card.Medium .card-accent { background: var(--medium); box-shadow: 0 0 15px var(--medium); }
229
+ .issue-card.Low .card-accent { background: var(--low); box-shadow: 0 0 15px var(--low); }
230
+
231
+ .issue-meta {
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: space-between;
235
+ margin-bottom: 24px;
236
+ }
237
+
238
+ .severity-tag {
239
+ font-size: 0.75rem;
240
+ text-transform: uppercase;
241
+ font-weight: 800;
242
+ padding: 6px 14px;
243
+ border-radius: 8px;
244
+ letter-spacing: 0.05em;
245
+ }
246
+
247
+ .severity-tag.Critical { background: rgba(244, 63, 94, 0.15); color: var(--critical); border: 1px solid var(--critical); }
248
+ .severity-tag.High { background: rgba(251, 146, 60, 0.15); color: var(--high); border: 1px solid var(--high); }
249
+ .severity-tag.Medium { background: rgba(251, 191, 36, 0.15); color: var(--medium); border: 1px solid var(--medium); }
250
+ .severity-tag.Low { background: rgba(74, 222, 128, 0.15); color: var(--low); border: 1px solid var(--low); }
251
+
252
+ .line-info { font-family: 'JetBrains Mono', monospace; color: var(--text-dim); font-size: 0.9rem; }
253
+
254
+ .field-group { margin-bottom: 24px; }
255
+ .field-label {
256
+ font-size: 0.7rem;
257
+ font-weight: 800;
258
+ color: var(--text-dim);
259
+ text-transform: uppercase;
260
+ letter-spacing: 0.1em;
261
+ margin-bottom: 10px;
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 8px;
265
+ }
266
+
267
+ .field-content { line-height: 1.7; color: #cbd5e1; font-size: 1.05rem; }
268
+
269
+ pre {
270
+ background: #000;
271
+ padding: 24px;
272
+ border-radius: 12px;
273
+ font-family: 'JetBrains Mono', monospace;
274
+ font-size: 0.9rem;
275
+ margin-top: 15px;
276
+ overflow-x: auto;
277
+ border: 1px solid rgba(255,255,255,0.05);
278
+ color: #e2e8f0;
279
+ }
280
+
281
+ .empty-state {
282
+ display: flex;
283
+ flex-direction: column;
284
+ align-items: center;
285
+ justify-content: center;
286
+ height: 60vh;
287
+ text-align: center;
288
+ opacity: 0.6;
289
+ }
290
+
291
+ .empty-state svg { width: 80px; height: 80px; margin-bottom: 24px; stroke: var(--text-dim); }
292
+
293
+ /* Scrollbar */
294
+ ::-webkit-scrollbar { width: 6px; }
295
+ ::-webkit-scrollbar-track { background: transparent; }
296
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
297
+ ::-webkit-scrollbar-thumb:hover { background: var(--accent); }
298
+ </style>
299
+ </head>
300
+ <body>
301
+ <aside>
302
+ <div class="brand">
303
+ <h1>🦊 PIKA REVIEW</h1>
304
+ <p>${projectName} • ${timestamp}</p>
305
+ </div>
306
+ <div class="sidebar-stats">
307
+ <div class="stat-box">
308
+ <span class="stat-val" id="totalFiles">0</span>
309
+ <span class="stat-lab">Files</span>
310
+ </div>
311
+ <div class="stat-box">
312
+ <span class="stat-val" id="totalIssues">0</span>
313
+ <span class="stat-lab">Issues</span>
314
+ </div>
315
+ </div>
316
+ <div class="file-list" id="fileList"></div>
317
+ </aside>
318
+
319
+ <main>
320
+ <div class="content-container" id="mainContent">
321
+ <div class="empty-state">
322
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
323
+ <h3>Select a file to inspect</h3>
324
+ <p>Architectural findings and security insights will appear here.</p>
325
+ </div>
326
+ </div>
327
+ </main>
328
+
329
+ <script id="pika-data" type="application/json">${data}</script>
330
+ <script>
331
+ (function() {
332
+ const findings = JSON.parse(document.getElementById('pika-data').textContent);
333
+ const fileList = document.getElementById('fileList');
334
+ const mainContent = document.getElementById('mainContent');
335
+ const totalFilesEl = document.getElementById('totalFiles');
336
+ const totalIssuesEl = document.getElementById('totalIssues');
337
+
338
+ function escape(str) {
339
+ if (!str) return '';
340
+ const div = document.createElement('div');
341
+ div.textContent = str;
342
+ return div.innerHTML;
343
+ }
344
+
345
+ function init() {
346
+ totalFilesEl.textContent = findings.length;
347
+ let issuesCount = 0;
348
+
349
+ findings.forEach((f, index) => {
350
+ issuesCount += f.reviews.length;
351
+ const item = document.createElement('div');
352
+ item.className = 'file-item';
353
+
354
+ const name = document.createElement('span');
355
+ name.className = 'name';
356
+ name.textContent = f.fileName;
357
+ item.appendChild(name);
358
+
359
+ if (f.reviews.length > 0) {
360
+ const pills = document.createElement('div');
361
+ pills.className = 'severity-pills';
362
+
363
+ const counts = f.reviews.reduce((acc, r) => {
364
+ acc[r.severity] = (acc[r.severity] || 0) + 1;
365
+ return acc;
366
+ }, {});
367
+
368
+ Object.entries(counts).forEach(([sev, count]) => {
369
+ const pill = document.createElement('span');
370
+ pill.className = 'pill';
371
+ pill.style.color = 'var(--' + sev.toLowerCase() + ')';
372
+ pill.textContent = count;
373
+ pills.appendChild(pill);
374
+ });
375
+ item.appendChild(pills);
376
+ } else {
377
+ const clean = document.createElement('span');
378
+ clean.className = 'pill';
379
+ clean.style.color = 'var(--low)';
380
+ clean.textContent = 'CLEAN';
381
+ item.appendChild(clean);
382
+ }
383
+
384
+ item.onclick = () => selectFile(index, item);
385
+ fileList.appendChild(item);
386
+ });
387
+
388
+ totalIssuesEl.textContent = issuesCount;
389
+ }
390
+
391
+ function selectFile(index, element) {
392
+ document.querySelectorAll('.file-item').forEach(i => i.classList.remove('active'));
393
+ element.classList.add('active');
394
+
395
+ const f = findings[index];
396
+ mainContent.innerHTML = '';
397
+
398
+ const header = document.createElement('div');
399
+ header.className = 'page-header';
400
+
401
+ const title = document.createElement('h2');
402
+ title.textContent = f.fileName;
403
+ header.appendChild(title);
404
+
405
+ const subtitle = document.createElement('p');
406
+ subtitle.textContent = f.reviews.length + ' findings identified in this analysis.';
407
+ header.appendChild(subtitle);
408
+
409
+ mainContent.appendChild(header);
410
+
411
+ if (f.reviews.length === 0) {
412
+ const empty = document.createElement('div');
413
+ empty.className = 'empty-state';
414
+ empty.innerHTML = '<h3>✨ Code is pristine</h3><p>No architectural anomalies or security risks found in this file.</p>';
415
+ mainContent.appendChild(empty);
416
+ return;
417
+ }
418
+
419
+ f.reviews.forEach(r => {
420
+ const card = document.createElement('div');
421
+ card.className = 'issue-card ' + r.severity;
422
+
423
+ card.innerHTML = [
424
+ '<div class="card-accent"></div>',
425
+ '<div class="issue-meta">',
426
+ '<span class="severity-tag ' + r.severity + '">' + r.severity + '</span>',
427
+ '<span class="line-info">Line ' + (r.line || 'N/A') + '</span>',
428
+ '</div>',
429
+ '<div class="field-group">',
430
+ '<div class="field-label">💡 Finding</div>',
431
+ '<div class="field-content">' + escape(r.finding) + '</div>',
432
+ '</div>',
433
+ '<div class="field-group">',
434
+ '<div class="field-label">💥 Impact</div>',
435
+ '<div class="field-content">' + escape(r.impact) + '</div>',
436
+ '</div>',
437
+ '<div class="field-group">',
438
+ '<div class="field-label">🛠️ Recommendation</div>',
439
+ '<pre><code>' + escape(r.recommendation) + '</code></pre>',
440
+ '</div>'
441
+ ].join('');
442
+
443
+ mainContent.appendChild(card);
444
+ });
445
+
446
+ mainContent.scrollTop = 0;
447
+ }
448
+
449
+ init();
450
+ })();
451
+ </script>
452
+ </body>
453
+ </html>
454
+ `;
455
+ fs.writeFileSync(reportPath, htmlContent);
456
+ // Point 2: Update history.json for stats tracking
457
+ try {
458
+ const historyPath = path.join(process.cwd(), ".pika-reports", "history.json");
459
+ let history = [];
460
+ if (fs.existsSync(historyPath)) {
461
+ history = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
462
+ }
463
+ const totalIssues = allFindings.reduce((sum, f) => sum + f.reviews.length, 0);
464
+ const severityCounts = allFindings.reduce((acc, f) => {
465
+ f.reviews.forEach(r => {
466
+ acc[r.severity] = (acc[r.severity] || 0) + 1;
467
+ });
468
+ return acc;
469
+ }, { Critical: 0, High: 0, Medium: 0, Low: 0 });
470
+ history.push({
471
+ timestamp: new Date().toISOString(),
472
+ projectName,
473
+ totalFiles,
474
+ totalIssues,
475
+ severityCounts,
476
+ reportPath: path.relative(path.join(process.cwd(), ".pika-reports"), reportPath)
477
+ });
478
+ // Keep only last 50 runs to prevent file bloat
479
+ if (history.length > 50)
480
+ history.shift();
481
+ fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
482
+ }
483
+ catch (e) { }
484
+ return reportPath;
485
+ }
@@ -3,7 +3,7 @@ import fs from "fs";
3
3
  import cliProgress from "cli-progress";
4
4
  import pLimit from "p-limit";
5
5
  import { analyzeDiff } from "./ai.js";
6
- import { setupReportDir, writeMarkdownReport } from "./reporter.js";
6
+ import { setupReportDir, writeMarkdownReport, writeHTMLReport } from "./reporter.js";
7
7
  import { getIgnoredFiles } from "../utils/config.js";
8
8
  import { logger } from "../utils/logger.js";
9
9
  import path from "path";
@@ -65,7 +65,7 @@ export function getDiffFiles(type) {
65
65
  }
66
66
  /**
67
67
  * Scan Orchestrator: Manages the review lifecycle.
68
- * Concurrency is limited to 3 to stay within the Cloudflare Workers AI free tier constraints.
68
+ * Concurrency is limited to 3 to stay within default provider free tier constraints.
69
69
  */
70
70
  export async function runScan(target, isCI, specificFiles) {
71
71
  let files = [];
@@ -98,6 +98,7 @@ export async function runScan(target, isCI, specificFiles) {
98
98
  if (bar)
99
99
  bar.start(files.length, 0, { file: "Starting..." });
100
100
  const generatedReports = [];
101
+ const allFindings = [];
101
102
  let criticalIssuesFound = false;
102
103
  const limit = pLimit(3);
103
104
  let completed = 0;
@@ -115,7 +116,7 @@ export async function runScan(target, isCI, specificFiles) {
115
116
  }
116
117
  else {
117
118
  try {
118
- contentToScan = execFileSync("git", ["diff", target === "staged" ? "--cached" : "", "--", file], {
119
+ contentToScan = execFileSync("git", ["diff", "-U10", target === "staged" ? "--cached" : "", "--", file], {
119
120
  encoding: "utf-8",
120
121
  stdio: ["ignore", "pipe", "ignore"] // Suppress stderr
121
122
  });
@@ -134,20 +135,42 @@ export async function runScan(target, isCI, specificFiles) {
134
135
  }
135
136
  // Token Estimator Check
136
137
  validateTokenLimit(contentToScan);
137
- const result = await analyzeDiff(contentToScan);
138
+ const result = await analyzeDiff(contentToScan, file);
138
139
  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);
140
+ // Point 5: "Ignore" Comments Logic
141
+ const filteredReviews = result.reviews.filter(r => {
142
+ if (!r.line)
143
+ return true;
144
+ try {
145
+ const fileLines = fs.readFileSync(file, "utf-8").split("\n");
146
+ const targetLineContent = fileLines[r.line - 1] || "";
147
+ if (targetLineContent.includes("pika-ignore"))
148
+ return false;
149
+ }
150
+ catch (e) { }
151
+ return true;
152
+ });
153
+ if (filteredReviews.length > 0) {
154
+ const hasCritical = filteredReviews.some(r => r.severity === "Critical" || r.severity === "High");
155
+ if (hasCritical)
156
+ criticalIssuesFound = true;
157
+ const reportPath = writeMarkdownReport(file, filteredReviews, reportDir);
158
+ generatedReports.push(reportPath);
159
+ allFindings.push({ fileName: file, reviews: filteredReviews });
160
+ }
161
+ else {
162
+ allFindings.push({ fileName: file, reviews: [] });
163
+ }
164
+ }
165
+ else {
166
+ allFindings.push({ fileName: file, reviews: [] });
144
167
  }
145
168
  }
146
169
  catch (e) {
147
170
  if (e.message === "RATE_LIMIT") {
148
171
  if (bar)
149
172
  bar.stop();
150
- logger.critical("DAILY NEURON LIMIT REACHED (10,000). Scanning aborted.");
173
+ logger.critical("DAILY RATE LIMIT REACHED. Scanning aborted.");
151
174
  process.exit(1);
152
175
  }
153
176
  logger.error(`Error scanning ${file}: ${e.message}`);
@@ -165,5 +188,10 @@ export async function runScan(target, isCI, specificFiles) {
165
188
  logger.critical("CI Pipeline Failed: Critical issues detected.");
166
189
  process.exit(1);
167
190
  }
191
+ const htmlPath = writeHTMLReport(reportDir, files.length, allFindings);
192
+ if (!isCI) {
193
+ logger.success("\nScan complete!");
194
+ logger.info(`Interactive Report: ${htmlPath}`);
195
+ }
168
196
  return generatedReports;
169
197
  }
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ import { Command } from "commander";
3
3
  import chalk from "chalk";
4
4
  import { initAction } from "./cmd/init.js";
5
5
  import { scanAction } from "./cmd/scan.js";
6
+ import { viewAction } from "./cmd/view.js";
7
+ import { statsAction } from "./cmd/stats.js";
6
8
  /**
7
9
  * Pika Review: The Enterprise-Grade AI Code Reviewer.
8
10
  * Modular entry point for CLI operations.
@@ -10,19 +12,22 @@ import { scanAction } from "./cmd/scan.js";
10
12
  const program = new Command();
11
13
  // Branding Header
12
14
  const BRAND = `
13
- ${chalk.bgCyan.black.bold(" PIKA REVIEW ")} ${chalk.dim("v2.0.0")}
14
- ${chalk.italic.gray("Enterprise-grade AI Architectural Sentinel")}
15
+ ${chalk.bgCyan.black.bold(" PIKA REVIEW ")} ${chalk.dim("v2.0.0 Enterprise")}
16
+ ${chalk.italic.gray("AI Architectural Sentinel & Compliance Engine")}
15
17
  `;
16
18
  const HELPER_TEXT = `
17
19
  ${chalk.bold.cyan("🚀 Quick Start:")}
18
20
  $ pika-review scan ${chalk.dim("# Scan staged git changes (default)")}
19
- $ pika-review scan -u ${chalk.dim("# Scan unstaged git changes")}
21
+ $ pika-review scan -i ${chalk.dim("# Interactive file selection mode")}
22
+ $ pika-review view ${chalk.dim("# Open the latest interactive report")}
23
+ $ pika-review stats ${chalk.dim("# View architectural health trends")}
20
24
  $ pika-review scan file.ts ${chalk.dim("# Scan a specific file")}
21
25
  $ pika-review scan f1 f2 ${chalk.dim("# Scan multiple specific files")}
22
26
 
23
27
  ${chalk.bold.cyan("⌨️ Shortcuts & Tips:")}
24
28
  - Use ${chalk.yellow("--ci")} in GitHub Actions to fail on Critical/High issues.
25
- - Create a ${chalk.yellow(".pikaignore")} file to skip specific directories.
29
+ - Create ${chalk.yellow(".pika-rules.md")} to enforce project-specific architecture.
30
+ - Use ${chalk.yellow("pika-ignore")} in code comments to skip specific lines.
26
31
  - Review artifacts are stored in ${chalk.yellow(".pika-reports/")} automatically.
27
32
 
28
33
  ${chalk.bold.cyan("📊 Progress & Concurrency:")}
@@ -31,7 +36,7 @@ ${chalk.bold.cyan("📊 Progress & Concurrency:")}
31
36
  `;
32
37
  program
33
38
  .name("pika-review")
34
- .description("Enterprise-grade AI Code Reviewer for Cloudflare Workers AI")
39
+ .description("Enterprise-grade AI Architectural Code Reviewer")
35
40
  .version("2.0.0")
36
41
  .addHelpText("before", BRAND)
37
42
  .addHelpText("after", HELPER_TEXT);
@@ -42,10 +47,25 @@ program
42
47
  console.log(BRAND);
43
48
  await initAction();
44
49
  });
50
+ program
51
+ .command("view")
52
+ .description("Open the latest interactive HTML report in your browser")
53
+ .action(async () => {
54
+ console.log(BRAND);
55
+ await viewAction();
56
+ });
57
+ program
58
+ .command("stats")
59
+ .description("View architectural health trends and scan history")
60
+ .action(async () => {
61
+ console.log(BRAND);
62
+ await statsAction();
63
+ });
45
64
  program
46
65
  .command("scan [files...]", { isDefault: true }) // Set scan as the default command
47
66
  .description("Scan git changes or specific files for architectural anomalies")
48
67
  .option("-u, --unstaged", "Analyze unstaged changes instead of staged")
68
+ .option("-i, --interactive", "Interactively select files to scan")
49
69
  .option("--ci", "Headless CI/CD mode (fails on Critical/High issues, strips visuals)")
50
70
  .action(async (files, options) => {
51
71
  // Only show branding if not in CI mode and if files were explicitly passed or if it's the default
@@ -6,11 +6,11 @@ import { logger } from "./logger.js";
6
6
  const CONFIG_PATH = path.join(os.homedir(), ".pika-review.yaml");
7
7
  const DEFAULT_CONFIG = {
8
8
  ai: {
9
- accountId: "", // Required for Cloudflare default
9
+ accountId: "", // Account ID if required by provider
10
10
  apiKey: "",
11
11
  model: "@cf/meta/llama-3-8b-instruct",
12
12
  prompt: "",
13
- baseURL: "", // Leave empty for Cloudflare, or set for OpenAI/Grok/Local
13
+ baseURL: "", // Leave empty for default provider, or set for custom API
14
14
  },
15
15
  };
16
16
  /**
@@ -23,7 +23,7 @@ export function initConfig() {
23
23
  mode: 0o600,
24
24
  });
25
25
  logger.success(`Configuration initialized at ${CONFIG_PATH}`);
26
- logger.info("Please edit this file and add your Cloudflare Account ID and API Token.");
26
+ logger.info("Please edit this file and add your AI provider credentials.");
27
27
  }
28
28
  else {
29
29
  logger.warn(`Configuration already exists at ${CONFIG_PATH}`);
@@ -46,13 +46,44 @@ export function getIgnoredFiles() {
46
46
  const ignorePath = path.join(process.cwd(), ".pikaignore");
47
47
  if (!fs.existsSync(ignorePath)) {
48
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"
49
+ ".svg",
50
+ ".lock",
51
+ "package-lock.json",
52
+ ".png",
53
+ ".jpg",
54
+ ".jpeg",
55
+ ".ico",
56
+ "node_modules",
57
+ ".git",
58
+ "dist",
59
+ "build",
60
+ "out",
61
+ ".next",
62
+ "public",
63
+ ".pika-reports",
64
+ ".env",
65
+ ".DS_Store",
66
+ "bun.lockb",
67
+ "pnpm-lock.yaml",
68
+ "venv",
69
+ ".venv",
70
+ "__pycache__",
71
+ ".pytest_cache",
72
+ "target",
73
+ ".gradle",
74
+ ".idea",
75
+ ".vscode",
76
+ "vendor",
77
+ "coverage",
78
+ ".turbo",
79
+ "tests",
80
+ "__tests__",
81
+ "spec",
82
+ "specs",
83
+ "cypress",
84
+ "playwright-report",
85
+ "test-results",
86
+ ".nyc_output",
56
87
  ];
57
88
  }
58
89
  const content = fs.readFileSync(ignorePath, "utf-8");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pika-review",
3
- "version": "2.0.1",
4
- "description": "Enterprise-grade AI Architectural Code Reviewer powered by Cloudflare Workers AI.",
3
+ "version": "2.1.0",
4
+ "description": "Enterprise-grade AI Architectural Code Reviewer.",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
7
7
  "repository": {
@@ -29,8 +29,6 @@
29
29
  "keywords": [
30
30
  "ai",
31
31
  "code-review",
32
- "cloudflare",
33
- "workers-ai",
34
32
  "architectural-analysis",
35
33
  "cli"
36
34
  ],