create-walle 0.9.3 → 0.9.4
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/README.md +2 -1
- package/package.json +1 -1
- package/template/claude-task-manager/db.js +5 -1
- package/template/claude-task-manager/public/css/walle.css +317 -0
- package/template/claude-task-manager/public/index.html +404 -101
- package/template/claude-task-manager/public/js/walle.js +1256 -86
- package/template/claude-task-manager/server.js +189 -14
- package/template/docs/site/api/README.md +146 -0
- package/template/docs/site/skills/README.md +99 -5
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +54 -0
- package/template/wall-e/api-walle.js +452 -3
- package/template/wall-e/brain.js +45 -1
- package/template/wall-e/channels/telegram-channel.js +96 -0
- package/template/wall-e/chat.js +61 -2
- package/template/wall-e/coding-context.js +252 -0
- package/template/wall-e/coding-orchestrator.js +625 -0
- package/template/wall-e/coding-review.js +189 -0
- package/template/wall-e/core-tasks.js +12 -3
- package/template/wall-e/deploy.sh +4 -4
- package/template/wall-e/fly.toml +2 -2
- package/template/wall-e/package.json +4 -1
- package/template/wall-e/skills/_bundled/coding-agent/SKILL.md +17 -0
- package/template/wall-e/skills/_bundled/coding-agent/run.js +142 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +12 -7
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +76 -46
- package/template/wall-e/skills/_bundled/email-sync/run.js +42 -2
- package/template/wall-e/skills/_bundled/glean-team-sync/SKILL.md +57 -0
- package/template/wall-e/skills/_bundled/glean-team-sync/run.js +254 -0
- package/template/wall-e/skills/_bundled/slack-mentions/SKILL.md +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +268 -121
- package/template/wall-e/skills/_templates/data-fetcher.md +27 -0
- package/template/wall-e/skills/_templates/manual-action.md +19 -0
- package/template/wall-e/skills/_templates/periodic-checker.md +29 -0
- package/template/wall-e/skills/_templates/script-runner.md +21 -0
- package/template/wall-e/skills/claude-code-reader.js +16 -4
- package/template/wall-e/skills/skill-executor.js +23 -1
- package/template/wall-e/skills/skill-validator.js +73 -0
- package/template/wall-e/tests/brain.test.js +3 -3
- package/template/wall-e/tests/coding-agent-integration.test.js +240 -0
- package/template/wall-e/tests/coding-context.test.js +212 -0
- package/template/wall-e/tests/coding-orchestrator.test.js +303 -0
- package/template/wall-e/tests/coding-review.test.js +141 -0
- package/template/claude-task-manager/package-lock.json +0 -1607
- package/template/claude-task-manager/tests/test-ai-search.js +0 -61
- package/template/claude-task-manager/tests/test-editor-ux.js +0 -76
- package/template/claude-task-manager/tests/test-editor-ux2.js +0 -51
- package/template/claude-task-manager/tests/test-features-v2.js +0 -127
- package/template/claude-task-manager/tests/test-insights-cached.js +0 -78
- package/template/claude-task-manager/tests/test-insights.js +0 -124
- package/template/claude-task-manager/tests/test-permissions-v2.js +0 -127
- package/template/claude-task-manager/tests/test-permissions.js +0 -122
- package/template/claude-task-manager/tests/test-pin.js +0 -51
- package/template/claude-task-manager/tests/test-prompts.js +0 -164
- package/template/claude-task-manager/tests/test-recent-sessions.js +0 -96
- package/template/claude-task-manager/tests/test-review.js +0 -104
- package/template/claude-task-manager/tests/test-send-dropdown.js +0 -76
- package/template/claude-task-manager/tests/test-send-final.js +0 -30
- package/template/claude-task-manager/tests/test-send-fixes.js +0 -76
- package/template/claude-task-manager/tests/test-send-integration.js +0 -107
- package/template/claude-task-manager/tests/test-send-visual.js +0 -34
- package/template/claude-task-manager/tests/test-session-create.js +0 -147
- package/template/claude-task-manager/tests/test-sidebar-ux.js +0 -83
- package/template/claude-task-manager/tests/test-url-hash.js +0 -68
- package/template/claude-task-manager/tests/test-ux-crop.js +0 -34
- package/template/claude-task-manager/tests/test-ux-review.js +0 -130
- package/template/claude-task-manager/tests/test-zoom-card.js +0 -76
- package/template/claude-task-manager/tests/test-zoom.js +0 -92
- package/template/claude-task-manager/tests/test-zoom2.js +0 -67
- package/template/docs/openclaw-vs-walle-comparison.md +0 -103
- package/template/docs/ux-improvement-plan.md +0 -84
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +0 -112
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +0 -326
- package/template/wall-e/package-lock.json +0 -533
- package/template/wall-e/skills/_bundled/slack-mentions/.watermark.json +0 -4
|
@@ -7,8 +7,8 @@ const brain = require(path.resolve(__dirname, '..', '..', '..', 'brain'));
|
|
|
7
7
|
const slackMcp = require(path.resolve(__dirname, '..', '..', '..', 'tools', 'slack-mcp'));
|
|
8
8
|
|
|
9
9
|
const WATERMARK_FILE = path.join(__dirname, '.watermark.json');
|
|
10
|
-
const OWNER_HANDLE = process.env.SLACK_OWNER_HANDLE || '
|
|
11
|
-
const OWNER_NAME = process.env.WALLE_OWNER_NAME || '
|
|
10
|
+
const OWNER_HANDLE = process.env.SLACK_OWNER_HANDLE || '';
|
|
11
|
+
const OWNER_NAME = process.env.WALLE_OWNER_NAME || '';
|
|
12
12
|
const PAUSE_MS = 800;
|
|
13
13
|
const WATCH_DURATION_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
14
14
|
|
|
@@ -49,6 +49,12 @@ function classifyMention(text) {
|
|
|
49
49
|
const cleaned = trimmed.replace(/@wall-?e/gi, '').trim();
|
|
50
50
|
if (/\?\s*$/.test(cleaned)) return 'question';
|
|
51
51
|
if (/^(what|who|where|when|why|how|is|are|do|does|did|can|could|would|should|will)\b/i.test(cleaned)) return 'question';
|
|
52
|
+
|
|
53
|
+
// Coding requests: action verbs + code concepts
|
|
54
|
+
const hasActionVerb = /\b(add|fix|build|create|implement|refactor|update|remove|delete|write|change|move|rename|extract|optimize)\b/i.test(cleaned);
|
|
55
|
+
const hasCodeConcept = /\b(api|endpoint|function|module|class|component|middleware|route|handler|test|database|schema|migration|config|server|client|ui|page|service|model)\b/i.test(cleaned);
|
|
56
|
+
if (hasActionVerb && hasCodeConcept) return 'coding';
|
|
57
|
+
|
|
52
58
|
return 'task';
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -244,6 +250,151 @@ async function pollWatchedThreads() {
|
|
|
244
250
|
|
|
245
251
|
// ── New mention processing ──
|
|
246
252
|
|
|
253
|
+
// Shared per-message processor — creates tasks/questions from a mention
|
|
254
|
+
async function _processMention(msg, seenTs, stats) {
|
|
255
|
+
const dedupeKey = msg.ts || msg.permalink || msg.content;
|
|
256
|
+
if (seenTs.has(dedupeKey)) return;
|
|
257
|
+
seenTs.add(dedupeKey);
|
|
258
|
+
|
|
259
|
+
const now = new Date();
|
|
260
|
+
const sourceRef = (msg.permalink || (msg.channelId ? `${msg.channelId}:${msg.ts || 'unknown'}` : `unknown:${msg.ts}`)).split('?')[0];
|
|
261
|
+
const existing = brain.listTasks({ source: 'slack' }).find(t => t.source_ref && t.source_ref.split('?')[0] === sourceRef);
|
|
262
|
+
if (existing) {
|
|
263
|
+
console.log(`[slack-mentions] Skipping already-processed mention: ${sourceRef}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
stats.newMentions++;
|
|
268
|
+
const kind = classifyMention(msg.content);
|
|
269
|
+
const effectiveTs = msg.ts || msg.isoTimestamp || now.toISOString();
|
|
270
|
+
const threadTs = msg.threadTs || msg.ts;
|
|
271
|
+
|
|
272
|
+
// Fetch full thread context
|
|
273
|
+
let threadContext = null;
|
|
274
|
+
if (msg.channelId && threadTs) {
|
|
275
|
+
threadContext = await fetchThreadContext(msg.channelId, threadTs);
|
|
276
|
+
await sleep(PAUSE_MS);
|
|
277
|
+
}
|
|
278
|
+
const contextBlock = threadContext ? `\n\nFull thread context:\n${threadContext}` : '';
|
|
279
|
+
|
|
280
|
+
if (kind === 'coding') {
|
|
281
|
+
const title = extractTaskTitle(msg.content);
|
|
282
|
+
try {
|
|
283
|
+
const { id } = brain.insertTask({
|
|
284
|
+
title: title.slice(0, 100),
|
|
285
|
+
description: `Coding request from Slack:\n\n${msg.content}`,
|
|
286
|
+
priority: 'normal',
|
|
287
|
+
type: 'once',
|
|
288
|
+
execution: 'skill',
|
|
289
|
+
skill: 'coding-agent',
|
|
290
|
+
skill_config: JSON.stringify({
|
|
291
|
+
request: msg.content.replace(/@wall-?e/gi, '').trim(),
|
|
292
|
+
cwd: process.env.HOME || '/tmp',
|
|
293
|
+
options: { delivery: 'commit' },
|
|
294
|
+
}),
|
|
295
|
+
source: 'slack',
|
|
296
|
+
source_ref: sourceRef,
|
|
297
|
+
});
|
|
298
|
+
stats.tasksCreated++;
|
|
299
|
+
console.log(`[slack-mentions] Coding task created: ${id} — ${title}`);
|
|
300
|
+
|
|
301
|
+
if (msg.channelId) {
|
|
302
|
+
try {
|
|
303
|
+
await slackMcp.callSlackMcp('slack_send_message', {
|
|
304
|
+
channel_id: msg.channelId,
|
|
305
|
+
message: `On it — planning the implementation now. I'll update this thread when done.`,
|
|
306
|
+
thread_ts: threadTs,
|
|
307
|
+
});
|
|
308
|
+
} catch (replyErr) {
|
|
309
|
+
console.warn(`[slack-mentions] Could not reply in thread: ${replyErr.message}`);
|
|
310
|
+
}
|
|
311
|
+
await sleep(PAUSE_MS);
|
|
312
|
+
}
|
|
313
|
+
} catch (taskErr) {
|
|
314
|
+
console.error(`[slack-mentions] Failed to create coding task: ${taskErr.message}`);
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (kind === 'task') {
|
|
320
|
+
const title = extractTaskTitle(msg.content);
|
|
321
|
+
try {
|
|
322
|
+
const { id } = brain.insertTask({
|
|
323
|
+
title,
|
|
324
|
+
description: `Slack task from ${OWNER_NAME}:\n\n${msg.content}${contextBlock}\n\n---\n**IMPORTANT**: When done, reply with results in the Slack thread using mcp_call with:\n- server: slack\n- tool: slack_send_message\n- arguments: { "channel_id": "${msg.channelId}", "thread_ts": "${threadTs}", "message": "<your results>" }`,
|
|
325
|
+
priority: 'normal',
|
|
326
|
+
type: 'once',
|
|
327
|
+
execution: 'chat',
|
|
328
|
+
source: 'slack',
|
|
329
|
+
source_ref: sourceRef,
|
|
330
|
+
});
|
|
331
|
+
stats.tasksCreated++;
|
|
332
|
+
console.log(`[slack-mentions] Task created: ${id} — ${title}`);
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
brain.upsertSlackThread({ channelId: msg.channelId, threadTs, taskId: id, sessionId: `task-${id}` });
|
|
336
|
+
} catch (upsertErr) {
|
|
337
|
+
console.error(`[slack-mentions] FAILED to upsert watched thread: ${upsertErr.message}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (msg.channelId) {
|
|
341
|
+
try {
|
|
342
|
+
await slackMcp.callSlackMcp('slack_send_message', {
|
|
343
|
+
channel_id: msg.channelId,
|
|
344
|
+
message: `Got it! I've created a task for this. I'll update you here when it's done.`,
|
|
345
|
+
thread_ts: threadTs,
|
|
346
|
+
});
|
|
347
|
+
} catch (replyErr) {
|
|
348
|
+
console.warn(`[slack-mentions] Could not reply in thread: ${replyErr.message}`);
|
|
349
|
+
}
|
|
350
|
+
await sleep(PAUSE_MS);
|
|
351
|
+
}
|
|
352
|
+
} catch (taskErr) {
|
|
353
|
+
console.error(`[slack-mentions] Failed to create task: ${taskErr.message}`);
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
stats.questionsFound++;
|
|
357
|
+
console.log(`[slack-mentions] Question detected: ${msg.content.slice(0, 80)}...`);
|
|
358
|
+
|
|
359
|
+
if (msg.channelId) {
|
|
360
|
+
try {
|
|
361
|
+
await slackMcp.callSlackMcp('slack_send_message', {
|
|
362
|
+
channel_id: msg.channelId,
|
|
363
|
+
message: `Let me look into that — I'll get back to you shortly.`,
|
|
364
|
+
thread_ts: threadTs,
|
|
365
|
+
});
|
|
366
|
+
} catch (replyErr) {
|
|
367
|
+
console.warn(`[slack-mentions] Could not reply in thread: ${replyErr.message}`);
|
|
368
|
+
}
|
|
369
|
+
await sleep(PAUSE_MS);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const { id } = brain.insertTask({
|
|
374
|
+
title: `Answer Slack question: ${extractTaskTitle(msg.content)}`,
|
|
375
|
+
description: `Question from ${OWNER_NAME} in Slack:\n\n${msg.content}${contextBlock}\n\n---\n**IMPORTANT**: After answering, you MUST reply in the Slack thread using mcp_call with:\n- server: slack\n- tool: slack_send_message\n- arguments: { "channel_id": "${msg.channelId}", "thread_ts": "${threadTs}", "message": "<your answer>" }`,
|
|
376
|
+
priority: 'high',
|
|
377
|
+
type: 'once',
|
|
378
|
+
execution: 'chat',
|
|
379
|
+
source: 'slack',
|
|
380
|
+
source_ref: sourceRef,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
brain.upsertSlackThread({ channelId: msg.channelId, threadTs, taskId: id, sessionId: `task-${id}` });
|
|
385
|
+
} catch (upsertErr) {
|
|
386
|
+
console.error(`[slack-mentions] FAILED to upsert watched thread: ${upsertErr.message}`);
|
|
387
|
+
}
|
|
388
|
+
} catch (taskErr) {
|
|
389
|
+
console.error(`[slack-mentions] Failed to create question task: ${taskErr.message}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (effectiveTs > (stats.maxTs || '')) {
|
|
394
|
+
stats.maxTs = effectiveTs;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
247
398
|
async function processNewMentions() {
|
|
248
399
|
const lastTs = loadWatermark();
|
|
249
400
|
const now = new Date();
|
|
@@ -252,14 +403,12 @@ async function processNewMentions() {
|
|
|
252
403
|
|
|
253
404
|
console.log(`[slack-mentions] Checking for @walle mentions (watermark: ${lastTs || 'none'})`);
|
|
254
405
|
|
|
255
|
-
|
|
256
|
-
let tasksCreated = 0;
|
|
257
|
-
let questionsFound = 0;
|
|
258
|
-
let maxTs = lastTs;
|
|
406
|
+
const stats = { newMentions: 0, tasksCreated: 0, questionsFound: 0, maxTs: lastTs };
|
|
259
407
|
const seenTs = new Set();
|
|
260
408
|
|
|
261
409
|
const queries = ['@walle', '@wall-e'];
|
|
262
410
|
|
|
411
|
+
// Phase 2a: Search-based discovery
|
|
263
412
|
for (const query of queries) {
|
|
264
413
|
try {
|
|
265
414
|
const result = await slackMcp.callSlackMcp('slack_search_public_and_private', {
|
|
@@ -280,124 +429,15 @@ async function processNewMentions() {
|
|
|
280
429
|
if (lastTs && msg.ts && msg.ts <= lastTs) continue;
|
|
281
430
|
if (lastTs && msg.isoTimestamp && msg.isoTimestamp <= lastTs) continue;
|
|
282
431
|
|
|
283
|
-
const dedupeKey = msg.ts || msg.permalink || msg.content;
|
|
284
|
-
if (seenTs.has(dedupeKey)) continue;
|
|
285
|
-
seenTs.add(dedupeKey);
|
|
286
|
-
|
|
287
432
|
const senderName = (msg.fromName || '').toLowerCase();
|
|
288
433
|
const isFromOwner = senderName.includes(OWNER_HANDLE.toLowerCase()) ||
|
|
289
434
|
senderName.includes(OWNER_NAME.split(' ')[0].toLowerCase());
|
|
290
|
-
|
|
291
435
|
if (!isFromOwner) {
|
|
292
436
|
console.log(`[slack-mentions] Skipping non-owner mention from: ${msg.fromName || 'unknown'}`);
|
|
293
437
|
continue;
|
|
294
438
|
}
|
|
295
439
|
|
|
296
|
-
|
|
297
|
-
const kind = classifyMention(msg.content);
|
|
298
|
-
const effectiveTs = msg.ts || msg.isoTimestamp || now.toISOString();
|
|
299
|
-
|
|
300
|
-
const rawRef = msg.permalink || (msg.channelId ? `${msg.channelId}:${msg.ts || 'unknown'}` : `${kind}:${effectiveTs}`);
|
|
301
|
-
const sourceRef = rawRef.split('?')[0];
|
|
302
|
-
const existing = brain.listTasks({ source: 'slack' }).find(t => t.source_ref && t.source_ref.split('?')[0] === sourceRef);
|
|
303
|
-
if (existing) {
|
|
304
|
-
console.log(`[slack-mentions] Skipping already-processed mention: ${sourceRef}`);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Fetch full thread context before creating the task
|
|
309
|
-
const threadTs = msg.threadTs || msg.ts;
|
|
310
|
-
let threadContext = null;
|
|
311
|
-
if (msg.channelId && threadTs) {
|
|
312
|
-
threadContext = await fetchThreadContext(msg.channelId, threadTs);
|
|
313
|
-
await sleep(PAUSE_MS);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const contextBlock = threadContext
|
|
317
|
-
? `\n\nFull thread context:\n${threadContext}`
|
|
318
|
-
: '';
|
|
319
|
-
|
|
320
|
-
if (kind === 'task') {
|
|
321
|
-
const title = extractTaskTitle(msg.content);
|
|
322
|
-
try {
|
|
323
|
-
const { id } = brain.insertTask({
|
|
324
|
-
title,
|
|
325
|
-
description: `Slack task from ${OWNER_NAME}:\n\n${msg.content}${contextBlock}\n\n---\n**IMPORTANT**: When done, reply with results in the Slack thread using mcp_call with:\n- server: slack\n- tool: slack_send_message\n- arguments: { "channel_id": "${msg.channelId}", "thread_ts": "${threadTs}", "message": "<your results>" }`,
|
|
326
|
-
priority: 'normal',
|
|
327
|
-
type: 'once',
|
|
328
|
-
execution: 'chat',
|
|
329
|
-
source: 'slack',
|
|
330
|
-
source_ref: sourceRef,
|
|
331
|
-
});
|
|
332
|
-
tasksCreated++;
|
|
333
|
-
console.log(`[slack-mentions] Task created: ${id} — ${title}`);
|
|
334
|
-
|
|
335
|
-
// Watch this thread for follow-ups (in DB)
|
|
336
|
-
try {
|
|
337
|
-
const threadId1 = brain.upsertSlackThread({ channelId: msg.channelId, threadTs, taskId: id, sessionId: `task-${id}` });
|
|
338
|
-
console.log(`[slack-mentions] Upserted watched thread: ${threadId1} (task, channel=${msg.channelId})`);
|
|
339
|
-
} catch (upsertErr) {
|
|
340
|
-
console.error(`[slack-mentions] FAILED to upsert watched thread: ${upsertErr.message} (channelId=${msg.channelId}, threadTs=${threadTs})`);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (msg.channelId) {
|
|
344
|
-
try {
|
|
345
|
-
await slackMcp.callSlackMcp('slack_send_message', {
|
|
346
|
-
channel_id: msg.channelId,
|
|
347
|
-
message: `Got it! I've created a task for this. I'll update you here when it's done.`,
|
|
348
|
-
thread_ts: threadTs,
|
|
349
|
-
});
|
|
350
|
-
} catch (replyErr) {
|
|
351
|
-
console.warn(`[slack-mentions] Could not reply in thread: ${replyErr.message}`);
|
|
352
|
-
}
|
|
353
|
-
await sleep(PAUSE_MS);
|
|
354
|
-
}
|
|
355
|
-
} catch (taskErr) {
|
|
356
|
-
console.error(`[slack-mentions] Failed to create task: ${taskErr.message}`);
|
|
357
|
-
}
|
|
358
|
-
} else {
|
|
359
|
-
questionsFound++;
|
|
360
|
-
console.log(`[slack-mentions] Question detected: ${msg.content.slice(0, 80)}...`);
|
|
361
|
-
|
|
362
|
-
if (msg.channelId) {
|
|
363
|
-
try {
|
|
364
|
-
await slackMcp.callSlackMcp('slack_send_message', {
|
|
365
|
-
channel_id: msg.channelId,
|
|
366
|
-
message: `Let me look into that — I'll get back to you shortly.`,
|
|
367
|
-
thread_ts: threadTs,
|
|
368
|
-
});
|
|
369
|
-
} catch (replyErr) {
|
|
370
|
-
console.warn(`[slack-mentions] Could not reply in thread: ${replyErr.message}`);
|
|
371
|
-
}
|
|
372
|
-
await sleep(PAUSE_MS);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
try {
|
|
376
|
-
const { id } = brain.insertTask({
|
|
377
|
-
title: `Answer Slack question: ${extractTaskTitle(msg.content)}`,
|
|
378
|
-
description: `Question from ${OWNER_NAME} in Slack:\n\n${msg.content}${contextBlock}\n\n---\n**IMPORTANT**: After answering, you MUST reply in the Slack thread using mcp_call with:\n- server: slack\n- tool: slack_send_message\n- arguments: { "channel_id": "${msg.channelId}", "thread_ts": "${threadTs}", "message": "<your answer>" }`,
|
|
379
|
-
priority: 'high',
|
|
380
|
-
type: 'once',
|
|
381
|
-
execution: 'chat',
|
|
382
|
-
source: 'slack',
|
|
383
|
-
source_ref: sourceRef,
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// Watch this thread for follow-ups (in DB)
|
|
387
|
-
try {
|
|
388
|
-
const threadId2 = brain.upsertSlackThread({ channelId: msg.channelId, threadTs, taskId: id, sessionId: `task-${id}` });
|
|
389
|
-
console.log(`[slack-mentions] Upserted watched thread: ${threadId2} (question, channel=${msg.channelId})`);
|
|
390
|
-
} catch (upsertErr) {
|
|
391
|
-
console.error(`[slack-mentions] FAILED to upsert watched thread: ${upsertErr.message} (channelId=${msg.channelId}, threadTs=${threadTs})`);
|
|
392
|
-
}
|
|
393
|
-
} catch (taskErr) {
|
|
394
|
-
console.error(`[slack-mentions] Failed to create question task: ${taskErr.message}`);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (effectiveTs > (maxTs || '')) {
|
|
399
|
-
maxTs = effectiveTs;
|
|
400
|
-
}
|
|
440
|
+
await _processMention(msg, seenTs, stats);
|
|
401
441
|
}
|
|
402
442
|
} catch (err) {
|
|
403
443
|
if (err.message.includes('401') || err.message.includes('expired')) {
|
|
@@ -411,15 +451,122 @@ async function processNewMentions() {
|
|
|
411
451
|
await sleep(PAUSE_MS);
|
|
412
452
|
}
|
|
413
453
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
454
|
+
// Phase 2b: DM channel polling — catches thread replies that search misses
|
|
455
|
+
try {
|
|
456
|
+
await pollDmChannels(lastTs, seenTs, async (msg, seen) => {
|
|
457
|
+
await _processMention(msg, seen, stats);
|
|
458
|
+
});
|
|
459
|
+
} catch (err) {
|
|
460
|
+
console.warn(`[slack-mentions] DM polling error: ${err.message}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (stats.maxTs && stats.maxTs !== lastTs) {
|
|
464
|
+
saveWatermark(stats.maxTs);
|
|
465
|
+
console.log(`[slack-mentions] Watermark updated: ${stats.maxTs}`);
|
|
417
466
|
}
|
|
418
467
|
|
|
419
|
-
if (newMentions === 0) {
|
|
468
|
+
if (stats.newMentions === 0) {
|
|
420
469
|
console.log('[slack-mentions] No new mentions.');
|
|
421
470
|
} else {
|
|
422
|
-
console.log(`[slack-mentions] Processed ${newMentions} mention(s): ${tasksCreated} task(s), ${questionsFound} question(s).`);
|
|
471
|
+
console.log(`[slack-mentions] Processed ${stats.newMentions} mention(s): ${stats.tasksCreated} task(s), ${stats.questionsFound} question(s).`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── DM channel polling (catches thread replies that search misses) ──
|
|
476
|
+
|
|
477
|
+
function parseDmMessages(rawText) {
|
|
478
|
+
const messages = [];
|
|
479
|
+
// Slack MCP read_channel returns messages in a structured format
|
|
480
|
+
const blocks = rawText.split(/(?=--- (?:Message|Reply) \d)/);
|
|
481
|
+
|
|
482
|
+
for (const block of blocks) {
|
|
483
|
+
if (!block.trim()) continue;
|
|
484
|
+
|
|
485
|
+
const fromMatch = block.match(/From:\s*(.+?)(?:\s*\(ID:\s*(\w+)\)|\n)/);
|
|
486
|
+
const fromName = fromMatch ? fromMatch[1].trim() : null;
|
|
487
|
+
const fromUserId = fromMatch ? fromMatch[2] : null;
|
|
488
|
+
|
|
489
|
+
const tsMatch = block.match(/(?:Message[_ ]ts|TS|Timestamp):\s*(\d+\.\d+)/i);
|
|
490
|
+
const msgTs = tsMatch ? tsMatch[1] : null;
|
|
491
|
+
|
|
492
|
+
const threadMatch = block.match(/Thread[_ ]ts:\s*(\d+\.\d+)/i);
|
|
493
|
+
const threadTs = threadMatch ? threadMatch[1] : null;
|
|
494
|
+
|
|
495
|
+
const linkMatch = block.match(/Permalink:\s*\[.*?\]\((https?:\/\/[^\s)]+)\)/);
|
|
496
|
+
let permalink = linkMatch ? linkMatch[1] : null;
|
|
497
|
+
if (permalink) permalink = permalink.replace(/^(https:\/\/\w+)\.slack\.com/, '$1.enterprise.slack.com');
|
|
498
|
+
|
|
499
|
+
// Extract text content — everything after metadata lines
|
|
500
|
+
const textMatch = block.match(/(?:Text|Content|Message):\s*\n?([\s\S]*?)(?:\n---|$)/i);
|
|
501
|
+
let content = textMatch ? textMatch[1].trim() : '';
|
|
502
|
+
if (!content) {
|
|
503
|
+
// Fallback: grab everything after the last metadata line
|
|
504
|
+
const lines = block.split('\n');
|
|
505
|
+
const metaEnd = lines.reduce((last, l, i) => /^(?:From|Time|TS|Thread|Permalink|Channel):/.test(l) ? i : last, -1);
|
|
506
|
+
content = lines.slice(metaEnd + 1).join('\n').trim();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (msgTs && /@wall-?e/i.test(content)) {
|
|
510
|
+
messages.push({ ts: msgTs, threadTs, fromName, fromUserId, permalink, content });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return messages;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function pollDmChannels(lastTs, seenTs, processMsg) {
|
|
518
|
+
// Gather known channel IDs from existing Slack tasks and watched threads
|
|
519
|
+
const channelIds = new Set();
|
|
520
|
+
|
|
521
|
+
const allTasks = brain.listTasks({ source: 'slack' });
|
|
522
|
+
for (const t of allTasks) {
|
|
523
|
+
if (t.source_ref) {
|
|
524
|
+
const m = t.source_ref.match(/\/archives\/([A-Z0-9]+)\//);
|
|
525
|
+
if (m) channelIds.add(m[1]);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const activeThreads = brain.listActiveSlackThreads(WATCH_DURATION_MS);
|
|
530
|
+
for (const th of activeThreads) {
|
|
531
|
+
if (th.channel_id) channelIds.add(th.channel_id);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (channelIds.size === 0) return;
|
|
535
|
+
|
|
536
|
+
// Only poll DM channels (D prefix) and group DMs (G prefix) — skip public channels to avoid noise
|
|
537
|
+
const dmChannels = [...channelIds].filter(id => /^[DG]/.test(id));
|
|
538
|
+
if (dmChannels.length === 0) return;
|
|
539
|
+
|
|
540
|
+
console.log(`[slack-mentions] Polling ${dmChannels.length} DM channel(s) for missed mentions`);
|
|
541
|
+
|
|
542
|
+
for (const channelId of dmChannels) {
|
|
543
|
+
try {
|
|
544
|
+
const result = await slackMcp.callSlackMcp('slack_read_channel', {
|
|
545
|
+
channel_id: channelId,
|
|
546
|
+
limit: 20,
|
|
547
|
+
});
|
|
548
|
+
const rawText = extractText(result);
|
|
549
|
+
if (!rawText || rawText.length < 10) continue;
|
|
550
|
+
|
|
551
|
+
const messages = parseDmMessages(rawText);
|
|
552
|
+
for (const msg of messages) {
|
|
553
|
+
if (lastTs && msg.ts <= lastTs) continue;
|
|
554
|
+
if (seenTs.has(msg.ts)) continue;
|
|
555
|
+
|
|
556
|
+
// Check sender is owner
|
|
557
|
+
const senderName = (msg.fromName || '').toLowerCase();
|
|
558
|
+
const isFromOwner = senderName.includes(OWNER_HANDLE.toLowerCase()) ||
|
|
559
|
+
senderName.includes(OWNER_NAME.split(' ')[0].toLowerCase());
|
|
560
|
+
if (!isFromOwner) continue;
|
|
561
|
+
|
|
562
|
+
msg.channelId = channelId;
|
|
563
|
+
await processMsg(msg, seenTs);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
await sleep(PAUSE_MS);
|
|
567
|
+
} catch (err) {
|
|
568
|
+
console.warn(`[slack-mentions] DM poll error for ${channelId}: ${err.message}`);
|
|
569
|
+
}
|
|
423
570
|
}
|
|
424
571
|
}
|
|
425
572
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: my-data-fetcher
|
|
3
|
+
description: Fetches data from an external source and stores observations as memories
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
execution: agent
|
|
6
|
+
trigger:
|
|
7
|
+
type: interval
|
|
8
|
+
interval_ms: 3600000
|
|
9
|
+
tags: [data, sync]
|
|
10
|
+
requires:
|
|
11
|
+
env: []
|
|
12
|
+
config:
|
|
13
|
+
source_url:
|
|
14
|
+
type: string
|
|
15
|
+
description: "URL or identifier of the data source"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Data Fetcher
|
|
19
|
+
|
|
20
|
+
Use the available tools to fetch data from the configured source. Parse the response
|
|
21
|
+
and return a structured list of observations. Each observation should include:
|
|
22
|
+
|
|
23
|
+
- A clear subject line
|
|
24
|
+
- The relevant data points
|
|
25
|
+
- A timestamp if available
|
|
26
|
+
|
|
27
|
+
Focus on new or changed data since the last run.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: my-action
|
|
3
|
+
description: A manually triggered one-shot action
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
execution: agent
|
|
6
|
+
trigger:
|
|
7
|
+
type: manual
|
|
8
|
+
tags: [utility]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Manual Action
|
|
12
|
+
|
|
13
|
+
When triggered, perform the following steps:
|
|
14
|
+
|
|
15
|
+
1. Gather the necessary context
|
|
16
|
+
2. Execute the action
|
|
17
|
+
3. Report the result
|
|
18
|
+
|
|
19
|
+
This skill runs on-demand only — it will not run automatically.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: my-checker
|
|
3
|
+
description: Periodically checks a condition and reports changes
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
execution: agent
|
|
6
|
+
trigger:
|
|
7
|
+
type: interval
|
|
8
|
+
interval_ms: 300000
|
|
9
|
+
tags: [monitoring]
|
|
10
|
+
config:
|
|
11
|
+
check_target:
|
|
12
|
+
type: string
|
|
13
|
+
description: "What to monitor (URL, file path, service name)"
|
|
14
|
+
alert_on:
|
|
15
|
+
type: string
|
|
16
|
+
default: change
|
|
17
|
+
description: "When to alert: 'change', 'error', 'threshold'"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Periodic Checker
|
|
21
|
+
|
|
22
|
+
Check the configured target at each interval. Compare the current state
|
|
23
|
+
with the previous run. If a notable change is detected:
|
|
24
|
+
|
|
25
|
+
1. Summarize what changed
|
|
26
|
+
2. Assess the importance (low/medium/high)
|
|
27
|
+
3. Store the observation as a memory
|
|
28
|
+
|
|
29
|
+
Only report meaningful changes — skip routine/expected variations.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: my-script
|
|
3
|
+
description: Runs a custom Node.js script
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
execution: script
|
|
6
|
+
entry: run.js
|
|
7
|
+
trigger:
|
|
8
|
+
type: manual
|
|
9
|
+
tags: [automation]
|
|
10
|
+
config:
|
|
11
|
+
input_path:
|
|
12
|
+
type: string
|
|
13
|
+
description: "Path to input file or directory"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Script Runner
|
|
17
|
+
|
|
18
|
+
This skill executes `run.js` in the skill directory. The script receives
|
|
19
|
+
configuration via the `WALL_E_SKILL_CONFIG` environment variable (JSON).
|
|
20
|
+
|
|
21
|
+
Create a `run.js` file alongside this SKILL.md with your logic.
|
|
@@ -59,10 +59,15 @@ function readClaudeSkills() {
|
|
|
59
59
|
} catch {}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
// Deduplicate: keep latest entry per name (last one wins since deeper/newer versions scan later)
|
|
63
|
+
const seen = new Map();
|
|
64
|
+
for (const s of skills) {
|
|
65
|
+
seen.set(s.name, s);
|
|
66
|
+
}
|
|
67
|
+
return Array.from(seen.values());
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
function scanPluginSkills(dir, skills, depth) {
|
|
70
|
+
function scanPluginSkills(dir, skills, depth, parentName) {
|
|
66
71
|
if ((depth || 0) > 4) return;
|
|
67
72
|
try {
|
|
68
73
|
for (const entry of fs.readdirSync(dir)) {
|
|
@@ -77,11 +82,18 @@ function scanPluginSkills(dir, skills, depth) {
|
|
|
77
82
|
try {
|
|
78
83
|
const content = fs.readFileSync(sfPath, 'utf8');
|
|
79
84
|
const description = extractDescription(content);
|
|
80
|
-
|
|
85
|
+
// Derive name from path: prefer parent dir over version-like entry names
|
|
86
|
+
const isVersion = /^[\da-f]{6,}$|^\d+\.\d+/.test(entry);
|
|
87
|
+
const name = isVersion ? (parentName || entry) : entry;
|
|
88
|
+
// Derive group from path (e.g., "slack" from .../slack/1.0.0/README.md)
|
|
89
|
+
const pathParts = sfPath.split(path.sep);
|
|
90
|
+
const pluginIdx = pathParts.findIndex(p => p.includes('plugins'));
|
|
91
|
+
const group = pluginIdx >= 0 && pluginIdx + 1 < pathParts.length ? pathParts[pluginIdx + 1] : '';
|
|
92
|
+
skills.push({ name, description, source: 'plugin', path: sfPath, group });
|
|
81
93
|
} catch {}
|
|
82
94
|
}
|
|
83
95
|
}
|
|
84
|
-
scanPluginSkills(fullPath, skills, (depth || 0) + 1);
|
|
96
|
+
scanPluginSkills(fullPath, skills, (depth || 0) + 1, entry);
|
|
85
97
|
}
|
|
86
98
|
}
|
|
87
99
|
} catch {}
|
|
@@ -26,8 +26,12 @@ async function runSkill(skill, opts = {}) {
|
|
|
26
26
|
let memoriesCreated = 0;
|
|
27
27
|
|
|
28
28
|
// Agentic loop — Claude may make multiple tool calls
|
|
29
|
-
|
|
29
|
+
// Allow SKILL.md to override max_turns and timeout_ms
|
|
30
|
+
const MAX_TURNS = opts.max_turns || 5;
|
|
31
|
+
const timeoutMs = opts.timeout_ms || 120000;
|
|
32
|
+
try {
|
|
30
33
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
34
|
+
if (Date.now() - startTime > timeoutMs) break;
|
|
31
35
|
const response = await client.messages.create({
|
|
32
36
|
model: opts.model || process.env.WALLE_MODEL || 'claude-haiku-4-5-20251001',
|
|
33
37
|
max_tokens: 4096,
|
|
@@ -98,6 +102,24 @@ async function runSkill(skill, opts = {}) {
|
|
|
98
102
|
brain.updateSkillStats(skill.id, success);
|
|
99
103
|
|
|
100
104
|
return { success, memoriesCreated, toolCalls: allToolCalls.length, duration };
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const duration = Date.now() - startTime;
|
|
107
|
+
// Store full error stack for debugging
|
|
108
|
+
const errorDetail = err.stack || err.message || String(err);
|
|
109
|
+
try {
|
|
110
|
+
brain.insertSkillExecution({
|
|
111
|
+
skill_id: skill.id,
|
|
112
|
+
status: 'failure',
|
|
113
|
+
tool_calls: JSON.stringify(allToolCalls),
|
|
114
|
+
tool_results: JSON.stringify(allToolResults.map(r => ({ name: r.name, success: r.success, error: r.error }))),
|
|
115
|
+
memories_created: memoriesCreated,
|
|
116
|
+
error: errorDetail,
|
|
117
|
+
duration_ms: duration,
|
|
118
|
+
});
|
|
119
|
+
brain.updateSkillStats(skill.id, false);
|
|
120
|
+
} catch {}
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
101
123
|
}
|
|
102
124
|
|
|
103
125
|
function buildDefaultPrompt(skill, ownerName) {
|