pika-review 2.1.0 → 2.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 CHANGED
@@ -11,12 +11,15 @@ Pika Review is a high-performance CLI tool designed to perform surgical code rev
11
11
  ## 🚀 Key Features
12
12
 
13
13
  - **🧠 Architecture Rules Engine**: Enforce project-specific standards via `.pika-rules.md`.
14
+ - **🦙 Native Local Ollama Support**: Run 100% private, free, offline scans using local LLMs (e.g. Qwen, Llama).
15
+ - **🎛️ Local Model Selector**: Interactively choose and configure local models from the command line.
16
+ - **🛡️ Git Commit Safeguard Hook**: Prevent pushing high-severity compliance debt and vulnerabilities automatically.
14
17
  - **📊 Health Dashboard**: Track architectural health trends over time with `pika-review stats`.
15
18
  - **🎨 Premium Interactive Reports**: Immersive, dark-mode HTML reports for deep triage.
16
19
  - **🔍 Smart Context Scanning**: Uses `-U10` context windows for higher AI reasoning accuracy.
17
20
  - **🛡️ "Pika-Ignore" support**: Suppress specific lines using `// pika-ignore` comments.
18
21
  - **⚡ Parallel Orchestration**: Scans multiple files concurrently with `p-limit`.
19
- - **🌍 Provider Agnostic**: Works with OpenAI, Claude, Grok, or any OpenAI-compatible endpoint.
22
+ - **🌍 Provider Agnostic**: Works with OpenAI, Claude, Grok, local Ollama, or any OpenAI-compatible endpoint.
20
23
 
21
24
  ---
22
25
 
@@ -80,13 +83,57 @@ pika-review stats
80
83
 
81
84
  ## 🔍 Usage
