create-walle 0.1.0 → 0.2.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 (29) hide show
  1. package/bin/create-walle.js +276 -61
  2. package/package.json +1 -1
  3. package/template/CHANGELOG.md +44 -0
  4. package/template/bin/github-polish.sh +18 -0
  5. package/template/bin/install-service.sh +72 -0
  6. package/template/claude-task-manager/public/css/walle.css +9 -8
  7. package/template/claude-task-manager/public/index.html +1 -0
  8. package/template/claude-task-manager/public/setup.html +38 -1
  9. package/template/claude-task-manager/server.js +59 -0
  10. package/template/docs/site/astro.config.mjs +28 -0
  11. package/template/docs/site/package.json +19 -0
  12. package/template/docs/site/src/content/docs/api.md +189 -0
  13. package/template/docs/site/src/content/docs/guides/claude-code.md +62 -0
  14. package/template/docs/site/src/content/docs/guides/configuration.md +100 -0
  15. package/template/docs/site/src/content/docs/guides/quickstart.md +162 -0
  16. package/template/docs/site/src/content/docs/index.mdx +32 -0
  17. package/template/docs/site/src/content/docs/skills.md +137 -0
  18. package/template/docs/site/src/content.config.ts +7 -0
  19. package/template/package.json +11 -0
  20. package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +2 -2
  21. package/template/wall-e/docs/specs/SKILL-FORMAT.md +1 -1
  22. package/template/wall-e/skills/_bundled/email-digest/SKILL.md +1 -1
  23. package/template/wall-e/skills/_bundled/email-sync/SKILL.md +1 -1
  24. package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +1 -1
  25. package/template/wall-e/skills/_bundled/memory-search/SKILL.md +1 -1
  26. package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +1 -1
  27. package/template/wall-e/skills/_bundled/morning-briefing/run.js +110 -56
  28. package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +1 -1
  29. package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +1 -1
