seclaw 0.1.9 → 0.1.10

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.
@@ -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,7 +1107,7 @@ 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 } from "fs";
1030
1111
  import { resolve as resolve2 } from "path";
1031
1112
  var WORKSPACE = process.env.WORKSPACE_PATH || "/workspace";
1032
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.
@@ -1062,19 +1143,28 @@ function loadConfig() {
1062
1143
  };
1063
1144
  }
1064
1145
  function loadSystemPrompt() {
1065
- const installedPath = resolve2(WORKSPACE, "config", "installed.json");
1146
+ return reloadSystemPrompt(WORKSPACE);
1147
+ }
1148
+ function reloadSystemPrompt(workspace) {
1149
+ const installedPath = resolve2(workspace, "config", "installed.json");
1066
1150
  if (existsSync3(installedPath)) {
1067
1151
  try {
1068
1152
  const installed = JSON.parse(readFileSync3(installedPath, "utf-8"));
1069
1153
  if (installed.capabilities && installed.capabilities.length > 0) {
1070
- return composeCapabilityPrompt(installed.capabilities);
1154
+ const active = installed.active || "auto";
1155
+ if (active === "auto") {
1156
+ return composeCapabilityPrompt(installed.capabilities);
1157
+ }
1158
+ const base = installed.capabilities[0];
1159
+ const caps = base === active ? [active] : [base, active];
1160
+ return composeCapabilityPrompt(caps);
1071
1161
  }
1072
1162
  } catch (err) {
1073
1163
  console.error(`[config] Failed to parse installed.json: ${err.message}`);
1074
1164
  }
1075
1165
  }
1076
1166
  const paths = [
1077
- resolve2(WORKSPACE, "config", "system-prompt.md"),
1167
+ resolve2(workspace, "config", "system-prompt.md"),
1078
1168
  "/templates/system-prompt.md"
1079
1169
  ];
1080
1170
  for (const p of paths) {
@@ -1086,6 +1176,28 @@ function loadSystemPrompt() {
1086
1176
  Always be concise in Telegram messages. Use bullet points.
1087
1177
  Detect the user's language and respond in the same language.`;
1088
1178
  }
1179
+ function getActiveMode(workspace) {
1180
+ const installedPath = resolve2(workspace, "config", "installed.json");
1181
+ if (!existsSync3(installedPath)) return "auto";
1182
+ try {
1183
+ const installed = JSON.parse(readFileSync3(installedPath, "utf-8"));
1184
+ return installed.active || "auto";
1185
+ } catch {
1186
+ return "auto";
1187
+ }
1188
+ }
1189
+ function setActiveMode(workspace, active) {
1190
+ const installedPath = resolve2(workspace, "config", "installed.json");
1191
+ if (!existsSync3(installedPath)) return;
1192
+ try {
1193
+ const installed = JSON.parse(readFileSync3(installedPath, "utf-8"));
1194
+ installed.active = active;
1195
+ writeFileSync2(installedPath, JSON.stringify(installed, null, 2) + "\n");
1196
+ console.log(`[config] Active mode set to: ${active}`);
1197
+ } catch (err) {
1198
+ console.error(`[config] Failed to update installed.json: ${err.message}`);
1199
+ }
1200
+ }
1089
1201
  function composeCapabilityPrompt(capabilities) {
1090
1202
  const sections = [];
1091
1203
  for (const id of capabilities) {
@@ -1290,6 +1402,10 @@ async function handleWebhook(req, res, config2, toolCtx2, inngestClient) {
1290
1402
  const confirmId = data.replace(/^confirm_(yes|no):/, "");
1291
1403
  const msgId = callback.message?.message_id;
1292
1404
  await handleConfirmation(chatId2, msgId, confirmId, isApprove, config2, toolCtx2);
1405
+ } else if (data.startsWith("cap_switch:")) {
1406
+ const capId = data.replace("cap_switch:", "");
1407
+ const msgId = callback.message?.message_id;
1408
+ await handleSwitchCallback(chatId2, msgId, capId, config2);
1293
1409
  }
1294
1410
  return;
1295
1411
  }
@@ -1315,6 +1431,10 @@ async function handleWebhook(req, res, config2, toolCtx2, inngestClient) {
1315
1431
  await handleCapabilities(chatId, config2);
1316
1432
  return;
1317
1433
  }
1434
+ if (userText.startsWith("/switch")) {
1435
+ await handleSwitch(chatId, config2);
1436
+ return;
1437
+ }
1318
1438
  try {
1319
1439
  const history = loadHistory(chatId, config2.workspace);
1320
1440
  const augmented = `[chat_id: ${chatId}, user: ${userName}]
@@ -1401,12 +1521,17 @@ function loadHistory(chatId, workspace) {
1401
1521
  function saveHistory(chatId, workspace, messages) {
1402
1522
  const p = historyPath(chatId, workspace);
1403
1523
  mkdirSync2(dirname(p), { recursive: true });
1404
- writeFileSync2(p, JSON.stringify(messages.slice(-MAX_HISTORY), null, 2));
1524
+ writeFileSync3(p, JSON.stringify(messages.slice(-MAX_HISTORY), null, 2));
1405
1525
  }
1406
1526
  var INTEGRATIONS = {
1407
1527
  gmail: { name: "Gmail", app: "gmail", hint: "email" },
1408
1528
  drive: { name: "Google Drive", app: "googledrive", hint: "files" },
1409
1529
  calendar: { name: "Google Calendar", app: "googlecalendar", hint: "events" },
1530
+ twitter: { name: "X (Twitter)", app: "twitter", hint: "tweets, leads, monitoring" },
1531
+ reddit: { name: "Reddit", app: "reddit", hint: "subreddits, posts" },
1532
+ youtube: { name: "YouTube", app: "youtube", hint: "channels, videos" },
1533
+ tavily: { name: "Tavily", app: "tavily", hint: "web search" },
1534
+ hackernews: { name: "Hacker News", app: "hackernews", hint: "tech news, stories" },
1410
1535
  notion: { name: "Notion", app: "notion", hint: "notes, databases" },
1411
1536
  github: { name: "GitHub", app: "github", hint: "repos, issues" },
1412
1537
  slack: { name: "Slack", app: "slack", hint: "messaging" },
@@ -1758,7 +1883,7 @@ async function handleScheduleToggle(chatId, msgId, scheduleId, config2) {
1758
1883
  return;
1759
1884
  }
1760
1885
  entry.enabled = entry.enabled === false ? true : false;
1761
- writeFileSync2(loc.filePath, JSON.stringify(data, null, 2) + "\n");
1886
+ writeFileSync3(loc.filePath, JSON.stringify(data, null, 2) + "\n");
1762
1887
  const state = entry.enabled ? "enabled" : "disabled";
1763
1888
  const icon = entry.enabled ? "\u2705" : "\u23F8\uFE0F";
1764
1889
  if (msgId) await removeButtons(config2.telegramToken, chatId, msgId, `${icon} ${scheduleId} \u2014 ${state}`);
@@ -1836,7 +1961,7 @@ async function handleScheduleDeleteExecute(chatId, msgId, scheduleId, config2) {
1836
1961
  }
1837
1962
  const removed = data.schedules.splice(idx, 1)[0];
1838
1963
  delete data.actions[removed.action];
1839
- writeFileSync2(loc.filePath, JSON.stringify(data, null, 2) + "\n");
1964
+ writeFileSync3(loc.filePath, JSON.stringify(data, null, 2) + "\n");
1840
1965
  if (msgId) await removeButtons(config2.telegramToken, chatId, msgId, `Deleted: ${scheduleId}`);
1841
1966
  console.log(`[schedules] ${scheduleId} deleted`);
1842
1967
  }
@@ -1893,6 +2018,56 @@ async function handleCapabilities(chatId, config2) {
1893
2018
  ${lines.join("\n")}`
1894
2019
  );
1895
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;
2031
+ }
2032
+ 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));
2043
+ }
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.";
2054
+ }
2055
+ await sendMessageWithButtons(config2.telegramToken, chatId, statusText, keyboard);
2056
+ }
2057
+ async function handleSwitchCallback(chatId, msgId, capId, config2) {
2058
+ const capabilities = loadInstalledCapabilities(config2.workspace);
2059
+ if (capId !== "auto" && !capabilities.includes(capId)) {
2060
+ await sendMessage(config2.telegramToken, chatId, `Capability "${capId}" is not installed.`);
2061
+ return;
2062
+ }
2063
+ setActiveMode(config2.workspace, capId);
2064
+ config2.systemPrompt = reloadSystemPrompt(config2.workspace);
2065
+ if (msgId) {
2066
+ 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(" ")}*`;
2067
+ await removeButtons(config2.telegramToken, chatId, msgId, label);
2068
+ }
2069
+ console.log(`[switch] Mode changed to: ${capId}`);
2070
+ }
1896
2071
  async function sendMessageWithButtons(token, chatId, text, keyboard) {
1897
2072
  if (!token || !chatId) return;
1898
2073
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seclaw",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Secure autonomous AI agents in 60 seconds",
5
5
  "type": "module",
6
6
  "bin": {