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.5",
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 costRange = costs.length ? _costExact(minCost) + ' - ' + _costExact(maxCost) : 'No pricing';
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:#e0af68;margin-top:2px;">' + costRange + '</div>' +
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>' +
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "walle",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "private": true,
5
5
  "description": "Wall-E — your personal digital twin",
6
6
  "scripts": {
@@ -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
- next_run_at: task.type === 'recurring' ? computeNextDue(task.schedule) : null,
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**CRITICAL INSTRUCTIONS follow exactly:**\n1. Do focused research (max 3 search calls)\n2. Draft your response\n3. IMMEDIATELY reply in Slack — the reply is the #1 priority. Do NOT over-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\nBetter to reply with what you know than to exhaust turns researching and never reply.`,
432
+ description: `Slack task from ${OWNER_NAME}:\n\n${msg.content}${contextBlock}\n\n---\n**MANDATORYREPLY 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**CRITICAL INSTRUCTIONS follow exactly:**\n1. Do ONE focused search_memories call (max 2 searches total)\n2. Use the think tool to draft a concise answer\n3. IMMEDIATELY reply in Slack do NOT do more research. The Slack reply is the #1 priority.\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\nKeep the answer brief (2-4 sentences). Better to reply quickly with what you know than to research forever and never reply.`,
483
+ description: `Question from ${OWNER_NAME} in Slack:\n\n${msg.content}${contextBlock}\n\n---\n**MANDATORYREPLY 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',