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
|
@@ -5,6 +5,34 @@ const path = require('path');
|
|
|
5
5
|
const DATA_DIR = process.env.WALL_E_DATA_DIR || path.join(process.env.HOME, '.walle', 'data');
|
|
6
6
|
const BRAIN_DB_PATH = path.join(DATA_DIR, 'wall-e-brain.db');
|
|
7
7
|
|
|
8
|
+
// Build a SKILL.md from structured form data
|
|
9
|
+
function buildSkillMd(body) {
|
|
10
|
+
const lines = ['---'];
|
|
11
|
+
lines.push(`name: ${body.name}`);
|
|
12
|
+
if (body.description) lines.push(`description: ${body.description}`);
|
|
13
|
+
if (body.version) lines.push(`version: ${body.version}`);
|
|
14
|
+
if (body.author) lines.push(`author: ${body.author}`);
|
|
15
|
+
lines.push(`execution: ${body.execution || 'agent'}`);
|
|
16
|
+
if (body.entry) lines.push(`entry: ${body.entry}`);
|
|
17
|
+
lines.push('trigger:');
|
|
18
|
+
lines.push(` type: ${body.trigger_type || 'manual'}`);
|
|
19
|
+
if (body.interval_ms) lines.push(` interval_ms: ${body.interval_ms}`);
|
|
20
|
+
if (body.tags && body.tags.length) lines.push(`tags: [${body.tags.join(', ')}]`);
|
|
21
|
+
if (body.config_schema) {
|
|
22
|
+
lines.push('config:');
|
|
23
|
+
for (const [k, v] of Object.entries(body.config_schema)) {
|
|
24
|
+
lines.push(` ${k}:`);
|
|
25
|
+
if (v.type) lines.push(` type: ${v.type}`);
|
|
26
|
+
if (v.description) lines.push(` description: "${v.description}"`);
|
|
27
|
+
if (v.default !== undefined) lines.push(` default: ${v.default}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
lines.push('---');
|
|
31
|
+
lines.push('');
|
|
32
|
+
lines.push(body.instructions || `# ${body.name}\n\nDescribe what this skill does.`);
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
let readDb = null;
|
|
9
37
|
|
|
10
38
|
function getReadDb() {
|
|
@@ -561,6 +589,61 @@ function handleWalleApi(req, res, url) {
|
|
|
561
589
|
return true;
|
|
562
590
|
}
|
|
563
591
|
|
|
592
|
+
// POST /api/wall-e/chat/clear — clear all messages in a session (for branch switching)
|
|
593
|
+
if (p === '/api/wall-e/chat/clear' && m === 'POST') {
|
|
594
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
595
|
+
readBody(req).then(body => {
|
|
596
|
+
try {
|
|
597
|
+
brain.clearChatSession(body.session_id || 'default');
|
|
598
|
+
jsonResponse(res, { data: { ok: true } });
|
|
599
|
+
} catch (e) {
|
|
600
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
601
|
+
}
|
|
602
|
+
}).catch(e => jsonResponse(res, { error: e.message }, 400));
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// POST /api/wall-e/chat/insert — insert a single chat message (for branch sync)
|
|
607
|
+
if (p === '/api/wall-e/chat/insert' && m === 'POST') {
|
|
608
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
609
|
+
readBody(req).then(body => {
|
|
610
|
+
try {
|
|
611
|
+
brain.insertChatMessage({
|
|
612
|
+
role: body.role,
|
|
613
|
+
content: body.content,
|
|
614
|
+
session_id: body.session_id || 'default',
|
|
615
|
+
channel: body.channel || 'ctm',
|
|
616
|
+
});
|
|
617
|
+
jsonResponse(res, { data: { ok: true } });
|
|
618
|
+
} catch (e) {
|
|
619
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
620
|
+
}
|
|
621
|
+
}).catch(e => jsonResponse(res, { error: e.message }, 400));
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// GET /api/wall-e/chat/branches — load branch data for a session
|
|
626
|
+
if (p === '/api/wall-e/chat/branches' && m === 'GET') {
|
|
627
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
628
|
+
const sid = url.searchParams.get('session_id') || 'default';
|
|
629
|
+
try {
|
|
630
|
+
jsonResponse(res, { data: brain.loadChatBranches(sid) });
|
|
631
|
+
} catch (e) { jsonResponse(res, { error: e.message }, 500); }
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// POST /api/wall-e/chat/branches — save branch data for a session
|
|
636
|
+
if (p === '/api/wall-e/chat/branches' && m === 'POST') {
|
|
637
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
638
|
+
readBody(req).then(body => {
|
|
639
|
+
try {
|
|
640
|
+
brain.saveChatBranches(body.session_id || 'default', body.branches || {}, body.active || {}, body.history || []);
|
|
641
|
+
jsonResponse(res, { data: { ok: true } });
|
|
642
|
+
} catch (e) { jsonResponse(res, { error: e.message }, 500); }
|
|
643
|
+
}).catch(e => jsonResponse(res, { error: e.message }, 400));
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
|
|
564
647
|
// GET /api/wall-e/backups — list brain backups
|
|
565
648
|
if (p === '/api/wall-e/backups' && m === 'GET') {
|
|
566
649
|
if (!brain) return jsonResponse(res, { error: 'Brain not available' }, 503), true;
|
|
@@ -958,11 +1041,54 @@ function handleWalleApi(req, res, url) {
|
|
|
958
1041
|
|
|
959
1042
|
// GET /api/wall-e/skills
|
|
960
1043
|
if (p === '/api/wall-e/skills' && m === 'GET') {
|
|
961
|
-
if (!brain) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
1044
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
962
1045
|
try {
|
|
963
1046
|
const enabled = params.get('enabled');
|
|
964
1047
|
const filter = enabled !== null ? { enabled: parseInt(enabled) } : {};
|
|
965
|
-
|
|
1048
|
+
|
|
1049
|
+
// Auto-register any filesystem skills not yet in the DB
|
|
1050
|
+
try {
|
|
1051
|
+
const { loadAllSkills, reloadSkills } = require('./skills/skill-loader');
|
|
1052
|
+
reloadSkills(); // clear cache so new dirs are picked up
|
|
1053
|
+
const fsSkills = loadAllSkills();
|
|
1054
|
+
const dbSkills = brain.listSkills({});
|
|
1055
|
+
const dbNames = new Set(dbSkills.map(s => s.name));
|
|
1056
|
+
for (const skill of fsSkills) {
|
|
1057
|
+
if (dbNames.has(skill.name)) continue;
|
|
1058
|
+
const triggerType = (skill.trigger && skill.trigger.type) || skill.execution || 'manual';
|
|
1059
|
+
const triggerConfig = skill.trigger && skill.trigger.interval_ms
|
|
1060
|
+
? JSON.stringify({ interval_ms: skill.trigger.interval_ms }) : null;
|
|
1061
|
+
brain.insertSkill({
|
|
1062
|
+
name: skill.name,
|
|
1063
|
+
description: skill.description || '',
|
|
1064
|
+
trigger_type: triggerType,
|
|
1065
|
+
trigger_config: triggerConfig,
|
|
1066
|
+
prompt_template: skill.execution === 'script'
|
|
1067
|
+
? `INTERNAL_SKILL:${skill.name}` : skill.instructions || '',
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
} catch (syncErr) {
|
|
1071
|
+
console.warn('[api-walle] Skill sync error:', syncErr.message);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const dbSkills = brain.listSkills(filter);
|
|
1075
|
+
// Merge filesystem metadata (tags, source, config, requires, version, execution) into DB skills
|
|
1076
|
+
const { findSkill } = require('./skills/skill-loader');
|
|
1077
|
+
const data = dbSkills.map(s => {
|
|
1078
|
+
const fs = findSkill(s.name);
|
|
1079
|
+
if (!fs) return s;
|
|
1080
|
+
return Object.assign({}, s, {
|
|
1081
|
+
tags: fs.tags || [],
|
|
1082
|
+
source: fs.source || 'bundled',
|
|
1083
|
+
config_schema: fs.config || {},
|
|
1084
|
+
requires: fs.requires || {},
|
|
1085
|
+
version: fs.version || '0.0.0',
|
|
1086
|
+
execution: fs.execution || 'agent',
|
|
1087
|
+
author: fs.author || null,
|
|
1088
|
+
permissions: fs.permissions || [],
|
|
1089
|
+
skill_dir: fs.dir || null,
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
966
1092
|
jsonResponse(res, { data });
|
|
967
1093
|
} catch (e) {
|
|
968
1094
|
jsonResponse(res, { error: e.message }, 500);
|
|
@@ -1088,7 +1214,7 @@ function handleWalleApi(req, res, url) {
|
|
|
1088
1214
|
// GET /api/wall-e/skills/:id/executions
|
|
1089
1215
|
const skillExecMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/executions$/);
|
|
1090
1216
|
if (skillExecMatch && m === 'GET') {
|
|
1091
|
-
if (!brain) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
1217
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
1092
1218
|
try {
|
|
1093
1219
|
const limit = parseLimit(params, 20);
|
|
1094
1220
|
const data = brain.listSkillExecutions({ skill_id: skillExecMatch[1], limit });
|
|
@@ -1146,6 +1272,250 @@ function handleWalleApi(req, res, url) {
|
|
|
1146
1272
|
return true;
|
|
1147
1273
|
}
|
|
1148
1274
|
|
|
1275
|
+
// DELETE /api/wall-e/skills/:id
|
|
1276
|
+
const skillDeleteMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)$/);
|
|
1277
|
+
if (skillDeleteMatch && m === 'DELETE') {
|
|
1278
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
1279
|
+
try {
|
|
1280
|
+
const skill = brain.deleteSkill(skillDeleteMatch[1]);
|
|
1281
|
+
// If user-created skill, also delete the directory
|
|
1282
|
+
if (skill) {
|
|
1283
|
+
const { findSkill } = require('./skills/skill-loader');
|
|
1284
|
+
const fsSkill = findSkill(skill.name);
|
|
1285
|
+
if (fsSkill && fsSkill.source === 'user' && fsSkill.dir) {
|
|
1286
|
+
try {
|
|
1287
|
+
const fs = require('fs');
|
|
1288
|
+
fs.rmSync(fsSkill.dir, { recursive: true, force: true });
|
|
1289
|
+
} catch (rmErr) {
|
|
1290
|
+
console.warn('[api-walle] Could not delete skill dir:', rmErr.message);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const { reloadSkills } = require('./skills/skill-loader');
|
|
1294
|
+
reloadSkills();
|
|
1295
|
+
}
|
|
1296
|
+
jsonResponse(res, { data: { ok: true, deleted: skill?.name } });
|
|
1297
|
+
} catch (e) {
|
|
1298
|
+
jsonResponse(res, { error: e.message }, 400);
|
|
1299
|
+
}
|
|
1300
|
+
return true;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// PUT /api/wall-e/skills/:id/config — save user config overrides
|
|
1304
|
+
const skillConfigMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/config$/);
|
|
1305
|
+
if (skillConfigMatch && m === 'PUT') {
|
|
1306
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
1307
|
+
readBody(req).then(body => {
|
|
1308
|
+
try {
|
|
1309
|
+
brain.updateSkill(skillConfigMatch[1], {
|
|
1310
|
+
config_values: typeof body === 'string' ? body : JSON.stringify(body)
|
|
1311
|
+
});
|
|
1312
|
+
jsonResponse(res, { data: { ok: true } });
|
|
1313
|
+
} catch (e) {
|
|
1314
|
+
jsonResponse(res, { error: e.message }, 400);
|
|
1315
|
+
}
|
|
1316
|
+
}).catch(e => jsonResponse(res, { error: e.message }, 400));
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// GET /api/wall-e/skills/:id/source — read raw SKILL.md content
|
|
1321
|
+
const skillSourceReadMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/source$/);
|
|
1322
|
+
if (skillSourceReadMatch && m === 'GET') {
|
|
1323
|
+
try {
|
|
1324
|
+
// Try by DB id first, then by name
|
|
1325
|
+
let skillName = skillSourceReadMatch[1];
|
|
1326
|
+
if (brain && ensureBrainInit()) {
|
|
1327
|
+
const dbSkill = brain.getSkill(skillName);
|
|
1328
|
+
if (dbSkill) skillName = dbSkill.name;
|
|
1329
|
+
}
|
|
1330
|
+
const { findSkill } = require('./skills/skill-loader');
|
|
1331
|
+
const fsSkill = findSkill(skillName);
|
|
1332
|
+
if (!fsSkill) return jsonResponse(res, { error: 'Skill not found' }, 404), true;
|
|
1333
|
+
const fs = require('fs');
|
|
1334
|
+
const content = fs.readFileSync(fsSkill.filePath, 'utf8');
|
|
1335
|
+
jsonResponse(res, { data: { content, path: fsSkill.filePath, source: fsSkill.source } });
|
|
1336
|
+
} catch (e) {
|
|
1337
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
1338
|
+
}
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// PUT /api/wall-e/skills/:id/source — rewrite SKILL.md file
|
|
1343
|
+
const skillSourceWriteMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/source$/);
|
|
1344
|
+
if (skillSourceWriteMatch && m === 'PUT') {
|
|
1345
|
+
readBody(req).then(body => {
|
|
1346
|
+
try {
|
|
1347
|
+
let skillName = skillSourceWriteMatch[1];
|
|
1348
|
+
if (brain && ensureBrainInit()) {
|
|
1349
|
+
const dbSkill = brain.getSkill(skillName);
|
|
1350
|
+
if (dbSkill) skillName = dbSkill.name;
|
|
1351
|
+
}
|
|
1352
|
+
const { findSkill, reloadSkills } = require('./skills/skill-loader');
|
|
1353
|
+
const fsSkill = findSkill(skillName);
|
|
1354
|
+
if (!fsSkill) return jsonResponse(res, { error: 'Skill not found' }, 404);
|
|
1355
|
+
if (fsSkill.source === 'bundled') return jsonResponse(res, { error: 'Cannot edit bundled skills' }, 403);
|
|
1356
|
+
const fs = require('fs');
|
|
1357
|
+
fs.writeFileSync(fsSkill.filePath, body.content, 'utf8');
|
|
1358
|
+
reloadSkills();
|
|
1359
|
+
jsonResponse(res, { data: { ok: true } });
|
|
1360
|
+
} catch (e) {
|
|
1361
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
1362
|
+
}
|
|
1363
|
+
}).catch(e => jsonResponse(res, { error: e.message }, 400));
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// POST /api/wall-e/skills/create-file — create new skill directory + SKILL.md
|
|
1368
|
+
if (p === '/api/wall-e/skills/create-file' && m === 'POST') {
|
|
1369
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
1370
|
+
readBody(req).then(body => {
|
|
1371
|
+
try {
|
|
1372
|
+
const fs = require('fs');
|
|
1373
|
+
const path = require('path');
|
|
1374
|
+
const { USER_DIR, reloadSkills, parseSkillMd } = require('./skills/skill-loader');
|
|
1375
|
+
if (!body.name) return jsonResponse(res, { error: 'name is required' }, 400);
|
|
1376
|
+
const slug = body.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
1377
|
+
const skillDir = path.join(USER_DIR, slug);
|
|
1378
|
+
if (fs.existsSync(skillDir)) return jsonResponse(res, { error: 'Skill directory already exists' }, 409);
|
|
1379
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1380
|
+
const content = body.content || buildSkillMd(body);
|
|
1381
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content, 'utf8');
|
|
1382
|
+
reloadSkills();
|
|
1383
|
+
// Auto-register in DB
|
|
1384
|
+
const parsed = parseSkillMd(content);
|
|
1385
|
+
const triggerType = (parsed?.meta?.trigger?.type) || body.trigger_type || 'manual';
|
|
1386
|
+
const result = brain.insertSkill({
|
|
1387
|
+
name: body.name,
|
|
1388
|
+
description: body.description || parsed?.meta?.description || '',
|
|
1389
|
+
trigger_type: triggerType,
|
|
1390
|
+
prompt_template: parsed?.body || body.instructions || '',
|
|
1391
|
+
});
|
|
1392
|
+
jsonResponse(res, { data: { id: result.id, dir: skillDir } }, 201);
|
|
1393
|
+
} catch (e) {
|
|
1394
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
1395
|
+
}
|
|
1396
|
+
}).catch(e => jsonResponse(res, { error: e.message }, 400));
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// GET /api/wall-e/skills/:id/export — export skill as JSON bundle
|
|
1401
|
+
const skillExportMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/export$/);
|
|
1402
|
+
if (skillExportMatch && m === 'GET') {
|
|
1403
|
+
try {
|
|
1404
|
+
let skillName = skillExportMatch[1];
|
|
1405
|
+
if (brain && ensureBrainInit()) {
|
|
1406
|
+
const dbSkill = brain.getSkill(skillName);
|
|
1407
|
+
if (dbSkill) skillName = dbSkill.name;
|
|
1408
|
+
}
|
|
1409
|
+
const { findSkill } = require('./skills/skill-loader');
|
|
1410
|
+
const fsSkill = findSkill(skillName);
|
|
1411
|
+
if (!fsSkill) return jsonResponse(res, { error: 'Skill not found' }, 404), true;
|
|
1412
|
+
const fs = require('fs');
|
|
1413
|
+
const path = require('path');
|
|
1414
|
+
const bundle = { name: fsSkill.name, skill_md: fs.readFileSync(fsSkill.filePath, 'utf8'), files: {} };
|
|
1415
|
+
// Include other files in the skill directory
|
|
1416
|
+
const entries = fs.readdirSync(fsSkill.dir);
|
|
1417
|
+
for (const entry of entries) {
|
|
1418
|
+
if (entry === 'SKILL.md') continue;
|
|
1419
|
+
const fp = path.join(fsSkill.dir, entry);
|
|
1420
|
+
const stat = fs.statSync(fp);
|
|
1421
|
+
if (stat.isFile() && stat.size < 512 * 1024) {
|
|
1422
|
+
bundle.files[entry] = fs.readFileSync(fp, 'utf8');
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
jsonResponse(res, { data: bundle });
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
1428
|
+
}
|
|
1429
|
+
return true;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// POST /api/wall-e/skills/import — import skill from JSON bundle
|
|
1433
|
+
if (p === '/api/wall-e/skills/import' && m === 'POST') {
|
|
1434
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
1435
|
+
readBody(req).then(body => {
|
|
1436
|
+
try {
|
|
1437
|
+
const fs = require('fs');
|
|
1438
|
+
const path = require('path');
|
|
1439
|
+
const { USER_DIR, reloadSkills, parseSkillMd } = require('./skills/skill-loader');
|
|
1440
|
+
if (!body.skill_md) return jsonResponse(res, { error: 'skill_md is required' }, 400);
|
|
1441
|
+
const parsed = parseSkillMd(body.skill_md);
|
|
1442
|
+
if (!parsed || !parsed.meta || !parsed.meta.name) return jsonResponse(res, { error: 'Invalid SKILL.md' }, 400);
|
|
1443
|
+
const slug = parsed.meta.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
1444
|
+
const skillDir = path.join(USER_DIR, slug);
|
|
1445
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1446
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), body.skill_md, 'utf8');
|
|
1447
|
+
if (body.files) {
|
|
1448
|
+
for (const [fname, fcontent] of Object.entries(body.files)) {
|
|
1449
|
+
const safeName = path.basename(fname);
|
|
1450
|
+
fs.writeFileSync(path.join(skillDir, safeName), fcontent, 'utf8');
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
reloadSkills();
|
|
1454
|
+
const triggerType = (parsed.meta.trigger?.type) || 'manual';
|
|
1455
|
+
const result = brain.insertSkill({
|
|
1456
|
+
name: parsed.meta.name,
|
|
1457
|
+
description: parsed.meta.description || '',
|
|
1458
|
+
trigger_type: triggerType,
|
|
1459
|
+
prompt_template: parsed.body || '',
|
|
1460
|
+
});
|
|
1461
|
+
jsonResponse(res, { data: { id: result.id, name: parsed.meta.name, dir: skillDir } }, 201);
|
|
1462
|
+
} catch (e) {
|
|
1463
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
1464
|
+
}
|
|
1465
|
+
}).catch(e => jsonResponse(res, { error: e.message }, 400));
|
|
1466
|
+
return true;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// GET /api/wall-e/skills/templates — list available starter templates
|
|
1470
|
+
if (p === '/api/wall-e/skills/templates' && m === 'GET') {
|
|
1471
|
+
try {
|
|
1472
|
+
const fs = require('fs');
|
|
1473
|
+
const tplDir = path.join(__dirname, 'skills', '_templates');
|
|
1474
|
+
const templates = [];
|
|
1475
|
+
if (fs.existsSync(tplDir)) {
|
|
1476
|
+
for (const file of fs.readdirSync(tplDir)) {
|
|
1477
|
+
if (!file.endsWith('.md')) continue;
|
|
1478
|
+
const content = fs.readFileSync(path.join(tplDir, file), 'utf8');
|
|
1479
|
+
const { parseSkillMd } = require('./skills/skill-loader');
|
|
1480
|
+
const parsed = parseSkillMd(content);
|
|
1481
|
+
templates.push({
|
|
1482
|
+
id: file.replace('.md', ''),
|
|
1483
|
+
name: parsed?.meta?.name || file.replace('.md', ''),
|
|
1484
|
+
description: parsed?.meta?.description || '',
|
|
1485
|
+
execution: parsed?.meta?.execution || 'agent',
|
|
1486
|
+
trigger: parsed?.meta?.trigger?.type || 'manual',
|
|
1487
|
+
content,
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
jsonResponse(res, { data: templates });
|
|
1492
|
+
} catch (e) {
|
|
1493
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
1494
|
+
}
|
|
1495
|
+
return true;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// POST /api/wall-e/skills/:id/validate — pre-flight validation
|
|
1499
|
+
const skillValidateMatch = p.match(/^\/api\/wall-e\/skills\/([^/]+)\/validate$/);
|
|
1500
|
+
if (skillValidateMatch && m === 'POST') {
|
|
1501
|
+
try {
|
|
1502
|
+
let skillName = skillValidateMatch[1];
|
|
1503
|
+
if (brain && ensureBrainInit()) {
|
|
1504
|
+
const dbSkill = brain.getSkill(skillName);
|
|
1505
|
+
if (dbSkill) skillName = dbSkill.name;
|
|
1506
|
+
}
|
|
1507
|
+
const { findSkill } = require('./skills/skill-loader');
|
|
1508
|
+
const fsSkill = findSkill(skillName);
|
|
1509
|
+
if (!fsSkill) return jsonResponse(res, { error: 'Skill not found' }, 404), true;
|
|
1510
|
+
const { validateSkillRequirements } = require('./skills/skill-validator');
|
|
1511
|
+
const result = validateSkillRequirements(fsSkill);
|
|
1512
|
+
jsonResponse(res, { data: result });
|
|
1513
|
+
} catch (e) {
|
|
1514
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
1515
|
+
}
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1149
1519
|
// --- Slack Ingest ---
|
|
1150
1520
|
|
|
1151
1521
|
// GET /api/wall-e/slack-ingest/progress
|
|
@@ -1266,6 +1636,85 @@ function handleWalleApi(req, res, url) {
|
|
|
1266
1636
|
return true;
|
|
1267
1637
|
}
|
|
1268
1638
|
|
|
1639
|
+
// POST /api/wall-e/coding — start a coding task
|
|
1640
|
+
if (p === '/api/wall-e/coding' && m === 'POST') {
|
|
1641
|
+
if (!brain || !ensureBrainInit()) return jsonResponse(res, { error: 'Brain module not available' }, 503), true;
|
|
1642
|
+
readBody(req).then(body => {
|
|
1643
|
+
try {
|
|
1644
|
+
if (!body.request) {
|
|
1645
|
+
jsonResponse(res, { error: 'request field is required' }, 400);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
const result = brain.insertTask({
|
|
1649
|
+
title: (body.request || '').slice(0, 100),
|
|
1650
|
+
description: `Coding request: ${body.request}`,
|
|
1651
|
+
priority: body.priority || 'normal',
|
|
1652
|
+
type: 'once',
|
|
1653
|
+
execution: 'skill',
|
|
1654
|
+
skill: 'coding-agent',
|
|
1655
|
+
skill_config: JSON.stringify({
|
|
1656
|
+
request: body.request,
|
|
1657
|
+
cwd: body.cwd || process.cwd(),
|
|
1658
|
+
options: body.options || { delivery: 'commit' },
|
|
1659
|
+
}),
|
|
1660
|
+
source: 'api',
|
|
1661
|
+
source_ref: '',
|
|
1662
|
+
});
|
|
1663
|
+
jsonResponse(res, { ok: true, task_id: result.id });
|
|
1664
|
+
} catch (e) {
|
|
1665
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
1666
|
+
}
|
|
1667
|
+
}).catch(e => jsonResponse(res, { error: e.message }, 500));
|
|
1668
|
+
return true;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// GET /api/wall-e/config — read wall-e-config.json
|
|
1672
|
+
if (p === '/api/wall-e/config' && m === 'GET') {
|
|
1673
|
+
try {
|
|
1674
|
+
const fs = require('fs');
|
|
1675
|
+
const cfgPath = require('path').join(__dirname, 'wall-e-config.json');
|
|
1676
|
+
if (!fs.existsSync(cfgPath)) return jsonResponse(res, { data: {} }), true;
|
|
1677
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
1678
|
+
// Redact tokens — only send whether they're set (show last 4 chars if long enough)
|
|
1679
|
+
const safe = JSON.parse(JSON.stringify(cfg));
|
|
1680
|
+
const redact = (t) => t && t.length > 8 ? '••••' + t.slice(-4) : '••••';
|
|
1681
|
+
if (safe.channels?.telegram?.bot_token) safe.channels.telegram.bot_token = redact(safe.channels.telegram.bot_token);
|
|
1682
|
+
if (safe.channels?.slack_dm?.bot_token) safe.channels.slack_dm.bot_token = redact(safe.channels.slack_dm.bot_token);
|
|
1683
|
+
return jsonResponse(res, { data: safe }), true;
|
|
1684
|
+
} catch (e) {
|
|
1685
|
+
return jsonResponse(res, { error: e.message }, 500), true;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// PUT /api/wall-e/config — update wall-e-config.json (partial merge)
|
|
1690
|
+
if (p === '/api/wall-e/config' && m === 'PUT') {
|
|
1691
|
+
return readBody(req).then(body => {
|
|
1692
|
+
try {
|
|
1693
|
+
const fs = require('fs');
|
|
1694
|
+
const cfgPath = require('path').join(__dirname, 'wall-e-config.json');
|
|
1695
|
+
let cfg = {};
|
|
1696
|
+
if (fs.existsSync(cfgPath)) cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
1697
|
+
// Deep merge channels
|
|
1698
|
+
if (body.channels) {
|
|
1699
|
+
if (!cfg.channels) cfg.channels = {};
|
|
1700
|
+
for (const [ch, val] of Object.entries(body.channels)) {
|
|
1701
|
+
if (!cfg.channels[ch]) cfg.channels[ch] = {};
|
|
1702
|
+
for (const [k, v] of Object.entries(val)) {
|
|
1703
|
+
// Skip redacted token values (don't overwrite real token with masked one)
|
|
1704
|
+
if (typeof v === 'string' && v.startsWith('••••')) continue;
|
|
1705
|
+
cfg.channels[ch][k] = v;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
1710
|
+
jsonResponse(res, { ok: true, message: 'Config saved. Restart Wall-E for changes to take effect.' });
|
|
1711
|
+
} catch (e) {
|
|
1712
|
+
jsonResponse(res, { error: e.message }, 500);
|
|
1713
|
+
}
|
|
1714
|
+
}).catch(e => jsonResponse(res, { error: e.message }, 500));
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1269
1718
|
// No matching route under /api/wall-e
|
|
1270
1719
|
jsonResponse(res, { error: 'Not found' }, 404);
|
|
1271
1720
|
return true;
|
package/template/wall-e/brain.js
CHANGED
|
@@ -86,6 +86,10 @@ function initDb(dbPath) {
|
|
|
86
86
|
}
|
|
87
87
|
try { newDb.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_tasks_source'").get() ||
|
|
88
88
|
newDb.prepare("CREATE INDEX IF NOT EXISTS idx_tasks_source ON tasks(source)").run(); } catch (_) {}
|
|
89
|
+
// Migration: add config_values column to skills table
|
|
90
|
+
try { newDb.prepare("SELECT config_values FROM skills LIMIT 0").run(); } catch (_) {
|
|
91
|
+
newDb.prepare("ALTER TABLE skills ADD COLUMN config_values TEXT").run();
|
|
92
|
+
}
|
|
89
93
|
} catch (err) {
|
|
90
94
|
newDb.close();
|
|
91
95
|
db = null;
|
|
@@ -317,6 +321,13 @@ function createTables() {
|
|
|
317
321
|
CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id, created_at);
|
|
318
322
|
CREATE INDEX IF NOT EXISTS idx_chat_messages_time ON chat_messages(created_at);
|
|
319
323
|
|
|
324
|
+
CREATE TABLE IF NOT EXISTS chat_branches (
|
|
325
|
+
session_id TEXT PRIMARY KEY,
|
|
326
|
+
branches TEXT NOT NULL DEFAULT '{}',
|
|
327
|
+
active TEXT NOT NULL DEFAULT '{}',
|
|
328
|
+
history TEXT NOT NULL DEFAULT '[]'
|
|
329
|
+
);
|
|
330
|
+
|
|
320
331
|
CREATE TABLE IF NOT EXISTS skills (
|
|
321
332
|
id TEXT PRIMARY KEY,
|
|
322
333
|
name TEXT NOT NULL UNIQUE,
|
|
@@ -1048,6 +1059,28 @@ function clearChatSession(session_id) {
|
|
|
1048
1059
|
getDb().prepare('DELETE FROM chat_messages WHERE session_id = ?').run(session_id);
|
|
1049
1060
|
}
|
|
1050
1061
|
|
|
1062
|
+
// ── Chat Branches (ChatGPT-style edit/resend) ──
|
|
1063
|
+
|
|
1064
|
+
function saveChatBranches(session_id, branches, active, history) {
|
|
1065
|
+
getDb().prepare(`
|
|
1066
|
+
INSERT INTO chat_branches (session_id, branches, active, history)
|
|
1067
|
+
VALUES (?, ?, ?, ?)
|
|
1068
|
+
ON CONFLICT(session_id) DO UPDATE SET branches = excluded.branches, active = excluded.active, history = excluded.history
|
|
1069
|
+
`).run(session_id, JSON.stringify(branches), JSON.stringify(active), JSON.stringify(history || []));
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function loadChatBranches(session_id) {
|
|
1073
|
+
const row = getDb().prepare('SELECT branches, active, history FROM chat_branches WHERE session_id = ?').get(session_id);
|
|
1074
|
+
if (!row) return { branches: {}, active: {}, history: [] };
|
|
1075
|
+
try {
|
|
1076
|
+
return {
|
|
1077
|
+
branches: JSON.parse(row.branches),
|
|
1078
|
+
active: JSON.parse(row.active),
|
|
1079
|
+
history: JSON.parse(row.history || '[]'),
|
|
1080
|
+
};
|
|
1081
|
+
} catch (e) { return { branches: {}, active: {}, history: [] }; }
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1051
1084
|
// -- Skills CRUD --
|
|
1052
1085
|
|
|
1053
1086
|
function insertSkill({ name, description, tool_definitions, trigger_type, trigger_config, prompt_template }) {
|
|
@@ -1081,7 +1114,7 @@ function listSkills({ enabled } = {}) {
|
|
|
1081
1114
|
return getDb().prepare('SELECT * FROM skills ORDER BY name').all();
|
|
1082
1115
|
}
|
|
1083
1116
|
|
|
1084
|
-
const SKILL_ALLOWED_FIELDS = ['name', 'description', 'tool_definitions', 'trigger_type', 'trigger_config', 'prompt_template', 'enabled'];
|
|
1117
|
+
const SKILL_ALLOWED_FIELDS = ['name', 'description', 'tool_definitions', 'trigger_type', 'trigger_config', 'prompt_template', 'enabled', 'config_values'];
|
|
1085
1118
|
|
|
1086
1119
|
function updateSkill(id, updates) {
|
|
1087
1120
|
if (!updates || typeof updates !== 'object') throw new Error('Updates must be an object');
|
|
@@ -1137,6 +1170,14 @@ function updateSkillStats(skillId, success) {
|
|
|
1137
1170
|
if (result.changes === 0) throw new Error(`Skill not found: ${skillId}`);
|
|
1138
1171
|
}
|
|
1139
1172
|
|
|
1173
|
+
function deleteSkill(id) {
|
|
1174
|
+
const skill = getDb().prepare('SELECT * FROM skills WHERE id = ?').get(id);
|
|
1175
|
+
if (!skill) throw new Error(`Skill not found: ${id}`);
|
|
1176
|
+
getDb().prepare('DELETE FROM skill_executions WHERE skill_id = ?').run(id);
|
|
1177
|
+
getDb().prepare('DELETE FROM skills WHERE id = ?').run(id);
|
|
1178
|
+
return skill;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1140
1181
|
// ── Tasks ──
|
|
1141
1182
|
|
|
1142
1183
|
function insertTask({ title, description, priority, type, execution, script, schedule, due_at, next_run_at, skill, skill_config, source, source_ref }) {
|
|
@@ -1412,6 +1453,8 @@ module.exports = {
|
|
|
1412
1453
|
searchChatMessages,
|
|
1413
1454
|
deleteChatMessages,
|
|
1414
1455
|
clearChatSession,
|
|
1456
|
+
saveChatBranches,
|
|
1457
|
+
loadChatBranches,
|
|
1415
1458
|
// Backup
|
|
1416
1459
|
createBackup,
|
|
1417
1460
|
listBackups,
|
|
@@ -1423,6 +1466,7 @@ module.exports = {
|
|
|
1423
1466
|
getSkillByName,
|
|
1424
1467
|
listSkills,
|
|
1425
1468
|
updateSkill,
|
|
1469
|
+
deleteSkill,
|
|
1426
1470
|
insertSkillExecution,
|
|
1427
1471
|
listSkillExecutions,
|
|
1428
1472
|
updateSkillStats,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { Bot } = require('grammy');
|
|
3
|
+
const ChannelBase = require('./channel-base');
|
|
4
|
+
const eventBus = require('../events/event-bus');
|
|
5
|
+
|
|
6
|
+
class TelegramChannel extends ChannelBase {
|
|
7
|
+
constructor(opts = {}) {
|
|
8
|
+
super('telegram');
|
|
9
|
+
this.botToken = opts.botToken || process.env.TELEGRAM_BOT_TOKEN || null;
|
|
10
|
+
this.ownerChatId = opts.ownerChatId ? Number(opts.ownerChatId) : null;
|
|
11
|
+
this._onMessage = opts.onMessage || null;
|
|
12
|
+
this._bot = null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async start() {
|
|
16
|
+
if (!this.botToken) {
|
|
17
|
+
console.log('[telegram] No bot token configured, skipping');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this._bot = new Bot(this.botToken);
|
|
22
|
+
|
|
23
|
+
// Setup helper — tells the user their chat ID for config
|
|
24
|
+
this._bot.command('whoami', (ctx) => {
|
|
25
|
+
ctx.reply(`Your chat ID: ${ctx.from.id}`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Text message handler
|
|
29
|
+
this._bot.on('message:text', async (ctx) => {
|
|
30
|
+
const senderId = ctx.from.id;
|
|
31
|
+
const text = ctx.message.text;
|
|
32
|
+
|
|
33
|
+
// Owner-only gate (both should be Numbers, but coerce for safety)
|
|
34
|
+
if (this.ownerChatId && Number(senderId) !== Number(this.ownerChatId)) {
|
|
35
|
+
await ctx.reply('This is a private assistant.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`[telegram] Message from ${senderId}: ${text.slice(0, 50)}...`);
|
|
40
|
+
eventBus.emitMessage('telegram', text, String(senderId));
|
|
41
|
+
|
|
42
|
+
if (this._onMessage) {
|
|
43
|
+
try {
|
|
44
|
+
const response = await this._onMessage(text, String(senderId));
|
|
45
|
+
if (response) {
|
|
46
|
+
// Telegram has a 4096 char limit per message — split if needed
|
|
47
|
+
const MAX_LEN = 4096;
|
|
48
|
+
if (response.length <= MAX_LEN) {
|
|
49
|
+
await ctx.reply(response);
|
|
50
|
+
} else {
|
|
51
|
+
for (let i = 0; i < response.length; i += MAX_LEN) {
|
|
52
|
+
await ctx.reply(response.slice(i, i + MAX_LEN));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error('[telegram] onMessage error:', err.message);
|
|
58
|
+
await ctx.reply('Sorry, something went wrong processing your message.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Error handler
|
|
64
|
+
this._bot.catch((err) => {
|
|
65
|
+
console.error('[telegram] Bot error:', err.message);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Start long polling (non-blocking)
|
|
69
|
+
this._bot.start({
|
|
70
|
+
onStart: () => {
|
|
71
|
+
this.running = true;
|
|
72
|
+
console.log('[telegram] Bot started (long polling)');
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async stop() {
|
|
78
|
+
this.running = false;
|
|
79
|
+
if (this._bot) {
|
|
80
|
+
await this._bot.stop();
|
|
81
|
+
console.log('[telegram] Bot stopped');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async send(chatId, message) {
|
|
86
|
+
if (!this._bot) return;
|
|
87
|
+
try {
|
|
88
|
+
await this._bot.api.sendMessage(chatId, message);
|
|
89
|
+
console.log(`[telegram] Sent message to ${chatId}`);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('[telegram] Send error:', err.message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = TelegramChannel;
|