aisec-cli 0.1.0

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/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @aisec-foundation/cli
2
+
3
+ CLI for **aisec** — AI-powered web security scanner.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx @aisec-foundation/cli scan https://target.com --token YOUR_TOKEN
9
+ ```
10
+
11
+ Or install globally:
12
+
13
+ ```bash
14
+ npm i -g @aisec-foundation/cli
15
+ ```
16
+
17
+ ## Authentication
18
+
19
+ Get your token at [app.aisec.tools/developer](https://app.aisec.tools/developer).
20
+
21
+ ```bash
22
+ export AISEC_TOKEN=ask_...
23
+ aisec scan https://target.com
24
+ ```
25
+
26
+ ## Commands
27
+
28
+ ### `aisec scan <target>`
29
+
30
+ ```bash
31
+ aisec scan https://target.com # Default balanced scan
32
+ aisec scan https://target.com --full # Aggressive + subdomains
33
+ aisec scan https://target.com --aggressive # Full port scan, sqlmap
34
+ aisec scan https://target.com --stealth # WAF evasion, slow
35
+ ```
36
+
37
+ Options: `--engine`, `--model`, `--temperature`, `--max-iterations`, `--scope`, `--timeout`, `--skip-recon`, `--skip-browser`, `--username`, `--password`, `--cookies`, `--proxy`, `--headers`
38
+
39
+ ### `aisec scans`
40
+
41
+ ```bash
42
+ aisec scans # Last 10 scans
43
+ aisec scans -l 20 # Last 20 scans
44
+ ```
45
+
46
+ ### `aisec status`
47
+
48
+ ```bash
49
+ aisec status # Check connection & auth
50
+ ```
51
+
52
+ ## License
53
+
54
+ MIT
package/bin/aisec.mjs ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from "commander";
4
+ import { cmdScan } from "../lib/scan.mjs";
5
+ import { cmdScans } from "../lib/scans.mjs";
6
+ import { cmdStatus } from "../lib/status.mjs";
7
+
8
+ program
9
+ .name("aisec")
10
+ .description("AI-powered web security scanner")
11
+ .version("0.1.0");
12
+
13
+ program
14
+ .command("scan <target>")
15
+ .description("Launch a security scan against a target URL")
16
+ .option("--stealth", "Stealth profile — slower, WAF evasion")
17
+ .option("--aggressive", "Aggressive — full port scan, brute force, sqlmap")
18
+ .option("--full", "Full — aggressive + subdomain scope + 50 iterations")
19
+ .option("-e, --engine <engine>", "AI engine: claude or ollama", "claude")
20
+ .option("-m, --model <model>", "Model name")
21
+ .option("--temperature <temp>", "AI temperature 0.0-1.0", parseFloat)
22
+ .option("-n, --max-iterations <n>", "Max AI iterations", parseInt)
23
+ .option("--scope <scope>", "Scan scope: target, domain, subdomain")
24
+ .option("-t, --timeout <minutes>", "Timeout in minutes, 0=unlimited", parseInt)
25
+ .option("--skip-recon", "Skip infrastructure recon")
26
+ .option("--skip-browser", "Skip browser-based recon")
27
+ .option("-u, --username <user>", "Username for auth scanning")
28
+ .option("-p, --password <pass>", "Password for auth scanning")
29
+ .option("--cookies <json>", "Session cookies as JSON or @file")
30
+ .option("--proxy <url>", "Proxy URL")
31
+ .option("--headers <headers>", "Custom headers: 'Key:Val,Key2:Val2' or @file")
32
+ .option("--fail-on <severity>", "Exit 1 if findings at this severity or above (critical, high, medium, low)")
33
+ .option("--source <source>", "Scan source identifier (cli, ci, api)", "cli")
34
+ .option("--token <token>", "API token (or AISEC_TOKEN env)")
35
+ .option("--api <url>", "API URL override")
36
+ .action(cmdScan);
37
+
38
+ program
39
+ .command("scans")
40
+ .description("List recent scans")
41
+ .option("-l, --limit <n>", "Number of scans to show", parseInt, 10)
42
+ .option("--token <token>", "API token (or AISEC_TOKEN env)")
43
+ .option("--api <url>", "API URL override")
44
+ .action(cmdScans);
45
+
46
+ program
47
+ .command("status")
48
+ .description("Check API connection and authentication")
49
+ .option("--token <token>", "API token (or AISEC_TOKEN env)")
50
+ .option("--api <url>", "API URL override")
51
+ .action(cmdStatus);
52
+
53
+ program.parse();
package/lib/api.mjs ADDED
@@ -0,0 +1,31 @@
1
+ import chalk from "chalk";
2
+
3
+ export async function request(apiUrl, path, token, opts = {}) {
4
+ const url = `${apiUrl}${path}`;
5
+ const headers = {
6
+ "Authorization": `Bearer ${token}`,
7
+ "Content-Type": "application/json",
8
+ };
9
+
10
+ const res = await fetch(url, { ...opts, headers: { ...headers, ...opts.headers } });
11
+
12
+ if (!res.ok) {
13
+ if (res.status === 401) {
14
+ console.error(chalk.red("Invalid API token."));
15
+ process.exit(1);
16
+ }
17
+ const body = await res.text();
18
+ throw new Error(`${res.status} ${res.statusText}: ${body}`);
19
+ }
20
+
21
+ return res.json();
22
+ }
23
+
24
+ export async function healthCheck(apiUrl) {
25
+ try {
26
+ const res = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(5000) });
27
+ return res.ok;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
package/lib/config.mjs ADDED
@@ -0,0 +1,24 @@
1
+ import chalk from "chalk";
2
+
3
+ const DEFAULT_API = "https://api.aisec.tools";
4
+
5
+ export function resolveAuth(opts) {
6
+ const token = opts.token || process.env.AISEC_TOKEN;
7
+ if (!token) {
8
+ console.error(chalk.red("No API token provided."));
9
+ console.error(
10
+ `Set ${chalk.cyan("AISEC_TOKEN")} env var or pass ${chalk.cyan("--token")}\n` +
11
+ `Get your token at ${chalk.underline("https://app.aisec.tools/developer")}`
12
+ );
13
+ process.exit(1);
14
+ }
15
+ return token;
16
+ }
17
+
18
+ export function resolveApi(opts) {
19
+ return (opts.api || process.env.AISEC_API || DEFAULT_API).replace(/\/$/, "");
20
+ }
21
+
22
+ export function wsUrl(apiUrl) {
23
+ return apiUrl.replace(/^http/, "ws");
24
+ }
package/lib/scan.mjs ADDED
@@ -0,0 +1,313 @@
1
+ import chalk from "chalk";
2
+ import WebSocket from "ws";
3
+ import { readFileSync } from "fs";
4
+ import { resolveAuth, resolveApi, wsUrl } from "./config.mjs";
5
+ import { request, healthCheck } from "./api.mjs";
6
+
7
+ function resolveProfile(opts) {
8
+ if (opts.full) return "full";
9
+ if (opts.aggressive) return "aggressive";
10
+ if (opts.stealth) return "stealth";
11
+ return undefined;
12
+ }
13
+
14
+ function parseHeaders(raw) {
15
+ if (!raw) return undefined;
16
+ if (raw.startsWith("@")) {
17
+ raw = readFileSync(raw.slice(1), "utf-8").trim();
18
+ }
19
+ const headers = {};
20
+ for (const pair of raw.split(",")) {
21
+ const idx = pair.indexOf(":");
22
+ if (idx > 0) headers[pair.slice(0, idx).trim()] = pair.slice(idx + 1).trim();
23
+ }
24
+ return headers;
25
+ }
26
+
27
+ function parseCookies(raw) {
28
+ if (!raw) return undefined;
29
+ if (raw.startsWith("@")) {
30
+ return readFileSync(raw.slice(1), "utf-8").trim();
31
+ }
32
+ return raw;
33
+ }
34
+
35
+ function buildBody(target, opts) {
36
+ const body = { target, source: opts.source || "cli" };
37
+ const profile = resolveProfile(opts);
38
+ if (profile) body.profile = profile;
39
+ if (opts.engine !== "claude") body.engine = opts.engine;
40
+ if (opts.model) body.model = opts.model;
41
+ if (opts.temperature != null) body.temperature = opts.temperature;
42
+ if (opts.maxIterations) body.max_iterations = opts.maxIterations;
43
+ if (opts.scope) body.scope = opts.scope;
44
+ if (opts.timeout != null) body.timeout_minutes = opts.timeout;
45
+ if (opts.skipRecon) body.skip_recon = true;
46
+ if (opts.skipBrowser) body.skip_browser = true;
47
+ if (opts.username) body.username = opts.username;
48
+ if (opts.password) body.password = opts.password;
49
+ if (opts.proxy) body.proxy = opts.proxy;
50
+ const cookies = parseCookies(opts.cookies);
51
+ if (cookies) body.cookies_json = cookies;
52
+ const headers = parseHeaders(opts.headers);
53
+ if (headers) body.custom_headers = headers;
54
+ return body;
55
+ }
56
+
57
+ function formatDuration(seconds) {
58
+ const m = Math.floor(seconds / 60);
59
+ const s = seconds % 60;
60
+ return m > 0 ? `${m}m ${s}s` : `${s}s`;
61
+ }
62
+
63
+ const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
64
+
65
+ function sevAboveThreshold(finding, threshold) {
66
+ if (!threshold) return false;
67
+ const fRank = SEV_RANK[(finding || "").toLowerCase()] ?? -1;
68
+ const tRank = SEV_RANK[threshold.toLowerCase()] ?? 99;
69
+ return fRank >= tRank;
70
+ }
71
+
72
+ const THINKING_VERBS = [
73
+ "Thinking", "Analyzing", "Probing", "Investigating", "Evaluating",
74
+ "Inspecting", "Scanning", "Crafting", "Assessing", "Examining",
75
+ "Mapping", "Enumerating", "Fingerprinting", "Strategizing",
76
+ ];
77
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
78
+
79
+ function createThinkingSpinner() {
80
+ let interval = null;
81
+ let frame = 0;
82
+ let verb = THINKING_VERBS[Math.floor(Math.random() * THINKING_VERBS.length)];
83
+ let verbInterval = null;
84
+
85
+ return {
86
+ start() {
87
+ this.stop();
88
+ verb = THINKING_VERBS[Math.floor(Math.random() * THINKING_VERBS.length)];
89
+ frame = 0;
90
+ interval = setInterval(() => {
91
+ const f = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
92
+ process.stdout.write(`\r${chalk.cyan(f)} ${chalk.dim.italic(verb)} `);
93
+ frame++;
94
+ }, 80);
95
+ verbInterval = setInterval(() => {
96
+ verb = THINKING_VERBS[Math.floor(Math.random() * THINKING_VERBS.length)];
97
+ }, 3000 + Math.random() * 2000);
98
+ },
99
+ stop() {
100
+ if (interval) {
101
+ clearInterval(interval);
102
+ interval = null;
103
+ process.stdout.write("\r" + " ".repeat(40) + "\r");
104
+ }
105
+ if (verbInterval) {
106
+ clearInterval(verbInterval);
107
+ verbInterval = null;
108
+ }
109
+ },
110
+ };
111
+ }
112
+
113
+ export async function cmdScan(target, opts) {
114
+ if (!/^https?:\/\//i.test(target)) {
115
+ console.error(chalk.red(`Target must start with http:// or https://`));
116
+ process.exit(1);
117
+ }
118
+
119
+ const token = resolveAuth(opts);
120
+ const apiUrl = resolveApi(opts);
121
+
122
+ const ok = await healthCheck(apiUrl);
123
+ if (!ok) {
124
+ console.error(chalk.red(`API unreachable at ${apiUrl}`));
125
+ process.exit(1);
126
+ }
127
+
128
+ const body = buildBody(target, opts);
129
+ const profile = resolveProfile(opts) || "default";
130
+
131
+ // Fetch account info before scan
132
+ let accountPlan = "?";
133
+ let accountCredits = "?";
134
+ try {
135
+ const me = await request(apiUrl, "/api/v1/auth/me", token);
136
+ accountPlan = me.plan || "free";
137
+ accountCredits = parseFloat(me.credits_balance || 0).toFixed(1);
138
+ } catch {}
139
+
140
+ console.log(
141
+ chalk.red("━".repeat(50)) + "\n" +
142
+ chalk.bold.red(" aisec") + chalk.dim(" — AI security scanner\n") +
143
+ chalk.dim(` Target: `) + chalk.white(target) + "\n" +
144
+ chalk.dim(` Account: `) + chalk.white(accountPlan) + chalk.dim(" · ") + chalk.yellow.bold(accountCredits) + chalk.dim(" credits") + "\n" +
145
+ chalk.dim(` Profile: `) + chalk.cyan(profile) + "\n" +
146
+ chalk.dim(` Engine: `) + chalk.cyan(opts.engine || "claude") + "\n" +
147
+ chalk.red("━".repeat(50))
148
+ );
149
+
150
+ let scan;
151
+ try {
152
+ scan = await request(apiUrl, "/api/v1/scans", token, {
153
+ method: "POST",
154
+ body: JSON.stringify(body),
155
+ });
156
+ } catch (err) {
157
+ console.error(chalk.red(`Failed to create scan: ${err.message}`));
158
+ process.exit(1);
159
+ }
160
+
161
+ const scanId = scan.id;
162
+ console.log(chalk.dim(`Scan ${scanId.slice(0, 8)}... created`));
163
+
164
+ // Expose scan ID for CI (GitHub Actions, etc.)
165
+ if (process.env.GITHUB_OUTPUT) {
166
+ const { appendFileSync } = await import("fs");
167
+ appendFileSync(process.env.GITHUB_OUTPUT, `scan-id=${scanId}\n`);
168
+ }
169
+
170
+ if (scan.queue_position && scan.queue_position > 0) {
171
+ console.log(chalk.yellow(`Queued at position ${scan.queue_position}`));
172
+ }
173
+
174
+ // WebSocket streaming
175
+ const wsBase = wsUrl(apiUrl);
176
+ const wsUrlFull = `${wsBase}/ws/scans/${scanId}?token=${token}`;
177
+
178
+ const ws = new WebSocket(wsUrlFull);
179
+ let cancelled = false;
180
+ const foundFindings = []; // track severities for --fail-on
181
+ let exitCode = 0;
182
+
183
+ const cancel = async () => {
184
+ if (cancelled) process.exit(1);
185
+ cancelled = true;
186
+ console.log(chalk.yellow("\nCancelling scan..."));
187
+ try {
188
+ await request(apiUrl, `/api/v1/scans/${scanId}/cancel`, token, { method: "POST" });
189
+ } catch {}
190
+ setTimeout(() => process.exit(0), 2000);
191
+ };
192
+
193
+ process.on("SIGINT", cancel);
194
+ process.on("SIGTERM", cancel);
195
+
196
+ ws.on("open", () => {
197
+ // keepalive
198
+ const ping = setInterval(() => {
199
+ if (ws.readyState === WebSocket.OPEN) {
200
+ ws.send(JSON.stringify({ type: "ping" }));
201
+ } else {
202
+ clearInterval(ping);
203
+ }
204
+ }, 30_000);
205
+ ws.once("close", () => clearInterval(ping));
206
+ });
207
+
208
+ const spinner = createThinkingSpinner();
209
+
210
+ ws.on("message", async (data) => {
211
+ let msg;
212
+ try {
213
+ msg = JSON.parse(data.toString());
214
+ } catch {
215
+ return;
216
+ }
217
+
218
+ switch (msg.type) {
219
+ case "thinking":
220
+ if (msg.data?.status === "start") spinner.start();
221
+ else spinner.stop();
222
+ break;
223
+
224
+ case "console":
225
+ spinner.stop();
226
+ if (msg.data?.text) process.stdout.write(msg.data.text + "\n");
227
+ break;
228
+
229
+ case "finding":
230
+ spinner.stop();
231
+ console.log(chalk.bold.yellow(`\n[FINDING] ${msg.data?.title || "Vulnerability found"}`));
232
+ if (msg.data?.severity) {
233
+ console.log(chalk.dim(` Severity: ${msg.data.severity}`));
234
+ foundFindings.push(msg.data.severity);
235
+ }
236
+ break;
237
+
238
+ case "credits_update":
239
+ case "cost_update":
240
+ {
241
+ const cr = msg.data?.credits_used ?? msg.data?.cost;
242
+ if (cr != null) {
243
+ process.stdout.write(chalk.dim(` [${cr.toFixed(1)} credits]\r`));
244
+ }
245
+ }
246
+ break;
247
+
248
+ case "error":
249
+ spinner.stop();
250
+ console.error(chalk.red(`\nError: ${msg.data?.message || "Unknown error"}`));
251
+ break;
252
+
253
+ case "scan_complete": {
254
+ spinner.stop();
255
+ const d = msg.data || {};
256
+ const duration = d.duration ? formatDuration(d.duration) : "?";
257
+ const creditsUsed = (d.credits_used ?? d.cost ?? 0).toFixed(1);
258
+
259
+ // Fetch remaining credits
260
+ let remaining = "?";
261
+ try {
262
+ const me = await request(apiUrl, "/api/v1/auth/me", token);
263
+ remaining = parseFloat(me.credits_balance || 0).toFixed(1);
264
+ } catch {}
265
+
266
+ const reportUrl = `https://app.aisec.tools/scans/${scanId}`;
267
+ console.log(
268
+ "\n" + chalk.green("━".repeat(50)) + "\n" +
269
+ chalk.bold.green(" Scan complete\n") +
270
+ chalk.dim(` Findings: `) + chalk.white(d.findings ?? 0) + "\n" +
271
+ chalk.dim(` Credits: `) + chalk.white(creditsUsed) + chalk.dim(" used · ") + chalk.yellow.bold(remaining) + chalk.dim(" remaining") + "\n" +
272
+ chalk.dim(` Duration: `) + chalk.white(duration) + "\n" +
273
+ chalk.dim(` Report: `) + chalk.underline.cyan(reportUrl) + "\n" +
274
+ chalk.green("━".repeat(50))
275
+ );
276
+
277
+ // CI outputs
278
+ if (process.env.GITHUB_OUTPUT) {
279
+ const { appendFileSync } = await import("fs");
280
+ appendFileSync(process.env.GITHUB_OUTPUT, `findings=${d.findings ?? 0}\n`);
281
+ appendFileSync(process.env.GITHUB_OUTPUT, `report-url=${reportUrl}\n`);
282
+ }
283
+
284
+ // --fail-on check
285
+ if (opts.failOn) {
286
+ const failed = foundFindings.some(s => sevAboveThreshold(s, opts.failOn));
287
+ if (failed) {
288
+ console.log(chalk.red(`\n✗ Findings at ${opts.failOn}+ severity detected — exiting with code 1`));
289
+ exitCode = 1;
290
+ } else {
291
+ console.log(chalk.green(`\n✓ No findings at ${opts.failOn}+ severity`));
292
+ }
293
+ }
294
+
295
+ ws.close();
296
+ break;
297
+ }
298
+
299
+ case "scan_started":
300
+ console.log(chalk.cyan("Scan started, streaming output...\n"));
301
+ break;
302
+ }
303
+ });
304
+
305
+ ws.on("error", (err) => {
306
+ console.error(chalk.red(`WebSocket error: ${err.message}`));
307
+ });
308
+
309
+ ws.on("close", () => {
310
+ spinner.stop();
311
+ if (!cancelled) process.exit(exitCode);
312
+ });
313
+ }
package/lib/scans.mjs ADDED
@@ -0,0 +1,62 @@
1
+ import chalk from "chalk";
2
+ import { resolveAuth, resolveApi } from "./config.mjs";
3
+ import { request } from "./api.mjs";
4
+
5
+ const STATUS_COLORS = {
6
+ running: chalk.green,
7
+ completed: chalk.blue,
8
+ failed: chalk.red,
9
+ cancelled: chalk.dim,
10
+ pending: chalk.yellow,
11
+ queued: chalk.yellow,
12
+ };
13
+
14
+ export async function cmdScans(opts) {
15
+ const token = resolveAuth(opts);
16
+ const apiUrl = resolveApi(opts);
17
+ const limit = opts.limit || 10;
18
+
19
+ let data;
20
+ try {
21
+ data = await request(apiUrl, `/api/v1/scans?limit=${limit}`, token);
22
+ } catch (err) {
23
+ console.error(chalk.red(`Failed to fetch scans: ${err.message}`));
24
+ process.exit(1);
25
+ }
26
+
27
+ const scans = Array.isArray(data) ? data : data.items || data.scans || [];
28
+
29
+ if (scans.length === 0) {
30
+ console.log(chalk.dim("No scans found."));
31
+ return;
32
+ }
33
+
34
+ console.log(
35
+ chalk.dim("Status".padEnd(12)) +
36
+ chalk.dim("Domain".padEnd(30)) +
37
+ chalk.dim("Finds".padStart(6)) +
38
+ chalk.dim("Cost".padStart(9)) +
39
+ chalk.dim("Date".padStart(13)) +
40
+ chalk.dim("ID".padStart(11))
41
+ );
42
+ console.log(chalk.dim("─".repeat(81)));
43
+
44
+ for (const s of scans) {
45
+ const color = STATUS_COLORS[s.status] || chalk.white;
46
+ const domain = (s.target || "").replace(/^https?:\/\//, "").slice(0, 28);
47
+ const findings = String(s.findings_count ?? s.total_findings ?? 0);
48
+ const cr = s.credits_used ?? s.total_cost;
49
+ const cost = cr != null ? `${cr.toFixed(1)}` : "-";
50
+ const date = s.created_at ? s.created_at.slice(0, 10) : "-";
51
+ const id = (s.id || "").slice(0, 8);
52
+
53
+ console.log(
54
+ color(s.status.padEnd(12)) +
55
+ chalk.white(domain.padEnd(30)) +
56
+ chalk.white(findings.padStart(6)) +
57
+ chalk.white(cost.padStart(9)) +
58
+ chalk.dim(date.padStart(13)) +
59
+ chalk.dim(id.padStart(11))
60
+ );
61
+ }
62
+ }
package/lib/status.mjs ADDED
@@ -0,0 +1,28 @@
1
+ import chalk from "chalk";
2
+ import { resolveAuth, resolveApi } from "./config.mjs";
3
+ import { request, healthCheck } from "./api.mjs";
4
+
5
+ export async function cmdStatus(opts) {
6
+ const token = resolveAuth(opts);
7
+ const apiUrl = resolveApi(opts);
8
+
9
+ const ok = await healthCheck(apiUrl);
10
+ if (!ok) {
11
+ console.error(chalk.red(`API unreachable at ${apiUrl}`));
12
+ process.exit(1);
13
+ }
14
+ console.log(chalk.green(`✓ API reachable at ${apiUrl}`));
15
+
16
+ try {
17
+ const stats = await request(apiUrl, "/api/v1/stats", token);
18
+ console.log(chalk.green("✓ Authenticated"));
19
+ const scans = stats.total_scans ?? "?";
20
+ const findings = stats.total_findings ?? "?";
21
+ const cr = stats.credits_used ?? stats.total_cost;
22
+ const credits = cr != null ? cr.toFixed(1) : "?";
23
+ console.log(chalk.dim(`Scans: ${scans} | Findings: ${findings} | Credits: ${credits}`));
24
+ } catch (err) {
25
+ console.error(chalk.red(`Auth check failed: ${err.message}`));
26
+ process.exit(1);
27
+ }
28
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "aisec-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for aisec — AI-powered web security scanner",
5
+ "type": "module",
6
+ "bin": {
7
+ "aisec": "./bin/aisec.mjs"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "keywords": [
17
+ "security",
18
+ "scanner",
19
+ "pentest",
20
+ "ai",
21
+ "cli"
22
+ ],
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/aisec-foundation/cli-node.git"
27
+ },
28
+ "dependencies": {
29
+ "commander": "^12.1.0",
30
+ "chalk": "^5.3.0",
31
+ "ws": "^8.18.0"
32
+ }
33
+ }