create-walle 0.9.3 → 0.9.4
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/README.md +2 -1
- package/package.json +1 -1
- package/template/claude-task-manager/db.js +5 -1
- package/template/claude-task-manager/public/css/walle.css +317 -0
- package/template/claude-task-manager/public/index.html +404 -101
- package/template/claude-task-manager/public/js/walle.js +1256 -86
- package/template/claude-task-manager/server.js +189 -14
- package/template/docs/site/api/README.md +146 -0
- package/template/docs/site/skills/README.md +99 -5
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +54 -0
- package/template/wall-e/api-walle.js +452 -3
- package/template/wall-e/brain.js +45 -1
- package/template/wall-e/channels/telegram-channel.js +96 -0
- package/template/wall-e/chat.js +61 -2
- package/template/wall-e/coding-context.js +252 -0
- package/template/wall-e/coding-orchestrator.js +625 -0
- package/template/wall-e/coding-review.js +189 -0
- package/template/wall-e/core-tasks.js +12 -3
- package/template/wall-e/deploy.sh +4 -4
- package/template/wall-e/fly.toml +2 -2
- package/template/wall-e/package.json +4 -1
- package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
- package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
- package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
- package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
- package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
- package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
- package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
- package/template/wall-e/skills/_templates/manual-action.md +19 -0
- package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
- package/template/wall-e/skills/_templates/script-runner.md +21 -0
- package/template/wall-e/skills/claude-code-reader.js +16 -4
- package/template/wall-e/skills/skill-executor.js +23 -1
- package/template/wall-e/skills/skill-validator.js +73 -0
- package/template/wall-e/tests/brain.test.js +3 -3
- package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
- package/template/wall-e/tests/coding-context.test.js +212 -0
- package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
- package/template/wall-e/tests/coding-review.test.js +141 -0
- package/template/claude-task-manager/package-lock.json +0 -1607
- package/template/claude-task-manager/tests/test-ai-search.js +0 -61
- package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
- package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
- package/template/claude-task-manager/tests/test-features-v2.js +0 -127
- package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
- package/template/claude-task-manager/tests/test-insights.js +0 -124
- package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
- package/template/claude-task-manager/tests/test-permissions.js +0 -122
- package/template/claude-task-manager/tests/test-pin.js +0 -51
- package/template/claude-task-manager/tests/test-prompts.js +0 -164
- package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
- package/template/claude-task-manager/tests/test-review.js +0 -104
- package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
- package/template/claude-task-manager/tests/test-send-final.js +0 -30
- package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
- package/template/claude-task-manager/tests/test-send-integration.js +0 -107
- package/template/claude-task-manager/tests/test-send-visual.js +0 -34
- package/template/claude-task-manager/tests/test-session-create.js +0 -147
- package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
- package/template/claude-task-manager/tests/test-url-hash.js +0 -68
- package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
- package/template/claude-task-manager/tests/test-ux-review.js +0 -130
- package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
- package/template/claude-task-manager/tests/test-zoom.js +0 -92
- package/template/claude-task-manager/tests/test-zoom2.js +0 -67
- package/template/docs/openclaw-vs-walle-comparison.md +0 -103
- package/template/docs/ux-improvement-plan.md +0 -84
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
- package/template/wall-e/package-lock.json +0 -533
- package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +0 -4
|
@@ -647,6 +647,9 @@ function handleApi(req, res, url) {
|
|
|
647
647
|
if (url.pathname === '/api/sessions/rename' && req.method === 'POST') {
|
|
648
648
|
return apiRenameSession(req, res);
|
|
649
649
|
}
|
|
650
|
+
if (url.pathname === '/api/sessions/attach' && req.method === 'POST') {
|
|
651
|
+
return apiAttachSession(req, res);
|
|
652
|
+
}
|
|
650
653
|
// --- Service control endpoints ---
|
|
651
654
|
if (url.pathname === '/api/services/status' && req.method === 'GET') {
|
|
652
655
|
return apiServicesStatus(req, res);
|
|
@@ -1451,6 +1454,9 @@ function apiRenameSession(req, res) {
|
|
|
1451
1454
|
const trimmed = title.trim().slice(0, 120);
|
|
1452
1455
|
dbModule.setSessionTitle(sessionId, trimmed, true);
|
|
1453
1456
|
|
|
1457
|
+
// Update startup_tasks so the label survives CTM restart
|
|
1458
|
+
try { dbModule.updateStartupTaskLabel(sessionId, trimmed); } catch {}
|
|
1459
|
+
|
|
1454
1460
|
// Update in-memory session if active
|
|
1455
1461
|
const session = sessions.get(sessionId);
|
|
1456
1462
|
if (session) {
|
|
@@ -1954,21 +1960,29 @@ class VTermScreen {
|
|
|
1954
1960
|
}
|
|
1955
1961
|
|
|
1956
1962
|
function checkAutoApproval(sessionId, session, data) {
|
|
1957
|
-
// Accumulate output
|
|
1963
|
+
// Accumulate output — defer expensive VTermScreen.feed() until we actually check
|
|
1958
1964
|
let buf = autoApprovalBuffers.get(sessionId);
|
|
1959
1965
|
if (!buf) {
|
|
1960
|
-
buf = { vterm: new VTermScreen(), lastCheck: 0, cooldown: 0, reviewing: false };
|
|
1966
|
+
buf = { vterm: new VTermScreen(), lastCheck: 0, cooldown: 0, reviewing: false, pending: '' };
|
|
1961
1967
|
autoApprovalBuffers.set(sessionId, buf);
|
|
1962
1968
|
}
|
|
1963
|
-
buf.
|
|
1969
|
+
buf.pending += data;
|
|
1970
|
+
// Cap pending buffer to avoid unbounded growth
|
|
1971
|
+
if (buf.pending.length > 32768) buf.pending = buf.pending.slice(-16384);
|
|
1964
1972
|
|
|
1965
|
-
// Throttle checks
|
|
1973
|
+
// Throttle checks — only do heavy VTermScreen processing every 500ms
|
|
1966
1974
|
const now = Date.now();
|
|
1967
1975
|
if (now - buf.lastCheck < 500) return;
|
|
1968
1976
|
if (now < buf.cooldown) return;
|
|
1969
1977
|
if (buf.reviewing) return; // AI review in progress
|
|
1970
1978
|
buf.lastCheck = now;
|
|
1971
1979
|
|
|
1980
|
+
// Feed accumulated data to VTermScreen in one batch (much cheaper than per-chunk)
|
|
1981
|
+
if (buf.pending) {
|
|
1982
|
+
buf.vterm.feed(buf.pending);
|
|
1983
|
+
buf.pending = '';
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1972
1986
|
// Read the virtual screen as text
|
|
1973
1987
|
const clean = buf.vterm.getText();
|
|
1974
1988
|
|
|
@@ -1997,6 +2011,7 @@ function checkAutoApproval(sessionId, session, data) {
|
|
|
1997
2011
|
console.log(`[shadow-approver] handleApprovalCheck result: handled=${handled}`);
|
|
1998
2012
|
if (handled) {
|
|
1999
2013
|
buf.vterm = new VTermScreen();
|
|
2014
|
+
buf.pending = '';
|
|
2000
2015
|
buf.cooldown = Date.now() + 3000;
|
|
2001
2016
|
}
|
|
2002
2017
|
})
|
|
@@ -2152,6 +2167,13 @@ function handleCreate(ws, msg) {
|
|
|
2152
2167
|
}
|
|
2153
2168
|
}
|
|
2154
2169
|
|
|
2170
|
+
// If a user-renamed title exists in the DB (e.g. from a previous session that was
|
|
2171
|
+
// renamed before CTM restart), prefer it over the stale startup_tasks label
|
|
2172
|
+
const existingTitle = dbModule.getSessionTitle(id);
|
|
2173
|
+
if (existingTitle && existingTitle.userRenamed && existingTitle.title) {
|
|
2174
|
+
label = existingTitle.title;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2155
2177
|
let ptyProcess;
|
|
2156
2178
|
try {
|
|
2157
2179
|
ptyProcess = pty.spawn(cmd, args, {
|
|
@@ -2182,19 +2204,40 @@ function handleCreate(ws, msg) {
|
|
|
2182
2204
|
// Persist to startup_tasks for crash-safe restore
|
|
2183
2205
|
try { dbModule.addStartupTask(id, label, cmd, args, cwd); } catch {}
|
|
2184
2206
|
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2207
|
+
// Output batching — coalesce rapid PTY chunks into fewer WS messages.
|
|
2208
|
+
// During Claude Code streaming, PTY fires 100s of chunks/sec. Sending each
|
|
2209
|
+
// as a separate WS message causes client-side JSON parse + dispatch overhead
|
|
2210
|
+
// that creates input lag. Batching at ~16ms (one animation frame) reduces
|
|
2211
|
+
// message count 10-50x with imperceptible latency increase.
|
|
2212
|
+
let outputBuf = '';
|
|
2213
|
+
let outputFlushTimer = null;
|
|
2214
|
+
|
|
2215
|
+
function flushOutput() {
|
|
2216
|
+
outputFlushTimer = null;
|
|
2217
|
+
if (!outputBuf) return;
|
|
2218
|
+
const batch = outputBuf;
|
|
2219
|
+
outputBuf = '';
|
|
2190
2220
|
// Keep scrollback (limit to ~50KB)
|
|
2191
|
-
session.scrollback.push(
|
|
2221
|
+
session.scrollback.push(batch);
|
|
2192
2222
|
if (session.scrollback.length > 500) session.scrollback.shift();
|
|
2193
2223
|
|
|
2194
|
-
const payload = JSON.stringify({ type: 'output', id, data:
|
|
2224
|
+
const payload = JSON.stringify({ type: 'output', id, data: batch });
|
|
2195
2225
|
for (const client of session.clients) {
|
|
2196
2226
|
if (client.readyState === 1) client.send(payload);
|
|
2197
2227
|
}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
ptyProcess.onData((data) => {
|
|
2231
|
+
session.lastActivity = Date.now();
|
|
2232
|
+
// Only strip \e[3J (Erase Scrollback) — it destroys scroll history on replay.
|
|
2233
|
+
// Do NOT strip \e[2J or \e[?1049h/l — those are needed for Claude Code's TUI rendering.
|
|
2234
|
+
const cleanData = data.indexOf('\x1b[3J') >= 0 ? data.replace(/\x1b\[3J/g, '') : data;
|
|
2235
|
+
|
|
2236
|
+
// Batch output for WS delivery
|
|
2237
|
+
outputBuf += cleanData;
|
|
2238
|
+
if (!outputFlushTimer) {
|
|
2239
|
+
outputFlushTimer = setTimeout(flushOutput, 16);
|
|
2240
|
+
}
|
|
2198
2241
|
|
|
2199
2242
|
// Feed output to queue engine for completion detection
|
|
2200
2243
|
queueEngine.feedOutput(id, data);
|
|
@@ -2207,6 +2250,8 @@ function handleCreate(ws, msg) {
|
|
|
2207
2250
|
});
|
|
2208
2251
|
|
|
2209
2252
|
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
2253
|
+
// Flush any remaining buffered output before sending exit
|
|
2254
|
+
if (outputFlushTimer) { clearTimeout(outputFlushTimer); flushOutput(); }
|
|
2210
2255
|
const payload = JSON.stringify({ type: 'exit', id, exitCode, signal });
|
|
2211
2256
|
for (const client of session.clients) {
|
|
2212
2257
|
if (client.readyState === 1) client.send(payload);
|
|
@@ -2222,15 +2267,142 @@ function handleCreate(ws, msg) {
|
|
|
2222
2267
|
// Wire up queue engine hooks if a queue exists for this session
|
|
2223
2268
|
setupQueueForSession(id);
|
|
2224
2269
|
|
|
2225
|
-
//
|
|
2226
|
-
if (
|
|
2227
|
-
|
|
2270
|
+
// Persist label to session_titles if the client provided one explicitly.
|
|
2271
|
+
// Skip if the DB already has a user-renamed title (avoids overwriting renames on restart)
|
|
2272
|
+
if (msg.label && !(existingTitle && existingTitle.userRenamed)) {
|
|
2273
|
+
dbModule.setSessionTitle(id, label, true);
|
|
2228
2274
|
}
|
|
2229
2275
|
|
|
2230
2276
|
if (ws) ws.send(JSON.stringify({ type: 'created', id, pid: ptyProcess.pid, label, cwd }));
|
|
2231
2277
|
broadcastSessionList();
|
|
2232
2278
|
}
|
|
2233
2279
|
|
|
2280
|
+
function apiAttachSession(req, res) {
|
|
2281
|
+
let body = '';
|
|
2282
|
+
req.on('data', chunk => { body += chunk; if (body.length > 1e6) req.destroy(); });
|
|
2283
|
+
req.on('end', () => {
|
|
2284
|
+
try {
|
|
2285
|
+
const { session_id, cwd: rawCwd } = JSON.parse(body);
|
|
2286
|
+
if (!session_id) {
|
|
2287
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2288
|
+
res.end(JSON.stringify({ error: 'session_id required' }));
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
const tabId = crypto.randomUUID();
|
|
2293
|
+
const cwdDir = (rawCwd || process.env.HOME).replace(/^~/, process.env.HOME);
|
|
2294
|
+
const env = { ...process.env };
|
|
2295
|
+
delete env.CLAUDECODE;
|
|
2296
|
+
|
|
2297
|
+
// Restore .jsonl from .bak if needed (Claude Code migrates sessions to directory format)
|
|
2298
|
+
const claudeProjectsDir = path.join(process.env.HOME, '.claude', 'projects');
|
|
2299
|
+
try {
|
|
2300
|
+
for (const entry of fs.readdirSync(claudeProjectsDir)) {
|
|
2301
|
+
const jsonl = path.join(claudeProjectsDir, entry, `${session_id}.jsonl`);
|
|
2302
|
+
const bak = jsonl + '.bak';
|
|
2303
|
+
if (!fs.existsSync(jsonl) && fs.existsSync(bak)) {
|
|
2304
|
+
fs.copyFileSync(bak, jsonl);
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
} catch { /* ignore — best effort */ }
|
|
2309
|
+
|
|
2310
|
+
let ptyProcess;
|
|
2311
|
+
try {
|
|
2312
|
+
ptyProcess = pty.spawn('claude', ['--resume', session_id], {
|
|
2313
|
+
name: 'xterm-256color',
|
|
2314
|
+
cols: 120, rows: 30,
|
|
2315
|
+
cwd: cwdDir,
|
|
2316
|
+
env,
|
|
2317
|
+
});
|
|
2318
|
+
} catch (e) {
|
|
2319
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2320
|
+
res.end(JSON.stringify({ error: 'Failed to spawn: ' + e.message }));
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
const label = 'Attached: ' + session_id.slice(0, 8) + '...';
|
|
2325
|
+
const session = {
|
|
2326
|
+
id: tabId,
|
|
2327
|
+
label,
|
|
2328
|
+
cmd: 'claude',
|
|
2329
|
+
args: ['--resume', session_id],
|
|
2330
|
+
cwd: cwdDir,
|
|
2331
|
+
pid: ptyProcess.pid,
|
|
2332
|
+
ptyProcess,
|
|
2333
|
+
clients: [],
|
|
2334
|
+
scrollback: [],
|
|
2335
|
+
createdAt: Date.now(),
|
|
2336
|
+
lastActivity: Date.now(),
|
|
2337
|
+
};
|
|
2338
|
+
|
|
2339
|
+
sessions.set(tabId, session);
|
|
2340
|
+
|
|
2341
|
+
// Persist to startup_tasks for crash-safe restore
|
|
2342
|
+
try { dbModule.addStartupTask(tabId, label, 'claude', ['--resume', session_id], cwdDir); } catch {}
|
|
2343
|
+
|
|
2344
|
+
// Output batching (same as main session path)
|
|
2345
|
+
let outputBuf2 = '';
|
|
2346
|
+
let outputFlushTimer2 = null;
|
|
2347
|
+
function flushOutput2() {
|
|
2348
|
+
outputFlushTimer2 = null;
|
|
2349
|
+
if (!outputBuf2) return;
|
|
2350
|
+
const batch = outputBuf2;
|
|
2351
|
+
outputBuf2 = '';
|
|
2352
|
+
session.scrollback.push(batch);
|
|
2353
|
+
if (session.scrollback.length > 500) session.scrollback.shift();
|
|
2354
|
+
const payload = JSON.stringify({ type: 'output', id: tabId, data: batch });
|
|
2355
|
+
for (const client of session.clients) {
|
|
2356
|
+
if (client.readyState === 1) client.send(payload);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
ptyProcess.onData((data) => {
|
|
2361
|
+
session.lastActivity = Date.now();
|
|
2362
|
+
const cleanData = data.indexOf('\x1b[3J') >= 0 ? data.replace(/\x1b\[3J/g, '') : data;
|
|
2363
|
+
outputBuf2 += cleanData;
|
|
2364
|
+
if (!outputFlushTimer2) {
|
|
2365
|
+
outputFlushTimer2 = setTimeout(flushOutput2, 16);
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// Feed output to queue engine for completion detection
|
|
2369
|
+
queueEngine.feedOutput(tabId, data);
|
|
2370
|
+
|
|
2371
|
+
// Feed output to auto-approval engine
|
|
2372
|
+
checkAutoApproval(tabId, session, data);
|
|
2373
|
+
|
|
2374
|
+
// Feed output to idle/waiting-for-input detection
|
|
2375
|
+
checkIdleNotify(tabId, session, data);
|
|
2376
|
+
});
|
|
2377
|
+
|
|
2378
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
2379
|
+
if (outputFlushTimer2) { clearTimeout(outputFlushTimer2); flushOutput2(); }
|
|
2380
|
+
const payload = JSON.stringify({ type: 'exit', id: tabId, exitCode, signal });
|
|
2381
|
+
for (const client of session.clients) {
|
|
2382
|
+
if (client.readyState === 1) client.send(payload);
|
|
2383
|
+
}
|
|
2384
|
+
sessions.delete(tabId);
|
|
2385
|
+
try { dbModule.removeStartupTask(tabId); } catch {}
|
|
2386
|
+
queueEngine.onSessionExit(tabId);
|
|
2387
|
+
cleanAutoApprovalBuffer(tabId);
|
|
2388
|
+
cleanIdleNotify(tabId);
|
|
2389
|
+
broadcastSessionList();
|
|
2390
|
+
});
|
|
2391
|
+
|
|
2392
|
+
// Wire up queue engine hooks if a queue exists for this session
|
|
2393
|
+
setupQueueForSession(tabId);
|
|
2394
|
+
|
|
2395
|
+
broadcastSessionList();
|
|
2396
|
+
|
|
2397
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2398
|
+
res.end(JSON.stringify({ ok: true, tab_id: tabId, session_id }));
|
|
2399
|
+
} catch (e) {
|
|
2400
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2401
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2234
2406
|
function handleAttach(ws, msg) {
|
|
2235
2407
|
const session = sessions.get(msg.id);
|
|
2236
2408
|
if (!session) {
|
|
@@ -2291,6 +2463,9 @@ function handleRename(ws, msg) {
|
|
|
2291
2463
|
// Persist to DB with user_renamed flag
|
|
2292
2464
|
dbModule.setSessionTitle(id, trimmed, true);
|
|
2293
2465
|
|
|
2466
|
+
// Update startup_tasks so the label survives CTM restart
|
|
2467
|
+
try { dbModule.updateStartupTaskLabel(id, trimmed); } catch {}
|
|
2468
|
+
|
|
2294
2469
|
ws.send(JSON.stringify({ type: 'renamed', id, label: trimmed }));
|
|
2295
2470
|
broadcastSessionList();
|
|
2296
2471
|
}
|
|
@@ -97,6 +97,152 @@ Brain statistics — memory counts by source, knowledge counts, people count.
|
|
|
97
97
|
|
|
98
98
|
---
|
|
99
99
|
|
|
100
|
+
## Wall-E Skills
|
|
101
|
+
|
|
102
|
+
### GET /api/wall-e/skills
|
|
103
|
+
|
|
104
|
+
List all skills. DB skills are merged with filesystem metadata (tags, source, config schema, requires, version, execution type, author, permissions, directory path).
|
|
105
|
+
|
|
106
|
+
**Query params:**
|
|
107
|
+
- `enabled` — `1` or `0` to filter by enabled status
|
|
108
|
+
|
|
109
|
+
### POST /api/wall-e/skills
|
|
110
|
+
|
|
111
|
+
Create a skill in the database (without filesystem SKILL.md).
|
|
112
|
+
|
|
113
|
+
**Body:**
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"name": "my-skill",
|
|
117
|
+
"description": "What it does",
|
|
118
|
+
"trigger_type": "manual",
|
|
119
|
+
"prompt_template": "Instructions..."
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### PUT /api/wall-e/skills/:id
|
|
124
|
+
|
|
125
|
+
Update a skill's fields (name, description, trigger_type, trigger_config, prompt_template, enabled, config_values).
|
|
126
|
+
|
|
127
|
+
### DELETE /api/wall-e/skills/:id
|
|
128
|
+
|
|
129
|
+
Delete a skill and its execution history. For user-created skills, also deletes the `~/.wall-e/skills/<name>/` directory. Bundled skills cannot be deleted (disable them instead).
|
|
130
|
+
|
|
131
|
+
### POST /api/wall-e/skills/:id/run
|
|
132
|
+
|
|
133
|
+
Run a skill. Tries DB skill first, then falls back to filesystem skill by name.
|
|
134
|
+
|
|
135
|
+
**Response:**
|
|
136
|
+
```json
|
|
137
|
+
{ "data": { "success": true, "memoriesCreated": 3, "toolCalls": 2, "duration": 4500 } }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### GET /api/wall-e/skills/:id/executions
|
|
141
|
+
|
|
142
|
+
List execution history for a skill.
|
|
143
|
+
|
|
144
|
+
**Query params:**
|
|
145
|
+
- `limit` (default: 20)
|
|
146
|
+
|
|
147
|
+
### PUT /api/wall-e/skills/:id/config
|
|
148
|
+
|
|
149
|
+
Save user config overrides for a skill. Body is a JSON object of key-value pairs matching the SKILL.md config schema.
|
|
150
|
+
|
|
151
|
+
### GET /api/wall-e/skills/:id/source
|
|
152
|
+
|
|
153
|
+
Read the raw SKILL.md content for a skill.
|
|
154
|
+
|
|
155
|
+
**Response:**
|
|
156
|
+
```json
|
|
157
|
+
{ "data": { "content": "---\nname: ...", "path": "/abs/path/SKILL.md", "source": "bundled" } }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### PUT /api/wall-e/skills/:id/source
|
|
161
|
+
|
|
162
|
+
Rewrite the SKILL.md file for a user-created skill. Bundled skills return 403.
|
|
163
|
+
|
|
164
|
+
**Body:**
|
|
165
|
+
```json
|
|
166
|
+
{ "content": "---\nname: my-skill\n..." }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### POST /api/wall-e/skills/create-file
|
|
170
|
+
|
|
171
|
+
Create a new skill directory + SKILL.md file in `~/.wall-e/skills/` and register it in the database.
|
|
172
|
+
|
|
173
|
+
**Body:**
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"name": "my-new-skill",
|
|
177
|
+
"description": "Description",
|
|
178
|
+
"execution": "agent",
|
|
179
|
+
"trigger_type": "manual",
|
|
180
|
+
"tags": ["data", "sync"],
|
|
181
|
+
"instructions": "# My Skill\n\nDo the thing."
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Or provide raw SKILL.md content:
|
|
186
|
+
```json
|
|
187
|
+
{ "name": "my-skill", "content": "---\nname: my-skill\n---\n# Instructions" }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### GET /api/wall-e/skills/:id/export
|
|
191
|
+
|
|
192
|
+
Export a skill as a JSON bundle containing the SKILL.md and all associated files.
|
|
193
|
+
|
|
194
|
+
**Response:**
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"data": {
|
|
198
|
+
"name": "my-skill",
|
|
199
|
+
"skill_md": "---\nname: ...",
|
|
200
|
+
"files": { "run.js": "const brain = ..." }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### POST /api/wall-e/skills/import
|
|
206
|
+
|
|
207
|
+
Import a skill from a JSON bundle. Creates the skill directory in `~/.wall-e/skills/` and registers in DB.
|
|
208
|
+
|
|
209
|
+
**Body:** Same format as export response's `data` field.
|
|
210
|
+
|
|
211
|
+
### POST /api/wall-e/skills/:id/validate
|
|
212
|
+
|
|
213
|
+
Run pre-flight validation on a skill's `requires` field.
|
|
214
|
+
|
|
215
|
+
**Response:**
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"data": {
|
|
219
|
+
"valid": false,
|
|
220
|
+
"missing": [
|
|
221
|
+
{ "type": "bin", "name": "npx", "suggestion": "Install npx or add it to PATH" },
|
|
222
|
+
{ "type": "env", "name": "SLACK_TOKEN", "suggestion": "Set SLACK_TOKEN in .env or environment" }
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### GET /api/wall-e/skills/templates
|
|
229
|
+
|
|
230
|
+
List available starter skill templates.
|
|
231
|
+
|
|
232
|
+
### GET /api/wall-e/skills/installed
|
|
233
|
+
|
|
234
|
+
List filesystem-based skills (all sources: bundled, user, workspace, registry).
|
|
235
|
+
|
|
236
|
+
### GET /api/wall-e/skills/search?q=...
|
|
237
|
+
|
|
238
|
+
Search skills by name, description, and tags.
|
|
239
|
+
|
|
240
|
+
### GET /api/wall-e/skills/suggestions
|
|
241
|
+
|
|
242
|
+
Discover skills from Claude Code's MCP servers and installed skills.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
100
246
|
## Sessions
|
|
101
247
|
|
|
102
248
|
### GET /api/sessions
|
|
@@ -66,11 +66,76 @@ Full-text search across all memories using SQLite FTS5. Available as a tool in W
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
+
## Skill Management GUI
|
|
70
|
+
|
|
71
|
+
The Skills tab in the Wall-E dashboard provides a full management interface:
|
|
72
|
+
|
|
73
|
+
### Analytics Bar
|
|
74
|
+
A summary row at the top showing total skills (by source), enabled count, skills run in the last 24 hours, overall success rate, and a count of currently failing skills (clickable to filter).
|
|
75
|
+
|
|
76
|
+
### Search, Filter & Sort
|
|
77
|
+
- **Search** — real-time filter on name, description, and tags
|
|
78
|
+
- **Category pills** — derived from skill `tags[]` (e.g. sync, data, coding, communication)
|
|
79
|
+
- **Status filter** — All / Enabled / Disabled / Failing
|
|
80
|
+
- **Sort** — Name, Last Run, Success Rate, Run Count
|
|
81
|
+
|
|
82
|
+
### Skill Cards
|
|
83
|
+
Each skill is displayed as a card with:
|
|
84
|
+
- Left color stripe: green (healthy + enabled), amber (enabled + failing), gray (disabled)
|
|
85
|
+
- Name, version badge, source badge (bundled/user/workspace), execution type badge (agent/script)
|
|
86
|
+
- Tag pills, 2-line description, stats row (success rate bar, run count, last run, result dot)
|
|
87
|
+
- Run button, toggle switch, overflow menu (View Details, Edit, Export, Delete)
|
|
88
|
+
|
|
89
|
+
### Detail Panel
|
|
90
|
+
Click any card to open a right-side slide-over with five tabs:
|
|
91
|
+
|
|
92
|
+
| Tab | Contents |
|
|
93
|
+
|-----|----------|
|
|
94
|
+
| **Overview** | Full description, metadata grid (version, author, source, execution, trigger, path), tags, permissions, pre-flight requirements validation, statistics |
|
|
95
|
+
| **Config** | Auto-generated form from SKILL.md config schema — text inputs, number inputs, toggles, selects — with save button |
|
|
96
|
+
| **History** | Execution log table: timestamp, status, duration, memories created, error |
|
|
97
|
+
| **Source** | Read-only SKILL.md content, monospace formatted |
|
|
98
|
+
| **Logs** | Last execution's tool_calls and tool_results, plus error stack if applicable |
|
|
99
|
+
|
|
100
|
+
### Pre-Flight Validation
|
|
101
|
+
Skills with a `requires` field are validated before execution:
|
|
102
|
+
- `bins: [node, npx]` — checks binary availability via `which`
|
|
103
|
+
- `env: [SLACK_TOKEN]` — checks environment variables
|
|
104
|
+
- `mcp: [slack]` — checks MCP server configuration
|
|
105
|
+
|
|
106
|
+
The Overview tab shows a live validation result with actionable suggestions for any missing requirements.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
69
110
|
## Creating Custom Skills
|
|
70
111
|
|
|
71
|
-
|
|
112
|
+
### From the UI
|
|
113
|
+
|
|
114
|
+
Click **"+ New Skill"** in the Skills tab toolbar. You can:
|
|
115
|
+
|
|
116
|
+
1. **Pick a template** — Data Fetcher, Script Runner, Periodic Checker, or Manual Action
|
|
117
|
+
2. **Fill in the form** — name, description, execution type, trigger, tags, instructions
|
|
118
|
+
3. **Toggle to Raw mode** — edit the full SKILL.md directly
|
|
72
119
|
|
|
73
|
-
|
|
120
|
+
Skills are saved to `~/.wall-e/skills/<name>/SKILL.md` and auto-registered in the database.
|
|
121
|
+
|
|
122
|
+
### From the Filesystem
|
|
123
|
+
|
|
124
|
+
Create a directory with a `SKILL.md` file:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
~/.wall-e/skills/my-skill/
|
|
128
|
+
SKILL.md
|
|
129
|
+
run.js # (optional, for script skills)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Wall-E auto-discovers skills from these directories (highest precedence first):
|
|
133
|
+
1. **Workspace** — `.wall-e/skills/` in the current project
|
|
134
|
+
2. **User** — `~/.wall-e/skills/`
|
|
135
|
+
3. **Bundled** — `wall-e/skills/_bundled/`
|
|
136
|
+
4. **Registry** — `~/.wall-e/registry-skills/`
|
|
137
|
+
|
|
138
|
+
### SKILL.md Format
|
|
74
139
|
|
|
75
140
|
```yaml
|
|
76
141
|
---
|
|
@@ -81,14 +146,24 @@ execution: script
|
|
|
81
146
|
entry: run.js
|
|
82
147
|
trigger:
|
|
83
148
|
type: interval
|
|
84
|
-
|
|
85
|
-
tags: [custom]
|
|
149
|
+
interval_ms: 3600000
|
|
150
|
+
tags: [custom, sync]
|
|
151
|
+
requires:
|
|
152
|
+
bins: [node]
|
|
153
|
+
env: [MY_API_KEY]
|
|
154
|
+
config:
|
|
155
|
+
target:
|
|
156
|
+
type: string
|
|
157
|
+
description: "URL to fetch"
|
|
158
|
+
verbose:
|
|
159
|
+
type: boolean
|
|
160
|
+
default: false
|
|
86
161
|
permissions:
|
|
87
162
|
- brain:write
|
|
88
163
|
---
|
|
89
164
|
# My Skill
|
|
90
165
|
|
|
91
|
-
Additional documentation here.
|
|
166
|
+
Additional documentation and agent instructions here.
|
|
92
167
|
```
|
|
93
168
|
|
|
94
169
|
### Execution Modes
|
|
@@ -97,6 +172,19 @@ Additional documentation here.
|
|
|
97
172
|
|
|
98
173
|
**Agent mode** (`execution: agent`): LLM-driven. The markdown body of SKILL.md becomes the agent's prompt. Use for summarization, synthesis, and tasks requiring judgment.
|
|
99
174
|
|
|
175
|
+
### Config Schema
|
|
176
|
+
|
|
177
|
+
Define typed config fields in SKILL.md frontmatter:
|
|
178
|
+
|
|
179
|
+
| Type | UI Element |
|
|
180
|
+
|------|-----------|
|
|
181
|
+
| `string` | Text input |
|
|
182
|
+
| `string` + `enum` | Select dropdown |
|
|
183
|
+
| `boolean` | Toggle switch |
|
|
184
|
+
| `number` | Number input |
|
|
185
|
+
|
|
186
|
+
Users configure values in the Config tab of the detail panel. Saved values are stored per-skill in the database and merged with SKILL.md defaults at execution time.
|
|
187
|
+
|
|
100
188
|
### Script Pattern
|
|
101
189
|
|
|
102
190
|
```javascript
|
|
@@ -127,9 +215,15 @@ console.log(JSON.stringify({ inserted: 1 }));
|
|
|
127
215
|
brain.closeDb();
|
|
128
216
|
```
|
|
129
217
|
|
|
218
|
+
### Export / Import
|
|
219
|
+
|
|
220
|
+
- **Export**: From the overflow menu on any card, or from the detail panel. Downloads a JSON bundle containing the SKILL.md and all associated files.
|
|
221
|
+
- **Import**: Click "Import" in the toolbar, select a `.json` bundle file. The skill is created in `~/.wall-e/skills/` and auto-registered.
|
|
222
|
+
|
|
130
223
|
### Key Conventions
|
|
131
224
|
|
|
132
225
|
- **Deduplication**: Always use `source` + `source_id` for dedup. Check existing before inserting.
|
|
133
226
|
- **Exit codes**: 0 = success, 1 = failure, 2 = partial success (checkpoint saved)
|
|
134
227
|
- **Checkpoints**: Emit `CHECKPOINT:value` lines to stdout for resumable tasks
|
|
135
228
|
- **Config**: Passed via `WALL_E_SKILL_CONFIG` env var (JSON string)
|
|
229
|
+
- **Deletion**: User-created skills can be deleted from the UI (removes directory + DB entry). Bundled skills can only be disabled.
|
package/template/package.json
CHANGED
package/template/wall-e/agent.js
CHANGED
|
@@ -3,11 +3,13 @@ const ingest = require('./loops/ingest');
|
|
|
3
3
|
const think = require('./loops/think');
|
|
4
4
|
const reflect = require('./loops/reflect');
|
|
5
5
|
const { runDueSkills } = require('./skills/skill-planner');
|
|
6
|
+
const { loadAllSkills } = require('./skills/skill-loader');
|
|
6
7
|
const { runDueTasks, recoverInterruptedTasks } = require('./loops/tasks');
|
|
7
8
|
const CtmAdapter = require('./adapters/ctm');
|
|
8
9
|
const SlackAdapter = require('./adapters/slack');
|
|
9
10
|
const IMessageChannel = require('./channels/imessage-channel');
|
|
10
11
|
const SlackChannel = require('./channels/slack-channel');
|
|
12
|
+
const TelegramChannel = require('./channels/telegram-channel');
|
|
11
13
|
const { chat: walleChat } = require('./chat');
|
|
12
14
|
const fs = require('fs');
|
|
13
15
|
const path = require('path');
|
|
@@ -62,6 +64,7 @@ function loadOrCreateConfig() {
|
|
|
62
64
|
channels: {
|
|
63
65
|
imessage: { enabled: false, buddy_id: '' },
|
|
64
66
|
slack_dm: { enabled: false, bot_token: '' },
|
|
67
|
+
telegram: { enabled: false, bot_token: '', owner_chat_id: null },
|
|
65
68
|
},
|
|
66
69
|
};
|
|
67
70
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
@@ -106,6 +109,43 @@ function bootstrapSkills() {
|
|
|
106
109
|
console.log('[wall-e] Bootstrapped initial skills');
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Ensure all bundled filesystem skills are registered in the DB.
|
|
114
|
+
* bootstrapSkills() only runs on first boot (empty DB), so new bundled skills
|
|
115
|
+
* added later never get registered. This syncs any missing ones on every startup.
|
|
116
|
+
*/
|
|
117
|
+
function syncBundledSkills() {
|
|
118
|
+
const filesystemSkills = loadAllSkills();
|
|
119
|
+
const dbSkills = brain.listSkills({});
|
|
120
|
+
const dbNames = new Set(dbSkills.map(s => s.name));
|
|
121
|
+
|
|
122
|
+
let added = 0;
|
|
123
|
+
for (const skill of filesystemSkills) {
|
|
124
|
+
if (dbNames.has(skill.name)) continue;
|
|
125
|
+
|
|
126
|
+
const triggerType = (skill.trigger && skill.trigger.type) || skill.execution || 'manual';
|
|
127
|
+
const triggerConfig = skill.trigger && skill.trigger.interval_ms
|
|
128
|
+
? JSON.stringify({ interval_ms: skill.trigger.interval_ms })
|
|
129
|
+
: null;
|
|
130
|
+
|
|
131
|
+
brain.insertSkill({
|
|
132
|
+
name: skill.name,
|
|
133
|
+
description: skill.description || '',
|
|
134
|
+
trigger_type: triggerType,
|
|
135
|
+
trigger_config: triggerConfig,
|
|
136
|
+
prompt_template: skill.execution === 'script'
|
|
137
|
+
? `INTERNAL_SKILL:${skill.name}`
|
|
138
|
+
: skill.instructions || '',
|
|
139
|
+
});
|
|
140
|
+
added++;
|
|
141
|
+
console.log(`[wall-e] Registered bundled skill: ${skill.name}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (added > 0) {
|
|
145
|
+
console.log(`[wall-e] Synced ${added} new bundled skill(s) to DB`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
109
149
|
function bootstrapTasks() {
|
|
110
150
|
const existing = brain.listTasks({ limit: 1 });
|
|
111
151
|
if (existing.length > 0) return;
|
|
@@ -128,6 +168,7 @@ async function main() {
|
|
|
128
168
|
brain.initDb();
|
|
129
169
|
brain.startDailyBackup();
|
|
130
170
|
bootstrapSkills();
|
|
171
|
+
syncBundledSkills();
|
|
131
172
|
bootstrapTasks();
|
|
132
173
|
|
|
133
174
|
// Set owner from config
|
|
@@ -179,6 +220,19 @@ async function main() {
|
|
|
179
220
|
console.log('[wall-e] Slack DM channel enabled');
|
|
180
221
|
}
|
|
181
222
|
|
|
223
|
+
if (config.channels?.telegram?.enabled && config.channels?.telegram?.bot_token) {
|
|
224
|
+
const tg = new TelegramChannel({
|
|
225
|
+
botToken: config.channels.telegram.bot_token,
|
|
226
|
+
ownerChatId: config.channels.telegram.owner_chat_id,
|
|
227
|
+
onMessage: async (text, sender) => {
|
|
228
|
+
const result = await walleChat(text, { channel: 'telegram' });
|
|
229
|
+
return result.reply;
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
channels.push(tg);
|
|
233
|
+
console.log('[wall-e] Telegram channel enabled');
|
|
234
|
+
}
|
|
235
|
+
|
|
182
236
|
// Start all channels
|
|
183
237
|
for (const ch of channels) {
|
|
184
238
|
ch.start().catch(err => console.error(`[wall-e] Channel ${ch.name} start failed:`, err.message));
|