@jeeva1398/eventa 0.1.0 → 0.1.2
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 +19 -1
- package/dist/index.js +162 -46
- package/package.json +2 -1
- package/runtime/package-lock.json +1797 -0
- package/runtime/package.json +7 -0
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Eventa parses the stack trace or `tsc` output, reads the source lines it points
|
|
|
27
27
|
Reviews `git diff --staged`, falling back to unstaged changes. Use `--base main` to review everything since `main`. Each file gets static checks first (missing `await`, SQL/command injection, `eval`, sync fs, empty `catch`, disabled TLS, hard-coded secrets), then the model confirms or dismisses them and reports `- [severity] line N: problem → fix`.
|
|
28
28
|
|
|
29
29
|
### `eventa deps`
|
|
30
|
-
Runs `npm audit` and `npm outdated` and scans your imports. It prints the vulnerabilities with their **exact fix commands**, outdated packages (flagging major bumps), unused dependencies and packages that are imported but not declared. Then it
|
|
30
|
+
Runs `npm audit` and `npm outdated` and scans your imports. It prints the vulnerabilities with their **exact fix commands**, outdated packages (flagging major bumps), unused dependencies and packages that are imported but not declared. Then it prints an **action plan** built only from facts: the fix commands, verified breaking-change notes for about 25 popular packages (others say "read the changelog" instead of guessing), and cleanup commands. It is instant and works without the model. Add `--ai` for optional extra commentary from the model.
|
|
31
31
|
|
|
32
32
|
### More
|
|
33
33
|
- `eventa ask <question>`: any Node.js question
|
|
@@ -46,6 +46,24 @@ Global flags: `-p, --provider auto|ollama|local`, `-m, --model <name>`, `--host
|
|
|
46
46
|
| `contextSize` | `8192` | |
|
|
47
47
|
| `temperature` | `0.2` | |
|
|
48
48
|
|
|
49
|
+
## Privacy & security
|
|
50
|
+
|
|
51
|
+
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:
|
|
52
|
+
|
|
53
|
+
| When | Where | What is sent |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| First local use | npm registry | Downloads the runtime from a **pinned lockfile** with integrity hashes and **install scripts disabled** |
|
|
56
|
+
| First local use | huggingface.co | Downloads the model from a pinned commit and verifies its **SHA-256** |
|
|
57
|
+
| `eventa deps` | npm registry | Runs `npm audit` / `npm outdated`, which send your dependency names and versions (exactly as running them yourself would) |
|
|
58
|
+
|
|
59
|
+
Other safeguards:
|
|
60
|
+
- `explain` only reads source files inside the current project, never arbitrary paths from a pasted stack trace.
|
|
61
|
+
- Model output is printed as text with terminal control sequences stripped, and is never executed.
|
|
62
|
+
- `--run` executes only the command you type.
|
|
63
|
+
- The npm package is published with provenance from GitHub Actions.
|
|
64
|
+
|
|
65
|
+
Found a security problem? Please open a private advisory: https://github.com/Jeeva1398/eventa/security/advisories/new
|
|
66
|
+
|
|
49
67
|
## Limitations
|
|
50
68
|
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
69
|
|
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,
|
|
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
|
|
141
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
}
|
|
@@ -559,6 +578,11 @@ function fixPlan(vulns) {
|
|
|
559
578
|
}
|
|
560
579
|
return [...groups].map(([command, fixes]) => ({ command, fixes }));
|
|
561
580
|
}
|
|
581
|
+
function breaking(current, latest) {
|
|
582
|
+
const [cMajor, cMinor] = current.split(".");
|
|
583
|
+
const [lMajor, lMinor] = latest.split(".");
|
|
584
|
+
return cMajor !== lMajor || cMajor === "0" && cMinor !== lMinor;
|
|
585
|
+
}
|
|
562
586
|
function parseOutdated(text) {
|
|
563
587
|
const json = parseJson(text) ?? {};
|
|
564
588
|
return Object.entries(json).map(([name, v]) => ({
|
|
@@ -566,7 +590,7 @@ function parseOutdated(text) {
|
|
|
566
590
|
current: v.current,
|
|
567
591
|
wanted: v.wanted,
|
|
568
592
|
latest: v.latest,
|
|
569
|
-
major: Boolean(v.current && v.latest && v.current
|
|
593
|
+
major: Boolean(v.current && v.latest && breaking(v.current, v.latest))
|
|
570
594
|
}));
|
|
571
595
|
}
|
|
572
596
|
function packageName(specifier) {
|
|
@@ -590,9 +614,11 @@ function scanImports(cwd, maxFiles = 3e3) {
|
|
|
590
614
|
if (count >= maxFiles) return;
|
|
591
615
|
if (e.isDirectory()) {
|
|
592
616
|
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) walk(join4(dir, e.name));
|
|
593
|
-
} 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;
|
|
594
620
|
count++;
|
|
595
|
-
const text =
|
|
621
|
+
const text = readFileSync3(path, "utf8");
|
|
596
622
|
for (const m of text.matchAll(IMPORT_RE)) {
|
|
597
623
|
const name = packageName(m[1] ?? m[2] ?? m[3] ?? m[4]);
|
|
598
624
|
if (name) found.add(name);
|
|
@@ -606,7 +632,7 @@ function scanImports(cwd, maxFiles = 3e3) {
|
|
|
606
632
|
function binNames(cwd, dep) {
|
|
607
633
|
let text;
|
|
608
634
|
try {
|
|
609
|
-
text =
|
|
635
|
+
text = readFileSync3(join4(cwd, "node_modules", dep, "package.json"), "utf8");
|
|
610
636
|
} catch {
|
|
611
637
|
return [];
|
|
612
638
|
}
|
|
@@ -644,21 +670,99 @@ async function collectDeps(cwd) {
|
|
|
644
670
|
};
|
|
645
671
|
}
|
|
646
672
|
|
|
673
|
+
// src/prompts/upgrades.ts
|
|
674
|
+
var UPGRADE_NOTES = {
|
|
675
|
+
"express@5": "Express 5 uses path-to-regexp v8: wildcard routes must be named (`/*splat`) and optional params use braces; `req.param()` and `app.del()` were removed; rejected promises in async handlers now go to error middleware. Run your route tests.",
|
|
676
|
+
"body-parser@2": "body-parser 2 (used by Express 5) drops deprecated options and changes `urlencoded` `extended` default to false; check form parsing.",
|
|
677
|
+
"chalk@5": "chalk 5 is ESM-only; `require('chalk')` fails in CommonJS. Stay on 4 for CJS or migrate the project to ESM.",
|
|
678
|
+
"node-fetch@3": "node-fetch 3 is ESM-only. On Node 18+ prefer the built-in global `fetch` and remove the dependency.",
|
|
679
|
+
uuid: "Deep imports such as `require('uuid/v4')` were removed in v8; use `const { v4 } = require('uuid')`.",
|
|
680
|
+
"mongoose@7": "Mongoose 7 removed callback support from model methods (use async/await) and changed the `strictQuery` default.",
|
|
681
|
+
"mongoose@8": "Mongoose 7 removed callback support from model methods (use async/await) and changed the `strictQuery` default; Mongoose 8 needs MongoDB 4.4+.",
|
|
682
|
+
"jest@28": "Jest 28 no longer bundles jsdom (install `jest-environment-jsdom`) and renamed `testURL` to `testEnvironmentOptions.url`.",
|
|
683
|
+
"jest@29": "Jest 28 no longer bundles jsdom (install `jest-environment-jsdom`) and renamed `testURL` to `testEnvironmentOptions.url`; Jest 29 changed the default snapshot format.",
|
|
684
|
+
"eslint@9": "ESLint 9 uses flat config (`eslint.config.js`) by default and ignores `.eslintrc`; many plugins need updated versions.",
|
|
685
|
+
"typescript@5": "TypeScript 5 deprecates flags such as `importsNotUsedAsValues` and implements standard decorators unless `experimentalDecorators` is set; expect some new type errors.",
|
|
686
|
+
"axios@1": "axios 1.x adds an `exports` map, so deep imports like `axios/lib/...` break, and some TypeScript types were renamed.",
|
|
687
|
+
helmet: "Newer helmet enables a strict Content-Security-Policy and cross-origin headers by default, which can block inline scripts and third-party assets; test your pages.",
|
|
688
|
+
inquirer: "inquirer 9+ is ESM-only and v10+ moved to the new `@inquirer/prompts` API.",
|
|
689
|
+
got: "got 12+ is ESM-only and changed several option names; consider native `fetch` for simple calls.",
|
|
690
|
+
"webpack@5": "webpack 5 removed automatic Node.js polyfills (`Buffer`, `process`, `crypto`) for browser bundles; add `resolve.fallback` where needed. It also fixes ERR_OSSL_EVP_UNSUPPORTED on Node 17+.",
|
|
691
|
+
dotenv: "Low risk: dotenv 16 keeps the same `config()` API and adds multiline values.",
|
|
692
|
+
"@nestjs/core@11": "Nest 11 requires Node 20+ and uses Express 5 by default, so wildcard routes change syntax (`*` must be named, e.g. `*splat`). Upgrade all `@nestjs/*` packages together.",
|
|
693
|
+
"@nestjs/common@11": "Nest 11 requires Node 20+ and uses Express 5 by default, so wildcard routes change syntax (`*` must be named, e.g. `*splat`). Upgrade all `@nestjs/*` packages together.",
|
|
694
|
+
"@prisma/client": "Prisma 5 removed `rejectOnNotFound` (use `findUniqueOrThrow`); Prisma 6 needs Node 18.18+ and TypeScript 5.1+, and `Bytes` fields become `Uint8Array`. Upgrade `prisma` and `@prisma/client` to the same version and re-run `prisma generate`.",
|
|
695
|
+
prisma: "Upgrade `prisma` and `@prisma/client` to the same version and re-run `prisma generate`; Prisma 5 removed `rejectOnNotFound`, Prisma 6 needs Node 18.18+ and TypeScript 5.1+.",
|
|
696
|
+
"class-validator@0": "Low risk for most apps: 0.14 fixes a security issue where unknown values skipped validation; `forbidUnknownValues` now defaults to true, so plain objects without decorators are rejected.",
|
|
697
|
+
"redis@4": "node-redis 4 is promise-based: call `await client.connect()` before use, and commands are camelCase (`hSet`, not `hset`) with no callbacks.",
|
|
698
|
+
glob: "glob 9+ has a promise API only (no callbacks) and a named export: `import { glob } from 'glob'`.",
|
|
699
|
+
rimraf: "rimraf 4+ has a promise API only and a named export: `import { rimraf } from 'rimraf'`."
|
|
700
|
+
};
|
|
701
|
+
var major = (version) => version?.split(".")[0];
|
|
702
|
+
function upgradeNote(name, latest) {
|
|
703
|
+
return UPGRADE_NOTES[`${name}@${major(latest)}`] ?? UPGRADE_NOTES[name];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/context/depsPlan.ts
|
|
707
|
+
var SEVERITY_RANK = ["critical", "high", "moderate", "low", "info"];
|
|
708
|
+
function buildPlan(r) {
|
|
709
|
+
const sections = [];
|
|
710
|
+
const byName = new Map(r.vulnerabilities.map((v) => [v.name, v]));
|
|
711
|
+
if (r.vulnerabilities.length) {
|
|
712
|
+
const items = fixPlan(r.vulnerabilities).map(({ command, fixes }) => {
|
|
713
|
+
const vulns = fixes.map((n) => byName.get(n));
|
|
714
|
+
const worst = vulns.map((v) => v.severity).sort((a, b) => SEVERITY_RANK.indexOf(a) - SEVERITY_RANK.indexOf(b))[0];
|
|
715
|
+
const titles = vulns.map((v) => `${v.name}: ${v.title ?? "advisory"}`).join("; ");
|
|
716
|
+
if (command === "no fix available") return `- **No fix available** (${worst}): ${titles}. Replace the package or check whether the vulnerable code is reachable.`;
|
|
717
|
+
const major2 = command.endsWith("(major)");
|
|
718
|
+
return `- \`${command.replace(" (major)", "")}\`${major2 ? " **(major version, see notes below)**" : ""} (${worst}): ${titles}.`;
|
|
719
|
+
});
|
|
720
|
+
sections.push(`**1. Security**
|
|
721
|
+
${items.join("\n")}`);
|
|
722
|
+
}
|
|
723
|
+
const majors = r.outdated.filter((o) => o.major);
|
|
724
|
+
if (majors.length) {
|
|
725
|
+
const items = majors.map((o) => {
|
|
726
|
+
const note = upgradeNote(o.name, o.latest);
|
|
727
|
+
return `- **${o.name} ${o.current ?? "?"} \u2192 ${o.latest}**: ${note ?? "no verified notes. Read its changelog / release notes before upgrading."}`;
|
|
728
|
+
});
|
|
729
|
+
sections.push(`**2. Major upgrades**
|
|
730
|
+
${items.join("\n")}`);
|
|
731
|
+
}
|
|
732
|
+
const cleanup = [];
|
|
733
|
+
if (r.unused.length) cleanup.push(`- \`npm uninstall ${r.unused.join(" ")}\`: not imported anywhere (double-check config files, scripts and dynamic requires first).`);
|
|
734
|
+
if (r.missing.length) cleanup.push(`- \`npm install ${r.missing.join(" ")}\`: imported in code but missing from package.json.`);
|
|
735
|
+
const minors = r.outdated.filter((o) => !o.major);
|
|
736
|
+
if (minors.length) cleanup.push(`- \`npm update\`: safe minor/patch updates for ${minors.map((m) => m.name).join(", ")}.`);
|
|
737
|
+
if (cleanup.length) sections.push(`**3. Cleanup**
|
|
738
|
+
${cleanup.join("\n")}`);
|
|
739
|
+
return sections.join("\n\n");
|
|
740
|
+
}
|
|
741
|
+
|
|
647
742
|
// src/prompts/deps.ts
|
|
648
743
|
function depsPrompt(r, nodeVersion) {
|
|
649
|
-
const
|
|
744
|
+
const lines2 = [`Node ${nodeVersion}`];
|
|
650
745
|
if (r.vulnerabilities.length) {
|
|
651
|
-
|
|
652
|
-
|
|
746
|
+
const title = new Map(r.vulnerabilities.map((v) => [v.name, v.title]));
|
|
747
|
+
lines2.push("Vulnerabilities:", ...r.vulnerabilities.slice(0, 20).map((v) => `- ${v.name} [${v.severity}] ${v.title ?? ""}`));
|
|
748
|
+
lines2.push(
|
|
749
|
+
"Fix commands (already computed and correct):",
|
|
750
|
+
...fixPlan(r.vulnerabilities).map((f) => `- ${f.command} \u2192 fixes ${f.fixes.map((n) => title.get(n) ? `${n} (${title.get(n)})` : n).join(", ")}`)
|
|
751
|
+
);
|
|
653
752
|
}
|
|
654
753
|
if (r.outdated.length) {
|
|
655
|
-
|
|
754
|
+
lines2.push("Outdated:", ...r.outdated.slice(0, 25).map((o) => `- ${o.name} ${o.current ?? "not installed"} \u2192 latest ${o.latest}${o.major ? " (MAJOR)" : ""}`));
|
|
755
|
+
const notes = r.outdated.filter((o) => o.major).map((o) => [o, upgradeNote(o.name, o.latest)]);
|
|
756
|
+
const known = notes.filter(([, n]) => n);
|
|
757
|
+
if (known.length) lines2.push("Verified upgrade notes:", ...known.map(([o, n]) => `- ${o.name} \u2192 ${o.latest}: ${n}`));
|
|
758
|
+
const unknown = notes.filter(([, n]) => !n).map(([o]) => o.name);
|
|
759
|
+
if (unknown.length) lines2.push(`No verified notes for: ${unknown.join(", ")}`);
|
|
656
760
|
}
|
|
657
|
-
if (r.unused.length)
|
|
658
|
-
if (r.missing.length)
|
|
761
|
+
if (r.unused.length) lines2.push(`Possibly unused dependencies: ${r.unused.join(", ")}`);
|
|
762
|
+
if (r.missing.length) lines2.push(`Imported but not declared in package.json: ${r.missing.join(", ")}`);
|
|
659
763
|
return {
|
|
660
|
-
instruction: "You are given facts about a Node.js project's dependencies. Write a short, prioritized action plan:\n1. Security: repeat each fix command exactly as given and say in one line why it matters. Never change versions.\n2. Major upgrades (marked MAJOR only): for each,
|
|
661
|
-
input:
|
|
764
|
+
instruction: "You are given facts about a Node.js project's dependencies. Write a short, prioritized action plan:\n1. Security: repeat each fix command exactly as given and say in one line why it matters, based only on the advisory titles. Never change versions.\n2. Major upgrades (marked MAJOR only): restate the verified upgrade note for each. For packages with no verified notes, say to read their changelog before upgrading; do not guess API changes.\n3. Cleanup: unused packages to remove and missing packages to add.\nDo not repeat the raw lists. Do not invent versions, advisories or APIs. Skip any section with nothing to do.",
|
|
765
|
+
input: lines2.join("\n")
|
|
662
766
|
};
|
|
663
767
|
}
|
|
664
768
|
|
|
@@ -702,9 +806,10 @@ async function depsCommand(config2, opts) {
|
|
|
702
806
|
const spinner = !opts.json && process.stderr.isTTY ? ora2({ text: "Running npm audit and npm outdated\u2026", stream: process.stderr }).start() : null;
|
|
703
807
|
const report = await collectDeps(cwd).finally(() => spinner?.stop());
|
|
704
808
|
const wantAi = opts.ai && !isHealthy(report);
|
|
809
|
+
const plan = isHealthy(report) ? "" : buildPlan(report);
|
|
705
810
|
if (opts.json) {
|
|
706
811
|
const analysis = wantAi ? await generate(config2, depsPrompt(report, process.versions.node), { json: true }) : void 0;
|
|
707
|
-
printJson({ ...report, analysis });
|
|
812
|
+
printJson({ ...report, plan, analysis });
|
|
708
813
|
return;
|
|
709
814
|
}
|
|
710
815
|
printReport(report);
|
|
@@ -712,11 +817,18 @@ async function depsCommand(config2, opts) {
|
|
|
712
817
|
console.log(chalk3.green("\u2714 Dependencies look healthy."));
|
|
713
818
|
return;
|
|
714
819
|
}
|
|
820
|
+
console.log(chalk3.bold.underline("Action plan"));
|
|
821
|
+
await renderStream(lines(plan), { raw: opts.raw });
|
|
715
822
|
if (wantAi) {
|
|
716
|
-
console.log(chalk3.bold.underline("
|
|
823
|
+
console.log(chalk3.bold.underline("\nAI notes"));
|
|
717
824
|
await generate(config2, depsPrompt(report, process.versions.node), opts);
|
|
825
|
+
} else {
|
|
826
|
+
console.log(chalk3.dim("\nRun with --ai for extra AI commentary."));
|
|
718
827
|
}
|
|
719
828
|
}
|
|
829
|
+
async function* lines(text) {
|
|
830
|
+
yield text;
|
|
831
|
+
}
|
|
720
832
|
|
|
721
833
|
// src/commands/doctor.ts
|
|
722
834
|
import chalk4 from "chalk";
|
|
@@ -735,8 +847,8 @@ async function probe(name, check) {
|
|
|
735
847
|
}
|
|
736
848
|
}
|
|
737
849
|
async function doctorCommand(config2) {
|
|
738
|
-
const
|
|
739
|
-
if (
|
|
850
|
+
const major2 = Number(process.versions.node.split(".")[0]);
|
|
851
|
+
if (major2 >= 20) ok(`Node.js ${process.versions.node}`);
|
|
740
852
|
else fail(`Node.js ${process.versions.node}`, "Eventa needs Node.js 20 or newer");
|
|
741
853
|
const ollamaOk = await probe(`Ollama (${config2.model})`, () => new OllamaProvider(config2.model, config2.ollamaHost).check());
|
|
742
854
|
const localOk = await probe(`Built-in runtime (${config2.localModel})`, () => new LocalProvider(config2.localModel).check());
|
|
@@ -744,11 +856,11 @@ async function doctorCommand(config2) {
|
|
|
744
856
|
console.log(chalk4.dim(`
|
|
745
857
|
Provider setting: ${config2.provider} \u2192 using ${active.name} (${active.model})`));
|
|
746
858
|
if (!ollamaOk && !localOk) console.log(chalk4.dim("Neither is ready yet. Eventa installs the built-in runtime and model on first use, or run `eventa model pull` now."));
|
|
747
|
-
return
|
|
859
|
+
return major2 >= 20 && (ollamaOk || localOk);
|
|
748
860
|
}
|
|
749
861
|
|
|
750
862
|
// src/commands/model.ts
|
|
751
|
-
import { rmSync } from "fs";
|
|
863
|
+
import { rmSync as rmSync2 } from "fs";
|
|
752
864
|
import chalk5 from "chalk";
|
|
753
865
|
function modelList() {
|
|
754
866
|
const { localModel } = loadConfig();
|
|
@@ -774,20 +886,20 @@ function modelUse(id) {
|
|
|
774
886
|
function modelRemove(id) {
|
|
775
887
|
const spec = resolveModel(id);
|
|
776
888
|
if (!spec.url) throw new Error("Refusing to delete a local file you provided yourself.");
|
|
777
|
-
|
|
778
|
-
|
|
889
|
+
rmSync2(modelPath(spec), { force: true });
|
|
890
|
+
rmSync2(`${modelPath(spec)}.part`, { force: true });
|
|
779
891
|
console.log(`Removed ${spec.id}`);
|
|
780
892
|
}
|
|
781
893
|
|
|
782
894
|
// src/commands/explain.ts
|
|
783
|
-
import { readFileSync as
|
|
895
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
784
896
|
import { relative as relative2 } from "path";
|
|
785
897
|
import chalk6 from "chalk";
|
|
786
898
|
|
|
787
899
|
// src/context/stackParser.ts
|
|
788
|
-
import { existsSync as existsSync4, readFileSync as
|
|
900
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
789
901
|
import { isAbsolute, relative, resolve as resolve2 } from "path";
|
|
790
|
-
import { fileURLToPath } from "url";
|
|
902
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
791
903
|
var FRAME_RE = /^\s*at\s+(?:async\s+(?=[^(]*$))?(?:(.+?)\s+\()?((?:file:\/\/|node:|[A-Za-z]:[\\/]|[\\/.]).+?):(\d+)(?::(\d+))?\)?\s*$/;
|
|
792
904
|
var HEADER_RE = /^((?:file:\/\/\/?)?(?:[A-Za-z]:)?[^\s:]+\.(?:[cm]?[jt]sx?)):(\d+)$/;
|
|
793
905
|
var MESSAGE_RE = /^\s*(?:Uncaught\s+)?((?:[A-Z][A-Za-z]*)?Error(?:\s\[[A-Z0-9_]+\])?:\s.*)$/;
|
|
@@ -802,7 +914,7 @@ var PATTERN_CODES = [
|
|
|
802
914
|
function toPath(file) {
|
|
803
915
|
if (file.startsWith("file://")) {
|
|
804
916
|
try {
|
|
805
|
-
return
|
|
917
|
+
return fileURLToPath2(file);
|
|
806
918
|
} catch {
|
|
807
919
|
return file;
|
|
808
920
|
}
|
|
@@ -855,17 +967,21 @@ function parseStack(raw) {
|
|
|
855
967
|
result.code = PATTERN_CODES.find(([re]) => re.test(text))?.[1] ?? CODE_RE.exec(text)?.[1];
|
|
856
968
|
return result;
|
|
857
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
|
+
}
|
|
858
974
|
function userFrames(parsed, cwd, limit = 3) {
|
|
859
|
-
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);
|
|
860
976
|
}
|
|
861
977
|
function readSnippet(file, marked, radius = 15, cwd = process.cwd()) {
|
|
862
978
|
const targets = (Array.isArray(marked) ? marked : [marked]).slice().sort((a, b) => a - b);
|
|
863
|
-
const
|
|
979
|
+
const lines2 = readFileSync4(file, "utf8").split(/\r?\n/);
|
|
864
980
|
const start = Math.max(1, targets[0] - radius);
|
|
865
|
-
const end = Math.min(
|
|
981
|
+
const end = Math.min(lines2.length, targets[targets.length - 1] + radius);
|
|
866
982
|
const body = [];
|
|
867
983
|
for (let n = start; n <= end; n++) {
|
|
868
|
-
body.push(targets.includes(n) ? `${
|
|
984
|
+
body.push(targets.includes(n) ? `${lines2[n - 1]} // <-- line ${n}, in the stack trace` : lines2[n - 1]);
|
|
869
985
|
}
|
|
870
986
|
return `// ${relative(cwd, file) || file} (lines ${start}-${end})
|
|
871
987
|
${body.join("\n")}`;
|
|
@@ -873,12 +989,12 @@ ${body.join("\n")}`;
|
|
|
873
989
|
function snippetsFor(frames, radius, cwd) {
|
|
874
990
|
const byFile = /* @__PURE__ */ new Map();
|
|
875
991
|
for (const f of frames) {
|
|
876
|
-
const
|
|
877
|
-
const last =
|
|
878
|
-
if (last === void 0 || Math.abs(f.line - last) <= radius * 2)
|
|
879
|
-
byFile.set(f.file,
|
|
992
|
+
const lines2 = byFile.get(f.file) ?? [];
|
|
993
|
+
const last = lines2[lines2.length - 1];
|
|
994
|
+
if (last === void 0 || Math.abs(f.line - last) <= radius * 2) lines2.push(f.line);
|
|
995
|
+
byFile.set(f.file, lines2);
|
|
880
996
|
}
|
|
881
|
-
return [...byFile].map(([file,
|
|
997
|
+
return [...byFile].map(([file, lines2]) => readSnippet(file, lines2, radius, cwd));
|
|
882
998
|
}
|
|
883
999
|
|
|
884
1000
|
// src/context/tokens.ts
|
|
@@ -987,7 +1103,7 @@ ${r.stdout}`.trim();
|
|
|
987
1103
|
if (r.code === 0 && !parseStack(output).message) return null;
|
|
988
1104
|
return output;
|
|
989
1105
|
}
|
|
990
|
-
if (opts.file) return
|
|
1106
|
+
if (opts.file) return readFileSync5(opts.file, "utf8");
|
|
991
1107
|
if (words.length) return words.join(" ");
|
|
992
1108
|
return readStdin();
|
|
993
1109
|
}
|
|
@@ -1197,7 +1313,7 @@ async function reviewCommand(config2, opts) {
|
|
|
1197
1313
|
|
|
1198
1314
|
// src/index.ts
|
|
1199
1315
|
var program = new Command();
|
|
1200
|
-
program.name("eventa").description("Offline AI assistant for Node.js developers").version("0.1.
|
|
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");
|
|
1201
1317
|
var configFrom = (cmd) => {
|
|
1202
1318
|
const g = cmd.optsWithGlobals();
|
|
1203
1319
|
if (g.provider && !["auto", "ollama", "local"].includes(g.provider)) throw new Error("--provider must be auto, ollama or local");
|
|
@@ -1215,7 +1331,7 @@ program.command("review").description("review your staged git diff (falls back t
|
|
|
1215
1331
|
const { config: config2, raw } = configFrom(cmd);
|
|
1216
1332
|
await reviewCommand(config2, { ...opts, raw });
|
|
1217
1333
|
});
|
|
1218
|
-
program.command("deps").description("audit dependencies: vulnerabilities, outdated, unused and missing packages").option("--
|
|
1334
|
+
program.command("deps").description("audit dependencies: vulnerabilities, outdated, unused and missing packages").option("--ai", "also ask the model for extra commentary (the action plan itself is always deterministic)").option("--json", "print JSON output").action(async (opts, cmd) => {
|
|
1219
1335
|
const { config: config2, raw } = configFrom(cmd);
|
|
1220
1336
|
await depsCommand(config2, { ...opts, raw });
|
|
1221
1337
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeeva1398/eventa",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Offline AI assistant for Node.js developers: explain errors, review diffs, audit dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
+
"runtime",
|
|
11
12
|
"README.md",
|
|
12
13
|
"LICENSE"
|
|
13
14
|
],
|