82
85
 
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 |
86
+ | Command | Description |
87
+ | :---------------------- | :----------------------------------------------------------- |
88
+ | `pika-review scan` | Scan staged git changes (Default) |
89
+ | `pika-review scan -i` | Interactive file selection mode |
90
+ | `pika-review view` | Open the latest interactive report |
91
+ | `pika-review stats` | View architectural health trends and scan dashboard |
92
+ | `pika-review models` | Interactively select and configure local Ollama models |
93
+ | `pika-review hook <act>`| Install (`install`) or uninstall (`uninstall`) Git safeguards |
94
+ | `pika-review rules -g` | Auto-generate architectural `.pika-rules.md` guidelines |
95
+ | `pika-review scan --ci` | Fail pipeline on Critical/High issues |
96
+
97
+ ---
98
+
99
+ ## 🦙 Local Ollama & Offline Setup
100
+
101
+ Pika Review fully supports local, 100% private, offline code reviews via [Ollama](https://ollama.com).
102
+
103
+ ### 1. Configure Ollama Provider
104
+ Initialize your configuration:
105
+ ```bash
106
+ pika-review init
107
+ ```
108
+ Open `~/.pika-review.yaml` and configure the Ollama provider:
109
+ ```yaml
110
+ ai:
111
+ provider: "ollama"
112
+ model: "qwen2.5-coder:7b" # Or your pulled model
113
+ baseURL: "http://localhost:11434/v1"
114
+ ```
115
+
116
+ ### 2. Interactive Local Setup
117
+ To avoid editing files manually, you can manage everything directly from the command line:
118
+
119
+ ```bash
120
+ # Discover local pulled models and switch active model instantly
121
+ pika-review models
122
+
123
+ # Automatically bootstrap customized architectural rules for your tech stack
124
+ pika-review rules --generate
125
+
126
+ # Register git safeguard hooks to run scans automatically before commits
127
+ pika-review hook install
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 🤝 Contributing
133
+
134
+ We welcome contributions from the community to help make Pika Review the ultimate architectural code reviewer!
135
+
136
+ Please read our **[Contributing Guide](CONTRIBUTING.md)** for details on how to set up local development, run compiler checks, write custom CLI commands, and submit high-quality Pull Requests.
90
137
 
91
138
  ---
92
139
 
@@ -0,0 +1,85 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * CLI Command Action: hook [action]
7
+ * Manages the Git pre-commit hook installation/removal.
8
+ */
9
+ export async function hookAction(action) {
10
+ const gitPath = path.join(process.cwd(), ".git");
11
+ if (!fs.existsSync(gitPath)) {
12
+ logger.error("Not a Git repository.");
13
+ logger.dim("Pika Review Git hooks can only be installed in the root of a Git repository.");
14
+ return;
15
+ }
16
+ const hooksPath = path.join(gitPath, "hooks");
17
+ const hookFile = path.join(hooksPath, "pre-commit");
18
+ // Ensure hooks directory exists
19
+ if (!fs.existsSync(hooksPath)) {
20
+ fs.mkdirSync(hooksPath, { recursive: true });
21
+ }
22
+ const HOOK_SCRIPT = `#!/bin/sh
23
+ # Pika Review - Architectural Sentinel Git Safeguard Hook
24
+ echo "🦊 Running local architectural compliance checks..."
25
+ pika-review scan --ci
26
+
27
+ EXIT_CODE=$?
28
+ if [ $EXIT_CODE -ne 0 ]; then
29
+ echo "❌ [Pika Review] Commit rejected due to high-severity compliance anomalies."
30
+ exit $EXIT_CODE
31
+ fi
32
+
33
+ exit 0
34
+ `;
35
+ if (action === "install") {
36
+ let backupCreated = false;
37
+ if (fs.existsSync(hookFile)) {
38
+ const content = fs.readFileSync(hookFile, "utf-8");
39
+ if (content.includes("Pika Review")) {
40
+ logger.info("Pika Review hook is already installed!");
41
+ return;
42
+ }
43
+ // Create backup of existing hook
44
+ const backupPath = `${hookFile}.bak`;
45
+ fs.writeFileSync(backupPath, content);
46
+ backupCreated = true;
47
+ logger.info(`Existing hook backed up to ${chalk.cyan("pre-commit.bak")}`);
48
+ }
49
+ try {
50
+ fs.writeFileSync(hookFile, HOOK_SCRIPT, { mode: 0o755 });
51
+ logger.success("Git pre-commit hook installed successfully!");
52
+ logger.info(`Path: ${chalk.dim(hookFile)}`);
53
+ logger.dim("Pika Review will now scan your staged files automatically before every commit.");
54
+ }
55
+ catch (e) {
56
+ logger.error(`Failed to install Git hook: ${e.message}`);
57
+ }
58
+ }
59
+ else if (action === "uninstall") {
60
+ if (!fs.existsSync(hookFile)) {
61
+ logger.warn("No pre-commit hook found to uninstall.");
62
+ return;
63
+ }
64
+ const content = fs.readFileSync(hookFile, "utf-8");
65
+ if (!content.includes("Pika Review")) {
66
+ logger.warn("The existing pre-commit hook was not installed by Pika Review. Leaving it untouched.");
67
+ return;
68
+ }
69
+ try {
70
+ fs.unlinkSync(hookFile);
71
+ logger.success("Pika Review Git pre-commit hook uninstalled successfully.");
72
+ // Restore backup if it exists
73
+ const backupPath = `${hookFile}.bak`;
74
+ if (fs.existsSync(backupPath)) {
75
+ fs.renameSync(backupPath, hookFile);
76
+ // Make sure it remains executable
77
+ fs.chmodSync(hookFile, 0o755);
78
+ logger.success("Restored previous pre-commit hook from backup!");
79
+ }
80
+ }
81
+ catch (e) {
82
+ logger.error(`Failed to uninstall Git hook: ${e.message}`);
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,68 @@
1
+ import prompts from "prompts";
2
+ import chalk from "chalk";
3
+ import { getConfig, saveConfig } from "../utils/config.js";
4
+ import { logger } from "../utils/logger.js";
5
+ /**
6
+ * CLI Command Action: models
7
+ * Queries local Ollama daemon for available models and updates active configuration.
8
+ */
9
+ export async function modelsAction() {
10
+ const config = getConfig();
11
+ // Extract base host from configuration baseURL or fallback to localhost
12
+ let host = "http://localhost:11434";
13
+ if (config.ai.baseURL) {
14
+ try {
15
+ const url = new URL(config.ai.baseURL);
16
+ host = `${url.protocol}//${url.host}`;
17
+ }
18
+ catch (e) { }
19
+ }
20
+ logger.info(`Connecting to Ollama daemon at ${chalk.cyan(host)}...`);
21
+ try {
22
+ const res = await fetch(`${host}/api/tags`);
23
+ if (!res.ok) {
24
+ throw new Error(`Server returned HTTP ${res.status}`);
25
+ }
26
+ const data = (await res.json());
27
+ if (!data.models || data.models.length === 0) {
28
+ logger.warn(`No models found inside your local Ollama instance.`);
29
+ logger.info(`Try pulling a model first using: ${chalk.yellow("ollama pull llama3")}`);
30
+ return;
31
+ }
32
+ const choices = data.models.map((m) => {
33
+ const sizeGB = m.size ? `${(m.size / (1024 * 1024 * 1024)).toFixed(2)} GB` : "Unknown size";
34
+ const paramSize = m.details?.parameter_size ? ` [${m.details.parameter_size}]` : "";
35
+ return {
36
+ title: `${chalk.bold(m.name)} ${chalk.dim(`(${sizeGB}${paramSize})`)}`,
37
+ value: m.name,
38
+ };
39
+ });
40
+ const response = await prompts({
41
+ type: "select",
42
+ name: "model",
43
+ message: "Select your active local Ollama model:",
44
+ choices,
45
+ initial: 0,
46
+ });
47
+ if (response.model) {
48
+ config.ai.provider = "ollama";
49
+ config.ai.model = response.model;
50
+ // Auto-set standard baseURL if empty or not set to localhost
51
+ if (!config.ai.baseURL || config.ai.baseURL.includes("cloudflare")) {
52
+ config.ai.baseURL = "http://localhost:11434/v1";
53
+ }
54
+ saveConfig(config);
55
+ logger.success(`Configuration updated successfully!`);
56
+ logger.info(`Active Model: ${chalk.green(response.model)}`);
57
+ logger.info(`AI Provider: ${chalk.cyan("ollama")}`);
58
+ logger.info(`API Base URL: ${chalk.cyan(config.ai.baseURL)}`);
59
+ }
60
+ }
61
+ catch (error) {
62
+ logger.error(`Could not connect to Ollama daemon.`);
63
+ console.log(chalk.dim(`\n💡 To solve this, make sure:`));
64
+ console.log(` 1. Ollama is installed on your machine (https://ollama.com).`);
65
+ console.log(` 2. The daemon is running (${chalk.yellow("ollama serve")} or desktop application).`);
66
+ console.log(` 3. Your config baseURL is pointing to your active daemon (current: ${chalk.red(host)}).\n`);
67
+ }
68
+ }
@@ -0,0 +1,114 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import prompts from "prompts";
4
+ import chalk from "chalk";
5
+ import OpenAI from "openai";
6
+ import { getConfig } from "../utils/config.js";
7
+ import { listProjectFiles } from "../core/scanner.js";
8
+ import { logger } from "../utils/logger.js";
9
+ /**
10
+ * CLI Command Action: rules generate
11
+ * Uses the configured AI provider to bootstrap architectural rules tailored to the current codebase.
12
+ */
13
+ export async function rulesAction() {
14
+ const config = getConfig();
15
+ const isOllama = config.ai.provider === "ollama";
16
+ if (!isOllama && (!config.ai.apiKey || !config.ai.accountId)) {
17
+ logger.error("Missing AI credentials in ~/.pika-review.yaml.");
18
+ logger.info("Run 'pika-review init' to initialize config or set provider to 'ollama' for offline mode.");
19
+ return;
20
+ }
21
+ const rulesPath = path.join(process.cwd(), ".pika-rules.md");
22
+ if (fs.existsSync(rulesPath)) {
23
+ const overwrite = await prompts({
24
+ type: "confirm",
25
+ name: "confirm",
26
+ message: "An existing .pika-rules.md already exists. Do you want to overwrite it?",
27
+ initial: false,
28
+ });
29
+ if (!overwrite.confirm) {
30
+ logger.info("Operation cancelled.");
31
+ return;
32
+ }
33
+ }
34
+ else {
35
+ const confirm = await prompts({
36
+ type: "confirm",
37
+ name: "confirm",
38
+ message: "Generate a custom, AI-crafted .pika-rules.md architectural guidelines guide for this codebase?",
39
+ initial: true,
40
+ });
41
+ if (!confirm.confirm) {
42
+ logger.info("Operation cancelled.");
43
+ return;
44
+ }
45
+ }
46
+ logger.info("Analyzing codebase structure and dependencies...");
47
+ // 1. Gather package.json context
48
+ let projectTechStack = "";
49
+ try {
50
+ const packageJsonPath = path.join(process.cwd(), "package.json");
51
+ if (fs.existsSync(packageJsonPath)) {
52
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
53
+ const deps = Object.keys(packageJson.dependencies || {}).join(", ");
54
+ const devDeps = Object.keys(packageJson.devDependencies || {}).join(", ");
55
+ projectTechStack += `Package Name: ${packageJson.name || "unknown"}\nDependencies: ${deps || "none"}\nDevDependencies: ${devDeps || "none"}\n`;
56
+ }
57
+ }
58
+ catch (e) { }
59
+ // 2. Gather file layout context
60
+ const files = listProjectFiles();
61
+ const fileSummary = files.slice(0, 30).map(f => ` - ${f}`).join("\n");
62
+ const projectStructure = `Detected files (subset):\n${fileSummary}\nTotal files discovered: ${files.length}\n`;
63
+ logger.info(`Synthesizing tailored rules using AI model (${chalk.green(config.ai.model)})...`);
64
+ const prompt = `You are an Elite Senior Software Architect and Compliance Officer.
65
+ Your task is to write a highly detailed, professional, and practical architectural rules file called ".pika-rules.md" for a project with the following tech stack and structure:
66
+
67
+ <project_tech_stack>
68
+ ${projectTechStack || "Generic web/software codebase"}
69
+ </project_tech_stack>
70
+
71
+ <project_structure>
72
+ ${projectStructure}
73
+ </project_structure>
74
+
75
+ Instructions:
76
+ - Write the output in clean, professional Markdown format.
77
+ - Establish 5 to 8 strict, concrete, actionable architectural rules tailored to the libraries, patterns, and folder layout of this specific project (e.g. naming conventions, folder segregation, API patterns, state management, security).
78
+ - Provide a brief justification for each rule.
79
+ - Do NOT output any preamble, greeting, markdown backticks wrapper, or postscript.
80
+ - Start directly with "# Pika Architectural Rules" and start rules lists immediately.`;
81
+ const baseURL = config.ai.baseURL && config.ai.baseURL.trim()
82
+ ? config.ai.baseURL
83
+ : (isOllama
84
+ ? "http://localhost:11434/v1"
85
+ : `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`);
86
+ const openai = new OpenAI({
87
+ apiKey: config.ai.apiKey || "ollama",
88
+ baseURL,
89
+ });
90
+ try {
91
+ const response = await openai.chat.completions.create({
92
+ model: config.ai.model,
93
+ messages: [{ role: "user", content: prompt }],
94
+ temperature: 0.3,
95
+ max_tokens: 1500,
96
+ });
97
+ let rawMarkdown = response.choices[0]?.message?.content || "";
98
+ // Clean code block ticks if LLM ignored instructions
99
+ rawMarkdown = rawMarkdown
100
+ .replace(/```markdown/g, "")
101
+ .replace(/```/g, "")
102
+ .trim();
103
+ if (!rawMarkdown.startsWith("#")) {
104
+ rawMarkdown = `# Pika Architectural Rules\n\n${rawMarkdown}`;
105
+ }
106
+ fs.writeFileSync(rulesPath, rawMarkdown, "utf-8");
107
+ logger.success(".pika-rules.md generated successfully!");
108
+ logger.info(`Location: ${chalk.dim(rulesPath)}`);
109
+ logger.dim("The local Pika Review engine will now enforce these rules during future scans!");
110
+ }
111
+ catch (error) {
112
+ logger.error(`Failed to generate rules: ${error.message}`);
113
+ }
114
+ }
package/dist/cmd/scan.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import prompts from "prompts";
2
2
  import chalk from "chalk";
