create-walle 0.1.0

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 (136) hide show
  1. package/bin/create-walle.js +134 -0
  2. package/package.json +18 -0
  3. package/template/.env.example +40 -0
  4. package/template/CLAUDE.md +12 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +167 -0
  7. package/template/bin/setup.js +100 -0
  8. package/template/claude-code-skill.md +60 -0
  9. package/template/claude-task-manager/api-prompts.js +1841 -0
  10. package/template/claude-task-manager/api-reviews.js +275 -0
  11. package/template/claude-task-manager/approval-agent.js +454 -0
  12. package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
  13. package/template/claude-task-manager/db.js +1721 -0
  14. package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
  15. package/template/claude-task-manager/git-utils.js +214 -0
  16. package/template/claude-task-manager/package-lock.json +1607 -0
  17. package/template/claude-task-manager/package.json +31 -0
  18. package/template/claude-task-manager/prompt-harvest.js +1148 -0
  19. package/template/claude-task-manager/public/css/prompts.css +880 -0
  20. package/template/claude-task-manager/public/css/reviews.css +430 -0
  21. package/template/claude-task-manager/public/css/walle.css +732 -0
  22. package/template/claude-task-manager/public/favicon.ico +0 -0
  23. package/template/claude-task-manager/public/icon.svg +37 -0
  24. package/template/claude-task-manager/public/index.html +8346 -0
  25. package/template/claude-task-manager/public/js/prompts.js +3159 -0
  26. package/template/claude-task-manager/public/js/reviews.js +1292 -0
  27. package/template/claude-task-manager/public/js/walle.js +3081 -0
  28. package/template/claude-task-manager/public/manifest.json +13 -0
  29. package/template/claude-task-manager/public/prompts.html +4353 -0
  30. package/template/claude-task-manager/public/setup.html +216 -0
  31. package/template/claude-task-manager/queue-engine.js +404 -0
  32. package/template/claude-task-manager/server-state.js +5 -0
  33. package/template/claude-task-manager/server.js +2254 -0
  34. package/template/claude-task-manager/session-utils.js +124 -0
  35. package/template/claude-task-manager/start.sh +17 -0
  36. package/template/claude-task-manager/tests/test-ai-search.js +61 -0
  37. package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
  38. package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
  39. package/template/claude-task-manager/tests/test-features-v2.js +127 -0
  40. package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
  41. package/template/claude-task-manager/tests/test-insights.js +124 -0
  42. package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
  43. package/template/claude-task-manager/tests/test-permissions.js +122 -0
  44. package/template/claude-task-manager/tests/test-pin.js +51 -0
  45. package/template/claude-task-manager/tests/test-prompts.js +164 -0
  46. package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
  47. package/template/claude-task-manager/tests/test-review.js +104 -0
  48. package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
  49. package/template/claude-task-manager/tests/test-send-final.js +30 -0
  50. package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
  51. package/template/claude-task-manager/tests/test-send-integration.js +107 -0
  52. package/template/claude-task-manager/tests/test-send-visual.js +34 -0
  53. package/template/claude-task-manager/tests/test-session-create.js +147 -0
  54. package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
  55. package/template/claude-task-manager/tests/test-url-hash.js +68 -0
  56. package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
  57. package/template/claude-task-manager/tests/test-ux-review.js +130 -0
  58. package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
  59. package/template/claude-task-manager/tests/test-zoom.js +92 -0
  60. package/template/claude-task-manager/tests/test-zoom2.js +67 -0
  61. package/template/docs/site/api/README.md +187 -0
  62. package/template/docs/site/guides/claude-code.md +58 -0
  63. package/template/docs/site/guides/configuration.md +96 -0
  64. package/template/docs/site/guides/quickstart.md +158 -0
  65. package/template/docs/site/index.md +14 -0
  66. package/template/docs/site/skills/README.md +135 -0
  67. package/template/wall-e/.dockerignore +11 -0
  68. package/template/wall-e/Dockerfile +25 -0
  69. package/template/wall-e/adapters/adapter-base.js +37 -0
  70. package/template/wall-e/adapters/ctm.js +193 -0
  71. package/template/wall-e/adapters/slack.js +56 -0
  72. package/template/wall-e/agent.js +319 -0
  73. package/template/wall-e/api-walle.js +1073 -0
  74. package/template/wall-e/brain.js +1235 -0
  75. package/template/wall-e/channels/agent-api.js +172 -0
  76. package/template/wall-e/channels/channel-base.js +14 -0
  77. package/template/wall-e/channels/imessage-channel.js +113 -0
  78. package/template/wall-e/channels/slack-channel.js +118 -0
  79. package/template/wall-e/chat.js +778 -0
  80. package/template/wall-e/decision/confidence.js +93 -0
  81. package/template/wall-e/deploy.sh +35 -0
  82. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
  83. package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
  84. package/template/wall-e/extraction/contradiction.js +168 -0
  85. package/template/wall-e/extraction/knowledge-extractor.js +190 -0
  86. package/template/wall-e/fly.toml +24 -0
  87. package/template/wall-e/loops/ingest.js +34 -0
  88. package/template/wall-e/loops/reflect.js +63 -0
  89. package/template/wall-e/loops/tasks.js +487 -0
  90. package/template/wall-e/loops/think.js +125 -0
  91. package/template/wall-e/package-lock.json +533 -0
  92. package/template/wall-e/package.json +18 -0
  93. package/template/wall-e/scripts/ingest-slack-search.js +85 -0
  94. package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
  95. package/template/wall-e/scripts/slack-backfill.js +295 -0
  96. package/template/wall-e/scripts/slack-channel-history.js +454 -0
  97. package/template/wall-e/server.js +93 -0
  98. package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
  99. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
  100. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
  101. package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
  102. package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
  103. package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
  104. package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
  105. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
  106. package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
  107. package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
  108. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
  109. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
  110. package/template/wall-e/skills/claude-code-reader.js +144 -0
  111. package/template/wall-e/skills/mcp-client.js +407 -0
  112. package/template/wall-e/skills/skill-executor.js +163 -0
  113. package/template/wall-e/skills/skill-loader.js +410 -0
  114. package/template/wall-e/skills/skill-planner.js +88 -0
  115. package/template/wall-e/skills/slack-ingest.js +329 -0
  116. package/template/wall-e/skills/slack-pull-live.js +270 -0
  117. package/template/wall-e/skills/tool-executor.js +188 -0
  118. package/template/wall-e/tests/adapter-base.test.js +20 -0
  119. package/template/wall-e/tests/adapter-ctm.test.js +122 -0
  120. package/template/wall-e/tests/adapter-slack.test.js +98 -0
  121. package/template/wall-e/tests/agent-api.test.js +256 -0
  122. package/template/wall-e/tests/api-walle.test.js +222 -0
  123. package/template/wall-e/tests/brain.test.js +602 -0
  124. package/template/wall-e/tests/channels.test.js +104 -0
  125. package/template/wall-e/tests/chat.test.js +103 -0
  126. package/template/wall-e/tests/confidence.test.js +134 -0
  127. package/template/wall-e/tests/contradiction.test.js +217 -0
  128. package/template/wall-e/tests/ingest.test.js +113 -0
  129. package/template/wall-e/tests/mcp-client.test.js +71 -0
  130. package/template/wall-e/tests/reflect.test.js +103 -0
  131. package/template/wall-e/tests/server.test.js +111 -0
  132. package/template/wall-e/tests/skills.test.js +198 -0
  133. package/template/wall-e/tests/slack-ingest.test.js +103 -0
  134. package/template/wall-e/tests/think.test.js +435 -0
  135. package/template/wall-e/tools/local-tools.js +697 -0
  136. package/template/wall-e/tools/slack-mcp.js +290 -0
