create-walle 0.9.6 → 0.9.7

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 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 ./my-agent
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 ./my-agent
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
- - [Full Documentation](https://walle.sh)
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.6",
3
+ "version": "0.9.7",
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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "walle",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "private": true,
5
5
  "description": "Wall-E — your personal digital twin",
6
6
  "scripts": {
@@ -1826,6 +1826,12 @@ function handleWalleApi(req, res, url) {
1826
1826
  if (p === '/api/wall-e/telemetry/ingest' && m === 'POST') {
1827
1827
  const tdb = getTelemetryDb();
1828
1828
  if (!tdb) return jsonResponse(res, { error: 'Telemetry not enabled' }, 404), true;
1829
+ // Basic rate limiting: max 60 requests per minute per IP
1830
+ const clientIpRL = (req.headers['x-forwarded-for'] || '').split(',')[0].trim() || req.socket?.remoteAddress || 'unknown';
1831
+ if (!handleWalleApi._rlMap) { handleWalleApi._rlMap = new Map(); setInterval(() => handleWalleApi._rlMap.clear(), 60000); }
1832
+ const rlCount = (handleWalleApi._rlMap.get(clientIpRL) || 0) + 1;
1833
+ handleWalleApi._rlMap.set(clientIpRL, rlCount);
1834
+ if (rlCount > 60) return jsonResponse(res, { error: 'Rate limited' }, 429), true;
1829
1835
  readBody(req).then(body => {
1830
1836
  if (!body || !body.id || !Array.isArray(body.events)) {
1831
1837
  return jsonResponse(res, { error: 'Invalid payload' }, 400);
@@ -598,7 +598,7 @@ async function chat(message, opts = {}) {
598
598
  const MAX_TURNS = 8; // search(2-3) + think(1) + response(1) + possible follow-up tools
599
599
 
600
600
  // Guardrails
601
- const MAX_TOOL_CALLS = opts.maxToolCalls || 15;
601
+ const MAX_TOOL_CALLS = opts.maxToolCalls != null ? opts.maxToolCalls : 15;
602
602
  let toolCallCount = 0;
603
603
  const MESSAGE_TIMEOUT_MS = opts.timeoutMs || 180000; // 3 minutes
604
604
  const messageDeadline = Date.now() + MESSAGE_TIMEOUT_MS;
@@ -655,7 +655,7 @@ async function chat(message, opts = {}) {
655
655
  maxTokens: 4096,
656
656
  system: systemPrompt,
657
657
  messages,
658
- tools: chatTools,
658
+ tools: opts.allowedTools ? chatTools.filter(t => opts.allowedTools.includes(t.name)) : chatTools,
659
659
  signal: controller.signal,
660
660
  });
661
661
 
@@ -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('[tasks] Brief reply generation failed');
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
- return !result.error;
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;
@@ -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
- try {
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
- if (msg.channelId) {
468
- try {
469
- await slackMcp.callSlackMcp('slack_send_message', {
470
- channel_id: msg.channelId,
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: ${extractTaskTitle(msg.content)}`,
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
- 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 question task: ${taskErr.message}`);
521
+ console.error(`[slack-mentions] Failed to create task record: ${taskErr.message}`);
498
522
  }
499
523
  }
500
524
 
@@ -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://github.com/anthropics/claude-code">GitHub</a>
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 ./my-agent');this.querySelector('.copy-hint').textContent='Copied!'">
247
- <code>npx create-walle install ./my-agent</code>
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 ./my-agent</span>
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 ./my-agent</code><br>Copies the project, installs deps, and starts the server.</p>
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>