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.
- package/bin/create-walle.js +134 -0
- package/package.json +18 -0
- package/template/.env.example +40 -0
- package/template/CLAUDE.md +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +167 -0
- package/template/bin/setup.js +100 -0
- package/template/claude-code-skill.md +60 -0
- package/template/claude-task-manager/api-prompts.js +1841 -0
- package/template/claude-task-manager/api-reviews.js +275 -0
- package/template/claude-task-manager/approval-agent.js +454 -0
- package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
- package/template/claude-task-manager/db.js +1721 -0
- package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
- package/template/claude-task-manager/git-utils.js +214 -0
- package/template/claude-task-manager/package-lock.json +1607 -0
- package/template/claude-task-manager/package.json +31 -0
- package/template/claude-task-manager/prompt-harvest.js +1148 -0
- package/template/claude-task-manager/public/css/prompts.css +880 -0
- package/template/claude-task-manager/public/css/reviews.css +430 -0
- package/template/claude-task-manager/public/css/walle.css +732 -0
- package/template/claude-task-manager/public/favicon.ico +0 -0
- package/template/claude-task-manager/public/icon.svg +37 -0
- package/template/claude-task-manager/public/index.html +8346 -0
- package/template/claude-task-manager/public/js/prompts.js +3159 -0
- package/template/claude-task-manager/public/js/reviews.js +1292 -0
- package/template/claude-task-manager/public/js/walle.js +3081 -0
- package/template/claude-task-manager/public/manifest.json +13 -0
- package/template/claude-task-manager/public/prompts.html +4353 -0
- package/template/claude-task-manager/public/setup.html +216 -0
- package/template/claude-task-manager/queue-engine.js +404 -0
- package/template/claude-task-manager/server-state.js +5 -0
- package/template/claude-task-manager/server.js +2254 -0
- package/template/claude-task-manager/session-utils.js +124 -0
- package/template/claude-task-manager/start.sh +17 -0
- package/template/claude-task-manager/tests/test-ai-search.js +61 -0
- package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
- package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
- package/template/claude-task-manager/tests/test-features-v2.js +127 -0
- package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
- package/template/claude-task-manager/tests/test-insights.js +124 -0
- package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
- package/template/claude-task-manager/tests/test-permissions.js +122 -0
- package/template/claude-task-manager/tests/test-pin.js +51 -0
- package/template/claude-task-manager/tests/test-prompts.js +164 -0
- package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
- package/template/claude-task-manager/tests/test-review.js +104 -0
- package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
- package/template/claude-task-manager/tests/test-send-final.js +30 -0
- package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
- package/template/claude-task-manager/tests/test-send-integration.js +107 -0
- package/template/claude-task-manager/tests/test-send-visual.js +34 -0
- package/template/claude-task-manager/tests/test-session-create.js +147 -0
- package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
- package/template/claude-task-manager/tests/test-url-hash.js +68 -0
- package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
- package/template/claude-task-manager/tests/test-ux-review.js +130 -0
- package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
- package/template/claude-task-manager/tests/test-zoom.js +92 -0
- package/template/claude-task-manager/tests/test-zoom2.js +67 -0
- package/template/docs/site/api/README.md +187 -0
- package/template/docs/site/guides/claude-code.md +58 -0
- package/template/docs/site/guides/configuration.md +96 -0
- package/template/docs/site/guides/quickstart.md +158 -0
- package/template/docs/site/index.md +14 -0
- package/template/docs/site/skills/README.md +135 -0
- package/template/wall-e/.dockerignore +11 -0
- package/template/wall-e/Dockerfile +25 -0
- package/template/wall-e/adapters/adapter-base.js +37 -0
- package/template/wall-e/adapters/ctm.js +193 -0
- package/template/wall-e/adapters/slack.js +56 -0
- package/template/wall-e/agent.js +319 -0
- package/template/wall-e/api-walle.js +1073 -0
- package/template/wall-e/brain.js +1235 -0
- package/template/wall-e/channels/agent-api.js +172 -0
- package/template/wall-e/channels/channel-base.js +14 -0
- package/template/wall-e/channels/imessage-channel.js +113 -0
- package/template/wall-e/channels/slack-channel.js +118 -0
- package/template/wall-e/chat.js +778 -0
- package/template/wall-e/decision/confidence.js +93 -0
- package/template/wall-e/deploy.sh +35 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
- package/template/wall-e/extraction/contradiction.js +168 -0
- package/template/wall-e/extraction/knowledge-extractor.js +190 -0
- package/template/wall-e/fly.toml +24 -0
- package/template/wall-e/loops/ingest.js +34 -0
- package/template/wall-e/loops/reflect.js +63 -0
- package/template/wall-e/loops/tasks.js +487 -0
- package/template/wall-e/loops/think.js +125 -0
- package/template/wall-e/package-lock.json +533 -0
- package/template/wall-e/package.json +18 -0
- package/template/wall-e/scripts/ingest-slack-search.js +85 -0
- package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
- package/template/wall-e/scripts/slack-backfill.js +295 -0
- package/template/wall-e/scripts/slack-channel-history.js +454 -0
- package/template/wall-e/server.js +93 -0
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
- package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
- package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
- package/template/wall-e/skills/claude-code-reader.js +144 -0
- package/template/wall-e/skills/mcp-client.js +407 -0
- package/template/wall-e/skills/skill-executor.js +163 -0
- package/template/wall-e/skills/skill-loader.js +410 -0
- package/template/wall-e/skills/skill-planner.js +88 -0
- package/template/wall-e/skills/slack-ingest.js +329 -0
- package/template/wall-e/skills/slack-pull-live.js +270 -0
- package/template/wall-e/skills/tool-executor.js +188 -0
- package/template/wall-e/tests/adapter-base.test.js +20 -0
- package/template/wall-e/tests/adapter-ctm.test.js +122 -0
- package/template/wall-e/tests/adapter-slack.test.js +98 -0
- package/template/wall-e/tests/agent-api.test.js +256 -0
- package/template/wall-e/tests/api-walle.test.js +222 -0
- package/template/wall-e/tests/brain.test.js +602 -0
- package/template/wall-e/tests/channels.test.js +104 -0
- package/template/wall-e/tests/chat.test.js +103 -0
- package/template/wall-e/tests/confidence.test.js +134 -0
- package/template/wall-e/tests/contradiction.test.js +217 -0
- package/template/wall-e/tests/ingest.test.js +113 -0
- package/template/wall-e/tests/mcp-client.test.js +71 -0
- package/template/wall-e/tests/reflect.test.js +103 -0
- package/template/wall-e/tests/server.test.js +111 -0
- package/template/wall-e/tests/skills.test.js +198 -0
- package/template/wall-e/tests/slack-ingest.test.js +103 -0
- package/template/wall-e/tests/think.test.js +435 -0
- package/template/wall-e/tools/local-tools.js +697 -0
- 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¤t=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 };
|