3
+ import { execSync } from "child_process";
3
4
  import { runScan, getDiffFiles, listProjectFiles } from "../core/scanner.js";
4
5
  import { logger } from "../utils/logger.js";
5
6
  /**
@@ -32,35 +33,80 @@ export async function scanAction(files, options) {
32
33
  }
33
34
  }
34
35
  }
35
- let reports = await runScan(target, isCI, targets);
36
- if (!reports || reports.length === 0) {
36
+ const { markdownReports, htmlReport, findings } = await runScan(target, isCI, targets);
37
+ if (!markdownReports || markdownReports.length === 0) {
37
38
  if (!isCI) {
38
- logger.success("Scan complete. No critical architectural anomalies detected.");
39
- logger.dim("Try scanning with '--unstaged' if you have uncommitted changes.");
39
+ console.log(`\n ${chalk.green("✓")} ${chalk.bold("Scan complete. No architectural anomalies or security risks detected.")}`);
40
+ console.log(` ${chalk.dim("Try scanning with '--unstaged' if you have unstaged changes.")}\n`);
40
41
  }
41
42
  return;
42
43
  }
43
44
  if (!isCI) {
44
- console.log(chalk.bold(`\n${"=".repeat(50)}`));
45
- logger.warn(`ANALYTICAL REPORT READY`);
46
- logger.dim(`${reports.length} file(s) require your immediate attention.\n`);
45
+ // Count findings by severity
46
+ let criticalCount = 0;
47
+ let highCount = 0;
48
+ let mediumCount = 0;
49
+ let lowCount = 0;
50
+ let totalIssues = 0;
51
+ findings.forEach((f) => {
52
+ totalIssues += f.reviews.length;
53
+ f.reviews.forEach((r) => {
54
+ if (r.severity === "Critical")
55
+ criticalCount++;
56
+ else if (r.severity === "High")
57
+ highCount++;
58
+ else if (r.severity === "Medium")
59
+ mediumCount++;
60
+ else if (r.severity === "Low")
61
+ lowCount++;
62
+ });
63
+ });
64
+ // Sleek summary box
65
+ console.log(`\n ${chalk.cyan.bold("◆ Scan Completed Successfully")}`);
66
+ console.log(` ${chalk.dim("─".repeat(34))}`);
67
+ console.log(` ${chalk.bold("Files Audited:")} ${findings.length}`);
68
+ console.log(` ${chalk.bold("Total Findings:")} ${totalIssues}`);
69
+ if (totalIssues > 0) {
70
+ console.log(`\n ${chalk.bold("Severity Breakdown:")}`);
71
+ if (criticalCount > 0)
72
+ console.log(` 🚨 ${chalk.red.bold(criticalCount)} Critical`);
73
+ if (highCount > 0)
74
+ console.log(` 🔥 ${chalk.yellow.bold(highCount)} High`);
75
+ if (mediumCount > 0)
76
+ console.log(` ⚠️ ${chalk.blue.bold(mediumCount)} Medium`);
77
+ if (lowCount > 0)
78
+ console.log(` 📝 ${chalk.gray.bold(lowCount)} Low`);
79
+ }
80
+ console.log(`\n ${chalk.bold("Obsidian Report:")} ${chalk.cyan.underline(`file://${htmlReport}`)}\n`);
47
81
  const response = await prompts({
48
82
  type: "select",
49
83
  name: "action",
50
- message: "What is your next step?",
84
+ message: "Choose post-scan action:",
51
85
  choices: [
52
- { title: "📂 Open generated Markdown reports", value: "view" },
86
+ { title: " Open Interactive HTML Dashboard (Recommended)", value: "html" },
87
+ { title: "📂 List generated Markdown file paths", value: "markdown" },
53
88
  { title: "🛑 Exit and start refactoring", value: "exit" },
54
89
  ],
55
90
  initial: 0,
56
91
  });
57
- if (response.action === "view") {
58
- logger.info("\nGenerated Artifacts:");
59
- reports.forEach((r) => {
92
+ if (response.action === "html") {
93
+ console.log(`\n ${chalk.cyan("")} Opening interactive report in browser...`);
94
+ try {
95
+ const command = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
96
+ execSync(`${command} "${htmlReport}"`);
97
+ }
98
+ catch (e) {
99
+ console.log(` ${chalk.red("✖")} Failed to open browser automatically.`);
100
+ console.log(` ${chalk.dim(`Manually open: file://${htmlReport}`)}`);
101
+ }
102
+ }
103
+ else if (response.action === "markdown") {
104
+ console.log(`\n ${chalk.bold("Generated Markdown Artifacts:")}`);
105
+ markdownReports.forEach((r) => {
60
106
  const fileName = r.split("/").pop();
61
- console.log(` ${chalk.cyan("")} ${chalk.bold(fileName)} ${chalk.dim(`(${r})`)}`);
107
+ console.log(` ${chalk.cyan("")} ${chalk.bold(fileName)} ${chalk.dim(`(file://${r})`)}`);
62
108
  });
63
- console.log(chalk.dim("\nTip: Use 'cat' or a Markdown viewer to read the findings."));
109
+ console.log(`\n ${chalk.dim("Tip: Click any file:// link above to open directly in your editor.")}\n`);
64
110
  }
65
111
  }
66
112
  }
package/dist/core/ai.js CHANGED
@@ -18,6 +18,8 @@ export function extractJSON(raw) {
18
18
  // Keep only safe whitespace: \n, \r, \t
19
19
  return ["\n", "\r", "\t"].includes(match) ? match : "";
20
20
  });
21
+ // Remove single-line comments that are not part of URLs
22
+ cleaned = cleaned.replace(/(?<!http:|https:)\/\/.*$/gm, "");
21
23
  // Find the outermost JSON structure
22
24
  const firstBrace = cleaned.indexOf("{");
23
25
  const firstBracket = cleaned.indexOf("[");
@@ -36,6 +38,8 @@ export function extractJSON(raw) {
36
38
  let snippet = cleaned.substring(start, end + 1);
37
39
  // Heuristic: MoE models sometimes miss commas between objects in an array
38
40
  snippet = snippet.replace(/\}\s*\{/g, "},{");
41
+ // Clean trailing commas before closing braces/brackets (which break JSON.parse)
42
+ snippet = snippet.replace(/,\s*(\}|\])/g, "$1");
39
43
  // Heuristic Structural Recovery for Truncated Responses
40
44
  const openBraces = (snippet.match(/\{/g) || []).length;
41
45
  const closeBraces = (snippet.match(/\}/g) || []).length;
@@ -56,8 +60,15 @@ export function extractJSON(raw) {
56
60
  */