@@ -0,0 +1,1073 @@
1
+ 'use strict';
2
+ const Database = require('better-sqlite3');
3
+ const path = require('path');
4
+
5
+ const DATA_DIR = process.env.WALL_E_DATA_DIR || path.join(process.env.HOME, '.walle', 'data');
6
+ const BRAIN_DB_PATH = path.join(DATA_DIR, 'wall-e-brain.db');
7
+
8
+ let readDb = null;
9
+
10
+ function getReadDb() {
11
+ if (!readDb) {
12
+ try {
13
+ readDb = new Database(BRAIN_DB_PATH, { readonly: true, fileMustExist: true });
14
+ readDb.pragma('journal_mode = WAL');
15
+ } catch (err) {
16
+ return null;
17
+ }
18
+ }
19
+ return readDb;
20
+ }
21
+
22
+ let brain = null;
23
+ try { brain = require('./brain'); } catch {}
24
+
25
+ function ensureBrainInit() {
26
+ if (!brain) return false;
27
+ try { brain.getDb(); } catch {
28
+ try { brain.initDb(); } catch { return false; }
29
+ }
30
+ return true;
31
+ }
32
+
33
+ // --- Helpers ---
34
+
35
+ function jsonResponse(res, data, status = 200) {
36
+ const body = JSON.stringify(data);
37
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
38
+ res.end(body);
39
+ }
40
+
41
+ function readBody(req) {
42
+ return new Promise((resolve, reject) => {
43
+ const chunks = [];
44
+ let size = 0;
45
+ req.on('data', chunk => {
46
+ size += chunk.length;
47
+ if (size > 1024 * 1024) { req.destroy(); reject(new Error('Body too large')); return; }
48
+ chunks.push(chunk);
49
+ });
50
+ req.on('end', () => {
51
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
52
+ catch { resolve({}); }
53
+ });
54
+ req.on('error', reject);
55
+ });
56
+ }
57
+
58
+ function parseLimit(params, defaultVal = 50) {
59
+ return Math.min(Math.max(parseInt(params.get('limit')) || defaultVal, 1), 500);
60
+ }
61
+
62
+ function parseOffset(params) {
63
+ return Math.max(parseInt(params.get('offset')) || 0, 0);
64
+ }
65
+
66
+ // --- Individual route handlers (exported for testability) ---
67
+
68
+ function getStatus() {
69
+ const db = getReadDb();
70
+ if (!db) return { error: 'Brain database not available' };
71
+
72
+ const ownerRow = db.prepare('SELECT value FROM owner WHERE key = ?').get('name');
73
+ const owner = ownerRow ? ownerRow.value : null;
74
+
75
+ const memory_count = db.prepare('SELECT count(*) as cnt FROM memories').get().cnt;
76
+ const knowledge_count = db.prepare('SELECT count(*) as cnt FROM knowledge').get().cnt;
77
+ const people_count = db.prepare('SELECT count(*) as cnt FROM people').get().cnt;
78
+ const pattern_count = db.prepare('SELECT count(*) as cnt FROM patterns').get().cnt;
79
+ const pending_question_count = db.prepare("SELECT count(*) as cnt FROM pending_questions WHERE status = 'pending'").get().cnt;
80
+
81
+ return {
82
+ owner,
83
+ stats: { memory_count, knowledge_count, people_count, pattern_count, pending_question_count }
84
+ };
85
+ }
86
+
87
+ function getKnowledgeList(params) {
88
+ const db = getReadDb();
89
+ if (!db) return [];
90
+
91
+ const limit = parseLimit(params);
92
+ const offset = parseOffset(params);
93
+ const subject = params.get('subject');
94
+ const category = params.get('category');
95
+ const status = params.get('status');
96
+
97
+ const conditions = [];
98
+ const values = [];
99
+
100
+ if (subject) { conditions.push('subject = ?'); values.push(subject); }
101
+ if (category) { conditions.push('category = ?'); values.push(category); }
102
+ if (status) { conditions.push('status = ?'); values.push(status); }
103
+
104
+ let sql = 'SELECT * FROM knowledge';
105
+ if (conditions.length > 0) sql += ' WHERE ' + conditions.join(' AND ');
106
+ sql += ' ORDER BY last_confirmed DESC LIMIT ? OFFSET ?';
107
+ values.push(limit, offset);
108
+
109
+ return db.prepare(sql).all(...values);
110
+ }
111
+
112
+ function getMemoriesList(params) {
113
+ const db = getReadDb();
114
+ if (!db) return [];
115
+
116
+ const limit = parseLimit(params);
117
+ const offset = parseOffset(params);
118
+ const source = params.get('source');
119
+ const since = params.get('since');
120
+
121
+ const conditions = [];
122
+ const values = [];
123
+
124
+ if (source) { conditions.push('source = ?'); values.push(source); }
125
+ if (since) { conditions.push('timestamp >= ?'); values.push(since); }
126
+
127
+ let sql = 'SELECT * FROM memories';
128
+ if (conditions.length > 0) sql += ' WHERE ' + conditions.join(' AND ');
129
+ sql += ' ORDER BY timestamp DESC LIMIT ? OFFSET ?';
130
+ values.push(limit, offset);
131
+
132
+ return db.prepare(sql).all(...values);
133
+ }
134
+
135
+ function getPeopleList() {
136
+ const db = getReadDb();
137
+ if (!db) return [];
138
+ return db.prepare('SELECT * FROM people ORDER BY name').all();
139
+ }
140
+
141
+ function getTimeline(params) {
142
+ const db = getReadDb();
143
+ if (!db) return [];
144
+
145
+ const limit = parseLimit(params, 100);
146
+ const offset = parseOffset(params);
147
+
148
+ return db.prepare('SELECT * FROM memories ORDER BY timestamp DESC LIMIT ? OFFSET ?').all(limit, offset);
149
+ }
150
+
151
+ function getQuestionsList(params) {
152
+ const db = getReadDb();
153
+ if (!db) return [];
154
+
155
+ const status = params.get('status') || 'pending';
156
+ return db.prepare('SELECT * FROM pending_questions WHERE status = ? ORDER BY created_at DESC').all(status);
157
+ }
158
+
159
+ function getStats() {
160
+ const db = getReadDb();
161
+ if (!db) return { error: 'Brain database not available' };
162
+
163
+ const memory_count = db.prepare('SELECT count(*) as cnt FROM memories').get().cnt;
164
+ const knowledge_count = db.prepare('SELECT count(*) as cnt FROM knowledge').get().cnt;
165
+ const people_count = db.prepare('SELECT count(*) as cnt FROM people').get().cnt;
166
+ const pattern_count = db.prepare('SELECT count(*) as cnt FROM patterns').get().cnt;
167
+ const pending_question_count = db.prepare("SELECT count(*) as cnt FROM pending_questions WHERE status = 'pending'").get().cnt;
168
+
169
+ return { memory_count, knowledge_count, people_count, pattern_count, pending_question_count };
170
+ }
171
+
172
+ function getBrief(params) {
173
+ const db = getReadDb();
174
+ if (!db) return null;
175
+
176
+ const date = params.get('date') || new Date().toISOString().slice(0, 10);
177
+ return db.prepare('SELECT * FROM daily_summaries WHERE date = ?').get(date) || null;
178
+ }
179
+
180
+ // --- Main Route Handler ---
181
+
182
+ function handleWalleApi(req, res, url) {
183
+ const p = url.pathname;
184
+ const m = req.method;
185
+ const params = url.searchParams;
186
+
187
+ // Only handle /api/wall-e/* routes
188
+ if (!p.startsWith('/api/wall-e')) return false;
189
+
190
+ // GET /api/wall-e/health — simple liveness check
191
+ if (p === '/api/wall-e/health' && m === 'GET') {
192
+ return jsonResponse(res, { status: 'ok', uptime: process.uptime(), version: '0.1.0' }), true;
193
+ }
194
+
195
+ // GET /api/wall-e/slack/status — check Slack OAuth status
196
+ if (p === '/api/wall-e/slack/status' && m === 'GET') {
197
+ try {
198
+ const slackMcp = require('./tools/slack-mcp');
199
+ const token = slackMcp.loadToken();
200
+ jsonResponse(res, { data: { authenticated: !!token?.access_token, team: token?.team_name, user: token?.user_id, obtained_at: token?.obtained_at } });
201
+ } catch (e) {
202
+ jsonResponse(res, { data: { authenticated: false } });
203
+ }
204
+ return true;
205
+ }
206
+
207
+ // POST /api/wall-e/slack/auth — start OAuth flow (opens browser)
208
+ if (p === '/api/wall-e/slack/auth' && m === 'POST') {
209
+ try {
210
+ const slackMcp = require('./tools/slack-mcp');
211
+ slackMcp.authenticate().then(token => {
212
+ console.log('[wall-e] Slack OAuth completed');
213
+ }).catch(err => {
214
+ console.error('[wall-e] Slack OAuth failed:', err.message);
215
+ });
216
+ jsonResponse(res, { data: { message: 'OAuth flow started — check your browser' } });
217
+ } catch (e) {
218
+ jsonResponse(res, { error: e.message }, 500);
219
+ }
220
+ return true;
221
+ }
222
+
223
+ // GET /api/wall-e/status
224
+ if (p === '/api/wall-e/status' && m === 'GET') {
225
+ const result = getStatus();
226
+ if (result.error) return jsonResponse(res, { error: result.error }, 503);
227
+
228
+ // Enrich with daemon/loop health info
229
+ const db = getReadDb();
230
+ let checkpoints = {};
231
+ if (db) {
232
+ try {
233
+ const rows = db.prepare('SELECT * FROM loop_checkpoints').all();
234
+ for (const row of rows) {
235
+ checkpoints[row.loop_name] = { last_run_at: row.last_run_at, updated_at: row.updated_at };
236
+ }
237
+ } catch {}
238
+ }
239
+
240
+ // Adapter statuses (best-effort from config)
241
+ let adapterStatuses = [];
242
+ try {
243
+ const fs = require('fs');
244
+ const cfgPath = require('path').join(__dirname, 'wall-e-config.json');
245
+ if (fs.existsSync(cfgPath)) {
246
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
247
+ if (cfg.adapters) {
248
+ for (const [name, conf] of Object.entries(cfg.adapters)) {
249
+ adapterStatuses.push({ name, enabled: conf.enabled !== false });
250
+ }
251
+ }
252
+ }
253
+ } catch {}
254
+
255
+ return jsonResponse(res, {
256
+ data: {
257
+ ...result,
258
+ version: '0.1.0',
259
+ uptime: process.uptime(),
260
+ loop_health: checkpoints,
261
+ adapters: adapterStatuses,
262
+ }
263
+ }), true;
264
+ }
265
+
266
+ // GET /api/wall-e/knowledge
267
+ if (p === '/api/wall-e/knowledge' && m === 'GET') {
268
+ const data = getKnowledgeList(params);
269
+ return jsonResponse(res, { data }), true;
270
+ }
271
+
272
+ // GET /api/wall-e/memories
273
+ if (p === '/api/wall-e/memories' && m === 'GET') {
274
+ const data = getMemoriesList(params);
275
+ return jsonResponse(res, { data }), true;
276
+ }
277
+
278
+ // GET /api/wall-e/people
279
+ if (p === '/api/wall-e/people' && m === 'GET') {
280
+ const data = getPeopleList();
281
+ return jsonResponse(res, { data }), true;
282
+ }
283
+
284
+ // GET /api/wall-e/timeline
285
+ if (p === '/api/wall-e/timeline' && m === 'GET') {
286
+ const data = getTimeline(params);
287
+ return jsonResponse(res, { data }), true;
288
+ }
289
+
290
+ // GET /api/wall-e/questions
291
+ if (p === '/api/wall-e/questions' && m === 'GET') {
292
+ const data = getQuestionsList(params);
293
+ return jsonResponse(res, { data }), true;
294
+ }
295
+
296
+ // POST /api/wall-e/questions/:id/answer
297
+ const answerMatch = p.match(/^\/api\/wall-e\/questions\/([^/]+)\/answer$/);
298
+ if (answerMatch && m === 'POST') {
299
+ if (!brain) return jsonResponse(res, { error: 'Brain module not available for writes' }, 503), true;
300
+ readBody(req).then(body => {
301
+ try {
302
+ brain.answerQuestion(answerMatch[1], {
303
+ answer: body.answer,
304
+ resolution_type: body.resolution_type
305
+ });
306
+ jsonResponse(res, { data: { ok: true } });
307
+ } catch (e) {
308
+ jsonResponse(res, { error: e.message }, 400);
309
+ }
310
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
311
+ return true;
312
+ }
313
+
314
+ // GET /api/wall-e/brief
315
+ if (p === '/api/wall-e/brief' && m === 'GET') {
316
+ const data = getBrief(params);
317
+ return jsonResponse(res, { data }), true;
318
+ }
319
+
320
+ // GET /api/wall-e/stats
321
+ if (p === '/api/wall-e/stats' && m === 'GET') {
322
+ const result = getStats();
323
+ if (result.error) return jsonResponse(res, { error: result.error }, 503), true;
324
+ return jsonResponse(res, { data: result }), true;
325
+ }
326
+
327
+ // PUT /api/wall-e/knowledge/:id
328
+ const knowledgeMatch = p.match(/^\/api\/wall-e\/knowledge\/([^/]+)$/);
329
+ if (knowledgeMatch && m === 'PUT') {
330
+ if (!brain) return jsonResponse(res, { error: 'Brain module not available for writes' }, 503), true;
331
+ readBody(req).then(body => {
332
+ try {
333
+ brain.updateKnowledgeStatus(knowledgeMatch[1], body.status);
334
+ jsonResponse(res, { data: { ok: true } });
335
+ } catch (e) {
336
+ jsonResponse(res, { error: e.message }, 400);
337
+ }
338
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
339
+ return true;
340
+ }
341
+
342
+ // POST /api/wall-e/chat — supports SSE streaming via Accept header or ?stream=1
343
+ if (p === '/api/wall-e/chat' && m === 'POST') {
344
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
345
+ const wantsStream = params.get('stream') === '1' || (req.headers.accept || '').includes('text/event-stream');
346
+ readBody(req).then(async body => {
347
+ if (!body.message) {
348
+ jsonResponse(res, { error: 'message is required' }, 400);
349
+ return;
350
+ }
351
+ try {
352
+ const chatModule = require('./chat');
353
+ if (wantsStream) {
354
+ // SSE streaming mode — send progress events in real-time
355
+ res.writeHead(200, {
356
+ 'Content-Type': 'text/event-stream',
357
+ 'Cache-Control': 'no-cache',
358
+ 'Connection': 'keep-alive',
359
+ });
360
+ const sendEvent = (event) => {
361
+ try { res.write('data: ' + JSON.stringify(event) + '\n\n'); } catch {}
362
+ };
363
+ const result = await chatModule.chat(body.message, {
364
+ channel: body.channel || 'ctm',
365
+ session_id: body.session_id || 'default',
366
+ onProgress: sendEvent,
367
+ });
368
+ sendEvent({ type: 'done', reply: result.reply });
369
+ res.end();
370
+ } else {
371
+ // Classic JSON mode (backwards compatible)
372
+ const result = await chatModule.chat(body.message, {
373
+ channel: body.channel || 'ctm',
374
+ session_id: body.session_id || 'default',
375
+ });
376
+ jsonResponse(res, { data: result });
377
+ }
378
+ } catch (err) {
379
+ if (wantsStream) {
380
+ try { res.write('data: ' + JSON.stringify({ type: 'error', error: err.message }) + '\n\n'); } catch {}
381
+ try { res.end(); } catch {}
382
+ } else {
383
+ jsonResponse(res, { error: err.message }, 500);
384
+ }
385
+ }
386
+ }).catch(e => {
387
+ if (wantsStream) {
388
+ try { res.write('data: ' + JSON.stringify({ type: 'error', error: e.message }) + '\n\n'); } catch {}
389
+ try { res.end(); } catch {}
390
+ } else {
391
+ jsonResponse(res, { error: e.message }, 400);
392
+ }
393
+ });
394
+ return true;
395
+ }
396
+
397
+ // GET /api/wall-e/chat/history
398
+ if (p === '/api/wall-e/chat/history' && m === 'GET') {
399
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
400
+ try {
401
+ const sessionId = params.get('session_id') || 'default';
402
+ const limit = parseLimit(params, 100);
403
+ const data = brain.listChatMessages({ session_id: sessionId, limit });
404
+ jsonResponse(res, { data });
405
+ } catch (e) {
406
+ jsonResponse(res, { error: e.message }, 500);
407
+ }
408
+ return true;
409
+ }
410
+
411
+ // POST /api/wall-e/chat/delete
412
+ if (p === '/api/wall-e/chat/delete' && m === 'POST') {
413
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
414
+ readBody(req).then(body => {
415
+ try {
416
+ brain.deleteChatMessages({
417
+ session_id: body.session_id || 'default',
418
+ user_content: body.user_content,
419
+ assistant_content: body.assistant_content,
420
+ });
421
+ jsonResponse(res, { data: { ok: true } });
422
+ } catch (e) {
423
+ jsonResponse(res, { error: e.message }, 500);
424
+ }
425
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
426
+ return true;
427
+ }
428
+
429
+ // POST /api/wall-e/tasks/:id/stop — proxy to WALL-E daemon to kill running process
430
+ const taskStopMatch = p.match(/^\/api\/wall-e\/tasks\/([^/]+)\/stop$/);
431
+ if (taskStopMatch && m === 'POST') {
432
+ const daemonPort = process.env.WALL_E_PORT || 3457;
433
+ fetch(`http://localhost:${daemonPort}/api/wall-e/tasks/${taskStopMatch[1]}/stop`, { method: 'POST' })
434
+ .then(resp => resp.json())
435
+ .then(data => jsonResponse(res, data))
436
+ .catch(() => jsonResponse(res, { data: { stopped: false, error: 'daemon_offline' } }));
437
+ return true;
438
+ }
439
+
440
+ // GET /api/wall-e/tasks/:id/logs — proxy to WALL-E daemon (port 3457) for live logs
441
+ const taskLogsMatch = p.match(/^\/api\/wall-e\/tasks\/([^/]+)\/logs$/);
442
+ if (taskLogsMatch && m === 'GET') {
443
+ const daemonPort = process.env.WALL_E_PORT || 3457;
444
+ fetch(`http://localhost:${daemonPort}/api/wall-e/tasks/${taskLogsMatch[1]}/logs`)
445
+ .then(resp => resp.json())
446
+ .then(data => jsonResponse(res, data))
447
+ .catch(() => jsonResponse(res, { data: { lines: [], status: 'daemon_offline' } }));
448
+ return true;
449
+ }
450
+
451
+ // GET /api/wall-e/tasks
452
+ if (p === '/api/wall-e/tasks' && m === 'GET') {
453
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
454
+ try {
455
+ const status = params.get('status') || undefined;
456
+ const limit = parseLimit(params, 50);
457
+ const data = brain.listTasks({ status, limit }).map(function(t) {
458
+ if (t.started_at && t.completed_at) {
459
+ // Normalize timestamps: append 'Z' if missing to ensure UTC parsing
460
+ const s = t.started_at.includes('Z') || t.started_at.includes('+') ? t.started_at : t.started_at.replace(' ', 'T') + 'Z';
461
+ const c = t.completed_at.includes('Z') || t.completed_at.includes('+') ? t.completed_at : t.completed_at.replace(' ', 'T') + 'Z';
462
+ const dur = new Date(c).getTime() - new Date(s).getTime();
463
+ if (dur > 0 && dur < 86400000) t.last_duration_ms = dur; // sanity: max 24h
464
+ }
465
+ return t;
466
+ });
467
+ jsonResponse(res, { data });
468
+ } catch (e) {
469
+ jsonResponse(res, { error: e.message }, 500);
470
+ }
471
+ return true;
472
+ }
473
+
474
+ // POST /api/wall-e/tasks
475
+ if (p === '/api/wall-e/tasks' && m === 'POST') {
476
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
477
+ readBody(req).then(body => {
478
+ try {
479
+ if (!body.title) { jsonResponse(res, { error: 'title is required' }, 400); return; }
480
+ const result = brain.insertTask({
481
+ title: body.title,
482
+ description: body.description,
483
+ priority: body.priority,
484
+ type: body.type,
485
+ schedule: body.schedule,
486
+ due_at: body.due_at,
487
+ skill: body.skill,
488
+ skill_config: body.skill_config ? (typeof body.skill_config === 'string' ? body.skill_config : JSON.stringify(body.skill_config)) : undefined,
489
+ });
490
+ jsonResponse(res, { data: { id: result.id } }, 201);
491
+ } catch (e) {
492
+ jsonResponse(res, { error: e.message }, 400);
493
+ }
494
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
495
+ return true;
496
+ }
497
+
498
+ // PUT /api/wall-e/tasks/:id
499
+ const taskUpdateMatch = p.match(/^\/api\/wall-e\/tasks\/([^/]+)$/);
500
+ if (taskUpdateMatch && m === 'PUT') {
501
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
502
+ readBody(req).then(body => {
503
+ try {
504
+ // When schedule changes, recompute next_run_at
505
+ if (body.schedule && !body.next_run_at) {
506
+ try {
507
+ const { computeNextDue } = require('./loops/tasks');
508
+ body.next_run_at = computeNextDue(body.schedule);
509
+ } catch {}
510
+ }
511
+ brain.updateTask(taskUpdateMatch[1], body);
512
+ jsonResponse(res, { data: { ok: true } });
513
+ } catch (e) {
514
+ jsonResponse(res, { error: e.message }, 400);
515
+ }
516
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
517
+ return true;
518
+ }
519
+
520
+ // DELETE /api/wall-e/tasks/:id (reuse taskUpdateMatch regex)
521
+ if (taskUpdateMatch && m === 'DELETE') {
522
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
523
+ try {
524
+ brain.deleteTask(taskUpdateMatch[1]);
525
+ jsonResponse(res, { data: { ok: true } });
526
+ } catch (e) {
527
+ jsonResponse(res, { error: e.message }, 500);
528
+ }
529
+ return true;
530
+ }
531
+
532
+ // GET /api/wall-e/chat/search
533
+ if (p === '/api/wall-e/chat/search' && m === 'GET') {
534
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
535
+ try {
536
+ const query = params.get('q') || '';
537
+ const limit = parseLimit(params, 50);
538
+ const data = brain.searchChatMessages({ query, limit });
539
+ jsonResponse(res, { data });
540
+ } catch (e) {
541
+ jsonResponse(res, { error: e.message }, 500);
542
+ }
543
+ return true;
544
+ }
545
+
546
+ // GET /api/wall-e/confidence
547
+ if (p === '/api/wall-e/confidence' && m === 'GET') {
548
+ if (!brain) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
549
+ try {
550
+ const confidence = require('./decision/confidence');
551
+ const data = confidence.listDomainConfidences();
552
+ jsonResponse(res, { data });
553
+ } catch (e) {
554
+ jsonResponse(res, { error: e.message }, 500);
555
+ }
556
+ return true;
557
+ }
558
+
559
+ // GET /api/wall-e/actions
560
+ if (p === '/api/wall-e/actions' && m === 'GET') {
561
+ if (!brain) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
562
+ try {
563
+ const status = params.get('status') || undefined;
564
+ const domain = params.get('domain') || undefined;
565
+ const limit = parseLimit(params, 50);
566
+ const data = brain.listActions({ status, domain, limit });
567
+ jsonResponse(res, { data });
568
+ } catch (e) {
569
+ jsonResponse(res, { error: e.message }, 500);
570
+ }
571
+ return true;
572
+ }
573
+
574
+ // POST /api/wall-e/actions/:id/approve
575
+ const approveMatch = p.match(/^\/api\/wall-e\/actions\/([^/]+)\/approve$/);
576
+ if (approveMatch && m === 'POST') {
577
+ if (!brain) return jsonResponse(res, { error: 'Brain module not available for writes' }, 503), true;
578
+ readBody(req).then(body => {
579
+ try {
580
+ const actionId = approveMatch[1];
581
+ const action = brain.getAction(actionId);
582
+ if (!action) { jsonResponse(res, { error: 'Action not found' }, 404); return; }
583
+ brain.updateActionStatus(actionId, 'approved');
584
+ // Record in confidence system
585
+ const confidence = require('./decision/confidence');
586
+ if (action.domain) {
587
+ const graduation = confidence.recordAction(action.domain, true);
588
+ jsonResponse(res, { data: { ok: true, graduation } });
589
+ } else {
590
+ jsonResponse(res, { data: { ok: true } });
591
+ }
592
+ } catch (e) {
593
+ jsonResponse(res, { error: e.message }, 400);
594
+ }
595
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
596
+ return true;
597
+ }
598
+
599
+ // POST /api/wall-e/actions/:id/reject
600
+ const rejectMatch = p.match(/^\/api\/wall-e\/actions\/([^/]+)\/reject$/);
601
+ if (rejectMatch && m === 'POST') {
602
+ if (!brain) return jsonResponse(res, { error: 'Brain module not available for writes' }, 503), true;
603
+ readBody(req).then(body => {
604
+ try {
605
+ const actionId = rejectMatch[1];
606
+ const action = brain.getAction(actionId);
607
+ if (!action) { jsonResponse(res, { error: 'Action not found' }, 404); return; }
608
+ brain.updateActionStatus(actionId, 'rejected', body.review_note);
609
+ // Record in confidence system
610
+ const confidence = require('./decision/confidence');
611
+ if (action.domain) {
612
+ const graduation = confidence.recordAction(action.domain, false);
613
+ jsonResponse(res, { data: { ok: true, graduation } });
614
+ } else {
615
+ jsonResponse(res, { data: { ok: true } });
616
+ }
617
+ } catch (e) {
618
+ jsonResponse(res, { error: e.message }, 400);
619
+ }
620
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
621
+ return true;
622
+ }
623
+
624
+ // --- Agent-to-Agent Protocol ---
625
+
626
+ // POST /api/wall-e/agent/exchange
627
+ if (p === '/api/wall-e/agent/exchange' && m === 'POST') {
628
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
629
+ readBody(req).then(async body => {
630
+ try {
631
+ const agentApi = require('./channels/agent-api');
632
+ const result = await agentApi.handleInboundExchange(body);
633
+ if (result.error) {
634
+ jsonResponse(res, { error: result.error }, 400);
635
+ } else {
636
+ jsonResponse(res, { data: result });
637
+ }
638
+ } catch (err) {
639
+ jsonResponse(res, { error: err.message }, 500);
640
+ }
641
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
642
+ return true;
643
+ }
644
+
645
+ // GET /api/wall-e/agent/paired
646
+ if (p === '/api/wall-e/agent/paired' && m === 'GET') {
647
+ try {
648
+ const agentApi = require('./channels/agent-api');
649
+ const data = agentApi.listPairedAgents();
650
+ jsonResponse(res, { data });
651
+ } catch (e) {
652
+ jsonResponse(res, { error: e.message }, 500);
653
+ }
654
+ return true;
655
+ }
656
+
657
+ // POST /api/wall-e/agent/pair
658
+ if (p === '/api/wall-e/agent/pair' && m === 'POST') {
659
+ readBody(req).then(body => {
660
+ try {
661
+ if (!body.agent_id || !body.endpoint || !body.owner) {
662
+ jsonResponse(res, { error: 'agent_id, endpoint, and owner are required' }, 400);
663
+ return;
664
+ }
665
+ const agentApi = require('./channels/agent-api');
666
+ agentApi.pairAgent(body.agent_id, body.endpoint, body.owner);
667
+ jsonResponse(res, { data: { ok: true } });
668
+ } catch (e) {
669
+ jsonResponse(res, { error: e.message }, 400);
670
+ }
671
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
672
+ return true;
673
+ }
674
+
675
+ // POST /api/wall-e/agent/unpair
676
+ if (p === '/api/wall-e/agent/unpair' && m === 'POST') {
677
+ readBody(req).then(body => {
678
+ try {
679
+ if (!body.agent_id) {
680
+ jsonResponse(res, { error: 'agent_id is required' }, 400);
681
+ return;
682
+ }
683
+ const agentApi = require('./channels/agent-api');
684
+ agentApi.unpairAgent(body.agent_id);
685
+ jsonResponse(res, { data: { ok: true } });
686
+ } catch (e) {
687
+ jsonResponse(res, { error: e.message }, 400);
688
+ }
689
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
690
+ return true;
691
+ }
692
+
693
+ // GET /api/wall-e/exchanges
694
+ if (p === '/api/wall-e/exchanges' && m === 'GET') {
695
+ if (!brain) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
696
+ try {
697
+ const limit = parseLimit(params, 50);
698
+ const data = brain.listExchanges({ limit });
699
+ jsonResponse(res, { data });
700
+ } catch (e) {
701
+ jsonResponse(res, { error: e.message }, 500);
702
+ }
703
+ return true;
704
+ }
705
+
706
+ // --- Persona Management ---
707
+
708
+ // GET /api/wall-e/personas
709
+ if (p === '/api/wall-e/personas' && m === 'GET') {
710
+ if (!brain) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
711
+ try {
712
+ const data = brain.listPersonas();
713
+ jsonResponse(res, { data });
714
+ } catch (e) {
715
+ jsonResponse(res, { error: e.message }, 500);
716
+ }
717
+ return true;
718
+ }
719
+
720
+ // POST /api/wall-e/personas
721
+ if (p === '/api/wall-e/personas' && m === 'POST') {
722
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
723
+ readBody(req).then(body => {
724
+ try {
725
+ if (!body.person_id) {
726
+ jsonResponse(res, { error: 'person_id is required' }, 400);
727
+ return;
728
+ }
729
+ // Check if persona exists for this person — update if so, create if not
730
+ const existing = brain.getPersonaForPerson(body.person_id);
731
+ if (existing) {
732
+ const updates = {};
733
+ if (body.persona_type !== undefined) updates.persona_type = body.persona_type;
734
+ if (body.sharing_rules !== undefined) updates.sharing_rules = body.sharing_rules;
735
+ if (body.tone !== undefined) updates.tone = body.tone;
736
+ if (body.boundaries !== undefined) updates.boundaries = body.boundaries;
737
+ if (body.examples !== undefined) updates.examples = body.examples;
738
+ if (Object.keys(updates).length > 0) {
739
+ brain.updatePersona(existing.id, updates);
740
+ }
741
+ jsonResponse(res, { data: { id: existing.id, updated: true } });
742
+ } else {
743
+ const result = brain.insertPersona(body);
744
+ jsonResponse(res, { data: { id: result.id, created: true } }, 201);
745
+ }
746
+ } catch (e) {
747
+ jsonResponse(res, { error: e.message }, 400);
748
+ }
749
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
750
+ return true;
751
+ }
752
+
753
+ // --- Skills Management ---
754
+
755
+ // GET /api/wall-e/skills
756
+ if (p === '/api/wall-e/skills' && m === 'GET') {
757
+ if (!brain) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
758
+ try {
759
+ const enabled = params.get('enabled');
760
+ const filter = enabled !== null ? { enabled: parseInt(enabled) } : {};
761
+ const data = brain.listSkills(filter);
762
+ jsonResponse(res, { data });
763
+ } catch (e) {
764
+ jsonResponse(res, { error: e.message }, 500);
765
+ }
766
+ return true;
767
+ }
768
+
769
+ // POST /api/wall-e/skills
770
+ if (p === '/api/wall-e/skills' && m === 'POST') {
771
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
772
+ readBody(req).then(body => {
773
+ try {
774
+ if (!body.name) {
775
+ jsonResponse(res, { error: 'name is required' }, 400);
776
+ return;
777
+ }
778
+ const result = brain.insertSkill({
779
+ name: body.name,
780
+ description: body.description,
781
+ tool_definitions: body.tool_definitions ? (typeof body.tool_definitions === 'string' ? body.tool_definitions : JSON.stringify(body.tool_definitions)) : undefined,
782
+ trigger_type: body.trigger_type,
783
+ trigger_config: body.trigger_config ? (typeof body.trigger_config === 'string' ? body.trigger_config : JSON.stringify(body.trigger_config)) : undefined,
784
+ prompt_template: body.prompt_template,
785
+ });
786
+ jsonResponse(res, { data: { id: result.id } }, 201);
787
+ } catch (e) {
788
+ jsonResponse(res, { error: e.message }, 400);
789
+ }
790
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
791
+ return true;
792
+ }
793
+
794
+ // GET /api/wall-e/skills/suggestions
795
+ if (p === '/api/wall-e/skills/suggestions' && m === 'GET') {
796
+ try {
797
+ const { suggestSkillsFromClaudeCode } = require('./skills/claude-code-reader');
798
+ const data = suggestSkillsFromClaudeCode();
799
+ jsonResponse(res, { data });
800
+ } catch (e) {
801
+ jsonResponse(res, { error: e.message }, 500);
802
+ }
803
+ return true;
804
+ }
805
+
806
+ // PUT /api/wall-e/skills/:id
807
+ const skillUpdateMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)$/);
808
+ if (skillUpdateMatch && m === 'PUT') {
809
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
810
+ readBody(req).then(body => {
811
+ try {
812
+ const updates = {};
813
+ if (body.name !== undefined) updates.name = body.name;
814
+ if (body.description !== undefined) updates.description = body.description;
815
+ if (body.tool_definitions !== undefined) updates.tool_definitions = typeof body.tool_definitions === 'string' ? body.tool_definitions : JSON.stringify(body.tool_definitions);
816
+ if (body.trigger_type !== undefined) updates.trigger_type = body.trigger_type;
817
+ if (body.trigger_config !== undefined) updates.trigger_config = typeof body.trigger_config === 'string' ? body.trigger_config : JSON.stringify(body.trigger_config);
818
+ if (body.prompt_template !== undefined) updates.prompt_template = body.prompt_template;
819
+ if (body.enabled !== undefined) updates.enabled = body.enabled;
820
+ brain.updateSkill(skillUpdateMatch[1], updates);
821
+ jsonResponse(res, { data: { ok: true } });
822
+ } catch (e) {
823
+ jsonResponse(res, { error: e.message }, 400);
824
+ }
825
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
826
+ return true;
827
+ }
828
+
829
+ // POST /api/wall-e/skills/:id/run — tries DB skill first, then installed (filesystem) skill
830
+ const skillRunMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/run$/);
831
+ if (skillRunMatch && m === 'POST') {
832
+ // Try DB-based skill by ID first
833
+ if (brain && ensureBrainInit()) {
834
+ const dbSkill = brain.getSkill(skillRunMatch[1]);
835
+ if (dbSkill) {
836
+ const { runSkill } = require('./skills/skill-executor');
837
+ runSkill(dbSkill).then(result => {
838
+ jsonResponse(res, { data: result });
839
+ }).catch(e => {
840
+ jsonResponse(res, { error: e.message }, 500);
841
+ });
842
+ return true;
843
+ }
844
+ }
845
+ // Fall through to installed (filesystem) skill by name
846
+ const skillName = skillRunMatch[1];
847
+ const { getSkillByName: getInstalledSkill } = require('./skills/skill-loader');
848
+ const installedSkill = getInstalledSkill(skillName);
849
+ if (!installedSkill) return jsonResponse(res, { error: 'Skill not found: ' + skillName }, 404), true;
850
+
851
+ readBody(req).then(async body => {
852
+ const startTime = Date.now();
853
+ try {
854
+ if (installedSkill.execution === 'script') {
855
+ const { execFileSync } = require('child_process');
856
+ const entryPath = require('path').resolve(installedSkill.dir, installedSkill.entry || 'run.js');
857
+ const scriptArgs = body.args || installedSkill.args || [];
858
+ const env = {
859
+ ...process.env,
860
+ WALL_E_SKILL_CONFIG: JSON.stringify(body.config || installedSkill.config || {}),
861
+ WALL_E_SKILL_DIR: installedSkill.dir,
862
+ WALL_E_DATA_DIR: brain ? brain.DATA_DIR : '',
863
+ };
864
+ const output = execFileSync('node', [entryPath, ...scriptArgs], {
865
+ env,
866
+ timeout: 120000,
867
+ encoding: 'utf8',
868
+ maxBuffer: 5 * 1024 * 1024,
869
+ });
870
+ const duration_ms = Date.now() - startTime;
871
+ jsonResponse(res, { data: { result: output.trim(), duration_ms } });
872
+ } else {
873
+ const duration_ms = Date.now() - startTime;
874
+ jsonResponse(res, { data: { mode: 'agent', instructions: installedSkill.body, config: installedSkill.config, duration_ms } });
875
+ }
876
+ } catch (e) {
877
+ const duration_ms = Date.now() - startTime;
878
+ jsonResponse(res, { error: e.message, data: { duration_ms } }, 500);
879
+ }
880
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
881
+ return true;
882
+ }
883
+
884
+ // GET /api/wall-e/skills/:id/executions
885
+ const skillExecMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/executions$/);
886
+ if (skillExecMatch && m === 'GET') {
887
+ if (!brain) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
888
+ try {
889
+ const limit = parseLimit(params, 20);
890
+ const data = brain.listSkillExecutions({ skill_id: skillExecMatch[1], limit });
891
+ jsonResponse(res, { data });
892
+ } catch (e) {
893
+ jsonResponse(res, { error: e.message }, 500);
894
+ }
895
+ return true;
896
+ }
897
+
898
+ // --- Installed Skills (filesystem-based) ---
899
+
900
+ // GET /api/wall-e/skills/installed
901
+ if (p === '/api/wall-e/skills/installed' && m === 'GET') {
902
+ try {
903
+ const { loadAllSkills } = require('./skills/skill-loader');
904
+ const data = loadAllSkills().map(s => ({
905
+ name: s.name,
906
+ description: s.description,
907
+ version: s.version,
908
+ execution: s.execution,
909
+ entry: s.entry,
910
+ args: s.args,
911
+ author: s.author,
912
+ trigger: s.trigger,
913
+ config: s.config,
914
+ tags: s.tags,
915
+ permissions: s.permissions,
916
+ source: s.source,
917
+ }));
918
+ jsonResponse(res, { data });
919
+ } catch (e) {
920
+ jsonResponse(res, { error: e.message }, 500);
921
+ }
922
+ return true;
923
+ }
924
+
925
+ // GET /api/wall-e/skills/search?q=...
926
+ if (p === '/api/wall-e/skills/search' && m === 'GET') {
927
+ try {
928
+ const { searchSkills } = require('./skills/skill-loader');
929
+ const q = params.get('q') || '';
930
+ const data = searchSkills(q).map(s => ({
931
+ name: s.name,
932
+ description: s.description,
933
+ version: s.version,
934
+ execution: s.execution,
935
+ tags: s.tags,
936
+ source: s.source,
937
+ }));
938
+ jsonResponse(res, { data });
939
+ } catch (e) {
940
+ jsonResponse(res, { error: e.message }, 500);
941
+ }
942
+ return true;
943
+ }
944
+
945
+ // --- Slack Ingest ---
946
+
947
+ // GET /api/wall-e/slack-ingest/progress
948
+ if (p === '/api/wall-e/slack-ingest/progress' && m === 'GET') {
949
+ try {
950
+ const slackIngest = require('./skills/slack-ingest');
951
+ const data = slackIngest.getProgress();
952
+ jsonResponse(res, { data });
953
+ } catch (e) {
954
+ jsonResponse(res, { error: e.message }, 500);
955
+ }
956
+ return true;
957
+ }
958
+
959
+ // POST /api/wall-e/slack-ingest/start
960
+ if (p === '/api/wall-e/slack-ingest/start' && m === 'POST') {
961
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
962
+ try {
963
+ const slackIngest = require('./skills/slack-ingest');
964
+ // Reset checkpoint to start fresh
965
+ slackIngest.resetCheckpoint();
966
+ // Enable the skill
967
+ const skill = brain.getSkillByName('ingest-slack-history');
968
+ if (skill) {
969
+ brain.updateSkill(skill.id, { enabled: 1 });
970
+ }
971
+ jsonResponse(res, { data: { ok: true, message: 'Slack ingest started' } });
972
+ } catch (e) {
973
+ jsonResponse(res, { error: e.message }, 500);
974
+ }
975
+ return true;
976
+ }
977
+
978
+ // POST /api/wall-e/slack-ingest/reset
979
+ if (p === '/api/wall-e/slack-ingest/reset' && m === 'POST') {
980
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
981
+ try {
982
+ const slackIngest = require('./skills/slack-ingest');
983
+ slackIngest.resetCheckpoint();
984
+ jsonResponse(res, { data: { ok: true, message: 'Checkpoint reset' } });
985
+ } catch (e) {
986
+ jsonResponse(res, { error: e.message }, 500);
987
+ }
988
+ return true;
989
+ }
990
+
991
+ // --- Briefing Items ---
992
+
993
+ // GET /api/wall-e/briefing-items
994
+ if (p === '/api/wall-e/briefing-items' && m === 'GET') {
995
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
996
+ try {
997
+ const skill = params.get('skill') || undefined;
998
+ const status = params.get('status') || undefined;
999
+ const limit = parseLimit(params, 100);
1000
+ const data = brain.listBriefingItems({ skill, status, limit });
1001
+ // Un-snooze expired items in the response
1002
+ const now = new Date().toISOString();
1003
+ for (const item of data) {
1004
+ if (item.status === 'snoozed' && item.snooze_until && item.snooze_until <= now) {
1005
+ brain.updateBriefingItem(item.id, { status: 'open', snooze_until: null });
1006
+ item.status = 'open';
1007
+ item.snooze_until = null;
1008
+ }
1009
+ }
1010
+ jsonResponse(res, { data });
1011
+ } catch (e) {
1012
+ jsonResponse(res, { error: e.message }, 500);
1013
+ }
1014
+ return true;
1015
+ }
1016
+
1017
+ // PUT /api/wall-e/briefing-items/:id
1018
+ const briefingItemMatch = p.match(/^\/api\/wall-e\/briefing-items\/([^/]+)$/);
1019
+ if (briefingItemMatch && m === 'PUT') {
1020
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
1021
+ readBody(req).then(body => {
1022
+ try {
1023
+ const updates = {};
1024
+ if (body.status !== undefined) {
1025
+ updates.status = body.status;
1026
+ if (body.status === 'done' || body.status === 'dismissed') {
1027
+ updates.resolved_at = new Date().toISOString();
1028
+ }
1029
+ }
1030
+ if (body.notes !== undefined) updates.notes = body.notes;
1031
+ if (body.urgency !== undefined) updates.urgency = body.urgency;
1032
+ if (body.snooze_until !== undefined) {
1033
+ updates.snooze_until = body.snooze_until;
1034
+ if (body.snooze_until) updates.status = 'snoozed';
1035
+ }
1036
+ brain.updateBriefingItem(briefingItemMatch[1], updates);
1037
+ jsonResponse(res, { data: { ok: true } });
1038
+ } catch (e) {
1039
+ jsonResponse(res, { error: e.message }, 400);
1040
+ }
1041
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
1042
+ return true;
1043
+ }
1044
+
1045
+ // No matching route under /api/wall-e
1046
+ jsonResponse(res, { error: 'Not found' }, 404);
1047
+ return true;
1048
+ }
1049
+
1050
+ // Allow overriding the read DB (for testing)
1051
+ function _setReadDb(db) {
1052
+ readDb = db;
1053
+ }
1054
+
1055
+ function _setBrain(b) {
1056
+ brain = b;
1057
+ }
1058
+
1059
+ module.exports = {
1060
+ handleWalleApi,
1061
+ // Individual handlers for testing
1062
+ getStatus,
1063
+ getKnowledgeList,
1064
+ getMemoriesList,
1065
+ getPeopleList,
1066
+ getTimeline,
1067
+ getQuestionsList,
1068
+ getStats,
1069
+ getBrief,
1070
+ // Test helpers
1071
+ _setReadDb,
1072
+ _setBrain,
1073
+ };