@jeeva1398/eventa 0.1.1 → 0.1.3

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,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
@@ -46,6 +51,24 @@ Global flags: `-p, --provider auto|ollama|local`, `-m, --model <name>`, `--host
46
51
  | `contextSize` | `8192` | |
47
52
  | `temperature` | `0.2` | |
48
53
 
54
+ ## Privacy & security
55
+
56
+ Your code is only sent to the model running on your machine: the built-in runtime, or Ollama at `ollamaHost`. If you point that at a remote server, code goes there. Eventa makes these network calls:
57
+
58
+ | When | Where | What is sent |
59
+ |---|---|---|
60
+ | First local use | npm registry | Downloads the runtime from a **pinned lockfile** with integrity hashes and **install scripts disabled** |
61
+ | First local use | huggingface.co | Downloads the model from a pinned commit and verifies its **SHA-256** |
62
+ | `eventa deps` | npm registry | Runs `npm audit` / `npm outdated`, which send your dependency names and versions (exactly as running them yourself would) |
63
+
64
+ Other safeguards:
65
+ - `explain` only reads source files inside the current project, never arbitrary paths from a pasted stack trace.
66
+ - Model output is printed as text with terminal control sequences stripped, and is never executed.
67
+ - `--run` executes only the command you type.
68
+ - The npm package is published with provenance from GitHub Actions.
69
+
70
+ Found a security problem? Please open a private advisory: https://github.com/Jeeva1398/eventa/security/advisories/new
71
+
49
72
  ## Limitations
50
73
  This is a 1.5B model. It is fast and private, but it can be wrong, so treat its answers as a strong hint, not a verdict. The facts Eventa computes itself (stack frames, audit fixes, unused/missing packages, static checks) are deterministic.
51
74
 
package/dist/index.js CHANGED
@@ -92,10 +92,10 @@ var modelPath = (spec) => spec.url ? join2(modelsDir(), spec.file) : spec.file;
92
92
  var isDownloaded = (spec) => existsSync(modelPath(spec));
93
93
 
94
94
  // src/model/runtime.ts
95
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
95
+ import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync } from "fs";
96
96
  import { createRequire } from "module";
97
97
  import { join as join3 } from "path";
98
- import { pathToFileURL } from "url";
98
+ import { fileURLToPath, pathToFileURL } from "url";
99
99
 
100
100
  // src/context/exec.ts
101
101
  import { spawn } from "child_process";
@@ -135,15 +135,32 @@ async function installRuntime(log2 = () => {
135
135
  if (Number(process.versions.node.split(".")[0]) < 20) {
136
136
  throw new Error("The built-in runtime needs Node.js 20 or newer. Upgrade Node, or use Ollama.");
137
137
  }
138
+ if (!existsSync2(join3(LOCK_DIR, "package-lock.json"))) {
139
+ throw new Error("The built-in runtime is installed by the npm package. Use `npx @jeeva1398/eventa`, or run Ollama with this binary.");
140
+ }
138
141
  const dir = runtimeDir();
142
+ const platform = platformPackage();
143
+ rmSync(join3(dir, "node_modules"), { recursive: true, force: true });
139
144
  mkdirSync2(dir, { recursive: true });
140
- const manifest = join3(dir, "package.json");
141
- if (!existsSync2(manifest)) writeFileSync2(manifest, JSON.stringify({ name: "eventa-runtime", private: true }, null, 2));
145
+ for (const f of ["package.json", "package-lock.json"]) copyFileSync(join3(LOCK_DIR, f), join3(dir, f));
146
+ const lock = JSON.parse(readFileSync2(join3(dir, "package-lock.json"), "utf8"));
147
+ const expected = lock.packages[`node_modules/${platform}`]?.integrity;
148
+ if (!expected) throw new Error(`No pinned runtime binary for ${platform}.`);
142
149
  log2(`Installing the local runtime (node-llama-cpp ${LLAMA_VERSION}, about 80 MB, one time)\u2026`);
143
- const r = await exec("npm", ["install", `node-llama-cpp@${LLAMA_VERSION}`, `${platformPackage()}@${LLAMA_VERSION}`, "--omit=optional", "--no-audit", "--no-fund", "--loglevel=error"], { cwd: dir });
144
- if (r.code !== 0) throw new Error(`Runtime install failed:
145
- ${r.stderr.trim()}`);
150
+ const npm = (args) => exec("npm", [...args, "--ignore-scripts", "--no-audit", "--no-fund", "--loglevel=error"], { cwd: dir });
151
+ const ci = await npm(["ci", "--omit=optional"]);
152
+ if (ci.code !== 0) throw new Error(`Runtime install failed:
153
+ ${ci.stderr.trim()}`);
154
+ const bin = await npm(["install", "--no-save", "--omit=optional", `${platform}@${LLAMA_VERSION}`]);
155
+ if (bin.code !== 0) throw new Error(`Runtime install failed:
156
+ ${bin.stderr.trim()}`);
157
+ const installed = JSON.parse(readFileSync2(join3(dir, "node_modules", ".package-lock.json"), "utf8")).packages[`node_modules/${platform}`]?.integrity;
158
+ if (installed !== expected) {
159
+ rmSync(join3(dir, "node_modules"), { recursive: true, force: true });
160
+ throw new Error(`Integrity check failed for ${platform}: expected ${expected}, got ${installed ?? "nothing"}.`);
161
+ }
146
162
  }
163
+ var LOCK_DIR = fileURLToPath(new URL("../runtime/", import.meta.url));
147
164
  var cached;
148
165
  function loadRuntime() {
149
166
  cached ??= (async () => {
@@ -429,6 +446,7 @@ function formatLine(line, state) {
429
446
  if (heading) return chalk2.bold.underline(heading[2]);
430
447
  return line.replace(/\*\*([^*]+)\*\*/g, (_, t) => chalk2.bold(t)).replace(/`([^`]+)`/g, (_, t) => chalk2.yellow(t)).replace(/^(\s*)[-*]\s+/, (_, s) => `${s}\u2022 `);
431
448
  }
