engrm 0.4.40 → 0.4.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -20,6 +20,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
21
21
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
22
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
24
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
23
25
 
24
26
  // node_modules/zod/v4/classic/external.js
25
27
  var exports_external = {};
@@ -19480,18 +19482,23 @@ function getCaptureStatus(db, input = {}) {
19480
19482
  const codexHooks = join3(home, ".codex", "hooks.json");
19481
19483
  const opencodeConfig = join3(home, ".config", "opencode", "opencode.json");
19482
19484
  const opencodePlugin = join3(home, ".config", "opencode", "plugins", "engrm.js");
19485
+ const openclawConfig = join3(home, ".openclaw", "openclaw.json");
19486
+ const openclawPlugin = join3(home, ".openclaw", "extensions", "engrm", "openclaw.plugin.json");
19483
19487
  const config2 = configExists() ? loadConfig() : null;
19484
19488
  const claudeJsonContent = existsSync3(claudeJson) ? readFileSync3(claudeJson, "utf-8") : "";
19485
19489
  const claudeSettingsContent = existsSync3(claudeSettings) ? readFileSync3(claudeSettings, "utf-8") : "";
19486
19490
  const codexConfigContent = existsSync3(codexConfig) ? readFileSync3(codexConfig, "utf-8") : "";
19487
19491
  const codexHooksContent = existsSync3(codexHooks) ? readFileSync3(codexHooks, "utf-8") : "";
19488
19492
  const opencodeConfigContent = existsSync3(opencodeConfig) ? readFileSync3(opencodeConfig, "utf-8") : "";
19493
+ const openclawConfigContent = existsSync3(openclawConfig) ? readFileSync3(openclawConfig, "utf-8") : "";
19489
19494
  const claudeMcpRegistered = claudeJsonContent.includes('"engrm"');
19490
19495
  const claudeHooksRegistered = claudeSettingsContent.includes("engrm") || claudeSettingsContent.includes("session-start") || claudeSettingsContent.includes("user-prompt-submit");
19491
19496
  const codexMcpRegistered = codexConfigContent.includes("[mcp_servers.engrm]") || codexConfigContent.includes(`[mcp_servers.${LEGACY_CODEX_SERVER_NAME}]`);
19492
19497
  const codexHooksRegistered = codexHooksContent.includes('"SessionStart"') && codexHooksContent.includes('"Stop"');
19493
19498
  const opencodeMcpRegistered = opencodeConfigContent.includes('"engrm"') && opencodeConfigContent.includes('"type"') && opencodeConfigContent.includes('"local"');
19494
19499
  const opencodePluginRegistered = existsSync3(opencodePlugin);
19500
+ const openclawMcpRegistered = hasOpenClawMcpRegistration(openclawConfigContent);
19501
+ const openclawPluginRegistered = existsSync3(openclawPlugin);
19495
19502
  let claudeHookCount = 0;
19496
19503
  let claudeSessionStartHook = false;
19497
19504
  let claudeUserPromptHook = false;
@@ -19581,6 +19588,8 @@ function getCaptureStatus(db, input = {}) {
19581
19588
  codex_session_start_hook: codexSessionStartHook,
19582
19589
  codex_stop_hook: codexStopHook,
19583
19590
  codex_raw_chronology_supported: false,
19591
+ openclaw_mcp_registered: openclawMcpRegistered,
19592
+ openclaw_plugin_registered: openclawPluginRegistered,
19584
19593
  opencode_mcp_registered: opencodeMcpRegistered,
19585
19594
  opencode_plugin_registered: opencodePluginRegistered,
19586
19595
  recent_user_prompts: recentUserPrompts,
@@ -19601,6 +19610,16 @@ function parseNullableInt(value) {
19601
19610
  const parsed = Number.parseInt(value, 10);
19602
19611
  return Number.isFinite(parsed) ? parsed : null;
19603
19612
  }
19613
+ function hasOpenClawMcpRegistration(content) {
19614
+ if (!content)
19615
+ return false;
19616
+ try {
19617
+ const parsed = JSON.parse(content);
19618
+ return Boolean(parsed.mcp?.servers?.engrm);
19619
+ } catch {
19620
+ return content.includes('"mcp"') && content.includes('"servers"') && content.includes('"engrm"');
19621
+ }
19622
+ }
19604
19623
 
19605
19624
  // src/tools/capture-quality.ts
19606
19625
  function getCaptureQuality(db, input = {}) {
@@ -23061,388 +23080,389 @@ process.on("SIGTERM", () => {
23061
23080
  db.close();
23062
23081
  process.exit(0);
23063
23082
  });
23064
- var server = new McpServer({
23065
- name: "engrm",
23066
- version: "0.4.40"
23067
- });
23068
- 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.", {
23069
- type: exports_external.enum([
23070
- "bugfix",
23071
- "discovery",
23072
- "decision",
23073
- "pattern",
23074
- "change",
23075
- "feature",
23076
- "refactor",
23077
- "digest",
23078
- "message"
23079
- ]),
23080
- title: exports_external.string().describe("Brief title"),
23081
- narrative: exports_external.string().optional().describe("What happened and why"),
23082
- facts: exports_external.array(exports_external.string()).optional().describe("Key facts"),
23083
- concepts: exports_external.array(exports_external.string()).optional().describe("Tags"),
23084
- files_read: exports_external.array(exports_external.string()).optional().describe("Files read (project-relative)"),
23085
- files_modified: exports_external.array(exports_external.string()).optional().describe("Files modified (project-relative)"),
23086
- sensitivity: exports_external.enum(["shared", "personal", "secret"]).optional(),
23087
- session_id: exports_external.string().optional(),
23088
- supersedes: exports_external.number().optional().describe("ID of observation this replaces")
23089
- }, async (params) => {
23090
- const result = await saveObservation(db, config2, { ...params, agent: getDetectedAgent() });
23091
- if (!result.success) {
23083
+ function buildServer() {
23084
+ const server = new McpServer({
23085
+ name: "engrm",
23086
+ version: "0.4.42"
23087
+ });
23088
+ 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.", {
23089
+ type: exports_external.enum([
23090
+ "bugfix",
23091
+ "discovery",
23092
+ "decision",
23093
+ "pattern",
23094
+ "change",
23095
+ "feature",
23096
+ "refactor",
23097
+ "digest",
23098
+ "message"
23099
+ ]),
23100
+ title: exports_external.string().describe("Brief title"),
23101
+ narrative: exports_external.string().optional().describe("What happened and why"),
23102
+ facts: exports_external.array(exports_external.string()).optional().describe("Key facts"),
23103
+ concepts: exports_external.array(exports_external.string()).optional().describe("Tags"),
23104
+ files_read: exports_external.array(exports_external.string()).optional().describe("Files read (project-relative)"),
23105
+ files_modified: exports_external.array(exports_external.string()).optional().describe("Files modified (project-relative)"),
23106
+ sensitivity: exports_external.enum(["shared", "personal", "secret"]).optional(),
23107
+ session_id: exports_external.string().optional(),
23108
+ supersedes: exports_external.number().optional().describe("ID of observation this replaces")
23109
+ }, async (params) => {
23110
+ const result = await saveObservation(db, config2, { ...params, agent: getDetectedAgent() });
23111
+ if (!result.success) {
23112
+ return {
23113
+ content: [
23114
+ {
23115
+ type: "text",
23116
+ text: `Not saved: ${result.reason}`
23117
+ }
23118
+ ]
23119
+ };
23120
+ }
23121
+ if (result.merged_into) {
23122
+ return {
23123
+ content: [
23124
+ {
23125
+ type: "text",
23126
+ text: `Merged into observation #${result.merged_into} (quality: ${result.quality_score?.toFixed(2)})`
23127
+ }
23128
+ ]
23129
+ };
23130
+ }
23131
+ let supersessionNote = "";
23132
+ if (params.supersedes && result.observation_id) {
23133
+ const superseded = db.supersedeObservation(params.supersedes, result.observation_id);
23134
+ if (superseded) {
23135
+ supersessionNote = `, supersedes #${params.supersedes}`;
23136
+ }
23137
+ }
23092
23138
  return {
23093
23139
  content: [
23094
23140
  {
23095
23141
  type: "text",
23096
- text: `Not saved: ${result.reason}`
23142
+ text: `Saved observation #${result.observation_id} (quality: ${result.quality_score?.toFixed(2)}${supersessionNote})`
23097
23143
  }
23098
23144
  ]
23099
23145
  };
23100
- }
23101
- if (result.merged_into) {
23146
+ });
23147
+ server.tool("plugin_catalog", "List Engrm plugin manifests so tools can produce memory in a compatible shape", {
23148
+ surface: exports_external.enum(PLUGIN_SURFACES).optional().describe("Optional surface filter")
23149
+ }, async (params) => {
23150
+ const manifests = listPluginManifests(params.surface);
23151
+ const lines = manifests.map((manifest) => `- ${manifest.id} (${manifest.kind}) -> produces ${manifest.produces.join(", ")}; surfaces ${manifest.surfaces.join(", ")}`);
23102
23152
  return {
23103
23153
  content: [
23104
23154
  {
23105
23155
  type: "text",
23106
- text: `Merged into observation #${result.merged_into} (quality: ${result.quality_score?.toFixed(2)})`
23107
- }
23108
- ]
23109
- };
23110
- }
23111
- let supersessionNote = "";
23112
- if (params.supersedes && result.observation_id) {
23113
- const superseded = db.supersedeObservation(params.supersedes, result.observation_id);
23114
- if (superseded) {
23115
- supersessionNote = `, supersedes #${params.supersedes}`;
23116
- }
23117
- }
23118
- return {
23119
- content: [
23120
- {
23121
- type: "text",
23122
- text: `Saved observation #${result.observation_id} (quality: ${result.quality_score?.toFixed(2)}${supersessionNote})`
23123
- }
23124
- ]
23125
- };
23126
- });
23127
- server.tool("plugin_catalog", "List Engrm plugin manifests so tools can produce memory in a compatible shape", {
23128
- surface: exports_external.enum(PLUGIN_SURFACES).optional().describe("Optional surface filter")
23129
- }, async (params) => {
23130
- const manifests = listPluginManifests(params.surface);
23131
- const lines = manifests.map((manifest) => `- ${manifest.id} (${manifest.kind}) -> produces ${manifest.produces.join(", ")}; surfaces ${manifest.surfaces.join(", ")}`);
23132
- return {
23133
- content: [
23134
- {
23135
- type: "text",
23136
- text: `Engrm plugin spec: ${PLUGIN_SPEC_VERSION}
23156
+ text: `Engrm plugin spec: ${PLUGIN_SPEC_VERSION}
23137
23157
 
23138
23158
  ` + (params.surface ? `Surface filter: ${params.surface}
23139
23159
 
23140
23160
  ` : "") + (lines.length > 0 ? lines.join(`
23141
23161
  `) : "No plugin manifests available.")
23142
- }
23143
- ]
23144
- };
23145
- });
23146
- server.tool("save_plugin_memory", "Save reduced plugin output as durable Engrm memory with stable plugin provenance", {
23147
- plugin_id: exports_external.string().describe("Stable plugin identifier such as 'engrm.git-diff'"),
23148
- type: exports_external.enum([
23149
- "bugfix",
23150
- "discovery",
23151
- "decision",
23152
- "pattern",
23153
- "change",
23154
- "feature",
23155
- "refactor",
23156
- "digest",
23157
- "message"
23158
- ]),
23159
- title: exports_external.string().describe("Short durable memory title"),
23160
- summary: exports_external.string().optional().describe("Reduced summary of what happened and why it matters"),
23161
- facts: exports_external.array(exports_external.string()).optional().describe("Reusable facts worth remembering"),
23162
- tags: exports_external.array(exports_external.string()).optional().describe("Plugin-specific tags"),
23163
- source: exports_external.string().optional().describe("Upstream source like git, openclaw, ci, or issues"),
23164
- source_refs: exports_external.array(exports_external.object({
23165
- kind: exports_external.enum(["file", "url", "ticket", "commit", "thread", "command", "other"]),
23166
- value: exports_external.string()
23167
- })).optional().describe("Pointers back to the original evidence"),
23168
- surfaces: exports_external.array(exports_external.enum(PLUGIN_SURFACES)).optional().describe("Engrm surfaces this memory is designed to feed"),
23169
- files_read: exports_external.array(exports_external.string()).optional().describe("Files read (project-relative when possible)"),
23170
- files_modified: exports_external.array(exports_external.string()).optional().describe("Files modified (project-relative when possible)"),
23171
- sensitivity: exports_external.enum(["shared", "personal", "secret"]).optional(),
23172
- session_id: exports_external.string().optional(),
23173
- cwd: exports_external.string().optional().describe("Project root for relative-path normalization")
23174
- }, async (params) => {
23175
- const result = await savePluginMemory(db, config2, {
23176
- ...params,
23177
- agent: getDetectedAgent()
23178
- });
23179
- if (!result.success) {
23180
- return {
23181
- content: [
23182
- {
23183
- type: "text",
23184
- text: `Not saved: ${result.reason}`
23185
- }
23186
- ]
23187
- };
23188
- }
23189
- if (result.merged_into) {
23190
- return {
23191
- content: [
23192
- {
23193
- type: "text",
23194
- text: `Merged plugin memory into observation #${result.merged_into} (quality: ${result.quality_score?.toFixed(2)})`
23195
23162
  }
23196
23163
  ]
23197
23164
  };
23198
- }
23199
- return {
23200
- content: [
23201
- {
23202
- type: "text",
23203
- text: `Saved plugin memory as observation #${result.observation_id} (quality: ${result.quality_score?.toFixed(2)})`
23204
- }
23205
- ]
23206
- };
23207
- });
23208
- server.tool("capture_git_diff", "Reduce a git diff into a durable Engrm memory object and save it with plugin provenance", {
23209
- diff: exports_external.string().describe("Unified git diff text"),
23210
- summary: exports_external.string().optional().describe("Optional human summary or commit-style title"),
23211
- files: exports_external.array(exports_external.string()).optional().describe("Optional changed file paths if already known"),
23212
- session_id: exports_external.string().optional(),
23213
- cwd: exports_external.string().optional().describe("Project root for relative-path normalization")
23214
- }, async (params) => {
23215
- const reduced = reduceGitDiffToMemory({
23216
- ...params,
23217
- cwd: params.cwd ?? process.cwd(),
23218
- agent: getDetectedAgent()
23219
23165
  });
23220
- const result = await savePluginMemory(db, config2, reduced);
23221
- if (!result.success) {
23222
- return {
23223
- content: [
23224
- {
23225
- type: "text",
23226
- text: `Not saved: ${result.reason}`
23227
- }
23228
- ]
23229
- };
23230
- }
23231
- const reducedFacts = reduced.facts && reduced.facts.length > 0 ? `
23232
- Facts: ${reduced.facts.join("; ")}` : "";
23233
- return {
23234
- content: [
23235
- {
23236
- type: "text",
23237
- text: `Saved git diff as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})${reducedFacts}`
23238
- }
23239
- ]
23240
- };
23241
- });
23242
- server.tool("capture_git_worktree", "Capture the current git worktree as durable memory. Best for saving a meaningful local diff before context is lost.", {
23243
- cwd: exports_external.string().optional().describe("Git repo path. Defaults to the current working directory."),
23244
- staged: exports_external.boolean().optional().describe("If true, capture staged changes instead of unstaged worktree changes."),
23245
- summary: exports_external.string().optional().describe("Optional human summary or commit-style title to steer the saved memory."),
23246
- session_id: exports_external.string().optional().describe("Optional session ID to link this capture to active work.")
23247
- }, async (params) => {
23248
- let worktree;
23249
- try {
23250
- worktree = captureGitWorktree({
23251
- cwd: params.cwd ?? process.cwd(),
23252
- staged: params.staged
23166
+ server.tool("save_plugin_memory", "Save reduced plugin output as durable Engrm memory with stable plugin provenance", {
23167
+ plugin_id: exports_external.string().describe("Stable plugin identifier such as 'engrm.git-diff'"),
23168
+ type: exports_external.enum([
23169
+ "bugfix",
23170
+ "discovery",
23171
+ "decision",
23172
+ "pattern",
23173
+ "change",
23174
+ "feature",
23175
+ "refactor",
23176
+ "digest",
23177
+ "message"
23178
+ ]),
23179
+ title: exports_external.string().describe("Short durable memory title"),
23180
+ summary: exports_external.string().optional().describe("Reduced summary of what happened and why it matters"),
23181
+ facts: exports_external.array(exports_external.string()).optional().describe("Reusable facts worth remembering"),
23182
+ tags: exports_external.array(exports_external.string()).optional().describe("Plugin-specific tags"),
23183
+ source: exports_external.string().optional().describe("Upstream source like git, openclaw, ci, or issues"),
23184
+ source_refs: exports_external.array(exports_external.object({
23185
+ kind: exports_external.enum(["file", "url", "ticket", "commit", "thread", "command", "other"]),
23186
+ value: exports_external.string()
23187
+ })).optional().describe("Pointers back to the original evidence"),
23188
+ surfaces: exports_external.array(exports_external.enum(PLUGIN_SURFACES)).optional().describe("Engrm surfaces this memory is designed to feed"),
23189
+ files_read: exports_external.array(exports_external.string()).optional().describe("Files read (project-relative when possible)"),
23190
+ files_modified: exports_external.array(exports_external.string()).optional().describe("Files modified (project-relative when possible)"),
23191
+ sensitivity: exports_external.enum(["shared", "personal", "secret"]).optional(),
23192
+ session_id: exports_external.string().optional(),
23193
+ cwd: exports_external.string().optional().describe("Project root for relative-path normalization")
23194
+ }, async (params) => {
23195
+ const result = await savePluginMemory(db, config2, {
23196
+ ...params,
23197
+ agent: getDetectedAgent()
23253
23198
  });
23254
- } catch (error48) {
23255
- return {
23256
- content: [
23257
- {
23258
- type: "text",
23259
- text: `Not captured: ${error48 instanceof Error ? error48.message : "unable to read git worktree"}`
23260
- }
23261
- ]
23262
- };
23263
- }
23264
- if (!worktree.diff.trim()) {
23199
+ if (!result.success) {
23200
+ return {
23201
+ content: [
23202
+ {
23203
+ type: "text",
23204
+ text: `Not saved: ${result.reason}`
23205
+ }
23206
+ ]
23207
+ };
23208
+ }
23209
+ if (result.merged_into) {
23210
+ return {
23211
+ content: [
23212
+ {
23213
+ type: "text",
23214
+ text: `Merged plugin memory into observation #${result.merged_into} (quality: ${result.quality_score?.toFixed(2)})`
23215
+ }
23216
+ ]
23217
+ };
23218
+ }
23265
23219
  return {
23266
23220
  content: [
23267
23221
  {
23268
23222
  type: "text",
23269
- text: `No ${params.staged ? "staged" : "unstaged"} git diff found in ${worktree.cwd}`
23223
+ text: `Saved plugin memory as observation #${result.observation_id} (quality: ${result.quality_score?.toFixed(2)})`
23270
23224
  }
23271
23225
  ]
23272
23226
  };
23273
- }
23274
- const reduced = reduceGitDiffToMemory({
23275
- diff: worktree.diff,
23276
- summary: params.summary,
23277
- files: worktree.files,
23278
- session_id: params.session_id,
23279
- cwd: worktree.cwd,
23280
- agent: getDetectedAgent()
23281
23227
  });
23282
- const result = await savePluginMemory(db, config2, reduced);
23283
- if (!result.success) {
23284
- return {
23285
- content: [
23286
- {
23287
- type: "text",
23288
- text: `Not saved: ${result.reason}`
23289
- }
23290
- ]
23291
- };
23292
- }
23293
- return {
23294
- content: [
23295
- {
23296
- type: "text",
23297
- text: `Saved ${params.staged ? "staged" : "worktree"} git diff as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})`
23298
- }
23299
- ]
23300
- };
23301
- });
23302
- server.tool("capture_repo_scan", "Run a lightweight repository scan and save reduced findings as durable memory. Best for quick architecture, risk, or implementation scans.", {
23303
- cwd: exports_external.string().optional().describe("Repo path to scan. Defaults to the current working directory."),
23304
- focus: exports_external.array(exports_external.string()).optional().describe("Optional topics to bias the scan toward, for example 'billing', 'auth', or 'validation'."),
23305
- max_findings: exports_external.number().optional().describe("Maximum findings to keep before reduction."),
23306
- summary: exports_external.string().optional().describe("Optional human summary for the saved memory."),
23307
- session_id: exports_external.string().optional().describe("Optional session ID to link this scan to active work.")
23308
- }, async (params) => {
23309
- let scan;
23310
- try {
23311
- scan = captureRepoScan({
23228
+ server.tool("capture_git_diff", "Reduce a git diff into a durable Engrm memory object and save it with plugin provenance", {
23229
+ diff: exports_external.string().describe("Unified git diff text"),
23230
+ summary: exports_external.string().optional().describe("Optional human summary or commit-style title"),
23231
+ files: exports_external.array(exports_external.string()).optional().describe("Optional changed file paths if already known"),
23232
+ session_id: exports_external.string().optional(),
23233
+ cwd: exports_external.string().optional().describe("Project root for relative-path normalization")
23234
+ }, async (params) => {
23235
+ const reduced = reduceGitDiffToMemory({
23236
+ ...params,
23312
23237
  cwd: params.cwd ?? process.cwd(),
23313
- focus: params.focus,
23314
- max_findings: params.max_findings
23238
+ agent: getDetectedAgent()
23315
23239
  });
23316
- } catch (error48) {
23240
+ const result = await savePluginMemory(db, config2, reduced);
23241
+ if (!result.success) {
23242
+ return {
23243
+ content: [
23244
+ {
23245
+ type: "text",
23246
+ text: `Not saved: ${result.reason}`
23247
+ }
23248
+ ]
23249
+ };
23250
+ }
23251
+ const reducedFacts = reduced.facts && reduced.facts.length > 0 ? `
23252
+ Facts: ${reduced.facts.join("; ")}` : "";
23317
23253
  return {
23318
23254
  content: [
23319
23255
  {
23320
23256
  type: "text",
23321
- text: `Not captured: ${error48 instanceof Error ? error48.message : "unable to scan repository"}`
23257
+ text: `Saved git diff as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})${reducedFacts}`
23322
23258
  }
23323
23259
  ]
23324
23260
  };
23325
- }
23326
- if (scan.findings.length === 0) {
23261
+ });
23262
+ server.tool("capture_git_worktree", "Capture the current git worktree as durable memory. Best for saving a meaningful local diff before context is lost.", {
23263
+ cwd: exports_external.string().optional().describe("Git repo path. Defaults to the current working directory."),
23264
+ staged: exports_external.boolean().optional().describe("If true, capture staged changes instead of unstaged worktree changes."),
23265
+ summary: exports_external.string().optional().describe("Optional human summary or commit-style title to steer the saved memory."),
23266
+ session_id: exports_external.string().optional().describe("Optional session ID to link this capture to active work.")
23267
+ }, async (params) => {
23268
+ let worktree;
23269
+ try {
23270
+ worktree = captureGitWorktree({
23271
+ cwd: params.cwd ?? process.cwd(),
23272
+ staged: params.staged
23273
+ });
23274
+ } catch (error48) {
23275
+ return {
23276
+ content: [
23277
+ {
23278
+ type: "text",
23279
+ text: `Not captured: ${error48 instanceof Error ? error48.message : "unable to read git worktree"}`
23280
+ }
23281
+ ]
23282
+ };
23283
+ }
23284
+ if (!worktree.diff.trim()) {
23285
+ return {
23286
+ content: [
23287
+ {
23288
+ type: "text",
23289
+ text: `No ${params.staged ? "staged" : "unstaged"} git diff found in ${worktree.cwd}`
23290
+ }
23291
+ ]
23292
+ };
23293
+ }
23294
+ const reduced = reduceGitDiffToMemory({
23295
+ diff: worktree.diff,
23296
+ summary: params.summary,
23297
+ files: worktree.files,
23298
+ session_id: params.session_id,
23299
+ cwd: worktree.cwd,
23300
+ agent: getDetectedAgent()
23301
+ });
23302
+ const result = await savePluginMemory(db, config2, reduced);
23303
+ if (!result.success) {
23304
+ return {
23305
+ content: [
23306
+ {
23307
+ type: "text",
23308
+ text: `Not saved: ${result.reason}`
23309
+ }
23310
+ ]
23311
+ };
23312
+ }
23327
23313
  return {
23328
23314
  content: [
23329
23315
  {
23330
23316
  type: "text",
23331
- text: `No lightweight repo-scan findings found in ${scan.cwd}`
23317
+ text: `Saved ${params.staged ? "staged" : "worktree"} git diff as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})`
23332
23318
  }
23333
23319
  ]
23334
23320
  };
23335
- }
23336
- const reduced = reduceRepoScanToMemory({
23337
- summary: params.summary,
23338
- findings: scan.findings,
23339
- session_id: params.session_id,
23340
- cwd: scan.cwd,
23341
- agent: getDetectedAgent()
23342
23321
  });
23343
- const result = await savePluginMemory(db, config2, reduced);
23344
- if (!result.success) {
23322
+ server.tool("capture_repo_scan", "Run a lightweight repository scan and save reduced findings as durable memory. Best for quick architecture, risk, or implementation scans.", {
23323
+ cwd: exports_external.string().optional().describe("Repo path to scan. Defaults to the current working directory."),
23324
+ focus: exports_external.array(exports_external.string()).optional().describe("Optional topics to bias the scan toward, for example 'billing', 'auth', or 'validation'."),
23325
+ max_findings: exports_external.number().optional().describe("Maximum findings to keep before reduction."),
23326
+ summary: exports_external.string().optional().describe("Optional human summary for the saved memory."),
23327
+ session_id: exports_external.string().optional().describe("Optional session ID to link this scan to active work.")
23328
+ }, async (params) => {
23329
+ let scan;
23330
+ try {
23331
+ scan = captureRepoScan({
23332
+ cwd: params.cwd ?? process.cwd(),
23333
+ focus: params.focus,
23334
+ max_findings: params.max_findings
23335
+ });
23336
+ } catch (error48) {
23337
+ return {
23338
+ content: [
23339
+ {
23340
+ type: "text",
23341
+ text: `Not captured: ${error48 instanceof Error ? error48.message : "unable to scan repository"}`
23342
+ }
23343
+ ]
23344
+ };
23345
+ }
23346
+ if (scan.findings.length === 0) {
23347
+ return {
23348
+ content: [
23349
+ {
23350
+ type: "text",
23351
+ text: `No lightweight repo-scan findings found in ${scan.cwd}`
23352
+ }
23353
+ ]
23354
+ };
23355
+ }
23356
+ const reduced = reduceRepoScanToMemory({
23357
+ summary: params.summary,
23358
+ findings: scan.findings,
23359
+ session_id: params.session_id,
23360
+ cwd: scan.cwd,
23361
+ agent: getDetectedAgent()
23362
+ });
23363
+ const result = await savePluginMemory(db, config2, reduced);
23364
+ if (!result.success) {
23365
+ return {
23366
+ content: [
23367
+ {
23368
+ type: "text",
23369
+ text: `Not saved: ${result.reason}`
23370
+ }
23371
+ ]
23372
+ };
23373
+ }
23374
+ const findingSummary = scan.findings.slice(0, 3).map((finding) => finding.title).join("; ");
23345
23375
  return {
23346
23376
  content: [
23347
23377
  {
23348
23378
  type: "text",
23349
- text: `Not saved: ${result.reason}`
23379
+ text: `Saved repo scan as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})` + `${findingSummary ? `
23380
+ Findings: ${findingSummary}` : ""}`
23350
23381
  }
23351
23382
  ]
23352
23383
  };
23353
- }
23354
- const findingSummary = scan.findings.slice(0, 3).map((finding) => finding.title).join("; ");
23355
- return {
23356
- content: [
23357
- {
23358
- type: "text",
23359
- text: `Saved repo scan as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})` + `${findingSummary ? `
23360
- Findings: ${findingSummary}` : ""}`
23361
- }
23362
- ]
23363
- };
23364
- });
23365
- server.tool("capture_openclaw_content", "Directly save OpenClaw content, research, and follow-up work as durable memory. Best for preserving posted outcomes, discoveries, and next actions during or right after the run.", {
23366
- title: exports_external.string().optional().describe("Short content, campaign, or research title."),
23367
- posted: exports_external.array(exports_external.string()).optional().describe("Concrete posted items or shipped content outcomes."),
23368
- researched: exports_external.array(exports_external.string()).optional().describe("Research or discovery items worth retaining."),
23369
- outcomes: exports_external.array(exports_external.string()).optional().describe("Meaningful outcomes from the run."),
23370
- next_actions: exports_external.array(exports_external.string()).optional().describe("Real follow-up actions that remain."),
23371
- links: exports_external.array(exports_external.string()).optional().describe("Thread or source URLs tied to the work."),
23372
- session_id: exports_external.string().optional().describe("Optional session ID to link this memory to active work."),
23373
- cwd: exports_external.string().optional().describe("Optional project path for attribution.")
23374
- }, async (params) => {
23375
- const reduced = reduceOpenClawContentToMemory({
23376
- ...params,
23377
- cwd: params.cwd ?? process.cwd(),
23378
- agent: getDetectedAgent()
23379
23384
  });
23380
- const result = await savePluginMemory(db, config2, reduced);
23381
- if (!result.success) {
23385
+ server.tool("capture_openclaw_content", "Directly save OpenClaw content, research, and follow-up work as durable memory. Best for preserving posted outcomes, discoveries, and next actions during or right after the run.", {
23386
+ title: exports_external.string().optional().describe("Short content, campaign, or research title."),
23387
+ posted: exports_external.array(exports_external.string()).optional().describe("Concrete posted items or shipped content outcomes."),
23388
+ researched: exports_external.array(exports_external.string()).optional().describe("Research or discovery items worth retaining."),
23389
+ outcomes: exports_external.array(exports_external.string()).optional().describe("Meaningful outcomes from the run."),
23390
+ next_actions: exports_external.array(exports_external.string()).optional().describe("Real follow-up actions that remain."),
23391
+ links: exports_external.array(exports_external.string()).optional().describe("Thread or source URLs tied to the work."),
23392
+ session_id: exports_external.string().optional().describe("Optional session ID to link this memory to active work."),
23393
+ cwd: exports_external.string().optional().describe("Optional project path for attribution.")
23394
+ }, async (params) => {
23395
+ const reduced = reduceOpenClawContentToMemory({
23396
+ ...params,
23397
+ cwd: params.cwd ?? process.cwd(),
23398
+ agent: getDetectedAgent()
23399
+ });
23400
+ const result = await savePluginMemory(db, config2, reduced);
23401
+ if (!result.success) {
23402
+ return {
23403
+ content: [
23404
+ {
23405
+ type: "text",
23406
+ text: `Not saved: ${result.reason}`
23407
+ }
23408
+ ]
23409
+ };
23410
+ }
23382
23411
  return {
23383
23412
  content: [
23384
23413
  {
23385
23414
  type: "text",
23386
- text: `Not saved: ${result.reason}`
23415
+ text: `Saved OpenClaw content memory as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})`
23387
23416
  }
23388
23417
  ]
23389
23418
  };
23390
- }
23391
- return {
23392
- content: [
23393
- {
23394
- type: "text",
23395
- text: `Saved OpenClaw content memory as observation #${result.observation_id} ` + `(${reduced.type}: ${reduced.title})`
23396
- }
23397
- ]
23398
- };
23399
- });
23400
- server.tool("search", "Search memory for observations", {
23401
- query: exports_external.string().describe("Search query"),
23402
- project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
23403
- limit: exports_external.number().optional().describe("Max results (default: 10)")
23404
- }, async (params) => {
23405
- const result = await searchObservations(db, {
23406
- ...params,
23407
- user_id: config2.user_id
23408
23419
  });
23409
- _lastSearchSessionId = "active";
23410
- sessionMetrics.searchCount++;
23411
- sessionMetrics.searchResultsTotal += result.total;
23412
- persistSessionMetrics();
23413
- if (result.total === 0) {
23420
+ server.tool("search", "Search memory for observations", {
23421
+ query: exports_external.string().describe("Search query"),
23422
+ project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
23423
+ limit: exports_external.number().optional().describe("Max results (default: 10)")
23424
+ }, async (params) => {
23425
+ const result = await searchObservations(db, {
23426
+ ...params,
23427
+ user_id: config2.user_id
23428
+ });
23429
+ _lastSearchSessionId = "active";
23430
+ sessionMetrics.searchCount++;
23431
+ sessionMetrics.searchResultsTotal += result.total;
23432
+ persistSessionMetrics();
23433
+ if (result.total === 0) {
23434
+ return {
23435
+ content: [
23436
+ {
23437
+ type: "text",
23438
+ text: result.project ? `No observations found for "${params.query}" in project ${result.project}` : `No observations found for "${params.query}"`
23439
+ }
23440
+ ]
23441
+ };
23442
+ }
23443
+ const includeProjectColumn = result.observations.some((obs) => obs.project_name);
23444
+ const header = includeProjectColumn ? "| ID | Type | Q | Title | Project | Created |" : "| ID | Type | Q | Title | Created |";
23445
+ const separator = includeProjectColumn ? "|---|---|---|---|---|---|" : "|---|---|---|---|---|";
23446
+ const rows = result.observations.map((obs) => {
23447
+ const qualityDots = qualityIndicator(obs.quality);
23448
+ const date5 = obs.created_at.split("T")[0];
23449
+ if (includeProjectColumn) {
23450
+ return `| ${obs.id} | ${obs.type} | ${qualityDots} | ${obs.title} | ${obs.project_name ?? "-"} | ${date5} |`;
23451
+ }
23452
+ return `| ${obs.id} | ${obs.type} | ${qualityDots} | ${obs.title} | ${date5} |`;
23453
+ });
23454
+ const previews = result.observations.slice(0, Math.min(3, result.observations.length)).map((obs) => {
23455
+ const preview = formatFactPreview(obs.facts, obs.narrative);
23456
+ const projectSuffix = obs.project_name ? ` [${obs.project_name}]` : "";
23457
+ return preview ? `- #${obs.id} [${obs.type}] ${obs.title}${projectSuffix}: ${preview}` : `- #${obs.id} [${obs.type}] ${obs.title}${projectSuffix}`;
23458
+ });
23459
+ const projectLine = result.project ? `Project: ${result.project}
23460
+ ` : "";
23414
23461
  return {
23415
23462
  content: [
23416
23463
  {
23417
23464
  type: "text",
23418
- text: result.project ? `No observations found for "${params.query}" in project ${result.project}` : `No observations found for "${params.query}"`
23419
- }
23420
- ]
23421
- };
23422
- }
23423
- const includeProjectColumn = result.observations.some((obs) => obs.project_name);
23424
- const header = includeProjectColumn ? "| ID | Type | Q | Title | Project | Created |" : "| ID | Type | Q | Title | Created |";
23425
- const separator = includeProjectColumn ? "|---|---|---|---|---|---|" : "|---|---|---|---|---|";
23426
- const rows = result.observations.map((obs) => {
23427
- const qualityDots = qualityIndicator(obs.quality);
23428
- const date5 = obs.created_at.split("T")[0];
23429
- if (includeProjectColumn) {
23430
- return `| ${obs.id} | ${obs.type} | ${qualityDots} | ${obs.title} | ${obs.project_name ?? "-"} | ${date5} |`;
23431
- }
23432
- return `| ${obs.id} | ${obs.type} | ${qualityDots} | ${obs.title} | ${date5} |`;
23433
- });
23434
- const previews = result.observations.slice(0, Math.min(3, result.observations.length)).map((obs) => {
23435
- const preview = formatFactPreview(obs.facts, obs.narrative);
23436
- const projectSuffix = obs.project_name ? ` [${obs.project_name}]` : "";
23437
- return preview ? `- #${obs.id} [${obs.type}] ${obs.title}${projectSuffix}: ${preview}` : `- #${obs.id} [${obs.type}] ${obs.title}${projectSuffix}`;
23438
- });
23439
- const projectLine = result.project ? `Project: ${result.project}
23440
- ` : "";
23441
- return {
23442
- content: [
23443
- {
23444
- type: "text",
23445
- text: `${projectLine}Found ${result.total} result(s):
23465
+ text: `${projectLine}Found ${result.total} result(s):
23446
23466
 
23447
23467
  ${header}
23448
23468
  ${separator}
@@ -23452,149 +23472,149 @@ ${rows.join(`
23452
23472
  Top context:
23453
23473
  ${previews.join(`
23454
23474
  `)}`
23455
- }
23456
- ]
23457
- };
23458
- });
23459
- server.tool("search_recall", "Search live recall across durable memory and chat together. Best for questions like 'what were we just talking about?'", {
23460
- query: exports_external.string().describe("Recall query"),
23461
- project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
23462
- limit: exports_external.number().optional().describe("Max results (default: 10)"),
23463
- cwd: exports_external.string().optional().describe("Optional cwd override for project-scoped recall"),
23464
- user_id: exports_external.string().optional().describe("Optional user override")
23465
- }, async (params) => {
23466
- const result = await searchRecall(db, {
23467
- query: params.query,
23468
- project_scoped: params.project_scoped,
23469
- limit: params.limit,
23470
- cwd: params.cwd,
23471
- user_id: params.user_id ?? config2.user_id
23472
- });
23473
- if (result.results.length === 0) {
23474
- return {
23475
- content: [
23476
- {
23477
- type: "text",
23478
- text: result.project ? `No recall found for "${params.query}" in project ${result.project}` : `No recall found for "${params.query}"`
23479
23475
  }
23480
23476
  ]
23481
23477
  };
23482
- }
23483
- const projectLine = result.project ? `Project: ${result.project}
23478
+ });
23479
+ server.tool("search_recall", "Search live recall across durable memory and chat together. Best for questions like 'what were we just talking about?'", {
23480
+ query: exports_external.string().describe("Recall query"),
23481
+ project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
23482
+ limit: exports_external.number().optional().describe("Max results (default: 10)"),
23483
+ cwd: exports_external.string().optional().describe("Optional cwd override for project-scoped recall"),
23484
+ user_id: exports_external.string().optional().describe("Optional user override")
23485
+ }, async (params) => {
23486
+ const result = await searchRecall(db, {
23487
+ query: params.query,
23488
+ project_scoped: params.project_scoped,
23489
+ limit: params.limit,
23490
+ cwd: params.cwd,
23491
+ user_id: params.user_id ?? config2.user_id
23492
+ });
23493
+ if (result.results.length === 0) {
23494
+ return {
23495
+ content: [
23496
+ {
23497
+ type: "text",
23498
+ text: result.project ? `No recall found for "${params.query}" in project ${result.project}` : `No recall found for "${params.query}"`
23499
+ }
23500
+ ]
23501
+ };
23502
+ }
23503
+ const projectLine = result.project ? `Project: ${result.project}
23484
23504
  ` : "";
23485
- const summaryLine = `Matches: ${result.results.length} · memory ${result.totals.memory} · chat ${result.totals.chat}
23505
+ const summaryLine = `Matches: ${result.results.length} · memory ${result.totals.memory} · chat ${result.totals.chat}
23486
23506
  `;
23487
- const rows = result.results.map((item) => {
23488
- const sourceBits = [item.kind];
23489
- if (item.type)
23490
- sourceBits.push(item.type);
23491
- if (item.role)
23492
- sourceBits.push(item.role);
23493
- if (item.source_kind)
23494
- sourceBits.push(item.source_kind);
23495
- const idBit = item.observation_id ? `#${item.observation_id}` : item.id ? `chat:${item.id}` : "";
23496
- const title = `${idBit ? `${idBit} ` : ""}${item.title}${item.project_name ? ` (${item.project_name})` : ""}`;
23497
- return `- [${sourceBits.join(" · ")}] ${title}
23507
+ const rows = result.results.map((item) => {
23508
+ const sourceBits = [item.kind];
23509
+ if (item.type)
23510
+ sourceBits.push(item.type);
23511
+ if (item.role)
23512
+ sourceBits.push(item.role);
23513
+ if (item.source_kind)
23514
+ sourceBits.push(item.source_kind);
23515
+ const idBit = item.observation_id ? `#${item.observation_id}` : item.id ? `chat:${item.id}` : "";
23516
+ const title = `${idBit ? `${idBit} ` : ""}${item.title}${item.project_name ? ` (${item.project_name})` : ""}`;
23517
+ return `- [${sourceBits.join(" · ")}] ${title}
23498
23518
  ${item.detail.slice(0, 220)}`;
23499
- }).join(`
23519
+ }).join(`
23500
23520
  `);
23501
- return {
23502
- content: [
23503
- {
23504
- type: "text",
23505
- text: `${projectLine}${summaryLine}Recall search for "${params.query}":
23506
- ${rows}`
23507
- }
23508
- ]
23509
- };
23510
- });
23511
- server.tool("list_recall_items", "USE FIRST when continuity feels fuzzy. List the best current handoffs, session threads, chat snippets, and memory entries before opening one exact item.", {
23512
- cwd: exports_external.string().optional().describe("Optional cwd override for project-scoped recall"),
23513
- project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
23514
- user_id: exports_external.string().optional().describe("Optional user override"),
23515
- limit: exports_external.number().optional().describe("Max recall items to list")
23516
- }, async (params) => {
23517
- const result = listRecallItems(db, {
23518
- cwd: params.cwd ?? process.cwd(),
23519
- project_scoped: params.project_scoped,
23520
- user_id: params.user_id ?? config2.user_id,
23521
- current_device_id: config2.device_id,
23522
- limit: params.limit
23523
- });
23524
- if (result.items.length === 0) {
23525
23521
  return {
23526
23522
  content: [
23527
23523
  {
23528
23524
  type: "text",
23529
- text: result.project ? `No recall items found yet for project ${result.project}` : "No recall items found yet."
23525
+ text: `${projectLine}${summaryLine}Recall search for "${params.query}":
23526
+ ${rows}`
23530
23527
  }
23531
23528
  ]
23532
23529
  };
23533
- }
23534
- const projectLine = result.project ? `Project: ${result.project}
23530
+ });
23531
+ server.tool("list_recall_items", "USE FIRST when continuity feels fuzzy. List the best current handoffs, session threads, chat snippets, and memory entries before opening one exact item.", {
23532
+ cwd: exports_external.string().optional().describe("Optional cwd override for project-scoped recall"),
23533
+ project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)"),
23534
+ user_id: exports_external.string().optional().describe("Optional user override"),
23535
+ limit: exports_external.number().optional().describe("Max recall items to list")
23536
+ }, async (params) => {
23537
+ const result = listRecallItems(db, {
23538
+ cwd: params.cwd ?? process.cwd(),
23539
+ project_scoped: params.project_scoped,
23540
+ user_id: params.user_id ?? config2.user_id,
23541
+ current_device_id: config2.device_id,
23542
+ limit: params.limit
23543
+ });
23544
+ if (result.items.length === 0) {
23545
+ return {
23546
+ content: [
23547
+ {
23548
+ type: "text",
23549
+ text: result.project ? `No recall items found yet for project ${result.project}` : "No recall items found yet."
23550
+ }
23551
+ ]
23552
+ };
23553
+ }
23554
+ const projectLine = result.project ? `Project: ${result.project}
23535
23555
  ` : "";
23536
- const rows = result.items.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}${item.source_agent ? ` · ${item.source_agent}` : ""}] ${item.title}${item.source_device_id ? ` (${item.source_device_id})` : ""}
23556
+ const rows = result.items.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}${item.source_agent ? ` · ${item.source_agent}` : ""}] ${item.title}${item.source_device_id ? ` (${item.source_device_id})` : ""}
23537
23557
  ${item.detail}`).join(`
23538
23558
  `);
23539
- return {
23540
- content: [
23541
- {
23542
- type: "text",
23543
- text: `${projectLine}Recall index (${result.continuity_mode} mode):
23544
- ` + `${rows}
23545
-
23546
- ` + `Suggested next step: use load_handoff for handoff:* items, get_observations([...]) for obs:* items, or resume_thread when you want one merged resume point.`
23547
- }
23548
- ]
23549
- };
23550
- });
23551
- server.tool("load_recall_item", "USE AFTER list_recall_items. Load one exact recall item key so you can inspect a specific handoff, thread, chat message, or memory entry without fuzzy recall guessing.", {
23552
- key: exports_external.string().describe("Exact recall key from list_recall_items, such as handoff:12, session:sess-1, chat:55, or obs:402"),
23553
- cwd: exports_external.string().optional().describe("Optional cwd override"),
23554
- user_id: exports_external.string().optional().describe("Optional user override")
23555
- }, async (params) => {
23556
- const result = loadRecallItem(db, {
23557
- key: params.key,
23558
- cwd: params.cwd ?? process.cwd(),
23559
- user_id: params.user_id ?? config2.user_id,
23560
- current_device_id: config2.device_id
23561
- });
23562
- if (!result.payload) {
23563
23559
  return {
23564
23560
  content: [
23565
23561
  {
23566
23562
  type: "text",
23567
- text: `No recall item found for ${params.key}`
23563
+ text: `${projectLine}Recall index (${result.continuity_mode} mode):
23564
+ ` + `${rows}
23565
+
23566
+ ` + `Suggested next step: use load_handoff for handoff:* items, get_observations([...]) for obs:* items, or resume_thread when you want one merged resume point.`
23568
23567
  }
23569
23568
  ]
23570
23569
  };
23571
- }
23572
- if (result.payload.type === "handoff") {
23573
- return {
23574
- content: [
23575
- {
23576
- type: "text",
23577
- text: `Recall item ${result.key} [handoff]
23570
+ });
23571
+ server.tool("load_recall_item", "USE AFTER list_recall_items. Load one exact recall item key so you can inspect a specific handoff, thread, chat message, or memory entry without fuzzy recall guessing.", {
23572
+ key: exports_external.string().describe("Exact recall key from list_recall_items, such as handoff:12, session:sess-1, chat:55, or obs:402"),
23573
+ cwd: exports_external.string().optional().describe("Optional cwd override"),
23574
+ user_id: exports_external.string().optional().describe("Optional user override")
23575
+ }, async (params) => {
23576
+ const result = loadRecallItem(db, {
23577
+ key: params.key,
23578
+ cwd: params.cwd ?? process.cwd(),
23579
+ user_id: params.user_id ?? config2.user_id,
23580
+ current_device_id: config2.device_id
23581
+ });
23582
+ if (!result.payload) {
23583
+ return {
23584
+ content: [
23585
+ {
23586
+ type: "text",
23587
+ text: `No recall item found for ${params.key}`
23588
+ }
23589
+ ]
23590
+ };
23591
+ }
23592
+ if (result.payload.type === "handoff") {
23593
+ return {
23594
+ content: [
23595
+ {
23596
+ type: "text",
23597
+ text: `Recall item ${result.key} [handoff]
23578
23598
  ` + `Title: ${result.title}
23579
23599
  ` + `Session: ${result.session_id ?? "(unknown)"}
23580
23600
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23581
23601
  ` + `Agent: ${result.source_agent ?? "(unknown)"}
23582
23602
 
23583
23603
  ` + `${result.payload.narrative ?? "(no narrative)"}`
23584
- }
23585
- ]
23586
- };
23587
- }
23588
- if (result.payload.type === "thread") {
23589
- const outcomes = result.payload.recent_outcomes.length > 0 ? result.payload.recent_outcomes.map((item) => `- ${item}`).join(`
23604
+ }
23605
+ ]
23606
+ };
23607
+ }
23608
+ if (result.payload.type === "thread") {
23609
+ const outcomes = result.payload.recent_outcomes.length > 0 ? result.payload.recent_outcomes.map((item) => `- ${item}`).join(`
23590
23610
  `) : "- (none)";
23591
- const hotFiles = result.payload.hot_files.length > 0 ? result.payload.hot_files.map((item) => `- ${item.path}${item.count > 1 ? ` (${item.count})` : ""}`).join(`
23611
+ const hotFiles = result.payload.hot_files.length > 0 ? result.payload.hot_files.map((item) => `- ${item.path}${item.count > 1 ? ` (${item.count})` : ""}`).join(`
23592
23612
  `) : "- (none)";
23593
- return {
23594
- content: [
23595
- {
23596
- type: "text",
23597
- text: `Recall item ${result.key} [thread]
23613
+ return {
23614
+ content: [
23615
+ {
23616
+ type: "text",
23617
+ text: `Recall item ${result.key} [thread]
23598
23618
  ` + `Title: ${result.title}
23599
23619
  ` + `Session: ${result.session_id ?? "(unknown)"}
23600
23620
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
@@ -23607,31 +23627,31 @@ ${outcomes}
23607
23627
 
23608
23628
  ` + `Hot files:
23609
23629
  ${hotFiles}`
23610
- }
23611
- ]
23612
- };
23613
- }
23614
- if (result.payload.type === "chat") {
23615
- return {
23616
- content: [
23617
- {
23618
- type: "text",
23619
- text: `Recall item ${result.key} [chat]
23630
+ }
23631
+ ]
23632
+ };
23633
+ }
23634
+ if (result.payload.type === "chat") {
23635
+ return {
23636
+ content: [
23637
+ {
23638
+ type: "text",
23639
+ text: `Recall item ${result.key} [chat]
23620
23640
  ` + `Title: ${result.title}
23621
23641
  ` + `Session: ${result.session_id ?? "(unknown)"}
23622
23642
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
23623
23643
  ` + `Agent: ${result.source_agent ?? "(unknown)"}
23624
23644
 
23625
23645
  ` + `${result.payload.content}`
23626
- }
23627
- ]
23628
- };
23629
- }
23630
- return {
23631
- content: [
23632
- {
23633
- type: "text",
23634
- text: `Recall item ${result.key} [memory]
23646
+ }
23647
+ ]
23648
+ };
23649
+ }
23650
+ return {
23651
+ content: [
23652
+ {
23653
+ type: "text",
23654
+ text: `Recall item ${result.key} [memory]
23635
23655
  ` + `Title: ${result.title}
23636
23656
  ` + `Session: ${result.session_id ?? "(unknown)"}
23637
23657
  ` + `Source: ${result.source_device_id ?? "(unknown)"}
@@ -23639,63 +23659,63 @@ ${hotFiles}`
23639
23659
  ` + `Type: ${result.payload.observation_type}
23640
23660
 
23641
23661
  ` + `${result.payload.narrative ?? result.payload.facts ?? "(no detail)"}`
23642
- }
23643
- ]
23644
- };
23645
- });
23646
- server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?' answer. Build a clear resume point for the current project by combining handoff, live recall, current thread, and recent chat continuity.", {
23647
- cwd: exports_external.string().optional().describe("Optional cwd override for the project to resume"),
23648
- limit: exports_external.number().optional().describe("Max recall hits/chat snippets to include"),
23649
- agent: exports_external.string().optional().describe("Optional agent to resume specifically, such as claude-code or codex-cli"),
23650
- user_id: exports_external.string().optional().describe("Optional user override"),
23651
- repair_if_needed: exports_external.boolean().optional().describe("If true, attempt recall repair before resuming when continuity is still weak")
23652
- }, async (params) => {
23653
- const result = await resumeThread(db, config2, {
23654
- cwd: params.cwd ?? process.cwd(),
23655
- limit: params.limit,
23656
- agent: params.agent,
23657
- user_id: params.user_id ?? config2.user_id,
23658
- current_device_id: config2.device_id,
23659
- repair_if_needed: params.repair_if_needed
23660
- });
23661
- const projectLine = result.project_name ? `Project: ${result.project_name}
23662
+ }
23663
+ ]
23664
+ };
23665
+ });
23666
+ server.tool("resume_thread", "USE FIRST when you want one direct 'where were we?' answer. Build a clear resume point for the current project by combining handoff, live recall, current thread, and recent chat continuity.", {
23667
+ cwd: exports_external.string().optional().describe("Optional cwd override for the project to resume"),
23668
+ limit: exports_external.number().optional().describe("Max recall hits/chat snippets to include"),
23669
+ agent: exports_external.string().optional().describe("Optional agent to resume specifically, such as claude-code or codex-cli"),
23670
+ user_id: exports_external.string().optional().describe("Optional user override"),
23671
+ repair_if_needed: exports_external.boolean().optional().describe("If true, attempt recall repair before resuming when continuity is still weak")
23672
+ }, async (params) => {
23673
+ const result = await resumeThread(db, config2, {
23674
+ cwd: params.cwd ?? process.cwd(),
23675
+ limit: params.limit,
23676
+ agent: params.agent,
23677
+ user_id: params.user_id ?? config2.user_id,
23678
+ current_device_id: config2.device_id,
23679
+ repair_if_needed: params.repair_if_needed
23680
+ });
23681
+ const projectLine = result.project_name ? `Project: ${result.project_name}
23662
23682
  ` : "";
23663
- const handoffLine = result.handoff ? `Handoff: #${result.handoff.id} ${result.handoff.title}${result.handoff.source ? ` (${result.handoff.source})` : ""}
23683
+ const handoffLine = result.handoff ? `Handoff: #${result.handoff.id} ${result.handoff.title}${result.handoff.source ? ` (${result.handoff.source})` : ""}
23664
23684
  ` : `Handoff: (none)
23665
23685
  `;
23666
- const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23686
+ const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
23667
23687
  ` : "";
23668
- const basisLines = result.resume_basis.length > 0 ? result.resume_basis.map((item) => `- ${item}`).join(`
23688
+ const basisLines = result.resume_basis.length > 0 ? result.resume_basis.map((item) => `- ${item}`).join(`
23669
23689
  `) : "- (none)";
23670
- const toolTrailLines = result.tool_trail.length > 0 ? result.tool_trail.map((item) => `- ${item}`).join(`
23690
+ const toolTrailLines = result.tool_trail.length > 0 ? result.tool_trail.map((item) => `- ${item}`).join(`
23671
23691
  `) : "- (none)";
23672
- const hotFileLines = result.hot_files.length > 0 ? result.hot_files.map((item) => `- ${item.path}${item.count > 1 ? ` (${item.count})` : ""}`).join(`
23692
+ const hotFileLines = result.hot_files.length > 0 ? result.hot_files.map((item) => `- ${item.path}${item.count > 1 ? ` (${item.count})` : ""}`).join(`
23673
23693
  `) : "- (none)";
23674
- const nextActionLines = result.next_actions.length > 0 ? result.next_actions.map((item) => `- ${item}`).join(`
23694
+ const nextActionLines = result.next_actions.length > 0 ? result.next_actions.map((item) => `- ${item}`).join(`
23675
23695
  `) : "- (none)";
23676
- const repairLine = result.repair_attempted ? `Recall repair: attempted${result.repair_result ? ` · imported ${result.repair_result.imported_chat_messages} chat across ${result.repair_result.sessions_with_imports} session(s)` : ""}
23696
+ const repairLine = result.repair_attempted ? `Recall repair: attempted${result.repair_result ? ` · imported ${result.repair_result.imported_chat_messages} chat across ${result.repair_result.sessions_with_imports} session(s)` : ""}
23677
23697
  ` : "";
23678
- const outcomes = result.recent_outcomes.length > 0 ? result.recent_outcomes.map((item) => `- ${item}`).join(`
23698
+ const outcomes = result.recent_outcomes.length > 0 ? result.recent_outcomes.map((item) => `- ${item}`).join(`
23679
23699
  `) : "- (none)";
23680
- const chatLines = result.recent_chat.length > 0 ? result.recent_chat.map((item) => `- [${item.role}] [${item.source}] ${item.content.slice(0, 180)}`).join(`
23700
+ const chatLines = result.recent_chat.length > 0 ? result.recent_chat.map((item) => `- [${item.role}] [${item.source}] ${item.content.slice(0, 180)}`).join(`
23681
23701
  `) : "- (none)";
23682
- const recallLines = result.recall_hits.length > 0 ? result.recall_hits.map((item) => {
23683
- const bits = [item.kind];
23684
- if (item.role)
23685
- bits.push(item.role);
23686
- if (item.source_kind)
23687
- bits.push(item.source_kind);
23688
- if (item.type)
23689
- bits.push(item.type);
23690
- return `- [${bits.join(" · ")}] ${item.title}
23702
+ const recallLines = result.recall_hits.length > 0 ? result.recall_hits.map((item) => {
23703
+ const bits = [item.kind];
23704
+ if (item.role)
23705
+ bits.push(item.role);
23706
+ if (item.source_kind)
23707
+ bits.push(item.source_kind);
23708
+ if (item.type)
23709
+ bits.push(item.type);
23710
+ return `- [${bits.join(" · ")}] ${item.title}
23691
23711
  ${item.detail.slice(0, 200)}`;
23692
- }).join(`
23712
+ }).join(`
23693
23713
  `) : "- (none)";
23694
- return {
23695
- content: [
23696
- {
23697
- type: "text",
23698
- text: `${projectLine}` + `${result.target_agent ? `Target agent: ${result.target_agent}
23714
+ return {
23715
+ content: [
23716
+ {
23717
+ type: "text",
23718
+ text: `${projectLine}` + `${result.target_agent ? `Target agent: ${result.target_agent}
23699
23719
  ` : ""}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
23700
23720
  ` + `Freshness: ${result.resume_freshness}
23701
23721
  ` + `Source: ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
@@ -23725,254 +23745,254 @@ ${chatLines}
23725
23745
 
23726
23746
  ` + `Recall hits:
23727
23747
  ${recallLines}`
23728
- }
23729
- ]
23730
- };
23731
- });
23732
- server.tool("get_observations", "Get observations by ID", {
23733
- ids: exports_external.array(exports_external.number()).describe("Observation IDs")
23734
- }, async (params) => {
23735
- const result = getObservations(db, {
23736
- ...params,
23737
- user_id: config2.user_id
23738
- });
23739
- if (result.observations.length === 0) {
23740
- return {
23741
- content: [
23742
- {
23743
- type: "text",
23744
- text: `No observations found for IDs: ${params.ids.join(", ")}`
23745
23748
  }
23746
23749
  ]
23747
23750
  };
23748
- }
23749
- const formatted = result.observations.map((obs) => {
23750
- const parts = [
23751
- `## Observation #${obs.id}`,
23752
- `**Type**: ${obs.type} | **Quality**: ${obs.quality.toFixed(2)} | **Lifecycle**: ${obs.lifecycle}`,
23753
- `**Title**: ${obs.title}`
23754
- ];
23755
- if (obs.narrative)
23756
- parts.push(`**Narrative**: ${obs.narrative}`);
23757
- if (obs.facts)
23758
- parts.push(`**Facts**: ${obs.facts}`);
23759
- if (obs.concepts)
23760
- parts.push(`**Concepts**: ${obs.concepts}`);
23761
- if (obs.files_modified)
23762
- parts.push(`**Files modified**: ${obs.files_modified}`);
23763
- if (obs.files_read)
23764
- parts.push(`**Files read**: ${obs.files_read}`);
23765
- parts.push(`**Created**: ${obs.created_at}`);
23766
- return parts.join(`
23767
- `);
23768
23751
  });
23769
- let text = formatted.join(`
23752
+ server.tool("get_observations", "Get observations by ID", {
23753
+ ids: exports_external.array(exports_external.number()).describe("Observation IDs")
23754
+ }, async (params) => {
23755
+ const result = getObservations(db, {
23756
+ ...params,
23757
+ user_id: config2.user_id
23758
+ });
23759
+ if (result.observations.length === 0) {
23760
+ return {
23761
+ content: [
23762
+ {
23763
+ type: "text",
23764
+ text: `No observations found for IDs: ${params.ids.join(", ")}`
23765
+ }
23766
+ ]
23767
+ };
23768
+ }
23769
+ const formatted = result.observations.map((obs) => {
23770
+ const parts = [
23771
+ `## Observation #${obs.id}`,
23772
+ `**Type**: ${obs.type} | **Quality**: ${obs.quality.toFixed(2)} | **Lifecycle**: ${obs.lifecycle}`,
23773
+ `**Title**: ${obs.title}`
23774
+ ];
23775
+ if (obs.narrative)
23776
+ parts.push(`**Narrative**: ${obs.narrative}`);
23777
+ if (obs.facts)
23778
+ parts.push(`**Facts**: ${obs.facts}`);
23779
+ if (obs.concepts)
23780
+ parts.push(`**Concepts**: ${obs.concepts}`);
23781
+ if (obs.files_modified)
23782
+ parts.push(`**Files modified**: ${obs.files_modified}`);
23783
+ if (obs.files_read)
23784
+ parts.push(`**Files read**: ${obs.files_read}`);
23785
+ parts.push(`**Created**: ${obs.created_at}`);
23786
+ return parts.join(`
23787
+ `);
23788
+ });
23789
+ let text = formatted.join(`
23770
23790
 
23771
23791
  ---
23772
23792
 
23773
23793
  `);
23774
- if (result.not_found.length > 0) {
23775
- text += `
23794
+ if (result.not_found.length > 0) {
23795
+ text += `
23776
23796
 
23777
23797
  Not found: ${result.not_found.join(", ")}`;
23778
- }
23779
- return {
23780
- content: [{ type: "text", text }]
23781
- };
23782
- });
23783
- server.tool("timeline", "Timeline around an observation", {
23784
- anchor: exports_external.number().describe("Observation ID to centre on"),
23785
- depth_before: exports_external.number().optional().describe("Before anchor (default: 3)"),
23786
- depth_after: exports_external.number().optional().describe("After anchor (default: 3)"),
23787
- project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)")
23788
- }, async (params) => {
23789
- const result = getTimeline(db, {
23790
- anchor_id: params.anchor,
23791
- depth_before: params.depth_before,
23792
- depth_after: params.depth_after,
23793
- project_scoped: params.project_scoped,
23794
- user_id: config2.user_id
23795
- });
23796
- if (result.observations.length === 0) {
23798
+ }
23797
23799
  return {
23798
- content: [
23799
- {
23800
- type: "text",
23801
- text: `Observation #${params.anchor} not found`
23802
- }
23803
- ]
23800
+ content: [{ type: "text", text }]
23804
23801
  };
23805
- }
23806
- const lines = result.observations.map((obs, i) => {
23807
- const marker = i === result.anchor_index ? "→" : " ";
23808
- const date5 = obs.created_at.split("T")[0];
23809
- return `${marker} #${obs.id} [${date5}] ${obs.type}: ${obs.title}`;
23810
23802
  });
23811
- const projectLine = result.project ? `Project: ${result.project}
23803
+ server.tool("timeline", "Timeline around an observation", {
23804
+ anchor: exports_external.number().describe("Observation ID to centre on"),
23805
+ depth_before: exports_external.number().optional().describe("Before anchor (default: 3)"),
23806
+ depth_after: exports_external.number().optional().describe("After anchor (default: 3)"),
23807
+ project_scoped: exports_external.boolean().optional().describe("Scope to project (default: true)")
23808
+ }, async (params) => {
23809
+ const result = getTimeline(db, {
23810
+ anchor_id: params.anchor,
23811
+ depth_before: params.depth_before,
23812
+ depth_after: params.depth_after,
23813
+ project_scoped: params.project_scoped,
23814
+ user_id: config2.user_id
23815
+ });
23816
+ if (result.observations.length === 0) {
23817
+ return {
23818
+ content: [
23819
+ {
23820
+ type: "text",
23821
+ text: `Observation #${params.anchor} not found`
23822
+ }
23823
+ ]
23824
+ };
23825
+ }
23826
+ const lines = result.observations.map((obs, i) => {
23827
+ const marker = i === result.anchor_index ? "→" : " ";
23828
+ const date5 = obs.created_at.split("T")[0];
23829
+ return `${marker} #${obs.id} [${date5}] ${obs.type}: ${obs.title}`;
23830
+ });
23831
+ const projectLine = result.project ? `Project: ${result.project}
23812
23832
  ` : "";
23813
- const promptSection = result.session_prompts && result.session_prompts.length > 0 ? `
23833
+ const promptSection = result.session_prompts && result.session_prompts.length > 0 ? `
23814
23834
 
23815
23835
  Session requests:
23816
23836
  ${result.session_prompts.map((prompt) => `- #${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`).join(`
23817
23837
  `)}` : "";
23818
- const toolSection = result.session_tool_events && result.session_tool_events.length > 0 ? `
23838
+ const toolSection = result.session_tool_events && result.session_tool_events.length > 0 ? `
23819
23839
 
23820
23840
  Session tools:
23821
23841
  ${result.session_tool_events.slice(-8).map((tool) => {
23822
- const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
23823
- return `- ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
23824
- }).join(`
23842
+ const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
23843
+ return `- ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
23844
+ }).join(`
23825
23845
  `)}` : "";
23826
- return {
23827
- content: [
23828
- {
23829
- type: "text",
23830
- text: `${projectLine}Timeline around #${params.anchor}:
23846
+ return {
23847
+ content: [
23848
+ {
23849
+ type: "text",
23850
+ text: `${projectLine}Timeline around #${params.anchor}:
23831
23851
 
23832
23852
  ${lines.join(`
23833
23853
  `)}${promptSection}${toolSection}`
23834
- }
23835
- ]
23836
- };
23837
- });
23838
- server.tool("pin_observation", "Pin/unpin observation", {
23839
- id: exports_external.number().describe("Observation ID"),
23840
- pinned: exports_external.boolean().describe("true=pin, false=unpin")
23841
- }, async (params) => {
23842
- const result = pinObservation(db, params);
23843
- return {
23844
- content: [
23845
- {
23846
- type: "text",
23847
- text: result.success ? `Observation #${params.id} ${params.pinned ? "pinned" : "unpinned"}` : `Failed: ${result.reason}`
23848
- }
23849
- ]
23850
- };
23851
- });
23852
- server.tool("check_messages", "Check for messages sent from other devices or sessions. Messages are cross-device notes left by you or your team.", {
23853
- mark_read: exports_external.boolean().optional().describe("Mark messages as read after viewing (default: true)")
23854
- }, async (params) => {
23855
- const markRead = params.mark_read !== false;
23856
- const readKey = `messages_read_${config2.device_id}`;
23857
- const lastReadId = parseInt(db.getSyncState(readKey) ?? "0", 10);
23858
- const messages = getUnreadInboxMessages(db, config2.device_id, config2.user_id, lastReadId, 20);
23859
- if (messages.length === 0) {
23854
+ }
23855
+ ]
23856
+ };
23857
+ });
23858
+ server.tool("pin_observation", "Pin/unpin observation", {
23859
+ id: exports_external.number().describe("Observation ID"),
23860
+ pinned: exports_external.boolean().describe("true=pin, false=unpin")
23861
+ }, async (params) => {
23862
+ const result = pinObservation(db, params);
23860
23863
  return {
23861
- content: [{ type: "text", text: "No new messages." }]
23864
+ content: [
23865
+ {
23866
+ type: "text",
23867
+ text: result.success ? `Observation #${params.id} ${params.pinned ? "pinned" : "unpinned"}` : `Failed: ${result.reason}`
23868
+ }
23869
+ ]
23862
23870
  };
23863
- }
23864
- if (markRead && messages.length > 0) {
23865
- const maxId = Math.max(...messages.map((m) => m.id));
23866
- db.setSyncState(readKey, String(maxId));
23867
- }
23868
- const lines = messages.map((m) => {
23869
- const from = m.device_id === config2.device_id ? "you (this device)" : m.device_id;
23870
- const ago = formatTimeAgo(m.created_at);
23871
- return `[${ago}] from ${from}:
23871
+ });
23872
+ server.tool("check_messages", "Check for messages sent from other devices or sessions. Messages are cross-device notes left by you or your team.", {
23873
+ mark_read: exports_external.boolean().optional().describe("Mark messages as read after viewing (default: true)")
23874
+ }, async (params) => {
23875
+ const markRead = params.mark_read !== false;
23876
+ const readKey = `messages_read_${config2.device_id}`;
23877
+ const lastReadId = parseInt(db.getSyncState(readKey) ?? "0", 10);
23878
+ const messages = getUnreadInboxMessages(db, config2.device_id, config2.user_id, lastReadId, 20);
23879
+ if (messages.length === 0) {
23880
+ return {
23881
+ content: [{ type: "text", text: "No new messages." }]
23882
+ };
23883
+ }
23884
+ if (markRead && messages.length > 0) {
23885
+ const maxId = Math.max(...messages.map((m) => m.id));
23886
+ db.setSyncState(readKey, String(maxId));
23887
+ }
23888
+ const lines = messages.map((m) => {
23889
+ const from = m.device_id === config2.device_id ? "you (this device)" : m.device_id;
23890
+ const ago = formatTimeAgo(m.created_at);
23891
+ return `[${ago}] from ${from}:
23872
23892
  ${m.title}${m.narrative ? `
23873
23893
  ` + m.narrative : ""}`;
23874
- });
23875
- return {
23876
- content: [{
23877
- type: "text",
23878
- text: `${messages.length} message(s):
23894
+ });
23895
+ return {
23896
+ content: [{
23897
+ type: "text",
23898
+ text: `${messages.length} message(s):
23879
23899
 
23880
23900
  ${lines.join(`
23881
23901
 
23882
23902
  `)}`
23883
- }]
23884
- };
23885
- });
23886
- server.tool("send_message", "Leave a cross-device or team note in Engrm's shared inbox", {
23887
- title: exports_external.string().describe("Short message title"),
23888
- narrative: exports_external.string().optional().describe("Optional message body"),
23889
- concepts: exports_external.array(exports_external.string()).optional().describe("Optional tags"),
23890
- session_id: exports_external.string().optional()
23891
- }, async (params) => {
23892
- const result = await sendMessage(db, config2, {
23893
- ...params,
23894
- cwd: process.cwd()
23895
- });
23896
- return {
23897
- content: [
23898
- {
23899
- type: "text",
23900
- text: result.success ? `Message saved as observation #${result.observation_id}` : `Failed: ${result.reason}`
23901
- }
23902
- ]
23903
- };
23904
- });
23905
- server.tool("recent_activity", "Inspect the most recent observations, notes, and handoffs captured by Engrm", {
23906
- limit: exports_external.number().optional().describe("Max observations to return (default: 10)"),
23907
- project_scoped: exports_external.boolean().optional().describe("Scope to current project (default: true)"),
23908
- type: exports_external.string().optional().describe("Optional observation type filter")
23909
- }, async (params) => {
23910
- const result = getRecentActivity(db, {
23911
- ...params,
23912
- cwd: process.cwd(),
23913
- user_id: config2.user_id
23903
+ }]
23904
+ };
23914
23905
  });
23915
- if (result.observations.length === 0) {
23906
+ server.tool("send_message", "Leave a cross-device or team note in Engrm's shared inbox", {
23907
+ title: exports_external.string().describe("Short message title"),
23908
+ narrative: exports_external.string().optional().describe("Optional message body"),
23909
+ concepts: exports_external.array(exports_external.string()).optional().describe("Optional tags"),
23910
+ session_id: exports_external.string().optional()
23911
+ }, async (params) => {
23912
+ const result = await sendMessage(db, config2, {
23913
+ ...params,
23914
+ cwd: process.cwd()
23915
+ });
23916
23916
  return {
23917
23917
  content: [
23918
23918
  {
23919
23919
  type: "text",
23920
- text: result.project ? `No recent observations found in project ${result.project}.` : "No recent observations found."
23920
+ text: result.success ? `Message saved as observation #${result.observation_id}` : `Failed: ${result.reason}`
23921
23921
  }
23922
23922
  ]
23923
23923
  };
23924
- }
23925
- const showProject = !result.project;
23926
- const header = showProject ? "| ID | Project | Type | Title | Created |" : "| ID | Type | Title | Created |";
23927
- const separator = showProject ? "|---|---|---|---|---|" : "|---|---|---|---|";
23928
- const displayType = (obs) => {
23929
- if (obs.message_kind === "draft-handoff")
23930
- return "handoff:draft";
23931
- if (obs.message_kind === "handoff")
23932
- return "handoff";
23933
- if (obs.message_kind === "inbox-note")
23934
- return "note";
23935
- return obs.type;
23936
- };
23937
- const rows = result.observations.map((obs) => {
23938
- const date5 = obs.created_at.split("T")[0];
23939
- if (showProject) {
23940
- return `| ${obs.id} | ${obs.project_name ?? "(unknown)"} | ${displayType(obs)} | ${obs.title} | ${date5} |`;
23941
- }
23942
- return `| ${obs.id} | ${displayType(obs)} | ${obs.title} | ${date5} |`;
23943
23924
  });
23944
- const projectLine = result.project ? `Project: ${result.project}
23925
+ server.tool("recent_activity", "Inspect the most recent observations, notes, and handoffs captured by Engrm", {
23926
+ limit: exports_external.number().optional().describe("Max observations to return (default: 10)"),
23927
+ project_scoped: exports_external.boolean().optional().describe("Scope to current project (default: true)"),
23928
+ type: exports_external.string().optional().describe("Optional observation type filter")
23929
+ }, async (params) => {
23930
+ const result = getRecentActivity(db, {
23931
+ ...params,
23932
+ cwd: process.cwd(),
23933
+ user_id: config2.user_id
23934
+ });
23935
+ if (result.observations.length === 0) {
23936
+ return {
23937
+ content: [
23938
+ {
23939
+ type: "text",
23940
+ text: result.project ? `No recent observations found in project ${result.project}.` : "No recent observations found."
23941
+ }
23942
+ ]
23943
+ };
23944
+ }
23945
+ const showProject = !result.project;
23946
+ const header = showProject ? "| ID | Project | Type | Title | Created |" : "| ID | Type | Title | Created |";
23947
+ const separator = showProject ? "|---|---|---|---|---|" : "|---|---|---|---|";
23948
+ const displayType = (obs) => {
23949
+ if (obs.message_kind === "draft-handoff")
23950
+ return "handoff:draft";
23951
+ if (obs.message_kind === "handoff")
23952
+ return "handoff";
23953
+ if (obs.message_kind === "inbox-note")
23954
+ return "note";
23955
+ return obs.type;
23956
+ };
23957
+ const rows = result.observations.map((obs) => {
23958
+ const date5 = obs.created_at.split("T")[0];
23959
+ if (showProject) {
23960
+ return `| ${obs.id} | ${obs.project_name ?? "(unknown)"} | ${displayType(obs)} | ${obs.title} | ${date5} |`;
23961
+ }
23962
+ return `| ${obs.id} | ${displayType(obs)} | ${obs.title} | ${date5} |`;
23963
+ });
23964
+ const projectLine = result.project ? `Project: ${result.project}
23945
23965
  ` : "";
23946
- return {
23947
- content: [
23948
- {
23949
- type: "text",
23950
- text: `${projectLine}Recent activity:
23966
+ return {
23967
+ content: [
23968
+ {
23969
+ type: "text",
23970
+ text: `${projectLine}Recent activity:
23951
23971
 
23952
23972
  ${header}
23953
23973
  ${separator}
23954
23974
  ${rows.join(`
23955
23975
  `)}`
23956
- }
23957
- ]
23958
- };
23959
- });
23960
- server.tool("memory_stats", "Show high-level Engrm capture and sync statistics", {}, async () => {
23961
- const stats = getMemoryStats(db);
23962
- const packs = stats.installed_packs.length > 0 ? stats.installed_packs.join(", ") : "(none)";
23963
- const recentRequests = stats.recent_requests.length > 0 ? stats.recent_requests.map((item) => `- ${item}`).join(`
23976
+ }
23977
+ ]
23978
+ };
23979
+ });
23980
+ server.tool("memory_stats", "Show high-level Engrm capture and sync statistics", {}, async () => {
23981
+ const stats = getMemoryStats(db);
23982
+ const packs = stats.installed_packs.length > 0 ? stats.installed_packs.join(", ") : "(none)";
23983
+ const recentRequests = stats.recent_requests.length > 0 ? stats.recent_requests.map((item) => `- ${item}`).join(`
23964
23984
  `) : "- (none)";
23965
- const recentLessons = stats.recent_lessons.length > 0 ? stats.recent_lessons.map((item) => `- ${item}`).join(`
23985
+ const recentLessons = stats.recent_lessons.length > 0 ? stats.recent_lessons.map((item) => `- ${item}`).join(`
23966
23986
  `) : "- (none)";
23967
- const recentCompleted = stats.recent_completed.length > 0 ? stats.recent_completed.map((item) => `- ${item}`).join(`
23987
+ const recentCompleted = stats.recent_completed.length > 0 ? stats.recent_completed.map((item) => `- ${item}`).join(`
23968
23988
  `) : "- (none)";
23969
- const nextSteps = stats.next_steps.length > 0 ? stats.next_steps.map((item) => `- ${item}`).join(`
23989
+ const nextSteps = stats.next_steps.length > 0 ? stats.next_steps.map((item) => `- ${item}`).join(`
23970
23990
  `) : "- (none)";
23971
- return {
23972
- content: [
23973
- {
23974
- type: "text",
23975
- text: `Active observations: ${stats.active_observations}
23991
+ return {
23992
+ content: [
23993
+ {
23994
+ type: "text",
23995
+ text: `Active observations: ${stats.active_observations}
23976
23996
  ` + `User prompts: ${stats.user_prompts}
23977
23997
  ` + `Tool events: ${stats.tool_events}
23978
23998
  ` + `Inbox notes: ${stats.inbox_messages}
@@ -23993,72 +24013,72 @@ ${recentCompleted}
23993
24013
 
23994
24014
  ` + `Next steps:
23995
24015
  ${nextSteps}`
23996
- }
23997
- ]
23998
- };
23999
- });
24000
- server.tool("memory_console", "Show a high-signal local overview of what Engrm currently knows about this project", {
24001
- cwd: exports_external.string().optional(),
24002
- project_scoped: exports_external.boolean().optional(),
24003
- user_id: exports_external.string().optional()
24004
- }, async (params) => {
24005
- const result = getMemoryConsole(db, {
24006
- ...params,
24007
- cwd: params.cwd ?? process.cwd(),
24008
- user_id: params.user_id ?? config2.user_id
24016
+ }
24017
+ ]
24018
+ };
24009
24019
  });
24010
- const sessionLines = result.sessions.length > 0 ? result.sessions.map((session) => {
24011
- const label = session.request ?? session.completed ?? "(no summary)";
24012
- return `- ${session.session_id} :: ${label.replace(/\s+/g, " ").trim()}`;
24013
- }).join(`
24020
+ server.tool("memory_console", "Show a high-signal local overview of what Engrm currently knows about this project", {
24021
+ cwd: exports_external.string().optional(),
24022
+ project_scoped: exports_external.boolean().optional(),
24023
+ user_id: exports_external.string().optional()
24024
+ }, async (params) => {
24025
+ const result = getMemoryConsole(db, {
24026
+ ...params,
24027
+ cwd: params.cwd ?? process.cwd(),
24028
+ user_id: params.user_id ?? config2.user_id
24029
+ });
24030
+ const sessionLines = result.sessions.length > 0 ? result.sessions.map((session) => {
24031
+ const label = session.request ?? session.completed ?? "(no summary)";
24032
+ return `- ${session.session_id} :: ${label.replace(/\s+/g, " ").trim()}`;
24033
+ }).join(`
24014
24034
  `) : "- (none)";
24015
- const requestLines = result.requests.length > 0 ? result.requests.map((prompt) => `- #${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`).join(`
24035
+ const requestLines = result.requests.length > 0 ? result.requests.map((prompt) => `- #${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`).join(`
24016
24036
  `) : "- (none)";
24017
- const toolLines = result.tools.length > 0 ? result.tools.map((tool) => {
24018
- const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
24019
- return `- ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
24020
- }).join(`
24037
+ const toolLines = result.tools.length > 0 ? result.tools.map((tool) => {
24038
+ const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
24039
+ return `- ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
24040
+ }).join(`
24021
24041
  `) : "- (none)";
24022
- const handoffLines = result.recent_handoffs.length > 0 ? result.recent_handoffs.map((obs) => `- #${obs.id} ${obs.title}`).join(`
24042
+ const handoffLines = result.recent_handoffs.length > 0 ? result.recent_handoffs.map((obs) => `- #${obs.id} ${obs.title}`).join(`
24023
24043
  `) : "- (none)";
24024
- const inboxNoteLines = result.recent_inbox_notes.length > 0 ? result.recent_inbox_notes.map((obs) => `- #${obs.id} ${obs.title}`).join(`
24044
+ const inboxNoteLines = result.recent_inbox_notes.length > 0 ? result.recent_inbox_notes.map((obs) => `- #${obs.id} ${obs.title}`).join(`
24025
24045
  `) : "- (none)";
24026
- const recentChatLines = result.recent_chat.length > 0 ? result.recent_chat.map((msg) => `- [${msg.role}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 180)}`).join(`
24046
+ const recentChatLines = result.recent_chat.length > 0 ? result.recent_chat.map((msg) => `- [${msg.role}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 180)}`).join(`
24027
24047
  `) : "- (none)";
24028
- const observationLines = result.observations.length > 0 ? result.observations.map((obs) => {
24029
- const provenance = [];
24030
- if (obs.source_tool)
24031
- provenance.push(`via ${obs.source_tool}`);
24032
- if (typeof obs.source_prompt_number === "number")
24033
- provenance.push(`#${obs.source_prompt_number}`);
24034
- return `- #${obs.id} [${obs.type}] ${obs.title}${provenance.length ? ` (${provenance.join(" · ")})` : ""}`;
24035
- }).join(`
24048
+ const observationLines = result.observations.length > 0 ? result.observations.map((obs) => {
24049
+ const provenance = [];
24050
+ if (obs.source_tool)
24051
+ provenance.push(`via ${obs.source_tool}`);
24052
+ if (typeof obs.source_prompt_number === "number")
24053
+ provenance.push(`#${obs.source_prompt_number}`);
24054
+ return `- #${obs.id} [${obs.type}] ${obs.title}${provenance.length ? ` (${provenance.join(" · ")})` : ""}`;
24055
+ }).join(`
24036
24056
  `) : "- (none)";
24037
- const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
24057
+ const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
24038
24058
  `) : "- (none)";
24039
- const provenanceMixLines = result.provenance_type_mix.length > 0 ? result.provenance_type_mix.map((item) => `- ${item.tool}: ${item.top_types.map((entry) => `${entry.type} ${entry.count}`).join(", ")}`).join(`
24059
+ const provenanceMixLines = result.provenance_type_mix.length > 0 ? result.provenance_type_mix.map((item) => `- ${item.tool}: ${item.top_types.map((entry) => `${entry.type} ${entry.count}`).join(", ")}`).join(`
24040
24060
  `) : "- (none)";
24041
- const checkpointTypeLines = result.assistant_checkpoint_types.length > 0 ? result.assistant_checkpoint_types.map((item) => `- ${item.type}: ${item.count}`).join(`
24061
+ const checkpointTypeLines = result.assistant_checkpoint_types.length > 0 ? result.assistant_checkpoint_types.map((item) => `- ${item.type}: ${item.count}`).join(`
24042
24062
  `) : "- (none)";
24043
- const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
24063
+ const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
24044
24064
  `) : "- (none)";
24045
- const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}${item.source_agent ? ` · ${item.source_agent}` : ""}] ${item.title}`).join(`
24065
+ const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}${item.source_agent ? ` · ${item.source_agent}` : ""}] ${item.title}`).join(`
24046
24066
  `) : "- (none)";
24047
- const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
24067
+ const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
24048
24068
  ` : "";
24049
- const projectLine = result.project ? `Project: ${result.project}
24069
+ const projectLine = result.project ? `Project: ${result.project}
24050
24070
 
24051
24071
  ` : "";
24052
- const captureLine = result.capture_mode === "rich" ? `Raw chronology: active (${result.requests.length} requests, ${result.tools.length} tools)
24072
+ const captureLine = result.capture_mode === "rich" ? `Raw chronology: active (${result.requests.length} requests, ${result.tools.length} tools)
24053
24073
 
24054
24074
  ` : `Raw chronology: observations-only so far (prompt/tool hooks have not produced local history here yet)
24055
24075
 
24056
24076
  `;
24057
- return {
24058
- content: [
24059
- {
24060
- type: "text",
24061
- text: `${projectLine}` + `${captureLine}` + `${result.active_agents.length > 0 ? `Agents active: ${result.active_agents.join(", ")}
24077
+ return {
24078
+ content: [
24079
+ {
24080
+ type: "text",
24081
+ text: `${projectLine}` + `${captureLine}` + `${result.active_agents.length > 0 ? `Agents active: ${result.active_agents.join(", ")}
24062
24082
  ` : ""}` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
24063
24083
  ` + `Recall index: ${result.recall_mode} · ${result.recall_items_ready} items ready
24064
24084
  ` + `Resume readiness: ${result.resume_freshness} · ${result.resume_source_session_id ?? "(unknown session)"}${result.resume_source_device_id ? ` (${result.resume_source_device_id})` : ""}
@@ -24105,25 +24125,25 @@ ${recentChatLines}
24105
24125
 
24106
24126
  ` + `Recent observations:
24107
24127
  ${observationLines}`
24108
- }
24109
- ]
24110
- };
24111
- });
24112
- server.tool("capture_status", "Show whether Engrm hook registration and recent prompt/tool chronology capture are actually active on this machine", {
24113
- lookback_hours: exports_external.number().optional(),
24114
- user_id: exports_external.string().optional()
24115
- }, async (params) => {
24116
- const result = getCaptureStatus(db, {
24117
- lookback_hours: params.lookback_hours,
24118
- user_id: params.user_id ?? config2.user_id
24119
- });
24120
- const latestPrompt = result.latest_prompt_epoch ? new Date(result.latest_prompt_epoch * 1000).toISOString() : "none";
24121
- const latestTool = result.latest_tool_event_epoch ? new Date(result.latest_tool_event_epoch * 1000).toISOString() : "none";
24122
- return {
24123
- content: [
24124
- {
24125
- type: "text",
24126
- text: `Schema: v${result.schema_version} (${result.schema_current ? "current" : "outdated"})
24128
+ }
24129
+ ]
24130
+ };
24131
+ });
24132
+ server.tool("capture_status", "Show whether Engrm hook registration and recent prompt/tool chronology capture are actually active on this machine", {
24133
+ lookback_hours: exports_external.number().optional(),
24134
+ user_id: exports_external.string().optional()
24135
+ }, async (params) => {
24136
+ const result = getCaptureStatus(db, {
24137
+ lookback_hours: params.lookback_hours,
24138
+ user_id: params.user_id ?? config2.user_id
24139
+ });
24140
+ const latestPrompt = result.latest_prompt_epoch ? new Date(result.latest_prompt_epoch * 1000).toISOString() : "none";
24141
+ const latestTool = result.latest_tool_event_epoch ? new Date(result.latest_tool_event_epoch * 1000).toISOString() : "none";
24142
+ return {
24143
+ content: [
24144
+ {
24145
+ type: "text",
24146
+ text: `Schema: v${result.schema_version} (${result.schema_current ? "current" : "outdated"})
24127
24147
  ` + `HTTP MCP: ${result.http_enabled ? `enabled${result.http_port ? ` (:${result.http_port})` : ""}` : "disabled"}
24128
24148
  ` + `HTTP bearer tokens: ${result.http_bearer_token_count}
24129
24149
  ` + `Fleet project: ${result.fleet_project_name ?? "none"}
@@ -24136,6 +24156,8 @@ server.tool("capture_status", "Show whether Engrm hook registration and recent p
24136
24156
  ` + `Codex hooks: ${result.codex_hooks_registered ? "registered" : "missing"}
24137
24157
  ` + `Codex raw chronology: ${result.codex_raw_chronology_supported ? "supported" : "not yet supported (start/stop only)"}
24138
24158
 
24159
+ ` + `OpenClaw MCP: ${result.openclaw_mcp_registered ? "registered" : "missing"}
24160
+ ` + `OpenClaw plugin: ${result.openclaw_plugin_registered ? "installed" : "missing"}
24139
24161
  ` + `OpenCode MCP: ${result.opencode_mcp_registered ? "registered" : "missing"}
24140
24162
  ` + `OpenCode plugin: ${result.opencode_plugin_registered ? "installed" : "missing"}
24141
24163
 
@@ -24149,31 +24171,31 @@ server.tool("capture_status", "Show whether Engrm hook registration and recent p
24149
24171
  ` + `Latest PostToolUse hook: ${formatEpoch(result.latest_post_tool_hook_epoch)}
24150
24172
  ` + `Last PostToolUse parse: ${result.latest_post_tool_parse_status ?? "unknown"}
24151
24173
  ` + `Last PostToolUse tool: ${result.latest_post_tool_name ?? "unknown"}`
24152
- }
24153
- ]
24154
- };
24155
- });
24156
- server.tool("capture_quality", "Show how healthy Engrm capture is across the workspace: raw chronology coverage, checkpoints, and provenance by tool.", {
24157
- limit: exports_external.number().optional().describe("Maximum projects to include in the top-projects section."),
24158
- user_id: exports_external.string().optional().describe("Optional user override; defaults to the configured user.")
24159
- }, async (params) => {
24160
- const result = getCaptureQuality(db, {
24161
- limit: params.limit,
24162
- user_id: params.user_id ?? config2.user_id
24163
- });
24164
- const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
24174
+ }
24175
+ ]
24176
+ };
24177
+ });
24178
+ server.tool("capture_quality", "Show how healthy Engrm capture is across the workspace: raw chronology coverage, checkpoints, and provenance by tool.", {
24179
+ limit: exports_external.number().optional().describe("Maximum projects to include in the top-projects section."),
24180
+ user_id: exports_external.string().optional().describe("Optional user override; defaults to the configured user.")
24181
+ }, async (params) => {
24182
+ const result = getCaptureQuality(db, {
24183
+ limit: params.limit,
24184
+ user_id: params.user_id ?? config2.user_id
24185
+ });
24186
+ const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
24165
24187
  `) : "- (none)";
24166
- const checkpointTypeLines = result.assistant_checkpoint_types.length > 0 ? result.assistant_checkpoint_types.map((item) => `- ${item.type}: ${item.count}`).join(`
24188
+ const checkpointTypeLines = result.assistant_checkpoint_types.length > 0 ? result.assistant_checkpoint_types.map((item) => `- ${item.type}: ${item.count}`).join(`
24167
24189
  `) : "- (none)";
24168
- const provenanceMixLines = result.provenance_type_mix.length > 0 ? result.provenance_type_mix.map((item) => `- ${item.tool}: ${item.top_types.map((entry) => `${entry.type} ${entry.count}`).join(", ")}`).join(`
24190
+ const provenanceMixLines = result.provenance_type_mix.length > 0 ? result.provenance_type_mix.map((item) => `- ${item.tool}: ${item.top_types.map((entry) => `${entry.type} ${entry.count}`).join(", ")}`).join(`
24169
24191
  `) : "- (none)";
24170
- const projectLines = result.top_projects.length > 0 ? result.top_projects.map((project) => `- ${project.name} [${project.raw_capture_state}] obs=${project.observation_count} sessions=${project.session_count} prompts=${project.prompt_count} tools=${project.tool_event_count} checkpoints=${project.assistant_checkpoint_count} chat=${project.chat_message_count} (${project.chat_coverage_state})`).join(`
24192
+ const projectLines = result.top_projects.length > 0 ? result.top_projects.map((project) => `- ${project.name} [${project.raw_capture_state}] obs=${project.observation_count} sessions=${project.session_count} prompts=${project.prompt_count} tools=${project.tool_event_count} checkpoints=${project.assistant_checkpoint_count} chat=${project.chat_message_count} (${project.chat_coverage_state})`).join(`
24171
24193
  `) : "- (none)";
24172
- return {
24173
- content: [
24174
- {
24175
- type: "text",
24176
- text: `Workspace totals: projects=${result.totals.projects}, observations=${result.totals.observations}, sessions=${result.totals.sessions}, prompts=${result.totals.prompts}, tools=${result.totals.tool_events}, checkpoints=${result.totals.assistant_checkpoints}, chat=${result.totals.chat_messages}
24194
+ return {
24195
+ content: [
24196
+ {
24197
+ type: "text",
24198
+ text: `Workspace totals: projects=${result.totals.projects}, observations=${result.totals.observations}, sessions=${result.totals.sessions}, prompts=${result.totals.prompts}, tools=${result.totals.tool_events}, checkpoints=${result.totals.assistant_checkpoints}, chat=${result.totals.chat_messages}
24177
24199
 
24178
24200
  ` + `Session capture states: rich=${result.session_states.rich}, partial=${result.session_states.partial}, summary-only=${result.session_states.summary_only}, legacy=${result.session_states.legacy}
24179
24201
 
@@ -24192,126 +24214,126 @@ ${provenanceMixLines}
24192
24214
 
24193
24215
  ` + `Top projects:
24194
24216
  ${projectLines}`
24195
- }
24196
- ]
24197
- };
24198
- });
24199
- server.tool("agent_memory_index", "Compare continuity and capture health across Claude Code, Codex, OpenClaw, and other agents for the current project or workspace.", {
24200
- cwd: exports_external.string().optional().describe("Project path to inspect. Defaults to the current working directory."),
24201
- project_scoped: exports_external.boolean().optional().describe("If true, limit results to the current project instead of the whole workspace."),
24202
- user_id: exports_external.string().optional().describe("Optional user override; defaults to the configured user.")
24203
- }, async (params) => {
24204
- const result = getAgentMemoryIndex(db, {
24205
- cwd: params.cwd ?? process.cwd(),
24206
- project_scoped: params.project_scoped,
24207
- user_id: params.user_id ?? config2.user_id
24208
- });
24209
- const rows = result.agents.length > 0 ? result.agents.map((agent) => {
24210
- const lastSeen = agent.last_seen_epoch ? new Date(agent.last_seen_epoch * 1000).toISOString().replace("T", " ").slice(0, 16) : "unknown";
24211
- const latest = agent.latest_summary ? ` latest="${agent.latest_summary.replace(/\s+/g, " ").trim().slice(0, 120)}"` : "";
24212
- const devices = agent.devices.length > 0 ? ` devices=[${agent.devices.join(", ")}]` : "";
24213
- const exact = agent.best_recall_key ? ` open=load_recall_item("${agent.best_recall_key}")` : "";
24214
- return `- ${agent.agent}: continuity=${agent.continuity_state} capture=${agent.capture_state} resume=${agent.resume_freshness} chat=${agent.chat_coverage_state} sessions=${agent.session_count} prompts=${agent.prompt_count} tools=${agent.tool_event_count} obs=${agent.observation_count} handoffs=${agent.handoff_count} chat_msgs=${agent.chat_message_count} last_seen=${lastSeen}${devices}${latest}${exact} resume_call=resume_thread(agent="${agent.agent}")`;
24215
- }).join(`
24217
+ }
24218
+ ]
24219
+ };
24220
+ });
24221
+ server.tool("agent_memory_index", "Compare continuity and capture health across Claude Code, Codex, OpenClaw, and other agents for the current project or workspace.", {
24222
+ cwd: exports_external.string().optional().describe("Project path to inspect. Defaults to the current working directory."),
24223
+ project_scoped: exports_external.boolean().optional().describe("If true, limit results to the current project instead of the whole workspace."),
24224
+ user_id: exports_external.string().optional().describe("Optional user override; defaults to the configured user.")
24225
+ }, async (params) => {
24226
+ const result = getAgentMemoryIndex(db, {
24227
+ cwd: params.cwd ?? process.cwd(),
24228
+ project_scoped: params.project_scoped,
24229
+ user_id: params.user_id ?? config2.user_id
24230
+ });
24231
+ const rows = result.agents.length > 0 ? result.agents.map((agent) => {
24232
+ const lastSeen = agent.last_seen_epoch ? new Date(agent.last_seen_epoch * 1000).toISOString().replace("T", " ").slice(0, 16) : "unknown";
24233
+ const latest = agent.latest_summary ? ` latest="${agent.latest_summary.replace(/\s+/g, " ").trim().slice(0, 120)}"` : "";
24234
+ const devices = agent.devices.length > 0 ? ` devices=[${agent.devices.join(", ")}]` : "";
24235
+ const exact = agent.best_recall_key ? ` open=load_recall_item("${agent.best_recall_key}")` : "";
24236
+ return `- ${agent.agent}: continuity=${agent.continuity_state} capture=${agent.capture_state} resume=${agent.resume_freshness} chat=${agent.chat_coverage_state} sessions=${agent.session_count} prompts=${agent.prompt_count} tools=${agent.tool_event_count} obs=${agent.observation_count} handoffs=${agent.handoff_count} chat_msgs=${agent.chat_message_count} last_seen=${lastSeen}${devices}${latest}${exact} resume_call=resume_thread(agent="${agent.agent}")`;
24237
+ }).join(`
24216
24238
  `) : "- (none)";
24217
- return {
24218
- content: [
24219
- {
24220
- type: "text",
24221
- text: `${result.project ? `Project: ${result.project}
24239
+ return {
24240
+ content: [
24241
+ {
24242
+ type: "text",
24243
+ text: `${result.project ? `Project: ${result.project}
24222
24244
 
24223
24245
  ` : ""}` + `Agent memory index:
24224
24246
  ${rows}
24225
24247
 
24226
24248
  ` + `Suggested next step: ${result.suggested_tools.join(", ") || "(none)"}`
24227
- }
24228
- ]
24229
- };
24230
- });
24231
- server.tool("tool_memory_index", "Show which tools are actually producing durable memory, which plugins they exercise, and what memory types they create.", {
24232
- cwd: exports_external.string().optional().describe("Project path to inspect. Defaults to the current working directory."),
24233
- project_scoped: exports_external.boolean().optional().describe("If true, limit results to the current project instead of the whole workspace."),
24234
- limit: exports_external.number().optional().describe("Maximum tools to include."),
24235
- user_id: exports_external.string().optional().describe("Optional user override; defaults to the configured user.")
24236
- }, async (params) => {
24237
- const result = getToolMemoryIndex(db, {
24238
- cwd: params.cwd ?? process.cwd(),
24239
- project_scoped: params.project_scoped,
24240
- limit: params.limit,
24241
- user_id: params.user_id ?? config2.user_id
24242
- });
24243
- const toolLines = result.tools.length > 0 ? result.tools.map((tool) => {
24244
- const typeMix = tool.top_types.map((item) => `${item.type} ${item.count}`).join(", ");
24245
- const pluginMix = tool.top_plugins.length > 0 ? ` plugins=[${tool.top_plugins.map((item) => `${item.plugin} ${item.count}`).join(", ")}]` : "";
24246
- const sample = tool.sample_titles[0] ? ` sample="${tool.sample_titles[0]}"` : "";
24247
- const promptInfo = typeof tool.latest_prompt_number === "number" ? ` latest_prompt=#${tool.latest_prompt_number}` : "";
24248
- return `- ${tool.tool}: obs=${tool.observation_count} sessions=${tool.session_count}${promptInfo} types=[${typeMix}]${pluginMix}${sample}`;
24249
- }).join(`
24249
+ }
24250
+ ]
24251
+ };
24252
+ });
24253
+ server.tool("tool_memory_index", "Show which tools are actually producing durable memory, which plugins they exercise, and what memory types they create.", {
24254
+ cwd: exports_external.string().optional().describe("Project path to inspect. Defaults to the current working directory."),
24255
+ project_scoped: exports_external.boolean().optional().describe("If true, limit results to the current project instead of the whole workspace."),
24256
+ limit: exports_external.number().optional().describe("Maximum tools to include."),
24257
+ user_id: exports_external.string().optional().describe("Optional user override; defaults to the configured user.")
24258
+ }, async (params) => {
24259
+ const result = getToolMemoryIndex(db, {
24260
+ cwd: params.cwd ?? process.cwd(),
24261
+ project_scoped: params.project_scoped,
24262
+ limit: params.limit,
24263
+ user_id: params.user_id ?? config2.user_id
24264
+ });
24265
+ const toolLines = result.tools.length > 0 ? result.tools.map((tool) => {
24266
+ const typeMix = tool.top_types.map((item) => `${item.type} ${item.count}`).join(", ");
24267
+ const pluginMix = tool.top_plugins.length > 0 ? ` plugins=[${tool.top_plugins.map((item) => `${item.plugin} ${item.count}`).join(", ")}]` : "";
24268
+ const sample = tool.sample_titles[0] ? ` sample="${tool.sample_titles[0]}"` : "";
24269
+ const promptInfo = typeof tool.latest_prompt_number === "number" ? ` latest_prompt=#${tool.latest_prompt_number}` : "";
24270
+ return `- ${tool.tool}: obs=${tool.observation_count} sessions=${tool.session_count}${promptInfo} types=[${typeMix}]${pluginMix}${sample}`;
24271
+ }).join(`
24250
24272
  `) : "- (none)";
24251
- return {
24252
- content: [
24253
- {
24254
- type: "text",
24255
- text: `${result.project ? `Project: ${result.project}
24273
+ return {
24274
+ content: [
24275
+ {
24276
+ type: "text",
24277
+ text: `${result.project ? `Project: ${result.project}
24256
24278
 
24257
24279
  ` : ""}` + `Tools producing durable memory:
24258
24280
  ${toolLines}`
24259
- }
24260
- ]
24261
- };
24262
- });
24263
- server.tool("session_tool_memory", "Show which tools in one session produced durable memory and which tools produced none", {
24264
- session_id: exports_external.string().describe("Session ID to inspect")
24265
- }, async (params) => {
24266
- const result = getSessionToolMemory(db, params);
24267
- const toolLines = result.tools.length > 0 ? result.tools.map((tool) => {
24268
- const typeMix = tool.top_types.map((item) => `${item.type} ${item.count}`).join(", ");
24269
- const pluginMix = tool.top_plugins.length > 0 ? ` plugins=[${tool.top_plugins.map((item) => `${item.plugin} ${item.count}`).join(", ")}]` : "";
24270
- const sample = tool.sample_titles[0] ? ` sample="${tool.sample_titles[0]}"` : "";
24271
- const promptInfo = typeof tool.latest_prompt_number === "number" ? ` latest_prompt=#${tool.latest_prompt_number}` : "";
24272
- return `- ${tool.tool}: events=${tool.tool_event_count} observations=${tool.observation_count}${promptInfo} types=[${typeMix}]${pluginMix}${sample}`;
24273
- }).join(`
24281
+ }
24282
+ ]
24283
+ };
24284
+ });
24285
+ server.tool("session_tool_memory", "Show which tools in one session produced durable memory and which tools produced none", {
24286
+ session_id: exports_external.string().describe("Session ID to inspect")
24287
+ }, async (params) => {
24288
+ const result = getSessionToolMemory(db, params);
24289
+ const toolLines = result.tools.length > 0 ? result.tools.map((tool) => {
24290
+ const typeMix = tool.top_types.map((item) => `${item.type} ${item.count}`).join(", ");
24291
+ const pluginMix = tool.top_plugins.length > 0 ? ` plugins=[${tool.top_plugins.map((item) => `${item.plugin} ${item.count}`).join(", ")}]` : "";
24292
+ const sample = tool.sample_titles[0] ? ` sample="${tool.sample_titles[0]}"` : "";
24293
+ const promptInfo = typeof tool.latest_prompt_number === "number" ? ` latest_prompt=#${tool.latest_prompt_number}` : "";
24294
+ return `- ${tool.tool}: events=${tool.tool_event_count} observations=${tool.observation_count}${promptInfo} types=[${typeMix}]${pluginMix}${sample}`;
24295
+ }).join(`
24274
24296
  `) : "- (none)";
24275
- const unmappedLines = result.tools_without_memory.length > 0 ? result.tools_without_memory.map((tool) => `- ${tool.tool}: events=${tool.tool_event_count}`).join(`
24297
+ const unmappedLines = result.tools_without_memory.length > 0 ? result.tools_without_memory.map((tool) => `- ${tool.tool}: events=${tool.tool_event_count}`).join(`
24276
24298
  `) : "- (none)";
24277
- return {
24278
- content: [
24279
- {
24280
- type: "text",
24281
- text: `Session: ${result.session_id}
24299
+ return {
24300
+ content: [
24301
+ {
24302
+ type: "text",
24303
+ text: `Session: ${result.session_id}
24282
24304
 
24283
24305
  ` + `Tools producing durable memory:
24284
24306
  ${toolLines}
24285
24307
 
24286
24308
  ` + `Tools without durable memory:
24287
24309
  ${unmappedLines}`
24288
- }
24289
- ]
24290
- };
24291
- });
24292
- server.tool("session_context", "Preview the exact project memory context Engrm would inject at session start", {
24293
- cwd: exports_external.string().optional(),
24294
- token_budget: exports_external.number().optional(),
24295
- scope: exports_external.enum(["personal", "team", "all"]).optional(),
24296
- user_id: exports_external.string().optional()
24297
- }, async (params) => {
24298
- const result = getSessionContext(db, {
24299
- cwd: params.cwd ?? process.cwd(),
24300
- token_budget: params.token_budget,
24301
- scope: params.scope,
24302
- user_id: params.user_id ?? config2.user_id,
24303
- current_device_id: config2.device_id
24304
- });
24305
- if (!result) {
24306
- return {
24307
- content: [{ type: "text", text: "No session context available." }]
24310
+ }
24311
+ ]
24308
24312
  };
24309
- }
24310
- return {
24311
- content: [
24312
- {
24313
- type: "text",
24314
- text: `Project: ${result.project_name}
24313
+ });
24314
+ server.tool("session_context", "Preview the exact project memory context Engrm would inject at session start", {
24315
+ cwd: exports_external.string().optional(),
24316
+ token_budget: exports_external.number().optional(),
24317
+ scope: exports_external.enum(["personal", "team", "all"]).optional(),
24318
+ user_id: exports_external.string().optional()
24319
+ }, async (params) => {
24320
+ const result = getSessionContext(db, {
24321
+ cwd: params.cwd ?? process.cwd(),
24322
+ token_budget: params.token_budget,
24323
+ scope: params.scope,
24324
+ user_id: params.user_id ?? config2.user_id,
24325
+ current_device_id: config2.device_id
24326
+ });
24327
+ if (!result) {
24328
+ return {
24329
+ content: [{ type: "text", text: "No session context available." }]
24330
+ };
24331
+ }
24332
+ return {
24333
+ content: [
24334
+ {
24335
+ type: "text",
24336
+ text: `Project: ${result.project_name}
24315
24337
  ` + `Canonical ID: ${result.canonical_id}
24316
24338
  ` + `Agents active: ${result.active_agents.join(", ") || "(none)"}
24317
24339
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
@@ -24335,77 +24357,77 @@ server.tool("session_context", "Preview the exact project memory context Engrm w
24335
24357
  ` + `Raw chronology active: ${result.raw_capture_active ? "yes" : "no"}
24336
24358
 
24337
24359
  ` + result.preview
24338
- }
24339
- ]
24340
- };
24341
- });
24342
- server.tool("activity_feed", "Show one chronological local feed across prompts, tools, chat, observations, handoffs, and summaries", {
24343
- limit: exports_external.number().optional(),
24344
- project_scoped: exports_external.boolean().optional(),
24345
- session_id: exports_external.string().optional(),
24346
- cwd: exports_external.string().optional(),
24347
- user_id: exports_external.string().optional()
24348
- }, async (params) => {
24349
- const result = getActivityFeed(db, {
24350
- ...params,
24351
- cwd: params.cwd ?? process.cwd(),
24352
- user_id: params.user_id ?? config2.user_id
24360
+ }
24361
+ ]
24362
+ };
24353
24363
  });
24354
- const projectLine = result.project ? `Project: ${result.project}
24364
+ server.tool("activity_feed", "Show one chronological local feed across prompts, tools, chat, observations, handoffs, and summaries", {
24365
+ limit: exports_external.number().optional(),
24366
+ project_scoped: exports_external.boolean().optional(),
24367
+ session_id: exports_external.string().optional(),
24368
+ cwd: exports_external.string().optional(),
24369
+ user_id: exports_external.string().optional()
24370
+ }, async (params) => {
24371
+ const result = getActivityFeed(db, {
24372
+ ...params,
24373
+ cwd: params.cwd ?? process.cwd(),
24374
+ user_id: params.user_id ?? config2.user_id
24375
+ });
24376
+ const projectLine = result.project ? `Project: ${result.project}
24355
24377
  ` : "";
24356
- const rows = result.events.length > 0 ? result.events.map((event) => {
24357
- const stamp = new Date(event.created_at_epoch * 1000).toISOString().replace("T", " ").slice(0, 16);
24358
- const kind = event.kind === "handoff" && event.handoff_kind ? `${event.kind}:${event.handoff_kind}` : event.kind === "observation" && event.observation_type ? `${event.kind}:${event.observation_type}` : event.kind;
24359
- return `- ${stamp} [${kind}] ${event.title}${event.detail ? ` — ${event.detail}` : ""}`;
24360
- }).join(`
24378
+ const rows = result.events.length > 0 ? result.events.map((event) => {
24379
+ const stamp = new Date(event.created_at_epoch * 1000).toISOString().replace("T", " ").slice(0, 16);
24380
+ const kind = event.kind === "handoff" && event.handoff_kind ? `${event.kind}:${event.handoff_kind}` : event.kind === "observation" && event.observation_type ? `${event.kind}:${event.observation_type}` : event.kind;
24381
+ return `- ${stamp} [${kind}] ${event.title}${event.detail ? ` — ${event.detail}` : ""}`;
24382
+ }).join(`
24361
24383
  `) : "- (none)";
24362
- return {
24363
- content: [
24364
- {
24365
- type: "text",
24366
- text: `${projectLine}Activity feed:
24367
- ${rows}`
24368
- }
24369
- ]
24370
- };
24371
- });
24372
- server.tool("project_memory_index", "Show a typed local memory index for the current project", {
24373
- cwd: exports_external.string().optional(),
24374
- user_id: exports_external.string().optional()
24375
- }, async (params) => {
24376
- const result = getProjectMemoryIndex(db, {
24377
- cwd: params.cwd ?? process.cwd(),
24378
- user_id: params.user_id ?? config2.user_id
24379
- });
24380
- if (!result) {
24381
24384
  return {
24382
- content: [{ type: "text", text: "No project memory found for this folder yet." }]
24385
+ content: [
24386
+ {
24387
+ type: "text",
24388
+ text: `${projectLine}Activity feed:
24389
+ ${rows}`
24390
+ }
24391
+ ]
24383
24392
  };
24384
- }
24385
- const counts = Object.entries(result.observation_counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([type, count]) => `- ${type}: ${count}`).join(`
24393
+ });
24394
+ server.tool("project_memory_index", "Show a typed local memory index for the current project", {
24395
+ cwd: exports_external.string().optional(),
24396
+ user_id: exports_external.string().optional()
24397
+ }, async (params) => {
24398
+ const result = getProjectMemoryIndex(db, {
24399
+ cwd: params.cwd ?? process.cwd(),
24400
+ user_id: params.user_id ?? config2.user_id
24401
+ });
24402
+ if (!result) {
24403
+ return {
24404
+ content: [{ type: "text", text: "No project memory found for this folder yet." }]
24405
+ };
24406
+ }
24407
+ const counts = Object.entries(result.observation_counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([type, count]) => `- ${type}: ${count}`).join(`
24386
24408
  `) || "- (none)";
24387
- const sessions = result.recent_sessions.length > 0 ? result.recent_sessions.map((session) => {
24388
- const label = session.request ?? session.completed ?? "(no summary)";
24389
- return `- ${session.session_id} :: ${label.replace(/\s+/g, " ").trim()}`;
24390
- }).join(`
24409
+ const sessions = result.recent_sessions.length > 0 ? result.recent_sessions.map((session) => {
24410
+ const label = session.request ?? session.completed ?? "(no summary)";
24411
+ return `- ${session.session_id} :: ${label.replace(/\s+/g, " ").trim()}`;
24412
+ }).join(`
24391
24413
  `) : "- (none)";
24392
- const hotFiles = result.hot_files.length > 0 ? result.hot_files.map((file2) => `- ${file2.path} (${file2.count})`).join(`
24414
+ const hotFiles = result.hot_files.length > 0 ? result.hot_files.map((file2) => `- ${file2.path} (${file2.count})`).join(`
24393
24415
  `) : "- (none)";
24394
- const provenance = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
24416
+ const provenance = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
24395
24417
  `) : "- (none)";
24396
- const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
24418
+ const topTypes = result.top_types.length > 0 ? result.top_types.map((item) => `- ${item.type}: ${item.count}`).join(`
24397
24419
  `) : "- (none)";
24398
- const topTitles = result.top_titles.length > 0 ? result.top_titles.map((item) => `- #${item.id} [${item.type}] ${item.title}`).join(`
24420
+ const topTitles = result.top_titles.length > 0 ? result.top_titles.map((item) => `- #${item.id} [${item.type}] ${item.title}`).join(`
24399
24421
  `) : "- (none)";
24400
- const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}${item.source_agent ? ` · ${item.source_agent}` : ""}] ${item.title}`).join(`
24422
+ const recallPreviewLines = result.recall_index_preview.length > 0 ? result.recall_index_preview.map((item) => `- ${item.key} [${item.kind} · ${item.freshness}${item.source_agent ? ` · ${item.source_agent}` : ""}] ${item.title}`).join(`
24401
24423
  `) : "- (none)";
24402
- const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
24424
+ const openExactLine = result.best_recall_key ? `Open exact: load_recall_item("${result.best_recall_key}")${result.best_recall_title ? ` # ${result.best_recall_title}` : ""}
24403
24425
  ` : "";
24404
- return {
24405
- content: [
24406
- {
24407
- type: "text",
24408
- text: `Project: ${result.project}
24426
+ return {
24427
+ content: [
24428
+ {
24429
+ type: "text",
24430
+ text: `Project: ${result.project}
24409
24431
  ` + `Canonical ID: ${result.canonical_id}
24410
24432
  ` + `Agents active: ${result.active_agents.join(", ") || "(none)"}
24411
24433
  ` + `Continuity: ${result.continuity_state} — ${result.continuity_summary}
@@ -24450,84 +24472,84 @@ ${provenance}
24450
24472
 
24451
24473
  ` + `Recent memory objects:
24452
24474
  ${topTitles}`
24453
- }
24454
- ]
24455
- };
24456
- });
24457
- server.tool("project_related_work", "Show work that looks relevant to the current repo but is currently stored under other projects", {
24458
- cwd: exports_external.string().optional(),
24459
- user_id: exports_external.string().optional(),
24460
- limit: exports_external.number().optional()
24461
- }, async (params) => {
24462
- const result = getProjectRelatedWork(db, {
24463
- cwd: params.cwd ?? process.cwd(),
24464
- user_id: params.user_id ?? config2.user_id,
24465
- limit: params.limit
24466
- });
24467
- const rows = result.related.length > 0 ? result.related.map((item) => `- #${item.id} [${item.type}] ${item.title} :: stored under ${item.source_project} (${item.matched_on})`).join(`
24475
+ }
24476
+ ]
24477
+ };
24478
+ });
24479
+ server.tool("project_related_work", "Show work that looks relevant to the current repo but is currently stored under other projects", {
24480
+ cwd: exports_external.string().optional(),
24481
+ user_id: exports_external.string().optional(),
24482
+ limit: exports_external.number().optional()
24483
+ }, async (params) => {
24484
+ const result = getProjectRelatedWork(db, {
24485
+ cwd: params.cwd ?? process.cwd(),
24486
+ user_id: params.user_id ?? config2.user_id,
24487
+ limit: params.limit
24488
+ });
24489
+ const rows = result.related.length > 0 ? result.related.map((item) => `- #${item.id} [${item.type}] ${item.title} :: stored under ${item.source_project} (${item.matched_on})`).join(`
24468
24490
  `) : "- (none)";
24469
- return {
24470
- content: [
24471
- {
24472
- type: "text",
24473
- text: `Project: ${result.project}
24491
+ return {
24492
+ content: [
24493
+ {
24494
+ type: "text",
24495
+ text: `Project: ${result.project}
24474
24496
  ` + `Canonical ID: ${result.canonical_id}
24475
24497
 
24476
24498
  ` + `Related work stored under other projects:
24477
24499
  ${rows}`
24478
- }
24479
- ]
24480
- };
24481
- });
24482
- server.tool("reclassify_project_memory", "Move repo-relevant observations currently stored under other projects into the current git project", {
24483
- cwd: exports_external.string().optional(),
24484
- user_id: exports_external.string().optional(),
24485
- limit: exports_external.number().optional(),
24486
- dry_run: exports_external.boolean().optional()
24487
- }, async (params) => {
24488
- const result = reclassifyProjectMemory(db, {
24489
- cwd: params.cwd ?? process.cwd(),
24490
- user_id: params.user_id ?? config2.user_id,
24491
- limit: params.limit,
24492
- dry_run: params.dry_run
24493
- });
24494
- const rows = result.candidates.length > 0 ? result.candidates.map((item) => `- #${item.id} [${item.type}] ${item.title} :: from ${item.from} (${item.matched_on})${item.moved ? " -> moved" : ""}`).join(`
24500
+ }
24501
+ ]
24502
+ };
24503
+ });
24504
+ server.tool("reclassify_project_memory", "Move repo-relevant observations currently stored under other projects into the current git project", {
24505
+ cwd: exports_external.string().optional(),
24506
+ user_id: exports_external.string().optional(),
24507
+ limit: exports_external.number().optional(),
24508
+ dry_run: exports_external.boolean().optional()
24509
+ }, async (params) => {
24510
+ const result = reclassifyProjectMemory(db, {
24511
+ cwd: params.cwd ?? process.cwd(),
24512
+ user_id: params.user_id ?? config2.user_id,
24513
+ limit: params.limit,
24514
+ dry_run: params.dry_run
24515
+ });
24516
+ const rows = result.candidates.length > 0 ? result.candidates.map((item) => `- #${item.id} [${item.type}] ${item.title} :: from ${item.from} (${item.matched_on})${item.moved ? " -> moved" : ""}`).join(`
24495
24517
  `) : "- (none)";
24496
- return {
24497
- content: [
24498
- {
24499
- type: "text",
24500
- text: `Project: ${result.project}
24518
+ return {
24519
+ content: [
24520
+ {
24521
+ type: "text",
24522
+ text: `Project: ${result.project}
24501
24523
  ` + `Canonical ID: ${result.canonical_id}
24502
24524
  ` + `Dry run: ${params.dry_run === true ? "yes" : "no"}
24503
24525
  ` + `Moved: ${result.moved}
24504
24526
 
24505
24527
  ` + `Candidates:
24506
24528
  ${rows}`
24507
- }
24508
- ]
24509
- };
24510
- });
24511
- server.tool("workspace_memory_index", "Show a cross-project local memory index for the whole Engrm workspace", {
24512
- limit: exports_external.number().optional(),
24513
- user_id: exports_external.string().optional()
24514
- }, async (params) => {
24515
- const result = getWorkspaceMemoryIndex(db, {
24516
- limit: params.limit,
24517
- user_id: params.user_id ?? config2.user_id
24518
- });
24519
- const projectLines = result.projects.length > 0 ? result.projects.map((project) => {
24520
- const when = new Date(project.last_active_epoch * 1000).toISOString().split("T")[0];
24521
- return `- ${project.name} (${when}) obs=${project.observation_count} sessions=${project.session_count} prompts=${project.prompt_count} tools=${project.tool_event_count} checkpoints=${project.assistant_checkpoint_count}`;
24522
- }).join(`
24529
+ }
24530
+ ]
24531
+ };
24532
+ });
24533
+ server.tool("workspace_memory_index", "Show a cross-project local memory index for the whole Engrm workspace", {
24534
+ limit: exports_external.number().optional(),
24535
+ user_id: exports_external.string().optional()
24536
+ }, async (params) => {
24537
+ const result = getWorkspaceMemoryIndex(db, {
24538
+ limit: params.limit,
24539
+ user_id: params.user_id ?? config2.user_id
24540
+ });
24541
+ const projectLines = result.projects.length > 0 ? result.projects.map((project) => {
24542
+ const when = new Date(project.last_active_epoch * 1000).toISOString().split("T")[0];
24543
+ return `- ${project.name} (${when}) obs=${project.observation_count} sessions=${project.session_count} prompts=${project.prompt_count} tools=${project.tool_event_count} checkpoints=${project.assistant_checkpoint_count}`;
24544
+ }).join(`
24523
24545
  `) : "- (none)";
24524
- const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
24546
+ const provenanceLines = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
24525
24547
  `) : "- (none)";
24526
- return {
24527
- content: [
24528
- {
24529
- type: "text",
24530
- text: `Workspace totals: observations=${result.totals.observations}, sessions=${result.totals.sessions}, prompts=${result.totals.prompts}, tools=${result.totals.tool_events}, checkpoints=${result.totals.assistant_checkpoints}
24548
+ return {
24549
+ content: [
24550
+ {
24551
+ type: "text",
24552
+ text: `Workspace totals: observations=${result.totals.observations}, sessions=${result.totals.sessions}, prompts=${result.totals.prompts}, tools=${result.totals.tool_events}, checkpoints=${result.totals.assistant_checkpoints}
24531
24553
 
24532
24554
  ` + `Projects with raw chronology: ${result.projects_with_raw_capture}
24533
24555
 
@@ -24536,364 +24558,364 @@ ${provenanceLines}
24536
24558
 
24537
24559
  ` + `Projects:
24538
24560
  ${projectLines}`
24539
- }
24540
- ]
24541
- };
24542
- });
24543
- server.tool("create_handoff", "Capture an explicit cross-device handoff from the current or specified session into syncable memory", {
24544
- session_id: exports_external.string().optional().describe("Optional session ID to hand off; defaults to the latest recent session"),
24545
- cwd: exports_external.string().optional().describe("Repo path used to scope the handoff when no session ID is provided"),
24546
- title: exports_external.string().optional().describe("Optional short handoff title"),
24547
- include_chat: exports_external.boolean().optional().describe("Include a few recent chat snippets in the handoff"),
24548
- chat_limit: exports_external.number().optional().describe("How many recent chat snippets to include when include_chat is true")
24549
- }, async (params) => {
24550
- const result = await createHandoff(db, config2, params);
24551
- if (!result.success) {
24552
- return {
24553
- content: [{ type: "text", text: `Handoff not created: ${result.reason}` }]
24561
+ }
24562
+ ]
24554
24563
  };
24555
- }
24556
- return {
24557
- content: [
24558
- {
24559
- type: "text",
24560
- text: `Created handoff #${result.observation_id} for session ${result.session_id}
24561
- Title: ${result.title}`
24562
- }
24563
- ]
24564
- };
24565
- });
24566
- server.tool("refresh_handoff", "Refresh the rolling live handoff draft for the current or specified session without creating a new saved handoff", {
24567
- session_id: exports_external.string().optional().describe("Optional session ID to refresh; defaults to the latest recent session"),
24568
- cwd: exports_external.string().optional().describe("Repo path used to scope the rolling handoff when no session ID is provided"),
24569
- include_chat: exports_external.boolean().optional().describe("Include a few recent chat snippets in the rolling handoff draft"),
24570
- chat_limit: exports_external.number().optional().describe("How many recent chat snippets to include when include_chat is true")
24571
- }, async (params) => {
24572
- const result = await upsertRollingHandoff(db, config2, params);
24573
- if (!result.success) {
24564
+ });
24565
+ server.tool("create_handoff", "Capture an explicit cross-device handoff from the current or specified session into syncable memory", {
24566
+ session_id: exports_external.string().optional().describe("Optional session ID to hand off; defaults to the latest recent session"),
24567
+ cwd: exports_external.string().optional().describe("Repo path used to scope the handoff when no session ID is provided"),
24568
+ title: exports_external.string().optional().describe("Optional short handoff title"),
24569
+ include_chat: exports_external.boolean().optional().describe("Include a few recent chat snippets in the handoff"),
24570
+ chat_limit: exports_external.number().optional().describe("How many recent chat snippets to include when include_chat is true")
24571
+ }, async (params) => {
24572
+ const result = await createHandoff(db, config2, params);
24573
+ if (!result.success) {
24574
+ return {
24575
+ content: [{ type: "text", text: `Handoff not created: ${result.reason}` }]
24576
+ };
24577
+ }
24574
24578
  return {
24575
- content: [{ type: "text", text: `Rolling handoff not refreshed: ${result.reason}` }]
24576
- };
24577
- }
24578
- return {
24579
- content: [
24580
- {
24581
- type: "text",
24582
- text: `Rolling handoff draft #${result.observation_id} refreshed for session ${result.session_id}
24579
+ content: [
24580
+ {
24581
+ type: "text",
24582
+ text: `Created handoff #${result.observation_id} for session ${result.session_id}
24583
24583
  Title: ${result.title}`
24584
- }
24585
- ]
24586
- };
24587
- });
24588
- server.tool("refresh_chat_recall", "Hydrate the separate chat lane from the current Claude transcript so long sessions keep their full user/assistant thread", {
24589
- session_id: exports_external.string().optional().describe("Optional session ID; defaults to the current session when called from hooks or the active repo session when known"),
24590
- cwd: exports_external.string().optional().describe("Project directory used to resolve the Claude transcript path"),
24591
- transcript_path: exports_external.string().optional().describe("Optional explicit Claude transcript JSONL path")
24592
- }, async (params) => {
24593
- const cwd = params.cwd ?? process.cwd();
24594
- const sessionId = params.session_id ?? db.getRecentSessions(null, 1, config2.user_id)[0]?.session_id ?? null;
24595
- if (!sessionId) {
24584
+ }
24585
+ ]
24586
+ };
24587
+ });
24588
+ server.tool("refresh_handoff", "Refresh the rolling live handoff draft for the current or specified session without creating a new saved handoff", {
24589
+ session_id: exports_external.string().optional().describe("Optional session ID to refresh; defaults to the latest recent session"),
24590
+ cwd: exports_external.string().optional().describe("Repo path used to scope the rolling handoff when no session ID is provided"),
24591
+ include_chat: exports_external.boolean().optional().describe("Include a few recent chat snippets in the rolling handoff draft"),
24592
+ chat_limit: exports_external.number().optional().describe("How many recent chat snippets to include when include_chat is true")
24593
+ }, async (params) => {
24594
+ const result = await upsertRollingHandoff(db, config2, params);
24595
+ if (!result.success) {
24596
+ return {
24597
+ content: [{ type: "text", text: `Rolling handoff not refreshed: ${result.reason}` }]
24598
+ };
24599
+ }
24596
24600
  return {
24597
- content: [{ type: "text", text: "No session available to hydrate chat recall from." }]
24601
+ content: [
24602
+ {
24603
+ type: "text",
24604
+ text: `Rolling handoff draft #${result.observation_id} refreshed for session ${result.session_id}
24605
+ Title: ${result.title}`
24606
+ }
24607
+ ]
24598
24608
  };
24599
- }
24600
- const result = await syncTranscriptChat(db, config2, sessionId, cwd, params.transcript_path);
24601
- return {
24602
- content: [
24603
- {
24604
- type: "text",
24605
- text: `Chat recall refreshed for session ${sessionId}
24609
+ });
24610
+ server.tool("refresh_chat_recall", "Hydrate the separate chat lane from the current Claude transcript so long sessions keep their full user/assistant thread", {
24611
+ session_id: exports_external.string().optional().describe("Optional session ID; defaults to the current session when called from hooks or the active repo session when known"),
24612
+ cwd: exports_external.string().optional().describe("Project directory used to resolve the Claude transcript path"),
24613
+ transcript_path: exports_external.string().optional().describe("Optional explicit Claude transcript JSONL path")
24614
+ }, async (params) => {
24615
+ const cwd = params.cwd ?? process.cwd();
24616
+ const sessionId = params.session_id ?? db.getRecentSessions(null, 1, config2.user_id)[0]?.session_id ?? null;
24617
+ if (!sessionId) {
24618
+ return {
24619
+ content: [{ type: "text", text: "No session available to hydrate chat recall from." }]
24620
+ };
24621
+ }
24622
+ const result = await syncTranscriptChat(db, config2, sessionId, cwd, params.transcript_path);
24623
+ return {
24624
+ content: [
24625
+ {
24626
+ type: "text",
24627
+ text: `Chat recall refreshed for session ${sessionId}
24606
24628
  Imported: ${result.imported}
24607
24629
  Transcript messages seen: ${result.total}`
24608
- }
24609
- ]
24610
- };
24611
- });
24612
- server.tool("repair_recall", "USE WHEN recall feels thin or under-captured. Rehydrate recent session recall for the current project from Claude transcripts or history fallback before resuming.", {
24613
- session_id: exports_external.string().optional().describe("Optional single session ID to repair instead of scanning recent project sessions"),
24614
- cwd: exports_external.string().optional().describe("Project directory used to resolve project sessions and Claude history/transcript files"),
24615
- limit: exports_external.number().optional().describe("How many recent sessions to inspect when repairing a project"),
24616
- user_id: exports_external.string().optional().describe("Optional user override; defaults to the configured user"),
24617
- transcript_path: exports_external.string().optional().describe("Optional explicit transcript JSONL path when repairing a single session")
24618
- }, async (params) => {
24619
- const result = await repairRecall(db, config2, {
24620
- session_id: params.session_id,
24621
- cwd: params.cwd ?? process.cwd(),
24622
- limit: params.limit,
24623
- user_id: params.user_id ?? config2.user_id,
24624
- transcript_path: params.transcript_path
24625
- });
24626
- const projectLine = result.project_name ? `Project: ${result.project_name}
24630
+ }
24631
+ ]
24632
+ };
24633
+ });
24634
+ server.tool("repair_recall", "USE WHEN recall feels thin or under-captured. Rehydrate recent session recall for the current project from Claude transcripts or history fallback before resuming.", {
24635
+ session_id: exports_external.string().optional().describe("Optional single session ID to repair instead of scanning recent project sessions"),
24636
+ cwd: exports_external.string().optional().describe("Project directory used to resolve project sessions and Claude history/transcript files"),
24637
+ limit: exports_external.number().optional().describe("How many recent sessions to inspect when repairing a project"),
24638
+ user_id: exports_external.string().optional().describe("Optional user override; defaults to the configured user"),
24639
+ transcript_path: exports_external.string().optional().describe("Optional explicit transcript JSONL path when repairing a single session")
24640
+ }, async (params) => {
24641
+ const result = await repairRecall(db, config2, {
24642
+ session_id: params.session_id,
24643
+ cwd: params.cwd ?? process.cwd(),
24644
+ limit: params.limit,
24645
+ user_id: params.user_id ?? config2.user_id,
24646
+ transcript_path: params.transcript_path
24647
+ });
24648
+ const projectLine = result.project_name ? `Project: ${result.project_name}
24627
24649
  ` : "";
24628
- const rows = result.results.length > 0 ? result.results.map((session) => `- ${session.session_id} [${session.chat_coverage_state}] imported=${session.imported_chat_messages} chat=${session.chat_messages_after} prompts=${session.prompt_count_after} ` + `(transcript ${session.chat_source_summary.transcript} · history ${session.chat_source_summary.history} · hook ${session.chat_source_summary.hook})`).join(`
24650
+ const rows = result.results.length > 0 ? result.results.map((session) => `- ${session.session_id} [${session.chat_coverage_state}] imported=${session.imported_chat_messages} chat=${session.chat_messages_after} prompts=${session.prompt_count_after} ` + `(transcript ${session.chat_source_summary.transcript} · history ${session.chat_source_summary.history} · hook ${session.chat_source_summary.hook})`).join(`
24629
24651
  `) : "- (none)";
24630
- return {
24631
- content: [
24632
- {
24633
- type: "text",
24634
- text: `${projectLine}` + `Scope: ${result.scope}
24652
+ return {
24653
+ content: [
24654
+ {
24655
+ type: "text",
24656
+ text: `${projectLine}` + `Scope: ${result.scope}
24635
24657
  ` + `Inspected sessions: ${result.inspected_sessions}
24636
24658
  ` + `Sessions with new recall: ${result.sessions_with_imports}
24637
24659
  ` + `Imported chat messages: ${result.imported_chat_messages}
24638
24660
 
24639
24661
  ` + `Repair results:
24640
24662
  ${rows}`
24641
- }
24642
- ]
24643
- };
24644
- });
24645
- server.tool("recent_handoffs", "List recent saved handoffs and rolling handoff drafts so you can resume work on another device or in a new session", {
24646
- limit: exports_external.number().optional(),
24647
- project_scoped: exports_external.boolean().optional(),
24648
- cwd: exports_external.string().optional(),
24649
- user_id: exports_external.string().optional()
24650
- }, async (params) => {
24651
- const result = getRecentHandoffs(db, {
24652
- ...params,
24653
- user_id: params.user_id ?? config2.user_id,
24654
- current_device_id: config2.device_id
24663
+ }
24664
+ ]
24665
+ };
24655
24666
  });
24656
- const projectLine = result.project ? `Project: ${result.project}
24667
+ server.tool("recent_handoffs", "List recent saved handoffs and rolling handoff drafts so you can resume work on another device or in a new session", {
24668
+ limit: exports_external.number().optional(),
24669
+ project_scoped: exports_external.boolean().optional(),
24670
+ cwd: exports_external.string().optional(),
24671
+ user_id: exports_external.string().optional()
24672
+ }, async (params) => {
24673
+ const result = getRecentHandoffs(db, {
24674
+ ...params,
24675
+ user_id: params.user_id ?? config2.user_id,
24676
+ current_device_id: config2.device_id
24677
+ });
24678
+ const projectLine = result.project ? `Project: ${result.project}
24657
24679
  ` : "";
24658
- const rows = result.handoffs.length > 0 ? result.handoffs.map((handoff) => {
24659
- const stamp = new Date(handoff.created_at_epoch * 1000).toISOString().replace("T", " ").slice(0, 16);
24660
- const kind = isDraftHandoff(handoff) ? "draft" : "saved";
24661
- return `- #${handoff.id} (${stamp}) [${kind}] ${handoff.title}${handoff.project_name ? ` [${handoff.project_name}]` : ""} (${formatHandoffSource(handoff)})`;
24662
- }).join(`
24680
+ const rows = result.handoffs.length > 0 ? result.handoffs.map((handoff) => {
24681
+ const stamp = new Date(handoff.created_at_epoch * 1000).toISOString().replace("T", " ").slice(0, 16);
24682
+ const kind = isDraftHandoff(handoff) ? "draft" : "saved";
24683
+ return `- #${handoff.id} (${stamp}) [${kind}] ${handoff.title}${handoff.project_name ? ` [${handoff.project_name}]` : ""} (${formatHandoffSource(handoff)})`;
24684
+ }).join(`
24663
24685
  `) : "- (none)";
24664
- return {
24665
- content: [{ type: "text", text: `${projectLine}Recent handoffs:
24666
- ${rows}` }]
24667
- };
24668
- });
24669
- server.tool("load_handoff", "Open the best saved handoff or rolling draft and turn it back into a clear resume point for a new session", {
24670
- id: exports_external.number().optional().describe("Optional handoff observation ID; defaults to the latest recent handoff"),
24671
- cwd: exports_external.string().optional(),
24672
- project_scoped: exports_external.boolean().optional(),
24673
- user_id: exports_external.string().optional()
24674
- }, async (params) => {
24675
- const result = loadHandoff(db, {
24676
- ...params,
24677
- user_id: params.user_id ?? config2.user_id,
24678
- current_device_id: config2.device_id
24679
- });
24680
- if (!result.handoff) {
24681
24686
  return {
24682
- content: [{ type: "text", text: "No matching handoff found" }]
24687
+ content: [{ type: "text", text: `${projectLine}Recent handoffs:
24688
+ ${rows}` }]
24683
24689
  };
24684
- }
24685
- const facts = result.handoff.facts ? (() => {
24686
- try {
24687
- const parsed = JSON.parse(result.handoff.facts);
24688
- return Array.isArray(parsed) ? parsed : [];
24689
- } catch {
24690
- return [];
24690
+ });
24691
+ server.tool("load_handoff", "Open the best saved handoff or rolling draft and turn it back into a clear resume point for a new session", {
24692
+ id: exports_external.number().optional().describe("Optional handoff observation ID; defaults to the latest recent handoff"),
24693
+ cwd: exports_external.string().optional(),
24694
+ project_scoped: exports_external.boolean().optional(),
24695
+ user_id: exports_external.string().optional()
24696
+ }, async (params) => {
24697
+ const result = loadHandoff(db, {
24698
+ ...params,
24699
+ user_id: params.user_id ?? config2.user_id,
24700
+ current_device_id: config2.device_id
24701
+ });
24702
+ if (!result.handoff) {
24703
+ return {
24704
+ content: [{ type: "text", text: "No matching handoff found" }]
24705
+ };
24691
24706
  }
24692
- })() : [];
24693
- const factLines = facts.length > 0 ? `
24707
+ const facts = result.handoff.facts ? (() => {
24708
+ try {
24709
+ const parsed = JSON.parse(result.handoff.facts);
24710
+ return Array.isArray(parsed) ? parsed : [];
24711
+ } catch {
24712
+ return [];
24713
+ }
24714
+ })() : [];
24715
+ const factLines = facts.length > 0 ? `
24694
24716
 
24695
24717
  Facts:
24696
24718
  ${facts.map((fact) => `- ${fact}`).join(`
24697
24719
  `)}` : "";
24698
- const projectLine = result.handoff.project_name ? `Project: ${result.handoff.project_name}
24720
+ const projectLine = result.handoff.project_name ? `Project: ${result.handoff.project_name}
24699
24721
  ` : "";
24700
- const sourceLine = `Source: ${formatHandoffSource(result.handoff)}
24722
+ const sourceLine = `Source: ${formatHandoffSource(result.handoff)}
24701
24723
  `;
24702
- return {
24703
- content: [
24704
- {
24705
- type: "text",
24706
- text: `${projectLine}Handoff #${result.handoff.id}
24724
+ return {
24725
+ content: [
24726
+ {
24727
+ type: "text",
24728
+ text: `${projectLine}Handoff #${result.handoff.id}
24707
24729
  ` + sourceLine + `Title: ${result.handoff.title}
24708
24730
 
24709
24731
  ` + `${result.handoff.narrative ?? "(no handoff narrative stored)"}${factLines}`
24710
- }
24711
- ]
24712
- };
24713
- });
24714
- server.tool("recent_chat", "Inspect recently captured chat messages in the separate chat lane", {
24715
- limit: exports_external.number().optional(),
24716
- project_scoped: exports_external.boolean().optional(),
24717
- session_id: exports_external.string().optional(),
24718
- cwd: exports_external.string().optional(),
24719
- user_id: exports_external.string().optional()
24720
- }, async (params) => {
24721
- const result = getRecentChat(db, params);
24722
- const projectLine = result.project ? `Project: ${result.project}
24732
+ }
24733
+ ]
24734
+ };
24735
+ });
24736
+ server.tool("recent_chat", "Inspect recently captured chat messages in the separate chat lane", {
24737
+ limit: exports_external.number().optional(),
24738
+ project_scoped: exports_external.boolean().optional(),
24739
+ session_id: exports_external.string().optional(),
24740
+ cwd: exports_external.string().optional(),
24741
+ user_id: exports_external.string().optional()
24742
+ }, async (params) => {
24743
+ const result = getRecentChat(db, params);
24744
+ const projectLine = result.project ? `Project: ${result.project}
24723
24745
  ` : "";
24724
- const coverageLine = `Coverage: ${result.messages.length} messages across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · history ${result.source_summary.history} · hook ${result.source_summary.hook}
24746
+ const coverageLine = `Coverage: ${result.messages.length} messages across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · history ${result.source_summary.history} · hook ${result.source_summary.hook}
24725
24747
  ` + `${result.transcript_backed ? "" : `Hint: run refresh_chat_recall for one session or repair_recall for recent project sessions if this looks under-captured.
24726
24748
  `}`;
24727
- const rows = result.messages.length > 0 ? result.messages.map((msg) => {
24728
- const stamp = new Date(msg.created_at_epoch * 1000).toISOString().split("T")[0];
24729
- return `- ${stamp} [${msg.role}] [${getChatCaptureOrigin(msg)}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`;
24730
- }).join(`
24749
+ const rows = result.messages.length > 0 ? result.messages.map((msg) => {
24750
+ const stamp = new Date(msg.created_at_epoch * 1000).toISOString().split("T")[0];
24751
+ return `- ${stamp} [${msg.role}] [${getChatCaptureOrigin(msg)}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`;
24752
+ }).join(`
24731
24753
  `) : "- (none)";
24732
- return {
24733
- content: [
24734
- {
24735
- type: "text",
24736
- text: `${projectLine}${coverageLine}Recent chat:
24754
+ return {
24755
+ content: [
24756
+ {
24757
+ type: "text",
24758
+ text: `${projectLine}${coverageLine}Recent chat:
24737
24759
  ${rows}`
24738
- }
24739
- ]
24740
- };
24741
- });
24742
- server.tool("search_chat", "Search the separate chat lane without mixing it into durable memory observations", {
24743
- query: exports_external.string().describe("Text to search for in captured chat"),
24744
- limit: exports_external.number().optional(),
24745
- project_scoped: exports_external.boolean().optional(),
24746
- cwd: exports_external.string().optional(),
24747
- user_id: exports_external.string().optional()
24748
- }, async (params) => {
24749
- const result = await searchChat(db, params);
24750
- const projectLine = result.project ? `Project: ${result.project}
24760
+ }
24761
+ ]
24762
+ };
24763
+ });
24764
+ server.tool("search_chat", "Search the separate chat lane without mixing it into durable memory observations", {
24765
+ query: exports_external.string().describe("Text to search for in captured chat"),
24766
+ limit: exports_external.number().optional(),
24767
+ project_scoped: exports_external.boolean().optional(),
24768
+ cwd: exports_external.string().optional(),
24769
+ user_id: exports_external.string().optional()
24770
+ }, async (params) => {
24771
+ const result = await searchChat(db, params);
24772
+ const projectLine = result.project ? `Project: ${result.project}
24751
24773
  ` : "";
24752
- const coverageLine = `Coverage: ${result.messages.length} matches across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · history ${result.source_summary.history} · hook ${result.source_summary.hook}` + `${result.semantic_backed ? " · semantic yes" : ""}
24774
+ const coverageLine = `Coverage: ${result.messages.length} matches across ${result.session_count} session${result.session_count === 1 ? "" : "s"} ` + `· transcript ${result.source_summary.transcript} · history ${result.source_summary.history} · hook ${result.source_summary.hook}` + `${result.semantic_backed ? " · semantic yes" : ""}
24753
24775
  ` + `${result.transcript_backed ? "" : `Hint: run refresh_chat_recall for one session or repair_recall for recent project sessions if this looks under-captured.
24754
24776
  `}`;
24755
- const rows = result.messages.length > 0 ? result.messages.map((msg) => {
24756
- const stamp = new Date(msg.created_at_epoch * 1000).toISOString().split("T")[0];
24757
- return `- ${stamp} [${msg.role}] [${getChatCaptureOrigin(msg)}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`;
24758
- }).join(`
24777
+ const rows = result.messages.length > 0 ? result.messages.map((msg) => {
24778
+ const stamp = new Date(msg.created_at_epoch * 1000).toISOString().split("T")[0];
24779
+ return `- ${stamp} [${msg.role}] [${getChatCaptureOrigin(msg)}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`;
24780
+ }).join(`
24759
24781
  `) : "- (none)";
24760
- return {
24761
- content: [
24762
- {
24763
- type: "text",
24764
- text: `${projectLine}${coverageLine}Chat search for "${params.query}":
24782
+ return {
24783
+ content: [
24784
+ {
24785
+ type: "text",
24786
+ text: `${projectLine}${coverageLine}Chat search for "${params.query}":
24765
24787
  ${rows}`
24766
- }
24767
- ]
24768
- };
24769
- });
24770
- server.tool("recent_requests", "Inspect recently captured raw user requests and prompt chronology", {
24771
- limit: exports_external.number().optional(),
24772
- project_scoped: exports_external.boolean().optional(),
24773
- session_id: exports_external.string().optional(),
24774
- cwd: exports_external.string().optional(),
24775
- user_id: exports_external.string().optional()
24776
- }, async (params) => {
24777
- const result = getRecentRequests(db, params);
24778
- const projectLine = result.project ? `Project: ${result.project}
24788
+ }
24789
+ ]
24790
+ };
24791
+ });
24792
+ server.tool("recent_requests", "Inspect recently captured raw user requests and prompt chronology", {
24793
+ limit: exports_external.number().optional(),
24794
+ project_scoped: exports_external.boolean().optional(),
24795
+ session_id: exports_external.string().optional(),
24796
+ cwd: exports_external.string().optional(),
24797
+ user_id: exports_external.string().optional()
24798
+ }, async (params) => {
24799
+ const result = getRecentRequests(db, params);
24800
+ const projectLine = result.project ? `Project: ${result.project}
24779
24801
  ` : "";
24780
- const rows = result.prompts.length > 0 ? result.prompts.map((prompt) => {
24781
- const stamp = new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
24782
- return `- #${prompt.prompt_number} (${stamp}) ${prompt.prompt.replace(/\s+/g, " ").trim()}`;
24783
- }).join(`
24802
+ const rows = result.prompts.length > 0 ? result.prompts.map((prompt) => {
24803
+ const stamp = new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
24804
+ return `- #${prompt.prompt_number} (${stamp}) ${prompt.prompt.replace(/\s+/g, " ").trim()}`;
24805
+ }).join(`
24784
24806
  `) : "- (none)";
24785
- return {
24786
- content: [
24787
- {
24788
- type: "text",
24789
- text: `${projectLine}Recent requests:
24807
+ return {
24808
+ content: [
24809
+ {
24810
+ type: "text",
24811
+ text: `${projectLine}Recent requests:
24790
24812
  ${rows}`
24791
- }
24792
- ]
24793
- };
24794
- });
24795
- server.tool("recent_tools", "Inspect recently captured raw tool chronology", {
24796
- limit: exports_external.number().optional(),
24797
- project_scoped: exports_external.boolean().optional(),
24798
- session_id: exports_external.string().optional(),
24799
- cwd: exports_external.string().optional(),
24800
- user_id: exports_external.string().optional()
24801
- }, async (params) => {
24802
- const result = getRecentTools(db, params);
24803
- const projectLine = result.project ? `Project: ${result.project}
24813
+ }
24814
+ ]
24815
+ };
24816
+ });
24817
+ server.tool("recent_tools", "Inspect recently captured raw tool chronology", {
24818
+ limit: exports_external.number().optional(),
24819
+ project_scoped: exports_external.boolean().optional(),
24820
+ session_id: exports_external.string().optional(),
24821
+ cwd: exports_external.string().optional(),
24822
+ user_id: exports_external.string().optional()
24823
+ }, async (params) => {
24824
+ const result = getRecentTools(db, params);
24825
+ const projectLine = result.project ? `Project: ${result.project}
24804
24826
  ` : "";
24805
- const rows = result.tool_events.length > 0 ? result.tool_events.map((tool) => {
24806
- const stamp = new Date(tool.created_at_epoch * 1000).toISOString().split("T")[0];
24807
- const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
24808
- return `- ${stamp} ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
24809
- }).join(`
24827
+ const rows = result.tool_events.length > 0 ? result.tool_events.map((tool) => {
24828
+ const stamp = new Date(tool.created_at_epoch * 1000).toISOString().split("T")[0];
24829
+ const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
24830
+ return `- ${stamp} ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
24831
+ }).join(`
24810
24832
  `) : "- (none)";
24811
- return {
24812
- content: [
24813
- {
24814
- type: "text",
24815
- text: `${projectLine}Recent tools:
24833
+ return {
24834
+ content: [
24835
+ {
24836
+ type: "text",
24837
+ text: `${projectLine}Recent tools:
24816
24838
  ${rows}`
24817
- }
24818
- ]
24819
- };
24820
- });
24821
- server.tool("recent_sessions", "List the latest captured sessions so you can inspect one in detail", {
24822
- limit: exports_external.number().optional(),
24823
- project_scoped: exports_external.boolean().optional(),
24824
- cwd: exports_external.string().optional(),
24825
- user_id: exports_external.string().optional()
24826
- }, async (params) => {
24827
- const result = getRecentSessions(db, params);
24828
- const projectLine = result.project ? `Project: ${result.project}
24839
+ }
24840
+ ]
24841
+ };
24842
+ });
24843
+ server.tool("recent_sessions", "List the latest captured sessions so you can inspect one in detail", {
24844
+ limit: exports_external.number().optional(),
24845
+ project_scoped: exports_external.boolean().optional(),
24846
+ cwd: exports_external.string().optional(),
24847
+ user_id: exports_external.string().optional()
24848
+ }, async (params) => {
24849
+ const result = getRecentSessions(db, params);
24850
+ const projectLine = result.project ? `Project: ${result.project}
24829
24851
  ` : "";
24830
- const rows = result.sessions.length > 0 ? result.sessions.map((session) => {
24831
- const whenEpoch = session.completed_at_epoch ?? session.started_at_epoch ?? 0;
24832
- const when = whenEpoch > 0 ? new Date(whenEpoch * 1000).toISOString().split("T")[0] : "unknown";
24833
- const summary = session.request ?? session.completed ?? "(no summary)";
24834
- return `- ${session.session_id} (${when}) [${session.capture_state}] prompts=${session.prompt_count} tools=${session.tool_event_count} obs=${session.observation_count} :: ${summary.replace(/\s+/g, " ").trim()}`;
24835
- }).join(`
24852
+ const rows = result.sessions.length > 0 ? result.sessions.map((session) => {
24853
+ const whenEpoch = session.completed_at_epoch ?? session.started_at_epoch ?? 0;
24854
+ const when = whenEpoch > 0 ? new Date(whenEpoch * 1000).toISOString().split("T")[0] : "unknown";
24855
+ const summary = session.request ?? session.completed ?? "(no summary)";
24856
+ return `- ${session.session_id} (${when}) [${session.capture_state}] prompts=${session.prompt_count} tools=${session.tool_event_count} obs=${session.observation_count} :: ${summary.replace(/\s+/g, " ").trim()}`;
24857
+ }).join(`
24836
24858
  `) : "- (none)";
24837
- return {
24838
- content: [
24839
- {
24840
- type: "text",
24841
- text: `${projectLine}Recent sessions:
24842
- ${rows}`
24843
- }
24844
- ]
24845
- };
24846
- });
24847
- server.tool("session_story", "Show the full local memory story for one session", {
24848
- session_id: exports_external.string().describe("Session ID to inspect")
24849
- }, async (params) => {
24850
- const result = getSessionStory(db, params);
24851
- if (!result.session) {
24852
24859
  return {
24853
- content: [{ type: "text", text: `Session ${params.session_id} not found` }]
24860
+ content: [
24861
+ {
24862
+ type: "text",
24863
+ text: `${projectLine}Recent sessions:
24864
+ ${rows}`
24865
+ }
24866
+ ]
24854
24867
  };
24855
- }
24856
- const summaryLines = result.summary ? [
24857
- result.summary.request ? `Request: ${result.summary.request}` : null,
24858
- result.summary.investigated ? `Investigated: ${result.summary.investigated}` : null,
24859
- result.summary.learned ? `Learned: ${result.summary.learned}` : null,
24860
- result.summary.completed ? `Completed: ${result.summary.completed}` : null,
24861
- result.summary.next_steps ? `Next steps: ${result.summary.next_steps}` : null
24862
- ].filter(Boolean).join(`
24868
+ });
24869
+ server.tool("session_story", "Show the full local memory story for one session", {
24870
+ session_id: exports_external.string().describe("Session ID to inspect")
24871
+ }, async (params) => {
24872
+ const result = getSessionStory(db, params);
24873
+ if (!result.session) {
24874
+ return {
24875
+ content: [{ type: "text", text: `Session ${params.session_id} not found` }]
24876
+ };
24877
+ }
24878
+ const summaryLines = result.summary ? [
24879
+ result.summary.request ? `Request: ${result.summary.request}` : null,
24880
+ result.summary.investigated ? `Investigated: ${result.summary.investigated}` : null,
24881
+ result.summary.learned ? `Learned: ${result.summary.learned}` : null,
24882
+ result.summary.completed ? `Completed: ${result.summary.completed}` : null,
24883
+ result.summary.next_steps ? `Next steps: ${result.summary.next_steps}` : null
24884
+ ].filter(Boolean).join(`
24863
24885
  `) : "(none)";
24864
- const promptLines = result.prompts.length > 0 ? result.prompts.map((prompt) => `- #${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`).join(`
24886
+ const promptLines = result.prompts.length > 0 ? result.prompts.map((prompt) => `- #${prompt.prompt_number} ${prompt.prompt.replace(/\s+/g, " ").trim()}`).join(`
24865
24887
  `) : "- (none)";
24866
- const chatLines = result.chat_messages.length > 0 ? result.chat_messages.slice(-12).map((msg) => `- [${msg.role}] [${msg.source_kind}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`).join(`
24888
+ const chatLines = result.chat_messages.length > 0 ? result.chat_messages.slice(-12).map((msg) => `- [${msg.role}] [${msg.source_kind}] ${msg.content.replace(/\s+/g, " ").trim().slice(0, 200)}`).join(`
24867
24889
  `) : "- (none)";
24868
- const toolLines = result.tool_events.length > 0 ? result.tool_events.slice(-15).map((tool) => {
24869
- const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
24870
- return `- ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
24871
- }).join(`
24890
+ const toolLines = result.tool_events.length > 0 ? result.tool_events.slice(-15).map((tool) => {
24891
+ const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
24892
+ return `- ${tool.tool_name}${detail ? ` — ${detail}` : ""}`;
24893
+ }).join(`
24872
24894
  `) : "- (none)";
24873
- const observationLines = result.observations.length > 0 ? result.observations.slice(-15).map((obs) => {
24874
- const provenance = [];
24875
- if (obs.source_tool)
24876
- provenance.push(`via ${obs.source_tool}`);
24877
- if (typeof obs.source_prompt_number === "number")
24878
- provenance.push(`#${obs.source_prompt_number}`);
24879
- return `- #${obs.id} [${obs.type}] ${obs.title}${provenance.length ? ` (${provenance.join(" · ")})` : ""}`;
24880
- }).join(`
24895
+ const observationLines = result.observations.length > 0 ? result.observations.slice(-15).map((obs) => {
24896
+ const provenance = [];
24897
+ if (obs.source_tool)
24898
+ provenance.push(`via ${obs.source_tool}`);
24899
+ if (typeof obs.source_prompt_number === "number")
24900
+ provenance.push(`#${obs.source_prompt_number}`);
24901
+ return `- #${obs.id} [${obs.type}] ${obs.title}${provenance.length ? ` (${provenance.join(" · ")})` : ""}`;
24902
+ }).join(`
24881
24903
  `) : "- (none)";
24882
- const handoffLines = result.handoffs.length > 0 ? result.handoffs.slice(-8).map((obs) => {
24883
- const kind = isDraftHandoff(obs) ? "draft" : "saved";
24884
- return `- #${obs.id} [${kind}] ${obs.title}`;
24885
- }).join(`
24904
+ const handoffLines = result.handoffs.length > 0 ? result.handoffs.slice(-8).map((obs) => {
24905
+ const kind = isDraftHandoff(obs) ? "draft" : "saved";
24906
+ return `- #${obs.id} [${kind}] ${obs.title}`;
24907
+ }).join(`
24886
24908
  `) : "- (none)";
24887
- const metrics = result.metrics ? `files=${result.metrics.files_touched_count}, searches=${result.metrics.searches_performed}, tools=${result.metrics.tool_calls_count}, observations=${result.metrics.observation_count}` : "metrics unavailable";
24888
- const captureGaps = result.capture_gaps.length > 0 ? result.capture_gaps.map((gap) => `- ${gap}`).join(`
24909
+ const metrics = result.metrics ? `files=${result.metrics.files_touched_count}, searches=${result.metrics.searches_performed}, tools=${result.metrics.tool_calls_count}, observations=${result.metrics.observation_count}` : "metrics unavailable";
24910
+ const captureGaps = result.capture_gaps.length > 0 ? result.capture_gaps.map((gap) => `- ${gap}`).join(`
24889
24911
  `) : "- none";
24890
- const provenanceSummary = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
24912
+ const provenanceSummary = result.provenance_summary.length > 0 ? result.provenance_summary.map((item) => `- ${item.tool}: ${item.count}`).join(`
24891
24913
  `) : "- none";
24892
- return {
24893
- content: [
24894
- {
24895
- type: "text",
24896
- text: `Session: ${result.session.session_id}
24914
+ return {
24915
+ content: [
24916
+ {
24917
+ type: "text",
24918
+ text: `Session: ${result.session.session_id}
24897
24919
  ` + `Status: ${result.session.status}
24898
24920
  ` + `Capture: ${result.capture_state}
24899
24921
  ` + `Handoff split: ${result.saved_handoffs.length} saved, ${result.rolling_handoff_drafts.length} rolling drafts
@@ -24924,131 +24946,134 @@ ${captureGaps}
24924
24946
 
24925
24947
  ` + `Observations:
24926
24948
  ${observationLines}`
24927
- }
24928
- ]
24929
- };
24930
- });
24931
- function formatTimeAgo(isoDate) {
24932
- const diff = Date.now() - new Date(isoDate).getTime();
24933
- const mins = Math.floor(diff / 60000);
24934
- if (mins < 60)
24935
- return `${mins}m ago`;
24936
- const hrs = Math.floor(mins / 60);
24937
- if (hrs < 24)
24938
- return `${hrs}h ago`;
24939
- return `${Math.floor(hrs / 24)}d ago`;
24940
- }
24941
- server.tool("install_pack", "Install a help pack (pre-curated observations for a technology stack)", {
24942
- pack_name: exports_external.string().describe("Pack name (e.g. 'typescript-patterns', 'react-gotchas')")
24943
- }, async (params) => {
24944
- const installed = db.getInstalledPacks();
24945
- if (installed.includes(params.pack_name)) {
24946
- return {
24947
- content: [
24948
- {
24949
- type: "text",
24950
- text: `Pack '${params.pack_name}' is already installed.`
24951
24949
  }
24952
24950
  ]
24953
24951
  };
24954
- }
24955
- const pack = loadPack(params.pack_name);
24956
- if (!pack) {
24952
+ });
24953
+ function formatTimeAgo(isoDate) {
24954
+ const diff = Date.now() - new Date(isoDate).getTime();
24955
+ const mins = Math.floor(diff / 60000);
24956
+ if (mins < 60)
24957
+ return `${mins}m ago`;
24958
+ const hrs = Math.floor(mins / 60);
24959
+ if (hrs < 24)
24960
+ return `${hrs}h ago`;
24961
+ return `${Math.floor(hrs / 24)}d ago`;
24962
+ }
24963
+ server.tool("install_pack", "Install a help pack (pre-curated observations for a technology stack)", {
24964
+ pack_name: exports_external.string().describe("Pack name (e.g. 'typescript-patterns', 'react-gotchas')")
24965
+ }, async (params) => {
24966
+ const installed = db.getInstalledPacks();
24967
+ if (installed.includes(params.pack_name)) {
24968
+ return {
24969
+ content: [
24970
+ {
24971
+ type: "text",
24972
+ text: `Pack '${params.pack_name}' is already installed.`
24973
+ }
24974
+ ]
24975
+ };
24976
+ }
24977
+ const pack = loadPack(params.pack_name);
24978
+ if (!pack) {
24979
+ return {
24980
+ content: [
24981
+ {
24982
+ type: "text",
24983
+ text: `Pack '${params.pack_name}' not found. Available packs can be recommended based on your project's technology stack.`
24984
+ }
24985
+ ]
24986
+ };
24987
+ }
24988
+ let savedCount = 0;
24989
+ for (const obs of pack.observations) {
24990
+ try {
24991
+ await saveObservation(db, config2, {
24992
+ type: obs.type,
24993
+ title: obs.title,
24994
+ narrative: obs.narrative,
24995
+ concepts: obs.concepts,
24996
+ agent: getDetectedAgent()
24997
+ });
24998
+ savedCount++;
24999
+ } catch {}
25000
+ }
25001
+ db.markPackInstalled(params.pack_name, savedCount);
24957
25002
  return {
24958
25003
  content: [
24959
25004
  {
24960
25005
  type: "text",
24961
- text: `Pack '${params.pack_name}' not found. Available packs can be recommended based on your project's technology stack.`
25006
+ text: `Installed pack '${params.pack_name}': ${savedCount}/${pack.observations.length} observations saved.`
24962
25007
  }
24963
25008
  ]
24964
25009
  };
24965
- }
24966
- let savedCount = 0;
24967
- for (const obs of pack.observations) {
24968
- try {
24969
- await saveObservation(db, config2, {
24970
- type: obs.type,
24971
- title: obs.title,
24972
- narrative: obs.narrative,
24973
- concepts: obs.concepts,
24974
- agent: getDetectedAgent()
24975
- });
24976
- savedCount++;
24977
- } catch {}
24978
- }
24979
- db.markPackInstalled(params.pack_name, savedCount);
24980
- return {
24981
- content: [
24982
- {
24983
- type: "text",
24984
- text: `Installed pack '${params.pack_name}': ${savedCount}/${pack.observations.length} observations saved.`
24985
- }
24986
- ]
24987
- };
24988
- });
24989
- server.tool("load_session_context", "Load project memory for this session", {
24990
- max_observations: exports_external.number().optional().describe("Max observations (default: token-budgeted)")
24991
- }, async (params) => {
24992
- if (contextServed) {
25010
+ });
25011
+ server.tool("load_session_context", "Load project memory for this session", {
25012
+ max_observations: exports_external.number().optional().describe("Max observations (default: token-budgeted)")
25013
+ }, async (params) => {
25014
+ if (contextServed) {
25015
+ return {
25016
+ content: [
25017
+ {
25018
+ type: "text",
25019
+ text: "Context already loaded for this session. Use search for specific queries."
25020
+ }
25021
+ ]
25022
+ };
25023
+ }
25024
+ const context = buildSessionContext(db, process.cwd(), params.max_observations ? { maxCount: params.max_observations, userId: config2.user_id, currentDeviceId: config2.device_id } : { tokenBudget: 800, userId: config2.user_id, currentDeviceId: config2.device_id });
25025
+ if (!context || context.observations.length === 0) {
25026
+ return {
25027
+ content: [
25028
+ {
25029
+ type: "text",
25030
+ text: context ? `Project: ${context.project_name} — no prior observations found.` : "Could not detect project."
25031
+ }
25032
+ ]
25033
+ };
25034
+ }
25035
+ contextServed = true;
25036
+ sessionMetrics.contextObsInjected = context.observations.length;
25037
+ sessionMetrics.contextTotalAvailable = context.total_active;
25038
+ persistSessionMetrics();
24993
25039
  return {
24994
25040
  content: [
24995
25041
  {
24996
25042
  type: "text",
24997
- text: "Context already loaded for this session. Use search for specific queries."
25043
+ text: formatContextForInjection(context)
24998
25044
  }
24999
25045
  ]
25000
25046
  };
25047
+ });
25048
+ function qualityIndicator(quality) {
25049
+ const filled = Math.round(quality * 5);
25050
+ return "●".repeat(filled) + "○".repeat(5 - filled);
25001
25051
  }
25002
- const context = buildSessionContext(db, process.cwd(), params.max_observations ? { maxCount: params.max_observations, userId: config2.user_id, currentDeviceId: config2.device_id } : { tokenBudget: 800, userId: config2.user_id, currentDeviceId: config2.device_id });
25003
- if (!context || context.observations.length === 0) {
25004
- return {
25005
- content: [
25006
- {
25007
- type: "text",
25008
- text: context ? `Project: ${context.project_name} — no prior observations found.` : "Could not detect project."
25052
+ function formatFactPreview(factsRaw, narrative) {
25053
+ if (factsRaw) {
25054
+ try {
25055
+ const parsed = JSON.parse(factsRaw);
25056
+ if (Array.isArray(parsed)) {
25057
+ const facts = parsed.filter((item) => typeof item === "string" && item.trim().length > 0).slice(0, 2);
25058
+ if (facts.length > 0) {
25059
+ return facts.join("; ");
25060
+ }
25009
25061
  }
25010
- ]
25011
- };
25012
- }
25013
- contextServed = true;
25014
- sessionMetrics.contextObsInjected = context.observations.length;
25015
- sessionMetrics.contextTotalAvailable = context.total_active;
25016
- persistSessionMetrics();
25017
- return {
25018
- content: [
25019
- {
25020
- type: "text",
25021
- text: formatContextForInjection(context)
25022
- }
25023
- ]
25024
- };
25025
- });
25026
- function qualityIndicator(quality) {
25027
- const filled = Math.round(quality * 5);
25028
- return "●".repeat(filled) + "○".repeat(5 - filled);
25029
- }
25030
- function formatFactPreview(factsRaw, narrative) {
25031
- if (factsRaw) {
25032
- try {
25033
- const parsed = JSON.parse(factsRaw);
25034
- if (Array.isArray(parsed)) {
25035
- const facts = parsed.filter((item) => typeof item === "string" && item.trim().length > 0).slice(0, 2);
25036
- if (facts.length > 0) {
25037
- return facts.join("; ");
25062
+ } catch {
25063
+ const trimmedFacts = factsRaw.trim();
25064
+ if (trimmedFacts.length > 0) {
25065
+ return trimmedFacts.length > 160 ? `${trimmedFacts.slice(0, 157)}...` : trimmedFacts;
25038
25066
  }
25039
25067
  }
25040
- } catch {
25041
- const trimmedFacts = factsRaw.trim();
25042
- if (trimmedFacts.length > 0) {
25043
- return trimmedFacts.length > 160 ? `${trimmedFacts.slice(0, 157)}...` : trimmedFacts;
25044
- }
25045
25068
  }
25069
+ if (!narrative)
25070
+ return null;
25071
+ const trimmed = narrative.trim().replace(/\s+/g, " ");
25072
+ return trimmed.length > 160 ? `${trimmed.slice(0, 157)}...` : trimmed;
25046
25073
  }
25047
- if (!narrative)
25048
- return null;
25049
- const trimmed = narrative.trim().replace(/\s+/g, " ");
25050
- return trimmed.length > 160 ? `${trimmed.slice(0, 157)}...` : trimmed;
25074
+ return server;
25051
25075
  }
25076
+ var server = buildServer();
25052
25077
  async function main() {
25053
25078
  runDueJobs(db);
25054
25079
  if (db.vecAvailable) {
@@ -25088,13 +25113,17 @@ async function startHttpServer() {
25088
25113
  if (tokens.length === 0) {
25089
25114
  throw new Error("HTTP mode requires at least one bearer token via settings.json http.bearer_tokens or ENGRM_HTTP_BEARER_TOKENS");
25090
25115
  }
25091
- const transport = new StreamableHTTPServerTransport({
25092
- sessionIdGenerator: () => randomUUID()
25093
- });
25094
- await server.connect(transport);
25116
+ const streamableTransports = new Map;
25117
+ const sseTransports = new Map;
25095
25118
  const httpServer = createServer(async (req, res) => {
25096
25119
  try {
25097
- if (!req.url || !req.url.startsWith("/mcp")) {
25120
+ if (!req.url) {
25121
+ res.writeHead(404).end("Not found");
25122
+ return;
25123
+ }
25124
+ const url2 = new URL(req.url, "http://localhost");
25125
+ const pathname = url2.pathname;
25126
+ if (pathname !== "/mcp" && pathname !== "/sse" && pathname !== "/messages") {
25098
25127
  res.writeHead(404).end("Not found");
25099
25128
  return;
25100
25129
  }
@@ -25108,7 +25137,89 @@ async function startHttpServer() {
25108
25137
  const authorizedReq = req;
25109
25138
  authorizedReq.auth = { token };
25110
25139
  const parsedBody = req.method === "POST" ? await readJsonBody(req) : undefined;
25111
- await transport.handleRequest(authorizedReq, res, parsedBody);
25140
+ if (pathname === "/sse" && req.method === "GET") {
25141
+ const transport = new SSEServerTransport("/messages", res);
25142
+ sseTransports.set(transport.sessionId, transport);
25143
+ transport.onclose = () => {
25144
+ sseTransports.delete(transport.sessionId);
25145
+ };
25146
+ const sseServer = buildServer();
25147
+ await sseServer.connect(transport);
25148
+ return;
25149
+ }
25150
+ if (pathname === "/messages" && req.method === "POST") {
25151
+ const sessionId = url2.searchParams.get("sessionId") ?? "";
25152
+ const transport = sseTransports.get(sessionId);
25153
+ if (!transport) {
25154
+ res.writeHead(400, { "Content-Type": "application/json" });
25155
+ res.end(JSON.stringify({
25156
+ jsonrpc: "2.0",
25157
+ error: { code: -32000, message: "Bad Request: No SSE transport found for sessionId" },
25158
+ id: null
25159
+ }));
25160
+ return;
25161
+ }
25162
+ await transport.handlePostMessage(authorizedReq, res, parsedBody);
25163
+ return;
25164
+ }
25165
+ const sessionIdHeader = Array.isArray(req.headers["mcp-session-id"]) ? req.headers["mcp-session-id"][0] : req.headers["mcp-session-id"];
25166
+ if (sessionIdHeader) {
25167
+ const transport = streamableTransports.get(sessionIdHeader);
25168
+ if (!transport) {
25169
+ res.writeHead(400, { "Content-Type": "application/json" });
25170
+ res.end(JSON.stringify({
25171
+ jsonrpc: "2.0",
25172
+ error: { code: -32000, message: "Bad Request: No valid session ID provided" },
25173
+ id: null
25174
+ }));
25175
+ return;
25176
+ }
25177
+ await transport.handleRequest(authorizedReq, res, parsedBody);
25178
+ return;
25179
+ }
25180
+ if (pathname === "/mcp" && req.method === "POST" && isInitializeRequest(parsedBody)) {
25181
+ let transport;
25182
+ transport = new StreamableHTTPServerTransport({
25183
+ sessionIdGenerator: () => randomUUID(),
25184
+ onsessioninitialized: (sessionId) => {
25185
+ streamableTransports.set(sessionId, transport);
25186
+ }
25187
+ });
25188
+ transport.onclose = () => {
25189
+ const sid = transport.sessionId;
25190
+ if (sid)
25191
+ streamableTransports.delete(sid);
25192
+ };
25193
+ const streamableServer = buildServer();
25194
+ await streamableServer.connect(transport);
25195
+ await transport.handleRequest(authorizedReq, res, parsedBody);
25196
+ return;
25197
+ }
25198
+ const wantsSse = (req.headers.accept ?? "").includes("text/event-stream");
25199
+ if (pathname === "/mcp" && req.method === "GET" && wantsSse) {
25200
+ const transport = new SSEServerTransport("/mcp", res);
25201
+ sseTransports.set(transport.sessionId, transport);
25202
+ transport.onclose = () => {
25203
+ sseTransports.delete(transport.sessionId);
25204
+ };
25205
+ const sseServer = buildServer();
25206
+ await sseServer.connect(transport);
25207
+ return;
25208
+ }
25209
+ if (pathname === "/mcp" && req.method === "POST") {
25210
+ const sessionId = url2.searchParams.get("sessionId") ?? "";
25211
+ const transport = sseTransports.get(sessionId);
25212
+ if (transport) {
25213
+ await transport.handlePostMessage(authorizedReq, res, parsedBody);
25214
+ return;
25215
+ }
25216
+ }
25217
+ res.writeHead(400, { "Content-Type": "application/json" });
25218
+ res.end(JSON.stringify({
25219
+ jsonrpc: "2.0",
25220
+ error: { code: -32000, message: "Bad Request: No valid session ID provided" },
25221
+ id: null
25222
+ }));
25112
25223
  } catch (error48) {
25113
25224
  res.writeHead(500, { "Content-Type": "application/json" });
25114
25225
  res.end(JSON.stringify({ error: error48 instanceof Error ? error48.message : String(error48) }));
@@ -25118,7 +25229,7 @@ async function startHttpServer() {
25118
25229
  httpServer.once("error", reject);
25119
25230
  httpServer.listen(port, () => resolve4());
25120
25231
  });
25121
- console.error(`Engrm HTTP MCP listening on :${port}/mcp`);
25232
+ console.error(`Engrm HTTP MCP listening on :${port}/mcp (compat: /sse, /messages)`);
25122
25233
  }
25123
25234
  async function readJsonBody(req) {
25124
25235
  const chunks = [];