engrm 0.4.38 → 0.4.39

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/dist/server.js CHANGED
@@ -19,6 +19,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
  // src/server.ts
20
20
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
21
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
22
23
 
23
24
  // node_modules/zod/v4/classic/external.js
24
25
  var exports_external = {};
@@ -13552,6 +13553,9 @@ function date4(params) {
13552
13553
 
13553
13554
  // node_modules/zod/v4/classic/external.js
13554
13555
  config(en_default());
13556
+ // src/server.ts
13557
+ import { createServer } from "node:http";
13558
+
13555
13559
  // src/config.ts
13556
13560
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13557
13561
  import { homedir, hostname as hostname3, networkInterfaces } from "node:os";
@@ -13626,6 +13630,16 @@ function createDefaultConfig() {
13626
13630
  },
13627
13631
  transcript_analysis: {
13628
13632
  enabled: false
13633
+ },
13634
+ http: {
13635
+ enabled: false,
13636
+ port: 3767,
13637
+ bearer_tokens: []
13638
+ },
13639
+ fleet: {
13640
+ project_name: "shared-experience",
13641
+ namespace: "",
13642
+ api_key: ""
13629
13643
  }
13630
13644
  };
13631
13645
  }
@@ -13687,6 +13701,16 @@ function loadConfig() {
13687
13701
  },
13688
13702
  transcript_analysis: {
13689
13703
  enabled: asBool(config2["transcript_analysis"]?.["enabled"], defaults.transcript_analysis.enabled)
13704
+ },
13705
+ http: {
13706
+ enabled: asBool(config2["http"]?.["enabled"], defaults.http.enabled),
13707
+ port: asNumber(config2["http"]?.["port"], defaults.http.port),
13708
+ bearer_tokens: asStringArray(config2["http"]?.["bearer_tokens"], defaults.http.bearer_tokens)
13709
+ },
13710
+ fleet: {
13711
+ project_name: asString(config2["fleet"]?.["project_name"], defaults.fleet.project_name),
13712
+ namespace: asString(config2["fleet"]?.["namespace"], defaults.fleet.namespace),
13713
+ api_key: asString(config2["fleet"]?.["api_key"], defaults.fleet.api_key)
13690
13714
  }
13691
13715
  };
13692
13716
  }
@@ -15506,6 +15530,12 @@ function containsSecrets(text, customPatterns = []) {
15506
15530
  }
15507
15531
  return false;
15508
15532
  }
15533
+ var FLEET_HOSTNAME_PATTERN = /\b(?=.{1,253}\b)(?!-)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}\b/gi;
15534
+ var FLEET_IP_PATTERN = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
15535
+ var FLEET_MAC_PATTERN = /\b(?:[0-9a-f]{2}[:-]){5}[0-9a-f]{2}\b/gi;
15536
+ function scrubFleetIdentifiers(text) {
15537
+ return text.replace(FLEET_MAC_PATTERN, "[REDACTED_MAC]").replace(FLEET_IP_PATTERN, "[REDACTED_IP]").replace(FLEET_HOSTNAME_PATTERN, "[REDACTED_HOSTNAME]");
15538
+ }
15509
15539
 
15510
15540
  // src/capture/quality.ts
15511
15541
  var QUALITY_THRESHOLD = 0.1;
@@ -16093,6 +16123,35 @@ function narrativesConflict(narrative1, narrative2) {
16093
16123
  return null;
16094
16124
  }
16095
16125
 
16126
+ // src/sync/targets.ts
16127
+ function isFleetProjectName(projectName, config2) {
16128
+ const fleetProjectName = config2.fleet?.project_name ?? "shared-experience";
16129
+ if (!projectName || !fleetProjectName)
16130
+ return false;
16131
+ return projectName.trim().toLowerCase() === fleetProjectName.trim().toLowerCase();
16132
+ }
16133
+ function hasFleetTarget(config2) {
16134
+ return Boolean(config2.fleet?.namespace?.trim() && config2.fleet?.api_key?.trim() && (config2.fleet?.project_name ?? "shared-experience").trim());
16135
+ }
16136
+ function resolveSyncTarget(config2, projectName) {
16137
+ if (isFleetProjectName(projectName, config2) && hasFleetTarget(config2)) {
16138
+ return {
16139
+ key: `fleet:${config2.fleet.namespace}`,
16140
+ apiKey: config2.fleet.api_key,
16141
+ namespace: config2.fleet.namespace,
16142
+ siteId: config2.site_id,
16143
+ isFleet: true
16144
+ };
16145
+ }
16146
+ return {
16147
+ key: `default:${config2.namespace}`,
16148
+ apiKey: config2.candengo_api_key,
16149
+ namespace: config2.namespace,
16150
+ siteId: config2.site_id,
16151
+ isFleet: false
16152
+ };
16153
+ }
16154
+
16096
16155
  // src/tools/save.ts
16097
16156
  var VALID_TYPES = [
16098
16157
  "bugfix",
@@ -16141,7 +16200,8 @@ async function saveObservation(db, config2, input) {
16141
16200
  const factsJson = structuredFacts.length > 0 ? config2.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
16142
16201
  const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
16143
16202
  const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
16144
- let sensitivity = input.sensitivity ?? config2.scrubbing.default_sensitivity;
16203
+ const fleetProject = isFleetProjectName(project.name, config2);
16204
+ let sensitivity = input.sensitivity ?? (fleetProject ? "shared" : config2.scrubbing.default_sensitivity);
16145
16205
  if (config2.scrubbing.enabled && containsSecrets([input.title, input.narrative, JSON.stringify(input.facts)].filter(Boolean).join(" "), customPatterns)) {
16146
16206
  if (sensitivity === "shared") {
16147
16207
  sensitivity = "personal";
@@ -18489,20 +18549,21 @@ function listRecallItems(db, input) {
18489
18549
  source_agent: null
18490
18550
  }))
18491
18551
  ];
18492
- const deduped = dedupeRecallItems(items).sort((a, b) => compareRecallItems(a, b, input.current_device_id)).slice(0, limit);
18552
+ const deduped = dedupeRecallItems(items).sort((a, b) => compareRecallItems(a, b, input.current_device_id, input.preferred_agent)).slice(0, limit);
18493
18553
  return {
18494
18554
  project: projectName,
18495
18555
  continuity_mode: deduped.some((item) => item.kind === "handoff" || item.kind === "thread") ? "direct" : "indexed",
18496
18556
  items: deduped
18497
18557
  };
18498
18558
  }
