@vedtechsolutions/engram-mcp 1.0.19 → 1.0.21

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.
@@ -11178,14 +11178,17 @@ function inferRole(nodeType, filePath, analysis) {
11178
11178
 
11179
11179
  // src/engines/curator.ts
11180
11180
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync, realpathSync as realpathSync2, lstatSync as lstatSync2, renameSync } from "fs";
11181
- import { join as join5, dirname as dirname3 } from "path";
11181
+ import { join as join5, dirname as dirname3, resolve as resolve3 } from "path";
11182
11182
  import { homedir as homedir3 } from "os";
11183
11183
  var logger14 = createLogger("curator");
11184
11184
  var lastBridgeWriteTime = 0;
11185
11185
  function discoverMemoryDir(cwd) {
11186
11186
  const envDir = process.env.CLAUDE_MEMORY_DIR;
11187
- if (envDir && existsSync5(envDir)) {
11188
- return envDir;
11187
+ if (envDir && envDir.startsWith("/") && !envDir.includes("\0") && envDir.length < 1e3) {
11188
+ const resolvedEnv = resolve3(envDir);
11189
+ if (resolvedEnv === resolve3(resolvedEnv) && existsSync5(resolvedEnv)) {
11190
+ return resolvedEnv;
11191
+ }
11189
11192
  }
11190
11193
  const home = homedir3();
11191
11194
  const projectsBase = join5(home, ".claude", "projects");
@@ -12672,4 +12675,4 @@ export {
12672
12675
  composeProjectUnderstanding,
12673
12676
  formatMentalModelInjection
12674
12677
  };
12675
- //# sourceMappingURL=chunk-OY2XHPUF.js.map
12678
+ //# sourceMappingURL=chunk-O3ZP4K3T.js.map
package/dist/hook.js CHANGED
@@ -174,10 +174,10 @@ import {
174
174
  updateReasoningChain,
175
175
  updateSelfModelFromSession,
176
176
  updateTask
177
- } from "./chunk-OY2XHPUF.js";
177
+ } from "./chunk-O3ZP4K3T.js";
178
178
 
179
179
  // src/hook.ts
180
- import { readFileSync, writeFileSync, existsSync, renameSync, statSync, readdirSync, unlinkSync, appendFileSync, openSync, readSync, closeSync } from "fs";
180
+ import { readFileSync, writeFileSync, existsSync, renameSync, statSync, readdirSync, unlinkSync, openSync, readSync, closeSync } from "fs";
181
181
  import { join, basename, resolve } from "path";
182
182
  import { homedir } from "os";
183
183
 
@@ -2886,15 +2886,39 @@ function readStdin() {
2886
2886
  return;
2887
2887
  }
2888
2888
  let data = "";
2889
+ const MAX_STDIN_BYTES = 10 * 1024 * 1024;
2889
2890
  process.stdin.setEncoding("utf-8");
2890
2891
  process.stdin.on("data", (chunk) => {
2891
2892
  data += chunk;
2893
+ if (data.length > MAX_STDIN_BYTES) {
2894
+ data = data.slice(0, MAX_STDIN_BYTES);
2895
+ process.stdin.destroy();
2896
+ resolve2(data);
2897
+ }
2892
2898
  });
2893
2899
  process.stdin.on("end", () => resolve2(data));
2894
2900
  process.stdin.on("error", () => resolve2(""));
2895
2901
  setTimeout(() => resolve2(data), 2e3);
2896
2902
  });
2897
2903
  }
