pika-review 2.1.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
 
@@ -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,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
+ }
@@ -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,131 @@
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
+ const frameworkClassifications = [];
50
+ try {
51
+ const packageJsonPath = path.join(process.cwd(), "package.json");
52
+ if (fs.existsSync(packageJsonPath)) {
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");
67
+ const deps = Object.keys(packageJson.dependencies || {}).join(", ");
68
+ const devDeps = Object.keys(packageJson.devDependencies || {}).join(", ");
69
+ projectTechStack += `Package Name: ${packageJson.name || "unknown"}\nDependencies: ${deps || "none"}\nDevDependencies: ${devDeps || "none"}\n`;
70
+ }
71
+ }
72
+ catch (e) { }
73
+ if (frameworkClassifications.length > 0) {
74
+ projectTechStack += `Detected Frameworks & Archetypes: ${frameworkClassifications.join(", ")}\n`;
75
+ }
76
+ // 2. Gather file layout context
77
+ const files = listProjectFiles();
78
+ const fileSummary = files.slice(0, 30).map(f => ` - ${f}`).join("\n");
79
+ const projectStructure = `Detected files (subset):\n${fileSummary}\nTotal files discovered: ${files.length}\n`;
80
+ logger.info(`Synthesizing tailored rules using AI model (${chalk.green(config.ai.model)})...`);
81
+ const prompt = `You are an Elite Senior Software Architect and Compliance Officer.
82
+ 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:
83
+
84
+ <project_tech_stack>
85
+ ${projectTechStack || "Generic web/software codebase"}
86
+ </project_tech_stack>
87
+
88
+ <project_structure>
89
+ ${projectStructure}
90
+ </project_structure>
91
+
92
+ Instructions:
93
+ - Write the output in clean, professional Markdown format.
94
+ - 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).
95
+ - Provide a brief justification for each rule.
96
+ - Do NOT output any preamble, greeting, markdown backticks wrapper, or postscript.
97
+ - Start directly with "# Pika Architectural Rules" and start rules lists immediately.`;
98
+ const baseURL = config.ai.baseURL && config.ai.baseURL.trim()
99
+ ? config.ai.baseURL
100
+ : (isOllama
101
+ ? "http://localhost:11434/v1"
102
+ : `https://api.cloudflare.com/client/v4/accounts/${config.ai.accountId}/ai/v1`);
103
+ const openai = new OpenAI({
104
+ apiKey: config.ai.apiKey || "ollama",
105
+ baseURL,
106
+ });
107
+ try {
108
+ const response = await openai.chat.completions.create({
109
+ model: config.ai.model,
110
+ messages: [{ role: "user", content: prompt }],
111
+ temperature: 0.3,
112
+ max_tokens: 1500,
113
+ });
114
+ let rawMarkdown = response.choices[0]?.message?.content || "";
115
+ // Clean code block ticks if LLM ignored instructions
116
+ rawMarkdown = rawMarkdown
117
+ .replace(/```markdown/g, "")
118
+ .replace(/```/g, "")
119
+ .trim();
120
+ if (!rawMarkdown.startsWith("#")) {
121
+ rawMarkdown = `# Pika Architectural Rules\n\n${rawMarkdown}`;
122
+ }
123
+ fs.writeFileSync(rulesPath, rawMarkdown, "utf-8");
124
+ logger.success(".pika-rules.md generated successfully!");
125
+ logger.info(`Location: ${chalk.dim(rulesPath)}`);
126
+ logger.dim("The local Pika Review engine will now enforce these rules during future scans!");
127
+ }
128
+ catch (error) {
129
+ logger.error(`Failed to generate rules: ${error.message}`);
130
+ }
131
+ }