engramx 2.0.0 → 2.0.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ All notable changes to engram are documented here. Format based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.0.1] — 2026-04-17 — Windows CI + favicon route
8
+
9
+ Patch release fixing two issues caught immediately after v2.0.0 shipped.
10
+
11
+ ### Fixed
12
+
13
+ - **Windows cross-platform bug in the plugin loader.** `PLUGINS_DIR` was a
14
+ module-load-time constant that baked in `homedir()` at import time. Windows
15
+ uses `USERPROFILE` while Unix uses `HOME`, and a frozen constant meant any
16
+ runtime override (tests, future `--plugins-dir` flag, programmatic use)
17
+ couldn't take effect without a module reload. Windows CI failed on the
18
+ plugin-loader tests because `process.env.HOME` mutation had no effect.
19
+ Fixed by introducing `getPluginsDir()` that resolves on every call, and
20
+ accepting an optional `dir` parameter on `loadPlugins()`,
21
+ `getLoadedPlugins()`, and `ensurePluginsDir()`. The `PLUGINS_DIR` constant
22
+ is retained for back-compat but runtime paths now go through the getter.
23
+ - **`/favicon.ico` returning 404 for clients that ignore `<link rel="icon">`.**
24
+ Added an explicit `GET /favicon.ico` route to the HTTP server that serves
25
+ a 238-byte inline SVG favicon with `Cache-Control: public, max-age=86400`.
26
+ The dashboard HTML still inlines the same favicon via `<link>` so modern
27
+ browsers avoid the request entirely.
28
+
29
+ ### Changed
30
+
31
+ - Test count: 640 → 641 (+1 for the "plugins directory does not exist"
32
+ branch of `loadPlugins()`).
33
+
34
+ ### CI
35
+
36
+ - Verified green on GitHub Actions matrix: Ubuntu + Windows × Node 20 + 22.
37
+ Commit `7c6001c`.
38
+
7
39
  ## [2.0.0] — 2026-04-17 — "Ecosystem"
8
40
 
9
41
  The biggest release since v1.0.0. Completes the v2.0 roadmap Phases 1–4:
package/dist/cli.js CHANGED
@@ -1351,7 +1351,7 @@ var BUILTIN_PROVIDERS = [
1351
1351
  ];
1352
1352
  var BUILTIN_NAMES = new Set(BUILTIN_PROVIDERS.map((p) => p.name));
1353
1353
  async function getAllProviders() {
1354
- const { getLoadedPlugins } = await import("./plugin-loader-FCOMVOX7.js");
1354
+ const { getLoadedPlugins } = await import("./plugin-loader-STTGYIL5.js");
1355
1355
  const { loaded } = await getLoadedPlugins();
1356
1356
  const safePlugins = loaded.filter((p) => !BUILTIN_NAMES.has(p.name));
1357
1357
  return [...BUILTIN_PROVIDERS, ...safePlugins];
@@ -3605,7 +3605,7 @@ program.command("stress-test").description("Run stress tests: memory, concurrenc
3605
3605
  }
3606
3606
  });
3607
3607
  program.command("server").description("Start engram HTTP REST server (binds to 127.0.0.1 only)").option("--http", "Enable HTTP server (default)").option("--port <port>", "HTTP port", "7337").option("-p, --project <path>", "Project directory", ".").action(async (opts) => {
3608
- const { startHttpServer } = await import("./server-VBRTTECZ.js");
3608
+ const { startHttpServer } = await import("./server-6AOI7NQP.js");
3609
3609
  await startHttpServer(pathResolve(opts.project), parseInt(opts.port, 10));
3610
3610
  });
