create-walle 0.7.1 → 0.8.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.
@@ -5,9 +5,10 @@ let currentView = 'chat';
5
5
  let cache = {};
6
6
  let timelineOffset = 0;
7
7
 
8
- // WALL-E API base URL — points directly to the WALL-E process (port 3457)
9
- // Falls back to same-origin proxy if WALL-E is unreachable
10
- var WALLE_BASE = 'http://localhost:3457';
8
+ // WALL-E API base URL — derived from page's CTM port (Wall-E = CTM + 1).
9
+ // Falls back to same-origin proxy if WALL-E is unreachable.
10
+ var _ctmPort = parseInt(location.port) || 3456;
11
+ var WALLE_BASE = 'http://localhost:' + (_ctmPort + 1);
11
12
  var walleBaseResolved = false;
12
13
 
13
14
  function resolveWalleBase() {
@@ -187,6 +188,21 @@ WE.init = function() {
187
188
  WE.showView(currentView);
188
189
  };
189
190
 
191
+ WE.toggleMoreTabs = function() {
192
+ const dd = document.getElementById('we-more-dropdown');
193
+ dd.style.display = dd.style.display === 'none' ? 'block' : 'none';
194
+ if (dd.style.display === 'block') {
195
+ setTimeout(() => document.addEventListener('click', WE._closeMoreTabsOutside, true), 0);
196
+ }
197
+ };
198
+ WE.closeMoreTabs = function() {
199
+ document.getElementById('we-more-dropdown').style.display = 'none';
200
+ document.removeEventListener('click', WE._closeMoreTabsOutside, true);
201
+ };
202
+ WE._closeMoreTabsOutside = function(e) {
203
+ if (!document.getElementById('we-more-wrap').contains(e.target)) WE.closeMoreTabs();
204
+ };
205
+
190
206
  WE.showView = function(view) {
191
207
  // Clean up task pollers/timers when leaving tasks view
192
208
  if (currentView === 'tasks' && view !== 'tasks') {
@@ -739,7 +755,7 @@ WE.renderChat = function() {
739
755
  api('/chat/history?session_id=default&limit=200').then(function(data) {
740
756
  var msgs = data.data || [];
741
757
  if (Array.isArray(msgs) && msgs.length > 0) {
742
- chatHistory = msgs.map(function(m) { return { role: m.role, text: m.content }; });
758
+ chatHistory = msgs.map(function(m) { return { role: m.role, text: m.content, created_at: m.created_at }; });
743
759
  // Populate user message history (index 0 = most recent)
744
760
  userMessageHistory = [];
745
761
  for (var i = msgs.length - 1; i >= 0; i--) {
@@ -788,7 +804,7 @@ function renderChatUI() {
788
804
  html += '<div class="we-search-header">' + chatSearchResults.length + ' result' + (chatSearchResults.length !== 1 ? 's' : '') + ' for "' + esc(chatSearchQuery) + '"</div>';
789
805
  chatSearchResults.forEach(function(msg) {
790
806
  var roleClass = msg.role === 'user' ? 'user' : 'assistant';
791
- var roleLabel = msg.role === 'user' ? 'YOU' : 'WALL-E';
807
+ var roleLabel = msg.role === 'user' ? 'You' : 'WALL-E';
792
808
  var ts = msg.created_at ? new Date(msg.created_at + 'Z').toLocaleString() : '';
793
809
  html += '<div class="we-search-result">';
794
810
  html += '<div class="we-search-result-meta"><span class="walle-chat-msg-role ' + roleClass + '">' + roleLabel + '</span> <span class="we-search-result-time">' + esc(ts) + '</span></div>';
@@ -820,7 +836,7 @@ function renderChatUI() {
820
836
  }
821
837
  // User message
822
838
  html += '<div class="walle-chat-msg user" data-idx="' + i + '">';
823
- html += '<div class="walle-chat-msg-role user">YOU';
839
+ html += '<div class="walle-chat-msg-role user">You';
824
840
  html += ' <button class="we-edit-btn" onclick="WE._editMessage(' + i + ')" title="Edit & resend">&#9998;</button>';
825
841
  html += ' <button class="we-edit-btn we-delete-btn" onclick="WE._deleteMessage(' + i + ')" title="Delete this exchange">&#128465;</button>';
826
842
  html += '</div>';
@@ -1502,7 +1518,7 @@ WE._exportAsImage = function() {
1502
1518
  turns.forEach(function(t) {
1503
1519
  turnHtml += '<div class="turn">';
1504
1520
  if (t.userText) {
1505
- turnHtml += '<div class="role user">YOU</div>';
1521
+ turnHtml += '<div class="role user">You</div>';
1506
1522
  turnHtml += '<div class="msg">' + esc(t.userText).replace(/\n/g, '<br>') + '</div>';
1507
1523
  }
1508
1524
  if (t.assistantText) {
@@ -2160,8 +2176,8 @@ function _renderTaskResultPanel(t) {
2160
2176
  var lineCount = t.result.split('\n').length;
2161
2177
  var isMarkdown = _hasMarkdown(t.result);
2162
2178
  var isShort = lineCount <= 12;
2163
- // Auto-expand short outputs
2164
- var expandedClass = isShort ? ' expanded' : '';
2179
+ // Collapsed by default — user clicks to expand
2180
+ var expandedClass = '';
2165
2181
 
2166
2182
  html += '<div class="we-task-output-panel' + expandedClass + '">';
2167
2183
  html += '<div class="we-task-output-header" onclick="this.parentElement.classList.toggle(\'expanded\')">';
@@ -2169,18 +2185,17 @@ function _renderTaskResultPanel(t) {
2169
2185
  if (timeLabel) html += '<span class="we-task-output-time">' + esc(timeLabel) + '</span>';
2170
2186
  html += '<span class="we-task-output-lines">' + lineCount + ' lines</span>';
2171
2187
  html += '<button class="we-task-output-copy" onclick="event.stopPropagation();WE._copyTaskOutput(\'' + tid + '\')" title="Copy output">Copy</button>';
2188
+ html += '<button class="we-task-output-copy" onclick="event.stopPropagation();WE._copyTaskLink(\'' + tid + '\')" title="Copy shareable link">Link</button>';
2172
2189
  html += '<span class="we-task-output-toggle"></span>';
2173
2190
  html += '</div>';
2174
2191
 
2175
- if (!isShort) {
2176
- // Show a summary preview for long outputs
2177
- var summary = _extractResultSummary(t.result);
2178
- if (summary) {
2179
- if (isMarkdown) {
2180
- html += '<div class="we-task-output-summary we-task-md">' + _renderMarkdown(summary) + '</div>';
2181
- } else {
2182
- html += '<div class="we-task-output-summary">' + esc(summary) + '</div>';
2183
- }
2192
+ // Show a summary preview (collapsed by default)
2193
+ var summary = isShort ? t.result.split('\n').slice(0, 3).join('\n') : _extractResultSummary(t.result);
2194
+ if (summary) {
2195
+ if (isMarkdown) {
2196
+ html += '<div class="we-task-output-summary we-task-md">' + _renderMarkdown(summary) + '</div>';
2197
+ } else {
2198
+ html += '<div class="we-task-output-summary">' + esc(summary) + '</div>';
2184
2199
  }
2185
2200
  }
2186
2201
 
@@ -2213,12 +2228,19 @@ WE._copyTaskOutput = function(taskId) {
2213
2228
  }
2214
2229
  };
2215
2230
 
2231
+ WE._copyTaskLink = function(taskId) {
2232
+ var url = location.origin + '/#walle/task/' + taskId;
2233
+ navigator.clipboard.writeText(url).then(function() {
2234
+ if (typeof window.toast === 'function') window.toast('Task link copied', { type: 'success' });
2235
+ });
2236
+ };
2237
+
2216
2238
  function renderTaskCard(t) {
2217
2239
  var q = _taskFilter.search || '';
2218
2240
  var statusColors = { running: '#228be6', pending: '#fab005', completed: '#5c940d', failed: '#e03131', paused: '#888', cancelled: '#666' };
2219
2241
  var borderColor = statusColors[t.status] || '#333';
2220
2242
 
2221
- var html = '<div class="we-task-card" style="border-left-color:' + borderColor + '">';
2243
+ var html = '<div class="we-task-card" id="task-' + esc(t.id) + '" style="border-left-color:' + borderColor + '">';
2222
2244
 
2223
2245
  // Row 1: title + badges + status
2224
2246
  html += '<div class="we-task-card-header">';
@@ -2228,40 +2250,47 @@ function renderTaskCard(t) {
2228
2250
  else html += ' <span class="walle-tag" style="background:#1a1a3e;color:#60a5fa">AI</span>';
2229
2251
  if (t.skill) html += ' <span class="walle-tag" style="background:#1a3a1a;color:#4ade80">' + esc(t.skill) + '</span>';
2230
2252
  html += '</span>';
2231
- html += '<span class="we-task-card-status" style="color:' + borderColor + '">' + esc(t.status) + '</span>';
2253
+ html += '<span class="we-task-card-status" style="color:' + borderColor + '"><span class="we-status-dot we-status-dot--' + esc(t.status) + '"></span>' + esc(t.status) + '</span>';
2232
2254
  html += '</div>';
2233
2255
 
2234
2256
  // Row 2: description
2235
2257
  if (t.description) html += '<div class="we-task-card-desc">' + _hl(t.description, q) + '</div>';
2236
2258
 
2237
- // Row 3: metadata chips
2259
+ // Row 3: metadata chips — primary (schedule + last run) always visible
2238
2260
  html += '<div class="we-task-meta">';
2239
2261
  if (t.type === 'recurring' && t.schedule) html += '<span>\uD83D\uDD01 ' + esc(t.schedule) + '</span>';
2240
- if (t.run_count > 0) html += '<span>\u25B6 ' + t.run_count + ' runs</span>';
2241
2262
  if (t.last_run_at) html += '<span>\u23F1 Last: ' + esc(timeAgo(t.last_run_at)) + '</span>';
2242
2263
  if (t.status === 'running' && t.started_at) {
2243
- // Show live-ticking elapsed time for currently running task
2244
2264
  var startMs = new Date(t.started_at + (t.started_at.includes('Z') ? '' : 'Z')).getTime();
2245
2265
  var elapsedSec = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
2246
2266
  var elStr = elapsedSec < 60 ? elapsedSec + 's' : Math.floor(elapsedSec / 60) + 'm ' + (elapsedSec % 60) + 's';
2247
2267
  html += '<span class="we-task-running-timer" data-start-ms="' + startMs + '" style="color:#228be6">\u23F3 Running: ' + elStr + '</span>';
2248
- } else if (t.last_duration_ms && t.last_duration_ms > 0) {
2249
- var durSec = Math.round(t.last_duration_ms / 1000);
2250
- var durStr = durSec < 60 ? durSec + 's' : Math.floor(durSec / 60) + 'm ' + (durSec % 60) + 's';
2251
- html += '<span>\u23F1 Took: ' + durStr + '</span>';
2252
- }
2253
- if (t.next_run_at && (t.status === 'pending' || t.status === 'paused')) {
2254
- var nextDate = new Date(t.next_run_at + (t.next_run_at.includes('Z') ? '' : 'Z'));
2255
- var isFarFuture = nextDate.getFullYear() > 2099;
2256
- if (!isFarFuture) {
2257
- var secUntil = Math.floor((nextDate - new Date()) / 1000);
2258
- if (secUntil <= 0) {
2259
- html += '<span style="color:#fab005">\u23ED Next: overdue</span>';
2260
- } else {
2261
- var nextStr = secUntil < 60 ? secUntil + 's' : secUntil < 3600 ? Math.floor(secUntil / 60) + 'm' : Math.floor(secUntil / 3600) + 'h';
2262
- html += '<span>\u23ED Next: ' + nextStr + '</span>';
2268
+ }
2269
+ // Secondary metadata collapsed behind "details" toggle
2270
+ var _hasExtra = (t.run_count > 0) || (t.last_duration_ms && t.last_duration_ms > 0 && t.status !== 'running') || (t.next_run_at && (t.status === 'pending' || t.status === 'paused'));
2271
+ if (_hasExtra) {
2272
+ html += '<span class="we-task-meta-toggle" onclick="this.parentElement.classList.toggle(\'we-meta-expanded\');event.stopPropagation()">details</span>';
2273
+ html += '<span class="we-task-meta-extra">';
2274
+ if (t.run_count > 0) html += '<span>\u25B6 ' + t.run_count + ' runs</span>';
2275
+ if (t.last_duration_ms && t.last_duration_ms > 0 && t.status !== 'running') {
2276
+ var durSec = Math.round(t.last_duration_ms / 1000);
2277
+ var durStr = durSec < 60 ? durSec + 's' : Math.floor(durSec / 60) + 'm ' + (durSec % 60) + 's';
2278
+ html += '<span>\u23F1 Took: ' + durStr + '</span>';
2279
+ }
2280
+ if (t.next_run_at && (t.status === 'pending' || t.status === 'paused')) {
2281
+ var nextDate = new Date(t.next_run_at + (t.next_run_at.includes('Z') ? '' : 'Z'));
2282
+ var isFarFuture = nextDate.getFullYear() > 2099;
2283
+ if (!isFarFuture) {
2284
+ var secUntil = Math.floor((nextDate - new Date()) / 1000);
2285
+ if (secUntil <= 0) {
2286
+ html += '<span style="color:#fab005">\u23ED Next: overdue</span>';
2287
+ } else {
2288
+ var nextStr = secUntil < 60 ? secUntil + 's' : secUntil < 3600 ? Math.floor(secUntil / 60) + 'm' : Math.floor(secUntil / 3600) + 'h';
2289
+ html += '<span>\u23ED Next: ' + nextStr + '</span>';
2290
+ }
2263
2291
  }
2264
2292
  }
2293
+ html += '</span>';
2265
2294
  }
2266
2295
  html += '</div>';
2267
2296
 
@@ -0,0 +1,84 @@
1
+ # CTM & Wall-E UX Improvement Plan
2
+
3
+ Date: 2026-04-03
4
+
5
+ ## Phase 1: First Impressions (New User Experience)
6
+
7
+ ### 1.1 Simplify Navigation
8
+ - Group nav into primary (Sessions, Prompts, WALL-E) and secondary
9
+ - Move Insights, Permissions, Review, Rules, Backups behind "More" dropdown
10
+ - Keeps nav clean for new users, power users still have access
11
+
12
+ ### 1.2 Better Empty State
13
+ - Sessions welcome: 1 clear CTA "Start a Claude Session" with brief explanation
14
+ - Remove keyboard shortcuts from welcome (move to ? help overlay)
15
+ - Add "What can I do?" quick examples
16
+
17
+ ### 1.3 Hide Queue Builder by Default
18
+ - Queue Builder panel takes ~25% screen width, always open
19
+ - Make it a toggle/drawer, collapsed by default
20
+ - Show "Queue" button in toolbar that opens it
21
+
22
+ ### 1.4 Wall-E Onboarding
23
+ - First-time visitor sees intro card: "I'm Wall-E" with 3-4 example actions
24
+ - Disappears after first interaction or dismiss
25
+
26
+ ### 1.5 Reduce Wall-E Sub-tabs
27
+ - Current: Chat, Tasks, Brain, Actions, Skills, Timeline, Questions, Status (8 tabs)
28
+ - Proposed: Chat, Tasks, Skills as primary; Brain/Actions/Timeline/Questions/Status behind "More"
29
+
30
+ ## Phase 2: Information Hierarchy & Readability
31
+
32
+ ### 2.1 Task Cards Cleanup
33
+ - Collapse output by default (click to expand)
34
+ - Add colored status dots (green=running, yellow=pending, red=failed, gray=paused)
35
+ - Reduce metadata noise — show schedule + last run, hide others until hover/expand
36
+
37
+ ### 2.2 Prompts Toolbar Simplification
38
+ - Essential always visible: bold, italic, lists, headings
39
+ - Advanced in overflow: font selector, zoom, chains/templates/patterns/harvest/copilot sub-tabs
40
+ - Move sub-tabs into prompt editor sidebar or contextual menu
41
+
42
+ ### 2.3 Insights Action-First Layout
43
+ - Put the action item (green arrow text) FIRST, then explanation
44
+ - Add category icons for workflow/prompt-effectiveness/skill-gap/cost-saving
45
+
46
+ ### 2.4 Permissions Explainer
47
+ - Add brief card at top: "Permissions control what Claude Code can do automatically"
48
+ - Better risk color coding (red=high, yellow=medium, green=low)
49
+
50
+ ## Phase 3: Visual Polish & Consistency
51
+
52
+ ### 3.1 Unified Status Indicators
53
+ - Colored dots + icons for all states across tasks, sessions, integrations
54
+ - Running=green pulse, Pending=yellow, Failed=red, Paused=gray, Connected=green, Disconnected=red
55
+
56
+ ### 3.2 Chat Improvements
57
+ - Date separators between conversations
58
+ - Softer "YOU" label (lowercase or muted)
59
+ - Proper markdown table rendering in chat
60
+
61
+ ### 3.3 Setup Page Polish
62
+ - Move Data Storage into collapsible "Advanced" section
63
+ - Add step progress indicator
64
+
65
+ ### 3.4 Consistent Card Styling
66
+ - Unify border radius, padding, shadow across all pages
67
+
68
+ ## Phase 4: Progressive Disclosure
69
+
70
+ ### 4.1 Contextual Tooltips
71
+ - First-time hints for Shadow Approver, Queue Builder, Harvest, Copilot
72
+
73
+ ### 4.2 Keyboard Shortcuts Overlay
74
+ - Press ? to see all shortcuts (like GitHub)
75
+
76
+ ### 4.3 Collapsible Sidebar Filters
77
+ - Sessions filters collapse to single row
78
+
79
+ ## Implementation Notes
80
+
81
+ - Test all changes on dev instance (port 4000), never restart primary (port 3456)
82
+ - Use /ctm-dev skill for testing
83
+ - Each phase can be implemented independently
84
+ - Phase 1 has highest impact for new users
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "walle",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "description": "Wall-E — your personal digital twin",
6
6
  "scripts": {
7
7
  "start": "node claude-task-manager/server.js",
8
8
  "stop": "curl -sX POST http://localhost:${CTM_PORT:-3456}/api/stop/walle; echo 'Stopped.'",
9
- "setup": "node bin/setup.js"
9
+ "setup": "node bin/setup.js",
10
+ "dev": "bash bin/dev.sh",
11
+ "dev:fresh": "bash bin/dev.sh --fresh",
12
+ "dev:refresh": "bash bin/dev.sh --refresh"
10
13
  }
11
14
  }
@@ -98,64 +98,10 @@ function bootstrapSkills() {
98
98
  }
99
99
 
100
100
  function bootstrapTasks() {
101
- const existing = brain.listTasks({ limit: 100 });
102
- if (existing.length > 0) return; // Already has tasks
103
-
104
- const coreTasks = [
105
- // Recurring scheduled tasks
106
- {
107
- title: 'Morning Briefing',
108
- description: 'Generate a daily morning briefing with calendar, Slack activity, pending tasks, and mentions.',
109
- type: 'recurring',
110
- schedule: 'daily at 7am',
111
- skill: 'morning-briefing',
112
- priority: 'normal',
113
- },
114
- {
115
- title: 'Sync Calendar',
116
- description: 'Sync macOS Calendar events (Google, iCloud, Outlook) into Wall-E brain via EventKit.',
117
- type: 'recurring',
118
- schedule: 'every 30m',
119
- skill: 'google-calendar',
120
- priority: 'normal',
121
- },
122
- {
123
- title: 'Slack: Conversation Sync',
124
- description: 'Pull latest Slack messages from active conversations into Wall-E brain.',
125
- type: 'recurring',
126
- schedule: 'every 15m',
127
- skill: 'slack-sync',
128
- priority: 'normal',
129
- },
130
- {
131
- title: 'Email: Conversation Sync',
132
- description: 'Sync sent and inbox emails from macOS Mail into Wall-E brain.',
133
- type: 'recurring',
134
- schedule: 'every 6h',
135
- skill: 'email-sync',
136
- priority: 'normal',
137
- },
138
- // Manual one-time tasks
139
- {
140
- title: 'Slack: Full History Backfill',
141
- description: 'One-time full Slack history pull (2022-present). Run manually to backfill all past conversations.',
142
- type: 'once',
143
- skill: 'slack-backfill',
144
- priority: 'normal',
145
- },
146
- {
147
- title: 'Email: Full Send History Pull',
148
- description: 'One-time full sync of sent email history from macOS Mail. Run manually to backfill.',
149
- type: 'once',
150
- skill: 'email-sync',
151
- skill_config: JSON.stringify({ days_back: 90, sync_inbox: true }),
152
- priority: 'normal',
153
- },
154
- ];
155
-
156
- for (const t of coreTasks) {
157
- brain.insertTask(t);
158
- }
101
+ const existing = brain.listTasks({ limit: 1 });
102
+ if (existing.length > 0) return;
103
+ const coreTasks = require('./core-tasks');
104
+ for (const t of coreTasks) brain.insertTask(t);
159
105
  console.log('[wall-e] Bootstrapped ' + coreTasks.length + ' core tasks');
160
106
  }
161
107
 
@@ -34,14 +34,7 @@ function ensureBrainInit() {
34
34
  try {
35
35
  const tasks = brain.listTasks({ limit: 1 });
36
36
  if (tasks.length === 0) {
37
- const coreTasks = [
38
- { title: 'Morning Briefing', description: 'Daily briefing: calendar, Slack, tasks, mentions.', type: 'recurring', schedule: 'daily at 7am', skill: 'morning-briefing' },
39
- { title: 'Sync Calendar', description: 'Sync macOS Calendar events via EventKit.', type: 'recurring', schedule: 'every 30m', skill: 'google-calendar' },
40
- { title: 'Slack: Conversation Sync', description: 'Pull latest Slack messages into brain.', type: 'recurring', schedule: 'every 15m', skill: 'slack-sync' },
41
- { title: 'Email: Conversation Sync', description: 'Sync emails from macOS Mail.', type: 'recurring', schedule: 'every 6h', skill: 'email-sync' },
42
- { title: 'Slack: Full History Backfill', description: 'One-time full Slack history pull. Run manually.', type: 'once', skill: 'slack-backfill' },
43
- { title: 'Email: Full Send History Pull', description: 'One-time full email sync (90 days). Run manually.', type: 'once', skill: 'email-sync', skill_config: JSON.stringify({ days_back: 90, sync_inbox: true }) },
44
- ];
37
+ const coreTasks = require('./core-tasks');
45
38
  for (const t of coreTasks) brain.insertTask(t);
46
39
  console.log('[api-walle] Bootstrapped ' + coreTasks.length + ' core tasks');
47
40
  }
@@ -530,6 +523,20 @@ function handleWalleApi(req, res, url) {
530
523
  return true;
531
524
  }
532
525
 
526
+ // GET /api/wall-e/tasks/:id — single task by ID
527
+ const taskGetMatch = p.match(/^\/api\/wall-e\/tasks\/([^/]+)$/);
528
+ if (taskGetMatch && m === 'GET') {
529
+ if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
530
+ try {
531
+ const t = brain.getTask(taskGetMatch[1]);
532
+ if (!t) return jsonResponse(res, { error: 'Task not found' }, 404), true;
533
+ jsonResponse(res, { data: t });
534
+ } catch (e) {
535
+ jsonResponse(res, { error: e.message }, 500);
536
+ }
537
+ return true;
538
+ }
539
+
533
540
  // POST /api/wall-e/tasks
534
541
  if (p === '/api/wall-e/tasks' && m === 'POST') {
535
542
  if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ // Core tasks bootstrapped on first run.
4
+ // Shared between agent.js and api-walle.js to avoid duplication.
5
+ module.exports = [
6
+ {
7
+ title: 'Morning Briefing',
8
+ description: 'Generate a daily morning briefing with calendar, Slack activity, pending tasks, and mentions.',
9
+ type: 'recurring',
10
+ schedule: 'daily at 7am',
11
+ skill: 'morning-briefing',
12
+ priority: 'normal',
13
+ },
14
+ {
15
+ title: 'Sync Calendar',
16
+ description: 'Sync macOS Calendar events (Google, iCloud, Outlook) into Wall-E brain via EventKit.',
17
+ type: 'recurring',
18
+ schedule: 'every 30m',
19
+ skill: 'google-calendar',
20
+ priority: 'normal',
21
+ },
22
+ {
23
+ title: 'Slack: Conversation Sync',
24
+ description: 'Pull latest Slack messages from active conversations into Wall-E brain.',
25
+ type: 'recurring',
26
+ schedule: 'every 15m',
27
+ skill: 'slack-sync',
28
+ priority: 'normal',
29
+ },
30
+ {
31
+ title: 'Email: Conversation Sync',
32
+ description: 'Sync sent and inbox emails from macOS Mail into Wall-E brain.',
33
+ type: 'recurring',
34
+ schedule: 'every 6h',
35
+ skill: 'email-sync',
36
+ priority: 'normal',
37
+ },
38
+ {
39
+ title: 'Slack: Full History Backfill',
40
+ description: 'One-time full Slack history pull (2022-present). Run manually to backfill all past conversations.',
41
+ type: 'once',
42
+ skill: 'slack-backfill',
43
+ priority: 'normal',
44
+ },
45
+ {
46
+ title: 'Email: Full Send History Pull',
47
+ description: 'One-time full sync of sent email history from macOS Mail. Run manually to backfill.',
48
+ type: 'once',
49
+ skill: 'email-sync',
50
+ skill_config: JSON.stringify({ days_back: 90, sync_inbox: true }),
51
+ priority: 'normal',
52
+ },
53
+ ];
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: file-ingest
3
+ description: >
4
+ Read files from a local folder and ingest their contents into Wall-E's brain
5
+ as memories. Supports text files (.md, .txt, .json, .csv, .js, .py, .html, etc).
6
+ Use for importing notes, documents, code, meeting notes, or any text-based
7
+ knowledge into the brain for search and context.
8
+ version: 1.0.0
9
+ author: wall-e
10
+ execution: script
11
+ entry: run.js
12
+ trigger:
13
+ type: manual
14
+ config:
15
+ folder:
16
+ type: string
17
+ description: "Path to the folder to ingest (required)"
18
+ pattern:
19
+ type: string
20
+ default: "*"
21
+ description: "Glob pattern to filter files (e.g. '*.md', '*.txt')"
22
+ recursive:
23
+ type: boolean
24
+ default: true
25
+ description: "Whether to recurse into subdirectories"
26
+ max_file_size_kb:
27
+ type: number
28
+ default: 500
29
+ description: "Skip files larger than this (KB)"
30
+ tags: [sync, data, files]
31
+ permissions:
32
+ - brain:write
33
+ ---
34
+
35
+ # File Ingest
36
+
37
+ Reads text files from a local folder and stores each file's content as a memory
38
+ in Wall-E's brain. Files are deduplicated by path — re-running on the same folder
39
+ updates existing memories rather than creating duplicates.
40
+
41
+ ## Usage
42
+
43
+ Run manually from the Tasks tab, or via the API:
44
+
45
+ ```bash
46
+ curl -X POST http://localhost:3456/api/wall-e/skills/file-ingest/run \
47
+ -H 'Content-Type: application/json' \
48
+ -d '{"config": {"folder": "~/Documents/notes"}}'
49
+ ```
50
+
51
+ ## Supported File Types
52
+
53
+ `.md` `.txt` `.json` `.csv` `.js` `.ts` `.py` `.html` `.xml` `.yaml` `.yml`
54
+ `.sh` `.css` `.sql` `.env` `.cfg` `.ini` `.log` `.rst` `.org`
55
+
56
+ Binary files, images, and files exceeding the size limit are skipped.
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * File Ingest — read files from a folder and store as brain memories.
5
+ *
6
+ * Each file becomes one memory with:
7
+ * source: 'file'
8
+ * source_id: absolute file path (for dedup)
9
+ * source_channel: folder name
10
+ * content: file contents (truncated if very large)
11
+ * subject: filename
12
+ * memory_type: 'document'
13
+ */
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const brain = require(path.resolve(__dirname, '..', '..', '..', 'brain'));
17
+
18
+ brain.initDb();
19
+
20
+ const TEXT_EXTENSIONS = new Set([
21
+ '.md', '.txt', '.json', '.csv', '.js', '.ts', '.jsx', '.tsx',
22
+ '.py', '.html', '.xml', '.yaml', '.yml', '.sh', '.bash', '.zsh',
23
+ '.css', '.scss', '.sql', '.env', '.cfg', '.ini', '.log', '.rst',
24
+ '.org', '.toml', '.rb', '.go', '.java', '.c', '.cpp', '.h',
25
+ '.swift', '.kt', '.rs', '.r', '.m', '.php', '.pl', '.lua',
26
+ ]);
27
+
28
+ function expandHome(p) {
29
+ if (p.startsWith('~/')) return path.join(process.env.HOME, p.slice(2));
30
+ return p;
31
+ }
32
+
33
+ function walkDir(dir, recursive, pattern) {
34
+ const results = [];
35
+ let entries;
36
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; }
37
+
38
+ for (const entry of entries) {
39
+ if (entry.name.startsWith('.')) continue; // skip hidden
40
+ const fullPath = path.join(dir, entry.name);
41
+ if (entry.isDirectory() && recursive) {
42
+ results.push(...walkDir(fullPath, recursive, pattern));
43
+ } else if (entry.isFile()) {
44
+ const ext = path.extname(entry.name).toLowerCase();
45
+ if (!TEXT_EXTENSIONS.has(ext)) continue;
46
+ if (pattern && pattern !== '*') {
47
+ const glob = pattern.replace(/\*/g, '.*').replace(/\?/g, '.');
48
+ if (!new RegExp('^' + glob + '$', 'i').test(entry.name)) continue;
49
+ }
50
+ results.push(fullPath);
51
+ }
52
+ }
53
+ return results;
54
+ }
55
+
56
+ function main() {
57
+ // Read config from env or args
58
+ const configStr = process.env.SKILL_CONFIG || process.argv[2] || '{}';
59
+ let config;
60
+ try { config = JSON.parse(configStr); } catch { config = {}; }
61
+
62
+ const folder = expandHome(config.folder || '');
63
+ if (!folder) {
64
+ console.error('Error: folder is required. Pass via config: {"folder": "~/path/to/files"}');
65
+ process.exit(1);
66
+ }
67
+
68
+ const resolvedFolder = path.resolve(folder);
69
+ if (!fs.existsSync(resolvedFolder)) {
70
+ console.error('Error: folder not found:', resolvedFolder);
71
+ process.exit(1);
72
+ }
73
+
74
+ const recursive = config.recursive !== false;
75
+ const pattern = config.pattern || '*';
76
+ const maxSizeKb = config.max_file_size_kb || 500;
77
+ const maxSizeBytes = maxSizeKb * 1024;
78
+
79
+ console.log(`# File Ingest\n`);
80
+ console.log(`Folder: ${resolvedFolder}`);
81
+ console.log(`Pattern: ${pattern} | Recursive: ${recursive} | Max size: ${maxSizeKb}KB\n`);
82
+
83
+ const files = walkDir(resolvedFolder, recursive, pattern);
84
+ console.log(`Found ${files.length} text file(s)\n`);
85
+
86
+ let ingested = 0, skipped = 0, updated = 0;
87
+ const folderName = path.basename(resolvedFolder);
88
+
89
+ for (const filePath of files) {
90
+ const stat = fs.statSync(filePath);
91
+ if (stat.size > maxSizeBytes) {
92
+ skipped++;
93
+ continue;
94
+ }
95
+
96
+ let content;
97
+ try { content = fs.readFileSync(filePath, 'utf8'); } catch { skipped++; continue; }
98
+
99
+ // Truncate very long files to keep brain manageable
100
+ if (content.length > 50000) {
101
+ content = content.slice(0, 50000) + '\n\n[... truncated at 50KB]';
102
+ }
103
+
104
+ const relPath = path.relative(resolvedFolder, filePath);
105
+ const fileName = path.basename(filePath);
106
+ const sourceId = 'file:' + filePath;
107
+
108
+ // Check if already exists (dedup by source + source_id)
109
+ const existing = brain.getDb().prepare(
110
+ 'SELECT id FROM memories WHERE source = ? AND source_id = ?'
111
+ ).get('file', sourceId);
112
+
113
+ if (existing) {
114
+ // Update content if changed
115
+ brain.getDb().prepare(
116
+ 'UPDATE memories SET content = ?, timestamp = ?, subject = ? WHERE id = ?'
117
+ ).run(content, stat.mtime.toISOString(), relPath, existing.id);
118
+ updated++;
119
+ } else {
120
+ brain.insertMemory({
121
+ source: 'file',
122
+ source_id: sourceId,
123
+ source_channel: folderName,
124
+ memory_type: 'document',
125
+ content,
126
+ subject: relPath,
127
+ timestamp: stat.mtime.toISOString(),
128
+ });
129
+ ingested++;
130
+ }
131
+ }
132
+
133
+ console.log(`Results:`);
134
+ console.log(` New: ${ingested}`);
135
+ console.log(` Updated: ${updated}`);
136
+ console.log(` Skipped: ${skipped} (too large or unreadable)`);
137
+ console.log(` Total: ${files.length}`);
138
+
139
+ brain.closeDb();
140
+ }
141
+
142
+ main();