@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.
Files changed (3) hide show
  1. package/README.md +42 -2
  2. package/dist/index.js +66 -7
  3. package/package.json +21 -5
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # eventa
2
2
 
3
- **Offline AI assistant for Node.js 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.
3
+ [![npm](https://img.shields.io/npm/v/@jeeva1398/eventa?color=cb3837&label=npm)](https://www.npmjs.com/package/@jeeva1398/eventa)
4
+ [![downloads](https://img.shields.io/npm/dm/@jeeva1398/eventa)](https://www.npmjs.com/package/@jeeva1398/eventa)
5
+ [![model](https://img.shields.io/badge/%F0%9F%A4%97%20model-eventa--1.5b-yellow)](https://huggingface.co/jeeva1398/eventa-1.5b-gguf)
6
+ ![offline](https://img.shields.io/badge/runs-100%25%20offline-2ea44f)
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 (!opts.json) console.log(chalk7.dim(`Reviewing ${label} in ${files.length} file(s)
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 (!opts.json) {
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 (!opts.json && /^no issues found\.?$/i.test(analysis.trim()) && partHints.some((h) => h.severity !== "low")) {
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 (!opts.json) console.log();
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.1.2").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");
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.1.2",
4
- "description": "Offline AI assistant for Node.js developers: explain errors, review diffs, audit dependencies.",
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",