@pentoshi/clai 0.6.0 → 0.7.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.
Files changed (116) hide show
  1. package/README.md +9 -17
  2. package/dist/agent/context-manager.d.ts +27 -0
  3. package/dist/agent/context-manager.js +75 -0
  4. package/dist/agent/context-manager.js.map +1 -0
  5. package/dist/agent/runner.d.ts +21 -1
  6. package/dist/agent/runner.js +176 -73
  7. package/dist/agent/runner.js.map +1 -1
  8. package/dist/commands/doctor.js +20 -2
  9. package/dist/commands/doctor.js.map +1 -1
  10. package/dist/commands/update.js +11 -2
  11. package/dist/commands/update.js.map +1 -1
  12. package/dist/index.js +156 -5
  13. package/dist/index.js.map +1 -1
  14. package/dist/llm/anthropic.js +29 -38
  15. package/dist/llm/anthropic.js.map +1 -1
  16. package/dist/llm/gemini.js +31 -40
  17. package/dist/llm/gemini.js.map +1 -1
  18. package/dist/llm/http.d.ts +21 -0
  19. package/dist/llm/http.js +140 -1
  20. package/dist/llm/http.js.map +1 -1
  21. package/dist/llm/ollama.js +18 -27
  22. package/dist/llm/ollama.js.map +1 -1
  23. package/dist/llm/router.d.ts +7 -0
  24. package/dist/llm/router.js +14 -23
  25. package/dist/llm/router.js.map +1 -1
  26. package/dist/modes/agent.d.ts +4 -2
  27. package/dist/modes/agent.js +2 -2
  28. package/dist/modes/agent.js.map +1 -1
  29. package/dist/modes/ask.js +3 -4
  30. package/dist/modes/ask.js.map +1 -1
  31. package/dist/os/pkgmgr.d.ts +7 -1
  32. package/dist/os/pkgmgr.js +97 -18
  33. package/dist/os/pkgmgr.js.map +1 -1
  34. package/dist/prompts/index.d.ts +7 -0
  35. package/dist/prompts/index.js +12 -4
  36. package/dist/prompts/index.js.map +1 -1
  37. package/dist/repl.d.ts +1 -0
  38. package/dist/repl.js +283 -43
  39. package/dist/repl.js.map +1 -1
  40. package/dist/safety/classifier.d.ts +5 -1
  41. package/dist/safety/classifier.js +244 -88
  42. package/dist/safety/classifier.js.map +1 -1
  43. package/dist/safety/patterns.d.ts +48 -1
  44. package/dist/safety/patterns.js +140 -7
  45. package/dist/safety/patterns.js.map +1 -1
  46. package/dist/store/config.d.ts +21 -3
  47. package/dist/store/config.js +28 -9
  48. package/dist/store/config.js.map +1 -1
  49. package/dist/store/history.d.ts +9 -0
  50. package/dist/store/history.js +58 -1
  51. package/dist/store/history.js.map +1 -1
  52. package/dist/store/keys.d.ts +2 -1
  53. package/dist/store/keys.js +7 -3
  54. package/dist/store/keys.js.map +1 -1
  55. package/dist/store/logs.d.ts +7 -0
  56. package/dist/store/logs.js +39 -1
  57. package/dist/store/logs.js.map +1 -1
  58. package/dist/store/project.d.ts +1 -0
  59. package/dist/store/project.js +34 -9
  60. package/dist/store/project.js.map +1 -1
  61. package/dist/store/scope.d.ts +29 -0
  62. package/dist/store/scope.js +113 -0
  63. package/dist/store/scope.js.map +1 -0
  64. package/dist/tools/fs.d.ts +6 -2
  65. package/dist/tools/fs.js +99 -87
  66. package/dist/tools/fs.js.map +1 -1
  67. package/dist/tools/http.d.ts +5 -3
  68. package/dist/tools/http.js +170 -31
  69. package/dist/tools/http.js.map +1 -1
  70. package/dist/tools/policies/output-policy.d.ts +13 -0
  71. package/dist/tools/policies/output-policy.js +56 -0
  72. package/dist/tools/policies/output-policy.js.map +1 -0
  73. package/dist/tools/reducers/ffuf.d.ts +6 -0
  74. package/dist/tools/reducers/ffuf.js +74 -0
  75. package/dist/tools/reducers/ffuf.js.map +1 -0
  76. package/dist/tools/reducers/generic.d.ts +2 -0
  77. package/dist/tools/reducers/generic.js +60 -0
  78. package/dist/tools/reducers/generic.js.map +1 -0
  79. package/dist/tools/reducers/gobuster.d.ts +2 -0
  80. package/dist/tools/reducers/gobuster.js +36 -0
  81. package/dist/tools/reducers/gobuster.js.map +1 -0
  82. package/dist/tools/reducers/httpx.d.ts +2 -0
  83. package/dist/tools/reducers/httpx.js +38 -0
  84. package/dist/tools/reducers/httpx.js.map +1 -0
  85. package/dist/tools/reducers/nmap.d.ts +7 -0
  86. package/dist/tools/reducers/nmap.js +82 -0
  87. package/dist/tools/reducers/nmap.js.map +1 -0
  88. package/dist/tools/reducers/nuclei.d.ts +2 -0
  89. package/dist/tools/reducers/nuclei.js +51 -0
  90. package/dist/tools/reducers/nuclei.js.map +1 -0
  91. package/dist/tools/reducers/sqlmap.d.ts +2 -0
  92. package/dist/tools/reducers/sqlmap.js +39 -0
  93. package/dist/tools/reducers/sqlmap.js.map +1 -0
  94. package/dist/tools/reducers/subdomains.d.ts +6 -0
  95. package/dist/tools/reducers/subdomains.js +31 -0
  96. package/dist/tools/reducers/subdomains.js.map +1 -0
  97. package/dist/tools/reducers/types.d.ts +14 -0
  98. package/dist/tools/reducers/types.js +2 -0
  99. package/dist/tools/reducers/types.js.map +1 -0
  100. package/dist/tools/registry.d.ts +1 -1
  101. package/dist/tools/registry.js +223 -79
  102. package/dist/tools/registry.js.map +1 -1
  103. package/dist/tools/shell.d.ts +45 -4
  104. package/dist/tools/shell.js +419 -88
  105. package/dist/tools/shell.js.map +1 -1
  106. package/dist/tools/validate.d.ts +37 -0
  107. package/dist/tools/validate.js +144 -0
  108. package/dist/tools/validate.js.map +1 -0
  109. package/dist/types.d.ts +7 -15
  110. package/dist/ui/keys.d.ts +21 -0
  111. package/dist/ui/keys.js +13 -0
  112. package/dist/ui/keys.js.map +1 -0
  113. package/dist/ui/output-pane.d.ts +31 -0
  114. package/dist/ui/output-pane.js +81 -0
  115. package/dist/ui/output-pane.js.map +1 -0
  116. 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,2 @@
