create-walle 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/bin/create-walle.js +134 -0
  2. package/package.json +18 -0
  3. package/template/.env.example +40 -0
  4. package/template/CLAUDE.md +12 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +167 -0
  7. package/template/bin/setup.js +100 -0
  8. package/template/claude-code-skill.md +60 -0
  9. package/template/claude-task-manager/api-prompts.js +1841 -0
  10. package/template/claude-task-manager/api-reviews.js +275 -0
  11. package/template/claude-task-manager/approval-agent.js +454 -0
  12. package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
  13. package/template/claude-task-manager/db.js +1721 -0
  14. package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
  15. package/template/claude-task-manager/git-utils.js +214 -0
  16. package/template/claude-task-manager/package-lock.json +1607 -0
  17. package/template/claude-task-manager/package.json +31 -0
  18. package/template/claude-task-manager/prompt-harvest.js +1148 -0
  19. package/template/claude-task-manager/public/css/prompts.css +880 -0
  20. package/template/claude-task-manager/public/css/reviews.css +430 -0
  21. package/template/claude-task-manager/public/css/walle.css +732 -0
  22. package/template/claude-task-manager/public/favicon.ico +0 -0
  23. package/template/claude-task-manager/public/icon.svg +37 -0
  24. package/template/claude-task-manager/public/index.html +8346 -0
  25. package/template/claude-task-manager/public/js/prompts.js +3159 -0
  26. package/template/claude-task-manager/public/js/reviews.js +1292 -0
  27. package/template/claude-task-manager/public/js/walle.js +3081 -0
  28. package/template/claude-task-manager/public/manifest.json +13 -0
  29. package/template/claude-task-manager/public/prompts.html +4353 -0
  30. package/template/claude-task-manager/public/setup.html +216 -0
  31. package/template/claude-task-manager/queue-engine.js +404 -0
  32. package/template/claude-task-manager/server-state.js +5 -0
  33. package/template/claude-task-manager/server.js +2254 -0
  34. package/template/claude-task-manager/session-utils.js +124 -0
  35. package/template/claude-task-manager/start.sh +17 -0
  36. package/template/claude-task-manager/tests/test-ai-search.js +61 -0
  37. package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
  38. package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
  39. package/template/claude-task-manager/tests/test-features-v2.js +127 -0
  40. package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
  41. package/template/claude-task-manager/tests/test-insights.js +124 -0
  42. package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
  43. package/template/claude-task-manager/tests/test-permissions.js +122 -0
  44. package/template/claude-task-manager/tests/test-pin.js +51 -0
  45. package/template/claude-task-manager/tests/test-prompts.js +164 -0
  46. package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
  47. package/template/claude-task-manager/tests/test-review.js +104 -0
  48. package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
  49. package/template/claude-task-manager/tests/test-send-final.js +30 -0
  50. package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
  51. package/template/claude-task-manager/tests/test-send-integration.js +107 -0
  52. package/template/claude-task-manager/tests/test-send-visual.js +34 -0
  53. package/template/claude-task-manager/tests/test-session-create.js +147 -0
  54. package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
  55. package/template/claude-task-manager/tests/test-url-hash.js +68 -0
  56. package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
  57. package/template/claude-task-manager/tests/test-ux-review.js +130 -0
  58. package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
  59. package/template/claude-task-manager/tests/test-zoom.js +92 -0
  60. package/template/claude-task-manager/tests/test-zoom2.js +67 -0
  61. package/template/docs/site/api/README.md +187 -0
  62. package/template/docs/site/guides/claude-code.md +58 -0
  63. package/template/docs/site/guides/configuration.md +96 -0
  64. package/template/docs/site/guides/quickstart.md +158 -0
  65. package/template/docs/site/index.md +14 -0
  66. package/template/docs/site/skills/README.md +135 -0
  67. package/template/wall-e/.dockerignore +11 -0
  68. package/template/wall-e/Dockerfile +25 -0
  69. package/template/wall-e/adapters/adapter-base.js +37 -0
  70. package/template/wall-e/adapters/ctm.js +193 -0
  71. package/template/wall-e/adapters/slack.js +56 -0
  72. package/template/wall-e/agent.js +319 -0
  73. package/template/wall-e/api-walle.js +1073 -0
  74. package/template/wall-e/brain.js +1235 -0
  75. package/template/wall-e/channels/agent-api.js +172 -0
  76. package/template/wall-e/channels/channel-base.js +14 -0
  77. package/template/wall-e/channels/imessage-channel.js +113 -0
  78. package/template/wall-e/channels/slack-channel.js +118 -0
  79. package/template/wall-e/chat.js +778 -0
  80. package/template/wall-e/decision/confidence.js +93 -0
  81. package/template/wall-e/deploy.sh +35 -0
  82. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
  83. package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
  84. package/template/wall-e/extraction/contradiction.js +168 -0
  85. package/template/wall-e/extraction/knowledge-extractor.js +190 -0
  86. package/template/wall-e/fly.toml +24 -0
  87. package/template/wall-e/loops/ingest.js +34 -0
  88. package/template/wall-e/loops/reflect.js +63 -0
  89. package/template/wall-e/loops/tasks.js +487 -0
  90. package/template/wall-e/loops/think.js +125 -0
  91. package/template/wall-e/package-lock.json +533 -0
  92. package/template/wall-e/package.json +18 -0
  93. package/template/wall-e/scripts/ingest-slack-search.js +85 -0
  94. package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
  95. package/template/wall-e/scripts/slack-backfill.js +295 -0
  96. package/template/wall-e/scripts/slack-channel-history.js +454 -0
  97. package/template/wall-e/server.js +93 -0
  98. package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
  99. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
  100. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
  101. package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
  102. package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
  103. package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
  104. package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
  105. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
  106. package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
  107. package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
  108. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
  109. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
  110. package/template/wall-e/skills/claude-code-reader.js +144 -0
  111. package/template/wall-e/skills/mcp-client.js +407 -0
  112. package/template/wall-e/skills/skill-executor.js +163 -0
  113. package/template/wall-e/skills/skill-loader.js +410 -0
  114. package/template/wall-e/skills/skill-planner.js +88 -0
  115. package/template/wall-e/skills/slack-ingest.js +329 -0
  116. package/template/wall-e/skills/slack-pull-live.js +270 -0
  117. package/template/wall-e/skills/tool-executor.js +188 -0
  118. package/template/wall-e/tests/adapter-base.test.js +20 -0
  119. package/template/wall-e/tests/adapter-ctm.test.js +122 -0
  120. package/template/wall-e/tests/adapter-slack.test.js +98 -0
  121. package/template/wall-e/tests/agent-api.test.js +256 -0
  122. package/template/wall-e/tests/api-walle.test.js +222 -0
  123. package/template/wall-e/tests/brain.test.js +602 -0
  124. package/template/wall-e/tests/channels.test.js +104 -0
  125. package/template/wall-e/tests/chat.test.js +103 -0
  126. package/template/wall-e/tests/confidence.test.js +134 -0
  127. package/template/wall-e/tests/contradiction.test.js +217 -0
  128. package/template/wall-e/tests/ingest.test.js +113 -0
  129. package/template/wall-e/tests/mcp-client.test.js +71 -0
  130. package/template/wall-e/tests/reflect.test.js +103 -0
  131. package/template/wall-e/tests/server.test.js +111 -0
  132. package/template/wall-e/tests/skills.test.js +198 -0
  133. package/template/wall-e/tests/slack-ingest.test.js +103 -0
  134. package/template/wall-e/tests/think.test.js +435 -0
  135. package/template/wall-e/tools/local-tools.js +697 -0
  136. package/template/wall-e/tools/slack-mcp.js +290 -0
