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 +74 -4
- package/dist/runtime/agent.js +240 -30
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -331,9 +331,9 @@ function openUrl(url) {
|
|
|
331
331
|
});
|
|
332
332
|
}
|
|
333
333
|
function discoverInstalledTemplates(targetDir) {
|
|
334
|
-
const
|
|
334
|
+
const templates2 = [];
|
|
335
335
|
const templatesDir = join(targetDir, "templates");
|
|
336
|
-
if (!existsSync2(templatesDir)) return
|
|
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
|
-
|
|
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
|
|
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();
|
package/dist/runtime/agent.js
CHANGED
|
@@ -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);
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
1229
|
+
return base;
|
|
1104
1230
|
}
|
|
1105
|
-
return
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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("/
|
|
1315
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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;
|