seclaw 0.1.10 → 0.1.12

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 CHANGED
@@ -73,7 +73,20 @@ async function fetchTunnelUrlFromLogs(cwd) {
73
73
  }
74
74
  async function setTelegramWebhook(botToken, tunnelUrl) {
75
75
  const webhookUrl = `${tunnelUrl}/webhook`;
76
- for (let attempt = 0; attempt < 3; attempt++) {
76
+ const hostname = tunnelUrl.replace("https://", "");
77
+ for (let i = 0; i < 30; i++) {
78
+ try {
79
+ const { resolve4: resolve42 } = await import("dns/promises");
80
+ await resolve42(hostname);
81
+ break;
82
+ } catch {
83
+ if (i === 29) {
84
+ break;
85
+ }
86
+ await new Promise((r) => setTimeout(r, 2e3));
87
+ }
88
+ }
89
+ for (let attempt = 0; attempt < 5; attempt++) {
77
90
  try {
78
91
  const res = await fetch(
79
92
  `https://api.telegram.org/bot${botToken}/setWebhook`,
@@ -89,9 +102,13 @@ async function setTelegramWebhook(botToken, tunnelUrl) {
89
102
  );
90
103
  const data = await res.json();
91
104
  if (data.ok) return true;
105
+ if (data.description?.includes("resolve host")) {
106
+ await new Promise((r) => setTimeout(r, 5e3));
107
+ continue;
108
+ }
92
109
  } catch {
93
110
  }
94
- if (attempt < 2) await new Promise((r) => setTimeout(r, 2e3));
111
+ if (attempt < 4) await new Promise((r) => setTimeout(r, 3e3));
95
112
  }
96
113
  try {
97
114
  const res = await fetch(
@@ -331,9 +348,9 @@ function openUrl(url) {
331
348
  });
332
349
  }
333
350
  function discoverInstalledTemplates(targetDir) {
334
- const templates = [];
351
+ const templates2 = [];
335
352
  const templatesDir = join(targetDir, "templates");
336
- if (!existsSync2(templatesDir)) return templates;
353
+ if (!existsSync2(templatesDir)) return templates2;
337
354
  try {
338
355
  const entries = readdirSync(templatesDir);
339
356
  for (const entry of entries) {
@@ -342,7 +359,7 @@ function discoverInstalledTemplates(targetDir) {
342
359
  if (!existsSync2(manifestPath)) continue;
343
360
  try {
344
361
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
345
- templates.push({
362
+ templates2.push({
346
363
  id: manifest.id || entry,
347
364
  name: manifest.name || entry,
348
365
  description: manifest.description || "",
@@ -353,7 +370,7 @@ function discoverInstalledTemplates(targetDir) {
353
370
  }
354
371
  } catch {
355
372
  }
356
- return templates;
373
+ return templates2;
357
374
  }
358
375
  function readComposioLocalKey() {
359
376
  try {
@@ -542,10 +559,20 @@ ENTRYPOINT ["/bin/sh"]
542
559
  const script = `#!/bin/sh
543
560
  # Auto-updates Telegram webhook when tunnel URL changes on restart.
544
561
 
562
+ wait_for_dns() {
563
+ HOST=$(echo "$1" | sed 's|https://||')
564
+ for i in $(seq 1 30); do
565
+ nslookup "$HOST" >/dev/null 2>&1 && return 0
566
+ printf '[tunnel] Waiting for DNS (%s, attempt %d/30)...\\n' "$HOST" "$i" >&2
567
+ sleep 2
568
+ done
569
+ return 1
570
+ }
571
+
545
572
  set_webhook() {
546
573
  URL="$1"
547
- for i in 1 2 3; do
548
- sleep 10
574
+ wait_for_dns "$URL" || { printf '[tunnel] DNS resolution failed for %s\\n' "$URL" >&2; return 1; }
575
+ for i in 1 2 3 4 5; do
549
576
  RESP=$(curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \\
550
577
  -H "Content-Type: application/json" \\
551
578
  -d "{\\"url\\":\\"$URL/webhook\\",\\"allowed_updates\\":[\\"message\\",\\"callback_query\\"]}")
@@ -556,6 +583,7 @@ set_webhook() {
556
583
  ;;
557
584
  esac
558
585
  printf '[tunnel] Webhook attempt %d failed: %s\\n' "$i" "$RESP" >&2
586
+ sleep 5
559
587
  done
560
588
  return 1
561
589
  }
@@ -2428,10 +2456,10 @@ async function checkTelegram(projectDir, tunnelCheck) {
2428
2456
  const wh = whData.result;
2429
2457
  const tunnelUrl = tunnelCheck.tunnelUrl || "";
2430
2458
  const webhookFix = async () => {
2431
- const freshUrl = await getTunnelUrl(projectDir, 5);
2459
+ const freshUrl = await getTunnelUrl(projectDir, 15);
2432
2460
  if (!freshUrl) return "No tunnel URL available \u2014 fix tunnel first";
2433
2461
  const ok = await setTelegramWebhook(botToken, freshUrl);
2434
- return ok ? `Webhook set to ${freshUrl}` : "Could not set webhook";
2462
+ return ok ? `Webhook set to ${freshUrl}` : "Could not set webhook \u2014 DNS may still be propagating, try again in 30s";
2435
2463
  };
2436
2464
  if (!wh.url) {
2437
2465
  return {
@@ -2561,6 +2589,75 @@ async function upgrade() {
2561
2589
  p9.outro("Upgrade complete!");
2562
2590
  }
2563
2591
 
2592
+ // src/commands/templates.ts
2593
+ import { resolve as resolve12 } from "path";
2594
+ import { existsSync as existsSync9 } from "fs";
2595
+ import { readFile as readFile9 } from "fs/promises";
2596
+ import * as p10 from "@clack/prompts";
2597
+ import pc10 from "picocolors";
2598
+ async function templates() {
2599
+ const { configDir, installed } = await loadInstalled();
2600
+ if (!installed || installed.capabilities.length === 0) {
2601
+ p10.intro(`${pc10.bgCyan(pc10.black(" seclaw "))} Templates`);
2602
+ p10.log.warning("No templates installed.");
2603
+ p10.log.info(`Add one: ${pc10.cyan("npx seclaw add <template>")}`);
2604
+ p10.log.info(`Browse: ${pc10.cyan("https://seclawai.com/templates")}`);
2605
+ p10.outro("");
2606
+ return;
2607
+ }
2608
+ const capabilities = installed.capabilities;
2609
+ const activeMode = installed.active || "auto";
2610
+ p10.intro(`${pc10.bgCyan(pc10.black(" seclaw "))} Installed Templates (${capabilities.length})`);
2611
+ for (const capId of capabilities) {
2612
+ const displayName = capId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2613
+ const promptPath = resolve12(configDir, "capabilities", capId, "system-prompt.md");
2614
+ const hasPrompt = existsSync9(promptPath);
2615
+ const schedPath = resolve12(configDir, "capabilities", capId, "schedules.json");
2616
+ let schedCount = 0;
2617
+ if (existsSync9(schedPath)) {
2618
+ try {
2619
+ const schedData = JSON.parse(await readFile9(schedPath, "utf-8"));
2620
+ schedCount = schedData.schedules.filter((s) => s.enabled !== false).length;
2621
+ } catch {
2622
+ }
2623
+ }
2624
+ const icon = hasPrompt ? pc10.green("\u2713") : pc10.yellow("\u26A0");
2625
+ const schedLabel = schedCount > 0 ? pc10.dim(` \u2014 ${schedCount} schedule${schedCount > 1 ? "s" : ""}`) : "";
2626
+ p10.log.info(`${icon} ${pc10.bold(displayName)}${schedLabel}`);
2627
+ }
2628
+ let modeLabel;
2629
+ if (activeMode === "auto") {
2630
+ modeLabel = `Auto \u2014 all ${capabilities.length} capabilities active`;
2631
+ } else {
2632
+ const activeName = activeMode.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2633
+ modeLabel = `${activeName} (focus mode)`;
2634
+ }
2635
+ p10.log.info("");
2636
+ p10.log.info(`Mode: ${pc10.cyan(modeLabel)}`);
2637
+ if (capabilities.length >= 2) {
2638
+ p10.log.info(`Switch: ${pc10.dim("npx seclaw switch")}`);
2639
+ }
2640
+ p10.outro("");
2641
+ }
2642
+ async function loadInstalled() {
2643
+ let wsHostPath = "shared";
2644
+ try {
2645
+ const envContent = await readFile9(resolve12(process.cwd(), ".env"), "utf-8");
2646
+ const wsMatch = envContent.match(/WORKSPACE_HOST_PATH=(.+)/);
2647
+ if (wsMatch?.[1]?.trim()) wsHostPath = wsMatch[1].trim();
2648
+ } catch {
2649
+ }
2650
+ const configDir = resolve12(process.cwd(), wsHostPath, "config");
2651
+ const installedPath = resolve12(configDir, "installed.json");
2652
+ if (!existsSync9(installedPath)) return { configDir, installed: null };
2653
+ try {
2654
+ const installed = JSON.parse(await readFile9(installedPath, "utf-8"));
2655
+ return { configDir, installed };
2656
+ } catch {
2657
+ return { configDir, installed: null };
2658
+ }
2659
+ }
2660
+
2564
2661
  // src/cli.ts
2565
2662
  program.name("seclaw").description("Secure autonomous AI agents in 60 seconds").version("0.1.0");
2566
2663
  program.command("create", { isDefault: true }).alias("init").description("Set up a new seclaw project").argument("[directory]", "Project directory", ".").action(create);
@@ -2571,4 +2668,5 @@ program.command("stop").description("Stop all services").action(stop);
2571
2668
  program.command("reconnect").description("Reconnect tunnel + Telegram webhook").action(reconnect);
2572
2669
  program.command("doctor").description("Diagnose and fix common issues").action(doctor);
2573
2670
  program.command("upgrade").description("Pull latest images and restart").action(upgrade);
2671
+ program.command("templates").description("List installed templates and capabilities").action(templates);
2574
2672
  program.parse();
@@ -1107,20 +1107,26 @@ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1107
1107
  import { resolve as resolve3 } from "path";
1108
1108
 
1109
1109
  // config.ts
1110
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
1110
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
1111
1111
  import { resolve as resolve2 } from "path";
1112
1112
  var WORKSPACE = process.env.WORKSPACE_PATH || "/workspace";
1113
- var BASE_PROMPT = `You are a personal AI assistant running on seclaw. You have multiple capabilities installed \u2014 use the right one based on the user's request.
1113
+ var AUTO_BASE_PROMPT = `You are a personal AI assistant running on seclaw. You have multiple capabilities installed \u2014 use the right one based on the user's request.
1114
1114
 
1115
1115
  ## Communication
1116
1116
  - Detect the user's language and respond in the same language
1117
1117
  - Keep Telegram messages concise \u2014 use bullet points and short paragraphs
1118
- - Be proactive when you have relevant context to share
1118
+ - Be proactive when you have relevant context to share`;
1119
+ function focusBasePrompt(capId) {
1120
+ const displayName = capId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1121
+ return `You are a specialized AI agent running on seclaw, focused exclusively on: ${displayName}.
1119
1122
 
1120
- ## Response Format
1121
- At the end of every response, write on a new line:
1122
- --- CapabilityName
1123
- Where CapabilityName is the capability you primarily used (e.g. "Inbox Management", "Research & Intelligence"). If no specific capability applies, write: --- General`;
1123
+ IMPORTANT: You are in FOCUS MODE. Only use the capability described below. Do NOT answer questions outside your specialization \u2014 politely redirect the user to switch to the right agent via /templates.
1124
+
1125
+ ## Communication
1126
+ - Detect the user's language and respond in the same language
1127
+ - Keep Telegram messages concise \u2014 use bullet points and short paragraphs
1128
+ - Be proactive when you have relevant context to share`;
1129
+ }
1124
1130
  function loadConfig() {
1125
1131
  return {
1126
1132
  // LLM
@@ -1153,11 +1159,9 @@ function reloadSystemPrompt(workspace) {
1153
1159
  if (installed.capabilities && installed.capabilities.length > 0) {
1154
1160
  const active = installed.active || "auto";
1155
1161
  if (active === "auto") {
1156
- return composeCapabilityPrompt(installed.capabilities);
1162
+ return composeCapabilityPrompt(installed.capabilities, "auto");
1157
1163
  }
1158
- const base = installed.capabilities[0];
1159
- const caps = base === active ? [active] : [base, active];
1160
- return composeCapabilityPrompt(caps);
1164
+ return composeCapabilityPrompt([active], active);
1161
1165
  }
1162
1166
  } catch (err) {
1163
1167
  console.error(`[config] Failed to parse installed.json: ${err.message}`);
@@ -1198,7 +1202,7 @@ function setActiveMode(workspace, active) {
1198
1202
  console.error(`[config] Failed to update installed.json: ${err.message}`);
1199
1203
  }
1200
1204
  }
1201
- function composeCapabilityPrompt(capabilities) {
1205
+ function composeCapabilityPrompt(capabilities, mode) {
1202
1206
  const sections = [];
1203
1207
  for (const id of capabilities) {
1204
1208
  const promptPath = resolve2(WORKSPACE, "config", "capabilities", id, "system-prompt.md");
@@ -1210,21 +1214,58 @@ function composeCapabilityPrompt(capabilities) {
1210
1214
  console.warn(`[config] Capability prompt not found: ${promptPath}`);
1211
1215
  }
1212
1216
  }
1217
+ const base = mode === "auto" ? AUTO_BASE_PROMPT : focusBasePrompt(mode);
1213
1218
  if (sections.length === 0) {
1214
1219
  console.warn("[config] No capability prompts loaded, using base prompt only");
1215
- return BASE_PROMPT;
1220
+ return base;
1216
1221
  }
1217
- return BASE_PROMPT + "\n\n" + sections.join("\n\n");
1222
+ const capNames = capabilities.map(
1223
+ (id) => id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")
1224
+ );
1225
+ const footerInstruction = mode === "auto" ? `## MANDATORY Response Footer
1226
+ You MUST end EVERY response with this exact format on a new line:
1227
+ --- CapabilityName
1228
+ Replace CapabilityName with the capability you used (one of: ${capNames.join(", ")}). If none applies, write: --- General
1229
+ This line is REQUIRED. Never skip it.` : `## MANDATORY Response Footer
1230
+ You MUST end EVERY response with this exact format on a new line:
1231
+ --- ${capNames[0]}
1232
+ This line is REQUIRED. Never skip it.`;
1233
+ return base + "\n\n" + sections.join("\n\n") + "\n\n" + footerInstruction;
1218
1234
  }
1219
1235
  function loadInstalledCapabilities(workspace) {
1220
1236
  const installedPath = resolve2(workspace, "config", "installed.json");
1221
- if (!existsSync3(installedPath)) return [];
1222
- try {
1223
- const installed = JSON.parse(readFileSync3(installedPath, "utf-8"));
1224
- return installed.capabilities || [];
1225
- } catch {
1226
- return [];
1237
+ const capsDir = resolve2(workspace, "config", "capabilities");
1238
+ let installed = { capabilities: [] };
1239
+ if (existsSync3(installedPath)) {
1240
+ try {
1241
+ installed = JSON.parse(readFileSync3(installedPath, "utf-8"));
1242
+ if (!installed.capabilities) installed.capabilities = [];
1243
+ } catch {
1244
+ }
1227
1245
  }
1246
+ if (existsSync3(capsDir)) {
1247
+ let dirty = false;
1248
+ try {
1249
+ const dirs = readdirSync2(capsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1250
+ for (const dir of dirs) {
1251
+ const promptPath = resolve2(capsDir, dir, "system-prompt.md");
1252
+ if (existsSync3(promptPath) && !installed.capabilities.includes(dir)) {
1253
+ installed.capabilities.push(dir);
1254
+ dirty = true;
1255
+ console.log(`[config] Auto-discovered capability: ${dir}`);
1256
+ }
1257
+ }
1258
+ } catch {
1259
+ }
1260
+ if (dirty) {
1261
+ try {
1262
+ writeFileSync2(installedPath, JSON.stringify(installed, null, 2) + "\n");
1263
+ console.log(`[config] Updated installed.json: ${installed.capabilities.length} capabilities`);
1264
+ } catch {
1265
+ }
1266
+ }
1267
+ }
1268
+ return installed.capabilities;
1228
1269
  }
1229
1270
 
1230
1271
  // scheduler.ts
@@ -1427,12 +1468,8 @@ async function handleWebhook(req, res, config2, toolCtx2, inngestClient) {
1427
1468
  await handleSchedules(chatId, config2);
1428
1469
  return;
1429
1470
  }
1430
- if (userText.startsWith("/capabilities")) {
1431
- await handleCapabilities(chatId, config2);
1432
- return;
1433
- }
1434
- if (userText.startsWith("/switch")) {
1435
- await handleSwitch(chatId, config2);
1471
+ if (userText.startsWith("/templates")) {
1472
+ await handleTemplates(chatId, config2);
1436
1473
  return;
1437
1474
  }
1438
1475
  try {
@@ -1442,12 +1479,12 @@ ${userText}`;
1442
1479
  let response = await runAgent(config2, toolCtx2, history, augmented);
1443
1480
  console.log(`[telegram] Reply (${response.length} chars): ${response.substring(0, 120)}`);
1444
1481
  let footer = "";
1445
- const footerMatch = response.match(/\n--- (.+)$/);
1482
+ const footerMatch = response.match(/\n[-—]{2,3}\s*(.+?)\s*$/);
1446
1483
  if (footerMatch) {
1447
- response = response.replace(/\n--- .+$/, "");
1484
+ response = response.replace(/\n[-—]{2,3}\s*.+?\s*$/, "").trimEnd();
1448
1485
  footer = `
1449
1486
 
1450
- _\u2014 ${footerMatch[1]}_`;
1487
+ \u2014 ${footerMatch[1].trim()}`;
1451
1488
  }
1452
1489
  saveHistory(chatId, config2.workspace, [
1453
1490
  ...history,
@@ -1468,7 +1505,7 @@ async function sendMessage(token, chatId, text) {
1468
1505
  const chunks = splitMessage(text, 4e3);
1469
1506
  for (const chunk of chunks) {
1470
1507
  try {
1471
- await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
1508
+ const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
1472
1509
  method: "POST",
1473
1510
  headers: { "Content-Type": "application/json" },
1474
1511
  body: JSON.stringify({
@@ -1477,6 +1514,15 @@ async function sendMessage(token, chatId, text) {
1477
1514
  parse_mode: "Markdown"
1478
1515
  })
1479
1516
  });
1517
+ const data = await res.json();
1518
+ if (!data.ok) {
1519
+ console.log(`[telegram] Markdown failed: ${data.description} \u2014 retrying plain`);
1520
+ await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
1521
+ method: "POST",
1522
+ headers: { "Content-Type": "application/json" },
1523
+ body: JSON.stringify({ chat_id: chatId, text: chunk })
1524
+ });
1525
+ }
1480
1526
  } catch {
1481
1527
  try {
1482
1528
  await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
@@ -1980,16 +2026,46 @@ async function editMessageWithButtons(token, chatId, messageId, text, keyboard)
1980
2026
  } catch {
1981
2027
  }
1982
2028
  }
1983
- async function handleCapabilities(chatId, config2) {
2029
+ var TEMPLATE_CATALOG = [
2030
+ { id: "productivity-agent", name: "Productivity Agent", price: "Free" },
2031
+ { id: "data-analyst", name: "Data Analyst", price: "Free" },
2032
+ { id: "inbox-agent", name: "Inbox Agent", price: "$19" },
2033
+ { id: "reddit-hn-digest", name: "Reddit & HN Digest", price: "$19" },
2034
+ { id: "youtube-digest", name: "YouTube Digest", price: "$19" },
2035
+ { id: "health-tracker", name: "Health Tracker", price: "$29" },
2036
+ { id: "earnings-tracker", name: "Earnings Tracker", price: "$29" },
2037
+ { id: "research-agent", name: "Research Agent", price: "$39" },
2038
+ { id: "knowledge-base", name: "Knowledge Base", price: "$39" },
2039
+ { id: "family-calendar", name: "Family Calendar", price: "$39" },
2040
+ { id: "content-agent", name: "Content Agent", price: "$49" },
2041
+ { id: "personal-crm", name: "Personal CRM", price: "$49" },
2042
+ { id: "youtube-creator", name: "YouTube Creator", price: "$69" },
2043
+ { id: "devops-agent", name: "DevOps Agent", price: "$79" },
2044
+ { id: "customer-service", name: "Customer Service", price: "$79" },
2045
+ { id: "sales-agent", name: "Sales Agent", price: "$79" },
2046
+ { id: "six-agent-company", name: "6-Agent Company", price: "$149" }
2047
+ ];
2048
+ async function handleTemplates(chatId, config2) {
1984
2049
  const capabilities = loadInstalledCapabilities(config2.workspace);
1985
2050
  if (capabilities.length === 0) {
1986
- await sendMessage(
1987
- config2.telegramToken,
1988
- chatId,
1989
- "No capabilities installed. Using default single-prompt mode."
1990
- );
2051
+ const lines2 = TEMPLATE_CATALOG.map((t) => {
2052
+ const tag = t.price === "Free" ? "FREE" : t.price;
2053
+ return `\u2022 *${t.name}* \u2014 ${tag}`;
2054
+ });
2055
+ const text2 = `*seclaw Templates (${TEMPLATE_CATALOG.length})*
2056
+
2057
+ ` + lines2.join("\n") + "\n\n_Tap a template to view details:_";
2058
+ const keyboard2 = [];
2059
+ for (const t of TEMPLATE_CATALOG) {
2060
+ keyboard2.push([{
2061
+ text: `${t.name} \u2014 ${t.price}`,
2062
+ url: `https://seclawai.com/templates/${t.id}`
2063
+ }]);
2064
+ }
2065
+ await sendMessageWithButtons(config2.telegramToken, chatId, text2, keyboard2);
1991
2066
  return;
1992
2067
  }
2068
+ const activeMode = getActiveMode(config2.workspace);
1993
2069
  const scheduleConfig = loadScheduleConfig(config2.workspace);
1994
2070
  const scheduleCountByCapability = {};
1995
2071
  if (scheduleConfig) {
@@ -2010,49 +2086,46 @@ async function handleCapabilities(chatId, config2) {
2010
2086
  const statusIcon = hasPrompt ? "\u2705" : "\u26A0\uFE0F";
2011
2087
  return `${statusIcon} *${displayName}*${schedLabel}`;
2012
2088
  });
2013
- await sendMessage(
2014
- config2.telegramToken,
2015
- chatId,
2016
- `*Installed Capabilities*
2089
+ let modeLabel;
2090
+ if (activeMode === "auto") {
2091
+ modeLabel = `Auto \u2014 all ${capabilities.length} templates active`;
2092
+ } else {
2093
+ const activeName = activeMode.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2094
+ modeLabel = `${activeName} (focus mode)`;
2095
+ }
2096
+ const notInstalled = TEMPLATE_CATALOG.filter((t) => !capabilities.includes(t.id));
2097
+ let text = `*Installed Templates (${capabilities.length})*
2017
2098
 
2018
- ${lines.join("\n")}`
2019
- );
2020
- }
2021
- async function handleSwitch(chatId, config2) {
2022
- const capabilities = loadInstalledCapabilities(config2.workspace);
2023
- const activeMode = getActiveMode(config2.workspace);
2024
- if (capabilities.length <= 1) {
2025
- await sendMessage(
2026
- config2.telegramToken,
2027
- chatId,
2028
- "Only one capability installed. Add more templates to use /switch.\n\nBrowse templates: seclawai.com/templates"
2029
- );
2030
- return;
2099
+ ${lines.join("\n")}
2100
+
2101
+ Mode: *${modeLabel}*`;
2102
+ if (notInstalled.length > 0) {
2103
+ text += `
2104
+
2105
+ *Browse More (${notInstalled.length})*
2106
+ _Tap to view details:_`;
2031
2107
  }
2032
2108
  const keyboard = [];
2033
- const autoLabel = activeMode === "auto" ? "\u2705 Auto (All Capabilities)" : "\u{1F504} Auto (All Capabilities)";
2034
- keyboard.push([{ text: autoLabel, callback_data: "cap_switch:auto" }]);
2035
- const capButtons = [];
2036
- for (const capId of capabilities) {
2037
- const displayName = capId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2038
- const icon = activeMode === capId ? "\u2705" : "\u25CB";
2039
- capButtons.push({ text: `${icon} ${displayName}`, callback_data: `cap_switch:${capId}` });
2040
- }
2041
- for (let i = 0; i < capButtons.length; i += 2) {
2042
- keyboard.push(capButtons.slice(i, i + 2));
2109
+ if (capabilities.length >= 2) {
2110
+ const autoLabel = activeMode === "auto" ? "\u2705 Auto (All Templates)" : "\u{1F504} Auto (All Templates)";
2111
+ keyboard.push([{ text: autoLabel, callback_data: "cap_switch:auto" }]);
2112
+ const capButtons = [];
2113
+ for (const capId of capabilities) {
2114
+ const displayName = capId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2115
+ const icon = activeMode === capId ? "\u2705" : "\u25CB";
2116
+ capButtons.push({ text: `${icon} ${displayName}`, callback_data: `cap_switch:${capId}` });
2117
+ }
2118
+ for (let i = 0; i < capButtons.length; i += 2) {
2119
+ keyboard.push(capButtons.slice(i, i + 2));
2120
+ }
2043
2121
  }
2044
- let statusText = "*Agent Mode*\n\n";
2045
- if (activeMode === "auto") {
2046
- statusText += `Current: *Auto* \u2014 all ${capabilities.length} capabilities active
2047
- `;
2048
- statusText += "The agent uses the right capability based on your message.";
2049
- } else {
2050
- const activeName = activeMode.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2051
- statusText += `Current: *${activeName}* (focus mode)
2052
- `;
2053
- statusText += "Only this capability + base is active.";
2122
+ for (const t of notInstalled) {
2123
+ keyboard.push([{
2124
+ text: `${t.name} \u2014 ${t.price}`,
2125
+ url: `https://seclawai.com/templates/${t.id}`
2126
+ }]);
2054
2127
  }
2055
- await sendMessageWithButtons(config2.telegramToken, chatId, statusText, keyboard);
2128
+ await sendMessageWithButtons(config2.telegramToken, chatId, text, keyboard);
2056
2129
  }
2057
2130
  async function handleSwitchCallback(chatId, msgId, capId, config2) {
2058
2131
  const capabilities = loadInstalledCapabilities(config2.workspace);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seclaw",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Secure autonomous AI agents in 60 seconds",
5
5
  "type": "module",
6
6
  "bin": {