termux-dev 1.6.0 → 1.8.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.
- package/README.md +214 -166
- package/assets/banner.svg +23 -28
- package/assets/preview.png +0 -0
- package/assets/preview.svg +101 -10
- package/dist/cli/banner.js +3 -4
- package/dist/cli/clipboard-write.js +55 -0
- package/dist/cli/clipboard.js +182 -106
- package/dist/cli/colors.js +67 -0
- package/dist/cli/confirm.js +50 -0
- package/dist/cli/doctor.js +4 -4
- package/dist/cli/effort.js +80 -0
- package/dist/cli/export.js +3 -3
- package/dist/cli/files.js +19 -5
- package/dist/cli/finder.js +241 -0
- package/dist/cli/headless.js +23 -11
- package/dist/cli/index.js +1557 -744
- package/dist/cli/keys.js +69 -0
- package/dist/cli/liveblock.js +45 -0
- package/dist/cli/log.js +19 -0
- package/dist/cli/logo.js +30 -0
- package/dist/cli/markdown.js +174 -37
- package/dist/cli/mcp-panel.js +432 -0
- package/dist/cli/permission.js +84 -0
- package/dist/cli/picker.js +143 -0
- package/dist/cli/prompt.js +256 -56
- package/dist/cli/resume.js +15 -0
- package/dist/cli/rewind.js +51 -0
- package/dist/cli/runtime_errors.js +6 -0
- package/dist/cli/server.js +85 -24
- package/dist/cli/settings.js +307 -0
- package/dist/cli/statusline.js +1 -1
- package/dist/cli/theme.js +126 -108
- package/dist/cli/transcript.js +134 -0
- package/dist/cli/typeahead.js +371 -0
- package/dist/cli/ui.js +1022 -0
- package/dist/cli/updater.js +23 -21
- package/dist/cli/usage-panel.js +242 -0
- package/dist/cli/welcome.js +56 -0
- package/dist/cli/width.js +178 -0
- package/dist/core/capabilities.js +433 -0
- package/dist/core/catalog-util.js +27 -0
- package/dist/core/checkpoint.js +90 -0
- package/dist/core/context-stats.js +29 -0
- package/dist/core/cost.js +191 -0
- package/dist/core/formats.js +199 -0
- package/dist/core/history.js +51 -1
- package/dist/core/hooks.js +79 -0
- package/dist/core/i18n.js +42 -0
- package/dist/core/local-config.js +69 -0
- package/dist/core/locales/de.js +265 -0
- package/dist/core/locales/en.js +276 -0
- package/dist/core/locales/es.js +265 -0
- package/dist/core/locales/fr.js +265 -0
- package/dist/core/locales/ja.js +265 -0
- package/dist/core/locales/pt.js +265 -0
- package/dist/core/locales/ru.js +265 -0
- package/dist/core/locales/uk.js +265 -0
- package/dist/core/locales/zh.js +265 -0
- package/dist/core/loop.js +125 -49
- package/dist/core/memory.js +39 -12
- package/dist/core/models.js +33 -20
- package/dist/core/network.js +12 -11
- package/dist/core/notify.js +54 -0
- package/dist/core/planfile.js +43 -0
- package/dist/core/price-catalog.json +1 -0
- package/dist/core/prices.js +249 -0
- package/dist/core/reasoning-catalog.json +1 -0
- package/dist/core/reasoning.js +393 -0
- package/dist/core/redact.js +41 -0
- package/dist/core/secure-file.js +22 -0
- package/dist/core/session.js +16 -5
- package/dist/core/snapshot.js +14 -2
- package/dist/core/title.js +61 -0
- package/dist/core/types.js +1 -1
- package/dist/core/usage.js +43 -7
- package/dist/core/vision-catalog.json +1 -0
- package/dist/core/vision.js +123 -0
- package/dist/mcp/client.js +36 -5
- package/dist/mcp/http-client.js +174 -0
- package/dist/mcp/manager.js +330 -93
- package/dist/permissions/guard.js +385 -117
- package/dist/prompts/builder.js +46 -38
- package/dist/providers/anthropic.js +517 -0
- package/dist/providers/index.js +18 -1
- package/dist/providers/openai.js +138 -125
- package/dist/providers/shared.js +116 -0
- package/dist/providers/toolguard.js +101 -0
- package/dist/tools/bash.js +141 -51
- package/dist/tools/diagnostics.js +76 -48
- package/dist/tools/fs.js +77 -5
- package/dist/tools/index.js +2 -1
- package/dist/tools/mcp.js +72 -0
- package/dist/tools/packages.js +16 -1
- package/dist/tools/plan.js +8 -8
- package/dist/tools/questions.js +50 -54
- package/dist/tools/server.js +6 -0
- package/dist/tools/todo.js +20 -18
- package/dist/tools/web.js +143 -8
- package/package.json +1 -1
package/dist/tools/questions.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { input } from '@inquirer/prompts';
|
|
2
|
+
import { runPicker } from '../cli/picker.js';
|
|
3
|
+
import { palette } from '../cli/ui.js';
|
|
4
|
+
import { wrapWords } from '../cli/width.js';
|
|
5
|
+
import { t } from '../core/i18n.js';
|
|
3
6
|
export const askQuestionsTool = {
|
|
4
7
|
name: 'ask_questions',
|
|
5
8
|
definition: {
|
|
@@ -44,75 +47,68 @@ export const askQuestionsTool = {
|
|
|
44
47
|
}
|
|
45
48
|
const results = [];
|
|
46
49
|
const total = args.questions.length;
|
|
50
|
+
const cols = process.stdout.columns || 80;
|
|
47
51
|
console.log();
|
|
48
|
-
// Pause raw mode for
|
|
52
|
+
// Pause raw mode for the typed answer (the picker handles its own keys)
|
|
49
53
|
const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
|
|
50
|
-
if (wasRaw) {
|
|
51
|
-
process.stdin.setRawMode(false);
|
|
52
|
-
}
|
|
53
54
|
try {
|
|
54
55
|
for (let i = 0; i < total; i++) {
|
|
55
56
|
const q = args.questions[i];
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
choices.push({
|
|
69
|
-
name: `${pc.dim('⏭️ Skip')}`,
|
|
70
|
-
value: '__skip__'
|
|
57
|
+
// The question stays on screen as plain text, the card under it holds the choices.
|
|
58
|
+
for (const line of wrapWords(q.question, Math.max(20, cols - 8)))
|
|
59
|
+
console.log(' ' + palette.bold(palette.text(line)));
|
|
60
|
+
const items = q.options.map((opt, idx) => ({ value: opt, label: `${idx + 1} ${opt}` }));
|
|
61
|
+
if (q.allowCustom !== false)
|
|
62
|
+
items.push({ value: '__custom__', label: t('questions.custom') });
|
|
63
|
+
items.push({ value: '__skip__', label: t('questions.skip') });
|
|
64
|
+
let answer = await runPicker({
|
|
65
|
+
title: q.id && q.id.length <= 24 ? q.id : t('questions.title'),
|
|
66
|
+
subtitle: total > 1 ? `${i + 1} / ${total}` : undefined,
|
|
67
|
+
items,
|
|
71
68
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
message: pc.cyan('Your custom answer:'),
|
|
81
|
-
validate: (v) => v.trim().length > 0 || 'Answer cannot be empty'
|
|
82
|
-
});
|
|
83
|
-
answer = customAns.trim();
|
|
69
|
+
if (answer === '__custom__') {
|
|
70
|
+
if (wasRaw)
|
|
71
|
+
process.stdin.setRawMode(false);
|
|
72
|
+
try {
|
|
73
|
+
answer = (await input({
|
|
74
|
+
message: palette.accent(t('questions.yourAnswer')),
|
|
75
|
+
validate: (v) => v.trim().length > 0 || t('questions.empty'),
|
|
76
|
+
})).trim();
|
|
84
77
|
}
|
|
85
|
-
|
|
86
|
-
answer =
|
|
78
|
+
catch {
|
|
79
|
+
answer = undefined;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
if (wasRaw)
|
|
83
|
+
process.stdin.setRawMode(true);
|
|
84
|
+
process.stdin.resume();
|
|
87
85
|
}
|
|
88
|
-
results.push({
|
|
89
|
-
question: q.question,
|
|
90
|
-
answer
|
|
91
|
-
});
|
|
92
86
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
question: q.question,
|
|
96
|
-
answer: '(Skipped)'
|
|
97
|
-
});
|
|
87
|
+
else if (answer === '__skip__') {
|
|
88
|
+
answer = undefined;
|
|
98
89
|
}
|
|
90
|
+
const given = answer ?? t('questions.skipped');
|
|
91
|
+
results.push({ question: q.question, answer: given });
|
|
92
|
+
// What was asked and what was answered stays in the conversation, quietly.
|
|
93
|
+
console.log(renderAnswerRow(q.question, given, answer === undefined, cols));
|
|
99
94
|
}
|
|
100
95
|
}
|
|
101
96
|
finally {
|
|
102
|
-
// Restore raw mode
|
|
103
97
|
if (wasRaw && process.stdin.isTTY) {
|
|
104
98
|
process.stdin.setRawMode(true);
|
|
105
99
|
process.stdin.resume();
|
|
106
100
|
}
|
|
107
101
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const formattedBlocks = results.map(r => {
|
|
111
|
-
console.log(pc.bold(pc.white(`Q: ${r.question}`)));
|
|
112
|
-
console.log(pc.green(`A: ${r.answer}\n`));
|
|
113
|
-
return `Q: ${r.question}\n\nA: ${r.answer}`;
|
|
114
|
-
});
|
|
115
|
-
console.log(pc.cyan('────────────────────────────────────────────\n'));
|
|
116
|
-
return formattedBlocks.join('\n\n');
|
|
102
|
+
console.log();
|
|
103
|
+
return results.map((r) => `Q: ${r.question}\n\nA: ${r.answer}`).join('\n\n');
|
|
117
104
|
}
|
|
118
105
|
};
|
|
106
|
+
/** ` ✓ question` and the answer under it; a skipped one is muted. */
|
|
107
|
+
export function renderAnswerRow(question, answer, skipped, cols) {
|
|
108
|
+
const room = Math.max(10, cols - 8);
|
|
109
|
+
const q = wrapWords(question.replace(/\s+/g, ' ').trim(), room);
|
|
110
|
+
const a = wrapWords(answer.replace(/\s+/g, ' ').trim(), room - 2);
|
|
111
|
+
const head = q.map((line, i) => (i === 0 ? ` ${skipped ? palette.faint('-') : palette.ok('✓')} ${palette.muted(line)}` : ` ${palette.muted(line)}`));
|
|
112
|
+
const body = a.map((line, i) => ` ${i === 0 ? palette.accent('›') : ' '} ${skipped ? palette.faint(line) : palette.text(line)}`);
|
|
113
|
+
return [...head, ...body].join('\n');
|
|
114
|
+
}
|
package/dist/tools/server.js
CHANGED
|
@@ -24,6 +24,12 @@ export const servePreviewTool = {
|
|
|
24
24
|
if (!args || !args.action) {
|
|
25
25
|
throw new Error('action is required');
|
|
26
26
|
}
|
|
27
|
+
if (!['start', 'stop', 'status'].includes(args.action)) {
|
|
28
|
+
throw new Error('action must be one of: start, stop, status');
|
|
29
|
+
}
|
|
30
|
+
if (args.port !== undefined && (!Number.isInteger(args.port) || args.port < 1024 || args.port > 65535)) {
|
|
31
|
+
throw new Error('port must be an integer between 1024 and 65535');
|
|
32
|
+
}
|
|
27
33
|
},
|
|
28
34
|
execute: async (args) => {
|
|
29
35
|
if (args.action === 'stop') {
|
package/dist/tools/todo.js
CHANGED
|
@@ -1,31 +1,33 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
+
import { displayWidth, padEndWidth, truncateWidth } from '../cli/width.js';
|
|
2
3
|
export let currentTodoList = [];
|
|
3
4
|
export function resetTodoList() {
|
|
4
5
|
currentTodoList = [];
|
|
5
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* The plan as a closed box (classic layout; the clean layout draws it with the theme, see
|
|
9
|
+
* `renderTaskCard`). Every row has the same width, so both borders line up.
|
|
10
|
+
*/
|
|
6
11
|
export function formatTodoListCard(tasks) {
|
|
7
12
|
if (!tasks || tasks.length === 0)
|
|
8
13
|
return '';
|
|
9
|
-
const cols =
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
+
const cols = process.stdout.columns || 80;
|
|
15
|
+
const width = Math.max(30, Math.min(cols - 4, 65));
|
|
16
|
+
const inner = width - 4;
|
|
17
|
+
const title = ' Plan & Tasks ';
|
|
18
|
+
const top = '┌─' + title + '─'.repeat(Math.max(1, width - 3 - displayWidth(title))) + '┐';
|
|
19
|
+
const lines = [pc.bold(pc.cyan(top))];
|
|
14
20
|
for (const task of tasks) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
else {
|
|
23
|
-
taskLine = pc.dim('[ ] ') + pc.white(task.text);
|
|
24
|
-
}
|
|
25
|
-
lines.push(pc.cyan('│ ') + taskLine);
|
|
21
|
+
const marker = task.status === 'completed' ? '[✓]' : task.status === 'in_progress' ? '[•]' : '[ ]';
|
|
22
|
+
const text = truncateWidth(String(task.text ?? '').replace(/\s+/g, ' ').trim(), inner - 4);
|
|
23
|
+
const plain = padEndWidth(`${marker} ${text}`, inner);
|
|
24
|
+
const body = task.status === 'completed' ? pc.green(plain)
|
|
25
|
+
: task.status === 'in_progress' ? pc.bold(pc.cyan(plain))
|
|
26
|
+
: plain;
|
|
27
|
+
lines.push(pc.cyan('│ ') + body + pc.cyan(' │'));
|
|
26
28
|
}
|
|
27
|
-
lines.push(pc.cyan('└' +
|
|
28
|
-
return lines.join('\n');
|
|
29
|
+
lines.push(pc.cyan('└' + '─'.repeat(width - 2) + '┘'));
|
|
30
|
+
return lines.join('\n') + '\n';
|
|
29
31
|
}
|
|
30
32
|
export const todoListTool = {
|
|
31
33
|
name: 'todo_list',
|
package/dist/tools/web.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import dns from 'dns/promises';
|
|
2
|
+
import net from 'net';
|
|
1
3
|
function stripHtml(html) {
|
|
2
4
|
return html
|
|
3
5
|
.replace(/<script\b[\s\S]*?<\/script>/gi, '')
|
|
@@ -5,14 +7,150 @@ function stripHtml(html) {
|
|
|
5
7
|
.replace(/<svg\b[\s\S]*?<\/svg>/gi, '')
|
|
6
8
|
.replace(/<[^>]+>/g, ' ')
|
|
7
9
|
.replace(/"/g, '"')
|
|
8
|
-
.replace(/&/g, '&')
|
|
9
10
|
.replace(/</g, '<')
|
|
10
11
|
.replace(/>/g, '>')
|
|
11
12
|
.replace(/'/g, "'")
|
|
12
13
|
.replace(/ /g, ' ')
|
|
14
|
+
// & last, otherwise "&lt;" would be decoded twice into "<"
|
|
15
|
+
.replace(/&/g, '&')
|
|
13
16
|
.replace(/\s+/g, ' ')
|
|
14
17
|
.trim();
|
|
15
18
|
}
|
|
19
|
+
// ───────────── SSRF protection for fetch_url ─────────────
|
|
20
|
+
// A prompt-injected page must not be able to make the agent talk to localhost,
|
|
21
|
+
// the LAN or cloud metadata endpoints (169.254.169.254). Set
|
|
22
|
+
// DEVX_ALLOW_PRIVATE_NETWORK=1 to fetch from local servers on purpose.
|
|
23
|
+
function isPrivateIPv4(ip) {
|
|
24
|
+
const [a, b, c] = ip.split('.').map(Number);
|
|
25
|
+
return (a === 0 || a === 10 || a === 127 ||
|
|
26
|
+
(a === 100 && b >= 64 && b <= 127) || // carrier-grade NAT
|
|
27
|
+
(a === 169 && b === 254) || // link-local + cloud metadata
|
|
28
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
29
|
+
(a === 192 && b === 0 && c === 0) ||
|
|
30
|
+
(a === 192 && b === 168) ||
|
|
31
|
+
(a === 198 && (b === 18 || b === 19)) ||
|
|
32
|
+
a >= 224 // multicast / reserved / broadcast
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
function parseIPv6(ip) {
|
|
36
|
+
let s = ip.split('%')[0].toLowerCase();
|
|
37
|
+
const embedded = s.match(/(\d+\.\d+\.\d+\.\d+)$/);
|
|
38
|
+
if (embedded) {
|
|
39
|
+
const o = embedded[1].split('.').map(Number);
|
|
40
|
+
s = s.slice(0, -embedded[1].length) + ((o[0] << 8) | o[1]).toString(16) + ':' + ((o[2] << 8) | o[3]).toString(16);
|
|
41
|
+
}
|
|
42
|
+
const parts = s.split('::');
|
|
43
|
+
if (parts.length > 2)
|
|
44
|
+
return null;
|
|
45
|
+
const head = parts[0] ? parts[0].split(':') : [];
|
|
46
|
+
const tail = parts.length === 2 && parts[1] ? parts[1].split(':') : [];
|
|
47
|
+
let groups;
|
|
48
|
+
if (parts.length === 1) {
|
|
49
|
+
groups = head;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const missing = 8 - head.length - tail.length;
|
|
53
|
+
if (missing < 0)
|
|
54
|
+
return null;
|
|
55
|
+
groups = [...head, ...Array(missing).fill('0'), ...tail];
|
|
56
|
+
}
|
|
57
|
+
if (groups.length !== 8)
|
|
58
|
+
return null;
|
|
59
|
+
const nums = groups.map(g => parseInt(g, 16));
|
|
60
|
+
return nums.some(n => Number.isNaN(n)) ? null : nums;
|
|
61
|
+
}
|
|
62
|
+
function isPrivateIPv6(ip) {
|
|
63
|
+
const g = parseIPv6(ip);
|
|
64
|
+
if (!g)
|
|
65
|
+
return true;
|
|
66
|
+
const v4 = (hi, lo) => isPrivateIPv4(`${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`);
|
|
67
|
+
if (g.slice(0, 7).every(n => n === 0) && g[7] <= 1)
|
|
68
|
+
return true; // :: and ::1
|
|
69
|
+
if ((g[0] & 0xffc0) === 0xfe80)
|
|
70
|
+
return true; // fe80::/10 link-local
|
|
71
|
+
if ((g[0] & 0xfe00) === 0xfc00)
|
|
72
|
+
return true; // fc00::/7 unique local
|
|
73
|
+
if ((g[0] & 0xff00) === 0xff00)
|
|
74
|
+
return true; // multicast
|
|
75
|
+
if (g.slice(0, 5).every(n => n === 0) && g[5] === 0xffff)
|
|
76
|
+
return v4(g[6], g[7]); // ::ffff:a.b.c.d
|
|
77
|
+
if (g[0] === 0x64 && g[1] === 0xff9b)
|
|
78
|
+
return v4(g[6], g[7]); // NAT64
|
|
79
|
+
if (g[0] === 0x2002)
|
|
80
|
+
return v4(g[1], g[2]); // 6to4
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
export function isPrivateAddress(ip) {
|
|
84
|
+
if (net.isIPv4(ip))
|
|
85
|
+
return isPrivateIPv4(ip);
|
|
86
|
+
if (net.isIPv6(ip))
|
|
87
|
+
return isPrivateIPv6(ip);
|
|
88
|
+
return true; // not an address we understand: refuse
|
|
89
|
+
}
|
|
90
|
+
async function assertPublicHost(rawHostname) {
|
|
91
|
+
if (process.env.DEVX_ALLOW_PRIVATE_NETWORK === '1')
|
|
92
|
+
return;
|
|
93
|
+
const host = rawHostname.replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
|
|
94
|
+
const blocked = () => new Error(`Blocked: ${rawHostname} points to a local or private network address (set DEVX_ALLOW_PRIVATE_NETWORK=1 to allow).`);
|
|
95
|
+
if (net.isIP(host)) {
|
|
96
|
+
if (isPrivateAddress(host))
|
|
97
|
+
throw blocked();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local') || host.endsWith('.internal')) {
|
|
101
|
+
throw blocked();
|
|
102
|
+
}
|
|
103
|
+
const addresses = await dns.lookup(host, { all: true });
|
|
104
|
+
if (addresses.length === 0 || addresses.some(a => isPrivateAddress(a.address)))
|
|
105
|
+
throw blocked();
|
|
106
|
+
}
|
|
107
|
+
const MAX_REDIRECTS = 5;
|
|
108
|
+
const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
109
|
+
/** fetch() that validates the target and every redirect hop against private networks. */
|
|
110
|
+
export async function fetchPublic(startUrl, headers, timeoutMs) {
|
|
111
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
112
|
+
let current = new URL(startUrl);
|
|
113
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
114
|
+
if (current.protocol !== 'http:' && current.protocol !== 'https:') {
|
|
115
|
+
throw new Error(`Unsupported protocol: ${current.protocol}`);
|
|
116
|
+
}
|
|
117
|
+
await assertPublicHost(current.hostname);
|
|
118
|
+
const res = await fetch(current, { headers, signal, redirect: 'manual' });
|
|
119
|
+
const location = res.headers.get('location');
|
|
120
|
+
if (res.status >= 300 && res.status < 400 && location) {
|
|
121
|
+
try {
|
|
122
|
+
await res.body?.cancel();
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
current = new URL(location, current);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
return res;
|
|
129
|
+
}
|
|
130
|
+
throw new Error('Too many redirects');
|
|
131
|
+
}
|
|
132
|
+
async function readTextLimited(res, maxBytes) {
|
|
133
|
+
const reader = res.body?.getReader();
|
|
134
|
+
if (!reader)
|
|
135
|
+
return (await res.text()).slice(0, maxBytes);
|
|
136
|
+
const chunks = [];
|
|
137
|
+
let total = 0;
|
|
138
|
+
while (true) {
|
|
139
|
+
const { done, value } = await reader.read();
|
|
140
|
+
if (done)
|
|
141
|
+
break;
|
|
142
|
+
chunks.push(Buffer.from(value));
|
|
143
|
+
total += value.byteLength;
|
|
144
|
+
if (total >= maxBytes) {
|
|
145
|
+
try {
|
|
146
|
+
await reader.cancel();
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
153
|
+
}
|
|
16
154
|
export const webSearchTool = {
|
|
17
155
|
name: 'web_search',
|
|
18
156
|
definition: {
|
|
@@ -128,16 +266,13 @@ export const fetchUrlTool = {
|
|
|
128
266
|
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
|
|
129
267
|
targetUrl = `https://${targetUrl}`;
|
|
130
268
|
}
|
|
131
|
-
const res = await
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
},
|
|
135
|
-
signal: AbortSignal.timeout(12000)
|
|
136
|
-
});
|
|
269
|
+
const res = await fetchPublic(targetUrl, {
|
|
270
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
271
|
+
}, 12000);
|
|
137
272
|
if (!res.ok) {
|
|
138
273
|
throw new Error(`HTTP Error ${res.status}: ${res.statusText}`);
|
|
139
274
|
}
|
|
140
|
-
const html = await res
|
|
275
|
+
const html = await readTextLimited(res, MAX_RESPONSE_BYTES);
|
|
141
276
|
const cleanText = stripHtml(html);
|
|
142
277
|
if (!cleanText) {
|
|
143
278
|
return 'Webpage returned empty text content.';
|
package/package.json
CHANGED