create-walle 0.9.13 → 0.9.15

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.
Files changed (98) hide show
  1. package/README.md +8 -3
  2. package/bin/create-walle.js +232 -32
  3. package/bin/mcp-inject.js +18 -53
  4. package/package.json +3 -1
  5. package/template/claude-task-manager/api-prompts.js +11 -2
  6. package/template/claude-task-manager/approval-agent.js +7 -0
  7. package/template/claude-task-manager/db.js +94 -75
  8. package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
  9. package/template/claude-task-manager/docs/session-tooltip-freshness-design.md +224 -0
  10. package/template/claude-task-manager/docs/session-ux-issue-review-2026-05-01.md +369 -0
  11. package/template/claude-task-manager/fuzzy-utils.js +10 -2
  12. package/template/claude-task-manager/git-utils.js +140 -10
  13. package/template/claude-task-manager/lib/agent-capabilities.js +1 -1
  14. package/template/claude-task-manager/lib/agent-presets.js +38 -5
  15. package/template/claude-task-manager/lib/codex-terminal-final.js +53 -0
  16. package/template/claude-task-manager/lib/ctm-session-context-api.js +222 -0
  17. package/template/claude-task-manager/lib/session-diagnostics.js +56 -0
  18. package/template/claude-task-manager/lib/session-history.js +309 -16
  19. package/template/claude-task-manager/lib/session-standup.js +409 -0
  20. package/template/claude-task-manager/lib/session-stream.js +253 -20
  21. package/template/claude-task-manager/lib/standup-attention.js +200 -0
  22. package/template/claude-task-manager/lib/status-hooks.js +8 -2
  23. package/template/claude-task-manager/lib/update-telemetry.js +114 -0
  24. package/template/claude-task-manager/lib/walle-ctm-history.js +49 -6
  25. package/template/claude-task-manager/lib/walle-default-model.js +55 -0
  26. package/template/claude-task-manager/lib/walle-mcp-auto-config.js +66 -0
  27. package/template/claude-task-manager/lib/walle-supervisor.js +86 -19
  28. package/template/claude-task-manager/lib/walle-transcript.js +1 -3
  29. package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
  30. package/template/claude-task-manager/package.json +1 -0
  31. package/template/claude-task-manager/providers/codex-mcp.js +104 -0
  32. package/template/claude-task-manager/providers/index.js +2 -0
  33. package/template/claude-task-manager/public/css/setup.css +2 -1
  34. package/template/claude-task-manager/public/css/walle.css +71 -0
  35. package/template/claude-task-manager/public/index.html +2388 -429
  36. package/template/claude-task-manager/public/js/message-renderer.js +314 -35
  37. package/template/claude-task-manager/public/js/session-search-utils.js +185 -3
  38. package/template/claude-task-manager/public/js/session-status-precedence.js +125 -0
  39. package/template/claude-task-manager/public/js/setup.js +62 -19
  40. package/template/claude-task-manager/public/js/stream-view.js +396 -55
  41. package/template/claude-task-manager/public/js/terminal-restore-state.js +57 -0
  42. package/template/claude-task-manager/public/js/walle-session.js +234 -26
  43. package/template/claude-task-manager/public/js/walle.js +143 -2
  44. package/template/claude-task-manager/server.js +1402 -433
  45. package/template/claude-task-manager/session-integrity.js +77 -28
  46. package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
  47. package/template/claude-task-manager/workers/scrollback-worker.js +5 -6
  48. package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
  49. package/template/package.json +1 -1
  50. package/template/wall-e/agent-runners/claude-code.js +2 -0
  51. package/template/wall-e/agent.js +63 -8
  52. package/template/wall-e/api-walle.js +330 -52
  53. package/template/wall-e/brain.js +291 -42
  54. package/template/wall-e/chat.js +172 -15
  55. package/template/wall-e/coding/compaction-service.js +19 -5
  56. package/template/wall-e/coding/stream-processor.js +22 -2
  57. package/template/wall-e/coding/workspace-replay.js +1 -4
  58. package/template/wall-e/coding-orchestrator.js +250 -80
  59. package/template/wall-e/compat.js +0 -28
  60. package/template/wall-e/context/context-builder.js +3 -1
  61. package/template/wall-e/embeddings.js +2 -7
  62. package/template/wall-e/eval/agent-runner.js +30 -9
  63. package/template/wall-e/eval/benchmark-generator.js +21 -1
  64. package/template/wall-e/eval/benchmarks/chat-eval.json +66 -6
  65. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
  66. package/template/wall-e/eval/cc-replay.js +1 -0
  67. package/template/wall-e/eval/codex-cli-baseline.js +633 -0
  68. package/template/wall-e/eval/debug-agent003.js +1 -0
  69. package/template/wall-e/eval/eval-orchestrator.js +3 -3
  70. package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
  71. package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
  72. package/template/wall-e/eval/run-model-comparison.js +1 -0
  73. package/template/wall-e/eval/swebench-adapter.js +1 -0
  74. package/template/wall-e/evaluation/quorum-evaluator.js +0 -1
  75. package/template/wall-e/extraction/knowledge-extractor.js +1 -2
  76. package/template/wall-e/lib/mcp-integration.js +336 -0
  77. package/template/wall-e/llm/ollama.js +47 -8
  78. package/template/wall-e/llm/ollama.plugin.json +1 -1
  79. package/template/wall-e/llm/tool-adapter.js +1 -0
  80. package/template/wall-e/loops/ingest.js +42 -8
  81. package/template/wall-e/loops/initiative.js +87 -2
  82. package/template/wall-e/mcp-server.js +872 -19
  83. package/template/wall-e/memory/ctm-context-client.js +230 -0
  84. package/template/wall-e/memory/ctm-session-context.js +1376 -0
  85. package/template/wall-e/prompts/coding/memory-protocol.md +6 -0
  86. package/template/wall-e/server.js +30 -1
  87. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +8 -0
  88. package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
  89. package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
  90. package/template/wall-e/skills/_bundled/slack-mentions/run.js +471 -188
  91. package/template/wall-e/skills/skill-planner.js +86 -4
  92. package/template/wall-e/slack/socket-mode-listener.js +276 -0
  93. package/template/wall-e/telemetry.js +70 -2
  94. package/template/wall-e/tools/builtin-middleware.js +55 -2
  95. package/template/wall-e/tools/shell-policy.js +1 -1
  96. package/template/wall-e/tools/slack-owner.js +104 -0
  97. package/template/website/index.html +4 -4
  98. package/template/builder-journal.md +0 -17
@@ -4,23 +4,249 @@ const path = require('node:path');
4
4
  const { v4: uuidv4 } = require('uuid');
5
5
  const brain = require('./brain');
6
6
  const { createSessionIngestService } = require('./memory/session-ingest-service');
7
+ const ctmSessionContext = require('./memory/ctm-session-context');
8
+ const ctmContextClient = require('./memory/ctm-context-client');
7
9
  const { collectIngestRecords } = require('./sources/base');
8
10
  const { ensureBuiltinSourceAdapters } = require('./sources/builtin');
9
11
  const sourceRegistry = require('./sources/registry');
10
12
  const { RECORD_TYPES } = require('./sources/record-types');
13
+ const { wallEAgentMemoryInstructions } = require('./lib/mcp-integration');
11
14
  let _embeddings;
12
15
  try { _embeddings = require('./embeddings'); } catch { _embeddings = null; }
13
16
 
14
17
  const PROTOCOL_VERSION = '2025-03-26';
15
18
 