@@ -0,0 +1,137 @@
1
+ ---
2
+ title: Skill Catalog
3
+ ---
4
+
5
+ Wall-E ships with bundled skills that run on a schedule to keep your brain up to date.
6
+
7
+ ## Bundled Skills
8
+
9
+ ### google-calendar
10
+ **Schedule:** every 30m | **Execution:** script
11
+
12
+ Syncs macOS Calendar events (Google, iCloud, Outlook) into the brain via EventKit. Stores event title, attendees, location, notes, and calendar name. Deduplicates by event UID + start time.
13
+
14
+ **Config:**
15
+ - `days_back` (number, default: 1) — days of past events to include
16
+ - `days_ahead` (number, default: 14) — days of future events to include
17
+
18
+ ---
19
+
20
+ ### slack-sync
21
+ **Schedule:** every 15m | **Execution:** script
22
+
23
+ Incremental Slack message sync via MCP. Pulls new messages since the last checkpoint and stores them as memories with sender, channel, and direction metadata.
24
+
25
+ **Requires:** `SLACK_TOKEN`, `SLACK_OWNER_USER_ID`
26
+
27
+ ---
28
+
29
+ ### slack-backfill
30
+ **Schedule:** manual | **Execution:** script
31
+
32
+ Full Slack history backfill from 2022 to present. Paginates through all search results (up to 20 pages per month). Supports checkpoint/resume for interrupted runs.
33
+
34
+ **Usage:**
35
+ ```bash
36
+ node scripts/slack-backfill.js # full backfill
37
+ node scripts/slack-backfill.js incremental # new messages only
38
+ node scripts/slack-backfill.js 2024-01 # single month
39
+ ```
40
+
41
+ ---
42
+
43
+ ### email-sync
44
+ **Schedule:** every 30m | **Execution:** script
45
+
46
+ Syncs sent emails from macOS Mail via JXA (JavaScript for Automation). Captures subject, recipients, date, and body text. Deduplicates by message ID.
47
+
48
+ ---
49
+
50
+ ### email-digest
51
+ **Schedule:** daily at 7am | **Execution:** agent
52
+
53
+ AI-generated summary of recent email activity. Uses the LLM to synthesize patterns, key threads, and action items from email memories.
54
+
55
+ ---
56
+
57
+ ### morning-briefing
58
+ **Schedule:** daily at 7am | **Execution:** agent
59
+
60
+ Generates a comprehensive daily briefing by searching memories across all sources. Includes: today's meetings, pending action items, Slack mentions, and active threads.
61
+
62
+ ---
63
+
64
+ ### memory-search
65
+ **Schedule:** on-demand | **Execution:** script
66
+
67
+ Full-text search across all memories using SQLite FTS5. Available as a tool in Wall-E chat.
68
+
69
+ ---
70
+
71
+ ## Creating Custom Skills
72
+
73
+ Skills are defined as `SKILL.md` files with YAML frontmatter. Place them in `wall-e/skills/_bundled/your-skill/`.
74
+
75
+ ### Minimal Example
76
+
77
+ ```yaml
78
+ ---
79
+ name: my-skill
80
+ description: What this skill does
81
+ version: 1.0.0
82
+ execution: script
83
+ entry: run.js
84
+ trigger:
85
+ type: interval
86
+ schedule: "every 1h"
87
+ tags: [custom]
88
+ permissions:
89
+ - brain:write
90
+ ---
91
+ # My Skill
92
+
93
+ Additional documentation here.
94
+ ```
95
+
96
+ ### Execution Modes
97
+
98
+ **Script mode** (`execution: script`): Deterministic, no LLM cost. The daemon runs your `entry` file (Node.js script) and captures stdout/stderr. Use for data ingestion, sync, and automation.
99
+
100
+ **Agent mode** (`execution: agent`): LLM-driven. The markdown body of SKILL.md becomes the agent's prompt. Use for summarization, synthesis, and tasks requiring judgment.
101
+
102
+ ### Script Pattern
103
+
104
+ ```javascript
105
+ #!/usr/bin/env node
106
+ 'use strict';
107
+
108
+ const brain = require('../../brain');
109
+ brain.initDb();
110
+
111
+ // Read config from environment
112
+ const config = JSON.parse(process.env.WALL_E_SKILL_CONFIG || '{}');
113
+
114
+ // Do work, store results
115
+ const mem = {
116
+ source: 'my-source',
117
+ source_id: 'unique-id',
118
+ memory_type: 'my_type',
119
+ subject: 'Title',
120
+ content: 'Human-readable content',
121
+ metadata: JSON.stringify({ raw: 'data' }),
122
+ importance: 0.5,
123
+ timestamp: new Date().toISOString(),
124
+ };
125
+ brain.insertMemory(mem);
126
+
127
+ // Output JSON summary
128
+ console.log(JSON.stringify({ inserted: 1 }));
129
+ brain.closeDb();
130
+ ```
131
+
132
+ ### Key Conventions
133
+
134
+ - **Deduplication**: Always use `source` + `source_id` for dedup. Check existing before inserting.
135
+ - **Exit codes**: 0 = success, 1 = failure, 2 = partial success (checkpoint saved)
136
+ - **Checkpoints**: Emit `CHECKPOINT:value` lines to stdout for resumable tasks
137
+ - **Config**: Passed via `WALL_E_SKILL_CONFIG` env var (JSON string)
@@ -0,0 +1,7 @@
1
+ import { docsLoader } from '@astrojs/starlight/loaders';
2
+ import { docsSchema } from '@astrojs/starlight/schema';
3
+ import { defineCollection } from 'astro:content';
4
+
5
+ export const collections = {
6
+ docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
7
+ };
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "walle",
3
+ "version": "0.1.1",
4
+ "private": true,
5
+ "description": "Wall-E — your personal digital twin",
6
+ "scripts": {
7
+ "start": "node claude-task-manager/server.js",
8
+ "stop": "curl -sX POST http://localhost:${CTM_PORT:-4567}/api/stop/walle; curl -sX POST http://localhost:${CTM_PORT:-4567}/api/restart/ctm 2>/dev/null; echo 'Stopped.'",
9
+ "setup": "node bin/setup.js"
10
+ }
11
+ }
@@ -1,7 +1,7 @@
1
1
  # Publishing Plan: CTM & Wall-E
2
2
 
3
3
  **Date:** 2026-04-01