@@ -0,0 +1,56 @@
1
+ const AdapterBase = require('./adapter-base');
2
+
3
+ class SlackAdapter extends AdapterBase {
4
+ constructor({ ownerNames = [] } = {}) {
5
+ super('slack');
6
+ this.ownerNames = ownerNames.map(n => n.toLowerCase());
7
+ this._pendingMessages = [];
8
+ }
9
+
10
+ feed(messages) {
11
+ this._pendingMessages.push(...messages);
12
+ }
13
+
14
+ async poll(since) {
15
+ const memories = this._pendingMessages
16
+ .map(msg => this.normalize(msg))
17
+ .filter(m => m !== null);
18
+
19
+ this._pendingMessages = [];
20
+
21
+ if (since) {
22
+ const sinceDate = new Date(since);
23
+ return memories.filter(m => new Date(m.timestamp) > sinceDate);
24
+ }
25
+
26
+ return memories;
27
+ }
28
+
29
+ normalize(rawData) {
30
+ if (!rawData) return null;
31
+ const { sender, text, channel, ts, permalink, thread_ts, timestamp } = rawData;
32
+
33
+ if (!text || !text.trim()) {
34
+ return null;
35
+ }
36
+
37
+ const isOwner = sender
38
+ ? this.ownerNames.includes(sender.toLowerCase())
39
+ : false;
40
+
41
+ return {
42
+ source: 'slack',
43
+ source_id: ts || `generated-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
44
+ source_channel: channel,
45
+ memory_type: isOwner ? 'message_sent' : 'message_received',
46
+ direction: isOwner ? 'outbound' : 'inbound',
47
+ participants: sender ? JSON.stringify([sender]) : null,
48
+ subject: thread_ts ? `thread:${thread_ts}` : null,
49
+ content: text.trim(),
50
+ metadata: JSON.stringify({ permalink, thread_ts }),
51
+ timestamp: timestamp || new Date().toISOString(),
52
+ };
53
+ }
54
+ }
55
+
56
+ module.exports = SlackAdapter;
@@ -0,0 +1,319 @@
1
+ const brain = require('./brain'); // brain.js loads .env from project root
2
+ const ingest = require('./loops/ingest');
3
+ const think = require('./loops/think');
4
+ const reflect = require('./loops/reflect');
5
+ const { runDueSkills } = require('./skills/skill-planner');
6
+ const { runDueTasks, recoverInterruptedTasks } = require('./loops/tasks');
7
+ const CtmAdapter = require('./adapters/ctm');
8
+ const SlackAdapter = require('./adapters/slack');
9
+ const IMessageChannel = require('./channels/imessage-channel');
10
+ const SlackChannel = require('./channels/slack-channel');
11
+ const { chat: walleChat } = require('./chat');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const CONFIG_PATH = path.join(__dirname, 'wall-e-config.json');
16
+ const MIN_INTERVAL_MS = 10000; // 10 seconds minimum
17
+ const MAX_INTERVAL_MS = 3600000; // 1 hour maximum
18
+
19
+ // Guards against concurrent execution of async interval callbacks
20
+ let ingestRunning = false;
21
+ let thinkRunning = false;
22
+ let reflectRunning = false;
23
+ let skillsRunning = false;
24
+ let shuttingDown = false;
25
+
26
+ function loadOrCreateConfig() {
27
+ if (fs.existsSync(CONFIG_PATH)) {
28
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
29
+ // Sync owner name from env if config still has placeholder
30
+ const envName = process.env.WALLE_OWNER_NAME;
31
+ if (envName && config.owner && (config.owner.name === 'Unknown' || !config.owner.name)) {
32
+ const parts = envName.split(/\s+/);
33
+ config.owner.name = envName;
34
+ config.owner.first_name = parts[0];
35
+ config.owner.last_name = parts.slice(1).join(' ');
36
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
37
+ console.log(`[wall-e] Updated config owner from WALLE_OWNER_NAME env var.`);
38
+ }
39
+ return config;
40
+ }
41
+ // No config file — create from env or defaults
42
+ const envName = process.env.WALLE_OWNER_NAME || 'Unknown';
43
+ const parts = envName.split(/\s+/);
44
+ const config = {
45
+ owner: {
46
+ name: envName,
47
+ first_name: parts[0],
48
+ last_name: parts.slice(1).join(' '),
49
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
50
+ },
51
+ adapters: { ctm: { enabled: true }, slack: { enabled: !!process.env.SLACK_TOKEN } },
52
+ intervals: { ingest_ms: 60000, think_ms: 120000 },
53
+ channels: {
54
+ imessage: { enabled: false, buddy_id: '' },
55
+ slack_dm: { enabled: false, bot_token: '' },
56
+ },
57
+ };
58
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
59
+ console.log(`[wall-e] Created config at ${CONFIG_PATH}`);
60
+ return config;
61
+ }
62
+
63
+ function clampInterval(value, fallback) {
64
+ const n = Number(value) || fallback;
65
+ return Math.max(MIN_INTERVAL_MS, Math.min(MAX_INTERVAL_MS, n));
66
+ }
67
+
68
+ function bootstrapSkills() {
69
+ const existing = brain.listSkills({});
70
+ if (existing.length > 0) return; // Already bootstrapped
71
+
72
+ brain.insertSkill({
73
+ name: 'scan-ctm-sessions',
74
+ description: 'Scan Claude Code session files for new conversations',
75
+ trigger_type: 'interval',
76
+ trigger_config: JSON.stringify({ interval_ms: 60000 }),
77
+ prompt_template: 'Scan the Claude Code session directory at ~/.claude/projects/ for any new or updated .jsonl session files. Read the most recently modified files and extract user messages and assistant responses as observations.',
78
+ });
79
+
80
+ brain.insertSkill({
81
+ name: 'learn-claude-code-skills',
82
+ description: 'Read Claude Code skills and MCP config to learn what tools are available',
83
+ trigger_type: 'interval',
84
+ trigger_config: JSON.stringify({ interval_ms: 3600000 }), // hourly
85
+ prompt_template: 'Read the Claude Code configuration at ~/.claude/settings.json and skill files at ~/.claude/skills/ to understand what MCP servers and skills are available. Summarize the capabilities as observations.',
86
+ });
87
+
88
+ brain.insertSkill({
89
+ name: 'ingest-slack-history',
90
+ description: 'Systematically pull all Slack conversation history (DMs, channels) going back 4 years',
91
+ trigger_type: 'interval',
92
+ trigger_config: JSON.stringify({ interval_ms: 30000 }), // Every 30s when active
93
+ prompt_template: 'INTERNAL_SKILL:slack-ingest', // Special marker -- not a Claude prompt
94
+ enabled: 0, // Disabled by default; start via UI or API
95
+ });
96
+
97
+ console.log('[wall-e] Bootstrapped initial skills');
98
+ }
99
+
100
+ async function main() {
101
+ console.log('[wall-e] Starting WALL-E agent daemon...');
102
+
103
+ if (!process.env.ANTHROPIC_API_KEY) {
104
+ console.warn('[wall-e] WARNING: ANTHROPIC_API_KEY not set — think/reflect loops will fail.');
105
+ console.warn('[wall-e] Copy .env.example to .env and add your API key.');
106
+ }
107
+
108
+ const config = loadOrCreateConfig();
109
+
110
+ // Init database (auto-creates data directory if needed)
111
+ brain.initDb();
112
+ brain.startDailyBackup();
113
+ bootstrapSkills();
114
+
115
+ // Set owner from config
116
+ if (config.owner) {
117
+ if (config.owner.name) brain.setOwner('name', String(config.owner.name).slice(0, 200));
118
+ if (config.owner.first_name) brain.setOwner('first_name', String(config.owner.first_name).slice(0, 100));
119
+ if (config.owner.last_name) brain.setOwner('last_name', String(config.owner.last_name).slice(0, 100));
120
+ if (config.owner.timezone) brain.setOwner('timezone', String(config.owner.timezone).slice(0, 50));
121
+ }
122
+ console.log(`[wall-e] Owner: ${brain.getOwnerName()}`);
123
+
124
+ // Build adapter list
125
+ const adapters = [];
126
+ if (config.adapters?.ctm?.enabled !== false) {
127
+ adapters.push(new CtmAdapter());
128
+ console.log('[wall-e] CTM adapter enabled');
129
+ }
130
+ if (config.adapters?.slack?.enabled) {
131
+ const ownerNames = [config.owner?.name, config.owner?.first_name].filter(Boolean);
132
+ adapters.push(new SlackAdapter({ ownerNames }));
133
+ console.log('[wall-e] Slack adapter enabled');
134
+ }
135
+ console.log(`[wall-e] ${adapters.length} adapter(s) active`);
136
+
137
+ // Initialize messaging channels
138
+ const channels = [];
139
+
140
+ if (config.channels?.imessage?.enabled && config.channels?.imessage?.buddy_id) {
141
+ const imsg = new IMessageChannel({
142
+ buddyId: config.channels.imessage.buddy_id,
143
+ onMessage: async (text, sender) => {
144
+ const result = await walleChat(text, { channel: 'imessage' });
145
+ return result.reply;
146
+ },
147
+ });
148
+ channels.push(imsg);
149
+ console.log('[wall-e] iMessage channel enabled');
150
+ }
151
+
152
+ if (config.channels?.slack_dm?.enabled) {
153
+ const slack = new SlackChannel({
154
+ botToken: config.channels.slack_dm.bot_token,
155
+ onMessage: async (text, sender) => {
156
+ const result = await walleChat(text, { channel: 'slack_dm' });
157
+ return result.reply;
158
+ },
159
+ });
160
+ channels.push(slack);
161
+ console.log('[wall-e] Slack DM channel enabled');
162
+ }
163
+
164
+ // Start all channels
165
+ for (const ch of channels) {
166
+ ch.start().catch(err => console.error(`[wall-e] Channel ${ch.name} start failed:`, err.message));
167
+ }
168
+ if (channels.length > 0) {
169
+ console.log(`[wall-e] ${channels.length} channel(s) started`);
170
+ }
171
+
172
+ // Always start HTTP server — WALL-E API should be independently reachable
173
+ const { startServer } = require('./server');
174
+ const httpServer = startServer();
175
+
176
+ // Recover tasks that were interrupted by previous shutdown
177
+ const recovered = recoverInterruptedTasks();
178
+ if (recovered > 0) console.log(`[wall-e] Recovered ${recovered} interrupted task(s)`);
179
+
180
+ // Initial ingest
181
+ console.log('[wall-e] Running initial ingest...');
182
+ const ingestResult = await ingest.runOnce(adapters);
183
+ console.log(`[wall-e] Initial ingest: ${ingestResult.memoriesIngested} memories`);
184
+
185
+ // Initial think
186
+ if (ingestResult.memoriesIngested > 0) {
187
+ console.log('[wall-e] Running initial think...');
188
+ const thinkResult = await think.runOnce();
189
+ console.log(`[wall-e] Initial think: ${thinkResult.memoriesProcessed} processed, ${thinkResult.knowledgeExtracted} knowledge`);
190
+ }
191
+
192
+ // Schedule loops with concurrency guards
193
+ const ingestMs = clampInterval(config.intervals?.ingest_ms, 60000);
194
+ const thinkMs = clampInterval(config.intervals?.think_ms, 120000);
195
+ const reflectMs = clampInterval(config.intervals?.reflect_ms, 3600000);
196
+
197
+ const ingestLoop = setInterval(async () => {
198
+ if (ingestRunning || shuttingDown) return;
199
+ ingestRunning = true;
200
+ try {
201
+ const result = await ingest.runOnce(adapters);
202
+ if (result.memoriesIngested > 0) {
203
+ console.log(`[wall-e] Ingested ${result.memoriesIngested} memories`);
204
+ }
205
+ } catch (err) {
206
+ console.error('[wall-e] Ingest error:', err.message);
207
+ } finally {
208
+ ingestRunning = false;
209
+ }
210
+ }, ingestMs);
211
+
212
+ const thinkLoop = setInterval(async () => {
213
+ if (thinkRunning || shuttingDown) return;
214
+ thinkRunning = true;
215
+ try {
216
+ const result = await think.runOnce();
217
+ if (result.knowledgeExtracted > 0) {
218
+ console.log(`[wall-e] Extracted ${result.knowledgeExtracted} knowledge entries`);
219
+ }
220
+ } catch (err) {
221
+ console.error('[wall-e] Think error:', err.message);
222
+ } finally {
223
+ thinkRunning = false;
224
+ }
225
+ }, thinkMs);
226
+
227
+ const reflectLoop = setInterval(async () => {
228
+ if (reflectRunning || shuttingDown) return;
229
+ reflectRunning = true;
230
+ try {
231
+ const result = await reflect.runOnce();
232
+ if (result.summaryGenerated) {
233
+ console.log('[wall-e] Daily summary generated');
234
+ }
235
+ } catch (err) {
236
+ console.error('[wall-e] Reflect error:', err.message);
237
+ } finally {
238
+ reflectRunning = false;
239
+ }
240
+ }, reflectMs);
241
+
242
+ // Skills loop
243
+ const skillMs = clampInterval(config.intervals?.skills_ms, 300000); // 5min default
244
+
245
+ const skillsLoop = setInterval(async () => {
246
+ if (skillsRunning || shuttingDown) return;
247
+ skillsRunning = true;
248
+ try {
249
+ const result = await runDueSkills();
250
+ if (result.executed > 0) {
251
+ console.log(`[wall-e] Skills: ${result.executed} executed, ${result.memoriesCreated} memories`);
252
+ }
253
+ } catch (err) {
254
+ console.error('[wall-e] Skills error:', err.message);
255
+ } finally {
256
+ skillsRunning = false;
257
+ }
258
+ }, skillMs);
259
+
260
+ // Task loop — check for due tasks every 30s
261
+ let tasksRunning = false;
262
+ const taskMs = clampInterval(config.intervals?.tasks_ms, 30000); // 30s default
263
+ const tasksLoop = setInterval(async () => {
264
+ if (tasksRunning || shuttingDown) return;
265
+ tasksRunning = true;
266
+ try {
267
+ const result = await runDueTasks();
268
+ if (result.processed > 0) {
269
+ console.log(`[wall-e] Tasks: ${result.processed} completed`);
270
+ }
271
+ } catch (err) {
272
+ console.error('[wall-e] Tasks error:', err.message);
273
+ } finally {
274
+ tasksRunning = false;
275
+ }
276
+ }, taskMs);
277
+
278
+ console.log(`[wall-e] Daemon running. Ingest every ${ingestMs / 1000}s, Think every ${thinkMs / 1000}s, Reflect every ${reflectMs / 1000}s, Skills every ${skillMs / 1000}s, Tasks every ${taskMs / 1000}s.`);
279
+ console.log('[wall-e] Press Ctrl+C to stop.');
280
+
281
+ // Graceful shutdown — wait for in-flight operations
282
+ const shutdown = async () => {
283
+ if (shuttingDown) return;
284
+ shuttingDown = true;
285
+ console.log('\n[wall-e] Shutting down...');
286
+ clearInterval(ingestLoop);
287
+ clearInterval(thinkLoop);
288
+ clearInterval(reflectLoop);
289
+ clearInterval(skillsLoop);
290
+ clearInterval(tasksLoop);
291
+
292
+ // Stop all channels
293
+ for (const ch of channels) {
294
+ try { await ch.stop(); } catch {}
295
+ }
296
+
297
+ // Wait up to 5s for in-flight operations
298
+ const deadline = Date.now() + 5000;
299
+ while ((ingestRunning || thinkRunning || reflectRunning || skillsRunning || tasksRunning) && Date.now() < deadline) {
300
+ await new Promise(r => setTimeout(r, 100));
301
+ }
302
+ if (ingestRunning || thinkRunning || reflectRunning || skillsRunning) {
303
+ console.warn('[wall-e] Force closing with operations still in-flight');
304
+ }
305
+
306
+ try { require('./skills/mcp-client').disconnectAll(); } catch {}
307
+ if (httpServer) httpServer.close();
308
+ brain.closeDb();
309
+ process.exit(0);
310
+ };
311
+ process.on('SIGINT', shutdown);
312
+ process.on('SIGTERM', shutdown);
313
+ }
314
+
315
+ main().catch(err => {
316
+ console.error('[wall-e] Fatal:', err);
317
+ try { brain.closeDb(); } catch {}
318
+ process.exit(1);
319
+ });