engrm 0.4.37 → 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";
@@ -16329,7 +16389,14 @@ async function searchObservations(db, input) {
16329
16389
  }
16330
16390
  }
16331
16391
  const safeQuery = sanitizeFtsQuery(query);
16332
- const ftsResults = safeQuery ? db.searchFts(safeQuery, projectId, undefined, limit * 2, input.user_id) : [];
16392
+ let ftsResults = [];
16393
+ if (safeQuery) {
16394
+ try {
16395
+ ftsResults = db.searchFts(safeQuery, projectId, undefined, limit * 2, input.user_id);
16396
+ } catch {
16397
+ ftsResults = [];
16398
+ }
16399
+ }
16333
16400
  let vecResults = [];
16334
16401
  const queryEmbedding = await embedText(query);
16335
16402
  if (queryEmbedding && db.vecAvailable) {
@@ -18482,20 +18549,21 @@ function listRecallItems(db, input) {
18482
18549
  source_agent: null
18483
18550
  }))
18484
18551
  ];
18485
- 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);
18486
18553
  return {
18487
18554
  project: projectName,
18488
18555
  continuity_mode: deduped.some((item) => item.kind === "handoff" || item.kind === "thread") ? "direct" : "indexed",
18489
18556
  items: deduped
18490
18557
  };
18491
18558
  }
18492
- function compareRecallItems(a, b, currentDeviceId) {
18559
+ function compareRecallItems(a, b, currentDeviceId, preferredAgent) {
18493
18560
  const priority = (item) => {
18494
18561
  const freshness = item.freshness === "live" ? 0 : item.freshness === "recent" ? 1 : 2;
18495
18562
  const kind = item.kind === "handoff" ? 0 : item.kind === "thread" ? 1 : item.kind === "chat" ? 2 : 3;
18496
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;
18497
18565
  const draftPenalty = item.kind === "handoff" && /draft/i.test(item.title) ? 0.25 : 0;
18498
- return freshness * 10 + kind + remoteBoost + draftPenalty;
18566
+ return freshness * 10 + kind + remoteBoost + preferredAgentBoost + draftPenalty;
18499
18567
  };
18500
18568
  const priorityDiff = priority(a) - priority(b);
18501
18569
  if (priorityDiff !== 0)
@@ -18657,18 +18725,20 @@ function getProjectMemoryIndex(db, input) {
18657
18725
  user_id: input.user_id,
18658
18726
  limit: 20
18659
18727
  });
18728
+ const activeAgents = collectActiveAgents(recentSessions);
18729
+ const preferredAgent = pickPreferredAgent(activeAgents, recentSessions[0]?.agent ?? null);
18660
18730
  const recentChatCount = recentChat.messages.length;
18661
18731
  const recallIndex = listRecallItems(db, {
18662
18732
  cwd,
18663
18733
  project_scoped: true,
18664
18734
  user_id: input.user_id,
18735
+ preferred_agent: preferredAgent,
18665
18736
  limit: 10
18666
18737
  });
18667
18738
  const latestSession = recentSessions[0] ?? null;
18668
18739
  const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
18669
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);
18670
18741
  const captureSummary = summarizeCaptureState(recentSessions);
18671
- const activeAgents = collectActiveAgents(recentSessions);
18672
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);
18673
18743
  const suggestedTools = buildSuggestedTools(recentSessions, recentRequestsCount, recentToolsCount, observations.length, recentChatCount, recentChat.coverage_state, activeAgents);