1
+ import type { Reducer } from "./types.js";
2
+ export declare const nucleiReducer: Reducer;
@@ -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,2 @@
1
+ import type { Reducer } from "./types.js";
2
+ export declare const sqlmapReducer: Reducer;
@@ -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,6 @@
1
+ import type { Reducer } from "./types.js";
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 declare const subdomainsReducer: Reducer;
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/tools/reducers/types.ts"],"names":[],"mappings":""}
@@ -1,4 +1,4 @@
1
- import type { ToolCall, ToolResult } from '../types.js';
1
+ import type { ToolCall, ToolResult } from "../types.js";
2
2
  export interface ToolRunOptions {
3
3
  signal?: AbortSignal | undefined;
4
4
  onOutput?: ((chunk: string, stream: "stdout" | "stderr") => void) | undefined;
@@ -1,124 +1,146 @@
1
- import { detectSystem } from '../os/detect.js';
2
- import { detectPackageManager } from '../os/pkgmgr.js';
3
- import { fsList, fsRead, fsSearch, fsWrite } from './fs.js';
4
- import { httpFetch } from './http.js';
5
- import { shellExec } from './shell.js';
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 !== 'string' || value.length === 0) {
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 === 'string' && value.length > 0 ? value : undefined;
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 === 'number' ? value : undefined;
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 'shell.exec'(args, options) {
54
- return shellExec({ command: requireString(args, 'command'), cwd: optionalString(args, 'cwd'), timeoutMs: optionalNumber(args, 'timeoutMs'), signal: options?.signal, onOutput: options?.onOutput });
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 'fs.read'(args) {
57
- return fsRead(requireString(args, 'path'));
34
+ async "fs.read"(args) {
35
+ return fsRead(requireString(args, "path"), {
36
+ maxBytes: optionalNumber(args, "maxBytes"),
37
+ });
58
38
  },
59
- async 'fs.write'(args) {
60
- return fsWrite(requireString(args, 'path'), requireString(args, 'content'));
39
+ async "fs.write"(args) {
40
+ return fsWrite(requireString(args, "path"), requireString(args, "content"));
61
41
  },
62
- async 'fs.list'(args) {
63
- return fsList(optionalString(args, 'path') ?? process.cwd());
42
+ async "fs.list"(args) {
43
+ return fsList(optionalString(args, "path") ?? process.cwd(), {
44
+ maxEntries: optionalNumber(args, "maxEntries"),
45
+ });
64
46
  },
65
- async 'fs.search'(args) {
66
- return fsSearch(requireString(args, 'pattern'), optionalString(args, 'path'));
47
+ async "fs.search"(args) {
48
+ return fsSearch(requireString(args, "pattern"), optionalString(args, "path"));
67
49
  },
68
- async 'pkg.install'(args, options) {
69
- const tool = safeToolName(requireString(args, 'tool'));
50
+ async "pkg.install"(args, options) {
51
+ const tool = assertSafePackageName(requireString(args, "tool"));
70
52
  const pkgmgr = await detectPackageManager();
71
- return shellExec({ command: pkgmgr.installCommand(tool), signal: options?.signal, onOutput: options?.onOutput });
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 'net.scan'(args, options) {
74
- const target = safeTarget(requireString(args, 'target'));
75
- const ports = optionalString(args, 'ports');
76
- const flags = safeFlagTokens(optionalString(args, 'flags') ?? '');
77
- const argv = ports
78
- ? ['-p', safePorts(ports), ...flags, target]
79
- : [...flags, target];
80
- return shellExec({ command: 'nmap', argv, shell: false, timeoutMs: 300_000, signal: options?.signal, onOutput: options?.onOutput });
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 'http.fetch'(args, options) {
83
- const headers = args.headers && typeof args.headers === 'object' && !Array.isArray(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, 'url'), {
87
- method: optionalString(args, 'method'),
88
- body: optionalString(args, 'body'),
97
+ return httpFetch(requireString(args, "url"), {
98
+ method: optionalString(args, "method"),
99
+ body: optionalString(args, "body"),
89
100
  headers,
90
- maxBytes: optionalNumber(args, 'maxBytes'),
91
- signal: options?.signal,
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 'pentest.recon'(args, options) {
98
- const target = safeTarget(requireString(args, 'target'));
99
- const commands = [
100
- { command: 'whois', argv: [target] },
101
- { command: 'dig', argv: [target, 'ANY', '+noall', '+answer'] },
102
- { command: 'nmap', argv: ['-sV', '--top-ports', '100', target] },
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 { command, argv } of commands) {
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
- const label = commandLabel(command, argv);
110
- options?.onOutput?.(`\n$ ${label}\n`, "stdout");
111
- const result = await shellExec({ command, argv, shell: false, timeoutMs: 180_000, signal: options?.signal, onOutput: options?.onOutput });
112
- outputs.push(`$ ${label}\n${result.output}`);
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 ? `${outputs.join('\n\n')}\n\nCommand aborted.`.trim() : outputs.join('\n\n'),
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