4
- **Author:** Juncao Li
4
+ **Author:** Wall-E Team
5
5
  **Domain:** https://walle.sh
6
6
 
7
7
  ## Overview
@@ -20,7 +20,7 @@ Publish CTM (Claude Task Manager) and Wall-E (personal digital twin agent) as op
20
20
  |---|---|---|---|
21
21
  | Hardcoded Slack User ID `YOUR_SLACK_USER_ID` | Critical | 5 files in `scripts/`, `slack-ingest.js` | Env var `SLACK_OWNER_USER_ID` |
22
22
  | `owner@example.com` in SKILL.md examples | High | `email-sync/SKILL.md`, `google-calendar/SKILL.md` | Replace with `owner@example.com` |
23
- | Hardcoded `juncao.li` in Slack queries | High | `slack-backfill.js`, `slack-channel-history.js`, `morning-briefing/run.js` | Read from config/brain |
23
+ | Hardcoded Slack handle in queries | High | `slack-backfill.js`, `slack-channel-history.js`, `morning-briefing/run.js` | Read from config/brain |
24
24
  | `OWNER_NAME` owner name constant | High | `pull-slack-via-claude.js`, `slack-ingest.js` | Use `brain.getOwnerName()` |
25
25
  | Hardcoded `.walle/data` | Medium | `server.js`, `db.js`, `brain.js`, `api-walle.js` | Default to `~/.walle/data` |
26
26
 
@@ -67,7 +67,7 @@ description: >
67
67
  Discovers recently active channels, fetches new messages,
68
68
  and stores them in WALL-E's brain for search and context.
69
69
  version: 1.2.0
70
- author: juncao
70
+ author: wall-e
71
71
 
72
72
  # Execution mode: "script" (deterministic) or "agent" (LLM-driven)
73
73
  execution: script
@@ -5,7 +5,7 @@ description: >
5
5
  Check inbox, summarize unread messages, read full emails, compose and send
6
6
  replies. Use for email triage, digests, and drafting responses.
7
7
  version: 1.0.0
8
- author: juncao
8
+ author: wall-e
9
9
  execution: agent
10
10
  trigger:
11
11
  type: manual
@@ -6,7 +6,7 @@ description: >
6
6
  content. Inbox sync is optional and filters to only emails addressed
7
7
  directly to the owner (on To/Cc line). Uses JXA to read Mail.app.
8
8
  version: 1.1.0
9
- author: juncao
9
+ author: wall-e
10
10
  execution: script
11
11
  entry: run.js
12
12
  args: ["--days-back", "3"]
@@ -6,7 +6,7 @@ description: >
6
6
  with attendees, location, notes. Supports incremental sync with
7
7
  update detection. Use for schedule awareness, meeting prep, calendar context.
8
8
  version: 1.1.0
9
- author: juncao
9
+ author: wall-e
10
10
  execution: script
11
11
  entry: run.js
12
12
  args: ["--days-back", "1", "--days-ahead", "14"]
@@ -5,7 +5,7 @@ description: >
5
5
  searches across Slack messages, chat history, and knowledge base.
6
6
  Use for research, finding past conversations, and building context.
7
7
  version: 1.0.0
8
- author: juncao
8
+ author: wall-e
9
9
  execution: agent
10
10
  trigger:
11
11
  type: manual
@@ -5,7 +5,7 @@ description: >
5
5
  activity, upcoming calendar events, pending tasks, and items needing
6
6
  attention. Use for daily meeting prep and situational awareness.
7
7
  version: 2.0.0
8
- author: juncao
8
+ author: wall-e
9
9
  execution: script
10
10
  entry: run.js
11
11
  trigger:
@@ -3,12 +3,14 @@
3
3
  /**
4
4
  * Morning Briefing — pure script, no Claude API calls.
5
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
6
+ * 1. Calendar events (today) via brain
7
+ * 2. Recent Slack activity grouped by channel
8
+ * 3. Direct mentions & urgent items
9
+ * 4. Pending tasks
10
+ * 5. Pending questions
11
+ * 6. Quick stats footer
10
12
  *
11
- * Outputs markdown to stdout. The task runner stores it as the task result.
13
+ * Outputs clean markdown to stdout. The task runner stores it as the task result.
12
14
  */