18674
18744
  const estimatedReadTokens = estimateTokens([
@@ -18682,7 +18752,7 @@ function getProjectMemoryIndex(db, input) {
18682
18752
  `));
18683
18753
  const continuityState = classifyContinuityState(recentRequestsCount, recentToolsCount, recentHandoffsCount.length, recentChatCount, recentSessions, recentOutcomes.length);
18684
18754
  const sourceTimestamp = pickResumeSourceTimestamp(latestSession, recentChat.messages);
18685
- const bestRecallItem = pickBestRecallItem(recallIndex.items);
18755
+ const bestRecallItem = pickBestRecallItem(recallIndex.items, preferredAgent);
18686
18756
  return {
18687
18757
  project: project.name,
18688
18758
  canonical_id: project.canonical_id,
@@ -18702,7 +18772,7 @@ function getProjectMemoryIndex(db, input) {
18702
18772
  best_recall_key: bestRecallItem?.key ?? null,
18703
18773
  best_recall_title: bestRecallItem?.title ?? null,
18704
18774
  best_recall_kind: bestRecallItem?.kind ?? null,
18705
- best_agent_resume_agent: activeAgents.length > 1 ? latestSession?.agent ?? null : null,
18775
+ best_agent_resume_agent: activeAgents.length > 1 ? preferredAgent : null,
18706
18776
  resume_freshness: classifyResumeFreshness(sourceTimestamp),
18707
18777
  resume_source_session_id: latestSession?.session_id ?? null,
18708
18778
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -18733,7 +18803,12 @@ function getProjectMemoryIndex(db, input) {
18733
18803
  suggested_tools: suggestedTools
18734
18804
  };
18735
18805
  }
18736
- 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
+ }
18737
18812
  return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
18738
18813
  }
18739
18814
  function pickResumeSourceTimestamp(latestSession, messages) {
@@ -18821,6 +18896,11 @@ function summarizeCaptureState(sessions) {
18821
18896
  function collectActiveAgents(sessions) {
18822
18897
  return Array.from(new Set(sessions.map((session) => session.agent?.trim()).filter((agent) => Boolean(agent) && !agent.startsWith("engrm-")))).sort();
18823
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
+ }
18824
18904
  function buildSuggestedTools(sessions, requestCount, toolCount, observationCount, recentChatCount, chatCoverageState, activeAgents) {
18825
18905
  const suggested = [];
18826
18906
  if (sessions.length > 0) {
@@ -18905,6 +18985,7 @@ function getMemoryConsole(db, input) {
18905
18985
  cwd,
18906
18986
  project_scoped: projectScoped,
18907
18987
  user_id: input.user_id,
18988
+ preferred_agent: pickPreferredAgent(projectIndex?.active_agents ?? collectActiveAgents(sessions), sessions[0]?.agent ?? null) ?? undefined,
18908
18989
  limit: 10
18909
18990
  });
18910
18991
  const projectIndex = projectScoped ? getProjectMemoryIndex(db, {
@@ -18929,10 +19010,10 @@ function getMemoryConsole(db, input) {
18929
19010
  title: item.title,
18930
19011
  source_agent: item.source_agent
18931
19012
  })),
18932
- best_recall_key: projectIndex?.best_recall_key ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.key ?? null,
18933
- best_recall_title: projectIndex?.best_recall_title ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.title ?? null,
18934
- best_recall_kind: projectIndex?.best_recall_kind ?? (recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null)?.kind ?? null,
18935
- 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),
18936
19017
  resume_freshness: projectIndex?.resume_freshness ?? "stale",
18937
19018
  resume_source_session_id: projectIndex?.resume_source_session_id ?? sessions[0]?.session_id ?? null,
18938
19019
  resume_source_device_id: projectIndex?.resume_source_device_id ?? sessions[0]?.device_id ?? null,
@@ -18962,6 +19043,15 @@ function getMemoryConsole(db, input) {
18962
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)
18963
19044
  };
18964
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
+ }
18965
19055
  function collectProvenanceTypeMix(observations) {
18966
19056
  const grouped = new Map;
18967
19057
  for (const observation of observations) {
@@ -19387,14 +19477,20 @@ function getCaptureStatus(db, input = {}) {
19387
19477
  const claudeSettings = join3(home, ".claude", "settings.json");
19388
19478
  const codexConfig = join3(home, ".codex", "config.toml");
19389
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;
19390
19483
  const claudeJsonContent = existsSync3(claudeJson) ? readFileSync3(claudeJson, "utf-8") : "";
19391
19484
  const claudeSettingsContent = existsSync3(claudeSettings) ? readFileSync3(claudeSettings, "utf-8") : "";
19392
19485
  const codexConfigContent = existsSync3(codexConfig) ? readFileSync3(codexConfig, "utf-8") : "";
19393
19486
  const codexHooksContent = existsSync3(codexHooks) ? readFileSync3(codexHooks, "utf-8") : "";
19487
+ const opencodeConfigContent = existsSync3(opencodeConfig) ? readFileSync3(opencodeConfig, "utf-8") : "";
19394
19488
  const claudeMcpRegistered = claudeJsonContent.includes('"engrm"');
19395
19489
  const claudeHooksRegistered = claudeSettingsContent.includes("engrm") || claudeSettingsContent.includes("session-start") || claudeSettingsContent.includes("user-prompt-submit");
19396
19490
  const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME}]`);
19397
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);
19398
19494
  let claudeHookCount = 0;
19399
19495
  let claudeSessionStartHook = false;
19400
19496
  let claudeUserPromptHook = false;
@@ -19467,6 +19563,11 @@ function getCaptureStatus(db, input = {}) {
19467
19563
  return {
19468
19564
  schema_version: schemaVersion,
19469
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),
19470
19571
  claude_mcp_registered: claudeMcpRegistered,
19471
19572
  claude_hooks_registered: claudeHooksRegistered,
19472
19573
  claude_hook_count: claudeHookCount,
@@ -19479,6 +19580,8 @@ function getCaptureStatus(db, input = {}) {
19479
19580
  codex_session_start_hook: codexSessionStartHook,
19480
19581
  codex_stop_hook: codexStopHook,
19481
19582
  codex_raw_chronology_supported: false,
19583
+ opencode_mcp_registered: opencodeMcpRegistered,
19584
+ opencode_plugin_registered: opencodePluginRegistered,
19482
19585
  recent_user_prompts: recentUserPrompts,
19483
19586
  recent_tool_events: recentToolEvents,
19484
19587
  recent_sessions_with_raw_capture: recentSessionsWithRawCapture,
@@ -20059,6 +20162,7 @@ function getSessionContext(db, input) {
20059
20162
  project_scoped: true,
20060
20163
  user_id: input.user_id,
20061
20164
  current_device_id: input.current_device_id,
20165
+ preferred_agent: pickPreferredAgent(collectActiveAgents(context.recentSessions ?? []), context.recentSessions?.[0]?.agent ?? null) ?? undefined,
20062
20166
  limit: 10
20063
20167
  });
20064
20168
  const latestSession = context.recentSessions?.[0] ?? null;
@@ -20068,8 +20172,9 @@ function getSessionContext(db, input) {
20068
20172
  const continuityState = classifyContinuityState(recentRequests, recentTools, recentHandoffs, recentChatMessages, context.recentSessions ?? [], (context.recentOutcomes ?? []).length);
20069
20173
  const latestChatEpoch = recentChat.messages.length > 0 ? recentChat.messages[recentChat.messages.length - 1]?.created_at_epoch ?? null : null;
20070
20174
  const resumeTimestamp = latestChatEpoch ?? latestSession?.completed_at_epoch ?? latestSession?.started_at_epoch ?? null;
20071
- const bestRecallItem = recallIndex.items.find((item) => item.kind !== "memory") ?? recallIndex.items[0] ?? null;
20072
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;
20073
20178
  return {
20074
20179
  project_name: context.project_name,
20075
20180
  canonical_id: context.canonical_id,
@@ -20089,7 +20194,7 @@ function getSessionContext(db, input) {
20089
20194
  best_recall_key: bestRecallItem?.key ?? null,
20090
20195
  best_recall_title: bestRecallItem?.title ?? null,
20091
20196
  best_recall_kind: bestRecallItem?.kind ?? null,
20092
- best_agent_resume_agent: activeAgents.length > 1 ? latestSession?.agent ?? null : null,
20197
+ best_agent_resume_agent: activeAgents.length > 1 ? preferredAgent : null,
20093
20198
  resume_freshness: classifyResumeFreshness(resumeTimestamp),
20094
20199
  resume_source_session_id: latestSession?.session_id ?? null,
20095
20200
  resume_source_device_id: latestSession?.device_id ?? null,
@@ -20753,8 +20858,10 @@ async function resumeThread(db, config2, input = {}) {
20753
20858
  const detected = detectProject(cwd);
20754
20859
  const project = db.getProjectByCanonicalId(detected.canonical_id);
20755
20860
  let snapshot = await buildResumeSnapshot(db, cwd, input.user_id, input.current_device_id, limit);
20756
- if (input.agent) {
20757
- 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);
20758
20865
  }
20759
20866
  let repairResult = null;
20760
20867
  const shouldRepair = repairIfNeeded && snapshot.recentChat.coverage_state !== "transcript-backed" && (snapshot.recentChat.messages.length > 0 || snapshot.recentSessions.length > 0 || snapshot.context?.continuity_state !== "cold");
@@ -20766,13 +20873,13 @@ async function resumeThread(db, config2, input = {}) {
20766
20873
  });
20767
20874
  if (repairResult.imported_chat_messages > 0) {
20768
20875
  snapshot = await buildResumeSnapshot(db, cwd, input.user_id, input.current_device_id, limit);
20769
- if (input.agent) {
20770
- snapshot = filterResumeSnapshotByAgent(snapshot, input.agent, input.current_device_id);
20876
+ if (effectiveAgent) {
20877
+ snapshot = filterResumeSnapshotByAgent(snapshot, effectiveAgent, input.current_device_id);
20771
20878
  }
20772
20879
  }
20773
20880
  }
20774
20881
  const { context, handoff, recentChat, recentSessions, recall } = snapshot;
20775
- const bestRecallItem = pickBestRecallItem2(snapshot.recallIndex.items);
20882
+ const bestRecallItem = pickBestRecallItem3(snapshot.recallIndex.items, effectiveAgent);
20776
20883
  const latestSession = recentSessions[0] ?? null;
20777
20884
  const latestSummary = latestSession ? db.getSessionSummary(latestSession.session_id) : null;
20778
20885
  const inferredRequest = latestSession?.request?.trim() || null;
@@ -20809,7 +20916,7 @@ async function resumeThread(db, config2, input = {}) {
20809
20916
  ])).slice(0, 4);
20810
20917
  return {
20811
20918
  project_name: project?.name ?? context?.project_name ?? null,
20812
- target_agent: input.agent ?? null,
20919
+ target_agent: effectiveAgent ?? null,
20813
20920
  continuity_state: context?.continuity_state ?? "cold",
20814
20921
  continuity_summary: context?.continuity_summary ?? "No fresh repo-local continuity yet; older memory should be treated cautiously.",
20815
20922
  resume_freshness: classifyResumeFreshness2(sourceTimestamp),
@@ -20929,7 +21036,12 @@ function filterResumeSnapshotByAgent(snapshot, agent, currentDeviceId) {
20929
21036
  recallIndex
20930
21037
  };
20931
21038
  }
20932
- 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
+ }
20933
21045
  return items.find((item) => item.kind !== "memory") ?? items[0] ?? null;
20934
21046
  }
