create-walle 0.9.0 → 0.9.3

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 (45) hide show
  1. package/README.md +35 -31
  2. package/package.json +3 -3
  3. package/template/CLAUDE.md +23 -1
  4. package/template/claude-task-manager/bin/restart-ctm.sh +3 -2
  5. package/template/claude-task-manager/db.js +38 -0
  6. package/template/claude-task-manager/public/css/walle.css +123 -0
  7. package/template/claude-task-manager/public/index.html +962 -69
  8. package/template/claude-task-manager/public/js/walle.js +374 -121
  9. package/template/claude-task-manager/public/prompts.html +84 -26
  10. package/template/claude-task-manager/public/walle-icon.svg +45 -0
  11. package/template/claude-task-manager/server.js +69 -4
  12. package/template/docs/openclaw-vs-walle-comparison.md +103 -0
  13. package/template/package.json +1 -1
  14. package/template/wall-e/agent.js +63 -3
  15. package/template/wall-e/api-walle.js +42 -0
  16. package/template/wall-e/brain.js +182 -5
  17. package/template/wall-e/channels/imessage-channel.js +4 -1
  18. package/template/wall-e/channels/slack-channel.js +3 -1
  19. package/template/wall-e/chat.js +106 -224
  20. package/template/wall-e/context/compactor.js +163 -0
  21. package/template/wall-e/context/context-builder.js +355 -0
  22. package/template/wall-e/context/state-snapshot.js +209 -0
  23. package/template/wall-e/context/token-counter.js +55 -0
  24. package/template/wall-e/context/topic-matcher.js +79 -0
  25. package/template/wall-e/core-tasks.js +24 -0
  26. package/template/wall-e/events/event-bus.js +23 -0
  27. package/template/wall-e/loops/ingest.js +4 -0
  28. package/template/wall-e/loops/initiative.js +316 -0
  29. package/template/wall-e/loops/tasks.js +55 -5
  30. package/template/wall-e/skills/_bundled/email-sync/run.js +3 -1
  31. package/template/wall-e/skills/_bundled/morning-briefing/run.js +41 -0
  32. package/template/wall-e/skills/_bundled/proactive-alerts/SKILL.md +20 -0
  33. package/template/wall-e/skills/_bundled/proactive-alerts/run.js +144 -0
  34. package/template/wall-e/skills/_bundled/slack-mentions/.watched-threads.json +18 -0
  35. package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +4 -0
  36. package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +52 -0
  37. package/template/wall-e/skills/_bundled/slack-mentions/run.js +470 -0
  38. package/template/wall-e/skills/_bundled/weekly-reflection/SKILL.md +69 -0
  39. package/template/wall-e/tests/brain.test.js +4 -4
  40. package/template/wall-e/tests/compactor.test.js +323 -0
  41. package/template/wall-e/tests/context-builder.test.js +215 -0
  42. package/template/wall-e/tests/event-bus.test.js +74 -0
  43. package/template/wall-e/tests/initiative.test.js +354 -0
  44. package/template/wall-e/tests/proactive-alerts.test.js +140 -0
  45. package/template/wall-e/tests/session-persistence.test.js +335 -0