57
61
  export async function analyzeDiff(diff, fileName) {
58
62
  const config = getConfig();
59
- if (!config.ai.apiKey || !config.ai.accountId) {
60
- throw new Error("Missing AI provider credentials in ~/.pika-review.yaml");
63
+ const isOllama = config.ai.provider === "ollama";
64
+ const isCloudflare = config.ai.provider === "cloudflare";
65
+ // Relax credentials check: Only enforce accountId/apiKey for Cloudflare Workers AI
66
+ if (isCloudflare && (!config.ai.apiKey || !config.ai.accountId)) {
67
+ throw new Error("Missing Cloudflare provider credentials (apiKey, accountId) in ~/.pika-review.yaml");
68
+ }
69
+ // For standard API providers (OpenAI, DeepSeek, etc.), we only need the apiKey
70
+ if (!isOllama && !isCloudflare && !config.ai.apiKey) {
71
+ throw new Error("Missing API Key (apiKey) in ~/.pika-review.yaml");
61
72
  }
62
73
  // Rate Limit Safety: Truncate input to protect the daily free limit.
63
74
  // 30,000 chars is roughly 7,500 tokens, leaving room for a detailed response.
@@ -68,9 +79,13 @@ export async function analyzeDiff(diff, fileName) {
68
79
  : diff;
69
80
  const baseURL = config.ai.baseURL && config.ai.baseURL.trim()
70
81
  ? config.ai.baseURL
71
- : `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`;
82
+ : (isOllama
83
+ ? "http://localhost:11434/v1"
84
+ : (isCloudflare
85
+ ? `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`
86
+ : "https://api.openai.com/v1"));
72
87
  const openai = new OpenAI({
73
- apiKey: config.ai.apiKey,
88
+ apiKey: config.ai.apiKey || "ollama",
74
89
  baseURL,
75
90
  });
76
91
  // Load custom architecture rules if they exist
@@ -86,140 +86,266 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
86
86
  <head>
87
87
  <meta charset="UTF-8">
88
88
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
89
- <title>Pika Review | ${projectName}</title>
89
+ <title>Pika Sentinel Report | ${projectName}</title>
90
90
  <link rel="preconnect" href="https://fonts.googleapis.com">
91
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">
92
+ <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Outfit:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
93
93
  <style>
94
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);
95
+ --bg: #09090b;
96
+ --sidebar-bg: #0c0c0e;
97
+ --card-bg: rgba(255, 255, 255, 0.02);
98
+ --card-hover: rgba(255, 255, 255, 0.04);
99
+ --text: #f3f4f6;
100
+ --text-dim: #a1a1aa;
101
+ --accent: #00F0FF;
102
+ --accent-glow: rgba(0, 240, 255, 0.15);
103
+ --critical: #ef4444;
104
+ --high: #f97316;
105
+ --medium: #eab308;
106
+ --low: #10b981;
107
+ --border: rgba(255, 255, 255, 0.08);
108
+ --font-sans: 'Outfit', sans-serif;
109
+ --font-mono: 'Fira Code', monospace;
106
110
  }
107
111
 
108
112
  * { margin: 0; padding: 0; box-sizing: border-box; }
109
113
  body {
110
- font-family: 'Inter', sans-serif;
114
+ font-family: var(--font-sans);
111
115
  background: var(--bg);
112
116
  color: var(--text);
113
117
  display: flex;
114
118
  height: 100vh;
115
119
  overflow: hidden;
120
+ -webkit-font-smoothing: antialiased;
116
121
  }
117
122
 
118
- /* Sidebar */
123
+ /* Ambient glows and high-tech grid overlays */
124
+ .bg-grid {
125
+ position: fixed;
126
+ top: 0; left: 0; width: 100%; height: 100%;
127
+ background-image:
128
+ linear-gradient(to right, rgba(255,255,255,0.01) 1px, transparent 1px),
129
+ linear-gradient(to bottom, rgba(255,255,255,0.01) 1px, transparent 1px);
130
+ background-size: 30px 30px;
131
+ pointer-events: none;
132
+ z-index: 1;
133
+ }
134
+
135
+ .bg-radial {
136
+ position: fixed;
137
+ top: -10%; right: -10%; width: 60vw; height: 60vw;
138
+ background: radial-gradient(circle, rgba(0, 240, 255, 0.03) 0%, transparent 70%);
139
+ pointer-events: none;
140
+ z-index: 1;
141
+ }
142
+
143
+ /* Sidebar container */
119
144
  aside {
120
- width: 350px;
145
+ width: 360px;
121
146
  background: var(--sidebar-bg);
122
147
  border-right: 1px solid var(--border);
123
148
  display: flex;
124
149
  flex-direction: column;
125
- box-shadow: 10px 0 30px rgba(0,0,0,0.3);
150
+ box-shadow: 10px 0 40px rgba(0,0,0,0.5);
126
151
  z-index: 10;
127
152
  }
128
153
 
129
154
  .brand {
130
- padding: 32px 24px;
155
+ padding: 30px 24px;
131
156
  border-bottom: 1px solid var(--border);
132
- background: linear-gradient(135deg, rgba(56, 189, 248, 0.1), transparent);
157
+ display: flex;
158
+ align-items: center;
159
+ gap: 16px;
160
+ background: linear-gradient(180deg, rgba(0, 240, 255, 0.02), transparent);
161
+ }
162
+
163
+ .brand-logo {
164
+ width: 38px;
165
+ height: 38px;
166
+ filter: drop-shadow(0 0 6px rgba(0, 240, 255, 0.5));
167
+ }
168
+
169
+ .brand-text h1 {
170
+ font-size: 1.25rem;
171
+ font-weight: 800;
172
+ letter-spacing: -0.5px;
173
+ color: var(--text);
174
+ display: flex;
175
+ align-items: center;
176
+ gap: 8px;
177
+ }
178
+
179
+ .brand-text h1 span {
180
+ color: var(--accent);
181
+ text-shadow: 0 0 10px rgba(0, 240, 255, 0.3);
133
182
  }
134
183
 
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; }
184
+ .brand-text p {
185
+ font-size: 0.75rem;
186
+ color: var(--text-dim);
187
+ margin-top: 4px;
188
+ font-weight: 500;
189
+ }
137
190
 
