@pentoshi/clai 3.0.0 → 3.6.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 +1 -1
- package/dist/agent/classic-renderer.js +9 -1
- package/dist/agent/classic-renderer.js.map +1 -1
- package/dist/agent/context-manager.js +8 -2
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/events.d.ts +5 -0
- package/dist/agent/loop-guard.d.ts +2 -0
- package/dist/agent/loop-guard.js +39 -2
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/plan-tool.d.ts +22 -0
- package/dist/agent/plan-tool.js +434 -49
- package/dist/agent/plan-tool.js.map +1 -1
- package/dist/agent/project-root.d.ts +33 -0
- package/dist/agent/project-root.js +158 -0
- package/dist/agent/project-root.js.map +1 -0
- package/dist/agent/runner.js +1204 -253
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/task-evidence.d.ts +103 -0
- package/dist/agent/task-evidence.js +413 -0
- package/dist/agent/task-evidence.js.map +1 -0
- package/dist/agent/tool-call-parser.d.ts +21 -0
- package/dist/agent/tool-call-parser.js +101 -28
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/agent/tool-history.d.ts +27 -0
- package/dist/agent/tool-history.js +150 -0
- package/dist/agent/tool-history.js.map +1 -0
- package/dist/agent/workspace-orient.d.ts +64 -0
- package/dist/agent/workspace-orient.js +418 -0
- package/dist/agent/workspace-orient.js.map +1 -0
- package/dist/app/adapters/agent-event-adapter.js +5 -0
- package/dist/app/adapters/agent-event-adapter.js.map +1 -1
- package/dist/app/controllers/session-controller.js +4 -0
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/events/app-event.d.ts +4 -0
- package/dist/app/events/app-event.js.map +1 -1
- package/dist/commands/doctor.js +14 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/index.js +40 -8
- package/dist/index.js.map +1 -1
- package/dist/llm/adapters/anthropic-tools.d.ts +99 -0
- package/dist/llm/adapters/anthropic-tools.js +225 -0
- package/dist/llm/adapters/anthropic-tools.js.map +1 -0
- package/dist/llm/adapters/gemini-tools.d.ts +54 -0
- package/dist/llm/adapters/gemini-tools.js +139 -0
- package/dist/llm/adapters/gemini-tools.js.map +1 -0
- package/dist/llm/adapters/ollama-tools.d.ts +22 -0
- package/dist/llm/adapters/ollama-tools.js +62 -0
- package/dist/llm/adapters/ollama-tools.js.map +1 -0
- package/dist/llm/adapters/openai-tools.d.ts +39 -0
- package/dist/llm/adapters/openai-tools.js +71 -0
- package/dist/llm/adapters/openai-tools.js.map +1 -0
- package/dist/llm/agentrouter.js +23 -4
- package/dist/llm/agentrouter.js.map +1 -1
- package/dist/llm/anthropic.js +93 -92
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/aws-mantle.js +93 -56
- package/dist/llm/aws-mantle.js.map +1 -1
- package/dist/llm/bynara.js +23 -4
- package/dist/llm/bynara.js.map +1 -1
- package/dist/llm/capabilities.d.ts +7 -0
- package/dist/llm/capabilities.js +74 -0
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/gemini.js +63 -48
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/groq.js +23 -4
- package/dist/llm/groq.js.map +1 -1
- package/dist/llm/http.d.ts +22 -17
- package/dist/llm/http.js +99 -26
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/kimchi.js +23 -4
- package/dist/llm/kimchi.js.map +1 -1
- package/dist/llm/nvidia.js +23 -4
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/ollama.js +49 -31
- package/dist/llm/ollama.js.map +1 -1
- package/dist/llm/openai.js +23 -4
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/openrouter.js +23 -4
- package/dist/llm/openrouter.js.map +1 -1
- package/dist/llm/qwen-cloud.js +23 -4
- package/dist/llm/qwen-cloud.js.map +1 -1
- package/dist/llm/router.js +48 -6
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/tool-protocol.d.ts +73 -0
- package/dist/llm/tool-protocol.js +282 -0
- package/dist/llm/tool-protocol.js.map +1 -0
- package/dist/modes/ask.js +74 -20
- package/dist/modes/ask.js.map +1 -1
- package/dist/prompts/index.d.ts +13 -5
- package/dist/prompts/index.js +122 -23
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl.js +7 -0
- package/dist/repl.js.map +1 -1
- package/dist/store/config.d.ts +7 -0
- package/dist/store/config.js +1 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/plan.d.ts +2 -0
- package/dist/store/plan.js.map +1 -1
- package/dist/tools/capabilities.d.ts +2 -0
- package/dist/tools/capabilities.js +101 -6
- package/dist/tools/capabilities.js.map +1 -1
- package/dist/tools/command-intent.d.ts +1 -0
- package/dist/tools/command-intent.js +31 -0
- package/dist/tools/command-intent.js.map +1 -1
- package/dist/tools/definitions.d.ts +20 -0
- package/dist/tools/definitions.js +509 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/fs.d.ts +6 -0
- package/dist/tools/fs.js +68 -9
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/net-ping-sweep.js +69 -16
- package/dist/tools/net-ping-sweep.js.map +1 -1
- package/dist/tools/nmap-runner.d.ts +8 -7
- package/dist/tools/nmap-runner.js +126 -65
- package/dist/tools/nmap-runner.js.map +1 -1
- package/dist/tools/registry.js +122 -33
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/validate.d.ts +11 -0
- package/dist/tools/validate.js +53 -0
- package/dist/tools/validate.js.map +1 -1
- package/dist/tools/web/search.js +48 -4
- package/dist/tools/web/search.js.map +1 -1
- package/dist/tui/runtime.d.ts +19 -0
- package/dist/tui/runtime.js +92 -0
- package/dist/tui/runtime.js.map +1 -0
- package/dist/tui/state.js +2 -0
- package/dist/tui/state.js.map +1 -1
- package/dist/tui-v2/app/App.d.ts +3 -3
- package/dist/tui-v2/app/App.js +22 -10
- package/dist/tui-v2/app/App.js.map +1 -1
- package/dist/tui-v2/app/plan-lifecycle.d.ts +6 -0
- package/dist/tui-v2/app/plan-lifecycle.js +43 -7
- package/dist/tui-v2/app/plan-lifecycle.js.map +1 -1
- package/dist/tui-v2/components/modal/confirm-modal.d.ts +5 -6
- package/dist/tui-v2/components/modal/confirm-modal.js +60 -21
- package/dist/tui-v2/components/modal/confirm-modal.js.map +1 -1
- package/dist/tui-v2/components/modal/secret-modal.d.ts +3 -9
- package/dist/tui-v2/components/modal/secret-modal.js +17 -17
- package/dist/tui-v2/components/modal/secret-modal.js.map +1 -1
- package/dist/tui-v2/components/overlay/overlay-host.d.ts +12 -3
- package/dist/tui-v2/components/overlay/overlay-host.js +19 -4
- package/dist/tui-v2/components/overlay/overlay-host.js.map +1 -1
- package/dist/tui-v2/components/plan/plan-view.d.ts +1 -1
- package/dist/tui-v2/components/plan/plan-view.js +8 -4
- package/dist/tui-v2/components/plan/plan-view.js.map +1 -1
- package/dist/tui-v2/components/status/status-line.js +10 -1
- package/dist/tui-v2/components/status/status-line.js.map +1 -1
- package/dist/tui-v2/components/transcript/assistant-message.js +6 -0
- package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
- package/dist/tui-v2/components/transcript/compacted-row.d.ts +5 -5
- package/dist/tui-v2/components/transcript/compacted-row.js +15 -40
- package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
- package/dist/tui-v2/components/transcript/tool-card.js +43 -13
- package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
- package/dist/tui-v2/components/transcript/transcript-row.js +8 -1
- package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
- package/dist/tui-v2/rendering/batch-sections.d.ts +12 -0
- package/dist/tui-v2/rendering/batch-sections.js +68 -0
- package/dist/tui-v2/rendering/batch-sections.js.map +1 -1
- package/dist/tui-v2/rendering/strip-tool-surfaces.d.ts +10 -0
- package/dist/tui-v2/rendering/strip-tool-surfaces.js +32 -0
- package/dist/tui-v2/rendering/strip-tool-surfaces.js.map +1 -0
- package/dist/tui-v2/rendering/tool-presenter.d.ts +6 -1
- package/dist/tui-v2/rendering/tool-presenter.js +68 -2
- package/dist/tui-v2/rendering/tool-presenter.js.map +1 -1
- package/dist/tui-v2/state/transcript-hydrate.js +6 -2
- package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
- package/dist/tui-v2/state/transcript-reducer.js +74 -16
- package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
- package/dist/tui-v2/state/transcript-types.d.ts +1 -1
- package/dist/types.d.ts +58 -0
- package/dist/ui/ansi-box.d.ts +2 -0
- package/dist/ui/ansi-box.js +8 -1
- package/dist/ui/ansi-box.js.map +1 -1
- package/package.json +13 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { platform } from "node:os";
|
|
2
2
|
import { commandAvailable } from "../os/pkgmgr.js";
|
|
3
3
|
import { spawnArgv } from "./shell.js";
|
|
4
|
-
import { nmapScanNeedsPrivilege, toConnectScanArgv } from "./validate.js";
|
|
4
|
+
import { isNmapSingleHostTarget, looksLikeNmapNoHostsUp, nmapArgvHasPn, nmapScanNeedsPrivilege, toConnectScanArgv, withNmapSkipDiscovery, } from "./validate.js";
|
|
5
5
|
/**
|
|
6
6
|
* Pick the OS-appropriate privilege-escalation prefix for a raw-socket scan.
|
|
7
7
|
* - macOS / Linux → `sudo` (clai forwards stdin so the user types their
|
|
@@ -36,24 +36,33 @@ async function elevationPrefix() {
|
|
|
36
36
|
function looksLikePrivilegeError(output) {
|
|
37
37
|
return /(?:requires root privileges|you (?:requested|need) (?:a scan type|root)|operation not permitted|must (?:be|run as) root|raw sockets?|sudo: (?:a (?:password|terminal) is required|no askpass|3 incorrect)|incorrect password|authentication failure|permission denied|requires (?:administrator|elevation))/i.test(output);
|
|
38
38
|
}
|
|
39
|
+
function canInteractiveSudo() {
|
|
40
|
+
return Boolean(process.stdin.isTTY);
|
|
41
|
+
}
|
|
39
42
|
/**
|
|
40
43
|
* Run an nmap scan, transparently obtaining the privileges a stealth/raw
|
|
41
44
|
* scan needs and falling back to an unprivileged TCP connect scan when those
|
|
42
45
|
* privileges can't be obtained (no sudo, password declined, etc.).
|
|
43
46
|
*
|
|
44
47
|
* Strategy:
|
|
45
|
-
* 1.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* 1. Single-host port scans get -Pn so failed host-discovery pings do not
|
|
49
|
+
* report "0 hosts up" while ports are actually open.
|
|
50
|
+
* 2. If the scan needs raw sockets and we're not root, wrap it in the
|
|
51
|
+
* OS-appropriate elevation helper (sudo / doas / gsudo). Prefer the
|
|
52
|
+
* secure requestSecret path (TUI); else interactive TTY sudo.
|
|
53
|
+
* 3. If elevation is unavailable, or the privileged attempt fails in a way
|
|
50
54
|
* that looks like a permission/privilege error, retry as `-sT` (TCP
|
|
51
55
|
* connect) which works for any user on every OS.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
56
|
+
* 4. If a run still looks like "host down / 0 hosts up" without -Pn, retry
|
|
57
|
+
* once with -Pn before returning.
|
|
54
58
|
*/
|
|
55
59
|
export async function runNmapScan(argv, options) {
|
|
56
|
-
|
|
60
|
+
// Known single hosts: skip discovery by default (ICMP/ARP often blocked or
|
|
61
|
+
// needs root; false "0 hosts up" is a common agent failure mode).
|
|
62
|
+
let scanArgv = isNmapSingleHostTarget(argv) && !nmapArgvHasPn(argv)
|
|
63
|
+
? withNmapSkipDiscovery(argv)
|
|
64
|
+
: [...argv];
|
|
65
|
+
const needsPrivilege = nmapScanNeedsPrivilege(scanArgv);
|
|
57
66
|
const prefix = needsPrivilege ? await elevationPrefix() : undefined;
|
|
58
67
|
const attempts = [];
|
|
59
68
|
if (needsPrivilege && prefix) {
|
|
@@ -61,74 +70,95 @@ export async function runNmapScan(argv, options) {
|
|
|
61
70
|
// Authenticate in a short, dedicated process. In the TUI, never inherit
|
|
62
71
|
// stdin for the long nmap scan: pipe the already-entered password to
|
|
63
72
|
// sudo so Ink keeps receiving Escape/Ctrl+C while nmap is running.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const password = await options.requestSecret({
|
|
71
|
-
title: "Administrator access",
|
|
72
|
-
prompt: "Enter your macOS password for sudo. It is sent only to sudo and is never stored.",
|
|
73
|
-
});
|
|
74
|
-
if (password === undefined) {
|
|
75
|
-
return { ok: false, output: "Administrator authentication cancelled.", exitCode: 130 };
|
|
76
|
-
}
|
|
77
|
-
sudoPassword = password;
|
|
78
|
-
auth = await spawnArgv({
|
|
79
|
-
command: "sudo",
|
|
80
|
-
argv: ["-S", "-p", "", "-v"],
|
|
81
|
-
stdinText: `${password}\n`,
|
|
82
|
-
timeoutMs: 30_000,
|
|
83
|
-
signal: options.signal,
|
|
84
|
-
onOutput: options.onOutput,
|
|
85
|
-
noArtifact: true,
|
|
86
|
-
interactiveStdin: false,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
// Classic REPL: let sudo read directly from its controlling terminal.
|
|
91
|
-
auth = await spawnArgv({
|
|
92
|
-
command: "sudo",
|
|
93
|
-
argv: [...prefix.argv, "-v"],
|
|
94
|
-
timeoutMs: 120_000,
|
|
95
|
-
signal: options?.signal,
|
|
96
|
-
onOutput: options?.onOutput,
|
|
97
|
-
interactiveStdin: true,
|
|
98
|
-
noArtifact: true,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
if (options?.signal?.aborted || auth.exitCode === 130)
|
|
102
|
-
return auth;
|
|
103
|
-
if (auth.ok) {
|
|
104
|
-
attempts.push({
|
|
105
|
-
command: "sudo",
|
|
106
|
-
argv: options?.requestSecret ? ["-S", "-p", "", "nmap", ...argv] : ["-n", "nmap", ...argv],
|
|
107
|
-
stdinText: options?.requestSecret ? `${sudoPassword ?? ""}\n` : undefined,
|
|
108
|
-
note: "Administrator access confirmed. Starting stealth scan (ESC cancels).",
|
|
109
|
-
});
|
|
73
|
+
const useSecret = Boolean(options?.requestSecret);
|
|
74
|
+
const useTty = !useSecret && canInteractiveSudo();
|
|
75
|
+
if (!useSecret && !useTty) {
|
|
76
|
+
// No password UI and no TTY — interactive sudo would hang or fail
|
|
77
|
+
// with "a terminal is required". Skip straight to connect-scan.
|
|
78
|
+
options?.onOutput?.("\nCannot prompt for sudo password here (no secret UI / no TTY). Using an unprivileged TCP connect scan (-sT -Pn).\n", "stderr");
|
|
110
79
|
}
|
|
111
80
|
else {
|
|
112
|
-
options?.onOutput?.(
|
|
81
|
+
options?.onOutput?.(useSecret
|
|
82
|
+
? "\nAdministrator access is required for a stealth scan. Complete the secure password prompt below.\n"
|
|
83
|
+
: "\nAdministrator access is required for a stealth scan. Enter your sudo password below; Ctrl+C cancels.\n", "stdout");
|
|
84
|
+
let auth;
|
|
85
|
+
let sudoPassword;
|
|
86
|
+
if (useSecret && options?.requestSecret) {
|
|
87
|
+
const password = await options.requestSecret({
|
|
88
|
+
title: "Administrator access",
|
|
89
|
+
prompt: "Enter your password for sudo (nmap raw-socket scan). It is sent only to sudo and is never stored.",
|
|
90
|
+
});
|
|
91
|
+
if (password === undefined) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
output: "Administrator authentication cancelled. Re-run and enter your password, or use profile.scanType \"tcp\" for an unprivileged connect scan.",
|
|
95
|
+
exitCode: 130,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
sudoPassword = password;
|
|
99
|
+
auth = await spawnArgv({
|
|
100
|
+
command: "sudo",
|
|
101
|
+
argv: ["-S", "-p", "", "-v"],
|
|
102
|
+
stdinText: `${password}\n`,
|
|
103
|
+
timeoutMs: 30_000,
|
|
104
|
+
signal: options.signal,
|
|
105
|
+
onOutput: options.onOutput,
|
|
106
|
+
noArtifact: true,
|
|
107
|
+
interactiveStdin: false,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Classic REPL: let sudo read directly from its controlling terminal.
|
|
112
|
+
auth = await spawnArgv({
|
|
113
|
+
command: "sudo",
|
|
114
|
+
argv: [...prefix.argv, "-v"],
|
|
115
|
+
timeoutMs: 120_000,
|
|
116
|
+
signal: options?.signal,
|
|
117
|
+
onOutput: options?.onOutput,
|
|
118
|
+
interactiveStdin: true,
|
|
119
|
+
noArtifact: true,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (options?.signal?.aborted || auth.exitCode === 130)
|
|
123
|
+
return auth;
|
|
124
|
+
if (auth.ok) {
|
|
125
|
+
attempts.push({
|
|
126
|
+
command: "sudo",
|
|
127
|
+
// -n after a successful -v uses the cached credential without
|
|
128
|
+
// another password prompt (and without needing another stdin).
|
|
129
|
+
argv: useSecret
|
|
130
|
+
? ["-S", "-p", "", "nmap", ...scanArgv]
|
|
131
|
+
: ["-n", "nmap", ...scanArgv],
|
|
132
|
+
stdinText: useSecret ? `${sudoPassword ?? ""}\n` : undefined,
|
|
133
|
+
note: "Administrator access confirmed. Starting stealth scan (ESC cancels).",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const detail = auth.output?.trim()
|
|
138
|
+
? `\n${auth.output.trim().slice(0, 400)}`
|
|
139
|
+
: "";
|
|
140
|
+
options?.onOutput?.(`\nSudo authentication failed or was not completed; using an unprivileged TCP connect scan instead.${detail}\n`, "stderr");
|
|
141
|
+
}
|
|
113
142
|
}
|
|
114
143
|
}
|
|
115
144
|
else if (prefix.command) {
|
|
116
145
|
attempts.push({
|
|
117
146
|
command: prefix.command,
|
|
118
|
-
argv: [...prefix.argv, "nmap", ...
|
|
147
|
+
argv: [...prefix.argv, "nmap", ...scanArgv],
|
|
119
148
|
interactiveStdin: true,
|
|
120
149
|
note: `Running a stealth scan with ${prefix.command} (you may be prompted for your password).`,
|
|
121
150
|
});
|
|
122
151
|
}
|
|
123
152
|
else {
|
|
124
153
|
// Already root.
|
|
125
|
-
attempts.push({ command: "nmap", argv });
|
|
154
|
+
attempts.push({ command: "nmap", argv: scanArgv });
|
|
126
155
|
}
|
|
127
156
|
// Fallback: unprivileged connect scan if elevation fails/declines.
|
|
157
|
+
const connectArgv = withNmapSkipDiscovery(toConnectScanArgv(scanArgv));
|
|
128
158
|
attempts.push({
|
|
129
159
|
command: "nmap",
|
|
130
|
-
argv:
|
|
131
|
-
note: "Privileged scan unavailable — falling back to an unprivileged TCP connect scan (-sT).",
|
|
160
|
+
argv: connectArgv,
|
|
161
|
+
note: "Privileged scan unavailable — falling back to an unprivileged TCP connect scan (-sT -Pn).",
|
|
132
162
|
});
|
|
133
163
|
}
|
|
134
164
|
else if (needsPrivilege && !prefix) {
|
|
@@ -136,14 +166,14 @@ export async function runNmapScan(argv, options) {
|
|
|
136
166
|
// but tell the user why the stealth scan was downgraded.
|
|
137
167
|
attempts.push({
|
|
138
168
|
command: "nmap",
|
|
139
|
-
argv: toConnectScanArgv(
|
|
169
|
+
argv: withNmapSkipDiscovery(toConnectScanArgv(scanArgv)),
|
|
140
170
|
note: platform() === "win32"
|
|
141
|
-
? "No elevation helper found (sudo/gsudo). Run from an Administrator terminal with Npcap for a SYN scan; using a TCP connect scan (-sT) for now."
|
|
142
|
-
: "No sudo/doas available for a raw-socket SYN scan — using an unprivileged TCP connect scan (-sT) instead.",
|
|
171
|
+
? "No elevation helper found (sudo/gsudo). Run from an Administrator terminal with Npcap for a SYN scan; using a TCP connect scan (-sT -Pn) for now."
|
|
172
|
+
: "No sudo/doas available for a raw-socket SYN scan — using an unprivileged TCP connect scan (-sT -Pn) instead.",
|
|
143
173
|
});
|
|
144
174
|
}
|
|
145
175
|
else {
|
|
146
|
-
attempts.push({ command: "nmap", argv });
|
|
176
|
+
attempts.push({ command: "nmap", argv: scanArgv });
|
|
147
177
|
}
|
|
148
178
|
let last;
|
|
149
179
|
for (let i = 0; i < attempts.length; i += 1) {
|
|
@@ -153,7 +183,7 @@ export async function runNmapScan(argv, options) {
|
|
|
153
183
|
}
|
|
154
184
|
if (attempt.note)
|
|
155
185
|
options?.onOutput?.(`\n${attempt.note}\n`, "stdout");
|
|
156
|
-
|
|
186
|
+
let result = await spawnArgv({
|
|
157
187
|
command: attempt.command,
|
|
158
188
|
argv: attempt.argv,
|
|
159
189
|
stdinText: attempt.stdinText,
|
|
@@ -164,10 +194,41 @@ export async function runNmapScan(argv, options) {
|
|
|
164
194
|
? { interactiveStdin: attempt.interactiveStdin }
|
|
165
195
|
: {}),
|
|
166
196
|
});
|
|
197
|
+
// Host discovery false negative: retry once with -Pn on the same elevation.
|
|
198
|
+
if (result.ok &&
|
|
199
|
+
looksLikeNmapNoHostsUp(result.output) &&
|
|
200
|
+
!nmapArgvHasPn(attempt.argv) &&
|
|
201
|
+
isNmapSingleHostTarget(attempt.argv)) {
|
|
202
|
+
options?.onOutput?.("\nHost discovery reported no hosts up — retrying once with -Pn (treat host as online).\n", "stdout");
|
|
203
|
+
result = await spawnArgv({
|
|
204
|
+
command: attempt.command,
|
|
205
|
+
argv: withNmapSkipDiscovery(attempt.argv),
|
|
206
|
+
stdinText: attempt.stdinText,
|
|
207
|
+
timeoutMs: 300_000,
|
|
208
|
+
signal: options?.signal,
|
|
209
|
+
onOutput: options?.onOutput,
|
|
210
|
+
...(attempt.interactiveStdin !== undefined
|
|
211
|
+
? { interactiveStdin: attempt.interactiveStdin }
|
|
212
|
+
: {}),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
167
215
|
last = result;
|
|
168
216
|
// Success, or a non-privilege failure we shouldn't paper over → return.
|
|
169
217
|
const isLastAttempt = i === attempts.length - 1;
|
|
170
218
|
if (result.ok || isLastAttempt || !looksLikePrivilegeError(result.output)) {
|
|
219
|
+
// Annotate empty single-host results so the model does not loop forever.
|
|
220
|
+
if (result.ok &&
|
|
221
|
+
looksLikeNmapNoHostsUp(result.output) &&
|
|
222
|
+
isNmapSingleHostTarget(attempt.argv)) {
|
|
223
|
+
return {
|
|
224
|
+
...result,
|
|
225
|
+
output: result.output +
|
|
226
|
+
"\n\nNote: nmap still reported no live host/open ports after -Pn. " +
|
|
227
|
+
"The target may be offline, firewalled, or blocking this host. " +
|
|
228
|
+
"Do not retry the same net.scan args in a loop — try different ports, " +
|
|
229
|
+
"confirm the IP with net.context / net.pingSweep, or use shell.exec for a custom nmap line.",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
171
232
|
return result;
|
|
172
233
|
}
|
|
173
234
|
// Otherwise loop to the next (fallback) attempt.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nmap-runner.js","sourceRoot":"","sources":["../../src/tools/nmap-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,
|
|
1
|
+
{"version":3,"file":"nmap-runner.js","sourceRoot":"","sources":["../../src/tools/nmap-runner.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/registry.js
CHANGED
|
@@ -4,6 +4,7 @@ import { safeCwd } from "../os/cwd.js";
|
|
|
4
4
|
import { fsEdit, fsDelete, fsList, fsRead, fsSearch, fsWrite, fsWriteMany, fsReplaceLines, fsAppend, } from "./fs.js";
|
|
5
5
|
import { httpFetch } from "./http.js";
|
|
6
6
|
import { shellExec, spawnArgv } from "./shell.js";
|
|
7
|
+
import { isLongQuietInstallOrScaffoldCommand, } from "../agent/task-evidence.js";
|
|
7
8
|
import { imageOcr } from "./image.js";
|
|
8
9
|
import { pdfRead } from "./pdf.js";
|
|
9
10
|
import { webFetch } from "./web/fetch.js";
|
|
@@ -20,6 +21,7 @@ import { jobManager } from "./jobs.js";
|
|
|
20
21
|
import { looksLongRunning } from "./command-intent.js";
|
|
21
22
|
import { packageBinaryName } from "./package-binary.js";
|
|
22
23
|
import { runNmapScan } from "./nmap-runner.js";
|
|
24
|
+
import { fromWireName, sanitizeToolName } from "../llm/tool-protocol.js";
|
|
23
25
|
function requireString(args, key) {
|
|
24
26
|
const value = args[key];
|
|
25
27
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -27,6 +29,14 @@ function requireString(args, key) {
|
|
|
27
29
|
}
|
|
28
30
|
return value;
|
|
29
31
|
}
|
|
32
|
+
/** Like requireString but allows empty string (e.g. replaceLines delete). */
|
|
33
|
+
function requireStringAllowEmpty(args, key) {
|
|
34
|
+
const value = args[key];
|
|
35
|
+
if (typeof value !== "string") {
|
|
36
|
+
throw new Error(`Tool argument "${key}" must be a string`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
30
40
|
function optionalString(args, key) {
|
|
31
41
|
const value = args[key];
|
|
32
42
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
@@ -82,10 +92,15 @@ export const toolRegistry = {
|
|
|
82
92
|
}
|
|
83
93
|
return job;
|
|
84
94
|
}
|
|
95
|
+
// create-next-app / npm install often exceed the default 3 min shell timeout
|
|
96
|
+
// when the model omits timeoutMs — bump automatically for known long jobs.
|
|
97
|
+
const explicitTimeout = optionalNumber(args, "timeoutMs");
|
|
98
|
+
const timeoutMs = explicitTimeout ??
|
|
99
|
+
(isLongQuietInstallOrScaffoldCommand(command) ? 900_000 : undefined);
|
|
85
100
|
return shellExec({
|
|
86
101
|
command,
|
|
87
102
|
cwd: optionalString(args, "cwd"),
|
|
88
|
-
timeoutMs
|
|
103
|
+
timeoutMs,
|
|
89
104
|
signal: options?.signal,
|
|
90
105
|
onOutput: options?.onOutput,
|
|
91
106
|
});
|
|
@@ -297,10 +312,12 @@ export const toolRegistry = {
|
|
|
297
312
|
*/
|
|
298
313
|
async "whois.lookup"(args, options) {
|
|
299
314
|
const host = parseHost(requireString(args, "target"));
|
|
315
|
+
// Keep whois short-lived: many servers hang or buffer until close. A hard
|
|
316
|
+
// 20s cap beats the outer 60s "no output" stall watchdog aborting mid-recon.
|
|
300
317
|
return spawnArgv({
|
|
301
318
|
command: "whois",
|
|
302
319
|
argv: [host.value],
|
|
303
|
-
timeoutMs:
|
|
320
|
+
timeoutMs: 20_000,
|
|
304
321
|
signal: options?.signal,
|
|
305
322
|
onOutput: options?.onOutput,
|
|
306
323
|
});
|
|
@@ -368,28 +385,56 @@ export const toolRegistry = {
|
|
|
368
385
|
}
|
|
369
386
|
const outputs = [];
|
|
370
387
|
const transcript = [];
|
|
388
|
+
let anyOk = false;
|
|
389
|
+
const userAborted = () => Boolean(options?.signal?.aborted);
|
|
371
390
|
for (const step of steps) {
|
|
372
|
-
if (
|
|
391
|
+
if (userAborted())
|
|
373
392
|
break;
|
|
374
393
|
const display = `${step.command} ${step.argv.join(" ")}`;
|
|
375
394
|
transcript.push(`$ ${display}`);
|
|
376
395
|
// Announce each sub-step so users see progress through long recons.
|
|
377
396
|
options?.onOutput?.(`\n$ ${display}\n`, "stdout");
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
397
|
+
// Heartbeat so the outer 60s stall watchdog doesn't kill a quiet whois.
|
|
398
|
+
const beat = setInterval(() => {
|
|
399
|
+
options?.onOutput?.(`[recon] ${step.key} still running…\n`, "stdout");
|
|
400
|
+
}, 5_000);
|
|
401
|
+
beat.unref?.();
|
|
402
|
+
// Per-step caps: whois often hangs; nmap needs longer; dns is quick.
|
|
403
|
+
const stepTimeoutMs = step.key === "whois" ? 20_000 : step.key === "dns" ? 30_000 : 180_000;
|
|
404
|
+
let result;
|
|
405
|
+
try {
|
|
406
|
+
result =
|
|
407
|
+
step.key === "nmap"
|
|
408
|
+
? await runNmapScan(step.argv, options)
|
|
409
|
+
: await spawnArgv({
|
|
410
|
+
command: step.command,
|
|
411
|
+
argv: step.argv,
|
|
412
|
+
timeoutMs: stepTimeoutMs,
|
|
413
|
+
signal: options?.signal,
|
|
414
|
+
onOutput: options?.onOutput,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
419
|
+
result = {
|
|
420
|
+
ok: false,
|
|
421
|
+
output: `${step.key} error: ${msg}`,
|
|
422
|
+
exitCode: 1,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
clearInterval(beat);
|
|
427
|
+
}
|
|
428
|
+
// Continue remaining steps even if whois/dns fails — never cancel
|
|
429
|
+
// sibling top-level tools just because whois hung.
|
|
430
|
+
if (result.ok)
|
|
431
|
+
anyOk = true;
|
|
432
|
+
const body = result.ok
|
|
433
|
+
? result.output
|
|
434
|
+
: `${result.output || `${step.key} failed`}${result.exitCode !== undefined ? ` (exit ${result.exitCode})` : ""}`;
|
|
435
|
+
outputs.push(body);
|
|
436
|
+
transcript.push(body);
|
|
437
|
+
if (userAborted())
|
|
393
438
|
break;
|
|
394
439
|
}
|
|
395
440
|
if (artifactPath) {
|
|
@@ -401,12 +446,15 @@ export const toolRegistry = {
|
|
|
401
446
|
artifactPath = undefined;
|
|
402
447
|
}
|
|
403
448
|
}
|
|
449
|
+
// Soft-success when any step worked and the user didn't cancel. Partial
|
|
450
|
+
// whois failure still returns useful dns/nmap output to the model.
|
|
451
|
+
const aborted = userAborted();
|
|
404
452
|
return {
|
|
405
|
-
ok:
|
|
406
|
-
output:
|
|
453
|
+
ok: aborted ? false : anyOk || outputs.length > 0,
|
|
454
|
+
output: aborted
|
|
407
455
|
? `${outputs.join("\n\n")}\n\nCommand aborted.`.trim()
|
|
408
456
|
: outputs.join("\n\n"),
|
|
409
|
-
exitCode:
|
|
457
|
+
exitCode: aborted ? 130 : anyOk ? 0 : 1,
|
|
410
458
|
...(artifactPath ? { outputPath: artifactPath } : {}),
|
|
411
459
|
};
|
|
412
460
|
},
|
|
@@ -460,11 +508,23 @@ export const toolRegistry = {
|
|
|
460
508
|
return fsEdit(requireString(args, "path"), requireString(args, "oldText"), requireString(args, "newText"), optionalNumber(args, "expectedReplacements"), { confirmed: options?.confirmed });
|
|
461
509
|
},
|
|
462
510
|
async "fs.replaceLines"(args, options) {
|
|
463
|
-
|
|
511
|
+
// Empty content / delete:true removes the line range (X6).
|
|
512
|
+
let content;
|
|
513
|
+
if (args.delete === true) {
|
|
514
|
+
content = "";
|
|
515
|
+
}
|
|
516
|
+
else if (typeof args.content === "string") {
|
|
517
|
+
content = requireStringAllowEmpty(args, "content");
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
throw new Error('Tool argument "content" must be a string (use "" or delete:true to delete the line range)');
|
|
521
|
+
}
|
|
522
|
+
return fsReplaceLines(requireString(args, "path"), requireNumber(args, "startLine"), requireNumber(args, "endLine"), content, { confirmed: options?.confirmed });
|
|
464
523
|
},
|
|
465
524
|
async "fs.append"(args, options) {
|
|
466
525
|
return fsAppend(requireString(args, "path"), requireString(args, "content"), {
|
|
467
526
|
position: optionalString(args, "position"),
|
|
527
|
+
expectedPriorBytes: optionalNumber(args, "expectedPriorBytes"),
|
|
468
528
|
confirmed: options?.confirmed,
|
|
469
529
|
});
|
|
470
530
|
},
|
|
@@ -535,13 +595,25 @@ function buildShellCommandFromCall(name, args) {
|
|
|
535
595
|
* dangerous commands are gated exactly as a hand-written shell.exec would be.
|
|
536
596
|
*/
|
|
537
597
|
export function normalizeToolCall(call) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
598
|
+
// X1: strip channel/commentary tokens before registry lookup.
|
|
599
|
+
let name = typeof call.name === "string" ? call.name.trim() : "";
|
|
600
|
+
if (name && !toolRegistry[name]) {
|
|
601
|
+
const cleaned = sanitizeToolName(name);
|
|
602
|
+
const mapped = fromWireName(cleaned) ?? fromWireName(name) ?? cleaned;
|
|
603
|
+
if (mapped && toolRegistry[mapped]) {
|
|
604
|
+
return { name: mapped, args: call.args ?? {} };
|
|
605
|
+
}
|
|
606
|
+
if (cleaned && cleaned !== name)
|
|
607
|
+
name = cleaned;
|
|
608
|
+
}
|
|
609
|
+
if (toolRegistry[name]) {
|
|
610
|
+
return name === call.name ? call : { name, args: call.args ?? {} };
|
|
611
|
+
}
|
|
541
612
|
// Leave genuinely unknown namespaced tools (e.g. a typo'd "fs.reed") to
|
|
542
613
|
// surface a clear error rather than guessing at a shell command.
|
|
543
|
-
if (!name || name.includes(".") || name.includes("/"))
|
|
544
|
-
return call;
|
|
614
|
+
if (!name || name.includes(".") || name.includes("/")) {
|
|
615
|
+
return name === call.name ? call : { name, args: call.args ?? {} };
|
|
616
|
+
}
|
|
545
617
|
const args = call.args ?? {};
|
|
546
618
|
const command = buildShellCommandFromCall(name, args);
|
|
547
619
|
if (!command)
|
|
@@ -716,10 +788,22 @@ async function runToolBatch(args, options) {
|
|
|
716
788
|
}
|
|
717
789
|
tick(`[batch] #${index + 1} ${spec.name} starting`);
|
|
718
790
|
try {
|
|
719
|
-
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
791
|
+
// Lightweight child heartbeats so silent tools (whois) never trip the
|
|
792
|
+
// outer 60s stall watchdog while still running under the batch.
|
|
793
|
+
const childHeartbeat = setInterval(() => {
|
|
794
|
+
tick(`[batch] #${index + 1} ${spec.name} still running…`);
|
|
795
|
+
}, BATCH_HEARTBEAT_MS);
|
|
796
|
+
childHeartbeat.unref?.();
|
|
797
|
+
let result;
|
|
798
|
+
try {
|
|
799
|
+
result = await runToolCall({ name: spec.name, args: spec.args },
|
|
800
|
+
// Do not fan full child stdout into the card (keeps final sections
|
|
801
|
+
// clean). Progress ticks above drive the live UI + stall watchdog.
|
|
802
|
+
{ signal: batchAc.signal });
|
|
803
|
+
}
|
|
804
|
+
finally {
|
|
805
|
+
clearInterval(childHeartbeat);
|
|
806
|
+
}
|
|
723
807
|
outcomes[index] = {
|
|
724
808
|
index,
|
|
725
809
|
name: spec.name,
|
|
@@ -770,6 +854,7 @@ async function runToolBatch(args, options) {
|
|
|
770
854
|
}
|
|
771
855
|
const finalOutcomes = outcomes;
|
|
772
856
|
const allOk = finalOutcomes.every((outcome) => outcome.ok);
|
|
857
|
+
const parentAborted = Boolean(options?.signal?.aborted);
|
|
773
858
|
const sections = finalOutcomes.map((outcome) => {
|
|
774
859
|
const status = outcome.ok ? "ok" : "fail";
|
|
775
860
|
const head = `── #${outcome.index + 1} ${outcome.name} [${status}${outcome.exitCode !== undefined ? ` exit=${outcome.exitCode}` : ""}]`;
|
|
@@ -784,10 +869,14 @@ async function runToolBatch(args, options) {
|
|
|
784
869
|
`[batch] timed out after ${BATCH_HARD_TIMEOUT_MS / 1000}s — partial results below\n\n` +
|
|
785
870
|
output;
|
|
786
871
|
}
|
|
872
|
+
// Soft-success for the agent turn: one failed whois/dns must NOT cancel
|
|
873
|
+
// sibling top-level tools (http.fetch, net.scan, …). Partial failures stay
|
|
874
|
+
// visible in the sectioned body and non-zero exitCode for the model.
|
|
875
|
+
const softOk = !parentAborted && !timedOut;
|
|
787
876
|
return {
|
|
788
|
-
ok: allOk,
|
|
877
|
+
ok: softOk ? true : allOk,
|
|
789
878
|
output,
|
|
790
|
-
exitCode: allOk ? 0 : timedOut ? 124 : 1,
|
|
879
|
+
exitCode: allOk ? 0 : timedOut ? 124 : parentAborted ? 130 : 1,
|
|
791
880
|
};
|
|
792
881
|
}
|
|
793
882
|
//# sourceMappingURL=registry.js.map
|