create-walle 0.9.6 → 0.9.8
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/README.md +3 -5
- package/package.json +1 -1
- package/template/claude-task-manager/public/index.html +76 -3
- package/template/claude-task-manager/public/js/walle-session.js +105 -0
- package/template/claude-task-manager/server.js +140 -1
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +7 -0
- package/template/wall-e/api-walle.js +10 -0
- package/template/wall-e/chat.js +13 -5
- package/template/wall-e/llm/ollama.js +20 -3
- package/template/wall-e/loops/tasks.js +19 -3
- package/template/wall-e/server.js +8 -0
- package/template/wall-e/skills/_bundled/model-trainer/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +79 -55
- package/template/wall-e/telemetry.js +22 -1
- package/template/wall-e/test/training/trainer.test.js +4 -4
- package/template/wall-e/tools/local-tools.js +2 -0
- package/template/wall-e/training/train.py +7 -2
- package/template/wall-e/training/trainer.js +5 -4
- package/template/website/index.html +5 -5
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ An always-on AI agent that learns from your Slack, email, calendar, and coding s
|
|
|
31
31
|
## Install
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
npx create-walle install ./
|
|
34
|
+
npx create-walle install ./walle
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
This copies the project, installs dependencies, auto-detects your name and timezone, and starts the server. Open **http://localhost:3456** to finish setup in the browser.
|
|
@@ -60,7 +60,7 @@ On first launch, the browser setup page guides you through:
|
|
|
60
60
|
## Custom Port
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
CTM_PORT=5000 npx create-walle install ./
|
|
63
|
+
CTM_PORT=5000 npx create-walle install ./walle
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Wall-E runs on `CTM_PORT + 1` (e.g., 5001).
|
|
@@ -76,9 +76,7 @@ Everything runs locally. CTM serves the dashboard, Wall-E runs as a background a
|
|
|
76
76
|
|
|
77
77
|
## Links
|
|
78
78
|
|
|
79
|
-
- [
|
|
80
|
-
- [Configuration Reference](https://walle.sh/docs/guides/configuration/)
|
|
81
|
-
- [Skill Catalog](https://walle.sh/docs/skills/)
|
|
79
|
+
- [Homepage](https://walle.sh)
|
|
82
80
|
|
|
83
81
|
## License
|
|
84
82
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-walle",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "CTM + Wall-E — AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini & Aider, plus prompt editor, task queue, and an agent that learns from Slack, email & calendar.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-walle": "bin/create-walle.js"
|
|
@@ -2546,6 +2546,12 @@
|
|
|
2546
2546
|
<button class="btn primary" id="topbar-new-session-btn" onclick="showNewSessionModal()">+ New Session</button>
|
|
2547
2547
|
</div>
|
|
2548
2548
|
</div>
|
|
2549
|
+
<div id="update-banner" style="display:none;background:linear-gradient(90deg,#1a1b2e,#1e2030);border-bottom:1px solid var(--border);padding:6px 16px;font-size:12px;color:var(--fg-dim,#a9b1d6);align-items:center;gap:10px;">
|
|
2550
|
+
<span style="color:#bb9af7;">↑</span>
|
|
2551
|
+
<span id="update-banner-msg">Update available</span>
|
|
2552
|
+
<button id="update-apply-btn" onclick="applyUpdate()" style="background:#7aa2f7;color:#1a1b26;border:none;padding:3px 10px;border-radius:4px;font-size:11px;cursor:pointer;font-weight:600;">Update Now</button>
|
|
2553
|
+
<button onclick="dismissUpdate()" style="background:none;border:none;color:var(--fg-dim);cursor:pointer;margin-left:auto;opacity:0.6;font-size:14px;">×</button>
|
|
2554
|
+
</div>
|
|
2549
2555
|
<div id="main">
|
|
2550
2556
|
<div id="sidebar">
|
|
2551
2557
|
<div class="sidebar-section">
|
|
@@ -3346,6 +3352,63 @@ function showApprovalToast(text, color, duration) {
|
|
|
3346
3352
|
toast(text, { type, duration });
|
|
3347
3353
|
}
|
|
3348
3354
|
|
|
3355
|
+
// --- Update Banner ---
|
|
3356
|
+
let _updateDismissedVersion = localStorage.getItem('update_dismissed_version') || '';
|
|
3357
|
+
|
|
3358
|
+
function showUpdateBanner(current, latest) {
|
|
3359
|
+
if (_updateDismissedVersion === latest) return;
|
|
3360
|
+
const banner = document.getElementById('update-banner');
|
|
3361
|
+
const msg = document.getElementById('update-banner-msg');
|
|
3362
|
+
if (!banner || !msg) return;
|
|
3363
|
+
msg.textContent = `Update available: v${current} \u2192 v${latest}`;
|
|
3364
|
+
banner.style.display = 'flex';
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
function dismissUpdate() {
|
|
3368
|
+
const banner = document.getElementById('update-banner');
|
|
3369
|
+
if (banner) banner.style.display = 'none';
|
|
3370
|
+
// Remember dismissal for this version
|
|
3371
|
+
const msg = document.getElementById('update-banner-msg')?.textContent || '';
|
|
3372
|
+
const m = msg.match(/v([\d.]+)$/);
|
|
3373
|
+
if (m) {
|
|
3374
|
+
_updateDismissedVersion = m[1];
|
|
3375
|
+
localStorage.setItem('update_dismissed_version', m[1]);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
async function applyUpdate() {
|
|
3380
|
+
const btn = document.getElementById('update-apply-btn');
|
|
3381
|
+
if (btn) { btn.textContent = 'Updating...'; btn.disabled = true; }
|
|
3382
|
+
try {
|
|
3383
|
+
const resp = await fetch('/api/updates/apply', { method: 'POST' });
|
|
3384
|
+
const data = await resp.json();
|
|
3385
|
+
if (data.status === 'updating') {
|
|
3386
|
+
toast('Update started. CTM will restart shortly...', { type: 'info', duration: 8000 });
|
|
3387
|
+
dismissUpdate();
|
|
3388
|
+
} else {
|
|
3389
|
+
toast('Already up to date.', { type: 'success' });
|
|
3390
|
+
if (btn) { btn.textContent = 'Update Now'; btn.disabled = false; }
|
|
3391
|
+
}
|
|
3392
|
+
} catch (e) {
|
|
3393
|
+
toast('Update failed: ' + e.message, { type: 'error' });
|
|
3394
|
+
if (btn) { btn.textContent = 'Update Now'; btn.disabled = false; }
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
function checkForUpdates() {
|
|
3399
|
+
fetch('/api/updates/check')
|
|
3400
|
+
.then(r => r.json())
|
|
3401
|
+
.then(data => {
|
|
3402
|
+
if (data.updateAvailable) {
|
|
3403
|
+
showUpdateBanner(data.currentVersion, data.latestVersion);
|
|
3404
|
+
}
|
|
3405
|
+
})
|
|
3406
|
+
.catch(() => {});
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// Check on page load
|
|
3410
|
+
setTimeout(checkForUpdates, 3000);
|
|
3411
|
+
|
|
3349
3412
|
// --- State ---
|
|
3350
3413
|
function getCookie(name) {
|
|
3351
3414
|
const m = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]+)'));
|
|
@@ -3401,6 +3464,7 @@ function connect() {
|
|
|
3401
3464
|
case 'waiting-for-input': onWaitingForInput(msg); break;
|
|
3402
3465
|
case 'files-changed': if (window.CR) CR.handleFilesChanged(msg); break;
|
|
3403
3466
|
case 'model-alert': showModelAlert(msg); break;
|
|
3467
|
+
case 'update-available': showUpdateBanner(msg.currentVersion, msg.latestVersion); break;
|
|
3404
3468
|
case 'walle-progress': WalleSession.handleProgress(msg); break;
|
|
3405
3469
|
case 'walle-response': WalleSession.handleResponse(msg); break;
|
|
3406
3470
|
case 'walle-history': WalleSession.handleHistory(msg); break;
|
|
@@ -4395,10 +4459,12 @@ function renderModelsHeader(providers, models) {
|
|
|
4395
4459
|
'</div>' +
|
|
4396
4460
|
'<div style="display:flex;gap:8px;align-items:center;">' +
|
|
4397
4461
|
'<input type="text" id="models-search" placeholder="Search models..." value="' + escHtml(_modelsRegistryFilter) + '" style="background:var(--bg-card,#24283b);color:var(--fg,#c0caf5);border:1px solid var(--border,#414868);border-radius:6px;padding:6px 12px;font-size:13px;width:200px;">' +
|
|
4398
|
-
'<button
|
|
4462
|
+
'<button id="models-add-provider-btn" style="padding:6px 14px;border-radius:6px;background:var(--accent,#7aa2f7);color:#1a1b26;border:none;cursor:pointer;font-weight:600;font-size:13px;white-space:nowrap;">+ Add Provider</button>' +
|
|
4399
4463
|
'</div>' +
|
|
4400
4464
|
'</div>';
|
|
4401
4465
|
el.innerHTML = DOMPurify.sanitize(h, { ADD_ATTR: ['style', 'placeholder'] });
|
|
4466
|
+
var addBtn = document.getElementById('models-add-provider-btn');
|
|
4467
|
+
if (addBtn) addBtn.addEventListener('click', showAddProviderDialog);
|
|
4402
4468
|
var searchInput = document.getElementById('models-search');
|
|
4403
4469
|
if (searchInput) searchInput.addEventListener('input', function() {
|
|
4404
4470
|
_modelsRegistryFilter = this.value;
|
|
@@ -10533,7 +10599,7 @@ function qpEditKeydown(e, idx) {
|
|
|
10533
10599
|
saveQpEdit(idx);
|
|
10534
10600
|
} else if (e.key === 'Escape') {
|
|
10535
10601
|
e.preventDefault();
|
|
10536
|
-
cancelQpEdit();
|
|
10602
|
+
cancelQpEdit(idx);
|
|
10537
10603
|
}
|
|
10538
10604
|
}
|
|
10539
10605
|
|
|
@@ -10585,7 +10651,14 @@ async function syncQpItemToPrompt(promptId, title, content) {
|
|
|
10585
10651
|
}
|
|
10586
10652
|
}
|
|
10587
10653
|
|
|
10588
|
-
function cancelQpEdit() {
|
|
10654
|
+
function cancelQpEdit(idx) {
|
|
10655
|
+
if (qpEditingIdx >= 0) {
|
|
10656
|
+
var inp = document.getElementById('qp-edit-input');
|
|
10657
|
+
var original = (idx != null && qpItems[idx]) ? qpItems[idx].text : '';
|
|
10658
|
+
if (inp && inp.value !== original) {
|
|
10659
|
+
if (!confirm('Discard unsaved changes?')) return;
|
|
10660
|
+
}
|
|
10661
|
+
}
|
|
10589
10662
|
qpEditingIdx = -1;
|
|
10590
10663
|
renderQpItems();
|
|
10591
10664
|
}
|
|
@@ -128,6 +128,85 @@ window.WalleSession = (function() {
|
|
|
128
128
|
|
|
129
129
|
header.appendChild(headerRight);
|
|
130
130
|
|
|
131
|
+
// Session toolbar (matches terminal session toolbar)
|
|
132
|
+
var toolbar = document.createElement('div');
|
|
133
|
+
toolbar.className = 'session-toolbar';
|
|
134
|
+
|
|
135
|
+
var copyBtn = document.createElement('button');
|
|
136
|
+
copyBtn.className = 'session-toolbar-btn';
|
|
137
|
+
copyBtn.title = 'Copy session ID';
|
|
138
|
+
copyBtn.textContent = '\uD83D\uDCCB Copy Command';
|
|
139
|
+
copyBtn.onclick = function() {
|
|
140
|
+
var s = state.sessions.get(id);
|
|
141
|
+
var cwd = s && s.meta ? s.meta.cwd || '' : '';
|
|
142
|
+
var cmd = cwd ? 'cd ' + cwd + ' && walle --session ' + id : 'walle --session ' + id;
|
|
143
|
+
navigator.clipboard.writeText(cmd).then(function() {
|
|
144
|
+
if (typeof toast === 'function') toast('Command copied to clipboard');
|
|
145
|
+
}).catch(function() {
|
|
146
|
+
var ta = document.createElement('textarea');
|
|
147
|
+
ta.value = cmd;
|
|
148
|
+
document.body.appendChild(ta);
|
|
149
|
+
ta.select();
|
|
150
|
+
document.execCommand('copy');
|
|
151
|
+
document.body.removeChild(ta);
|
|
152
|
+
if (typeof toast === 'function') toast('Command copied to clipboard');
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
toolbar.appendChild(copyBtn);
|
|
156
|
+
|
|
157
|
+
var reflowBtn = document.createElement('button');
|
|
158
|
+
reflowBtn.className = 'session-toolbar-btn';
|
|
159
|
+
reflowBtn.title = 'Scroll to bottom';
|
|
160
|
+
reflowBtn.textContent = '\u21BB Reflow';
|
|
161
|
+
reflowBtn.onclick = function() {
|
|
162
|
+
var el = document.getElementById('walle-messages-' + id);
|
|
163
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
164
|
+
};
|
|
165
|
+
toolbar.appendChild(reflowBtn);
|
|
166
|
+
|
|
167
|
+
// Model switcher (smart routing default)
|
|
168
|
+
var toolbarModelSelect = document.createElement('select');
|
|
169
|
+
toolbarModelSelect.className = 'model-switch-select';
|
|
170
|
+
toolbarModelSelect.title = 'Switch model for this session';
|
|
171
|
+
var defaultModelOpt = document.createElement('option');
|
|
172
|
+
defaultModelOpt.value = '';
|
|
173
|
+
defaultModelOpt.textContent = 'auto (smart routing)';
|
|
174
|
+
toolbarModelSelect.appendChild(defaultModelOpt);
|
|
175
|
+
toolbarModelSelect.onchange = function() {
|
|
176
|
+
var ws = getState(id);
|
|
177
|
+
if (ws) ws.selectedModel = toolbarModelSelect.value || '';
|
|
178
|
+
// Also sync the header model-select
|
|
179
|
+
var headerSel = document.getElementById('walle-model-' + id);
|
|
180
|
+
if (headerSel) headerSel.value = toolbarModelSelect.value;
|
|
181
|
+
};
|
|
182
|
+
toolbar.appendChild(toolbarModelSelect);
|
|
183
|
+
|
|
184
|
+
// Populate toolbar model selector from registry
|
|
185
|
+
populateToolbarModelSelector(toolbarModelSelect);
|
|
186
|
+
|
|
187
|
+
// Message count badge (like prompt nav badge)
|
|
188
|
+
var msgNav = document.createElement('div');
|
|
189
|
+
msgNav.className = 'prompt-nav';
|
|
190
|
+
msgNav.style.position = 'relative';
|
|
191
|
+
var msgBadge = document.createElement('span');
|
|
192
|
+
msgBadge.className = 'prompt-nav-badge';
|
|
193
|
+
msgBadge.id = 'walle-msg-badge-' + id;
|
|
194
|
+
msgBadge.title = 'Message count';
|
|
195
|
+
var ws0 = getState(id);
|
|
196
|
+
msgBadge.textContent = '\u26A0 ' + (ws0 ? ws0.messageCount : 0) + ' messages';
|
|
197
|
+
msgNav.appendChild(msgBadge);
|
|
198
|
+
toolbar.appendChild(msgNav);
|
|
199
|
+
|
|
200
|
+
// Review action button (rightmost)
|
|
201
|
+
var toolbarReviewBtn = document.createElement('button');
|
|
202
|
+
toolbarReviewBtn.className = 'session-toolbar-review-action';
|
|
203
|
+
toolbarReviewBtn.title = 'Open review panel for this session';
|
|
204
|
+
toolbarReviewBtn.textContent = 'Review \u2192';
|
|
205
|
+
toolbarReviewBtn.onclick = function() {
|
|
206
|
+
if (typeof openSessionReview === 'function') openSessionReview(id);
|
|
207
|
+
};
|
|
208
|
+
toolbar.appendChild(toolbarReviewBtn);
|
|
209
|
+
|
|
131
210
|
// Messages area
|
|
132
211
|
var messagesArea = document.createElement('div');
|
|
133
212
|
messagesArea.className = 'walle-messages';
|
|
@@ -175,6 +254,7 @@ window.WalleSession = (function() {
|
|
|
175
254
|
// Assemble — clear and rebuild
|
|
176
255
|
while (container.firstChild) container.removeChild(container.firstChild);
|
|
177
256
|
container.appendChild(header);
|
|
257
|
+
container.appendChild(toolbar);
|
|
178
258
|
container.appendChild(messagesArea);
|
|
179
259
|
container.appendChild(inputBar);
|
|
180
260
|
container.appendChild(hints);
|
|
@@ -587,6 +667,25 @@ window.WalleSession = (function() {
|
|
|
587
667
|
.catch(function() {});
|
|
588
668
|
}
|
|
589
669
|
|
|
670
|
+
// ---------- toolbar model selector ----------
|
|
671
|
+
function populateToolbarModelSelector(select) {
|
|
672
|
+
fetch('/api/models/registry')
|
|
673
|
+
.then(function(r) { return r.json(); })
|
|
674
|
+
.then(function(data) {
|
|
675
|
+
var models = data.models || data || [];
|
|
676
|
+
// Keep the default option, add models after it
|
|
677
|
+
for (var i = 0; i < models.length; i++) {
|
|
678
|
+
var m = models[i];
|
|
679
|
+
if (!m.enabled) continue;
|
|
680
|
+
var opt = document.createElement('option');
|
|
681
|
+
opt.value = m.id;
|
|
682
|
+
opt.textContent = (m.display_name || m.id) + ' (' + (m.provider_name || m.provider_type || '') + ')';
|
|
683
|
+
select.appendChild(opt);
|
|
684
|
+
}
|
|
685
|
+
})
|
|
686
|
+
.catch(function() {});
|
|
687
|
+
}
|
|
688
|
+
|
|
590
689
|
// ---------- helpers ----------
|
|
591
690
|
function scrollToBottom(el) {
|
|
592
691
|
if (el) {
|
|
@@ -675,6 +774,12 @@ window.WalleSession = (function() {
|
|
|
675
774
|
if (costEl) {
|
|
676
775
|
costEl.textContent = '$' + ws.totalCost.toFixed(2);
|
|
677
776
|
}
|
|
777
|
+
|
|
778
|
+
// Update toolbar message count badge
|
|
779
|
+
var msgBadge = document.getElementById('walle-msg-badge-' + id);
|
|
780
|
+
if (msgBadge) {
|
|
781
|
+
msgBadge.textContent = '\u26A0 ' + ws.messageCount + ' messages';
|
|
782
|
+
}
|
|
678
783
|
}
|
|
679
784
|
|
|
680
785
|
// ---------- public API ----------
|
|
@@ -964,6 +964,14 @@ function handleApi(req, res, url) {
|
|
|
964
964
|
if (url.pathname === '/api/services/status' && req.method === 'GET') {
|
|
965
965
|
return apiServicesStatus(req, res);
|
|
966
966
|
}
|
|
967
|
+
// --- Update check endpoints ---
|
|
968
|
+
if (url.pathname === '/api/updates/check' && req.method === 'GET') {
|
|
969
|
+
return apiUpdatesCheck(req, res);
|
|
970
|
+
}
|
|
971
|
+
if (url.pathname === '/api/updates/apply' && req.method === 'POST') {
|
|
972
|
+
return apiUpdatesApply(req, res);
|
|
973
|
+
}
|
|
974
|
+
|
|
967
975
|
if (url.pathname === '/api/restart/ctm' && req.method === 'POST') {
|
|
968
976
|
return apiRestartCtm(req, res);
|
|
969
977
|
}
|
|
@@ -3779,6 +3787,87 @@ try {
|
|
|
3779
3787
|
console.log(` Failed to restore sessions: ${e.message}`);
|
|
3780
3788
|
}
|
|
3781
3789
|
|
|
3790
|
+
// --- Update checker ---
|
|
3791
|
+
const _updateState = { latestVersion: null, currentVersion: null, checkedAt: null, updateAvailable: false, error: null };
|
|
3792
|
+
|
|
3793
|
+
function getCurrentVersion() {
|
|
3794
|
+
try {
|
|
3795
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
|
|
3796
|
+
return pkg.version;
|
|
3797
|
+
} catch { return null; }
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
// Simple semver comparison: returns 1 if a > b, -1 if a < b, 0 if equal
|
|
3801
|
+
function semverCompare(a, b) {
|
|
3802
|
+
const pa = a.split('.').map(Number);
|
|
3803
|
+
const pb = b.split('.').map(Number);
|
|
3804
|
+
for (let i = 0; i < 3; i++) {
|
|
3805
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
3806
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
3807
|
+
}
|
|
3808
|
+
return 0;
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
async function checkForUpdates() {
|
|
3812
|
+
_updateState.currentVersion = getCurrentVersion();
|
|
3813
|
+
try {
|
|
3814
|
+
const resp = await fetch('https://registry.npmjs.org/create-walle/latest', {
|
|
3815
|
+
headers: { 'Accept': 'application/json' },
|
|
3816
|
+
signal: AbortSignal.timeout(10000),
|
|
3817
|
+
});
|
|
3818
|
+
if (!resp.ok) throw new Error(`npm registry returned ${resp.status}`);
|
|
3819
|
+
const data = await resp.json();
|
|
3820
|
+
_updateState.latestVersion = data.version;
|
|
3821
|
+
_updateState.checkedAt = new Date().toISOString();
|
|
3822
|
+
_updateState.error = null;
|
|
3823
|
+
|
|
3824
|
+
// Only notify if latest is strictly newer (not just different — avoids dev-build false positives)
|
|
3825
|
+
_updateState.updateAvailable = !!(
|
|
3826
|
+
_updateState.latestVersion && _updateState.currentVersion &&
|
|
3827
|
+
semverCompare(_updateState.latestVersion, _updateState.currentVersion) > 0
|
|
3828
|
+
);
|
|
3829
|
+
|
|
3830
|
+
if (_updateState.updateAvailable) {
|
|
3831
|
+
broadcastToAll({
|
|
3832
|
+
type: 'update-available',
|
|
3833
|
+
currentVersion: _updateState.currentVersion,
|
|
3834
|
+
latestVersion: _updateState.latestVersion,
|
|
3835
|
+
});
|
|
3836
|
+
console.log(` Update available: v${_updateState.currentVersion} -> v${_updateState.latestVersion}`);
|
|
3837
|
+
}
|
|
3838
|
+
} catch (e) {
|
|
3839
|
+
_updateState.error = e.message;
|
|
3840
|
+
}
|
|
3841
|
+
return _updateState;
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
function apiUpdatesCheck(req, res) {
|
|
3845
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3846
|
+
res.end(JSON.stringify(_updateState));
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
function apiUpdatesApply(req, res) {
|
|
3850
|
+
const current = _updateState.currentVersion;
|
|
3851
|
+
const latest = _updateState.latestVersion;
|
|
3852
|
+
if (!_updateState.updateAvailable) {
|
|
3853
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3854
|
+
res.end(JSON.stringify({ status: 'up-to-date', version: current }));
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3859
|
+
res.end(JSON.stringify({ status: 'updating', from: current, to: latest }));
|
|
3860
|
+
|
|
3861
|
+
// Run update in background — npx create-walle@latest update handles stop/update/restart
|
|
3862
|
+
const { spawn } = require('child_process');
|
|
3863
|
+
const child = spawn('npx', ['create-walle@latest', 'update'], {
|
|
3864
|
+
cwd: path.join(__dirname, '..'),
|
|
3865
|
+
detached: true,
|
|
3866
|
+
stdio: 'ignore',
|
|
3867
|
+
});
|
|
3868
|
+
child.unref();
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3782
3871
|
// --- Auto-seed default model providers & registry on startup ---
|
|
3783
3872
|
function seedDefaultModels() {
|
|
3784
3873
|
const brain = getWalleBrain();
|
|
@@ -3810,7 +3899,7 @@ function seedDefaultModels() {
|
|
|
3810
3899
|
}
|
|
3811
3900
|
}
|
|
3812
3901
|
|
|
3813
|
-
// Seed Ollama provider
|
|
3902
|
+
// Seed Ollama provider and auto-discover installed models
|
|
3814
3903
|
if (!providers.find(p => p.type === 'ollama')) {
|
|
3815
3904
|
brain.upsertModelProvider({
|
|
3816
3905
|
id: 'ollama-local',
|
|
@@ -3819,6 +3908,52 @@ function seedDefaultModels() {
|
|
|
3819
3908
|
enabled: 1,
|
|
3820
3909
|
});
|
|
3821
3910
|
}
|
|
3911
|
+
// Auto-scan Ollama models (async, non-blocking)
|
|
3912
|
+
try {
|
|
3913
|
+
const { createOllamaProvider } = require(path.resolve(__dirname, '..', 'wall-e', 'llm', 'ollama'));
|
|
3914
|
+
const ollama = createOllamaProvider({});
|
|
3915
|
+
ollama.listModels().then(async (models) => {
|
|
3916
|
+
for (const m of models) {
|
|
3917
|
+
// Fetch detailed model info (context length, params) from /api/show
|
|
3918
|
+
let maxContextTokens = null;
|
|
3919
|
+
let paramSize = null;
|
|
3920
|
+
try {
|
|
3921
|
+
const showResp = await fetch('http://localhost:11434/api/show', {
|
|
3922
|
+
method: 'POST',
|
|
3923
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3924
|
+
body: JSON.stringify({ name: m.id }),
|
|
3925
|
+
signal: AbortSignal.timeout(3000),
|
|
3926
|
+
});
|
|
3927
|
+
if (showResp.ok) {
|
|
3928
|
+
const showData = await showResp.json();
|
|
3929
|
+
const info = showData.model_info || {};
|
|
3930
|
+
// Context length key varies by model family (e.g. "llama.context_length", "gemma4.context_length")
|
|
3931
|
+
for (const [k, v] of Object.entries(info)) {
|
|
3932
|
+
if (k.endsWith('.context_length')) { maxContextTokens = v; break; }
|
|
3933
|
+
}
|
|
3934
|
+
// Parameter size from details (e.g. "8.0B", "14.7B")
|
|
3935
|
+
if (showData.details && showData.details.parameter_size) {
|
|
3936
|
+
paramSize = showData.details.parameter_size;
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
} catch {}
|
|
3940
|
+
|
|
3941
|
+
brain.upsertModelRegistryEntry({
|
|
3942
|
+
id: 'ollama-local:' + m.id,
|
|
3943
|
+
providerId: 'ollama-local',
|
|
3944
|
+
modelId: m.id,
|
|
3945
|
+
displayName: `${m.name || m.id}${paramSize ? ' (' + paramSize + ')' : ''}`,
|
|
3946
|
+
capabilities: m.capabilities || ['code'],
|
|
3947
|
+
costPer1mInput: 0,
|
|
3948
|
+
costPer1mOutput: 0,
|
|
3949
|
+
maxContextTokens: maxContextTokens || null,
|
|
3950
|
+
speedTier: 3,
|
|
3951
|
+
enabled: 1,
|
|
3952
|
+
});
|
|
3953
|
+
}
|
|
3954
|
+
if (models.length) console.log(` Ollama: ${models.length} local model(s) registered`);
|
|
3955
|
+
}).catch(() => {});
|
|
3956
|
+
} catch {}
|
|
3822
3957
|
|
|
3823
3958
|
// Set defaults if none exist
|
|
3824
3959
|
const defaults = brain.listModelDefaults();
|
|
@@ -3876,6 +4011,10 @@ server.listen(PORT, HOST, () => {
|
|
|
3876
4011
|
});
|
|
3877
4012
|
} catch (e) { /* health module not available */ }
|
|
3878
4013
|
|
|
4014
|
+
// Check for updates on startup (after 10s delay) and every 24h
|
|
4015
|
+
setTimeout(() => checkForUpdates().catch(() => {}), 10000);
|
|
4016
|
+
setInterval(() => checkForUpdates().catch(() => {}), 24 * 60 * 60 * 1000);
|
|
4017
|
+
|
|
3879
4018
|
// Wall-E watchdog — auto-restart if it dies unexpectedly.
|
|
3880
4019
|
// _walleIntentionallyStopped is set by apiStopWalle, cleared by apiStartWalle.
|
|
3881
4020
|
setInterval(() => {
|
package/template/package.json
CHANGED
package/template/wall-e/agent.js
CHANGED
|
@@ -267,6 +267,7 @@ async function main() {
|
|
|
267
267
|
channels: channels.map(c => c.name),
|
|
268
268
|
adapters: adapters.map(a => a.constructor?.name || 'unknown'),
|
|
269
269
|
});
|
|
270
|
+
telemetry.trackFunnelStep('boot');
|
|
270
271
|
|
|
271
272
|
// Recover tasks that were interrupted by previous shutdown
|
|
272
273
|
const recovered = recoverInterruptedTasks();
|
|
@@ -278,6 +279,7 @@ async function main() {
|
|
|
278
279
|
console.log(`[wall-e] Initial ingest: ${ingestResult.memoriesIngested} memories`);
|
|
279
280
|
if (ingestResult.memoriesIngested > 0) {
|
|
280
281
|
telemetry.track('ingest', { memories: ingestResult.memoriesIngested, initial: true });
|
|
282
|
+
telemetry.trackFunnelStep('first_ingest');
|
|
281
283
|
}
|
|
282
284
|
|
|
283
285
|
// Initial think
|
|
@@ -315,6 +317,7 @@ async function main() {
|
|
|
315
317
|
if (result.knowledgeExtracted > 0) {
|
|
316
318
|
console.log(`[wall-e] Extracted ${result.knowledgeExtracted} knowledge entries`);
|
|
317
319
|
}
|
|
320
|
+
telemetry.track('think', { processed: result.memoriesProcessed, knowledge: result.knowledgeExtracted });
|
|
318
321
|
} catch (err) {
|
|
319
322
|
console.error('[wall-e] Think error:', err.message);
|
|
320
323
|
} finally {
|
|
@@ -330,6 +333,7 @@ async function main() {
|
|
|
330
333
|
if (result.summaryGenerated) {
|
|
331
334
|
console.log('[wall-e] Daily summary generated');
|
|
332
335
|
}
|
|
336
|
+
telemetry.track('reflect', { summary: result.summaryGenerated ? 1 : 0 });
|
|
333
337
|
} catch (err) {
|
|
334
338
|
console.error('[wall-e] Reflect error:', err.message);
|
|
335
339
|
} finally {
|
|
@@ -347,6 +351,8 @@ async function main() {
|
|
|
347
351
|
const result = await runDueSkills();
|
|
348
352
|
if (result.executed > 0) {
|
|
349
353
|
console.log(`[wall-e] Skills: ${result.executed} executed, ${result.memoriesCreated} memories`);
|
|
354
|
+
telemetry.track('skill_run', { executed: result.executed, memories: result.memoriesCreated });
|
|
355
|
+
telemetry.trackFunnelStep('first_skill');
|
|
350
356
|
}
|
|
351
357
|
} catch (err) {
|
|
352
358
|
console.error('[wall-e] Skills error:', err.message);
|
|
@@ -365,6 +371,7 @@ async function main() {
|
|
|
365
371
|
const result = await runDueTasks();
|
|
366
372
|
if (result.processed > 0) {
|
|
367
373
|
console.log(`[wall-e] Tasks: ${result.processed} completed`);
|
|
374
|
+
telemetry.track('task_run', { processed: result.processed });
|
|
368
375
|
}
|
|
369
376
|
} catch (err) {
|
|
370
377
|
console.error('[wall-e] Tasks error:', err.message);
|
|
@@ -575,6 +575,8 @@ function handleWalleApi(req, res, url) {
|
|
|
575
575
|
const result = await chatModule.chat(body.message, {
|
|
576
576
|
channel: body.channel || 'ctm',
|
|
577
577
|
session_id: body.session_id || 'default',
|
|
578
|
+
provider: body.provider || undefined,
|
|
579
|
+
model: body.model || undefined,
|
|
578
580
|
onProgress: sendEvent,
|
|
579
581
|
});
|
|
580
582
|
sendEvent({ type: 'done', reply: result.reply });
|
|
@@ -584,6 +586,8 @@ function handleWalleApi(req, res, url) {
|
|
|
584
586
|
const result = await chatModule.chat(body.message, {
|
|
585
587
|
channel: body.channel || 'ctm',
|
|
586
588
|
session_id: body.session_id || 'default',
|
|
589
|
+
provider: body.provider || undefined,
|
|
590
|
+
model: body.model || undefined,
|
|
587
591
|
});
|
|
588
592
|
jsonResponse(res, { data: result });
|
|
589
593
|
}
|
|
@@ -1826,6 +1830,12 @@ function handleWalleApi(req, res, url) {
|
|
|
1826
1830
|
if (p === '/api/wall-e/telemetry/ingest' && m === 'POST') {
|
|
1827
1831
|
const tdb = getTelemetryDb();
|
|
1828
1832
|
if (!tdb) return jsonResponse(res, { error: 'Telemetry not enabled' }, 404), true;
|
|
1833
|
+
// Basic rate limiting: max 60 requests per minute per IP
|
|
1834
|
+
const clientIpRL = (req.headers['x-forwarded-for'] || '').split(',')[0].trim() || req.socket?.remoteAddress || 'unknown';
|
|
1835
|
+
if (!handleWalleApi._rlMap) { handleWalleApi._rlMap = new Map(); setInterval(() => handleWalleApi._rlMap.clear(), 60000); }
|
|
1836
|
+
const rlCount = (handleWalleApi._rlMap.get(clientIpRL) || 0) + 1;
|
|
1837
|
+
handleWalleApi._rlMap.set(clientIpRL, rlCount);
|
|
1838
|
+
if (rlCount > 60) return jsonResponse(res, { error: 'Rate limited' }, 429), true;
|
|
1829
1839
|
readBody(req).then(body => {
|
|
1830
1840
|
if (!body || !body.id || !Array.isArray(body.events)) {
|
|
1831
1841
|
return jsonResponse(res, { error: 'Invalid payload' }, 400);
|
package/template/wall-e/chat.js
CHANGED
|
@@ -77,8 +77,12 @@ async function chat(message, opts = {}) {
|
|
|
77
77
|
console.error('[chat] Session expiry check failed:', expErr.message);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// Use injected provider (for testing) or
|
|
81
|
-
|
|
80
|
+
// Use injected provider (for testing), explicit override, or default from env
|
|
81
|
+
let provider = _clientOverride || getDefaultClient();
|
|
82
|
+
if (!_clientOverride && opts.provider && opts.provider !== 'anthropic') {
|
|
83
|
+
const { createClient } = require('./llm/client');
|
|
84
|
+
provider = createClient(opts.provider, opts.providerConfig || {});
|
|
85
|
+
}
|
|
82
86
|
|
|
83
87
|
// Per-turn abort controller — each API call gets its own 2-min timeout.
|
|
84
88
|
// This prevents long multi-turn tasks (e.g., Morning Briefing) from aborting
|
|
@@ -597,8 +601,9 @@ async function chat(message, opts = {}) {
|
|
|
597
601
|
let lastTurn = 0;
|
|
598
602
|
const MAX_TURNS = 8; // search(2-3) + think(1) + response(1) + possible follow-up tools
|
|
599
603
|
|
|
604
|
+
|
|
600
605
|
// Guardrails
|
|
601
|
-
const MAX_TOOL_CALLS = opts.maxToolCalls
|
|
606
|
+
const MAX_TOOL_CALLS = opts.maxToolCalls != null ? opts.maxToolCalls : 15;
|
|
602
607
|
let toolCallCount = 0;
|
|
603
608
|
const MESSAGE_TIMEOUT_MS = opts.timeoutMs || 180000; // 3 minutes
|
|
604
609
|
const messageDeadline = Date.now() + MESSAGE_TIMEOUT_MS;
|
|
@@ -655,7 +660,7 @@ async function chat(message, opts = {}) {
|
|
|
655
660
|
maxTokens: 4096,
|
|
656
661
|
system: systemPrompt,
|
|
657
662
|
messages,
|
|
658
|
-
tools: chatTools,
|
|
663
|
+
tools: opts.allowedTools ? chatTools.filter(t => opts.allowedTools.includes(t.name)) : chatTools,
|
|
659
664
|
signal: controller.signal,
|
|
660
665
|
});
|
|
661
666
|
|
|
@@ -752,7 +757,7 @@ async function chat(message, opts = {}) {
|
|
|
752
757
|
summary: resultSummary,
|
|
753
758
|
fullSizeBytes: Buffer.byteLength(resultStr, 'utf8'),
|
|
754
759
|
});
|
|
755
|
-
allToolCalls.push({ tool: tu.name, args: tu.input, result_summary: resultSummary });
|
|
760
|
+
allToolCalls.push({ tool: tu.name, args: tu.input, result_summary: resultSummary, error: !!result?.error });
|
|
756
761
|
return { type: 'tool_result', tool_use_id: tu.id, content: compactedResult };
|
|
757
762
|
}));
|
|
758
763
|
|
|
@@ -848,7 +853,10 @@ async function chat(message, opts = {}) {
|
|
|
848
853
|
model: usedModel, provider: usedProvider,
|
|
849
854
|
latency_ms: latencyMs, tokens_in: totalInputTokens, tokens_out: totalOutputTokens,
|
|
850
855
|
cost, tool_count: allToolCalls.length, channel,
|
|
856
|
+
tools: allToolCalls.map(t => t.tool),
|
|
857
|
+
tool_errors: allToolCalls.filter(t => t.error).length,
|
|
851
858
|
});
|
|
859
|
+
telemetry.trackFunnelStep('first_chat');
|
|
852
860
|
} catch {}
|
|
853
861
|
|
|
854
862
|
return {
|
|
@@ -27,7 +27,7 @@ function createOllamaProvider(config = {}) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const body = {
|
|
30
|
-
model: model || '
|
|
30
|
+
model: model || 'gemma4:e4b',
|
|
31
31
|
messages: openaiMessages,
|
|
32
32
|
stream: false,
|
|
33
33
|
};
|
|
@@ -41,13 +41,30 @@ function createOllamaProvider(config = {}) {
|
|
|
41
41
|
if (temperature != null) body.temperature = temperature;
|
|
42
42
|
|
|
43
43
|
const start = Date.now();
|
|
44
|
-
|
|
44
|
+
let resp = await fetch(`${baseUrl}/v1/chat/completions`, {
|
|
45
45
|
method: 'POST',
|
|
46
46
|
headers: { 'Content-Type': 'application/json' },
|
|
47
47
|
body: JSON.stringify(body),
|
|
48
48
|
signal,
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
// Retry without tools if model doesn't support them
|
|
52
|
+
if (!resp.ok && resp.status === 400 && body.tools) {
|
|
53
|
+
const errText = await resp.text().catch(() => '');
|
|
54
|
+
if (errText.includes('does not support tools')) {
|
|
55
|
+
console.log(`[ollama] ${model} does not support tools — retrying without`);
|
|
56
|
+
delete body.tools;
|
|
57
|
+
resp = await fetch(`${baseUrl}/v1/chat/completions`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify(body),
|
|
61
|
+
signal,
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(`Ollama API error ${resp.status}: ${errText}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
if (!resp.ok) {
|
|
52
69
|
const text = await resp.text().catch(() => '');
|
|
53
70
|
throw new Error(`Ollama API error ${resp.status}: ${text}`);
|
|
@@ -60,7 +77,7 @@ function createOllamaProvider(config = {}) {
|
|
|
60
77
|
if (normalized.stopReason === 'tool_calls') stopReason = 'tool_use';
|
|
61
78
|
else if (normalized.stopReason === 'stop' || !normalized.stopReason) stopReason = 'end_turn';
|
|
62
79
|
|
|
63
|
-
return { ...normalized, stopReason, latencyMs: Date.now() - start, model: model || '
|
|
80
|
+
return { ...normalized, stopReason, latencyMs: Date.now() - start, model: model || 'gemma4:e4b', provider: 'ollama', raw };
|
|
64
81
|
},
|
|
65
82
|
|
|
66
83
|
async listModels() {
|
|
@@ -617,6 +617,7 @@ async function retrySlackDelivery(task, resultText) {
|
|
|
617
617
|
// Extract channel_id and thread_ts from the task description
|
|
618
618
|
const channelMatch = task.description?.match(/"channel_id":\s*"([^"]+)"/);
|
|
619
619
|
const threadMatch = task.description?.match(/"thread_ts":\s*"([^"]+)"/);
|
|
620
|
+
console.log(`[tasks] retrySlackDelivery: channel=${channelMatch?.[1]}, thread=${threadMatch?.[1]}`);
|
|
620
621
|
if (!channelMatch) {
|
|
621
622
|
console.error('[tasks] Cannot retry Slack delivery: no channel_id in task description');
|
|
622
623
|
return false;
|
|
@@ -628,12 +629,17 @@ async function retrySlackDelivery(task, resultText) {
|
|
|
628
629
|
// Remove lines that are just research narration ("Let me...", "I'll...")
|
|
629
630
|
answer = answer.replace(/^(?:Let me|I'll|I will|Now let me|Let's)[^\n]*$/gm, '').trim();
|
|
630
631
|
|
|
632
|
+
// Strip error messages about delivery failure
|
|
633
|
+
answer = answer.replace(/(?:However|Unfortunately|I'm currently)[^.]*(?:unable to send|MCP server|authentication issue|technical issue|couldn't send|could not send)[^.]*\./gi, '').trim();
|
|
634
|
+
|
|
631
635
|
// Try to find substantive answer (before any error part)
|
|
632
636
|
const errorIdx = answer.search(/unfortunately.*(?:technical issue|error|couldn't send|MCP server)/i);
|
|
633
637
|
if (errorIdx > 50) {
|
|
634
638
|
answer = answer.slice(0, errorIdx).trim();
|
|
635
639
|
}
|
|
636
640
|
|
|
641
|
+
console.log(`[tasks] retrySlackDelivery: extracted answer (${answer.length} chars): ${answer.slice(0, 200)}`);
|
|
642
|
+
|
|
637
643
|
// If the answer is still just meta-narration with no real content, try to
|
|
638
644
|
// generate a brief reply using the chat engine with strict instructions
|
|
639
645
|
if (answer.length < 20 || !/[a-z]{3,}/i.test(answer)) {
|
|
@@ -645,8 +651,8 @@ async function retrySlackDelivery(task, resultText) {
|
|
|
645
651
|
{ channel: 'task', session_id: `retry-${task.id}`, maxToolCalls: 0, timeoutMs: 30000 }
|
|
646
652
|
);
|
|
647
653
|
answer = briefReply.reply || '';
|
|
648
|
-
} catch {
|
|
649
|
-
console.error(
|
|
654
|
+
} catch (chatErr) {
|
|
655
|
+
console.error(`[tasks] Brief reply generation failed: ${chatErr.message}`);
|
|
650
656
|
return false;
|
|
651
657
|
}
|
|
652
658
|
}
|
|
@@ -661,8 +667,18 @@ async function retrySlackDelivery(task, resultText) {
|
|
|
661
667
|
const args = { channel_id: channelMatch[1], message: answer };
|
|
662
668
|
if (threadMatch) args.thread_ts = threadMatch[1];
|
|
663
669
|
|
|
670
|
+
console.log(`[tasks] retrySlackDelivery: sending ${answer.length} chars to ${args.channel_id} thread=${args.thread_ts || 'none'}`);
|
|
664
671
|
const result = await slackMcp.callSlackMcp('slack_send_message', args);
|
|
665
|
-
|
|
672
|
+
const resultStr = JSON.stringify(result).slice(0, 200);
|
|
673
|
+
console.log(`[tasks] retrySlackDelivery: MCP result: ${resultStr}`);
|
|
674
|
+
|
|
675
|
+
// Check for actual success — look for message_link in response
|
|
676
|
+
const hasMessageLink = resultStr.includes('message_link') || resultStr.includes('message_ts');
|
|
677
|
+
if (!hasMessageLink) {
|
|
678
|
+
console.error(`[tasks] retrySlackDelivery: no message_link in response — likely failed`);
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
return true;
|
|
666
682
|
} catch (err) {
|
|
667
683
|
console.error(`[tasks] Slack delivery retry failed: ${err.message}`);
|
|
668
684
|
return false;
|
|
@@ -87,6 +87,14 @@ function startServer() {
|
|
|
87
87
|
console.log(`[wall-e] HTTP server listening on ${HOST}:${PORT}`);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
server.on('error', (err) => {
|
|
91
|
+
if (err.code === 'EADDRINUSE') {
|
|
92
|
+
console.error(`[wall-e] Port ${PORT} already in use. Try: lsof -ti :${PORT} | xargs kill`);
|
|
93
|
+
} else {
|
|
94
|
+
console.error('[wall-e] Server error:', err.message);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
90
98
|
return server;
|
|
91
99
|
}
|
|
92
100
|
|
|
@@ -24,7 +24,7 @@ Runs the full fine-tuning pipeline on a weekly schedule (disabled by default).
|
|
|
24
24
|
## Pipeline
|
|
25
25
|
|
|
26
26
|
1. **Export** training data from brain (knowledge, chat, patterns)
|
|
27
|
-
2. **Train** all supported base models (llama3.
|
|
27
|
+
2. **Train** all supported base models (llama3.1-8b, phi-4, qwen2.5-7b, gemma4-4b)
|
|
28
28
|
3. **Evaluate** trained models against a baseline using benchmark prompts
|
|
29
29
|
4. **Deploy** the winner to Ollama if it beats the baseline by >60%
|
|
30
30
|
|
|
@@ -71,6 +71,65 @@ function sleep(ms) {
|
|
|
71
71
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Generate an answer using chat (with limited tools) and send it directly to Slack.
|
|
76
|
+
* Returns the reply text on success, or null on failure.
|
|
77
|
+
*
|
|
78
|
+
* This bypasses the task→chat→mcp_call pipeline which is unreliable because:
|
|
79
|
+
* 1. The model ignores "reply first" instructions and calls slow tools
|
|
80
|
+
* 2. MCP tool calls from chat can hang indefinitely
|
|
81
|
+
* 3. There's no guarantee the model will call slack_send_message
|
|
82
|
+
*
|
|
83
|
+
* Instead: we call chat with search_memories + think only, get the answer text,
|
|
84
|
+
* then send it ourselves via callSlackMcp.
|
|
85
|
+
*/
|
|
86
|
+
async function generateAndSendReply(msg, threadTs, contextBlock, kind) {
|
|
87
|
+
const chatModule = require(path.resolve(__dirname, '..', '..', '..', 'chat'));
|
|
88
|
+
const question = (msg.content || '').replace(/@wall-?e/gi, '').trim();
|
|
89
|
+
|
|
90
|
+
// Step 1: Generate the answer with limited tools (search_memories + think only)
|
|
91
|
+
let answer;
|
|
92
|
+
try {
|
|
93
|
+
const prompt = `[SLACK ${kind.toUpperCase()}] ${OWNER_NAME} asked in Slack:\n\n${question}${contextBlock}\n\nSearch your memories for relevant info, then give a concise, helpful answer (2-6 sentences). Do NOT call any tools except search_memories and think. Just answer the question directly.`;
|
|
94
|
+
|
|
95
|
+
const result = await chatModule.chat(prompt, {
|
|
96
|
+
channel: 'task',
|
|
97
|
+
session_id: `slack-reply-${msg.ts || Date.now()}`,
|
|
98
|
+
allowedTools: ['search_memories', 'think', 'lookup_person'],
|
|
99
|
+
timeoutMs: 60000, // 1 minute max
|
|
100
|
+
});
|
|
101
|
+
answer = (result.reply || '').trim();
|
|
102
|
+
console.log(`[slack-mentions] Generated answer (${answer.length} chars): ${answer.slice(0, 150)}`);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`[slack-mentions] Failed to generate answer: ${err.message}`);
|
|
105
|
+
answer = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Step 2: Send the reply directly via Slack MCP
|
|
109
|
+
if (!answer || answer.length < 5) {
|
|
110
|
+
answer = `I'm not sure about that — let me look into it and get back to you.`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Truncate for Slack (4000 char limit)
|
|
114
|
+
if (answer.length > 3500) answer = answer.slice(0, 3500) + '...';
|
|
115
|
+
|
|
116
|
+
if (msg.channelId) {
|
|
117
|
+
try {
|
|
118
|
+
await slackMcp.callSlackMcp('slack_send_message', {
|
|
119
|
+
channel_id: msg.channelId,
|
|
120
|
+
thread_ts: threadTs,
|
|
121
|
+
message: answer,
|
|
122
|
+
});
|
|
123
|
+
console.log(`[slack-mentions] Reply sent to Slack thread ${threadTs}`);
|
|
124
|
+
return answer;
|
|
125
|
+
} catch (sendErr) {
|
|
126
|
+
console.error(`[slack-mentions] Failed to send reply to Slack: ${sendErr.message}`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return answer;
|
|
131
|
+
}
|
|
132
|
+
|
|
74
133
|
function classifyMention(text) {
|
|
75
134
|
const trimmed = (text || '').trim();
|
|
76
135
|
const cleaned = trimmed.replace(/@wall-?e/gi, '').trim();
|
|
@@ -424,77 +483,42 @@ async function _processMention(msg, seenTs, stats) {
|
|
|
424
483
|
return;
|
|
425
484
|
}
|
|
426
485
|
|
|
427
|
-
if (kind === 'task') {
|
|
486
|
+
if (kind === 'task' || kind === 'question') {
|
|
487
|
+
stats.questionsFound += kind === 'question' ? 1 : 0;
|
|
488
|
+
stats.tasksCreated += kind === 'task' ? 1 : 0;
|
|
428
489
|
const title = extractTaskTitle(msg.content);
|
|
429
|
-
|
|
430
|
-
const { id } = brain.insertTask({
|
|
431
|
-
title,
|
|
432
|
-
description: `Slack task from ${OWNER_NAME}:\n\n${msg.content}${contextBlock}\n\n---\n**MANDATORY — REPLY FIRST, RESEARCH SECOND:**\nYour #1 job is to SEND A REPLY IN SLACK. A quick partial answer is infinitely better than a perfect answer that never gets sent.\n\nStep 1: search_memories (max 2 calls)\nStep 2: think tool — draft your response\nStep 3: IMMEDIATELY call mcp_call to reply. Do NOT do more research.\n\nReply using mcp_call with:\n- server: slack\n- tool: slack_send_message\n- arguments: { "channel_id": "${msg.channelId}", "thread_ts": "${threadTs}", "message": "<your results>" }\n\n**FORBIDDEN:** Do NOT use run_shell, web_fetch, read_file, or any other tool before sending the Slack reply. Search memories → think → reply. That's it.\nIf you need more research, send a preliminary reply first, then continue researching.`,
|
|
433
|
-
priority: 'normal',
|
|
434
|
-
type: 'once',
|
|
435
|
-
execution: 'chat',
|
|
436
|
-
source: 'slack',
|
|
437
|
-
source_ref: sourceRef,
|
|
438
|
-
});
|
|
439
|
-
stats.tasksCreated++;
|
|
440
|
-
console.log(`[slack-mentions] Task created: ${id} — ${title}`);
|
|
441
|
-
|
|
442
|
-
try {
|
|
443
|
-
brain.upsertSlackThread({ channelId: msg.channelId, threadTs, taskId: id, sessionId: `task-${id}` });
|
|
444
|
-
} catch (upsertErr) {
|
|
445
|
-
console.error(`[slack-mentions] FAILED to upsert watched thread: ${upsertErr.message}`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (msg.channelId) {
|
|
449
|
-
try {
|
|
450
|
-
await slackMcp.callSlackMcp('slack_send_message', {
|
|
451
|
-
channel_id: msg.channelId,
|
|
452
|
-
message: `Got it! I've created a task for this. I'll update you here when it's done.`,
|
|
453
|
-
thread_ts: threadTs,
|
|
454
|
-
});
|
|
455
|
-
} catch (replyErr) {
|
|
456
|
-
console.warn(`[slack-mentions] Could not reply in thread: ${replyErr.message}`);
|
|
457
|
-
}
|
|
458
|
-
await sleep(PAUSE_MS);
|
|
459
|
-
}
|
|
460
|
-
} catch (taskErr) {
|
|
461
|
-
console.error(`[slack-mentions] Failed to create task: ${taskErr.message}`);
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
stats.questionsFound++;
|
|
465
|
-
console.log(`[slack-mentions] Question detected: ${msg.content.slice(0, 80)}...`);
|
|
490
|
+
console.log(`[slack-mentions] ${kind} detected: ${title.slice(0, 80)}`);
|
|
466
491
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
message: `Let me look into that — I'll get back to you shortly.`,
|
|
472
|
-
thread_ts: threadTs,
|
|
473
|
-
});
|
|
474
|
-
} catch (replyErr) {
|
|
475
|
-
console.warn(`[slack-mentions] Could not reply in thread: ${replyErr.message}`);
|
|
476
|
-
}
|
|
477
|
-
await sleep(PAUSE_MS);
|
|
478
|
-
}
|
|
492
|
+
// Phase 1: Generate answer directly and send it — no task intermediary.
|
|
493
|
+
// This is the most reliable approach: we control the chat call and send the
|
|
494
|
+
// reply ourselves, instead of hoping the task's chat engine will do it.
|
|
495
|
+
const reply = await generateAndSendReply(msg, threadTs, contextBlock, kind);
|
|
479
496
|
|
|
497
|
+
// Track as a completed task for history/dedup
|
|
480
498
|
try {
|
|
481
499
|
const { id } = brain.insertTask({
|
|
482
|
-
title: `Answer Slack question: ${
|
|
483
|
-
description:
|
|
484
|
-
priority: 'high',
|
|
500
|
+
title: kind === 'question' ? `Answer Slack question: ${title}` : title,
|
|
501
|
+
description: `${kind === 'question' ? 'Question' : 'Task'} from ${OWNER_NAME} in Slack:\n\n${msg.content}${contextBlock}`,
|
|
502
|
+
priority: kind === 'question' ? 'high' : 'normal',
|
|
485
503
|
type: 'once',
|
|
486
504
|
execution: 'chat',
|
|
487
505
|
source: 'slack',
|
|
488
506
|
source_ref: sourceRef,
|
|
489
507
|
});
|
|
490
|
-
|
|
508
|
+
// Mark completed/failed immediately since we handled it inline
|
|
509
|
+
brain.updateTask(id, {
|
|
510
|
+
status: reply ? 'completed' : 'failed',
|
|
511
|
+
result: (reply || 'Failed to generate or send reply').slice(0, 10000),
|
|
512
|
+
completed_at: new Date().toISOString(),
|
|
513
|
+
run_count: 1,
|
|
514
|
+
});
|
|
491
515
|
try {
|
|
492
516
|
brain.upsertSlackThread({ channelId: msg.channelId, threadTs, taskId: id, sessionId: `task-${id}` });
|
|
493
517
|
} catch (upsertErr) {
|
|
494
518
|
console.error(`[slack-mentions] FAILED to upsert watched thread: ${upsertErr.message}`);
|
|
495
519
|
}
|
|
496
520
|
} catch (taskErr) {
|
|
497
|
-
console.error(`[slack-mentions] Failed to create
|
|
521
|
+
console.error(`[slack-mentions] Failed to create task record: ${taskErr.message}`);
|
|
498
522
|
}
|
|
499
523
|
}
|
|
500
524
|
|
|
@@ -79,6 +79,26 @@ async function flush() {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// --- First-run funnel tracking ---
|
|
83
|
+
const FUNNEL_PATH = path.join(DATA_DIR, '.telemetry-funnel.json');
|
|
84
|
+
const FUNNEL_STEPS = ['install', 'boot', 'first_ingest', 'first_chat', 'first_skill'];
|
|
85
|
+
|
|
86
|
+
function trackFunnelStep(step) {
|
|
87
|
+
if (isDisabled()) return;
|
|
88
|
+
try {
|
|
89
|
+
let funnel = {};
|
|
90
|
+
try { funnel = JSON.parse(fs.readFileSync(FUNNEL_PATH, 'utf8')); } catch {}
|
|
91
|
+
if (funnel[step]) return; // already recorded
|
|
92
|
+
funnel[step] = Date.now();
|
|
93
|
+
fs.writeFileSync(FUNNEL_PATH, JSON.stringify(funnel, null, 2), 'utf8');
|
|
94
|
+
|
|
95
|
+
// Calculate time from install to this step
|
|
96
|
+
const installTime = funnel.install || funnel.boot || Date.now();
|
|
97
|
+
const elapsed = Math.round((Date.now() - installTime) / 1000);
|
|
98
|
+
track('funnel', { step, elapsed_seconds: elapsed, steps_completed: Object.keys(funnel).length });
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
// --- Version helper ---
|
|
83
103
|
let _version = null;
|
|
84
104
|
function getVersion() {
|
|
@@ -99,6 +119,7 @@ function start() {
|
|
|
99
119
|
track('startup', {
|
|
100
120
|
uptime: process.uptime(),
|
|
101
121
|
});
|
|
122
|
+
trackFunnelStep('install');
|
|
102
123
|
flushTimer = setInterval(flush, FLUSH_INTERVAL_MS);
|
|
103
124
|
// Don't keep process alive just for telemetry
|
|
104
125
|
if (flushTimer.unref) flushTimer.unref();
|
|
@@ -127,4 +148,4 @@ function printNoticeIfFirstRun() {
|
|
|
127
148
|
} catch {}
|
|
128
149
|
}
|
|
129
150
|
|
|
130
|
-
module.exports = { track, flush, start, stop, isDisabled, getInstallId, printNoticeIfFirstRun };
|
|
151
|
+
module.exports = { track, flush, start, stop, isDisabled, getInstallId, printNoticeIfFirstRun, trackFunnelStep };
|
|
@@ -66,7 +66,7 @@ describe('trainModel validation', () => {
|
|
|
66
66
|
describe('buildTrainCommand', () => {
|
|
67
67
|
it('constructs correct command with all flags', () => {
|
|
68
68
|
const args = buildTrainCommand({
|
|
69
|
-
baseModel: 'llama3.
|
|
69
|
+
baseModel: 'llama3.1-8b',
|
|
70
70
|
dataPath: '/data/train.jsonl',
|
|
71
71
|
outputDir: '/models/output',
|
|
72
72
|
outputName: 'my-model',
|
|
@@ -74,7 +74,7 @@ describe('buildTrainCommand', () => {
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
assert.ok(args.includes(TRAIN_SCRIPT), 'should include train script path');
|
|
77
|
-
assert.deepStrictEqual(args.slice(1, 3), ['--base', 'llama3.
|
|
77
|
+
assert.deepStrictEqual(args.slice(1, 3), ['--base', 'llama3.1-8b']);
|
|
78
78
|
assert.deepStrictEqual(args.slice(3, 5), ['--data', '/data/train.jsonl']);
|
|
79
79
|
assert.deepStrictEqual(args.slice(5, 7), ['--output', '/models/output']);
|
|
80
80
|
assert.deepStrictEqual(args.slice(7, 9), ['--name', 'my-model']);
|
|
@@ -158,9 +158,9 @@ describe('buildTrainCommand', () => {
|
|
|
158
158
|
// ---------------------------------------------------------------------------
|
|
159
159
|
|
|
160
160
|
describe('SUPPORTED_BASES', () => {
|
|
161
|
-
it('contains all
|
|
161
|
+
it('contains all 4 expected models', () => {
|
|
162
162
|
const keys = Object.keys(SUPPORTED_BASES);
|
|
163
|
-
assert.deepStrictEqual(keys.sort(), ['llama3.
|
|
163
|
+
assert.deepStrictEqual(keys.sort(), ['gemma4-4b', 'llama3.1-8b', 'phi-4', 'qwen2.5-7b'].sort());
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
it('each model has ollamaName and huggingFace', () => {
|
|
@@ -26,6 +26,8 @@ const SHELL_ALLOWLIST = new Set([
|
|
|
26
26
|
// Dev tools
|
|
27
27
|
'git', 'node', 'npm', 'npx', 'python3', 'pip3', 'bun', 'deno',
|
|
28
28
|
'make', 'cargo', 'go', 'ruby', 'perl',
|
|
29
|
+
// Cloud / infra
|
|
30
|
+
'fly', 'docker', 'kubectl',
|
|
29
31
|
// System info
|
|
30
32
|
'date', 'echo', 'env', 'whoami', 'hostname', 'uname', 'uptime', 'ps', 'top',
|
|
31
33
|
'id', 'groups', 'printenv', 'locale', 'lsof',
|
|
@@ -18,14 +18,15 @@ import sys
|
|
|
18
18
|
# ---------------------------------------------------------------------------
|
|
19
19
|
|
|
20
20
|
MODEL_REGISTRY = {
|
|
21
|
-
"llama3.
|
|
21
|
+
"llama3.1-8b": "unsloth/Meta-Llama-3.1-8B-Instruct",
|
|
22
22
|
"phi-4": "unsloth/Phi-4",
|
|
23
23
|
"qwen2.5-7b": "unsloth/Qwen2.5-7B-Instruct",
|
|
24
|
+
"gemma4-4b": "unsloth/gemma-4-E4B-it",
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
# LoRA target modules per architecture
|
|
27
28
|
LORA_TARGETS = {
|
|
28
|
-
"llama3.
|
|
29
|
+
"llama3.1-8b": [
|
|
29
30
|
"q_proj", "k_proj", "v_proj", "o_proj",
|
|
30
31
|
"gate_proj", "up_proj", "down_proj",
|
|
31
32
|
],
|
|
@@ -37,6 +38,10 @@ LORA_TARGETS = {
|
|
|
37
38
|
"q_proj", "k_proj", "v_proj", "o_proj",
|
|
38
39
|
"gate_proj", "up_proj", "down_proj",
|
|
39
40
|
],
|
|
41
|
+
"gemma4-4b": [
|
|
42
|
+
"q_proj", "k_proj", "v_proj", "o_proj",
|
|
43
|
+
"gate_proj", "up_proj", "down_proj",
|
|
44
|
+
],
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
|
|
@@ -9,9 +9,10 @@ const fs = require('node:fs');
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
|
|
11
11
|
const SUPPORTED_BASES = {
|
|
12
|
-
'llama3.
|
|
13
|
-
'phi-4':
|
|
14
|
-
'qwen2.5-7b':
|
|
12
|
+
'llama3.1-8b': { ollamaName: 'llama3.1:8b-instruct-q4_K_M', huggingFace: 'unsloth/Meta-Llama-3.1-8B-Instruct' },
|
|
13
|
+
'phi-4': { ollamaName: 'phi4:latest', huggingFace: 'unsloth/Phi-4' },
|
|
14
|
+
'qwen2.5-7b': { ollamaName: 'qwen2.5:7b-instruct-q4_K_M', huggingFace: 'unsloth/Qwen2.5-7B-Instruct' },
|
|
15
|
+
'gemma4-4b': { ollamaName: 'gemma4:e4b', huggingFace: 'unsloth/gemma-4-E4B-it' },
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const DEFAULT_HYPERPARAMS = {
|
|
@@ -60,7 +61,7 @@ function buildTrainCommand(opts) {
|
|
|
60
61
|
/**
|
|
61
62
|
* Run fine-tuning for a base model.
|
|
62
63
|
* @param {Object} options
|
|
63
|
-
* @param {string} options.baseModel - Base model name: 'llama3.
|
|
64
|
+
* @param {string} options.baseModel - Base model name: 'llama3.1-8b', 'phi-4', 'qwen2.5-7b', 'gemma4-4b'
|
|
64
65
|
* @param {string} options.dataPath - Path to JSONL training data
|
|
65
66
|
* @param {string} options.outputDir - Where to save the model
|
|
66
67
|
* @param {string} [options.outputName] - Model name (default: 'walle-{base}-v1')
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
<a href="#how">Get Started</a>
|
|
232
232
|
<a href="#architecture">Architecture</a>
|
|
233
233
|
<a href="https://www.npmjs.com/package/create-walle">npm</a>
|
|
234
|
-
<a href="https://
|
|
234
|
+
<a href="https://www.npmjs.com/package/create-walle?activeTab=versions">Changelog</a>
|
|
235
235
|
</div>
|
|
236
236
|
</div>
|
|
237
237
|
</nav>
|
|
@@ -243,8 +243,8 @@
|
|
|
243
243
|
Run Claude Code, Codex, Gemini, and Aider sessions side by side. Manage prompts,
|
|
244
244
|
queue tasks, and let an AI agent build a second brain from your work life.
|
|
245
245
|
</p>
|
|
246
|
-
<div class="install-box" onclick="navigator.clipboard.writeText('npx create-walle install ./
|
|
247
|
-
<code>npx create-walle install ./
|
|
246
|
+
<div class="install-box" onclick="navigator.clipboard.writeText('npx create-walle install ./walle');this.querySelector('.copy-hint').textContent='Copied!'">
|
|
247
|
+
<code>npx create-walle install ./walle</code>
|
|
248
248
|
<span class="copy-hint">click to copy</span>
|
|
249
249
|
</div>
|
|
250
250
|
<div class="badge-row">
|
|
@@ -259,7 +259,7 @@
|
|
|
259
259
|
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
|
|
260
260
|
<span class="title mono">Terminal</span>
|
|
261
261
|
</div>
|
|
262
|
-
<pre><span class="prompt">$</span> <span class="cmd">npx create-walle install ./
|
|
262
|
+
<pre><span class="prompt">$</span> <span class="cmd">npx create-walle install ./walle</span>
|
|
263
263
|
|
|
264
264
|
<span class="dim"> Installing CTM + Wall-E...</span>
|
|
265
265
|
<span class="dim"> Auto-detected owner:</span> <span class="hi">Your Name</span>
|
|
@@ -365,7 +365,7 @@
|
|
|
365
365
|
<div class="steps">
|
|
366
366
|
<div class="step">
|
|
367
367
|
<h3>Install</h3>
|
|
368
|
-
<p><code>npx create-walle install ./
|
|
368
|
+
<p><code>npx create-walle install ./walle</code><br>Copies the project, installs deps, and starts the server.</p>
|
|
369
369
|
</div>
|
|
370
370
|
<div class="step">
|
|
371
371
|
<h3>Open the browser</h3>
|