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.
Files changed (75) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/template/claude-task-manager/db.js +5 -1
  4. package/template/claude-task-manager/public/css/walle.css +317 -0
  5. package/template/claude-task-manager/public/index.html +404 -101
  6. package/template/claude-task-manager/public/js/walle.js +1256 -86
  7. package/template/claude-task-manager/server.js +189 -14
  8. package/template/docs/site/api/README.md +146 -0
  9. package/template/docs/site/skills/README.md +99 -5
  10. package/template/package.json +1 -1
  11. package/template/wall-e/agent.js +54 -0
  12. package/template/wall-e/api-walle.js +452 -3
  13. package/template/wall-e/brain.js +45 -1
  14. package/template/wall-e/channels/telegram-channel.js +96 -0
  15. package/template/wall-e/chat.js +61 -2
  16. package/template/wall-e/coding-context.js +252 -0
  17. package/template/wall-e/coding-orchestrator.js +625 -0
  18. package/template/wall-e/coding-review.js +189 -0
  19. package/template/wall-e/core-tasks.js +12 -3
  20. package/template/wall-e/deploy.sh +4 -4
  21. package/template/wall-e/fly.toml +2 -2
  22. package/template/wall-e/package.json +4 -1
  23. package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
  24. package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
  25. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
  26. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
  27. package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
  28. package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
  29. package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
  30. package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
  31. package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
  32. package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
  33. package/template/wall-e/skills/_templates/manual-action.md +19 -0
  34. package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
  35. package/template/wall-e/skills/_templates/script-runner.md +21 -0
  36. package/template/wall-e/skills/claude-code-reader.js +16 -4
  37. package/template/wall-e/skills/skill-executor.js +23 -1
  38. package/template/wall-e/skills/skill-validator.js +73 -0
  39. package/template/wall-e/tests/brain.test.js +3 -3
  40. package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
  41. package/template/wall-e/tests/coding-context.test.js +212 -0
  42. package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
  43. package/template/wall-e/tests/coding-review.test.js +141 -0
  44. package/template/claude-task-manager/package-lock.json +0 -1607
  45. package/template/claude-task-manager/tests/test-ai-search.js +0 -61
  46. package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
  47. package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
  48. package/template/claude-task-manager/tests/test-features-v2.js +0 -127
  49. package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
  50. package/template/claude-task-manager/tests/test-insights.js +0 -124
  51. package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
  52. package/template/claude-task-manager/tests/test-permissions.js +0 -122
  53. package/template/claude-task-manager/tests/test-pin.js +0 -51
  54. package/template/claude-task-manager/tests/test-prompts.js +0 -164
  55. package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
  56. package/template/claude-task-manager/tests/test-review.js +0 -104
  57. package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
  58. package/template/claude-task-manager/tests/test-send-final.js +0 -30
  59. package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
  60. package/template/claude-task-manager/tests/test-send-integration.js +0 -107
  61. package/template/claude-task-manager/tests/test-send-visual.js +0 -34
  62. package/template/claude-task-manager/tests/test-session-create.js +0 -147
  63. package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
  64. package/template/claude-task-manager/tests/test-url-hash.js +0 -68
  65. package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
  66. package/template/claude-task-manager/tests/test-ux-review.js +0 -130
  67. package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
  68. package/template/claude-task-manager/tests/test-zoom.js +0 -92
  69. package/template/claude-task-manager/tests/test-zoom2.js +0 -67
  70. package/template/docs/openclaw-vs-walle-comparison.md +0 -103
  71. package/template/docs/ux-improvement-plan.md +0 -84
  72. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
  73. package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
  74. package/template/wall-e/package-lock.json +0 -533
  75. 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 using virtual terminal screen
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.vterm.feed(data);
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
- ptyProcess.onData((data) => {
2186
- session.lastActivity = Date.now();
2187
- // Only strip \e[3J (Erase Scrollback) it destroys scroll history on replay.
2188
- // Do NOT strip \e[2J or \e[?1049h/l those are needed for Claude Code's TUI rendering.
2189
- const cleanData = data.replace(/\x1b\[3J/g, '');
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(cleanData);
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: cleanData });
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
- // If the client provided an explicit label (e.g. prompt title), persist it
2226
- if (msg.label) {
2227
- dbModule.setSessionTitle(id, label, false);
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
- Skills are defined as `SKILL.md` files with YAML frontmatter. Place them in `wall-e/skills/_bundled/your-skill/`.
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
- ### Minimal Example
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
- schedule: "every 1h"
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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "walle",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "private": true,
5
5
  "description": "Wall-E — your personal digital twin",
6
6
  "scripts": {
@@ -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));