@jeeva1398/eventa 0.1.2 → 0.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 +42 -2
- package/dist/index.js +66 -7
- package/package.json +21 -5
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# eventa
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@jeeva1398/eventa)
|
|
4
|
+
[](https://www.npmjs.com/package/@jeeva1398/eventa)
|
|
5
|
+
[](https://huggingface.co/jeeva1398/eventa-1.5b-gguf)
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
**Offline AI assistant for Node.js and TypeScript developers.** It explains crashes, reviews your diffs and audits dependencies, using a small code model that runs on your own machine. No API key, and no code leaves your laptop.
|
|
4
9
|
|
|
5
10
|
```bash
|
|
6
11
|
npx @jeeva1398/eventa explain --run "node app.js" # run it, explain the crash
|
|
@@ -44,7 +49,42 @@ Global flags: `-p, --provider auto|ollama|local`, `-m, --model <name>`, `--host
|
|
|
44
49
|
| `localModel` (built-in) | `eventa-1.5b` | `EVENTA_LOCAL_MODEL` |
|
|
45
50
|
| `ollamaHost` | `http://localhost:11434` | `EVENTA_OLLAMA_HOST` / `OLLAMA_HOST` |
|
|
46
51
|
| `contextSize` | `8192` | |
|
|
47
|
-
| `temperature` | `0.2` | |
|
|
52
|
+
| `temperature` | `0.2` | `EVENTA_TEMPERATURE` |
|
|
53
|
+
|
|
54
|
+
## Review pull requests in GitHub Actions
|
|
55
|
+
|
|
56
|
+
Eventa can review every pull request and post the findings as one comment, which it updates on each push. The model runs on the GitHub runner, so your code is not sent to any AI service.
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
# .github/workflows/eventa.yml
|
|
60
|
+
name: eventa
|
|
61
|
+
on: pull_request
|
|
62
|
+
permissions:
|
|
63
|
+
contents: read
|
|
64
|
+
pull-requests: write
|
|
65
|
+
jobs:
|
|
66
|
+
review:
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
with:
|
|
71
|
+
fetch-depth: 2
|
|
72
|
+
- uses: Jeeva1398/eventa@v0.2.0
|
|
73
|
+
with:
|
|
74
|
+
fail-on: none # or high | medium | low to fail the check
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Input | Default | |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `fail-on` | `none` | Fail the job when the AI reports an issue at this severity or above. |
|
|
80
|
+
| `max-files` | `20` | Maximum number of files to review. |
|
|
81
|
+
| `base` | PR base | Git ref to diff against. |
|
|
82
|
+
| `comment` | `true` | Post or update the PR comment. The report is always written to the job summary. |
|
|
83
|
+
| `version` | the action's tag | `@jeeva1398/eventa` version to run. |
|
|
84
|
+
|
|
85
|
+
The first run downloads the runtime and model (~1 GB). Later runs restore them from the Actions cache. Reviews run at temperature 0, so re-running a job gives the same result. Pull requests from forks get a read-only token, so for those the report goes only to the job summary.
|
|
86
|
+
|
|
87
|
+
Locally, `eventa review --markdown` prints the same report, and `--fail-on high` sets exit code 1 when the review finds a high-severity issue.
|
|
48
88
|
|
|
49
89
|
## Privacy & security
|
|
50
90
|
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ function fromEnv(env) {
|
|
|
39
39
|
if (env.EVENTA_MODEL) out.model = env.EVENTA_MODEL;
|
|
40
40
|
if (env.EVENTA_LOCAL_MODEL) out.localModel = env.EVENTA_LOCAL_MODEL;
|
|
41
41
|
if (env.EVENTA_PROVIDER) out.provider = env.EVENTA_PROVIDER;
|
|
42
|
+
if (env.EVENTA_TEMPERATURE && !Number.isNaN(Number(env.EVENTA_TEMPERATURE))) out.temperature = Number(env.EVENTA_TEMPERATURE);
|
|
42
43
|
const host = env.EVENTA_OLLAMA_HOST ?? env.OLLAMA_HOST;
|
|
43
44
|
if (host) out.ollamaHost = host.startsWith("http") ? host : `http://${host}`;
|
|
44
45
|
return out;
|
|
@@ -1258,6 +1259,57 @@ ${hints.map((h) => `- [${h.severity}] line ${h.line}: ${h.message}`).join("\n")}
|
|
|
1258
1259
|
};
|
|
1259
1260
|
}
|
|
1260
1261
|
|
|
1262
|
+
// src/commands/reviewReport.ts
|
|
1263
|
+
var MARKER = "<!-- eventa-review -->";
|
|
1264
|
+
var RANK = { low: 1, medium: 2, high: 3 };
|
|
1265
|
+
var ICON = { high: "\u{1F534}", medium: "\u{1F7E0}", low: "\u26AA" };
|
|
1266
|
+
function parseIssues(analysis) {
|
|
1267
|
+
const issues = [];
|
|
1268
|
+
for (const line of analysis.split(/\r?\n/)) {
|
|
1269
|
+
const m = /^\s*[-*]\s*\[(high|medium|low)\]\s*(?:line\s+(\d+)\s*:?)?\s*(.+)$/i.exec(line);
|
|
1270
|
+
if (m) issues.push({ severity: m[1].toLowerCase(), line: m[2] ? Number(m[2]) : void 0, text: m[3].trim() });
|
|
1271
|
+
}
|
|
1272
|
+
return issues;
|
|
1273
|
+
}
|
|
1274
|
+
function worstSeverity(results) {
|
|
1275
|
+
let worst;
|
|
1276
|
+
for (const r of results) for (const i of parseIssues(r.analysis)) if (!worst || RANK[i.severity] > RANK[worst]) worst = i.severity;
|
|
1277
|
+
return worst;
|
|
1278
|
+
}
|
|
1279
|
+
var meetsThreshold = (worst, failOn) => !!worst && failOn in RANK && RANK[worst] >= RANK[failOn];
|
|
1280
|
+
var cell = (s) => s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
1281
|
+
function toMarkdown(results, opts) {
|
|
1282
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1283
|
+
for (const r of results) {
|
|
1284
|
+
const entry = byFile.get(r.file) ?? { issues: [], hints: [] };
|
|
1285
|
+
entry.issues.push(...parseIssues(r.analysis));
|
|
1286
|
+
entry.hints.push(...r.hints);
|
|
1287
|
+
byFile.set(r.file, entry);
|
|
1288
|
+
}
|
|
1289
|
+
const all = [...byFile.values()].flatMap((f) => f.issues);
|
|
1290
|
+
const count = (s) => all.filter((i) => i.severity === s).length;
|
|
1291
|
+
const out = [MARKER, "### Eventa review", ""];
|
|
1292
|
+
if (!all.length) {
|
|
1293
|
+
out.push(`No issues found in ${byFile.size} file(s) (${opts.label}).`);
|
|
1294
|
+
} else {
|
|
1295
|
+
out.push(`Found **${all.length}** issue(s) in ${byFile.size} file(s) (${opts.label}): ${count("high")} high, ${count("medium")} medium, ${count("low")} low.`, "");
|
|
1296
|
+
out.push("| | File | Line | Issue |", "|---|---|---|---|");
|
|
1297
|
+
for (const [file, { issues }] of byFile) {
|
|
1298
|
+
for (const i of [...issues].sort((a, b) => RANK[b.severity] - RANK[a.severity])) {
|
|
1299
|
+
out.push(`| ${ICON[i.severity]} ${i.severity} | \`${file}\` | ${i.line ?? ""} | ${cell(i.text)} |`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
const hinted = [...byFile].filter(([, f]) => f.hints.length);
|
|
1304
|
+
if (hinted.length) {
|
|
1305
|
+
out.push("", "<details><summary>Static checks</summary>", "");
|
|
1306
|
+
for (const [file, { hints }] of hinted) for (const h of hints) out.push(`- \`${file}\` line ${h.line}: [${h.severity}] ${h.message}`);
|
|
1307
|
+
out.push("", "</details>");
|
|
1308
|
+
}
|
|
1309
|
+
out.push("", `<sub>Reviewed offline by <a href="https://github.com/Jeeva1398/eventa">eventa</a> ${opts.version}. AI findings can be wrong, check before acting.</sub>`);
|
|
1310
|
+
return out.join("\n") + "\n";
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1261
1313
|
// src/commands/review.ts
|
|
1262
1314
|
var SEVERITY = { high: chalk7.red, medium: chalk7.yellow, low: chalk7.dim };
|
|
1263
1315
|
var git = (args, cwd) => exec("git", args, { cwd, shell: false });
|
|
@@ -1276,13 +1328,16 @@ async function getDiff(cwd, base) {
|
|
|
1276
1328
|
async function reviewCommand(config2, opts) {
|
|
1277
1329
|
const cwd = process.cwd();
|
|
1278
1330
|
const { diff, label } = await getDiff(cwd, opts.base);
|
|
1331
|
+
if (opts.failOn && !["high", "medium", "low", "none"].includes(opts.failOn)) throw new Error("--fail-on must be high, medium, low or none");
|
|
1332
|
+
const quiet = opts.json || opts.markdown;
|
|
1279
1333
|
const files = parseDiff(diff).slice(0, Number(opts.maxFiles) || 20);
|
|
1280
1334
|
if (!files.length) {
|
|
1281
1335
|
if (opts.json) printJson([]);
|
|
1336
|
+
else if (opts.markdown) process.stdout.write(toMarkdown([], { label, version: "0.2.0" }));
|
|
1282
1337
|
else console.log(chalk7.yellow("Nothing to review: no changed source files found."));
|
|
1283
1338
|
return;
|
|
1284
1339
|
}
|
|
1285
|
-
if (!
|
|
1340
|
+
if (!quiet) console.log(chalk7.dim(`Reviewing ${label} in ${files.length} file(s)
|
|
1286
1341
|
`));
|
|
1287
1342
|
const maxChars = Math.min(12e3, Math.floor((config2.contextSize - 2e3) * 3.5));
|
|
1288
1343
|
const results = [];
|
|
@@ -1291,7 +1346,9 @@ async function reviewCommand(config2, opts) {
|
|
|
1291
1346
|
const parts = splitPatch(file.patch, maxChars);
|
|
1292
1347
|
for (const [i, part] of parts.entries()) {
|
|
1293
1348
|
const partHints = hints.filter((h) => part.includes(`${String(h.line).padStart(4)} +`));
|
|
1294
|
-
if (
|
|
1349
|
+
if (opts.markdown) process.stderr.write(`Reviewing ${file.path}${parts.length > 1 ? ` (part ${i + 1}/${parts.length})` : ""}
|
|
1350
|
+
`);
|
|
1351
|
+
if (!quiet) {
|
|
1295
1352
|
const suffix = parts.length > 1 ? chalk7.dim(` (part ${i + 1}/${parts.length})`) : "";
|
|
1296
1353
|
console.log(chalk7.bold.cyan(`\u25B8 ${file.path}`) + suffix);
|
|
1297
1354
|
if (partHints.length) {
|
|
@@ -1300,20 +1357,22 @@ async function reviewCommand(config2, opts) {
|
|
|
1300
1357
|
console.log(chalk7.bold("AI review"));
|
|
1301
1358
|
}
|
|
1302
1359
|
}
|
|
1303
|
-
const analysis = await generate(config2, reviewPrompt(file.path, part, partHints), opts);
|
|
1360
|
+
const analysis = await generate(config2, reviewPrompt(file.path, part, partHints), { ...opts, json: quiet });
|
|
1304
1361
|
results.push({ file: file.path, hints: partHints, analysis });
|
|
1305
|
-
if (!
|
|
1362
|
+
if (!quiet && /^no issues found\.?$/i.test(analysis.trim()) && partHints.some((h) => h.severity !== "low")) {
|
|
1306
1363
|
console.log(chalk7.yellow("The AI did not confirm the static checks above. Verify them yourself."));
|
|
1307
1364
|
}
|
|
1308
|
-
if (!
|
|
1365
|
+
if (!quiet) console.log();
|
|
1309
1366
|
}
|
|
1310
1367
|
}
|
|
1311
1368
|
if (opts.json) printJson(results);
|
|
1369
|
+
else if (opts.markdown) process.stdout.write(toMarkdown(results, { label, version: "0.2.0" }));
|
|
1370
|
+
if (opts.failOn && meetsThreshold(worstSeverity(results), opts.failOn)) process.exitCode = 1;
|
|
1312
1371
|
}
|
|
1313
1372
|
|
|
1314
1373
|
// src/index.ts
|
|
1315
1374
|
var program = new Command();
|
|
1316
|
-
program.name("eventa").description("Offline AI assistant for Node.js developers").version("0.
|
|
1375
|
+
program.name("eventa").description("Offline AI assistant for Node.js developers").version("0.2.0").option("-m, --model <name>", "model to use (overrides config)").option("--host <url>", "Ollama host URL").option("-p, --provider <name>", "auto | ollama | local").option("--raw", "print plain output without terminal formatting");
|
|
1317
1376
|
var configFrom = (cmd) => {
|
|
1318
1377
|
const g = cmd.optsWithGlobals();
|
|
1319
1378
|
if (g.provider && !["auto", "ollama", "local"].includes(g.provider)) throw new Error("--provider must be auto, ollama or local");
|
|
@@ -1327,7 +1386,7 @@ program.command("explain").description("explain a Node.js error or stack trace a
|
|
|
1327
1386
|
const { config: config2, raw } = configFrom(cmd);
|
|
1328
1387
|
await explainCommand(words, config2, { ...opts, raw });
|
|
1329
1388
|
});
|
|
1330
|
-
program.command("review").description("review your staged git diff (falls back to unstaged changes)").option("-b, --base <ref>", "review all changes since a branch or commit, e.g. main").option("--max-files <n>", "maximum files to review", "20").option("--json", "print JSON output").action(async (opts, cmd) => {
|
|
1389
|
+
program.command("review").description("review your staged git diff (falls back to unstaged changes)").option("-b, --base <ref>", "review all changes since a branch or commit, e.g. main").option("--max-files <n>", "maximum files to review", "20").option("--json", "print JSON output").option("--markdown", "print a Markdown report (for PR comments)").option("--fail-on <severity>", "exit with code 1 if the AI reports an issue at this severity or above: high | medium | low | none").action(async (opts, cmd) => {
|
|
1331
1390
|
const { config: config2, raw } = configFrom(cmd);
|
|
1332
1391
|
await reviewCommand(config2, { ...opts, raw });
|
|
1333
1392
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeeva1398/eventa",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Offline AI assistant for Node.js
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Offline AI assistant for Node.js & TypeScript: explains crashes, reviews git diffs, audits npm dependencies. Runs a local 1 GB model, no API key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"eventa": "dist/index.js"
|
|
@@ -24,13 +24,29 @@
|
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"nodejs",
|
|
27
|
+
"typescript",
|
|
27
28
|
"ai",
|
|
28
29
|
"cli",
|
|
29
|
-
"llm",
|
|
30
|
-
"offline",
|
|
31
30
|
"code-review",
|
|
31
|
+
"ai-code-review",
|
|
32
|
+
"debugging",
|
|
33
|
+
"stack-trace",
|
|
34
|
+
"error",
|
|
35
|
+
"explain",
|
|
36
|
+
"npm-audit",
|
|
37
|
+
"dependencies",
|
|
38
|
+
"security",
|
|
39
|
+
"offline",
|
|
40
|
+
"local-llm",
|
|
41
|
+
"llm",
|
|
32
42
|
"ollama",
|
|
33
|
-
"gguf"
|
|
43
|
+
"gguf",
|
|
44
|
+
"llama.cpp",
|
|
45
|
+
"express",
|
|
46
|
+
"nestjs",
|
|
47
|
+
"prisma",
|
|
48
|
+
"privacy",
|
|
49
|
+
"developer-tools"
|
|
34
50
|
],
|
|
35
51
|
"repository": {
|
|
36
52
|
"type": "git",
|