2904
+ function validateTranscriptPath(p) {
2905
+ if (!p || typeof p !== "string") return null;
2906
+ const resolved = resolve(p);
2907
+ const allowed = join(homedir(), ".claude", "projects");
2908
+ if (!resolved.startsWith(allowed + "/") || !resolved.endsWith(".jsonl")) return null;
2909
+ return resolved;
2910
+ }
2911
+ function validateSessionId(sid) {
2912
+ if (!sid || typeof sid !== "string") return null;
2913
+ if (!/^[a-zA-Z0-9_-]{1,100}$/.test(sid)) return null;
2914
+ return sid;
2915
+ }
2916
+ function validateCwd(cwd) {
2917
+ if (!cwd || typeof cwd !== "string") return null;
2918
+ if (!cwd.startsWith("/")) return null;
2919
+ if (cwd.includes("\0") || cwd.length > 1e3) return null;
2920
+ return cwd;
2921
+ }
2898
2922
  function distillLesson(context) {
2899
2923
  const parts = [];
2900
2924
  if (context.errors && context.errors.length > 0 && context.fix) {
@@ -3226,13 +3250,29 @@ function sanitizeCognitiveState(state) {
3226
3250
  const cog = state.cognitive_state;
3227
3251
  if (!cog) return;
3228
3252
  const placeholders = ["X", "X.", "Y", "Y.", "Z", "Z."];
3229
- if (cog.current_approach && placeholders.includes(cog.current_approach)) {
3253
+ const templatePatterns = [
3254
+ /^X[\.\s]/,
3255
+ // Starts with X. or X<space>
3256
+ /^Approach:\s*X/i,
3257
+ // "Approach: X..."
3258
+ /Hypothesis:\s*[XYZ][\.\s]/,
3259
+ // Contains "Hypothesis: X."
3260
+ /Discovery:\s*[XYZ][\.\s]/,
3261
+ // Contains "Discovery: Z."
3262
+ /^[XYZ]\.\s+(?:Hypothesis|Discovery|Approach):/i
3263
+ // "X. Hypothesis: Y..."
3264
+ ];
3265
+ const isPlaceholderValue = (val) => {
3266
+ if (placeholders.includes(val)) return true;
3267
+ return templatePatterns.some((p) => p.test(val));
3268
+ };
3269
+ if (cog.current_approach && isPlaceholderValue(cog.current_approach)) {
3230
3270
  cog.current_approach = null;
3231
3271
  }
3232
- if (cog.active_hypothesis && placeholders.includes(cog.active_hypothesis)) {
3272
+ if (cog.active_hypothesis && isPlaceholderValue(cog.active_hypothesis)) {
3233
3273
  cog.active_hypothesis = null;
3234
3274
  }
3235
- if (cog.recent_discovery && placeholders.includes(cog.recent_discovery)) {
3275
+ if (cog.recent_discovery && isPlaceholderValue(cog.recent_discovery)) {
3236
3276
  cog.recent_discovery = null;
3237
3277
  }
3238
3278
  if (cog.active_hypothesis && cog.active_hypothesis.startsWith("/")) {
@@ -3261,11 +3301,35 @@ function sanitizeCognitiveState(state) {
3261
3301
  }
3262
3302
  }
3263
3303
  }
3304
+ if (cog.recent_discovery && cog.recent_discovery.length < 15 && !cog.recent_discovery.includes(" ")) {
3305
+ cog.recent_discovery = null;
3306
+ }
3307
+ if (cog.recent_discovery && /^that\s/i.test(cog.recent_discovery) && cog.recent_discovery.length < 40) {
3308
+ cog.recent_discovery = null;
3309
+ }
3264
3310
  if (state.active_task && state.active_task.startsWith("<")) {
3265
3311
  state.active_task = null;
3266
3312
  }
3313
+ if (state.active_task) {
3314
+ const conversationalPatterns = [
3315
+ /^i have another/i,
3316
+ /^just letting you/i,
3317
+ /^just a quick/i,
3318
+ /^now tell me/i,
3319
+ /^do another final/i,
3320
+ /^reviewing$/i
3321
+ // Generic tool-inferred task, not specific
3322
+ ];
3323
+ if (conversationalPatterns.some((p) => p.test(state.active_task))) {
3324
+ state.active_task = null;
3325
+ }
3326
+ }
3267
3327
  if (!state.active_task || state.active_task === "unknown task") {
3268
- const editedFiles = state.recent_actions.filter((a) => a.tool === "Edit" || a.tool === "Write").map((a) => a.target.split(/[/\\]/).pop() ?? a.target);
3328
+ const editedFiles = state.recent_actions.filter((a) => a.tool === "Edit" || a.tool === "Write").map((a) => {
3329
+ const arrowIdx = a.target.indexOf(" \u2192");
3330
+ const path = arrowIdx > 0 ? a.target.slice(0, arrowIdx) : a.target;
3331
+ return path.split(/[/\\]/).pop() ?? path;
3332
+ });
3269
3333
  const uniqueFiles = [...new Set(editedFiles)].slice(-5);
3270
3334
  if (uniqueFiles.length > 0) {
3271
3335
  if (cog.current_approach && cog.current_approach.length >= 10) {
@@ -3275,6 +3339,16 @@ function sanitizeCognitiveState(state) {
3275
3339
  }
3276
3340
  }
3277
3341
  }
3342
+ if (state.session_files.length > 0) {
3343
+ state.session_files = state.session_files.filter((f) => {
3344
+ if (f.length < 3 || f.length > 500) return false;
3345
+ if (!f.includes("/") && !f.includes("\\")) return false;
3346
+ if (f.includes("/../") || f.startsWith("../")) return false;
3347
+ if (/^\/?\d+\.\d+/.test(f)) return false;
3348
+ if (f === "/root/.ssh" || f.includes("/etc/cron")) return false;
3349
+ return true;
3350
+ });
3351
+ }
3278
3352
  }
3279
3353
  function deleteSessionState() {
3280
3354
  const watcherPath = getWatcherPath(activeSessionId);
@@ -3346,6 +3420,12 @@ function writeSessionHandoff(state, narrative) {
3346
3420
  reasoning_trail: reasoningTrail.slice(0, 10)
3347
3421
  };
3348
3422
  try {
3423
+ const serialized = JSON.stringify(handoff, null, 2);
3424
+ if (serialized.length > 65536) {
3425
+ log.warn("Session handoff too large, truncating", { size: serialized.length });
3426
+ handoff.reasoning_trail = handoff.reasoning_trail.slice(0, 3);
3427
+ handoff.lessons = handoff.lessons.slice(0, 3);
3428
+ }
3349
3429
  const tmpPath = handoffPath + ".tmp";
3350
3430
  writeFileSync(tmpPath, JSON.stringify(handoff, null, 2), "utf-8");
3351
3431
  renameSync(tmpPath, handoffPath);
@@ -3466,16 +3546,16 @@ var [command, ...args] = process.argv.slice(2);
3466
3546
  async function main() {
3467
3547
  const stdinRaw = await readStdin();
3468
3548
  const stdinJson = safeParse(stdinRaw);
3469
- const stdinSessionId = stdinJson?.session_id;
3470
- if (stdinSessionId && typeof stdinSessionId === "string" && stdinSessionId.length > 0 && stdinSessionId.length <= 200) {
3471
- activeSessionId = stdinSessionId;
3549
+ const validatedSessionId = validateSessionId(stdinJson?.session_id);
3550
+ if (validatedSessionId) {
3551
+ activeSessionId = validatedSessionId;
3472
3552
  } else if (process.env.ENGRAM_SESSION_ID) {
3473
- activeSessionId = process.env.ENGRAM_SESSION_ID;
3553
+ const envSessionId = validateSessionId(process.env.ENGRAM_SESSION_ID);
3554
+ if (envSessionId) activeSessionId = envSessionId;
3474
3555
  }
3475
3556
  migrateLegacyWatcherState();
3476
3557
  try {
3477
- const stdinCwd = stdinJson?.cwd;
3478
- const cwdForDb = stdinCwd ?? process.cwd();
3558
+ const cwdForDb = validateCwd(stdinJson?.cwd) ?? process.cwd();
3479
3559
  const projectRoot = inferProjectPath(cwdForDb);
3480
3560
  const projectDbPath = deriveProjectDbPath(projectRoot);
3481
3561
  initProjectDatabase(projectDbPath);
@@ -3496,29 +3576,12 @@ async function main() {
3496
3576
  handlePostToolGeneric(stdinJson);
3497
3577
  break;
3498
3578
  case "notification": {
3499
- try {
3500
- appendFileSync(
3501
- join(homedir(), ".engram", "notification-debug.log"),
3502
- `[${(/* @__PURE__ */ new Date()).toISOString()}] NOTIFICATION stdin_keys=${Object.keys(stdinJson ?? {}).join(",")}
3503
- `
3504
- );
3505
- } catch {
3506
- }
3507
3579
  const notifType = stdinJson?.notification_type ?? args[0] ?? "general";
3508
3580
  const notifMessage = stdinJson?.message ?? args[1] ?? "";
3509
3581
  handleNotification(notifType, notifMessage);
3510
3582
  break;
3511
3583
  }
3512
3584
  case "session-start":
3513
- try {
3514
- const src = stdinJson?.source ?? "unknown";
3515
- appendFileSync(
3516
- join(homedir(), ".engram", "notification-debug.log"),
3517
- `[${(/* @__PURE__ */ new Date()).toISOString()}] SESSION-START source=${src} stdin_keys=${Object.keys(stdinJson ?? {}).join(",")}
3518
- `
3519
- );
3520
- } catch {
3521
- }
3522
3585
  handleSessionStart(stdinJson, args[0]);
3523
3586
  break;
3524
3587
  case "session-end":
@@ -3532,14 +3595,6 @@ async function main() {
3532
3595
  handleEngramUsed(stdinJson, args[0]);
3533
3596
  break;
3534
3597
  case "pre-compact":
3535
- try {
3536
- appendFileSync(
3537
- join(homedir(), ".engram", "notification-debug.log"),
3538
- `[${(/* @__PURE__ */ new Date()).toISOString()}] PRE-COMPACT stdin=${JSON.stringify(stdinJson).slice(0, 2e3)}
3539
- `
3540
- );
3541
- } catch {
3542
- }
3543
3598
  handlePreCompact();
3544
3599
  break;
3545
3600
  case "prompt-check":
@@ -4784,12 +4839,6 @@ function extractFilesFromToolCall(tool, input, output) {
4784
4839
  function handleNotification(type, data) {
4785
4840
  try {
4786
4841
  log.info("Notification received", { type, data: truncate(data, 300) });
4787
- try {
4788
- const debugLine = `[${(/* @__PURE__ */ new Date()).toISOString()}] type=${type} data=${truncate(data, 500)}
4789
- `;
4790
- appendFileSync(join(homedir(), ".engram", "notification-debug.log"), debugLine);
4791
- } catch {
4792
- }
4793
4842
  const isContextWarning = type === "context_window" || type === "context" || /context.*(low|full|limit|running out|compact)/i.test(data) || /remaining.*context/i.test(data);
4794
4843
  if (isContextWarning) {
4795
4844
  log.info("Context pressure detected \u2014 proactive offload triggered", { type, data });
@@ -5450,6 +5499,9 @@ function handleSubagentStop(stdinJson) {
5450
5499
  const fileMatches = lastMessage.match(/(?:\/[\w./+-]+\.\w+|[\w./+-]+\.(?:ts|js|py|xml|json|css|scss|md))/g);
5451
5500
  if (fileMatches) {
5452
5501
  for (const f of fileMatches) {
5502
+ if (!f.includes("/")) continue;
5503
+ if (f.includes("/../") || f.startsWith("../") || /^\/?\d+\.\d+/.test(f)) continue;
5504
+ if (f === "/root/.ssh" || f.includes("/etc/cron")) continue;
5453
5505
  if (!state.session_files.includes(f)) {
5454
5506
  state.session_files.push(f);
5455
5507
  }
@@ -5458,7 +5510,7 @@ function handleSubagentStop(stdinJson) {
5458
5510
  state.session_files = state.session_files.slice(-50);
5459
5511
  }
5460
5512
  }
5461
- const isMetaAnalysis = lastMessage.startsWith("<analysis>") || lastMessage.startsWith("<summary>") || /^#+\s/.test(lastMessage) || lastMessage.startsWith("Based on ");
5513
+ const isMetaAnalysis = lastMessage.startsWith("<analysis>") || lastMessage.startsWith("<summary>") || /^#+\s/.test(lastMessage) || lastMessage.startsWith("Based on ") || /^(I now have|Perfect!|I have (sufficient|enough|comprehensive))/i.test(lastMessage) || /^(Here('s| is) (the|my|a) (comprehensive|complete|detailed|full))/i.test(lastMessage) || lastMessage.includes("## ") && lastMessage.length > 500;
5462
5514
  const hasError = !isMetaAnalysis && containsError(lastMessage);
5463
5515
  if (hasError) {
5464
5516
  state.recent_errors.push(truncate(lastMessage, 200));
@@ -5697,13 +5749,20 @@ function buildContinuationBrief(state) {
5697
5749
  for (const id of state.decision_memory_ids.slice(-5)) {
5698
5750
  try {
5699
5751
  const mem = getMemory(id);
5700
- if (mem) decisions.push(truncate(mem.content, 150));
5752
+ if (mem) {
5753
+ const content = mem.content;
5754
+ if (/^Delegated:|^Decision:\s*Delegated/i.test(content)) continue;
5755
+ decisions.push(truncate(content, 150));
5756
+ }
5701
5757
  } catch {
5702
5758
  }
5703
5759
  }
5704
5760
  const triedFailed = (state.session_outcomes ?? []).filter((o) => o.includes("\u2192 fail") || o.includes("\u2192 dead end") || o.includes("\u2192 blocked")).slice(-5).map((o) => truncate(o, 120));
5705
5761
  const editActions = state.recent_actions.filter((a) => a.tool === "Edit" || a.tool === "Write");
5706
- const keyFiles = [...new Set(editActions.map((a) => a.target))].slice(-10);
5762
+ const keyFiles = [...new Set(editActions.map((a) => {
5763
+ const arrowIdx = a.target.indexOf(" \u2192");
5764
+ return arrowIdx > 0 ? a.target.slice(0, arrowIdx) : a.target;
5765
+ }))].slice(-10);
5707
5766
  const recentBash = state.recent_commands?.slice(-5) ?? [];
5708
5767
  if (recentBash.length > 0) {
5709
5768
  const bashSummary = recentBash.map((c) => truncate(c.cmd, 80));
@@ -5995,7 +6054,7 @@ function handlePromptCheck(stdinJson, argFallback) {
5995
6054
  }
5996
6055
  try {
5997
6056
  if (state.total_turns > 0 && state.total_turns % CONTEXT_PRESSURE.MIN_TURNS_BETWEEN_CHECKS === 0) {
5998
- const transcriptPath = stdinJson?.transcript_path;
6057
+ const transcriptPath = validateTranscriptPath(stdinJson?.transcript_path);
5999
6058
  const remaining = estimateContextRemaining(transcriptPath);
6000
6059
  if (remaining !== null) {
6001
6060
  state.last_context_remaining = remaining;
@@ -6178,7 +6237,7 @@ function handlePromptCheck(stdinJson, argFallback) {
6178
6237
  } catch {
6179
6238
  }
6180
6239
  try {
6181
- const transcriptPath2 = stdinJson?.transcript_path;
6240
+ const transcriptPath2 = validateTranscriptPath(stdinJson?.transcript_path);
6182
6241
  if (transcriptPath2 && state.total_turns > 0 && state.total_turns - state.last_reasoning_extraction_turn >= TRANSCRIPT_REASONING.EXTRACTION_INTERVAL_TURNS && state.reasoning_extraction_count < TRANSCRIPT_REASONING.MAX_PER_SESSION && canEncodeReasoning(state)) {
6183
6242
  const reasoningSnippets = extractReasoningFromTranscript(transcriptPath2);
6184
6243
  for (const snippet of reasoningSnippets.slice(0, 2)) {
@@ -7132,10 +7191,14 @@ function handlePostCompact(stdinJson) {
7132
7191
  if (!recovery) return;
7133
7192
  const budget = new OutputBudget(OUTPUT_BUDGET.POST_COMPACT_MAX_BYTES);
7134
7193
  const lines = [];
7135
- const brief = state.continuation_brief;
7136
- if (brief) {
7194
+ sanitizeCognitiveState(state);
7195
+ const brief = buildContinuationBrief(state);
7196
+ const briefUsable = brief.task !== "unknown task" || brief.last_actions.length > 0;
7197
+ if (briefUsable) {
7137
7198
  const mindLines = ["[Engram] Continue from where you left off:"];
7138
- mindLines.push(` Task: ${brief.task}`);
7199
+ if (brief.task !== "unknown task") {
7200
+ mindLines.push(` Task: ${brief.task}`);
7201
+ }
7139
7202
  mindLines.push(` Phase: ${brief.phase}`);
7140
7203
  if (brief.last_actions.length > 0) {
7141
7204
  mindLines.push(` Last actions:`);
@@ -7186,7 +7249,7 @@ function handlePostCompact(stdinJson) {
7186
7249
  }
7187
7250
  }
7188
7251
  try {
7189
- const transcriptPath = stdinJson?.transcript_path ?? null;
7252
+ const transcriptPath = validateTranscriptPath(stdinJson?.transcript_path);
7190
7253
  if (transcriptPath) {
7191
7254
  const reasoningSnippets = extractReasoningFromTranscript(transcriptPath, TRANSCRIPT_REASONING.POST_COMPACT_MAX_MESSAGES);
7192
7255
  if (reasoningSnippets.length > 0) {
package/dist/index.js CHANGED
@@ -154,7 +154,7 @@ import {
154
154
  vaccinate,
155
155
  vacuumDatabase,
156
156
  validateMultiPerspective
157
- } from "./chunk-OY2XHPUF.js";
157
+ } from "./chunk-O3ZP4K3T.js";
158
158
 
159
159
  // src/index.ts
160
160
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vedtechsolutions/engram-mcp",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "Cognitive memory system for AI — persistent, cross-session learning via MCP",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",