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.
- package/bin/create-walle.js +134 -0
- package/package.json +18 -0
- package/template/.env.example +40 -0
- package/template/CLAUDE.md +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +167 -0
- package/template/bin/setup.js +100 -0
- package/template/claude-code-skill.md +60 -0
- package/template/claude-task-manager/api-prompts.js +1841 -0
- package/template/claude-task-manager/api-reviews.js +275 -0
- package/template/claude-task-manager/approval-agent.js +454 -0
- package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
- package/template/claude-task-manager/db.js +1721 -0
- package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
- package/template/claude-task-manager/git-utils.js +214 -0
- package/template/claude-task-manager/package-lock.json +1607 -0
- package/template/claude-task-manager/package.json +31 -0
- package/template/claude-task-manager/prompt-harvest.js +1148 -0
- package/template/claude-task-manager/public/css/prompts.css +880 -0
- package/template/claude-task-manager/public/css/reviews.css +430 -0
- package/template/claude-task-manager/public/css/walle.css +732 -0
- package/template/claude-task-manager/public/favicon.ico +0 -0
- package/template/claude-task-manager/public/icon.svg +37 -0
- package/template/claude-task-manager/public/index.html +8346 -0
- package/template/claude-task-manager/public/js/prompts.js +3159 -0
- package/template/claude-task-manager/public/js/reviews.js +1292 -0
- package/template/claude-task-manager/public/js/walle.js +3081 -0
- package/template/claude-task-manager/public/manifest.json +13 -0
- package/template/claude-task-manager/public/prompts.html +4353 -0
- package/template/claude-task-manager/public/setup.html +216 -0
- package/template/claude-task-manager/queue-engine.js +404 -0
- package/template/claude-task-manager/server-state.js +5 -0
- package/template/claude-task-manager/server.js +2254 -0
- package/template/claude-task-manager/session-utils.js +124 -0
- package/template/claude-task-manager/start.sh +17 -0
- package/template/claude-task-manager/tests/test-ai-search.js +61 -0
- package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
- package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
- package/template/claude-task-manager/tests/test-features-v2.js +127 -0
- package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
- package/template/claude-task-manager/tests/test-insights.js +124 -0
- package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
- package/template/claude-task-manager/tests/test-permissions.js +122 -0
- package/template/claude-task-manager/tests/test-pin.js +51 -0
- package/template/claude-task-manager/tests/test-prompts.js +164 -0
- package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
- package/template/claude-task-manager/tests/test-review.js +104 -0
- package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
- package/template/claude-task-manager/tests/test-send-final.js +30 -0
- package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
- package/template/claude-task-manager/tests/test-send-integration.js +107 -0
- package/template/claude-task-manager/tests/test-send-visual.js +34 -0
- package/template/claude-task-manager/tests/test-session-create.js +147 -0
- package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
- package/template/claude-task-manager/tests/test-url-hash.js +68 -0
- package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
- package/template/claude-task-manager/tests/test-ux-review.js +130 -0
- package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
- package/template/claude-task-manager/tests/test-zoom.js +92 -0
- package/template/claude-task-manager/tests/test-zoom2.js +67 -0
- package/template/docs/site/api/README.md +187 -0
- package/template/docs/site/guides/claude-code.md +58 -0
- package/template/docs/site/guides/configuration.md +96 -0
- package/template/docs/site/guides/quickstart.md +158 -0
- package/template/docs/site/index.md +14 -0
- package/template/docs/site/skills/README.md +135 -0
- package/template/wall-e/.dockerignore +11 -0
- package/template/wall-e/Dockerfile +25 -0
- package/template/wall-e/adapters/adapter-base.js +37 -0
- package/template/wall-e/adapters/ctm.js +193 -0
- package/template/wall-e/adapters/slack.js +56 -0
- package/template/wall-e/agent.js +319 -0
- package/template/wall-e/api-walle.js +1073 -0
- package/template/wall-e/brain.js +1235 -0
- package/template/wall-e/channels/agent-api.js +172 -0
- package/template/wall-e/channels/channel-base.js +14 -0
- package/template/wall-e/channels/imessage-channel.js +113 -0
- package/template/wall-e/channels/slack-channel.js +118 -0
- package/template/wall-e/chat.js +778 -0
- package/template/wall-e/decision/confidence.js +93 -0
- package/template/wall-e/deploy.sh +35 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
- package/template/wall-e/extraction/contradiction.js +168 -0
- package/template/wall-e/extraction/knowledge-extractor.js +190 -0
- package/template/wall-e/fly.toml +24 -0
- package/template/wall-e/loops/ingest.js +34 -0
- package/template/wall-e/loops/reflect.js +63 -0
- package/template/wall-e/loops/tasks.js +487 -0
- package/template/wall-e/loops/think.js +125 -0
- package/template/wall-e/package-lock.json +533 -0
- package/template/wall-e/package.json +18 -0
- package/template/wall-e/scripts/ingest-slack-search.js +85 -0
- package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
- package/template/wall-e/scripts/slack-backfill.js +295 -0
- package/template/wall-e/scripts/slack-channel-history.js +454 -0
- package/template/wall-e/server.js +93 -0
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
- package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
- package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
- package/template/wall-e/skills/claude-code-reader.js +144 -0
- package/template/wall-e/skills/mcp-client.js +407 -0
- package/template/wall-e/skills/skill-executor.js +163 -0
- package/template/wall-e/skills/skill-loader.js +410 -0
- package/template/wall-e/skills/skill-planner.js +88 -0
- package/template/wall-e/skills/slack-ingest.js +329 -0
- package/template/wall-e/skills/slack-pull-live.js +270 -0
- package/template/wall-e/skills/tool-executor.js +188 -0
- package/template/wall-e/tests/adapter-base.test.js +20 -0
- package/template/wall-e/tests/adapter-ctm.test.js +122 -0
- package/template/wall-e/tests/adapter-slack.test.js +98 -0
- package/template/wall-e/tests/agent-api.test.js +256 -0
- package/template/wall-e/tests/api-walle.test.js +222 -0
- package/template/wall-e/tests/brain.test.js +602 -0
- package/template/wall-e/tests/channels.test.js +104 -0
- package/template/wall-e/tests/chat.test.js +103 -0
- package/template/wall-e/tests/confidence.test.js +134 -0
- package/template/wall-e/tests/contradiction.test.js +217 -0
- package/template/wall-e/tests/ingest.test.js +113 -0
- package/template/wall-e/tests/mcp-client.test.js +71 -0
- package/template/wall-e/tests/reflect.test.js +103 -0
- package/template/wall-e/tests/server.test.js +111 -0
- package/template/wall-e/tests/skills.test.js +198 -0
- package/template/wall-e/tests/slack-ingest.test.js +103 -0
- package/template/wall-e/tests/think.test.js +435 -0
- package/template/wall-e/tools/local-tools.js +697 -0
- 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
|
+
};
|