@engram-cli/engram 0.1.0 → 0.1.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.
Files changed (39) hide show
  1. package/README.md +52 -18
  2. package/bundled-backend/app.js +41 -0
  3. package/bundled-backend/capsule-assembler.js +238 -0
  4. package/bundled-backend/capsule-store.js +108 -0
  5. package/bundled-backend/config.js +31 -0
  6. package/bundled-backend/env-files.js +96 -0
  7. package/bundled-backend/index.js +22 -0
  8. package/bundled-backend/intent-filter.js +21 -0
  9. package/bundled-backend/package.json +7 -0
  10. package/bundled-backend/project-index.js +74 -0
  11. package/bundled-backend/project-slug.js +18 -0
  12. package/bundled-backend/routes/capsule.js +77 -0
  13. package/bundled-backend/routes/context.js +23 -0
  14. package/bundled-backend/routes/event.js +70 -0
  15. package/bundled-backend/routes/global-settings.js +125 -0
  16. package/bundled-backend/routes/health.js +24 -0
  17. package/bundled-backend/routes/projects.js +100 -0
  18. package/bundled-backend/routes/search.js +69 -0
  19. package/bundled-backend/routes/settings.js +82 -0
  20. package/bundled-backend/routes/shares.js +92 -0
  21. package/bundled-backend/supabase.js +47 -0
  22. package/bundled-backend/supermemory.js +46 -0
  23. package/dist/cli.js +25 -0
  24. package/dist/cli.js.map +1 -1
  25. package/dist/client.d.ts +54 -0
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +25 -0
  28. package/dist/client.js.map +1 -1
  29. package/dist/mcp.d.ts.map +1 -1
  30. package/dist/mcp.js +143 -31
  31. package/dist/mcp.js.map +1 -1
  32. package/dist/share.d.ts +14 -0
  33. package/dist/share.d.ts.map +1 -0
  34. package/dist/share.js +90 -0
  35. package/dist/share.js.map +1 -0
  36. package/dist/start.d.ts.map +1 -1
  37. package/dist/start.js +116 -36
  38. package/dist/start.js.map +1 -1
  39. package/package.json +10 -4
package/README.md CHANGED
@@ -55,16 +55,25 @@ Add to `~/.cursor/mcp.json` (or Cursor Settings → MCP):
55
55
 
56
56
  Available tools:
57
57
 
58
+ - `engram_status` — backend health + masked key status
58
59
  - `engram_load_capsule` — load the saved project capsule
60
+ - `engram_save_capsule` — freeze current Context as the project capsule
61
+ - `engram_list_capsules` — list saved capsules for a project
59
62
  - `engram_context` — assemble live Context from Supermemory
60
63
  - `engram_search` — search project memories with a natural-language query
64
+ - `engram_emit` — ingest a MemoryEvent (capture path)
65
+
66
+ MCP resources (when `ENGRAM_PROJECT` is set or cwd resolves a project):
67
+
68
+ - `engram://{project}/context` — live Context capsule
69
+ - `engram://{project}/capsule` — saved capsule snapshot
61
70
 
62
71
  ## Development
63
72
 
64
73
  ```bash
65
74
  cd cli
66
75
  npm run dev # tsx src/cli.ts start
67
- npm run build
76
+ npm run build # also bundles backend into bundled-backend/
68
77
  npm run typecheck
69
78
  ```
70
79
 
@@ -76,28 +85,53 @@ bash scripts/smoke.sh
76
85
 
77
86
  ## Publish (npm org `engram-cli`)
78
87
 
79
- Log in as an org owner (`npm login` as `shubh1305`). Publish **contracts**, then the CLI:
88
+ Packages on npm are owned by **`shubh1305`**. You must publish while logged in as that user.
89
+
90
+ ```bash
91
+ npm logout
92
+ npm login # use the shubh1305 account
93
+ npm whoami # must print: shubh1305
94
+
95
+ # If 2FA is enabled on the account:
96
+ export NPM_OTP=123456
97
+
98
+ # From repo root — publishes contracts@0.1.1 then engram@0.1.1
99
+ bash scripts/publish-npm.sh
100
+ ```
101
+
102
+ The script swaps `@engram-cli/contracts` from `file:../packages/contracts` to `^<version>` for the publish, then restores the monorepo `file:` link.
103
+
104
+ ### Troubleshooting `E404` on publish
105
+
106
+ `npm error 404 ... you do not have permission` almost always means **wrong npm account**, not a missing package:
107
+
108
+ | Symptom | Fix |
109
+ | --- | --- |
110
+ | `npm whoami` ≠ `shubh1305` | `npm logout && npm login` as `shubh1305` |
111
+ | `npm whoami` fails (401) | Token expired — run `npm login` again |
112
+ | 2FA enabled | `export NPM_OTP=<6-digit code>` then re-run publish |
113
+ | Still blocked | Confirm access: `npm access list collaborators @engram-cli/engram` |
114
+
115
+ ### After publish
80
116
 
