@wrongstack/tools 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.d.ts +25 -0
- package/dist/audit.js +209 -0
- package/dist/audit.js.map +1 -0
- package/dist/bash.d.ts +16 -0
- package/dist/bash.js +180 -0
- package/dist/bash.js.map +1 -0
- package/dist/batch-tool-use.d.ts +26 -0
- package/dist/batch-tool-use.js +106 -0
- package/dist/batch-tool-use.js.map +1 -0
- package/dist/builtin.d.ts +5 -0
- package/dist/builtin.js +3735 -0
- package/dist/builtin.js.map +1 -0
- package/dist/diff.d.ts +20 -0
- package/dist/diff.js +142 -0
- package/dist/diff.js.map +1 -0
- package/dist/document.d.ts +27 -0
- package/dist/document.js +148 -0
- package/dist/document.js.map +1 -0
- package/dist/edit.d.ts +22 -0
- package/dist/edit.js +138 -0
- package/dist/edit.js.map +1 -0
- package/dist/exec.d.ts +21 -0
- package/dist/exec.js +159 -0
- package/dist/exec.js.map +1 -0
- package/dist/fetch.d.ts +15 -0
- package/dist/fetch.js +213 -0
- package/dist/fetch.js.map +1 -0
- package/dist/format.d.ts +18 -0
- package/dist/format.js +194 -0
- package/dist/format.js.map +1 -0
- package/dist/git.d.ts +27 -0
- package/dist/git.js +174 -0
- package/dist/git.js.map +1 -0
- package/dist/glob.d.ts +14 -0
- package/dist/glob.js +101 -0
- package/dist/glob.js.map +1 -0
- package/dist/grep.d.ts +20 -0
- package/dist/grep.js +264 -0
- package/dist/grep.js.map +1 -0
- package/dist/index.d.ts +34 -563
- package/dist/index.js +717 -442
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +19 -0
- package/dist/install.js +186 -0
- package/dist/install.js.map +1 -0
- package/dist/json.d.ts +20 -0
- package/dist/json.js +124 -0
- package/dist/json.js.map +1 -0
- package/dist/lint.d.ts +20 -0
- package/dist/lint.js +191 -0
- package/dist/lint.js.map +1 -0
- package/dist/logs.d.ts +27 -0
- package/dist/logs.js +180 -0
- package/dist/logs.js.map +1 -0
- package/dist/memory.d.ts +22 -0
- package/dist/memory.js +53 -0
- package/dist/memory.js.map +1 -0
- package/dist/mode.d.ts +20 -0
- package/dist/mode.js +81 -0
- package/dist/mode.js.map +1 -0
- package/dist/outdated.d.ts +26 -0
- package/dist/outdated.js +138 -0
- package/dist/outdated.js.map +1 -0
- package/dist/patch.d.ts +18 -0
- package/dist/patch.js +101 -0
- package/dist/patch.js.map +1 -0
- package/dist/read.d.ts +16 -0
- package/dist/read.js +81 -0
- package/dist/read.js.map +1 -0
- package/dist/replace.d.ts +23 -0
- package/dist/replace.js +196 -0
- package/dist/replace.js.map +1 -0
- package/dist/scaffold.d.ts +20 -0
- package/dist/scaffold.js +185 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/search.d.ts +20 -0
- package/dist/search.js +212 -0
- package/dist/search.js.map +1 -0
- package/dist/test.d.ts +24 -0
- package/dist/test.js +247 -0
- package/dist/test.js.map +1 -0
- package/dist/todo.d.ts +12 -0
- package/dist/todo.js +53 -0
- package/dist/todo.js.map +1 -0
- package/dist/tool-help.d.ts +23 -0
- package/dist/tool-help.js +122 -0
- package/dist/tool-help.js.map +1 -0
- package/dist/tool-search.d.ts +22 -0
- package/dist/tool-search.js +70 -0
- package/dist/tool-search.js.map +1 -0
- package/dist/tool-use.d.ts +16 -0
- package/dist/tool-use.js +79 -0
- package/dist/tool-use.js.map +1 -0
- package/dist/tree.d.ts +21 -0
- package/dist/tree.js +176 -0
- package/dist/tree.js.map +1 -0
- package/dist/typecheck.d.ts +19 -0
- package/dist/typecheck.js +181 -0
- package/dist/typecheck.js.map +1 -0
- package/dist/write.d.ts +15 -0
- package/dist/write.js +77 -0
- package/dist/write.js.map +1 -0
- package/package.json +137 -4
package/dist/exec.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
// src/exec.ts
|
|
4
|
+
var ALLOWED_COMMANDS = {
|
|
5
|
+
node: ["--version", "-e", "-p", "-r", "--input-type=module"],
|
|
6
|
+
npm: ["--version", "init", "install", "test", "run", "list", "pkg", "doctor"],
|
|
7
|
+
pnpm: ["--version", "init", "install", "add", "remove", "exec", "list", "run", "dlx"],
|
|
8
|
+
npx: ["--version"],
|
|
9
|
+
git: ["--version", "status", "log", "diff", "branch", "checkout", "stash", "add", "commit", "push", "pull"],
|
|
10
|
+
ls: ["-la", "-l", "-a"],
|
|
11
|
+
cat: [],
|
|
12
|
+
head: ["-n"],
|
|
13
|
+
tail: ["-n"],
|
|
14
|
+
wc: ["-l", "-w", "-c"],
|
|
15
|
+
grep: [],
|
|
16
|
+
find: [],
|
|
17
|
+
echo: [],
|
|
18
|
+
mkdir: ["-p"],
|
|
19
|
+
cp: ["-r"],
|
|
20
|
+
mv: [],
|
|
21
|
+
rm: ["-rf"],
|
|
22
|
+
touch: [],
|
|
23
|
+
bun: ["--version", "run", "add", "init"],
|
|
24
|
+
tsc: ["--version", "--noEmit", "--project"],
|
|
25
|
+
vitest: ["--version", "run", "--coverage"],
|
|
26
|
+
biome: ["--version", "lint", "format", "check"],
|
|
27
|
+
cargo: ["--version", "build", "test", "check"],
|
|
28
|
+
rustc: ["--version"],
|
|
29
|
+
go: ["version", "run", "build", "test"],
|
|
30
|
+
python: ["--version", "-c"],
|
|
31
|
+
pip: ["--version", "install", "list"],
|
|
32
|
+
docker: ["--version", "ps", "images", "build"],
|
|
33
|
+
kubectl: ["version", "get", "describe", "logs"]
|
|
34
|
+
};
|
|
35
|
+
var FORBIDDEN_PATTERNS = [
|
|
36
|
+
/;\s*rm\s+-rf/i,
|
|
37
|
+
/\|\s*rm\s/i,
|
|
38
|
+
/\&\&\s*rm/i,
|
|
39
|
+
/\$\(.*rm/s,
|
|
40
|
+
/`.*rm/s,
|
|
41
|
+
/eval\s*\(/i,
|
|
42
|
+
/exec\s+/i,
|
|
43
|
+
/nc\s+-e/i,
|
|
44
|
+
/bash\s+-i/i,
|
|
45
|
+
/\/dev\/tcp\//i,
|
|
46
|
+
/curl\s+.*\|/i,
|
|
47
|
+
/wget\s+.*\|/i,
|
|
48
|
+
/chmod\s+777/i,
|
|
49
|
+
/chmod\s+4755/i,
|
|
50
|
+
/>\s*\/dev\//i,
|
|
51
|
+
/2>\s*\/dev\//i,
|
|
52
|
+
/tee\s+/i
|
|
53
|
+
];
|
|
54
|
+
var MAX_ARGS = 20;
|
|
55
|
+
var MAX_OUTPUT = 2e5;
|
|
56
|
+
var TIMEOUT_MS = 3e4;
|
|
57
|
+
var execTool = {
|
|
58
|
+
name: "exec",
|
|
59
|
+
description: "Restricted shell that only runs pre-approved commands with constrained arguments. Safer alternative to `bash`.",
|
|
60
|
+
usageHint: "Set `command` (must be in allowlist). `args` passed through. Unknown commands require `allow_unknown: true`. Blocks dangerous patterns.",
|
|
61
|
+
permission: "confirm",
|
|
62
|
+
mutating: false,
|
|
63
|
+
timeoutMs: TIMEOUT_MS,
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
command: { type: "string", description: "Command to run (must be in allowlist)" },
|
|
68
|
+
args: { type: "array", items: { type: "string" }, description: "Arguments" },
|
|
69
|
+
cwd: { type: "string", description: "Working directory" },
|
|
70
|
+
timeout: { type: "integer", description: "Timeout in ms (default: 30000)" },
|
|
71
|
+
allow_unknown: {
|
|
72
|
+
type: "boolean",
|
|
73
|
+
description: "Allow commands not in allowlist (DANGEROUS, use with caution)"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
required: ["command"]
|
|
77
|
+
},
|
|
78
|
+
async execute(input, ctx, opts) {
|
|
79
|
+
const cmd = input.command.trim();
|
|
80
|
+
if (!cmd) return { command: cmd, args: [], stdout: "", stderr: "Empty command", exitCode: 1, truncated: false, allowed: false };
|
|
81
|
+
if (FORBIDDEN_PATTERNS.some((re) => re.test(cmd))) {
|
|
82
|
+
return {
|
|
83
|
+
command: cmd,
|
|
84
|
+
args: input.args ?? [],
|
|
85
|
+
stdout: "",
|
|
86
|
+
stderr: `Command blocked: dangerous pattern detected`,
|
|
87
|
+
exitCode: 1,
|
|
88
|
+
truncated: false,
|
|
89
|
+
allowed: false
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const allowedCommands = { ...ALLOWED_COMMANDS };
|
|
93
|
+
if (input.allow_unknown) {
|
|
94
|
+
allowedCommands[cmd] = [];
|
|
95
|
+
}
|
|
96
|
+
if (!(cmd in allowedCommands)) {
|
|
97
|
+
return {
|
|
98
|
+
command: cmd,
|
|
99
|
+
args: input.args ?? [],
|
|
100
|
+
stdout: "",
|
|
101
|
+
stderr: `Command "${cmd}" not in allowlist. Set allow_unknown: true to bypass.`,
|
|
102
|
+
exitCode: 1,
|
|
103
|
+
truncated: false,
|
|
104
|
+
allowed: false
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const args = (input.args ?? []).slice(0, MAX_ARGS);
|
|
108
|
+
const timeout = Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS);
|
|
109
|
+
const cwd = input.cwd ?? ctx.cwd;
|
|
110
|
+
const signal = opts.signal;
|
|
111
|
+
return runCommand(cmd, args, cwd, timeout, signal);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
function runCommand(cmd, args, cwd, timeout, signal) {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
let stdout = "";
|
|
117
|
+
let stderr = "";
|
|
118
|
+
let killed = false;
|
|
119
|
+
const child = spawn(cmd, args, { cwd, signal, stdio: ["ignore", "pipe", "pipe"] });
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
killed = true;
|
|
122
|
+
child.kill("SIGTERM");
|
|
123
|
+
}, timeout);
|
|
124
|
+
child.stdout?.on("data", (chunk) => {
|
|
125
|
+
if (stdout.length < MAX_OUTPUT) stdout += chunk.toString();
|
|
126
|
+
});
|
|
127
|
+
child.stderr?.on("data", (chunk) => {
|
|
128
|
+
if (stderr.length < MAX_OUTPUT) stderr += chunk.toString();
|
|
129
|
+
});
|
|
130
|
+
child.on("close", (code) => {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
resolve({
|
|
133
|
+
command: cmd,
|
|
134
|
+
args,
|
|
135
|
+
stdout: stdout.slice(0, MAX_OUTPUT),
|
|
136
|
+
stderr: stderr.slice(0, MAX_OUTPUT),
|
|
137
|
+
exitCode: killed ? 124 : code ?? 1,
|
|
138
|
+
truncated: stdout.length >= MAX_OUTPUT || stderr.length >= MAX_OUTPUT,
|
|
139
|
+
allowed: true
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
child.on("error", (err) => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
resolve({
|
|
145
|
+
command: cmd,
|
|
146
|
+
args,
|
|
147
|
+
stdout: stdout.slice(0, MAX_OUTPUT),
|
|
148
|
+
stderr: err.message,
|
|
149
|
+
exitCode: 1,
|
|
150
|
+
truncated: false,
|
|
151
|
+
allowed: true
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { execTool };
|
|
158
|
+
//# sourceMappingURL=exec.js.map
|
|
159
|
+
//# sourceMappingURL=exec.js.map
|
package/dist/exec.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/exec.ts"],"names":[],"mappings":"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","file":"exec.js","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\nimport type { Tool } from '@wrongstack/core';\n\nconst ALLOWED_COMMANDS: Record<string, string[]> = {\n node: ['--version', '-e', '-p', '-r', '--input-type=module'],\n npm: ['--version', 'init', 'install', 'test', 'run', 'list', 'pkg', 'doctor'],\n pnpm: ['--version', 'init', 'install', 'add', 'remove', 'exec', 'list', 'run', 'dlx'],\n npx: ['--version'],\n git: ['--version', 'status', 'log', 'diff', 'branch', 'checkout', 'stash', 'add', 'commit', 'push', 'pull'],\n ls: ['-la', '-l', '-a'],\n cat: [],\n head: ['-n'],\n tail: ['-n'],\n wc: ['-l', '-w', '-c'],\n grep: [],\n find: [],\n echo: [],\n mkdir: ['-p'],\n cp: ['-r'],\n mv: [],\n rm: ['-rf'],\n touch: [],\n bun: ['--version', 'run', 'add', 'init'],\n tsc: ['--version', '--noEmit', '--project'],\n vitest: ['--version', 'run', '--coverage'],\n biome: ['--version', 'lint', 'format', 'check'],\n cargo: ['--version', 'build', 'test', 'check'],\n rustc: ['--version'],\n go: ['version', 'run', 'build', 'test'],\n python: ['--version', '-c'],\n pip: ['--version', 'install', 'list'],\n docker: ['--version', 'ps', 'images', 'build'],\n kubectl: ['version', 'get', 'describe', 'logs'],\n};\n\nconst FORBIDDEN_PATTERNS = [\n /;\\s*rm\\s+-rf/i,\n /\\|\\s*rm\\s/i,\n /\\&\\&\\s*rm/i,\n /\\$\\(.*rm/s,\n /`.*rm/s,\n /eval\\s*\\(/i,\n /exec\\s+/i,\n /nc\\s+-e/i,\n /bash\\s+-i/i,\n /\\/dev\\/tcp\\//i,\n /curl\\s+.*\\|/i,\n /wget\\s+.*\\|/i,\n /chmod\\s+777/i,\n /chmod\\s+4755/i,\n />\\s*\\/dev\\//i,\n /2>\\s*\\/dev\\//i,\n /tee\\s+/i,\n];\n\nconst MAX_ARGS = 20;\nconst MAX_OUTPUT = 200_000;\nconst TIMEOUT_MS = 30_000;\n\ninterface ExecInput {\n command: string;\n args?: string[];\n cwd?: string;\n timeout?: number;\n allow_unknown?: boolean;\n}\n\ninterface ExecOutput {\n command: string;\n args: string[];\n stdout: string;\n stderr: string;\n exitCode: number;\n truncated: boolean;\n allowed: boolean;\n}\n\nexport const execTool: Tool<ExecInput, ExecOutput> = {\n name: 'exec',\n description:\n 'Restricted shell that only runs pre-approved commands with constrained arguments. Safer alternative to `bash`.',\n usageHint:\n 'Set `command` (must be in allowlist). `args` passed through. Unknown commands require `allow_unknown: true`. Blocks dangerous patterns.',\n permission: 'confirm',\n mutating: false,\n timeoutMs: TIMEOUT_MS,\n inputSchema: {\n type: 'object',\n properties: {\n command: { type: 'string', description: 'Command to run (must be in allowlist)' },\n args: { type: 'array', items: { type: 'string' }, description: 'Arguments' },\n cwd: { type: 'string', description: 'Working directory' },\n timeout: { type: 'integer', description: 'Timeout in ms (default: 30000)' },\n allow_unknown: {\n type: 'boolean',\n description: 'Allow commands not in allowlist (DANGEROUS, use with caution)',\n },\n },\n required: ['command'],\n },\n async execute(input, ctx, opts) {\n const cmd = input.command.trim();\n if (!cmd) return { command: cmd, args: [], stdout: '', stderr: 'Empty command', exitCode: 1, truncated: false, allowed: false };\n\n if (FORBIDDEN_PATTERNS.some((re) => re.test(cmd))) {\n return {\n command: cmd,\n args: input.args ?? [],\n stdout: '',\n stderr: `Command blocked: dangerous pattern detected`,\n exitCode: 1,\n truncated: false,\n allowed: false,\n };\n }\n\n const allowedCommands = { ...ALLOWED_COMMANDS };\n if (input.allow_unknown) {\n allowedCommands[cmd] = [];\n }\n\n if (!(cmd in allowedCommands)) {\n return {\n command: cmd,\n args: input.args ?? [],\n stdout: '',\n stderr: `Command \"${cmd}\" not in allowlist. Set allow_unknown: true to bypass.`,\n exitCode: 1,\n truncated: false,\n allowed: false,\n };\n }\n\n const args = (input.args ?? []).slice(0, MAX_ARGS);\n const timeout = Math.min(input.timeout ?? TIMEOUT_MS, TIMEOUT_MS);\n\n const cwd = input.cwd ?? ctx.cwd;\n const signal = opts.signal;\n\n return runCommand(cmd, args, cwd, timeout, signal);\n },\n};\n\nfunction runCommand(\n cmd: string,\n args: string[],\n cwd: string,\n timeout: number,\n signal: AbortSignal,\n): Promise<ExecOutput> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n let killed = false;\n\n const child = spawn(cmd, args, { cwd, signal, stdio: ['ignore', 'pipe', 'pipe'] });\n const timer = setTimeout(() => {\n killed = true;\n child.kill('SIGTERM');\n }, timeout);\n\n child.stdout?.on('data', (chunk: Buffer) => {\n if (stdout.length < MAX_OUTPUT) stdout += chunk.toString();\n });\n\n child.stderr?.on('data', (chunk: Buffer) => {\n if (stderr.length < MAX_OUTPUT) stderr += chunk.toString();\n });\n\n child.on('close', (code) => {\n clearTimeout(timer);\n resolve({\n command: cmd,\n args,\n stdout: stdout.slice(0, MAX_OUTPUT),\n stderr: stderr.slice(0, MAX_OUTPUT),\n exitCode: killed ? 124 : (code ?? 1),\n truncated: stdout.length >= MAX_OUTPUT || stderr.length >= MAX_OUTPUT,\n allowed: true,\n });\n });\n\n child.on('error', (err) => {\n clearTimeout(timer);\n resolve({\n command: cmd,\n args,\n stdout: stdout.slice(0, MAX_OUTPUT),\n stderr: err.message,\n exitCode: 1,\n truncated: false,\n allowed: true,\n });\n });\n });\n}"]}
|
package/dist/fetch.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Tool } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
interface FetchInput {
|
|
4
|
+
url: string;
|
|
5
|
+
format?: 'markdown' | 'text' | 'raw';
|
|
6
|
+
}
|
|
7
|
+
interface FetchOutput {
|
|
8
|
+
content: string;
|
|
9
|
+
status: number;
|
|
10
|
+
content_type: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
declare const fetchTool: Tool<FetchInput, FetchOutput>;
|
|
14
|
+
|
|
15
|
+
export { fetchTool };
|
package/dist/fetch.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import * as dns from 'dns/promises';
|
|
2
|
+
import 'path';
|
|
3
|
+
import 'child_process';
|
|
4
|
+
|
|
5
|
+
// src/fetch.ts
|
|
6
|
+
function truncateMiddle(s, max) {
|
|
7
|
+
if (Buffer.byteLength(s, "utf8") <= max) return s;
|
|
8
|
+
const half = Math.floor(max / 2);
|
|
9
|
+
return s.slice(0, half) + `
|
|
10
|
+
\u2026[truncated ${Buffer.byteLength(s, "utf8") - max} bytes from middle]\u2026
|
|
11
|
+
` + s.slice(-half);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/fetch.ts
|
|
15
|
+
var MAX_BYTES = 131072;
|
|
16
|
+
var TIMEOUT_MS = 2e4;
|
|
17
|
+
var PRIVATE_RANGES = [
|
|
18
|
+
/^10\./,
|
|
19
|
+
/^192\.168\./,
|
|
20
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
21
|
+
/^127\./,
|
|
22
|
+
/^0\./,
|
|
23
|
+
/^169\.254\./,
|
|
24
|
+
/^::1$/,
|
|
25
|
+
/^fc/i,
|
|
26
|
+
/^fe80:/i
|
|
27
|
+
];
|
|
28
|
+
var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
|
|
29
|
+
async function fetchWithRedirectLimit(url, maxRedirects, signal) {
|
|
30
|
+
const headers = {
|
|
31
|
+
"user-agent": "WrongStack/1.0 (+https://wrongstack.com)",
|
|
32
|
+
accept: "text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1"
|
|
33
|
+
};
|
|
34
|
+
let redirectCount = 0;
|
|
35
|
+
let currentUrl = url;
|
|
36
|
+
for (; ; ) {
|
|
37
|
+
const res = await fetch(currentUrl, {
|
|
38
|
+
redirect: "manual",
|
|
39
|
+
signal,
|
|
40
|
+
headers
|
|
41
|
+
});
|
|
42
|
+
if (res.status < 300 || res.status > 399) {
|
|
43
|
+
return res;
|
|
44
|
+
}
|
|
45
|
+
redirectCount++;
|
|
46
|
+
if (redirectCount > maxRedirects) {
|
|
47
|
+
throw new Error(`fetch: exceeded ${maxRedirects} redirects`);
|
|
48
|
+
}
|
|
49
|
+
const location = res.headers.get("location");
|
|
50
|
+
if (!location) {
|
|
51
|
+
throw new Error("fetch: redirect status with no location header");
|
|
52
|
+
}
|
|
53
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var fetchTool = {
|
|
57
|
+
name: "fetch",
|
|
58
|
+
description: "Fetch the contents of a URL. HTML is converted to markdown by default.",
|
|
59
|
+
usageHint: "HTTPS only by default. Localhost and RFC1918 ranges blocked unless WRONGSTACK_FETCH_ALLOW_PRIVATE=1. Max 5 redirects, 20s timeout, 128KB cap.",
|
|
60
|
+
permission: "confirm",
|
|
61
|
+
mutating: false,
|
|
62
|
+
timeoutMs: TIMEOUT_MS,
|
|
63
|
+
maxOutputBytes: MAX_BYTES,
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
url: { type: "string" },
|
|
68
|
+
format: { type: "string", enum: ["markdown", "text", "raw"] }
|
|
69
|
+
},
|
|
70
|
+
required: ["url"]
|
|
71
|
+
},
|
|
72
|
+
async execute(input, ctx, opts) {
|
|
73
|
+
let final;
|
|
74
|
+
for await (const ev of fetchTool.executeStream(input, ctx, opts)) {
|
|
75
|
+
if (ev.type === "final") final = ev.output;
|
|
76
|
+
}
|
|
77
|
+
if (!final) throw new Error("fetch: stream ended without final event");
|
|
78
|
+
return final;
|
|
79
|
+
},
|
|
80
|
+
async *executeStream(input, _ctx, opts) {
|
|
81
|
+
if (!input?.url) throw new Error("fetch: url is required");
|
|
82
|
+
const u = new URL(input.url);
|
|
83
|
+
if (u.protocol !== "https:" && u.protocol !== "http:") {
|
|
84
|
+
throw new Error(`fetch: unsupported protocol "${u.protocol}"`);
|
|
85
|
+
}
|
|
86
|
+
if (u.protocol === "http:" && !ALLOW_PRIVATE) {
|
|
87
|
+
throw new Error("fetch: http:// blocked (HTTPS required by default)");
|
|
88
|
+
}
|
|
89
|
+
await assertNotPrivate(u.hostname);
|
|
90
|
+
yield { type: "log", text: `GET ${input.url}` };
|
|
91
|
+
const ctrl = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => ctrl.abort(new Error("fetch timeout")), TIMEOUT_MS);
|
|
93
|
+
const combined = combineSignals(opts.signal, ctrl.signal);
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetchWithRedirectLimit(input.url, 5, combined);
|
|
96
|
+
const ct = res.headers.get("content-type") ?? "application/octet-stream";
|
|
97
|
+
if (/^image\/|^audio\/|^video\/|application\/octet-stream/.test(ct)) {
|
|
98
|
+
throw new Error(`fetch: refusing to read binary content-type "${ct}"`);
|
|
99
|
+
}
|
|
100
|
+
yield { type: "log", text: `HTTP ${res.status} ${ct}`, data: { status: res.status, contentType: ct } };
|
|
101
|
+
const reader = res.body?.getReader();
|
|
102
|
+
let received = 0;
|
|
103
|
+
const chunks = [];
|
|
104
|
+
let pendingBytes = 0;
|
|
105
|
+
const FLUSH_AT = 4 * 1024;
|
|
106
|
+
if (reader) {
|
|
107
|
+
for (; ; ) {
|
|
108
|
+
const { value, done } = await reader.read();
|
|
109
|
+
if (done) break;
|
|
110
|
+
if (!value) continue;
|
|
111
|
+
received += value.byteLength;
|
|
112
|
+
pendingBytes += value.byteLength;
|
|
113
|
+
chunks.push(value);
|
|
114
|
+
if (pendingBytes >= FLUSH_AT) {
|
|
115
|
+
const recent = Buffer.from(value).toString("utf8");
|
|
116
|
+
yield {
|
|
117
|
+
type: "partial_output",
|
|
118
|
+
text: recent,
|
|
119
|
+
data: { received }
|
|
120
|
+
};
|
|
121
|
+
pendingBytes = 0;
|
|
122
|
+
}
|
|
123
|
+
if (received > MAX_BYTES) break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf8");
|
|
127
|
+
const format = input.format ?? (ct.includes("text/html") ? "markdown" : "text");
|
|
128
|
+
let content;
|
|
129
|
+
if (format === "raw") content = text;
|
|
130
|
+
else if (format === "markdown" && ct.includes("text/html")) content = htmlToMarkdown(text);
|
|
131
|
+
else if (ct.includes("application/json")) content = prettyJson(text);
|
|
132
|
+
else content = text;
|
|
133
|
+
yield {
|
|
134
|
+
type: "final",
|
|
135
|
+
output: {
|
|
136
|
+
content: truncateMiddle(content, MAX_BYTES),
|
|
137
|
+
status: res.status,
|
|
138
|
+
content_type: ct,
|
|
139
|
+
url: res.url
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
} finally {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
async function assertNotPrivate(hostname) {
|
|
148
|
+
if (ALLOW_PRIVATE) return;
|
|
149
|
+
if (PRIVATE_RANGES.some((r) => r.test(hostname))) {
|
|
150
|
+
throw new Error(`fetch: blocked private/loopback address "${hostname}"`);
|
|
151
|
+
}
|
|
152
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
153
|
+
throw new Error("fetch: blocked localhost target");
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const records = await dns.lookup(hostname, { all: true });
|
|
157
|
+
for (const r of records) {
|
|
158
|
+
if (PRIVATE_RANGES.some((re) => re.test(r.address))) {
|
|
159
|
+
throw new Error(`fetch: resolved to private address ${r.address}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function combineSignals(...sigs) {
|
|
167
|
+
if (typeof AbortSignal.any === "function") {
|
|
168
|
+
return AbortSignal.any(sigs);
|
|
169
|
+
}
|
|
170
|
+
const ctrl = new AbortController();
|
|
171
|
+
for (const s of sigs) {
|
|
172
|
+
if (s.aborted) {
|
|
173
|
+
ctrl.abort(s.reason);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
|
|
177
|
+
}
|
|
178
|
+
return ctrl.signal;
|
|
179
|
+
}
|
|
180
|
+
function prettyJson(s) {
|
|
181
|
+
try {
|
|
182
|
+
return JSON.stringify(JSON.parse(s), null, 2);
|
|
183
|
+
} catch {
|
|
184
|
+
return s;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function htmlToMarkdown(html) {
|
|
188
|
+
let s = html;
|
|
189
|
+
s = s.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
190
|
+
s = s.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
191
|
+
s = s.replace(/<noscript[\s\S]*?<\/noscript>/gi, "");
|
|
192
|
+
s = s.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, n, c) => {
|
|
193
|
+
return "\n" + "#".repeat(Number(n)) + " " + stripTags(c).trim() + "\n";
|
|
194
|
+
});
|
|
195
|
+
s = s.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi, "**$2**");
|
|
196
|
+
s = s.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi, "*$2*");
|
|
197
|
+
s = s.replace(/<a [^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)");
|
|
198
|
+
s = s.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_m, c) => "\n```\n" + stripTags(c) + "\n```\n");
|
|
199
|
+
s = s.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, "`$1`");
|
|
200
|
+
s = s.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n");
|
|
201
|
+
s = s.replace(/<br\s*\/?>/gi, "\n");
|
|
202
|
+
s = s.replace(/<\/p>/gi, "\n\n");
|
|
203
|
+
s = stripTags(s);
|
|
204
|
+
s = s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ");
|
|
205
|
+
return s.replace(/\n{3,}/g, "\n\n").trim();
|
|
206
|
+
}
|
|
207
|
+
function stripTags(s) {
|
|
208
|
+
return s.replace(/<[^>]+>/g, "");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export { fetchTool };
|
|
212
|
+
//# sourceMappingURL=fetch.js.map
|
|
213
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/_util.ts","../src/fetch.ts"],"names":[],"mappings":"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","file":"fetch.js","sourcesContent":["import * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Context, ToolProgressEvent } from '@wrongstack/core';\r\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport interface SpawnStreamResult {\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SpawnStreamOptions {\r\n cmd: string;\r\n args: string[];\r\n cwd: string;\r\n signal: AbortSignal;\r\n maxBytes?: number;\r\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\r\n flushBytes?: number;\r\n}\r\n\r\n/**\r\n * Spawn a child process and yield `partial_output` progress events as\r\n * stdout/stderr arrive (batched by byte threshold), then return the full\r\n * buffered result. Shared between install/lint/format/typecheck/test/audit\r\n * so the TUI live tail sees consistent progress regardless of which tool\r\n * is running.\r\n */\r\nexport async function* spawnStream(\r\n opts: SpawnStreamOptions,\r\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\r\n const max = opts.maxBytes ?? 200_000;\r\n const flushAt = opts.flushBytes ?? 4 * 1024;\r\n let stdout = '';\r\n let stderr = '';\r\n let pending = '';\r\n let error: string | undefined;\r\n\r\n const child = spawn(opts.cmd, opts.args, {\r\n cwd: opts.cwd,\r\n signal: opts.signal,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n\r\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n\r\n child.stdout?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stdout.length < max) stdout += s;\r\n queue.push({ kind: 'out', data: s });\r\n wake();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stderr.length < max) stderr += s;\r\n queue.push({ kind: 'err', data: s });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n error = e.message;\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', (code) => {\r\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\r\n wake();\r\n });\r\n\r\n let exitCode = 0;\r\n let spawnFailed = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((resolve) => {\r\n waiter = resolve;\r\n });\r\n }\r\n const chunk = queue.shift()!;\r\n if (chunk.kind === 'close') {\r\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\r\n // rather than the negative platform code Node fabricates.\r\n if (!spawnFailed) exitCode = chunk.code ?? 0;\r\n break;\r\n }\r\n if (chunk.kind === 'error') {\r\n spawnFailed = true;\r\n exitCode = 1;\r\n // close usually follows\r\n continue;\r\n }\r\n pending += chunk.data;\r\n if (pending.length >= flushAt) {\r\n yield { type: 'partial_output', text: pending };\r\n pending = '';\r\n }\r\n }\r\n if (pending.length > 0) {\r\n yield { type: 'partial_output', text: pending };\r\n }\r\n\r\n return {\r\n stdout,\r\n stderr,\r\n exitCode,\r\n truncated: stdout.length >= max || stderr.length >= max,\r\n error,\r\n };\r\n}\r\n","import * as dns from 'node:dns/promises';\r\nimport type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\nimport { truncateMiddle } from './_util.js';\r\n\r\ninterface FetchInput {\r\n url: string;\r\n format?: 'markdown' | 'text' | 'raw';\r\n}\r\n\r\ninterface FetchOutput {\r\n content: string;\r\n status: number;\r\n content_type: string;\r\n url: string;\r\n}\r\n\r\nconst MAX_BYTES = 131_072;\r\nconst TIMEOUT_MS = 20_000;\r\n\r\nconst PRIVATE_RANGES = [\r\n /^10\\./,\r\n /^192\\.168\\./,\r\n /^172\\.(1[6-9]|2[0-9]|3[01])\\./,\r\n /^127\\./,\r\n /^0\\./,\r\n /^169\\.254\\./,\r\n /^::1$/,\r\n /^fc/i,\r\n /^fe80:/i,\r\n];\r\n\r\nconst ALLOW_PRIVATE = process.env['WRONGSTACK_FETCH_ALLOW_PRIVATE'] === '1';\r\n\r\nasync function fetchWithRedirectLimit(\r\n url: string,\r\n maxRedirects: number,\r\n signal: AbortSignal,\r\n): Promise<Response> {\r\n const headers = {\r\n 'user-agent': 'WrongStack/1.0 (+https://wrongstack.com)',\r\n accept: 'text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.1',\r\n };\r\n let redirectCount = 0;\r\n let currentUrl = url;\r\n for (;;) {\r\n const res = await fetch(currentUrl, {\r\n redirect: 'manual',\r\n signal,\r\n headers,\r\n });\r\n if (res.status < 300 || res.status > 399) {\r\n return res;\r\n }\r\n redirectCount++;\r\n if (redirectCount > maxRedirects) {\r\n throw new Error(`fetch: exceeded ${maxRedirects} redirects`);\r\n }\r\n const location = res.headers.get('location');\r\n if (!location) {\r\n throw new Error('fetch: redirect status with no location header');\r\n }\r\n currentUrl = new URL(location, currentUrl).toString();\r\n }\r\n}\r\n\r\nexport const fetchTool: Tool<FetchInput, FetchOutput> = {\r\n name: 'fetch',\r\n description: 'Fetch the contents of a URL. HTML is converted to markdown by default.',\r\n usageHint:\r\n 'HTTPS only by default. Localhost and RFC1918 ranges blocked unless WRONGSTACK_FETCH_ALLOW_PRIVATE=1. Max 5 redirects, 20s timeout, 128KB cap.',\r\n permission: 'confirm',\r\n mutating: false,\r\n timeoutMs: TIMEOUT_MS,\r\n maxOutputBytes: MAX_BYTES,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n url: { type: 'string' },\r\n format: { type: 'string', enum: ['markdown', 'text', 'raw'] },\r\n },\r\n required: ['url'],\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: FetchOutput | undefined;\r\n for await (const ev of fetchTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('fetch: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<FetchOutput>> {\r\n if (!input?.url) throw new Error('fetch: url is required');\r\n const u = new URL(input.url);\r\n if (u.protocol !== 'https:' && u.protocol !== 'http:') {\r\n throw new Error(`fetch: unsupported protocol \"${u.protocol}\"`);\r\n }\r\n if (u.protocol === 'http:' && !ALLOW_PRIVATE) {\r\n throw new Error('fetch: http:// blocked (HTTPS required by default)');\r\n }\r\n await assertNotPrivate(u.hostname);\r\n\r\n yield { type: 'log', text: `GET ${input.url}` };\r\n\r\n const ctrl = new AbortController();\r\n const timer = setTimeout(() => ctrl.abort(new Error('fetch timeout')), TIMEOUT_MS);\r\n const combined = combineSignals(opts.signal, ctrl.signal);\r\n\r\n try {\r\n const res = await fetchWithRedirectLimit(input.url, 5, combined);\r\n\r\n const ct = res.headers.get('content-type') ?? 'application/octet-stream';\r\n if (/^image\\/|^audio\\/|^video\\/|application\\/octet-stream/.test(ct)) {\r\n throw new Error(`fetch: refusing to read binary content-type \"${ct}\"`);\r\n }\r\n\r\n yield { type: 'log', text: `HTTP ${res.status} ${ct}`, data: { status: res.status, contentType: ct } };\r\n\r\n const reader = res.body?.getReader();\r\n let received = 0;\r\n const chunks: Uint8Array[] = [];\r\n let pendingBytes = 0;\r\n const FLUSH_AT = 4 * 1024;\r\n if (reader) {\r\n for (;;) {\r\n const { value, done } = await reader.read();\r\n if (done) break;\r\n if (!value) continue;\r\n received += value.byteLength;\r\n pendingBytes += value.byteLength;\r\n chunks.push(value);\r\n if (pendingBytes >= FLUSH_AT) {\r\n // Snapshot recent bytes for the partial_output. Keep it cheap —\r\n // don't try to decode UTF-8 boundaries; the TUI just needs a\r\n // \"things are happening\" signal.\r\n const recent = Buffer.from(value).toString('utf8');\r\n yield {\r\n type: 'partial_output',\r\n text: recent,\r\n data: { received },\r\n };\r\n pendingBytes = 0;\r\n }\r\n if (received > MAX_BYTES) break;\r\n }\r\n }\r\n const text = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8');\r\n\r\n const format = input.format ?? (ct.includes('text/html') ? 'markdown' : 'text');\r\n let content: string;\r\n if (format === 'raw') content = text;\r\n else if (format === 'markdown' && ct.includes('text/html')) content = htmlToMarkdown(text);\r\n else if (ct.includes('application/json')) content = prettyJson(text);\r\n else content = text;\r\n\r\n yield {\r\n type: 'final',\r\n output: {\r\n content: truncateMiddle(content, MAX_BYTES),\r\n status: res.status,\r\n content_type: ct,\r\n url: res.url,\r\n },\r\n };\r\n } finally {\r\n clearTimeout(timer);\r\n }\r\n },\r\n};\r\n\r\nasync function assertNotPrivate(hostname: string): Promise<void> {\r\n if (ALLOW_PRIVATE) return;\r\n if (PRIVATE_RANGES.some((r) => r.test(hostname))) {\r\n throw new Error(`fetch: blocked private/loopback address \"${hostname}\"`);\r\n }\r\n if (hostname === 'localhost' || hostname.endsWith('.localhost')) {\r\n throw new Error('fetch: blocked localhost target');\r\n }\r\n try {\r\n const records = await dns.lookup(hostname, { all: true });\r\n for (const r of records) {\r\n if (PRIVATE_RANGES.some((re) => re.test(r.address))) {\r\n throw new Error(`fetch: resolved to private address ${r.address}`);\r\n }\r\n }\r\n } catch (err) {\r\n if (err instanceof Error && err.message.startsWith('fetch:')) throw err;\r\n // DNS failure — let fetch handle it\r\n }\r\n}\r\n\r\nfunction combineSignals(...sigs: AbortSignal[]): AbortSignal {\r\n if (typeof (AbortSignal as { any?: unknown }).any === 'function') {\r\n return (AbortSignal as { any: (s: AbortSignal[]) => AbortSignal }).any(sigs);\r\n }\r\n const ctrl = new AbortController();\r\n for (const s of sigs) {\r\n if (s.aborted) {\r\n ctrl.abort(s.reason);\r\n break;\r\n }\r\n s.addEventListener('abort', () => ctrl.abort(s.reason), { once: true });\r\n }\r\n return ctrl.signal;\r\n}\r\n\r\nfunction prettyJson(s: string): string {\r\n try {\r\n return JSON.stringify(JSON.parse(s), null, 2);\r\n } catch {\r\n return s;\r\n }\r\n}\r\n\r\nfunction htmlToMarkdown(html: string): string {\r\n let s = html;\r\n // Strip scripts/styles\r\n s = s.replace(/<script[\\s\\S]*?<\\/script>/gi, '');\r\n s = s.replace(/<style[\\s\\S]*?<\\/style>/gi, '');\r\n s = s.replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, '');\r\n // Headings\r\n s = s.replace(/<h([1-6])[^>]*>([\\s\\S]*?)<\\/h\\1>/gi, (_m, n, c) => {\r\n return '\\n' + '#'.repeat(Number(n)) + ' ' + stripTags(c).trim() + '\\n';\r\n });\r\n // Bold / italic\r\n s = s.replace(/<(strong|b)[^>]*>([\\s\\S]*?)<\\/\\1>/gi, '**$2**');\r\n s = s.replace(/<(em|i)[^>]*>([\\s\\S]*?)<\\/\\1>/gi, '*$2*');\r\n // Links\r\n s = s.replace(/<a [^>]*href=\"([^\"]+)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, '[$2]($1)');\r\n // Code\r\n s = s.replace(/<pre[^>]*>([\\s\\S]*?)<\\/pre>/gi, (_m, c) => '\\n```\\n' + stripTags(c) + '\\n```\\n');\r\n s = s.replace(/<code[^>]*>([\\s\\S]*?)<\\/code>/gi, '`$1`');\r\n // Lists\r\n s = s.replace(/<li[^>]*>([\\s\\S]*?)<\\/li>/gi, '- $1\\n');\r\n // Breaks / paragraphs\r\n s = s.replace(/<br\\s*\\/?>/gi, '\\n');\r\n s = s.replace(/<\\/p>/gi, '\\n\\n');\r\n // Strip remaining tags\r\n s = stripTags(s);\r\n // Decode common entities\r\n s = s\r\n .replace(/&/g, '&')\r\n .replace(/</g, '<')\r\n .replace(/>/g, '>')\r\n .replace(/"/g, '\"')\r\n .replace(/'/g, \"'\")\r\n .replace(/ /g, ' ');\r\n // Collapse whitespace\r\n return s.replace(/\\n{3,}/g, '\\n\\n').trim();\r\n}\r\n\r\nfunction stripTags(s: string): string {\r\n return s.replace(/<[^>]+>/g, '');\r\n}\r\n"]}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Tool } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
interface FormatInput {
|
|
4
|
+
files?: string | string[];
|
|
5
|
+
fixer?: 'biome' | 'prettier' | 'auto';
|
|
6
|
+
check?: boolean;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
}
|
|
9
|
+
interface FormatOutput {
|
|
10
|
+
fixer: string;
|
|
11
|
+
files_checked: number;
|
|
12
|
+
files_changed: number;
|
|
13
|
+
output: string;
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
}
|
|
16
|
+
declare const formatTool: Tool<FormatInput, FormatOutput>;
|
|
17
|
+
|
|
18
|
+
export { formatTool };
|