138
191
  .sidebar-stats {
139
192
  padding: 20px 24px;
140
193
  display: grid;
141
- grid-template-columns: 1fr 1fr;
142
- gap: 12px;
194
+ grid-template-columns: repeat(3, 1fr);
195
+ gap: 10px;
143
196
  border-bottom: 1px solid var(--border);
144
197
  }
145
198
 
146
199
  .stat-box {
147
- background: rgba(255,255,255,0.03);
148
- padding: 12px;
149
- border-radius: 10px;
150
- text-align: center;
200
+ background: var(--card-bg);
151
201
  border: 1px solid var(--border);
202
+ padding: 12px 6px;
203
+ border-radius: 8px;
204
+ text-align: center;
205
+ transition: all 0.2s ease;
152
206
  }
153
207
 
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; }
208
+ .stat-box:hover {
209
+ border-color: rgba(255, 255, 255, 0.15);
210
+ }
211
+
212
+ .stat-val {
213
+ display: block;
214
+ font-size: 1.15rem;
215
+ font-weight: 800;
216
+ color: var(--accent);
217
+ text-shadow: 0 0 8px rgba(0, 240, 255, 0.2);
218
+ }
219
+
220
+ .stat-box.crit-stat .stat-val {
221
+ color: var(--critical);
222
+ text-shadow: 0 0 8px rgba(239, 68, 68, 0.2);
223
+ }
224
+
225
+ .stat-lab {
226
+ display: block;
227
+ font-size: 0.6rem;
228
+ font-weight: 700;
229
+ text-transform: uppercase;
230
+ color: var(--text-dim);
231
+ margin-top: 4px;
232
+ letter-spacing: 0.05em;
233
+ }
156
234
 
157
235
  .file-list {
158
236
  flex: 1;
159
237
  overflow-y: auto;
160
- padding: 16px;
238
+ padding: 20px 16px;
161
239
  }
162
240
 
163
241
  .file-item {
164
242
  padding: 14px 16px;
165
- border-radius: 12px;
243
+ border-radius: 10px;
166
244
  cursor: pointer;
167
245
  margin-bottom: 8px;
168
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
246
+ background: var(--card-bg);
247
+ border: 1px solid var(--border);
248
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
169
249
  display: flex;
170
250
  flex-direction: column;
171
- gap: 6px;
172
- border: 1px solid transparent;
251
+ gap: 8px;
252
+ }
253
+
254
+ .file-item:hover {
255
+ background: var(--card-hover);
256
+ border-color: rgba(0, 240, 255, 0.2);
257
+ transform: translateX(4px);
258
+ }
259
+
260
+ .file-item.active {
261
+ background: rgba(0, 240, 255, 0.04);
262
+ border-color: var(--accent);
263
+ box-shadow: 0 0 15px rgba(0, 240, 255, 0.08);
173
264
  }
174
265
 
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; }
266
+ .file-item .name {
267
+ font-size: 0.85rem;
268
+ font-family: var(--font-mono);
269
+ font-weight: 500;
270
+ word-break: break-all;
271
+ color: var(--text-dim);
272
+ transition: color 0.2s;
273
+ }
274
+
275
+ .file-item.active .name {
276
+ color: var(--accent);
277
+ text-shadow: 0 0 8px rgba(0, 240, 255, 0.4);
278
+ }
178
279
 
179
280
  .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; }
281
+ .pill {
282
+ font-size: 0.65rem;
283
+ padding: 2px 6px;
284
+ border-radius: 4px;
285
+ background: rgba(0,0,0,0.3);
286
+ font-weight: 700;
287
+ border: 1px solid rgba(255,255,255,0.05);
288
+ }
182
289
 
183
- /* Main Content */
290
+ /* Main Content area */
184
291
  main {
185
292
  flex: 1;
186
293
  overflow-y: auto;
187
- padding: 60px;
188
- background: radial-gradient(circle at top right, rgba(56, 189, 248, 0.05), transparent 40%);
294
+ padding: 60px 80px;
295
+ position: relative;
296
+ z-index: 5;
189
297
  }
190
298
 
191
299
  .content-container { max-width: 900px; margin: 0 auto; }
192
300
 
193
301
  .page-header {
194
- margin-bottom: 50px;
195
- animation: fadeInDown 0.5s ease-out;
302
+ margin-bottom: 40px;
303
+ animation: fadeInDown 0.4s cubic-bezier(0.16, 1, 0.3, 1);
196
304
  }
197
305
 
198
306
  @keyframes fadeInDown {
199
- from { opacity: 0; transform: translateY(-20px); }
307
+ from { opacity: 0; transform: translateY(-12px); }
200
308
  to { opacity: 1; transform: translateY(0); }
201
309
  }
202
310
 
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; }
311
+ .page-header h2 {
312
+ font-size: 1.85rem;
313
+ font-family: var(--font-mono);
314
+ margin-bottom: 12px;
315
+ color: #fff;
316
+ letter-spacing: -0.5px;
317
+ }
318
+
319
+ .page-header p { color: var(--text-dim); font-size: 0.95rem; }
205
320
 
321
+ /* Anomaly Findings Cards */
206
322
  .issue-card {
207
323
  background: var(--card-bg);
208
324
  border: 1px solid var(--border);
209
- border-radius: 20px;
210
- padding: 32px;
211
- margin-bottom: 30px;
325
+ border-radius: 16px;
326
+ padding: 30px;
327
+ margin-bottom: 24px;
212
328
  position: relative;
213
- box-shadow: 0 10px 40px rgba(0,0,0,0.2);
214
- transition: transform 0.3s;
329
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
330
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
331
+ animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
332
+ }
333
+
334
+ @keyframes fadeInUp {
335
+ from { opacity: 0; transform: translateY(12px); }
336
+ to { opacity: 1; transform: translateY(0); }
215
337
  }
216
338
 