449
+ var stripControl = (s) => s.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
432
450
  async function renderStream(tokens, opts = {}) {
433
451
  const out = opts.out ?? process.stdout;
434
452
  const pretty = !opts.raw && Boolean(out.isTTY);
@@ -437,8 +455,9 @@ async function renderStream(tokens, opts = {}) {
437
455
  let full = "";
438
456
  let pending = "";
439
457
  try {
440
- for await (const token of tokens) {
458
+ for await (const raw of tokens) {
441
459
  spinner?.stop();
460
+ const token = stripControl(raw);
442
461
  full += token;
443
462
  if (!pretty) {
444
463
  out.write(token);
@@ -517,7 +536,7 @@ import chalk3 from "chalk";
517
536
  import ora2 from "ora";
518
537
 
519
538
  // src/context/deps.ts
520
- import { readdirSync, readFileSync as readFileSync2 } from "fs";
539
+ import { readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
521
540
  import { builtinModules } from "module";
522
541
  import { extname, join as join4 } from "path";
523
542
  var SEVERITY_ORDER = ["critical", "high", "moderate", "low", "info"];
@@ -527,7 +546,7 @@ var IMPORT_RE = /(?:import|export)\s[^'"]*?from\s*['"]([^'"]+)['"]|import\s*\(\s
527
546
  var BUILTINS = new Set(builtinModules);
528
547
  function readPackageJson(cwd) {
529
548
  try {
530
- return JSON.parse(readFileSync2(join4(cwd, "package.json"), "utf8"));
549
+ return JSON.parse(readFileSync3(join4(cwd, "package.json"), "utf8"));
531
550
  } catch {
532
551
  throw new Error(`No readable package.json in ${cwd}`);
533
552
  }
@@ -595,9 +614,11 @@ function scanImports(cwd, maxFiles = 3e3) {
595
614
  if (count >= maxFiles) return;
596
615
  if (e.isDirectory()) {
597
616
  if (!SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) walk(join4(dir, e.name));
598
- } else if (SOURCE_EXT.has(extname(e.name)) || /\.config\.[cm]?[jt]s$/.test(e.name)) {
617
+ } else if ((SOURCE_EXT.has(extname(e.name)) || /\.config\.[cm]?[jt]s$/.test(e.name)) && !/\.min\.[cm]?js$/.test(e.name)) {
618
+ const path = join4(dir, e.name);
619
+ if (statSync2(path).size > 1e6) continue;
599
620
  count++;
600
- const text = readFileSync2(join4(dir, e.name), "utf8");
621
+ const text = readFileSync3(path, "utf8");
601
622
  for (const m of text.matchAll(IMPORT_RE)) {
602
623
  const name = packageName(m[1] ?? m[2] ?? m[3] ?? m[4]);
603
624
  if (name) found.add(name);
@@ -611,7 +632,7 @@ function scanImports(cwd, maxFiles = 3e3) {
611
632
  function binNames(cwd, dep) {
612
633
  let text;
613
634
  try {
614
- text = readFileSync2(join4(cwd, "node_modules", dep, "package.json"), "utf8");
635
+ text = readFileSync3(join4(cwd, "node_modules", dep, "package.json"), "utf8");
615
636
  } catch {
616
637
  return [];
617
638
  }
@@ -839,7 +860,7 @@ Provider setting: ${config2.provider} \u2192 using ${active.name} (${active.mode
839
860
  }
840
861
 
841
862
  // src/commands/model.ts
842
- import { rmSync } from "fs";
863
+ import { rmSync as rmSync2 } from "fs";
843
864
  import chalk5 from "chalk";
844
865
  function modelList() {
845
866
  const { localModel } = loadConfig();
@@ -865,20 +886,20 @@ function modelUse(id) {
865
886
  function modelRemove(id) {
866
887
  const spec = resolveModel(id);
867
888
  if (!spec.url) throw new Error("Refusing to delete a local file you provided yourself.");
868
- rmSync(modelPath(spec), { force: true });
869
- rmSync(`${modelPath(spec)}.part`, { force: true });
889
+ rmSync2(modelPath(spec), { force: true });
890
+ rmSync2(`${modelPath(spec)}.part`, { force: true });
870
891
  console.log(`Removed ${spec.id}`);
871
892
  }
872
893
 
873
894
  // src/commands/explain.ts
874
- import { readFileSync as readFileSync4 } from "fs";
895
+ import { readFileSync as readFileSync5 } from "fs";
875
896
  import { relative as relative2 } from "path";
876
897
  import chalk6 from "chalk";
877
898
 
878
899
  // src/context/stackParser.ts
879
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
900
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
880
901
  import { isAbsolute, relative, resolve as resolve2 } from "path";
881
- import { fileURLToPath } from "url";
902
+ import { fileURLToPath as fileURLToPath2 } from "url";
882
903
  var FRAME_RE = /^\s*at\s+(?:async\s+(?=[^(]*$))?(?:(.+?)\s+\()?((?:file:\/\/|node:|[A-Za-z]:[\\/]|[\\/.]).+?):(\d+)(?::(\d+))?\)?\s*$/;
883
904
  var HEADER_RE = /^((?:file:\/\/\/?)?(?:[A-Za-z]:)?[^\s:]+\.(?:[cm]?[jt]sx?)):(\d+)$/;
884
905
  var MESSAGE_RE = /^\s*(?:Uncaught\s+)?((?:[A-Z][A-Za-z]*)?Error(?:\s\[[A-Z0-9_]+\])?:\s.*)$/;
@@ -893,7 +914,7 @@ var PATTERN_CODES = [
893
914
  function toPath(file) {
894
915
  if (file.startsWith("file://")) {
895
916
  try {
896
- return fileURLToPath(file);
917
+ return fileURLToPath2(file);
897
918
  } catch {
898
919
  return file;
899
920
  }
@@ -946,12 +967,16 @@ function parseStack(raw) {
946
967
  result.code = PATTERN_CODES.find(([re]) => re.test(text))?.[1] ?? CODE_RE.exec(text)?.[1];
947
968
  return result;
948
969
  }
970
+ function insideProject(file, cwd) {
971
+ const rel = relative(resolve2(cwd), resolve2(file));
972
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel) && !/(^|[\\/])node_modules([\\/]|$)/.test(rel);
973
+ }
949
974
  function userFrames(parsed, cwd, limit = 3) {
950
- return parsed.frames.filter((f) => !f.internal && !f.dependency).map((f) => ({ ...f, file: isAbsolute(f.file) ? f.file : resolve2(cwd, f.file) })).filter((f) => existsSync4(f.file)).slice(0, limit);
975
+ return parsed.frames.filter((f) => !f.internal && !f.dependency).map((f) => ({ ...f, file: isAbsolute(f.file) ? f.file : resolve2(cwd, f.file) })).filter((f) => insideProject(f.file, cwd) && existsSync4(f.file)).slice(0, limit);
951
976
  }
952
977
  function readSnippet(file, marked, radius = 15, cwd = process.cwd()) {
953
978
  const targets = (Array.isArray(marked) ? marked : [marked]).slice().sort((a, b) => a - b);
954
- const lines2 = readFileSync3(file, "utf8").split(/\r?\n/);
979
+ const lines2 = readFileSync4(file, "utf8").split(/\r?\n/);
955
980
  const start = Math.max(1, targets[0] - radius);
956
981
  const end = Math.min(lines2.length, targets[targets.length - 1] + radius);
957
982
  const body = [];
@@ -1078,7 +1103,7 @@ ${r.stdout}`.trim();
1078
1103
  if (r.code === 0 && !parseStack(output).message) return null;
1079
1104
  return output;
1080
1105
  }
1081
- if (opts.file) return readFileSync4(opts.file, "utf8");
1106
+ if (opts.file) return readFileSync5(opts.file, "utf8");
1082
1107
  if (words.length) return words.join(" ");
1083
1108
  return readStdin();
1084
1109
  }
@@ -1288,7 +1313,7 @@ async function reviewCommand(config2, opts) {
1288
1313
 
1289
1314
  // src/index.ts
1290
1315
  var program = new Command();
1291
- program.name("eventa").description("Offline AI assistant for Node.js developers").version("0.1.1").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");
1316
+ program.name("eventa").description("Offline AI assistant for Node.js developers").version("0.1.3").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");
1292
1317
  var configFrom = (cmd) => {
1293
1318
  const g = cmd.optsWithGlobals();
1294
1319
  if (g.provider && !["auto", "ollama", "local"].includes(g.provider)) throw new Error("--provider must be auto, ollama or local");
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@jeeva1398/eventa",
3
- "version": "0.1.1",
4
- "description": "Offline AI assistant for Node.js developers: explain errors, review diffs, audit dependencies.",
3
+ "version": "0.1.3",
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"
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
+ "runtime",
11
12
  "README.md",
12
13
  "LICENSE"
13
14
  ],
@@ -23,13 +24,29 @@
23
24
  },
24
25
  "keywords": [
25
26
  "nodejs",
27
+ "typescript",
26
28
  "ai",
27
29
  "cli",
28
- "llm",
29
- "offline",
30
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",
31
42
  "ollama",
32
- "gguf"
43
+ "gguf",
44
+ "llama.cpp",
45
+ "express",
46
+ "nestjs",
47
+ "prisma",
48
+ "privacy",
49
+ "developer-tools"
33
50
  ],
34
51
  "repository": {
35
52
  "type": "git",