engrm 0.4.39 → 0.4.41

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