nuvanta-context-guard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/budget/index.js +11 -0
- package/dist/index.js +56 -0
- package/dist/output/index.js +21 -0
- package/dist/relevance/index.js +45 -0
- package/dist/scanner/index.js +53 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Nuvanta Context Guard
|
|
2
|
+
|
|
3
|
+
A developer tool that scans a software project, identifies files relevant to a task, and produces a context report for AI coding agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g nuvanta-context-guard
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
nuvanta scan <path> --task <task> [--budget <tokens>]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Examples
|
|
18
|
+
|
|
19
|
+
Scan a project for files relevant to fixing a login bug:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
nuvanta scan ./my-project --task "fix login button"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Scan with a token budget:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
nuvanta scan ./my-project --task "fix login button" --budget 5000
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Example Output
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Nuvanta Context Guard
|
|
35
|
+
─────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
Task: fix login button
|
|
38
|
+
Keywords: login, button
|
|
39
|
+
Budget: 5000 tokens
|
|
40
|
+
|
|
41
|
+
Files found: 24
|
|
42
|
+
Relevant: 5
|
|
43
|
+
Selected: 4
|
|
44
|
+
Context used: 3,842 / 5,000 tokens
|
|
45
|
+
Reduction: ~83%
|
|
46
|
+
|
|
47
|
+
Selected files:
|
|
48
|
+
src\auth\login.ts 820 tokens score 1.6
|
|
49
|
+
src\components\LoginButton.tsx 612 tokens score 1.0
|
|
50
|
+
src\api\auth.ts 445 tokens score 0.5
|
|
51
|
+
src\index.ts 392 tokens score 0.5
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## How It Works
|
|
55
|
+
|
|
56
|
+
1. **Scanner** — recursively discovers all files in the project
|
|
57
|
+
2. **Relevance Engine** — scores each file based on filename, directory, and content
|
|
58
|
+
3. **Budget Selector** — picks the most relevant files within the token limit
|
|
59
|
+
4. **Output** — prints a clean report
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { scanDirectory } from "./scanner/index.js";
|
|
4
|
+
import { extractKeywords, scoreFile, scoreContent } from "./relevance/index.js";
|
|
5
|
+
import { selectFiles } from "./budget/index.js";
|
|
6
|
+
import { printReport } from "./output/index.js";
|
|
7
|
+
import fs from "fs/promises";
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program
|
|
10
|
+
.name("nuvanta")
|
|
11
|
+
.description("AI context management for developer projects")
|
|
12
|
+
.version("0.1.0");
|
|
13
|
+
program
|
|
14
|
+
.command("scan")
|
|
15
|
+
.description("Scan a project and find relevant files for a task")
|
|
16
|
+
.argument("<path>", "Path to the project directory")
|
|
17
|
+
.option("--task <task>", "The task you are working on")
|
|
18
|
+
.option("--budget <number>", "Max token budget", "10000")
|
|
19
|
+
.action(async (dirPath, options) => {
|
|
20
|
+
try {
|
|
21
|
+
const budget = parseInt(options.budget);
|
|
22
|
+
const files = await scanDirectory(dirPath);
|
|
23
|
+
const keywords = extractKeywords(options.task);
|
|
24
|
+
const scored = [];
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const content = await fs.readFile(file.path, "utf-8");
|
|
27
|
+
const score = scoreFile(file.path, keywords) + scoreContent(content, keywords);
|
|
28
|
+
if (score > 0) {
|
|
29
|
+
scored.push({ ...file, score });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
scored.sort((a, b) => b.score - a.score);
|
|
33
|
+
const selected = selectFiles(scored, budget);
|
|
34
|
+
printReport({
|
|
35
|
+
task: options.task,
|
|
36
|
+
keywords,
|
|
37
|
+
budget,
|
|
38
|
+
totalFiles: files.length,
|
|
39
|
+
relevantFiles: scored.length,
|
|
40
|
+
selected,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
45
|
+
console.error(`\nError: Folder "${dirPath}" does not exist.\n`);
|
|
46
|
+
}
|
|
47
|
+
else if (error instanceof Error) {
|
|
48
|
+
console.error(`\nError: ${error.message}\n`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.error("\nUnknown error.\n");
|
|
52
|
+
}
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
program.parse();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function printReport(data) {
|
|
2
|
+
const totalTokens = data.selected.reduce((sum, f) => sum + f.tokens, 0);
|
|
3
|
+
const reduction = Math.round((1 - data.selected.length / data.totalFiles) * 100);
|
|
4
|
+
console.log("\nNuvanta Context Guard");
|
|
5
|
+
console.log("─────────────────────────────────────\n");
|
|
6
|
+
console.log(`Task: ${data.task}`);
|
|
7
|
+
console.log(`Keywords: ${data.keywords.join(", ")}`);
|
|
8
|
+
console.log(`Budget: ${data.budget} tokens\n`);
|
|
9
|
+
console.log(`Files found: ${data.totalFiles}`);
|
|
10
|
+
console.log(`Relevant: ${data.relevantFiles}`);
|
|
11
|
+
console.log(`Selected: ${data.selected.length}`);
|
|
12
|
+
console.log(`Context used: ${totalTokens} / ${data.budget} tokens`);
|
|
13
|
+
console.log(`Reduction: ~${reduction}%\n`);
|
|
14
|
+
console.log("Selected files:");
|
|
15
|
+
for (const file of data.selected) {
|
|
16
|
+
const tokens = `${file.tokens} tokens`.padStart(12);
|
|
17
|
+
const score = `score ${file.score}`.padEnd(10);
|
|
18
|
+
console.log(` ${file.path.padEnd(35)} ${tokens} ${score}`);
|
|
19
|
+
}
|
|
20
|
+
console.log("");
|
|
21
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
const STOP_WORDS = new Set([
|
|
3
|
+
"fix",
|
|
4
|
+
"add",
|
|
5
|
+
"update",
|
|
6
|
+
"change",
|
|
7
|
+
"make",
|
|
8
|
+
"the",
|
|
9
|
+
"a",
|
|
10
|
+
"an",
|
|
11
|
+
"is",
|
|
12
|
+
"in",
|
|
13
|
+
"for",
|
|
14
|
+
"to",
|
|
15
|
+
]);
|
|
16
|
+
export function extractKeywords(task) {
|
|
17
|
+
return task
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.split(" ")
|
|
20
|
+
.filter((word) => !STOP_WORDS.has(word));
|
|
21
|
+
}
|
|
22
|
+
export function scoreFile(filePath, keywords) {
|
|
23
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
24
|
+
const dirName = path.dirname(filePath).toLowerCase();
|
|
25
|
+
let score = 0;
|
|
26
|
+
for (const keyword of keywords) {
|
|
27
|
+
if (fileName.includes(keyword)) {
|
|
28
|
+
score += 1.0;
|
|
29
|
+
}
|
|
30
|
+
if (dirName.includes(keyword)) {
|
|
31
|
+
score += 0.6;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return score;
|
|
35
|
+
}
|
|
36
|
+
export function scoreContent(content, keywords) {
|
|
37
|
+
const normalizedContent = content.toLowerCase();
|
|
38
|
+
let score = 0;
|
|
39
|
+
for (const keyword of keywords) {
|
|
40
|
+
if (normalizedContent.includes(keyword)) {
|
|
41
|
+
score += 0.5;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return score;
|
|
45
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const IGNORED_DIRS = new Set([
|
|
4
|
+
"node_modules",
|
|
5
|
+
".git",
|
|
6
|
+
"dist",
|
|
7
|
+
".next",
|
|
8
|
+
"build",
|
|
9
|
+
"coverage",
|
|
10
|
+
".cache",
|
|
11
|
+
]);
|
|
12
|
+
const IGNORED_EXTENSIONS = new Set([
|
|
13
|
+
".md",
|
|
14
|
+
".lock",
|
|
15
|
+
".env",
|
|
16
|
+
".log",
|
|
17
|
+
".png",
|
|
18
|
+
".jpg",
|
|
19
|
+
".jpeg",
|
|
20
|
+
".gif",
|
|
21
|
+
".svg",
|
|
22
|
+
".ico",
|
|
23
|
+
".woff",
|
|
24
|
+
".woff2",
|
|
25
|
+
".ttf",
|
|
26
|
+
]);
|
|
27
|
+
export async function scanDirectory(dirPath) {
|
|
28
|
+
const result = [];
|
|
29
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
if (IGNORED_DIRS.has(entry.name))
|
|
34
|
+
continue;
|
|
35
|
+
const subFiles = await scanDirectory(fullPath);
|
|
36
|
+
result.push(...subFiles);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const ext = path.extname(entry.name);
|
|
40
|
+
if (IGNORED_EXTENSIONS.has(ext))
|
|
41
|
+
continue;
|
|
42
|
+
const stat = await fs.stat(fullPath);
|
|
43
|
+
result.push({
|
|
44
|
+
path: fullPath,
|
|
45
|
+
name: entry.name,
|
|
46
|
+
extension: path.extname(entry.name),
|
|
47
|
+
size: stat.size,
|
|
48
|
+
tokens: Math.round(stat.size / 4),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuvanta-context-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A developer tool that identifies relevant project files for AI coding agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nuvanta": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "node dist/index.js",
|
|
12
|
+
"test": "vitest run --dir src",
|
|
13
|
+
"lint": "eslint src/**/*.ts",
|
|
14
|
+
"format": "prettier --write src/**/*.ts",
|
|
15
|
+
"prepublishOnly": "npm run build && npm test"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["ai", "context", "developer-tools", "cli"],
|
|
18
|
+
"author": "zeeqsleepy",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@eslint/js": "^10.0.1",
|
|
26
|
+
"@types/node": "^26.5.0",
|
|
27
|
+
"eslint": "^10.10.0",
|
|
28
|
+
"prettier": "^3.9.6",
|
|
29
|
+
"typescript": "5.8",
|
|
30
|
+
"typescript-eslint": "^8.70.0",
|
|
31
|
+
"vitest": "^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^15.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|