@pentoshi/clai 0.6.0 → 0.7.2
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 +18 -15
- package/dist/agent/context-manager.d.ts +27 -0
- package/dist/agent/context-manager.js +75 -0
- package/dist/agent/context-manager.js.map +1 -0
- package/dist/agent/runner.d.ts +21 -1
- package/dist/agent/runner.js +185 -74
- package/dist/agent/runner.js.map +1 -1
- package/dist/commands/doctor.js +21 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/update.js +11 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +172 -5
- package/dist/index.js.map +1 -1
- package/dist/llm/anthropic.js +29 -38
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/gemini.d.ts +2 -0
- package/dist/llm/gemini.js +40 -43
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/http.d.ts +23 -1
- package/dist/llm/http.js +197 -13
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/nvidia.js +1 -0
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/ollama.js +18 -27
- package/dist/llm/ollama.js.map +1 -1
- package/dist/llm/router.d.ts +7 -0
- package/dist/llm/router.js +16 -23
- package/dist/llm/router.js.map +1 -1
- package/dist/modes/agent.d.ts +4 -2
- package/dist/modes/agent.js +2 -2
- package/dist/modes/agent.js.map +1 -1
- package/dist/modes/ask.js +3 -4
- package/dist/modes/ask.js.map +1 -1
- package/dist/os/pkgmgr.d.ts +7 -1
- package/dist/os/pkgmgr.js +97 -18
- package/dist/os/pkgmgr.js.map +1 -1
- package/dist/prompts/index.d.ts +7 -0
- package/dist/prompts/index.js +12 -4
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl.d.ts +1 -0
- package/dist/repl.js +363 -45
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.d.ts +7 -1
- package/dist/safety/classifier.js +260 -86
- package/dist/safety/classifier.js.map +1 -1
- package/dist/safety/patterns.d.ts +48 -1
- package/dist/safety/patterns.js +140 -7
- package/dist/safety/patterns.js.map +1 -1
- package/dist/store/config.d.ts +23 -3
- package/dist/store/config.js +31 -11
- package/dist/store/config.js.map +1 -1
- package/dist/store/history.d.ts +9 -0
- package/dist/store/history.js +58 -1
- package/dist/store/history.js.map +1 -1
- package/dist/store/keys.d.ts +2 -1
- package/dist/store/keys.js +7 -3
- package/dist/store/keys.js.map +1 -1
- package/dist/store/logs.d.ts +7 -0
- package/dist/store/logs.js +39 -1
- package/dist/store/logs.js.map +1 -1
- package/dist/store/project.d.ts +1 -0
- package/dist/store/project.js +34 -9
- package/dist/store/project.js.map +1 -1
- package/dist/store/scope.d.ts +32 -0
- package/dist/store/scope.js +161 -0
- package/dist/store/scope.js.map +1 -0
- package/dist/tools/fs.d.ts +6 -2
- package/dist/tools/fs.js +99 -87
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/http.d.ts +5 -3
- package/dist/tools/http.js +170 -31
- package/dist/tools/http.js.map +1 -1
- package/dist/tools/policies/output-policy.d.ts +13 -0
- package/dist/tools/policies/output-policy.js +56 -0
- package/dist/tools/policies/output-policy.js.map +1 -0
- package/dist/tools/reducers/ffuf.d.ts +6 -0
- package/dist/tools/reducers/ffuf.js +74 -0
- package/dist/tools/reducers/ffuf.js.map +1 -0
- package/dist/tools/reducers/generic.d.ts +2 -0
- package/dist/tools/reducers/generic.js +60 -0
- package/dist/tools/reducers/generic.js.map +1 -0
- package/dist/tools/reducers/gobuster.d.ts +2 -0
- package/dist/tools/reducers/gobuster.js +36 -0
- package/dist/tools/reducers/gobuster.js.map +1 -0
- package/dist/tools/reducers/httpx.d.ts +2 -0
- package/dist/tools/reducers/httpx.js +38 -0
- package/dist/tools/reducers/httpx.js.map +1 -0
- package/dist/tools/reducers/nmap.d.ts +7 -0
- package/dist/tools/reducers/nmap.js +82 -0
- package/dist/tools/reducers/nmap.js.map +1 -0
- package/dist/tools/reducers/nuclei.d.ts +2 -0
- package/dist/tools/reducers/nuclei.js +51 -0
- package/dist/tools/reducers/nuclei.js.map +1 -0
- package/dist/tools/reducers/sqlmap.d.ts +2 -0
- package/dist/tools/reducers/sqlmap.js +39 -0
- package/dist/tools/reducers/sqlmap.js.map +1 -0
- package/dist/tools/reducers/subdomains.d.ts +6 -0
- package/dist/tools/reducers/subdomains.js +31 -0
- package/dist/tools/reducers/subdomains.js.map +1 -0
- package/dist/tools/reducers/types.d.ts +14 -0
- package/dist/tools/reducers/types.js +2 -0
- package/dist/tools/reducers/types.js.map +1 -0
- package/dist/tools/registry.d.ts +1 -1
- package/dist/tools/registry.js +223 -79
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.d.ts +45 -4
- package/dist/tools/shell.js +419 -88
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/validate.d.ts +37 -0
- package/dist/tools/validate.js +144 -0
- package/dist/tools/validate.js.map +1 -0
- package/dist/types.d.ts +7 -15
- package/dist/ui/keys.d.ts +21 -0
- package/dist/ui/keys.js +13 -0
- package/dist/ui/keys.js.map +1 -0
- package/dist/ui/output-pane.d.ts +31 -0
- package/dist/ui/output-pane.js +81 -0
- package/dist/ui/output-pane.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const PORT_LINE_RE = /^\s*(\d+)\/(tcp|udp)\s+(open|closed|filtered|open\|filtered)\s+(\S+)(?:\s+(.*))?$/i;
|
|
2
|
+
const HOST_LINE_RE = /^Nmap scan report for\s+(.+)$/i;
|
|
3
|
+
const STATUS_LINE_RE = /^Host is\s+(.+?)(?:\s+\(.*\))?\.?$/i;
|
|
4
|
+
const OS_LINE_RE = /^(?:OS details|Running):\s+(.+)$/i;
|
|
5
|
+
const SUMMARY_RE = /Nmap done:.+/i;
|
|
6
|
+
/**
|
|
7
|
+
* Parse plain "nmap -sV" text output into structured findings. We avoid
|
|
8
|
+
* forcing -oX here so the agent can still pass any flags it wants — but if
|
|
9
|
+
* the output looks like XML we just include it verbatim.
|
|
10
|
+
*/
|
|
11
|
+
export const nmapReducer = (raw) => {
|
|
12
|
+
if (raw.trim().startsWith("<?xml")) {
|
|
13
|
+
return { summary: raw.slice(0, 8_000) };
|
|
14
|
+
}
|
|
15
|
+
const hosts = [];
|
|
16
|
+
let current;
|
|
17
|
+
let summaryLine = "";
|
|
18
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
19
|
+
const hostMatch = HOST_LINE_RE.exec(line);
|
|
20
|
+
if (hostMatch) {
|
|
21
|
+
current = { host: hostMatch[1].trim(), ports: [] };
|
|
22
|
+
hosts.push(current);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!current)
|
|
26
|
+
continue;
|
|
27
|
+
const statusMatch = STATUS_LINE_RE.exec(line);
|
|
28
|
+
if (statusMatch) {
|
|
29
|
+
current.status = statusMatch[1];
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const osMatch = OS_LINE_RE.exec(line);
|
|
33
|
+
if (osMatch) {
|
|
34
|
+
current.os = osMatch[1];
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const portMatch = PORT_LINE_RE.exec(line);
|
|
38
|
+
if (portMatch) {
|
|
39
|
+
current.ports.push({
|
|
40
|
+
port: Number(portMatch[1]),
|
|
41
|
+
protocol: portMatch[2].toLowerCase(),
|
|
42
|
+
state: portMatch[3].toLowerCase(),
|
|
43
|
+
service: portMatch[4],
|
|
44
|
+
version: portMatch[5]?.trim() || undefined,
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (SUMMARY_RE.test(line)) {
|
|
49
|
+
summaryLine = line.trim();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const totalHosts = hosts.length;
|
|
53
|
+
const totalOpen = hosts.reduce((n, host) => n + host.ports.filter((p) => p.state === "open").length, 0);
|
|
54
|
+
const lines = [];
|
|
55
|
+
lines.push(`# nmap reduced summary — ${totalHosts} host(s), ${totalOpen} open port(s)`);
|
|
56
|
+
if (summaryLine)
|
|
57
|
+
lines.push(summaryLine);
|
|
58
|
+
for (const host of hosts) {
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push(`## ${host.host}${host.status ? ` (${host.status})` : ""}`);
|
|
61
|
+
if (host.os)
|
|
62
|
+
lines.push(`OS: ${host.os}`);
|
|
63
|
+
if (host.ports.length === 0) {
|
|
64
|
+
lines.push("(no ports parsed)");
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
for (const port of host.ports) {
|
|
68
|
+
const parts = [
|
|
69
|
+
`${port.port}/${port.protocol}`,
|
|
70
|
+
port.state,
|
|
71
|
+
port.service ?? "",
|
|
72
|
+
port.version ?? "",
|
|
73
|
+
];
|
|
74
|
+
lines.push(parts.filter(Boolean).join(" — "));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
summary: lines.join("\n"),
|
|
79
|
+
findings: { hosts, totalHosts, totalOpen },
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=nmap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nmap.js","sourceRoot":"","sources":["../../../src/tools/reducers/nmap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const SEVERITY_ORDER = ["critical", "high", "medium", "low", "info", "unknown"];
|
|
2
|
+
export const nucleiReducer = (raw) => {
|
|
3
|
+
const hits = [];
|
|
4
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
5
|
+
const trimmed = line.trim();
|
|
6
|
+
if (!trimmed.startsWith("{"))
|
|
7
|
+
continue;
|
|
8
|
+
try {
|
|
9
|
+
hits.push(JSON.parse(trimmed));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// skip malformed lines
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (hits.length === 0) {
|
|
16
|
+
return { summary: "# nuclei — no JSONL hits parsed (pass -jsonl to get structured output)" };
|
|
17
|
+
}
|
|
18
|
+
const bySeverity = new Map();
|
|
19
|
+
for (const hit of hits) {
|
|
20
|
+
const sev = hit.info?.severity?.toLowerCase() ?? "unknown";
|
|
21
|
+
const list = bySeverity.get(sev) ?? [];
|
|
22
|
+
list.push(hit);
|
|
23
|
+
bySeverity.set(sev, list);
|
|
24
|
+
}
|
|
25
|
+
const counts = SEVERITY_ORDER
|
|
26
|
+
.filter((sev) => bySeverity.has(sev))
|
|
27
|
+
.map((sev) => `${sev}=${bySeverity.get(sev).length}`);
|
|
28
|
+
const lines = [
|
|
29
|
+
`# nuclei reduced summary — ${hits.length} hit(s) ${counts.join(" ")}`,
|
|
30
|
+
];
|
|
31
|
+
for (const sev of SEVERITY_ORDER) {
|
|
32
|
+
const list = bySeverity.get(sev);
|
|
33
|
+
if (!list)
|
|
34
|
+
continue;
|
|
35
|
+
lines.push("");
|
|
36
|
+
lines.push(`## ${sev.toUpperCase()} (${list.length})`);
|
|
37
|
+
for (const hit of list.slice(0, 15)) {
|
|
38
|
+
const id = hit["template-id"] ?? hit.template ?? "?";
|
|
39
|
+
const name = hit.info?.name ?? "";
|
|
40
|
+
const matched = hit["matched-at"] ?? hit.matched ?? hit.host ?? "?";
|
|
41
|
+
lines.push(`- ${id}${name ? ` [${name}]` : ""} — ${matched}`);
|
|
42
|
+
}
|
|
43
|
+
if (list.length > 15)
|
|
44
|
+
lines.push(`- ... ${list.length - 15} more`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
summary: lines.join("\n"),
|
|
48
|
+
findings: { total: hits.length, bySeverity: Object.fromEntries([...bySeverity.entries()].map(([k, v]) => [k, v.length])) },
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=nuclei.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nuclei.js","sourceRoot":"","sources":["../../../src/tools/reducers/nuclei.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const INJECTABLE_RE = /Parameter:\s*([^\s(]+)\s*\(([^)]+)\)/g;
|
|
2
|
+
const DBMS_RE = /back-end DBMS:\s*(.+)/i;
|
|
3
|
+
const PAYLOAD_RE = /Payload:\s*(.+)/g;
|
|
4
|
+
export const sqlmapReducer = (raw) => {
|
|
5
|
+
const injectables = [];
|
|
6
|
+
let dbms;
|
|
7
|
+
const payloads = [];
|
|
8
|
+
for (const match of raw.matchAll(INJECTABLE_RE)) {
|
|
9
|
+
injectables.push({ parameter: match[1], place: match[2] });
|
|
10
|
+
}
|
|
11
|
+
const dbmsMatch = DBMS_RE.exec(raw);
|
|
12
|
+
if (dbmsMatch)
|
|
13
|
+
dbms = dbmsMatch[1].trim();
|
|
14
|
+
for (const match of raw.matchAll(PAYLOAD_RE)) {
|
|
15
|
+
payloads.push(match[1].trim());
|
|
16
|
+
if (payloads.length >= 5)
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
if (injectables.length === 0 && !dbms) {
|
|
20
|
+
return { summary: "# sqlmap — no injectable parameters or DBMS detected" };
|
|
21
|
+
}
|
|
22
|
+
const lines = [
|
|
23
|
+
`# sqlmap reduced summary — ${injectables.length} injectable parameter(s)${dbms ? `, DBMS=${dbms}` : ""}`,
|
|
24
|
+
];
|
|
25
|
+
for (const inj of injectables) {
|
|
26
|
+
lines.push(`- ${inj.parameter} (${inj.place})`);
|
|
27
|
+
}
|
|
28
|
+
if (payloads.length > 0) {
|
|
29
|
+
lines.push("");
|
|
30
|
+
lines.push("## Sample payloads");
|
|
31
|
+
for (const payload of payloads)
|
|
32
|
+
lines.push(`- ${payload}`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
summary: lines.join("\n"),
|
|
36
|
+
findings: { injectables, dbms, samplePayloads: payloads },
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=sqlmap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlmap.js","sourceRoot":"","sources":["../../../src/tools/reducers/sqlmap.ts"],"names":[],"mappings":"AAEA,MAAM,aAAa,GAAG,uCAAuC,CAAC;AAC9D,MAAM,OAAO,GAAG,wBAAwB,CAAC;AACzC,MAAM,UAAU,GAAG,kBAAkB,CAAC;AAEtC,MAAM,CAAC,MAAM,aAAa,GAAY,CAAC,GAAG,EAAiB,EAAE;IAC3D,MAAM,WAAW,GAAgD,EAAE,CAAC;IACpE,IAAI,IAAwB,CAAC;IAC7B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QAChD,WAAW,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAE,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,SAAS;QAAE,IAAI,GAAG,SAAS,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7C,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC,CAAC;QAChC,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC;YAAE,MAAM;IAClC,CAAC;IACD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACtC,OAAO,EAAE,OAAO,EAAE,sDAAsD,EAAE,CAAC;IAC7E,CAAC;IACD,MAAM,KAAK,GAAa;QACtB,8BAA8B,WAAW,CAAC,MAAM,2BAA2B,IAAI,CAAC,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;KAC1G,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,SAAS,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC;IAClD,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QACjC,KAAK,MAAM,OAAO,IAAI,QAAQ;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO;QACL,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QACzB,QAAQ,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE;KAC1D,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const DOMAIN_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
|
2
|
+
/**
|
|
3
|
+
* Reducer for subfinder / amass / sublist3r output. Dedup, normalize,
|
|
4
|
+
* sort. The user can still pull the raw list from the artifact.
|
|
5
|
+
*/
|
|
6
|
+
export const subdomainsReducer = (raw) => {
|
|
7
|
+
const set = new Set();
|
|
8
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
9
|
+
const token = line.trim();
|
|
10
|
+
if (DOMAIN_RE.test(token)) {
|
|
11
|
+
set.add(token.toLowerCase());
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (set.size === 0) {
|
|
15
|
+
return { summary: "# subdomains — none parsed from output" };
|
|
16
|
+
}
|
|
17
|
+
const sorted = [...set].sort();
|
|
18
|
+
const preview = sorted.slice(0, 200);
|
|
19
|
+
const lines = [
|
|
20
|
+
`# subdomain enumeration — ${sorted.length} unique domain(s)`,
|
|
21
|
+
...preview,
|
|
22
|
+
];
|
|
23
|
+
if (sorted.length > preview.length) {
|
|
24
|
+
lines.push(`... ${sorted.length - preview.length} more in artifact`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
summary: lines.join("\n"),
|
|
28
|
+
findings: { count: sorted.length, sample: preview },
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=subdomains.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subdomains.js","sourceRoot":"","sources":["../../../src/tools/reducers/subdomains.ts"],"names":[],"mappings":"AAEA,MAAM,SAAS,GAAG,oFAAoF,CAAC;AAEvG;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAY,CAAC,GAAG,EAAiB,EAAE;IAC/D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC1B,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACnB,OAAO,EAAE,OAAO,EAAE,wCAAwC,EAAE,CAAC;IAC/D,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACrC,MAAM,KAAK,GAAa;QACtB,6BAA6B,MAAM,CAAC,MAAM,mBAAmB;QAC7D,GAAG,OAAO;KACX,CAAC;IACF,IAAI,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,mBAAmB,CAAC,CAAC;IACvE,CAAC;IACD,OAAO;QACL,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;QACzB,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE;KACpD,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reducer takes raw tool output and returns a compact, finding-aware
|
|
3
|
+
* summary that the model sees instead of the raw blob. The full raw output
|
|
4
|
+
* still lives in the artifact file for the user to inspect.
|
|
5
|
+
*/
|
|
6
|
+
export interface ReducerOutput {
|
|
7
|
+
summary: string;
|
|
8
|
+
findings?: unknown;
|
|
9
|
+
warnings?: string[];
|
|
10
|
+
}
|
|
11
|
+
export type Reducer = (raw: string, context: {
|
|
12
|
+
command: string;
|
|
13
|
+
argv?: string[] | undefined;
|
|
14
|
+
}) => ReducerOutput;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/tools/reducers/types.ts"],"names":[],"mappings":""}
|
package/dist/tools/registry.d.ts
CHANGED
package/dist/tools/registry.js
CHANGED
|
@@ -1,124 +1,146 @@
|
|
|
1
|
-
import { detectSystem } from
|
|
2
|
-
import { detectPackageManager } from
|
|
3
|
-
import { fsList, fsRead, fsSearch, fsWrite } from
|
|
4
|
-
import { httpFetch } from
|
|
5
|
-
import { shellExec } from
|
|
1
|
+
import { detectSystem } from "../os/detect.js";
|
|
2
|
+
import { detectPackageManager, assertSafePackageName } from "../os/pkgmgr.js";
|
|
3
|
+
import { fsList, fsRead, fsSearch, fsWrite } from "./fs.js";
|
|
4
|
+
import { httpFetch } from "./http.js";
|
|
5
|
+
import { shellExec, spawnArgv } from "./shell.js";
|
|
6
|
+
import { classifyToolCall } from "../safety/classifier.js";
|
|
7
|
+
import { loadScope } from "../store/scope.js";
|
|
8
|
+
import { parseHost, parsePortSpec, parseLegacyFlags, profileToNmapArgs, } from "./validate.js";
|
|
6
9
|
function requireString(args, key) {
|
|
7
10
|
const value = args[key];
|
|
8
|
-
if (typeof value !==
|
|
11
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
9
12
|
throw new Error(`Tool argument "${key}" must be a non-empty string`);
|
|
10
13
|
}
|
|
11
14
|
return value;
|
|
12
15
|
}
|
|
13
16
|
function optionalString(args, key) {
|
|
14
17
|
const value = args[key];
|
|
15
|
-
return typeof value ===
|
|
18
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
16
19
|
}
|
|
17
20
|
function optionalNumber(args, key) {
|
|
18
21
|
const value = args[key];
|
|
19
|
-
return typeof value ===
|
|
20
|
-
}
|
|
21
|
-
function safeToolName(value) {
|
|
22
|
-
if (!/^[A-Za-z0-9_.+-]+$/.test(value)) {
|
|
23
|
-
throw new Error(`Unsafe package/tool name: ${value}`);
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
function safeTarget(value) {
|
|
28
|
-
if (!/^[A-Za-z0-9_.:-]+(?:\/\d{1,3})?$/.test(value)) {
|
|
29
|
-
throw new Error(`Unsafe target syntax: ${value}`);
|
|
30
|
-
}
|
|
31
|
-
return value;
|
|
32
|
-
}
|
|
33
|
-
function safePorts(value) {
|
|
34
|
-
if (!/^[A-Za-z0-9,:-]+$/.test(value)) {
|
|
35
|
-
throw new Error(`Unsafe port syntax: ${value}`);
|
|
36
|
-
}
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
function safeFlagTokens(value) {
|
|
40
|
-
if (!value.trim())
|
|
41
|
-
return [];
|
|
42
|
-
return value.trim().split(/\s+/).map((token) => {
|
|
43
|
-
if (!/^-{1,2}[A-Za-z0-9][A-Za-z0-9_.:=,+/-]*$/.test(token)) {
|
|
44
|
-
throw new Error(`Unsafe flag syntax: ${token}`);
|
|
45
|
-
}
|
|
46
|
-
return token;
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
function commandLabel(command, argv) {
|
|
50
|
-
return [command, ...argv].join(' ');
|
|
22
|
+
return typeof value === "number" ? value : undefined;
|
|
51
23
|
}
|
|
52
24
|
export const toolRegistry = {
|
|
53
|
-
async
|
|
54
|
-
return shellExec({
|
|
25
|
+
async "shell.exec"(args, options) {
|
|
26
|
+
return shellExec({
|
|
27
|
+
command: requireString(args, "command"),
|
|
28
|
+
cwd: optionalString(args, "cwd"),
|
|
29
|
+
timeoutMs: optionalNumber(args, "timeoutMs"),
|
|
30
|
+
signal: options?.signal,
|
|
31
|
+
onOutput: options?.onOutput,
|
|
32
|
+
});
|
|
55
33
|
},
|
|
56
|
-
async
|
|
57
|
-
return fsRead(requireString(args,
|
|
34
|
+
async "fs.read"(args) {
|
|
35
|
+
return fsRead(requireString(args, "path"), {
|
|
36
|
+
maxBytes: optionalNumber(args, "maxBytes"),
|
|
37
|
+
});
|
|
58
38
|
},
|
|
59
|
-
async
|
|
60
|
-
return fsWrite(requireString(args,
|
|
39
|
+
async "fs.write"(args) {
|
|
40
|
+
return fsWrite(requireString(args, "path"), requireString(args, "content"));
|
|
61
41
|
},
|
|
62
|
-
async
|
|
63
|
-
return fsList(optionalString(args,
|
|
42
|
+
async "fs.list"(args) {
|
|
43
|
+
return fsList(optionalString(args, "path") ?? process.cwd(), {
|
|
44
|
+
maxEntries: optionalNumber(args, "maxEntries"),
|
|
45
|
+
});
|
|
64
46
|
},
|
|
65
|
-
async
|
|
66
|
-
return fsSearch(requireString(args,
|
|
47
|
+
async "fs.search"(args) {
|
|
48
|
+
return fsSearch(requireString(args, "pattern"), optionalString(args, "path"));
|
|
67
49
|
},
|
|
68
|
-
async
|
|
69
|
-
const tool =
|
|
50
|
+
async "pkg.install"(args, options) {
|
|
51
|
+
const tool = assertSafePackageName(requireString(args, "tool"));
|
|
70
52
|
const pkgmgr = await detectPackageManager();
|
|
71
|
-
|
|
53
|
+
const spec = pkgmgr.installArgv(tool);
|
|
54
|
+
if (!spec) {
|
|
55
|
+
// Unknown manager: fall back to an instructional message instead of
|
|
56
|
+
// executing a malformed shell string.
|
|
57
|
+
return { ok: false, output: pkgmgr.installCommand(tool), exitCode: 1 };
|
|
58
|
+
}
|
|
59
|
+
return spawnArgv({
|
|
60
|
+
command: spec.command,
|
|
61
|
+
argv: spec.argv,
|
|
62
|
+
timeoutMs: 600_000,
|
|
63
|
+
signal: options?.signal,
|
|
64
|
+
onOutput: options?.onOutput,
|
|
65
|
+
});
|
|
72
66
|
},
|
|
73
|
-
async
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
67
|
+
async "net.scan"(args, options) {
|
|
68
|
+
const host = parseHost(requireString(args, "target"));
|
|
69
|
+
const portsRaw = optionalString(args, "ports");
|
|
70
|
+
const ports = portsRaw ? parsePortSpec(portsRaw) : undefined;
|
|
71
|
+
const profile = args.profile &&
|
|
72
|
+
typeof args.profile === "object" &&
|
|
73
|
+
!Array.isArray(args.profile)
|
|
74
|
+
? args.profile
|
|
75
|
+
: undefined;
|
|
76
|
+
const legacyFlags = optionalString(args, "flags");
|
|
77
|
+
const profileArgs = profileToNmapArgs(profile);
|
|
78
|
+
const legacyArgs = legacyFlags ? parseLegacyFlags(legacyFlags) : [];
|
|
79
|
+
const argv = [];
|
|
80
|
+
if (ports)
|
|
81
|
+
argv.push("-p", ports);
|
|
82
|
+
argv.push(...profileArgs, ...legacyArgs, host.value);
|
|
83
|
+
return spawnArgv({
|
|
84
|
+
command: "nmap",
|
|
85
|
+
argv,
|
|
86
|
+
timeoutMs: 300_000,
|
|
87
|
+
signal: options?.signal,
|
|
88
|
+
onOutput: options?.onOutput,
|
|
89
|
+
});
|
|
81
90
|
},
|
|
82
|
-
async
|
|
83
|
-
const headers = args.headers &&
|
|
91
|
+
async "http.fetch"(args) {
|
|
92
|
+
const headers = args.headers &&
|
|
93
|
+
typeof args.headers === "object" &&
|
|
94
|
+
!Array.isArray(args.headers)
|
|
84
95
|
? args.headers
|
|
85
96
|
: undefined;
|
|
86
|
-
return httpFetch(requireString(args,
|
|
87
|
-
method: optionalString(args,
|
|
88
|
-
body: optionalString(args,
|
|
97
|
+
return httpFetch(requireString(args, "url"), {
|
|
98
|
+
method: optionalString(args, "method"),
|
|
99
|
+
body: optionalString(args, "body"),
|
|
89
100
|
headers,
|
|
90
|
-
maxBytes: optionalNumber(args,
|
|
91
|
-
|
|
101
|
+
maxBytes: optionalNumber(args, "maxBytes"),
|
|
102
|
+
iOwnThis: args.iOwnThis === true || args.own === true,
|
|
92
103
|
});
|
|
93
104
|
},
|
|
94
105
|
async sysinfo() {
|
|
95
106
|
return { ok: true, output: JSON.stringify(detectSystem(), null, 2) };
|
|
96
107
|
},
|
|
97
|
-
async
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
{ command:
|
|
101
|
-
{ command:
|
|
102
|
-
{ command:
|
|
108
|
+
async "pentest.recon"(args, options) {
|
|
109
|
+
const host = parseHost(requireString(args, "target"));
|
|
110
|
+
const steps = [
|
|
111
|
+
{ command: "whois", argv: [host.value] },
|
|
112
|
+
{ command: "dig", argv: [host.value, "ANY", "+noall", "+answer"] },
|
|
113
|
+
{ command: "nmap", argv: ["-sV", "--top-ports", "100", host.value] },
|
|
103
114
|
];
|
|
104
115
|
const outputs = [];
|
|
105
|
-
for (const
|
|
116
|
+
for (const step of steps) {
|
|
106
117
|
if (options?.signal?.aborted)
|
|
107
118
|
break;
|
|
119
|
+
const display = `${step.command} ${step.argv.join(" ")}`;
|
|
108
120
|
// Announce each sub-step so users see progress through long recons.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
options?.onOutput?.(`\n$ ${display}\n`, "stdout");
|
|
122
|
+
const result = await spawnArgv({
|
|
123
|
+
command: step.command,
|
|
124
|
+
argv: step.argv,
|
|
125
|
+
timeoutMs: 180_000,
|
|
126
|
+
signal: options?.signal,
|
|
127
|
+
onOutput: options?.onOutput,
|
|
128
|
+
});
|
|
129
|
+
outputs.push(result.output);
|
|
113
130
|
if (options?.signal?.aborted)
|
|
114
131
|
break;
|
|
115
132
|
}
|
|
116
133
|
return {
|
|
117
134
|
ok: !options?.signal?.aborted,
|
|
118
|
-
output: options?.signal?.aborted
|
|
135
|
+
output: options?.signal?.aborted
|
|
136
|
+
? `${outputs.join("\n\n")}\n\nCommand aborted.`.trim()
|
|
137
|
+
: outputs.join("\n\n"),
|
|
119
138
|
exitCode: options?.signal?.aborted ? 130 : 0,
|
|
120
139
|
};
|
|
121
140
|
},
|
|
141
|
+
async "tool.batch"(args, options) {
|
|
142
|
+
return runToolBatch(args, options);
|
|
143
|
+
},
|
|
122
144
|
};
|
|
123
145
|
export function availableToolNames() {
|
|
124
146
|
return Object.keys(toolRegistry);
|
|
@@ -130,4 +152,126 @@ export async function runToolCall(call, options = {}) {
|
|
|
130
152
|
}
|
|
131
153
|
return handler(call.args, options);
|
|
132
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Tools that `tool.batch` is allowed to invoke. Limited to read-only
|
|
157
|
+
* operations so the batch runner cannot escalate into shell execution
|
|
158
|
+
* or mutating HTTP methods. http.fetch is allowed but downstream
|
|
159
|
+
* GET/HEAD enforcement still happens in the classifier when individual
|
|
160
|
+
* calls are routed.
|
|
161
|
+
*/
|
|
162
|
+
const BATCH_SAFE_TOOLS = new Set([
|
|
163
|
+
"fs.read",
|
|
164
|
+
"fs.list",
|
|
165
|
+
"fs.search",
|
|
166
|
+
"http.fetch",
|
|
167
|
+
"sysinfo",
|
|
168
|
+
]);
|
|
169
|
+
const BATCH_MAX_CALLS = 8;
|
|
170
|
+
const BATCH_DEFAULT_CONCURRENCY = 3;
|
|
171
|
+
const BATCH_MAX_CONCURRENCY = 4;
|
|
172
|
+
function parseBatchCalls(value) {
|
|
173
|
+
if (!Array.isArray(value)) {
|
|
174
|
+
throw new Error("tool.batch expects { calls: [{name, args}, ...] }");
|
|
175
|
+
}
|
|
176
|
+
if (value.length === 0) {
|
|
177
|
+
throw new Error("tool.batch requires at least one call");
|
|
178
|
+
}
|
|
179
|
+
if (value.length > BATCH_MAX_CALLS) {
|
|
180
|
+
throw new Error(`tool.batch accepts at most ${BATCH_MAX_CALLS} calls per invocation`);
|
|
181
|
+
}
|
|
182
|
+
return value.map((entry, index) => {
|
|
183
|
+
if (!entry ||
|
|
184
|
+
typeof entry !== "object" ||
|
|
185
|
+
Array.isArray(entry) ||
|
|
186
|
+
typeof entry.name !== "string" ||
|
|
187
|
+
typeof entry.args !== "object" ||
|
|
188
|
+
entry.args === null) {
|
|
189
|
+
throw new Error(`tool.batch call #${index} must be { name: string, args: object }`);
|
|
190
|
+
}
|
|
191
|
+
const { name, args } = entry;
|
|
192
|
+
if (!BATCH_SAFE_TOOLS.has(name)) {
|
|
193
|
+
throw new Error(`tool.batch refuses to run "${name}" — only read-only tools are allowed (${[...BATCH_SAFE_TOOLS].join(", ")})`);
|
|
194
|
+
}
|
|
195
|
+
return { name, args };
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async function runWithLimit(items, limit, worker) {
|
|
199
|
+
const queue = items.map((item, index) => ({ item, index }));
|
|
200
|
+
const runners = [];
|
|
201
|
+
for (let n = 0; n < Math.min(limit, queue.length); n += 1) {
|
|
202
|
+
runners.push((async () => {
|
|
203
|
+
while (queue.length > 0) {
|
|
204
|
+
const next = queue.shift();
|
|
205
|
+
if (!next)
|
|
206
|
+
break;
|
|
207
|
+
await worker(next.item, next.index);
|
|
208
|
+
}
|
|
209
|
+
})());
|
|
210
|
+
}
|
|
211
|
+
await Promise.all(runners);
|
|
212
|
+
}
|
|
213
|
+
async function runToolBatch(args, options) {
|
|
214
|
+
const calls = parseBatchCalls(args.calls);
|
|
215
|
+
// Re-classify each child call so confirm/block tools (eg http.fetch POST,
|
|
216
|
+
// public scans without scope) cannot ride in on tool.batch's safe label.
|
|
217
|
+
const scope = await loadScope().catch(() => undefined);
|
|
218
|
+
for (const spec of calls) {
|
|
219
|
+
const decision = classifyToolCall({ name: spec.name, args: spec.args }, { scope });
|
|
220
|
+
if (decision.level !== "safe") {
|
|
221
|
+
throw new Error(`tool.batch refuses ${spec.name}: ${decision.reason} (only safe-classified calls are allowed inside a batch)`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const concurrency = Math.max(1, Math.min(typeof args.concurrency === "number"
|
|
225
|
+
? Math.floor(args.concurrency)
|
|
226
|
+
: BATCH_DEFAULT_CONCURRENCY, BATCH_MAX_CONCURRENCY));
|
|
227
|
+
const outcomes = new Array(calls.length);
|
|
228
|
+
await runWithLimit(calls, concurrency, async (spec, index) => {
|
|
229
|
+
if (options?.signal?.aborted) {
|
|
230
|
+
outcomes[index] = {
|
|
231
|
+
index,
|
|
232
|
+
name: spec.name,
|
|
233
|
+
ok: false,
|
|
234
|
+
output: "Aborted before execution.",
|
|
235
|
+
exitCode: 130,
|
|
236
|
+
};
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const result = await runToolCall({ name: spec.name, args: spec.args },
|
|
241
|
+
// Skip onOutput streaming: batch results are summarized after all
|
|
242
|
+
// members complete to keep the live preview readable.
|
|
243
|
+
{ signal: options?.signal });
|
|
244
|
+
outcomes[index] = {
|
|
245
|
+
index,
|
|
246
|
+
name: spec.name,
|
|
247
|
+
ok: result.ok,
|
|
248
|
+
output: result.output,
|
|
249
|
+
exitCode: result.exitCode,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
outcomes[index] = {
|
|
254
|
+
index,
|
|
255
|
+
name: spec.name,
|
|
256
|
+
ok: false,
|
|
257
|
+
output: "",
|
|
258
|
+
error: error instanceof Error ? error.message : String(error),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
const allOk = outcomes.every((outcome) => outcome.ok);
|
|
263
|
+
const sections = outcomes.map((outcome) => {
|
|
264
|
+
const status = outcome.ok ? "ok" : "fail";
|
|
265
|
+
const head = `── #${outcome.index + 1} ${outcome.name} [${status}${outcome.exitCode !== undefined ? ` exit=${outcome.exitCode}` : ""}]`;
|
|
266
|
+
const body = outcome.error
|
|
267
|
+
? `error: ${outcome.error}`
|
|
268
|
+
: outcome.output.trim();
|
|
269
|
+
return `${head}\n${body}`;
|
|
270
|
+
});
|
|
271
|
+
return {
|
|
272
|
+
ok: allOk,
|
|
273
|
+
output: sections.join("\n\n"),
|
|
274
|
+
exitCode: allOk ? 0 : 1,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
133
277
|
//# sourceMappingURL=registry.js.map
|