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.
- package/bin/create-walle.js +10 -1
- package/package.json +1 -1
- package/template/bin/dev.sh +72 -0
- package/template/claude-task-manager/public/css/walle.css +23 -2
- package/template/claude-task-manager/public/index.html +194 -63
- package/template/claude-task-manager/public/js/walle.js +67 -38
- package/template/docs/ux-improvement-plan.md +84 -0
- package/template/package.json +5 -2
- package/template/wall-e/agent.js +4 -58
- package/template/wall-e/api-walle.js +15 -8
- package/template/wall-e/core-tasks.js +53 -0
- package/template/wall-e/skills/_bundled/file-ingest/SKILL.md +56 -0
- package/template/wall-e/skills/_bundled/file-ingest/run.js +142 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +2 -2
- package/template/wall-e/tools/local-tools.js +28 -0
|
@@ -5,9 +5,10 @@ let currentView = 'chat';
|
|
|
5
5
|
let cache = {};
|
|
6
6
|
let timelineOffset = 0;
|
|
7
7
|
|
|
8
|
-
// WALL-E API base URL —
|
|
9
|
-
// Falls back to same-origin proxy if WALL-E is unreachable
|
|
10
|
-
var
|
|
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' ? '
|
|
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">
|
|
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">✎</button>';
|
|
825
841
|
html += ' <button class="we-edit-btn we-delete-btn" onclick="WE._deleteMessage(' + i + ')" title="Delete this exchange">🗑</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">
|
|
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
|
-
//
|
|
2164
|
-
var expandedClass =
|
|
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
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
if (
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
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
|
-
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
var
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
package/template/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "walle",
|
|
3
|
-
"version": "0.
|
|
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
|
}
|
package/template/wall-e/agent.js
CHANGED
|
@@ -98,64 +98,10 @@ function bootstrapSkills() {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function bootstrapTasks() {
|
|
101
|
-
const existing = brain.listTasks({ limit:
|
|
102
|
-
if (existing.length > 0) return;
|
|
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();
|