pika-review 2.2.0 → 2.3.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,4 +1,4 @@
1
- # Pika Review 🦊
1
+ # Pika Review 🦊 [![Pika Sentinel Grade](badge.svg)](https://github.com/HackX-IN/pika-review)
2
2
 
3
3
  ### Enterprise-grade AI Architectural Sentinel & Compliance Engine
4
4
 
@@ -0,0 +1,61 @@
1
+ import fs from "fs";
2
+ import chalk from "chalk";
3
+ import { execSync } from "child_process";
4
+ /**
5
+ * Extracts the first markdown fenced code block from a recommendation string.
6
+ */
7
+ export function extractCodeBlock(markdown) {
8
+ // Regex matches triple backtick block with optional language identifier
9
+ const match = markdown.match(/```[a-zA-Z]*\n([\s\S]*?)\n```/);
10
+ return match ? match[1] : null;
11
+ }
12
+ /**
13
+ * Patches a target file in-place with an AI code block at a specific line number.
14
+ * Preserves the original indentation of the line being replaced.
15
+ */
16
+ export function applyAutoFix(filePath, lineNum, recommendation) {
17
+ try {
18
+ if (!fs.existsSync(filePath)) {
19
+ console.log(` ${chalk.red("✖")} Target file not found: ${filePath}`);
20
+ return false;
21
+ }
22
+ const code = extractCodeBlock(recommendation);
23
+ if (!code) {
24
+ console.log(` ${chalk.red("✖")} No valid code block found in recommendation to apply.`);
25
+ return false;
26
+ }
27
+ const content = fs.readFileSync(filePath, "utf-8");
28
+ const lines = content.split(/\r?\n/);
29
+ if (lineNum !== undefined && lineNum > 0 && lineNum <= lines.length) {
30
+ // Find indentation of the original line to preserve nesting structure
31
+ const indent = lines[lineNum - 1].match(/^\s*/)?.[0] || "";
32
+ const indentedCode = code.split("\n").map(l => indent + l).join("\n");
33
+ lines[lineNum - 1] = indentedCode;
34
+ fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
35
+ console.log(`\n ${chalk.green("✓")} Clean code applied to ${chalk.bold(filePath)} at line ${lineNum}.`);
36
+ // Render Git Diff for immediate visual confirmation
37
+ try {
38
+ console.log(`\n ${chalk.cyan.bold("◆ Git Diff Confirmation:")}`);
39
+ const diff = execSync(`git diff --color "${filePath}"`, { encoding: "utf-8" });
40
+ if (diff.trim()) {
41
+ console.log(diff);
42
+ }
43
+ else {
44
+ console.log(` ${chalk.dim("Lines matched exactly; no raw text diff produced.")}`);
45
+ }
46
+ }
47
+ catch (diffErr) {
48
+ // Fail silently if git is not initialized or diff fails
49
+ }
50
+ return true;
51
+ }
52
+ else {
53
+ console.log(` ${chalk.red("✖")} Target line number is out of bounds or undefined.`);
54
+ return false;
55
+ }
56
+ }
57
+ catch (e) {
58
+ console.log(` ${chalk.red("✖")} Failed to apply auto-fix: ${e.message}`);
59
+ return false;
60
+ }
61
+ }
@@ -0,0 +1,87 @@
1
+ import fs from "fs";
2
+ import chalk from "chalk";
3
+ import prompts from "prompts";
4
+ import OpenAI from "openai";
5
+ import { getConfig } from "../utils/config.js";
6
+ /**
7
+ * CLI Command: discuss [file]
8
+ * Launches an interactive Socratic chat session inside the console focusing on design context.
9
+ */
10
+ export async function discussAction(filePath) {
11
+ if (!fs.existsSync(filePath)) {
12
+ console.log(` ${chalk.red("✖")} Target file not found: ${filePath}`);
13
+ return;
14
+ }
15
+ const config = getConfig();
16
+ const isOllama = config.ai.provider === "ollama";
17
+ const isCloudflare = config.ai.provider === "cloudflare";
18
+ const baseURL = config.ai.baseURL && config.ai.baseURL.trim()
19
+ ? config.ai.baseURL
20
+ : (isOllama
21
+ ? "http://localhost:11434/v1"
22
+ : (isCloudflare
23
+ ? `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`
24
+ : "https://api.openai.com/v1"));
25
+ const openai = new OpenAI({
26
+ apiKey: config.ai.apiKey || "ollama",
27
+ baseURL,
28
+ });
29
+ const fileContent = fs.readFileSync(filePath, "utf-8");
30
+ // Load custom architecture standards rules if they exist
31
+ let complianceSection = "";
32
+ try {
33
+ const rulesPath = ".pika-rules.md";
34
+ if (fs.existsSync(rulesPath)) {
35
+ complianceSection = `\nCustom Rules:\n${fs.readFileSync(rulesPath, "utf-8")}`;
36
+ }
37
+ }
38
+ catch (e) { }
39
+ const messages = [
40
+ {
41
+ role: "system",
42
+ content: `You are an Elite Senior Full-Stack Architect and Security Mentor.
43
+ Your task is to have an interactive Socratic discussion with the developer regarding the provided file.
44
+ Help them brainstorm alternative patterns, explain the reasoning behind quality benchmarks, and walk them through refactoring steps.
45
+
46
+ Target File Path: ${filePath}
47
+ File Content:
48
+ \`\`\`
49
+ ${fileContent}
50
+ \`\`\`
51
+ ${complianceSection}`
52
+ }
53
+ ];
54
+ console.log(`\n ${chalk.cyan.bold("◆ Socratic Chat Started")}`);
55
+ console.log(` Discussing: ${chalk.bold(filePath)}`);
56
+ console.log(` Type ${chalk.yellow("exit")} or ${chalk.yellow("quit")} to end the session.\n`);
57
+ while (true) {
58
+ const userInput = await prompts({
59
+ type: "text",
60
+ name: "message",
61
+ message: `${chalk.green("💬 You:")}`,
62
+ });
63
+ if (!userInput.message || userInput.message.trim() === "")
64
+ continue;
65
+ const trimmed = userInput.message.trim();
66
+ if (trimmed.toLowerCase() === "exit" || trimmed.toLowerCase() === "quit") {
67
+ console.log(`\n ${chalk.cyan("→")} Discuss session finished. Happy coding!\n`);
68
+ break;
69
+ }
70
+ messages.push({ role: "user", content: trimmed });
71
+ process.stdout.write(`\n ${chalk.cyan.bold("◆ Pika Sentinel:")}\n `);
72
+ try {
73
+ const response = await openai.chat.completions.create({
74
+ model: config.ai.model,
75
+ messages,
76
+ temperature: 0.2,
77
+ });
78
+ const reply = response.choices[0]?.message?.content || "";
79
+ console.log(reply.split("\n").join("\n "));
80
+ console.log();
81
+ messages.push({ role: "assistant", content: reply });
82
+ }
83
+ catch (e) {
84
+ console.log(`${chalk.red("✖")} Error communicating with AI: ${e.message}\n`);
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,79 @@
1
+ import fs from "fs";
2
+ import chalk from "chalk";
3
+ /**
4
+ * Automates Pull Request inline code-reviews by posting reviews directly to GitHub.
5
+ * Utilizes lightweight native fetch calls to keep dependencies at zero.
6
+ */
7
+ export async function postGitHubPRComments(findings) {
8
+ const token = process.env.GITHUB_TOKEN;
9
+ const eventPath = process.env.GITHUB_EVENT_PATH;
10
+ if (!token || !eventPath) {
11
+ return false;
12
+ }
13
+ try {
14
+ if (!fs.existsSync(eventPath)) {
15
+ console.log(` ${chalk.red("✖")} GITHUB_EVENT_PATH file not found.`);
16
+ return false;
17
+ }
18
+ const event = JSON.parse(fs.readFileSync(eventPath, "utf-8"));
19
+ const prNumber = event.pull_request?.number;
20
+ const repoFullName = event.repository?.full_name; // e.g. "owner/repo"
21
+ if (!prNumber || !repoFullName) {
22
+ console.log(` ${chalk.red("✖")} Missing PR number or Repository Name in GitHub Event.`);
23
+ return false;
24
+ }
25
+ // Get current head commit SHA required for target PR diff annotations
26
+ const commitSha = event.pull_request?.head?.sha;
27
+ if (!commitSha) {
28
+ console.log(` ${chalk.red("✖")} Head Commit SHA is missing.`);
29
+ return false;
30
+ }
31
+ console.log(`\n ${chalk.cyan("→")} Syncing ${findings.length} findings with GitHub PR #${prNumber}...`);
32
+ for (const f of findings) {
33
+ const fileName = f.fileName;
34
+ for (const r of f.reviews) {
35
+ if (!r.line)
36
+ continue;
37
+ const body = `### 🚨 Pika Sentinel: ${r.severity} Finding
38
+ **Finding:** ${r.finding}
39
+ **Impact:** ${r.impact}
40
+
41
+ **Recommendation:**
42
+ \`\`\`
43
+ ${r.recommendation}
44
+ \`\`\`
45
+ `;
46
+ const url = `https://api.github.com/repos/${repoFullName}/pulls/${prNumber}/comments`;
47
+ const payload = {
48
+ body,
49
+ commit_id: commitSha,
50
+ path: fileName,
51
+ side: "RIGHT",
52
+ line: r.line,
53
+ };
54
+ const response = await fetch(url, {
55
+ method: "POST",
56
+ headers: {
57
+ "Authorization": `Bearer ${token}`,
58
+ "Accept": "application/vnd.github.v3+json",
59
+ "Content-Type": "application/json",
60
+ "User-Agent": "Pika-Sentinel-Reviewer"
61
+ },
62
+ body: JSON.stringify(payload),
63
+ });
64
+ if (!response.ok) {
65
+ const errMsg = await response.text();
66
+ console.log(` ${chalk.red("✖")} Failed to post comment on ${fileName}:${r.line}: ${errMsg}`);
67
+ }
68
+ else {
69
+ console.log(` ${chalk.green("✓")} Posted inline review on ${fileName}:${r.line}`);
70
+ }
71
+ }
72
+ }
73
+ return true;
74
+ }
75
+ catch (e) {
76
+ console.log(` ${chalk.red("✖")} Failed to post comments to GitHub PR: ${e.message}`);
77
+ return false;
78
+ }
79
+ }
package/dist/cmd/rules.js CHANGED
@@ -46,16 +46,33 @@ export async function rulesAction() {
46
46
  logger.info("Analyzing codebase structure and dependencies...");
47
47
  // 1. Gather package.json context
48
48
  let projectTechStack = "";
49
+ const frameworkClassifications = [];
49
50
  try {
50
51
  const packageJsonPath = path.join(process.cwd(), "package.json");
51
52
  if (fs.existsSync(packageJsonPath)) {
52
53
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
54
+ const allDeps = { ...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {}) };
55
+ if (allDeps["next"])
56
+ frameworkClassifications.push("Next.js Framework");
57
+ if (allDeps["react"])
58
+ frameworkClassifications.push("React UI Library");
59
+ if (allDeps["express"])
60
+ frameworkClassifications.push("Express.js Server");
61
+ if (allDeps["tailwindcss"])
62
+ frameworkClassifications.push("Tailwind CSS v4 styling");
63
+ if (allDeps["typescript"])
64
+ frameworkClassifications.push("TypeScript Static Typings");
65
+ if (allDeps["bun-types"] || allDeps["@types/bun"])
66
+ frameworkClassifications.push("Bun High-Performance Runtime");
53
67
  const deps = Object.keys(packageJson.dependencies || {}).join(", ");
54
68
  const devDeps = Object.keys(packageJson.devDependencies || {}).join(", ");
55
69
  projectTechStack += `Package Name: ${packageJson.name || "unknown"}\nDependencies: ${deps || "none"}\nDevDependencies: ${devDeps || "none"}\n`;
56
70
  }
57
71
  }
58
72
  catch (e) { }
73
+ if (frameworkClassifications.length > 0) {
74
+ projectTechStack += `Detected Frameworks & Archetypes: ${frameworkClassifications.join(", ")}\n`;
75
+ }
59
76
  // 2. Gather file layout context
60
77
  const files = listProjectFiles();
61
78
  const fileSummary = files.slice(0, 30).map(f => ` - ${f}`).join("\n");
package/dist/cmd/scan.js CHANGED
@@ -3,6 +3,8 @@ import chalk from "chalk";
3
3
  import { execSync } from "child_process";
4
4
  import { runScan, getDiffFiles, listProjectFiles } from "../core/scanner.js";
5
5
  import { logger } from "../utils/logger.js";
6
+ import { applyAutoFix } from "./autofix.js";
7
+ import { postGitHubPRComments } from "./github.js";
6
8
  /**
7
9
  * CLI Command: scan
8
10
  * Executes the scanning logic and handles user interaction for reports.
@@ -34,6 +36,10 @@ export async function scanAction(files, options) {
34
36
  }
35
37
  }
36
38
  const { markdownReports, htmlReport, findings } = await runScan(target, isCI, targets);
39
+ // If in CI and GITHUB_TOKEN is present, automatically post reviews
40
+ if (isCI && process.env.GITHUB_TOKEN && process.env.GITHUB_EVENT_PATH) {
41
+ await postGitHubPRComments(findings);
42
+ }
37
43
  if (!markdownReports || markdownReports.length === 0) {
38
44
  if (!isCI) {
39
45
  console.log(`\n ${chalk.green("✓")} ${chalk.bold("Scan complete. No architectural anomalies or security risks detected.")}`);
@@ -84,6 +90,7 @@ export async function scanAction(files, options) {
84
90
  message: "Choose post-scan action:",
85
91
  choices: [
86
92
  { title: "✨ Open Interactive HTML Dashboard (Recommended)", value: "html" },
93
+ { title: "🔧 Launch Interactive Auto-Fixer", value: "autofix" },
87
94
  { title: "📂 List generated Markdown file paths", value: "markdown" },
88
95
  { title: "🛑 Exit and start refactoring", value: "exit" },
89
96
  ],
@@ -100,6 +107,34 @@ export async function scanAction(files, options) {
100
107
  console.log(` ${chalk.dim(`Manually open: file://${htmlReport}`)}`);
101
108
  }
102
109
  }
110
+ else if (response.action === "autofix") {
111
+ const fixableIssues = [];
112
+ findings.forEach((f) => {
113
+ f.reviews.forEach((r) => {
114
+ if (r.line) {
115
+ fixableIssues.push({
116
+ title: `[${r.severity}] ${f.fileName}:${r.line} - ${r.finding.substring(0, 45)}...`,
117
+ value: { filePath: f.fileName, review: r }
118
+ });
119
+ }
120
+ });
121
+ });
122
+ if (fixableIssues.length === 0) {
123
+ console.log(`\n ${chalk.yellow("⚠")} No fixable issues with valid line numbers found.`);
124
+ }
125
+ else {
126
+ const issueSelect = await prompts({
127
+ type: "select",
128
+ name: "issue",
129
+ message: "Select an architectural issue to auto-patch:",
130
+ choices: fixableIssues,
131
+ });
132
+ if (issueSelect.issue) {
133
+ const { filePath, review } = issueSelect.issue;
134
+ applyAutoFix(filePath, review.line, review.recommendation);
135
+ }
136
+ }
137
+ }
103
138
  else if (response.action === "markdown") {
104
139
  console.log(`\n ${chalk.bold("Generated Markdown Artifacts:")}`);
105
140
  markdownReports.forEach((r) => {
package/dist/core/ai.js CHANGED
@@ -112,10 +112,10 @@ ${fs.readFileSync(rulesPath, "utf-8")}
112
112
 
113
113
  <task>
114
114
  Identify high-impact issues related to:
115
- 1. SYSTEM COMPROMISE: Security vulnerabilities, insecure data handling.
116
- 2. COMPUTATIONAL INEFFICIENCY: Algorithmic bottlenecks, memory leaks.
117
- 3. ARCHITECTURAL DEBT: Logic flaws, redundant complexity.
118
- 4. UI/UX ANOMALIES: Layout shifts, accessibility (a11y) violations, and CSS performance issues.
115
+ 1. SYSTEM COMPROMISE & SECURITY: Vulnerabilities (OWASP Top 10 API/Web standard compliance), hardcoded credentials, private keys, API secrets, or insecure default configurations.
116
+ 2. COMPUTATIONAL INEFFICIENCY: Algorithmic bottlenecks, memory leaks, nested processing.
117
+ 3. ARCHITECTURAL DEBT: Logic flaws, breaking structures, redundant modular complexity.
118
+ 4. UI/UX ANOMALIES: Layout shifts, accessibility (a11y) standard violations, and inefficient CSS paths.
119
119
  </task>
120
120
 
121
121
  <instructions>
@@ -0,0 +1,55 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+ const CACHE_FILE = path.join(".pika-reports", "cache.json");
5
+ /**
6
+ * Computes a unique SHA-256 hash representing the current files scan state and custom rules.
7
+ */
8
+ export function computeScanHash(files, rulesContent) {
9
+ const hash = crypto.createHash("sha256");
10
+ // Sort files by path to ensure consistent hash ordering
11
+ const sortedFiles = [...files].sort((a, b) => a.filePath.localeCompare(b.filePath));
12
+ for (const file of sortedFiles) {
13
+ hash.update(`file:${file.filePath}\ncontent:${file.content}\n`);
14
+ }
15
+ hash.update(`rules:${rulesContent}`);
16
+ return hash.digest("hex");
17
+ }
18
+ /**
19
+ * Retrieves the cached scan finding results if it exists.
20
+ */
21
+ export function getCache(hash) {
22
+ try {
23
+ if (!fs.existsSync(CACHE_FILE))
24
+ return null;
25
+ const data = fs.readFileSync(CACHE_FILE, "utf-8");
26
+ const json = JSON.parse(data);
27
+ return json[hash] || null;
28
+ }
29
+ catch (e) {
30
+ return null;
31
+ }
32
+ }
33
+ /**
34
+ * Caches scan finding results locally inside cache.json.
35
+ */
36
+ export function setCache(hash, payload) {
37
+ try {
38
+ let cache = {};
39
+ if (fs.existsSync(CACHE_FILE)) {
40
+ const data = fs.readFileSync(CACHE_FILE, "utf-8");
41
+ cache = JSON.parse(data);
42
+ }
43
+ else {
44
+ const dir = path.dirname(CACHE_FILE);
45
+ if (!fs.existsSync(dir)) {
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ }
48
+ }
49
+ cache[hash] = payload;
50
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
51
+ }
52
+ catch (e) {
53
+ // Fail silently to prevent cache issues from blocking core review execution
54
+ }
55
+ }
@@ -1,5 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { calculateGrade, getGradeColor, generateBadge } from "./stats.js";
3
4
  /**
4
5
  * Helper to get a sanitized project name.
5
6
  */
@@ -80,6 +81,9 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
80
81
  const projectName = getProjectName();
81
82
  const timestamp = new Date().toLocaleString();
82
83
  const data = JSON.stringify(allFindings);
84
+ const grade = calculateGrade(allFindings);
85
+ const gradeColor = getGradeColor(grade);
86
+ generateBadge(grade);
83
87
  const htmlContent = `
84
88
  <!DOCTYPE html>
85
89
  <html lang="en">
@@ -502,7 +506,7 @@ export function writeHTMLReport(sessionDir, totalFiles, allFindings) {
502
506
  <polygon points="60,52 54,50 56,56" fill="#00F0FF" />
503
507
  </svg>
504
508
  <div class="brand-text">
505
- <h1>Pika <span>Sentinel</span></h1>
509
+ <h1 style="display: flex; align-items: center; gap: 8px;">Pika <span>Sentinel</span> <span style="font-size: 0.8rem; background: ${gradeColor}; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: 800; text-shadow: none;">${grade}</span></h1>
506
510
  <p>${projectName} • ${timestamp}</p>
507
511
  </div>
508
512
  </div>
@@ -9,6 +9,7 @@ import { getIgnoredFiles } from "../utils/config.js";
9
9
  import { logger } from "../utils/logger.js";
10
10
  import path from "path";
11
11
  import { validateTokenLimit } from "../utils/token.js";
12
+ import { computeScanHash, getCache, setCache } from "./cache.js";
12
13
  /**
13
14
  * Project Discovery: List all relevant files for interactive selection.
14
15
  */
@@ -91,6 +92,35 @@ export async function runScan(target, isCI, specificFiles) {
91
92
  findings: []
92
93
  };
93
94
  }
95
+ // Load custom architecture rules content
96
+ let rulesContent = "";
97
+ try {
98
+ const rulesPath = path.join(process.cwd(), ".pika-rules.md");
99
+ if (fs.existsSync(rulesPath)) {
100
+ rulesContent = fs.readFileSync(rulesPath, "utf-8");
101
+ }
102
+ }
103
+ catch (e) { }
104
+ // Gather target file content profiles
105
+ const fileStates = [];
106
+ for (const file of files) {
107
+ try {
108
+ if (fs.existsSync(file)) {
109
+ const content = fs.readFileSync(file, "utf-8");
110
+ fileStates.push({ filePath: file, content });
111
+ }
112
+ }
113
+ catch (e) { }
114
+ }
115
+ // Compute semantic hash and check database
116
+ const scanHash = computeScanHash(fileStates, rulesContent);
117
+ const cachedResult = getCache(scanHash);
118
+ if (cachedResult) {
119
+ if (!isCI) {
120
+ console.log(`\n ${chalk.cyan("✓")} ${chalk.bold("Instant Cache Hit: Loading results from local cache database (0ms).")}\n`);
121
+ }
122
+ return cachedResult;
123
+ }
94
124
  const reportDir = setupReportDir();
95
125
  if (!isCI)
96
126
  logger.info(`Initializing Pika Review on ${files.length} file(s)...`);
@@ -197,9 +227,11 @@ export async function runScan(target, isCI, specificFiles) {
197
227
  if (!isCI) {
198
228
  logger.success("\nScan complete!");
199
229
  }
200
- return {
230
+ const payload = {
201
231
  markdownReports: generatedReports,
202
232
  htmlReport: htmlPath,
203
233
  findings: allFindings
204
234
  };
235
+ setCache(scanHash, payload);
236
+ return payload;
205
237
  }
@@ -0,0 +1,81 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ const BADGE_FILE = path.join(process.cwd(), "badge.svg");
4
+ /**
5
+ * Computes the architectural grade based on the severities of identified findings.
6
+ */
7
+ export function calculateGrade(findings) {
8
+ let criticalCount = 0;
9
+ let highCount = 0;
10
+ let mediumCount = 0;
11
+ findings.forEach((f) => {
12
+ f.reviews.forEach((r) => {
13
+ const severity = r.severity;
14
+ if (severity === "Critical")
15
+ criticalCount++;
16
+ else if (severity === "High")
17
+ highCount++;
18
+ else if (severity === "Medium")
19
+ mediumCount++;
20
+ });
21
+ });
22
+ if (criticalCount >= 2)
23
+ return "F";
24
+ if (criticalCount === 1)
25
+ return "C";
26
+ if (highCount >= 3)
27
+ return "C";
28
+ if (highCount >= 1 || mediumCount >= 4)
29
+ return "B";
30
+ if (mediumCount >= 1)
31
+ return "A";
32
+ return "A+";
33
+ }
34
+ /**
35
+ * Retrieves the HSL color code corresponding to a Sentinel Grade.
36
+ */
37
+ export function getGradeColor(grade) {
38
+ switch (grade) {
39
+ case "A+": return "#00F0FF"; // Neon Cyan
40
+ case "A": return "#10B981"; // Emerald Green
41
+ case "B": return "#3B82F6"; // Royal Blue
42
+ case "C": return "#F59E0B"; // Amber Yellow
43
+ case "F": return "#EF4444"; // Ruby Red
44
+ }
45
+ }
46
+ /**
47
+ * Renders and saves a beautiful, high-quality, scalable SVG badge displaying the current Pika grade.
48
+ */
49
+ export function generateBadge(grade) {
50
+ const color = getGradeColor(grade);
51
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="140" height="20" role="img" aria-label="Pika Sentinel: Grade ${grade}">
52
+ <linearGradient id="s" x2="0" y2="100%">
53
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
54
+ <stop offset="1" stop-opacity=".1"/>
55
+ </linearGradient>
56
+ <clipPath id="r">
57
+ <rect width="140" height="20" rx="3" fill="#fff"/>
58
+ </clipPath>
59
+ <g clip-path="url(#r)">
60
+ <rect width="90" height="20" fill="#2d3748"/>
61
+ <rect x="90" width="50" height="20" fill="${color}"/>
62
+ <rect width="140" height="20" fill="url(#s)"/>
63
+ </g>
64
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,sans-serif" font-size="110">
65
+ <text aria-hidden="true" x="460" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="700">Pika Sentinel</text>
66
+ <text x="460" y="140" transform="scale(.1)" fill="#fff" textLength="700">Pika Sentinel</text>
67
+ <text aria-hidden="true" x="1150" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="300">${grade}</text>
68
+ <text x="1150" y="140" transform="scale(.1)" fill="#fff" textLength="300">${grade}</text>
69
+ </g>
70
+ </svg>`;
71
+ try {
72
+ const dir = path.dirname(BADGE_FILE);
73
+ if (!fs.existsSync(dir)) {
74
+ fs.mkdirSync(dir, { recursive: true });
75
+ }
76
+ fs.writeFileSync(BADGE_FILE, svg, "utf-8");
77
+ }
78
+ catch (e) {
79
+ // Fail silently to keep stats errors isolated from blocking CLI scan operations
80
+ }
81
+ }
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import chalk from "chalk";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
4
7
  import { initAction } from "./cmd/init.js";
5
8
  import { scanAction } from "./cmd/scan.js";
6
9
  import { viewAction } from "./cmd/view.js";
@@ -8,15 +11,29 @@ import { statsAction } from "./cmd/stats.js";
8
11
  import { modelsAction } from "./cmd/models.js";
9
12
  import { hookAction } from "./cmd/hook.js";
10
13
  import { rulesAction } from "./cmd/rules.js";
14
+ import { discussAction } from "./cmd/chat.js";
11
15
  import { logger } from "./utils/logger.js";
12
16
  /**
13
17
  * Pika Review: The Enterprise-Grade AI Code Reviewer.
14
18
  * Modular entry point for CLI operations.
15
19
  */
20
+ // Get package.json version dynamically
21
+ let version = "2.3.0";
22
+ try {
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = path.dirname(__filename);
25
+ const pkgPath = path.join(__dirname, "../package.json");
26
+ if (fs.existsSync(pkgPath)) {
27
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
28
+ if (pkg.version)
29
+ version = pkg.version;
30
+ }
31
+ }
32
+ catch (e) { }
16
33
  const program = new Command();
17
34
  // Branding Header
18
35
  const BRAND = `
19
- ${chalk.cyan.bold("◆ Pika Sentinel")} ${chalk.dim(`v2.2.0 (Enterprise)`)}
36
+ ${chalk.cyan.bold("◆ Pika Sentinel")} ${chalk.dim(`v${version} (Enterprise)`)}
20
37
  ${chalk.dim("─".repeat(42))}
21
38
  ${chalk.italic.gray("AI Architectural & Security Safeguard")}
22
39
  `;
@@ -27,9 +44,10 @@ const HELPER_TEXT = `
27
44
  -i, --interactive Interactively pick files to scan
28
45
  --ci Fail CI pipeline if critical/high issues are found
29
46
  ${chalk.bold("view")} Open the latest interactive HTML report in browser
47
+ ${chalk.bold("discuss [file]")} Launch an interactive Socratic chat session inside the console
30
48
  ${chalk.bold("stats")} Print scan history & key quality trends
31
49
  ${chalk.bold("models")} Interactively select Ollama models for offline audit
32
- ${chalk.bold("hook")} Install Git pre-commit scanner safeguard hook
50
+ ${chalk.bold("hook")} Install Git pre-commit safeguard hook
33
51
  ${chalk.bold("rules")} AI-generate architectural '.pika-rules.md'
34
52
 
35
53
  ${chalk.cyan.bold("Options & Advanced:")}
@@ -40,7 +58,7 @@ const HELPER_TEXT = `
40
58
  program
41
59
  .name("pika-review")
42
60
  .description("Enterprise-grade AI Architectural Code Reviewer")
43
- .version("2.2.0")
61
+ .version(version)
44
62
  .addHelpText("before", BRAND)
45
63
  .addHelpText("after", HELPER_TEXT);
46
64
  program
@@ -96,6 +114,13 @@ program
96
114
  logger.info("Use 'pika-review rules --generate' to auto-generate architecture rules.");
97
115
  }
98
116
  });
117
+ program
118
+ .command("discuss <file>")
119
+ .description("Launch an interactive Socratic chat session inside the console focusing on design context")
120
+ .action(async (file) => {
121
+ console.log(BRAND);
122
+ await discussAction(file);
123
+ });
99
124
  program
100
125
  .command("scan [files...]", { isDefault: true }) // Set scan as the default command
101
126
  .description("Scan git changes or specific files for architectural anomalies")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pika-review",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Enterprise-grade AI Architectural Code Reviewer.",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",