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
@@ -5,6 +5,34 @@ const path = require('path');
5
5
  const DATA_DIR = process.env.WALL_E_DATA_DIR || path.join(process.env.HOME, '.walle', 'data');
6
6
  const BRAIN_DB_PATH = path.join(DATA_DIR, 'wall-e-brain.db');
7
7
 
8
+ // Build a SKILL.md from structured form data
9
+ function buildSkillMd(body) {
10
+ const lines = ['---'];
11
+ lines.push(`name: ${body.name}`);
12
+ if (body.description) lines.push(`description: ${body.description}`);
13
+ if (body.version) lines.push(`version: ${body.version}`);
14
+ if (body.author) lines.push(`author: ${body.author}`);
15
+ lines.push(`execution: ${body.execution || 'agent'}`);
16
+ if (body.entry) lines.push(`entry: ${body.entry}`);
17
+ lines.push('trigger:');
18
+ lines.push(` type: ${body.trigger_type || 'manual'}`);
19
+ if (body.interval_ms) lines.push(` interval_ms: ${body.interval_ms}`);
20
+ if (body.tags && body.tags.length) lines.push(`tags: [${body.tags.join(', ')}]`);
21
+ if (body.config_schema) {
22
+ lines.push('config:');
23
+ for (const [k, v] of Object.entries(body.config_schema)) {
24
+ lines.push(` ${k}:`);
25
+ if (v.type) lines.push(` type: ${v.type}`);
26
+ if (v.description) lines.push(` description: "${v.description}"`);
27
+ if (v.default !== undefined) lines.push(` default: ${v.default}`);
28
+ }
29
+ }
30
+ lines.push('---');
31
+ lines.push('');
32
+ lines.push(body.instructions || `# ${body.name}\n\nDescribe what this skill does.`);
33
+ return lines.join('\n');
34
+ }
35
+
8
36
  let readDb = null;
9
37
 
