@jeeva1398/eventa 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/index.js +1243 -0
  4. package/package.json +56 -0
package/dist/index.js ADDED
@@ -0,0 +1,1243 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import chalk8 from "chalk";
5
+ import { Command } from "commander";
6
+
7
+ // src/model/registry.ts
8
+ import { existsSync } from "fs";
9
+ import { basename, join as join2, resolve } from "path";
10
+
11
+ // src/config.ts
12
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
13
+ import { homedir } from "os";
14
+ import { dirname, join } from "path";
15
+ var DEFAULTS = {
16
+ provider: "auto",
17
+ model: "hf.co/jeeva1398/eventa-1.5b-gguf",
18
+ localModel: "eventa-1.5b",
19
+ ollamaHost: "http://localhost:11434",
20
+ temperature: 0.2,
21
+ contextSize: 8192
22
+ };
23
+ var CONFIG_KEYS = Object.keys(DEFAULTS);
24
+ function configDir() {
25
+ return process.env.EVENTA_HOME ?? join(homedir(), ".eventa");
26
+ }
27
+ function configPath() {
28
+ return join(configDir(), "config.json");
29
+ }
30
+ function readFile() {
31
+ try {
32
+ return JSON.parse(readFileSync(configPath(), "utf8"));
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+ function fromEnv(env) {
38
+ const out = {};
39
+ if (env.EVENTA_MODEL) out.model = env.EVENTA_MODEL;
40
+ if (env.EVENTA_LOCAL_MODEL) out.localModel = env.EVENTA_LOCAL_MODEL;
41
+ if (env.EVENTA_PROVIDER) out.provider = env.EVENTA_PROVIDER;
42
+ const host = env.EVENTA_OLLAMA_HOST ?? env.OLLAMA_HOST;
43
+ if (host) out.ollamaHost = host.startsWith("http") ? host : `http://${host}`;
44
+ return out;
45
+ }
46
+ function loadConfig(overrides = {}, env = process.env) {
47
+ const defined = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== void 0));
48
+ return { ...DEFAULTS, ...readFile(), ...fromEnv(env), ...defined };
49
+ }
50
+ function setConfigValue(key, raw) {
51
+ const current = readFile();
52
+ const value = typeof DEFAULTS[key] === "number" ? Number(raw) : raw;
53
+ if (typeof value === "number" && Number.isNaN(value)) throw new Error(`${key} must be a number`);
54
+ const next = { ...current, [key]: value };
55
+ mkdirSync(dirname(configPath()), { recursive: true });
56
+ writeFileSync(configPath(), JSON.stringify(next, null, 2) + "\n");
57
+ return { ...DEFAULTS, ...next };
58
+ }
59
+
60
+ // src/model/registry.ts
61
+ var MODELS = {
62
+ "qwen2.5-coder-1.5b": {
63
+ id: "qwen2.5-coder-1.5b",
64
+ description: "Qwen2.5-Coder 1.5B Instruct, Q4_K_M (stock base model)",
65
+ url: "https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/qwen2.5-coder-1.5b-instruct-q4_k_m.gguf",
66
+ file: "qwen2.5-coder-1.5b-instruct-q4_k_m.gguf",
67
+ sha256: "cc324af070c2ecbfd324a30884d2f951a7ff756aba85cb811a6ec436933bb046",
68
+ size: 1117320768
69
+ },
70
+ "eventa-1.5b": {
71
+ id: "eventa-1.5b",
72
+ description: "Eventa 1.5B, fine-tuned for Node.js, Q4_K_M",
73
+ url: "https://huggingface.co/jeeva1398/eventa-1.5b-gguf/resolve/127365417879e6ac5e9b9742452590eda87ae353/eventa-1.5b-q4_k_m.gguf",
74
+ file: "eventa-1.5b-q4_k_m-4bd501d2.gguf",
75
+ sha256: "4bd501d2619a464e87c427bae594e59e60d1850ab9846d522bb9bd1d26265b04",
76
+ size: 986048192
77
+ }
78
+ };
79
+ var modelsDir = () => join2(configDir(), "models");
80
+ function resolveModel(idOrPath) {
81
+ if (MODELS[idOrPath]) return MODELS[idOrPath];
82
+ if (/^https?:\/\/.+\.gguf$/.test(idOrPath)) {
83
+ const file = basename(new URL(idOrPath).pathname);
84
+ return { id: file.replace(/\.gguf$/, ""), description: "custom URL", url: idOrPath, file };
85
+ }
86
+ if (idOrPath.endsWith(".gguf") && existsSync(idOrPath)) {
87
+ return { id: basename(idOrPath, ".gguf"), description: "local file", url: "", file: resolve(idOrPath) };
88
+ }
89
+ throw new Error(`Unknown model "${idOrPath}". Known models: ${Object.keys(MODELS).join(", ")}, or pass a .gguf path or URL.`);
90
+ }
91
+ var modelPath = (spec) => spec.url ? join2(modelsDir(), spec.file) : spec.file;
92
+ var isDownloaded = (spec) => existsSync(modelPath(spec));
93
+
94
+ // src/model/runtime.ts
95
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
96
+ import { createRequire } from "module";
97
+ import { join as join3 } from "path";
98
+ import { pathToFileURL } from "url";
99
+
100
+ // src/context/exec.ts
101
+ import { spawn } from "child_process";
102
+ function exec(cmd, args, opts = {}) {
103
+ return new Promise((resolve3, reject) => {
104
+ const child = spawn(cmd, args, {
105
+ cwd: opts.cwd,
106
+ shell: opts.shell ?? process.platform === "win32",
107
+ windowsHide: true
108
+ });
109
+ let stdout = "";
110
+ let stderr = "";
111
+ child.stdout.on("data", (d) => stdout += d);
112
+ child.stderr.on("data", (d) => stderr += d);
113
+ child.on("error", reject);
114
+ child.on("close", (code) => resolve3({ code: code ?? 1, stdout, stderr }));
115
+ });
116
+ }
117
+ var runShell = (command, cwd) => exec(command, [], { cwd, shell: true });
118
+
119
+ // src/model/runtime.ts
120
+ var LLAMA_VERSION = "3.22.1";
121
+ var runtimeDir = () => join3(configDir(), "runtime");
122
+ function platformPackage(platform = process.platform, arch = process.arch) {
123
+ const os = platform === "win32" ? "win" : platform === "darwin" ? "mac" : platform;
124
+ const cpu = arch === "arm" ? "armv7l" : arch;
125
+ if (os === "mac" && cpu === "arm64") return "@node-llama-cpp/mac-arm64-metal";
126
+ const supported = ["win-x64", "win-arm64", "mac-x64", "linux-x64", "linux-arm64", "linux-armv7l", "linux-riscv64"];
127
+ const id = `${os}-${cpu}`;
128
+ if (!supported.includes(id)) throw new Error(`The built-in runtime has no prebuilt binary for ${platform}-${arch}. Use Ollama instead.`);
129
+ return `@node-llama-cpp/${id}`;
130
+ }
131
+ var entryPath = () => join3(runtimeDir(), "node_modules", "node-llama-cpp", "package.json");
132
+ var isRuntimeInstalled = () => existsSync2(entryPath());
133
+ async function installRuntime(log2 = () => {
134
+ }) {
135
+ if (Number(process.versions.node.split(".")[0]) < 20) {
136
+ throw new Error("The built-in runtime needs Node.js 20 or newer. Upgrade Node, or use Ollama.");
137
+ }
138
+ const dir = runtimeDir();
139
+ 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));
142
+ 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()}`);
146
+ }
147
+ var cached;
148
+ function loadRuntime() {
149
+ cached ??= (async () => {
150
+ const entry = createRequire(join3(runtimeDir(), "package.json")).resolve("node-llama-cpp");
151
+ return await import(pathToFileURL(entry).href);
152
+ })();
153
+ return cached;
154
+ }
155
+
156
+ // src/model/setup.ts
157
+ import chalk from "chalk";
158
+
159
+ // src/model/download.ts
160
+ import { createHash } from "crypto";
161
+ import { createReadStream, createWriteStream, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync, statSync, unlinkSync } from "fs";
162
+ import { dirname as dirname2 } from "path";
163
+ import { Readable } from "stream";
164
+ import { pipeline } from "stream/promises";
165
+ var mb = (n) => (n / 1024 / 1024).toFixed(0);
166
+ function formatProgress(p, width = 24) {
167
+ const speed = `${(p.bytesPerSecond / 1024 / 1024).toFixed(1)} MB/s`;
168
+ if (!p.total) return `${mb(p.received)} MB ${speed}`;
169
+ const ratio = Math.min(1, p.received / p.total);
170
+ const filled = Math.round(ratio * width);
171
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}] ${(ratio * 100).toFixed(0).padStart(3)}% ${mb(p.received)}/${mb(p.total)} MB ${speed}`;
172
+ }
173
+ async function sha256File(path) {
174
+ const hash = createHash("sha256");
175
+ await pipeline(createReadStream(path), hash);
176
+ return hash.digest("hex");
177
+ }
178
+ var HF_RE = /^https:\/\/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/([^/]+)\/([^/]+)$/;
179
+ async function hubChecksum(url, fetchImpl = fetch) {
180
+ const m = HF_RE.exec(url);
181
+ if (!m) return {};
182
+ try {
183
+ const res = await fetchImpl(`https://huggingface.co/api/models/${m[1]}/tree/${m[2]}`);
184
+ if (!res.ok) return {};
185
+ const files = await res.json();
186
+ const lfs = files.find((f) => f.path === decodeURIComponent(m[3]))?.lfs;
187
+ return lfs ? { sha256: lfs.oid, size: lfs.size } : {};
188
+ } catch {
189
+ return {};
190
+ }
191
+ }
192
+ async function downloadModel(model2, onProgress, fetchImpl = fetch) {
193
+ const target = modelPath(model2);
194
+ if (existsSync3(target)) return target;
195
+ const spec = model2.sha256 ? model2 : { ...model2, ...await hubChecksum(model2.url, fetchImpl) };
196
+ mkdirSync3(dirname2(target), { recursive: true });
197
+ const part = `${target}.part`;
198
+ const offset = existsSync3(part) ? statSync(part).size : 0;
199
+ const res = await fetchImpl(spec.url, { headers: offset ? { Range: `bytes=${offset}-` } : {} });
200
+ if (res.status === 404) throw new Error(`Model ${spec.id} is not published yet (${spec.url})`);
201
+ if (!res.ok || !res.body) throw new Error(`Download failed: HTTP ${res.status} for ${spec.url}`);
202
+ const resumed = res.status === 206;
203
+ const start = resumed ? offset : 0;
204
+ const length = Number(res.headers.get("content-length")) || void 0;
205
+ const total = spec.size ?? (length ? start + length : void 0);
206
+ let received = start;
207
+ const began = Date.now();
208
+ const counter = async function* (source) {
209
+ for await (const chunk of source) {
210
+ received += chunk.length;
211
+ const seconds = Math.max(1e-3, (Date.now() - began) / 1e3);
212
+ onProgress?.({ received, total, bytesPerSecond: (received - start) / seconds });
213
+ yield chunk;
214
+ }
215
+ };
216
+ await pipeline(Readable.fromWeb(res.body), counter, createWriteStream(part, { flags: resumed ? "a" : "w" }));
217
+ if (spec.sha256) {
218
+ const actual = await sha256File(part);
219
+ if (actual !== spec.sha256) {
220
+ unlinkSync(part);
221
+ throw new Error(`Checksum mismatch for ${spec.file} (expected ${spec.sha256}, got ${actual}). The partial file was removed; run the command again.`);
222
+ }
223
+ }
224
+ renameSync(part, target);
225
+ return target;
226
+ }
227
+
228
+ // src/model/setup.ts
229
+ var log = (msg) => process.stderr.write(chalk.dim(msg) + "\n");
230
+ async function prepareLocal(spec) {
231
+ if (!isRuntimeInstalled()) await installRuntime(log);
232
+ if (isDownloaded(spec)) return modelPath(spec);
233
+ log(`Downloading ${spec.id} (${spec.size ? `${(spec.size / 1024 ** 3).toFixed(1)} GB, ` : ""}one time)\u2026`);
234
+ const tty = process.stderr.isTTY;
235
+ let lastLog = 0;
236
+ const path = await downloadModel(spec, (p) => {
237
+ if (tty) process.stderr.write(`\r${formatProgress(p)}`);
238
+ else if (Date.now() - lastLog > 5e3) {
239
+ lastLog = Date.now();
240
+ log(formatProgress(p));
241
+ }
242
+ });
243
+ if (tty) process.stderr.write("\n");
244
+ log(`Saved to ${path}`);
245
+ return path;
246
+ }
247
+
248
+ // src/llm/provider.ts
249
+ var ProviderError = class extends Error {
250
+ constructor(message, hint) {
251
+ super(message);
252
+ this.hint = hint;
253
+ this.name = "ProviderError";
254
+ }
255
+ hint;
256
+ };
257
+
258
+ // src/llm/local.ts
259
+ var loaded = /* @__PURE__ */ new Map();
260
+ async function loadModel(path) {
261
+ let model2 = loaded.get(path);
262
+ if (!model2) {
263
+ model2 = loadRuntime().then(async (rt) => {
264
+ const llama = await rt.getLlama({ gpu: process.platform === "darwin" ? "auto" : false, build: "never", logLevel: "error" });
265
+ return llama.loadModel({ modelPath: path });
266
+ });
267
+ model2.catch(() => loaded.delete(path));
268
+ loaded.set(path, model2);
269
+ }
270
+ return model2;
271
+ }
272
+ var LocalProvider = class {
273
+ constructor(model2) {
274
+ this.model = model2;
275
+ }
276
+ model;
277
+ name = "local";
278
+ async check() {
279
+ const spec = resolveModel(this.model);
280
+ if (!isRuntimeInstalled()) throw new ProviderError("Local runtime is not installed", "Run `eventa model pull` (or any command, it installs on first use)");
281
+ if (!isDownloaded(spec)) throw new ProviderError(`Local model ${spec.id} is not downloaded`, `Run \`eventa model pull ${spec.id}\``);
282
+ }
283
+ async prepare() {
284
+ await loadModel(await prepareLocal(resolveModel(this.model)));
285
+ }
286
+ async *generate(prompt, opts = {}) {
287
+ const path = await prepareLocal(resolveModel(this.model));
288
+ const rt = await loadRuntime();
289
+ const model2 = await loadModel(path);
290
+ const context = await model2.createContext({ contextSize: opts.numCtx });
291
+ const session = new rt.LlamaChatSession({ contextSequence: context.getSequence(), systemPrompt: opts.system });
292
+ const chunks = [];
293
+ let done = false;
294
+ let failure;
295
+ let wake = null;
296
+ const notify = () => {
297
+ wake?.();
298
+ wake = null;
299
+ };
300
+ session.prompt(prompt, {
301
+ temperature: opts.temperature,
302
+ maxTokens: opts.maxTokens ?? 1024,
303
+ signal: opts.signal,
304
+ onTextChunk: (t) => {
305
+ chunks.push(t);
306
+ notify();
307
+ }
308
+ }).catch((err) => failure = err).finally(() => {
309
+ done = true;
310
+ notify();
311
+ });
312
+ try {
313
+ while (true) {
314
+ if (chunks.length) yield chunks.shift();
315
+ else if (done) break;
316
+ else await new Promise((r) => wake = r);
317
+ }
318
+ if (failure) throw new ProviderError(`Local model error: ${failure.message}`);
319
+ } finally {
320
+ session.dispose();
321
+ await context.dispose();
322
+ }
323
+ }
324
+ };
325
+
326
+ // src/llm/ollama.ts
327
+ async function* parseNdjson(body) {
328
+ const decoder = new TextDecoder();
329
+ let buffer = "";
330
+ for await (const chunk of body) {
331
+ buffer += decoder.decode(chunk, { stream: true });
332
+ let nl;
333
+ while ((nl = buffer.indexOf("\n")) >= 0) {
334
+ const line = buffer.slice(0, nl).trim();
335
+ buffer = buffer.slice(nl + 1);
336
+ if (line) yield JSON.parse(line);
337
+ }
338
+ }
339
+ const rest = buffer.trim();
340
+ if (rest) yield JSON.parse(rest);
341
+ }
342
+ var OllamaProvider = class {
343
+ constructor(model2, host, fetchImpl = fetch) {
344
+ this.model = model2;
345
+ this.host = host;
346
+ this.fetchImpl = fetchImpl;
347
+ }
348
+ model;
349
+ host;
350
+ fetchImpl;
351
+ name = "ollama";
352
+ unreachable() {
353
+ return new ProviderError(`Cannot reach Ollama at ${this.host}`, "Start it with `ollama serve`, or install it from https://ollama.com");
354
+ }
355
+ async check() {
356
+ let res;
357
+ try {
358
+ res = await this.fetchImpl(`${this.host}/api/tags`, { signal: AbortSignal.timeout(2e3) });
359
+ } catch {
360
+ throw this.unreachable();
361
+ }
362
+ const { models = [] } = await res.json();
363
+ const wanted = this.model.includes(":") ? this.model : `${this.model}:latest`;
364
+ if (!models.some((m) => m.name === wanted || m.name === this.model)) {
365
+ throw new ProviderError(`Model "${this.model}" is not installed in Ollama`, `Run \`ollama pull ${this.model}\``);
366
+ }
367
+ }
368
+ async *generate(prompt, opts = {}) {
369
+ let res;
370
+ try {
371
+ res = await this.fetchImpl(`${this.host}/api/generate`, {
372
+ method: "POST",
373
+ headers: { "content-type": "application/json" },
374
+ body: JSON.stringify({
375
+ model: this.model,
376
+ prompt,
377
+ system: opts.system,
378
+ stream: true,
379
+ options: { temperature: opts.temperature, num_ctx: opts.numCtx, num_predict: opts.maxTokens ?? 1024 }
380
+ }),
381
+ signal: opts.signal
382
+ });
383
+ } catch (err) {
384
+ if (err.name === "AbortError") return;
385
+ throw this.unreachable();
386
+ }
387
+ if (res.status === 404) {
388
+ throw new ProviderError(`Model "${this.model}" is not installed in Ollama`, `Run \`ollama pull ${this.model}\``);
389
+ }
390
+ if (!res.ok || !res.body) throw new ProviderError(`Ollama returned HTTP ${res.status}: ${await res.text()}`);
391
+ for await (const line of parseNdjson(res.body)) {
392
+ if (line.error) throw new ProviderError(`Ollama error: ${line.error}`);
393
+ if (line.response) yield line.response;
394
+ if (line.done) return;
395
+ }
396
+ }
397
+ };
398
+
399
+ // src/llm/index.ts
400
+ var ollamaFor = (config2) => new OllamaProvider(config2.model, config2.ollamaHost.replace(/\/+$/, ""));
401
+ async function createProvider(config2) {
402
+ if (config2.provider === "ollama") return ollamaFor(config2);
403
+ if (config2.provider === "local") return new LocalProvider(config2.localModel);
404
+ const ollama = ollamaFor(config2);
405
+ try {
406
+ await ollama.check();
407
+ return ollama;
408
+ } catch {
409
+ return new LocalProvider(config2.localModel);
410
+ }
411
+ }
412
+
413
+ // src/prompts/index.ts
414
+ var SYSTEM_PROMPT = "You are Eventa, an expert Node.js engineer. Answer concisely and accurately. Prefer modern Node.js (ESM, async/await, node: built-ins). Use markdown and fenced code blocks.";
415
+ var toPrompt = ({ instruction, input }) => input ? `${instruction}
416
+
417
+ ${input}` : instruction;
418
+
419
+ // src/ui/render.ts
420
+ import chalk2 from "chalk";
421
+ import ora from "ora";
422
+ function formatLine(line, state) {
423
+ if (line.trimStart().startsWith("```")) {
424
+ state.inCode = !state.inCode;
425
+ return chalk2.dim(line);
426
+ }
427
+ if (state.inCode) return chalk2.cyan(line);
428
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
429
+ if (heading) return chalk2.bold.underline(heading[2]);
430
+ return line.replace(/\*\*([^*]+)\*\*/g, (_, t) => chalk2.bold(t)).replace(/`([^`]+)`/g, (_, t) => chalk2.yellow(t)).replace(/^(\s*)[-*]\s+/, (_, s) => `${s}\u2022 `);
431
+ }
432
+ async function renderStream(tokens, opts = {}) {
433
+ const out = opts.out ?? process.stdout;
434
+ const pretty = !opts.raw && Boolean(out.isTTY);
435
+ const spinner = pretty ? ora({ text: "Thinking\u2026", stream: process.stderr }).start() : null;
436
+ const state = { inCode: false };
437
+ let full = "";
438
+ let pending = "";
439
+ try {
440
+ for await (const token of tokens) {
441
+ spinner?.stop();
442
+ full += token;
443
+ if (!pretty) {
444
+ out.write(token);
445
+ continue;
446
+ }
447
+ pending += token;
448
+ let nl;
449
+ while ((nl = pending.indexOf("\n")) >= 0) {
450
+ out.write(formatLine(pending.slice(0, nl), state) + "\n");
451
+ pending = pending.slice(nl + 1);
452
+ }
453
+ }
454
+ if (pretty && pending) out.write(formatLine(pending, state));
455
+ if (!full.endsWith("\n")) out.write("\n");
456
+ } finally {
457
+ spinner?.stop();
458
+ }
459
+ return full;
460
+ }
461
+
462
+ // src/commands/shared.ts
463
+ async function generate(config2, parts, opts = {}) {
464
+ const provider = await createProvider(config2);
465
+ await provider.prepare?.();
466
+ const tokens = provider.generate(toPrompt(parts), {
467
+ system: opts.system ?? SYSTEM_PROMPT,
468
+ temperature: config2.temperature,
469
+ numCtx: config2.contextSize
470
+ });
471
+ if (opts.json) {
472
+ let text = "";
473
+ for await (const t of tokens) text += t;
474
+ return text.trim();
475
+ }
476
+ return renderStream(tokens, { raw: opts.raw });
477
+ }
478
+ async function readStdin() {
479
+ if (process.stdin.isTTY) return "";
480
+ let data = "";
481
+ for await (const chunk of process.stdin) data += chunk;
482
+ return data;
483
+ }
484
+ var printJson = (value) => console.log(JSON.stringify(value, null, 2));
485
+
486
+ // src/commands/ask.ts
487
+ async function askCommand(question, config2, opts) {
488
+ await generate(config2, { instruction: question, input: "" }, { raw: opts.raw });
489
+ }
490
+
491
+ // src/commands/config.ts
492
+ function assertKey(key) {
493
+ if (!CONFIG_KEYS.includes(key)) {
494
+ throw new Error(`Unknown config key "${key}". Valid keys: ${CONFIG_KEYS.join(", ")}`);
495
+ }
496
+ }
497
+ function configGet(key) {
498
+ const config2 = loadConfig();
499
+ if (!key) {
500
+ console.log(JSON.stringify(config2, null, 2));
501
+ return;
502
+ }
503
+ assertKey(key);
504
+ console.log(String(config2[key]));
505
+ }
506
+ function configSet(key, value) {
507
+ assertKey(key);
508
+ setConfigValue(key, value);
509
+ console.log(`${key} = ${value}`);
510
+ }
511
+ function configWhere() {
512
+ console.log(configPath());
513
+ }
514
+
515
+ // src/commands/deps.ts
516
+ import chalk3 from "chalk";
517
+ import ora2 from "ora";
518
+
519
+ // src/context/deps.ts
520
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
521
+ import { builtinModules } from "module";
522
+ import { extname, join as join4 } from "path";
523
+ var SEVERITY_ORDER = ["critical", "high", "moderate", "low", "info"];
524
+ var SOURCE_EXT = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx", ".vue", ".svelte"]);
525
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".nuxt", "out", ".turbo", ".cache"]);
526
+ var IMPORT_RE = /(?:import|export)\s[^'"]*?from\s*['"]([^'"]+)['"]|import\s*\(\s*['"]([^'"]+)['"]\s*\)|require\s*\(\s*['"]([^'"]+)['"]\s*\)|import\s+['"]([^'"]+)['"]/g;
527
+ var BUILTINS = new Set(builtinModules);
528
+ function readPackageJson(cwd) {
529
+ try {
530
+ return JSON.parse(readFileSync2(join4(cwd, "package.json"), "utf8"));
531
+ } catch {
532
+ throw new Error(`No readable package.json in ${cwd}`);
533
+ }
534
+ }
535
+ function parseJson(text) {
536
+ try {
537
+ return JSON.parse(text);
538
+ } catch {
539
+ return void 0;
540
+ }
541
+ }
542
+ function parseAudit(text) {
543
+ const json = parseJson(text);
544
+ if (!json) return { vulnerabilities: [], error: "npm audit returned no JSON" };
545
+ if (json.error) return { vulnerabilities: [], error: json.error.summary ?? json.error.code };
546
+ const vulnerabilities = Object.entries(json.vulnerabilities ?? {}).map(([name, v]) => {
547
+ const advisory = v.via.find((x) => typeof x === "object");
548
+ const fix = v.fixAvailable === true ? "npm audit fix" : v.fixAvailable ? `npm install ${v.fixAvailable.name}@${v.fixAvailable.version}${v.fixAvailable.isSemVerMajor ? " (major)" : ""}` : void 0;
549
+ return { name, severity: v.severity, title: advisory?.title ?? (v.via.length ? `via ${v.via.join(", ")}` : void 0), url: advisory?.url, range: v.range, fix };
550
+ });
551
+ vulnerabilities.sort((a, b) => SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity));
552
+ return { vulnerabilities };
553
+ }
554
+ function fixPlan(vulns) {
555
+ const groups = /* @__PURE__ */ new Map();
556
+ for (const v of vulns) {
557
+ const command = v.fix ?? "no fix available";
558
+ groups.set(command, [...groups.get(command) ?? [], v.name]);
559
+ }
560
+ return [...groups].map(([command, fixes]) => ({ command, fixes }));
561
+ }
562
+ function parseOutdated(text) {
563
+ const json = parseJson(text) ?? {};
564
+ return Object.entries(json).map(([name, v]) => ({
565
+ name,
566
+ current: v.current,
567
+ wanted: v.wanted,
568
+ latest: v.latest,
569
+ major: Boolean(v.current && v.latest && v.current.split(".")[0] !== v.latest.split(".")[0])
570
+ }));
571
+ }
572
+ function packageName(specifier) {
573
+ if (/^(\.|\/|#|~|@\/|[a-z]+:)/i.test(specifier) || BUILTINS.has(specifier)) return void 0;
574
+ const parts = specifier.split("/");
575
+ const name = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
576
+ if (BUILTINS.has(name) || !/^(@[\w.-]+\/)?[\w.-]+$/.test(name)) return void 0;
577
+ return name;
578
+ }
579
+ function scanImports(cwd, maxFiles = 3e3) {
580
+ const found = /* @__PURE__ */ new Set();
581
+ let count = 0;
582
+ const walk = (dir) => {
583
+ let entries;
584
+ try {
585
+ entries = readdirSync(dir, { withFileTypes: true });
586
+ } catch {
587
+ return;
588
+ }
589
+ for (const e of entries) {
590
+ if (count >= maxFiles) return;
591
+ if (e.isDirectory()) {
592
+ 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)) {
594
+ count++;
595
+ const text = readFileSync2(join4(dir, e.name), "utf8");
596
+ for (const m of text.matchAll(IMPORT_RE)) {
597
+ const name = packageName(m[1] ?? m[2] ?? m[3] ?? m[4]);
598
+ if (name) found.add(name);
599
+ }
600
+ }
601
+ }
602
+ };
603
+ walk(cwd);
604
+ return found;
605
+ }
606
+ function binNames(cwd, dep) {
607
+ let text;
608
+ try {
609
+ text = readFileSync2(join4(cwd, "node_modules", dep, "package.json"), "utf8");
610
+ } catch {
611
+ return [];
612
+ }
613
+ const pkg = parseJson(text);
614
+ if (!pkg?.bin) return [];
615
+ return typeof pkg.bin === "string" ? [dep.split("/").pop()] : Object.keys(pkg.bin);
616
+ }
617
+ function findUnusedAndMissing(pkg, imported, cwd = process.cwd()) {
618
+ const scriptTokens = new Set(Object.values(pkg.scripts ?? {}).join(" ").split(/[\s&|;()'"=]+/));
619
+ const usedInScripts = (d) => scriptTokens.has(d) || binNames(cwd, d).some((b) => scriptTokens.has(b));
620
+ const declared = /* @__PURE__ */ new Set([
621
+ ...Object.keys(pkg.dependencies ?? {}),
622
+ ...Object.keys(pkg.devDependencies ?? {}),
623
+ ...Object.keys(pkg.peerDependencies ?? {}),
624
+ ...Object.keys(pkg.optionalDependencies ?? {})
625
+ ]);
626
+ const unused = Object.keys(pkg.dependencies ?? {}).filter(
627
+ (d) => !imported.has(d) && !d.startsWith("@types/") && !usedInScripts(d)
628
+ );
629
+ const missing = [...imported].filter((d) => !declared.has(d) && d !== pkg.name && !declared.has(`@types/${d}`)).sort();
630
+ return { unused, missing };
631
+ }
632
+ async function collectDeps(cwd) {
633
+ const pkg = readPackageJson(cwd);
634
+ const [audit, outdated] = await Promise.all([
635
+ exec("npm", ["audit", "--json"], { cwd }),
636
+ exec("npm", ["outdated", "--json"], { cwd })
637
+ ]);
638
+ const { vulnerabilities, error } = parseAudit(audit.stdout);
639
+ return {
640
+ vulnerabilities,
641
+ auditError: error,
642
+ outdated: parseOutdated(outdated.stdout),
643
+ ...findUnusedAndMissing(pkg, scanImports(cwd), cwd)
644
+ };
645
+ }
646
+
647
+ // src/prompts/deps.ts
648
+ function depsPrompt(r, nodeVersion) {
649
+ const lines = [`Node ${nodeVersion}`];
650
+ if (r.vulnerabilities.length) {
651
+ lines.push("Vulnerabilities:", ...r.vulnerabilities.slice(0, 20).map((v) => `- ${v.name} [${v.severity}] ${v.title ?? ""}`));
652
+ lines.push("Fix commands (already computed and correct):", ...fixPlan(r.vulnerabilities).map((f) => `- ${f.command} \u2192 fixes ${f.fixes.join(", ")}`));
653
+ }
654
+ if (r.outdated.length) {
655
+ lines.push("Outdated:", ...r.outdated.slice(0, 25).map((o) => `- ${o.name} ${o.current ?? "not installed"} \u2192 latest ${o.latest}${o.major ? " (MAJOR)" : ""}`));
656
+ }
657
+ if (r.unused.length) lines.push(`Possibly unused dependencies: ${r.unused.join(", ")}`);
658
+ if (r.missing.length) lines.push(`Imported but not declared in package.json: ${r.missing.join(", ")}`);
659
+ 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, the main breaking-change risk you know of and what to check.\n3. Cleanup: unused packages to remove and missing packages to add.\nDo not repeat the raw lists. Do not invent versions or advisories. Skip any section with nothing to do.",
661
+ input: lines.join("\n")
662
+ };
663
+ }
664
+
665
+ // src/commands/deps.ts
666
+ var SEVERITY_COLOR = {
667
+ critical: chalk3.bgRed.white,
668
+ high: chalk3.red,
669
+ moderate: chalk3.yellow,
670
+ low: chalk3.blue,
671
+ info: chalk3.dim
672
+ };
673
+ var isHealthy = (r) => !r.vulnerabilities.length && !r.outdated.length && !r.unused.length && !r.missing.length;
674
+ function printReport(r) {
675
+ if (r.auditError) console.log(chalk3.yellow(`npm audit: ${r.auditError}`));
676
+ if (r.vulnerabilities.length) {
677
+ console.log(chalk3.bold(`Vulnerabilities (${r.vulnerabilities.length})`));
678
+ for (const v of r.vulnerabilities) {
679
+ const color = SEVERITY_COLOR[v.severity] ?? chalk3.white;
680
+ console.log(` ${color(v.severity.padEnd(8))} ${v.name.padEnd(20)} ${chalk3.dim(v.title ?? "")}`);
681
+ }
682
+ console.log(chalk3.bold("\nFix"));
683
+ for (const f of fixPlan(r.vulnerabilities)) console.log(` ${chalk3.green(f.command)} ${chalk3.dim(`\u2192 ${f.fixes.join(", ")}`)}`);
684
+ console.log();
685
+ }
686
+ if (r.outdated.length) {
687
+ console.log(chalk3.bold(`Outdated (${r.outdated.length})`));
688
+ for (const o of r.outdated) {
689
+ const latest = o.major ? chalk3.red(`${o.latest} major`) : chalk3.green(o.latest);
690
+ console.log(` ${o.name.padEnd(28)} ${(o.current ?? "missing").padEnd(10)} \u2192 ${latest}`);
691
+ }
692
+ console.log();
693
+ }
694
+ if (r.unused.length) console.log(`${chalk3.bold("Possibly unused:")} ${r.unused.join(", ")}
695
+ `);
696
+ if (r.missing.length) console.log(`${chalk3.bold("Imported but not in package.json:")} ${r.missing.join(", ")}
697
+ `);
698
+ }
699
+ async function depsCommand(config2, opts) {
700
+ const cwd = process.cwd();
701
+ readPackageJson(cwd);
702
+ const spinner = !opts.json && process.stderr.isTTY ? ora2({ text: "Running npm audit and npm outdated\u2026", stream: process.stderr }).start() : null;
703
+ const report = await collectDeps(cwd).finally(() => spinner?.stop());
704
+ const wantAi = opts.ai && !isHealthy(report);
705
+ if (opts.json) {
706
+ const analysis = wantAi ? await generate(config2, depsPrompt(report, process.versions.node), { json: true }) : void 0;
707
+ printJson({ ...report, analysis });
708
+ return;
709
+ }
710
+ printReport(report);
711
+ if (isHealthy(report)) {
712
+ console.log(chalk3.green("\u2714 Dependencies look healthy."));
713
+ return;
714
+ }
715
+ if (wantAi) {
716
+ console.log(chalk3.bold.underline("Advice"));
717
+ await generate(config2, depsPrompt(report, process.versions.node), opts);
718
+ }
719
+ }
720
+
721
+ // src/commands/doctor.ts
722
+ import chalk4 from "chalk";
723
+ var ok = (msg) => console.log(`${chalk4.green("\u2714")} ${msg}`);
724
+ var fail = (msg, hint) => console.log(`${chalk4.red("\u2716")} ${msg}${hint ? chalk4.dim(`
725
+ ${hint}`) : ""}`);
726
+ async function probe(name, check) {
727
+ try {
728
+ await check();
729
+ ok(name);
730
+ return true;
731
+ } catch (err) {
732
+ const e = err;
733
+ fail(`${name}: ${e.message}`, e.hint);
734
+ return false;
735
+ }
736
+ }
737
+ async function doctorCommand(config2) {
738
+ const major = Number(process.versions.node.split(".")[0]);
739
+ if (major >= 20) ok(`Node.js ${process.versions.node}`);
740
+ else fail(`Node.js ${process.versions.node}`, "Eventa needs Node.js 20 or newer");
741
+ const ollamaOk = await probe(`Ollama (${config2.model})`, () => new OllamaProvider(config2.model, config2.ollamaHost).check());
742
+ const localOk = await probe(`Built-in runtime (${config2.localModel})`, () => new LocalProvider(config2.localModel).check());
743
+ const active = await createProvider(config2);
744
+ console.log(chalk4.dim(`
745
+ Provider setting: ${config2.provider} \u2192 using ${active.name} (${active.model})`));
746
+ 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 major >= 20 && (ollamaOk || localOk);
748
+ }
749
+
750
+ // src/commands/model.ts
751
+ import { rmSync } from "fs";
752
+ import chalk5 from "chalk";
753
+ function modelList() {
754
+ const { localModel } = loadConfig();
755
+ console.log(chalk5.bold("Built-in models"));
756
+ for (const spec of Object.values(MODELS)) {
757
+ const mark = spec.id === localModel ? chalk5.green("\u25CF") : " ";
758
+ const state = isDownloaded(spec) ? chalk5.green("downloaded") : chalk5.dim("not downloaded");
759
+ console.log(`${mark} ${spec.id.padEnd(22)} ${state.padEnd(24)} ${chalk5.dim(spec.description)}`);
760
+ }
761
+ console.log(chalk5.dim(`
762
+ Runtime: ${isRuntimeInstalled() ? `installed in ${runtimeDir()}` : "not installed"}`));
763
+ }
764
+ async function modelPull(id) {
765
+ const spec = resolveModel(id ?? loadConfig().localModel);
766
+ const path = await prepareLocal(spec);
767
+ console.log(chalk5.green(`\u2714 ${spec.id} is ready (${path})`));
768
+ }
769
+ function modelUse(id) {
770
+ const spec = resolveModel(id);
771
+ setConfigValue("localModel", MODELS[spec.id] ? spec.id : spec.url || spec.file);
772
+ console.log(`localModel = ${spec.id}${isDownloaded(spec) ? "" : chalk5.dim(" (downloads on first use, or run `eventa model pull`)")}`);
773
+ }
774
+ function modelRemove(id) {
775
+ const spec = resolveModel(id);
776
+ if (!spec.url) throw new Error("Refusing to delete a local file you provided yourself.");
777
+ rmSync(modelPath(spec), { force: true });
778
+ rmSync(`${modelPath(spec)}.part`, { force: true });
779
+ console.log(`Removed ${spec.id}`);
780
+ }
781
+
782
+ // src/commands/explain.ts
783
+ import { readFileSync as readFileSync4 } from "fs";
784
+ import { relative as relative2 } from "path";
785
+ import chalk6 from "chalk";
786
+
787
+ // src/context/stackParser.ts
788
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
789
+ import { isAbsolute, relative, resolve as resolve2 } from "path";
790
+ import { fileURLToPath } from "url";
791
+ var FRAME_RE = /^\s*at\s+(?:async\s+(?=[^(]*$))?(?:(.+?)\s+\()?((?:file:\/\/|node:|[A-Za-z]:[\\/]|[\\/.]).+?):(\d+)(?::(\d+))?\)?\s*$/;
792
+ var HEADER_RE = /^((?:file:\/\/\/?)?(?:[A-Za-z]:)?[^\s:]+\.(?:[cm]?[jt]sx?)):(\d+)$/;
793
+ var MESSAGE_RE = /^\s*(?:Uncaught\s+)?((?:[A-Z][A-Za-z]*)?Error(?:\s\[[A-Z0-9_]+\])?:\s.*)$/;
794
+ var CODE_RE = /\b(TS\d{4,5}|P[1-3]\d{3}|ERR_[A-Z0-9_]+|E(?:CONNREFUSED|CONNRESET|ADDRINUSE|ACCES|NOENT|PERM|TIMEDOUT|NOTFOUND|EXIST|ISDIR|MFILE|PIPE))\b/;
795
+ var TSC_RE = /^(.+?\.(?:[cm]?tsx?))(?:\((\d+),(\d+)\)|:(\d+):(\d+)) ?[:-] ?error (TS\d+): (.*)$/;
796
+ var ANSI_RE = /\u001b\[[0-9;]*m/g;
797
+ var PATTERN_CODES = [
798
+ [/Nest can't resolve dependencies/, "NEST_UNKNOWN_DEPENDENCIES"],
799
+ [/circular dependency/i, "NEST_CIRCULAR_DEPENDENCY"],
800
+ [/@prisma\/client did not initialize yet/, "PRISMA_NOT_GENERATED"]
801
+ ];
802
+ function toPath(file) {
803
+ if (file.startsWith("file://")) {
804
+ try {
805
+ return fileURLToPath(file);
806
+ } catch {
807
+ return file;
808
+ }
809
+ }
810
+ return file;
811
+ }
812
+ function frame(fn, file, line, column) {
813
+ const path = toPath(file);
814
+ return {
815
+ fn: fn?.trim() || void 0,
816
+ file: path,
817
+ line: Number(line),
818
+ column: column ? Number(column) : void 0,
819
+ internal: path.startsWith("node:") || path.startsWith("internal/"),
820
+ dependency: /[\\/]node_modules[\\/]/.test(path)
821
+ };
822
+ }
823
+ function parseStack(raw) {
824
+ const text = raw.replace(ANSI_RE, "");
825
+ const result = { frames: [] };
826
+ const seen = /* @__PURE__ */ new Map();
827
+ const add = (f) => {
828
+ const key = `${f.file}:${f.line}`;
829
+ const existing = seen.get(key);
830
+ if (existing) {
831
+ existing.fn ??= f.fn;
832
+ existing.column ??= f.column;
833
+ return;
834
+ }
835
+ seen.set(key, f);
836
+ result.frames.push(f);
837
+ };
838
+ for (const line of text.split(/\r?\n/)) {
839
+ const t = TSC_RE.exec(line.trim());
840
+ if (t) {
841
+ add(frame(void 0, t[1], t[2] ?? t[4], t[3] ?? t[5]));
842
+ result.message ??= `error ${t[6]}: ${t[7]}`;
843
+ continue;
844
+ }
845
+ const m = FRAME_RE.exec(line);
846
+ if (m) {
847
+ add(frame(m[1], m[2], m[3], m[4]));
848
+ continue;
849
+ }
850
+ const h = HEADER_RE.exec(line.trim());
851
+ if (h && result.frames.length === 0) add(frame(void 0, h[1], h[2]));
852
+ const msg = MESSAGE_RE.exec(line);
853
+ if (msg && !result.message) result.message = msg[1].trim();
854
+ }
855
+ result.code = PATTERN_CODES.find(([re]) => re.test(text))?.[1] ?? CODE_RE.exec(text)?.[1];
856
+ return result;
857
+ }
858
+ 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);
860
+ }
861
+ function readSnippet(file, marked, radius = 15, cwd = process.cwd()) {
862
+ const targets = (Array.isArray(marked) ? marked : [marked]).slice().sort((a, b) => a - b);
863
+ const lines = readFileSync3(file, "utf8").split(/\r?\n/);
864
+ const start = Math.max(1, targets[0] - radius);
865
+ const end = Math.min(lines.length, targets[targets.length - 1] + radius);
866
+ const body = [];
867
+ for (let n = start; n <= end; n++) {
868
+ body.push(targets.includes(n) ? `${lines[n - 1]} // <-- line ${n}, in the stack trace` : lines[n - 1]);
869
+ }
870
+ return `// ${relative(cwd, file) || file} (lines ${start}-${end})
871
+ ${body.join("\n")}`;
872
+ }
873
+ function snippetsFor(frames, radius, cwd) {
874
+ const byFile = /* @__PURE__ */ new Map();
875
+ for (const f of frames) {
876
+ const lines = byFile.get(f.file) ?? [];
877
+ const last = lines[lines.length - 1];
878
+ if (last === void 0 || Math.abs(f.line - last) <= radius * 2) lines.push(f.line);
879
+ byFile.set(f.file, lines);
880
+ }
881
+ return [...byFile].map(([file, lines]) => readSnippet(file, lines, radius, cwd));
882
+ }
883
+
884
+ // src/context/tokens.ts
885
+ var estimateTokens = (text) => Math.ceil(text.length / 3.5);
886
+ function truncateMiddle(text, maxTokens) {
887
+ const maxChars = Math.floor(maxTokens * 3.5);
888
+ if (text.length <= maxChars) return text;
889
+ const half = Math.floor((maxChars - 40) / 2);
890
+ return `${text.slice(0, half)}
891
+ \u2026 [${text.length - 2 * half} chars truncated] \u2026
892
+ ${text.slice(-half)}`;
893
+ }
894
+
895
+ // src/prompts/knowledge.ts
896
+ var ERROR_CODES = {
897
+ ERR_REQUIRE_ESM: 'require() of an ES module. Use import / dynamic import(), switch the file to ESM ("type": "module" or .mjs), or pin an older CJS version of the package.',
898
+ ERR_MODULE_NOT_FOUND: "ESM import could not be resolved. ESM needs full relative paths with extensions (./util.js) and the package must be installed.",
899
+ MODULE_NOT_FOUND: "require() could not find the module. Check the path, spelling, and that the package is installed in dependencies.",
900
+ ERR_UNKNOWN_FILE_EXTENSION: "Node cannot run this extension directly (often .ts). Use tsx/ts-node, compile first, or Node >= 22.6 with --experimental-strip-types.",
901
+ ERR_PACKAGE_PATH_NOT_EXPORTED: 'Importing a subpath that the package "exports" field does not expose. Import the public entry point instead.',
902
+ ERR_HTTP_HEADERS_SENT: "Response headers were already sent, usually res.send/res.json called twice. Return after sending, and check async branches.",
903
+ ERR_INVALID_ARG_TYPE: "A Node API received the wrong type (often undefined). Validate the value before the call.",
904
+ ERR_UNHANDLED_REJECTION: "A promise rejected with no handler. Add await inside try/catch or a .catch().",
905
+ ERR_OSSL_EVP_UNSUPPORTED: "OpenSSL 3 rejected a legacy algorithm, common with old webpack. Upgrade the tool, or use NODE_OPTIONS=--openssl-legacy-provider as a temporary workaround.",
906
+ ERR_INVALID_URL: "new URL() received a relative or malformed string. Provide an absolute URL or a base argument.",
907
+ ERR_STREAM_PREMATURE_CLOSE: "A stream closed before finishing. Use stream.pipeline() and handle errors on both ends.",
908
+ ERR_SOCKET_CONNECTION_TIMEOUT: "Socket connect timed out. Check the host/port, firewall, and the timeout settings.",
909
+ EADDRINUSE: "The port is already taken by another process. Stop it, or use a different PORT.",
910
+ ECONNREFUSED: "Nothing is listening at the target host:port (DB/API down or wrong address). Start the service or fix the connection config; in Docker use the service name instead of localhost.",
911
+ ECONNRESET: "The peer closed the connection abruptly. Add retries/timeouts and check the server and proxy logs.",
912
+ ENOTFOUND: "DNS lookup failed. Check the hostname and the network.",
913
+ ETIMEDOUT: "The operation timed out. Check network reachability and timeouts.",
914
+ ENOENT: "File or directory does not exist. Resolve paths from import.meta.dirname / __dirname, not from process.cwd().",
915
+ EACCES: "Permission denied. Check file permissions; ports < 1024 need elevated rights.",
916
+ EPERM: "Operation not permitted, often a file locked by another process on Windows or missing rights.",
917
+ EMFILE: "Too many open files. Close handles, limit concurrency, or use graceful-fs.",
918
+ TS2304: "Cannot find name: the identifier is not declared or imported. Import it, or install its @types package.",
919
+ TS2307: "Cannot find module: the path or package does not resolve, or it has no types. Fix the path, install the package, or add @types / a declaration.",
920
+ TS2322: "Type is not assignable: the value does not match the declared type. Fix the value or widen the type deliberately.",
921
+ TS2339: "Property does not exist on type: TypeScript does not know about the property. Fix the name, narrow the type, or extend the type definition.",
922
+ TS2345: "Argument type mismatch: the argument does not match the parameter type. Convert or validate the value before the call.",
923
+ TS2532: "Object is possibly undefined: narrow with a check, optional chaining, or a default before using it.",
924
+ TS18048: "Value is possibly undefined: narrow with a check, optional chaining, or a default before using it.",
925
+ TS18046: "Value is of type 'unknown' (often `catch (err)`): narrow with `err instanceof Error` before reading properties.",
926
+ TS2554: "Wrong number of arguments for the function signature.",
927
+ TS2580: `Cannot find a Node.js global such as 'process' or 'require': install @types/node (and add "node" to compilerOptions.types if types are restricted).`,
928
+ TS2591: `Cannot find a Node.js global such as 'process' or 'Buffer': install @types/node (and add "node" to compilerOptions.types if types are restricted).`,
929
+ TS2367: "The comparison can never be true because the two types do not overlap (e.g. number vs string). Convert one side first.",
930
+ TS2741: "A required property is missing from the object literal.",
931
+ TS2366: "The function has a return type but not every code path returns a value.",
932
+ TS7006: "Parameter implicitly has an 'any' type (noImplicitAny): add a type annotation.",
933
+ TS1308: "'await' is only allowed in async functions: mark the function async.",
934
+ TS2835: "Relative imports need explicit file extensions under node16/nodenext module resolution: write ./file.js.",
935
+ TS1484: "A type is imported with a value import while verbatimModuleSyntax is on: use `import type`.",
936
+ P1001: "Prisma cannot reach the database server: check DATABASE_URL, that the database is running, and network/Docker host names.",
937
+ P2002: "Prisma unique constraint failed: a row with the same unique field already exists. Handle it (e.g. return 409) or use upsert.",
938
+ P2003: "Prisma foreign key constraint failed: the referenced record does not exist.",
939
+ P2025: "Prisma record not found for update/delete (or findUniqueOrThrow): check that the record exists or catch P2025 and return 404.",
940
+ PRISMA_NOT_GENERATED: "@prisma/client was not generated: run `npx prisma generate` after installing or changing schema.prisma (add it to postinstall/build).",
941
+ NEST_UNKNOWN_DEPENDENCIES: "Nest can't inject a constructor argument: the provider is not in the module's providers, not exported by its module, or the class lacks @Injectable(). The `?` marks the missing argument.",
942
+ NEST_CIRCULAR_DEPENDENCY: "Two modules or providers import each other. Break the cycle (move shared code to a third module) or use forwardRef(() => X) on both sides."
943
+ };
944
+ var knowledgeFor = (code) => code ? ERROR_CODES[code] : void 0;
945
+
946
+ // src/prompts/explain.ts
947
+ function explainPrompt(ctx) {
948
+ const sections = [`Error output:
949
+ \`\`\`
950
+ ${ctx.errorText.trim()}
951
+ \`\`\``];
952
+ const lang = ctx.snippets.some((s) => /^\/\/ \S+\.[cm]?tsx?\b/.test(s)) ? "ts" : "js";
953
+ if (ctx.snippets.length) sections.push(`Source around the failing lines:
954
+ \`\`\`${lang}
955
+ ${ctx.snippets.join("\n\n")}
956
+ \`\`\``);
957
+ const known = knowledgeFor(ctx.code);
958
+ if (known) sections.push(`Reference for ${ctx.code}: ${known}`);
959
+ if (ctx.project) {
960
+ const deps = ctx.project.dependencies.slice(0, 30).join(", ") || "none";
961
+ sections.push(`Project: Node ${ctx.project.nodeVersion}, ${ctx.project.moduleType} modules, dependencies: ${deps}`);
962
+ }
963
+ return {
964
+ instruction: "Explain this Node.js / TypeScript error. Reply with exactly three sections:\n**Cause**: the root cause in 1-3 sentences. Name the exact expression that failed and why its value is wrong.\n**Fix**: only the changed lines as a fenced code block, then one sentence on why it works.\n**Prevent**: one short tip.\nBase the answer on the code shown. Do not invent files or APIs.",
965
+ input: sections.join("\n\n")
966
+ };
967
+ }
968
+
969
+ // src/commands/explain.ts
970
+ function projectInfo(cwd) {
971
+ try {
972
+ const pkg = readPackageJson(cwd);
973
+ return {
974
+ nodeVersion: process.versions.node,
975
+ moduleType: pkg.type === "module" ? "ESM" : "CommonJS",
976
+ dependencies: Object.keys(pkg.dependencies ?? {})
977
+ };
978
+ } catch {
979
+ return void 0;
980
+ }
981
+ }
982
+ async function collectErrorText(words, opts, cwd) {
983
+ if (opts.run) {
984
+ const r = await runShell(opts.run, cwd);
985
+ const output = `${r.stderr}
986
+ ${r.stdout}`.trim();
987
+ if (r.code === 0 && !parseStack(output).message) return null;
988
+ return output;
989
+ }
990
+ if (opts.file) return readFileSync4(opts.file, "utf8");
991
+ if (words.length) return words.join(" ");
992
+ return readStdin();
993
+ }
994
+ async function explainCommand(words, config2, opts) {
995
+ const cwd = process.cwd();
996
+ const errorText = await collectErrorText(words, opts, cwd);
997
+ if (errorText === null) {
998
+ console.log(chalk6.green("\u2714 Command exited cleanly, nothing to explain."));
999
+ return;
1000
+ }
1001
+ if (!errorText.trim()) {
1002
+ throw new Error('No error to explain. Pipe it in (node app.js 2>&1 | eventa explain), pass --file crash.log, or use --run "node app.js".');
1003
+ }
1004
+ const parsed = parseStack(errorText);
1005
+ const frames = userFrames(parsed, cwd);
1006
+ const budget = config2.contextSize - 1500;
1007
+ const snippets = snippetsFor(frames, 12, cwd);
1008
+ while (snippets.length > 1 && estimateTokens(snippets.join("\n")) > budget * 0.5) snippets.pop();
1009
+ const parts = explainPrompt({
1010
+ errorText: truncateMiddle(errorText, Math.floor(budget * 0.35)),
1011
+ code: parsed.code,
1012
+ snippets,
1013
+ project: projectInfo(cwd)
1014
+ });
1015
+ const where = frames.map((f) => `${relative2(cwd, f.file)}:${f.line}`);
1016
+ if (!opts.json) {
1017
+ console.log(chalk6.bold.red(parsed.message ?? errorText.trim().split("\n")[0]));
1018
+ if (where.length) console.log(chalk6.dim(`Looking at ${where.join(", ")}`));
1019
+ console.log();
1020
+ }
1021
+ const analysis = await generate(config2, parts, opts);
1022
+ if (opts.json) printJson({ message: parsed.message, code: parsed.code, frames: where, analysis });
1023
+ }
1024
+
1025
+ // src/commands/review.ts
1026
+ import chalk7 from "chalk";
1027
+
1028
+ // src/context/diff.ts
1029
+ var SKIP = [/(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?)$/, /(^|\/)(dist|build|coverage|node_modules)\//, /\.min\.(js|css)$/, /\.(png|jpe?g|gif|ico|woff2?|gguf|map)$/];
1030
+ var isReviewable = (path) => !SKIP.some((re) => re.test(path));
1031
+ function parseDiff(diff) {
1032
+ const files = [];
1033
+ let current = null;
1034
+ let newLine = 0;
1035
+ for (const line of diff.split(/\r?\n/)) {
1036
+ if (line.startsWith("diff --git ")) {
1037
+ const m = / b\/(.+)$/.exec(line);
1038
+ current = { path: m?.[1] ?? line, status: "modified", patch: "", added: [] };
1039
+ files.push(current);
1040
+ continue;
1041
+ }
1042
+ if (!current) continue;
1043
+ if (line.startsWith("new file mode")) current.status = "added";
1044
+ else if (line.startsWith("deleted file mode")) current.status = "deleted";
1045
+ else if (line.startsWith("rename to ")) current.status = "renamed";
1046
+ if (/^(index |--- |\+\+\+ |new file|deleted file|similarity|rename |old mode|new mode)/.test(line)) continue;
1047
+ if (line.startsWith("Binary files")) {
1048
+ current.patch = "";
1049
+ current.status = "deleted";
1050
+ continue;
1051
+ }
1052
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
1053
+ if (hunk) {
1054
+ newLine = Number(hunk[1]);
1055
+ current.patch += line + "\n";
1056
+ continue;
1057
+ }
1058
+ if (line.startsWith("+")) {
1059
+ current.added.push({ line: newLine, text: line.slice(1) });
1060
+ current.patch += `${String(newLine).padStart(4)} ${line}
1061
+ `;
1062
+ newLine++;
1063
+ } else if (line.startsWith("-")) {
1064
+ current.patch += ` ${line}
1065
+ `;
1066
+ } else if (line.startsWith(" ")) {
1067
+ current.patch += `${String(newLine).padStart(4)} ${line}
1068
+ `;
1069
+ newLine++;
1070
+ }
1071
+ }
1072
+ return files.filter((f) => f.status !== "deleted" && f.patch && isReviewable(f.path));
1073
+ }
1074
+ function splitPatch(patch, maxChars) {
1075
+ if (patch.length <= maxChars) return [patch];
1076
+ const hunks = patch.split(/(?=^@@ )/m);
1077
+ const parts = [];
1078
+ let buf = "";
1079
+ for (const h of hunks) {
1080
+ if (buf && buf.length + h.length > maxChars) {
1081
+ parts.push(buf);
1082
+ buf = "";
1083
+ }
1084
+ buf += h.length > maxChars ? h.slice(0, maxChars) + "\n\u2026 [hunk truncated]\n" : h;
1085
+ }
1086
+ if (buf) parts.push(buf);
1087
+ return parts;
1088
+ }
1089
+
1090
+ // src/context/heuristics.ts
1091
+ var RULES = [
1092
+ { rule: "eval", severity: "high", re: /\b(eval|new Function)\s*\(/, message: "dynamic code execution (eval / new Function)" },
1093
+ { rule: "shell-injection", severity: "high", re: /\bexec(Sync)?\s*\(\s*`[^`]*\$\{/, message: "child_process exec with an interpolated string, possible command injection" },
1094
+ { rule: "sql-injection", severity: "high", re: /\b(query|execute|raw)\s*\(\s*`\s*(SELECT|INSERT|UPDATE|DELETE)[^`]*\$\{/i, message: "SQL built with template interpolation, use parameters" },
1095
+ { rule: "sync-fs", severity: "medium", re: /\b(readFileSync|writeFileSync|readdirSync|existsSync|statSync)\s*\(/, message: "synchronous fs call, blocks the event loop if used in a request path" },
1096
+ { rule: "secret", severity: "high", re: /\b(api[_-]?key|secret|password|token)\b\s*[:=]\s*['"][^'"]{8,}['"]/i, message: "possible hard-coded secret" },
1097
+ { rule: "promise-no-await", severity: "high", re: /^\s*(?!return\b|await\b|const\b|let\b|var\b)[\w.]+\.(save|create|update\w*|delete\w*|insert\w*|send|fetch|query|find\w+|remove\w*)\s*\([^)]*\)\s*;?\s*$/, message: "async-looking call without await or return, result and errors are dropped" },
1098
+ { rule: "empty-catch", severity: "medium", re: /catch\s*(\(\w*\))?\s*\{\s*\}/, message: "empty catch swallows errors" },
1099
+ { rule: "then-no-catch", severity: "medium", re: /\.then\([^)]*\)\s*;\s*$/, message: ".then() without .catch(), possible unhandled rejection" },
1100
+ { rule: "console-log", severity: "low", re: /\bconsole\.log\(/, message: "leftover console.log" },
1101
+ { rule: "tls-off", severity: "high", re: /rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED/, message: "TLS certificate verification disabled" },
1102
+ { rule: "foreach-async", severity: "high", re: /\.forEach\(\s*async\b/, message: "forEach does not await async callbacks; the caller continues before they finish" },
1103
+ { rule: "jwt-decode", severity: "high", re: /\bjwt\.decode\(/, message: "jwt.decode does not verify the signature" },
1104
+ { rule: "prisma-raw-unsafe", severity: "high", re: /\$(queryRawUnsafe|executeRawUnsafe)\(\s*`[^`]*\$\{/, message: 'Prisma raw "Unsafe" query with interpolation, possible SQL injection' },
1105
+ { rule: "open-redirect", severity: "high", re: /\bredirect\(\s*req\.(query|body|params)\b/, message: "redirect target comes from user input (open redirect)" },
1106
+ { rule: "assign-in-if", severity: "high", re: /\bif\s*\(\s*[\w.$[\]'"]+\s*=(?![=>])/, message: "assignment inside an if condition, probably meant === " },
1107
+ { rule: "insecure-random", severity: "medium", re: /Math\.random\(\)\.toString\(\d+\)/, message: "Math.random() is not cryptographically secure; do not use it for tokens or IDs" },
1108
+ { rule: "parseint-unchecked", severity: "medium", re: /\bparseInt\(\s*[^,()]+\)/, message: "parseInt without radix; NaN and out-of-range values are not handled" },
1109
+ { rule: "sort-default", severity: "low", re: /\.sort\(\)/, message: "sort() without a comparator compares as strings (wrong for numbers)" },
1110
+ { rule: "weak-hash", severity: "medium", re: /createHash\(\s*['"](md5|sha1)['"]/, message: "md5/sha1 are weak; never use fast hashes for passwords" }
1111
+ ];
1112
+ var LOOP_START_RE = /^\s*(for|while)\s*\(|\.forEach\(|\.map\(\s*async/;
1113
+ var QUERY_RE = /\bawait\s+[\w.$]+\.(find\w*|query|count|aggregate|get\w*|load\w*)\(/;
1114
+ var braceDelta = (text) => (text.match(/\{/g)?.length ?? 0) - (text.match(/\}/g)?.length ?? 0);
1115
+ function scanAddedLines(added) {
1116
+ const hints = [];
1117
+ const push = (line, r) => hints.push({ line, rule: r.rule, severity: r.severity, message: r.message });
1118
+ let loopDepth = 0;
1119
+ let depth = 0;
1120
+ for (const { line, text } of added) {
1121
+ for (const r of RULES) if (r.re.test(text)) push(line, r);
1122
+ if (loopDepth && QUERY_RE.test(text)) push(line, { rule: "query-in-loop", severity: "medium", message: "database query inside a loop (N+1); batch it or load relations in one query" });
1123
+ if (!loopDepth && LOOP_START_RE.test(text)) loopDepth = depth + 1;
1124
+ depth += braceDelta(text);
1125
+ if (loopDepth && depth < loopDepth) loopDepth = 0;
1126
+ }
1127
+ return hints;
1128
+ }
1129
+
1130
+ // src/prompts/review.ts
1131
+ function reviewPrompt(path, patch, hints) {
1132
+ const sections = [`File: ${path}
1133
+ Diff (left column is the new line number, + added, - removed):
1134
+ \`\`\`diff
1135
+ ${patch.trimEnd()}
1136
+ \`\`\``];
1137
+ if (hints.length) sections.push(`Static checks flagged:
1138
+ ${hints.map((h) => `- [${h.severity}] line ${h.line}: ${h.message}`).join("\n")}`);
1139
+ return {
1140
+ instruction: 'Review this Node.js code change. Report only real problems in the added lines: bugs, missing await, unhandled promise rejections, error handling, security (injection, path traversal, secrets, unsafe TLS), and event-loop blocking work in request paths. Confirm or dismiss each static check.\nFormat each issue as: `- [high|medium|low] line N: problem \u2192 fix`. Add a short code block only when the fix is not obvious.\nSeverity: injection, secrets, disabled TLS and lost errors from a missing await are high; swallowed errors and blocking I/O are medium; style is low.\nWrite "No issues found." only when you list no issues at all.',
1141
+ input: sections.join("\n\n")
1142
+ };
1143
+ }
1144
+
1145
+ // src/commands/review.ts
1146
+ var SEVERITY = { high: chalk7.red, medium: chalk7.yellow, low: chalk7.dim };
1147
+ var git = (args, cwd) => exec("git", args, { cwd, shell: false });
1148
+ async function getDiff(cwd, base) {
1149
+ if ((await git(["rev-parse", "--is-inside-work-tree"], cwd)).code !== 0) throw new Error("Not inside a git repository.");
1150
+ const flags = ["diff", "--no-color", "--no-ext-diff", "-U3"];
1151
+ if (base) {
1152
+ const r = await git([...flags, `${base}...HEAD`], cwd);
1153
+ if (r.code !== 0) throw new Error(r.stderr.trim() || `git diff ${base}...HEAD failed`);
1154
+ return { diff: r.stdout, label: `changes since ${base}` };
1155
+ }
1156
+ const staged = await git([...flags, "--staged"], cwd);
1157
+ if (staged.stdout.trim()) return { diff: staged.stdout, label: "staged changes" };
1158
+ return { diff: (await git(flags, cwd)).stdout, label: "unstaged changes" };
1159
+ }
1160
+ async function reviewCommand(config2, opts) {
1161
+ const cwd = process.cwd();
1162
+ const { diff, label } = await getDiff(cwd, opts.base);
1163
+ const files = parseDiff(diff).slice(0, Number(opts.maxFiles) || 20);
1164
+ if (!files.length) {
1165
+ if (opts.json) printJson([]);
1166
+ else console.log(chalk7.yellow("Nothing to review: no changed source files found."));
1167
+ return;
1168
+ }
1169
+ if (!opts.json) console.log(chalk7.dim(`Reviewing ${label} in ${files.length} file(s)
1170
+ `));
1171
+ const maxChars = Math.min(12e3, Math.floor((config2.contextSize - 2e3) * 3.5));
1172
+ const results = [];
1173
+ for (const file of files) {
1174
+ const hints = scanAddedLines(file.added);
1175
+ const parts = splitPatch(file.patch, maxChars);
1176
+ for (const [i, part] of parts.entries()) {
1177
+ const partHints = hints.filter((h) => part.includes(`${String(h.line).padStart(4)} +`));
1178
+ if (!opts.json) {
1179
+ const suffix = parts.length > 1 ? chalk7.dim(` (part ${i + 1}/${parts.length})`) : "";
1180
+ console.log(chalk7.bold.cyan(`\u25B8 ${file.path}`) + suffix);
1181
+ if (partHints.length) {
1182
+ console.log(chalk7.bold("Static checks"));
1183
+ for (const h of partHints) console.log(` ${SEVERITY[h.severity](`[${h.severity}]`)} line ${h.line}: ${h.message}`);
1184
+ console.log(chalk7.bold("AI review"));
1185
+ }
1186
+ }
1187
+ const analysis = await generate(config2, reviewPrompt(file.path, part, partHints), opts);
1188
+ results.push({ file: file.path, hints: partHints, analysis });
1189
+ if (!opts.json && /^no issues found\.?$/i.test(analysis.trim()) && partHints.some((h) => h.severity !== "low")) {
1190
+ console.log(chalk7.yellow("The AI did not confirm the static checks above. Verify them yourself."));
1191
+ }
1192
+ if (!opts.json) console.log();
1193
+ }
1194
+ }
1195
+ if (opts.json) printJson(results);
1196
+ }
1197
+
1198
+ // src/index.ts
1199
+ var program = new Command();
1200
+ program.name("eventa").description("Offline AI assistant for Node.js developers").version("0.1.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");
1201
+ var configFrom = (cmd) => {
1202
+ const g = cmd.optsWithGlobals();
1203
+ if (g.provider && !["auto", "ollama", "local"].includes(g.provider)) throw new Error("--provider must be auto, ollama or local");
1204
+ return { config: loadConfig({ model: g.model, localModel: g.model, ollamaHost: g.host, provider: g.provider }), raw: g.raw };
1205
+ };
1206
+ program.command("ask").description("ask a Node.js question").argument("<question...>", "your question").action(async (words, _opts, cmd) => {
1207
+ const { config: config2, raw } = configFrom(cmd);
1208
+ await askCommand(words.join(" "), config2, { raw });
1209
+ });
1210
+ program.command("explain").description("explain a Node.js error or stack trace and suggest a fix").argument("[error...]", "error text (or pipe it via stdin)").option("-f, --file <path>", "read the error from a log file").option("-r, --run <command>", "run a command and explain its failure").option("--json", "print JSON output").addHelpText("after", '\nExamples:\n node app.js 2>&1 | eventa explain\n eventa explain --run "npm test"\n eventa explain --file crash.log').action(async (words, opts, cmd) => {
1211
+ const { config: config2, raw } = configFrom(cmd);
1212
+ await explainCommand(words, config2, { ...opts, raw });
1213
+ });
1214
+ 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) => {
1215
+ const { config: config2, raw } = configFrom(cmd);
1216
+ await reviewCommand(config2, { ...opts, raw });
1217
+ });
1218
+ program.command("deps").description("audit dependencies: vulnerabilities, outdated, unused and missing packages").option("--no-ai", "only print the report, skip AI advice").option("--json", "print JSON output").action(async (opts, cmd) => {
1219
+ const { config: config2, raw } = configFrom(cmd);
1220
+ await depsCommand(config2, { ...opts, raw });
1221
+ });
1222
+ program.command("doctor").description("check that Node.js, Ollama and the model are ready").action(async (_opts, cmd) => {
1223
+ const healthy = await doctorCommand(configFrom(cmd).config);
1224
+ if (!healthy) process.exitCode = 1;
1225
+ });
1226
+ var model = program.command("model").description("manage the built-in local model");
1227
+ model.command("list").description("show built-in models and what is downloaded").action(modelList);
1228
+ model.command("pull").argument("[id]", "model id, .gguf URL or path (default: configured localModel)").description("install the runtime and download a model").action(modelPull);
1229
+ model.command("use").argument("<id>").description("set the default local model").action(modelUse);
1230
+ model.command("rm").argument("<id>").description("delete a downloaded model").action(modelRemove);
1231
+ var config = program.command("config").description("view or change settings");
1232
+ config.command("get").argument("[key]").description("print a setting or all settings").action(configGet);
1233
+ config.command("set").argument("<key>").argument("<value>").description("change a setting").action(configSet);
1234
+ config.command("path").description("print the config file location").action(configWhere);
1235
+ program.parseAsync().catch((err) => {
1236
+ if (err instanceof ProviderError) {
1237
+ console.error(chalk8.red(`\u2716 ${err.message}`));
1238
+ if (err.hint) console.error(chalk8.dim(` ${err.hint}`));
1239
+ } else {
1240
+ console.error(chalk8.red(`\u2716 ${err.message}`));
1241
+ }
1242
+ process.exitCode = 1;
1243
+ });