20935
21047
  function compareRecallCandidates(epochA, epochB, deviceA, deviceB, currentDeviceId) {
@@ -21466,16 +21578,16 @@ class VectorClient {
21466
21578
  apiKey;
21467
21579
  siteId;
21468
21580
  namespace;
21469
- constructor(config2) {
21581
+ constructor(config2, overrides = {}) {
21470
21582
  const baseUrl = getBaseUrl(config2);
21471
- const apiKey = getApiKey(config2);
21583
+ const apiKey = overrides.apiKey ?? getApiKey(config2);
21472
21584
  if (!baseUrl || !apiKey) {
21473
21585
  throw new Error("VectorClient requires candengo_url and candengo_api_key");
21474
21586
  }
21475
21587
  this.baseUrl = baseUrl.replace(/\/$/, "");
21476
21588
  this.apiKey = apiKey;
21477
- this.siteId = config2.site_id;
21478
- this.namespace = config2.namespace;
21589
+ this.siteId = overrides.siteId ?? config2.site_id;
21590
+ this.namespace = overrides.namespace ?? config2.namespace;
21479
21591
  }
21480
21592
  static isConfigured(config2) {
21481
21593
  return getApiKey(config2) !== null && getBaseUrl(config2) !== null;
@@ -21641,10 +21753,10 @@ function parseJsonArray5(value) {
21641
21753
  }
21642
21754
 
21643
21755
  // src/sync/push.ts
21644
- function buildChatVectorDocument(chat, config2, project) {
21756
+ function buildChatVectorDocument(chat, config2, project, target = resolveSyncTarget(config2, project.name)) {
21645
21757
  return {
21646
- site_id: config2.site_id,
21647
- namespace: config2.namespace,
21758
+ site_id: target.siteId,
21759
+ namespace: target.namespace,
21648
21760
  source_type: "chat",
21649
21761
  source_id: buildSourceId(config2, chat.id, "chat"),
21650
21762
  content: chat.content,
@@ -21665,7 +21777,7 @@ function buildChatVectorDocument(chat, config2, project) {
21665
21777
  }
21666
21778
  };
21667
21779
  }
21668
- function buildVectorDocument(obs, config2, project) {
21780
+ function buildVectorDocument(obs, config2, project, target = resolveSyncTarget(config2, project.name)) {
21669
21781
  const parts = [obs.title];
21670
21782
  if (obs.narrative)
21671
21783
  parts.push(obs.narrative);
@@ -21682,8 +21794,8 @@ function buildVectorDocument(obs, config2, project) {
21682
21794
  }
21683
21795
  }
21684
21796
  return {
21685
- site_id: config2.site_id,
21686
- namespace: config2.namespace,
21797
+ site_id: target.siteId,
21798
+ namespace: target.namespace,
21687
21799
  source_type: obs.type,
21688
21800
  source_id: buildSourceId(config2, obs.id),
21689
21801
  content: parts.join(`
@@ -21714,7 +21826,10 @@ function buildVectorDocument(obs, config2, project) {
21714
21826
  }
21715
21827
  };
21716
21828
  }
21717
- 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;
21718
21833
  const parts = [];
21719
21834
  if (summary.request)
21720
21835
  parts.push(`Request: ${summary.request}`);
@@ -21727,9 +21842,17 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
21727
21842
  if (summary.next_steps)
21728
21843
  parts.push(`Next Steps: ${summary.next_steps}`);
21729
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);
21730
21853
  return {
21731
- site_id: config2.site_id,
21732
- namespace: config2.namespace,
21854
+ site_id: target.siteId,
21855
+ namespace: target.namespace,
21733
21856
  source_type: "summary",
21734
21857
  source_id: buildSourceId(config2, summary.id, "summary"),
21735
21858
  content: parts.join(`
@@ -21751,18 +21874,18 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
21751
21874
  learned_items: extractSectionItems2(summary.learned),
21752
21875
  completed_items: extractSectionItems2(summary.completed),
21753
21876
  next_step_items: extractSectionItems2(summary.next_steps),
21754
- prompt_count: captureContext?.prompt_count ?? 0,
21755
- tool_event_count: captureContext?.tool_event_count ?? 0,
21756
- capture_state: captureContext?.capture_state ?? "summary-only",
21757
- recent_request_prompts: captureContext?.recent_request_prompts ?? [],
21758
- latest_request: captureContext?.latest_request ?? null,
21759
- current_thread: captureContext?.current_thread ?? null,
21760
- recent_tool_names: captureContext?.recent_tool_names ?? [],
21761
- recent_tool_commands: captureContext?.recent_tool_commands ?? [],
21762
- hot_files: captureContext?.hot_files ?? [],
21763
- recent_outcomes: captureContext?.recent_outcomes ?? [],
21764
- observation_source_tools: captureContext?.observation_source_tools ?? [],
21765
- 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,
21766
21889
  decisions_count: valueSignals.decisions_count,
21767
21890
  lessons_count: valueSignals.lessons_count,
21768
21891
  discoveries_count: valueSignals.discoveries_count,
@@ -21776,7 +21899,7 @@ function buildSummaryVectorDocument(summary, config2, project, observations = []
21776
21899
  }
21777
21900
  };
21778
21901
  }
21779
- async function pushOutbox(db, client, config2, batchSize = 50) {
21902
+ async function pushOutbox(db, config2, batchSize = 50) {
21780
21903
  const entries = getPendingEntries(db, batchSize);
21781
21904
  let pushed = 0;
21782
21905
  let failed = 0;
@@ -21803,11 +21926,12 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
21803
21926
  const summaryObservations = db.getObservationsBySession(summary.session_id);
21804
21927
  const sessionPrompts = db.getSessionUserPrompts(summary.session_id, 20);
21805
21928
  const sessionToolEvents = db.getSessionToolEvents(summary.session_id, 20);
21929
+ const target2 = resolveSyncTarget(config2, project2.name);
21806
21930
  const doc3 = buildSummaryVectorDocument(summary, config2, {
21807
21931
  canonical_id: project2.canonical_id,
21808
21932
  name: project2.name
21809
- }, summaryObservations, buildSessionHandoffMetadata(sessionPrompts, sessionToolEvents, summaryObservations));
21810
- 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 });
21811
21935
  continue;
21812
21936
  }
21813
21937
  if (entry.record_type === "chat_message") {
@@ -21829,11 +21953,12 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
21829
21953
  continue;
21830
21954
  }
21831
21955
  markSyncing(db, entry.id);
21956
+ const target2 = resolveSyncTarget(config2, project2.name);
21832
21957
  const doc3 = buildChatVectorDocument(chat, config2, {
21833
21958
  canonical_id: project2.canonical_id,
21834
21959
  name: project2.name
21835
- });
21836
- batch.push({ entryId: entry.id, doc: doc3 });
21960
+ }, target2);
21961
+ batch.push({ entryId: entry.id, doc: maybeScrubFleetDocument(doc3, target2), target: target2 });
21837
21962
  continue;
21838
21963
  }
21839
21964
  if (entry.record_type !== "observation") {
@@ -21863,30 +21988,33 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
21863
21988
  continue;
21864
21989
  }
21865
21990
  markSyncing(db, entry.id);
21991
+ const target = resolveSyncTarget(config2, project.name);
21866
21992
  const doc2 = buildVectorDocument(obs, config2, {
21867
21993
  canonical_id: project.canonical_id,
21868
21994
  name: project.name
21869
- });
21870
- batch.push({ entryId: entry.id, doc: doc2 });
21995
+ }, target);
21996
+ batch.push({ entryId: entry.id, doc: maybeScrubFleetDocument(doc2, target), target });
21871
21997
  }
