create-walle 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/bin/create-walle.js +134 -0
  2. package/package.json +18 -0
  3. package/template/.env.example +40 -0
  4. package/template/CLAUDE.md +12 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +167 -0
  7. package/template/bin/setup.js +100 -0
  8. package/template/claude-code-skill.md +60 -0
  9. package/template/claude-task-manager/api-prompts.js +1841 -0
  10. package/template/claude-task-manager/api-reviews.js +275 -0
  11. package/template/claude-task-manager/approval-agent.js +454 -0
  12. package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
  13. package/template/claude-task-manager/db.js +1721 -0
  14. package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
  15. package/template/claude-task-manager/git-utils.js +214 -0
  16. package/template/claude-task-manager/package-lock.json +1607 -0
  17. package/template/claude-task-manager/package.json +31 -0
  18. package/template/claude-task-manager/prompt-harvest.js +1148 -0
  19. package/template/claude-task-manager/public/css/prompts.css +880 -0
  20. package/template/claude-task-manager/public/css/reviews.css +430 -0
  21. package/template/claude-task-manager/public/css/walle.css +732 -0
  22. package/template/claude-task-manager/public/favicon.ico +0 -0
  23. package/template/claude-task-manager/public/icon.svg +37 -0
  24. package/template/claude-task-manager/public/index.html +8346 -0
  25. package/template/claude-task-manager/public/js/prompts.js +3159 -0
  26. package/template/claude-task-manager/public/js/reviews.js +1292 -0
  27. package/template/claude-task-manager/public/js/walle.js +3081 -0
  28. package/template/claude-task-manager/public/manifest.json +13 -0
  29. package/template/claude-task-manager/public/prompts.html +4353 -0
  30. package/template/claude-task-manager/public/setup.html +216 -0
  31. package/template/claude-task-manager/queue-engine.js +404 -0
  32. package/template/claude-task-manager/server-state.js +5 -0
  33. package/template/claude-task-manager/server.js +2254 -0
  34. package/template/claude-task-manager/session-utils.js +124 -0
  35. package/template/claude-task-manager/start.sh +17 -0
  36. package/template/claude-task-manager/tests/test-ai-search.js +61 -0
  37. package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
  38. package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
  39. package/template/claude-task-manager/tests/test-features-v2.js +127 -0
  40. package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
  41. package/template/claude-task-manager/tests/test-insights.js +124 -0
  42. package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
  43. package/template/claude-task-manager/tests/test-permissions.js +122 -0
  44. package/template/claude-task-manager/tests/test-pin.js +51 -0
  45. package/template/claude-task-manager/tests/test-prompts.js +164 -0
  46. package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
  47. package/template/claude-task-manager/tests/test-review.js +104 -0
  48. package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
  49. package/template/claude-task-manager/tests/test-send-final.js +30 -0
  50. package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
  51. package/template/claude-task-manager/tests/test-send-integration.js +107 -0
  52. package/template/claude-task-manager/tests/test-send-visual.js +34 -0
  53. package/template/claude-task-manager/tests/test-session-create.js +147 -0
  54. package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
  55. package/template/claude-task-manager/tests/test-url-hash.js +68 -0
  56. package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
  57. package/template/claude-task-manager/tests/test-ux-review.js +130 -0
  58. package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
  59. package/template/claude-task-manager/tests/test-zoom.js +92 -0
  60. package/template/claude-task-manager/tests/test-zoom2.js +67 -0
  61. package/template/docs/site/api/README.md +187 -0
  62. package/template/docs/site/guides/claude-code.md +58 -0
  63. package/template/docs/site/guides/configuration.md +96 -0
  64. package/template/docs/site/guides/quickstart.md +158 -0
  65. package/template/docs/site/index.md +14 -0
  66. package/template/docs/site/skills/README.md +135 -0
  67. package/template/wall-e/.dockerignore +11 -0
  68. package/template/wall-e/Dockerfile +25 -0
  69. package/template/wall-e/adapters/adapter-base.js +37 -0
  70. package/template/wall-e/adapters/ctm.js +193 -0
  71. package/template/wall-e/adapters/slack.js +56 -0
  72. package/template/wall-e/agent.js +319 -0
  73. package/template/wall-e/api-walle.js +1073 -0
  74. package/template/wall-e/brain.js +1235 -0
  75. package/template/wall-e/channels/agent-api.js +172 -0
  76. package/template/wall-e/channels/channel-base.js +14 -0
  77. package/template/wall-e/channels/imessage-channel.js +113 -0
  78. package/template/wall-e/channels/slack-channel.js +118 -0
  79. package/template/wall-e/chat.js +778 -0
  80. package/template/wall-e/decision/confidence.js +93 -0
  81. package/template/wall-e/deploy.sh +35 -0
  82. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
  83. package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
  84. package/template/wall-e/extraction/contradiction.js +168 -0
  85. package/template/wall-e/extraction/knowledge-extractor.js +190 -0
  86. package/template/wall-e/fly.toml +24 -0
  87. package/template/wall-e/loops/ingest.js +34 -0
  88. package/template/wall-e/loops/reflect.js +63 -0
  89. package/template/wall-e/loops/tasks.js +487 -0
  90. package/template/wall-e/loops/think.js +125 -0
  91. package/template/wall-e/package-lock.json +533 -0
  92. package/template/wall-e/package.json +18 -0
  93. package/template/wall-e/scripts/ingest-slack-search.js +85 -0
  94. package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
  95. package/template/wall-e/scripts/slack-backfill.js +295 -0
  96. package/template/wall-e/scripts/slack-channel-history.js +454 -0
  97. package/template/wall-e/server.js +93 -0
  98. package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
  99. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
  100. package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
  101. package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
  102. package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
  103. package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
  104. package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
  105. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
  106. package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
  107. package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
  108. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
  109. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
  110. package/template/wall-e/skills/claude-code-reader.js +144 -0
  111. package/template/wall-e/skills/mcp-client.js +407 -0
  112. package/template/wall-e/skills/skill-executor.js +163 -0
  113. package/template/wall-e/skills/skill-loader.js +410 -0
  114. package/template/wall-e/skills/skill-planner.js +88 -0
  115. package/template/wall-e/skills/slack-ingest.js +329 -0
  116. package/template/wall-e/skills/slack-pull-live.js +270 -0
  117. package/template/wall-e/skills/tool-executor.js +188 -0
  118. package/template/wall-e/tests/adapter-base.test.js +20 -0
  119. package/template/wall-e/tests/adapter-ctm.test.js +122 -0
  120. package/template/wall-e/tests/adapter-slack.test.js +98 -0
  121. package/template/wall-e/tests/agent-api.test.js +256 -0
  122. package/template/wall-e/tests/api-walle.test.js +222 -0
  123. package/template/wall-e/tests/brain.test.js +602 -0
  124. package/template/wall-e/tests/channels.test.js +104 -0
  125. package/template/wall-e/tests/chat.test.js +103 -0
  126. package/template/wall-e/tests/confidence.test.js +134 -0
  127. package/template/wall-e/tests/contradiction.test.js +217 -0
  128. package/template/wall-e/tests/ingest.test.js +113 -0
  129. package/template/wall-e/tests/mcp-client.test.js +71 -0
  130. package/template/wall-e/tests/reflect.test.js +103 -0
  131. package/template/wall-e/tests/server.test.js +111 -0
  132. package/template/wall-e/tests/skills.test.js +198 -0
  133. package/template/wall-e/tests/slack-ingest.test.js +103 -0
  134. package/template/wall-e/tests/think.test.js +435 -0
  135. package/template/wall-e/tools/local-tools.js +697 -0
  136. package/template/wall-e/tools/slack-mcp.js +290 -0