19
+ const MCP_RESOURCES = [
20
+ {
21
+ uri: 'walle://status/session-memory',
22
+ name: 'Wall-E session memory status',
23
+ description: 'Operational status for Wall-E brain memory and CTM cached session-context access.',
24
+ mimeType: 'application/json',
25
+ },
26
+ ];
27
+
28
+ const MCP_RESOURCE_TEMPLATES = [
29
+ {
30
+ uriTemplate: 'walle-session://ctm/{session_id}/full',
31
+ name: 'CTM session full context',
32
+ description: 'Full cached CTM session context from session_conversations/session_messages. Use for explicit session transfer.',
33
+ mimeType: 'application/json',
34
+ },
35
+ {
36
+ uriTemplate: 'walle-session://ctm/{session_id}/compact',
37
+ name: 'CTM session compact context',
38
+ description: 'Prompt-friendly markdown view of cached CTM session context.',
39
+ mimeType: 'text/markdown',
40
+ },
41
+ {
42
+ uriTemplate: 'walle-context://task/{query}',
43
+ name: 'Task-scoped CTM context pack',
44
+ description: 'Search cached CTM sessions for a task prompt and return a deduplicated context pack.',
45
+ mimeType: 'text/markdown',
46
+ },
47
+ ];
48
+
49
+ const MCP_PROMPTS = [
50
+ {
51
+ name: 'walle_memory_routing',
52
+ title: 'Wall-E Memory Routing Policy',
53
+ description: 'Instructions for when to use Wall-E MCP memory, CTM session context, live tools, or public web search.',
54
+ arguments: [
55
+ { name: 'walle_port', description: 'Optional Wall-E port used in the instruction text', required: false },
56
+ ],
57
+ },
58
+ {
59
+ name: 'walle_coding_resume',
60
+ title: 'Resume With Wall-E Session Context',
61
+ description: 'Prompt template for resuming coding work using Wall-E CTM session memory and diary context.',
62
+ arguments: [
63
+ { name: 'task', description: 'Current coding task or user request', required: false },
64
+ { name: 'session_id', description: 'Optional CTM or agent session id to inspect first', required: false },
65
+ ],
66
+ },
67
+ ];
68
+
69
+ const ANY_OBJECT_SCHEMA = { type: 'object', additionalProperties: true };
70
+
71
+ const MEMORY_RESULT_SCHEMA = {
72
+ type: 'object',
73
+ properties: {
74
+ id: { type: 'string' },
75
+ source: { type: 'string' },
76
+ source_id: { type: 'string' },
77
+ memory_type: { type: 'string' },
78
+ content: { type: 'string' },
79
+ timestamp: { type: 'string' },
80
+ },
81
+ additionalProperties: true,
82
+ };
83
+
84
+ const ROUTE_OUTPUT_SCHEMA = {
85
+ type: 'object',
86
+ properties: {
87
+ route: { type: 'string' },
88
+ should_use_walle: { type: 'boolean' },
89
+ primary_tools: { type: 'array', items: { type: 'string' } },
90
+ secondary_tools: { type: 'array', items: { type: 'string' } },
91
+ avoid_tools: { type: 'array', items: { type: 'string' } },
92
+ avoid_until_memory_miss: { type: 'array', items: { type: 'string' } },
93
+ reason: { type: 'string' },
94
+ matched: { type: 'array', items: ANY_OBJECT_SCHEMA },
95
+ },
96
+ required: ['route', 'should_use_walle', 'reason', 'matched'],
97
+ additionalProperties: true,
98
+ };
99
+
100
+ const SEARCH_MEMORIES_OUTPUT_SCHEMA = {
101
+ type: 'object',
102
+ properties: {
103
+ results: { type: 'array', items: MEMORY_RESULT_SCHEMA },
104
+ filters_applied: ANY_OBJECT_SCHEMA,
105
+ vector_search_used: { type: 'boolean' },
106
+ },
107
+ required: ['results', 'filters_applied', 'vector_search_used'],
108
+ additionalProperties: true,
109
+ };
110
+
111
+ const LOOKUP_CONTEXT_OUTPUT_SCHEMA = {
112
+ type: 'object',
113
+ properties: {
114
+ route: ROUTE_OUTPUT_SCHEMA,
115
+ query: { type: 'string' },
116
+ memory_results: { type: 'array', items: MEMORY_RESULT_SCHEMA },
117
+ session_results: { type: 'array', items: ANY_OBJECT_SCHEMA },
118
+ entity_results: { type: 'array', items: ANY_OBJECT_SCHEMA },
119
+ context_pack: { anyOf: [ANY_OBJECT_SCHEMA, { type: 'null' }] },
120
+ sources: ANY_OBJECT_SCHEMA,
121
+ },
122
+ required: ['route', 'query', 'sources'],
123
+ additionalProperties: true,
124
+ };
125
+
126
+ const TOOL_OUTPUT_SCHEMAS = {
127
+ walle_route_query: ROUTE_OUTPUT_SCHEMA,
128
+ walle_lookup_context: LOOKUP_CONTEXT_OUTPUT_SCHEMA,
129
+ search_memories: SEARCH_MEMORIES_OUTPUT_SCHEMA,
130
+ remember: {
131
+ type: 'object',
132
+ properties: {
133
+ stored: { type: 'boolean' },
134
+ id: { type: 'string' },
135
+ },
136
+ required: ['stored', 'id'],
137
+ additionalProperties: true,
138
+ },
139
+ walle_memory_status: ANY_OBJECT_SCHEMA,
140
+ walle_search_sessions: ANY_OBJECT_SCHEMA,
141
+ walle_get_session: ANY_OBJECT_SCHEMA,
142
+ walle_context_pack: ANY_OBJECT_SCHEMA,
143
+ walle_diary_write: ANY_OBJECT_SCHEMA,
144
+ walle_diary_read: ANY_OBJECT_SCHEMA,
145
+ };
146
+
147
+ const READ_ONLY_TOOL_ANNOTATION = Object.freeze({
148
+ readOnlyHint: true,
149
+ destructiveHint: false,
150
+ idempotentHint: true,
151
+ openWorldHint: false,
152
+ });
153
+
154
+ const ADDITIVE_WRITE_TOOL_ANNOTATION = Object.freeze({
155
+ readOnlyHint: false,
156
+ destructiveHint: false,
157
+ idempotentHint: false,
158
+ openWorldHint: false,
159
+ });
160
+
161
+ const IDEMPOTENT_WRITE_TOOL_ANNOTATION = Object.freeze({
162
+ readOnlyHint: false,
163
+ destructiveHint: false,
164
+ idempotentHint: true,
165
+ openWorldHint: false,
166
+ });
167
+
168
+ const TOOL_ANNOTATIONS = {
169
+ walle_route_query: READ_ONLY_TOOL_ANNOTATION,
170
+ walle_lookup_context: READ_ONLY_TOOL_ANNOTATION,
171
+ search_memories: READ_ONLY_TOOL_ANNOTATION,
172
+ remember: ADDITIVE_WRITE_TOOL_ANNOTATION,
173
+ list_knowledge: READ_ONLY_TOOL_ANNOTATION,
174
+ list_people: READ_ONLY_TOOL_ANNOTATION,
175
+ get_brief: READ_ONLY_TOOL_ANNOTATION,
176
+ ask_walle: {
177
+ readOnlyHint: false,
178
+ destructiveHint: false,
179
+ idempotentHint: false,
180
+ openWorldHint: true,
181
+ },
182
+ entity_search: READ_ONLY_TOOL_ANNOTATION,
183
+ entity_graph: READ_ONLY_TOOL_ANNOTATION,
184
+ knowledge_timeline: READ_ONLY_TOOL_ANNOTATION,
185
+ knowledge_query: READ_ONLY_TOOL_ANNOTATION,
186
+ dedup_scan: READ_ONLY_TOOL_ANNOTATION,
187
+ export_brain: READ_ONLY_TOOL_ANNOTATION,
188
+ brain_stats: READ_ONLY_TOOL_ANNOTATION,
189
+ walle_memory_status: READ_ONLY_TOOL_ANNOTATION,
190
+ walle_list_sources: READ_ONLY_TOOL_ANNOTATION,
191
+ walle_source_ingest: ADDITIVE_WRITE_TOOL_ANNOTATION,
192
+ walle_search_sessions: READ_ONLY_TOOL_ANNOTATION,
193
+ walle_get_session: READ_ONLY_TOOL_ANNOTATION,
194
+ walle_context_pack: READ_ONLY_TOOL_ANNOTATION,
195
+ walle_diary_write: IDEMPOTENT_WRITE_TOOL_ANNOTATION,
196
+ walle_diary_read: READ_ONLY_TOOL_ANNOTATION,
197
+ walle_reconnect: IDEMPOTENT_WRITE_TOOL_ANNOTATION,
198
+ walle_repair_status: READ_ONLY_TOOL_ANNOTATION,
199
+ walle_rebuild_ctm_message_index: IDEMPOTENT_WRITE_TOOL_ANNOTATION,
200
+ walle_rebuild_source_index: IDEMPOTENT_WRITE_TOOL_ANNOTATION,
201
+ };
202
+
16
203
  const MCP_TOOLS = [
204
+ {
205
+ name: 'walle_route_query',
206
+ description: 'Classify whether a user request should use Wall-E memory/session tools, live source tools, or public web search. Use when unsure before choosing web/search for private, remembered, work, or prior-session context.',
207
+ inputSchema: {
208
+ type: 'object',
209
+ properties: {
210
+ query: { type: 'string', description: 'The user request or task to route' },
211
+ context: { type: 'string', description: 'Optional nearby conversation context' },
212
+ },
213
+ required: ['query'],
214
+ },
215
+ },
216
+ {
217
+ name: 'walle_lookup_context',
218
+ description: 'One-stop Wall-E context lookup. Routes the request, then returns the most relevant private memory, entity, and CTM session context when Wall-E should be used. Use before public web search for ambiguous private/work-context requests.',
219
+ inputSchema: {
220
+ type: 'object',
221
+ properties: {
222
+ query: { type: 'string', description: 'The user request or task to look up' },
223
+ context: { type: 'string', description: 'Optional nearby conversation context' },
224
+ limit: { type: 'number', description: 'Max memories/session snippets/entities per source (default 5, max 20)' },
225
+ include_memories: { type: 'boolean', description: 'Include Wall-E memory search results (default true when route needs Wall-E)' },
226
+ include_sessions: { type: 'boolean', description: 'Include CTM coding-session context (default true for session routes)' },
227
+ include_entities: { type: 'boolean', description: 'Include knowledge-graph entity matches (default true for people/work routes)' },
228
+ entity_name: { type: 'string', description: 'Optional explicit entity/person/project/tool name to look up' },
229
+ force_memory: { type: 'boolean', description: 'Search Wall-E memory even when routing says live/public/direct' },
230
+ prefer_db: { type: 'boolean', description: 'Prefer cached CTM DB for session context (default true)' },
231
+ source: { type: 'string', description: 'Optional memory source filter' },
232
+ memory_type: { type: 'string', description: 'Optional memory_type filter' },
233
+ since: { type: 'string', description: 'Optional inclusive ISO timestamp lower bound for memory results' },
234
+ until: { type: 'string', description: 'Optional inclusive ISO timestamp upper bound for memory results' },
235
+ },
236
+ required: ['query'],
237
+ },
238
+ },
17
239
  {
18
240
  name: 'search_memories',
19
- description: "Search across all of Wall-E's ingested memories (Slack, email, calendar, files). Returns matching memories sorted by recency.",
241
+ description: "Search Wall-E's private/user memory across ingested Slack, email, calendar, files, and work context. Use before public web search for remembered context, prior discussions, decisions, preferences, people, projects, tools, or 'do you know' / 'what did we discuss' questions.",
20
242
  inputSchema: {
21
243
  type: 'object',
22
244
  properties: {
23
245
  query: { type: 'string', description: 'Search query (space-separated terms, all must match)' },
246
+ source: { type: 'string', description: 'Optional source filter, e.g. slack, email, calendar, ctm, walle-diary, codex-jsonl' },
247
+ memory_type: { type: 'string', description: 'Optional memory_type filter, e.g. message, email, agent_diary, coding_session_user_message' },
248
+ since: { type: 'string', description: 'Optional inclusive ISO timestamp lower bound' },
249
+ until: { type: 'string', description: 'Optional inclusive ISO timestamp upper bound' },
24
250
  limit: { type: 'number', description: 'Max results to return (default 20, max 200)' },
25
251
  },
26
252
  required: ['query'],
@@ -72,7 +298,7 @@ const MCP_TOOLS = [
72
298
  },
73
299
  {
74
300
  name: 'ask_walle',
75
- description: 'Ask Wall-E a question. Uses the full chat pipeline with context from memories, knowledge, and conversation history. Good for complex questions that need reasoning across stored information.',
301
+ description: 'Ask Wall-E a question through the full memory-aware chat pipeline. Use for complex private/work-context questions that need reasoning across stored memories, knowledge, people, projects, and conversation history.',
76
302
  inputSchema: {
77
303
  type: 'object',
78
304
  properties: {
@@ -83,7 +309,7 @@ const MCP_TOOLS = [
83
309
  },
84
310
  {
85
311
  name: 'entity_search',
86
- description: 'Search for entities (people, projects, tools, orgs) in Wall-E\'s knowledge graph by name or type.',
312
+ description: 'Search for entities (people, projects, tools, orgs, concepts) in Wall-E\'s private knowledge graph by name or type. Use for user-context lookups before public web search.',
87
313
  inputSchema: {
88
314
  type: 'object',
89
315
  properties: {
@@ -181,7 +407,7 @@ const MCP_TOOLS = [
181
407
  },
182
408
  {
183
409
  name: 'walle_source_ingest',
184
- description: 'Ingest a source file through a Wall-E source adapter. Use dry_run first when inspecting an unfamiliar source.',
410
+ description: 'Ingest a source file through a Wall-E source adapter. Use dry_run first when inspecting an unfamiliar source; set confirm_write=true for non-dry-run ingestion.',
185
411
  inputSchema: {
186
412
  type: 'object',
187
413
  properties: {
@@ -192,6 +418,7 @@ const MCP_TOOLS = [
192
418
  privacy_class: { type: 'string', description: 'Optional source privacy class' },
193
419
  privacy_floor: { type: 'string', description: 'Optional maximum privacy class allowed to write' },
194
420
  dry_run: { type: 'boolean', description: 'Parse and validate without writing memories' },
421
+ confirm_write: { type: 'boolean', description: 'Required when dry_run is false because ingestion writes Wall-E memories' },
195
422
  metadata: { type: 'object', description: 'Optional adapter metadata' },
196
423
  },
197
424
  required: ['adapter_id', 'uri'],
@@ -199,30 +426,54 @@ const MCP_TOOLS = [
199
426
  },
200
427
  {
201
428
  name: 'walle_search_sessions',
202
- description: 'Search ingested coding sessions and Wall-E diary entries. Returns attributed snippets plus session pointers.',
429
+ description: 'Search coding-session history. Tries cached CTM DB session messages first, then ingested Wall-E memories/diary entries. Returns deduplicated attributed snippets plus session pointers.',
203
430
  inputSchema: {
204
431
  type: 'object',
205
432
  properties: {
206
433
  query: { type: 'string', description: 'Search query' },
207
434
  limit: { type: 'number', description: 'Max session snippets (default 10, max 50)' },
435
+ prefer_db: { type: 'boolean', description: 'Try cached CTM DB tables before Wall-E memory (default true)' },
436
+ include_memory: { type: 'boolean', description: 'Also search Wall-E ingested memories and diary entries (default true)' },
208
437
  },
209
438
  required: ['query'],
210
439
  },
211
440
  },
212
441
  {
213
442
  name: 'walle_get_session',
214
- description: 'Retrieve session snippets from ingested memory, or parse a JSONL session directly when adapter_id and uri are provided.',
443
+ description: 'Retrieve cached CTM session context by session id/source id, or parse a JSONL session directly when adapter_id and uri are provided. DB tables are preferred for full context transfer.',
215
444
  inputSchema: {
216
445
  type: 'object',
217
446
  properties: {
447
+ session_id: { type: 'string', description: 'CTM session id or agent session id to retrieve from cached DB tables' },
448
+ session_ids: { type: 'array', items: { type: 'string' }, description: 'Several CTM/agent session ids to retrieve together' },
218
449
  source_id: { type: 'string', description: 'Stable source/session id to retrieve from memory' },
219
450
  adapter_id: { type: 'string', description: 'Optional adapter id for direct JSONL parse' },
220
451
  uri: { type: 'string', description: 'Optional JSONL path for direct parse' },
221
- limit: { type: 'number', description: 'Max records (default 50, max 200)' },
452
+ limit: { type: 'number', description: 'Max records/messages (default 50 for memory, 200 for CTM DB; use 0/full/all for all cached DB messages)' },
453
+ cursor: { type: 'number', description: 'Message offset for cached DB pagination' },
454
+ format: { type: 'string', description: 'Cached DB output format: messages, markdown, or compact' },
455
+ dedupe: { type: 'boolean', description: 'Deduplicate repeated message content (default true)' },
456
+ prefer_db: { type: 'boolean', description: 'Use cached CTM DB tables before Wall-E memory fallback (default true)' },
222
457
  include_raw: { type: 'boolean', description: 'Include raw content when available' },
223
458
  },
224
459
  },
225
460
  },
461
+ {
462
+ name: 'walle_context_pack',
463
+ description: 'Build a deduplicated task-scoped context pack from cached CTM session history. Use before answering or starting a coding task when the user references prior attempts, old bugs, branches, decisions, restarts, or past agent work.',
464
+ inputSchema: {
465
+ type: 'object',
466
+ properties: {
467
+ task: { type: 'string', description: 'Current user prompt or task description' },
468
+ query: { type: 'string', description: 'Optional explicit search query; defaults to task' },
469
+ session_ids: { type: 'array', items: { type: 'string' }, description: 'Optional CTM/agent session ids to force include' },
470
+ limit: { type: 'number', description: 'Max sessions to include (default 5, max 50)' },
471
+ token_budget: { type: 'number', description: 'Approximate context-pack token budget (default 12000)' },
472
+ mode: { type: 'string', description: 'auto, compact, or full' },
473
+ include_raw: { type: 'boolean', description: 'Include raw cached messages when mode/full transfer needs exact source payloads' },
474
+ },
475
+ },
476
+ },
226
477
  {
227
478
  name: 'walle_diary_write',
228
479
  description: 'Write an idempotent agent diary entry for a coding session stop, handoff, or compaction boundary.',
@@ -272,26 +523,72 @@ const MCP_TOOLS = [
272
523
  properties: {},
273
524
  },
274
525
  },
526
+ {
527
+ name: 'walle_rebuild_ctm_message_index',
528
+ description: 'Ask the CTM API to backfill session_messages and FTS rows from cached session_conversations. Use dry_run first to inspect planned work; set confirm_write=true for writes.',
529
+ inputSchema: {
530
+ type: 'object',
531
+ properties: {
532
+ limit: { type: 'number', description: 'Max session_conversations rows to repair (default 100, max 1000)' },
533
+ dry_run: { type: 'boolean', description: 'Plan the repair without writing (default false)' },
534
+ confirm_write: { type: 'boolean', description: 'Required when dry_run is false because rebuild writes CTM index rows' },
535
+ },
536
+ },
537
+ },
275
538
  {
276
539
  name: 'walle_rebuild_source_index',
277
- description: 'Rebuild the memory/source index from stored memories. Use dry_run to inspect expected work without writing.',
540
+ description: 'Rebuild the memory/source index from stored memories. Use dry_run to inspect expected work without writing; set confirm_write=true for writes.',
278
541
  inputSchema: {
279
542
  type: 'object',
280
543
  properties: {
281
544
  source_id: { type: 'string', description: 'Optional stable source/session id to rebuild' },
282
545
  dry_run: { type: 'boolean', description: 'Return current index status without rebuilding' },
546
+ confirm_write: { type: 'boolean', description: 'Required when dry_run is false because rebuild writes Wall-E source-index rows' },
283
547
  },
284
548
  },
285
549
  },
286
550
  ];
287
551
 
552
+ function toolTitle(name) {
553
+ return String(name || '')
554
+ .split('_')
555
+ .filter(Boolean)
556
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
557
+ .join(' ');
558
+ }
559
+
560
+ function applyMcpToolMetadata(tools) {
561
+ for (const tool of tools) {
562
+ const annotations = TOOL_ANNOTATIONS[tool.name] || READ_ONLY_TOOL_ANNOTATION;
563
+ tool.annotations = { title: toolTitle(tool.name), ...annotations };
564
+ if (TOOL_OUTPUT_SCHEMAS[tool.name]) {
565
+ tool.outputSchema = TOOL_OUTPUT_SCHEMAS[tool.name];
566
+ }
567
+ }
568
+ }
569
+
570
+ applyMcpToolMetadata(MCP_TOOLS);
571
+
288
572
  async function executeTool(name, args) {
289
573
  switch (name) {
574
+ case 'walle_route_query': {
575
+ return routeWallEQuery(args.query, { context: args.context });
576
+ }
577
+ case 'walle_lookup_context': {
578
+ return lookupWallEContext(args);
579
+ }
290
580
  case 'search_memories': {
291
581
  const limit = args.limit || 20;
292
- let results = brain.searchMemories({ query: args.query, limit });
582
+ const filters = {
583
+ source: args.source,
584
+ memory_type: args.memory_type,
585
+ since: args.since,
586
+ until: args.until,
587
+ };
588
+ const hasFilters = Object.values(filters).some(Boolean);
589
+ let results = brain.searchMemories({ query: args.query, limit, ...filters });
293
590
  // Hybrid: merge with vector results if embeddings available
294
- if (_embeddings && _embeddings.isAvailable()) {
591
+ if (!hasFilters && _embeddings && _embeddings.isAvailable()) {
295
592
  try {
296
593
  const queryEmb = await _embeddings.computeEmbedding(args.query);
297
594
  if (queryEmb) {
@@ -302,7 +599,11 @@ async function executeTool(name, args) {
302
599
  }
303
600
  } catch {}
304
601
  }
305
- return { results: results.map(m => ({ id: m.id, source: m.source, content: m.content, timestamp: m.timestamp })) };
602
+ return {
603
+ results: results.map(formatMemoryResult),
604
+ filters_applied: Object.fromEntries(Object.entries(filters).filter(([, value]) => Boolean(value))),
605
+ vector_search_used: Boolean(!hasFilters && _embeddings && _embeddings.isAvailable()),
606
+ };
306
607
  }
307
608
  case 'remember': {
308
609
  const result = brain.insertKnowledge({
@@ -412,6 +713,11 @@ async function executeTool(name, args) {
412
713
  return {
413
714
  ok: true,
414
715
  stats,
716
+ ctm_db: await getCtmDbStatus(),
717
+ resources: {
718
+ static: MCP_RESOURCES.map((resource) => resource.uri),
719
+ templates: MCP_RESOURCE_TEMPLATES.map((template) => template.uriTemplate),
720
+ },
415
721
  source_adapters: sourceRegistry.list().map(formatSourceAdapter),
416
722
  diary_count: countDiaryEntries(),
417
723
  tools: MCP_TOOLS.filter((tool) => tool.name.startsWith('walle_')).map((tool) => tool.name),
@@ -428,6 +734,7 @@ async function executeTool(name, args) {
428
734
  }
429
735
  case 'walle_source_ingest': {
430
736
  ensureBuiltinSourceAdapters();
737
+ requireWriteConfirmation('walle_source_ingest', args);
431
738
  assertJsonlPath(args.uri);
432
739
  const sourceRef = {
433
740
  adapterId: args.adapter_id,
@@ -446,11 +753,52 @@ async function executeTool(name, args) {
446
753
  }
447
754
  case 'walle_search_sessions': {
448
755
  const limit = clampLimit(args.limit, 10, 50);
449
- const results = brain.searchMemories({ query: args.query, limit: limit * 4 })
450
- .filter(isSessionMemory)
451
- .slice(0, limit)
452
- .map(formatSessionMemory);
453
- return { results };
756
+ const ctmResults = args.prefer_db === false
757
+ ? { ok: false, results: [], skipped: true, reason: 'prefer_db_false' }
758
+ : await safeCtmSearchSessions({ query: args.query, limit });
759
+ const memoryResults = args.include_memory === false
760
+ ? []
761
+ : brain.searchMemories({ query: args.query, limit: limit * 4 })
762
+ .filter(isSessionMemory)
763
+ .slice(0, limit * 2)
764
+ .map((memory, index) => ({
765
+ ...formatSessionMemory(memory),
766
+ source_layer: 'wall-e-memory',
767
+ rank: 10000 + index,
768
+ }));
769
+ const ctmSourceLayer = ctmResults.source === 'ctm-api' ? 'ctm-api' : 'ctm-db';
770
+ const dbResults = (ctmResults.results || []).map((result, index) => ({
771
+ ...result,
772
+ source_layer: ctmSourceLayer,
773
+ rank: Number.isFinite(result.rank) ? result.rank : index,
774
+ }));
775
+ const results = ctmSessionContext.dedupeItems([...dbResults, ...memoryResults], {
776
+ limit,
777
+ contentFields: ['snippet', 'content'],
778
+ keyPrefix: (item) => `${item.session_id || item.source_id || ''}:${item.role || item.memory_type || ''}`,
779
+ }).map(stripRank);
780
+ return {
781
+ results,
782
+ sources: {
783
+ ctm_db: {
784
+ ok: Boolean(ctmResults.ok),
785
+ unavailable: Boolean(ctmResults.unavailable),
786
+ source: ctmResults.source || '',
787
+ backend_source: ctmResults.backend_source || '',
788
+ api_transport: ctmResults.api_transport || null,
789
+ db_path: ctmResults.db_path || '',
790
+ active_source: ctmResults.active_source || '',
791
+ fallback_used: Boolean(ctmResults.fallback_used),
792
+ reason: ctmResults.reason || '',
793
+ count: (ctmResults.results || []).length,
794
+ },
795
+ wall_e_memory: {
796
+ searched: args.include_memory !== false,
797
+ count: memoryResults.length,
798
+ },
799
+ },
800
+ deduped: true,
801
+ };
454
802
  }
455
803
  case 'walle_get_session': {
456
804
  const limit = clampLimit(args.limit, 50, 200);
@@ -475,12 +823,47 @@ async function executeTool(name, args) {
475
823
  .map((record) => formatSourceRecord(record, { includeRaw: Boolean(args.include_raw) })),
476
824
  };
477
825
  }
826
+ const wantsDb = args.prefer_db !== false;
827
+ const dbSessionIds = args.session_ids || args.session_id || args.source_id;
828
+ if (wantsDb && dbSessionIds) {
829
+ const dbContext = await safeCtmSessionContext({
830
+ session_ids: args.session_ids,
831
+ session_id: args.session_id,
832
+ source_id: args.source_id,
833
+ limit: args.limit ?? 200,
834
+ cursor: args.cursor || 0,
835
+ include_raw: Boolean(args.include_raw),
836
+ dedupe: args.dedupe !== false,
837
+ format: args.format || 'messages',
838
+ });
839
+ if (dbContext.ok && (dbContext.sessions.length > 0 || dbContext.messages.length > 0)) {
840
+ return dbContext;
841
+ }
842
+ if (args.session_id || args.session_ids) return dbContext;
843
+ }
478
844
  if (!args.source_id) throw new Error('source_id is required when adapter_id and uri are not provided');
479
845
  return {
480
846
  source_id: args.source_id,
847
+ transfer: {
848
+ full_context_supported: false,
849
+ served_from: 'wall-e-memory',
850
+ jsonl_fallback_used: false,
851
+ ctm_db_preferred: wantsDb,
852
+ },
481
853
  records: getSessionMemories(args.source_id, { limit }).map(formatSessionMemory),
482
854
  };
483
855
  }
856
+ case 'walle_context_pack': {
857
+ return await safeCtmContextPack({
858
+ task: args.task || '',
859
+ query: args.query || '',
860
+ session_ids: args.session_ids,
861
+ limit: args.limit || 5,
862
+ token_budget: args.token_budget || 12000,
863
+ mode: args.mode || 'auto',
864
+ include_raw: Boolean(args.include_raw),
865
+ });
866
+ }
484
867
  case 'walle_diary_write': {
485
868
  return writeDiaryEntry(args);
486
869
  }
@@ -495,7 +878,15 @@ async function executeTool(name, args) {
495
878
  const repair = require('./utils/repair');
496
879
  return repair.scanIntegrity();
497
880
  }
881
+ case 'walle_rebuild_ctm_message_index': {
882
+ requireWriteConfirmation('walle_rebuild_ctm_message_index', args);
883
+ return ctmContextClient.backfillMessageIndex({
884
+ limit: args.limit || 100,
885
+ dry_run: Boolean(args.dry_run),
886
+ });
887
+ }
498
888
  case 'walle_rebuild_source_index': {
889
+ requireWriteConfirmation('walle_rebuild_source_index', args);
499
890
  const { rebuildSourceIndex } = require('./memory/source-indexer');
500
891
  return rebuildSourceIndex({
501
892
  brain,
@@ -508,12 +899,281 @@ async function executeTool(name, args) {
508
899
  }
509
900
  }
510
901
 
902
+ function routeWallEQuery(query, opts = {}) {
903
+ const text = `${query || ''}\n${opts.context || ''}`.toLowerCase();
904
+ const has = (patterns) => patterns.some(pattern => pattern.test(text));
905
+ const matched = [];
906
+ const add = (name, patterns) => {
907
+ const hits = patterns.filter(pattern => pattern.test(text)).map(pattern => pattern.source);
908
+ if (hits.length) matched.push({ name, hits });
909
+ return hits.length > 0;
910
+ };
911
+
912
+ const explicitWalle = add('explicit_wall_e', [/\bwall-?e\b/, /\bwalle\b/, /\bmcp\b/, /\bmemory\b/, /\bctm\b/]);
913
+ const priorContext = add('prior_context', [/\blast time\b/, /\bprevious(ly)?\b/, /\bremember\b/, /\bwhat did we (discuss|decide)\b/, /\bwhat happened\b/, /\bfollow.?up\b/, /\bcontext\b/]);
914
+ const sessionContext = add('session_context', [/\bsession\b/, /\bagent\b/, /\bbranch\b/, /\brestart\b/, /\bresume\b/, /\bhandoff\b/, /\bdiary\b/, /\bbug\b/, /\bregression\b/, /\btitle\b/, /\bcommit\b/]);
915
+ const workMemory = add('work_memory', [/\bslack\b/, /\bemail\b/, /\bcalendar\b/, /\bcolleague\b/, /\bmanager\b/, /\bteam\b/, /\bproject\b/, /\bdoc(ument)?\b/, /\btool\b/, /\bdecision\b/, /\bpreference\b/]);
916
+ const personLookup = add('person_lookup', [/\bwho is\b/, /\bdo you know\b/, /\bwhat is .* role\b/, /\breports? to\b/]);
917
+ const liveAction = add('live_action', [/\bsend\b/, /\bpost\b/, /\bcreate\b/, /\bschedule\b/, /\bopen\b/, /\bdelete\b/, /\bupdate\b/, /\bunread\b/, /\binbox\b/, /\btoday'?s calendar\b/, /\btomorrow'?s calendar\b/]);
918
+ const publicCurrent = add('public_current', [/\bweb\b/, /\bpublic\b/, /\bnews\b/, /\blatest\b/, /\bcurrent\b/, /\bprice\b/, /\bstock\b/, /\bweather\b/, /\bdocs?\b/, /\bofficial\b/]);
919
+
920
+ if (liveAction && !priorContext && !explicitWalle) {
921
+ return {
922
+ route: 'live_source_or_action',
923
+ should_use_walle: false,
924
+ primary_tools: ['calendar/email/slack/action tools exposed by the host'],
925
+ secondary_tools: ['search_memories only if the user asks for remembered context'],
926
+ avoid_tools: ['walle_context_pack', 'walle_search_sessions'],
927
+ reason: 'The request appears to need live source data or a side-effecting action rather than stored memory.',
928
+ matched,
929
+ };
930
+ }
931
+
932
+ if (sessionContext || (explicitWalle && priorContext)) {
933
+ return {
934
+ route: 'walle_session_context',
935
+ should_use_walle: true,
936
+ primary_tools: ['walle_context_pack', 'walle_search_sessions', 'walle_get_session'],
937
+ secondary_tools: ['walle_diary_read', 'search_memories'],
938
+ avoid_until_memory_miss: ['web_search', 'web_fetch'],
939
+ reason: 'The request appears to depend on prior coding-agent, CTM, branch, bug, restart, or handoff context.',
940
+ matched,
941
+ };
942
+ }
943
+
944
+ if (priorContext || workMemory || personLookup || explicitWalle) {
945
+ return {
946
+ route: 'walle_private_memory',
947
+ should_use_walle: true,
948
+ primary_tools: personLookup ? ['entity_search', 'search_memories', 'ask_walle'] : ['search_memories', 'knowledge_query', 'ask_walle'],
949
+ secondary_tools: ['entity_graph', 'knowledge_timeline'],
950
+ avoid_until_memory_miss: ['web_search', 'web_fetch'],
951
+ reason: 'The request appears to depend on private, remembered, or work-context data Wall-E owns.',
952
+ matched,
953
+ };
954
+ }
955
+
956
+ if (publicCurrent) {
957
+ return {
958
+ route: 'public_or_current_fact',
959
+ should_use_walle: false,
960
+ primary_tools: ['web_search', 'web_fetch', 'official docs or public data tools'],
961
+ secondary_tools: ['search_memories only if the user asks for personal/work context'],
962
+ avoid_tools: ['walle_context_pack'],
963
+ reason: 'The request appears to ask for public/current information rather than private memory.',
964
+ matched,
965
+ };
966
+ }
967
+
968
+ return {
969
+ route: 'answer_directly_or_clarify',
970
+ should_use_walle: false,
971
+ primary_tools: [],
972
+ secondary_tools: ['walle_route_query again with more context if uncertain'],
973
+ reason: 'No strong signal that Wall-E memory, live tools, or public web are required.',
974
+ matched,
975
+ };
976
+ }
977
+
978
+ async function lookupWallEContext(args = {}) {
979
+ const query = String(args.query || '').trim();
980
+ if (!query) throw new Error('query is required');
981
+ const route = routeWallEQuery(query, { context: args.context });
982
+ const limit = clampLimit(args.limit, 5, 20);
983
+ const shouldUseWallE = Boolean(route.should_use_walle || args.force_memory);
984
+ const includeMemories = args.include_memories === true || (args.include_memories !== false && shouldUseWallE);
985
+ const includeSessions = args.include_sessions === true
986
+ || (args.include_sessions !== false && route.route === 'walle_session_context');
987
+ const includeEntities = args.include_entities === true
988
+ || (args.include_entities !== false && (route.route === 'walle_private_memory' || args.force_memory));
989
+ const inferredEntityName = String(args.entity_name || inferEntityName(query) || '').trim();
990
+ const memoryQuery = inferredEntityName || query;
991
+ const result = {
992
+ route,
993
+ query,
994
+ memory_results: [],
995
+ session_results: [],
996
+ entity_results: [],
997
+ context_pack: null,
998
+ sources: {
999
+ memories: { searched: false, count: 0 },
1000
+ ctm_sessions: { searched: false, count: 0 },
1001
+ entities: { searched: false, count: 0 },
1002
+ },
1003
+ };
1004
+
1005
+ if (includeSessions) {
1006
+ const sessionSearch = await safeCtmSearchSessions({
1007
+ query,
1008
+ limit,
1009
+ prefer_db: args.prefer_db !== false,
1010
+ });
1011
+ result.session_results = (sessionSearch.results || []).slice(0, limit).map(stripRank);
1012
+ result.sources.ctm_sessions = {
1013
+ searched: true,
1014
+ ok: Boolean(sessionSearch.ok),
1015
+ source: sessionSearch.source || '',
1016
+ reason: sessionSearch.reason || '',
1017
+ count: result.session_results.length,
1018
+ };
1019
+ result.context_pack = await safeCtmContextPack({
1020
+ task: query,
1021
+ query,
1022
+ limit: Math.min(limit, 5),
1023
+ token_budget: 6000,
1024
+ mode: 'compact',
1025
+ include_raw: false,
1026
+ });
1027
+ }
1028
+
1029
+ if (includeMemories) {
1030
+ const filters = {
1031
+ source: args.source,
1032
+ memory_type: args.memory_type,
1033
+ since: args.since,
1034
+ until: args.until,
1035
+ };
1036
+ result.memory_results = brain.searchMemories({ query: memoryQuery, limit, ...filters }).map(formatMemoryResult);
1037
+ result.sources.memories = {
1038
+ searched: true,
1039
+ query: memoryQuery,
1040
+ filters_applied: Object.fromEntries(Object.entries(filters).filter(([, value]) => Boolean(value))),
1041
+ count: result.memory_results.length,
1042
+ };
1043
+ }
1044
+
1045
+ if (includeEntities) {
1046
+ if (inferredEntityName) {
1047
+ result.entity_results = searchEntitiesForContext(inferredEntityName, limit);
1048
+ result.sources.entities = {
1049
+ searched: true,
1050
+ query: inferredEntityName,
1051
+ count: result.entity_results.length,
1052
+ };
1053
+ } else {
1054
+ result.sources.entities = {
1055
+ searched: false,
1056
+ reason: 'no_entity_name_inferred',
1057
+ count: 0,
1058
+ };
1059
+ }
1060
+ }
1061
+
1062
+ return result;
1063
+ }
1064
+
1065
+ function inferEntityName(query) {
1066
+ const text = String(query || '').trim();
1067
+ for (const pattern of [
1068
+ /\bdo you know\s+(.+?)[?.!]?$/i,
1069
+ /\bwho is\s+(.+?)[?.!]?$/i,
1070
+ /\bwhat is\s+(.+?)'s role[?.!]?$/i,
1071
+ /\bwhat is the role of\s+(.+?)[?.!]?$/i,
1072
+ ]) {
1073
+ const match = text.match(pattern);
1074
+ if (match?.[1]) {
1075
+ return match[1].replace(/^["']|["']$/g, '').trim();
1076
+ }
1077
+ }
1078
+ return '';
1079
+ }
1080
+
1081
+ function searchEntitiesForContext(name, limit) {
1082
+ const results = [];
1083
+ const exact = brain.findEntity(name);
1084
+ if (exact) results.push(exact);
1085
+ if (results.length === 0) {
1086
+ const fuzzy = brain.findEntityFuzzy(name);
1087
+ if (fuzzy) results.push(fuzzy);
1088
+ }
1089
+ if (results.length === 0) {
1090
+ const lower = String(name || '').toLowerCase();
1091
+ results.push(...brain.listEntities({ limit }).filter(entity =>
1092
+ String(entity.canonical_name || '').toLowerCase().includes(lower)
1093
+ ));
1094
+ }
1095
+ return results.slice(0, limit).map(entity => ({
1096
+ id: entity.id,
1097
+ name: entity.canonical_name,
1098
+ type: entity.entity_type,
1099
+ aliases: entity.aliases,
1100
+ }));
1101
+ }
1102
+
511
1103
  function clampLimit(value, fallback, max) {
512
1104
  const n = Number(value || fallback);
513
1105
  if (!Number.isFinite(n)) return fallback;
514
1106
  return Math.min(Math.max(Math.trunc(n), 1), max);
515
1107
  }
516
1108
 
1109
+ function formatMemoryResult(memory) {
1110
+ return {
1111
+ id: memory.id,
1112
+ source: memory.source,
1113
+ source_id: memory.source_id,
1114
+ memory_type: memory.memory_type,
1115
+ content: memory.content,
1116
+ timestamp: memory.timestamp,
1117
+ };
1118
+ }
1119
+
1120
+ function requireWriteConfirmation(toolName, args = {}) {
1121
+ if (args.dry_run === true || args.confirm_write === true) return;
1122
+ throw new Error(`${toolName} writes Wall-E or CTM state. Run with dry_run=true first, or set confirm_write=true to apply changes.`);
1123
+ }
1124
+
1125
+ function stripRank(item) {
1126
+ if (!item || typeof item !== 'object') return item;
1127
+ const { rank, ...rest } = item;
1128
+ return rest;
1129
+ }
1130
+
1131
+ async function getCtmDbStatus() {
1132
+ const health = await ctmContextClient.getHealth();
1133
+ return {
1134
+ available: !health.unavailable,
1135
+ db_path: health.db_path,
1136
+ active_source: health.active_source || '',
1137
+ fallback_used: Boolean(health.fallback_used),
1138
+ serving_priority: health.source === 'ctm-api' ? 'ctm-api-first' : 'ctm-db-first',
1139
+ source: health.source || 'ctm-db',
1140
+ backend_source: health.backend_source || health.source || 'ctm-db',
1141
+ api_transport: health.api_transport || null,
1142
+ schema_ok: Boolean(health.ok),
1143
+ reason: health.reason || '',
1144
+ error: health.error || '',
1145
+ cached_tables: health.tables || [],
1146
+ expected_tables: ctmSessionContext.CTM_SESSION_TABLES || [],
1147
+ missing_tables: health.missing_tables || [],
1148
+ table_counts: health.table_counts || {},
1149
+ attempts: health.attempts || [],
1150
+ };
1151
+ }
1152
+
1153
+ async function safeCtmSearchSessions(args) {
1154
+ try {
1155
+ return await ctmContextClient.searchSessions(args);
1156
+ } catch (err) {
1157
+ return { ok: false, source: 'ctm-db', results: [], reason: err.message };
1158
+ }
1159
+ }
1160
+
1161
+ async function safeCtmSessionContext(args) {
1162
+ try {
1163
+ return await ctmContextClient.getSessionContext(args);
1164
+ } catch (err) {
1165
+ return { ok: false, source: 'ctm-db', sessions: [], messages: [], reason: err.message };
1166
+ }
1167
+ }
1168
+
1169
+ async function safeCtmContextPack(args) {
1170
+ try {
1171
+ return await ctmContextClient.buildContextPack(args);
1172
+ } catch (err) {
1173
+ return { ok: false, source: 'ctm-db', sessions: [], messages: [], reason: err.message };
1174
+ }
1175
+ }
1176
+
517
1177
  function assertJsonlPath(filePath) {
518
1178
  if (!filePath || typeof filePath !== 'string') throw new Error('uri must be a JSONL file path');
519
1179
  if (filePath.split(/[\\/]+/).includes('..')) throw new Error('uri cannot contain traversal segments');
@@ -748,6 +1408,137 @@ function jsonResponse(res, data, status = 200) {
748
1408
  res.end(body);
749
1409
  }
750
1410
 
1411
+ async function readMcpResource(uri) {
1412
+ if (!uri || typeof uri !== 'string') throw new Error('resource uri is required');
1413
+
1414
+ if (uri === 'walle://status/session-memory') {
1415
+ return {
1416
+ uri,
1417
+ mimeType: 'application/json',
1418
+ text: JSON.stringify(await executeTool('walle_memory_status', {})),
1419
+ };
1420
+ }
1421
+
1422
+ let parsed;
1423
+ try {
1424
+ parsed = new URL(uri);
1425
+ } catch {
1426
+ throw new Error(`Invalid resource uri: ${uri}`);
1427
+ }
1428
+
1429
+ if (parsed.protocol === 'walle-session:' && parsed.hostname === 'ctm') {
1430
+ const [rawSessionId, rawMode] = parsed.pathname.split('/').filter(Boolean);
1431
+ if (!rawSessionId) throw new Error('walle-session resource requires a session id');
1432
+ const sessionId = decodeURIComponent(rawSessionId);
1433
+ const mode = rawMode || 'full';
1434
+ const context = await ctmContextClient.getSessionContext({
1435
+ session_id: sessionId,
1436
+ limit: mode === 'full' || mode === 'messages' ? 0 : 200,
1437
+ format: mode === 'compact' || mode === 'summary' ? 'compact' : 'messages',
1438
+ dedupe: true,
1439
+ });
1440
+ if (mode === 'compact' || mode === 'summary') {
1441
+ return {
1442
+ uri,
1443
+ mimeType: 'text/markdown',
1444
+ text: context.text || ctmSessionContext.renderContextMarkdown(context, { compact: true }),
1445
+ };
1446
+ }
1447
+ return {
1448
+ uri,
1449
+ mimeType: 'application/json',
1450
+ text: JSON.stringify(context),
1451
+ };
1452
+ }
1453
+
1454
+ if (parsed.protocol === 'walle-context:' && parsed.hostname === 'task') {
1455
+ const task = decodeURIComponent(parsed.pathname.replace(/^\/+/, ''));
1456
+ if (!task) throw new Error('walle-context task resource requires an encoded task query');
1457
+ const pack = await ctmContextClient.buildContextPack({ task, mode: 'compact' });
1458
+ return {
1459
+ uri,
1460
+ mimeType: 'text/markdown',
1461
+ text: pack.text || ctmSessionContext.renderContextMarkdown(pack, { compact: true }),
1462
+ };
1463
+ }
1464
+
1465
+ throw new Error(`Unknown resource uri: ${uri}`);
1466
+ }
1467
+
1468
+ function memoryRoutingPromptText(wallePort) {
1469
+ return wallEAgentMemoryInstructions(wallePort)
1470
+ .replace(/<!-- wall-e-memory-routing:start -->\n?/g, '')
1471
+ .replace(/<!-- wall-e-memory-routing:end -->\n?/g, '')
1472
+ .trim();
1473
+ }
1474
+
1475
+ function readMcpPrompt(name, args = {}) {
1476
+ if (name === 'walle_memory_routing') {
1477
+ const port = Number(args.walle_port || process.env.WALL_E_PORT || 3457);
1478
+ return {
1479
+ description: 'Wall-E MCP memory routing policy',
1480
+ messages: [{
1481
+ role: 'user',
1482
+ content: { type: 'text', text: memoryRoutingPromptText(port) },
1483
+ }],
1484
+ };
1485
+ }
1486
+ if (name === 'walle_coding_resume') {
1487
+ const task = String(args.task || '').trim();
1488
+ const sessionId = String(args.session_id || '').trim();
1489
+ const lines = [
1490
+ 'Resume this coding work using Wall-E MCP session memory before relying on web search or raw files.',
1491
+ '',
1492
+ 'Steps:',
1493
+ '1. If Wall-E health is uncertain, call `walle_memory_status`.',
1494
+ '2. Call `walle_context_pack` with the current task to recover prior attempts, decisions, changed files, and next steps.',
1495
+ '3. If a session id is provided, call `walle_get_session` for exact source context.',
1496
+ '4. Use `walle_search_sessions` for targeted follow-up searches and `walle_diary_read` for recent handoff notes.',
1497
+ '5. Cite returned session ids/timestamps when using retrieved context. Use public web only for public/current facts or after Wall-E misses.',
1498
+ ];
1499
+ if (task) lines.push('', `Task: ${task}`);
1500
+ if (sessionId) lines.push(`Session id: ${sessionId}`);
1501
+ return {
1502
+ description: 'Resume coding work with Wall-E CTM session context',
1503
+ messages: [{
1504
+ role: 'user',
1505
+ content: { type: 'text', text: lines.join('\n') },
1506
+ }],
1507
+ };
1508
+ }
1509
+ return null;
1510
+ }
1511
+
1512
+ function buildToolResourceLinks(toolName, result) {
1513
+ if (!result || typeof result !== 'object') return [];
1514
+ const links = [];
1515
+ const addSession = (sessionId, title) => {
1516
+ if (!sessionId || links.some(link => link.uri.includes(encodeURIComponent(sessionId)))) return;
1517
+ links.push({
1518
+ type: 'resource_link',
1519
+ uri: `walle-session://ctm/${encodeURIComponent(sessionId)}/compact`,
1520
+ name: title || `CTM session ${sessionId}`,
1521
+ description: 'Compact CTM session context linked from Wall-E tool output',
1522
+ mimeType: 'text/markdown',
1523
+ annotations: { audience: ['assistant'], priority: 0.85 },
1524
+ });
1525
+ };
1526
+
1527
+ if (toolName === 'walle_search_sessions') {
1528
+ for (const item of result.results || []) {
1529
+ if (item.source_layer === 'ctm-db' || item.source_layer === 'ctm-api') {
1530
+ addSession(item.session_id, item.title);
1531
+ }
1532
+ }
1533
+ }
1534
+ if (toolName === 'walle_context_pack') {
1535
+ for (const item of result.sessions || []) {
1536
+ addSession(item.session_id || item.id, item.title);
1537
+ }
1538
+ }
1539
+ return links.slice(0, 5);
1540
+ }
1541
+
751
1542
  async function handleMcp(req, res) {
752
1543
  let msg;
753
1544
  try {
@@ -775,7 +1566,7 @@ async function handleMcp(req, res) {
775
1566
  jsonrpc: '2.0', id,
776
1567
  result: {
777
1568
  protocolVersion: PROTOCOL_VERSION,
778
- capabilities: { tools: {} },
1569
+ capabilities: { tools: {}, resources: {}, prompts: {} },
779
1570
  serverInfo: { name: 'wall-e', version: (() => { try { return require('./package.json').version; } catch { return '0.0.0'; } })() },
780
1571
  },
781
1572
  });
@@ -788,6 +1579,56 @@ async function handleMcp(req, res) {
788
1579
  });
789
1580
  }
790
1581
 
1582
+ case 'resources/list': {
1583
+ return jsonResponse(res, {
1584
+ jsonrpc: '2.0', id,
1585
+ result: { resources: MCP_RESOURCES },
1586
+ });
1587
+ }
1588
+
1589
+ case 'resources/templates/list': {
1590
+ return jsonResponse(res, {
1591
+ jsonrpc: '2.0', id,
1592
+ result: { resourceTemplates: MCP_RESOURCE_TEMPLATES },
1593
+ });
1594
+ }
1595
+
1596
+ case 'prompts/list': {
1597
+ return jsonResponse(res, {
1598
+ jsonrpc: '2.0', id,
1599
+ result: { prompts: MCP_PROMPTS },
1600
+ });
1601
+ }
1602
+
1603
+ case 'prompts/get': {
1604
+ const prompt = readMcpPrompt(params?.name, params?.arguments || {});
1605
+ if (!prompt) {
1606
+ return jsonResponse(res, {
1607
+ jsonrpc: '2.0', id,
1608
+ result: { content: [{ type: 'text', text: `Unknown prompt: ${params?.name}` }], isError: true },
1609
+ });
1610
+ }
1611
+ return jsonResponse(res, {
1612
+ jsonrpc: '2.0', id,
1613
+ result: prompt,
1614
+ });
1615
+ }
1616
+
1617
+ case 'resources/read': {
1618
+ try {
1619
+ const content = await readMcpResource(params?.uri);
1620
+ return jsonResponse(res, {
1621
+ jsonrpc: '2.0', id,
1622
+ result: { contents: [{ uri: content.uri, mimeType: content.mimeType, text: content.text }] },
1623
+ });
1624
+ } catch (err) {
1625
+ return jsonResponse(res, {
1626
+ jsonrpc: '2.0', id,
1627
+ result: { contents: [{ uri: params?.uri || '', mimeType: 'text/plain', text: `Error: ${err.message}` }], isError: true },
1628
+ });
1629
+ }
1630
+ }
1631
+
791
1632
  case 'tools/call': {
792
1633
  const toolName = params?.name;
793
1634
  const toolArgs = params?.arguments || {};
@@ -799,9 +1640,10 @@ async function handleMcp(req, res) {
799
1640
  result: { content: [{ type: 'text', text: `Unknown tool: ${toolName}` }], isError: true },
800
1641
  });
801
1642
  }
1643
+ const content = [{ type: 'text', text: JSON.stringify(result) }].concat(buildToolResourceLinks(toolName, result));
802
1644
  return jsonResponse(res, {
803
1645
  jsonrpc: '2.0', id,
804
- result: { content: [{ type: 'text', text: JSON.stringify(result) }] },
1646
+ result: { content, structuredContent: result },
805
1647
  });
806
1648
  } catch (err) {
807
1649
  return jsonResponse(res, {
@@ -819,4 +1661,15 @@ async function handleMcp(req, res) {
819
1661
  }
820
1662
  }
821
1663
 
822
- module.exports = { handleMcp, MCP_TOOLS, executeTool };
1664
+ module.exports = {
1665
+ handleMcp,
1666
+ MCP_TOOLS,
1667
+ MCP_RESOURCES,
1668
+ MCP_RESOURCE_TEMPLATES,
1669
+ MCP_PROMPTS,
1670
+ executeTool,
1671
+ lookupWallEContext,
1672
+ readMcpPrompt,
1673
+ readMcpResource,
1674
+ routeWallEQuery,
1675
+ };