seclaw 0.1.8 → 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.
- package/dist/runtime/agent.js +186 -11
- package/package.json +1 -1
package/dist/runtime/agent.js
CHANGED
|
@@ -241,7 +241,7 @@ ${memory}` : ""}
|
|
|
241
241
|
- When the user asks to save/note/draft/track something, use the appropriate workspace tool. Always confirm what was saved.`;
|
|
242
242
|
return prompt;
|
|
243
243
|
}
|
|
244
|
-
var MAX_TOOL_RESULT =
|
|
244
|
+
var MAX_TOOL_RESULT = 1e4;
|
|
245
245
|
function truncateToolResult(text) {
|
|
246
246
|
let cleaned = text.replace(/<[^>]+>/g, " ");
|
|
247
247
|
cleaned = cleaned.replace(/[A-Za-z0-9+/=]{100,}/g, "[base64-data]");
|
|
@@ -251,7 +251,7 @@ function truncateToolResult(text) {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// telegram.ts
|
|
254
|
-
import { readFileSync as readFileSync5, writeFileSync as
|
|
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);
|
|
@@ -410,7 +411,7 @@ async function connectComposio(config2) {
|
|
|
410
411
|
}
|
|
411
412
|
const toolResults = await Promise.allSettled(
|
|
412
413
|
activeApps.map(
|
|
413
|
-
(app) => apiFetch(`/tools?toolkit_slug=${app}&
|
|
414
|
+
(app) => apiFetch(`/tools?toolkit_slug=${app}&limit=20`)
|
|
414
415
|
)
|
|
415
416
|
);
|
|
416
417
|
const tools = [];
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|