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,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
+ });