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,98 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pull Slack history into WALL-E brain by parsing search results.
4
+ * This script is meant to be run manually — it reads pre-fetched search results
5
+ * from stdin or a file and ingests them into the brain.
6
+ *
7
+ * Usage:
8
+ * node scripts/pull-slack-via-claude.js < /tmp/slack-results.txt
9
+ * node scripts/pull-slack-via-claude.js /tmp/slack-results.txt
10
+ */
11
+ 'use strict';
12
+ const fs = require('fs');
13
+ const brain = require('../brain');
14
+
15
+ brain.initDb();
16
+
17
+ const OWNER_ID = process.env.SLACK_OWNER_USER_ID;
18
+ if (!OWNER_ID) throw new Error('SLACK_OWNER_USER_ID env var required');
19
+ const OWNER_NAME = process.env.WALLE_OWNER_NAME || 'Owner';
20
+
21
+ function parseSearchResults(text) {
22
+ const messages = [];
23
+ const blocks = text.split('---').filter(b => b.trim());
24
+
25
+ for (const block of blocks) {
26
+ const lines = block.trim().split('\n');
27
+ let channel = '', channelId = '', sender = '', senderId = '', ts = '', msgTs = '', msgText = '';
28
+ let participants = [];
29
+
30
+ for (const line of lines) {
31
+ const l = line.trim();
32
+ if (l.startsWith('Channel:')) {
33
+ const m = l.match(/Channel:\s*(.*?)\s*\(ID:\s*(.*?)\)/);
34
+ if (m) { channel = m[1].trim(); channelId = m[2]; }
35
+ }
36
+ if (l.startsWith('From:')) {
37
+ const m = l.match(/From:\s*(.*?)\s*\(ID:\s*(.*?)\)/);
38
+ if (m) { sender = m[1].trim(); senderId = m[2]; }
39
+ }
40
+ if (l.startsWith('Time:')) {
41
+ ts = l.replace('Time:', '').trim();
42
+ }
43
+ if (l.startsWith('Message_ts:')) {
44
+ msgTs = l.replace('Message_ts:', '').trim();
45
+ }
46
+ if (l.startsWith('Text:')) {
47
+ msgText = lines.slice(lines.indexOf(line) + 1).join('\n').split('Context')[0].trim();
48
+ break;
49
+ }
50
+ if (l.startsWith('Participants:')) {
51
+ const pMatch = l.matchAll(/(\w[\w\s]+?)\s*\(ID:/g);
52
+ for (const pm of pMatch) participants.push(pm[1].trim());
53
+ }
54
+ }
55
+
56
+ if (!msgText || !msgTs) continue;
57
+
58
+ const isOwner = senderId === OWNER_ID;
59
+ const timestamp = ts ? new Date(ts + ' UTC').toISOString() : new Date(parseFloat(msgTs) * 1000).toISOString();
60
+
61
+ messages.push({
62
+ source_id: `slack-${channelId}-${msgTs}`,
63
+ channel: channel || channelId,
64
+ sender: sender || (isOwner ? OWNER_NAME : 'unknown'),
65
+ is_owner: isOwner,
66
+ text: msgText,
67
+ timestamp,
68
+ participants: participants.length > 0 ? participants : [sender || 'unknown'],
69
+ });
70
+ }
71
+
72
+ return messages;
73
+ }
74
+
75
+ // Read input
76
+ const inputFile = process.argv[2] || '/dev/stdin';
77
+ const text = fs.readFileSync(inputFile, 'utf8');
78
+ const messages = parseSearchResults(text);
79
+
80
+ let ingested = 0;
81
+ for (const m of messages) {
82
+ const result = brain.insertMemory({
83
+ source: 'slack',
84
+ source_id: m.source_id,
85
+ source_channel: m.channel,
86
+ memory_type: m.is_owner ? 'message_sent' : 'message_received',
87
+ direction: m.is_owner ? 'outbound' : 'inbound',
88
+ participants: JSON.stringify(m.participants),
89
+ content: m.sender + ': ' + m.text,
90
+ timestamp: m.timestamp,
91
+ metadata: JSON.stringify({ source: 'slack_import' }),
92
+ });
93
+ if (result) ingested++;
94
+ }
95
+
96
+ console.log(`Parsed: ${messages.length} messages, Ingested: ${ingested} new`);
97
+ console.log(`Total memories: ${brain.getBrainStats().memory_count}`);
98
+ brain.closeDb();
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * Standalone Slack backfill script — NO Claude API tokens used.
5
+ * Directly calls Slack MCP + writes to brain DB.
6
+ * Paginates through all results (up to 20 pages per query).
7
+ *
8
+ * Usage:
9
+ * node scripts/slack-backfill.js # full backfill (2022-now)
10
+ * node scripts/slack-backfill.js incremental # only new messages
11
+ * node scripts/slack-backfill.js 2024-01 # single month
12
+ */
13
+
14
+ const brain = require('../brain');
15
+ const slackMcp = require('../tools/slack-mcp');
16
+
17
+ const PAUSE_MS = 1500;
18
+ const MAX_PAGES = 20;
19
+ const OWNER_ID = process.env.SLACK_OWNER_USER_ID;
20
+ if (!OWNER_ID) throw new Error('SLACK_OWNER_USER_ID env var required');
21
+ const SLACK_OWNER_HANDLE = process.env.SLACK_OWNER_HANDLE || 'owner';
22
+
23
+ async function main() {
24
+ const mode = process.argv[2] || 'full';
25
+
26
+ // Init
27
+ brain.initDb();
28
+ const db = brain.getDb();
29
+
30
+ if (!slackMcp.isAuthenticated()) {
31
+ console.error('Not authenticated with Slack. Run: node tools/slack-mcp.js auth');
32
+ process.exit(1);
33
+ }
34
+
35
+ const totalBefore = db.prepare("SELECT count(*) as c FROM memories WHERE source = 'slack'").get().c;
36
+ console.log(`Starting. Mode: ${mode}. Currently have ${totalBefore} Slack messages.\n`);
37
+
38
+ let stats = { new_messages: 0, skipped: 0 };
39
+
40
+ if (mode === 'incremental') {
41
+ const latest = db.prepare("SELECT max(timestamp) as ts FROM memories WHERE source = 'slack'").get();
42
+ const latestDate = (latest?.ts || '2020-01-01').slice(0, 10);
43
+ console.log(`Incremental from ${latestDate}\n`);
44
+ await pullMonth(db, stats, `from:${SLACK_OWNER_HANDLE} after:${latestDate}`, 'incremental');
45
+ } else if (mode.match(/^\d{4}-\d{2}$/)) {
46
+ // Single month
47
+ const [y, m] = mode.split('-').map(Number);
48
+ const after = `${y}-${String(m).padStart(2, '0')}-01`;
49
+ const before = m === 12 ? `${y + 1}-01-01` : `${y}-${String(m + 1).padStart(2, '0')}-01`;
50
+ console.log(`Single month: ${mode}\n`);
51
+ await pullMonth(db, stats, `from:${SLACK_OWNER_HANDLE} after:${after} before:${before}`, mode);
52
+ } else {
53
+ // Full backfill with checkpoint support
54
+ const months = generateMonths(2022, 6);
55
+ const checkpoint = process.env.WALL_E_CHECKPOINT || '';
56
+ let startIdx = 0;
57
+ if (checkpoint) {
58
+ const idx = months.indexOf(checkpoint);
59
+ if (idx >= 0) {
60
+ startIdx = idx;
61
+ console.log(`Resuming from checkpoint: ${checkpoint}\n`);
62
+ }
63
+ }
64
+ console.log(`Full backfill: ${months.length} months (starting from ${months[startIdx]})\n`);
65
+
66
+ for (let mi = startIdx; mi < months.length; mi++) {
67
+ const month = months[mi];
68
+ const existing = db.prepare(
69
+ "SELECT count(*) as c FROM memories WHERE source='slack' AND substr(timestamp,1,7)=?"
70
+ ).get(month).c;
71
+
72
+ const [y, m] = month.split('-').map(Number);
73
+ const after = `${y}-${String(m).padStart(2, '0')}-01`;
74
+ const before = m === 12 ? `${y + 1}-01-01` : `${y}-${String(m + 1).padStart(2, '0')}-01`;
75
+
76
+ process.stdout.write(`[${month}] ${existing} existing... `);
77
+ const beforeNew = stats.new_messages;
78
+ await pullMonth(db, stats, `from:${SLACK_OWNER_HANDLE} after:${after} before:${before}`, month);
79
+ const added = stats.new_messages - beforeNew;
80
+
81
+ const nowCount = db.prepare(
82
+ "SELECT count(*) as c FROM memories WHERE source='slack' AND substr(timestamp,1,7)=?"
83
+ ).get(month).c;
84
+ console.log(`+${added} new → ${nowCount} total`);
85
+
86
+ // Emit checkpoint so task runner can save progress
87
+ console.log(`CHECKPOINT:${month}`);
88
+ }
89
+ }
90
+
91
+ const totalAfter = db.prepare("SELECT count(*) as c FROM memories WHERE source = 'slack'").get().c;
92
+ console.log(`\nDone! New: ${stats.new_messages}, Skipped dupes: ${stats.skipped}`);
93
+ console.log(`Total: ${totalBefore} → ${totalAfter} (+${totalAfter - totalBefore})`);
94
+ }
95
+
96
+ /**
97
+ * Pull all pages for a single query, paginating with cursors.
98
+ */
99
+ async function pullMonth(db, stats, query, label) {
100
+ let cursor = undefined;
101
+
102
+ for (let page = 1; page <= MAX_PAGES; page++) {
103
+ try {
104
+ const args = {
105
+ query,
106
+ sort: 'timestamp',
107
+ sort_dir: 'asc',
108
+ limit: 20,
109
+ include_context: false,
110
+ response_format: 'detailed',
111
+ };
112
+ if (cursor) args.cursor = cursor;
113
+
114
+ const result = await slackMcp.callSlackMcp('slack_search_public_and_private', args);
115
+ const texts = (result?.content || []).filter(c => c.type === 'text').map(c => c.text);
116
+ let combined = texts.join('\n');
117
+ if (!combined || combined.length < 10) break;
118
+
119
+ // The Slack MCP wraps the markdown in a JSON envelope: {"results":"...","pagination_info":"..."}
120
+ let paginationInfo = '';
121
+ try {
122
+ const parsed = JSON.parse(combined);
123
+ combined = parsed.results || combined;
124
+ paginationInfo = parsed.pagination_info || '';
125
+ } catch {}
126
+
127
+ // Parse the markdown response
128
+ const messages = parseSlackResults(combined);
129
+ if (messages.length === 0) break;
130
+
131
+ for (const msg of messages) {
132
+ const sourceId = `slack-${msg.message_ts}`;
133
+ const exists = db.prepare("SELECT 1 FROM memories WHERE source = 'slack' AND source_id = ?").get(sourceId);
134
+ if (exists) { stats.skipped++; continue; }
135
+
136
+ brain.insertMemory({
137
+ source: 'slack',
138
+ source_id: sourceId,
139
+ memory_type: msg.isOwner ? 'message_sent' : 'message_received',
140
+ direction: msg.isOwner ? 'outbound' : 'inbound',
141
+ content: msg.text,
142
+ source_channel: msg.channel,
143
+ participants: msg.participants || msg.sender,
144
+ timestamp: msg.timestamp,
145
+ metadata: JSON.stringify({ channel: msg.channel, sender: msg.sender, channel_id: msg.channelId }),
146
+ });
147
+ stats.new_messages++;
148
+ }
149
+
150
+ // Check for next page cursor (in pagination_info or in the text body)
151
+ const cursorMatch = (paginationInfo + '\n' + combined).match(/use cursor `([^`]+)`/);
152
+ if (!cursorMatch) break;
153
+ cursor = cursorMatch[1];
154
+
155
+ await sleep(PAUSE_MS);
156
+ } catch (err) {
157
+ if (err.message.includes('401') || err.message.includes('expired')) {
158
+ console.error('\nSlack token expired! Run: node tools/slack-mcp.js auth');
159
+ process.exit(1);
160
+ }
161
+ // Other errors: stop this month, continue
162
+ process.stderr.write(`[${label} p${page}] Error: ${err.message.slice(0, 100)}\n`);
163
+ break;
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Parse the Slack MCP detailed response format.
170
+ * Each result looks like:
171
+ * ### Result N of M
172
+ * Channel: #channel-name (ID: CXXX) or DM (ID: DXXX)
173
+ * Participants: Name1 (ID: UXXX), Name2 (ID: UXXX)
174
+ * From: Name (ID: UXXX)
175
+ * Time: 2024-09-04 17:12:28 BST
176
+ * Message_ts: 1725466348.869379
177
+ * Text:
178
+ * The actual message content
179
+ */
180
+ function parseSlackResults(text) {
181
+ const messages = [];
182
+ // Split by result headers
183
+ const blocks = text.split(/### Result \d+ of \d+/).slice(1);
184
+
185
+ for (const block of blocks) {
186
+ const lines = block.trim().split('\n');
187
+ let channel = 'unknown', channelId = '', sender = '', participants = '', timestamp = '', message_ts = '', content = '';
188
+ let inText = false;
189
+
190
+ for (const line of lines) {
191
+ if (inText) {
192
+ if (line.startsWith('---')) break;
193
+ content += (content ? '\n' : '') + line;
194
+ continue;
195
+ }
196
+
197
+ const channelMatch = line.match(/^Channel:\s*(?:#?(\S+)|DM|Group DM)\s*\(ID:\s*(\w+)\)/);
198
+ if (channelMatch) {
199
+ channel = channelMatch[1] || (line.includes('DM') ? 'DM' : 'unknown');
200
+ channelId = channelMatch[2] || '';
201
+ continue;
202
+ }
203
+
204
+ const participantsMatch = line.match(/^Participants:\s*(.+)/);
205
+ if (participantsMatch) {
206
+ participants = participantsMatch[1].replace(/\s*\(ID:\s*\w+\)/g, '').trim();
207
+ continue;
208
+ }
209
+
210
+ const fromMatch = line.match(/^From:\s*(.+?)\s*\(ID:\s*(\w+)\)/);
211
+ if (fromMatch) {
212
+ sender = fromMatch[1].trim() || fromMatch[2];
213
+ continue;
214
+ }
215
+
216
+ const timeMatch = line.match(/^Time:\s*(.+)/);
217
+ if (timeMatch) {
218
+ try {
219
+ const d = new Date(timeMatch[1].trim());
220
+ if (!isNaN(d.getTime())) timestamp = d.toISOString();
221
+ } catch {}
222
+ continue;
223
+ }
224
+
225
+ const tsMatch = line.match(/^Message_ts:\s*(\S+)/);
226
+ if (tsMatch) {
227
+ message_ts = tsMatch[1];
228
+ // Derive timestamp from message_ts if not already set
229
+ if (!timestamp) {
230
+ try {
231
+ const epoch = parseFloat(message_ts);
232
+ if (epoch > 0) timestamp = new Date(epoch * 1000).toISOString();
233
+ } catch {}
234
+ }
235
+ continue;
236
+ }
237
+
238
+ if (line.match(/^Text:\s*$/)) {
239
+ inText = true;
240
+ continue;
241
+ }
242
+ if (line.startsWith('Text:')) {
243
+ inText = true;
244
+ content = line.replace(/^Text:\s*/, '');
245
+ continue;
246
+ }
247
+ }
248
+
249
+ content = content.trim();
250
+ if (!content || !message_ts) continue;
251
+
252
+ const isOwner = sender.toLowerCase().includes(SLACK_OWNER_HANDLE.split('.')[0]) || sender === OWNER_ID;
253
+
254
+ // For DMs/Group DMs, use participant names as channel name instead of raw ID
255
+ if ((channel === 'DM' || channel === 'unknown' || channel === 'Group') && participants) {
256
+ const ownerFirst = (process.env.WALLE_OWNER_NAME || '').split(' ')[0].toLowerCase();
257
+ const others = participants.split(',')
258
+ .map(p => p.trim())
259
+ .filter(p => p && (!ownerFirst || !p.toLowerCase().startsWith(ownerFirst)))
260
+ .map(p => p.split(' ')[0]) // first names only
261
+ .slice(0, 3);
262
+ if (others.length > 0) {
263
+ channel = `DM with ${others.join(', ')}`;
264
+ }
265
+ }
266
+
267
+ messages.push({
268
+ channel,
269
+ channelId,
270
+ sender,
271
+ participants,
272
+ timestamp,
273
+ message_ts,
274
+ text: content,
275
+ isOwner,
276
+ });
277
+ }
278
+
279
+ return messages;
280
+ }
281
+
282
+ function generateMonths(startYear, startMonth) {
283
+ const months = [];
284
+ const now = new Date();
285
+ let y = startYear, m = startMonth;
286
+ while (y < now.getFullYear() || (y === now.getFullYear() && m <= now.getMonth() + 1)) {
287
+ months.push(`${y}-${String(m).padStart(2, '0')}`);
288
+ m++; if (m > 12) { m = 1; y++; }
289
+ }
290
+ return months;
291
+ }
292
+
293
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
294
+
295
+ main().catch(err => { console.error('Fatal:', err.message); process.exit(1); });