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,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* google-calendar sync skill — reads events from macOS Calendar (which syncs
|
|
5
|
+
* with Google Calendar) via a compiled Swift binary using EventKit, then
|
|
6
|
+
* stores them as memories in WALL-E's brain.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node run.js [--days-back N] [--days-ahead N]
|
|
9
|
+
*
|
|
10
|
+
* Requires: Calendar access granted to the terminal in
|
|
11
|
+
* System Settings > Privacy & Security > Calendars
|
|
12
|
+
*/
|
|
13
|
+
const { execFileSync } = require('child_process');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
|
|
17
|
+
const SKILL_DIR = __dirname;
|
|
18
|
+
const SWIFT_SRC = path.join(SKILL_DIR, 'cal-reader.swift');
|
|
19
|
+
const BINARY = path.join(SKILL_DIR, 'cal-reader');
|
|
20
|
+
|
|
21
|
+
// Parse CLI args
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
let daysBack = 1;
|
|
24
|
+
let daysAhead = 14;
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
if (args[i] === '--days-back' && args[i + 1]) daysBack = parseInt(args[i + 1], 10);
|
|
27
|
+
if (args[i] === '--days-ahead' && args[i + 1]) daysAhead = parseInt(args[i + 1], 10);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Step 1: Compile Swift binary if needed ───────────────────────────
|
|
31
|
+
|
|
32
|
+
function ensureBinary() {
|
|
33
|
+
const srcStat = fs.statSync(SWIFT_SRC);
|
|
34
|
+
let needCompile = true;
|
|
35
|
+
if (fs.existsSync(BINARY)) {
|
|
36
|
+
const binStat = fs.statSync(BINARY);
|
|
37
|
+
needCompile = srcStat.mtimeMs > binStat.mtimeMs;
|
|
38
|
+
}
|
|
39
|
+
if (needCompile) {
|
|
40
|
+
console.error('[calendar-sync] Compiling cal-reader.swift...');
|
|
41
|
+
execFileSync('swiftc', ['-O', SWIFT_SRC, '-o', BINARY, '-framework', 'EventKit'], {
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
|
+
timeout: 120_000,
|
|
44
|
+
});
|
|
45
|
+
console.error('[calendar-sync] Compiled successfully');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Step 2: Read events from macOS Calendar ──────────────────────────
|
|
50
|
+
|
|
51
|
+
function readEvents() {
|
|
52
|
+
const result = execFileSync(BINARY, [
|
|
53
|
+
'--days-back', String(daysBack),
|
|
54
|
+
'--days-ahead', String(daysAhead),
|
|
55
|
+
], { timeout: 30_000, maxBuffer: 10 * 1024 * 1024 });
|
|
56
|
+
|
|
57
|
+
const events = JSON.parse(result.toString('utf8'));
|
|
58
|
+
console.error(`[calendar-sync] Read ${events.length} events (${daysBack}d back, ${daysAhead}d ahead)`);
|
|
59
|
+
return events;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Step 3: Store in brain ───────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function storeToBrain(events) {
|
|
65
|
+
const brainPath = path.resolve(SKILL_DIR, '..', '..', '..', 'brain.js');
|
|
66
|
+
if (!fs.existsSync(brainPath)) {
|
|
67
|
+
console.error(`[calendar-sync] brain.js not found at ${brainPath}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const brain = require(brainPath);
|
|
71
|
+
brain.initDb();
|
|
72
|
+
|
|
73
|
+
let inserted = 0;
|
|
74
|
+
let skipped = 0;
|
|
75
|
+
let updated = 0;
|
|
76
|
+
|
|
77
|
+
for (const evt of events) {
|
|
78
|
+
// Build a readable content string
|
|
79
|
+
const parts = [evt.title];
|
|
80
|
+
if (evt.location) parts.push(`Location: ${evt.location}`);
|
|
81
|
+
if (evt.attendees && evt.attendees.length > 0) {
|
|
82
|
+
const names = evt.attendees.map(a => a.name || a.email).filter(Boolean);
|
|
83
|
+
if (names.length > 0) parts.push(`Attendees: ${names.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
if (evt.organizer) parts.push(`Organizer: ${evt.organizer}`);
|
|
86
|
+
if (evt.notes) parts.push(`Notes: ${evt.notes}`);
|
|
87
|
+
const content = parts.join('\n');
|
|
88
|
+
|
|
89
|
+
// Use uid + start as source_id for dedup
|
|
90
|
+
const sourceId = `cal:${evt.uid}:${evt.start}`;
|
|
91
|
+
|
|
92
|
+
// Check if this exact event already exists
|
|
93
|
+
const existing = brain.getDb().prepare(
|
|
94
|
+
'SELECT id, content FROM memories WHERE source = ? AND source_id = ?'
|
|
95
|
+
).get('calendar', sourceId);
|
|
96
|
+
|
|
97
|
+
if (existing) {
|
|
98
|
+
// Update if content changed (attendees added, notes updated, etc.)
|
|
99
|
+
if (existing.content !== content) {
|
|
100
|
+
brain.getDb().prepare(
|
|
101
|
+
'UPDATE memories SET content = ?, metadata = ?, timestamp = ? WHERE id = ?'
|
|
102
|
+
).run(content, JSON.stringify(evt), evt.start, existing.id);
|
|
103
|
+
updated++;
|
|
104
|
+
} else {
|
|
105
|
+
skipped++;
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Insert new event
|
|
111
|
+
const mem = {
|
|
112
|
+
source: 'calendar',
|
|
113
|
+
source_id: sourceId,
|
|
114
|
+
source_channel: evt.calendar,
|
|
115
|
+
memory_type: evt.allDay ? 'calendar_allday' : 'calendar_event',
|
|
116
|
+
subject: evt.title,
|
|
117
|
+
content,
|
|
118
|
+
metadata: JSON.stringify(evt),
|
|
119
|
+
importance: evt.allDay ? 0.3 : 0.5,
|
|
120
|
+
timestamp: evt.start,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = brain.insertMemory(mem);
|
|
124
|
+
if (result) inserted++;
|
|
125
|
+
else skipped++;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
brain.closeDb();
|
|
129
|
+
return { inserted, skipped, updated, total: events.length };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Main ─────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
function openCalendarPrivacySettings() {
|
|
135
|
+
try {
|
|
136
|
+
execFileSync('open', ['x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars'], {
|
|
137
|
+
timeout: 5_000,
|
|
138
|
+
});
|
|
139
|
+
console.error('[calendar-sync] Opened System Settings > Privacy & Security > Calendars');
|
|
140
|
+
} catch {
|
|
141
|
+
console.error('[calendar-sync] Could not open System Settings automatically.');
|
|
142
|
+
console.error('[calendar-sync] Please open: System Settings > Privacy & Security > Calendars');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sleep(ms) {
|
|
147
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function main() {
|
|
151
|
+
ensureBinary();
|
|
152
|
+
|
|
153
|
+
const MAX_RETRIES = 2;
|
|
154
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
155
|
+
try {
|
|
156
|
+
const events = readEvents();
|
|
157
|
+
const stats = storeToBrain(events);
|
|
158
|
+
console.log(JSON.stringify(stats));
|
|
159
|
+
console.error(`[calendar-sync] Done: ${stats.inserted} new, ${stats.updated} updated, ${stats.skipped} unchanged`);
|
|
160
|
+
return;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
const isAccessDenied = err.stderr && err.stderr.toString().includes('access_denied');
|
|
163
|
+
if (isAccessDenied && attempt < MAX_RETRIES) {
|
|
164
|
+
console.error(`[calendar-sync] Calendar access denied (attempt ${attempt + 1}/${MAX_RETRIES + 1}).`);
|
|
165
|
+
console.error('[calendar-sync] Opening System Settings — please grant Calendar access to your terminal app, then wait...');
|
|
166
|
+
openCalendarPrivacySettings();
|
|
167
|
+
await sleep(15_000); // wait 15s for user to toggle the checkbox
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (isAccessDenied) {
|
|
171
|
+
console.error('[calendar-sync] Calendar access still denied after retries.');
|
|
172
|
+
console.error('[calendar-sync] Grant access to your terminal in System Settings > Privacy & Security > Calendars');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
console.error(`[calendar-sync] Error: ${err.message}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-search
|
|
3
|
+
description: >
|
|
4
|
+
Search and summarize memories from WALL-E's brain. Performs multi-query
|
|
5
|
+
searches across Slack messages, chat history, and knowledge base.
|
|
6
|
+
Use for research, finding past conversations, and building context.
|
|
7
|
+
version: 1.0.0
|
|
8
|
+
author: juncao
|
|
9
|
+
execution: agent
|
|
10
|
+
trigger:
|
|
11
|
+
type: manual
|
|
12
|
+
config:
|
|
13
|
+
query:
|
|
14
|
+
type: string
|
|
15
|
+
default: ""
|
|
16
|
+
description: "Search query or topic to research"
|
|
17
|
+
source:
|
|
18
|
+
type: string
|
|
19
|
+
enum: [all, slack, ctm, wall-e-chat]
|
|
20
|
+
default: all
|
|
21
|
+
description: "Filter to a specific memory source"
|
|
22
|
+
depth:
|
|
23
|
+
type: string
|
|
24
|
+
enum: [quick, deep]
|
|
25
|
+
default: quick
|
|
26
|
+
description: "quick = 1-2 searches, deep = exhaustive multi-query"
|
|
27
|
+
tags: [search, memory, research, knowledge, context]
|
|
28
|
+
permissions:
|
|
29
|
+
- brain:read
|
|
30
|
+
---
|
|
31
|
+
# Memory Search & Summarize
|
|
32
|
+
|
|
33
|
+
## Your Goal
|
|
34
|
+
|
|
35
|
+
Search WALL-E's brain for information on a given topic and produce a clear,
|
|
36
|
+
organized summary of what was found. This skill is the foundation for
|
|
37
|
+
answering questions about past conversations, decisions, and context.
|
|
38
|
+
|
|
39
|
+
## Steps
|
|
40
|
+
|
|
41
|
+
### 1. Understand the Query
|
|
42
|
+
Parse the task description or config query to understand what information
|
|
43
|
+
is being sought. Identify:
|
|
44
|
+
- Key people involved
|
|
45
|
+
- Time period of interest
|
|
46
|
+
- Specific channels or topics
|
|
47
|
+
- Whether this is a factual lookup or a pattern/trend analysis
|
|
48
|
+
|
|
49
|
+
### 2. Execute Searches
|
|
50
|
+
Use `search_memories` with MULTIPLE queries in the SAME turn:
|
|
51
|
+
|
|
52
|
+
**For people-related queries:**
|
|
53
|
+
- Search by person's name
|
|
54
|
+
- Search by their common topics or projects
|
|
55
|
+
- Search in Slack specifically (`source: "slack"`)
|
|
56
|
+
|
|
57
|
+
**For topic-related queries:**
|
|
58
|
+
- Search the topic directly
|
|
59
|
+
- Search related/synonym terms
|
|
60
|
+
- Search in both English and Chinese (the owner uses both)
|
|
61
|
+
|
|
62
|
+
**For time-based queries:**
|
|
63
|
+
- Search with date-range keywords
|
|
64
|
+
- Search for events or milestones around that time
|
|
65
|
+
|
|
66
|
+
### 3. Analyze & Cross-Reference
|
|
67
|
+
Use the `think` tool to:
|
|
68
|
+
- Identify patterns across search results
|
|
69
|
+
- Note contradictions or evolving opinions
|
|
70
|
+
- Distinguish between the owner's views and others' views
|
|
71
|
+
- Consider what might be missing from the results
|
|
72
|
+
|
|
73
|
+
### 4. Synthesize
|
|
74
|
+
Produce a structured summary:
|
|
75
|
+
|
|
76
|
+
**Key Findings**
|
|
77
|
+
- Most important discoveries, with dates and people cited
|
|
78
|
+
|
|
79
|
+
**Evidence**
|
|
80
|
+
- Direct quotes from Slack messages (use > blockquotes)
|
|
81
|
+
- Dates and channels for context
|
|
82
|
+
|
|
83
|
+
**Gaps**
|
|
84
|
+
- What information is missing or ambiguous
|
|
85
|
+
- Suggested follow-up queries
|
|
86
|
+
|
|
87
|
+
## Guidelines
|
|
88
|
+
|
|
89
|
+
- Always cite sources: include timestamps and channel names
|
|
90
|
+
- Translate Chinese content -- it often contains the most candid opinions
|
|
91
|
+
- Distinguish between what the owner SAID vs what others SAID ABOUT/TO them
|
|
92
|
+
- When evidence is thin, say so explicitly rather than speculating
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: morning-briefing
|
|
3
|
+
description: >
|
|
4
|
+
Generate a morning briefing for the day ahead. Summarizes overnight Slack
|
|
5
|
+
activity, upcoming calendar events, pending tasks, and items needing
|
|
6
|
+
attention. Use for daily meeting prep and situational awareness.
|
|
7
|
+
version: 2.0.0
|
|
8
|
+
author: juncao
|
|
9
|
+
execution: script
|
|
10
|
+
entry: run.js
|
|
11
|
+
trigger:
|
|
12
|
+
type: interval
|
|
13
|
+
schedule: "daily at 7am"
|
|
14
|
+
tags: [briefing, morning, daily, summary, calendar, slack]
|
|
15
|
+
permissions:
|
|
16
|
+
- brain:read
|
|
17
|
+
- calendar:read
|
|
18
|
+
---
|
|
19
|
+
# Morning Briefing
|
|
20
|
+
|
|
21
|
+
## Your Goal
|
|
22
|
+
|
|
23
|
+
Generate a concise, actionable morning briefing for the owner. This should take less than 2 minutes to read and highlight what needs attention TODAY.
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
### 1. Check Calendar
|
|
28
|
+
Use the `calendar_events` tool to get today's meetings and events. Note:
|
|
29
|
+
- Meeting times and attendees
|
|
30
|
+
- Back-to-back meetings that need prep
|
|
31
|
+
- Any all-day events or deadlines
|
|
32
|
+
|
|
33
|
+
### 2. Scan Overnight Slack Activity (USE BRAIN, NOT LIVE SLACK)
|
|
34
|
+
**IMPORTANT**: All Slack data is already synced into WALL-E's brain. Use `search_memories`
|
|
35
|
+
with `source: "slack"` — do NOT use `slack_search` or any live Slack API calls. The brain
|
|
36
|
+
has 20,000+ Slack messages already indexed and searchable via FTS5.
|
|
37
|
+
|
|
38
|
+
Run these searches IN PARALLEL (all in one turn):
|
|
39
|
+
- `search_memories({query: "<owner name> mention", source: "slack", limit: 20})` — recent mentions
|
|
40
|
+
- `search_memories({query: "<key project or topic>", source: "slack", limit: 15})` — active threads
|
|
41
|
+
- `search_memories({source: "slack", limit: 30})` — latest messages across all channels
|
|
42
|
+
|
|
43
|
+
Look for:
|
|
44
|
+
- Direct mentions or questions directed at the owner
|
|
45
|
+
- Active threads that need responses
|
|
46
|
+
- Decisions made overnight that the owner should know about
|
|
47
|
+
- Anything time-sensitive or blocking
|
|
48
|
+
|
|
49
|
+
### 3. Check Pending Tasks
|
|
50
|
+
Use `list_tasks` to see what's pending or overdue.
|
|
51
|
+
|
|
52
|
+
### 4. Review Pending Questions
|
|
53
|
+
Check if there are any pending questions from WALL-E that need the owner's input.
|
|
54
|
+
|
|
55
|
+
## Output Format
|
|
56
|
+
|
|
57
|
+
Structure the briefing as:
|
|
58
|
+
|
|
59
|
+
### Today's Schedule
|
|
60
|
+
- List meetings chronologically with times and key attendees
|
|
61
|
+
|
|
62
|
+
### Overnight Activity
|
|
63
|
+
- Summarize key Slack messages and threads needing attention
|
|
64
|
+
- Highlight any urgent or time-sensitive items
|
|
65
|
+
|
|
66
|
+
### Action Items
|
|
67
|
+
A table of SPECIFIC, CONCRETE things the owner needs to do. Each item must have:
|
|
68
|
+
- A clear next action (not just a topic — what exactly should they do?)
|
|
69
|
+
- Who they need to talk to or respond to
|
|
70
|
+
- A real deadline or reason for urgency
|
|
71
|
+
|
|
72
|
+
| Action | Who | By When | Why |
|
|
73
|
+
|--------|-----|---------|-----|
|
|
74
|
+
| Reply to Alice's question about API migration in #eng-platform | Alice | Today | She's blocked waiting for your input |
|
|
75
|
+
| Review PR #456 (database schema change) | Bob | Today | Merge deadline is EOD |
|
|
76
|
+
| Prep talking points for 1:1 with VP at 2pm | Sarah | Before 2pm | Weekly sync, she raised perf concerns last week |
|
|
77
|
+
|
|
78
|
+
**Rules for action items:**
|
|
79
|
+
- NO vague items like "Follow up on project X" or "Monitor situation Y"
|
|
80
|
+
- NO items that are just FYI — those go in Overnight Activity
|
|
81
|
+
- Every item must answer: "What exactly should I DO, and WHY now?"
|
|
82
|
+
- If there's nothing actionable, say "No action items — clean slate today"
|
|
83
|
+
- Max 5 items. If more exist, prioritize ruthlessly.
|
|
84
|
+
|
|
85
|
+
### Quick Stats
|
|
86
|
+
- Pending tasks count
|
|
87
|
+
- Any system alerts (e.g., Slack auth expiry, skill failures)
|
|
88
|
+
|
|
89
|
+
## Structured Items Block (REQUIRED)
|
|
90
|
+
|
|
91
|
+
After the markdown briefing, you MUST include a structured JSON block with all
|
|
92
|
+
action items. This enables the UI to render interactive action buttons. Use this exact format:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
<!-- BRIEFING_ITEMS
|
|
96
|
+
[
|
|
97
|
+
{
|
|
98
|
+
"title": "Reply to Alice re: API migration — she's blocked",
|
|
99
|
+
"action": "Reply in #eng-platform thread or DM Alice",
|
|
100
|
+
"category": "comms|review|meeting-prep|decision|deliverable|other",
|
|
101
|
+
"who": "Alice Chen",
|
|
102
|
+
"urgency": "critical|today|this_week",
|
|
103
|
+
"why": "She's blocked on the schema decision and asked you directly yesterday",
|
|
104
|
+
"context": {
|
|
105
|
+
"search_queries": ["Alice API migration schema"],
|
|
106
|
+
"people": ["Alice Chen"],
|
|
107
|
+
"channels": ["eng-platform"]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
-->
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Rules for BRIEFING_ITEMS:**
|
|
115
|
+
- `title` must be a specific action, not a topic. "Reply to X" not "API migration"
|
|
116
|
+
- `action` is what exactly to do (reply, review, prep, decide)
|
|
117
|
+
- `why` explains urgency — why today and not tomorrow?
|
|
118
|
+
- `who` is the specific person involved
|
|
119
|
+
- Do NOT include async/FYI items — only things requiring action
|
|
120
|
+
- Max 5 items
|
|
121
|
+
|
|
122
|
+
Urgency levels:
|
|
123
|
+
- `critical` = must act right now, someone is blocked
|
|
124
|
+
- `today` = must handle today, has a deadline or meeting
|
|
125
|
+
- `this_week` = due this week but not today
|
|
126
|
+
|
|
127
|
+
## Tone
|
|
128
|
+
|
|
129
|
+
Write like a sharp executive assistant briefing their boss. Be direct,
|
|
130
|
+
prioritize ruthlessly, and flag anything that looks urgent or time-sensitive.
|
|
131
|
+
Skip the pleasantries -- get straight to what matters.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* Morning Briefing — pure script, no Claude API calls.
|
|
5
|
+
*
|
|
6
|
+
* 1. Calendar events (today) via AppleScript
|
|
7
|
+
* 2. Recent Slack activity via brain FTS5 search
|
|
8
|
+
* 3. Pending tasks from brain
|
|
9
|
+
* 4. Pending questions from brain
|
|
10
|
+
*
|
|
11
|
+
* Outputs markdown to stdout. The task runner stores it as the task result.
|
|
12
|
+
*/
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const brain = require(path.resolve(__dirname, '..', '..', '..', 'brain'));
|
|
15
|
+
const { getCalendarEvents } = require(path.resolve(__dirname, '..', '..', '..', 'tools', 'local-tools'));
|
|
16
|
+
|
|
17
|
+
brain.initDb();
|
|
18
|
+
const db = brain.getDb();
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const now = new Date();
|
|
22
|
+
const todayStr = now.toISOString().slice(0, 10);
|
|
23
|
+
const sections = [];
|
|
24
|
+
|
|
25
|
+
// ── 1. Today's Schedule ──
|
|
26
|
+
let calSection = "## Today's Schedule\n";
|
|
27
|
+
try {
|
|
28
|
+
const { events } = await getCalendarEvents({ days_ahead: 0 });
|
|
29
|
+
// Filter to today only
|
|
30
|
+
const todayEvents = events.filter(e => {
|
|
31
|
+
if (e.allDay) return true;
|
|
32
|
+
const eDate = new Date(e.start);
|
|
33
|
+
return !isNaN(eDate) && eDate.toISOString().slice(0, 10) === todayStr;
|
|
34
|
+
});
|
|
35
|
+
// De-duplicate by title + time
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const deduped = todayEvents.filter(e => {
|
|
38
|
+
const key = `${e.title}|${formatTime(e.start)}`;
|
|
39
|
+
if (seen.has(key)) return false;
|
|
40
|
+
seen.add(key);
|
|
41
|
+
return true;
|
|
42
|
+
});
|
|
43
|
+
if (deduped.length === 0) {
|
|
44
|
+
calSection += '_No meetings today._\n';
|
|
45
|
+
} else {
|
|
46
|
+
for (const evt of deduped) {
|
|
47
|
+
const time = evt.allDay ? 'All day' : formatTime(evt.start);
|
|
48
|
+
// Show first names only for attendees, skip owner
|
|
49
|
+
const ownerEmail = (process.env.WALLE_OWNER_EMAIL || '').toLowerCase().trim();
|
|
50
|
+
const ownerPrefix = ownerEmail.includes('@') ? ownerEmail.split('@')[0] : '';
|
|
51
|
+
const filteredAttendees = evt.attendees
|
|
52
|
+
.filter(a => !ownerPrefix || !a.toLowerCase().includes(ownerPrefix))
|
|
53
|
+
.map(a => a.split('@')[0].split('.').filter(Boolean).map(s => s[0].toUpperCase() + s.slice(1)).join(' '));
|
|
54
|
+
const attendees = filteredAttendees.slice(0, 4);
|
|
55
|
+
const overflow = filteredAttendees.length > 4 ? ` +${filteredAttendees.length - 4}` : '';
|
|
56
|
+
const attendeeStr = attendees.length > 0 ? ` — ${attendees.join(', ')}${overflow}` : '';
|
|
57
|
+
calSection += `| **${time}** | ${evt.title}${attendeeStr} |\n`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
calSection += `_Calendar unavailable: ${err.message}_\n`;
|
|
62
|
+
}
|
|
63
|
+
sections.push(calSection);
|
|
64
|
+
|
|
65
|
+
// ── 2. Overnight Slack Activity (from brain) ──
|
|
66
|
+
const hoursBack = 18;
|
|
67
|
+
const cutoff = new Date(now.getTime() - hoursBack * 3600000).toISOString();
|
|
68
|
+
|
|
69
|
+
const recentInbound = db.prepare(`
|
|
70
|
+
SELECT content, source_channel, participants, timestamp
|
|
71
|
+
FROM memories
|
|
72
|
+
WHERE source = 'slack' AND direction = 'inbound'
|
|
73
|
+
AND timestamp > ?
|
|
74
|
+
ORDER BY timestamp DESC
|
|
75
|
+
LIMIT 30
|
|
76
|
+
`).all(cutoff);
|
|
77
|
+
|
|
78
|
+
const recentOutbound = db.prepare(`
|
|
79
|
+
SELECT content, source_channel, timestamp
|
|
80
|
+
FROM memories
|
|
81
|
+
WHERE source = 'slack' AND direction = 'outbound'
|
|
82
|
+
AND timestamp > ?
|
|
83
|
+
ORDER BY timestamp DESC
|
|
84
|
+
LIMIT 10
|
|
85
|
+
`).all(cutoff);
|
|
86
|
+
|
|
87
|
+
let slackSection = '## Overnight Slack\n';
|
|
88
|
+
|
|
89
|
+
if (recentInbound.length === 0 && recentOutbound.length === 0) {
|
|
90
|
+
slackSection += '_No Slack activity in the last 18 hours._\n';
|
|
91
|
+
} else {
|
|
92
|
+
// Group by channel, resolve channel names
|
|
93
|
+
const byChannel = {};
|
|
94
|
+
for (const msg of recentInbound) {
|
|
95
|
+
const ch = msg.source_channel || 'DM';
|
|
96
|
+
if (!byChannel[ch]) byChannel[ch] = [];
|
|
97
|
+
byChannel[ch].push(msg);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Sort channels: most active first, top 6
|
|
101
|
+
const channels = Object.entries(byChannel)
|
|
102
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
103
|
+
.slice(0, 6);
|
|
104
|
+
|
|
105
|
+
for (const [channel, msgs] of channels) {
|
|
106
|
+
const chName = resolveChannelName(channel);
|
|
107
|
+
slackSection += `\n**${chName}** · ${msgs.length} message${msgs.length > 1 ? 's' : ''}\n`;
|
|
108
|
+
// Show up to 3 most recent, condensed
|
|
109
|
+
for (const msg of msgs.slice(0, 3)) {
|
|
110
|
+
const time = formatTime(msg.timestamp);
|
|
111
|
+
const who = extractFirstName(msg.participants);
|
|
112
|
+
const preview = condenseLine(msg.content, 120);
|
|
113
|
+
if (who) {
|
|
114
|
+
slackSection += `> **${who}** _(${time})_ — ${preview}\n`;
|
|
115
|
+
} else {
|
|
116
|
+
slackSection += `> _(${time})_ ${preview}\n`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (msgs.length > 3) {
|
|
120
|
+
slackSection += `> _+${msgs.length - 3} more_\n`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (recentOutbound.length > 0) {
|
|
125
|
+
slackSection += `\n_You sent ${recentOutbound.length} messages in the last ${hoursBack}h._\n`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
sections.push(slackSection);
|
|
129
|
+
|
|
130
|
+
// ── 3. Mentions & Urgent ──
|
|
131
|
+
let mentionSection = '';
|
|
132
|
+
try {
|
|
133
|
+
const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
|
|
134
|
+
if (hasFts) {
|
|
135
|
+
const ownerFirst = (process.env.WALLE_OWNER_NAME || 'owner').split(' ')[0]
|
|
136
|
+
.replace(/['"*()]/g, ''); // sanitize FTS5 special chars
|
|
137
|
+
const matchExpr = `${ownerFirst} OR urgent OR blocked OR ASAP`;
|
|
138
|
+
const mentions = db.prepare(`
|
|
139
|
+
SELECT m.content, m.source_channel, m.participants, m.timestamp
|
|
140
|
+
FROM memories_fts f
|
|
141
|
+
JOIN memories m ON m.rowid = f.rowid
|
|
142
|
+
WHERE memories_fts MATCH ?
|
|
143
|
+
AND m.source = 'slack' AND m.timestamp > ?
|
|
144
|
+
ORDER BY m.timestamp DESC
|
|
145
|
+
LIMIT 5
|
|
146
|
+
`).all(matchExpr, cutoff);
|
|
147
|
+
|
|
148
|
+
if (mentions.length > 0) {
|
|
149
|
+
mentionSection = '\n## Mentions & Urgent\n';
|
|
150
|
+
for (const m of mentions) {
|
|
151
|
+
const time = formatTime(m.timestamp);
|
|
152
|
+
const ch = resolveChannelName(m.source_channel);
|
|
153
|
+
const who = extractFirstName(m.participants);
|
|
154
|
+
const preview = condenseLine(m.content, 140);
|
|
155
|
+
mentionSection += `> ${who ? `**${who}**` : ch} _(${time})_ — ${preview}\n`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch {}
|
|
160
|
+
if (mentionSection) sections.push(mentionSection);
|
|
161
|
+
|
|
162
|
+
// ── 4. Pending Tasks ──
|
|
163
|
+
const tasks = brain.listTasks({ status: 'pending', limit: 10 });
|
|
164
|
+
const failedTasks = brain.listTasks({ status: 'failed', limit: 5 });
|
|
165
|
+
if (tasks.length > 0 || failedTasks.length > 0) {
|
|
166
|
+
let taskSection = '## Tasks\n';
|
|
167
|
+
if (tasks.length > 0) {
|
|
168
|
+
taskSection += '| Task | Priority | Due |\n|------|----------|-----|\n';
|
|
169
|
+
for (const t of tasks) {
|
|
170
|
+
const due = t.due_at ? t.due_at.slice(0, 10) : '—';
|
|
171
|
+
const pri = t.priority === 'urgent' ? '🔴 urgent' : t.priority === 'high' ? '🟡 high' : '—';
|
|
172
|
+
taskSection += `| ${t.title} | ${pri} | ${due} |\n`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (failedTasks.length > 0) {
|
|
176
|
+
taskSection += '\n**Failed:**\n';
|
|
177
|
+
for (const t of failedTasks) {
|
|
178
|
+
taskSection += `- ❌ ${t.title} — _${condenseLine(t.error || 'unknown', 60)}_\n`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
sections.push(taskSection);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── 5. Pending Questions ──
|
|
185
|
+
const questions = brain.listQuestions({ status: 'pending', limit: 5 });
|
|
186
|
+
if (questions.length > 0) {
|
|
187
|
+
let qSection = '## Questions for You\n';
|
|
188
|
+
for (const q of questions) {
|
|
189
|
+
qSection += `- **${q.question_type}**: ${q.question}\n`;
|
|
190
|
+
}
|
|
191
|
+
sections.push(qSection);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── 6. Quick Stats ──
|
|
195
|
+
const stats = brain.getBrainStats();
|
|
196
|
+
const totalSlack = db.prepare("SELECT count(*) as c FROM memories WHERE source = 'slack'").get().c;
|
|
197
|
+
let statsSection = '---\n';
|
|
198
|
+
statsSection += `📊 ${recentInbound.length} inbound · ${tasks.length} pending · ${questions.length} questions · ${stats.memory_count.toLocaleString()} memories · ${totalSlack.toLocaleString()} Slack\n`;
|
|
199
|
+
sections.push(statsSection);
|
|
200
|
+
|
|
201
|
+
// ── Output ──
|
|
202
|
+
const output = `# Morning Briefing — ${todayStr}\n\n${sections.join('\n')}`;
|
|
203
|
+
console.log(output);
|
|
204
|
+
|
|
205
|
+
brain.closeDb();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Helpers ──
|
|
209
|
+
|
|
210
|
+
function formatTime(dateStr) {
|
|
211
|
+
if (!dateStr) return '??:??';
|
|
212
|
+
try {
|
|
213
|
+
const d = new Date(dateStr);
|
|
214
|
+
if (isNaN(d.getTime())) {
|
|
215
|
+
const match = dateStr.match(/(\d{1,2}:\d{2}(?::\d{2})?\s*(?:AM|PM)?)/i);
|
|
216
|
+
return match ? match[1] : dateStr.slice(0, 16);
|
|
217
|
+
}
|
|
218
|
+
return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
|
219
|
+
} catch {
|
|
220
|
+
return dateStr.slice(0, 16);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Resolve Slack channel IDs to readable names
|
|
225
|
+
const CHANNEL_MAP = {};
|
|
226
|
+
function resolveChannelName(channelId) {
|
|
227
|
+
if (!channelId) return '#unknown';
|
|
228
|
+
// If it looks like a readable name already (not all-caps+digits), use it
|
|
229
|
+
if (!channelId.match(/^[A-Z][A-Z0-9]{7,}$/)) return `#${channelId}`;
|
|
230
|
+
// Check cache
|
|
231
|
+
if (CHANNEL_MAP[channelId]) return CHANNEL_MAP[channelId];
|
|
232
|
+
// Fallback: shortened ID
|
|
233
|
+
CHANNEL_MAP[channelId] = `#${channelId.slice(0, 6)}…`;
|
|
234
|
+
return CHANNEL_MAP[channelId];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function extractFirstName(participants) {
|
|
238
|
+
if (!participants) return '';
|
|
239
|
+
// "Derek Holevinsky" → "Derek", "Yu Tan" → "Yu"
|
|
240
|
+
const name = participants.split(',')[0].trim().replace(/^\*+|\*+$/g, '');
|
|
241
|
+
return name.split(' ')[0] || name;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function condenseLine(text, maxLen) {
|
|
245
|
+
if (!text) return '';
|
|
246
|
+
let oneLine = text.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
247
|
+
// Clean Slack formatting
|
|
248
|
+
oneLine = oneLine
|
|
249
|
+
.replace(/<@[A-Z0-9]+\|([^>]+)>/g, '@$1') // <@U123|Name> → @Name
|
|
250
|
+
.replace(/<@[A-Z0-9]+>/g, '@someone') // <@U123> → @someone
|
|
251
|
+
.replace(/<(https?:\/\/[^|>]+)\|([^>]+)>/g, '$2') // <url|label> → label
|
|
252
|
+
.replace(/<(https?:\/\/[^>]+)>/g, '[link]') // <url> → [link]
|
|
253
|
+
.replace(/:[a-z_-]+:/g, '') // :emoji: → remove
|
|
254
|
+
.replace(/<!(?:here|channel|everyone)>/g, '@here') // <!here> → @here
|
|
255
|
+
.replace(/\*([^*\n]+)\*/g, '$1') // *bold* → bold (avoid markdown clash)
|
|
256
|
+
.replace(/\s+/g, ' ').trim();
|
|
257
|
+
if (oneLine.length <= maxLen) return oneLine;
|
|
258
|
+
return oneLine.slice(0, maxLen - 1) + '…';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
main().catch(err => {
|
|
262
|
+
console.error('[morning-briefing] Error:', err.message);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
});
|