Skip to content

Commit 6120283

Browse files
authored
Merge pull request #10 from ptkvaibhav/v5
2 parents 0bb12b6 + 508cd1c commit 6120283

21 files changed

Lines changed: 1157 additions & 264 deletions

.gitignore

-38 Bytes
Binary file not shown.

nyx.config.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,11 @@
7070
"quotaWarningPercent": 85,
7171
"staleFileDays": 730,
7272
"pricingRefreshDays": 14
73-
}
74-
}
73+
},
74+
"ai": {
75+
"model": "gemma4:latest"
76+
},
77+
"pdfPasswords": [
78+
"PRAT1997"
79+
]
80+
}

src/cli.js

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { createMockProviderSnapshots } from "./providers/mock-snapshots.js";
2121
import { explainPricingStrategy } from "./advisory/pricing-catalog.js";
2222
import { Catalog } from "./core/catalog.js";
2323
import { startServer } from "./server.js";
24+
import { setupOllama } from "./core/ollama-manager.js";
2425

2526
const [, , command, ...args] = process.argv;
2627
const DEFAULT_DB_PATH = ".nyx/nyx.db";
@@ -405,23 +406,13 @@ async function runUi(args) {
405406
const port = parseInt(args[0] ?? "3030", 10);
406407
const dbPath = args[1] ?? DEFAULT_DB_PATH;
407408

408-
const uiDistPath = path.resolve("ui", "dist", "index.html");
409-
if (!(await exists(uiDistPath))) {
410-
console.log("First time setup: Building the Nyx Dashboard UI...");
411-
try {
412-
const { execSync } = await import("node:child_process");
413-
console.log("Installing UI dependencies...");
414-
execSync("npm install", { cwd: path.resolve("ui"), stdio: "inherit", shell: true });
415-
console.log("Building UI bundle...");
416-
execSync("npm run build", { cwd: path.resolve("ui"), stdio: "inherit", shell: true });
417-
console.log("UI build complete.");
418-
} catch (error) {
419-
console.error("Failed to build the UI automatically:", error.message);
420-
console.log("Please cd into the 'ui' directory and run 'npm install && npm run build' manually.");
421-
process.exitCode = 1;
422-
return;
423-
}
424-
}
409+
// Resolve the actual project root directory where the "ui" folder lives
410+
// import.meta.url is file:///C:/path/to/nyx/src/cli.js
411+
const __dirname = path.dirname(new URL(import.meta.url).pathname).replace(/^\/([A-Za-z]:)/, '$1');
412+
const projectRoot = path.resolve(__dirname, "..");
413+
const uiDistPath = path.join(projectRoot, "ui", "dist", "index.html");
414+
415+
await setupOllama();
425416

426417
await startServer({ port, dbPath });
427418
try {

src/core/ai.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
33
import process from "node:process";
4+
import { loadConfig } from "./config.js";
45

56
const OLLAMA_URL = "http://localhost:11434/api/chat";
6-
const OLLAMA_MODEL = "gemma"; // Assuming the user pulled 'gemma' or 'gemma:4b' or 'gemma:7b'. We will use 'gemma' as the default tag.
7+
let OLLAMA_MODEL = "gemma";
78

89
export async function initAI(mock = false) {
910
if (mock) {
@@ -12,6 +13,11 @@ export async function initAI(mock = false) {
1213
}
1314

1415
try {
16+
const { config } = await loadConfig();
17+
if (config.ai?.model) {
18+
OLLAMA_MODEL = config.ai.model;
19+
}
20+
1521
// Ping Ollama to see if it's running
1622
const response = await fetch("http://localhost:11434/api/tags");
1723
if (!response.ok) {

src/core/config.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import { readFile } from "node:fs/promises";
1+
import { readFile, writeFile } from "node:fs/promises";
22
import path from "node:path";
33

44
export async function loadConfig(configPath = "nyx.config.json") {
55
const resolvedPath = path.resolve(configPath);
6-
const raw = await readFile(resolvedPath, "utf8");
7-
const config = JSON.parse(raw);
6+
let config = { ai: { model: "gemma" }, pdfPasswords: [], watchedDirectories: [] };
7+
8+
try {
9+
const raw = await readFile(resolvedPath, "utf8");
10+
config = JSON.parse(raw);
11+
} catch (error) {
12+
if (error.code !== 'ENOENT') {
13+
throw error;
14+
}
15+
}
816

917
return {
1018
configPath: resolvedPath,
@@ -13,6 +21,11 @@ export async function loadConfig(configPath = "nyx.config.json") {
1321
};
1422
}
1523

24+
export async function saveConfig(config, configPath = "nyx.config.json") {
25+
const resolvedPath = path.resolve(configPath);
26+
await writeFile(resolvedPath, JSON.stringify(config, null, 2), "utf8");
27+
}
28+
1629
export function resolveWatchedRoot(baseDirectory, watchedRoot) {
1730
return path.resolve(baseDirectory, watchedRoot.path);
1831
}

src/core/content-extractor.js

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
3+
import { loadConfig } from "./config.js";
4+
5+
async function parsePdfSilently(pdf, buffer, options = {}) {
6+
const originalWarn = console.warn;
7+
console.warn = () => {}; // Suppress noise
8+
try {
9+
return await pdf(buffer, options);
10+
} finally {
11+
console.warn = originalWarn; // Restore
12+
}
13+
}
314

415
export async function extractContent(filePath) {
516
const ext = path.extname(filePath).toLowerCase();
@@ -8,10 +19,43 @@ export async function extractContent(filePath) {
819
try {
920
const pdf = (await import("pdf-parse")).default;
1021
const dataBuffer = await fs.readFile(filePath);
11-
const data = await pdf(dataBuffer);
12-
return data.text || "";
22+
23+
const { config } = await loadConfig();
24+
const passwords = config.pdfPasswords || [];
25+
26+
let data = null;
27+
let lastError = null;
28+
29+
// Try without password
30+
try {
31+
data = await parsePdfSilently(pdf, dataBuffer);
32+
} catch (err) {
33+
lastError = err;
34+
}
35+
36+
// If failed due to password
37+
if (lastError && (lastError.message.toLowerCase().includes("password") || lastError.name === "PasswordException")) {
38+
let success = false;
39+
for (const pwd of passwords) {
40+
try {
41+
data = await parsePdfSilently(pdf, dataBuffer, { password: pwd });
42+
success = true;
43+
break;
44+
} catch (e) {
45+
// Ignore
46+
}
47+
}
48+
if (!success) {
49+
return "[[PASSWORD_REQUIRED]]";
50+
}
51+
} else if (lastError) {
52+
// console.warn(`Failed to extract text from PDF: ${filePath}`, lastError.message);
53+
return "";
54+
}
55+
56+
return data ? (data.text || "") : "";
1357
} catch (err) {
14-
console.warn(`Failed to extract text from PDF: ${filePath}`, err.message);
58+
// console.warn(`Failed to extract text from PDF: ${filePath}`, err.message);
1559
return "";
1660
}
1761
}

src/core/ollama-manager.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { execSync, spawn } from "node:child_process";
2+
import readline from "node:readline";
3+
import process from "node:process";
4+
import { loadConfig, saveConfig } from "./config.js";
5+
6+
const MODELS = [
7+
{ id: "gemma", name: "Gemma (Standard, Great balance of speed and intelligence)" },
8+
{ id: "gemma:2b", name: "Gemma 2B (Fastest, Good for Mobile/Laptops)" },
9+
{ id: "llama3:8b", name: "Llama 3 8B (Smarter, Needs 8GB+ RAM)" },
10+
{ id: "phi3:mini", name: "Phi 3 Mini (Great reasoning, Lightweight)" }
11+
];
12+
13+
export async function setupOllama() {
14+
console.log("Checking AI engine prerequisites...");
15+
16+
if (!isOllamaInstalled()) {
17+
console.log("Ollama is not installed. Installing Ollama...");
18+
installOllama();
19+
}
20+
21+
await ensureOllamaRunning();
22+
23+
const installedModels = await getInstalledModels();
24+
const { config, configPath } = await loadConfig();
25+
26+
let selectedModel = config.ai?.model;
27+
28+
if (!selectedModel) {
29+
selectedModel = await promptModelSelection(installedModels);
30+
config.ai = { ...config.ai, model: selectedModel };
31+
await saveConfig(config, configPath);
32+
}
33+
34+
// Resolve to actual installed model if it's a close match to avoid re-downloading
35+
const exactMatch = installedModels.find(m => m === selectedModel || m === `${selectedModel}:latest`);
36+
const partialMatch = installedModels.find(m => m.includes(selectedModel));
37+
38+
if (exactMatch || partialMatch) {
39+
const finalModel = exactMatch || partialMatch;
40+
if (finalModel !== selectedModel) {
41+
console.log(`Auto-resolved '${selectedModel}' to installed model '${finalModel}'.`);
42+
selectedModel = finalModel;
43+
config.ai.model = finalModel;
44+
await saveConfig(config, configPath);
45+
}
46+
} else {
47+
console.log(`Model '${selectedModel}' not found locally. Downloading...`);
48+
pullModel(selectedModel);
49+
}
50+
51+
console.log(`AI engine ready using model: ${selectedModel}`);
52+
return selectedModel;
53+
}
54+
55+
async function getInstalledModels() {
56+
try {
57+
const response = await fetch("http://localhost:11434/api/tags");
58+
if (response.ok) {
59+
const data = await response.json();
60+
return data.models.map(m => m.name);
61+
}
62+
} catch {}
63+
return [];
64+
}
65+
66+
function isOllamaInstalled() {
67+
try {
68+
execSync("ollama --version", { stdio: "ignore" });
69+
return true;
70+
} catch {
71+
return false;
72+
}
73+
}
74+
75+
function installOllama() {
76+
try {
77+
if (process.platform === "win32") {
78+
execSync("winget install Ollama.Ollama -e --silent", { stdio: "inherit" });
79+
} else if (process.platform === "darwin") {
80+
execSync("brew install ollama", { stdio: "inherit" });
81+
} else {
82+
execSync("curl -fsSL https://ollama.com/install.sh | sh", { stdio: "inherit" });
83+
}
84+
} catch (error) {
85+
console.error("Failed to automatically install Ollama. Please install it manually from https://ollama.com");
86+
process.exit(1);
87+
}
88+
}
89+
90+
async function ensureOllamaRunning() {
91+
try {
92+
await fetch("http://localhost:11434/api/tags");
93+
} catch {
94+
console.log("Starting Ollama background service...");
95+
const subprocess = spawn("ollama", ["serve"], {
96+
detached: true,
97+
stdio: "ignore",
98+
windowsHide: true
99+
});
100+
subprocess.unref();
101+
102+
// Wait for it to start
103+
for (let i = 0; i < 10; i++) {
104+
await new Promise(r => setTimeout(r, 1000));
105+
try {
106+
await fetch("http://localhost:11434/api/tags");
107+
return;
108+
} catch {}
109+
}
110+
console.warn("Ollama service might not have started correctly, but we will continue.");
111+
}
112+
}
113+
114+
async function promptModelSelection() {
115+
const rl = readline.createInterface({
116+
input: process.stdin,
117+
output: process.stdout
118+
});
119+
120+
console.log("\n=========================================");
121+
console.log(" Select an AI Model for Nyx ");
122+
console.log("=========================================");
123+
MODELS.forEach((m, i) => {
124+
console.log(` ${i + 1}) ${m.name} (${m.id})`);
125+
});
126+
console.log("=========================================\n");
127+
128+
return new Promise((resolve) => {
129+
const ask = () => {
130+
rl.question("Enter the number of your choice (default 1): ", (answer) => {
131+
const choice = parseInt(answer.trim(), 10);
132+
if (!answer.trim() || choice === 1) {
133+
rl.close();
134+
resolve(MODELS[0].id);
135+
} else if (choice > 1 && choice <= MODELS.length) {
136+
rl.close();
137+
resolve(MODELS[choice - 1].id);
138+
} else {
139+
console.log("Invalid choice. Please try again.");
140+
ask();
141+
}
142+
});
143+
};
144+
ask();
145+
});
146+
}
147+
148+
function pullModel(model) {
149+
try {
150+
execSync(`ollama pull ${model}`, { stdio: "inherit" });
151+
} catch (error) {
152+
console.error(`Failed to pull model ${model}.`, error.message);
153+
}
154+
}

src/organization/local-audit.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import { extractContent } from "../core/content-extractor.js";
1212

1313
export async function buildLocalAudit({
1414
engagementPath = "docs/engagement.md",
15-
dbPath = ".nyx/nyx.db"
15+
dbPath = ".nyx/nyx.db",
16+
onProgress,
17+
skippedFiles = []
1618
} = {}) {
1719
const engagement = await loadEngagement(engagementPath);
1820
const scanResult = await scanManagedDirectories({
@@ -24,7 +26,13 @@ export async function buildLocalAudit({
2426
const files = [];
2527
const scannedPaths = [];
2628

29+
let current = 0;
30+
const total = scanResult.files.length;
31+
2732
for (const scannedFile of scanResult.files) {
33+
current++;
34+
if (onProgress) onProgress(current, total, scannedFile.absolutePath);
35+
2836
scannedPaths.push(scannedFile.absolutePath);
2937

3038
// Check if we already have this file and if it has changed
@@ -37,7 +45,21 @@ export async function buildLocalAudit({
3745
const profile = await fingerprintFile(scannedFile.absolutePath);
3846

3947
// Extract content for deep intelligence (PDFs, txt, etc.)
40-
profile.extractedText = await extractContent(scannedFile.absolutePath);
48+
if (skippedFiles.includes(scannedFile.absolutePath)) {
49+
profile.extractedText = "";
50+
} else {
51+
profile.extractedText = await extractContent(scannedFile.absolutePath);
52+
}
53+
const passwordRequired = profile.extractedText === "[[PASSWORD_REQUIRED]]";
54+
55+
if (passwordRequired) {
56+
// SAVE PROGRESS: Upsert the files we have processed so far so we don't scan them again
57+
catalog.upsertFiles(files.filter(f => !f.lastScannedAt));
58+
return {
59+
needsPassword: true,
60+
passwordFile: scannedFile.absolutePath
61+
};
62+
}
4163

4264
const classification = classifyFile(profile);
4365
const structure = analyzeFileStructure({
@@ -50,7 +72,8 @@ export async function buildLocalAudit({
5072
...scannedFile,
5173
...profile,
5274
classification,
53-
structure
75+
structure,
76+
passwordRequired
5477
};
5578

5679
files.push(fileRecord);

0 commit comments

Comments
 (0)