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/README.md +53 -2
- package/dist/cli.js +446 -62
- package/dist/hooks/elicitation-result.js +57 -1
- package/dist/hooks/post-tool-use.js +57 -1
- package/dist/hooks/pre-compact.js +57 -1
- package/dist/hooks/sentinel.js +20 -0
- package/dist/hooks/session-start.js +82 -12
- package/dist/hooks/stop.js +305 -186
- package/dist/hooks/user-prompt-submit.js +57 -1
- package/dist/server.js +347 -77
- package/opencode/README.md +70 -0
- package/opencode/install-or-update-opencode-plugin.sh +46 -0
- package/opencode/opencode.example.json +14 -0
- package/opencode/plugin/engrm-opencode.js +61 -0
- package/package.json +10 -4
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
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
|
18933
|
-
best_recall_title: projectIndex?.best_recall_title ?? (recallIndex.items
|
|
18934
|
-
best_recall_kind: projectIndex?.best_recall_kind ?? (recallIndex.items
|
|
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 ?
|
|
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
|
-
|
|
20757
|
-
|
|
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 (
|
|
20770
|
-
snapshot = filterResumeSnapshotByAgent(snapshot,
|
|
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 =
|
|
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:
|
|
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
|
|
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:
|
|
21647
|
-
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:
|
|
21686
|
-
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,
|
|
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:
|
|
21732
|
-
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:
|
|
21755
|
-
tool_event_count:
|
|
21756
|
-
capture_state:
|
|
21757
|
-
recent_request_prompts:
|
|
21758
|
-
latest_request:
|
|
21759
|
-
current_thread:
|
|
21760
|
-
recent_tool_names:
|
|
21761
|
-
recent_tool_commands:
|
|
21762
|
-
hot_files:
|
|
21763
|
-
recent_outcomes:
|
|
21764
|
-
observation_source_tools:
|
|
21765
|
-
latest_observation_prompt_number:
|
|
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,
|
|
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
|
-
|
|
21875
|
-
|
|
21876
|
-
|
|
21877
|
-
|
|
21878
|
-
|
|
21879
|
-
|
|
21880
|
-
|
|
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
|
-
}
|
|
21887
|
-
|
|
21888
|
-
|
|
21889
|
-
|
|
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
|
-
}
|
|
21899
|
-
|
|
21900
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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();
|