@@ -0,0 +1,778 @@
1
+ 'use strict';
2
+ const Anthropic = require('@anthropic-ai/sdk');
3
+ const brain = require('./brain');
4
+ const { buildClientOpts } = require('./extraction/knowledge-extractor');
5
+ const { executeLocalTool, LOCAL_TOOL_DEFINITIONS } = require('./tools/local-tools');
6
+ const slackMcp = require('./tools/slack-mcp');
7
+
8
+ /**
9
+ * Core "talk to WALL-E" handler.
10
+ * Builds a system prompt from brain context and calls Claude.
11
+ */
12
+ function ensureBrainInit() {
13
+ try { brain.getDb(); } catch {
14
+ brain.initDb();
15
+ }
16
+ }
17
+
18
+ async function chat(message, opts = {}) {
19
+ ensureBrainInit();
20
+ const ownerName = brain.getOwnerName() || 'Owner';
21
+ const channel = opts.channel || 'ctm';
22
+
23
+ // Build brain context — pull ALL available data so WALL-E can answer directly
24
+ const knowledge = brain.findKnowledge({ status: 'active' });
25
+ const recentMemories = brain.listMemories({ limit: 20 });
26
+ const people = brain.listPeople({});
27
+ const stats = brain.getBrainStats();
28
+ const pendingQuestions = brain.listQuestions({ status: 'pending', limit: 5 });
29
+ const skills = brain.listSkills({});
30
+ let domainConfidences = [];
31
+ try { domainConfidences = brain.getDb().prepare('SELECT * FROM domain_confidence').all(); } catch {}
32
+
33
+ // Pull a diverse sample of Slack messages to give WALL-E personality/style context
34
+ let slackSample = '';
35
+ try {
36
+ const db = brain.getDb();
37
+ // Get a spread of Slack messages across time — owner's outbound messages reveal personality
38
+ const slackMsgs = db.prepare(`
39
+ SELECT content, source_channel, timestamp FROM memories
40
+ WHERE source = 'slack' AND direction = 'outbound' AND length(content) > 20
41
+ ORDER BY RANDOM() LIMIT 20
42
+ `).all();
43
+ if (slackMsgs.length > 0) {
44
+ slackSample = slackMsgs.map(m => {
45
+ const date = m.timestamp ? m.timestamp.slice(0, 10) : '';
46
+ return `[${date} #${m.source_channel || '?'}] ${(m.content || '').slice(0, 200)}`;
47
+ }).join('\n');
48
+ }
49
+ } catch {}
50
+
51
+ // Load available MCP servers so WALL-E knows what it can connect to
52
+ let mcpServerList = '';
53
+ try {
54
+ const { loadMcpConfigs } = require('./skills/mcp-client');
55
+ const configs = loadMcpConfigs();
56
+ mcpServerList = Object.entries(configs).map(([name, cfg]) => {
57
+ const hasAuth = cfg.oauth?.accessToken ? 'authenticated' : 'needs auth';
58
+ return `- ${name}: ${cfg.type || 'stdio'} (${cfg.url || cfg.command || 'local'}) [${hasAuth}]`;
59
+ }).join('\n');
60
+ } catch {}
61
+
62
+ // Build knowledge summary
63
+ const knowledgeSummary = knowledge.slice(0, 50).map(k =>
64
+ `- ${k.subject} ${k.predicate} ${k.object} (${Math.round((k.confidence || 0.5) * 100)}%)`
65
+ ).join('\n');
66
+
67
+ // Recent memories summary — prefer Slack over CTM tool calls
68
+ const slackRecent = brain.getDb().prepare(`
69
+ SELECT content, timestamp, source_channel FROM memories
70
+ WHERE source = 'slack' AND direction = 'outbound' AND length(content) > 20
71
+ ORDER BY timestamp DESC LIMIT 10
72
+ `).all();
73
+ const memorySummary = slackRecent.map(m =>
74
+ `[${m.timestamp?.slice(0, 10)} ${m.source_channel || 'DM'}] ${m.content?.slice(0, 200)}`
75
+ ).join('\n');
76
+
77
+ // Load pre-computed metadata for richer context
78
+ let peopleMeta = '', topicMeta = '', slackCoverage = '';
79
+ try {
80
+ const getMeta = brain.getDb().prepare('SELECT value FROM brain_metadata WHERE key = ?');
81
+ peopleMeta = getMeta.get('people_interaction_summary')?.value || '';
82
+ topicMeta = getMeta.get('topic_frequency')?.value || '';
83
+ slackCoverage = getMeta.get('slack_coverage')?.value || '';
84
+ } catch {}
85
+
86
+ // People summary
87
+ const peopleSummary = people.map(p =>
88
+ `- ${p.name}: ${p.relationship || 'unknown'} (trust: ${p.trust_level || 0.5})`
89
+ ).join('\n');
90
+
91
+ // Pending questions
92
+ const questionsSummary = pendingQuestions.map(q =>
93
+ `- [${q.question_type}] ${q.question}`
94
+ ).join('\n');
95
+
96
+ // Skills summary
97
+ const skillsSummary = skills.map(s => {
98
+ const rate = (s.success_count + s.failure_count) > 0
99
+ ? Math.round(s.success_count / (s.success_count + s.failure_count) * 100) + '%' : 'not run yet';
100
+ return `- ${s.name}: ${s.description || 'no description'} (${s.enabled ? 'enabled' : 'disabled'}, success rate: ${rate})`;
101
+ }).join('\n');
102
+
103
+ // Memory source breakdown
104
+ let sourceBreakdown = '';
105
+ try {
106
+ const sources = brain.getDb().prepare('SELECT source, count(*) as c FROM memories GROUP BY source ORDER BY c DESC').all();
107
+ sourceBreakdown = sources.map(s => `- ${s.source}: ${s.c} memories`).join('\n');
108
+ } catch {}
109
+
110
+ // Domain confidence summary
111
+ const tierNames = { 1: 'Observe', 2: 'Draft', 3: 'Guarded', 4: 'Autonomous' };
112
+ const confidenceSummary = domainConfidences.map(dc =>
113
+ `- ${dc.domain}: Tier ${dc.current_tier} (${tierNames[dc.current_tier] || '?'}), ${dc.approved_actions}/${dc.total_actions} approved`
114
+ ).join('\n');
115
+
116
+ const systemPrompt = `You are WALL-E, ${ownerName}'s personal digital twin. You know ${ownerName} deeply through ${stats.memory_count} memories including ${slackCoverage || '1500+ Slack messages'}.
117
+
118
+ ## ${ownerName}'s Key Relationships (by message volume)
119
+ ${peopleMeta || peopleSummary || 'No people data yet.'}
120
+
121
+ ## Topics ${ownerName} Cares About (by frequency in conversations)
122
+ ${topicMeta || 'No topic data yet.'}
123
+
124
+ ## Knowledge Base
125
+ ${knowledgeSummary || 'Still learning...'}
126
+
127
+ ## Recent Slack Messages (${ownerName}'s actual words)
128
+ ${memorySummary || 'No recent activity.'}
129
+
130
+ ${slackSample ? `## ${ownerName}'s Voice — Random Sample\n${slackSample}` : ''}
131
+
132
+ ## Memory Sources
133
+ ${sourceBreakdown || 'No breakdown available'}
134
+
135
+ ## Skills & Tools
136
+ ${skillsSummary || 'No skills configured yet.'}
137
+ MCP Servers: ${mcpServerList || 'None'}
138
+
139
+ ${pendingQuestions.length > 0 ? `## Questions Pending\n${questionsSummary}` : ''}
140
+
141
+ ## How to Reason and Respond
142
+
143
+ ### Step 1: SEARCH — gather evidence (call ALL searches in ONE turn)
144
+ For ANY question beyond small talk, call search_memories MULTIPLE TIMES IN THE SAME TURN. Do NOT do one search per turn — batch them all together.
145
+ Example: if asked about leadership, call these ALL AT ONCE in one response:
146
+ - search_memories({query: "leadership coaching feedback"})
147
+ - search_memories({query: "团队 管理 反馈", source: "slack"})
148
+ - search_memories({query: "Danni Mengyang Zohaib"})
149
+
150
+ ### Step 2: THINK — reason through the evidence (same turn as search results if possible)
151
+ After gathering evidence, ALWAYS use the **think** tool before responding. This is your internal scratchpad — ${ownerName} won't see it. Use it to:
152
+
153
+ **Analyze patterns, not just surface content:**
154
+ - What does the evidence ACTUALLY show vs what it SEEMS to show?
155
+ - Am I attributing behavior correctly? (Is this ${ownerName}'s gap, or someone else's?)
156
+ - What's the counterargument? What evidence contradicts my initial take?
157
+ - Are there multiple interpretations of the same message?
158
+
159
+ **Challenge your own conclusions:**
160
+ - If I'm about to say "${ownerName} does X poorly" — do I have 3+ examples? Or am I over-generalizing from one message?
161
+ - Could this behavior be DELIBERATE and STRATEGIC rather than a gap?
162
+ - What's the full context? Who was ${ownerName} talking to, and why?
163
+
164
+ **Think about nuance:**
165
+ - ${ownerName} is a director managing 60+ engineers. What looks like "venting" might be calculated information sharing.
166
+ - What looks like "brevity" might be appropriate trust-based communication with close reports.
167
+ - What looks like "delegating the hard conversation" might be empowering direct reports.
168
+
169
+ ### Step 3: RESPOND — with depth and nuance
170
+ - Use **bold** for key names, dates, and decisions
171
+ - Use > blockquotes when quoting actual Slack messages
172
+ - Use ### headers to organize multi-part answers
173
+ - Include dates and people: "On **2024-12-12**, you told **Zohaib**: ..."
174
+ - **Bilingual-aware**: Translate Chinese quotes — they contain the most candid opinions
175
+ - Present BOTH sides before drawing conclusions
176
+ - Acknowledge where the evidence is thin or ambiguous
177
+
178
+ ### What makes a DEEP answer vs a SHALLOW one
179
+ **SHALLOW** (bad): "You sometimes vent in group DMs" → no examples, no analysis of WHY
180
+ **DEEP** (good): "You coached Xiao Bai on venting technique (Dec 2023), but in your own group DMs you do X, Y, Z — however, this may be intentional because your audience (Sonic, Mengyang) is your inner circle where candor is valued"
181
+
182
+ ### Tool usage
183
+ - **think**: Internal reasoning scratchpad. Use BEFORE every substantive response. ${ownerName} won't see this.
184
+ - **search_memories**: Full-text search with BM25 ranking. Use source:"slack" for Slack only.
185
+ - remember_fact: Store new knowledge the user teaches you.
186
+ - run_skill, mcp_call, list_mcp_tools: For actions and external services.
187
+
188
+ ### Local Machine Tools (macOS)
189
+ - **web_fetch**: Fetch any URL — weather, news, APIs, documentation. Use for ALL real-time data requests.
190
+ - **run_shell**: Execute shell commands (git, node, grep, mdfind, open, etc.). Destructive commands blocked.
191
+ - **read_file / write_file**: Read or write local files (under $HOME).
192
+ - **search_files**: Spotlight search (mdfind) for fast file discovery.
193
+ - **clipboard_read / clipboard_write**: System clipboard access.
194
+ - **open_url / open_app**: Open URLs in browser or launch macOS apps.
195
+ - **notification**: Show macOS notification banner.
196
+ - **applescript**: Run AppleScript for deep macOS automation (Finder, Mail, etc.).
197
+ - **calendar_events / calendar_create**: Read/create events in macOS Calendar.
198
+ - **reminder_create**: Create reminders in macOS Reminders app.
199
+ - **screenshot**: Capture screen to file.
200
+ - **system_info**: Get macOS version, uptime, disk space.
201
+
202
+ **IMPORTANT**: You are an AGENT, not just a chatbot. NEVER say "I can't access that" or "I don't have access to real-time data." You have tools — USE THEM.
203
+
204
+ **Location awareness**: When ${ownerName} asks location-dependent questions (weather, "where am I", local time, nearby places), FIRST determine their current location by:
205
+ 1. Check calendar_events for travel/meetings that reveal location
206
+ 2. Search memories for recent travel plans or location mentions
207
+ 3. Only after determining location, fetch the relevant data
208
+
209
+ **Weather**: Use web_fetch with Open-Meteo API (free, no key needed):
210
+ \`https://api.open-meteo.com/v1/forecast?latitude=LAT&longitude=LON&current=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,relative_humidity_2m&timezone=auto\`
211
+ Common coords: Seattle(47.61,-122.33), London(51.51,-0.13), Helsinki(60.17,24.94), San Francisco(37.77,-122.42), Beijing(39.90,116.40)
212
+ Weather codes: 0=clear, 1-3=partly cloudy, 45-48=fog, 51-55=drizzle, 61-65=rain, 71-77=snow, 80-82=showers, 95-99=thunderstorm
213
+
214
+ When ${ownerName} asks you to DO something (create a file, set a reminder, search for a document, etc.), use these tools directly.
215
+ - Channel: ${channel}${channel === 'imessage' ? ' (keep responses brief)' : ''}`;
216
+
217
+ // Use injected client (for testing) or build one from env
218
+ const client = _clientOverride || getClientForChat();
219
+
220
+ // Per-turn abort controller — each API call gets its own 2-min timeout.
221
+ // This prevents long multi-turn tasks (e.g., Morning Briefing) from aborting
222
+ // as long as each individual turn completes within the timeout.
223
+ let controller = new AbortController();
224
+ let timeout = null;
225
+ function resetTurnTimeout() {
226
+ if (timeout) clearTimeout(timeout);
227
+ controller = new AbortController();
228
+ timeout = setTimeout(() => controller.abort(), 120000); // 2 min per turn
229
+ }
230
+
231
+ // Define internal tools WALL-E can use during chat
232
+ const chatTools = [
233
+ {
234
+ name: 'think',
235
+ description: 'Internal reasoning scratchpad. Use this BEFORE every substantive response to analyze evidence, challenge assumptions, consider counterarguments, and build a nuanced take. The user will NOT see this output — it is purely for your reasoning process. Think like a thoughtful advisor, not a search engine.',
236
+ input_schema: {
237
+ type: 'object',
238
+ properties: {
239
+ reasoning: {
240
+ type: 'string',
241
+ description: 'Your internal analysis: What does the evidence show? What are the counterarguments? What nuance am I missing? Is my conclusion actually supported?'
242
+ }
243
+ },
244
+ required: ['reasoning']
245
+ },
246
+ },
247
+ {
248
+ name: 'run_skill',
249
+ description: 'Run one of my skills to fetch data or perform an action. Use this when the user asks me to do something actionable.',
250
+ input_schema: { type: 'object', properties: { skill_name: { type: 'string', description: 'Name of the skill to run' } }, required: ['skill_name'] },
251
+ },
252
+ {
253
+ name: 'search_memories',
254
+ description: 'Full-text search with BM25 relevance ranking. Call MULTIPLE searches in ONE turn to batch them. Supports: multi-word (auto-OR), prefix ("lead*"), phrases, source filtering.',
255
+ input_schema: { type: 'object', properties: { query: { type: 'string', description: 'Search query. Multi-word = OR. Prefix: "lead*". Phrase: quoted.' }, source: { type: 'string', description: 'Filter: slack, ctm, wall-e-chat' }, limit: { type: 'number', default: 15 } }, required: ['query'] },
256
+ },
257
+ {
258
+ name: 'lookup_person',
259
+ description: 'Get detailed info about a person I know.',
260
+ input_schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
261
+ },
262
+ {
263
+ name: 'remember_fact',
264
+ description: 'Store a new fact the user tells me as knowledge.',
265
+ input_schema: { type: 'object', properties: { subject: { type: 'string' }, predicate: { type: 'string' }, object: { type: 'string' }, category: { type: 'string', enum: ['technical', 'preference', 'relationship', 'work', 'personal', 'world'] } }, required: ['subject', 'predicate', 'object', 'category'] },
266
+ },
267
+ {
268
+ name: 'mcp_call',
269
+ description: 'Call a tool on an MCP server (e.g., search Slack, send email). Use list_mcp_tools first to see available tools.',
270
+ input_schema: {
271
+ type: 'object',
272
+ properties: {
273
+ server: { type: 'string', description: 'MCP server name' },
274
+ tool: { type: 'string', description: 'Tool name' },
275
+ arguments: { type: 'object', description: 'Tool arguments' },
276
+ },
277
+ required: ['server', 'tool'],
278
+ },
279
+ },
280
+ {
281
+ name: 'list_mcp_tools',
282
+ description: 'List available MCP tools from all connected servers.',
283
+ input_schema: { type: 'object', properties: {} },
284
+ },
285
+ // Direct Slack access (auto-authenticates on first use)
286
+ {
287
+ name: 'pull_slack',
288
+ description: 'Pull Slack messages into WALL-E\'s brain. mode="incremental" (default) only fetches new messages. mode="full" does a complete month-by-month backfill from 2022 to now. Both modes skip duplicates.',
289
+ input_schema: {
290
+ type: 'object',
291
+ properties: {
292
+ mode: { type: 'string', enum: ['incremental', 'full'], description: 'incremental (only new) or full (all history month by month)' },
293
+ },
294
+ },
295
+ },
296
+ {
297
+ name: 'slack_connect',
298
+ description: 'Connect WALL-E to Slack. Opens browser for OAuth login. Only needed if Slack tools fail with auth errors.',
299
+ input_schema: { type: 'object', properties: {} },
300
+ },
301
+ {
302
+ name: 'slack_search',
303
+ description: 'Search Slack messages in real-time. Auto-connects if not authenticated. Use for finding recent conversations, mentions, or topics.',
304
+ input_schema: {
305
+ type: 'object',
306
+ properties: {
307
+ query: { type: 'string', description: 'Search query (supports Slack search syntax: from:user, in:channel, before:date, etc.)' },
308
+ },
309
+ required: ['query'],
310
+ },
311
+ },
312
+ {
313
+ name: 'slack_read_channel',
314
+ description: 'Read recent messages from a Slack channel or DM. Auto-connects if not authenticated.',
315
+ input_schema: {
316
+ type: 'object',
317
+ properties: {
318
+ channel_name: { type: 'string', description: 'Channel name (without #) or user name for DMs' },
319
+ limit: { type: 'number', description: 'Number of messages (default 20)' },
320
+ },
321
+ required: ['channel_name'],
322
+ },
323
+ },
324
+ {
325
+ name: 'slack_send_message',
326
+ description: 'Send a message to a Slack channel or DM. Use only when explicitly asked to send. Auto-connects if not authenticated.',
327
+ input_schema: {
328
+ type: 'object',
329
+ properties: {
330
+ channel_name: { type: 'string', description: 'Channel name (without #) or user name for DMs' },
331
+ message: { type: 'string', description: 'Message text (supports Slack mrkdwn)' },
332
+ },
333
+ required: ['channel_name', 'message'],
334
+ },
335
+ },
336
+ // Task management
337
+ {
338
+ name: 'create_task',
339
+ description: 'Create a background task for WALL-E to work on. Tasks run asynchronously — results appear in the Tasks tab. Use for: research, monitoring, scheduled actions, anything that takes time.',
340
+ input_schema: {
341
+ type: 'object',
342
+ properties: {
343
+ title: { type: 'string', description: 'Short task title' },
344
+ description: { type: 'string', description: 'Detailed instructions for what to do' },
345
+ priority: { type: 'string', enum: ['urgent', 'high', 'normal', 'low'] },
346
+ due_at: { type: 'string', description: 'When to execute (ISO datetime). Omit for immediate.' },
347
+ type: { type: 'string', enum: ['once', 'recurring'] },
348
+ schedule: { type: 'string', description: 'For recurring: "hourly", "daily", "weekly", or "every 2h"' },
349
+ },
350
+ required: ['title', 'description'],
351
+ },
352
+ },
353
+ {
354
+ name: 'list_tasks',
355
+ description: 'List current tasks and their status.',
356
+ input_schema: {
357
+ type: 'object',
358
+ properties: { status: { type: 'string', enum: ['pending', 'running', 'completed', 'failed'] } },
359
+ },
360
+ },
361
+ // Local machine tools — file ops, shell, macOS automation, calendar, etc.
362
+ ...LOCAL_TOOL_DEFINITIONS,
363
+ ];
364
+
365
+ const onProgress = opts.onProgress || (() => {});
366
+
367
+ // Execute a chat tool call
368
+ async function executeChatTool(name, input) {
369
+ if (name === 'think') {
370
+ // Internal reasoning tool — just acknowledge it. The reasoning is in the tool call itself
371
+ // which becomes part of the conversation context for Claude's next response.
372
+ console.log('[chat] Think tool used:', (input.reasoning || '').slice(0, 200));
373
+ return { acknowledged: true };
374
+ }
375
+ if (name === 'run_skill') {
376
+ const skill = brain.getSkillByName(input.skill_name);
377
+ if (!skill) return { error: `Skill "${input.skill_name}" not found` };
378
+ try {
379
+ const { runSkill } = require('./skills/skill-executor');
380
+ const result = await runSkill(skill);
381
+ return { success: true, memories_created: result.memoriesCreated, tool_calls: result.toolCalls, duration_ms: result.duration };
382
+ } catch (err) {
383
+ return { error: err.message };
384
+ }
385
+ }
386
+ if (name === 'search_memories') {
387
+ // FTS5 full-text search with BM25 ranking
388
+ console.log('[chat] search_memories called with input:', JSON.stringify(input));
389
+ const db = brain.getDb();
390
+ const limit = input.limit || 30;
391
+ let results = [];
392
+
393
+ // Check if FTS5 table exists
394
+ const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
395
+
396
+ if (hasFts && input.query) {
397
+ try {
398
+ // Build FTS5 MATCH query
399
+ let terms = input.query.split('|').map(t => t.trim()).filter(Boolean);
400
+ if (terms.length === 1 && terms[0].includes(' ')) {
401
+ terms = terms[0].split(/\s+/).filter(t => t.length >= 2);
402
+ }
403
+ // FTS5 OR query: "word1 OR word2 OR word3"
404
+ let ftsQuery = terms.map(t => t.replace(/['"]/g, '')).join(' OR ');
405
+ // Add source filter if specified
406
+ if (input.source) {
407
+ ftsQuery = `source:${input.source} AND (${ftsQuery})`;
408
+ }
409
+
410
+ console.log('[chat] FTS5 query:', ftsQuery);
411
+ results = db.prepare(`
412
+ SELECT m.*, bm25(memories_fts) as relevance_score
413
+ FROM memories_fts f
414
+ JOIN memories m ON m.rowid = f.rowid
415
+ WHERE memories_fts MATCH ?
416
+ ORDER BY bm25(memories_fts)
417
+ LIMIT ?
418
+ `).all(ftsQuery, limit);
419
+ console.log('[chat] FTS5 returned', results.length, 'results (ranked by relevance)');
420
+ } catch (ftsErr) {
421
+ console.log('[chat] FTS5 error, falling back to LIKE:', ftsErr.message);
422
+ results = []; // fall through to LIKE
423
+ }
424
+ }
425
+
426
+ // Fallback to LIKE search if FTS5 unavailable or failed
427
+ if (results.length === 0 && input.query) {
428
+ let terms = input.query.split('|').map(t => t.trim()).filter(Boolean);
429
+ if (terms.length === 1 && terms[0].includes(' ')) {
430
+ terms = terms[0].split(/\s+/).filter(t => t.length >= 2);
431
+ }
432
+ let sql = 'SELECT * FROM memories WHERE 1=1';
433
+ const params = [];
434
+ if (input.source) { sql += ' AND source = ?'; params.push(input.source); }
435
+ if (terms.length > 1) {
436
+ sql += ' AND (' + terms.map(() => 'content LIKE ?').join(' OR ') + ')';
437
+ terms.forEach(t => params.push('%' + t + '%'));
438
+ } else if (terms.length === 1) {
439
+ sql += ' AND content LIKE ?';
440
+ params.push('%' + terms[0] + '%');
441
+ }
442
+ sql += ' ORDER BY timestamp DESC LIMIT ?';
443
+ params.push(limit);
444
+ results = db.prepare(sql).all(...params);
445
+ console.log('[chat] LIKE fallback returned', results.length, 'results');
446
+ }
447
+
448
+ const totalSlack = db.prepare('SELECT count(*) as c FROM memories WHERE source = ?').get('slack').c;
449
+ return {
450
+ count: results.length,
451
+ total_memories: db.prepare('SELECT count(*) as c FROM memories').get().c,
452
+ total_slack: totalSlack,
453
+ search_method: results[0]?.relevance_score !== undefined ? 'fts5_bm25' : 'like',
454
+ memories: results.map(m => ({
455
+ source: m.source,
456
+ channel: m.source_channel,
457
+ participants: m.participants,
458
+ content: m.content?.slice(0, 300),
459
+ timestamp: m.timestamp,
460
+ direction: m.direction,
461
+ relevance: m.relevance_score ? Math.round(m.relevance_score * -100) / 100 : undefined,
462
+ })),
463
+ };
464
+ }
465
+ if (name === 'lookup_person') {
466
+ const people = brain.listPeople({});
467
+ const person = people.find(p => p.name && p.name.toLowerCase().includes(input.name.toLowerCase()));
468
+ if (!person) return { error: `Person "${input.name}" not found` };
469
+ let persona = null;
470
+ try { persona = brain.getPersonaForPerson(person.id); } catch {}
471
+ return { ...person, persona };
472
+ }
473
+ if (name === 'remember_fact') {
474
+ brain.insertKnowledge({ category: input.category, subject: input.subject, predicate: input.predicate, object: input.object, confidence: 0.95, source_memory_ids: '[]' });
475
+ return { stored: true };
476
+ }
477
+ if (name === 'mcp_call') {
478
+ try {
479
+ const { callMcpTool } = require('./skills/mcp-client');
480
+ const result = await callMcpTool(input.server, input.tool, input.arguments || {});
481
+ if (result && result.content) {
482
+ const texts = result.content.filter(c => c.type === 'text').map(c => c.text);
483
+ return texts.join('\n').slice(0, 10000);
484
+ }
485
+ return result;
486
+ } catch (err) {
487
+ return { error: err.message };
488
+ }
489
+ }
490
+ if (name === 'list_mcp_tools') {
491
+ try {
492
+ const { listAllTools } = require('./skills/mcp-client');
493
+ const tools = await listAllTools();
494
+ return tools.map(t => ({ server: t.server, name: t.name, description: (t.description || '').slice(0, 150) }));
495
+ } catch (err) {
496
+ return { error: err.message };
497
+ }
498
+ }
499
+ if (name === 'pull_slack') {
500
+ try {
501
+ const { pullNewSlackMessages, pullAllSlackMessages } = require('./skills/slack-pull-live');
502
+ const logs = [];
503
+ const progressCb = (msg) => { logs.push(msg); onProgress({ type: 'tool_call', tool: 'pull_slack', summary: msg }); };
504
+ const pullFn = input.mode === 'full' ? pullAllSlackMessages : pullNewSlackMessages;
505
+ const result = await pullFn({ onProgress: progressCb });
506
+ return { ...result, logs };
507
+ } catch (err) {
508
+ return { error: err.message };
509
+ }
510
+ }
511
+ if (name === 'slack_connect') {
512
+ try {
513
+ console.log('[chat] Starting Slack OAuth flow...');
514
+ onProgress({ type: 'tool_call', tool: 'slack_connect', summary: 'Opening browser for Slack login...' });
515
+ const token = await slackMcp.authenticate();
516
+ return { connected: true, message: 'Slack connected! I can now search and read your Slack messages.' };
517
+ } catch (err) {
518
+ return { error: 'Slack auth failed: ' + err.message + '. Please try again.' };
519
+ }
520
+ }
521
+ if (name === 'slack_search' || name === 'slack_read_channel' || name === 'slack_send_message') {
522
+ // Auto-auth: if not authenticated, trigger OAuth flow automatically
523
+ if (!slackMcp.isAuthenticated()) {
524
+ try {
525
+ console.log('[chat] Slack not authenticated — triggering auto-auth...');
526
+ onProgress({ type: 'tool_call', tool: 'slack_connect', summary: 'Slack not connected — opening browser for login...' });
527
+ await slackMcp.authenticate();
528
+ onProgress({ type: 'tool_done', tool: 'slack_connect', summary: 'Slack connected!' });
529
+ } catch (authErr) {
530
+ return { error: 'Slack not connected. Auth failed: ' + authErr.message };
531
+ }
532
+ }
533
+ try {
534
+ let toolName, toolArgs;
535
+ if (name === 'slack_search') {
536
+ toolName = 'slack_search_public_and_private';
537
+ toolArgs = { query: input.query };
538
+ } else if (name === 'slack_read_channel') {
539
+ toolName = 'slack_read_channel';
540
+ toolArgs = { channel_name: input.channel_name, limit: input.limit || 20 };
541
+ } else {
542
+ toolName = 'slack_send_message';
543
+ toolArgs = { channel_name: input.channel_name, message: input.message };
544
+ }
545
+ const result = await slackMcp.callSlackMcp(toolName, toolArgs);
546
+ const texts = (result?.content || []).filter(c => c.type === 'text').map(c => c.text);
547
+ const combined = texts.join('\n').slice(0, 10000);
548
+ if (name === 'slack_send_message') return { sent: true, response: combined.slice(0, 2000) };
549
+ return name === 'slack_search' ? { results: combined } : { messages: combined };
550
+ } catch (err) {
551
+ // If auth error, clear token and suggest re-auth
552
+ if (err.message.includes('401') || err.message.includes('expired') || err.message.includes('invalid_auth')) {
553
+ slackMcp.clearToken();
554
+ return { error: 'Slack token expired. I\'ll reconnect on the next attempt — please try again.' };
555
+ }
556
+ return { error: err.message };
557
+ }
558
+ }
559
+ if (name === 'create_task') {
560
+ const result = brain.insertTask({
561
+ title: input.title,
562
+ description: input.description,
563
+ priority: input.priority,
564
+ type: input.type,
565
+ schedule: input.schedule,
566
+ due_at: input.due_at,
567
+ });
568
+ return { created: true, id: result.id, title: input.title, status: 'pending', due_at: input.due_at || 'immediate' };
569
+ }
570
+ if (name === 'list_tasks') {
571
+ const tasks = brain.listTasks({ status: input.status, limit: 20 });
572
+ return { count: tasks.length, tasks: tasks.map(t => ({ id: t.id, title: t.title, status: t.status, priority: t.priority, due_at: t.due_at, created_at: t.created_at, result: t.result?.slice(0, 200) })) };
573
+ }
574
+ // Try local tools (shell, files, macOS automation, calendar, etc.)
575
+ try {
576
+ const localResult = await executeLocalTool(name, input);
577
+ if (localResult !== null) return localResult;
578
+ } catch (err) {
579
+ return { error: err.message };
580
+ }
581
+ return { error: 'Unknown tool' };
582
+ }
583
+
584
+ try {
585
+ // Save user message FIRST (before calling Claude) so it gets an earlier timestamp
586
+ const sessionId = opts.session_id || 'default';
587
+ brain.insertChatMessage({ role: 'user', content: message, channel, session_id: sessionId });
588
+ brain.insertMemory({
589
+ source: 'wall-e-chat',
590
+ source_id: `chat-${Date.now()}`,
591
+ memory_type: 'message_received',
592
+ direction: 'inbound',
593
+ content: message,
594
+ timestamp: new Date().toISOString(),
595
+ metadata: JSON.stringify({ channel }),
596
+ });
597
+
598
+ // Load recent chat history — limit to last 10 messages to reduce input tokens
599
+ // (20 messages was ~5K tokens; 10 is ~2.5K — saves ~2s per turn)
600
+ const chatSessionId = sessionId;
601
+ const recentChat = brain.listChatMessages({ session_id: chatSessionId, limit: 10 });
602
+ const historyMessages = recentChat.map(m => ({ role: m.role, content: m.content }));
603
+ // Agentic chat loop — history already includes the new user message
604
+ const messages = [...historyMessages];
605
+ let finalText = '';
606
+ const MAX_TURNS = 8; // search(2-3) + think(1) + response(1) + possible follow-up tools
607
+
608
+ const chatStart = Date.now();
609
+ for (let turn = 0; turn < MAX_TURNS; turn++) {
610
+ const turnStart = Date.now();
611
+ onProgress({ type: 'thinking', turn });
612
+ resetTurnTimeout();
613
+ const response = await client.messages.create({
614
+ model: opts.model || 'claude-sonnet-4-6',
615
+ max_tokens: 4096,
616
+ system: systemPrompt,
617
+ messages,
618
+ tools: chatTools,
619
+ }, { signal: controller.signal });
620
+
621
+ const textBlocks = response.content.filter(b => b.type === 'text');
622
+ const toolUseBlocks = response.content.filter(b => b.type === 'tool_use');
623
+
624
+ console.log('[chat] Turn', turn, 'took', Date.now() - turnStart, 'ms, tools:', toolUseBlocks.map(t => t.name).join(',') || 'none');
625
+
626
+ // If no tool calls, we have our final answer
627
+ if (toolUseBlocks.length === 0) {
628
+ finalText = textBlocks.map(b => b.text).join('');
629
+ console.log('[chat] Total time:', Date.now() - chatStart, 'ms across', turn + 1, 'turns');
630
+ break;
631
+ }
632
+
633
+ // Emit progress for each tool call before executing
634
+ for (const tu of toolUseBlocks) {
635
+ const summary = tu.name === 'search_memories'
636
+ ? `Searching: "${tu.input.query}"${tu.input.source ? ` (${tu.input.source})` : ''}`
637
+ : tu.name === 'think'
638
+ ? 'Analyzing evidence...'
639
+ : tu.name === 'lookup_person'
640
+ ? `Looking up: ${tu.input.name}`
641
+ : tu.name === 'mcp_call'
642
+ ? `Calling ${tu.input.server}.${tu.input.tool}`
643
+ : tu.name === 'run_skill'
644
+ ? `Running skill: ${tu.input.skill_name}`
645
+ : tu.name === 'run_shell'
646
+ ? `Running: ${tu.input.command} ${(tu.input.args || []).join(' ')}`.slice(0, 80)
647
+ : tu.name === 'read_file'
648
+ ? `Reading: ${tu.input.file_path}`
649
+ : tu.name === 'write_file'
650
+ ? `Writing: ${tu.input.file_path}`
651
+ : tu.name === 'search_files'
652
+ ? `Searching files: "${tu.input.query}"`
653
+ : tu.name === 'calendar_events'
654
+ ? 'Checking calendar...'
655
+ : tu.name === 'calendar_create'
656
+ ? `Creating event: ${tu.input.title}`
657
+ : tu.name === 'reminder_create'
658
+ ? `Creating reminder: ${tu.input.title}`
659
+ : tu.name === 'notification'
660
+ ? `Notification: ${tu.input.title}`
661
+ : tu.name === 'applescript'
662
+ ? 'Running AppleScript...'
663
+ : tu.name === 'open_url'
664
+ ? `Opening: ${tu.input.url}`
665
+ : tu.name === 'open_app'
666
+ ? `Opening: ${tu.input.app_name}`
667
+ : tu.name === 'web_fetch'
668
+ ? `Fetching: ${tu.input.url}`
669
+ : `Using tool: ${tu.name}`;
670
+ onProgress({ type: 'tool_call', tool: tu.name, summary });
671
+ }
672
+
673
+ // Execute tool calls in PARALLEL (searches are independent, no need to serialize)
674
+ const toolResults = await Promise.all(toolUseBlocks.map(async (tu) => {
675
+ const t0 = Date.now();
676
+ console.log('[chat] Tool call:', tu.name, JSON.stringify(tu.input).slice(0, 150));
677
+ const result = await executeChatTool(tu.name, tu.input);
678
+ const resultStr = JSON.stringify(result);
679
+ const elapsed = Date.now() - t0;
680
+ console.log('[chat] Tool', tu.name, 'done in', elapsed, 'ms, result:', resultStr.length, 'chars');
681
+ // Emit tool completion with result summary
682
+ const resultSummary = tu.name === 'search_memories'
683
+ ? `Found ${result.count || 0} results (${result.search_method || 'search'})`
684
+ : tu.name === 'think'
685
+ ? 'Done thinking'
686
+ : `Completed in ${elapsed}ms`;
687
+ onProgress({ type: 'tool_done', tool: tu.name, summary: resultSummary });
688
+ return { type: 'tool_result', tool_use_id: tu.id, content: resultStr };
689
+ }));
690
+
691
+ messages.push({ role: 'assistant', content: response.content });
692
+ messages.push({ role: 'user', content: toolResults });
693
+
694
+ // Accumulate any text from this turn
695
+ if (textBlocks.length > 0) {
696
+ finalText += textBlocks.map(b => b.text).join('');
697
+ }
698
+
699
+ // If Claude ended, we're done
700
+ if (response.stop_reason === 'end_turn') {
701
+ break;
702
+ }
703
+ }
704
+
705
+ // If no text was produced after tool calls, make one more call to get a summary
706
+ if (!finalText.trim() && messages.length > historyMessages.length + 1) {
707
+ try {
708
+ const summaryResponse = await client.messages.create({
709
+ model: opts.model || 'claude-sonnet-4-6',
710
+ max_tokens: 1024,
711
+ system: systemPrompt,
712
+ messages: [...messages, { role: 'user', content: 'Summarize what you just did and the results.' }],
713
+ }, { signal: controller.signal });
714
+ finalText = summaryResponse.content.filter(b => b.type === 'text').map(b => b.text).join('');
715
+ } catch {}
716
+ }
717
+
718
+ const text = finalText || 'I completed the action but couldn\'t generate a summary.';
719
+
720
+ // Save assistant response (user message was already saved before calling Claude)
721
+ brain.insertChatMessage({ role: 'assistant', content: text, channel, session_id: sessionId });
722
+
723
+ brain.insertMemory({
724
+ source: 'wall-e-chat',
725
+ source_id: `chat-reply-${Date.now()}`,
726
+ memory_type: 'message_sent',
727
+ direction: 'outbound',
728
+ content: text,
729
+ timestamp: new Date().toISOString(),
730
+ metadata: JSON.stringify({ channel }),
731
+ });
732
+
733
+ return { reply: text };
734
+ } finally {
735
+ clearTimeout(timeout);
736
+ }
737
+ }
738
+
739
+ /**
740
+ * Build a client using the same Portkey-compatible approach as knowledge-extractor.
741
+ * getClient is not exported from knowledge-extractor, so we replicate the logic here.
742
+ */
743
+ function getClientForChat() {
744
+ const opts = buildClientOpts();
745
+ const isPortkey = opts.defaultHeaders && opts.defaultHeaders['x-portkey-api-key'];
746
+
747
+ if (isPortkey) {
748
+ return {
749
+ messages: {
750
+ async create(params, fetchOpts) {
751
+ const headers = {
752
+ 'content-type': 'application/json',
753
+ 'anthropic-version': '2023-06-01',
754
+ ...opts.defaultHeaders,
755
+ };
756
+ const res = await fetch(opts.baseURL + '/messages', {
757
+ method: 'POST',
758
+ headers,
759
+ body: JSON.stringify(params),
760
+ signal: fetchOpts?.signal,
761
+ });
762
+ if (!res.ok) {
763
+ const text = await res.text();
764
+ throw new Error(`${res.status} ${text}`);
765
+ }
766
+ return res.json();
767
+ }
768
+ }
769
+ };
770
+ }
771
+ return new Anthropic(opts);
772
+ }
773
+
774
+ // Allow injecting a custom client (for testing)
775
+ let _clientOverride = null;
776
+ function _setClient(client) { _clientOverride = client; }
777
+
778
+ module.exports = { chat, getClientForChat, _setClient };