seclaw 0.1.1
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.d.ts +1 -0
- package/dist/cli.js +2573 -0
- package/dist/runtime/Dockerfile +17 -0
- package/dist/runtime/agent.js +1989 -0
- package/dist/runtime/package.json +20 -0
- package/dist/templates/free/data-analyst/config.json +9 -0
- package/dist/templates/free/data-analyst/manifest.json +21 -0
- package/dist/templates/free/data-analyst/schedules.json +30 -0
- package/dist/templates/free/data-analyst/system-prompt.md +125 -0
- package/dist/templates/free/productivity-agent/README.md +27 -0
- package/dist/templates/free/productivity-agent/config.json +17 -0
- package/dist/templates/free/productivity-agent/manifest.json +8 -0
- package/dist/templates/free/productivity-agent/schedules.json +4 -0
- package/dist/templates/free/productivity-agent/system-prompt.md +36 -0
- package/package.json +52 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2573 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/create.ts
|
|
7
|
+
import { resolve as resolve5 } from "path";
|
|
8
|
+
import { existsSync as existsSync5 } from "fs";
|
|
9
|
+
import { cp as cp2, mkdir as mkdir2 } from "fs/promises";
|
|
10
|
+
import * as p2 from "@clack/prompts";
|
|
11
|
+
import { execa as execa3 } from "execa";
|
|
12
|
+
import pc2 from "picocolors";
|
|
13
|
+
|
|
14
|
+
// src/prompts.ts
|
|
15
|
+
import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
|
|
16
|
+
import { resolve as resolve2, join } from "path";
|
|
17
|
+
import { homedir, platform } from "os";
|
|
18
|
+
import { exec } from "child_process";
|
|
19
|
+
import * as p from "@clack/prompts";
|
|
20
|
+
import pc from "picocolors";
|
|
21
|
+
|
|
22
|
+
// src/tunnel.ts
|
|
23
|
+
import { execa } from "execa";
|
|
24
|
+
import { writeFile, readFile } from "fs/promises";
|
|
25
|
+
import { existsSync } from "fs";
|
|
26
|
+
import { resolve } from "path";
|
|
27
|
+
async function getTunnelUrl(cwd, maxRetries = 20) {
|
|
28
|
+
const projectDir = cwd || process.cwd();
|
|
29
|
+
const cachedPath = resolve(projectDir, ".tunnel-url");
|
|
30
|
+
if (existsSync(cachedPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const cached = (await readFile(cachedPath, "utf-8")).trim();
|
|
33
|
+
if (cached.includes("trycloudflare.com")) {
|
|
34
|
+
const fresh = await fetchTunnelUrlFromLogs(projectDir);
|
|
35
|
+
if (fresh) {
|
|
36
|
+
if (fresh !== cached) {
|
|
37
|
+
await writeFile(cachedPath, fresh);
|
|
38
|
+
return fresh;
|
|
39
|
+
}
|
|
40
|
+
return cached;
|
|
41
|
+
}
|
|
42
|
+
return cached;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
48
|
+
const url = await fetchTunnelUrlFromLogs(projectDir);
|
|
49
|
+
if (url) {
|
|
50
|
+
try {
|
|
51
|
+
await writeFile(cachedPath, url);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
return url;
|
|
55
|
+
}
|
|
56
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
async function fetchTunnelUrlFromLogs(cwd) {
|
|
61
|
+
try {
|
|
62
|
+
const result = await execa(
|
|
63
|
+
"docker",
|
|
64
|
+
["compose", "logs", "cloudflared", "--no-log-prefix"],
|
|
65
|
+
{ cwd, env: { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" } }
|
|
66
|
+
);
|
|
67
|
+
const combined = result.stdout + "\n" + result.stderr;
|
|
68
|
+
const match = combined.match(
|
|
69
|
+
/https:\/\/[a-z0-9-]+\.trycloudflare\.com/
|
|
70
|
+
);
|
|
71
|
+
return match ? match[0] : null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function setTelegramWebhook(botToken, tunnelUrl) {
|
|
77
|
+
const webhookUrl = `${tunnelUrl}/webhook`;
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(
|
|
80
|
+
`https://api.telegram.org/bot${botToken}/setWebhook`,
|
|
81
|
+
{
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json" },
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
url: webhookUrl,
|
|
86
|
+
allowed_updates: ["message", "callback_query"]
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
return data.ok;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function clearTunnelCache(projectDir) {
|
|
97
|
+
const cachedPath = resolve(projectDir, ".tunnel-url");
|
|
98
|
+
if (existsSync(cachedPath)) {
|
|
99
|
+
try {
|
|
100
|
+
await writeFile(cachedPath, "");
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function verifyTelegramToken(botToken) {
|
|
106
|
+
try {
|
|
107
|
+
const res = await fetch(
|
|
108
|
+
`https://api.telegram.org/bot${botToken}/getMe`
|
|
109
|
+
);
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
if (data.ok && data.result) {
|
|
112
|
+
return { valid: true, botName: `@${data.result.username}` };
|
|
113
|
+
}
|
|
114
|
+
return { valid: false };
|
|
115
|
+
} catch {
|
|
116
|
+
return { valid: false };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/prompts.ts
|
|
121
|
+
var PROVIDER_CONFIG = {
|
|
122
|
+
anthropic: {
|
|
123
|
+
label: "Anthropic (Claude)",
|
|
124
|
+
hint: "Claude Opus, Sonnet, Haiku",
|
|
125
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
126
|
+
placeholder: "sk-ant-api03-...",
|
|
127
|
+
prefix: "sk-ant-",
|
|
128
|
+
keyUrl: "https://console.anthropic.com/settings/keys"
|
|
129
|
+
},
|
|
130
|
+
openai: {
|
|
131
|
+
label: "OpenAI (GPT-4o)",
|
|
132
|
+
hint: "GPT-4o, GPT-4o-mini, o1, o3",
|
|
133
|
+
envVar: "OPENAI_API_KEY",
|
|
134
|
+
placeholder: "sk-proj-...",
|
|
135
|
+
prefix: "sk-",
|
|
136
|
+
keyUrl: "https://platform.openai.com/api-keys"
|
|
137
|
+
},
|
|
138
|
+
gemini: {
|
|
139
|
+
label: "Google (Gemini)",
|
|
140
|
+
hint: "Gemini 2.5 Pro, Flash",
|
|
141
|
+
envVar: "GOOGLE_AI_API_KEY",
|
|
142
|
+
placeholder: "AI...",
|
|
143
|
+
prefix: "",
|
|
144
|
+
keyUrl: "https://aistudio.google.com/apikey"
|
|
145
|
+
},
|
|
146
|
+
openrouter: {
|
|
147
|
+
label: "OpenRouter (Gemini 3 Flash)",
|
|
148
|
+
hint: "Gemini 3 Flash \u2014 fast, affordable, 100+ models available",
|
|
149
|
+
envVar: "OPENROUTER_API_KEY",
|
|
150
|
+
placeholder: "sk-or-...",
|
|
151
|
+
prefix: "sk-or-",
|
|
152
|
+
keyUrl: "https://openrouter.ai/keys"
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
function getProviderConfig(provider) {
|
|
156
|
+
return PROVIDER_CONFIG[provider];
|
|
157
|
+
}
|
|
158
|
+
async function collectSetupAnswers(targetDir) {
|
|
159
|
+
p.intro(
|
|
160
|
+
`${pc.bgCyan(pc.black(" seclaw "))} ${pc.dim("Secure autonomous AI agents in 60 seconds.")}`
|
|
161
|
+
);
|
|
162
|
+
const provider = await p.select({
|
|
163
|
+
message: "Which LLM provider?",
|
|
164
|
+
options: [
|
|
165
|
+
{ value: "openrouter", label: "OpenRouter", hint: "recommended \u2014 Gemini 3 Flash, ~$3-10/mo" },
|
|
166
|
+
{ value: "anthropic", label: "Anthropic (Claude)", hint: "~$15-30/mo" },
|
|
167
|
+
{ value: "openai", label: "OpenAI (GPT-4o)", hint: "~$10-25/mo" },
|
|
168
|
+
{ value: "gemini", label: "Google (Gemini)", hint: "~$7-20/mo" }
|
|
169
|
+
]
|
|
170
|
+
});
|
|
171
|
+
if (p.isCancel(provider)) process.exit(0);
|
|
172
|
+
const config = PROVIDER_CONFIG[provider];
|
|
173
|
+
p.log.step(
|
|
174
|
+
`${pc.bold("Step 1:")} ${config.label} API Key
|
|
175
|
+
${pc.dim("Go to")} ${pc.cyan(config.keyUrl)}
|
|
176
|
+
${pc.dim("Create a key and copy it")}`
|
|
177
|
+
);
|
|
178
|
+
const llmApiKey = await p.text({
|
|
179
|
+
message: `Paste your ${config.label} API Key`,
|
|
180
|
+
placeholder: config.placeholder,
|
|
181
|
+
validate: (v) => {
|
|
182
|
+
if (!v || v.length < 10) return "API key is too short";
|
|
183
|
+
if (config.prefix && !v.startsWith(config.prefix)) {
|
|
184
|
+
return `Expected key starting with ${config.prefix}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
if (p.isCancel(llmApiKey)) process.exit(0);
|
|
189
|
+
p.log.step(
|
|
190
|
+
`${pc.bold("Step 2:")} Telegram Bot Token
|
|
191
|
+
${pc.dim("1. Open Telegram, search")} ${pc.cyan("@BotFather")}
|
|
192
|
+
${pc.dim("2. Send")} ${pc.bold("/newbot")} ${pc.dim("-> pick a name")}
|
|
193
|
+
${pc.dim("3. Copy the token it gives you")}`
|
|
194
|
+
);
|
|
195
|
+
let telegramToken = "";
|
|
196
|
+
let telegramBotName = "";
|
|
197
|
+
const tokenInput = await p.text({
|
|
198
|
+
message: "Paste your Telegram Bot Token (Enter to skip)",
|
|
199
|
+
placeholder: "7234567890:AAF...",
|
|
200
|
+
defaultValue: ""
|
|
201
|
+
});
|
|
202
|
+
if (p.isCancel(tokenInput)) process.exit(0);
|
|
203
|
+
if (tokenInput && tokenInput.includes(":")) {
|
|
204
|
+
telegramToken = tokenInput;
|
|
205
|
+
const s = p.spinner();
|
|
206
|
+
s.start("Verifying Telegram token...");
|
|
207
|
+
const result = await verifyTelegramToken(telegramToken);
|
|
208
|
+
if (result.valid) {
|
|
209
|
+
telegramBotName = result.botName || "";
|
|
210
|
+
s.stop(`Bot verified: ${pc.green(telegramBotName)}`);
|
|
211
|
+
} else {
|
|
212
|
+
s.stop("Token looks invalid \u2014 continuing anyway, you can fix it later.");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let composioApiKey = readComposioLocalKey() || "";
|
|
216
|
+
if (composioApiKey) {
|
|
217
|
+
p.log.step(
|
|
218
|
+
`${pc.bold("Step 3:")} Composio ${pc.green("auto-detected!")}
|
|
219
|
+
${pc.dim("Found API key in")} ~/.composio/user_data.json`
|
|
220
|
+
);
|
|
221
|
+
} else {
|
|
222
|
+
p.log.step(
|
|
223
|
+
`${pc.bold("Step 3:")} Composio ${pc.dim("(for Gmail, Calendar, etc.)")}`
|
|
224
|
+
);
|
|
225
|
+
const method = await p.select({
|
|
226
|
+
message: "How would you like to set up Composio?",
|
|
227
|
+
options: [
|
|
228
|
+
{ value: "browser", label: "Open browser", hint: "sign up + get API key (recommended)" },
|
|
229
|
+
{ value: "paste", label: "Paste API key", hint: "if you already have one" },
|
|
230
|
+
{ value: "skip", label: "Skip for now", hint: "add later with: npx seclaw integrations" }
|
|
231
|
+
]
|
|
232
|
+
});
|
|
233
|
+
if (p.isCancel(method)) process.exit(0);
|
|
234
|
+
if (method === "browser") {
|
|
235
|
+
composioApiKey = await composioFromBrowser();
|
|
236
|
+
} else if (method === "paste") {
|
|
237
|
+
const composioKey = await p.text({
|
|
238
|
+
message: "Paste your Composio API Key",
|
|
239
|
+
placeholder: "ak_..."
|
|
240
|
+
});
|
|
241
|
+
if (p.isCancel(composioKey)) process.exit(0);
|
|
242
|
+
composioApiKey = composioKey || "";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const templateOptions = [
|
|
246
|
+
{
|
|
247
|
+
value: "productivity-agent",
|
|
248
|
+
label: "Productivity Agent (recommended)",
|
|
249
|
+
hint: "free \u2014 task management, Telegram chat, file tools"
|
|
250
|
+
}
|
|
251
|
+
];
|
|
252
|
+
if (targetDir) {
|
|
253
|
+
const installedTemplates = discoverInstalledTemplates(targetDir);
|
|
254
|
+
for (const t of installedTemplates) {
|
|
255
|
+
templateOptions.push({
|
|
256
|
+
value: t.id,
|
|
257
|
+
label: `${t.name} (installed)`,
|
|
258
|
+
hint: `${t.tier} \u2014 ${t.description.substring(0, 60)}`
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
templateOptions.push({
|
|
263
|
+
value: "blank",
|
|
264
|
+
label: "Empty project",
|
|
265
|
+
hint: "just the infrastructure, no template"
|
|
266
|
+
});
|
|
267
|
+
const template = await p.select({
|
|
268
|
+
message: "Select a starter template",
|
|
269
|
+
options: templateOptions
|
|
270
|
+
});
|
|
271
|
+
if (p.isCancel(template)) process.exit(0);
|
|
272
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
273
|
+
return {
|
|
274
|
+
llmProvider: provider,
|
|
275
|
+
llmApiKey,
|
|
276
|
+
telegramToken,
|
|
277
|
+
telegramBotName,
|
|
278
|
+
composioApiKey,
|
|
279
|
+
composioUserId: "",
|
|
280
|
+
template,
|
|
281
|
+
timezone: tz
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function composioFromBrowser() {
|
|
285
|
+
const url = "https://platform.composio.dev";
|
|
286
|
+
p.log.info(
|
|
287
|
+
`${pc.dim("Opening:")} ${pc.cyan(url)}
|
|
288
|
+
${pc.dim("1.")} Sign up or log in
|
|
289
|
+
${pc.dim("2.")} Click ${pc.bold("Settings")} \u2192 ${pc.bold("API Keys")} tab
|
|
290
|
+
${pc.dim("3.")} Click ${pc.bold("New API key")}, copy and paste it below`
|
|
291
|
+
);
|
|
292
|
+
openUrl(url);
|
|
293
|
+
const key = await p.text({
|
|
294
|
+
message: "Paste your Composio API Key",
|
|
295
|
+
placeholder: "ak_..."
|
|
296
|
+
});
|
|
297
|
+
if (p.isCancel(key)) process.exit(0);
|
|
298
|
+
return key || "";
|
|
299
|
+
}
|
|
300
|
+
function openUrl(url) {
|
|
301
|
+
const cmd = platform() === "darwin" ? `open "${url}"` : platform() === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
|
|
302
|
+
exec(cmd, () => {
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function discoverInstalledTemplates(targetDir) {
|
|
306
|
+
const templates2 = [];
|
|
307
|
+
const templatesDir = join(targetDir, "templates");
|
|
308
|
+
if (!existsSync2(templatesDir)) return templates2;
|
|
309
|
+
try {
|
|
310
|
+
const entries = readdirSync(templatesDir);
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
if (entry === "productivity-agent") continue;
|
|
313
|
+
const manifestPath = join(templatesDir, entry, "manifest.json");
|
|
314
|
+
if (!existsSync2(manifestPath)) continue;
|
|
315
|
+
try {
|
|
316
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
317
|
+
templates2.push({
|
|
318
|
+
id: manifest.id || entry,
|
|
319
|
+
name: manifest.name || entry,
|
|
320
|
+
description: manifest.description || "",
|
|
321
|
+
tier: manifest.tier || "paid"
|
|
322
|
+
});
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
return templates2;
|
|
329
|
+
}
|
|
330
|
+
function readComposioLocalKey() {
|
|
331
|
+
try {
|
|
332
|
+
const dataPath = resolve2(homedir(), ".composio", "user_data.json");
|
|
333
|
+
if (!existsSync2(dataPath)) return null;
|
|
334
|
+
const data = JSON.parse(readFileSync(dataPath, "utf-8"));
|
|
335
|
+
const key = data.api_key;
|
|
336
|
+
if (key && typeof key === "string" && key.startsWith("ak_")) {
|
|
337
|
+
return key;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/scaffold.ts
|
|
346
|
+
import { mkdir, writeFile as writeFile2, cp, rm, readFile as readFile2 } from "fs/promises";
|
|
347
|
+
import { existsSync as existsSync3 } from "fs";
|
|
348
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
349
|
+
async function scaffoldProject(targetDir, answers) {
|
|
350
|
+
const dirs = [
|
|
351
|
+
"shared/tasks",
|
|
352
|
+
"shared/reports",
|
|
353
|
+
"shared/notes",
|
|
354
|
+
"shared/drafts",
|
|
355
|
+
"shared/memory",
|
|
356
|
+
"shared/config",
|
|
357
|
+
"templates",
|
|
358
|
+
"commander",
|
|
359
|
+
"agent"
|
|
360
|
+
];
|
|
361
|
+
for (const dir of dirs) {
|
|
362
|
+
await mkdir(join2(targetDir, dir), { recursive: true });
|
|
363
|
+
}
|
|
364
|
+
await writeEnv(targetDir, answers);
|
|
365
|
+
await writeDockerCompose(targetDir);
|
|
366
|
+
await writePermissions(targetDir);
|
|
367
|
+
await writeGitignore(targetDir);
|
|
368
|
+
await writeDockerignore(targetDir);
|
|
369
|
+
await copyAgentFiles(targetDir);
|
|
370
|
+
await copyCommanderFiles(targetDir);
|
|
371
|
+
await writeSeedFiles(targetDir, answers);
|
|
372
|
+
}
|
|
373
|
+
async function writeEnv(dir, answers) {
|
|
374
|
+
const config = getProviderConfig(answers.llmProvider);
|
|
375
|
+
const existing = await readExistingEnv(join2(dir, ".env"));
|
|
376
|
+
const lines = [
|
|
377
|
+
`# LLM Provider: ${config.label}`,
|
|
378
|
+
`LLM_PROVIDER=${answers.llmProvider}`,
|
|
379
|
+
...existing.LLM_MODEL ? [`LLM_MODEL=${existing.LLM_MODEL}`] : [],
|
|
380
|
+
`${config.envVar}=${answers.llmApiKey}`,
|
|
381
|
+
`TIMEZONE=${answers.timezone}`,
|
|
382
|
+
`TELEGRAM_BOT_TOKEN=${answers.telegramToken}`,
|
|
383
|
+
`TELEGRAM_CHAT_ID=${existing.TELEGRAM_CHAT_ID || ""}`
|
|
384
|
+
];
|
|
385
|
+
if (answers.composioApiKey) {
|
|
386
|
+
lines.push(`COMPOSIO_API_KEY=${answers.composioApiKey}`);
|
|
387
|
+
lines.push(`COMPOSIO_USER_ID=${existing.COMPOSIO_USER_ID || answers.composioUserId || generateUserId()}`);
|
|
388
|
+
}
|
|
389
|
+
if (existing.INNGEST_DEV) {
|
|
390
|
+
lines.push(`INNGEST_DEV=${existing.INNGEST_DEV}`);
|
|
391
|
+
}
|
|
392
|
+
await writeFile2(join2(dir, ".env"), lines.join("\n") + "\n");
|
|
393
|
+
}
|
|
394
|
+
async function readExistingEnv(path) {
|
|
395
|
+
const env = {};
|
|
396
|
+
try {
|
|
397
|
+
const content = await readFile2(path, "utf-8");
|
|
398
|
+
for (const line of content.split("\n")) {
|
|
399
|
+
const trimmed = line.trim();
|
|
400
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
401
|
+
const eqIdx = trimmed.indexOf("=");
|
|
402
|
+
if (eqIdx > 0) {
|
|
403
|
+
env[trimmed.substring(0, eqIdx)] = trimmed.substring(eqIdx + 1);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
return env;
|
|
409
|
+
}
|
|
410
|
+
function generateUserId() {
|
|
411
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
412
|
+
let id = "seclaw-user-";
|
|
413
|
+
for (let i = 0; i < 16; i++) {
|
|
414
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
415
|
+
}
|
|
416
|
+
return id;
|
|
417
|
+
}
|
|
418
|
+
async function writeDockerCompose(dir) {
|
|
419
|
+
const content = `services:
|
|
420
|
+
inngest:
|
|
421
|
+
image: inngest/inngest:latest
|
|
422
|
+
command: ["inngest", "dev", "-u", "http://agent:3000/api/inngest", "--no-discovery"]
|
|
423
|
+
ports:
|
|
424
|
+
- "8288:8288"
|
|
425
|
+
networks:
|
|
426
|
+
- agent-net
|
|
427
|
+
restart: unless-stopped
|
|
428
|
+
|
|
429
|
+
agent:
|
|
430
|
+
build:
|
|
431
|
+
context: ./agent
|
|
432
|
+
dockerfile: Dockerfile
|
|
433
|
+
image: seclaw/agent:latest
|
|
434
|
+
restart: unless-stopped
|
|
435
|
+
volumes:
|
|
436
|
+
- ./shared:/workspace:rw
|
|
437
|
+
- ./templates:/templates:ro
|
|
438
|
+
env_file:
|
|
439
|
+
- .env
|
|
440
|
+
environment:
|
|
441
|
+
- WORKSPACE_PATH=/workspace
|
|
442
|
+
- COMMANDER_URL=http://desktop-commander:3000
|
|
443
|
+
- INNGEST_BASE_URL=http://inngest:8288
|
|
444
|
+
- INNGEST_DEV=1
|
|
445
|
+
extra_hosts:
|
|
446
|
+
- "host.docker.internal:host-gateway"
|
|
447
|
+
networks:
|
|
448
|
+
- agent-net
|
|
449
|
+
depends_on:
|
|
450
|
+
- inngest
|
|
451
|
+
- desktop-commander
|
|
452
|
+
healthcheck:
|
|
453
|
+
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
|
|
454
|
+
interval: 10s
|
|
455
|
+
timeout: 5s
|
|
456
|
+
retries: 5
|
|
457
|
+
|
|
458
|
+
cloudflared:
|
|
459
|
+
image: cloudflare/cloudflared:latest
|
|
460
|
+
restart: unless-stopped
|
|
461
|
+
command: tunnel --no-autoupdate --url http://agent:3000
|
|
462
|
+
networks:
|
|
463
|
+
- agent-net
|
|
464
|
+
depends_on:
|
|
465
|
+
agent:
|
|
466
|
+
condition: service_healthy
|
|
467
|
+
healthcheck:
|
|
468
|
+
test: ["CMD-SHELL", "wget --spider -q http://agent:3000/health || exit 1"]
|
|
469
|
+
interval: 30s
|
|
470
|
+
timeout: 5s
|
|
471
|
+
retries: 3
|
|
472
|
+
start_period: 30s
|
|
473
|
+
|
|
474
|
+
desktop-commander:
|
|
475
|
+
build:
|
|
476
|
+
context: ./commander
|
|
477
|
+
dockerfile: Dockerfile
|
|
478
|
+
image: seclaw/desktop-commander:latest
|
|
479
|
+
restart: unless-stopped
|
|
480
|
+
volumes:
|
|
481
|
+
- ./shared:/workspace:rw
|
|
482
|
+
- ./permissions.yml:/permissions.yml:ro
|
|
483
|
+
security_opt:
|
|
484
|
+
- no-new-privileges:true
|
|
485
|
+
read_only: true
|
|
486
|
+
tmpfs:
|
|
487
|
+
- /tmp:size=100M
|
|
488
|
+
cap_drop:
|
|
489
|
+
- ALL
|
|
490
|
+
networks:
|
|
491
|
+
- agent-net
|
|
492
|
+
deploy:
|
|
493
|
+
resources:
|
|
494
|
+
limits:
|
|
495
|
+
memory: 512M
|
|
496
|
+
cpus: "1.0"
|
|
497
|
+
|
|
498
|
+
networks:
|
|
499
|
+
agent-net:
|
|
500
|
+
driver: bridge
|
|
501
|
+
`;
|
|
502
|
+
await writeFile2(join2(dir, "docker-compose.yml"), content);
|
|
503
|
+
}
|
|
504
|
+
async function copyAgentFiles(dir) {
|
|
505
|
+
const agentTemplateSrc = resolve3(import.meta.dirname, "runtime");
|
|
506
|
+
const agentDest = join2(dir, "agent");
|
|
507
|
+
if (existsSync3(agentDest)) {
|
|
508
|
+
await rm(agentDest, { recursive: true });
|
|
509
|
+
}
|
|
510
|
+
await mkdir(agentDest, { recursive: true });
|
|
511
|
+
if (existsSync3(agentTemplateSrc)) {
|
|
512
|
+
await cp(agentTemplateSrc, agentDest, { recursive: true });
|
|
513
|
+
} else {
|
|
514
|
+
await writeMinimalAgent(agentDest);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
async function writeMinimalAgent(dir) {
|
|
518
|
+
await mkdir(dir, { recursive: true });
|
|
519
|
+
await writeFile2(
|
|
520
|
+
join2(dir, "package.json"),
|
|
521
|
+
JSON.stringify(
|
|
522
|
+
{
|
|
523
|
+
name: "seclaw-agent",
|
|
524
|
+
version: "1.0.0",
|
|
525
|
+
private: true,
|
|
526
|
+
type: "module",
|
|
527
|
+
dependencies: {
|
|
528
|
+
openai: "^4.73.0",
|
|
529
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
530
|
+
inngest: "^3.22.0"
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
null,
|
|
534
|
+
2
|
|
535
|
+
)
|
|
536
|
+
);
|
|
537
|
+
await writeFile2(
|
|
538
|
+
join2(dir, "Dockerfile"),
|
|
539
|
+
`FROM node:20-alpine
|
|
540
|
+
RUN apk add --no-cache wget && rm -rf /var/cache/apk/*
|
|
541
|
+
WORKDIR /app
|
|
542
|
+
COPY package.json ./
|
|
543
|
+
RUN npm install --omit=dev && npm cache clean --force
|
|
544
|
+
COPY *.js ./
|
|
545
|
+
EXPOSE 3000
|
|
546
|
+
HEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD wget --spider -q http://localhost:3000/health || exit 1
|
|
547
|
+
CMD ["node", "agent.js"]
|
|
548
|
+
`
|
|
549
|
+
);
|
|
550
|
+
await writeFile2(
|
|
551
|
+
join2(dir, "agent.js"),
|
|
552
|
+
`import { createServer } from "node:http";
|
|
553
|
+
const PORT = parseInt(process.env.AGENT_PORT || "3000", 10);
|
|
554
|
+
const server = createServer(async (req, res) => {
|
|
555
|
+
if (req.url === "/health") {
|
|
556
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
557
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (req.method === "POST" && req.url === "/webhook") {
|
|
561
|
+
let data = "";
|
|
562
|
+
req.on("data", (c) => data += c);
|
|
563
|
+
req.on("end", async () => {
|
|
564
|
+
res.writeHead(200); res.end();
|
|
565
|
+
try {
|
|
566
|
+
const body = JSON.parse(data);
|
|
567
|
+
const chatId = body?.message?.chat?.id;
|
|
568
|
+
const text = body?.message?.text;
|
|
569
|
+
if (chatId && text) {
|
|
570
|
+
await fetch(\`https://api.telegram.org/bot\${process.env.TELEGRAM_BOT_TOKEN}/sendMessage\`, {
|
|
571
|
+
method: "POST",
|
|
572
|
+
headers: { "Content-Type": "application/json" },
|
|
573
|
+
body: JSON.stringify({ chat_id: chatId, text: "Agent is starting up. Please wait..." }),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
} catch {}
|
|
577
|
+
});
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
res.writeHead(404); res.end();
|
|
581
|
+
});
|
|
582
|
+
server.listen(PORT, () => console.log(\`seclaw agent on port \${PORT}\`));
|
|
583
|
+
`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
async function copyCommanderFiles(dir) {
|
|
587
|
+
const packageJson = `{
|
|
588
|
+
"name": "seclaw-commander",
|
|
589
|
+
"version": "1.0.0",
|
|
590
|
+
"private": true,
|
|
591
|
+
"type": "module",
|
|
592
|
+
"dependencies": {
|
|
593
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
`;
|
|
597
|
+
const dockerfile = `FROM node:20-alpine
|
|
598
|
+
|
|
599
|
+
RUN apk add --no-cache git python3 && rm -rf /var/cache/apk/*
|
|
600
|
+
|
|
601
|
+
RUN addgroup -g 1001 -S commander && \\
|
|
602
|
+
adduser -S commander -u 1001 -G commander
|
|
603
|
+
|
|
604
|
+
WORKDIR /app
|
|
605
|
+
|
|
606
|
+
COPY package.json ./
|
|
607
|
+
RUN npm install --omit=dev && npm cache clean --force
|
|
608
|
+
|
|
609
|
+
COPY server.js ./
|
|
610
|
+
|
|
611
|
+
RUN mkdir -p /workspace /tmp && \\
|
|
612
|
+
chown -R commander:commander /workspace /tmp
|
|
613
|
+
|
|
614
|
+
USER commander
|
|
615
|
+
|
|
616
|
+
EXPOSE 3000
|
|
617
|
+
|
|
618
|
+
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \\
|
|
619
|
+
CMD wget --spider -q http://localhost:3000/health || exit 1
|
|
620
|
+
|
|
621
|
+
CMD ["node", "server.js"]
|
|
622
|
+
`;
|
|
623
|
+
const serverJs = `import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
624
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
625
|
+
import {
|
|
626
|
+
CallToolRequestSchema,
|
|
627
|
+
ListToolsRequestSchema,
|
|
628
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
629
|
+
import { execSync } from "node:child_process";
|
|
630
|
+
import {
|
|
631
|
+
readFileSync, writeFileSync, mkdirSync,
|
|
632
|
+
readdirSync, statSync, existsSync,
|
|
633
|
+
} from "node:fs";
|
|
634
|
+
import { resolve, join, dirname } from "node:path";
|
|
635
|
+
import { createServer } from "node:http";
|
|
636
|
+
|
|
637
|
+
const WORKSPACE = "/workspace";
|
|
638
|
+
|
|
639
|
+
function safePath(p) {
|
|
640
|
+
const resolved = resolve(WORKSPACE, p);
|
|
641
|
+
if (!resolved.startsWith(WORKSPACE)) {
|
|
642
|
+
throw new Error("Path outside workspace");
|
|
643
|
+
}
|
|
644
|
+
return resolved;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const TOOLS = [
|
|
648
|
+
{
|
|
649
|
+
name: "read_file",
|
|
650
|
+
description: "Read a file from the workspace",
|
|
651
|
+
inputSchema: {
|
|
652
|
+
type: "object",
|
|
653
|
+
properties: { path: { type: "string", description: "Relative path within workspace" } },
|
|
654
|
+
required: ["path"],
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: "write_file",
|
|
659
|
+
description: "Write content to a file (creates parent directories automatically)",
|
|
660
|
+
inputSchema: {
|
|
661
|
+
type: "object",
|
|
662
|
+
properties: {
|
|
663
|
+
path: { type: "string", description: "Relative path within workspace" },
|
|
664
|
+
content: { type: "string", description: "File content to write" },
|
|
665
|
+
},
|
|
666
|
+
required: ["path", "content"],
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
name: "list_directory",
|
|
671
|
+
description: "List files and directories in the workspace",
|
|
672
|
+
inputSchema: {
|
|
673
|
+
type: "object",
|
|
674
|
+
properties: { path: { type: "string", description: "Relative path within workspace (use '.' for root)" } },
|
|
675
|
+
required: ["path"],
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
name: "execute_command",
|
|
680
|
+
description: "Execute a shell command in the workspace",
|
|
681
|
+
inputSchema: {
|
|
682
|
+
type: "object",
|
|
683
|
+
properties: { command: { type: "string", description: "Shell command to execute" } },
|
|
684
|
+
required: ["command"],
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
];
|
|
688
|
+
|
|
689
|
+
function handleTool(name, args) {
|
|
690
|
+
switch (name) {
|
|
691
|
+
case "read_file": {
|
|
692
|
+
const p = safePath(args.path);
|
|
693
|
+
if (!existsSync(p)) {
|
|
694
|
+
return { content: [{ type: "text", text: "File not found: " + args.path }], isError: true };
|
|
695
|
+
}
|
|
696
|
+
const content = readFileSync(p, "utf-8");
|
|
697
|
+
return { content: [{ type: "text", text: content }] };
|
|
698
|
+
}
|
|
699
|
+
case "write_file": {
|
|
700
|
+
const p = safePath(args.path);
|
|
701
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
702
|
+
writeFileSync(p, args.content);
|
|
703
|
+
return { content: [{ type: "text", text: "Written: " + args.path }] };
|
|
704
|
+
}
|
|
705
|
+
case "list_directory": {
|
|
706
|
+
const dirPath = safePath(args.path);
|
|
707
|
+
if (!existsSync(dirPath)) {
|
|
708
|
+
return { content: [{ type: "text", text: "Directory not found: " + args.path }], isError: true };
|
|
709
|
+
}
|
|
710
|
+
const entries = readdirSync(dirPath).map((name) => {
|
|
711
|
+
const full = join(dirPath, name);
|
|
712
|
+
const stat = statSync(full);
|
|
713
|
+
return (stat.isDirectory() ? "d" : "-") + " " + name;
|
|
714
|
+
});
|
|
715
|
+
return { content: [{ type: "text", text: entries.join("\\n") || "(empty)" }] };
|
|
716
|
+
}
|
|
717
|
+
case "execute_command": {
|
|
718
|
+
const output = execSync(args.command, {
|
|
719
|
+
cwd: WORKSPACE,
|
|
720
|
+
timeout: 30000,
|
|
721
|
+
maxBuffer: 1024 * 1024,
|
|
722
|
+
encoding: "utf-8",
|
|
723
|
+
});
|
|
724
|
+
return { content: [{ type: "text", text: output }] };
|
|
725
|
+
}
|
|
726
|
+
default:
|
|
727
|
+
return { content: [{ type: "text", text: "Unknown tool: " + name }], isError: true };
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function createMCPServer() {
|
|
732
|
+
const srv = new Server(
|
|
733
|
+
{ name: "seclaw-commander", version: "1.0.0" },
|
|
734
|
+
{ capabilities: { tools: {} } }
|
|
735
|
+
);
|
|
736
|
+
srv.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
737
|
+
srv.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
738
|
+
const { name, arguments: args } = request.params;
|
|
739
|
+
try { return handleTool(name, args); }
|
|
740
|
+
catch (err) { return { content: [{ type: "text", text: "Error: " + err.message }], isError: true }; }
|
|
741
|
+
});
|
|
742
|
+
return srv;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const sessions = new Map();
|
|
746
|
+
const httpServer = createServer(async (req, res) => {
|
|
747
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
748
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
749
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
750
|
+
if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
|
|
751
|
+
|
|
752
|
+
const url = new URL(req.url, "http://localhost:3000");
|
|
753
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
754
|
+
const mcpServer = createMCPServer();
|
|
755
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
756
|
+
sessions.set(transport.sessionId, { server: mcpServer, transport });
|
|
757
|
+
res.on("close", () => sessions.delete(transport.sessionId));
|
|
758
|
+
await mcpServer.connect(transport);
|
|
759
|
+
} else if (req.method === "POST" && url.pathname === "/messages") {
|
|
760
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
761
|
+
const session = sessions.get(sessionId);
|
|
762
|
+
if (session) { await session.transport.handlePostMessage(req, res); }
|
|
763
|
+
else { res.writeHead(400); res.end("Session not found"); }
|
|
764
|
+
} else if (url.pathname === "/health") {
|
|
765
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
766
|
+
res.end(JSON.stringify({ status: "ok", transport: "sse", sessions: sessions.size }));
|
|
767
|
+
} else { res.writeHead(404); res.end(); }
|
|
768
|
+
});
|
|
769
|
+
httpServer.listen(3000, () => { console.log("seclaw-commander MCP server (SSE) on port 3000"); });
|
|
770
|
+
`;
|
|
771
|
+
await writeFile2(join2(dir, "commander", "package.json"), packageJson);
|
|
772
|
+
await writeFile2(join2(dir, "commander", "Dockerfile"), dockerfile);
|
|
773
|
+
await writeFile2(join2(dir, "commander", "server.js"), serverJs);
|
|
774
|
+
}
|
|
775
|
+
async function writePermissions(dir) {
|
|
776
|
+
const content = `version: 1
|
|
777
|
+
rules:
|
|
778
|
+
filesystem:
|
|
779
|
+
allow:
|
|
780
|
+
- /workspace/**
|
|
781
|
+
deny:
|
|
782
|
+
- /workspace/.env*
|
|
783
|
+
- /workspace/.git/**
|
|
784
|
+
- /workspace/node_modules/**
|
|
785
|
+
commands:
|
|
786
|
+
allow:
|
|
787
|
+
- ls
|
|
788
|
+
- cat
|
|
789
|
+
- head
|
|
790
|
+
- tail
|
|
791
|
+
- grep
|
|
792
|
+
- find
|
|
793
|
+
- wc
|
|
794
|
+
- sort
|
|
795
|
+
- uniq
|
|
796
|
+
- node
|
|
797
|
+
- npm
|
|
798
|
+
- npx
|
|
799
|
+
- python3
|
|
800
|
+
- pip
|
|
801
|
+
- git status
|
|
802
|
+
- git log
|
|
803
|
+
- git diff
|
|
804
|
+
deny:
|
|
805
|
+
- rm -rf /
|
|
806
|
+
- curl
|
|
807
|
+
- wget
|
|
808
|
+
- ssh
|
|
809
|
+
- sudo
|
|
810
|
+
- chmod
|
|
811
|
+
- chown
|
|
812
|
+
max_execution_time: 30s
|
|
813
|
+
max_output_size: 1MB
|
|
814
|
+
confirmation_required:
|
|
815
|
+
- pattern: "send*email*"
|
|
816
|
+
- pattern: "delete*"
|
|
817
|
+
- pattern: "post*"
|
|
818
|
+
- pattern: "publish*"
|
|
819
|
+
- pattern: "deploy*"
|
|
820
|
+
`;
|
|
821
|
+
await writeFile2(join2(dir, "permissions.yml"), content);
|
|
822
|
+
}
|
|
823
|
+
async function writeIfMissing(path, content) {
|
|
824
|
+
if (!existsSync3(path)) {
|
|
825
|
+
await writeFile2(path, content);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function writeSeedFiles(dir, answers) {
|
|
829
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
830
|
+
await writeIfMissing(
|
|
831
|
+
join2(dir, "shared/memory/learnings.md"),
|
|
832
|
+
`# Agent Memory
|
|
833
|
+
|
|
834
|
+
## User Profile
|
|
835
|
+
- Timezone: ${answers.timezone}
|
|
836
|
+
- Setup date: ${today}
|
|
837
|
+
- Provider: ${answers.llmProvider}
|
|
838
|
+
|
|
839
|
+
## Preferences
|
|
840
|
+
- (Agent will learn and add preferences here over time)
|
|
841
|
+
|
|
842
|
+
## Lessons Learned
|
|
843
|
+
- (Agent will record what works and what doesn't)
|
|
844
|
+
`
|
|
845
|
+
);
|
|
846
|
+
await writeIfMissing(
|
|
847
|
+
join2(dir, "shared/tasks/welcome.md"),
|
|
848
|
+
`# Welcome Task
|
|
849
|
+
- [ ] Introduce yourself to the user on Telegram
|
|
850
|
+
- [ ] Explain what you can do (file management, email, task tracking)
|
|
851
|
+
- [ ] Ask the user what they'd like help with today
|
|
852
|
+
- [ ] Create today's report at reports/${today}.md after first interaction
|
|
853
|
+
`
|
|
854
|
+
);
|
|
855
|
+
await writeIfMissing(
|
|
856
|
+
join2(dir, "shared/config/agent.md"),
|
|
857
|
+
`# Agent Configuration
|
|
858
|
+
|
|
859
|
+
## Workspace Structure
|
|
860
|
+
- tasks/ \u2014 active tasks and TODO lists
|
|
861
|
+
- reports/ \u2014 daily reports (YYYY-MM-DD.md)
|
|
862
|
+
- notes/ \u2014 quick notes and references
|
|
863
|
+
- drafts/ \u2014 work in progress documents
|
|
864
|
+
- memory/ \u2014 persistent learnings (survives restarts)
|
|
865
|
+
- config/ \u2014 this file and user preferences
|
|
866
|
+
|
|
867
|
+
## Behavior Rules
|
|
868
|
+
- Always read memory/learnings.md at the start of a conversation
|
|
869
|
+
- Always check tasks/ for pending work
|
|
870
|
+
- Write daily reports to reports/
|
|
871
|
+
- Update memory/learnings.md when you learn something new about the user
|
|
872
|
+
- Keep Telegram messages concise \u2014 use bullet points
|
|
873
|
+
- Detect user language and respond in the same language
|
|
874
|
+
`
|
|
875
|
+
);
|
|
876
|
+
const integrations2 = [
|
|
877
|
+
`# Connected Integrations`,
|
|
878
|
+
``,
|
|
879
|
+
`Template: ${answers.template}`,
|
|
880
|
+
``,
|
|
881
|
+
`## Connected`,
|
|
882
|
+
`- [x] telegram \u2014 Telegram Bot`,
|
|
883
|
+
`- [x] ${answers.llmProvider} \u2014 LLM Provider`
|
|
884
|
+
];
|
|
885
|
+
if (answers.composioApiKey) {
|
|
886
|
+
integrations2.push(`- [x] composio \u2014 Integration Hub (Gmail, Calendar, etc.)`);
|
|
887
|
+
}
|
|
888
|
+
integrations2.push(
|
|
889
|
+
``,
|
|
890
|
+
`## Available`,
|
|
891
|
+
`Run \`npx seclaw integrations\` to add integrations (Gmail, Google Drive, GitHub, etc.)`,
|
|
892
|
+
``
|
|
893
|
+
);
|
|
894
|
+
await writeIfMissing(
|
|
895
|
+
join2(dir, "shared/config/integrations.md"),
|
|
896
|
+
integrations2.join("\n")
|
|
897
|
+
);
|
|
898
|
+
await writeIfMissing(
|
|
899
|
+
join2(dir, `shared/reports/${today}.md`),
|
|
900
|
+
`# Daily Report \u2014 ${today}
|
|
901
|
+
|
|
902
|
+
## Status
|
|
903
|
+
Agent initialized. Waiting for first interaction.
|
|
904
|
+
|
|
905
|
+
## Tasks
|
|
906
|
+
(Will be populated after first conversation)
|
|
907
|
+
|
|
908
|
+
## Notes
|
|
909
|
+
(Will be populated throughout the day)
|
|
910
|
+
`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
async function writeGitignore(dir) {
|
|
914
|
+
const content = `.env
|
|
915
|
+
.env.*
|
|
916
|
+
!.env.example
|
|
917
|
+
node_modules/
|
|
918
|
+
*.log
|
|
919
|
+
.DS_Store
|
|
920
|
+
.tunnel-url
|
|
921
|
+
`;
|
|
922
|
+
await writeIfMissing(join2(dir, ".gitignore"), content);
|
|
923
|
+
}
|
|
924
|
+
async function writeDockerignore(dir) {
|
|
925
|
+
const content = `.env
|
|
926
|
+
.env.*
|
|
927
|
+
.git
|
|
928
|
+
.gitignore
|
|
929
|
+
.DS_Store
|
|
930
|
+
*.log
|
|
931
|
+
*.md
|
|
932
|
+
shared/
|
|
933
|
+
templates/
|
|
934
|
+
.tunnel-url
|
|
935
|
+
`;
|
|
936
|
+
await writeIfMissing(join2(dir, ".dockerignore"), content);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/docker.ts
|
|
940
|
+
import { execa as execa2, execaSync } from "execa";
|
|
941
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
942
|
+
import { resolve as resolve4 } from "path";
|
|
943
|
+
async function checkDocker() {
|
|
944
|
+
try {
|
|
945
|
+
await execa2("docker", ["version"]);
|
|
946
|
+
} catch {
|
|
947
|
+
return { ok: false, error: "Docker is not installed. Download it at https://docker.com/get-started" };
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
await execa2("docker", ["info"]);
|
|
951
|
+
} catch {
|
|
952
|
+
return { ok: false, error: "Docker is not running. Open Docker Desktop and try again." };
|
|
953
|
+
}
|
|
954
|
+
return { ok: true };
|
|
955
|
+
}
|
|
956
|
+
async function stopExistingSeclaw() {
|
|
957
|
+
try {
|
|
958
|
+
const result = await execa2("docker", [
|
|
959
|
+
"ps",
|
|
960
|
+
"-a",
|
|
961
|
+
"--filter",
|
|
962
|
+
"label=com.docker.compose.service=agent",
|
|
963
|
+
"--format",
|
|
964
|
+
'{{index .Labels "com.docker.compose.project"}}'
|
|
965
|
+
]);
|
|
966
|
+
const projects = /* @__PURE__ */ new Set();
|
|
967
|
+
for (const line of result.stdout.split("\n").filter(Boolean)) {
|
|
968
|
+
projects.add(line.trim());
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
const portResult = await execa2("docker", [
|
|
972
|
+
"ps",
|
|
973
|
+
"--filter",
|
|
974
|
+
"publish=8288",
|
|
975
|
+
"--format",
|
|
976
|
+
'{{index .Labels "com.docker.compose.project"}}'
|
|
977
|
+
]);
|
|
978
|
+
for (const line of portResult.stdout.split("\n").filter(Boolean)) {
|
|
979
|
+
projects.add(line.trim());
|
|
980
|
+
}
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
for (const project of projects) {
|
|
984
|
+
if (!project) continue;
|
|
985
|
+
try {
|
|
986
|
+
await execa2("docker", ["compose", "-p", project, "down"]);
|
|
987
|
+
} catch {
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
} catch {
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async function findRunningSeclaw() {
|
|
994
|
+
const agentResult = await findContainerByLabel("seclaw");
|
|
995
|
+
if (agentResult) return agentResult;
|
|
996
|
+
try {
|
|
997
|
+
const result = await execa2("docker", [
|
|
998
|
+
"ps",
|
|
999
|
+
"--filter",
|
|
1000
|
+
"publish=5678",
|
|
1001
|
+
"--format",
|
|
1002
|
+
"{{.Names}}"
|
|
1003
|
+
]);
|
|
1004
|
+
const names = result.stdout.trim();
|
|
1005
|
+
if (!names) return null;
|
|
1006
|
+
const containerName = names.split("\n")[0];
|
|
1007
|
+
try {
|
|
1008
|
+
const inspect = await execa2("docker", [
|
|
1009
|
+
"inspect",
|
|
1010
|
+
containerName,
|
|
1011
|
+
"--format",
|
|
1012
|
+
'{{index .Config.Labels "com.docker.compose.project.working_dir"}}'
|
|
1013
|
+
]);
|
|
1014
|
+
return { dir: inspect.stdout.trim() || null };
|
|
1015
|
+
} catch {
|
|
1016
|
+
return { dir: null };
|
|
1017
|
+
}
|
|
1018
|
+
} catch {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
async function findContainerByLabel(project) {
|
|
1023
|
+
try {
|
|
1024
|
+
const result = await execa2("docker", [
|
|
1025
|
+
"ps",
|
|
1026
|
+
"--filter",
|
|
1027
|
+
`label=com.docker.compose.project=${project}`,
|
|
1028
|
+
"--format",
|
|
1029
|
+
"{{.Names}}"
|
|
1030
|
+
]);
|
|
1031
|
+
const names = result.stdout.trim();
|
|
1032
|
+
if (!names) return null;
|
|
1033
|
+
const containerName = names.split("\n")[0];
|
|
1034
|
+
try {
|
|
1035
|
+
const inspect = await execa2("docker", [
|
|
1036
|
+
"inspect",
|
|
1037
|
+
containerName,
|
|
1038
|
+
"--format",
|
|
1039
|
+
'{{index .Config.Labels "com.docker.compose.project.working_dir"}}'
|
|
1040
|
+
]);
|
|
1041
|
+
return { dir: inspect.stdout.trim() || null };
|
|
1042
|
+
} catch {
|
|
1043
|
+
return { dir: null };
|
|
1044
|
+
}
|
|
1045
|
+
} catch {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function findProjectDir() {
|
|
1050
|
+
try {
|
|
1051
|
+
const result = execaSync("docker", [
|
|
1052
|
+
"ps",
|
|
1053
|
+
"--filter",
|
|
1054
|
+
"label=com.docker.compose.service=agent",
|
|
1055
|
+
"--format",
|
|
1056
|
+
"{{.Names}}"
|
|
1057
|
+
]);
|
|
1058
|
+
const names = result.stdout.trim().split("\n").filter(Boolean);
|
|
1059
|
+
for (const name of names) {
|
|
1060
|
+
try {
|
|
1061
|
+
const inspect = execaSync("docker", [
|
|
1062
|
+
"inspect",
|
|
1063
|
+
name,
|
|
1064
|
+
"--format",
|
|
1065
|
+
'{{index .Config.Labels "com.docker.compose.project.working_dir"}}'
|
|
1066
|
+
]);
|
|
1067
|
+
const dir = inspect.stdout.trim();
|
|
1068
|
+
if (dir && existsSync4(resolve4(dir, "docker-compose.yml"))) {
|
|
1069
|
+
return dir;
|
|
1070
|
+
}
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
} catch {
|
|
1075
|
+
}
|
|
1076
|
+
const candidates = [
|
|
1077
|
+
process.cwd(),
|
|
1078
|
+
resolve4(process.cwd(), ".."),
|
|
1079
|
+
resolve4(process.env.HOME || "~", "seclaw"),
|
|
1080
|
+
resolve4(process.env.HOME || "~", "my-agent")
|
|
1081
|
+
];
|
|
1082
|
+
for (const dir of candidates) {
|
|
1083
|
+
const composePath = resolve4(dir, "docker-compose.yml");
|
|
1084
|
+
if (existsSync4(composePath)) {
|
|
1085
|
+
try {
|
|
1086
|
+
const content = readFileSync2(composePath, "utf-8");
|
|
1087
|
+
if (content.includes("agent") && content.includes("agent-net")) {
|
|
1088
|
+
return dir;
|
|
1089
|
+
}
|
|
1090
|
+
} catch {
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/commands/create.ts
|
|
1099
|
+
async function create(directory) {
|
|
1100
|
+
let targetDir = resolve5(process.cwd(), directory);
|
|
1101
|
+
try {
|
|
1102
|
+
await mkdir2(targetDir, { recursive: true });
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
p2.intro(`${pc2.bgCyan(pc2.black(" seclaw "))}`);
|
|
1105
|
+
p2.log.error(`Cannot create directory: ${pc2.cyan(targetDir)}`);
|
|
1106
|
+
p2.log.info(pc2.dim(err instanceof Error ? err.message : String(err)));
|
|
1107
|
+
p2.outro(`Try a different path, e.g.: ${pc2.cyan(`npx seclaw create ./${directory.replace(/^\/+/, "")}`)}`);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const docker = await checkDocker();
|
|
1111
|
+
if (!docker.ok) {
|
|
1112
|
+
p2.intro(`${pc2.bgCyan(pc2.black(" seclaw "))}`);
|
|
1113
|
+
p2.log.error(docker.error);
|
|
1114
|
+
p2.outro("Install Docker and try again.");
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const running = await findRunningSeclaw();
|
|
1118
|
+
const existingDir = findProjectDir();
|
|
1119
|
+
const hasExisting = existsSync5(resolve5(targetDir, "docker-compose.yml"));
|
|
1120
|
+
if (running || existingDir || hasExisting) {
|
|
1121
|
+
const location = existingDir || running?.dir || (hasExisting ? targetDir : null);
|
|
1122
|
+
if (location) {
|
|
1123
|
+
targetDir = location;
|
|
1124
|
+
}
|
|
1125
|
+
p2.intro(`${pc2.bgCyan(pc2.black(" seclaw "))}`);
|
|
1126
|
+
p2.log.warn(
|
|
1127
|
+
`An agent is already running` + (location ? ` at ${pc2.cyan(location)}` : "")
|
|
1128
|
+
);
|
|
1129
|
+
const proceed = await p2.confirm({
|
|
1130
|
+
message: "Stop everything and start fresh?",
|
|
1131
|
+
initialValue: false
|
|
1132
|
+
});
|
|
1133
|
+
if (p2.isCancel(proceed) || !proceed) {
|
|
1134
|
+
p2.outro("Cancelled.");
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const answers = await collectSetupAnswers(targetDir);
|
|
1139
|
+
const s = p2.spinner();
|
|
1140
|
+
s.start("Preparing environment...");
|
|
1141
|
+
try {
|
|
1142
|
+
await stopExistingSeclaw();
|
|
1143
|
+
s.stop("Environment ready.");
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
s.stop("Warning: could not stop existing containers.");
|
|
1146
|
+
p2.log.warn(err instanceof Error ? err.message : String(err));
|
|
1147
|
+
}
|
|
1148
|
+
s.start("Creating project files...");
|
|
1149
|
+
try {
|
|
1150
|
+
await scaffoldProject(targetDir, answers);
|
|
1151
|
+
s.stop("Project files created.");
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
s.stop("Failed to create project files.");
|
|
1154
|
+
p2.log.error(err instanceof Error ? err.message : String(err));
|
|
1155
|
+
if (err instanceof Error && err.stack) {
|
|
1156
|
+
p2.log.info(pc2.dim(err.stack));
|
|
1157
|
+
}
|
|
1158
|
+
p2.outro("Fix the issue above and try again.");
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
if (answers.template !== "blank") {
|
|
1162
|
+
s.start(`Copying template: ${answers.template}...`);
|
|
1163
|
+
try {
|
|
1164
|
+
const templateSrc = getTemplatePath(answers.template, targetDir);
|
|
1165
|
+
const templateDest = resolve5(targetDir, "templates", answers.template);
|
|
1166
|
+
if (templateSrc && existsSync5(templateSrc)) {
|
|
1167
|
+
if (resolve5(templateSrc) !== resolve5(templateDest)) {
|
|
1168
|
+
await cp2(templateSrc, templateDest, { recursive: true });
|
|
1169
|
+
}
|
|
1170
|
+
const promptSrc = resolve5(templateSrc, "system-prompt.md");
|
|
1171
|
+
if (existsSync5(promptSrc)) {
|
|
1172
|
+
await cp2(promptSrc, resolve5(targetDir, "shared", "config", "system-prompt.md"));
|
|
1173
|
+
}
|
|
1174
|
+
const schedulesSrc = resolve5(templateSrc, "schedules.json");
|
|
1175
|
+
if (existsSync5(schedulesSrc)) {
|
|
1176
|
+
await cp2(schedulesSrc, resolve5(targetDir, "shared", "config", "schedules.json"));
|
|
1177
|
+
}
|
|
1178
|
+
s.stop(`Template ready.`);
|
|
1179
|
+
} else {
|
|
1180
|
+
s.stop("Template will be available after setup.");
|
|
1181
|
+
}
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
s.stop("Failed to copy template.");
|
|
1184
|
+
p2.log.error(err instanceof Error ? err.message : String(err));
|
|
1185
|
+
if (err instanceof Error && err.stack) {
|
|
1186
|
+
p2.log.info(pc2.dim(err.stack));
|
|
1187
|
+
}
|
|
1188
|
+
p2.outro("Fix the issue above and try again.");
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
await startServices(targetDir, s);
|
|
1193
|
+
s.start("Waiting for agent...");
|
|
1194
|
+
const ready = await waitForAgent(targetDir);
|
|
1195
|
+
s.stop(ready ? "Agent is ready!" : "Agent is starting (may take a moment).");
|
|
1196
|
+
let tunnelUrl = "";
|
|
1197
|
+
s.start("Waiting for Cloudflare Tunnel...");
|
|
1198
|
+
const url = await getTunnelUrl(targetDir);
|
|
1199
|
+
if (url) {
|
|
1200
|
+
tunnelUrl = url;
|
|
1201
|
+
s.stop(`Tunnel ready: ${pc2.cyan(tunnelUrl)}`);
|
|
1202
|
+
} else {
|
|
1203
|
+
s.stop("Tunnel starting \u2014 check with: npx seclaw status");
|
|
1204
|
+
}
|
|
1205
|
+
if (tunnelUrl && answers.telegramToken) {
|
|
1206
|
+
s.start("Setting up Telegram webhook...");
|
|
1207
|
+
const webhookUrl = `${tunnelUrl}/webhook`;
|
|
1208
|
+
const ok = await setTelegramWebhook(answers.telegramToken, tunnelUrl);
|
|
1209
|
+
if (ok) {
|
|
1210
|
+
s.stop("Telegram webhook configured!");
|
|
1211
|
+
} else {
|
|
1212
|
+
s.stop(`Webhook URL: ${pc2.cyan(webhookUrl)}`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
showSuccess(targetDir, tunnelUrl, answers.telegramBotName, answers.llmProvider, answers.composioApiKey);
|
|
1216
|
+
}
|
|
1217
|
+
async function waitForAgent(targetDir, maxRetries = 30, intervalMs = 2e3) {
|
|
1218
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
1219
|
+
try {
|
|
1220
|
+
const result = await execa3("docker", [
|
|
1221
|
+
"compose",
|
|
1222
|
+
"ps",
|
|
1223
|
+
"--format",
|
|
1224
|
+
"json",
|
|
1225
|
+
"agent"
|
|
1226
|
+
], {
|
|
1227
|
+
cwd: targetDir,
|
|
1228
|
+
env: { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" }
|
|
1229
|
+
});
|
|
1230
|
+
const lines = result.stdout.trim().split("\n");
|
|
1231
|
+
for (const line of lines) {
|
|
1232
|
+
try {
|
|
1233
|
+
const container = JSON.parse(line);
|
|
1234
|
+
if (container.Health === "healthy" || container.State === "running") {
|
|
1235
|
+
try {
|
|
1236
|
+
const healthRes = await fetch("http://localhost:3000/health");
|
|
1237
|
+
if (healthRes.ok) return true;
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
if (container.Health === "healthy") return true;
|
|
1241
|
+
}
|
|
1242
|
+
} catch {
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1248
|
+
}
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
async function startServices(targetDir, s) {
|
|
1252
|
+
s.start("Starting services (first run builds images, ~1-2 min)...");
|
|
1253
|
+
try {
|
|
1254
|
+
await execa3("docker", ["compose", "up", "-d", "--build"], {
|
|
1255
|
+
cwd: targetDir,
|
|
1256
|
+
env: { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" }
|
|
1257
|
+
});
|
|
1258
|
+
s.stop("All services started.");
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
s.stop("Failed to start services.");
|
|
1261
|
+
const stderr = err instanceof Error && "stderr" in err ? err.stderr : "";
|
|
1262
|
+
if (stderr.includes("port is already allocated") || stderr.includes("address already in use")) {
|
|
1263
|
+
p2.log.error("Port 3000 is already in use by another application.");
|
|
1264
|
+
p2.log.info(` Run: ${pc2.cyan("npx seclaw stop")} first, then try again.`);
|
|
1265
|
+
} else if (stderr.includes("Cannot connect to the Docker daemon")) {
|
|
1266
|
+
p2.log.error("Docker is not running. Open Docker Desktop and try again.");
|
|
1267
|
+
} else {
|
|
1268
|
+
p2.log.error("Docker error:");
|
|
1269
|
+
p2.log.info(pc2.dim(stderr || "Unknown error \u2014 check Docker Desktop for details."));
|
|
1270
|
+
p2.log.info(` Manual start: ${pc2.cyan(`cd ${targetDir} && docker compose up -d --build`)}`);
|
|
1271
|
+
}
|
|
1272
|
+
p2.outro("Fix the issue above and try again.");
|
|
1273
|
+
process.exit(1);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
var PROVIDER_LABELS = {
|
|
1277
|
+
openrouter: { model: "Gemini 3 Flash", cost: "~$3-10/mo" },
|
|
1278
|
+
anthropic: { model: "Claude Sonnet 4.5", cost: "~$15-30/mo" },
|
|
1279
|
+
openai: { model: "GPT-4o", cost: "~$10-25/mo" },
|
|
1280
|
+
gemini: { model: "Gemini 2.5 Pro", cost: "~$7-20/mo" }
|
|
1281
|
+
};
|
|
1282
|
+
function showSuccess(targetDir, tunnelUrl, botName, provider, composioKey) {
|
|
1283
|
+
const label = (s) => pc2.white(pc2.bold(s));
|
|
1284
|
+
const lines = [
|
|
1285
|
+
`${pc2.green(pc2.bold("Your AI agent is live!"))}`,
|
|
1286
|
+
""
|
|
1287
|
+
];
|
|
1288
|
+
if (tunnelUrl) {
|
|
1289
|
+
lines.push(`${label("Public URL:")} ${pc2.cyan(tunnelUrl)}`);
|
|
1290
|
+
}
|
|
1291
|
+
if (botName) {
|
|
1292
|
+
lines.push(`${label("Telegram:")} Open ${pc2.green(botName)} and send a message`);
|
|
1293
|
+
}
|
|
1294
|
+
lines.push(`${label("Workspace:")} ${pc2.white(targetDir + "/shared/")}`);
|
|
1295
|
+
if (provider) {
|
|
1296
|
+
const info = PROVIDER_LABELS[provider];
|
|
1297
|
+
if (info) {
|
|
1298
|
+
lines.push(`${label("Model:")} ${pc2.cyan(info.model)}`);
|
|
1299
|
+
lines.push(`${label("Est. cost:")} ${pc2.green(info.cost)}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (composioKey) {
|
|
1303
|
+
lines.push(`${label("Composio:")} ${pc2.green("Connected")} \u2014 run ${pc2.cyan("npx seclaw integrations")} to add Gmail, etc.`);
|
|
1304
|
+
} else {
|
|
1305
|
+
lines.push(`${label("Composio:")} ${pc2.yellow("Skipped")} \u2014 run ${pc2.cyan("npx seclaw integrations")} later to add integrations`);
|
|
1306
|
+
}
|
|
1307
|
+
p2.note(lines.join("\n"), "seclaw");
|
|
1308
|
+
if (provider === "openrouter") {
|
|
1309
|
+
p2.log.info(
|
|
1310
|
+
`${pc2.bold("Gemini 3 Flash:")} Fast, affordable, and great with tools.
|
|
1311
|
+
Change model anytime in .env \u2192 LLM_MODEL=your/model`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
p2.outro(`${pc2.white("Manage:")} npx seclaw status | stop | integrations`);
|
|
1315
|
+
}
|
|
1316
|
+
function getTemplatePath(template, targetDir) {
|
|
1317
|
+
if (targetDir) {
|
|
1318
|
+
const installed = resolve5(targetDir, "templates", template);
|
|
1319
|
+
if (existsSync5(installed)) return installed;
|
|
1320
|
+
}
|
|
1321
|
+
const bundled = resolve5(import.meta.dirname, "templates", "free", template);
|
|
1322
|
+
if (existsSync5(bundled)) return bundled;
|
|
1323
|
+
const bundledPaid = resolve5(import.meta.dirname, "templates", "paid", template);
|
|
1324
|
+
if (existsSync5(bundledPaid)) return bundledPaid;
|
|
1325
|
+
const local = resolve5(process.cwd(), "packages", "cli", "templates", "free", template);
|
|
1326
|
+
if (existsSync5(local)) return local;
|
|
1327
|
+
return null;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/commands/add.ts
|
|
1331
|
+
import { resolve as resolve6 } from "path";
|
|
1332
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1333
|
+
import { writeFile as writeFile3, mkdir as mkdir3, readFile as readFile3, cp as cp3 } from "fs/promises";
|
|
1334
|
+
import { execSync } from "child_process";
|
|
1335
|
+
import * as p3 from "@clack/prompts";
|
|
1336
|
+
import pc3 from "picocolors";
|
|
1337
|
+
var API_URL = process.env.SECLAW_API || "https://seclawai.com";
|
|
1338
|
+
var FREE_TEMPLATES = ["productivity-agent", "data-analyst"];
|
|
1339
|
+
async function add(template, options) {
|
|
1340
|
+
p3.intro(`${pc3.bgCyan(pc3.black(" seclaw "))} Adding template: ${template}`);
|
|
1341
|
+
const isFree = FREE_TEMPLATES.includes(template);
|
|
1342
|
+
const s = p3.spinner();
|
|
1343
|
+
const monorepoTemplatesDir = resolve6(import.meta.dirname, "..", "..", "templates", "paid");
|
|
1344
|
+
const templateDir = !isFree && existsSync6(resolve6(import.meta.dirname, "..", "..", "templates")) ? resolve6(monorepoTemplatesDir, template) : resolve6(process.cwd(), "templates", template);
|
|
1345
|
+
if (isFree) {
|
|
1346
|
+
s.start("Setting up free template...");
|
|
1347
|
+
const bundledPath = resolve6(
|
|
1348
|
+
import.meta.dirname,
|
|
1349
|
+
"templates",
|
|
1350
|
+
"free",
|
|
1351
|
+
template
|
|
1352
|
+
);
|
|
1353
|
+
if (!existsSync6(bundledPath)) {
|
|
1354
|
+
s.stop("Template not found.");
|
|
1355
|
+
p3.log.error("Bundled template not found. Try updating: npm i -g seclaw");
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
await mkdir3(templateDir, { recursive: true });
|
|
1359
|
+
const filesToCopy = [
|
|
1360
|
+
"system-prompt.md",
|
|
1361
|
+
"config.json",
|
|
1362
|
+
"README.md",
|
|
1363
|
+
"manifest.json",
|
|
1364
|
+
"schedules.json"
|
|
1365
|
+
];
|
|
1366
|
+
for (const file of filesToCopy) {
|
|
1367
|
+
const src = resolve6(bundledPath, file);
|
|
1368
|
+
if (existsSync6(src)) {
|
|
1369
|
+
const content = await readFile3(src, "utf-8");
|
|
1370
|
+
await writeFile3(resolve6(templateDir, file), content);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
s.stop("Template files ready.");
|
|
1374
|
+
} else {
|
|
1375
|
+
if (!options.key) {
|
|
1376
|
+
p3.log.error(
|
|
1377
|
+
`${pc3.bold(template)} is a paid template. Provide your token with --key`
|
|
1378
|
+
);
|
|
1379
|
+
p3.log.info("");
|
|
1380
|
+
p3.log.info(` ${pc3.dim("1.")} Purchase at ${pc3.cyan("https://seclawai.com/templates")}`);
|
|
1381
|
+
p3.log.info(` ${pc3.dim("2.")} npx seclaw add ${template} --key YOUR_TOKEN`);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
s.start("Validating token...");
|
|
1385
|
+
try {
|
|
1386
|
+
const res = await fetch(`${API_URL}/api/templates/activate`, {
|
|
1387
|
+
method: "POST",
|
|
1388
|
+
headers: { "Content-Type": "application/json" },
|
|
1389
|
+
body: JSON.stringify({ token: options.key, templateId: template })
|
|
1390
|
+
});
|
|
1391
|
+
if (!res.ok) {
|
|
1392
|
+
const err = await res.json();
|
|
1393
|
+
s.stop("Activation failed.");
|
|
1394
|
+
p3.log.error(err.error || "Invalid token or template not owned.");
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
const data = await res.json();
|
|
1398
|
+
s.stop(`Template ready: ${pc3.green(data.templateName)}`);
|
|
1399
|
+
s.start("Writing template files...");
|
|
1400
|
+
await mkdir3(templateDir, { recursive: true });
|
|
1401
|
+
for (const [filename, content] of Object.entries(data.files)) {
|
|
1402
|
+
await writeFile3(resolve6(templateDir, filename), content);
|
|
1403
|
+
}
|
|
1404
|
+
s.stop(`${Object.keys(data.files).length} files written to ${pc3.dim(templateDir)}`);
|
|
1405
|
+
const cliDir = resolve6(import.meta.dirname, "..");
|
|
1406
|
+
const cliPkgPath = resolve6(cliDir, "package.json");
|
|
1407
|
+
if (existsSync6(cliPkgPath)) {
|
|
1408
|
+
s.start("Rebuilding CLI...");
|
|
1409
|
+
try {
|
|
1410
|
+
execSync("pnpm build", { cwd: cliDir, stdio: "pipe" });
|
|
1411
|
+
s.stop("CLI rebuilt with new template.");
|
|
1412
|
+
} catch {
|
|
1413
|
+
s.stop("CLI rebuild failed \u2014 run `pnpm build` manually.");
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
s.stop("Failed.");
|
|
1418
|
+
p3.log.error(`Network error: ${err}`);
|
|
1419
|
+
p3.log.info("Check your connection and try again.");
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const configDir = resolve6(process.cwd(), "shared", "config");
|
|
1424
|
+
if (existsSync6(configDir)) {
|
|
1425
|
+
const capDir = resolve6(configDir, "capabilities", template);
|
|
1426
|
+
await mkdir3(capDir, { recursive: true });
|
|
1427
|
+
const promptPath = resolve6(templateDir, "system-prompt.md");
|
|
1428
|
+
if (existsSync6(promptPath)) {
|
|
1429
|
+
await cp3(promptPath, resolve6(capDir, "system-prompt.md"));
|
|
1430
|
+
}
|
|
1431
|
+
const schedulesPath = resolve6(templateDir, "schedules.json");
|
|
1432
|
+
if (existsSync6(schedulesPath)) {
|
|
1433
|
+
await cp3(schedulesPath, resolve6(capDir, "schedules.json"));
|
|
1434
|
+
}
|
|
1435
|
+
const installedPath = resolve6(configDir, "installed.json");
|
|
1436
|
+
let installed = { capabilities: [] };
|
|
1437
|
+
try {
|
|
1438
|
+
if (existsSync6(installedPath)) {
|
|
1439
|
+
installed = JSON.parse(await readFile3(installedPath, "utf-8"));
|
|
1440
|
+
}
|
|
1441
|
+
} catch {
|
|
1442
|
+
}
|
|
1443
|
+
if (!installed.capabilities.includes(template)) {
|
|
1444
|
+
installed.capabilities.push(template);
|
|
1445
|
+
}
|
|
1446
|
+
await writeFile3(installedPath, JSON.stringify(installed, null, 2) + "\n");
|
|
1447
|
+
let templateName = template;
|
|
1448
|
+
const manifestPath = resolve6(templateDir, "manifest.json");
|
|
1449
|
+
try {
|
|
1450
|
+
if (existsSync6(manifestPath)) {
|
|
1451
|
+
const manifest = JSON.parse(await readFile3(manifestPath, "utf-8"));
|
|
1452
|
+
if (manifest.name) templateName = manifest.name;
|
|
1453
|
+
}
|
|
1454
|
+
} catch {
|
|
1455
|
+
}
|
|
1456
|
+
p3.log.success(
|
|
1457
|
+
`Capability '${pc3.bold(templateName)}' added. ${pc3.cyan(String(installed.capabilities.length))} capabilities active.`
|
|
1458
|
+
);
|
|
1459
|
+
p3.log.info("Restart agent to apply changes.");
|
|
1460
|
+
}
|
|
1461
|
+
p3.outro(`${pc3.green("Done!")} Template ${pc3.bold(template)} is ready.`);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// src/commands/integrations.ts
|
|
1465
|
+
import { resolve as resolve7 } from "path";
|
|
1466
|
+
import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
|
|
1467
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
1468
|
+
import { exec as exec2 } from "child_process";
|
|
1469
|
+
import { platform as platform2, homedir as homedir2 } from "os";
|
|
1470
|
+
import { execa as execa4 } from "execa";
|
|
1471
|
+
import * as p4 from "@clack/prompts";
|
|
1472
|
+
import pc4 from "picocolors";
|
|
1473
|
+
var INTEGRATIONS = {
|
|
1474
|
+
gmail: { name: "Gmail", app: "gmail", hint: "email reading, sending, labels" },
|
|
1475
|
+
"google-drive": { name: "Google Drive", app: "googledrive", hint: "file access, sharing" },
|
|
1476
|
+
"google-calendar": { name: "Google Calendar", app: "googlecalendar", hint: "events, scheduling" },
|
|
1477
|
+
"google-sheets": { name: "Google Sheets", app: "googlesheets", hint: "spreadsheets" },
|
|
1478
|
+
notion: { name: "Notion", app: "notion", hint: "notes, databases, pages" },
|
|
1479
|
+
github: { name: "GitHub", app: "github", hint: "repos, issues, PRs" },
|
|
1480
|
+
slack: { name: "Slack", app: "slack", hint: "team messaging, channels" },
|
|
1481
|
+
linear: { name: "Linear", app: "linear", hint: "issues, projects, teams" },
|
|
1482
|
+
trello: { name: "Trello", app: "trello", hint: "boards, cards, lists" },
|
|
1483
|
+
todoist: { name: "Todoist", app: "todoist", hint: "task management" },
|
|
1484
|
+
dropbox: { name: "Dropbox", app: "dropbox", hint: "file storage" },
|
|
1485
|
+
whatsapp: { name: "WhatsApp", app: "whatsapp", hint: "messaging", params: [
|
|
1486
|
+
{ key: "WABA_ID", label: "WhatsApp Business Account ID (WABA ID)", placeholder: "123456789012345" }
|
|
1487
|
+
] }
|
|
1488
|
+
};
|
|
1489
|
+
async function integrations() {
|
|
1490
|
+
const projectDir = findProjectDir2();
|
|
1491
|
+
if (!projectDir) {
|
|
1492
|
+
p4.intro(`${pc4.bgCyan(pc4.black(" seclaw "))}`);
|
|
1493
|
+
p4.log.error("No seclaw project found. Run from your project directory.");
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
let composioKey = await getComposioKey(projectDir);
|
|
1497
|
+
if (!composioKey) {
|
|
1498
|
+
const localKey = readComposioLocalKey2();
|
|
1499
|
+
if (localKey) {
|
|
1500
|
+
composioKey = localKey;
|
|
1501
|
+
await appendEnv(projectDir, "COMPOSIO_API_KEY", localKey);
|
|
1502
|
+
await appendEnv(projectDir, "COMPOSIO_USER_ID", generateUserId2());
|
|
1503
|
+
p4.intro(`${pc4.bgCyan(pc4.black(" seclaw "))}`);
|
|
1504
|
+
p4.log.success(`Composio API key auto-detected from ${pc4.dim("~/.composio/user_data.json")}`);
|
|
1505
|
+
await restartAgent(projectDir);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (!composioKey) {
|
|
1509
|
+
p4.intro(`${pc4.bgCyan(pc4.black(" seclaw "))}`);
|
|
1510
|
+
p4.log.warn("No Composio API key found.");
|
|
1511
|
+
const method = await p4.select({
|
|
1512
|
+
message: "How would you like to set up Composio?",
|
|
1513
|
+
options: [
|
|
1514
|
+
{ value: "browser", label: "Open browser", hint: "sign up + get API key (recommended)" },
|
|
1515
|
+
{ value: "paste", label: "Paste API key", hint: "if you already have one" },
|
|
1516
|
+
{ value: "cancel", label: "Cancel" }
|
|
1517
|
+
]
|
|
1518
|
+
});
|
|
1519
|
+
if (p4.isCancel(method) || method === "cancel") {
|
|
1520
|
+
p4.outro("Run again when ready.");
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (method === "browser") {
|
|
1524
|
+
const url = "https://platform.composio.dev";
|
|
1525
|
+
p4.log.info(
|
|
1526
|
+
`${pc4.dim("Opening:")} ${pc4.cyan(url)}
|
|
1527
|
+
${pc4.dim("1.")} Sign up or log in
|
|
1528
|
+
${pc4.dim("2.")} Click ${pc4.bold("Settings")} \u2192 ${pc4.bold("API Keys")} tab
|
|
1529
|
+
${pc4.dim("3.")} Click ${pc4.bold("New API key")}, copy and paste it below`
|
|
1530
|
+
);
|
|
1531
|
+
openBrowser(url);
|
|
1532
|
+
}
|
|
1533
|
+
const apiKey = await p4.text({
|
|
1534
|
+
message: "Paste your Composio API Key",
|
|
1535
|
+
placeholder: "ak_..."
|
|
1536
|
+
});
|
|
1537
|
+
if (p4.isCancel(apiKey) || !apiKey) {
|
|
1538
|
+
p4.outro("Cancelled.");
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
composioKey = apiKey;
|
|
1542
|
+
await appendEnv(projectDir, "COMPOSIO_API_KEY", composioKey);
|
|
1543
|
+
await appendEnv(projectDir, "COMPOSIO_USER_ID", generateUserId2());
|
|
1544
|
+
p4.log.success("Composio API key saved!");
|
|
1545
|
+
await restartAgent(projectDir);
|
|
1546
|
+
}
|
|
1547
|
+
const activeConnections = await getActiveConnections(composioKey);
|
|
1548
|
+
p4.intro(`${pc4.bgCyan(pc4.black(" seclaw "))} Manage Integrations`);
|
|
1549
|
+
const statusLines = [];
|
|
1550
|
+
const connectedCount = [...activeConnections.values()].length;
|
|
1551
|
+
for (const [, def] of Object.entries(INTEGRATIONS)) {
|
|
1552
|
+
if (activeConnections.has(def.app)) {
|
|
1553
|
+
statusLines.push(` ${pc4.green("\u2713")} ${def.name} ${pc4.dim("connected")}`);
|
|
1554
|
+
} else {
|
|
1555
|
+
statusLines.push(` ${pc4.dim("\u25CB")} ${def.name} ${pc4.dim(def.hint)}`);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
p4.log.message(statusLines.join("\n"));
|
|
1559
|
+
const hasConnected = connectedCount > 0;
|
|
1560
|
+
const hasAvailable = Object.values(INTEGRATIONS).some((def) => !activeConnections.has(def.app));
|
|
1561
|
+
const actionOptions = [];
|
|
1562
|
+
if (hasAvailable) {
|
|
1563
|
+
actionOptions.push({ value: "connect", label: "Connect a new integration" });
|
|
1564
|
+
}
|
|
1565
|
+
if (hasConnected) {
|
|
1566
|
+
actionOptions.push({ value: "disconnect", label: "Disconnect an integration" });
|
|
1567
|
+
}
|
|
1568
|
+
actionOptions.push({ value: "cancel", label: "Cancel" });
|
|
1569
|
+
if (actionOptions.length === 1) {
|
|
1570
|
+
p4.outro("All integrations are connected!");
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
const action = await p4.select({
|
|
1574
|
+
message: "What would you like to do?",
|
|
1575
|
+
options: actionOptions
|
|
1576
|
+
});
|
|
1577
|
+
if (p4.isCancel(action) || action === "cancel") {
|
|
1578
|
+
p4.outro("Done.");
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
if (action === "connect") {
|
|
1582
|
+
await handleConnect(projectDir, composioKey, activeConnections);
|
|
1583
|
+
} else if (action === "disconnect") {
|
|
1584
|
+
await handleDisconnect(projectDir, composioKey, activeConnections);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
async function handleConnect(projectDir, composioKey, activeConnections) {
|
|
1588
|
+
const available = Object.entries(INTEGRATIONS).filter(([, def2]) => !activeConnections.has(def2.app)).map(([id, def2]) => ({
|
|
1589
|
+
value: id,
|
|
1590
|
+
label: def2.name,
|
|
1591
|
+
hint: def2.hint
|
|
1592
|
+
}));
|
|
1593
|
+
if (available.length === 0) {
|
|
1594
|
+
p4.outro("All integrations are connected!");
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
const selected = await p4.select({
|
|
1598
|
+
message: "Connect an integration:",
|
|
1599
|
+
options: [
|
|
1600
|
+
...available,
|
|
1601
|
+
{ value: "_cancel", label: "Cancel" }
|
|
1602
|
+
]
|
|
1603
|
+
});
|
|
1604
|
+
if (p4.isCancel(selected) || selected === "_cancel") {
|
|
1605
|
+
p4.outro("Cancelled.");
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
const def = INTEGRATIONS[selected];
|
|
1609
|
+
const extraParams = {};
|
|
1610
|
+
if (def.params?.length) {
|
|
1611
|
+
for (const param of def.params) {
|
|
1612
|
+
const value = await p4.text({
|
|
1613
|
+
message: param.label,
|
|
1614
|
+
placeholder: param.placeholder
|
|
1615
|
+
});
|
|
1616
|
+
if (p4.isCancel(value) || !value) {
|
|
1617
|
+
p4.outro("Cancelled.");
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
extraParams[param.key] = value;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const s = p4.spinner();
|
|
1624
|
+
s.start(`Connecting ${def.name} via Composio...`);
|
|
1625
|
+
try {
|
|
1626
|
+
const userId = await getComposioUserId(projectDir);
|
|
1627
|
+
const authConfigId = await getOrCreateAuthConfig(composioKey, def.app);
|
|
1628
|
+
const connection = await createConnection(composioKey, authConfigId, userId, extraParams);
|
|
1629
|
+
s.stop(`${def.name} authorization ready.`);
|
|
1630
|
+
if (connection.redirectUrl) {
|
|
1631
|
+
p4.log.info(
|
|
1632
|
+
`${pc4.bold("Complete the authorization:")}
|
|
1633
|
+
${pc4.dim("Opening:")} ${pc4.cyan(connection.redirectUrl)}
|
|
1634
|
+
|
|
1635
|
+
${pc4.dim("Sign in and grant access to")} ${pc4.bold(def.name)}.
|
|
1636
|
+
${pc4.dim("If browser didn't open, copy the URL above.")}`
|
|
1637
|
+
);
|
|
1638
|
+
openBrowser(connection.redirectUrl);
|
|
1639
|
+
const done = await p4.confirm({
|
|
1640
|
+
message: `Did you complete the ${def.name} authorization?`,
|
|
1641
|
+
initialValue: true
|
|
1642
|
+
});
|
|
1643
|
+
if (p4.isCancel(done) || !done) {
|
|
1644
|
+
p4.outro(`You can retry later with: ${pc4.cyan("npx seclaw integrations")}`);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const restartSpinner = p4.spinner();
|
|
1648
|
+
restartSpinner.start("Restarting agent to activate integration...");
|
|
1649
|
+
await restartAgent(projectDir);
|
|
1650
|
+
restartSpinner.stop("Agent restarted.");
|
|
1651
|
+
p4.outro(`${pc4.green("\u2713")} ${def.name} connected! Your agent can now use it.`);
|
|
1652
|
+
} else {
|
|
1653
|
+
s.stop("No authorization URL returned.");
|
|
1654
|
+
p4.outro("Try again or check your Composio dashboard.");
|
|
1655
|
+
}
|
|
1656
|
+
} catch (err) {
|
|
1657
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1658
|
+
s.stop(`Failed to connect ${def.name}.`);
|
|
1659
|
+
if (msg.includes("abort")) {
|
|
1660
|
+
p4.log.error(`Request timed out. Composio API did not respond within 15s.`);
|
|
1661
|
+
} else {
|
|
1662
|
+
p4.log.error(`${msg}
|
|
1663
|
+
${pc4.dim("Check your Composio API key and try again.")}`);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
async function handleDisconnect(projectDir, composioKey, activeConnections) {
|
|
1668
|
+
const connected = Object.entries(INTEGRATIONS).filter(([, def2]) => activeConnections.has(def2.app)).map(([id, def2]) => ({
|
|
1669
|
+
value: id,
|
|
1670
|
+
label: def2.name
|
|
1671
|
+
}));
|
|
1672
|
+
if (connected.length === 0) {
|
|
1673
|
+
p4.outro("No integrations to disconnect.");
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
const selected = await p4.select({
|
|
1677
|
+
message: "Disconnect an integration:",
|
|
1678
|
+
options: [
|
|
1679
|
+
...connected,
|
|
1680
|
+
{ value: "_cancel", label: "Cancel" }
|
|
1681
|
+
]
|
|
1682
|
+
});
|
|
1683
|
+
if (p4.isCancel(selected) || selected === "_cancel") {
|
|
1684
|
+
p4.outro("Cancelled.");
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const def = INTEGRATIONS[selected];
|
|
1688
|
+
const accountId = activeConnections.get(def.app);
|
|
1689
|
+
if (!accountId) {
|
|
1690
|
+
p4.log.error(`Could not find account ID for ${def.name}.`);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
const confirmed = await p4.confirm({
|
|
1694
|
+
message: `Disconnect ${def.name}? Your agent will lose access to it.`,
|
|
1695
|
+
initialValue: false
|
|
1696
|
+
});
|
|
1697
|
+
if (p4.isCancel(confirmed) || !confirmed) {
|
|
1698
|
+
p4.outro("Cancelled.");
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const s = p4.spinner();
|
|
1702
|
+
s.start(`Disconnecting ${def.name}...`);
|
|
1703
|
+
try {
|
|
1704
|
+
await composioFetch(composioKey, `/connected_accounts/${accountId}`, {
|
|
1705
|
+
method: "DELETE"
|
|
1706
|
+
});
|
|
1707
|
+
s.stop(`${def.name} disconnected.`);
|
|
1708
|
+
const restartSpinner = p4.spinner();
|
|
1709
|
+
restartSpinner.start("Restarting agent to update tools...");
|
|
1710
|
+
await restartAgent(projectDir);
|
|
1711
|
+
restartSpinner.stop("Agent restarted.");
|
|
1712
|
+
p4.outro(`${pc4.green("\u2713")} ${def.name} disconnected.`);
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1715
|
+
s.stop(`Failed: ${msg}`);
|
|
1716
|
+
p4.log.error(
|
|
1717
|
+
`Could not disconnect ${def.name}.
|
|
1718
|
+
${pc4.dim("Check your Composio dashboard.")}`
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
var COMPOSIO_API = "https://backend.composio.dev/api/v3";
|
|
1723
|
+
async function composioFetch(apiKey, path, options) {
|
|
1724
|
+
const controller = new AbortController();
|
|
1725
|
+
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
1726
|
+
try {
|
|
1727
|
+
const res = await fetch(`${COMPOSIO_API}${path}`, {
|
|
1728
|
+
...options,
|
|
1729
|
+
signal: controller.signal,
|
|
1730
|
+
headers: {
|
|
1731
|
+
"x-api-key": apiKey,
|
|
1732
|
+
"Content-Type": "application/json",
|
|
1733
|
+
...options?.headers
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
if (!res.ok) {
|
|
1737
|
+
const body = await res.text();
|
|
1738
|
+
throw new Error(`Composio ${res.status}: ${body}`);
|
|
1739
|
+
}
|
|
1740
|
+
return res.json();
|
|
1741
|
+
} finally {
|
|
1742
|
+
clearTimeout(timeout);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async function getOrCreateAuthConfig(apiKey, appSlug) {
|
|
1746
|
+
const data = await composioFetch(apiKey, `/auth_configs?appName=${appSlug}`);
|
|
1747
|
+
const match = (data.items || []).find(
|
|
1748
|
+
(c) => c.toolkit?.slug === appSlug
|
|
1749
|
+
);
|
|
1750
|
+
if (match) {
|
|
1751
|
+
return match.id;
|
|
1752
|
+
}
|
|
1753
|
+
const created = await composioFetch(apiKey, "/auth_configs", {
|
|
1754
|
+
method: "POST",
|
|
1755
|
+
body: JSON.stringify({ toolkit: { slug: appSlug } })
|
|
1756
|
+
});
|
|
1757
|
+
return created.auth_config?.id ?? created.id;
|
|
1758
|
+
}
|
|
1759
|
+
async function createConnection(apiKey, authConfigId, entityId, params) {
|
|
1760
|
+
const connection = { entity_id: entityId };
|
|
1761
|
+
if (params && Object.keys(params).length > 0) {
|
|
1762
|
+
connection.config = params;
|
|
1763
|
+
}
|
|
1764
|
+
const data = await composioFetch(apiKey, "/connected_accounts", {
|
|
1765
|
+
method: "POST",
|
|
1766
|
+
body: JSON.stringify({
|
|
1767
|
+
auth_config: { id: authConfigId },
|
|
1768
|
+
connection
|
|
1769
|
+
})
|
|
1770
|
+
});
|
|
1771
|
+
return {
|
|
1772
|
+
id: data.id,
|
|
1773
|
+
redirectUrl: data.redirect_url || data.redirect_uri || null
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function findProjectDir2() {
|
|
1777
|
+
return findProjectDir();
|
|
1778
|
+
}
|
|
1779
|
+
async function getComposioKey(projectDir) {
|
|
1780
|
+
try {
|
|
1781
|
+
const env = await readFile4(resolve7(projectDir, ".env"), "utf-8");
|
|
1782
|
+
const match = env.match(/COMPOSIO_API_KEY=(\S+)/);
|
|
1783
|
+
return match ? match[1] : null;
|
|
1784
|
+
} catch {
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
async function getComposioUserId(projectDir) {
|
|
1789
|
+
try {
|
|
1790
|
+
const env = await readFile4(resolve7(projectDir, ".env"), "utf-8");
|
|
1791
|
+
const match = env.match(/COMPOSIO_USER_ID=(\S+)/);
|
|
1792
|
+
return match ? match[1] : "default-user";
|
|
1793
|
+
} catch {
|
|
1794
|
+
return "default-user";
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
function generateUserId2() {
|
|
1798
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1799
|
+
let id = "seclaw-user-";
|
|
1800
|
+
for (let i = 0; i < 16; i++) {
|
|
1801
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
1802
|
+
}
|
|
1803
|
+
return id;
|
|
1804
|
+
}
|
|
1805
|
+
async function appendEnv(projectDir, key, value) {
|
|
1806
|
+
const envPath = resolve7(projectDir, ".env");
|
|
1807
|
+
let content = "";
|
|
1808
|
+
try {
|
|
1809
|
+
content = await readFile4(envPath, "utf-8");
|
|
1810
|
+
} catch {
|
|
1811
|
+
}
|
|
1812
|
+
content = content.split("\n").filter((l) => !l.startsWith(`${key}=`)).join("\n");
|
|
1813
|
+
content = content.trimEnd() + `
|
|
1814
|
+
${key}=${value}
|
|
1815
|
+
`;
|
|
1816
|
+
await writeFile4(envPath, content);
|
|
1817
|
+
}
|
|
1818
|
+
async function restartAgent(projectDir) {
|
|
1819
|
+
const env = { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" };
|
|
1820
|
+
try {
|
|
1821
|
+
await execa4("docker", ["compose", "restart", "agent"], { cwd: projectDir, env });
|
|
1822
|
+
} catch {
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
async function getActiveConnections(apiKey) {
|
|
1826
|
+
try {
|
|
1827
|
+
const data = await composioFetch(apiKey, "/connected_accounts?status=ACTIVE");
|
|
1828
|
+
const map = /* @__PURE__ */ new Map();
|
|
1829
|
+
for (const a of data.items || []) {
|
|
1830
|
+
if (a.status === "ACTIVE" && a.toolkit?.slug) {
|
|
1831
|
+
if (!map.has(a.toolkit.slug)) {
|
|
1832
|
+
map.set(a.toolkit.slug, a.id);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return map;
|
|
1837
|
+
} catch {
|
|
1838
|
+
return /* @__PURE__ */ new Map();
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function readComposioLocalKey2() {
|
|
1842
|
+
try {
|
|
1843
|
+
const dataPath = resolve7(homedir2(), ".composio", "user_data.json");
|
|
1844
|
+
if (!existsSync7(dataPath)) return null;
|
|
1845
|
+
const data = JSON.parse(readFileSync3(dataPath, "utf-8"));
|
|
1846
|
+
const key = data.api_key;
|
|
1847
|
+
return key && typeof key === "string" && key.startsWith("ak_") ? key : null;
|
|
1848
|
+
} catch {
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
function openBrowser(url) {
|
|
1853
|
+
const cmd = platform2() === "darwin" ? `open "${url}"` : platform2() === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
|
|
1854
|
+
exec2(cmd, () => {
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// src/commands/templates.ts
|
|
1859
|
+
import { resolve as resolve8 } from "path";
|
|
1860
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1861
|
+
import { readFile as readFile5, readdir } from "fs/promises";
|
|
1862
|
+
import * as p5 from "@clack/prompts";
|
|
1863
|
+
import pc5 from "picocolors";
|
|
1864
|
+
var TEMPLATES = [
|
|
1865
|
+
{ id: "productivity-agent", name: "Productivity Agent", price: 0, tier: "free", description: "Personal assistant \u2014 task management, daily reports, email drafting, file organization", hook: "npx seclaw add productivity-agent" },
|
|
1866
|
+
{ id: "data-analyst", name: "Data Analyst", price: 0, tier: "free", description: "Privacy-first local data analysis \u2014 drop CSV/JSON files, ask questions, get Python-powered insights", hook: "npx seclaw add data-analyst" },
|
|
1867
|
+
{ id: "inbox-agent", name: "Inbox Agent", price: 19, tier: "paid", description: "AI inbox manager that categorizes, summarizes, and triages your email", hook: "3 urgent, 5 action needed, 12 FYI, 8 newsletter" },
|
|
1868
|
+
{ id: "reddit-hn-digest", name: "Reddit & HN Digest", price: 19, tier: "paid", description: "Daily curated digest from Reddit and Hacker News", hook: "Never miss what matters on Reddit and HN" },
|
|
1869
|
+
{ id: "youtube-digest", name: "YouTube Digest", price: 19, tier: "paid", description: "Morning summary of new videos from favorite channels", hook: "Your YouTube feed, distilled to what matters" },
|
|
1870
|
+
{ id: "health-tracker", name: "Health Tracker", price: 29, tier: "paid", description: "Food & symptom tracking with weekly correlation analysis", hook: "Understand what your body is telling you" },
|
|
1871
|
+
{ id: "earnings-tracker", name: "Earnings Tracker", price: 29, tier: "paid", description: "Tech/AI earnings reports with beat/miss analysis", hook: "Every earnings call, analyzed in minutes" },
|
|
1872
|
+
{ id: "research-agent", name: "Research Agent", price: 39, tier: "paid", description: "AI research analyst that monitors competitors, trends, and industry news", hook: "Know when competitors change anything -- in 5 minutes" },
|
|
1873
|
+
{ id: "knowledge-base", name: "Knowledge Base", price: 39, tier: "paid", description: "Personal knowledge management from URLs, articles, tweets", hook: "Your second brain, always organized" },
|
|
1874
|
+
{ id: "family-calendar", name: "Family Calendar", price: 39, tier: "paid", description: "Household coordination with calendar aggregation", hook: "Everyone in sync, nothing forgotten" },
|
|
1875
|
+
{ id: "content-agent", name: "Content Agent", price: 49, tier: "paid", description: "AI content strategist that researches trends, drafts posts in your voice, and tracks engagement", hook: "Your X account grows while you sleep" },
|
|
1876
|
+
{ id: "personal-crm", name: "Personal CRM", price: 49, tier: "paid", description: "Contact management with auto-discovery from email", hook: "Never lose touch with anyone important" },
|
|
1877
|
+
{ id: "youtube-creator", name: "YouTube Creator", price: 69, tier: "paid", description: "Content pipeline for YouTube creators", hook: "From idea to upload, fully assisted" },
|
|
1878
|
+
{ id: "devops-agent", name: "DevOps Agent", price: 79, tier: "paid", description: "Infrastructure monitoring + self-healing", hook: "Sleep through incidents, wake up to fixes" },
|
|
1879
|
+
{ id: "customer-service", name: "Customer Service", price: 79, tier: "paid", description: "Multi-channel customer support with knowledge base", hook: "24/7 support without the headcount" },
|
|
1880
|
+
{ id: "sales-agent", name: "Sales Agent", price: 79, tier: "paid", description: "AI-powered B2B sales development representative", hook: "Find leads overnight, inbox full by morning" },
|
|
1881
|
+
{ id: "six-agent-company", name: "Six Agent Company", price: 149, tier: "paid", description: "6 AI agents running your company: CEO, Engineer, QA, Data, Marketing, Growth", hook: "6 AI agents running your company for $8/month" }
|
|
1882
|
+
];
|
|
1883
|
+
async function templates() {
|
|
1884
|
+
p5.intro(`${pc5.bgCyan(pc5.black(" seclaw "))} Available Templates`);
|
|
1885
|
+
const all = await loadManifests();
|
|
1886
|
+
if (all.length === 0) {
|
|
1887
|
+
p5.log.warn("No templates found. Try updating: npm i -g seclaw");
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
const free = all.filter((t) => t.tier === "free");
|
|
1891
|
+
const paid = all.filter((t) => t.tier === "paid");
|
|
1892
|
+
if (free.length > 0) {
|
|
1893
|
+
p5.log.info(pc5.bold("Free"));
|
|
1894
|
+
for (const t of free) {
|
|
1895
|
+
p5.log.message(
|
|
1896
|
+
` ${pc5.green(t.id)}
|
|
1897
|
+
${t.description}
|
|
1898
|
+
${pc5.dim(t.hook)}`
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (paid.length > 0) {
|
|
1903
|
+
p5.log.info(pc5.bold("Paid"));
|
|
1904
|
+
for (const t of paid) {
|
|
1905
|
+
p5.log.message(
|
|
1906
|
+
` ${pc5.yellow(t.id)} ${pc5.dim(`$${t.price}`)}
|
|
1907
|
+
${t.description}
|
|
1908
|
+
${pc5.dim(t.hook)}`
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
p5.log.info("");
|
|
1913
|
+
p5.log.info(pc5.bold("Usage:"));
|
|
1914
|
+
p5.log.message(` ${pc5.cyan("npx seclaw add productivity-agent")} ${pc5.dim("free")}`);
|
|
1915
|
+
p5.log.message(` ${pc5.cyan("npx seclaw add content-agent --key KEY")} ${pc5.dim("paid")}`);
|
|
1916
|
+
p5.outro(`${pc5.dim("Browse:")} https://seclawai.com/templates`);
|
|
1917
|
+
}
|
|
1918
|
+
async function loadManifests() {
|
|
1919
|
+
const manifests = [];
|
|
1920
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1921
|
+
const searchRoots = [
|
|
1922
|
+
resolve8(import.meta.dirname, "templates"),
|
|
1923
|
+
resolve8(process.cwd(), "packages", "cli", "templates")
|
|
1924
|
+
];
|
|
1925
|
+
for (const root of searchRoots) {
|
|
1926
|
+
for (const tier of ["free", "paid"]) {
|
|
1927
|
+
const base = resolve8(root, tier);
|
|
1928
|
+
if (!existsSync8(base)) continue;
|
|
1929
|
+
let entries;
|
|
1930
|
+
try {
|
|
1931
|
+
entries = await readdir(base, { withFileTypes: true });
|
|
1932
|
+
} catch {
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
for (const entry of entries) {
|
|
1936
|
+
if (!entry.isDirectory()) continue;
|
|
1937
|
+
if (seen.has(entry.name)) continue;
|
|
1938
|
+
const manifestPath = resolve8(base, entry.name, "manifest.json");
|
|
1939
|
+
if (!existsSync8(manifestPath)) continue;
|
|
1940
|
+
try {
|
|
1941
|
+
const raw = await readFile5(manifestPath, "utf-8");
|
|
1942
|
+
manifests.push(JSON.parse(raw));
|
|
1943
|
+
seen.add(entry.name);
|
|
1944
|
+
} catch {
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
for (const t of TEMPLATES) {
|
|
1950
|
+
if (!seen.has(t.id)) {
|
|
1951
|
+
manifests.push(t);
|
|
1952
|
+
seen.add(t.id);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
manifests.sort((a, b) => a.price - b.price);
|
|
1956
|
+
return manifests;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// src/commands/status.ts
|
|
1960
|
+
import { resolve as resolve9 } from "path";
|
|
1961
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1962
|
+
import * as p6 from "@clack/prompts";
|
|
1963
|
+
import { execa as execa5 } from "execa";
|
|
1964
|
+
import pc6 from "picocolors";
|
|
1965
|
+
async function status() {
|
|
1966
|
+
p6.intro(`${pc6.bgCyan(pc6.black(" seclaw "))} Status`);
|
|
1967
|
+
const projectDir = findProjectDir();
|
|
1968
|
+
if (!projectDir) {
|
|
1969
|
+
p6.log.warn("No seclaw project found.");
|
|
1970
|
+
p6.log.info(` Start one with: ${pc6.cyan("npx seclaw my-agent")}`);
|
|
1971
|
+
p6.outro("");
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
try {
|
|
1975
|
+
const result = await execa5("docker", ["compose", "ps", "--format", "json"], {
|
|
1976
|
+
cwd: projectDir,
|
|
1977
|
+
env: { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" }
|
|
1978
|
+
});
|
|
1979
|
+
const lines = result.stdout.trim().split("\n").filter(Boolean);
|
|
1980
|
+
const services = lines.map(
|
|
1981
|
+
(l) => JSON.parse(l)
|
|
1982
|
+
);
|
|
1983
|
+
if (services.length === 0) {
|
|
1984
|
+
p6.log.warn("Services are not running.");
|
|
1985
|
+
p6.log.info(` Start with: ${pc6.cyan("npx seclaw")} in ${pc6.dim(projectDir)}`);
|
|
1986
|
+
p6.outro("");
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
for (const svc of services) {
|
|
1990
|
+
const icon = svc.State === "running" ? pc6.green("\u25CF") : pc6.red("\u25CF");
|
|
1991
|
+
const health = svc.Status.includes("healthy") ? pc6.green("healthy") : svc.Status.includes("starting") ? pc6.yellow("starting") : pc6.dim(svc.Status);
|
|
1992
|
+
p6.log.info(`${icon} ${pc6.bold(svc.Service.padEnd(20))} ${health}`);
|
|
1993
|
+
}
|
|
1994
|
+
const infoLines = [];
|
|
1995
|
+
let agentOk = false;
|
|
1996
|
+
try {
|
|
1997
|
+
const agentRes = await execa5("docker", [
|
|
1998
|
+
"compose",
|
|
1999
|
+
"exec",
|
|
2000
|
+
"agent",
|
|
2001
|
+
"wget",
|
|
2002
|
+
"-q",
|
|
2003
|
+
"-O-",
|
|
2004
|
+
"http://localhost:3000/health"
|
|
2005
|
+
], {
|
|
2006
|
+
cwd: projectDir,
|
|
2007
|
+
env: { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" }
|
|
2008
|
+
});
|
|
2009
|
+
if (agentRes.stdout.includes("ok")) agentOk = true;
|
|
2010
|
+
} catch {
|
|
2011
|
+
}
|
|
2012
|
+
infoLines.push(
|
|
2013
|
+
`${pc6.white(pc6.bold("Agent:"))} ${agentOk ? pc6.green("healthy") : pc6.yellow("starting...")}`
|
|
2014
|
+
);
|
|
2015
|
+
const tunnelUrl = await getTunnelUrl(projectDir, 3);
|
|
2016
|
+
if (tunnelUrl) {
|
|
2017
|
+
infoLines.push(`${pc6.white(pc6.bold("Public URL:"))} ${pc6.cyan(tunnelUrl)}`);
|
|
2018
|
+
} else {
|
|
2019
|
+
infoLines.push(`${pc6.white(pc6.bold("Public URL:"))} ${pc6.yellow("not detected yet")}`);
|
|
2020
|
+
}
|
|
2021
|
+
try {
|
|
2022
|
+
const env = await readFile6(resolve9(projectDir, ".env"), "utf-8");
|
|
2023
|
+
const tokenMatch = env.match(/TELEGRAM_BOT_TOKEN=(.+)/);
|
|
2024
|
+
if (tokenMatch && tokenMatch[1].includes(":")) {
|
|
2025
|
+
const botToken = tokenMatch[1].trim();
|
|
2026
|
+
try {
|
|
2027
|
+
const res = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
|
|
2028
|
+
const data = await res.json();
|
|
2029
|
+
if (data.ok && data.result) {
|
|
2030
|
+
infoLines.push(`${pc6.white(pc6.bold("Telegram:"))} ${pc6.green("@" + data.result.username)}`);
|
|
2031
|
+
}
|
|
2032
|
+
} catch {
|
|
2033
|
+
infoLines.push(`${pc6.white(pc6.bold("Telegram:"))} ${pc6.green("configured")}`);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
} catch {
|
|
2037
|
+
}
|
|
2038
|
+
try {
|
|
2039
|
+
const env = await readFile6(resolve9(projectDir, ".env"), "utf-8");
|
|
2040
|
+
if (env.includes("COMPOSIO_API_KEY=")) {
|
|
2041
|
+
infoLines.push(`${pc6.white(pc6.bold("Composio:"))} ${pc6.green("configured")}`);
|
|
2042
|
+
}
|
|
2043
|
+
} catch {
|
|
2044
|
+
}
|
|
2045
|
+
infoLines.push(`${pc6.white(pc6.bold("Workspace:"))} ${resolve9(projectDir, "shared")}`);
|
|
2046
|
+
infoLines.push(`${pc6.white(pc6.bold("Project:"))} ${projectDir}`);
|
|
2047
|
+
p6.note(infoLines.join("\n"), "seclaw");
|
|
2048
|
+
} catch {
|
|
2049
|
+
p6.log.error("Could not check status. Is Docker running?");
|
|
2050
|
+
}
|
|
2051
|
+
p6.outro(`${pc6.white("Manage:")} npx seclaw stop | integrations | upgrade`);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/commands/stop.ts
|
|
2055
|
+
import * as p7 from "@clack/prompts";
|
|
2056
|
+
import { execa as execa6 } from "execa";
|
|
2057
|
+
import pc7 from "picocolors";
|
|
2058
|
+
async function stop() {
|
|
2059
|
+
p7.intro(`${pc7.bgCyan(pc7.black(" seclaw "))} Stopping services...`);
|
|
2060
|
+
const projectDir = findProjectDir();
|
|
2061
|
+
if (!projectDir) {
|
|
2062
|
+
p7.log.warn("No seclaw project found. Nothing to stop.");
|
|
2063
|
+
p7.outro("");
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
const s = p7.spinner();
|
|
2067
|
+
s.start("Stopping containers...");
|
|
2068
|
+
try {
|
|
2069
|
+
await execa6("docker", ["compose", "down"], {
|
|
2070
|
+
cwd: projectDir,
|
|
2071
|
+
env: { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" }
|
|
2072
|
+
});
|
|
2073
|
+
s.stop("All services stopped.");
|
|
2074
|
+
} catch {
|
|
2075
|
+
s.stop("Failed to stop services.");
|
|
2076
|
+
p7.log.error(`Try manually: cd ${projectDir} && docker compose down`);
|
|
2077
|
+
}
|
|
2078
|
+
p7.outro(`Restart anytime: ${pc7.cyan("npx seclaw")} in ${pc7.dim(projectDir)}`);
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/commands/reconnect.ts
|
|
2082
|
+
import { resolve as resolve10 } from "path";
|
|
2083
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2084
|
+
import { execa as execa7 } from "execa";
|
|
2085
|
+
import * as p8 from "@clack/prompts";
|
|
2086
|
+
import pc8 from "picocolors";
|
|
2087
|
+
async function reconnect() {
|
|
2088
|
+
const projectDir = findProjectDir();
|
|
2089
|
+
if (!projectDir) {
|
|
2090
|
+
p8.intro(`${pc8.bgCyan(pc8.black(" seclaw "))}`);
|
|
2091
|
+
p8.log.error("No seclaw project found. Run from your project directory.");
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
p8.intro(`${pc8.bgCyan(pc8.black(" seclaw "))} Reconnect`);
|
|
2095
|
+
const s = p8.spinner();
|
|
2096
|
+
const env = { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" };
|
|
2097
|
+
s.start("Restarting all services...");
|
|
2098
|
+
await clearTunnelCache(projectDir);
|
|
2099
|
+
try {
|
|
2100
|
+
await execa7("docker", ["compose", "down"], { cwd: projectDir, env });
|
|
2101
|
+
await execa7("docker", ["compose", "up", "-d"], { cwd: projectDir, env });
|
|
2102
|
+
} catch {
|
|
2103
|
+
s.stop("Failed to restart services.");
|
|
2104
|
+
p8.log.error("Is Docker running? Try: npx seclaw status");
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
s.stop("Services restarted.");
|
|
2108
|
+
s.start("Waiting for agent...");
|
|
2109
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
2110
|
+
s.stop("Agent starting.");
|
|
2111
|
+
s.start("Waiting for Cloudflare Tunnel...");
|
|
2112
|
+
const tunnelUrl = await getTunnelUrl(projectDir, 30);
|
|
2113
|
+
if (!tunnelUrl) {
|
|
2114
|
+
s.stop("Could not detect tunnel URL.");
|
|
2115
|
+
p8.log.error("Tunnel may still be starting. Try again in a minute.");
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
s.stop(`Tunnel ready: ${pc8.cyan(tunnelUrl)}`);
|
|
2119
|
+
try {
|
|
2120
|
+
const envContent = await readFile7(resolve10(projectDir, ".env"), "utf-8");
|
|
2121
|
+
const tokenMatch = envContent.match(/TELEGRAM_BOT_TOKEN=(.+)/);
|
|
2122
|
+
if (tokenMatch && tokenMatch[1].includes(":")) {
|
|
2123
|
+
s.start("Setting Telegram webhook...");
|
|
2124
|
+
const ok = await setTelegramWebhook(tokenMatch[1].trim(), tunnelUrl);
|
|
2125
|
+
s.stop(ok ? "Telegram webhook updated!" : "Could not update webhook.");
|
|
2126
|
+
}
|
|
2127
|
+
} catch {
|
|
2128
|
+
}
|
|
2129
|
+
p8.note(
|
|
2130
|
+
`${pc8.white(pc8.bold("Tunnel:"))} ${pc8.cyan(tunnelUrl)}`,
|
|
2131
|
+
"seclaw"
|
|
2132
|
+
);
|
|
2133
|
+
p8.outro("Reconnected! Send a message on Telegram to test.");
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// src/commands/doctor.ts
|
|
2137
|
+
import { resolve as resolve11 } from "path";
|
|
2138
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2139
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2140
|
+
import { execa as execa8 } from "execa";
|
|
2141
|
+
import * as p9 from "@clack/prompts";
|
|
2142
|
+
import pc9 from "picocolors";
|
|
2143
|
+
var PASS = pc9.green("PASS");
|
|
2144
|
+
var FAIL = pc9.red("FAIL");
|
|
2145
|
+
var FIX = pc9.cyan("FIX ");
|
|
2146
|
+
async function doctor() {
|
|
2147
|
+
p9.intro(`${pc9.bgCyan(pc9.black(" seclaw "))} Doctor`);
|
|
2148
|
+
const results = [];
|
|
2149
|
+
const s = p9.spinner();
|
|
2150
|
+
s.start("Checking Docker...");
|
|
2151
|
+
const dockerCheck = await checkDockerHealth();
|
|
2152
|
+
results.push(dockerCheck);
|
|
2153
|
+
s.stop(formatCheck(dockerCheck));
|
|
2154
|
+
if (!dockerCheck.ok) {
|
|
2155
|
+
showSummary(results);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
s.start("Finding project...");
|
|
2159
|
+
const projectDir = findProjectDir();
|
|
2160
|
+
const projectCheck = projectDir ? { name: "Project", ok: true, message: `Found at ${pc9.dim(projectDir)}` } : { name: "Project", ok: false, message: "No seclaw project found. Run from your project directory." };
|
|
2161
|
+
results.push(projectCheck);
|
|
2162
|
+
s.stop(formatCheck(projectCheck));
|
|
2163
|
+
if (!projectDir) {
|
|
2164
|
+
showSummary(results);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
const env = { ...process.env };
|
|
2168
|
+
s.start("Checking containers...");
|
|
2169
|
+
const containerChecks = await checkContainers(projectDir, env);
|
|
2170
|
+
results.push(...containerChecks);
|
|
2171
|
+
for (const c of containerChecks) {
|
|
2172
|
+
s.stop(formatCheck(c));
|
|
2173
|
+
if (containerChecks.indexOf(c) < containerChecks.length - 1) s.start("Checking containers...");
|
|
2174
|
+
}
|
|
2175
|
+
s.start("Checking agent...");
|
|
2176
|
+
const agentCheck = await checkAgentHealth(projectDir, env);
|
|
2177
|
+
results.push(agentCheck);
|
|
2178
|
+
s.stop(formatCheck(agentCheck));
|
|
2179
|
+
s.start("Checking tunnel...");
|
|
2180
|
+
const tunnelCheck = await checkTunnel(projectDir);
|
|
2181
|
+
results.push(tunnelCheck);
|
|
2182
|
+
s.stop(formatCheck(tunnelCheck));
|
|
2183
|
+
s.start("Checking Telegram...");
|
|
2184
|
+
const telegramCheck = await checkTelegram(projectDir, tunnelCheck);
|
|
2185
|
+
results.push(telegramCheck);
|
|
2186
|
+
s.stop(formatCheck(telegramCheck));
|
|
2187
|
+
s.start("Checking Composio...");
|
|
2188
|
+
const composioCheck = await checkComposio(projectDir);
|
|
2189
|
+
results.push(composioCheck);
|
|
2190
|
+
s.stop(formatCheck(composioCheck));
|
|
2191
|
+
showSummary(results);
|
|
2192
|
+
const fixable = results.filter((r) => !r.ok && r.fix);
|
|
2193
|
+
if (fixable.length > 0) {
|
|
2194
|
+
let shouldFix = false;
|
|
2195
|
+
try {
|
|
2196
|
+
const answer = await p9.confirm({
|
|
2197
|
+
message: `Found ${fixable.length} fixable issue(s). Auto-fix?`,
|
|
2198
|
+
initialValue: true
|
|
2199
|
+
});
|
|
2200
|
+
shouldFix = !p9.isCancel(answer) && !!answer;
|
|
2201
|
+
} catch {
|
|
2202
|
+
shouldFix = true;
|
|
2203
|
+
}
|
|
2204
|
+
if (!shouldFix) {
|
|
2205
|
+
p9.outro("Run again after fixing manually.");
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
s.start("Stopping conflicting containers...");
|
|
2209
|
+
await stopExistingSeclaw();
|
|
2210
|
+
s.stop(`${FIX} Cleared conflicting containers`);
|
|
2211
|
+
for (const check of fixable) {
|
|
2212
|
+
s.start(`Fixing: ${check.name}...`);
|
|
2213
|
+
try {
|
|
2214
|
+
const result = await check.fix();
|
|
2215
|
+
s.stop(result ? `${FIX} ${check.name} \u2014 ${result}` : `${FAIL} ${check.name} \u2014 could not fix`);
|
|
2216
|
+
} catch (err) {
|
|
2217
|
+
s.stop(`${FAIL} ${check.name} \u2014 ${err instanceof Error ? err.message : "unknown error"}`);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
p9.outro("Fixes applied! Run doctor again to verify.");
|
|
2221
|
+
} else {
|
|
2222
|
+
const allOk = results.every((r) => r.ok);
|
|
2223
|
+
p9.outro(allOk ? "All checks passed!" : "Some issues need manual intervention.");
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
async function checkDockerHealth() {
|
|
2227
|
+
try {
|
|
2228
|
+
await execa8("docker", ["version"]);
|
|
2229
|
+
} catch {
|
|
2230
|
+
return { name: "Docker installed", ok: false, message: "Docker is not installed" };
|
|
2231
|
+
}
|
|
2232
|
+
try {
|
|
2233
|
+
await execa8("docker", ["info"]);
|
|
2234
|
+
return { name: "Docker", ok: true, message: "Running" };
|
|
2235
|
+
} catch {
|
|
2236
|
+
return { name: "Docker", ok: false, message: "Docker is not running. Open Docker Desktop." };
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
async function getContainerName(projectDir, service) {
|
|
2240
|
+
try {
|
|
2241
|
+
const result = await execa8("docker", [
|
|
2242
|
+
"compose",
|
|
2243
|
+
"ps",
|
|
2244
|
+
service,
|
|
2245
|
+
"--format",
|
|
2246
|
+
"{{.Name}}"
|
|
2247
|
+
], { cwd: projectDir, env: { ...process.env, COMPOSE_PROJECT_NAME: getProjectName(projectDir) } });
|
|
2248
|
+
const name = result.stdout.trim().split("\n")[0];
|
|
2249
|
+
return name || null;
|
|
2250
|
+
} catch {
|
|
2251
|
+
return null;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
function getProjectName(projectDir) {
|
|
2255
|
+
const base = projectDir.split("/").pop() || "seclaw";
|
|
2256
|
+
return base.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
2257
|
+
}
|
|
2258
|
+
async function checkContainers(projectDir, _env) {
|
|
2259
|
+
const expected = ["agent", "cloudflared", "desktop-commander"];
|
|
2260
|
+
const results = [];
|
|
2261
|
+
const env = { ..._env, COMPOSE_PROJECT_NAME: getProjectName(projectDir) };
|
|
2262
|
+
for (const svc of expected) {
|
|
2263
|
+
const containerName = await getContainerName(projectDir, svc) || `${getProjectName(projectDir)}-${svc}-1`;
|
|
2264
|
+
try {
|
|
2265
|
+
const statusResult = await execa8("docker", [
|
|
2266
|
+
"inspect",
|
|
2267
|
+
containerName,
|
|
2268
|
+
"--format",
|
|
2269
|
+
"{{.State.Status}}"
|
|
2270
|
+
]);
|
|
2271
|
+
const status2 = statusResult.stdout.trim();
|
|
2272
|
+
let health = "";
|
|
2273
|
+
if (svc !== "cloudflared") {
|
|
2274
|
+
try {
|
|
2275
|
+
const healthResult = await execa8("docker", [
|
|
2276
|
+
"inspect",
|
|
2277
|
+
containerName,
|
|
2278
|
+
"--format",
|
|
2279
|
+
"{{.State.Health.Status}}"
|
|
2280
|
+
]);
|
|
2281
|
+
health = healthResult.stdout.trim();
|
|
2282
|
+
} catch {
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
if (status2 !== "running") {
|
|
2286
|
+
results.push({
|
|
2287
|
+
name: `Container: ${svc}`,
|
|
2288
|
+
ok: false,
|
|
2289
|
+
message: `Status: ${status2}`,
|
|
2290
|
+
fix: async () => {
|
|
2291
|
+
await execa8("docker", ["compose", "up", "-d", svc], { cwd: projectDir, env });
|
|
2292
|
+
return "Started";
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
} else if (health && health !== "healthy" && health !== "") {
|
|
2296
|
+
results.push({
|
|
2297
|
+
name: `Container: ${svc}`,
|
|
2298
|
+
ok: false,
|
|
2299
|
+
message: `Running but ${health}`,
|
|
2300
|
+
fix: async () => {
|
|
2301
|
+
await execa8("docker", ["compose", "restart", svc], { cwd: projectDir, env });
|
|
2302
|
+
return "Restarted";
|
|
2303
|
+
}
|
|
2304
|
+
});
|
|
2305
|
+
} else {
|
|
2306
|
+
results.push({ name: `Container: ${svc}`, ok: true, message: "Running" });
|
|
2307
|
+
}
|
|
2308
|
+
} catch {
|
|
2309
|
+
results.push({
|
|
2310
|
+
name: `Container: ${svc}`,
|
|
2311
|
+
ok: false,
|
|
2312
|
+
message: "Not found",
|
|
2313
|
+
fix: async () => {
|
|
2314
|
+
await execa8("docker", ["compose", "up", "-d", svc], { cwd: projectDir, env });
|
|
2315
|
+
return "Started";
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
return results;
|
|
2321
|
+
}
|
|
2322
|
+
async function checkAgentHealth(projectDir, _env) {
|
|
2323
|
+
const env = { ..._env, COMPOSE_PROJECT_NAME: getProjectName(projectDir) };
|
|
2324
|
+
try {
|
|
2325
|
+
const result = await execa8("docker", [
|
|
2326
|
+
"compose",
|
|
2327
|
+
"exec",
|
|
2328
|
+
"agent",
|
|
2329
|
+
"wget",
|
|
2330
|
+
"-q",
|
|
2331
|
+
"-O-",
|
|
2332
|
+
"http://localhost:3000/health"
|
|
2333
|
+
], { cwd: projectDir, env });
|
|
2334
|
+
if (result.stdout.includes("ok")) {
|
|
2335
|
+
const data = JSON.parse(result.stdout);
|
|
2336
|
+
return {
|
|
2337
|
+
name: "Agent API",
|
|
2338
|
+
ok: true,
|
|
2339
|
+
message: `Healthy \u2014 ${data.tools || 0} tools loaded`
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
return { name: "Agent API", ok: false, message: "Unhealthy response" };
|
|
2343
|
+
} catch {
|
|
2344
|
+
return {
|
|
2345
|
+
name: "Agent API",
|
|
2346
|
+
ok: false,
|
|
2347
|
+
message: "Not reachable",
|
|
2348
|
+
fix: async () => {
|
|
2349
|
+
await execa8("docker", ["compose", "restart", "agent"], { cwd: projectDir, env });
|
|
2350
|
+
return "Agent restarted";
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
async function checkTunnel(projectDir) {
|
|
2356
|
+
try {
|
|
2357
|
+
const result = await execa8(
|
|
2358
|
+
"docker",
|
|
2359
|
+
["compose", "logs", "cloudflared", "--no-log-prefix"],
|
|
2360
|
+
{ cwd: projectDir, env: { ...process.env, COMPOSE_PROJECT_NAME: getProjectName(projectDir) } }
|
|
2361
|
+
);
|
|
2362
|
+
const combined = result.stdout + "\n" + result.stderr;
|
|
2363
|
+
const match = combined.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
2364
|
+
if (!match) {
|
|
2365
|
+
return {
|
|
2366
|
+
name: "Tunnel",
|
|
2367
|
+
ok: false,
|
|
2368
|
+
message: "No URL found in logs",
|
|
2369
|
+
fix: async () => {
|
|
2370
|
+
await clearTunnelCache(projectDir);
|
|
2371
|
+
const env = { ...process.env, COMPOSE_PROJECT_NAME: getProjectName(projectDir) };
|
|
2372
|
+
await execa8("docker", ["compose", "restart", "cloudflared"], { cwd: projectDir, env });
|
|
2373
|
+
const url = await getTunnelUrl(projectDir, 20);
|
|
2374
|
+
return url ? `New tunnel: ${url}` : "Restarted cloudflared";
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
const tunnelUrl = match[0];
|
|
2379
|
+
try {
|
|
2380
|
+
const res = await fetch(`${tunnelUrl}/health`, {
|
|
2381
|
+
signal: AbortSignal.timeout(1e4)
|
|
2382
|
+
});
|
|
2383
|
+
if (res.ok) {
|
|
2384
|
+
return { name: "Tunnel", ok: true, message: tunnelUrl, tunnelUrl };
|
|
2385
|
+
}
|
|
2386
|
+
return {
|
|
2387
|
+
name: "Tunnel",
|
|
2388
|
+
ok: false,
|
|
2389
|
+
message: `URL exists but returns HTTP ${res.status}`,
|
|
2390
|
+
tunnelUrl
|
|
2391
|
+
};
|
|
2392
|
+
} catch {
|
|
2393
|
+
return {
|
|
2394
|
+
name: "Tunnel",
|
|
2395
|
+
ok: false,
|
|
2396
|
+
message: `URL found (${pc9.dim(tunnelUrl)}) but not reachable`,
|
|
2397
|
+
tunnelUrl
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
} catch {
|
|
2401
|
+
return { name: "Tunnel", ok: false, message: "Cannot read cloudflared logs" };
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
async function checkTelegram(projectDir, tunnelCheck) {
|
|
2405
|
+
const envPath = resolve11(projectDir, ".env");
|
|
2406
|
+
if (!existsSync9(envPath)) {
|
|
2407
|
+
return { name: "Telegram", ok: false, message: ".env not found" };
|
|
2408
|
+
}
|
|
2409
|
+
let botToken = "";
|
|
2410
|
+
try {
|
|
2411
|
+
const envContent = await readFile8(envPath, "utf-8");
|
|
2412
|
+
const match = envContent.match(/TELEGRAM_BOT_TOKEN=(.+)/);
|
|
2413
|
+
botToken = match?.[1]?.trim() || "";
|
|
2414
|
+
} catch {
|
|
2415
|
+
return { name: "Telegram", ok: false, message: "Cannot read .env" };
|
|
2416
|
+
}
|
|
2417
|
+
if (!botToken || !botToken.includes(":")) {
|
|
2418
|
+
return { name: "Telegram", ok: false, message: "No valid bot token in .env" };
|
|
2419
|
+
}
|
|
2420
|
+
try {
|
|
2421
|
+
const res = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
|
|
2422
|
+
const data = await res.json();
|
|
2423
|
+
if (!data.ok) {
|
|
2424
|
+
return { name: "Telegram", ok: false, message: "Bot token is invalid" };
|
|
2425
|
+
}
|
|
2426
|
+
const botName = `@${data.result.username}`;
|
|
2427
|
+
const whRes = await fetch(`https://api.telegram.org/bot${botToken}/getWebhookInfo`);
|
|
2428
|
+
const whData = await whRes.json();
|
|
2429
|
+
if (!whData.ok || !whData.result) {
|
|
2430
|
+
return { name: "Telegram", ok: false, message: "Cannot get webhook info" };
|
|
2431
|
+
}
|
|
2432
|
+
const wh = whData.result;
|
|
2433
|
+
const tunnelUrl = tunnelCheck.tunnelUrl || "";
|
|
2434
|
+
if (!wh.url) {
|
|
2435
|
+
return {
|
|
2436
|
+
name: `Telegram (${botName})`,
|
|
2437
|
+
ok: false,
|
|
2438
|
+
message: "Webhook not set",
|
|
2439
|
+
fix: tunnelUrl ? async () => {
|
|
2440
|
+
const ok = await setTelegramWebhook(botToken, tunnelUrl);
|
|
2441
|
+
return ok ? "Webhook set" : "Could not set webhook";
|
|
2442
|
+
} : void 0
|
|
2443
|
+
};
|
|
2444
|
+
}
|
|
2445
|
+
if (tunnelUrl && !wh.url.includes(tunnelUrl.replace("https://", ""))) {
|
|
2446
|
+
return {
|
|
2447
|
+
name: `Telegram (${botName})`,
|
|
2448
|
+
ok: false,
|
|
2449
|
+
message: "Webhook points to old tunnel",
|
|
2450
|
+
fix: async () => {
|
|
2451
|
+
const ok = await setTelegramWebhook(botToken, tunnelUrl);
|
|
2452
|
+
return ok ? "Webhook updated" : "Could not update webhook";
|
|
2453
|
+
}
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
if (wh.last_error_message) {
|
|
2457
|
+
const age = wh.last_error_date ? Math.floor((Date.now() / 1e3 - wh.last_error_date) / 60) : 0;
|
|
2458
|
+
return {
|
|
2459
|
+
name: `Telegram (${botName})`,
|
|
2460
|
+
ok: false,
|
|
2461
|
+
message: `Error ${age}m ago: ${wh.last_error_message}`
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
return {
|
|
2465
|
+
name: `Telegram (${botName})`,
|
|
2466
|
+
ok: true,
|
|
2467
|
+
message: `Webhook active, ${wh.pending_update_count || 0} pending`
|
|
2468
|
+
};
|
|
2469
|
+
} catch {
|
|
2470
|
+
return { name: "Telegram", ok: false, message: "Cannot reach Telegram API" };
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
async function checkComposio(projectDir) {
|
|
2474
|
+
try {
|
|
2475
|
+
const env = await readFile8(resolve11(projectDir, ".env"), "utf-8");
|
|
2476
|
+
const match = env.match(/COMPOSIO_API_KEY=(\S+)/);
|
|
2477
|
+
if (!match) {
|
|
2478
|
+
return {
|
|
2479
|
+
name: "Composio",
|
|
2480
|
+
ok: false,
|
|
2481
|
+
message: `Not configured \u2014 run ${pc9.cyan("npx seclaw integrations")}`
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
return { name: "Composio", ok: true, message: "API key configured" };
|
|
2485
|
+
} catch {
|
|
2486
|
+
return { name: "Composio", ok: false, message: "Cannot read .env" };
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function formatCheck(check) {
|
|
2490
|
+
const status2 = check.ok ? PASS : FAIL;
|
|
2491
|
+
const fixable = !check.ok && check.fix ? pc9.dim(" (auto-fixable)") : "";
|
|
2492
|
+
return `${status2} ${check.name} \u2014 ${check.message}${fixable}`;
|
|
2493
|
+
}
|
|
2494
|
+
function showSummary(results) {
|
|
2495
|
+
const passed = results.filter((r) => r.ok).length;
|
|
2496
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
2497
|
+
const fixable = results.filter((r) => !r.ok && r.fix).length;
|
|
2498
|
+
p9.log.message("");
|
|
2499
|
+
p9.log.message(
|
|
2500
|
+
`${pc9.bold("Summary:")} ${pc9.green(`${passed} passed`)}, ${failed > 0 ? pc9.red(`${failed} failed`) : pc9.green("0 failed")}` + (fixable > 0 ? `, ${pc9.cyan(`${fixable} auto-fixable`)}` : "")
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// src/commands/upgrade.ts
|
|
2505
|
+
import * as p10 from "@clack/prompts";
|
|
2506
|
+
import { execa as execa9 } from "execa";
|
|
2507
|
+
import pc10 from "picocolors";
|
|
2508
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2509
|
+
import { resolve as resolve12 } from "path";
|
|
2510
|
+
async function upgrade() {
|
|
2511
|
+
p10.intro(`${pc10.bgCyan(pc10.black(" seclaw "))} Upgrading...`);
|
|
2512
|
+
const projectDir = findProjectDir();
|
|
2513
|
+
if (!projectDir) {
|
|
2514
|
+
p10.log.warn("No seclaw project found.");
|
|
2515
|
+
p10.log.info(` Start one with: ${pc10.cyan("npx seclaw my-agent")}`);
|
|
2516
|
+
p10.outro("");
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
const opts = {
|
|
2520
|
+
cwd: projectDir,
|
|
2521
|
+
env: { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" }
|
|
2522
|
+
};
|
|
2523
|
+
const s = p10.spinner();
|
|
2524
|
+
s.start("Pulling latest images...");
|
|
2525
|
+
try {
|
|
2526
|
+
await execa9("docker", ["compose", "pull"], opts);
|
|
2527
|
+
s.stop("Images updated.");
|
|
2528
|
+
} catch {
|
|
2529
|
+
s.stop("Failed to pull images.");
|
|
2530
|
+
p10.log.error("Check your internet connection and try again.");
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
s.start("Restarting services...");
|
|
2534
|
+
try {
|
|
2535
|
+
await execa9("docker", ["compose", "up", "-d", "--build", "--remove-orphans"], opts);
|
|
2536
|
+
s.stop("Services restarted.");
|
|
2537
|
+
} catch {
|
|
2538
|
+
s.stop("Failed to restart.");
|
|
2539
|
+
p10.log.error(`Try manually: cd ${projectDir} && docker compose up -d`);
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
s.start("Reconnecting tunnel...");
|
|
2543
|
+
const tunnelUrl = await getTunnelUrl(projectDir);
|
|
2544
|
+
if (tunnelUrl) {
|
|
2545
|
+
s.stop(`Tunnel ready: ${pc10.cyan(tunnelUrl)}`);
|
|
2546
|
+
try {
|
|
2547
|
+
const env = await readFile9(resolve12(projectDir, ".env"), "utf-8");
|
|
2548
|
+
const tokenMatch = env.match(/TELEGRAM_BOT_TOKEN=(.+)/);
|
|
2549
|
+
if (tokenMatch && tokenMatch[1].includes(":")) {
|
|
2550
|
+
s.start("Updating Telegram webhook...");
|
|
2551
|
+
const ok = await setTelegramWebhook(tokenMatch[1].trim(), tunnelUrl);
|
|
2552
|
+
s.stop(ok ? "Telegram webhook updated!" : "Could not update webhook.");
|
|
2553
|
+
}
|
|
2554
|
+
} catch {
|
|
2555
|
+
}
|
|
2556
|
+
} else {
|
|
2557
|
+
s.stop("Tunnel starting...");
|
|
2558
|
+
}
|
|
2559
|
+
p10.outro("Upgrade complete!");
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// src/cli.ts
|
|
2563
|
+
program.name("seclaw").description("Secure autonomous AI agents in 60 seconds").version("0.1.0");
|
|
2564
|
+
program.command("create", { isDefault: true }).alias("init").description("Set up a new seclaw project").argument("[directory]", "Project directory", ".").action(create);
|
|
2565
|
+
program.command("add <template>").description("Add a template to your project").option("--key <license>", "License key for paid templates").action(add);
|
|
2566
|
+
program.command("integrations").description("Manage integrations (Gmail, GitHub, Notion...)").action(integrations);
|
|
2567
|
+
program.command("templates").alias("list").description("List available templates").action(templates);
|
|
2568
|
+
program.command("status").description("Check running services").action(status);
|
|
2569
|
+
program.command("stop").description("Stop all services").action(stop);
|
|
2570
|
+
program.command("reconnect").description("Reconnect tunnel + Telegram webhook").action(reconnect);
|
|
2571
|
+
program.command("doctor").description("Diagnose and fix common issues").action(doctor);
|
|
2572
|
+
program.command("upgrade").description("Pull latest images and restart").action(upgrade);
|
|
2573
|
+
program.parse();
|