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