3611
3611
  program.command("ui").description("Open the web dashboard (auto-starts HTTP server if needed)").option("--port <port>", "HTTP port", "7337").option("-p, --project <path>", "Project directory", ".").option("--no-open", "Don't launch browser, just print the URL").action(async (opts) => {
@@ -3798,13 +3798,14 @@ dbCmd.command("rollback").description("Roll back to an earlier schema version (D
3798
3798
  });
3799
3799
  var pluginCmd = program.command("plugin").description("Manage context provider plugins");
3800
3800
  pluginCmd.command("list").description("List installed provider plugins").action(async () => {
3801
- const { loadPlugins, PLUGINS_DIR, ensurePluginsDir } = await import("./plugin-loader-FCOMVOX7.js");
3802
- ensurePluginsDir();
3803
- const { loaded, failed } = await loadPlugins();
3801
+ const { loadPlugins, getPluginsDir, ensurePluginsDir } = await import("./plugin-loader-STTGYIL5.js");
3802
+ const dir = getPluginsDir();
3803
+ ensurePluginsDir(dir);
3804
+ const { loaded, failed } = await loadPlugins(dir);
3804
3805
  if (loaded.length === 0 && failed.length === 0) {
3805
3806
  console.log(chalk2.dim(`No plugins installed.`));
3806
3807
  console.log(chalk2.dim(`Install with: engram plugin install <file.mjs>`));
3807
- console.log(chalk2.dim(`Plugins directory: ${PLUGINS_DIR}`));
3808
+ console.log(chalk2.dim(`Plugins directory: ${dir}`));
3808
3809
  return;
3809
3810
  }
3810
3811
  if (loaded.length > 0) {
@@ -3830,7 +3831,7 @@ pluginCmd.command("list").description("List installed provider plugins").action(
3830
3831
  pluginCmd.command("install").description("Install a plugin by copying its .mjs file into ~/.engram/plugins/").argument("<file>", "Path to plugin .mjs file").action(async (file) => {
3831
3832
  const { copyFileSync: copyFileSync2, statSync: statSync5 } = await import("fs");
3832
3833
  const { basename: basename6 } = await import("path");
3833
- const { PLUGINS_DIR, ensurePluginsDir, validatePlugin } = await import("./plugin-loader-FCOMVOX7.js");
3834
+ const { getPluginsDir, ensurePluginsDir, validatePlugin } = await import("./plugin-loader-STTGYIL5.js");
3834
3835
  const { pathToFileURL } = await import("url");
3835
3836
  const absPath = pathResolve(file);
3836
3837
  if (!existsSync9(absPath)) {
@@ -3859,18 +3860,20 @@ pluginCmd.command("install").description("Install a plugin by copying its .mjs f
3859
3860
  console.error(chalk2.red(`Failed to load plugin: ${e.message}`));
3860
3861
  process.exit(1);
3861
3862
  }
3862
- ensurePluginsDir();
3863
+ const pluginsDir = getPluginsDir();
3864
+ ensurePluginsDir(pluginsDir);
3863
3865
  const destName = basename6(absPath);
3864
- const destPath = join9(PLUGINS_DIR, destName);
3866
+ const destPath = join9(pluginsDir, destName);
3865
3867
  copyFileSync2(absPath, destPath);
3866
3868
  console.log(chalk2.green(`\u2713 Installed: ${destPath}`));
3867
3869
  });
3868
3870
  pluginCmd.command("remove").description("Remove an installed plugin by filename").argument("<filename>", "Plugin filename (e.g., my-provider.mjs)").action(async (filename) => {
3869
- const { PLUGINS_DIR } = await import("./plugin-loader-FCOMVOX7.js");
3870
- const target = join9(PLUGINS_DIR, filename);
3871
+ const { getPluginsDir } = await import("./plugin-loader-STTGYIL5.js");
3872
+ const pluginsDir = getPluginsDir();
3873
+ const target = join9(pluginsDir, filename);
3871
3874
  if (!existsSync9(target)) {
3872
3875
  console.error(chalk2.red(`No such plugin: ${filename}`));
3873
- console.log(chalk2.dim(`Plugins directory: ${PLUGINS_DIR}`));
3876
+ console.log(chalk2.dim(`Plugins directory: ${pluginsDir}`));
3874
3877
  process.exit(1);
3875
3878
  }
3876
3879
  unlinkSync(target);
@@ -3,10 +3,14 @@ import { existsSync, readdirSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
5
  import { pathToFileURL } from "url";
6
- var PLUGINS_DIR = join(homedir(), ".engram", "plugins");
7
- function ensurePluginsDir() {
8
- if (!existsSync(PLUGINS_DIR)) {
9
- mkdirSync(PLUGINS_DIR, { recursive: true });
6
+ function getPluginsDir() {
7
+ return join(homedir(), ".engram", "plugins");
8
+ }
9
+ var PLUGINS_DIR = getPluginsDir();
10
+ function ensurePluginsDir(dir) {
11
+ const target = dir ?? getPluginsDir();
12
+ if (!existsSync(target)) {
13
+ mkdirSync(target, { recursive: true });
10
14
  }
11
15
  }
12
16
  function validatePlugin(mod) {
@@ -48,20 +52,21 @@ function validatePlugin(mod) {
48
52
  }
49
53
  return { plugin: candidate, reason: "" };
50
54
  }
51
- async function loadPlugins() {
55
+ async function loadPlugins(dir) {
56
+ const pluginsDir = dir ?? getPluginsDir();
52
57
  const loaded = [];
53
58
  const failed = [];
54
- if (!existsSync(PLUGINS_DIR)) {
59
+ if (!existsSync(pluginsDir)) {
55
60
  return { loaded, failed };
56
61
  }
57
62
  let files;
58
63
  try {
59
- files = readdirSync(PLUGINS_DIR).filter((f) => f.endsWith(".mjs") || f.endsWith(".js"));
64
+ files = readdirSync(pluginsDir).filter((f) => f.endsWith(".mjs") || f.endsWith(".js"));
60
65
  } catch {
61
66
  return { loaded, failed };
62
67
  }
63
68
  for (const file of files) {
64
- const fullPath = join(PLUGINS_DIR, file);
69
+ const fullPath = join(pluginsDir, file);
65
70
  try {
66
71
  const mod = await import(pathToFileURL(fullPath).href);
67
72
  const { plugin, reason } = validatePlugin(mod);
@@ -81,9 +86,9 @@ async function loadPlugins() {
81
86
  return { loaded, failed };
82
87
  }
83
88
  var _cache = null;
84
- async function getLoadedPlugins() {
89
+ async function getLoadedPlugins(dir) {
85
90
  if (_cache === null) {
86
- _cache = await loadPlugins();
91
+ _cache = await loadPlugins(dir);
87
92
  }
88
93
  return _cache;
89
94
  }
@@ -95,6 +100,7 @@ export {
95
100
  _resetPluginCache,
96
101
  ensurePluginsDir,
97
102
  getLoadedPlugins,
103
+ getPluginsDir,
98
104
  loadPlugins,
99
105
  validatePlugin
100
106
  };
@@ -1325,6 +1325,13 @@ function createHttpServer(projectRoot, port) {
1325
1325
  "Cache-Control": "no-cache"
1326
1326
  });
1327
1327
  res.end(buildDashboardHtml());
1328
+ } else if (req.method === "GET" && path === "/favicon.ico") {
1329
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#0a0a0b"/><text x="50" y="62" font-size="56" text-anchor="middle" fill="#10b981" font-family="Menlo,monospace">&#9670;</text></svg>';
1330
+ res.writeHead(200, {
1331
+ "Content-Type": "image/svg+xml",
1332
+ "Cache-Control": "public, max-age=86400"
1333
+ });
1334
+ res.end(svg);
1328
1335
  } else {
1329
1336
  json(res, 404, { error: "Not found" });
1330
1337
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engramx",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "The context spine for AI coding agents. 8 providers + pluggable context sources, 3-layer memory cache, web dashboard, multi-IDE support (Claude Code, Cursor, Continue, Zed, Aider, Windsurf, Neovim, Emacs). 88.1% measured session-level token savings. Local SQLite, zero native deps, zero cloud.",
5
5
  "repository": {
6
6
  "type": "git",