seclaw 0.1.9 → 0.1.11

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
@@ -331,9 +331,9 @@ function openUrl(url) {
331
331
  });
332
332
  }
333
333
  function discoverInstalledTemplates(targetDir) {
334
- const templates = [];
334
+ const templates2 = [];
335
335
  const templatesDir = join(targetDir, "templates");
336
- if (!existsSync2(templatesDir)) return templates;
336
+ if (!existsSync2(templatesDir)) return templates2;
337
337
  try {
338
338
  const entries = readdirSync(templatesDir);
339
339
  for (const entry of entries) {
@@ -342,7 +342,7 @@ function discoverInstalledTemplates(targetDir) {
342
342
  if (!existsSync2(manifestPath)) continue;
343
343
  try {
344
344
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
345
- templates.push({
345
+ templates2.push({
346
346
  id: manifest.id || entry,
347
347
  name: manifest.name || entry,
348
348
  description: manifest.description || "",
@@ -353,7 +353,7 @@ function discoverInstalledTemplates(targetDir) {
353
353
  }
354
354
  } catch {
355
355
  }
356
- return templates;
356
+ return templates2;
357
357
  }
358
358
  function readComposioLocalKey() {
359
359
  try {
@@ -2561,6 +2561,75 @@ async function upgrade() {
2561
2561
  p9.outro("Upgrade complete!");
2562
2562
  }
2563
2563
 
2564
+ // src/commands/templates.ts
2565
+ import { resolve as resolve12 } from "path";
2566
+ import { existsSync as existsSync9 } from "fs";
2567
+ import { readFile as readFile9 } from "fs/promises";
2568
+ import * as p10 from "@clack/prompts";
2569
+ import pc10 from "picocolors";
2570
+ async function templates() {
2571
+ const { configDir, installed } = await loadInstalled();
2572
+ if (!installed || installed.capabilities.length === 0) {
2573
+ p10.intro(`${pc10.bgCyan(pc10.black(" seclaw "))} Templates`);
2574
+ p10.log.warning("No templates installed.");
2575
+ p10.log.info(`Add one: ${pc10.cyan("npx seclaw add <template>")}`);
2576
+ p10.log.info(`Browse: ${pc10.cyan("https://seclawai.com/templates")}`);
2577
+ p10.outro("");
2578
+ return;
2579
+ }
2580
+ const capabilities = installed.capabilities;
2581
+ const activeMode = installed.active || "auto";
2582
+ p10.intro(`${pc10.bgCyan(pc10.black(" seclaw "))} Installed Templates (${capabilities.length})`);
2583
+ for (const capId of capabilities) {
2584
+ const displayName = capId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2585
+ const promptPath = resolve12(configDir, "capabilities", capId, "system-prompt.md");
2586
+ const hasPrompt = existsSync9(promptPath);
2587
+ const schedPath = resolve12(configDir, "capabilities", capId, "schedules.json");
2588
+ let schedCount = 0;
2589
+ if (existsSync9(schedPath)) {
2590
+ try {
2591
+ const schedData = JSON.parse(await readFile9(schedPath, "utf-8"));
2592
+ schedCount = schedData.schedules.filter((s) => s.enabled !== false).length;
2593
+ } catch {
2594
+ }
2595
+ }
2596
+ const icon = hasPrompt ? pc10.green("\u2713") : pc10.yellow("\u26A0");
2597
+ const schedLabel = schedCount > 0 ? pc10.dim(` \u2014 ${schedCount} schedule${schedCount > 1 ? "s" : ""}`) : "";
2598
+ p10.log.info(`${icon} ${pc10.bold(displayName)}${schedLabel}`);
2599
+ }
2600
+ let modeLabel;
2601
+ if (activeMode === "auto") {
2602
+ modeLabel = `Auto \u2014 all ${capabilities.length} capabilities active`;
2603
+ } else {
2604
+ const activeName = activeMode.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2605
+ modeLabel = `${activeName} (focus mode)`;
2606
+ }
2607
+ p10.log.info("");
2608
+ p10.log.info(`Mode: ${pc10.cyan(modeLabel)}`);
2609
+ if (capabilities.length >= 2) {
2610
+ p10.log.info(`Switch: ${pc10.dim("npx seclaw switch")}`);
2611
+ }
2612
+ p10.outro("");
2613
+ }
2614
+ async function loadInstalled() {
2615
+ let wsHostPath = "shared";
2616
+ try {
2617
+ const envContent = await readFile9(resolve12(process.cwd(), ".env"), "utf-8");
2618
+ const wsMatch = envContent.match(/WORKSPACE_HOST_PATH=(.+)/);
2619
+ if (wsMatch?.[1]?.trim()) wsHostPath = wsMatch[1].trim();
2620
+ } catch {
2621
+ }
2622
+ const configDir = resolve12(process.cwd(), wsHostPath, "config");
2623
+ const installedPath = resolve12(configDir, "installed.json");
2624
+ if (!existsSync9(installedPath)) return { configDir, installed: null };
2625
+ try {
2626
+ const installed = JSON.parse(await readFile9(installedPath, "utf-8"));
2627
+ return { configDir, installed };
2628
+ } catch {
2629
+ return { configDir, installed: null };
2630
+ }
2631
+ }
2632
+
2564
2633
  // src/cli.ts
2565
2634
  program.name("seclaw").description("Secure autonomous AI agents in 60 seconds").version("0.1.0");
2566
2635
  program.command("create", { isDefault: true }).alias("init").description("Set up a new seclaw project").argument("[directory]", "Project directory", ".").action(create);
@@ -2571,4 +2640,5 @@ program.command("stop").description("Stop all services").action(stop);
2571
2640
  program.command("reconnect").description("Reconnect tunnel + Telegram webhook").action(reconnect);
2572
2641
  program.command("doctor").description("Diagnose and fix common issues").action(doctor);
2573
2642
  program.command("upgrade").description("Pull latest images and restart").action(upgrade);
2643
+ program.command("templates").description("List installed templates and capabilities").action(templates);
2574
2644
  program.parse();
@@ -251,7 +251,7 @@ function truncateToolResult(text) {
251
251
  }
252
252
 
253
253
  // telegram.ts
254
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
254
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
255
255
  import { resolve as resolve4, dirname } from "path";
256
256
 
257
257
  // tools.ts
@@ -342,7 +342,8 @@ async function initTools(config2) {
342
342
  };
343
343
  const advancedTools = [
344
344
  createTriggerScheduleTool(config2, ctx),
345
- createScheduleActionTool(config2)
345
+ createScheduleActionTool(config2),
346
+ createConnectIntegrationTool(config2, ctx)
346
347
  ];
347
348
  for (const t of advancedTools) {
348
349
  allTools.push(t.definition);
@@ -548,6 +549,7 @@ function createBuiltinTools(config2) {
548
549
  const delay = Math.min(Math.max(args.delay_seconds || 0, 0), 3600);
549
550
  if (!chatId) return "Error: no chat_id provided.";
550
551
  const confirmId = `confirm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
552
+ console.log(`[confirm] Created: ${confirmId} | action: ${onApprove.substring(0, 60)}`);
551
553
  pendingConfirmations.set(confirmId, {
552
554
  chatId,
553
555
  onApprove,
@@ -1018,6 +1020,85 @@ function createScheduleActionTool(config2) {
1018
1020
  }
1019
1021
  };
1020
1022
  }
1023
+ function createConnectIntegrationTool(config2, ctx) {
1024
+ return {
1025
+ definition: {
1026
+ name: "connect_integration",
1027
+ description: "Connect a new integration (Twitter, Reddit, YouTube, Tavily, Gmail, etc.) via OAuth. Returns an authorization URL that the user must open to grant access. Call this when you need an integration that isn't connected yet. After the user completes authorization, their tools become available automatically.",
1028
+ inputSchema: {
1029
+ type: "object",
1030
+ properties: {
1031
+ integration_key: {
1032
+ type: "string",
1033
+ description: "Integration to connect. Available: " + Object.entries(INTEGRATIONS).map(([k, v]) => `${k} (${v.hint})`).join(", ")
1034
+ }
1035
+ },
1036
+ required: ["integration_key"]
1037
+ }
1038
+ },
1039
+ execute: async (_name, args) => {
1040
+ const key = args.integration_key;
1041
+ const apiKey = config2.composioApiKey;
1042
+ if (!apiKey) {
1043
+ return "Error: Composio API key is not configured. Set COMPOSIO_API_KEY in .env";
1044
+ }
1045
+ const integration = INTEGRATIONS[key];
1046
+ if (!integration) {
1047
+ const available = Object.keys(INTEGRATIONS).join(", ");
1048
+ return `Unknown integration "${key}". Available: ${available}`;
1049
+ }
1050
+ try {
1051
+ const activeConnections = await getActiveConnections(apiKey);
1052
+ if (activeConnections.has(integration.app)) {
1053
+ await ctx.reloadComposio();
1054
+ return `${integration.name} is already connected. Tools have been reloaded.`;
1055
+ }
1056
+ const configRes = await composioFetch(apiKey, `/auth_configs?appName=${integration.app}`);
1057
+ const matchingConfig = (configRes.items || []).find(
1058
+ (c) => c.toolkit?.slug === integration.app
1059
+ );
1060
+ let authConfigId;
1061
+ if (matchingConfig) {
1062
+ authConfigId = matchingConfig.id;
1063
+ } else {
1064
+ const created = await composioFetch(apiKey, "/auth_configs", {
1065
+ method: "POST",
1066
+ body: JSON.stringify({ toolkit: { slug: integration.app } })
1067
+ });
1068
+ authConfigId = created.auth_config?.id ?? created.id;
1069
+ }
1070
+ const entityId = config2.composioUserId || "default";
1071
+ const connection = await composioFetch(apiKey, "/connected_accounts", {
1072
+ method: "POST",
1073
+ body: JSON.stringify({
1074
+ auth_config: { id: authConfigId },
1075
+ connection: { entity_id: entityId }
1076
+ })
1077
+ });
1078
+ const redirectUrl = connection.redirect_url || connection.redirect_uri;
1079
+ if (redirectUrl) {
1080
+ console.log(`[connect_integration] ${integration.name} OAuth URL generated`);
1081
+ return `Authorization link for ${integration.name}:
1082
+ ${redirectUrl}
1083
+
1084
+ The user needs to open this link, sign in, and grant access. After completing authorization, send any message and the new tools will be loaded automatically.`;
1085
+ }
1086
+ return `Could not generate authorization URL for ${integration.name}. The Composio API did not return a redirect URL.`;
1087
+ } catch (err) {
1088
+ const msg = err.message;
1089
+ console.error(`[connect_integration] Error: ${msg}`);
1090
+ if (msg.includes("DefaultAuthConfigNotFound") || msg.includes("auth config not found")) {
1091
+ return `${integration.name} does not support one-click OAuth authorization. It requires an API key to be configured manually. The user should add their ${integration.name} API key in the .env file and restart the agent. Do NOT attempt to use execute_command or any workaround.`;
1092
+ }
1093
+ if (msg.includes("NoAuthApp") || msg.includes("does not require authentication")) {
1094
+ await ctx.reloadComposio();
1095
+ return `${integration.name} does not require authentication \u2014 its tools should be available directly. Tools have been reloaded. Try using the ${integration.name} tools now. If no tools are available for ${integration.name}, tell the user this integration is not yet configured in Composio. Do NOT use execute_command or curl as a workaround.`;
1096
+ }
1097
+ return `Failed to connect ${integration.name}: ${msg}. Do NOT fall back to execute_command.`;
1098
+ }
1099
+ }
1100
+ };
1101
+ }
1021
1102
 
1022
1103
  // scheduler.ts
1023
1104
  import { Inngest } from "inngest";
@@ -1026,10 +1107,10 @@ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1026
1107
  import { resolve as resolve3 } from "path";
1027
1108
 
1028
1109
  // config.ts
1029
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
1110
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
1030
1111
  import { resolve as resolve2 } from "path";
1031
1112
  var WORKSPACE = process.env.WORKSPACE_PATH || "/workspace";
1032
- 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.
1033
1114
 
1034
1115
  ## Communication
1035
1116
  - Detect the user's language and respond in the same language
@@ -1040,6 +1121,21 @@ var BASE_PROMPT = `You are a personal AI assistant running on seclaw. You have m
1040
1121
  At the end of every response, write on a new line:
1041
1122
  --- CapabilityName
1042
1123
  Where CapabilityName is the capability you primarily used (e.g. "Inbox Management", "Research & Intelligence"). If no specific capability applies, write: --- General`;
1124
+ function focusBasePrompt(capId) {
1125
+ const displayName = capId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1126
+ return `You are a specialized AI agent running on seclaw, focused exclusively on: ${displayName}.
1127
+
1128
+ 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.
1129
+
1130
+ ## Communication
1131
+ - Detect the user's language and respond in the same language
1132
+ - Keep Telegram messages concise \u2014 use bullet points and short paragraphs
1133
+ - Be proactive when you have relevant context to share
1134
+
1135
+ ## Response Format
1136
+ At the end of every response, write on a new line:
1137
+ --- ${displayName}`;
1138
+ }
1043
1139
  function loadConfig() {
1044
1140
  return {
1045
1141
  // LLM
@@ -1062,19 +1158,26 @@ function loadConfig() {
1062
1158
  };
1063
1159
  }
1064
1160
  function loadSystemPrompt() {
1065
- const installedPath = resolve2(WORKSPACE, "config", "installed.json");
1161
+ return reloadSystemPrompt(WORKSPACE);
1162
+ }
1163
+ function reloadSystemPrompt(workspace) {
1164
+ const installedPath = resolve2(workspace, "config", "installed.json");
1066
1165
  if (existsSync3(installedPath)) {
1067
1166
  try {
1068
1167
  const installed = JSON.parse(readFileSync3(installedPath, "utf-8"));
1069
1168
  if (installed.capabilities && installed.capabilities.length > 0) {
1070
- return composeCapabilityPrompt(installed.capabilities);
1169
+ const active = installed.active || "auto";
1170
+ if (active === "auto") {
1171
+ return composeCapabilityPrompt(installed.capabilities, "auto");
1172
+ }
1173
+ return composeCapabilityPrompt([active], active);
1071
1174
  }
1072
1175
  } catch (err) {
1073
1176
  console.error(`[config] Failed to parse installed.json: ${err.message}`);
1074
1177
  }
1075
1178
  }
1076
1179
  const paths = [
1077
- resolve2(WORKSPACE, "config", "system-prompt.md"),
1180
+ resolve2(workspace, "config", "system-prompt.md"),
1078
1181
  "/templates/system-prompt.md"
1079
1182
  ];
1080
1183
  for (const p of paths) {
@@ -1086,7 +1189,29 @@ function loadSystemPrompt() {
1086
1189
  Always be concise in Telegram messages. Use bullet points.
1087
1190
  Detect the user's language and respond in the same language.`;
1088
1191
  }
1089
- function composeCapabilityPrompt(capabilities) {
1192
+ function getActiveMode(workspace) {
1193
+ const installedPath = resolve2(workspace, "config", "installed.json");
1194
+ if (!existsSync3(installedPath)) return "auto";
1195
+ try {
1196
+ const installed = JSON.parse(readFileSync3(installedPath, "utf-8"));
1197
+ return installed.active || "auto";
1198
+ } catch {
1199
+ return "auto";
1200
+ }
1201
+ }
1202
+ function setActiveMode(workspace, active) {
1203
+ const installedPath = resolve2(workspace, "config", "installed.json");
1204
+ if (!existsSync3(installedPath)) return;
1205
+ try {
1206
+ const installed = JSON.parse(readFileSync3(installedPath, "utf-8"));
1207
+ installed.active = active;
1208
+ writeFileSync2(installedPath, JSON.stringify(installed, null, 2) + "\n");
1209
+ console.log(`[config] Active mode set to: ${active}`);
1210
+ } catch (err) {
1211
+ console.error(`[config] Failed to update installed.json: ${err.message}`);
1212
+ }
1213
+ }
1214
+ function composeCapabilityPrompt(capabilities, mode) {
1090
1215
  const sections = [];
1091
1216
  for (const id of capabilities) {
1092
1217
  const promptPath = resolve2(WORKSPACE, "config", "capabilities", id, "system-prompt.md");
@@ -1098,21 +1223,47 @@ function composeCapabilityPrompt(capabilities) {
1098
1223
  console.warn(`[config] Capability prompt not found: ${promptPath}`);
1099
1224
  }
1100
1225
  }
1226
+ const base = mode === "auto" ? AUTO_BASE_PROMPT : focusBasePrompt(mode);
1101
1227
  if (sections.length === 0) {
1102
1228
  console.warn("[config] No capability prompts loaded, using base prompt only");
1103
- return BASE_PROMPT;
1229
+ return base;
1104
1230
  }
1105
- return BASE_PROMPT + "\n\n" + sections.join("\n\n");
1231
+ return base + "\n\n" + sections.join("\n\n");
1106
1232
  }
1107
1233
  function loadInstalledCapabilities(workspace) {
1108
1234
  const installedPath = resolve2(workspace, "config", "installed.json");
1109
- if (!existsSync3(installedPath)) return [];
1110
- try {
1111
- const installed = JSON.parse(readFileSync3(installedPath, "utf-8"));
1112
- return installed.capabilities || [];
1113
- } catch {
1114
- return [];
1235
+ const capsDir = resolve2(workspace, "config", "capabilities");
1236
+ let installed = { capabilities: [] };
1237
+ if (existsSync3(installedPath)) {
1238
+ try {
1239
+ installed = JSON.parse(readFileSync3(installedPath, "utf-8"));
1240
+ if (!installed.capabilities) installed.capabilities = [];
1241
+ } catch {
1242
+ }
1115
1243
  }
1244
+ if (existsSync3(capsDir)) {
1245
+ let dirty = false;
1246
+ try {
1247
+ const dirs = readdirSync2(capsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1248
+ for (const dir of dirs) {
1249
+ const promptPath = resolve2(capsDir, dir, "system-prompt.md");
1250
+ if (existsSync3(promptPath) && !installed.capabilities.includes(dir)) {
1251
+ installed.capabilities.push(dir);
1252
+ dirty = true;
1253
+ console.log(`[config] Auto-discovered capability: ${dir}`);
1254
+ }
1255
+ }
1256
+ } catch {
1257
+ }
1258
+ if (dirty) {
1259
+ try {
1260
+ writeFileSync2(installedPath, JSON.stringify(installed, null, 2) + "\n");
1261
+ console.log(`[config] Updated installed.json: ${installed.capabilities.length} capabilities`);
1262
+ } catch {
1263
+ }
1264
+ }
1265
+ }
1266
+ return installed.capabilities;
1116
1267
  }
1117
1268
 
1118
1269
  // scheduler.ts
@@ -1290,6 +1441,10 @@ async function handleWebhook(req, res, config2, toolCtx2, inngestClient) {
1290
1441
  const confirmId = data.replace(/^confirm_(yes|no):/, "");
1291
1442
  const msgId = callback.message?.message_id;
1292
1443
  await handleConfirmation(chatId2, msgId, confirmId, isApprove, config2, toolCtx2);
1444
+ } else if (data.startsWith("cap_switch:")) {
1445
+ const capId = data.replace("cap_switch:", "");
1446
+ const msgId = callback.message?.message_id;
1447
+ await handleSwitchCallback(chatId2, msgId, capId, config2);
1293
1448
  }
1294
1449
  return;
1295
1450
  }
@@ -1311,8 +1466,8 @@ async function handleWebhook(req, res, config2, toolCtx2, inngestClient) {
1311
1466
  await handleSchedules(chatId, config2);
1312
1467
  return;
1313
1468
  }
1314
- if (userText.startsWith("/capabilities")) {
1315
- await handleCapabilities(chatId, config2);
1469
+ if (userText.startsWith("/templates")) {
1470
+ await handleTemplates(chatId, config2);
1316
1471
  return;
1317
1472
  }
1318
1473
  try {
@@ -1348,7 +1503,7 @@ async function sendMessage(token, chatId, text) {
1348
1503
  const chunks = splitMessage(text, 4e3);
1349
1504
  for (const chunk of chunks) {
1350
1505
  try {
1351
- await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
1506
+ const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
1352
1507
  method: "POST",
1353
1508
  headers: { "Content-Type": "application/json" },
1354
1509
  body: JSON.stringify({
@@ -1357,6 +1512,15 @@ async function sendMessage(token, chatId, text) {
1357
1512
  parse_mode: "Markdown"
1358
1513
  })
1359
1514
  });
1515
+ const data = await res.json();
1516
+ if (!data.ok) {
1517
+ console.log(`[telegram] Markdown failed: ${data.description} \u2014 retrying plain`);
1518
+ await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
1519
+ method: "POST",
1520
+ headers: { "Content-Type": "application/json" },
1521
+ body: JSON.stringify({ chat_id: chatId, text: chunk })
1522
+ });
1523
+ }
1360
1524
  } catch {
1361
1525
  try {
1362
1526
  await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
@@ -1401,12 +1565,17 @@ function loadHistory(chatId, workspace) {
1401
1565
  function saveHistory(chatId, workspace, messages) {
1402
1566
  const p = historyPath(chatId, workspace);
1403
1567
  mkdirSync2(dirname(p), { recursive: true });
1404
- writeFileSync2(p, JSON.stringify(messages.slice(-MAX_HISTORY), null, 2));
1568
+ writeFileSync3(p, JSON.stringify(messages.slice(-MAX_HISTORY), null, 2));
1405
1569
  }
1406
1570
  var INTEGRATIONS = {
1407
1571
  gmail: { name: "Gmail", app: "gmail", hint: "email" },
1408
1572
  drive: { name: "Google Drive", app: "googledrive", hint: "files" },
1409
1573
  calendar: { name: "Google Calendar", app: "googlecalendar", hint: "events" },
1574
+ twitter: { name: "X (Twitter)", app: "twitter", hint: "tweets, leads, monitoring" },
1575
+ reddit: { name: "Reddit", app: "reddit", hint: "subreddits, posts" },
1576
+ youtube: { name: "YouTube", app: "youtube", hint: "channels, videos" },
1577
+ tavily: { name: "Tavily", app: "tavily", hint: "web search" },
1578
+ hackernews: { name: "Hacker News", app: "hackernews", hint: "tech news, stories" },
1410
1579
  notion: { name: "Notion", app: "notion", hint: "notes, databases" },
1411
1580
  github: { name: "GitHub", app: "github", hint: "repos, issues" },
1412
1581
  slack: { name: "Slack", app: "slack", hint: "messaging" },
@@ -1758,7 +1927,7 @@ async function handleScheduleToggle(chatId, msgId, scheduleId, config2) {
1758
1927
  return;
1759
1928
  }
1760
1929
  entry.enabled = entry.enabled === false ? true : false;
1761
- writeFileSync2(loc.filePath, JSON.stringify(data, null, 2) + "\n");
1930
+ writeFileSync3(loc.filePath, JSON.stringify(data, null, 2) + "\n");
1762
1931
  const state = entry.enabled ? "enabled" : "disabled";
1763
1932
  const icon = entry.enabled ? "\u2705" : "\u23F8\uFE0F";
1764
1933
  if (msgId) await removeButtons(config2.telegramToken, chatId, msgId, `${icon} ${scheduleId} \u2014 ${state}`);
@@ -1836,7 +2005,7 @@ async function handleScheduleDeleteExecute(chatId, msgId, scheduleId, config2) {
1836
2005
  }
1837
2006
  const removed = data.schedules.splice(idx, 1)[0];
1838
2007
  delete data.actions[removed.action];
1839
- writeFileSync2(loc.filePath, JSON.stringify(data, null, 2) + "\n");
2008
+ writeFileSync3(loc.filePath, JSON.stringify(data, null, 2) + "\n");
1840
2009
  if (msgId) await removeButtons(config2.telegramToken, chatId, msgId, `Deleted: ${scheduleId}`);
1841
2010
  console.log(`[schedules] ${scheduleId} deleted`);
1842
2011
  }
@@ -1855,16 +2024,17 @@ async function editMessageWithButtons(token, chatId, messageId, text, keyboard)
1855
2024
  } catch {
1856
2025
  }
1857
2026
  }
1858
- async function handleCapabilities(chatId, config2) {
2027
+ async function handleTemplates(chatId, config2) {
1859
2028
  const capabilities = loadInstalledCapabilities(config2.workspace);
1860
2029
  if (capabilities.length === 0) {
1861
2030
  await sendMessage(
1862
2031
  config2.telegramToken,
1863
2032
  chatId,
1864
- "No capabilities installed. Using default single-prompt mode."
2033
+ "No templates installed. Using default single-prompt mode.\n\nAdd templates: `npx seclaw add <template>`\nBrowse: seclawai.com/templates"
1865
2034
  );
1866
2035
  return;
1867
2036
  }
2037
+ const activeMode = getActiveMode(config2.workspace);
1868
2038
  const scheduleConfig = loadScheduleConfig(config2.workspace);
1869
2039
  const scheduleCountByCapability = {};
1870
2040
  if (scheduleConfig) {
@@ -1885,13 +2055,53 @@ async function handleCapabilities(chatId, config2) {
1885
2055
  const statusIcon = hasPrompt ? "\u2705" : "\u26A0\uFE0F";
1886
2056
  return `${statusIcon} *${displayName}*${schedLabel}`;
1887
2057
  });
1888
- await sendMessage(
1889
- config2.telegramToken,
1890
- chatId,
1891
- `*Installed Capabilities*
2058
+ let modeLabel;
2059
+ if (activeMode === "auto") {
2060
+ modeLabel = `Auto \u2014 all ${capabilities.length} templates active`;
2061
+ } else {
2062
+ const activeName = activeMode.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2063
+ modeLabel = `${activeName} (focus mode)`;
2064
+ }
2065
+ const text = `*Installed Templates (${capabilities.length})*
1892
2066
 
1893
- ${lines.join("\n")}`
1894
- );
2067
+ ${lines.join("\n")}
2068
+
2069
+ Mode: *${modeLabel}*`;
2070
+ if (capabilities.length >= 2) {
2071
+ const keyboard = [];
2072
+ const autoLabel = activeMode === "auto" ? "\u2705 Auto (All Templates)" : "\u{1F504} Auto (All Templates)";
2073
+ keyboard.push([{ text: autoLabel, callback_data: "cap_switch:auto" }]);
2074
+ const capButtons = [];
2075
+ for (const capId of capabilities) {
2076
+ const displayName = capId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2077
+ const icon = activeMode === capId ? "\u2705" : "\u25CB";
2078
+ capButtons.push({ text: `${icon} ${displayName}`, callback_data: `cap_switch:${capId}` });
2079
+ }
2080
+ for (let i = 0; i < capButtons.length; i += 2) {
2081
+ keyboard.push(capButtons.slice(i, i + 2));
2082
+ }
2083
+ await sendMessageWithButtons(config2.telegramToken, chatId, text, keyboard);
2084
+ } else {
2085
+ await sendMessage(
2086
+ config2.telegramToken,
2087
+ chatId,
2088
+ text + "\n\nAdd more templates to switch modes:\n`npx seclaw add <template> --key YOUR_KEY`"
2089
+ );
2090
+ }
2091
+ }
2092
+ async function handleSwitchCallback(chatId, msgId, capId, config2) {
2093
+ const capabilities = loadInstalledCapabilities(config2.workspace);
2094
+ if (capId !== "auto" && !capabilities.includes(capId)) {
2095
+ await sendMessage(config2.telegramToken, chatId, `Capability "${capId}" is not installed.`);
2096
+ return;
2097
+ }
2098
+ setActiveMode(config2.workspace, capId);
2099
+ config2.systemPrompt = reloadSystemPrompt(config2.workspace);
2100
+ if (msgId) {
2101
+ const label = capId === "auto" ? `\u2705 Switched to *Auto* \u2014 all capabilities active` : `\u2705 Switched to *${capId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}*`;
2102
+ await removeButtons(config2.telegramToken, chatId, msgId, label);
2103
+ }
2104
+ console.log(`[switch] Mode changed to: ${capId}`);
1895
2105
  }
1896
2106
  async function sendMessageWithButtons(token, chatId, text, keyboard) {
1897
2107
  if (!token || !chatId) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seclaw",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Secure autonomous AI agents in 60 seconds",
5
5
  "type": "module",
6
6
  "bin": {