217
- .issue-card:hover { transform: translateY(-4px); }
339
+ .issue-card:hover {
340
+ transform: translateY(-2px);
341
+ border-color: rgba(255, 255, 255, 0.15);
342
+ box-shadow: 0 15px 35px rgba(0,0,0,0.4);
343
+ }
218
344
 
219
345
  .card-accent {
220
346
  position: absolute;
221
- left: 0; top: 32px; bottom: 32px;
222
- width: 5px;
347
+ left: 0; top: 30px; bottom: 30px;
348
+ width: 4px;
223
349
  border-radius: 0 4px 4px 0;
224
350
  }
225
351
 
@@ -236,46 +362,89 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
236
362
  }
237
363
 
238
364
  .severity-tag {
239
- font-size: 0.75rem;
365
+ font-size: 0.7rem;
240
366
  text-transform: uppercase;
241
- font-weight: 800;
242
- padding: 6px 14px;
243
- border-radius: 8px;
367
+ font-weight: 850;
368
+ padding: 4px 12px;
369
+ border-radius: 6px;
244
370
  letter-spacing: 0.05em;
245
371
  }
246
372
 
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); }
373
+ .severity-tag.Critical { background: rgba(239, 68, 68, 0.1); color: var(--critical); border: 1px solid rgba(239, 68, 68, 0.2); }
374
+ .severity-tag.High { background: rgba(249, 115, 22, 0.1); color: var(--high); border: 1px solid rgba(249, 115, 22, 0.2); }
375
+ .severity-tag.Medium { background: rgba(234, 179, 8, 0.1); color: var(--medium); border: 1px solid rgba(234, 179, 8, 0.2); }
376
+ .severity-tag.Low { background: rgba(16, 185, 129, 0.1); color: var(--low); border: 1px solid rgba(16, 185, 129, 0.2); }
251
377
 
252
- .line-info { font-family: 'JetBrains Mono', monospace; color: var(--text-dim); font-size: 0.9rem; }
378
+ .line-info { font-family: var(--font-mono); color: var(--text-dim); font-size: 0.85rem; }
253
379
 
254
380
  .field-group { margin-bottom: 24px; }
381
+ .field-group:last-child { margin-bottom: 0; }
382
+
255
383
  .field-label {
256
384
  font-size: 0.7rem;
257
385
  font-weight: 800;
258
386
  color: var(--text-dim);
259
387
  text-transform: uppercase;
260
388
  letter-spacing: 0.1em;
261
- margin-bottom: 10px;
389
+ margin-bottom: 8px;
262
390
  display: flex;
263
391
  align-items: center;
264
- gap: 8px;
392
+ gap: 6px;
393
+ }
394
+
395
+ .field-content { line-height: 1.6; color: #d1d5db; font-size: 0.95rem; }
396
+
397
+ /* Recommendation Pre block with visual copy btn */
398
+ .pre-wrapper {
399
+ position: relative;
400
+ background: #040405;
401
+ border: 1px solid rgba(255,255,255,0.04);
402
+ border-radius: 10px;
403
+ margin-top: 10px;
404
+ overflow: hidden;
265
405
  }
266
406
 
267
- .field-content { line-height: 1.7; color: #cbd5e1; font-size: 1.05rem; }
407
+ .pre-header {
408
+ background: rgba(255,255,255,0.02);
409
+ border-bottom: 1px solid rgba(255,255,255,0.04);
410
+ padding: 6px 16px;
411
+ display: flex;
412
+ justify-content: space-between;
413
+ align-items: center;
414
+ }
415
+
416
+ .pre-lang {
417
+ font-size: 0.65rem;
418
+ font-weight: 700;
419
+ color: var(--text-dim);
420
+ text-transform: uppercase;
421
+ }
422
+
423
+ .card-copy-btn {
424
+ background: none;
425
+ border: 1px solid rgba(255,255,255,0.08);
426
+ color: var(--text-dim);
427
+ font-family: var(--font-sans);
428
+ font-size: 0.65rem;
429
+ font-weight: 600;
430
+ padding: 3px 6px;
431
+ border-radius: 4px;
432
+ cursor: pointer;
433
+ transition: all 0.2s ease;
434
+ }
435
+
436
+ .card-copy-btn:hover {
437
+ border-color: rgba(0, 240, 255, 0.4);
438
+ color: var(--accent);
439
+ }
268
440
 
269
441
  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;
442
+ padding: 16px;
443
+ font-family: var(--font-mono);
444
+ font-size: 0.85rem;
445
+ line-height: 1.5;
276
446
  overflow-x: auto;
277
- border: 1px solid rgba(255,255,255,0.05);
278
- color: #e2e8f0;
447
+ color: #c7d2fe;
279
448
  }
280
449
 
281
450
  .empty-state {
@@ -285,23 +454,57 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
285
454
  justify-content: center;
286
455
  height: 60vh;
287
456
  text-align: center;
288
- opacity: 0.6;
289
457
  }
290
458
 
291
- .empty-state svg { width: 80px; height: 80px; margin-bottom: 24px; stroke: var(--text-dim); }
459
+ .empty-state svg {
460
+ width: 64px;
461
+ height: 64px;
462
+ margin-bottom: 20px;
463
+ stroke: var(--text-dim);
464
+ opacity: 0.4;
465
+ filter: drop-shadow(0 0 8px rgba(0, 240, 255, 0.1));
466
+ }
467
+
468
+ .empty-state h3 {
469
+ font-size: 1.25rem;
470
+ font-weight: 600;
471
+ margin-bottom: 8px;
472
+ }
473
+
474
+ .empty-state p {
475
+ font-size: 0.9rem;
476
+ color: var(--text-dim);
477
+ max-width: 320px;
478
+ }
292
479
 
293
480
  /* Scrollbar */
294
481
  ::-webkit-scrollbar { width: 6px; }
295
482
  ::-webkit-scrollbar-track { background: transparent; }
296
- ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
483
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 10px; }
297
484
  ::-webkit-scrollbar-thumb:hover { background: var(--accent); }
298
485
  </style>
299
486
  </head>
300
487
  <body>
488
+ <div class="bg-grid"></div>
489
+ <div class="bg-radial"></div>
490
+
301
491
  <aside>
302
492
  <div class="brand">