@@ -622,6 +622,12 @@ function handleWalleApi(req, res, url) {
622
622
  try {
623
623
  const status = params.get('status') || undefined;
624
624
  const limit = parseLimit(params, 50);
625
+ // Load active watched threads from brain DB for Slack task enrichment
626
+ let watchedThreads = [];
627
+ try {
628
+ if (brain.listActiveSlackThreads) watchedThreads = brain.listActiveSlackThreads(2 * 60 * 60 * 1000);
629
+ } catch {}
630
+
625
631
  const data = brain.listTasks({ status, limit }).map(function(t) {
626
632
  if (t.started_at && t.completed_at) {
627
633
  // Normalize timestamps: append 'Z' if missing to ensure UTC parsing
@@ -630,6 +636,20 @@ function handleWalleApi(req, res, url) {
630
636
  const dur = new Date(c).getTime() - new Date(s).getTime();
631
637
  if (dur > 0 && dur < 86400000) t.last_duration_ms = dur; // sanity: max 24h
632
638
  }
639
+ // Enrich Slack tasks with watched thread info
640
+ if (t.source === 'slack' && t.id) {
641
+ const watched = watchedThreads.find(w => w.task_id === t.id);
642
+ if (watched) {
643
+ const expiresAt = new Date(new Date(watched.last_activity).getTime() + 2 * 60 * 60 * 1000);
644
+ t.slack_thread = {
645
+ channel_id: watched.channel_id,
646
+ thread_ts: watched.thread_ts,
647
+ last_activity: watched.last_activity,
648
+ expires_at: expiresAt.toISOString(),
649
+ active: expiresAt > new Date(),
650
+ };
651
+ }
652
+ }
633
653
  return t;
634
654
  });
635
655
  jsonResponse(res, { data });
@@ -668,6 +688,8 @@ function handleWalleApi(req, res, url) {
668
688
  due_at: body.due_at,
669
689
  skill: body.skill,
670
690
  skill_config: body.skill_config ? (typeof body.skill_config === 'string' ? body.skill_config : JSON.stringify(body.skill_config)) : undefined,
691
+ source: body.source,
692
+ source_ref: body.source_ref,
671
693
  });
672
694
  jsonResponse(res, { data: { id: result.id } }, 201);
673
695
  } catch (e) {
@@ -1224,6 +1246,26 @@ function handleWalleApi(req, res, url) {
1224
1246
  return true;
1225
1247
  }
1226
1248
 
1249
+ // POST /api/wall-e/webhook — receive external events
1250
+ if (p === '/api/wall-e/webhook' && m === 'POST') {
1251
+ // Verify webhook secret if configured
1252
+ const expectedToken = process.env.WALLE_WEBHOOK_SECRET;
1253
+ if (expectedToken && req.headers['authorization'] !== `Bearer ${expectedToken}`) {
1254
+ jsonResponse(res, { error: 'Unauthorized' }, 401);
1255
+ return true;
1256
+ }
1257
+ readBody(req).then(body => {
1258
+ if (!body.source || !body.event) {
1259
+ return jsonResponse(res, { error: 'source and event are required' }, 400);
1260
+ }
1261
+ const eventBus = require('./events/event-bus');
1262
+ const { event, source, ...payload } = body;
1263
+ eventBus.emitWebhook(source, { event, ...payload });
1264
+ jsonResponse(res, { ok: true, message: `Webhook received from ${source}` });
1265
+ }).catch(e => jsonResponse(res, { error: e.message }, 400));
1266
+ return true;
1267
+ }
1268
+
1227
1269
  // No matching route under /api/wall-e
1228
1270
  jsonResponse(res, { error: 'Not found' }, 404);
1229
1271
  return true;
@@ -77,6 +77,15 @@ function initDb(dbPath) {
77
77
  try { newDb.prepare("SELECT skill_config FROM tasks LIMIT 0").run(); } catch (_) {
78
78
  newDb.prepare("ALTER TABLE tasks ADD COLUMN skill_config TEXT").run();
79
79
  }
80
+ // Migration: add source/source_ref columns to tasks table
81
+ try { newDb.prepare("SELECT source FROM tasks LIMIT 0").run(); } catch (_) {
82
+ newDb.prepare("ALTER TABLE tasks ADD COLUMN source TEXT DEFAULT 'system'").run();
83
+ }
84
+ try { newDb.prepare("SELECT source_ref FROM tasks LIMIT 0").run(); } catch (_) {
85
+ newDb.prepare("ALTER TABLE tasks ADD COLUMN source_ref TEXT").run();
86
+ }
87
+ try { newDb.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_tasks_source'").get() ||
88
+ newDb.prepare("CREATE INDEX IF NOT EXISTS idx_tasks_source ON tasks(source)").run(); } catch (_) {}
80
89
  } catch (err) {
81
90
  newDb.close();
82
91
  db = null;
@@ -379,12 +388,57 @@ function createTables() {
379
388
  result TEXT,
380
389
  error TEXT,
381
390
  checkpoint TEXT,
391
+ source TEXT DEFAULT 'system',
392
+ source_ref TEXT,
382
393
  created_at TEXT DEFAULT (datetime('now')),
383
394
  updated_at TEXT DEFAULT (datetime('now'))
384
395
  );
385
396
  CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
386
397
  CREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_at);
387
398
  CREATE INDEX IF NOT EXISTS idx_tasks_next ON tasks(next_run_at);
399
+ CREATE INDEX IF NOT EXISTS idx_tasks_source ON tasks(source);
400
+
401
+ CREATE TABLE IF NOT EXISTS initiative_log (
402
+ id TEXT PRIMARY KEY,
403
+ trigger TEXT NOT NULL,
404
+ state_snapshot TEXT,
405
+ reasoning TEXT,
406
+ decision TEXT NOT NULL,
407
+ decision_data TEXT,
408
+ autonomy_tier INTEGER,
409
+ created_at TEXT DEFAULT (datetime('now'))
410
+ );
411
+ CREATE INDEX IF NOT EXISTS idx_initiative_time ON initiative_log(created_at);
412
+
413
+ CREATE TABLE IF NOT EXISTS sessions (
414
+ id TEXT PRIMARY KEY,
415
+ channel TEXT NOT NULL,
416
+ summary TEXT,
417
+ compacted_messages TEXT,
418
+ turn_count INTEGER DEFAULT 0,
419
+ token_estimate INTEGER DEFAULT 0,
420
+ metadata TEXT,
421
+ created_at TEXT DEFAULT (datetime('now')),
422
+ updated_at TEXT DEFAULT (datetime('now'))
423
+ );
424
+ CREATE INDEX IF NOT EXISTS idx_sessions_channel ON sessions(channel, updated_at);
425
+
426
+ CREATE TABLE IF NOT EXISTS brain_metadata (
427
+ key TEXT PRIMARY KEY,
428
+ value TEXT NOT NULL
429
+ );
430
+
431
+ CREATE TABLE IF NOT EXISTS slack_threads (
432
+ id TEXT PRIMARY KEY,
433
+ channel_id TEXT NOT NULL,
434
+ thread_ts TEXT NOT NULL,
435
+ task_id TEXT,
436
+ session_id TEXT,
437
+ last_activity TEXT NOT NULL,
438
+ last_seen_ts TEXT,
439
+ created_at TEXT DEFAULT (datetime('now'))
440
+ );
441
+ CREATE INDEX IF NOT EXISTS idx_slack_threads_active ON slack_threads(last_activity);
388
442
  `);
389
443
  }
390
444
 
@@ -1085,14 +1139,21 @@ function updateSkillStats(skillId, success) {
1085
1139
 
1086
1140
  // ── Tasks ──
1087
1141
 
1088
- function insertTask({ title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config }) {
1142
+ function insertTask({ title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config, source, source_ref }) {
1143
+ // Dedup: skip if a task with the same title+source+source_ref already exists and isn't archived
1144
+ if (title && source) {
1145
+ const existing = source_ref
1146
+ ? getDb().prepare(`SELECT id, status FROM tasks WHERE title = ? AND source = ? AND source_ref = ? AND status != 'archived' LIMIT 1`).get(title, source, source_ref)
1147
+ : getDb().prepare(`SELECT id, status FROM tasks WHERE title = ? AND source = ? AND status NOT IN ('archived','completed') LIMIT 1`).get(title, source);
1148
+ if (existing) return { id: existing.id, deduplicated: true };
1149
+ }
1089
1150
  const id = uuidv4();
1090
1151
  getDb().prepare(`
1091
- INSERT INTO tasks (id, title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config)
1092
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1152
+ INSERT INTO tasks (id, title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config, source, source_ref)
1153
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1093
1154
  `).run(id, title, description || null, priority || 'normal', type || 'once',
1094
1155
  execution || 'chat', script || null, schedule || null, due_at || null, next_run_at || due_at || null,
1095
- skill || null, skill_config || null);
1156
+ skill || null, skill_config || null, source || 'system', source_ref || null);
1096
1157
  return { id };
1097
1158
  }
1098
1159
 
@@ -1110,7 +1171,7 @@ function listTasks({ status, limit } = {}) {
1110
1171
  }
1111
1172
 
1112
1173
  function updateTask(id, updates) {
1113
- const allowed = ['title', 'description', 'status', 'priority', 'type', 'execution', 'script', 'schedule', 'due_at', 'next_run_at', 'started_at', 'completed_at', 'last_run_at', 'run_count', 'result', 'error', 'checkpoint', 'skill', 'skill_config'];
1174
+ const allowed = ['title', 'description', 'status', 'priority', 'type', 'execution', 'script', 'schedule', 'due_at', 'next_run_at', 'started_at', 'completed_at', 'last_run_at', 'run_count', 'result', 'error', 'checkpoint', 'skill', 'skill_config', 'source', 'source_ref'];
1114
1175
  const sets = [];
1115
1176
  const params = [];
1116
1177
  for (const [k, v] of Object.entries(updates)) {
@@ -1190,6 +1251,108 @@ function listOpenBriefingItems(skill) {
1190
1251
  ).all(skill);
1191
1252
  }
1192
1253
 
1254
+ // -- Sessions (context compaction) --
1255
+
1256
+ function getSession(id) {
1257
+ return getDb().prepare('SELECT * FROM sessions WHERE id = ?').get(id) || null;
1258
+ }
1259
+
1260
+ function upsertSession({ id, channel, summary, compacted_messages, turn_count, token_estimate, metadata }) {
1261
+ const now = new Date().toISOString();
1262
+ getDb().prepare(`
1263
+ INSERT INTO sessions (id, channel, summary, compacted_messages, turn_count, token_estimate, metadata, created_at, updated_at)
1264
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1265
+ ON CONFLICT(id) DO UPDATE SET
1266
+ summary = excluded.summary,
1267
+ compacted_messages = excluded.compacted_messages,
1268
+ turn_count = excluded.turn_count,
1269
+ token_estimate = excluded.token_estimate,
1270
+ metadata = excluded.metadata,
1271
+ updated_at = excluded.updated_at
1272
+ `).run(id, channel || 'ctm', summary || null, compacted_messages || null,
1273
+ turn_count || 0, token_estimate || 0, metadata || null, now, now);
1274
+ return id;
1275
+ }
1276
+
1277
+ function listSessions({ channel, limit } = {}) {
1278
+ const params = [];
1279
+ let sql = 'SELECT * FROM sessions';
1280
+ if (channel) { sql += ' WHERE channel = ?'; params.push(channel); }
1281
+ sql += ' ORDER BY updated_at DESC';
1282
+ if (limit) { sql += ' LIMIT ?'; params.push(limit); }
1283
+ return getDb().prepare(sql).all(...params);
1284
+ }
1285
+
1286
+ function expireSessions(maxAgeHours = 24) {
1287
+ const db = getDb();
1288
+ const cutoff = new Date(Date.now() - maxAgeHours * 3600000).toISOString();
1289
+ const result = db.prepare("DELETE FROM sessions WHERE updated_at < ?").run(cutoff);
1290
+ return result.changes;
1291
+ }
1292
+
1293
+ // -- Initiative Log CRUD --
1294
+
1295
+ function insertInitiativeLog({ trigger, state_snapshot, reasoning, decision, decision_data, autonomy_tier }) {
1296
+ if (!trigger) throw new Error('Initiative log requires trigger');
1297
+ if (!decision) throw new Error('Initiative log requires decision');
1298
+ const id = uuidv4();
1299
+ getDb().prepare(
1300
+ 'INSERT INTO initiative_log (id, trigger, state_snapshot, reasoning, decision, decision_data, autonomy_tier) VALUES (?, ?, ?, ?, ?, ?, ?)'
1301
+ ).run(id, trigger, state_snapshot || null, reasoning || null, decision, decision_data || null, autonomy_tier || 1);
1302
+ return id;
1303
+ }
1304
+
1305
+ function listInitiativeLogs({ limit, since } = {}) {
1306
+ const params = [];
1307
+ let sql = 'SELECT * FROM initiative_log';
1308
+ if (since) { sql += ' WHERE created_at >= ?'; params.push(since); }
1309
+ sql += ' ORDER BY created_at DESC';
1310
+ if (limit) { sql += ' LIMIT ?'; params.push(limit); }
1311
+ return getDb().prepare(sql).all(...params);
1312
+ }
1313
+
1314
+ // -- Slack Threads CRUD --
1315
+
1316
+ function upsertSlackThread({ channelId, threadTs, taskId, sessionId }) {
1317
+ const id = `${channelId}:${threadTs}`;
1318
+ const now = new Date().toISOString();
1319
+ getDb().prepare(`
1320
+ INSERT INTO slack_threads (id, channel_id, thread_ts, task_id, session_id, last_activity)
1321
+ VALUES (?, ?, ?, ?, ?, ?)
1322
+ ON CONFLICT(id) DO UPDATE SET
1323
+ task_id = excluded.task_id,
1324
+ session_id = excluded.session_id,
1325
+ last_activity = excluded.last_activity
1326
+ `).run(id, channelId, threadTs, taskId || null, sessionId || null, now);
1327
+ return id;
1328
+ }
1329
+
1330
+ function getSlackThread(channelId, threadTs) {
1331
+ return getDb().prepare('SELECT * FROM slack_threads WHERE id = ?').get(`${channelId}:${threadTs}`);
1332
+ }
1333
+
1334
+ function listActiveSlackThreads(watchDurationMs) {
1335
+ const cutoff = new Date(Date.now() - (watchDurationMs || 7200000)).toISOString();
1336
+ return getDb().prepare('SELECT * FROM slack_threads WHERE last_activity >= ? ORDER BY last_activity DESC').all(cutoff);
1337
+ }
1338
+
1339
+ function updateSlackThread(id, updates) {
1340
+ const allowed = ['last_activity', 'last_seen_ts', 'task_id', 'session_id'];
1341
+ const sets = [];
1342
+ const vals = [];
1343
+ for (const [k, v] of Object.entries(updates)) {
1344
+ if (allowed.includes(k)) { sets.push(`${k} = ?`); vals.push(v); }
1345
+ }
1346
+ if (sets.length === 0) return;
1347
+ vals.push(id);
1348
+ getDb().prepare(`UPDATE slack_threads SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
1349
+ }
1350
+
1351
+ function deleteExpiredSlackThreads(watchDurationMs) {
1352
+ const cutoff = new Date(Date.now() - (watchDurationMs || 7200000)).toISOString();
1353
+ return getDb().prepare('DELETE FROM slack_threads WHERE last_activity < ?').run(cutoff).changes;
1354
+ }
1355
+
1193
1356
  module.exports = {
1194
1357
  initDb,
1195
1358
  getDb,
@@ -1277,4 +1440,18 @@ module.exports = {
1277
1440
  updateBriefingItem,
1278
1441
  findBriefingItemByTitle,
1279
1442
  listOpenBriefingItems,
1443
+ // Sessions (context compaction)
1444
+ getSession,
1445
+ upsertSession,
1446
+ listSessions,
1447
+ expireSessions,
1448
+ // Initiative Log
1449
+ insertInitiativeLog,
1450
+ listInitiativeLogs,
1451
+ // Slack Threads
1452
+ upsertSlackThread,
1453
+ getSlackThread,
1454
+ listActiveSlackThreads,
1455
+ updateSlackThread,
1456
+ deleteExpiredSlackThreads,
1280
1457
  };
@@ -2,6 +2,7 @@
2
2
  const { execFile } = require('child_process');
3
3
  const { promisify } = require('util');
4
4
  const ChannelBase = require('./channel-base');
5
+ const eventBus = require('../events/event-bus');
5
6
 
6
7
  const execFileAsync = promisify(execFile);
7
8
 
@@ -43,9 +44,10 @@ class IMessageChannel extends ChannelBase {
43
44
 
44
45
  async _checkMessages() {
45
46
  // Use AppleScript to get the latest message from the buddy
47
+ const sanitizedBuddy = String(this.buddyId).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
46
48
  const script = `
47
49
  tell application "Messages"
48
- set targetBuddy to "${this.buddyId}"
50
+ set targetBuddy to "${sanitizedBuddy}"
49
51
  set latestMsg to ""
50
52
  set latestId to ""
51
53
  repeat with svc in services
@@ -76,6 +78,7 @@ class IMessageChannel extends ChannelBase {
76
78
  this.lastMessageId = msgId;
77
79
 
78
80
  console.log(`[imessage] New message from ${this.buddyId}: ${msgText.slice(0, 50)}...`);
81
+ eventBus.emitMessage('imessage', msgText, this.buddyId);
79
82
 
80
83
  if (this._onMessage) {
81
84
  const response = await this._onMessage(msgText, this.buddyId);
@@ -1,11 +1,12 @@
1
1
  'use strict';
2
2
  const ChannelBase = require('./channel-base');
3
+ const eventBus = require('../events/event-bus');
3
4
 
4
5
  class SlackChannel extends ChannelBase {
5
6
  constructor(opts = {}) {
6
7
  super('slack_dm');
7
8
  this.botToken = opts.botToken || process.env.SLACK_BOT_TOKEN || null;
8
- this.pollInterval = opts.pollInterval || 15000; // 15s
9
+ this.pollInterval = opts.pollInterval || 5000; // 5s
9
10
  this.lastTimestamp = null;
10
11
  this._pollTimer = null;
11
12
  this._onMessage = opts.onMessage || null;
@@ -70,6 +71,7 @@ class SlackChannel extends ChannelBase {
70
71
 
71
72
  this.lastTimestamp = msg.ts;
72
73
  console.log(`[slack_dm] New DM from ${msg.user}: ${(msg.text || '').slice(0, 50)}...`);
74
+ eventBus.emitMessage('slack_dm', msg.text, msg.user);
73
75
 
74
76
  if (this._onMessage && msg.text) {
75
77
  const response = await this._onMessage(msg.text, msg.user);