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.
- package/bin/create-walle.js +276 -61
- package/package.json +1 -1
- package/template/CHANGELOG.md +44 -0
- package/template/bin/github-polish.sh +18 -0
- package/template/bin/install-service.sh +72 -0
- package/template/claude-task-manager/public/css/walle.css +9 -8
- package/template/claude-task-manager/public/index.html +1 -0
- package/template/claude-task-manager/public/setup.html +38 -1
- package/template/claude-task-manager/server.js +59 -0
- package/template/docs/site/astro.config.mjs +28 -0
- package/template/docs/site/package.json +19 -0
- package/template/docs/site/src/content/docs/api.md +189 -0
- package/template/docs/site/src/content/docs/guides/claude-code.md +62 -0
- package/template/docs/site/src/content/docs/guides/configuration.md +100 -0
- package/template/docs/site/src/content/docs/guides/quickstart.md +162 -0
- package/template/docs/site/src/content/docs/index.mdx +32 -0
- package/template/docs/site/src/content/docs/skills.md +137 -0
- package/template/docs/site/src/content.config.ts +7 -0
- package/template/package.json +11 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +2 -2
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +1 -1
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +110 -56
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +1 -1
- 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:**
|
|
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
|
|
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:
|
|
70
|
+
author: wall-e
|
|
71
71
|
|
|
72
72
|
# Execution mode: "script" (deterministic) or "agent" (LLM-driven)
|
|
73
73
|
execution: script
|
|
@@ -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:
|
|
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:
|
|
9
|
+
author: wall-e
|
|
10
10
|
execution: script
|
|
11
11
|
entry: run.js
|
|
12
12
|
args: ["--days-back", "1", "--days-ahead", "14"]
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Morning Briefing — pure script, no Claude API calls.
|
|
5
5
|
*
|
|
6
|
-
* 1. Calendar events (today) via
|
|
7
|
-
* 2. Recent Slack activity
|
|
8
|
-
* 3.
|
|
9
|
-
* 4. Pending
|
|
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 =
|
|
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 += '
|
|
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
|
-
|
|
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 =>
|
|
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 ?
|
|
57
|
-
calSection += `|
|
|
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
|
|
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 += '
|
|
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
|
-
|
|
108
|
-
|
|
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
|
+
const preview = condenseLine(msg.content, 140);
|
|
113
121
|
if (who) {
|
|
114
|
-
slackSection +=
|
|
122
|
+
slackSection += `- **${who}** at ${time} — ${preview}\n`;
|
|
115
123
|
} else {
|
|
116
|
-
slackSection +=
|
|
124
|
+
slackSection += `- ${time} — ${preview}\n`;
|
|
117
125
|
}
|
|
118
126
|
}
|
|
119
127
|
if (msgs.length > 3) {
|
|
120
|
-
slackSection +=
|
|
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 +=
|
|
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, '');
|
|
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 = '
|
|
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,
|
|
155
|
-
|
|
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' ? '🔴
|
|
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',
|
|
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
|
-
|
|
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 +=
|
|
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
|
|
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
|
-
//
|
|
225
|
-
const
|
|
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
|
-
//
|
|
247
|
+
// Already a readable name
|
|
229
248
|
if (!channelId.match(/^[A-Z][A-Z0-9]{7,}$/)) return `#${channelId}`;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
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(
|
|
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')
|
|
250
|
-
.replace(/<@[A-Z0-9]+>/g, '@someone')
|
|
251
|
-
.replace(/<(https?:\/\/[^|>]+)\|([^>]+)>/g, '$2')
|
|
252
|
-
.replace(/<(https?:\/\/[^>]+)>/g, '[link]')
|
|
253
|
-
.replace(/:[a-z_
|
|
254
|
-
.replace(/<!(?:here|channel|everyone)>/g, '@here')
|
|
255
|
-
.replace(/\*([^*\n]+)\*/g, '$1')
|
|
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:
|
|
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:
|
|
8
|
+
author: wall-e
|
|
9
9
|
execution: script
|
|
10
10
|
entry: ../../../scripts/slack-channel-history.js
|
|
11
11
|
args: ["--sync"]
|