diff options
| author | Agando <roduccomm@gmail.com> | 2026-11-06 16:19:59 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-11-06 16:19:59 +0100 |
| commit | 0cb252dfbeb02d23cd92bad3ca8df76be22d9f92 (patch) | |
| tree | db51ae28d9603e262809ca4a58a029f2e9a7fc21 /modules/core/module-fetcher.js | |
| parent | 65b28012ed3aa492604310b43fc499cc13a592c6 (diff) | |
S1 (reported by agent-os4-maint-01, wrongly refuted by the chat auto-reply):
wget existed but was unusable and dishonest.
- Cause 1: _cmdWget used the page's own fetch(url), so every cross-origin URL
died on CORS ("Failed to fetch"). Measured in the served v4.6.150 page:
fetch(cgit README) threw, /api/fetch returned 200/1954 bytes.
- Cause 2: _vfsWriteBytes ignored writeFile()'s boolean, so the command printed
a green tick when the target directory did not exist. Measured:
fs.writeFile('nosuchdir/x.txt') === false.
- Cause 3: bytes were round-tripped through String.fromCharCode, mangling UTF-8.
- Fix: fetch through /api/fetch (raw source), mkdir -p, writeFile + read-back
verification, error instead of a tick on failure. Register curl with
-s/-o parsing delegating to the same writer so the two cannot drift.
S2 (same reporter): web_fetch could not page and said it could.
- Cause: the handler honoured max_chars up to 50 000, then _clipToolResult kept
toolResultChars-200 = 11 800, so 38 200 chars vanished silently; offset was
not in the schema and was never read, so next_offset pointed at a page the
caller could not request again.
- Fix: offset/next_offset/total_chars/source_bytes in the contract; the page is
built, measured against the clip budget and shrunk until the envelope fits,
so next_offset - returned_chars is always 0.
- Also fixed: _clipToolResult appended "0 of N chars withheld" when the content
already fitted, claiming a loss that did not happen.
C1: fetch <url> --save=FILE — the only terminal path from a remote URL to the
VFS, so a 90 KB source page no longer has to be read through 3 000-char
scrollback.
Proxy: /api/extract now returns source_bytes (raw page size) and
content_chars_utf16. Python len() counts code points while JS String.length
counts UTF-16 units; 7 astral characters in the changelog left a 7-char gap at
the end of a 90 KB page (measured). _html_ok declares charset=utf-8.
Verified: node --check on all four modules; py_compile; bundles rebuilt;
served bytes identical to disk for all five assets at ?v=v4.6.151; live in the
browser: curl -s -o review/main.md <cross-origin> saved 1954 chars with the
correct head, a 404 wrote no file, fetch --save wrote 91 518 chars,
web_fetch p1 returned 11 754 with next_offset 11 754 (gap 0), page 2 started at
a different offset with next 23 496.
Diffstat (limited to 'modules/core/module-fetcher.js')
| -rw-r--r-- | modules/core/module-fetcher.js | 17 |
1 files changed, 16 insertions, 1 deletions
diff --git a/modules/core/module-fetcher.js b/modules/core/module-fetcher.js index 45505a7..d1b7bcc 100644 --- a/modules/core/module-fetcher.js +++ b/modules/core/module-fetcher.js @@ -5,7 +5,7 @@ class WebFetcherModule { } getHelpInfo() { return { - 'fetch': 'fetch <url> [--post=BODY] [--type=CT] [--raw] — Fetch URL (raw HTML/text); --post enables POST with body, --type sets Content-Type, --raw shows untruncated content', + 'fetch': 'fetch <url> [--post=BODY] [--type=CT] [--raw] [--save=FILE] — Fetch URL (raw HTML/text); --post enables POST with body, --type sets Content-Type, --raw shows untruncated content, --save writes it to the VFS', 'extract': 'extract <url> — Extract readable article content (title + body text) from a URL', }; } @@ -69,6 +69,21 @@ class WebFetcherModule { return; } const text = await resp.text(); + // --save: the only terminal path from a remote URL to the VFS. Without it a + // 100 KB source page can only be read through 3 000-char scrollback, which is + // what sent the in-OS agent looking for a shell redirect (reported 2026-11-06). + if (flags.save) { + const path = String(flags.save === true ? (url.split('/').pop().split('?')[0] || 'fetch.txt') : flags.save); + const dir = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : ''; + if (dir) { try { this.terminal.fs.mkdir(dir); } catch (e) {} } + const wrote = this.terminal.fs.writeFile(path, text); + if (!wrote || this.terminal.fs.readFile(path) !== text) { + this.terminal.print(`Save failed: could not write ${path}`, 'error'); + return; + } + this.terminal.print(`💾 Saved ${text.length} chars to ${path}`, 'success'); + return; + } const limit = showRaw ? text.length : 3000; this.terminal.print(`\n=== Content from ${url} (${method}) ===\n`, 'success'); this.terminal.print(text.substring(0, limit) + (text.length > limit && !showRaw ? '...' : '')); |