13
15
  const path = require('path');
14
16
  const brain = require(path.resolve(__dirname, '..', '..', '..', 'brain'));
@@ -20,19 +22,18 @@ const db = brain.getDb();
20
22
  async function main() {
21
23
  const now = new Date();
22
24
  const todayStr = now.toISOString().slice(0, 10);
25
+ const dayLabel = now.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
23
26
  const sections = [];
24
27
 
25
28
  // ── 1. Today's Schedule ──
26
- let calSection = "## Today's Schedule\n";
29
+ let calSection = '## 📅 Today\'s Schedule\n\n';
27
30
  try {
28
31
  const { events } = await getCalendarEvents({ days_ahead: 0 });
29
- // Filter to today only
30
32
  const todayEvents = events.filter(e => {
31
33
  if (e.allDay) return true;
32
34
  const eDate = new Date(e.start);
33
35
  return !isNaN(eDate) && eDate.toISOString().slice(0, 10) === todayStr;
34
36
  });
35
- // De-duplicate by title + time
36
37
  const seen = new Set();
37
38
  const deduped = todayEvents.filter(e => {
38
39
  const key = `${e.title}|${formatTime(e.start)}`;
@@ -41,20 +42,28 @@ async function main() {
41
42
  return true;
42
43
  });
43
44
  if (deduped.length === 0) {
44
- calSection += '_No meetings today._\n';
45
+ calSection += 'No meetings today — clear calendar.\n';
45
46
  } else {
47
+ calSection += '| Time | Meeting | With |\n|------|---------|------|\n';
46
48
  for (const evt of deduped) {
47
49
  const time = evt.allDay ? 'All day' : formatTime(evt.start);
48
- // Show first names only for attendees, skip owner
50
+ const ownerName = (process.env.WALLE_OWNER_NAME || '').toLowerCase();
49
51
  const ownerEmail = (process.env.WALLE_OWNER_EMAIL || '').toLowerCase().trim();
50
52
  const ownerPrefix = ownerEmail.includes('@') ? ownerEmail.split('@')[0] : '';
53
+ const ownerParts = ownerName.split(/\s+/).filter(p => p.length > 1);
51
54
  const filteredAttendees = evt.attendees
52
- .filter(a => !ownerPrefix || !a.toLowerCase().includes(ownerPrefix))
55
+ .filter(a => {
56
+ const aLow = a.toLowerCase();
57
+ // Skip owner by email prefix or name match
58
+ if (ownerPrefix && aLow.includes(ownerPrefix)) return false;
59
+ if (ownerParts.length > 0 && ownerParts.every(p => aLow.includes(p))) return false;
60
+ return true;
61
+ })
53
62
  .map(a => a.split('@')[0].split('.').filter(Boolean).map(s => s[0].toUpperCase() + s.slice(1)).join(' '));
54
63
  const attendees = filteredAttendees.slice(0, 4);
55
64
  const overflow = filteredAttendees.length > 4 ? ` +${filteredAttendees.length - 4}` : '';
56
- const attendeeStr = attendees.length > 0 ? ` — ${attendees.join(', ')}${overflow}` : '';
57
- calSection += `| **${time}** | ${evt.title}${attendeeStr} |\n`;
65
+ const attendeeStr = attendees.length > 0 ? attendees.join(', ') + overflow : '';
66
+ calSection += `| ${time} | ${evt.title} | ${attendeeStr} |\n`;
58
67
  }
59
68
  }
60
69
  } catch (err) {
@@ -62,7 +71,7 @@ async function main() {
62
71
  }
63
72
  sections.push(calSection);
64
73
 
65
- // ── 2. Overnight Slack Activity (from brain) ──
74
+ // ── 2. Overnight Slack Activity ──
66
75
  const hoursBack = 18;
67
76
  const cutoff = new Date(now.getTime() - hoursBack * 3600000).toISOString();
68
77
 
@@ -84,12 +93,11 @@ async function main() {
84
93
  LIMIT 10
85
94
  `).all(cutoff);
86
95
 
87
- let slackSection = '## Overnight Slack\n';
96
+ let slackSection = '## 💬 Overnight Slack\n\n';
88
97
 
89
98
  if (recentInbound.length === 0 && recentOutbound.length === 0) {
90
- slackSection += '_No Slack activity in the last 18 hours._\n';
99
+ slackSection += 'No Slack activity in the last 18 hours.\n';
91
100
  } else {
92
- // Group by channel, resolve channel names
93
101
  const byChannel = {};
94
102
  for (const msg of recentInbound) {
95
103
  const ch = msg.source_channel || 'DM';
@@ -97,32 +105,33 @@ async function main() {
97
105
  byChannel[ch].push(msg);
98
106
  }
99
107
 
100
- // Sort channels: most active first, top 6
101
108
  const channels = Object.entries(byChannel)
102
109
  .sort((a, b) => b[1].length - a[1].length)
103
110
  .slice(0, 6);
104
111
 
105
112
  for (const [channel, msgs] of channels) {
106
113
  const chName = resolveChannelName(channel);
107
- slackSection += `\n**${chName}** · ${msgs.length} message${msgs.length > 1 ? 's' : ''}\n`;
108
- // Show up to 3 most recent, condensed
114
+ const countLabel = msgs.length === 1 ? '1 message' : `${msgs.length} messages`;
115
+ slackSection += `### ${chName} \`${countLabel}\`\n\n`;
116
+
109
117
  for (const msg of msgs.slice(0, 3)) {
110
118
  const time = formatTime(msg.timestamp);
111
119
  const who = extractFirstName(msg.participants);
112
- const preview = condenseLine(msg.content, 120);
120
+ const preview = condenseLine(msg.content, 140);
113
121
  if (who) {
114
- slackSection += `> **${who}** _(${time})_ — ${preview}\n`;
122
+ slackSection += `- **${who}** at ${time} — ${preview}\n`;
115
123
  } else {
116
- slackSection += `> _(${time})_ ${preview}\n`;
124
+ slackSection += `- ${time} ${preview}\n`;
117
125
  }
118
126
  }
119
127
  if (msgs.length > 3) {
120
- slackSection += `> _+${msgs.length - 3} more_\n`;
128
+ slackSection += `- _...and ${msgs.length - 3} more_\n`;
121
129
  }
130
+ slackSection += '\n';
122
131
  }
123
132
 
124
133
  if (recentOutbound.length > 0) {
125
- slackSection += `\n_You sent ${recentOutbound.length} messages in the last ${hoursBack}h._\n`;
134
+ slackSection += `You sent ${recentOutbound.length} message${recentOutbound.length > 1 ? 's' : ''} in the last ${hoursBack}h.\n`;
126
135
  }
127
136
  }
128
137
  sections.push(slackSection);
@@ -133,7 +142,7 @@ async function main() {
133
142
  const hasFts = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories_fts'").get();
134
143
  if (hasFts) {
135
144
  const ownerFirst = (process.env.WALLE_OWNER_NAME || 'owner').split(' ')[0]
136
- .replace(/['"*()]/g, ''); // sanitize FTS5 special chars
145
+ .replace(/['"*()]/g, '');
137
146
  const matchExpr = `${ownerFirst} OR urgent OR blocked OR ASAP`;
138
147
  const mentions = db.prepare(`
139
148
  SELECT m.content, m.source_channel, m.participants, m.timestamp
@@ -146,14 +155,16 @@ async function main() {
146
155
  `).all(matchExpr, cutoff);
147
156
 
148
157
  if (mentions.length > 0) {
149
- mentionSection = '\n## Mentions & Urgent\n';
158
+ mentionSection = '## 🔔 Mentions & Urgent\n\n';
150
159
  for (const m of mentions) {
151
160
  const time = formatTime(m.timestamp);
152
161
  const ch = resolveChannelName(m.source_channel);
153
162
  const who = extractFirstName(m.participants);
154
- const preview = condenseLine(m.content, 140);
155
- mentionSection += `> ${who ? `**${who}**` : ch} _(${time})_ ${preview}\n`;
163
+ const preview = condenseLine(m.content, 160);
164
+ const source = who ? `**${who}** in ${ch}` : ch;
165
+ mentionSection += `- ${source} at ${time}\n ${preview}\n`;
156
166
  }
167
+ mentionSection += '\n';
157
168
  }
158
169
  }
159
170
  } catch {}
@@ -163,43 +174,51 @@ async function main() {
163
174
  const tasks = brain.listTasks({ status: 'pending', limit: 10 });
164
175
  const failedTasks = brain.listTasks({ status: 'failed', limit: 5 });
165
176
  if (tasks.length > 0 || failedTasks.length > 0) {
166
- let taskSection = '## Tasks\n';
177
+ let taskSection = '## Tasks\n\n';
167
178
  if (tasks.length > 0) {
168
179
  taskSection += '| Task | Priority | Due |\n|------|----------|-----|\n';
169
180
  for (const t of tasks) {
170
181
  const due = t.due_at ? t.due_at.slice(0, 10) : '—';
171
- const pri = t.priority === 'urgent' ? '🔴 urgent' : t.priority === 'high' ? '🟡 high' : '—';
182
+ const pri = t.priority === 'urgent' ? '🔴 Urgent' : t.priority === 'high' ? '🟡 High' : '—';
172
183
  taskSection += `| ${t.title} | ${pri} | ${due} |\n`;
173
184
  }
174
185
  }
175
186
  if (failedTasks.length > 0) {
176
- taskSection += '\n**Failed:**\n';
187
+ taskSection += '\n**Failed tasks:**\n\n';
177
188
  for (const t of failedTasks) {
178
- taskSection += `- ❌ ${t.title} — _${condenseLine(t.error || 'unknown', 60)}_\n`;
189
+ taskSection += `- ❌ ${t.title} — _${condenseLine(t.error || 'unknown error', 80)}_\n`;
179
190
  }
180
191
  }
192
+ taskSection += '\n';
181
193
  sections.push(taskSection);
182
194
  }
183
195
 
184
196
  // ── 5. Pending Questions ──
185
197
  const questions = brain.listQuestions({ status: 'pending', limit: 5 });
186
198
  if (questions.length > 0) {
187
- let qSection = '## Questions for You\n';
199
+ let qSection = '## Questions for You\n\n';
188
200
  for (const q of questions) {
189
- qSection += `- **${q.question_type}**: ${q.question}\n`;
201
+ const typeLabel = q.question_type === 'contradiction' ? 'Contradiction'
202
+ : q.question_type === 'preference' ? 'Preference' : q.question_type;
203
+ qSection += `- **${typeLabel}:** ${q.question}\n`;
190
204
  }
205
+ qSection += '\n';
191
206
  sections.push(qSection);
192
207
  }
193
208
 
194
209
  // ── 6. Quick Stats ──
195
210
  const stats = brain.getBrainStats();
196
211
  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`;
212
+ let statsSection = '---\n\n';
213
+ statsSection += `${recentInbound.length} inbound messages · `;
214
+ statsSection += `${tasks.length} pending tasks · `;
215
+ statsSection += `${questions.length} open questions · `;
216
+ statsSection += `${stats.memory_count.toLocaleString()} total memories · `;
217
+ statsSection += `${totalSlack.toLocaleString()} Slack messages\n`;
199
218
  sections.push(statsSection);
200
219
 
201
220
  // ── Output ──
202
- const output = `# Morning Briefing — ${todayStr}\n\n${sections.join('\n')}`;
221
+ const output = `# Morning Briefing\n\n**${dayLabel}**\n\n${sections.join('\n')}`;
203
222
  console.log(output);
204
223
 
205
224
  brain.closeDb();
@@ -221,22 +240,58 @@ function formatTime(dateStr) {
221
240
  }
222
241
  }
223
242
 
224
- // Resolve Slack channel IDs to readable names
225
- const CHANNEL_MAP = {};
243
+ // Build a channel name lookup from brain memories
244
+ const CHANNEL_NAME_CACHE = {};
226
245
  function resolveChannelName(channelId) {
227
246
  if (!channelId) return '#unknown';
228
- // If it looks like a readable name already (not all-caps+digits), use it
247
+ // Already a readable name
229
248
  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];
249
+ if (CHANNEL_NAME_CACHE[channelId]) return CHANNEL_NAME_CACHE[channelId];
250
+
251
+ // Try to find a human-readable name from memories metadata
252
+ try {
253
+ const row = db.prepare(`
254
+ SELECT metadata FROM memories
255
+ WHERE source = 'slack' AND source_channel = ?
256
+ AND metadata LIKE '%channel_name%'
257
+ LIMIT 1
258
+ `).get(channelId);
259
+ if (row && row.metadata) {
260
+ const meta = JSON.parse(row.metadata);
261
+ if (meta.channel_name) {
262
+ CHANNEL_NAME_CACHE[channelId] = `#${meta.channel_name}`;
263
+ return CHANNEL_NAME_CACHE[channelId];
264
+ }
265
+ }
266
+ } catch {}
267
+
268
+ // Try to extract from participants or content patterns
269
+ try {
270
+ const row = db.prepare(`
271
+ SELECT participants FROM memories
272
+ WHERE source = 'slack' AND source_channel = ?
273
+ AND participants IS NOT NULL AND participants != ''
274
+ LIMIT 1
275
+ `).get(channelId);
276
+ if (row && row.participants && !row.participants.match(/^[A-Z][A-Z0-9]{7,}$/)) {
277
+ // For DMs, show participant name instead of channel ID
278
+ const name = row.participants.split(',')[0].trim();
279
+ if (name && name.length < 30) {
280
+ CHANNEL_NAME_CACHE[channelId] = `DM with ${extractFirstName({ participants: row.participants })}`;
281
+ return CHANNEL_NAME_CACHE[channelId];
282
+ }
283
+ }
284
+ } catch {}
285
+
286
+ CHANNEL_NAME_CACHE[channelId] = `#${channelId.slice(0, 4)}…`;
287
+ return CHANNEL_NAME_CACHE[channelId];
235
288
  }
236
289
 
237
- function extractFirstName(participants) {
290
+ function extractFirstName(msgOrParticipants) {
291
+ const participants = typeof msgOrParticipants === 'string'
292
+ ? msgOrParticipants
293
+ : (msgOrParticipants && msgOrParticipants.participants) || '';
238
294
  if (!participants) return '';
239
- // "Derek Holevinsky" → "Derek", "Yu Tan" → "Yu"
240
295
  const name = participants.split(',')[0].trim().replace(/^\*+|\*+$/g, '');
241
296
  return name.split(' ')[0] || name;
242
297
  }
@@ -244,15 +299,14 @@ function extractFirstName(participants) {
244
299
  function condenseLine(text, maxLen) {
245
300
  if (!text) return '';
246
301
  let oneLine = text.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
247
- // Clean Slack formatting
248
302
  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)
303
+ .replace(/<@[A-Z0-9]+\|([^>]+)>/g, '@$1')
304
+ .replace(/<@[A-Z0-9]+>/g, '@someone')
305
+ .replace(/<(https?:\/\/[^|>]+)\|([^>]+)>/g, '$2')
306
+ .replace(/<(https?:\/\/[^>]+)>/g, '[link]')
307
+ .replace(/:[a-z_+-]+:/g, '')
308
+ .replace(/<!(?:here|channel|everyone)>/g, '@here')
309
+ .replace(/\*([^*\n]+)\*/g, '$1')
256
310
  .replace(/\s+/g, ' ').trim();
257
311
  if (oneLine.length <= maxLen) return oneLine;
258
312
  return oneLine.slice(0, maxLen - 1) + '…';
@@ -5,7 +5,7 @@ description: >
5
5
  paginates through all results, and stores messages in WALL-E's brain.
6
6
  Use for initial setup or catching up on missed months.
7
7
  version: 1.0.0
8
- author: juncao
8
+ author: wall-e
9
9
  execution: script
10
10
  entry: ../../../scripts/slack-backfill.js
11
11
  args: []
@@ -5,7 +5,7 @@ description: >
5
5
  Discovers recently active channels, fetches new messages incrementally.
6
6
  Use for keeping Slack context fresh.
7
7
  version: 1.0.0
8
- author: juncao
8
+ author: wall-e
9
9
  execution: script
10
10
  entry: ../../../scripts/slack-channel-history.js
11
11
  args: ["--sync"]