@nanhara/hara 0.132.3 → 0.133.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/CHANGELOG.md +47 -3
- package/README.md +22 -1
- package/dist/agent/loop.js +15 -2
- package/dist/config.js +4 -2
- package/dist/context/agents-md.js +18 -1
- package/dist/exec/jobs.js +2 -2
- package/dist/fs-walk.js +52 -2
- package/dist/index.js +76 -9
- package/dist/package-registry.js +43 -0
- package/dist/tools/all.js +1 -1
- package/dist/tools/builtin.js +94 -5
- package/dist/tools/headless-web.js +417 -0
- package/dist/tools/session-search.js +34 -0
- package/dist/tools/web.js +41 -4
- package/dist/tui/InputBox.js +8 -3
- package/package.json +2 -2
package/dist/tools/web.js
CHANGED
|
@@ -10,6 +10,7 @@ import { request as httpsRequest } from "node:https";
|
|
|
10
10
|
import { ProxyAgent, fetch as undiciFetch, request as undiciRequest } from "undici";
|
|
11
11
|
import { wrapUntrusted } from "../security/external-content.js";
|
|
12
12
|
import { loadConfig } from "../config.js";
|
|
13
|
+
import { renderHeadlessHtml } from "./headless-web.js";
|
|
13
14
|
const MAX = 60_000;
|
|
14
15
|
const SEARCH_ATTEMPT_MS = 8_000;
|
|
15
16
|
const SEARCH_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/124.0 Safari/537.36";
|
|
@@ -699,22 +700,31 @@ registerTool({
|
|
|
699
700
|
});
|
|
700
701
|
registerTool({
|
|
701
702
|
name: "web_fetch",
|
|
702
|
-
description: "Fetch an http(s) URL and return its text content (HTML is reduced to readable text).
|
|
703
|
-
"
|
|
703
|
+
description: "Fetch an http(s) URL and return its text content (HTML is reduced to readable text). Ordinary fetches are read-only. " +
|
|
704
|
+
"For an SPA shell, retry with render=true to use an installed Chromium-family browser in a fresh isolated profile; " +
|
|
705
|
+
"rendering executes page JavaScript and therefore requires the computer-use approval boundary. Not sandboxed.",
|
|
704
706
|
input_schema: {
|
|
705
707
|
type: "object",
|
|
706
708
|
properties: {
|
|
707
709
|
url: { type: "string", description: "http:// or https:// URL" },
|
|
708
710
|
max_chars: { type: "number", description: "cap on returned text (default 60000)" },
|
|
711
|
+
render: { type: "boolean", description: "render JavaScript in an isolated headless browser (approval required; use for SPA shells)" },
|
|
709
712
|
},
|
|
710
713
|
required: ["url"],
|
|
711
714
|
},
|
|
712
715
|
kind: "read",
|
|
713
|
-
|
|
716
|
+
classify(input) {
|
|
717
|
+
return input?.render
|
|
718
|
+
? { effect: "computer", concurrencySafe: false }
|
|
719
|
+
: { effect: "read", concurrencySafe: true };
|
|
720
|
+
},
|
|
714
721
|
visibility: "deferred",
|
|
715
722
|
async run(input, ctx) {
|
|
716
723
|
if (ctx.signal?.aborted)
|
|
717
724
|
throw interrupted("web fetch");
|
|
725
|
+
if (input.render !== undefined && typeof input.render !== "boolean") {
|
|
726
|
+
return "Error: web_fetch `render` must be true or false.";
|
|
727
|
+
}
|
|
718
728
|
let url;
|
|
719
729
|
try {
|
|
720
730
|
url = new URL(input.url);
|
|
@@ -776,8 +786,35 @@ registerTool({
|
|
|
776
786
|
if (text.length > cap)
|
|
777
787
|
text = text.slice(0, cap) + `\n…[truncated ${text.length - cap} chars]`;
|
|
778
788
|
if (/html/i.test(ct) && looksLikeJsRenderedShell(raw, text)) {
|
|
789
|
+
if (input.render === true) {
|
|
790
|
+
const rendered = await renderHeadlessHtml(current, async (target) => {
|
|
791
|
+
const approved = await resolvePublicHost(target.hostname);
|
|
792
|
+
const proxy = webProxy(target, ctx.cwd);
|
|
793
|
+
return { ...approved, ...(proxy ? { proxyUri: proxy.uri } : {}) };
|
|
794
|
+
}, signal);
|
|
795
|
+
if (ctx.signal?.aborted)
|
|
796
|
+
throw interrupted("web fetch");
|
|
797
|
+
if (rendered.html) {
|
|
798
|
+
let renderedText = htmlToText(rendered.html);
|
|
799
|
+
if (renderedText.length > cap) {
|
|
800
|
+
renderedText = renderedText.slice(0, cap) + `\n…[truncated ${renderedText.length - cap} chars]`;
|
|
801
|
+
}
|
|
802
|
+
if (renderedText && !looksLikeJsRenderedShell(rendered.html, renderedText)) {
|
|
803
|
+
return `# ${current.href} (rendered in isolated headless browser)\n\n${wrapUntrusted(renderedText, current.href)}`;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
const reason = rendered.error === "browser-unavailable"
|
|
807
|
+
? "No installed Chromium-family browser was found. Install Chrome/Chromium/Edge or set HARA_BROWSER_PATH to its executable."
|
|
808
|
+
: rendered.error === "timed-out"
|
|
809
|
+
? "The isolated browser did not finish within 25 seconds."
|
|
810
|
+
: rendered.error === "output-too-large"
|
|
811
|
+
? "The rendered DOM exceeded the 4 MiB safety ceiling."
|
|
812
|
+
: "The isolated browser finished without usable rendered text.";
|
|
813
|
+
return `# ${current.href} (HTTP ${res.status})\n\n${wrapUntrusted(`${text || "(empty shell)"}\n\nHeadless render unavailable: ${reason}`, current.href)}`;
|
|
814
|
+
}
|
|
779
815
|
const hint = "This page appears to be JavaScript-rendered; web_fetch received only the SPA shell and does not execute page scripts. " +
|
|
780
|
-
"
|
|
816
|
+
"Retry web_fetch with render=true (it uses a fresh isolated browser and requires approval), use an available browser/web skill, " +
|
|
817
|
+
"or use the site's authenticated API/connector (for example the Feishu Docs API).";
|
|
781
818
|
return `# ${current.href} (HTTP ${res.status})\n\n${wrapUntrusted(`${text || "(empty shell)"}\n\n${hint}`, current.href)}`;
|
|
782
819
|
}
|
|
783
820
|
return `# ${current.href} (HTTP ${res.status})\n\n${wrapUntrusted(text || "(empty body)", current.href)}`;
|
package/dist/tui/InputBox.js
CHANGED
|
@@ -14,7 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
// of leaning on ink's soft-wrap of a single <Text>, which mis-aligns wrapped rows against the
|
|
15
15
|
// "› " prompt gutter and reflows unpredictably as you type.
|
|
16
16
|
import { Box, Text, useInput, useStdout } from "ink";
|
|
17
|
-
import { memo, useMemo, useRef, useState } from "react";
|
|
17
|
+
import { memo, useEffectEvent, useMemo, useRef, useState } from "react";
|
|
18
18
|
import { fileCandidates } from "../context/mentions.js";
|
|
19
19
|
import { imagePathFromPaste } from "../images.js";
|
|
20
20
|
import { vimNormal } from "./vim.js";
|
|
@@ -476,7 +476,11 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
476
476
|
setSel(0);
|
|
477
477
|
setDismissed(false);
|
|
478
478
|
};
|
|
479
|
-
useInput
|
|
479
|
+
// Ink's useInput effect depends on the callback identity. An inline callback therefore tears down and
|
|
480
|
+
// re-adds the stdin listener after every rendered keystroke; on a loaded/remote terminal, the next key can
|
|
481
|
+
// arrive after paint but before the passive effect re-subscribes and be lost. Effect Events keep one stable
|
|
482
|
+
// subscription while reading the latest render state, and are invoked by Ink's effect-owned listener.
|
|
483
|
+
const handleInput = useEffectEvent((input, key) => {
|
|
480
484
|
if (popupOpen && (key.upArrow || key.downArrow)) {
|
|
481
485
|
const n = candidates.length;
|
|
482
486
|
setSel((s) => (key.downArrow ? (s + 1) % n : (s - 1 + n) % n));
|
|
@@ -641,7 +645,8 @@ export function InputBox({ status, cwd, model, route, width, onSubmit, onClipboa
|
|
|
641
645
|
}
|
|
642
646
|
set(value.slice(0, cursor) + input + value.slice(cursor), cursor + input.length);
|
|
643
647
|
}
|
|
644
|
-
}
|
|
648
|
+
});
|
|
649
|
+
useInput(handleInput, { isActive });
|
|
645
650
|
const gutter = vim && mode === "normal" ? "◆ " : "› ";
|
|
646
651
|
const gutterColor = vim ? (mode === "normal" ? "yellow" : "green") : "cyan";
|
|
647
652
|
// The box borders (1 each side) + paddingX (1 each side) subtract 4 cells; InputLine's deterministic
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanhara/hara",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.133.0",
|
|
4
4
|
"description": "hara — a coding agent CLI that runs like an engineering org.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hara": "runtime-bootstrap.cjs"
|
|
@@ -81,6 +81,6 @@
|
|
|
81
81
|
},
|
|
82
82
|
"@hono/node-server": "2.0.11",
|
|
83
83
|
"hono": "4.12.31",
|
|
84
|
-
"fast-uri": "3.1.
|
|
84
|
+
"fast-uri": "3.1.4"
|
|
85
85
|
}
|
|
86
86
|
}
|