18499
- function compareRecallItems(a, b, currentDeviceId) {
18559
+ function compareRecallItems(a, b, currentDeviceId, preferredAgent) {
18500
18560
  const priority = (item) => {
18501
18561
  const freshness = item.freshness === "live" ? 0 : item.freshness === "recent" ? 1 : 2;
18502
18562
  const kind = item.kind === "handoff" ? 0 : item.kind === "thread" ? 1 : item.kind === "chat" ? 2 : 3;
18503
18563
  const remoteBoost = currentDeviceId && item.source_device_id && item.source_device_id !== currentDeviceId ? -0.5 : 0;
18564
+ const preferredAgentBoost = preferredAgent && item.source_agent === preferredAgent ? -0.75 : 0;
18504
18565
  const draftPenalty = item.kind === "handoff" && /draft/i.test(item.title) ? 0.25 : 0;
18505
- return freshness * 10 + kind + remoteBoost + draftPenalty;
18566
+ return freshness * 10 + kind + remoteBoost + preferredAgentBoost + draftPenalty;
18506
18567
  };
18507
18568
  const priorityDiff = priority(a) - priority(b);
18508
18569
  if (priorityDiff !== 0)
@@ -18664,18 +18725,20 @@ function getProjectMemoryIndex(db, input) {
18664
18725
  user_id: input.user_id,
18665
18726
  limit: 20
18666
18727
  });
18728
+ const activeAgents = collectActiveAgents(recentSessions);
18729
+ const preferredAgent = pickPreferredAgent(activeAgents, recentSessions[0]?.agent ?? null);
18667
18730
  const recentChatCount = recentChat.messages.length;
18668
18731
  const recallIndex = listRecallItems(db, {
18669
18732
  cwd,
18670
18733
  project_scoped: true,
18671
18734
  user_id: input.user_id,
18735
+ preferred_agent: preferredAgent,
18672
18736
  limit: 10
18673
18737
  });
18674
18738
  const latestSession = recentSessions[0] ?? null;
18675
18739
  const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
18676
18740
  const recentOutcomes = observations.filter((obs) => ["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type)).map((obs) => obs.title.trim()).filter((title) => title.length > 0 && !looksLikeFileOperationTitle4(title)).slice(0, 8);
18677
18741
  const captureSummary = summarizeCaptureState(recentSessions);
18678
- const activeAgents = collectActiveAgents(recentSessions);
18679
18742
  const topTypes = Object.entries(counts).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)).slice(0, 5);
18680
18743
  const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length, recentChatCount, recentChat.coverage_state, activeAgents);
