seclaw 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +125 -219
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -61,14 +61,12 @@ async function fetchTunnelUrlFromLogs(cwd) {
|
|
|
61
61
|
try {
|
|
62
62
|
const result = await execa(
|
|
63
63
|
"docker",
|
|
64
|
-
["compose", "logs", "cloudflared", "--no-log-prefix"],
|
|
65
|
-
{ cwd
|
|
64
|
+
["compose", "logs", "cloudflared", "--no-log-prefix", "--tail", "50"],
|
|
65
|
+
{ cwd }
|
|
66
66
|
);
|
|
67
67
|
const combined = result.stdout + "\n" + result.stderr;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
);
|
|
71
|
-
return match ? match[0] : null;
|
|
68
|
+
const matches = [...combined.matchAll(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/g)];
|
|
69
|
+
return matches.length > 0 ? matches[matches.length - 1][0] : null;
|
|
72
70
|
} catch {
|
|
73
71
|
return null;
|
|
74
72
|
}
|
|
@@ -315,9 +313,9 @@ function openUrl(url) {
|
|
|
315
313
|
});
|
|
316
314
|
}
|
|
317
315
|
function discoverInstalledTemplates(targetDir) {
|
|
318
|
-
const
|
|
316
|
+
const templates = [];
|
|
319
317
|
const templatesDir = join(targetDir, "templates");
|
|
320
|
-
if (!existsSync2(templatesDir)) return
|
|
318
|
+
if (!existsSync2(templatesDir)) return templates;
|
|
321
319
|
try {
|
|
322
320
|
const entries = readdirSync(templatesDir);
|
|
323
321
|
for (const entry of entries) {
|
|
@@ -326,7 +324,7 @@ function discoverInstalledTemplates(targetDir) {
|
|
|
326
324
|
if (!existsSync2(manifestPath)) continue;
|
|
327
325
|
try {
|
|
328
326
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
329
|
-
|
|
327
|
+
templates.push({
|
|
330
328
|
id: manifest.id || entry,
|
|
331
329
|
name: manifest.name || entry,
|
|
332
330
|
description: manifest.description || "",
|
|
@@ -337,7 +335,7 @@ function discoverInstalledTemplates(targetDir) {
|
|
|
337
335
|
}
|
|
338
336
|
} catch {
|
|
339
337
|
}
|
|
340
|
-
return
|
|
338
|
+
return templates;
|
|
341
339
|
}
|
|
342
340
|
function readComposioLocalKey() {
|
|
343
341
|
try {
|
|
@@ -1915,120 +1913,19 @@ function openBrowser(url) {
|
|
|
1915
1913
|
});
|
|
1916
1914
|
}
|
|
1917
1915
|
|
|
1918
|
-
// src/commands/
|
|
1916
|
+
// src/commands/status.ts
|
|
1919
1917
|
import { resolve as resolve8 } from "path";
|
|
1920
|
-
import {
|
|
1921
|
-
import { readFile as readFile5, readdir } from "fs/promises";
|
|
1918
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1922
1919
|
import * as p5 from "@clack/prompts";
|
|
1923
|
-
import pc5 from "picocolors";
|
|
1924
|
-
var TEMPLATES = [
|
|
1925
|
-
{ 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" },
|
|
1926
|
-
{ 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" },
|
|
1927
|
-
{ 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" },
|
|
1928
|
-
{ 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" },
|
|
1929
|
-
{ 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" },
|
|
1930
|
-
{ 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" },
|
|
1931
|
-
{ 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" },
|
|
1932
|
-
{ 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" },
|
|
1933
|
-
{ id: "knowledge-base", name: "Knowledge Base", price: 39, tier: "paid", description: "Personal knowledge management from URLs, articles, tweets", hook: "Your second brain, always organized" },
|
|
1934
|
-
{ id: "family-calendar", name: "Family Calendar", price: 39, tier: "paid", description: "Household coordination with calendar aggregation", hook: "Everyone in sync, nothing forgotten" },
|
|
1935
|
-
{ 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" },
|
|
1936
|
-
{ 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" },
|
|
1937
|
-
{ id: "youtube-creator", name: "YouTube Creator", price: 69, tier: "paid", description: "Content pipeline for YouTube creators", hook: "From idea to upload, fully assisted" },
|
|
1938
|
-
{ id: "devops-agent", name: "DevOps Agent", price: 79, tier: "paid", description: "Infrastructure monitoring + self-healing", hook: "Sleep through incidents, wake up to fixes" },
|
|
1939
|
-
{ 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" },
|
|
1940
|
-
{ 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" },
|
|
1941
|
-
{ 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" }
|
|
1942
|
-
];
|
|
1943
|
-
async function templates() {
|
|
1944
|
-
p5.intro(`${pc5.bgCyan(pc5.black(" seclaw "))} Available Templates`);
|
|
1945
|
-
const all = await loadManifests();
|
|
1946
|
-
if (all.length === 0) {
|
|
1947
|
-
p5.log.warn("No templates found. Try updating: npm i -g seclaw");
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
const free = all.filter((t) => t.tier === "free");
|
|
1951
|
-
const paid = all.filter((t) => t.tier === "paid");
|
|
1952
|
-
if (free.length > 0) {
|
|
1953
|
-
p5.log.info(pc5.bold("Free"));
|
|
1954
|
-
for (const t of free) {
|
|
1955
|
-
p5.log.message(
|
|
1956
|
-
` ${pc5.green(t.id)}
|
|
1957
|
-
${t.description}
|
|
1958
|
-
${pc5.dim(t.hook)}`
|
|
1959
|
-
);
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
if (paid.length > 0) {
|
|
1963
|
-
p5.log.info(pc5.bold("Paid"));
|
|
1964
|
-
for (const t of paid) {
|
|
1965
|
-
p5.log.message(
|
|
1966
|
-
` ${pc5.yellow(t.id)} ${pc5.dim(`$${t.price}`)}
|
|
1967
|
-
${t.description}
|
|
1968
|
-
${pc5.dim(t.hook)}`
|
|
1969
|
-
);
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
p5.log.info("");
|
|
1973
|
-
p5.log.info(pc5.bold("Usage:"));
|
|
1974
|
-
p5.log.message(` ${pc5.cyan("npx seclaw add productivity-agent")} ${pc5.dim("free")}`);
|
|
1975
|
-
p5.log.message(` ${pc5.cyan("npx seclaw add content-agent --key KEY")} ${pc5.dim("paid")}`);
|
|
1976
|
-
p5.outro(`${pc5.dim("Browse:")} https://seclawai.com/templates`);
|
|
1977
|
-
}
|
|
1978
|
-
async function loadManifests() {
|
|
1979
|
-
const manifests = [];
|
|
1980
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1981
|
-
const searchRoots = [
|
|
1982
|
-
resolve8(import.meta.dirname, "templates"),
|
|
1983
|
-
resolve8(process.cwd(), "packages", "cli", "templates")
|
|
1984
|
-
];
|
|
1985
|
-
for (const root of searchRoots) {
|
|
1986
|
-
for (const tier of ["free", "paid"]) {
|
|
1987
|
-
const base = resolve8(root, tier);
|
|
1988
|
-
if (!existsSync8(base)) continue;
|
|
1989
|
-
let entries;
|
|
1990
|
-
try {
|
|
1991
|
-
entries = await readdir(base, { withFileTypes: true });
|
|
1992
|
-
} catch {
|
|
1993
|
-
continue;
|
|
1994
|
-
}
|
|
1995
|
-
for (const entry of entries) {
|
|
1996
|
-
if (!entry.isDirectory()) continue;
|
|
1997
|
-
if (seen.has(entry.name)) continue;
|
|
1998
|
-
const manifestPath = resolve8(base, entry.name, "manifest.json");
|
|
1999
|
-
if (!existsSync8(manifestPath)) continue;
|
|
2000
|
-
try {
|
|
2001
|
-
const raw = await readFile5(manifestPath, "utf-8");
|
|
2002
|
-
manifests.push(JSON.parse(raw));
|
|
2003
|
-
seen.add(entry.name);
|
|
2004
|
-
} catch {
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
for (const t of TEMPLATES) {
|
|
2010
|
-
if (!seen.has(t.id)) {
|
|
2011
|
-
manifests.push(t);
|
|
2012
|
-
seen.add(t.id);
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
manifests.sort((a, b) => a.price - b.price);
|
|
2016
|
-
return manifests;
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
// src/commands/status.ts
|
|
2020
|
-
import { resolve as resolve9 } from "path";
|
|
2021
|
-
import { readFile as readFile6 } from "fs/promises";
|
|
2022
|
-
import * as p6 from "@clack/prompts";
|
|
2023
1920
|
import { execa as execa5 } from "execa";
|
|
2024
|
-
import
|
|
1921
|
+
import pc5 from "picocolors";
|
|
2025
1922
|
async function status() {
|
|
2026
|
-
|
|
1923
|
+
p5.intro(`${pc5.bgCyan(pc5.black(" seclaw "))} Status`);
|
|
2027
1924
|
const projectDir = findProjectDir();
|
|
2028
1925
|
if (!projectDir) {
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
1926
|
+
p5.log.warn("No seclaw project found.");
|
|
1927
|
+
p5.log.info(` Start one with: ${pc5.cyan("npx seclaw my-agent")}`);
|
|
1928
|
+
p5.outro("");
|
|
2032
1929
|
return;
|
|
2033
1930
|
}
|
|
2034
1931
|
try {
|
|
@@ -2041,15 +1938,15 @@ async function status() {
|
|
|
2041
1938
|
(l) => JSON.parse(l)
|
|
2042
1939
|
);
|
|
2043
1940
|
if (services.length === 0) {
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
1941
|
+
p5.log.warn("Services are not running.");
|
|
1942
|
+
p5.log.info(` Start with: ${pc5.cyan("npx seclaw")} in ${pc5.dim(projectDir)}`);
|
|
1943
|
+
p5.outro("");
|
|
2047
1944
|
return;
|
|
2048
1945
|
}
|
|
2049
1946
|
for (const svc of services) {
|
|
2050
|
-
const icon = svc.State === "running" ?
|
|
2051
|
-
const health = svc.Status.includes("healthy") ?
|
|
2052
|
-
|
|
1947
|
+
const icon = svc.State === "running" ? pc5.green("\u25CF") : pc5.red("\u25CF");
|
|
1948
|
+
const health = svc.Status.includes("healthy") ? pc5.green("healthy") : svc.Status.includes("starting") ? pc5.yellow("starting") : pc5.dim(svc.Status);
|
|
1949
|
+
p5.log.info(`${icon} ${pc5.bold(svc.Service.padEnd(20))} ${health}`);
|
|
2053
1950
|
}
|
|
2054
1951
|
const infoLines = [];
|
|
2055
1952
|
let agentOk = false;
|
|
@@ -2070,16 +1967,16 @@ async function status() {
|
|
|
2070
1967
|
} catch {
|
|
2071
1968
|
}
|
|
2072
1969
|
infoLines.push(
|
|
2073
|
-
`${
|
|
1970
|
+
`${pc5.white(pc5.bold("Agent:"))} ${agentOk ? pc5.green("healthy") : pc5.yellow("starting...")}`
|
|
2074
1971
|
);
|
|
2075
1972
|
const tunnelUrl = await getTunnelUrl(projectDir, 3);
|
|
2076
1973
|
if (tunnelUrl) {
|
|
2077
|
-
infoLines.push(`${
|
|
1974
|
+
infoLines.push(`${pc5.white(pc5.bold("Public URL:"))} ${pc5.cyan(tunnelUrl)}`);
|
|
2078
1975
|
} else {
|
|
2079
|
-
infoLines.push(`${
|
|
1976
|
+
infoLines.push(`${pc5.white(pc5.bold("Public URL:"))} ${pc5.yellow("not detected yet")}`);
|
|
2080
1977
|
}
|
|
2081
1978
|
try {
|
|
2082
|
-
const env = await
|
|
1979
|
+
const env = await readFile5(resolve8(projectDir, ".env"), "utf-8");
|
|
2083
1980
|
const tokenMatch = env.match(/TELEGRAM_BOT_TOKEN=(.+)/);
|
|
2084
1981
|
if (tokenMatch && tokenMatch[1].includes(":")) {
|
|
2085
1982
|
const botToken = tokenMatch[1].trim();
|
|
@@ -2087,50 +1984,50 @@ async function status() {
|
|
|
2087
1984
|
const res = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
|
|
2088
1985
|
const data = await res.json();
|
|
2089
1986
|
if (data.ok && data.result) {
|
|
2090
|
-
infoLines.push(`${
|
|
1987
|
+
infoLines.push(`${pc5.white(pc5.bold("Telegram:"))} ${pc5.green("@" + data.result.username)}`);
|
|
2091
1988
|
}
|
|
2092
1989
|
} catch {
|
|
2093
|
-
infoLines.push(`${
|
|
1990
|
+
infoLines.push(`${pc5.white(pc5.bold("Telegram:"))} ${pc5.green("configured")}`);
|
|
2094
1991
|
}
|
|
2095
1992
|
}
|
|
2096
1993
|
} catch {
|
|
2097
1994
|
}
|
|
2098
1995
|
try {
|
|
2099
|
-
const env = await
|
|
1996
|
+
const env = await readFile5(resolve8(projectDir, ".env"), "utf-8");
|
|
2100
1997
|
if (env.includes("COMPOSIO_API_KEY=")) {
|
|
2101
|
-
infoLines.push(`${
|
|
1998
|
+
infoLines.push(`${pc5.white(pc5.bold("Composio:"))} ${pc5.green("configured")}`);
|
|
2102
1999
|
}
|
|
2103
2000
|
} catch {
|
|
2104
2001
|
}
|
|
2105
2002
|
let wsPath = "shared";
|
|
2106
2003
|
try {
|
|
2107
|
-
const envContent = await
|
|
2004
|
+
const envContent = await readFile5(resolve8(projectDir, ".env"), "utf-8");
|
|
2108
2005
|
const wsMatch = envContent.match(/WORKSPACE_HOST_PATH=(.+)/);
|
|
2109
2006
|
if (wsMatch?.[1]?.trim()) wsPath = wsMatch[1].trim();
|
|
2110
2007
|
} catch {
|
|
2111
2008
|
}
|
|
2112
|
-
infoLines.push(`${
|
|
2113
|
-
infoLines.push(`${
|
|
2114
|
-
|
|
2009
|
+
infoLines.push(`${pc5.white(pc5.bold("Workspace:"))} ${resolve8(projectDir, wsPath)}`);
|
|
2010
|
+
infoLines.push(`${pc5.white(pc5.bold("Project:"))} ${projectDir}`);
|
|
2011
|
+
p5.note(infoLines.join("\n"), "seclaw");
|
|
2115
2012
|
} catch {
|
|
2116
|
-
|
|
2013
|
+
p5.log.error("Could not check status. Is Docker running?");
|
|
2117
2014
|
}
|
|
2118
|
-
|
|
2015
|
+
p5.outro(`${pc5.white("Manage:")} npx seclaw stop | integrations | upgrade`);
|
|
2119
2016
|
}
|
|
2120
2017
|
|
|
2121
2018
|
// src/commands/stop.ts
|
|
2122
|
-
import * as
|
|
2019
|
+
import * as p6 from "@clack/prompts";
|
|
2123
2020
|
import { execa as execa6 } from "execa";
|
|
2124
|
-
import
|
|
2021
|
+
import pc6 from "picocolors";
|
|
2125
2022
|
async function stop() {
|
|
2126
|
-
|
|
2023
|
+
p6.intro(`${pc6.bgCyan(pc6.black(" seclaw "))} Stopping services...`);
|
|
2127
2024
|
const projectDir = findProjectDir();
|
|
2128
2025
|
if (!projectDir) {
|
|
2129
|
-
|
|
2130
|
-
|
|
2026
|
+
p6.log.warn("No seclaw project found. Nothing to stop.");
|
|
2027
|
+
p6.outro("");
|
|
2131
2028
|
return;
|
|
2132
2029
|
}
|
|
2133
|
-
const s =
|
|
2030
|
+
const s = p6.spinner();
|
|
2134
2031
|
s.start("Stopping containers...");
|
|
2135
2032
|
try {
|
|
2136
2033
|
await execa6("docker", ["compose", "down"], {
|
|
@@ -2140,26 +2037,26 @@ async function stop() {
|
|
|
2140
2037
|
s.stop("All services stopped.");
|
|
2141
2038
|
} catch {
|
|
2142
2039
|
s.stop("Failed to stop services.");
|
|
2143
|
-
|
|
2040
|
+
p6.log.error(`Try manually: cd ${projectDir} && docker compose down`);
|
|
2144
2041
|
}
|
|
2145
|
-
|
|
2042
|
+
p6.outro(`Restart anytime: ${pc6.cyan("npx seclaw")} in ${pc6.dim(projectDir)}`);
|
|
2146
2043
|
}
|
|
2147
2044
|
|
|
2148
2045
|
// src/commands/reconnect.ts
|
|
2149
|
-
import { resolve as
|
|
2150
|
-
import { readFile as
|
|
2046
|
+
import { resolve as resolve9 } from "path";
|
|
2047
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2151
2048
|
import { execa as execa7 } from "execa";
|
|
2152
|
-
import * as
|
|
2153
|
-
import
|
|
2049
|
+
import * as p7 from "@clack/prompts";
|
|
2050
|
+
import pc7 from "picocolors";
|
|
2154
2051
|
async function reconnect() {
|
|
2155
2052
|
const projectDir = findProjectDir();
|
|
2156
2053
|
if (!projectDir) {
|
|
2157
|
-
|
|
2158
|
-
|
|
2054
|
+
p7.intro(`${pc7.bgCyan(pc7.black(" seclaw "))}`);
|
|
2055
|
+
p7.log.error("No seclaw project found. Run from your project directory.");
|
|
2159
2056
|
return;
|
|
2160
2057
|
}
|
|
2161
|
-
|
|
2162
|
-
const s =
|
|
2058
|
+
p7.intro(`${pc7.bgCyan(pc7.black(" seclaw "))} Reconnect`);
|
|
2059
|
+
const s = p7.spinner();
|
|
2163
2060
|
const env = { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" };
|
|
2164
2061
|
s.start("Restarting all services...");
|
|
2165
2062
|
await clearTunnelCache(projectDir);
|
|
@@ -2168,7 +2065,7 @@ async function reconnect() {
|
|
|
2168
2065
|
await execa7("docker", ["compose", "up", "-d"], { cwd: projectDir, env });
|
|
2169
2066
|
} catch {
|
|
2170
2067
|
s.stop("Failed to restart services.");
|
|
2171
|
-
|
|
2068
|
+
p7.log.error("Is Docker running? Try: npx seclaw status");
|
|
2172
2069
|
return;
|
|
2173
2070
|
}
|
|
2174
2071
|
s.stop("Services restarted.");
|
|
@@ -2179,12 +2076,12 @@ async function reconnect() {
|
|
|
2179
2076
|
const tunnelUrl = await getTunnelUrl(projectDir, 30);
|
|
2180
2077
|
if (!tunnelUrl) {
|
|
2181
2078
|
s.stop("Could not detect tunnel URL.");
|
|
2182
|
-
|
|
2079
|
+
p7.log.error("Tunnel may still be starting. Try again in a minute.");
|
|
2183
2080
|
return;
|
|
2184
2081
|
}
|
|
2185
|
-
s.stop(`Tunnel ready: ${
|
|
2082
|
+
s.stop(`Tunnel ready: ${pc7.cyan(tunnelUrl)}`);
|
|
2186
2083
|
try {
|
|
2187
|
-
const envContent = await
|
|
2084
|
+
const envContent = await readFile6(resolve9(projectDir, ".env"), "utf-8");
|
|
2188
2085
|
const tokenMatch = envContent.match(/TELEGRAM_BOT_TOKEN=(.+)/);
|
|
2189
2086
|
if (tokenMatch && tokenMatch[1].includes(":")) {
|
|
2190
2087
|
s.start("Setting Telegram webhook...");
|
|
@@ -2193,27 +2090,27 @@ async function reconnect() {
|
|
|
2193
2090
|
}
|
|
2194
2091
|
} catch {
|
|
2195
2092
|
}
|
|
2196
|
-
|
|
2197
|
-
`${
|
|
2093
|
+
p7.note(
|
|
2094
|
+
`${pc7.white(pc7.bold("Tunnel:"))} ${pc7.cyan(tunnelUrl)}`,
|
|
2198
2095
|
"seclaw"
|
|
2199
2096
|
);
|
|
2200
|
-
|
|
2097
|
+
p7.outro("Reconnected! Send a message on Telegram to test.");
|
|
2201
2098
|
}
|
|
2202
2099
|
|
|
2203
2100
|
// src/commands/doctor.ts
|
|
2204
|
-
import { resolve as
|
|
2205
|
-
import { existsSync as
|
|
2206
|
-
import { readFile as
|
|
2101
|
+
import { resolve as resolve10 } from "path";
|
|
2102
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2103
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2207
2104
|
import { execa as execa8 } from "execa";
|
|
2208
|
-
import * as
|
|
2209
|
-
import
|
|
2210
|
-
var PASS =
|
|
2211
|
-
var FAIL =
|
|
2212
|
-
var FIX =
|
|
2105
|
+
import * as p8 from "@clack/prompts";
|
|
2106
|
+
import pc8 from "picocolors";
|
|
2107
|
+
var PASS = pc8.green("PASS");
|
|
2108
|
+
var FAIL = pc8.red("FAIL");
|
|
2109
|
+
var FIX = pc8.cyan("FIX ");
|
|
2213
2110
|
async function doctor() {
|
|
2214
|
-
|
|
2111
|
+
p8.intro(`${pc8.bgCyan(pc8.black(" seclaw "))} Doctor`);
|
|
2215
2112
|
const results = [];
|
|
2216
|
-
const s =
|
|
2113
|
+
const s = p8.spinner();
|
|
2217
2114
|
s.start("Checking Docker...");
|
|
2218
2115
|
const dockerCheck = await checkDockerHealth();
|
|
2219
2116
|
results.push(dockerCheck);
|
|
@@ -2224,7 +2121,7 @@ async function doctor() {
|
|
|
2224
2121
|
}
|
|
2225
2122
|
s.start("Finding project...");
|
|
2226
2123
|
const projectDir = findProjectDir();
|
|
2227
|
-
const projectCheck = projectDir ? { name: "Project", ok: true, message: `Found at ${
|
|
2124
|
+
const projectCheck = projectDir ? { name: "Project", ok: true, message: `Found at ${pc8.dim(projectDir)}` } : { name: "Project", ok: false, message: "No seclaw project found. Run from your project directory." };
|
|
2228
2125
|
results.push(projectCheck);
|
|
2229
2126
|
s.stop(formatCheck(projectCheck));
|
|
2230
2127
|
if (!projectDir) {
|
|
@@ -2260,21 +2157,18 @@ async function doctor() {
|
|
|
2260
2157
|
if (fixable.length > 0) {
|
|
2261
2158
|
let shouldFix = false;
|
|
2262
2159
|
try {
|
|
2263
|
-
const answer = await
|
|
2160
|
+
const answer = await p8.confirm({
|
|
2264
2161
|
message: `Found ${fixable.length} fixable issue(s). Auto-fix?`,
|
|
2265
2162
|
initialValue: true
|
|
2266
2163
|
});
|
|
2267
|
-
shouldFix = !
|
|
2164
|
+
shouldFix = !p8.isCancel(answer) && !!answer;
|
|
2268
2165
|
} catch {
|
|
2269
2166
|
shouldFix = true;
|
|
2270
2167
|
}
|
|
2271
2168
|
if (!shouldFix) {
|
|
2272
|
-
|
|
2169
|
+
p8.outro("Run again after fixing manually.");
|
|
2273
2170
|
return;
|
|
2274
2171
|
}
|
|
2275
|
-
s.start("Stopping conflicting containers...");
|
|
2276
|
-
await stopExistingSeclaw();
|
|
2277
|
-
s.stop(`${FIX} Cleared conflicting containers`);
|
|
2278
2172
|
for (const check of fixable) {
|
|
2279
2173
|
s.start(`Fixing: ${check.name}...`);
|
|
2280
2174
|
try {
|
|
@@ -2284,10 +2178,10 @@ async function doctor() {
|
|
|
2284
2178
|
s.stop(`${FAIL} ${check.name} \u2014 ${err instanceof Error ? err.message : "unknown error"}`);
|
|
2285
2179
|
}
|
|
2286
2180
|
}
|
|
2287
|
-
|
|
2181
|
+
p8.outro("Fixes applied! Run doctor again to verify.");
|
|
2288
2182
|
} else {
|
|
2289
2183
|
const allOk = results.every((r) => r.ok);
|
|
2290
|
-
|
|
2184
|
+
p8.outro(allOk ? "All checks passed!" : "Some issues need manual intervention.");
|
|
2291
2185
|
}
|
|
2292
2186
|
}
|
|
2293
2187
|
async function checkDockerHealth() {
|
|
@@ -2423,12 +2317,12 @@ async function checkTunnel(projectDir) {
|
|
|
2423
2317
|
try {
|
|
2424
2318
|
const result = await execa8(
|
|
2425
2319
|
"docker",
|
|
2426
|
-
["compose", "logs", "cloudflared", "--no-log-prefix"],
|
|
2320
|
+
["compose", "logs", "cloudflared", "--no-log-prefix", "--tail", "50"],
|
|
2427
2321
|
{ cwd: projectDir, env: { ...process.env, COMPOSE_PROJECT_NAME: getProjectName(projectDir) } }
|
|
2428
2322
|
);
|
|
2429
2323
|
const combined = result.stdout + "\n" + result.stderr;
|
|
2430
|
-
const
|
|
2431
|
-
if (
|
|
2324
|
+
const matches = [...combined.matchAll(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/g)];
|
|
2325
|
+
if (matches.length === 0) {
|
|
2432
2326
|
return {
|
|
2433
2327
|
name: "Tunnel",
|
|
2434
2328
|
ok: false,
|
|
@@ -2442,7 +2336,7 @@ async function checkTunnel(projectDir) {
|
|
|
2442
2336
|
}
|
|
2443
2337
|
};
|
|
2444
2338
|
}
|
|
2445
|
-
const tunnelUrl =
|
|
2339
|
+
const tunnelUrl = matches[matches.length - 1][0];
|
|
2446
2340
|
try {
|
|
2447
2341
|
const res = await fetch(`${tunnelUrl}/health`, {
|
|
2448
2342
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -2460,8 +2354,15 @@ async function checkTunnel(projectDir) {
|
|
|
2460
2354
|
return {
|
|
2461
2355
|
name: "Tunnel",
|
|
2462
2356
|
ok: false,
|
|
2463
|
-
message: `URL found (${
|
|
2464
|
-
tunnelUrl
|
|
2357
|
+
message: `URL found (${pc8.dim(tunnelUrl)}) but not reachable`,
|
|
2358
|
+
tunnelUrl,
|
|
2359
|
+
fix: async () => {
|
|
2360
|
+
await clearTunnelCache(projectDir);
|
|
2361
|
+
const env = { ...process.env, COMPOSE_PROJECT_NAME: getProjectName(projectDir) };
|
|
2362
|
+
await execa8("docker", ["compose", "restart", "cloudflared"], { cwd: projectDir, env });
|
|
2363
|
+
const newUrl = await getTunnelUrl(projectDir, 20);
|
|
2364
|
+
return newUrl ? `New tunnel: ${newUrl}` : "Restarted cloudflared \u2014 run doctor again";
|
|
2365
|
+
}
|
|
2465
2366
|
};
|
|
2466
2367
|
}
|
|
2467
2368
|
} catch {
|
|
@@ -2469,13 +2370,13 @@ async function checkTunnel(projectDir) {
|
|
|
2469
2370
|
}
|
|
2470
2371
|
}
|
|
2471
2372
|
async function checkTelegram(projectDir, tunnelCheck) {
|
|
2472
|
-
const envPath =
|
|
2473
|
-
if (!
|
|
2373
|
+
const envPath = resolve10(projectDir, ".env");
|
|
2374
|
+
if (!existsSync8(envPath)) {
|
|
2474
2375
|
return { name: "Telegram", ok: false, message: ".env not found" };
|
|
2475
2376
|
}
|
|
2476
2377
|
let botToken = "";
|
|
2477
2378
|
try {
|
|
2478
|
-
const envContent = await
|
|
2379
|
+
const envContent = await readFile7(envPath, "utf-8");
|
|
2479
2380
|
const match = envContent.match(/TELEGRAM_BOT_TOKEN=(.+)/);
|
|
2480
2381
|
botToken = match?.[1]?.trim() || "";
|
|
2481
2382
|
} catch {
|
|
@@ -2498,15 +2399,18 @@ async function checkTelegram(projectDir, tunnelCheck) {
|
|
|
2498
2399
|
}
|
|
2499
2400
|
const wh = whData.result;
|
|
2500
2401
|
const tunnelUrl = tunnelCheck.tunnelUrl || "";
|
|
2402
|
+
const webhookFix = async () => {
|
|
2403
|
+
const freshUrl = await getTunnelUrl(projectDir, 5);
|
|
2404
|
+
if (!freshUrl) return "No tunnel URL available \u2014 fix tunnel first";
|
|
2405
|
+
const ok = await setTelegramWebhook(botToken, freshUrl);
|
|
2406
|
+
return ok ? `Webhook set to ${freshUrl}` : "Could not set webhook";
|
|
2407
|
+
};
|
|
2501
2408
|
if (!wh.url) {
|
|
2502
2409
|
return {
|
|
2503
2410
|
name: `Telegram (${botName})`,
|
|
2504
2411
|
ok: false,
|
|
2505
2412
|
message: "Webhook not set",
|
|
2506
|
-
fix:
|
|
2507
|
-
const ok = await setTelegramWebhook(botToken, tunnelUrl);
|
|
2508
|
-
return ok ? "Webhook set" : "Could not set webhook";
|
|
2509
|
-
} : void 0
|
|
2413
|
+
fix: webhookFix
|
|
2510
2414
|
};
|
|
2511
2415
|
}
|
|
2512
2416
|
if (tunnelUrl && !wh.url.includes(tunnelUrl.replace("https://", ""))) {
|
|
@@ -2514,10 +2418,7 @@ async function checkTelegram(projectDir, tunnelCheck) {
|
|
|
2514
2418
|
name: `Telegram (${botName})`,
|
|
2515
2419
|
ok: false,
|
|
2516
2420
|
message: "Webhook points to old tunnel",
|
|
2517
|
-
fix:
|
|
2518
|
-
const ok = await setTelegramWebhook(botToken, tunnelUrl);
|
|
2519
|
-
return ok ? "Webhook updated" : "Could not update webhook";
|
|
2520
|
-
}
|
|
2421
|
+
fix: webhookFix
|
|
2521
2422
|
};
|
|
2522
2423
|
}
|
|
2523
2424
|
if (wh.last_error_message) {
|
|
@@ -2525,7 +2426,13 @@ async function checkTelegram(projectDir, tunnelCheck) {
|
|
|
2525
2426
|
return {
|
|
2526
2427
|
name: `Telegram (${botName})`,
|
|
2527
2428
|
ok: false,
|
|
2528
|
-
message: `Error ${age}m ago: ${wh.last_error_message}
|
|
2429
|
+
message: `Error ${age}m ago: ${wh.last_error_message}`,
|
|
2430
|
+
fix: async () => {
|
|
2431
|
+
const freshUrl = await getTunnelUrl(projectDir, 5);
|
|
2432
|
+
if (!freshUrl) return "No tunnel URL available \u2014 fix tunnel first";
|
|
2433
|
+
const ok = await setTelegramWebhook(botToken, freshUrl);
|
|
2434
|
+
return ok ? `Webhook re-set to ${freshUrl}` : "Could not set webhook";
|
|
2435
|
+
}
|
|
2529
2436
|
};
|
|
2530
2437
|
}
|
|
2531
2438
|
return {
|
|
@@ -2539,13 +2446,13 @@ async function checkTelegram(projectDir, tunnelCheck) {
|
|
|
2539
2446
|
}
|
|
2540
2447
|
async function checkComposio(projectDir) {
|
|
2541
2448
|
try {
|
|
2542
|
-
const env = await
|
|
2449
|
+
const env = await readFile7(resolve10(projectDir, ".env"), "utf-8");
|
|
2543
2450
|
const match = env.match(/COMPOSIO_API_KEY=(\S+)/);
|
|
2544
2451
|
if (!match) {
|
|
2545
2452
|
return {
|
|
2546
2453
|
name: "Composio",
|
|
2547
2454
|
ok: false,
|
|
2548
|
-
message: `Not configured \u2014 run ${
|
|
2455
|
+
message: `Not configured \u2014 run ${pc8.cyan("npx seclaw integrations")}`
|
|
2549
2456
|
};
|
|
2550
2457
|
}
|
|
2551
2458
|
return { name: "Composio", ok: true, message: "API key configured" };
|
|
@@ -2555,46 +2462,46 @@ async function checkComposio(projectDir) {
|
|
|
2555
2462
|
}
|
|
2556
2463
|
function formatCheck(check) {
|
|
2557
2464
|
const status2 = check.ok ? PASS : FAIL;
|
|
2558
|
-
const fixable = !check.ok && check.fix ?
|
|
2465
|
+
const fixable = !check.ok && check.fix ? pc8.dim(" (auto-fixable)") : "";
|
|
2559
2466
|
return `${status2} ${check.name} \u2014 ${check.message}${fixable}`;
|
|
2560
2467
|
}
|
|
2561
2468
|
function showSummary(results) {
|
|
2562
2469
|
const passed = results.filter((r) => r.ok).length;
|
|
2563
2470
|
const failed = results.filter((r) => !r.ok).length;
|
|
2564
2471
|
const fixable = results.filter((r) => !r.ok && r.fix).length;
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
`${
|
|
2472
|
+
p8.log.message("");
|
|
2473
|
+
p8.log.message(
|
|
2474
|
+
`${pc8.bold("Summary:")} ${pc8.green(`${passed} passed`)}, ${failed > 0 ? pc8.red(`${failed} failed`) : pc8.green("0 failed")}` + (fixable > 0 ? `, ${pc8.cyan(`${fixable} auto-fixable`)}` : "")
|
|
2568
2475
|
);
|
|
2569
2476
|
}
|
|
2570
2477
|
|
|
2571
2478
|
// src/commands/upgrade.ts
|
|
2572
|
-
import * as
|
|
2479
|
+
import * as p9 from "@clack/prompts";
|
|
2573
2480
|
import { execa as execa9 } from "execa";
|
|
2574
|
-
import
|
|
2575
|
-
import { readFile as
|
|
2576
|
-
import { resolve as
|
|
2481
|
+
import pc9 from "picocolors";
|
|
2482
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
2483
|
+
import { resolve as resolve11 } from "path";
|
|
2577
2484
|
async function upgrade() {
|
|
2578
|
-
|
|
2485
|
+
p9.intro(`${pc9.bgCyan(pc9.black(" seclaw "))} Upgrading...`);
|
|
2579
2486
|
const projectDir = findProjectDir();
|
|
2580
2487
|
if (!projectDir) {
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2488
|
+
p9.log.warn("No seclaw project found.");
|
|
2489
|
+
p9.log.info(` Start one with: ${pc9.cyan("npx seclaw my-agent")}`);
|
|
2490
|
+
p9.outro("");
|
|
2584
2491
|
return;
|
|
2585
2492
|
}
|
|
2586
2493
|
const opts = {
|
|
2587
2494
|
cwd: projectDir,
|
|
2588
2495
|
env: { ...process.env, COMPOSE_PROJECT_NAME: "seclaw" }
|
|
2589
2496
|
};
|
|
2590
|
-
const s =
|
|
2497
|
+
const s = p9.spinner();
|
|
2591
2498
|
s.start("Pulling latest images...");
|
|
2592
2499
|
try {
|
|
2593
2500
|
await execa9("docker", ["compose", "pull"], opts);
|
|
2594
2501
|
s.stop("Images updated.");
|
|
2595
2502
|
} catch {
|
|
2596
2503
|
s.stop("Failed to pull images.");
|
|
2597
|
-
|
|
2504
|
+
p9.log.error("Check your internet connection and try again.");
|
|
2598
2505
|
return;
|
|
2599
2506
|
}
|
|
2600
2507
|
s.start("Restarting services...");
|
|
@@ -2603,15 +2510,15 @@ async function upgrade() {
|
|
|
2603
2510
|
s.stop("Services restarted.");
|
|
2604
2511
|
} catch {
|
|
2605
2512
|
s.stop("Failed to restart.");
|
|
2606
|
-
|
|
2513
|
+
p9.log.error(`Try manually: cd ${projectDir} && docker compose up -d`);
|
|
2607
2514
|
return;
|
|
2608
2515
|
}
|
|
2609
2516
|
s.start("Reconnecting tunnel...");
|
|
2610
2517
|
const tunnelUrl = await getTunnelUrl(projectDir);
|
|
2611
2518
|
if (tunnelUrl) {
|
|
2612
|
-
s.stop(`Tunnel ready: ${
|
|
2519
|
+
s.stop(`Tunnel ready: ${pc9.cyan(tunnelUrl)}`);
|
|
2613
2520
|
try {
|
|
2614
|
-
const env = await
|
|
2521
|
+
const env = await readFile8(resolve11(projectDir, ".env"), "utf-8");
|
|
2615
2522
|
const tokenMatch = env.match(/TELEGRAM_BOT_TOKEN=(.+)/);
|
|
2616
2523
|
if (tokenMatch && tokenMatch[1].includes(":")) {
|
|
2617
2524
|
s.start("Updating Telegram webhook...");
|
|
@@ -2623,7 +2530,7 @@ async function upgrade() {
|
|
|
2623
2530
|
} else {
|
|
2624
2531
|
s.stop("Tunnel starting...");
|
|
2625
2532
|
}
|
|
2626
|
-
|
|
2533
|
+
p9.outro("Upgrade complete!");
|
|
2627
2534
|
}
|
|
2628
2535
|
|
|
2629
2536
|
// src/cli.ts
|
|
@@ -2631,7 +2538,6 @@ program.name("seclaw").description("Secure autonomous AI agents in 60 seconds").
|
|
|
2631
2538
|
program.command("create", { isDefault: true }).alias("init").description("Set up a new seclaw project").argument("[directory]", "Project directory", ".").action(create);
|
|
2632
2539
|
program.command("add <template>").description("Add a template to your project").option("--key <license>", "License key for paid templates").action(add);
|
|
2633
2540
|
program.command("integrations").description("Manage integrations (Gmail, GitHub, Notion...)").action(integrations);
|
|
2634
|
-
program.command("templates").alias("list").description("List available templates").action(templates);
|
|
2635
2541
|
program.command("status").description("Check running services").action(status);
|
|
2636
2542
|
program.command("stop").description("Stop all services").action(stop);
|
|
2637
2543
|
program.command("reconnect").description("Reconnect tunnel + Telegram webhook").action(reconnect);
|