create-walle 0.9.5 → 0.9.6
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-walle",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
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"
|
|
@@ -4422,7 +4422,8 @@ function renderModelProviders(providers, models) {
|
|
|
4422
4422
|
var costs = pModels.map(function(m) { return m.cost_per_1m_input || 0; }).filter(function(c) { return c > 0; });
|
|
4423
4423
|
var minCost = costs.length ? Math.min.apply(null, costs) : 0;
|
|
4424
4424
|
var maxCost = costs.length ? Math.max.apply(null, costs) : 0;
|
|
4425
|
-
var
|
|
4425
|
+
var isLocal = p.type === 'ollama' || p.type === 'local';
|
|
4426
|
+
var costRange = isLocal ? 'Free (local)' : (costs.length ? _costExact(minCost) + ' - ' + _costExact(maxCost) : 'No pricing');
|
|
4426
4427
|
// Aggregate capabilities
|
|
4427
4428
|
var allCaps = new Set();
|
|
4428
4429
|
pModels.forEach(function(m) {
|
|
@@ -4443,7 +4444,7 @@ function renderModelProviders(providers, models) {
|
|
|
4443
4444
|
'<div style="margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:8px;">' +
|
|
4444
4445
|
'<div style="background:var(--bg,#1a1b26);border-radius:6px;padding:8px 10px;">' +
|
|
4445
4446
|
'<div style="font-size:10px;color:var(--fg-dim,#565f89);text-transform:uppercase;letter-spacing:0.5px;">Cost Range</div>' +
|
|
4446
|
-
'<div style="font-size:12px;color
|
|
4447
|
+
'<div style="font-size:12px;color:' + (isLocal ? '#9ece6a' : '#e0af68') + ';margin-top:2px;">' + costRange + '</div>' +
|
|
4447
4448
|
'</div>' +
|
|
4448
4449
|
'<div style="background:var(--bg,#1a1b26);border-radius:6px;padding:8px 10px;">' +
|
|
4449
4450
|
'<div style="font-size:10px;color:var(--fg-dim,#565f89);text-transform:uppercase;letter-spacing:0.5px;">Capabilities</div>' +
|
package/template/package.json
CHANGED
|
@@ -27,6 +27,7 @@ function getTelemetryDb() {
|
|
|
27
27
|
event TEXT NOT NULL,
|
|
28
28
|
meta TEXT,
|
|
29
29
|
client_ts INTEGER,
|
|
30
|
+
ip TEXT,
|
|
30
31
|
received_at TEXT DEFAULT (datetime('now'))
|
|
31
32
|
);
|
|
32
33
|
CREATE INDEX IF NOT EXISTS idx_te_install ON telemetry_events(install_id);
|
|
@@ -39,9 +40,13 @@ function getTelemetryDb() {
|
|
|
39
40
|
version TEXT,
|
|
40
41
|
os TEXT,
|
|
41
42
|
node_version TEXT,
|
|
43
|
+
ip TEXT,
|
|
42
44
|
event_count INTEGER DEFAULT 0
|
|
43
45
|
);
|
|
44
46
|
`);
|
|
47
|
+
// Migrate: add ip column if missing
|
|
48
|
+
try { telemetryDb.prepare('ALTER TABLE telemetry_events ADD COLUMN ip TEXT').run(); } catch {}
|
|
49
|
+
try { telemetryDb.prepare('ALTER TABLE telemetry_installs ADD COLUMN ip TEXT').run(); } catch {}
|
|
45
50
|
return telemetryDb;
|
|
46
51
|
} catch (e) {
|
|
47
52
|
console.error('[wall-e] Failed to init telemetry DB:', e.message);
|
|
@@ -1825,24 +1830,28 @@ function handleWalleApi(req, res, url) {
|
|
|
1825
1830
|
if (!body || !body.id || !Array.isArray(body.events)) {
|
|
1826
1831
|
return jsonResponse(res, { error: 'Invalid payload' }, 400);
|
|
1827
1832
|
}
|
|
1833
|
+
// Capture client IP from request (supports reverse proxy headers)
|
|
1834
|
+
const clientIp = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
|
|
1835
|
+
|| req.socket?.remoteAddress || '';
|
|
1828
1836
|
const insertEvent = tdb.prepare(
|
|
1829
|
-
'INSERT INTO telemetry_events (install_id, version, os, node_version, event, meta, client_ts) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
1837
|
+
'INSERT INTO telemetry_events (install_id, version, os, node_version, event, meta, client_ts, ip) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
|
1830
1838
|
);
|
|
1831
1839
|
const upsertInstall = tdb.prepare(`
|
|
1832
|
-
INSERT INTO telemetry_installs (install_id, version, os, node_version, event_count)
|
|
1833
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1840
|
+
INSERT INTO telemetry_installs (install_id, version, os, node_version, ip, event_count)
|
|
1841
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1834
1842
|
ON CONFLICT(install_id) DO UPDATE SET
|
|
1835
1843
|
last_seen = datetime('now'),
|
|
1836
1844
|
version = excluded.version,
|
|
1837
1845
|
os = excluded.os,
|
|
1838
1846
|
node_version = excluded.node_version,
|
|
1847
|
+
ip = excluded.ip,
|
|
1839
1848
|
event_count = telemetry_installs.event_count + excluded.event_count
|
|
1840
1849
|
`);
|
|
1841
1850
|
const runBatch = tdb.transaction((events) => {
|
|
1842
1851
|
for (const ev of events) {
|
|
1843
|
-
insertEvent.run(body.id, body.v, body.os, body.node, ev.e, JSON.stringify(ev.m || {}), ev.t);
|
|
1852
|
+
insertEvent.run(body.id, body.v, body.os, body.node, ev.e, JSON.stringify(ev.m || {}), ev.t, clientIp);
|
|
1844
1853
|
}
|
|
1845
|
-
upsertInstall.run(body.id, body.v, body.os, body.node, events.length);
|
|
1854
|
+
upsertInstall.run(body.id, body.v, body.os, body.node, clientIp, events.length);
|
|
1846
1855
|
});
|
|
1847
1856
|
runBatch(body.events.slice(0, 500)); // cap at 500 events per request
|
|
1848
1857
|
jsonResponse(res, { ok: true, received: Math.min(body.events.length, 500) });
|
|
@@ -1873,6 +1882,12 @@ function handleWalleApi(req, res, url) {
|
|
|
1873
1882
|
os_breakdown: tdb.prepare(
|
|
1874
1883
|
'SELECT os, count(*) as cnt FROM telemetry_installs GROUP BY os ORDER BY cnt DESC'
|
|
1875
1884
|
).all(),
|
|
1885
|
+
unique_ips: tdb.prepare(
|
|
1886
|
+
"SELECT count(DISTINCT ip) as cnt FROM telemetry_installs WHERE ip IS NOT NULL AND ip != ''"
|
|
1887
|
+
).get().cnt,
|
|
1888
|
+
ip_breakdown: tdb.prepare(
|
|
1889
|
+
"SELECT ip, count(*) as installs, group_concat(install_id) as install_ids FROM telemetry_installs WHERE ip IS NOT NULL AND ip != '' GROUP BY ip ORDER BY installs DESC"
|
|
1890
|
+
).all(),
|
|
1876
1891
|
recent_errors: tdb.prepare(
|
|
1877
1892
|
"SELECT install_id, version, meta, received_at FROM telemetry_events WHERE event = 'error' AND received_at >= ? ORDER BY received_at DESC LIMIT 50"
|
|
1878
1893
|
).all(since),
|
|
@@ -96,15 +96,36 @@ async function runDueTasks() {
|
|
|
96
96
|
// Strip BRIEFING_ITEMS block from displayed result, store items separately
|
|
97
97
|
const cleanResult = resultText.replace(/<!--\s*BRIEFING_ITEMS[\s\S]*?-->/, '').trim();
|
|
98
98
|
|
|
99
|
+
// Slack delivery verification: if the task originated from Slack but the
|
|
100
|
+
// LLM result suggests the reply was never delivered, retry once directly.
|
|
101
|
+
let slackDeliveryFailed = false;
|
|
102
|
+
if (task.source === 'slack' && (task.run_count || 0) < 3) {
|
|
103
|
+
const deliveryIssue = looksLikeSlackDeliveryFailure(cleanResult);
|
|
104
|
+
if (deliveryIssue) {
|
|
105
|
+
console.log(`[tasks] Slack delivery issue for task ${task.id}: ${deliveryIssue}`);
|
|
106
|
+
appendLog(task.id, `Slack delivery issue: ${deliveryIssue} — retrying send...`);
|
|
107
|
+
slackDeliveryFailed = !(await retrySlackDelivery(task, cleanResult));
|
|
108
|
+
if (!slackDeliveryFailed) {
|
|
109
|
+
appendLog(task.id, 'Slack retry succeeded');
|
|
110
|
+
console.log(`[tasks] Slack retry succeeded for task ${task.id}`);
|
|
111
|
+
} else {
|
|
112
|
+
appendLog(task.id, 'Slack retry also failed — marking task for retry');
|
|
113
|
+
console.log(`[tasks] Slack retry failed for task ${task.id}, will retry later`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
99
118
|
brain.updateTask(task.id, {
|
|
100
|
-
status: task.type === 'recurring' ? 'pending' : 'completed',
|
|
119
|
+
status: slackDeliveryFailed ? 'pending' : (task.type === 'recurring' ? 'pending' : 'completed'),
|
|
101
120
|
completed_at: completedAt,
|
|
102
121
|
last_run_at: completedAt,
|
|
103
122
|
run_count: (task.run_count || 0) + 1,
|
|
104
123
|
result: cleanResult.slice(0, 10000),
|
|
105
|
-
error: null,
|
|
106
124
|
checkpoint: null, // clear checkpoint on success
|
|
107
|
-
|
|
125
|
+
error: slackDeliveryFailed ? 'Slack reply delivery failed — scheduled for retry' : null,
|
|
126
|
+
next_run_at: slackDeliveryFailed
|
|
127
|
+
? new Date(Date.now() + 2 * 60 * 1000).toISOString() // retry in 2 min
|
|
128
|
+
: (task.type === 'recurring' ? computeNextDue(task.schedule) : null),
|
|
108
129
|
// Keep started_at so duration can be computed as completed_at - started_at
|
|
109
130
|
});
|
|
110
131
|
|
|
@@ -534,4 +555,118 @@ function consolidateBriefingItems(task, resultText, timestamp) {
|
|
|
534
555
|
}
|
|
535
556
|
}
|
|
536
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Detect if a task result text indicates the Slack reply was never delivered.
|
|
560
|
+
* Returns a string describing the issue, or null if delivery looks OK.
|
|
561
|
+
*/
|
|
562
|
+
function looksLikeSlackDeliveryFailure(resultText) {
|
|
563
|
+
if (!resultText) return 'empty result';
|
|
564
|
+
const lower = resultText.toLowerCase();
|
|
565
|
+
|
|
566
|
+
// Explicit failure patterns
|
|
567
|
+
const failurePatterns = [
|
|
568
|
+
'mcp server error',
|
|
569
|
+
'error sending',
|
|
570
|
+
'failed to send',
|
|
571
|
+
"couldn't send",
|
|
572
|
+
'could not send',
|
|
573
|
+
'unable to send',
|
|
574
|
+
'slack_send_message failed',
|
|
575
|
+
'failed to deliver',
|
|
576
|
+
'error posting message',
|
|
577
|
+
'connection refused',
|
|
578
|
+
'mcp call failed',
|
|
579
|
+
'encountering a technical issue sending',
|
|
580
|
+
];
|
|
581
|
+
for (const p of failurePatterns) {
|
|
582
|
+
if (lower.includes(p)) return `explicit failure: ${p}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// No reply was attempted — model researched but never sent
|
|
586
|
+
// Success patterns: if any of these appear, the reply was likely sent
|
|
587
|
+
const successPatterns = [
|
|
588
|
+
'replied to your slack',
|
|
589
|
+
'sent the message',
|
|
590
|
+
'message was sent',
|
|
591
|
+
'reply sent',
|
|
592
|
+
'posted to slack',
|
|
593
|
+
'slack_send_message',
|
|
594
|
+
'sent in thread',
|
|
595
|
+
'responded in the thread',
|
|
596
|
+
'replied in the slack thread',
|
|
597
|
+
'sent to the thread',
|
|
598
|
+
'delivered the answer',
|
|
599
|
+
];
|
|
600
|
+
if (!successPatterns.some(p => lower.includes(p))) {
|
|
601
|
+
return 'no reply sent (model exhausted turns without calling slack_send_message)';
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Attempt to retry sending the Slack reply directly via MCP.
|
|
609
|
+
* Extracts the answer from the result text and sends it to the channel/thread
|
|
610
|
+
* referenced in the task description.
|
|
611
|
+
* Returns true if the retry succeeded, false otherwise.
|
|
612
|
+
*/
|
|
613
|
+
async function retrySlackDelivery(task, resultText) {
|
|
614
|
+
try {
|
|
615
|
+
const slackMcp = require('../tools/slack-mcp');
|
|
616
|
+
|
|
617
|
+
// Extract channel_id and thread_ts from the task description
|
|
618
|
+
const channelMatch = task.description?.match(/"channel_id":\s*"([^"]+)"/);
|
|
619
|
+
const threadMatch = task.description?.match(/"thread_ts":\s*"([^"]+)"/);
|
|
620
|
+
if (!channelMatch) {
|
|
621
|
+
console.error('[tasks] Cannot retry Slack delivery: no channel_id in task description');
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Extract the useful content from the result, stripping failure/meta messages
|
|
626
|
+
let answer = resultText;
|
|
627
|
+
|
|
628
|
+
// Remove lines that are just research narration ("Let me...", "I'll...")
|
|
629
|
+
answer = answer.replace(/^(?:Let me|I'll|I will|Now let me|Let's)[^\n]*$/gm, '').trim();
|
|
630
|
+
|
|
631
|
+
// Try to find substantive answer (before any error part)
|
|
632
|
+
const errorIdx = answer.search(/unfortunately.*(?:technical issue|error|couldn't send|MCP server)/i);
|
|
633
|
+
if (errorIdx > 50) {
|
|
634
|
+
answer = answer.slice(0, errorIdx).trim();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// If the answer is still just meta-narration with no real content, try to
|
|
638
|
+
// generate a brief reply using the chat engine with strict instructions
|
|
639
|
+
if (answer.length < 20 || !/[a-z]{3,}/i.test(answer)) {
|
|
640
|
+
console.log('[tasks] No substantive answer in result — generating a brief reply via chat...');
|
|
641
|
+
try {
|
|
642
|
+
const chatModule = require('../chat');
|
|
643
|
+
const briefReply = await chatModule.chat(
|
|
644
|
+
`[URGENT] You MUST reply to a Slack question immediately. The question was:\n\n${task.description?.match(/(?:Question|task) from.*?:\n\n([\s\S]*?)(?:\n\nFull thread|\n\n---)/i)?.[1] || task.title}\n\nGive a helpful 2-3 sentence answer. Do NOT use any tools. Just reply with your best answer based on what you know.`,
|
|
645
|
+
{ channel: 'task', session_id: `retry-${task.id}`, maxToolCalls: 0, timeoutMs: 30000 }
|
|
646
|
+
);
|
|
647
|
+
answer = briefReply.reply || '';
|
|
648
|
+
} catch {
|
|
649
|
+
console.error('[tasks] Brief reply generation failed');
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Truncate if too long for Slack
|
|
655
|
+
if (answer.length > 3000) answer = answer.slice(0, 3000) + '...';
|
|
656
|
+
if (answer.length < 10) {
|
|
657
|
+
console.error('[tasks] Cannot retry Slack delivery: no substantive answer to send');
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const args = { channel_id: channelMatch[1], message: answer };
|
|
662
|
+
if (threadMatch) args.thread_ts = threadMatch[1];
|
|
663
|
+
|
|
664
|
+
const result = await slackMcp.callSlackMcp('slack_send_message', args);
|
|
665
|
+
return !result.error;
|
|
666
|
+
} catch (err) {
|
|
667
|
+
console.error(`[tasks] Slack delivery retry failed: ${err.message}`);
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
537
672
|
module.exports = { runDueTasks, runTaskById, recoverInterruptedTasks, buildTaskPrompt, computeNextDue, getTaskLogs, clearTaskLogs, stopTask, executeSkill, executeMultiTurnChat, consolidateBriefingItems, taskLogs };
|
|
@@ -429,7 +429,7 @@ async function _processMention(msg, seenTs, stats) {
|
|
|
429
429
|
try {
|
|
430
430
|
const { id } = brain.insertTask({
|
|
431
431
|
title,
|
|
432
|
-
description: `Slack task from ${OWNER_NAME}:\n\n${msg.content}${contextBlock}\n\n---\n**
|
|
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
433
|
priority: 'normal',
|
|
434
434
|
type: 'once',
|
|
435
435
|
execution: 'chat',
|
|
@@ -480,7 +480,7 @@ async function _processMention(msg, seenTs, stats) {
|
|
|
480
480
|
try {
|
|
481
481
|
const { id } = brain.insertTask({
|
|
482
482
|
title: `Answer Slack question: ${extractTaskTitle(msg.content)}`,
|
|
483
|
-
description: `Question from ${OWNER_NAME} in Slack:\n\n${msg.content}${contextBlock}\n\n---\n**
|
|
483
|
+
description: `Question from ${OWNER_NAME} in Slack:\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 (ONE call, max 2 total)\nStep 2: think tool — draft a 2-4 sentence answer with what you know\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 answer>" }\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 don't know the answer, reply honestly: "I'm not sure about that — let me look into it and get back to you."`,
|
|
484
484
|
priority: 'high',
|
|
485
485
|
type: 'once',
|
|
486
486
|
execution: 'chat',
|