10
38
  function getReadDb() {
@@ -561,6 +589,61 @@ function handleWalleApi(req, res, url) {
561
589
  return true;
562
590
  }
563
591
 
592
+ // POST /api/wall-e/chat/clear — clear all messages in a session (for branch switching)
593
+ if (p === '/api/wall-e/chat/clear' && m === 'POST') {
594
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
595
+ readBody(req).then(body => {
596
+ try {
597
+ brain.clearChatSession(body.session_id || 'default');
598
+ jsonResponse(res, { data: { ok: true } });
599
+ } catch (e) {
600
+ jsonResponse(res, { error: e.message }, 500);
601
+ }
602
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
603
+ return true;
604
+ }
605
+
606
+ // POST /api/wall-e/chat/insert — insert a single chat message (for branch sync)
607
+ if (p === '/api/wall-e/chat/insert' && m === 'POST') {
608
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
609
+ readBody(req).then(body => {
610
+ try {
611
+ brain.insertChatMessage({
612
+ role: body.role,
613
+ content: body.content,
614
+ session_id: body.session_id || 'default',
615
+ channel: body.channel || 'ctm',
616
+ });
617
+ jsonResponse(res, { data: { ok: true } });
618
+ } catch (e) {
619
+ jsonResponse(res, { error: e.message }, 500);
620
+ }
621
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
622
+ return true;
623
+ }
624
+
625
+ // GET /api/wall-e/chat/branches — load branch data for a session
626
+ if (p === '/api/wall-e/chat/branches' && m === 'GET') {
627
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
628
+ const sid = url.searchParams.get('session_id') || 'default';
629
+ try {
630
+ jsonResponse(res, { data: brain.loadChatBranches(sid) });
631
+ } catch (e) { jsonResponse(res, { error: e.message }, 500); }
632
+ return true;
633
+ }
634
+
635
+ // POST /api/wall-e/chat/branches — save branch data for a session
636
+ if (p === '/api/wall-e/chat/branches' && m === 'POST') {
637
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
638
+ readBody(req).then(body => {
639
+ try {
640
+ brain.saveChatBranches(body.session_id || 'default', body.branches || {}, body.active || {}, body.history || []);
641
+ jsonResponse(res, { data: { ok: true } });
642
+ } catch (e) { jsonResponse(res, { error: e.message }, 500); }
643
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
644
+ return true;
645
+ }
646
+
564
647
  // GET /api/wall-e/backups — list brain backups
565
648
  if (p === '/api/wall-e/backups' && m === 'GET') {
566
649
  if (!brain) return jsonResponse(res, { error: 'Brain not available' }, 503), true;
@@ -958,11 +1041,54 @@ function handleWalleApi(req, res, url) {
958
1041
 
959
1042
  // GET /api/wall-e/skills
960
1043
  if (p === '/api/wall-e/skills' && m === 'GET') {
961
- if (!brain) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
1044
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
962
1045
  try {
963
1046
  const enabled = params.get('enabled');
964
1047
  const filter = enabled !== null ? { enabled: parseInt(enabled) } : {};
965
- const data = brain.listSkills(filter);
1048
+
1049
+ // Auto-register any filesystem skills not yet in the DB
1050
+ try {
1051
+ const { loadAllSkills, reloadSkills } = require('./skills/skill-loader');
1052
+ reloadSkills(); // clear cache so new dirs are picked up
1053
+ const fsSkills = loadAllSkills();
1054
+ const dbSkills = brain.listSkills({});
1055
+ const dbNames = new Set(dbSkills.map(s => s.name));
1056
+ for (const skill of fsSkills) {
1057
+ if (dbNames.has(skill.name)) continue;
1058
+ const triggerType = (skill.trigger && skill.trigger.type) || skill.execution || 'manual';
1059
+ const triggerConfig = skill.trigger && skill.trigger.interval_ms
1060
+ ? JSON.stringify({ interval_ms: skill.trigger.interval_ms }) : null;
1061
+ brain.insertSkill({
1062
+ name: skill.name,
1063
+ description: skill.description || '',
1064
+ trigger_type: triggerType,
1065
+ trigger_config: triggerConfig,
1066
+ prompt_template: skill.execution === 'script'
1067
+ ? `INTERNAL_SKILL:${skill.name}` : skill.instructions || '',
1068
+ });
1069
+ }
1070
+ } catch (syncErr) {
1071
+ console.warn('[api-walle] Skill sync error:', syncErr.message);
1072
+ }
1073
+
1074
+ const dbSkills = brain.listSkills(filter);
1075
+ // Merge filesystem metadata (tags, source, config, requires, version, execution) into DB skills
1076
+ const { findSkill } = require('./skills/skill-loader');
1077
+ const data = dbSkills.map(s => {
1078
+ const fs = findSkill(s.name);
1079
+ if (!fs) return s;
1080
+ return Object.assign({}, s, {
1081
+ tags: fs.tags || [],
1082
+ source: fs.source || 'bundled',
1083
+ config_schema: fs.config || {},
1084
+ requires: fs.requires || {},
1085
+ version: fs.version || '0.0.0',
1086
+ execution: fs.execution || 'agent',
1087
+ author: fs.author || null,
1088
+ permissions: fs.permissions || [],
1089
+ skill_dir: fs.dir || null,
1090
+ });
1091
+ });
966
1092
  jsonResponse(res, { data });
967
1093
  } catch (e) {
968
1094
  jsonResponse(res, { error: e.message }, 500);
@@ -1088,7 +1214,7 @@ function handleWalleApi(req, res, url) {
1088
1214
  // GET /api/wall-e/skills/:id/executions
1089
1215
  const skillExecMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/executions$/);
1090
1216
  if (skillExecMatch && m === 'GET') {
1091
- if (!brain) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
1217
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
1092
1218
  try {
1093
1219
  const limit = parseLimit(params, 20);
1094
1220
  const data = brain.listSkillExecutions({ skill_id: skillExecMatch[1], limit });
@@ -1146,6 +1272,250 @@ function handleWalleApi(req, res, url) {
1146
1272
  return true;
1147
1273
  }
1148
1274
 
1275
+ // DELETE /api/wall-e/skills/:id
1276
+ const skillDeleteMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)$/);
1277
+ if (skillDeleteMatch && m === 'DELETE') {
1278
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
1279
+ try {
1280
+ const skill = brain.deleteSkill(skillDeleteMatch[1]);
1281
+ // If user-created skill, also delete the directory
1282
+ if (skill) {
1283
+ const { findSkill } = require('./skills/skill-loader');
1284
+ const fsSkill = findSkill(skill.name);
1285
+ if (fsSkill && fsSkill.source === 'user' && fsSkill.dir) {
1286
+ try {
1287
+ const fs = require('fs');
1288
+ fs.rmSync(fsSkill.dir, { recursive: true, force: true });
1289
+ } catch (rmErr) {
1290
+ console.warn('[api-walle] Could not delete skill dir:', rmErr.message);
1291
+ }
1292
+ }
1293
+ const { reloadSkills } = require('./skills/skill-loader');
1294
+ reloadSkills();
1295
+ }
1296
+ jsonResponse(res, { data: { ok: true, deleted: skill?.name } });
1297
+ } catch (e) {
1298
+ jsonResponse(res, { error: e.message }, 400);
1299
+ }
1300
+ return true;
1301
+ }
1302
+
1303
+ // PUT /api/wall-e/skills/:id/config — save user config overrides
1304
+ const skillConfigMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/config$/);
1305
+ if (skillConfigMatch && m === 'PUT') {
1306
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
1307
+ readBody(req).then(body => {
1308
+ try {
1309
+ brain.updateSkill(skillConfigMatch[1], {
1310
+ config_values: typeof body === 'string' ? body : JSON.stringify(body)
1311
+ });
1312
+ jsonResponse(res, { data: { ok: true } });
1313
+ } catch (e) {
1314
+ jsonResponse(res, { error: e.message }, 400);
1315
+ }
1316
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
1317
+ return true;
1318
+ }
1319
+
1320
+ // GET /api/wall-e/skills/:id/source — read raw SKILL.md content
1321
+ const skillSourceReadMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/source$/);
1322
+ if (skillSourceReadMatch && m === 'GET') {
1323
+ try {
1324
+ // Try by DB id first, then by name
1325
+ let skillName = skillSourceReadMatch[1];
1326
+ if (brain && ensureBrainInit()) {
1327
+ const dbSkill = brain.getSkill(skillName);
1328
+ if (dbSkill) skillName = dbSkill.name;
1329
+ }
1330
+ const { findSkill } = require('./skills/skill-loader');
1331
+ const fsSkill = findSkill(skillName);
1332
+ if (!fsSkill) return jsonResponse(res, { error: 'Skill not found' }, 404), true;
1333
+ const fs = require('fs');
1334
+ const content = fs.readFileSync(fsSkill.filePath, 'utf8');
1335
+ jsonResponse(res, { data: { content, path: fsSkill.filePath, source: fsSkill.source } });
1336
+ } catch (e) {
1337
+ jsonResponse(res, { error: e.message }, 500);
1338
+ }
1339
+ return true;
1340
+ }
1341
+
1342
+ // PUT /api/wall-e/skills/:id/source — rewrite SKILL.md file
1343
+ const skillSourceWriteMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/source$/);
1344
+ if (skillSourceWriteMatch && m === 'PUT') {
1345
+ readBody(req).then(body => {
1346
+ try {
1347
+ let skillName = skillSourceWriteMatch[1];
1348
+ if (brain && ensureBrainInit()) {
1349
+ const dbSkill = brain.getSkill(skillName);
1350
+ if (dbSkill) skillName = dbSkill.name;
1351
+ }
1352
+ const { findSkill, reloadSkills } = require('./skills/skill-loader');
1353
+ const fsSkill = findSkill(skillName);
1354
+ if (!fsSkill) return jsonResponse(res, { error: 'Skill not found' }, 404);
1355
+ if (fsSkill.source === 'bundled') return jsonResponse(res, { error: 'Cannot edit bundled skills' }, 403);
1356
+ const fs = require('fs');
1357
+ fs.writeFileSync(fsSkill.filePath, body.content, 'utf8');
1358
+ reloadSkills();
1359
+ jsonResponse(res, { data: { ok: true } });
1360
+ } catch (e) {
1361
+ jsonResponse(res, { error: e.message }, 500);
1362
+ }
1363
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
1364
+ return true;
1365
+ }
1366
+
1367
+ // POST /api/wall-e/skills/create-file — create new skill directory + SKILL.md
1368
+ if (p === '/api/wall-e/skills/create-file' && m === 'POST') {
1369
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
1370
+ readBody(req).then(body => {
1371
+ try {
1372
+ const fs = require('fs');
1373
+ const path = require('path');
1374
+ const { USER_DIR, reloadSkills, parseSkillMd } = require('./skills/skill-loader');
1375
+ if (!body.name) return jsonResponse(res, { error: 'name is required' }, 400);
1376
+ const slug = body.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
1377
+ const skillDir = path.join(USER_DIR, slug);
1378
+ if (fs.existsSync(skillDir)) return jsonResponse(res, { error: 'Skill directory already exists' }, 409);
1379
+ fs.mkdirSync(skillDir, { recursive: true });
1380
+ const content = body.content || buildSkillMd(body);
1381
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content, 'utf8');
1382
+ reloadSkills();
1383
+ // Auto-register in DB
1384
+ const parsed = parseSkillMd(content);
1385
+ const triggerType = (parsed?.meta?.trigger?.type) || body.trigger_type || 'manual';
1386
+ const result = brain.insertSkill({
1387
+ name: body.name,
1388
+ description: body.description || parsed?.meta?.description || '',
1389
+ trigger_type: triggerType,
1390
+ prompt_template: parsed?.body || body.instructions || '',
1391
+ });
1392
+ jsonResponse(res, { data: { id: result.id, dir: skillDir } }, 201);
1393
+ } catch (e) {
1394
+ jsonResponse(res, { error: e.message }, 500);
1395
+ }
1396
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
1397
+ return true;
1398
+ }
1399
+
1400
+ // GET /api/wall-e/skills/:id/export — export skill as JSON bundle
1401
+ const skillExportMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/export$/);
1402
+ if (skillExportMatch && m === 'GET') {
1403
+ try {
1404
+ let skillName = skillExportMatch[1];
1405
+ if (brain && ensureBrainInit()) {
1406
+ const dbSkill = brain.getSkill(skillName);
1407
+ if (dbSkill) skillName = dbSkill.name;
1408
+ }
1409
+ const { findSkill } = require('./skills/skill-loader');
1410
+ const fsSkill = findSkill(skillName);
1411
+ if (!fsSkill) return jsonResponse(res, { error: 'Skill not found' }, 404), true;
1412
+ const fs = require('fs');
1413
+ const path = require('path');
1414
+ const bundle = { name: fsSkill.name, skill_md: fs.readFileSync(fsSkill.filePath, 'utf8'), files: {} };
1415
+ // Include other files in the skill directory
1416
+ const entries = fs.readdirSync(fsSkill.dir);
1417
+ for (const entry of entries) {
1418
+ if (entry === 'SKILL.md') continue;
1419
+ const fp = path.join(fsSkill.dir, entry);
1420
+ const stat = fs.statSync(fp);
1421
+ if (stat.isFile() && stat.size < 512 * 1024) {
1422
+ bundle.files[entry] = fs.readFileSync(fp, 'utf8');
1423
+ }
1424
+ }
1425
+ jsonResponse(res, { data: bundle });
1426
+ } catch (e) {
1427
+ jsonResponse(res, { error: e.message }, 500);
1428
+ }
1429
+ return true;
1430
+ }
1431
+
1432
+ // POST /api/wall-e/skills/import — import skill from JSON bundle
1433
+ if (p === '/api/wall-e/skills/import' && m === 'POST') {
1434
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
1435
+ readBody(req).then(body => {
1436
+ try {
1437
+ const fs = require('fs');
1438
+ const path = require('path');
1439
+ const { USER_DIR, reloadSkills, parseSkillMd } = require('./skills/skill-loader');
1440
+ if (!body.skill_md) return jsonResponse(res, { error: 'skill_md is required' }, 400);
1441
+ const parsed = parseSkillMd(body.skill_md);
1442
+ if (!parsed || !parsed.meta || !parsed.meta.name) return jsonResponse(res, { error: 'Invalid SKILL.md' }, 400);
1443
+ const slug = parsed.meta.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
1444
+ const skillDir = path.join(USER_DIR, slug);
1445
+ fs.mkdirSync(skillDir, { recursive: true });
1446
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), body.skill_md, 'utf8');
1447
+ if (body.files) {
1448
+ for (const [fname, fcontent] of Object.entries(body.files)) {
1449
+ const safeName = path.basename(fname);
1450
+ fs.writeFileSync(path.join(skillDir, safeName), fcontent, 'utf8');
1451
+ }
1452
+ }
1453
+ reloadSkills();
1454
+ const triggerType = (parsed.meta.trigger?.type) || 'manual';
1455
+ const result = brain.insertSkill({
1456
+ name: parsed.meta.name,
1457
+ description: parsed.meta.description || '',
1458
+ trigger_type: triggerType,
1459
+ prompt_template: parsed.body || '',
1460
+ });
1461
+ jsonResponse(res, { data: { id: result.id, name: parsed.meta.name, dir: skillDir } }, 201);
1462
+ } catch (e) {
1463
+ jsonResponse(res, { error: e.message }, 500);
1464
+ }
1465
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
1466
+ return true;
1467
+ }
1468
+
1469
+ // GET /api/wall-e/skills/templates — list available starter templates
1470
+ if (p === '/api/wall-e/skills/templates' && m === 'GET') {
1471
+ try {
1472
+ const fs = require('fs');
1473
+ const tplDir = path.join(__dirname, 'skills', '_templates');
1474
+ const templates = [];
1475
+ if (fs.existsSync(tplDir)) {
1476
+ for (const file of fs.readdirSync(tplDir)) {
1477
+ if (!file.endsWith('.md')) continue;
1478
+ const content = fs.readFileSync(path.join(tplDir, file), 'utf8');
1479
+ const { parseSkillMd } = require('./skills/skill-loader');
1480
+ const parsed = parseSkillMd(content);
1481
+ templates.push({
1482
+ id: file.replace('.md', ''),
1483
+ name: parsed?.meta?.name || file.replace('.md', ''),
1484
+ description: parsed?.meta?.description || '',
1485
+ execution: parsed?.meta?.execution || 'agent',
1486
+ trigger: parsed?.meta?.trigger?.type || 'manual',
1487
+ content,
1488
+ });
1489
+ }
1490
+ }
1491
+ jsonResponse(res, { data: templates });
1492
+ } catch (e) {
1493
+ jsonResponse(res, { error: e.message }, 500);
1494
+ }
1495
+ return true;
1496
+ }
1497
+
1498
+ // POST /api/wall-e/skills/:id/validate — pre-flight validation
1499
+ const skillValidateMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/validate$/);
1500
+ if (skillValidateMatch && m === 'POST') {
1501
+ try {
1502
+ let skillName = skillValidateMatch[1];
1503
+ if (brain && ensureBrainInit()) {
1504
+ const dbSkill = brain.getSkill(skillName);
1505
+ if (dbSkill) skillName = dbSkill.name;
1506
+ }
1507
+ const { findSkill } = require('./skills/skill-loader');
1508
+ const fsSkill = findSkill(skillName);
1509
+ if (!fsSkill) return jsonResponse(res, { error: 'Skill not found' }, 404), true;
1510
+ const { validateSkillRequirements } = require('./skills/skill-validator');
1511
+ const result = validateSkillRequirements(fsSkill);
1512
+ jsonResponse(res, { data: result });
1513
+ } catch (e) {
1514
+ jsonResponse(res, { error: e.message }, 500);
1515
+ }
1516
+ return true;
1517
+ }
1518
+
1149
1519
  // --- Slack Ingest ---
1150
1520
 
1151
1521
  // GET /api/wall-e/slack-ingest/progress
@@ -1266,6 +1636,85 @@ function handleWalleApi(req, res, url) {
1266
1636
  return true;
1267
1637
  }
1268
1638
 
1639
+ // POST /api/wall-e/coding — start a coding task
1640
+ if (p === '/api/wall-e/coding' && m === 'POST') {
1641
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
1642
+ readBody(req).then(body => {
1643
+ try {
1644
+ if (!body.request) {
1645
+ jsonResponse(res, { error: 'request field is required' }, 400);
1646
+ return;
1647
+ }
1648
+ const result = brain.insertTask({
1649
+ title: (body.request || '').slice(0, 100),
1650
+ description: `Coding request: ${body.request}`,
1651
+ priority: body.priority || 'normal',
1652
+ type: 'once',
1653
+ execution: 'skill',
1654
+ skill: 'coding-agent',
1655
+ skill_config: JSON.stringify({
1656
+ request: body.request,
1657
+ cwd: body.cwd || process.cwd(),
1658
+ options: body.options || { delivery: 'commit' },
1659
+ }),
1660
+ source: 'api',
1661
+ source_ref: '',
1662
+ });
1663
+ jsonResponse(res, { ok: true, task_id: result.id });
1664
+ } catch (e) {
1665
+ jsonResponse(res, { error: e.message }, 500);
1666
+ }
1667
+ }).catch(e => jsonResponse(res, { error: e.message }, 500));
1668
+ return true;
1669
+ }
1670
+
1671
+ // GET /api/wall-e/config — read wall-e-config.json
1672
+ if (p === '/api/wall-e/config' && m === 'GET') {
1673
+ try {
1674
+ const fs = require('fs');
1675
+ const cfgPath = require('path').join(__dirname, 'wall-e-config.json');
1676
+ if (!fs.existsSync(cfgPath)) return jsonResponse(res, { data: {} }), true;
1677
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
1678
+ // Redact tokens — only send whether they're set (show last 4 chars if long enough)
1679
+ const safe = JSON.parse(JSON.stringify(cfg));
1680
+ const redact = (t) => t && t.length > 8 ? '••••' + t.slice(-4) : '••••';
1681
+ if (safe.channels?.telegram?.bot_token) safe.channels.telegram.bot_token = redact(safe.channels.telegram.bot_token);
1682
+ if (safe.channels?.slack_dm?.bot_token) safe.channels.slack_dm.bot_token = redact(safe.channels.slack_dm.bot_token);
1683
+ return jsonResponse(res, { data: safe }), true;
1684
+ } catch (e) {
1685
+ return jsonResponse(res, { error: e.message }, 500), true;
1686
+ }
1687
+ }
1688
+
1689
+ // PUT /api/wall-e/config — update wall-e-config.json (partial merge)
1690
+ if (p === '/api/wall-e/config' && m === 'PUT') {
1691
+ return readBody(req).then(body => {
1692
+ try {
1693
+ const fs = require('fs');
1694
+ const cfgPath = require('path').join(__dirname, 'wall-e-config.json');
1695
+ let cfg = {};
1696
+ if (fs.existsSync(cfgPath)) cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
1697
+ // Deep merge channels
1698
+ if (body.channels) {
1699
+ if (!cfg.channels) cfg.channels = {};
1700
+ for (const [ch, val] of Object.entries(body.channels)) {
1701
+ if (!cfg.channels[ch]) cfg.channels[ch] = {};
1702
+ for (const [k, v] of Object.entries(val)) {
1703
+ // Skip redacted token values (don't overwrite real token with masked one)
1704
+ if (typeof v === 'string' && v.startsWith('••••')) continue;
1705
+ cfg.channels[ch][k] = v;
1706
+ }
1707
+ }
1708
+ }
1709
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), { mode: 0o600 });
1710
+ jsonResponse(res, { ok: true, message: 'Config saved. Restart Wall-E for changes to take effect.' });
1711
+ } catch (e) {
1712
+ jsonResponse(res, { error: e.message }, 500);
1713
+ }
1714
+ }).catch(e => jsonResponse(res, { error: e.message }, 500));
1715
+ return true;
1716
+ }
1717
+
1269
1718
  // No matching route under /api/wall-e
1270
1719
  jsonResponse(res, { error: 'Not found' }, 404);
1271
1720
  return true;
@@ -86,6 +86,10 @@ function initDb(dbPath) {
86
86
  }
87
87
  try { newDb.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_tasks_source'").get() ||
88
88
  newDb.prepare("CREATE INDEX IF NOT EXISTS idx_tasks_source ON tasks(source)").run(); } catch (_) {}
89
+ // Migration: add config_values column to skills table
90
+ try { newDb.prepare("SELECT config_values FROM skills LIMIT 0").run(); } catch (_) {
91
+ newDb.prepare("ALTER TABLE skills ADD COLUMN config_values TEXT").run();
92
+ }
89
93
  } catch (err) {
90
94
  newDb.close();
91
95
  db = null;
@@ -317,6 +321,13 @@ function createTables() {
317
321
  CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id, created_at);
318
322
  CREATE INDEX IF NOT EXISTS idx_chat_messages_time ON chat_messages(created_at);
319
323
 
324
+ CREATE TABLE IF NOT EXISTS chat_branches (
325
+ session_id TEXT PRIMARY KEY,
326
+ branches TEXT NOT NULL DEFAULT '{}',
327
+ active TEXT NOT NULL DEFAULT '{}',
328
+ history TEXT NOT NULL DEFAULT '[]'
329
+ );
330
+
320
331
  CREATE TABLE IF NOT EXISTS skills (
321
332
  id TEXT PRIMARY KEY,
322
333
  name TEXT NOT NULL UNIQUE,
@@ -1048,6 +1059,28 @@ function clearChatSession(session_id) {
1048
1059
  getDb().prepare('DELETE FROM chat_messages WHERE session_id = ?').run(session_id);
1049
1060
  }
1050
1061
 
1062
+ // ── Chat Branches (ChatGPT-style edit/resend) ──
1063
+
1064
+ function saveChatBranches(session_id, branches, active, history) {
1065
+ getDb().prepare(`
1066
+ INSERT INTO chat_branches (session_id, branches, active, history)
1067
+ VALUES (?, ?, ?, ?)
1068
+ ON CONFLICT(session_id) DO UPDATE SET branches = excluded.branches, active = excluded.active, history = excluded.history
1069
+ `).run(session_id, JSON.stringify(branches), JSON.stringify(active), JSON.stringify(history || []));
1070
+ }
1071
+
1072
+ function loadChatBranches(session_id) {
1073
+ const row = getDb().prepare('SELECT branches, active, history FROM chat_branches WHERE session_id = ?').get(session_id);
1074
+ if (!row) return { branches: {}, active: {}, history: [] };
1075
+ try {
1076
+ return {
1077
+ branches: JSON.parse(row.branches),
1078
+ active: JSON.parse(row.active),
1079
+ history: JSON.parse(row.history || '[]'),
1080
+ };
1081
+ } catch (e) { return { branches: {}, active: {}, history: [] }; }
1082
+ }
1083
+
1051
1084
  // -- Skills CRUD --
1052
1085
 
1053
1086
  function insertSkill({ name, description, tool_definitions, trigger_type, trigger_config, prompt_template }) {
@@ -1081,7 +1114,7 @@ function listSkills({ enabled } = {}) {
1081
1114
  return getDb().prepare('SELECT * FROM skills ORDER BY name').all();
1082
1115
  }
1083
1116
 
1084
- const SKILL_ALLOWED_FIELDS = ['name', 'description', 'tool_definitions', 'trigger_type', 'trigger_config', 'prompt_template', 'enabled'];
1117
+ const SKILL_ALLOWED_FIELDS = ['name', 'description', 'tool_definitions', 'trigger_type', 'trigger_config', 'prompt_template', 'enabled', 'config_values'];
1085
1118
 
1086
1119
  function updateSkill(id, updates) {
1087
1120
  if (!updates || typeof updates !== 'object') throw new Error('Updates must be an object');
@@ -1137,6 +1170,14 @@ function updateSkillStats(skillId, success) {
1137
1170
  if (result.changes === 0) throw new Error(`Skill not found: ${skillId}`);
1138
1171
  }
1139
1172
 
1173
+ function deleteSkill(id) {
1174
+ const skill = getDb().prepare('SELECT * FROM skills WHERE id = ?').get(id);
1175
+ if (!skill) throw new Error(`Skill not found: ${id}`);
1176
+ getDb().prepare('DELETE FROM skill_executions WHERE skill_id = ?').run(id);
1177
+ getDb().prepare('DELETE FROM skills WHERE id = ?').run(id);
1178
+ return skill;
1179
+ }
1180
+
1140
1181
  // ── Tasks ──
1141
1182
 
1142
1183
  function insertTask({ title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config, source, source_ref }) {
@@ -1412,6 +1453,8 @@ module.exports = {
1412
1453
  searchChatMessages,
1413
1454
  deleteChatMessages,
1414
1455
  clearChatSession,
1456
+ saveChatBranches,
1457
+ loadChatBranches,
1415
1458
  // Backup
1416
1459
  createBackup,
1417
1460
  listBackups,
@@ -1423,6 +1466,7 @@ module.exports = {
1423
1466
  getSkillByName,
1424
1467
  listSkills,
1425
1468
  updateSkill,
1469
+ deleteSkill,
1426
1470
  insertSkillExecution,
1427
1471
  listSkillExecutions,
1428
1472
  updateSkillStats,
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+ const { Bot } = require('grammy');
3
+ const ChannelBase = require('./channel-base');
4
+ const eventBus = require('../events/event-bus');
5
+
6
+ class TelegramChannel extends ChannelBase {
7
+ constructor(opts = {}) {
8
+ super('telegram');
9
+ this.botToken = opts.botToken || process.env.TELEGRAM_BOT_TOKEN || null;
10
+ this.ownerChatId = opts.ownerChatId ? Number(opts.ownerChatId) : null;
11
+ this._onMessage = opts.onMessage || null;
12
+ this._bot = null;
13
+ }
14
+
15
+ async start() {
16
+ if (!this.botToken) {
17
+ console.log('[telegram] No bot token configured, skipping');
18
+ return;
19
+ }
20
+
21
+ this._bot = new Bot(this.botToken);
22
+
23
+ // Setup helper — tells the user their chat ID for config
24
+ this._bot.command('whoami', (ctx) => {
25
+ ctx.reply(`Your chat ID: ${ctx.from.id}`);
26
+ });
27
+
28
+ // Text message handler
29
+ this._bot.on('message:text', async (ctx) => {
30
+ const senderId = ctx.from.id;
31
+ const text = ctx.message.text;
32
+
33
+ // Owner-only gate (both should be Numbers, but coerce for safety)
34
+ if (this.ownerChatId && Number(senderId) !== Number(this.ownerChatId)) {
35
+ await ctx.reply('This is a private assistant.');
36
+ return;
37
+ }
38
+
39
+ console.log(`[telegram] Message from ${senderId}: ${text.slice(0, 50)}...`);
40
+ eventBus.emitMessage('telegram', text, String(senderId));
41
+
42
+ if (this._onMessage) {
43
+ try {
44
+ const response = await this._onMessage(text, String(senderId));
45
+ if (response) {
46
+ // Telegram has a 4096 char limit per message — split if needed
47
+ const MAX_LEN = 4096;
48
+ if (response.length <= MAX_LEN) {
49
+ await ctx.reply(response);
50
+ } else {
51
+ for (let i = 0; i < response.length; i += MAX_LEN) {
52
+ await ctx.reply(response.slice(i, i + MAX_LEN));
53
+ }
54
+ }
55
+ }
56
+ } catch (err) {
57
+ console.error('[telegram] onMessage error:', err.message);
58
+ await ctx.reply('Sorry, something went wrong processing your message.');
59
+ }
60
+ }
61
+ });
62
+
63
+ // Error handler
64
+ this._bot.catch((err) => {
65
+ console.error('[telegram] Bot error:', err.message);
66
+ });
67
+
68
+ // Start long polling (non-blocking)
69
+ this._bot.start({
70
+ onStart: () => {
71
+ this.running = true;
72
+ console.log('[telegram] Bot started (long polling)');
73
+ },
74
+ });
75
+ }
76
+
77
+ async stop() {
78
+ this.running = false;
79
+ if (this._bot) {
80
+ await this._bot.stop();
81
+ console.log('[telegram] Bot stopped');
82
+ }
83
+ }
84
+
85
+ async send(chatId, message) {
86
+ if (!this._bot) return;
87
+ try {
88
+ await this._bot.api.sendMessage(chatId, message);
89
+ console.log(`[telegram] Sent message to ${chatId}`);
90
+ } catch (err) {
91
+ console.error('[telegram] Send error:', err.message);
92
+ }
93
+ }
94
+ }
95
+
96
+ module.exports = TelegramChannel;