18681
18744
  const estimatedReadTokens = estimateTokens([
@@ -18689,7 +18752,7 @@ function getProjectMemoryIndex(db, input) {
18689
18752
  `));
18690
18753
  const continuityState = classifyContinuityState(recentRequestsCount, recentToolsCount, recentHandoffsCount.length, recentChatCount, recentSessions, recentOutcomes.length);
18691
18754
  const sourceTimestamp = pickResumeSourceTimestamp(latestSession, recentChat.messages);
18692
- const bestRecallItem = pickBestRecallItem(recallIndex.items);
18755
+ const bestRecallItem = pickBestRecallItem(recallIndex.items, preferredAgent);
18693
18756
  return {
18694
18757
  project: project.name,
18695
18758
  canonical_id: project.canonical_id,
@@ -18709,7 +18772,7 @@ function getProjectMemoryIndex(db, input) {
18709
18772
  best_recall_key: bestRecallItem?.key ?? null,
18710
18773
  best_recall_title: bestRecallItem?.title ?? null,
18711
18774
  best_recall_kind: bestRecallItem?.kind ?? null,
18712
- best_agent_resume_agent: activeAgents.length > 1 ? latestSession?.agent ?? null : null,
18775
+ best_agent_resume_agent: activeAgents.length > 1 ? preferredAgent : null,
18713
18776
  resume_freshness: classifyResumeFreshness(sourceTimestamp),
18714
18777
  resume_source_session_id: latestSession?.session_id ?? null,
18715
18778
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -18740,7 +18803,12 @@ function getProjectMemoryIndex(db, input) {
18740
18803
  suggested_tools: suggestedTools
18741
18804
  };
18742
18805
  }
18743
- function pickBestRecallItem(items) {
18806
+ function pickBestRecallItem(items, preferredAgent) {
18807
+ if (preferredAgent) {
18808
+ const preferred = items.find((item) => item.source_agent === preferredAgent && item.kind !== "memory") ?? items.find((item) => item.source_agent === preferredAgent);
18809
+ if (preferred)
18810
+ return preferred;
18811
+ }
18744
18812
  return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
18745
18813
  }
18746
18814
  function pickResumeSourceTimestamp(latestSession, messages) {
@@ -18828,6 +18896,11 @@ function summarizeCaptureState(sessions) {
18828
18896
  function collectActiveAgents(sessions) {
18829
18897
  return Array.from(new Set(sessions.map((session) => session.agent?.trim()).filter((agent) => Boolean(agent) && !agent.startsWith("engrm-")))).sort();
18830
18898
  }
18899
+ function pickPreferredAgent(activeAgents, latestAgent) {
18900
+ if (activeAgents.includes("claude-code"))
18901
+ return "claude-code";
18902
+ return latestAgent && activeAgents.includes(latestAgent) ? latestAgent : activeAgents[0] ?? null;
18903
+ }
18831
18904
  function buildSuggestedTools(sessions, requestCount, toolCount, observationCount, recentChatCount, chatCoverageState, activeAgents) {
18832
18905
  const suggested = [];
18833
18906
  if (sessions.length > 0) {
@@ -18912,6 +18985,7 @@ function getMemoryConsole(db, input) {
18912
18985
  cwd,
18913
18986
  project_scoped: projectScoped,
18914
18987
  user_id: input.user_id,
18988
+ preferred_agent: pickPreferredAgent(projectIndex?.active_agents ?? collectActiveAgents(sessions), sessions[0]?.agent ?? null) ?? undefined,
18915
18989
  limit: 10
18916
18990
  });
18917
18991
  const projectIndex = projectScoped ? getProjectMemoryIndex(db, {
@@ -18936,10 +19010,10 @@ function getMemoryConsole(db, input) {
18936
19010
  title: item.title,
18937
19011
  source_agent: item.source_agent
18938
19012
  })),
18939
- best_recall_key: projectIndex?.best_recall_key ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.key ?? null,
18940
- best_recall_title: projectIndex?.best_recall_title ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.title ?? null,
18941
- best_recall_kind: projectIndex?.best_recall_kind ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.kind ?? null,
18942
- best_agent_resume_agent: projectIndex?.best_agent_resume_agent ?? (activeAgents.length > 1 ? sessions[0]?.agent ?? null : null),
19013
+ best_recall_key: projectIndex?.best_recall_key ?? pickBestRecallItem2(recallIndex.items, activeAgents, sessions[0]?.agent ?? null)?.key ?? null,
19014
+ best_recall_title: projectIndex?.best_recall_title ?? pickBestRecallItem2(recallIndex.items, activeAgents, sessions[0]?.agent ?? null)?.title ?? null,
19015
+ best_recall_kind: projectIndex?.best_recall_kind ?? pickBestRecallItem2(recallIndex.items, activeAgents, sessions[0]?.agent ?? null)?.kind ?? null,
19016
+ best_agent_resume_agent: projectIndex?.best_agent_resume_agent ?? (activeAgents.length > 1 ? pickPreferredAgent(activeAgents, sessions[0]?.agent ?? null) : null),
18943
19017
  resume_freshness: projectIndex?.resume_freshness ?? "stale",
18944
19018
  resume_source_session_id: projectIndex?.resume_source_session_id ?? sessions[0]?.session_id ?? null,
18945
19019
  resume_source_device_id: projectIndex?.resume_source_device_id ?? sessions[0]?.device_id ?? null,
@@ -18969,6 +19043,15 @@ function getMemoryConsole(db, input) {
18969
19043
  suggested_tools: projectIndex?.suggested_tools ?? buildFallbackSuggestedTools(sessions.length, requests.length, tools.length, observations.length, recentHandoffs.length, recentChat.messages.length, recentChat.coverage_state, activeAgents.length)
18970
19044
  };
18971
19045
  }
19046
+ function pickBestRecallItem2(items, activeAgents, latestAgent) {
19047
+ const preferredAgent = pickPreferredAgent(activeAgents, latestAgent);
19048
+ if (preferredAgent) {
19049
+ const preferred = items.find((item) => item.source_agent === preferredAgent && item.kind !== "memory") ?? items.find((item) => item.source_agent === preferredAgent);
19050
+ if (preferred)
19051
+ return preferred;
19052
+ }
19053
+ return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
19054
+ }
18972
19055
  function collectProvenanceTypeMix(observations) {
18973
19056
  const grouped = new Map;
18974
19057
  for (const observation of observations) {
@@ -19394,14 +19477,20 @@ function getCaptureStatus(db, input = {}) {
19394
19477
  const claudeSettings = join3(home, ".claude", "settings.json");
19395
19478
  const codexConfig = join3(home, ".codex", "config.toml");
19396
19479
  const codexHooks = join3(home, ".codex", "hooks.json");
19480
+ const opencodeConfig = join3(home, ".config", "opencode", "opencode.json");
19481
+ const opencodePlugin = join3(home, ".config", "opencode", "plugins", "engrm.js");
19482
+ const config2 = configExists() ? loadConfig() : null;
19397
19483
  const claudeJsonContent = existsSync3(claudeJson) ? readFileSync3(claudeJson, "utf-8") : "";
19398
19484
  const claudeSettingsContent = existsSync3(claudeSettings) ? readFileSync3(claudeSettings, "utf-8") : "";
19399
19485
  const codexConfigContent = existsSync3(codexConfig) ? readFileSync3(codexConfig, "utf-8") : "";
19400
19486
  const codexHooksContent = existsSync3(codexHooks) ? readFileSync3(codexHooks, "utf-8") : "";
19487
+ const opencodeConfigContent = existsSync3(opencodeConfig) ? readFileSync3(opencodeConfig, "utf-8") : "";
19401
19488
  const claudeMcpRegistered = claudeJsonContent.includes('"engrm"');
19402
19489
  const claudeHooksRegistered = claudeSettingsContent.includes("engrm") || claudeSettingsContent.includes("session-start") || claudeSettingsContent.includes("user-prompt-submit");
19403
19490
  const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME}]`);
19404
19491
  const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
19492
+ const opencodeMcpRegistered = opencodeConfigContent.includes('"engrm"') && opencodeConfigContent.includes('"type"') && opencodeConfigContent.includes('"local"');
19493
+ const opencodePluginRegistered = existsSync3(opencodePlugin);
19405
19494
  let claudeHookCount = 0;
19406
19495
  let claudeSessionStartHook = false;
19407
19496
  let claudeUserPromptHook = false;
@@ -19474,6 +19563,11 @@ function getCaptureStatus(db, input = {}) {
19474
19563
  return {
19475
19564
  schema_version: schemaVersion,
19476
19565
  schema_current: schemaVersion >= LATEST_SCHEMA_VERSION,
19566
+ http_enabled: Boolean(config2?.http?.enabled || process.env.ENGRM_HTTP_PORT),
19567
+ http_port: config2?.http?.port ?? (process.env.ENGRM_HTTP_PORT ? Number(process.env.ENGRM_HTTP_PORT) : null),
19568
+ http_bearer_token_count: config2?.http?.bearer_tokens?.length ?? 0,
19569
+ fleet_project_name: config2?.fleet?.project_name ?? null,
19570
+ fleet_configured: Boolean(config2?.fleet?.namespace && config2?.fleet?.api_key),
19477
19571
  claude_mcp_registered: claudeMcpRegistered,
19478
19572
  claude_hooks_registered: claudeHooksRegistered,
19479
19573
  claude_hook_count: claudeHookCount,
@@ -19486,6 +19580,8 @@ function getCaptureStatus(db, input = {}) {
19486
19580
  codex_session_start_hook: codexSessionStartHook,
19487
19581
  codex_stop_hook: codexStopHook,
19488
19582
  codex_raw_chronology_supported: false,
19583
+ opencode_mcp_registered: opencodeMcpRegistered,
19584
+ opencode_plugin_registered: opencodePluginRegistered,
19489
19585
  recent_user_prompts: recentUserPrompts,
19490
19586
  recent_tool_events: recentToolEvents,
19491
19587
  recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
@@ -20066,6 +20162,7 @@ function getSessionContext(db, input) {
20066
20162
  project_scoped: true,
20067
20163
  user_id: input.user_id,
20068
20164
  current_device_id: input.current_device_id,
20165
+ preferred_agent: pickPreferredAgent(collectActiveAgents(context.recentSessions ?? []), context.recentSessions?.[0]?.agent ?? null) ?? undefined,
20069
20166
  limit: 10
20070
20167
  });
20071
20168
  const latestSession = context.recentSessions?.[0] ?? null;
@@ -20075,8 +20172,9 @@ function getSessionContext(db, input) {
20075
20172
  const continuityState = classifyContinuityState(recentRequests, recentTools, recentHandoffs, recentChatMessages, context.recentSessions ?? [], (context.recentOutcomes ?? []).length);
20076
20173
  const latestChatEpoch = recentChat.messages.length > 0 ? recentChat.messages[recentChat.messages.length - 1]?.created_at_epoch ?? null : null;
20077
20174
  const resumeTimestamp = latestChatEpoch ?? latestSession?.completed_at_epoch ?? latestSession?.started_at_epoch ?? null;
20078
- const bestRecallItem = recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null;
20079
20175
  const activeAgents = collectActiveAgents(context.recentSessions ?? []);
20176
+ const preferredAgent = pickPreferredAgent(activeAgents, latestSession?.agent ?? null);
20177
+ const bestRecallItem = preferredAgent ? recallIndex.items.find((item) => item.source_agent === preferredAgent && item.kind !== "memory") ?? recallIndex.items.find((item) => item.source_agent === preferredAgent) ?? recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null : recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null;
20080
20178
  return {
20081
20179
  project_name: context.project_name,
20082
20180
  canonical_id: context.canonical_id,
@@ -20096,7 +20194,7 @@ function getSessionContext(db, input) {
20096
20194
  best_recall_key: bestRecallItem?.key ?? null,
20097
20195
  best_recall_title: bestRecallItem?.title ?? null,
20098
20196
  best_recall_kind: bestRecallItem?.kind ?? null,
20099
- best_agent_resume_agent: activeAgents.length > 1 ? latestSession?.agent ?? null : null,
20197
+ best_agent_resume_agent: activeAgents.length > 1 ? preferredAgent : null,
20100
20198
  resume_freshness: classifyResumeFreshness(resumeTimestamp),
20101
20199
  resume_source_session_id: latestSession?.session_id ?? null,
20102
20200
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -20760,8 +20858,10 @@ async function resumeThread(db, config2, input = {}) {
20760
20858
  const detected = detectProject(cwd);
20761
20859
  const project = db.getProjectByCanonicalId(detected.canonical_id);
20762
20860
  let snapshot = await buildResumeSnapshot(db, cwd, input.user_id, input.current_device_id, limit);
20763
- if (input.agent) {
20764
- snapshot = filterResumeSnapshotByAgent(snapshot, input.agent, input.current_device_id);
20861
+ const activeAgents = Array.from(new Set(snapshot.recentSessions.map((session) => session.agent).filter((agent) => Boolean(agent))));
20862
+ const effectiveAgent = input.agent ?? pickPreferredAgent(activeAgents, snapshot.recentSessions[0]?.agent ?? null) ?? undefined;
20863
+ if (effectiveAgent) {
20864
+ snapshot = filterResumeSnapshotByAgent(snapshot, effectiveAgent, input.current_device_id);
20765
20865
  }
20766
20866
  let repairResult = null;
20767
20867
  const shouldRepair = repairIfNeeded && snapshot.recentChat.coverage_state !== "transcript-backed" && (snapshot.recentChat.messages.length > 0 || snapshot.recentSessions.length > 0 || snapshot.context?.continuity_state !== "cold");
@@ -20773,13 +20873,13 @@ async function resumeThread(db, config2, input = {}) {
20773
20873
  });
20774
20874
  if (repairResult.imported_chat_messages > 0) {
20775
20875
  snapshot = await buildResumeSnapshot(db, cwd, input.user_id, input.current_device_id, limit);
20776
- if (input.agent) {
20777
- snapshot = filterResumeSnapshotByAgent(snapshot, input.agent, input.current_device_id);
20876
+ if (effectiveAgent) {
20877
+ snapshot = filterResumeSnapshotByAgent(snapshot, effectiveAgent, input.current_device_id);
20778
20878
  }
20779
20879
  }
20780
20880
  }
20781
20881
  const { context, handoff, recentChat, recentSessions, recall } = snapshot;
20782
- const bestRecallItem = pickBestRecallItem2(snapshot.recallIndex.items);
20882
+ const bestRecallItem = pickBestRecallItem3(snapshot.recallIndex.items, effectiveAgent);
20783
20883
  const latestSession = recentSessions[0] ?? null;
20784
20884
  const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
20785
20885
  const inferredRequest = latestSession?.request?.trim() || null;
@@ -20816,7 +20916,7 @@ async function resumeThread(db, config2, input = {}) {
20816
20916
  ])).slice(0, 4);
20817
20917
  return {
20818
20918
  project_name: project?.name ?? context?.project_name ?? null,
20819
- target_agent: input.agent ?? null,
20919
+ target_agent: effectiveAgent ?? null,
20820
20920
  continuity_state: context?.continuity_state ?? "cold",
20821
20921
  continuity_summary: context?.continuity_summary ?? "No fresh repo-local continuity yet; older memory should be treated cautiously.",
20822
20922
  resume_freshness: classifyResumeFreshness2(sourceTimestamp),
@@ -20936,7 +21036,12 @@ function filterResumeSnapshotByAgent(snapshot, agent, currentDeviceId) {
20936
21036
  recallIndex
20937
21037
  };
20938
21038
  }
20939
- function pickBestRecallItem2(items) {
21039
+ function pickBestRecallItem3(items, preferredAgent) {
21040
+ if (preferredAgent) {
21041
+ const preferred = items.find((item) => item.source_agent === preferredAgent && item.kind !== "memory") ?? items.find((item) => item.source_agent === preferredAgent);
21042
+ if (preferred)
21043
+ return preferred;
21044
+ }
20940
21045
  return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
20941
21046
  }
20942
21047
  function compareRecallCandidates(epochA, epochB, deviceA, deviceB, currentDeviceId) {
@@ -21473,16 +21578,16 @@ class VectorClient {
21473
21578
  apiKey;
21474
21579
  siteId;
21475
21580
  namespace;
21476
- constructor(config2) {
21581
+ constructor(config2, overrides = {}) {
21477
21582
  const baseUrl = getBaseUrl(config2);
21478
- const apiKey = getApiKey(config2);
21583
+ const apiKey = overrides.apiKey ?? getApiKey(config2);
21479
21584
  if (!baseUrl || !apiKey) {
21480
21585
  throw new Error("VectorClient requires candengo_url and candengo_api_key");
21481
21586
  }
21482
21587
  this.baseUrl = baseUrl.replace(/\/$/, "");
21483
21588
  this.apiKey = apiKey;
21484
- this.siteId = config2.site_id;
21485
- this.namespace = config2.namespace;
21589
+ this.siteId = overrides.siteId ?? config2.site_id;
21590
+ this.namespace = overrides.namespace ?? config2.namespace;
21486
21591
  }
21487
21592
  static isConfigured(config2) {
21488
21593
  return getApiKey(config2) !== null && getBaseUrl(config2) !== null;
@@ -21648,10 +21753,10 @@ function parseJsonArray5(value) {
21648
21753
  }
21649
21754
 
21650
21755
  // src/sync/push.ts
21651
- function buildChatVectorDocument(chat, config2, project) {
21756
+ function buildChatVectorDocument(chat, config2, project, target = resolveSyncTarget(config2, project.name)) {
21652
21757
  return {
21653
- site_id: config2.site_id,
21654
- namespace: config2.namespace,
21758
+ site_id: target.siteId,
21759
+ namespace: target.namespace,
21655
21760
  source_type: "chat",
21656
21761
  source_id: buildSourceId(config2, chat.id, "chat"),
21657
21762
  content: chat.content,
@@ -21672,7 +21777,7 @@ function buildChatVectorDocument(chat, config2, project) {
21672
21777
  }
21673
21778
  };
21674
21779
  }
21675
- function buildVectorDocument(obs, config2, project) {
21780
+ function buildVectorDocument(obs, config2, project, target = resolveSyncTarget(config2, project.name)) {
21676
21781
  const parts = [obs.title];
21677
21782
  if (obs.narrative)
21678
21783
  parts.push(obs.narrative);
@@ -21689,8 +21794,8 @@ function buildVectorDocument(obs, config2, project) {
21689
21794
  }
21690
21795
  }
21691
21796
  return {
21692
- site_id: config2.site_id,
21693
- namespace: config2.namespace,
21797
+ site_id: target.siteId,
21798
+ namespace: target.namespace,
21694
21799
  source_type: obs.type,
21695
21800
  source_id: buildSourceId(config2, obs.id),
21696
21801
  content: parts.join(`
@@ -21721,7 +21826,10 @@ function buildVectorDocument(obs, config2, project) {
21721
21826
  }
21722
21827
  };
21723
21828
  }
21724
- function buildSummaryVectorDocument(summary, config2, project, observations = [], captureContext) {
21829
+ function buildSummaryVectorDocument(summary, config2, project, targetOrObservations = resolveSyncTarget(config2, project.name), observationsOrCaptureContext = [], captureContext) {
21830
+ const target = Array.isArray(targetOrObservations) ? resolveSyncTarget(config2, project.name) : targetOrObservations;
21831
+ const observations = Array.isArray(targetOrObservations) ? targetOrObservations : Array.isArray(observationsOrCaptureContext) ? observationsOrCaptureContext : [];
21832
+ const resolvedCaptureContext = Array.isArray(observationsOrCaptureContext) ? captureContext : observationsOrCaptureContext;
21725
21833
  const parts = [];
21726
21834
  if (summary.request)
21727
21835
  parts.push(`Request: ${summary.request}`);
@@ -21734,9 +21842,17 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
21734
21842
  if (summary.next_steps)
21735
21843
  parts.push(`Next Steps: ${summary.next_steps}`);
21736
21844
  const valueSignals = computeSessionValueSignals(observations, []);
21845
+ const observationSourceTools = resolvedCaptureContext?.observation_source_tools ?? summarizeObservationSourceTools(observations);
21846
+ const latestObservationPromptNumber = resolvedCaptureContext?.latest_observation_prompt_number ?? observations.reduce((latest, obs) => {
21847
+ if (typeof obs.source_prompt_number !== "number")
21848
+ return latest;
21849
+ if (latest === null || obs.source_prompt_number > latest)
21850
+ return obs.source_prompt_number;
21851
+ return latest;
21852
+ }, null);
21737
21853
  return {
21738
- site_id: config2.site_id,
21739
- namespace: config2.namespace,
21854
+ site_id: target.siteId,
21855
+ namespace: target.namespace,
21740
21856
  source_type: "summary",
21741
21857
  source_id: buildSourceId(config2, summary.id, "summary"),
21742
21858
  content: parts.join(`
@@ -21758,18 +21874,18 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
21758
21874
  learned_items: extractSectionItems2(summary.learned),
21759
21875
  completed_items: extractSectionItems2(summary.completed),
21760
21876
  next_step_items: extractSectionItems2(summary.next_steps),
21761
- prompt_count: captureContext?.prompt_count ?? 0,
21762
- tool_event_count: captureContext?.tool_event_count ?? 0,
21763
- capture_state: captureContext?.capture_state ?? "summary-only",
21764
- recent_request_prompts: captureContext?.recent_request_prompts ?? [],
21765
- latest_request: captureContext?.latest_request ?? null,
21766
- current_thread: captureContext?.current_thread ?? null,
21767
- recent_tool_names: captureContext?.recent_tool_names ?? [],
21768
- recent_tool_commands: captureContext?.recent_tool_commands ?? [],
21769
- hot_files: captureContext?.hot_files ?? [],
21770
- recent_outcomes: captureContext?.recent_outcomes ?? [],
21771
- observation_source_tools: captureContext?.observation_source_tools ?? [],
21772
- latest_observation_prompt_number: captureContext?.latest_observation_prompt_number ?? null,
21877
+ prompt_count: resolvedCaptureContext?.prompt_count ?? 0,
21878
+ tool_event_count: resolvedCaptureContext?.tool_event_count ?? 0,
21879
+ capture_state: resolvedCaptureContext?.capture_state ?? "summary-only",
21880
+ recent_request_prompts: resolvedCaptureContext?.recent_request_prompts ?? [],
21881
+ latest_request: resolvedCaptureContext?.latest_request ?? null,
21882
+ current_thread: resolvedCaptureContext?.current_thread ?? null,
21883
+ recent_tool_names: resolvedCaptureContext?.recent_tool_names ?? [],
21884
+ recent_tool_commands: resolvedCaptureContext?.recent_tool_commands ?? [],
21885
+ hot_files: resolvedCaptureContext?.hot_files ?? [],
21886
+ recent_outcomes: resolvedCaptureContext?.recent_outcomes ?? [],
21887
+ observation_source_tools: observationSourceTools,
21888
+ latest_observation_prompt_number: latestObservationPromptNumber,
21773
21889
  decisions_count: valueSignals.decisions_count,
21774
21890
  lessons_count: valueSignals.lessons_count,
21775
21891
  discoveries_count: valueSignals.discoveries_count,
@@ -21783,7 +21899,7 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
21783
21899
  }
21784
21900
  };
21785
21901
  }
21786
- async function pushOutbox(db, client, config2, batchSize = 50) {
21902
+ async function pushOutbox(db, config2, batchSize = 50) {
21787
21903
  const entries = getPendingEntries(db, batchSize);
21788
21904
  let pushed = 0;
21789
21905
  let failed = 0;
@@ -21810,11 +21926,12 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
21810
21926
  const summaryObservations = db.getObservationsBySession(summary.session_id);
21811
21927
  const sessionPrompts = db.getSessionUserPrompts(summary.session_id, 20);
21812
21928
  const sessionToolEvents = db.getSessionToolEvents(summary.session_id, 20);
21929
+ const target2 = resolveSyncTarget(config2, project2.name);
21813
21930
  const doc3 = buildSummaryVectorDocument(summary, config2, {
21814
21931
  canonical_id: project2.canonical_id,
21815
21932
  name: project2.name
21816
- }, summaryObservations, buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, summaryObservations));
21817
- batch.push({ entryId: entry.id, doc: doc3 });
21933
+ }, target2, summaryObservations, buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, summaryObservations));
21934
+ batch.push({ entryId: entry.id, doc: maybeScrubFleetDocument(doc3, target2), target: target2 });
21818
21935
  continue;
21819
21936
  }
21820
21937
  if (entry.record_type === "chat_message") {
@@ -21836,11 +21953,12 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
21836
21953
  continue;
21837
21954
  }
21838
21955
  markSyncing(db, entry.id);
21956
+ const target2 = resolveSyncTarget(config2, project2.name);
21839
21957
  const doc3 = buildChatVectorDocument(chat, config2, {
21840
21958
  canonical_id: project2.canonical_id,
21841
21959
  name: project2.name
21842
- });
21843
- batch.push({ entryId: entry.id, doc: doc3 });
21960
+ }, target2);
21961
+ batch.push({ entryId: entry.id, doc: maybeScrubFleetDocument(doc3, target2), target: target2 });
21844
21962
  continue;
21845
21963
  }
21846
21964
  if (entry.record_type !== "observation") {
@@ -21870,30 +21988,33 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
21870
21988
  continue;
21871
21989
  }
21872
21990
  markSyncing(db, entry.id);
21991
+ const target = resolveSyncTarget(config2, project.name);
21873
21992
  const doc2 = buildVectorDocument(obs, config2, {
21874
21993
  canonical_id: project.canonical_id,
21875
21994
  name: project.name
21876
- });
21877
- batch.push({ entryId: entry.id, doc: doc2 });
21995
+ }, target);
21996
+ batch.push({ entryId: entry.id, doc: maybeScrubFleetDocument(doc2, target), target });
21878
21997
  }
21879
21998
  if (batch.length === 0)
21880
21999
  return { pushed, failed, skipped };
21881
- try {
21882
- await client.batchIngest(batch.map((b) => b.doc));
21883
- for (const { entryId, doc: doc2 } of batch) {
21884
- if (doc2.source_type === "chat") {
21885
- const localId = typeof doc2.metadata?.local_id === "number" ? doc2.metadata.local_id : null;
21886
- if (localId !== null) {
21887
- db.db.query("UPDATE chat_messages SET remote_source_id = ? WHERE id = ?").run(doc2.source_id, localId);
21888
- }
21889
- }
21890
- markSynced(db, entryId);
21891
- pushed++;
22000
+ const grouped = new Map;
22001
+ for (const item of batch) {
22002
+ const existing = grouped.get(item.target.key);
22003
+ if (existing) {
22004
+ existing.items.push(item);
22005
+ } else {
22006
+ grouped.set(item.target.key, { target: item.target, items: [item] });
21892
22007
  }
21893
- } catch {
21894
- for (const { entryId, doc: doc2 } of batch) {
21895
- try {
21896
- await client.ingest(doc2);
22008
+ }
22009
+ for (const { target, items } of grouped.values()) {
22010
+ const client = new VectorClient(config2, {
22011
+ apiKey: target.apiKey,
22012
+ namespace: target.namespace,
22013
+ siteId: target.siteId
22014
+ });
22015
+ try {
22016
+ await client.batchIngest(items.map((b) => b.doc));
22017
+ for (const { entryId, doc: doc2 } of items) {
21897
22018
  if (doc2.source_type === "chat") {
21898
22019
  const localId = typeof doc2.metadata?.local_id === "number" ? doc2.metadata.local_id : null;
21899
22020
  if (localId !== null) {
@@ -21902,14 +22023,47 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
21902
22023
  }
21903
22024
  markSynced(db, entryId);
21904
22025
  pushed++;
21905
- } catch (err) {
21906
- markFailed(db, entryId, err instanceof Error ? err.message : String(err));
21907
- failed++;
22026
+ }
22027
+ } catch {
22028
+ for (const { entryId, doc: doc2 } of items) {
22029
+ try {
22030
+ await client.ingest(doc2);
22031
+ if (doc2.source_type === "chat") {
22032
+ const localId = typeof doc2.metadata?.local_id === "number" ? doc2.metadata.local_id : null;
22033
+ if (localId !== null) {
22034
+ db.db.query("UPDATE chat_messages SET remote_source_id = ? WHERE id = ?").run(doc2.source_id, localId);
22035
+ }
22036
+ }
22037
+ markSynced(db, entryId);
22038
+ pushed++;
22039
+ } catch (err) {
22040
+ markFailed(db, entryId, err instanceof Error ? err.message : String(err));
22041
+ failed++;
22042
+ }
21908
22043
  }
21909
22044
  }
21910
22045
  }
21911
22046
  return { pushed, failed, skipped };
21912
22047
  }
22048
+ function maybeScrubFleetDocument(doc2, target) {
22049
+ if (!target.isFleet)
22050
+ return doc2;
22051
+ return {
22052
+ ...doc2,
22053
+ content: scrubFleetIdentifiers(doc2.content),
22054
+ metadata: scrubFleetMetadata(doc2.metadata)
22055
+ };
22056
+ }
22057
+ function scrubFleetMetadata(value) {
22058
+ if (typeof value === "string")
22059
+ return scrubFleetIdentifiers(value);
22060
+ if (Array.isArray(value))
22061
+ return value.map((item) => scrubFleetMetadata(item));
22062
+ if (value && typeof value === "object") {
22063
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, scrubFleetMetadata(item)]));
22064
+ }
22065
+ return value;
22066
+ }
21913
22067
  function countPresentSections2(summary) {
21914
22068
  return [
21915
22069
  summary.request,
@@ -21922,12 +22076,29 @@ function countPresentSections2(summary) {
21922
22076
  function extractSectionItems2(section) {
21923
22077
  return extractSummaryItems(section, 4);
21924
22078
  }
22079
+ function summarizeObservationSourceTools(observations) {
22080
+ const counts = new Map;
22081
+ for (const obs of observations) {
22082
+ const tool = obs.source_tool;
22083
+ if (!tool)
22084
+ continue;
22085
+ counts.set(tool, (counts.get(tool) ?? 0) + 1);
22086
+ }
22087
+ return Array.from(counts.entries()).map(([tool, count]) => ({ tool, count })).sort((a, b) => {
22088
+ if (b.count !== a.count)
22089
+ return b.count - a.count;
22090
+ return a.tool.localeCompare(b.tool);
22091
+ });
22092
+ }
21925
22093
 
21926
22094
  // src/sync/pull.ts
21927
- var PULL_CURSOR_KEY = "pull_cursor";
22095
+ function pullCursorKey(namespace) {
22096
+ return namespace === "default" ? "pull_cursor" : `pull_cursor:${namespace}`;
22097
+ }
21928
22098
  var MAX_PAGES = 20;
21929
22099
  async function pullFromVector(db, client, config2, limit = 50) {
21930
- let cursor = db.getSyncState(PULL_CURSOR_KEY) ?? undefined;
22100
+ const cursorKey = pullCursorKey(client.namespace || "default");
22101
+ let cursor = db.getSyncState(cursorKey) ?? undefined;
21931
22102
  let totalReceived = 0;
21932
22103
  let totalMerged = 0;
21933
22104
  let totalSkipped = 0;
@@ -21938,7 +22109,7 @@ async function pullFromVector(db, client, config2, limit = 50) {
21938
22109
  totalMerged += merged;
21939
22110
  totalSkipped += skipped;
21940
22111
  if (response.cursor) {
21941
- db.setSyncState(PULL_CURSOR_KEY, response.cursor);
22112
+ db.setSyncState(cursorKey, response.cursor);
21942
22113
  cursor = response.cursor;
21943
22114
  }
21944
22115
  if (!response.has_more || response.changes.length === 0)
@@ -22156,6 +22327,7 @@ class SyncEngine {
22156
22327
  db;
22157
22328
  config;
22158
22329
  client = null;
22330
+ fleetClient = null;
22159
22331
  pushTimer = null;
22160
22332
  pullTimer = null;
22161
22333
  _pushing = false;
@@ -22167,6 +22339,13 @@ class SyncEngine {
22167
22339
  if (VectorClient.isConfigured(config2)) {
22168
22340
  try {
22169
22341
  this.client = new VectorClient(config2);
22342
+ if (hasFleetTarget(config2)) {
22343
+ this.fleetClient = new VectorClient(config2, {
22344
+ apiKey: config2.fleet.api_key,
22345
+ namespace: config2.fleet.namespace,
22346
+ siteId: config2.site_id
22347
+ });
22348
+ }
22170
22349
  } catch {}
22171
22350
  }
22172
22351
  }
@@ -22201,7 +22380,7 @@ class SyncEngine {
22201
22380
  return;
22202
22381
  this._pushing = true;
22203
22382
  try {
22204
- await pushOutbox(this.db, this.client, this.config, this.config.sync.batch_size);
22383
+ await pushOutbox(this.db, this.config, this.config.sync.batch_size);
22205
22384
  } finally {
22206
22385
  this._pushing = false;
22207
22386
  }
@@ -22212,6 +22391,9 @@ class SyncEngine {
22212
22391
  this._pulling = true;
22213
22392
  try {
22214
22393
  await pullFromVector(this.db, this.client, this.config);
22394
+ if (this.fleetClient) {
22395
+ await pullFromVector(this.db, this.fleetClient, this.config);
22396
+ }
22215
22397
  await pullSettings(this.client, this.config);
22216
22398
  } finally {
22217
22399
  this._pulling = false;
@@ -22880,7 +23062,7 @@ process.on("SIGTERM", () => {
22880
23062
  });
22881
23063
  var server = new McpServer({
22882
23064
  name: "engrm",
22883
- version: "0.4.38"
23065
+ version: "0.4.39"
22884
23066
  });
22885
23067
  server.tool("save_observation", "Directly save a durable memory item now. Use this when something should be remembered on purpose instead of waiting for an end-of-session digest.", {
22886
23068
  type: exports_external.enum([
@@ -23941,6 +24123,11 @@ server.tool("capture_status", "Show whether Engrm hook registration and recent p
23941
24123
  {
23942
24124
  type: "text",
23943
24125
  text: `Schema: v${result.schema_version} (${result.schema_current ? "current" : "outdated"})
24126
+ ` + `HTTP MCP: ${result.http_enabled ? `enabled${result.http_port ? ` (:${result.http_port})` : ""}` : "disabled"}
24127
+ ` + `HTTP bearer tokens: ${result.http_bearer_token_count}
24128
+ ` + `Fleet project: ${result.fleet_project_name ?? "none"}
24129
+ ` + `Fleet sync: ${result.fleet_configured ? "configured" : "not configured"}
24130
+
23944
24131
  ` + `Claude MCP: ${result.claude_mcp_registered ? "registered" : "missing"}
23945
24132
  ` + `Claude hooks: ${result.claude_hooks_registered ? `registered (${result.claude_hook_count})` : "missing"}
23946
24133
  ` + `Claude raw chronology hooks: session-start=${result.claude_session_start_hook ? "yes" : "no"}, prompt=${result.claude_user_prompt_hook ? "yes" : "no"}, post-tool=${result.claude_post_tool_hook ? "yes" : "no"}, stop=${result.claude_stop_hook ? "yes" : "no"}
@@ -23948,6 +24135,9 @@ server.tool("capture_status", "Show whether Engrm hook registration and recent p
23948
24135
  ` + `Codex hooks: ${result.codex_hooks_registered ? "registered" : "missing"}
23949
24136
  ` + `Codex raw chronology: ${result.codex_raw_chronology_supported ? "supported" : "not yet supported (start/stop only)"}
23950
24137
 
24138
+ ` + `OpenCode MCP: ${result.opencode_mcp_registered ? "registered" : "missing"}
24139
+ ` + `OpenCode plugin: ${result.opencode_plugin_registered ? "installed" : "missing"}
24140
+
23951
24141
  ` + `Recent user prompts: ${result.recent_user_prompts}
23952
24142
  ` + `Recent tool events: ${result.recent_tool_events}
23953
24143
  ` + `Recent sessions with raw chronology: ${result.recent_sessions_with_raw_capture}
@@ -24865,9 +25055,82 @@ async function main() {
24865
25055
  }
24866
25056
  syncEngine = new SyncEngine(db, config2);
24867
25057
  syncEngine.start();
25058
+ if (shouldStartHttpMode()) {
25059
+ await startHttpServer();
25060
+ return;
25061
+ }
24868
25062
  const transport = new StdioServerTransport;
24869
25063
  await server.connect(transport);
24870
25064
  }
25065
+ function shouldStartHttpMode() {
25066
+ return process.argv.includes("--http") || Boolean(process.env.ENGRM_HTTP_PORT) || config2.http.enabled;
25067
+ }
25068
+ function resolveHttpPort() {
25069
+ const raw = process.env.ENGRM_HTTP_PORT;
25070
+ if (raw) {
25071
+ const parsed = Number(raw);
25072
+ if (!Number.isNaN(parsed) && parsed > 0)
25073
+ return parsed;
25074
+ }
25075
+ return config2.http.port > 0 ? config2.http.port : 3767;
25076
+ }
25077
+ function getHttpBearerTokens() {
25078
+ const env = process.env.ENGRM_HTTP_BEARER_TOKENS;
25079
+ if (env && env.trim()) {
25080
+ return env.split(",").map((value) => value.trim()).filter(Boolean);
25081
+ }
25082
+ return config2.http.bearer_tokens.filter(Boolean);
25083
+ }
25084
+ async function startHttpServer() {
25085
+ const port = resolveHttpPort();
25086
+ const tokens = getHttpBearerTokens();
25087
+ if (tokens.length === 0) {
25088
+ throw new Error("HTTP mode requires at least one bearer token via settings.json http.bearer_tokens or ENGRM_HTTP_BEARER_TOKENS");
25089
+ }
25090
+ const transport = new StreamableHTTPServerTransport({
25091
+ sessionIdGenerator: undefined
25092
+ });
25093
+ await server.connect(transport);
25094
+ const httpServer = createServer(async (req, res) => {
25095
+ try {
25096
+ if (!req.url || !req.url.startsWith("/mcp")) {
25097
+ res.writeHead(404).end("Not found");
25098
+ return;
25099
+ }
25100
+ const authHeader = req.headers.authorization ?? "";
25101
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice("Bearer ".length).trim() : "";
25102
+ if (!token || !tokens.includes(token)) {
25103
+ res.writeHead(401, { "Content-Type": "application/json" });
25104
+ res.end(JSON.stringify({ error: "Unauthorized" }));
25105
+ return;
25106
+ }
25107
+ const authorizedReq = req;
25108
+ authorizedReq.auth = { token };
25109
+ const parsedBody = req.method === "POST" ? await readJsonBody(req) : undefined;
25110
+ await transport.handleRequest(authorizedReq, res, parsedBody);
25111
+ } catch (error48) {
25112
+ res.writeHead(500, { "Content-Type": "application/json" });
25113
+ res.end(JSON.stringify({ error: error48 instanceof Error ? error48.message : String(error48) }));
25114
+ }
25115
+ });
25116
+ await new Promise((resolve4, reject) => {
25117
+ httpServer.once("error", reject);
25118
+ httpServer.listen(port, () => resolve4());
25119
+ });
25120
+ console.error(`Engrm HTTP MCP listening on :${port}/mcp`);
25121
+ }
25122
+ async function readJsonBody(req) {
25123
+ const chunks = [];
25124
+ for await (const chunk of req) {
25125
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
25126
+ }
25127
+ if (chunks.length === 0)
25128
+ return;
25129
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
25130
+ if (!raw)
25131
+ return;
25132
+ return JSON.parse(raw);
25133
+ }
24871
25134
  main().catch((error48) => {
24872
25135
  console.error("Fatal:", error48);
24873
25136
  db.close();