seclaw 0.1.1 → 0.1.3
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/dist/cli.js +130 -55
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -61,14 +61,12 @@ async function fetchTunnelUrlFromLogs(cwd) {
|
|
|
61
61
|
try {
|
|
62
62
|
const result = await execa(
|
|
63
63
|
"docker",
|
|
64
|
-
["compose", "logs", "cloudflared", "--no-log-prefix"],
|
|
65
|
-
{ cwd
|
|
64
|
+
["compose", "logs", "cloudflared", "--no-log-prefix", "--tail", "50"],
|
|
65
|
+
{ cwd }
|
|
66
66
|
);
|
|
67
67
|
const combined = result.stdout + "\n" + result.stderr;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
);
|
|
71
|
-
return match ? match[0] : null;
|
|
68
|
+
const matches = [...combined.matchAll(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/g)];
|
|
69
|
+
return matches.length > 0 ? matches[matches.length - 1][0] : null;
|
|
72
70
|
} catch {
|
|
73
71
|
return null;
|
|
74
72
|
}
|
|
@@ -242,6 +240,17 @@ async function collectSetupAnswers(targetDir) {
|
|
|
242
240
|
composioApiKey = composioKey || "";
|
|
243
241
|
}
|
|
244
242
|
}
|
|
243
|
+
const defaultWorkspace = "./shared";
|
|
244
|
+
const workspaceInput = await p.text({
|
|
245
|
+
message: "Workspace directory (where agent stores files)",
|
|
246
|
+
placeholder: defaultWorkspace,
|
|
247
|
+
defaultValue: defaultWorkspace,
|
|
248
|
+
validate: (v) => {
|
|
249
|
+
if (!v) return "Workspace path cannot be empty";
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
if (p.isCancel(workspaceInput)) process.exit(0);
|
|
253
|
+
const workspacePath = workspaceInput || defaultWorkspace;
|
|
245
254
|
const templateOptions = [
|
|
246
255
|
{
|
|
247
256
|
value: "productivity-agent",
|
|
@@ -278,7 +287,8 @@ async function collectSetupAnswers(targetDir) {
|
|
|
278
287
|
composioApiKey,
|
|
279
288
|
composioUserId: "",
|
|
280
289
|
template,
|
|
281
|
-
timezone: tz
|
|
290
|
+
timezone: tz,
|
|
291
|
+
workspacePath
|
|
282
292
|
};
|
|
283
293
|
}
|
|
284
294
|
async function composioFromBrowser() {
|
|
@@ -347,28 +357,23 @@ import { mkdir, writeFile as writeFile2, cp, rm, readFile as readFile2 } from "f
|
|
|
347
357
|
import { existsSync as existsSync3 } from "fs";
|
|
348
358
|
import { join as join2, resolve as resolve3 } from "path";
|
|
349
359
|
async function scaffoldProject(targetDir, answers) {
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
"shared/config",
|
|
357
|
-
"templates",
|
|
358
|
-
"commander",
|
|
359
|
-
"agent"
|
|
360
|
-
];
|
|
361
|
-
for (const dir of dirs) {
|
|
360
|
+
const ws = answers.workspacePath || "./shared";
|
|
361
|
+
const wsSubdirs = ["tasks", "reports", "notes", "drafts", "memory", "config"];
|
|
362
|
+
for (const sub of wsSubdirs) {
|
|
363
|
+
await mkdir(resolve3(targetDir, ws, sub), { recursive: true });
|
|
364
|
+
}
|
|
365
|
+
for (const dir of ["templates", "commander", "agent"]) {
|
|
362
366
|
await mkdir(join2(targetDir, dir), { recursive: true });
|
|
363
367
|
}
|
|
364
368
|
await writeEnv(targetDir, answers);
|
|
365
|
-
await writeDockerCompose(targetDir);
|
|
369
|
+
await writeDockerCompose(targetDir, ws);
|
|
370
|
+
await writeTunnelScript(targetDir, ws);
|
|
366
371
|
await writePermissions(targetDir);
|
|
367
372
|
await writeGitignore(targetDir);
|
|
368
373
|
await writeDockerignore(targetDir);
|
|
369
374
|
await copyAgentFiles(targetDir);
|
|
370
375
|
await copyCommanderFiles(targetDir);
|
|
371
|
-
await writeSeedFiles(targetDir, answers);
|
|
376
|
+
await writeSeedFiles(targetDir, answers, ws);
|
|
372
377
|
}
|
|
373
378
|
async function writeEnv(dir, answers) {
|
|
374
379
|
const config = getProviderConfig(answers.llmProvider);
|
|
@@ -386,6 +391,9 @@ async function writeEnv(dir, answers) {
|
|
|
386
391
|
lines.push(`COMPOSIO_API_KEY=${answers.composioApiKey}`);
|
|
387
392
|
lines.push(`COMPOSIO_USER_ID=${existing.COMPOSIO_USER_ID || answers.composioUserId || generateUserId()}`);
|
|
388
393
|
}
|
|
394
|
+
if (answers.workspacePath && answers.workspacePath !== "./shared") {
|
|
395
|
+
lines.push(`WORKSPACE_HOST_PATH=${answers.workspacePath}`);
|
|
396
|
+
}
|
|
389
397
|
if (existing.INNGEST_DEV) {
|
|
390
398
|
lines.push(`INNGEST_DEV=${existing.INNGEST_DEV}`);
|
|
391
399
|
}
|
|
@@ -415,7 +423,7 @@ function generateUserId() {
|
|
|
415
423
|
}
|
|
416
424
|
return id;
|
|
417
425
|
}
|
|
418
|
-
async function writeDockerCompose(dir) {
|
|
426
|
+
async function writeDockerCompose(dir, workspacePath = "./shared") {
|
|
419
427
|
const content = `services:
|
|
420
428
|
inngest:
|
|
421
429
|
image: inngest/inngest:latest
|
|
@@ -433,7 +441,7 @@ async function writeDockerCompose(dir) {
|
|
|
433
441
|
image: seclaw/agent:latest
|
|
434
442
|
restart: unless-stopped
|
|
435
443
|
volumes:
|
|
436
|
-
-
|
|
444
|
+
- ${workspacePath}:/workspace:rw
|
|
437
445
|
- ./templates:/templates:ro
|
|
438
446
|
env_file:
|
|
439
447
|
- .env
|
|
@@ -458,7 +466,12 @@ async function writeDockerCompose(dir) {
|
|
|
458
466
|
cloudflared:
|
|
459
467
|
image: cloudflare/cloudflared:latest
|
|
460
468
|
restart: unless-stopped
|
|
461
|
-
|
|
469
|
+
entrypoint: ["/bin/sh", "/tunnel-start.sh"]
|
|
470
|
+
volumes:
|
|
471
|
+
- ${workspacePath}:/workspace:rw
|
|
472
|
+
- ./tunnel-start.sh:/tunnel-start.sh:ro
|
|
473
|
+
env_file:
|
|
474
|
+
- .env
|
|
462
475
|
networks:
|
|
463
476
|
- agent-net
|
|
464
477
|
depends_on:
|
|
@@ -478,7 +491,7 @@ async function writeDockerCompose(dir) {
|
|
|
478
491
|
image: seclaw/desktop-commander:latest
|
|
479
492
|
restart: unless-stopped
|
|
480
493
|
volumes:
|
|
481
|
-
-
|
|
494
|
+
- ${workspacePath}:/workspace:rw
|
|
482
495
|
- ./permissions.yml:/permissions.yml:ro
|
|
483
496
|
security_opt:
|
|
484
497
|
- no-new-privileges:true
|
|
@@ -501,6 +514,40 @@ networks:
|
|
|
501
514
|
`;
|
|
502
515
|
await writeFile2(join2(dir, "docker-compose.yml"), content);
|
|
503
516
|
}
|
|
517
|
+
async function writeTunnelScript(dir, _workspacePath = "./shared") {
|
|
518
|
+
const script = `#!/bin/sh
|
|
519
|
+
# Runs cloudflared and auto-updates Telegram webhook when tunnel URL changes.
|
|
520
|
+
# This solves the "webhook points to old tunnel" problem on container restart.
|
|
521
|
+
|
|
522
|
+
cloudflared tunnel --no-autoupdate --url http://agent:3000 2>&1 | while IFS= read -r line; do
|
|
523
|
+
# Forward all output to stderr so docker logs still works
|
|
524
|
+
printf '%s\\n' "$line" >&2
|
|
525
|
+
|
|
526
|
+
# Detect tunnel URL in log output
|
|
527
|
+
case "$line" in
|
|
528
|
+
*https://*trycloudflare.com*)
|
|
529
|
+
# Extract URL using shell-only (no grep dependency)
|
|
530
|
+
URL=$(echo "$line" | sed -n 's|.*\\(https://[a-z0-9-]*\\.trycloudflare\\.com\\).*|\\1|p')
|
|
531
|
+
if [ -z "$URL" ]; then continue; fi
|
|
532
|
+
|
|
533
|
+
# Write to shared volume so agent/CLI can read it
|
|
534
|
+
echo "$URL" > /workspace/.tunnel-url
|
|
535
|
+
|
|
536
|
+
# Auto-update Telegram webhook if bot token is available
|
|
537
|
+
if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
|
|
538
|
+
wget -q -O /dev/null \\
|
|
539
|
+
--post-data='{"url":"'"$URL"'/webhook","allowed_updates":["message","callback_query"]}' \\
|
|
540
|
+
--header='Content-Type: application/json' \\
|
|
541
|
+
"https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" 2>/dev/null && \\
|
|
542
|
+
printf '[tunnel] Webhook updated: %s\\n' "$URL" >&2 || \\
|
|
543
|
+
printf '[tunnel] Webhook update failed\\n' >&2
|
|
544
|
+
fi
|
|
545
|
+
;;
|
|
546
|
+
esac
|
|
547
|
+
done
|
|
548
|
+
`;
|
|
549
|
+
await writeFile2(join2(dir, "tunnel-start.sh"), script, { mode: 493 });
|
|
550
|
+
}
|
|
504
551
|
async function copyAgentFiles(dir) {
|
|
505
552
|
const agentTemplateSrc = resolve3(import.meta.dirname, "runtime");
|
|
506
553
|
const agentDest = join2(dir, "agent");
|
|
@@ -825,10 +872,11 @@ async function writeIfMissing(path, content) {
|
|
|
825
872
|
await writeFile2(path, content);
|
|
826
873
|
}
|
|
827
874
|
}
|
|
828
|
-
async function writeSeedFiles(dir, answers) {
|
|
875
|
+
async function writeSeedFiles(dir, answers, ws = "./shared") {
|
|
829
876
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
877
|
+
const wsDir = resolve3(dir, ws);
|
|
830
878
|
await writeIfMissing(
|
|
831
|
-
join2(
|
|
879
|
+
join2(wsDir, "memory/learnings.md"),
|
|
832
880
|
`# Agent Memory
|
|
833
881
|
|
|
834
882
|
## User Profile
|
|
@@ -844,7 +892,7 @@ async function writeSeedFiles(dir, answers) {
|
|
|
844
892
|
`
|
|
845
893
|
);
|
|
846
894
|
await writeIfMissing(
|
|
847
|
-
join2(
|
|
895
|
+
join2(wsDir, "tasks/welcome.md"),
|
|
848
896
|
`# Welcome Task
|
|
849
897
|
- [ ] Introduce yourself to the user on Telegram
|
|
850
898
|
- [ ] Explain what you can do (file management, email, task tracking)
|
|
@@ -853,7 +901,7 @@ async function writeSeedFiles(dir, answers) {
|
|
|
853
901
|
`
|
|
854
902
|
);
|
|
855
903
|
await writeIfMissing(
|
|
856
|
-
join2(
|
|
904
|
+
join2(wsDir, "config/agent.md"),
|
|
857
905
|
`# Agent Configuration
|
|
858
906
|
|
|
859
907
|
## Workspace Structure
|
|
@@ -892,11 +940,11 @@ async function writeSeedFiles(dir, answers) {
|
|
|
892
940
|
``
|
|
893
941
|
);
|
|
894
942
|
await writeIfMissing(
|
|
895
|
-
join2(
|
|
943
|
+
join2(wsDir, "config/integrations.md"),
|
|
896
944
|
integrations2.join("\n")
|
|
897
945
|
);
|
|
898
946
|
await writeIfMissing(
|
|
899
|
-
join2(
|
|
947
|
+
join2(wsDir, `reports/${today}.md`),
|
|
900
948
|
`# Daily Report \u2014 ${today}
|
|
901
949
|
|
|
902
950
|
## Status
|
|
@@ -1167,13 +1215,14 @@ async function create(directory) {
|
|
|
1167
1215
|
if (resolve5(templateSrc) !== resolve5(templateDest)) {
|
|
1168
1216
|
await cp2(templateSrc, templateDest, { recursive: true });
|
|
1169
1217
|
}
|
|
1218
|
+
const wsDir = resolve5(targetDir, answers.workspacePath || "./shared");
|
|
1170
1219
|
const promptSrc = resolve5(templateSrc, "system-prompt.md");
|
|
1171
1220
|
if (existsSync5(promptSrc)) {
|
|
1172
|
-
await cp2(promptSrc, resolve5(
|
|
1221
|
+
await cp2(promptSrc, resolve5(wsDir, "config", "system-prompt.md"));
|
|
1173
1222
|
}
|
|
1174
1223
|
const schedulesSrc = resolve5(templateSrc, "schedules.json");
|
|
1175
1224
|
if (existsSync5(schedulesSrc)) {
|
|
1176
|
-
await cp2(schedulesSrc, resolve5(
|
|
1225
|
+
await cp2(schedulesSrc, resolve5(wsDir, "config", "schedules.json"));
|
|
1177
1226
|
}
|
|
1178
1227
|
s.stop(`Template ready.`);
|
|
1179
1228
|
} else {
|
|
@@ -1212,7 +1261,7 @@ async function create(directory) {
|
|
|
1212
1261
|
s.stop(`Webhook URL: ${pc2.cyan(webhookUrl)}`);
|
|
1213
1262
|
}
|
|
1214
1263
|
}
|
|
1215
|
-
showSuccess(targetDir, tunnelUrl, answers.telegramBotName, answers.llmProvider, answers.composioApiKey);
|
|
1264
|
+
showSuccess(targetDir, tunnelUrl, answers.telegramBotName, answers.llmProvider, answers.composioApiKey, answers.workspacePath);
|
|
1216
1265
|
}
|
|
1217
1266
|
async function waitForAgent(targetDir, maxRetries = 30, intervalMs = 2e3) {
|
|
1218
1267
|
for (let i = 0; i < maxRetries; i++) {
|
|
@@ -1279,7 +1328,7 @@ var PROVIDER_LABELS = {
|
|
|
1279
1328
|
openai: { model: "GPT-4o", cost: "~$10-25/mo" },
|
|
1280
1329
|
gemini: { model: "Gemini 2.5 Pro", cost: "~$7-20/mo" }
|
|
1281
1330
|
};
|
|
1282
|
-
function showSuccess(targetDir, tunnelUrl, botName, provider, composioKey) {
|
|
1331
|
+
function showSuccess(targetDir, tunnelUrl, botName, provider, composioKey, workspacePath) {
|
|
1283
1332
|
const label = (s) => pc2.white(pc2.bold(s));
|
|
1284
1333
|
const lines = [
|
|
1285
1334
|
`${pc2.green(pc2.bold("Your AI agent is live!"))}`,
|
|
@@ -1291,7 +1340,9 @@ function showSuccess(targetDir, tunnelUrl, botName, provider, composioKey) {
|
|
|
1291
1340
|
if (botName) {
|
|
1292
1341
|
lines.push(`${label("Telegram:")} Open ${pc2.green(botName)} and send a message`);
|
|
1293
1342
|
}
|
|
1294
|
-
|
|
1343
|
+
const ws = workspacePath || "./shared";
|
|
1344
|
+
const wsAbsolute = resolve5(targetDir, ws);
|
|
1345
|
+
lines.push(`${label("Workspace:")} ${pc2.white(wsAbsolute + "/")}`);
|
|
1295
1346
|
if (provider) {
|
|
1296
1347
|
const info = PROVIDER_LABELS[provider];
|
|
1297
1348
|
if (info) {
|
|
@@ -1420,7 +1471,14 @@ async function add(template, options) {
|
|
|
1420
1471
|
return;
|
|
1421
1472
|
}
|
|
1422
1473
|
}
|
|
1423
|
-
|
|
1474
|
+
let wsHostPath = "shared";
|
|
1475
|
+
try {
|
|
1476
|
+
const envContent = await readFile3(resolve6(process.cwd(), ".env"), "utf-8");
|
|
1477
|
+
const wsMatch = envContent.match(/WORKSPACE_HOST_PATH=(.+)/);
|
|
1478
|
+
if (wsMatch?.[1]?.trim()) wsHostPath = wsMatch[1].trim();
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
const configDir = resolve6(process.cwd(), wsHostPath, "config");
|
|
1424
1482
|
if (existsSync6(configDir)) {
|
|
1425
1483
|
const capDir = resolve6(configDir, "capabilities", template);
|
|
1426
1484
|
await mkdir3(capDir, { recursive: true });
|
|
@@ -2042,7 +2100,14 @@ async function status() {
|
|
|
2042
2100
|
}
|
|
2043
2101
|
} catch {
|
|
2044
2102
|
}
|
|
2045
|
-
|
|
2103
|
+
let wsPath = "shared";
|
|
2104
|
+
try {
|
|
2105
|
+
const envContent = await readFile6(resolve9(projectDir, ".env"), "utf-8");
|
|
2106
|
+
const wsMatch = envContent.match(/WORKSPACE_HOST_PATH=(.+)/);
|
|
2107
|
+
if (wsMatch?.[1]?.trim()) wsPath = wsMatch[1].trim();
|
|
2108
|
+
} catch {
|
|
2109
|
+
}
|
|
2110
|
+
infoLines.push(`${pc6.white(pc6.bold("Workspace:"))} ${resolve9(projectDir, wsPath)}`);
|
|
2046
2111
|
infoLines.push(`${pc6.white(pc6.bold("Project:"))} ${projectDir}`);
|
|
2047
2112
|
p6.note(infoLines.join("\n"), "seclaw");
|
|
2048
2113
|
} catch {
|
|
@@ -2205,9 +2270,6 @@ async function doctor() {
|
|
|
2205
2270
|
p9.outro("Run again after fixing manually.");
|
|
2206
2271
|
return;
|
|
2207
2272
|
}
|
|
2208
|
-
s.start("Stopping conflicting containers...");
|
|
2209
|
-
await stopExistingSeclaw();
|
|
2210
|
-
s.stop(`${FIX} Cleared conflicting containers`);
|
|
2211
2273
|
for (const check of fixable) {
|
|
2212
2274
|
s.start(`Fixing: ${check.name}...`);
|
|
2213
2275
|
try {
|
|
@@ -2356,12 +2418,12 @@ async function checkTunnel(projectDir) {
|
|
|
2356
2418
|
try {
|
|
2357
2419
|
const result = await execa8(
|
|
2358
2420
|
"docker",
|
|
2359
|
-
["compose", "logs", "cloudflared", "--no-log-prefix"],
|
|
2421
|
+
["compose", "logs", "cloudflared", "--no-log-prefix", "--tail", "50"],
|
|
2360
2422
|
{ cwd: projectDir, env: { ...process.env, COMPOSE_PROJECT_NAME: getProjectName(projectDir) } }
|
|
2361
2423
|
);
|
|
2362
2424
|
const combined = result.stdout + "\n" + result.stderr;
|
|
2363
|
-
const
|
|
2364
|
-
if (
|
|
2425
|
+
const matches = [...combined.matchAll(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/g)];
|
|
2426
|
+
if (matches.length === 0) {
|
|
2365
2427
|
return {
|
|
2366
2428
|
name: "Tunnel",
|
|
2367
2429
|
ok: false,
|
|
@@ -2375,7 +2437,7 @@ async function checkTunnel(projectDir) {
|
|
|
2375
2437
|
}
|
|
2376
2438
|
};
|
|
2377
2439
|
}
|
|
2378
|
-
const tunnelUrl =
|
|
2440
|
+
const tunnelUrl = matches[matches.length - 1][0];
|
|
2379
2441
|
try {
|
|
2380
2442
|
const res = await fetch(`${tunnelUrl}/health`, {
|
|
2381
2443
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -2394,7 +2456,14 @@ async function checkTunnel(projectDir) {
|
|
|
2394
2456
|
name: "Tunnel",
|
|
2395
2457
|
ok: false,
|
|
2396
2458
|
message: `URL found (${pc9.dim(tunnelUrl)}) but not reachable`,
|
|
2397
|
-
tunnelUrl
|
|
2459
|
+
tunnelUrl,
|
|
2460
|
+
fix: async () => {
|
|
2461
|
+
await clearTunnelCache(projectDir);
|
|
2462
|
+
const env = { ...process.env, COMPOSE_PROJECT_NAME: getProjectName(projectDir) };
|
|
2463
|
+
await execa8("docker", ["compose", "restart", "cloudflared"], { cwd: projectDir, env });
|
|
2464
|
+
const newUrl = await getTunnelUrl(projectDir, 20);
|
|
2465
|
+
return newUrl ? `New tunnel: ${newUrl}` : "Restarted cloudflared \u2014 run doctor again";
|
|
2466
|
+
}
|
|
2398
2467
|
};
|
|
2399
2468
|
}
|
|
2400
2469
|
} catch {
|
|
@@ -2431,15 +2500,18 @@ async function checkTelegram(projectDir, tunnelCheck) {
|
|
|
2431
2500
|
}
|
|
2432
2501
|
const wh = whData.result;
|
|
2433
2502
|
const tunnelUrl = tunnelCheck.tunnelUrl || "";
|
|
2503
|
+
const webhookFix = async () => {
|
|
2504
|
+
const freshUrl = await getTunnelUrl(projectDir, 5);
|
|
2505
|
+
if (!freshUrl) return "No tunnel URL available \u2014 fix tunnel first";
|
|
2506
|
+
const ok = await setTelegramWebhook(botToken, freshUrl);
|
|
2507
|
+
return ok ? `Webhook set to ${freshUrl}` : "Could not set webhook";
|
|
2508
|
+
};
|
|
2434
2509
|
if (!wh.url) {
|
|
2435
2510
|
return {
|
|
2436
2511
|
name: `Telegram (${botName})`,
|
|
2437
2512
|
ok: false,
|
|
2438
2513
|
message: "Webhook not set",
|
|
2439
|
-
fix:
|
|
2440
|
-
const ok = await setTelegramWebhook(botToken, tunnelUrl);
|
|
2441
|
-
return ok ? "Webhook set" : "Could not set webhook";
|
|
2442
|
-
} : void 0
|
|
2514
|
+
fix: webhookFix
|
|
2443
2515
|
};
|
|
2444
2516
|
}
|
|
2445
2517
|
if (tunnelUrl && !wh.url.includes(tunnelUrl.replace("https://", ""))) {
|
|
@@ -2447,10 +2519,7 @@ async function checkTelegram(projectDir, tunnelCheck) {
|
|
|
2447
2519
|
name: `Telegram (${botName})`,
|
|
2448
2520
|
ok: false,
|
|
2449
2521
|
message: "Webhook points to old tunnel",
|
|
2450
|
-
fix:
|
|
2451
|
-
const ok = await setTelegramWebhook(botToken, tunnelUrl);
|
|
2452
|
-
return ok ? "Webhook updated" : "Could not update webhook";
|
|
2453
|
-
}
|
|
2522
|
+
fix: webhookFix
|
|
2454
2523
|
};
|
|
2455
2524
|
}
|
|
2456
2525
|
if (wh.last_error_message) {
|
|
@@ -2458,7 +2527,13 @@ async function checkTelegram(projectDir, tunnelCheck) {
|
|
|
2458
2527
|
return {
|
|
2459
2528
|
name: `Telegram (${botName})`,
|
|
2460
2529
|
ok: false,
|
|
2461
|
-
message: `Error ${age}m ago: ${wh.last_error_message}
|
|
2530
|
+
message: `Error ${age}m ago: ${wh.last_error_message}`,
|
|
2531
|
+
fix: async () => {
|
|
2532
|
+
const freshUrl = await getTunnelUrl(projectDir, 5);
|
|
2533
|
+
if (!freshUrl) return "No tunnel URL available \u2014 fix tunnel first";
|
|
2534
|
+
const ok = await setTelegramWebhook(botToken, freshUrl);
|
|
2535
|
+
return ok ? `Webhook re-set to ${freshUrl}` : "Could not set webhook";
|
|
2536
|
+
}
|
|
2462
2537
|
};
|
|
2463
2538
|
}
|
|
2464
2539
|
return {
|