81
117
  ```bash
82
- # 1) Contracts
83
- cd packages/contracts
84
- npm run build
85
- npm publish --access public
86
-
87
- # 2) CLI — switch the local file: dep to the registry version, then publish
88
- cd ../../cli
89
- # in package.json: "@engram-cli/contracts": "^0.1.0"
90
- npm install
91
- npm run build
92
- npm publish --access public
93
- # restore "file:../packages/contracts" for monorepo work after publishing
118
+ npx @engram-cli/engram@latest start # bundled backend (works outside monorepo)
119
+ npx @engram-cli/engram@latest init
94
120
  ```
95
121
 
96
- After publish:
122
+ `engram start` prefers the monorepo `backend/` when present; otherwise it runs the **bundled Hono backend** shipped inside the npm package. Data defaults to `~/.engram/data`. Configure keys via the extension Setup checklist, dashboard `/settings`, or `PUT /settings/global`.
123
+
124
+ **Monorepo dev** (uses your local build, not npm):
97
125
 
98
126
  ```bash
99
- npx @engram-cli/engram start
100
- npx @engram-cli/engram init
127
+ cd cli && npm run build && node dist/cli.js start
101
128
  ```
102
129
 
103
- Note: `engram start` looks for monorepo `backend/` via path walk. From a published install outside this repo, start the backend from a clone (or keep using the monorepo) until a standalone start path ships.
130
+ ### Verified (2026-07-22)
131
+
132
+ | Check | Expected |
133
+ | --- | --- |
134
+ | `npm view @engram-cli/contracts version` | `0.1.1` |
135
+ | `npm view @engram-cli/engram version` | `0.1.1` |
136
+ | `npx @engram-cli/engram --help` | Includes `share`, `import` |
137
+ | `npx @engram-cli/engram start` (outside repo) | Starts Supermemory + backend |
@@ -0,0 +1,41 @@
1
+ import { Hono } from "hono";
2
+ import { cors } from "hono/cors";
3
+ import { healthRoutes } from "./routes/health.js";
4
+ import { eventRoutes } from "./routes/event.js";
5
+ import { contextRoutes } from "./routes/context.js";
6
+ import { capsuleRoutes } from "./routes/capsule.js";
7
+ import { searchRoutes } from "./routes/search.js";
8
+ import { projectRoutes } from "./routes/projects.js";
9
+ import { settingsRoutes } from "./routes/settings.js";
10
+ import { globalSettingsRoutes } from "./routes/global-settings.js";
11
+ import { sharesRoutes } from "./routes/shares.js";
12
+ const CORS_ORIGINS = [
13
+ "http://localhost:5173",
14
+ "http://localhost:3000",
15
+ "https://engram-context.vercel.app",
16
+ process.env.ENGRAM_LANDING_ORIGIN,
17
+ ].filter((o) => Boolean(o));
18
+ export function createApp() {
19
+ const app = new Hono();
20
+ app.use("*", cors({
21
+ origin: CORS_ORIGINS,
22
+ allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
23
+ allowHeaders: ["Content-Type"],
24
+ credentials: false,
25
+ }));
26
+ app.route("/", healthRoutes);
27
+ app.route("/", eventRoutes);
28
+ app.route("/", contextRoutes);
29
+ app.route("/", capsuleRoutes);
30
+ app.route("/", searchRoutes);
31
+ app.route("/", projectRoutes);
32
+ app.route("/", settingsRoutes);
33
+ app.route("/", globalSettingsRoutes);
34
+ app.route("/", sharesRoutes);
35
+ app.notFound((c) => c.json({ error: "not_found" }, 404));
36
+ app.onError((err, c) => {
37
+ console.error("[engram] unhandled:", err);
38
+ return c.json({ error: "internal_error", message: err.message }, 500);
39
+ });
40
+ return app;
41
+ }
@@ -0,0 +1,238 @@
1
+ import { CAPS, CAPSULE_SCHEMA_VERSION, truncateBytes, } from "@engram-cli/contracts";
2
+ import { projectContainerTag } from "@engram-cli/contracts";
3
+ import { getSupermemory } from "./supermemory.js";
4
+ function asMeta(raw) {
5
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
6
+ return raw;
7
+ }
8
+ return {};
9
+ }
10
+ function metaString(meta, key) {
11
+ const v = meta[key];
12
+ if (typeof v === "string" && v.length > 0)
13
+ return v;
14
+ if (typeof v === "number" || typeof v === "boolean")
15
+ return String(v);
16
+ return undefined;
17
+ }
18
+ function metaNumber(meta, key) {
19
+ const v = meta[key];
20
+ if (typeof v === "number" && Number.isFinite(v))
21
+ return v;
22
+ if (typeof v === "string" && v !== "" && !Number.isNaN(Number(v))) {
23
+ return Number(v);
24
+ }
25
+ return undefined;
26
+ }
27
+ async function listRecentDocs(containerTag, limit = 40) {
28
+ const sm = getSupermemory();
29
+ const res = await sm.documents.list({
30
+ containerTags: [containerTag],
31
+ includeContent: true,
32
+ limit,
33
+ sort: "createdAt",
34
+ order: "desc",
35
+ });
36
+ return (res.memories ?? []).map((m) => ({
37
+ id: m.id,
38
+ content: m.content,
39
+ summary: m.summary,
40
+ title: m.title,
41
+ createdAt: m.createdAt,
42
+ metadata: asMeta(m.metadata),
43
+ }));
44
+ }
45
+ function eventTypeOf(doc) {
46
+ const t = metaString(doc.metadata, "event_type");
47
+ return t;
48
+ }
49
+ function textOf(doc) {
50
+ return (doc.content || doc.summary || doc.title || "").trim();
51
+ }
52
+ function buildRecentFiles(docs) {
53
+ const files = [];
54
+ const seen = new Set();
55
+ for (const doc of docs) {
56
+ const t = eventTypeOf(doc);
57
+ if (t !== "file_open" && t !== "file_save")
58
+ continue;
59
+ const path = metaString(doc.metadata, "file_path") ||
60
+ textOf(doc).replace(/^(opened|saved):\s*/i, "").trim();
61
+ if (!path || seen.has(path))
62
+ continue;
63
+ seen.add(path);
64
+ files.push(path);
65
+ if (files.length >= CAPS.recentFiles)
66
+ break;
67
+ }
68
+ return files;
69
+ }
70
+ function buildErrors(docs) {
71
+ const errors = [];
72
+ for (const doc of docs) {
73
+ if (eventTypeOf(doc) !== "error")
74
+ continue;
75
+ const message = textOf(doc).replace(/^error:\s*/i, "").trim() || "error";
76
+ // Strip trailing " in path:line" if present for cleaner message; keep path from meta.
77
+ const file_path = metaString(doc.metadata, "file_path");
78
+ const line = metaNumber(doc.metadata, "line");
79
+ let clean = message;
80
+ if (file_path) {
81
+ const suffix = ` in ${file_path}${line != null ? `:${line}` : ""}`;
82
+ if (clean.endsWith(suffix))
83
+ clean = clean.slice(0, -suffix.length).trim();
84
+ }
85
+ errors.push({ message: clean, file_path, line });
86
+ if (errors.length >= CAPS.errors)
87
+ break;
88
+ }
89
+ return errors;
90
+ }
91
+ function buildFailedCommands(docs) {
92
+ const out = [];
93
+ for (const doc of docs) {
94
+ if (eventTypeOf(doc) !== "command")
95
+ continue;
96
+ const exit = metaNumber(doc.metadata, "exit_code") ?? 0;
97
+ if (exit === 0)
98
+ continue;
99
+ const raw = textOf(doc);
100
+ // "cmd (exit N): <firstline>\n<stderr...>"
101
+ const withoutPrefix = raw.replace(/^cmd \(exit \d+\):\s*/i, "");
102
+ const [first, ...rest] = withoutPrefix.split("\n");
103
+ const command = (first ?? withoutPrefix).trim();
104
+ const stderr = rest.join("\n").trim();
105
+ out.push({
106
+ command,
107
+ exit_code: exit,
108
+ stderr_tail: stderr
109
+ ? truncateBytes(stderr, CAPS.stderrBytes)
110
+ : undefined,
111
+ cwd: metaString(doc.metadata, "cwd"),
112
+ });
113
+ if (out.length >= CAPS.failedCommands)
114
+ break;
115
+ }
116
+ return out;
117
+ }
118
+ function buildGit(docs) {
119
+ let branch;
120
+ let last_commit;
121
+ for (const doc of docs) {
122
+ const t = eventTypeOf(doc);
123
+ if (t === "branch_switch" && !branch) {
124
+ branch = textOf(doc).replace(/^branch:\s*/i, "").trim();
125
+ }
126
+ if (t === "commit" && !last_commit) {
127
+ last_commit = textOf(doc).replace(/^commit:\s*/i, "").trim();
128
+ }
129
+ if (branch && last_commit)
130
+ break;
131
+ }
132
+ return { branch, last_commit };
133
+ }
134
+ function buildManualNotes(docs) {
135
+ const notes = [];
136
+ for (const doc of docs) {
137
+ const t = eventTypeOf(doc);
138
+ if (t !== "code_selection" && t !== "clipboard" && t !== "chat_summary") {
139
+ continue;
140
+ }
141
+ let text = textOf(doc);
142
+ if (t === "code_selection")
143
+ text = text.replace(/^selection:\s*/i, "");
144
+ if (t === "clipboard")
145
+ text = text.replace(/^clipboard:\s*/i, "");
146
+ if (t === "chat_summary") {
147
+ text = text.replace(/^chat_summary(?:\[[^\]]*\])?:\s*/i, "");
148
+ }
149
+ notes.push({
150
+ kind: t,
151
+ text: text.trim(),
152
+ file_path: metaString(doc.metadata, "file_path"),
153
+ chat_label: metaString(doc.metadata, "chat_label"),
154
+ });
155
+ if (notes.length >= CAPS.manualNotes)
156
+ break;
157
+ }
158
+ return notes;
159
+ }
160
+ function buildSummary(parts) {
161
+ const lines = [];
162
+ if (parts.profileLine)
163
+ lines.push(parts.profileLine);
164
+ if (parts.git.branch)
165
+ lines.push(`On branch ${parts.git.branch}.`);
166
+ if (parts.git.last_commit)
167
+ lines.push(`Last commit: ${parts.git.last_commit}.`);
168
+ if (parts.recent_files.length) {
169
+ lines.push(`Recent files: ${parts.recent_files.slice(0, 5).join(", ")}.`);
170
+ }
171
+ if (parts.errors.length) {
172
+ const e = parts.errors[0];
173
+ const loc = e.file_path
174
+ ? `${e.file_path}${e.line != null ? `:${e.line}` : ""}`
175
+ : "unknown";
176
+ lines.push(`Error at ${loc}: ${e.message}`);
177
+ }
178
+ if (parts.failed_commands.length) {
179
+ const c = parts.failed_commands[0];
180
+ lines.push(`Failed: \`${c.command}\` (exit ${c.exit_code}).`);
181
+ }
182
+ if (parts.manual_notes.length) {
183
+ const n = parts.manual_notes[0];
184
+ lines.push(`Note (${n.kind}): ${n.text}`);
185
+ }
186
+ if (!lines.length)
187
+ lines.push("No recent Engram context for this project.");
188
+ return truncateBytes(lines.join(" "), CAPS.summaryBytes);
189
+ }
190
+ export async function assembleCapsule(opts) {
191
+ const containerTag = projectContainerTag(opts.project);
192
+ const sm = getSupermemory();
193
+ let profileLine;
194
+ try {
195
+ const profile = await sm.profile({
196
+ containerTag,
197
+ q: "current work session",
198
+ });
199
+ const dynamic = profile.profile?.dynamic ?? [];
200
+ const staticFacts = profile.profile?.static ?? [];
201
+ const pick = [...dynamic, ...staticFacts].find((s) => typeof s === "string" && s.trim().length > 0);
202
+ if (pick)
203
+ profileLine = String(pick).trim();
204
+ }
205
+ catch (err) {
206
+ console.warn("[engram] profile() failed (continuing with structured fields):", err instanceof Error ? err.message : err);
207
+ }
208
+ const docs = await listRecentDocs(containerTag);
209
+ const recent_files = buildRecentFiles(docs);
210
+ const errors = buildErrors(docs);
211
+ const failed_commands = buildFailedCommands(docs);
212
+ const git = buildGit(docs);
213
+ const manual_notes = buildManualNotes(docs);
214
+ // Prefer IDE from most recent doc metadata when not provided.
215
+ const sourceIde = opts.sourceIde ||
216
+ docs.map((d) => metaString(d.metadata, "ide")).find(Boolean);
217
+ const summary = buildSummary({
218
+ recent_files,
219
+ errors,
220
+ failed_commands,
221
+ git,
222
+ manual_notes,
223
+ profileLine,
224
+ });
225
+ return {
226
+ capsule_id: opts.capsuleId ?? crypto.randomUUID(),
227
+ project: opts.project,
228
+ created_at: opts.createdAt ?? new Date().toISOString(),
229
+ source_ide: sourceIde,
230
+ schema_version: CAPSULE_SCHEMA_VERSION,
231
+ summary,
232
+ recent_files,
233
+ errors,
234
+ failed_commands,
235
+ git,
236
+ manual_notes,
237
+ };
238
+ }
@@ -0,0 +1,108 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { CapsuleSchema, toMarkdown, } from "@engram-cli/contracts";
4
+ import { config } from "./config.js";
5
+ import { assertSafeProjectSlug } from "./project-slug.js";
6
+ const CURRENT_FILE = "current.json";
7
+ function projectDir(project) {
8
+ const safe = assertSafeProjectSlug(project);
9
+ return path.join(config.dataDir, "capsules", safe);
10
+ }
11
+ function currentPath(project) {
12
+ return path.join(projectDir(project), CURRENT_FILE);
13
+ }
14
+ export async function ensureDataDirs() {
15
+ await fs.mkdir(path.join(config.dataDir, "capsules"), { recursive: true });
16
+ }
17
+ /**
18
+ * One capsule per project at current.json.
19
+ * If missing, migrate the newest legacy {uuid}.json and remove the rest.
20
+ */
21
+ export async function ensureCurrentCapsule(project) {
22
+ const dir = projectDir(project);
23
+ const current = currentPath(project);
24
+ try {
25
+ const raw = await fs.readFile(current, "utf8");
26
+ return CapsuleSchema.parse(JSON.parse(raw));
27
+ }
28
+ catch (err) {
29
+ if (err.code !== "ENOENT")
30
+ throw err;
31
+ }
32
+ let files;
33
+ try {
34
+ files = await fs.readdir(dir);
35
+ }
36
+ catch (err) {
37
+ if (err.code === "ENOENT")
38
+ return null;
39
+ throw err;
40
+ }
41
+ const legacy = [];
42
+ for (const file of files) {
43
+ if (!file.endsWith(".json") || file === CURRENT_FILE)
44
+ continue;
45
+ try {
46
+ const raw = await fs.readFile(path.join(dir, file), "utf8");
47
+ const capsule = CapsuleSchema.parse(JSON.parse(raw));
48
+ legacy.push({ file, capsule });
49
+ }
50
+ catch {
51
+ // skip corrupt
52
+ }
53
+ }
54
+ if (!legacy.length)
55
+ return null;
56
+ legacy.sort((a, b) => b.capsule.created_at.localeCompare(a.capsule.created_at));
57
+ const newest = legacy[0];
58
+ await fs.mkdir(dir, { recursive: true });
59
+ await fs.writeFile(current, JSON.stringify(newest.capsule, null, 2), "utf8");
60
+ for (const item of legacy) {
61
+ try {
62
+ await fs.unlink(path.join(dir, item.file));
63
+ }
64
+ catch {
65
+ // best-effort cleanup
66
+ }
67
+ }
68
+ return newest.capsule;
69
+ }
70
+ export async function saveCapsule(capsule, opts) {
71
+ const parsed = CapsuleSchema.parse(capsule);
72
+ const dir = projectDir(parsed.project);
73
+ await fs.mkdir(dir, { recursive: true });
74
+ await fs.writeFile(currentPath(parsed.project), JSON.stringify(parsed, null, 2), "utf8");
75
+ // Remove any leftover legacy UUID files after upsert
76
+ try {
77
+ const files = await fs.readdir(dir);
78
+ for (const file of files) {
79
+ if (file.endsWith(".json") && file !== CURRENT_FILE) {
80
+ await fs.unlink(path.join(dir, file)).catch(() => undefined);
81
+ }
82
+ }
83
+ }
84
+ catch {
85
+ // ignore
86
+ }
87
+ if (opts?.projectRoot) {
88
+ const mdPath = path.join(opts.projectRoot, "ENGRAM_CONTEXT.md");
89
+ await fs.writeFile(mdPath, toMarkdown(parsed), "utf8");
90
+ }
91
+ return parsed;
92
+ }
93
+ /** Load the single project capsule (migrates legacy files if needed). */
94
+ export async function loadCapsule(project) {
95
+ return ensureCurrentCapsule(project);
96
+ }
97
+ export async function listCapsules(project) {
98
+ const capsule = await ensureCurrentCapsule(project);
99
+ if (!capsule)
100
+ return [];
101
+ return [
102
+ {
103
+ capsule_id: capsule.capsule_id,
104
+ created_at: capsule.created_at,
105
+ summary: capsule.summary.slice(0, 200),
106
+ },
107
+ ];
108
+ }
@@ -0,0 +1,31 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
4
+ export const config = {
5
+ port: Number(process.env.ENGRAM_PORT ?? 7731),
6
+ dataDir: path.resolve(process.env.ENGRAM_DATA_DIR ?? path.join(__dirname, "..", "data")),
7
+ supermemoryApiKey: process.env.SUPERMEMORY_API_KEY ?? "",
8
+ supermemoryBaseUrl: process.env.SUPERMEMORY_BASE_URL ?? "http://localhost:6767",
9
+ databaseUrl: process.env.DATABASE_URL ?? "",
10
+ supabaseUrl: process.env.SUPABASE_URL ?? "",
11
+ supabaseAnonKey: process.env.SUPABASE_ANON_KEY ?? "",
12
+ supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY ?? "",
13
+ shareBaseUrl: process.env.ENGRAM_SHARE_BASE_URL ?? "http://localhost:3000",
14
+ };
15
+ /** Hot-reload Supermemory credentials after /settings/global write. */
16
+ export function reloadSupermemoryKey(apiKey, baseUrl) {
17
+ config.supermemoryApiKey = apiKey;
18
+ process.env.SUPERMEMORY_API_KEY = apiKey;
19
+ if (baseUrl) {
20
+ config.supermemoryBaseUrl = baseUrl;
21
+ process.env.SUPERMEMORY_BASE_URL = baseUrl;
22
+ }
23
+ // Lazy import to avoid circular deps at module load
24
+ void import("./supermemory.js").then((m) => m.resetSupermemoryClient());
25
+ }
26
+ export function assertConfig() {
27
+ if (!config.supermemoryApiKey) {
28
+ console.warn("[engram] SUPERMEMORY_API_KEY not set — configure via PUT /settings/global or .env. " +
29
+ "Capture/search will fail until a key is set.");
30
+ }
31
+ }
@@ -0,0 +1,96 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { execSync } from "node:child_process";
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ export const LLM_ENV_KEYS = {
8
+ groq: "GROQ_API_KEY",
9
+ openai: "OPENAI_API_KEY",
10
+ anthropic: "ANTHROPIC_API_KEY",
11
+ };
12
+ export function findEnvRoot() {
13
+ if (process.env.ENGRAM_PROJECT_ROOT) {
14
+ return path.resolve(process.env.ENGRAM_PROJECT_ROOT);
15
+ }
16
+ try {
17
+ const gitRoot = execSync("git rev-parse --show-toplevel", {
18
+ cwd: process.cwd(),
19
+ encoding: "utf8",
20
+ stdio: ["ignore", "pipe", "ignore"],
21
+ }).trim();
22
+ if (gitRoot)
23
+ return gitRoot;
24
+ }
25
+ catch {
26
+ // not a git repo
27
+ }
28
+ // Monorepo fallback: backend/src → repo root
29
+ const monorepo = path.resolve(__dirname, "..", "..");
30
+ if (fs.existsSync(path.join(monorepo, "backend")))
31
+ return monorepo;
32
+ return process.cwd();
33
+ }
34
+ export function envFilePath() {
35
+ return path.join(findEnvRoot(), ".env");
36
+ }
37
+ export function supermemoryEnvPath() {
38
+ return path.join(os.homedir(), ".supermemory", "env");
39
+ }
40
+ export function supermemoryApiKeyPath() {
41
+ return path.join(os.homedir(), ".supermemory", "api-key");
42
+ }
43
+ export function readEnvValue(filePath, key) {
44
+ try {
45
+ const text = fs.readFileSync(filePath, "utf8");
46
+ const match = text.match(new RegExp(`^${key}=([^\\r\\n]*)`, "m"));
47
+ return match?.[1]?.trim() || undefined;
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ export function upsertEnvValue(filePath, key, value) {
54
+ const dir = path.dirname(filePath);
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ let text = "";
57
+ try {
58
+ text = fs.readFileSync(filePath, "utf8");
59
+ }
60
+ catch {
61
+ text = "";
62
+ }
63
+ const line = `${key}=${value}`;
64
+ const re = new RegExp(`^${key}=.*$`, "m");
65
+ if (re.test(text)) {
66
+ text = text.replace(re, line);
67
+ }
68
+ else {
69
+ text = text.trimEnd();
70
+ text = text ? `${text}\n${line}\n` : `${line}\n`;
71
+ }
72
+ fs.writeFileSync(filePath, text, "utf8");
73
+ }
74
+ export function maskKey(key) {
75
+ if (!key || key.length < 8) {
76
+ return { configured: Boolean(key) };
77
+ }
78
+ return {
79
+ configured: true,
80
+ prefix: `${key.slice(0, 6)}…`,
81
+ };
82
+ }
83
+ export function detectLlmProvider() {
84
+ const envPath = supermemoryEnvPath();
85
+ for (const [provider, envKey] of Object.entries(LLM_ENV_KEYS)) {
86
+ const val = readEnvValue(envPath, envKey);
87
+ if (val)
88
+ return { provider, configured: true };
89
+ }
90
+ // Also check process.env (loaded at boot)
91
+ for (const [provider, envKey] of Object.entries(LLM_ENV_KEYS)) {
92
+ if (process.env[envKey])
93
+ return { provider, configured: true };
94
+ }
95
+ return { provider: null, configured: false };
96
+ }
@@ -0,0 +1,22 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { createApp } from "./app.js";
3
+ import { assertConfig, config } from "./config.js";
4
+ import { ensureDataDirs } from "./capsule-store.js";
5
+ import { bootSupermemorySettings } from "./supermemory.js";
6
+ async function main() {
7
+ assertConfig();
8
+ await ensureDataDirs();
9
+ await bootSupermemorySettings();
10
+ const app = createApp();
11
+ console.log(`[engram] listening on http://localhost:${config.port}`);
12
+ console.log(`[engram] data dir: ${config.dataDir}`);
13
+ console.log(`[engram] supermemory: ${config.supermemoryBaseUrl}`);
14
+ serve({
15
+ fetch: app.fetch,
16
+ port: config.port,
17
+ });
18
+ }
19
+ main().catch((err) => {
20
+ console.error("[engram] fatal:", err);
21
+ process.exit(1);
22
+ });
@@ -0,0 +1,21 @@
1
+ import { CAPS, truncateBytes, } from "@engram-cli/contracts";
2
+ const NOISY_SUCCESS = /^(ls|cd|pwd|echo|clear|which|true|false|history|dirname|basename)\b/;
3
+ /** Authoritative ingest filter — clients stay dumb. */
4
+ export function intentFilter(event) {
5
+ const content = event.content.trim();
6
+ if (!content) {
7
+ return { keep: false, reason: "empty_content" };
8
+ }
9
+ if (event.event_type === "command") {
10
+ const exit = event.exit_code ?? 0;
11
+ const firstLine = content.split("\n")[0]?.trim() ?? content;
12
+ if (exit === 0 && NOISY_SUCCESS.test(firstLine)) {
13
+ return { keep: false, reason: "noisy_successful_command" };
14
+ }
15
+ }
16
+ const truncated = {
17
+ ...event,
18
+ content: truncateBytes(content, CAPS.contentBytes),
19
+ };
20
+ return { keep: true, event: truncated };
21
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@engram-cli/bundled-backend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "index.js"
7
+ }