303
- <h1>🦊 PIKA REVIEW</h1>
304
- <p>${projectName} ${timestamp}</p>
493
+ <svg viewBox="0 0 100 100" class="brand-logo" fill="none">
494
+ <circle cx="50" cy="50" r="44" stroke="#00F0FF" stroke-width="2" stroke-dasharray="4 8" opacity="0.3" />
495
+ <polygon points="50,42 22,26 34,50" fill="#0c4a6e" stroke="#00F0FF" stroke-width="1.5" stroke-linejoin="round" />
496
+ <polygon points="50,42 78,26 66,50" fill="#0c4a6e" stroke="#00F0FF" stroke-width="1.5" stroke-linejoin="round" />
497
+ <polygon points="50,78 20,48 36,48" fill="#0f172a" stroke="#00F0FF" stroke-width="1.5" stroke-linejoin="round" />
498
+ <polygon points="50,78 80,48 64,48" fill="#0f172a" stroke="#00F0FF" stroke-width="1.5" stroke-linejoin="round" />
499
+ <polygon points="50,42 36,48 50,78" fill="#1e293b" stroke="#00F0FF" stroke-width="1.5" stroke-linejoin="round" />
500
+ <polygon points="50,42 64,48 50,78" fill="#1e293b" stroke="#00F0FF" stroke-width="1.5" stroke-linejoin="round" />
501
+ <polygon points="40,52 46,50 44,56" fill="#00F0FF" />
502
+ <polygon points="60,52 54,50 56,56" fill="#00F0FF" />
503
+ </svg>
504
+ <div class="brand-text">
505
+ <h1>Pika <span>Sentinel</span></h1>
506
+ <p>${projectName} • ${timestamp}</p>
507
+ </div>
305
508
  </div>
306
509
  <div class="sidebar-stats">
307
510
  <div class="stat-box">
@@ -312,6 +515,10 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
312
515
  <span class="stat-val" id="totalIssues">0</span>
313
516
  <span class="stat-lab">Issues</span>
314
517
  </div>
518
+ <div class="stat-box crit-stat">
519
+ <span class="stat-val" id="criticalIssues">0</span>
520
+ <span class="stat-lab">Critical</span>
521
+ </div>
315
522
  </div>
316
523
  <div class="file-list" id="fileList"></div>
317
524
  </aside>
@@ -319,9 +526,9 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
319
526
  <main>
320
527
  <div class="content-container" id="mainContent">
321
528
  <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>
529
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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
530
  <h3>Select a file to inspect</h3>
324
- <p>Architectural findings and security insights will appear here.</p>
531
+ <p>Architectural anomalies and security compliance findings will appear here.</p>
325
532
  </div>
326
533
  </div>
327
534
  </main>
@@ -334,6 +541,7 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
334
541
  const mainContent = document.getElementById('mainContent');
335
542
  const totalFilesEl = document.getElementById('totalFiles');
336
543
  const totalIssuesEl = document.getElementById('totalIssues');
544
+ const criticalIssuesEl = document.getElementById('criticalIssues');
337
545
 
338
546
  function escape(str) {
339
547
  if (!str) return '';
@@ -345,9 +553,17 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
345
553
  function init() {
346
554
  totalFilesEl.textContent = findings.length;
347
555
  let issuesCount = 0;
556
+ let criticalCount = 0;
348
557
 
349
558
  findings.forEach((f, index) => {
350
559
  issuesCount += f.reviews.length;
560
+
561
+ f.reviews.forEach(r => {
562
+ if (r.severity === 'Critical') {
563
+ criticalCount++;
564
+ }
565
+ });
566
+
351
567
  const item = document.createElement('div');
352
568
  item.className = 'file-item';
353
569
 
@@ -386,6 +602,7 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
386
602
  });
387
603
 
388
604
  totalIssuesEl.textContent = issuesCount;
605
+ criticalIssuesEl.textContent = criticalCount;
389
606
  }
390
607
 
391
608
  function selectFile(index, element) {
@@ -420,6 +637,8 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
420
637
  const card = document.createElement('div');
421
638
  card.className = 'issue-card ' + r.severity;
422
639
 
640
+ const escRec = escape(r.recommendation);
641
+
423
642
  card.innerHTML = [
424
643
  '<div class="card-accent"></div>',
425
644
  '<div class="issue-meta">',
@@ -436,7 +655,13 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
436
655
  '</div>',
437
656
  '<div class="field-group">',
438
657
  '<div class="field-label">🛠️ Recommendation</div>',
439
- '<pre><code>' + escape(r.recommendation) + '</code></pre>',
658
+ '<div class="pre-wrapper">',
659
+ '<div class="pre-header">',
660
+ '<span class="pre-lang">Code Recommendation</span>',
661
+ '<button class="card-copy-btn" onclick="copyRecommendation(this)">Copy Code</button>',
662
+ '</div>',
663
+ '<pre><code>' + escRec + '</code></pre>',
664
+ '</div>',
440
665
  '</div>'
441
666
  ].join('');
442
667
 
@@ -446,6 +671,29 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
446
671
  mainContent.scrollTop = 0;
447
672
  }
448
673
 
674
+ window.copyRecommendation = function(btn) {
675
+ const preElement = btn.closest('.pre-wrapper').querySelector('pre code');
676
+ const text = preElement ? preElement.textContent : '';
677
+
678
+ // Decode HTML entities before copying
679
+ const textarea = document.createElement('textarea');
680
+ textarea.innerHTML = text;
681
+ const decodedText = textarea.value;
682
+
683
+ navigator.clipboard.writeText(decodedText).then(() => {
684
+ const originalText = btn.textContent;
685
+ btn.textContent = 'Copied! ✓';
686
+ btn.style.color = 'var(--accent)';
687
+ btn.style.borderColor = 'rgba(0, 240, 255, 0.4)';
688
+
689
+ setTimeout(() => {
690
+ btn.textContent = originalText;
691
+ btn.style.color = '';
692
+ btn.style.borderColor = '';
693
+ }, 2000);
694
+ });
695
+ };
696
+
449
697
  init();
450
698
  })();
451
699
  </script>
@@ -2,6 +2,7 @@ import { execFileSync } from "child_process";
2
2
  import fs from "fs";
3
3
  import cliProgress from "cli-progress";
4
4
  import pLimit from "p-limit";
5
+ import chalk from "chalk";
5
6
  import { analyzeDiff } from "./ai.js";
6
7
  import { setupReportDir, writeMarkdownReport, writeHTMLReport } from "./reporter.js";
7
8
  import { getIgnoredFiles } from "../utils/config.js";