21872
21998
  if (batch.length === 0)
21873
21999
  return { pushed, failed, skipped };
21874
- try {
21875
- await client.batchIngest(batch.map((b) => b.doc));
21876
- for (const { entryId, doc: doc2 } of batch) {
21877
- if (doc2.source_type === "chat") {
21878
- const localId = typeof doc2.metadata?.local_id === "number" ? doc2.metadata.local_id : null;
21879
- if (localId !== null) {
21880
- db.db.query("UPDATE chat_messages SET remote_source_id = ? WHERE id = ?").run(doc2.source_id, localId);
21881
- }
21882
- }
21883
- markSynced(db, entryId);
21884
- 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] });
21885
22007
  }
21886
- } catch {
21887
- for (const { entryId, doc: doc2 } of batch) {
21888
- try {
21889
- 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) {
21890
22018
  if (doc2.source_type === "chat") {
21891
22019
  const localId = typeof doc2.metadata?.local_id === "number" ? doc2.metadata.local_id : null;
21892
22020
  if (localId !== null) {
@@ -21895,14 +22023,47 @@ async function pushOutbox(db, client, config2, batchSize = 50) {
21895
22023
  }
21896
22024
  markSynced(db, entryId);
21897
22025
  pushed++;
21898
- } catch (err) {
21899
- markFailed(db, entryId, err instanceof Error ? err.message : String(err));
21900
- 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
+ }
21901
22043
  }
21902
22044
  }
21903
22045
  }
21904
22046
  return { pushed, failed, skipped };
21905
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
+ }
21906
22067
  function countPresentSections2(summary) {
21907
22068
  return [
21908
22069
  summary.request,
@@ -21915,12 +22076,29 @@ function countPresentSections2(summary) {
21915
22076
  function extractSectionItems2(section) {
21916
22077
  return extractSummaryItems(section, 4);
21917
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
+ }
21918
22093
 
21919
22094
  // src/sync/pull.ts
21920
- var PULL_CURSOR_KEY = "pull_cursor";
22095
+ function pullCursorKey(namespace) {
22096
+ return namespace === "default" ? "pull_cursor" : `pull_cursor:${namespace}`;
22097
+ }
21921
22098
  var MAX_PAGES = 20;
21922
22099
  async function pullFromVector(db, client, config2, limit = 50) {
21923
- let cursor = db.getSyncState(PULL_CURSOR_KEY) ?? undefined;
22100
+ const cursorKey = pullCursorKey(client.namespace || "default");
22101
+ let cursor = db.getSyncState(cursorKey) ?? undefined;
21924
22102
  let totalReceived = 0;
21925
22103
  let totalMerged = 0;
21926
22104
  let totalSkipped = 0;
@@ -21931,7 +22109,7 @@ async function pullFromVector(db, client, config2, limit = 50) {
21931
22109
  totalMerged += merged;
21932
22110
  totalSkipped += skipped;
21933
22111
  if (response.cursor) {
21934
- db.setSyncState(PULL_CURSOR_KEY, response.cursor);
22112
+ db.setSyncState(cursorKey, response.cursor);
21935
22113
  cursor = response.cursor;
21936
22114
  }
21937
22115
  if (!response.has_more || response.changes.length === 0)
@@ -22149,6 +22327,7 @@ class SyncEngine {
22149
22327
  db;
22150
22328
  config;
22151
22329
  client = null;
22330
+ fleetClient = null;
22152
22331
  pushTimer = null;
22153
22332
  pullTimer = null;
22154
22333
  _pushing = false;
@@ -22160,6 +22339,13 @@ class SyncEngine {
22160
22339
  if (VectorClient.isConfigured(config2)) {
22161
22340
  try {
22162
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
+ }
22163
22349
  } catch {}
22164
22350
  }
22165
22351
  }
@@ -22194,7 +22380,7 @@ class SyncEngine {
22194
22380
  return;
22195
22381
  this._pushing = true;
22196
22382
  try {
22197
- 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);
22198
22384
  } finally {
22199
22385
  this._pushing = false;
22200
22386
  }
@@ -22205,6 +22391,9 @@ class SyncEngine {
22205
22391
  this._pulling = true;
22206
22392
  try {
22207
22393
  await pullFromVector(this.db, this.client, this.config);
22394
+ if (this.fleetClient) {
22395
+ await pullFromVector(this.db, this.fleetClient, this.config);
22396
+ }
22208
22397
  await pullSettings(this.client, this.config);
22209
22398
  } finally {
22210
22399
  this._pulling = false;
@@ -22873,7 +23062,7 @@ process.on("SIGTERM", () => {
22873
23062
  });
22874
23063
  var server = new McpServer({
22875
23064
  name: "engrm",
22876
- version: "0.4.37"
23065
+ version: "0.4.39"
22877
23066
  });
22878
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.", {
22879
23068
  type: exports_external.enum([
@@ -23934,6 +24123,11 @@ server.tool("capture_status", "Show whether Engrm hook registration and recent p
23934
24123
  {
23935
24124
  type: "text",
23936
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
+
23937
24131
  ` + `Claude MCP: ${result.claude_mcp_registered ? "registered" : "missing"}
23938
24132
  ` + `Claude hooks: ${result.claude_hooks_registered ? `registered (${result.claude_hook_count})` : "missing"}
23939
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"}
@@ -23941,6 +24135,9 @@ server.tool("capture_status", "Show whether Engrm hook registration and recent p
23941
24135
  ` + `Codex hooks: ${result.codex_hooks_registered ? "registered" : "missing"}
23942
24136
  ` + `Codex raw chronology: ${result.codex_raw_chronology_supported ? "supported" : "not yet supported (start/stop only)"}
23943
24137
 
24138
+ ` + `OpenCode MCP: ${result.opencode_mcp_registered ? "registered" : "missing"}
24139
+ ` + `OpenCode plugin: ${result.opencode_plugin_registered ? "installed" : "missing"}
24140
+
23944
24141
  ` + `Recent user prompts: ${result.recent_user_prompts}
23945
24142
  ` + `Recent tool events: ${result.recent_tool_events}
23946
24143
  ` + `Recent sessions with raw chronology: ${result.recent_sessions_with_raw_capture}
@@ -24858,9 +25055,82 @@ async function main() {
24858
25055
  }
24859
25056
  syncEngine = new SyncEngine(db, config2);
24860
25057
  syncEngine.start();
25058
+ if (shouldStartHttpMode()) {
25059
+ await startHttpServer();
25060
+ return;
25061
+ }
24861
25062
  const transport = new StdioServerTransport;
24862
25063
  await server.connect(transport);
24863
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
+ }
24864
25134
  main().catch((error48) => {
24865
25135
  console.error("Fatal:", error48);
24866
25136
  db.close();