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-
|
|
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-
|
|
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,
|
|
3802
|
-
|
|
3803
|
-
|
|
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: ${
|
|
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 {
|
|
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
|
-
|
|
3863
|
+
const pluginsDir = getPluginsDir();
|
|
3864
|
+
ensurePluginsDir(pluginsDir);
|
|
3863
3865
|
const destName = basename6(absPath);
|
|
3864
|
-
const destPath = join9(
|
|
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 {
|
|
3870
|
-
const
|
|
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: ${
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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(
|
|
59
|
+
if (!existsSync(pluginsDir)) {
|
|
55
60
|
return { loaded, failed };
|
|
56
61
|
}
|
|
57
62
|
let files;
|
|
58
63
|
try {
|
|
59
|
-
files = readdirSync(
|
|
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(
|
|
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">◆</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.
|
|
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",
|