@@ -84,13 +85,17 @@ export async function runScan(target, isCI, specificFiles) {
84
85
  if (files.length === 0) {
85
86
  if (!isCI)
86
87
  logger.success("No changes detected. Codebase is clean.");
87
- return [];
88
+ return {
89
+ markdownReports: [],
90
+ htmlReport: "",
91
+ findings: []
92
+ };
88
93
  }
89
94
  const reportDir = setupReportDir();
90
95
  if (!isCI)
91
96
  logger.info(`Initializing Pika Review on ${files.length} file(s)...`);
92
97
  const bar = isCI ? null : new cliProgress.SingleBar({
93
- format: ' {bar} {percentage}% | ETA: {eta}s | Scanning: {file}',
98
+ format: ' ' + chalk.cyan('{bar}') + ' ' + chalk.bold('{percentage}%') + ' │ ' + chalk.dim('ETA: {eta}s') + ' │ ' + chalk.gray('{file}'),
94
99
  barCompleteChar: '\u2588',
95
100
  barIncompleteChar: '\u2591',
96
101
  hideCursor: true,
@@ -191,7 +196,10 @@ export async function runScan(target, isCI, specificFiles) {
191
196
  const htmlPath = writeHTMLReport(reportDir, files.length, allFindings);
192
197
  if (!isCI) {
193
198
  logger.success("\nScan complete!");
194
- logger.info(`Interactive Report: ${htmlPath}`);
195
199
  }
196
- return generatedReports;
200
+ return {
201
+ markdownReports: generatedReports,
202
+ htmlReport: htmlPath,
203
+ findings: allFindings
204
+ };
197
205
  }
package/dist/index.js CHANGED
@@ -5,6 +5,10 @@ import { initAction } from "./cmd/init.js";
5
5
  import { scanAction } from "./cmd/scan.js";
6
6
  import { viewAction } from "./cmd/view.js";
7
7
  import { statsAction } from "./cmd/stats.js";
8
+ import { modelsAction } from "./cmd/models.js";
9
+ import { hookAction } from "./cmd/hook.js";
10
+ import { rulesAction } from "./cmd/rules.js";
11
+ import { logger } from "./utils/logger.js";
8
12
  /**
9
13
  * Pika Review: The Enterprise-Grade AI Code Reviewer.
10
14
  * Modular entry point for CLI operations.
@@ -12,32 +16,31 @@ import { statsAction } from "./cmd/stats.js";
12
16
  const program = new Command();
13
17
  // Branding Header
14
18
  const BRAND = `
15
- ${chalk.bgCyan.black.bold(" PIKA REVIEW ")} ${chalk.dim("v2.0.0 Enterprise")}
16
- ${chalk.italic.gray("AI Architectural Sentinel & Compliance Engine")}
19
+ ${chalk.cyan.bold("◆ Pika Sentinel")} ${chalk.dim(`v2.2.0 (Enterprise)`)}
20
+ ${chalk.dim("".repeat(42))}
21
+ ${chalk.italic.gray("AI Architectural & Security Safeguard")}
17
22
  `;
18
23
  const HELPER_TEXT = `
19
- ${chalk.bold.cyan("🚀 Quick Start:")}
20
- $ pika-review scan ${chalk.dim("# Scan staged git changes (default)")}
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")}
24
- $ pika-review scan file.ts ${chalk.dim("# Scan a specific file")}
25
- $ pika-review scan f1 f2 ${chalk.dim("# Scan multiple specific files")}
24
+ ${chalk.cyan.bold("Commands:")}
25
+ ${chalk.bold("scan")} Scan git staged changes for issues (default)
26
+ -u, --unstaged Scan unstaged changes instead of staged
27
+ -i, --interactive Interactively pick files to scan
28
+ --ci Fail CI pipeline if critical/high issues are found
29
+ ${chalk.bold("view")} Open the latest interactive HTML report in browser
30
+ ${chalk.bold("stats")} Print scan history & key quality trends
31
+ ${chalk.bold("models")} Interactively select Ollama models for offline audit
32
+ ${chalk.bold("hook")} Install Git pre-commit scanner safeguard hook
33
+ ${chalk.bold("rules")} AI-generate architectural '.pika-rules.md'
26
34
 
27
- ${chalk.bold.cyan("⌨️ Shortcuts & Tips:")}
28
- - Use ${chalk.yellow("--ci")} in GitHub Actions to fail on Critical/High issues.
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.
31
- - Review artifacts are stored in ${chalk.yellow(".pika-reports/")} automatically.
32
-
33
- ${chalk.bold.cyan("📊 Progress & Concurrency:")}
34
- - Pika uses ${chalk.green("p-limit(3)")} to scan files in parallel without hitting rate limits.
35
- - Real-time progress bars will show you the exact status of each analysis.
35
+ ${chalk.cyan.bold("Options & Advanced:")}
36
+ Custom Rules: Create a ${chalk.yellow(".pika-rules.md")} to feed custom codebase standards to the AI.
37
+ Bypasses: Add ${chalk.yellow("// pika-ignore")} or ${chalk.yellow("/* pika-ignore */")} in code to skip lines.
38
+ Local LLM: Select an offline model via '${chalk.green("pika-review models")}' for passwordless private runs.
36
39
  `;
37
40
  program
38
41
  .name("pika-review")
39
42
  .description("Enterprise-grade AI Architectural Code Reviewer")
40
- .version("2.0.0")
43
+ .version("2.2.0")
41
44
  .addHelpText("before", BRAND)
42
45
  .addHelpText("after", HELPER_TEXT);
43
46
  program
@@ -61,6 +64,38 @@ program
61
64
  console.log(BRAND);
62
65
  await statsAction();
63
66
  });
67
+ program
68
+ .command("models")
69
+ .description("Select and configure local Ollama models interactively")
70
+ .action(async () => {
71
+ console.log(BRAND);
72
+ await modelsAction();
73
+ });
74
+ program
75
+ .command("hook <action>")
76
+ .description("Install or uninstall Git pre-commit scanner safeguard hook")
77
+ .addHelpText("after", `\nActions:\n install Install Git pre-commit hook\n uninstall Remove Git pre-commit hook`)
78
+ .action(async (action) => {
79
+ if (action !== "install" && action !== "uninstall") {
80
+ logger.error("Invalid action. Use 'install' or 'uninstall'.");
81
+ process.exit(1);
82
+ }
83
+ console.log(BRAND);
84
+ await hookAction(action);
85
+ });
86
+ program
87
+ .command("rules")
88
+ .description("AI architectural rules utilities")
89
+ .option("-g, --generate", "Auto-generate .pika-rules.md based on codebase")
90
+ .action(async (options) => {
91
+ console.log(BRAND);
92
+ if (options.generate) {
93
+ await rulesAction();
94
+ }
95
+ else {
96
+ logger.info("Use 'pika-review rules --generate' to auto-generate architecture rules.");
97
+ }
98
+ });
64
99
  program
65
100
  .command("scan [files...]", { isDefault: true }) // Set scan as the default command
66
101
  .description("Scan git changes or specific files for architectural anomalies")
@@ -6,11 +6,12 @@ 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: "", // Account ID if required by provider
9
+ provider: "openai",
10
+ accountId: "", // Account ID if required (e.g. Cloudflare)
10
11
  apiKey: "",
11
- model: "@cf/meta/llama-3-8b-instruct",
12
+ model: "gpt-4o",
12
13
  prompt: "",
13
- baseURL: "", // Leave empty for default provider, or set for custom API
14
+ baseURL: "https://api.openai.com/v1", // Default OpenAI endpoint
14
15
  },
15
16
  };
16
17
  /**
@@ -39,6 +40,19 @@ export function getConfig() {
39
40
  }
40
41
  return yaml.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
41
42
  }
43
+ /**
44
+ * Save configuration back to disk with correct permissions.
45
+ */
46
+ export function saveConfig(config) {
47
+ try {
48
+ fs.writeFileSync(CONFIG_PATH, yaml.stringify(config), {
49
+ mode: 0o600,
50
+ });
51
+ }
52
+ catch (error) {
53
+ logger.error(`Failed to save configuration: ${error.message}`);
54
+ }
55
+ }
42
56
  /**
43
57
  * Get files to ignore during scanning.
44
58
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pika-review",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Enterprise-grade AI Architectural Code Reviewer.",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",