@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/CHANGELOG.md
CHANGED
|
@@ -5,15 +5,59 @@ All notable changes to `@nanhara/hara`.
|
|
|
5
5
|
> Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
|
|
6
6
|
> **patch** (last) number bumps for **optimizations/fixes of existing features**.
|
|
7
7
|
|
|
8
|
-
## 0.
|
|
8
|
+
## 0.133.0 — 2026-07-22 — field-feedback closure: documents, web, recall, and setup
|
|
9
|
+
|
|
10
|
+
- A new approval-gated `python` tool sends Python 3 source directly over stdin, so one-shot APIs such as
|
|
11
|
+
`python-docx` no longer require Hara to create and later execute a durable `.py` helper. Source size,
|
|
12
|
+
active-run cancellation, process-tree timeout, output redaction, workspace sandboxing, and protected-file
|
|
13
|
+
preflight remain enforced; the tool reports a focused prerequisite error when Python 3 is unavailable.
|
|
14
|
+
- Existing user artifacts now keep their original path as the canonical output by default. Hara must not
|
|
15
|
+
invent “完整版”, “简版”, “new”, or similar copies unless the user explicitly requests a separate version.
|
|
16
|
+
Any temporary binary output used for atomic replacement must be removed on both success and failure.
|
|
17
|
+
- Regression coverage executes Python through stdin, proves no `.py` file is left behind, verifies protected
|
|
18
|
+
files cannot be reached through the new input path, and pins the in-place/no-suffix/no-helper policy in the
|
|
19
|
+
agent system prompt. Upgrade with `npm i -g @nanhara/hara@0.133.0`.
|
|
20
|
+
- The transitive `fast-uri` parser is pinned to 3.1.4, closing CVE-2026-16221/GHSA-v2hh-gcrm-f6hx's
|
|
21
|
+
backslash-authority host-confusion gap before URL policy and network consumers can disagree on a host.
|
|
22
|
+
- `--proxy <url>` and `--lang <tag>` make the existing scoped web proxy and same-language behavior visible
|
|
23
|
+
at launch. Proxy credentials remain config/environment-only so they never enter the process list. A user
|
|
24
|
+
who explicitly refers to an older chat (for example “继续上次的任务”) now triggers bounded, audience-safe
|
|
25
|
+
`session_search` before the model turn; ordinary prompts and explicit opt-outs do not scan transcripts.
|
|
26
|
+
- `web_fetch` identifies SPA shells and can retry with `render:true`. The render path requires computer-use
|
|
27
|
+
approval, launches an installed Chrome/Chromium/Edge with a fresh temporary profile, and forces every page,
|
|
28
|
+
redirect, and subresource request through a capped loopback proxy that re-applies public-IP/SSRF checks.
|
|
29
|
+
Browser cancellation, complete-DOM output, process-tree cleanup, and missing-browser failures are bounded.
|
|
30
|
+
- Package installs can explicitly switch sources with `--registry npmmirror`, global `packageRegistry`, or
|
|
31
|
+
`bash.registry`. Registry values are injected as environment rather than shell text and reject credentials.
|
|
32
|
+
Hara does not silently replay an install against a public mirror, because private scopes and supply-chain
|
|
33
|
+
trust must be preserved; timeout guidance now points to the explicit public-dependency recovery.
|
|
34
|
+
- A truly empty directory (including only `.DS_Store`/`.gitkeep`) no longer asks to generate `AGENTS.md`.
|
|
35
|
+
The Intel `--cwd` regression now disables update checks and uses a load-tolerant 30-second process ceiling;
|
|
36
|
+
product behavior remains checked by status/output rather than runner scheduling speed.
|
|
37
|
+
- The TUI composer now keeps one stable Ink input subscription across renders. Rapid follow-up keys (including
|
|
38
|
+
Up/Down history navigation on slower terminals) can no longer land between paint and listener resubscription
|
|
39
|
+
and disappear; the handler still observes the latest draft through React 19 Effect Events.
|
|
40
|
+
|
|
41
|
+
## 0.132.4 — 2026-07-22 — release-class Intel readiness handshakes
|
|
42
|
+
|
|
43
|
+
- Non-Git `@path` completion now skips the bounded Git inventory subprocess when no repository marker
|
|
44
|
+
exists. Slow or contended machines keep the full synchronous budget for the actual filesystem walk instead
|
|
45
|
+
of occasionally returning an empty completion list.
|
|
46
|
+
- TUI streaming and semantic-cancellation regressions now synchronize on concrete composer, render, provider,
|
|
47
|
+
and completion handshakes rather than fixed millisecond sleeps. Main-branch CI also runs the complete suite
|
|
48
|
+
on the same Intel macOS class used for native releases before any immutable version tag is created.
|
|
49
|
+
- This patch carries the complete 0.132 feature set into the native and Desktop release train. Upgrade with
|
|
50
|
+
`npm i -g @nanhara/hara@0.132.4`.
|
|
51
|
+
|
|
52
|
+
## 0.132.3 — 2026-07-22 — immediate prompt-key routing (npm published; native assets withheld)
|
|
9
53
|
|
|
10
54
|
- TUI approval/select input now reads the live visible prompt and selection through stable refs. A key pressed
|
|
11
55
|
immediately after the prompt paints can no longer hit the previous render's null-prompt closure and be
|
|
12
56
|
swallowed; rapid arrow-plus-Enter input also uses the current selection before the next paint.
|
|
13
57
|
- The confirmation regression waits for the visible prompt and then presses `y` without an arbitrary grace
|
|
14
58
|
delay, covering the Node 24 CI failure directly. Remaining process-tree fixtures also wait for valid PID
|
|
15
|
-
content rather than file creation alone.
|
|
16
|
-
|
|
59
|
+
content rather than file creation alone. The npm package is valid, but native assembly was withheld after
|
|
60
|
+
the release-class Intel lane exposed additional completion/render fixture races; upgrade to 0.132.4 instead.
|
|
17
61
|
|
|
18
62
|
## 0.132.2 — 2026-07-22 — complete bounded native release gates (npm published; native assets withheld)
|
|
19
63
|
|
package/README.md
CHANGED
|
@@ -130,6 +130,8 @@ hara config set model ...
|
|
|
130
130
|
**Proxy for web tools in mainland/restricted networks** — only `web_fetch` and `web_search` use it:
|
|
131
131
|
```bash
|
|
132
132
|
export HTTPS_PROXY=http://127.0.0.1:7890
|
|
133
|
+
# or for one Hara launch (do not put credentials in command-line arguments):
|
|
134
|
+
hara --proxy http://127.0.0.1:7890
|
|
133
135
|
# or persist the same endpoint privately:
|
|
134
136
|
hara config set proxy http://127.0.0.1:7890
|
|
135
137
|
```
|
|
@@ -138,6 +140,24 @@ organization enrollment, and chat gateways are not silently redirected. Even thr
|
|
|
138
140
|
pins each DNS-approved public IP and rechecks redirects; authenticated proxy URLs are masked by
|
|
139
141
|
`hara config get`.
|
|
140
142
|
|
|
143
|
+
When `web_fetch` receives only a JavaScript SPA shell, it tells the agent to retry with `render:true`.
|
|
144
|
+
That path asks for computer-use approval, uses an installed Chrome/Chromium/Edge with a fresh temporary
|
|
145
|
+
profile, and routes every browser request back through the same private-IP/redirect guard. Set
|
|
146
|
+
`HARA_BROWSER_PATH` only when the browser executable is installed in a non-standard location.
|
|
147
|
+
|
|
148
|
+
**Package registry for slow/restricted npm networks** — switching is explicit, so Hara never sends a
|
|
149
|
+
private scope to a public mirror without your decision:
|
|
150
|
+
```bash
|
|
151
|
+
hara --registry npmmirror # this launch
|
|
152
|
+
hara config set packageRegistry npmmirror # persist globally
|
|
153
|
+
```
|
|
154
|
+
The `bash` tool also accepts `registry:"npmmirror"` for one npm/pnpm/yarn/bun install. Use `npmjs`,
|
|
155
|
+
`npmmirror`, or a credential-free HTTP(S) registry URL; keep private-registry authentication in the package
|
|
156
|
+
manager's normal credential store. `HARA_PACKAGE_REGISTRY` is the environment equivalent.
|
|
157
|
+
|
|
158
|
+
Replies follow the latest user message's language by default. Use `hara --lang zh-CN` (or `en`) to pin one
|
|
159
|
+
launch, and `hara --lang auto` to restore per-message matching.
|
|
160
|
+
|
|
141
161
|
**Vision** — hara **auto-detects** whether your main model can see images. A vision model (Claude, gpt-4o,
|
|
142
162
|
qwen-vl, glm-4v…) gets pasted images **inline**. For a **text-only** model (DeepSeek, coding models), set a
|
|
143
163
|
describer — the "eyes" — and hara OCRs/describes each pasted image into text first:
|
|
@@ -402,7 +422,8 @@ agent.
|
|
|
402
422
|
### What it can do
|
|
403
423
|
|
|
404
424
|
A streaming agentic loop with built-in tools — `read_file`, `write_file`, **`edit_file`** /
|
|
405
|
-
**`apply_patch`** (surgical edits — single file, or **atomic multi-file** changes),
|
|
425
|
+
**`apply_patch`** (surgical edits — single file, or **atomic multi-file** changes), direct-stdin
|
|
426
|
+
**`python`** (one-shot APIs without helper scripts), `bash`, and
|
|
406
427
|
read-only **`grep`** / **`glob`** / **`ls`** / **`web_fetch`** — behind a human-in-the-loop confirmation gate on the
|
|
407
428
|
dangerous ones unless `-y`. Read-only tools run in parallel within a turn, and edits print a
|
|
408
429
|
**colored diff** of what changed. Shell output streams live; press **Esc** to interrupt a running
|
package/dist/agent/loop.js
CHANGED
|
@@ -63,9 +63,16 @@ export function needsConfirm(kind, mode) {
|
|
|
63
63
|
return kind === "exec";
|
|
64
64
|
return true; // suggest: confirm edits and exec
|
|
65
65
|
}
|
|
66
|
+
export function replyLanguageInstruction(env = process.env) {
|
|
67
|
+
const requested = String(env.HARA_REPLY_LANGUAGE ?? "").trim();
|
|
68
|
+
if (/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8}){0,3}$/u.test(requested)) {
|
|
69
|
+
return `Reply in ${requested} unless the user explicitly asks for another language;`;
|
|
70
|
+
}
|
|
71
|
+
return "Reply in the same language as the user's latest message unless they explicitly ask for another language;";
|
|
72
|
+
}
|
|
66
73
|
const HARA_SYSTEM = () => `You are hara, a coding agent running in the user's terminal.
|
|
67
|
-
Be concise and direct.
|
|
68
|
-
|
|
74
|
+
Be concise and direct. ${replyLanguageInstruction()} keep code, commands, paths, and technical identifiers
|
|
75
|
+
unchanged. Use the
|
|
69
76
|
provided tools to read files, edit/write files, and run shell
|
|
70
77
|
commands. Prefer small, verifiable steps; edit existing files with edit_file rather than rewriting
|
|
71
78
|
them whole. Batch INDEPENDENT tool calls in a single response — especially reads (read_file / grep /
|
|
@@ -76,6 +83,12 @@ grep/glob; don't read whole large files when a targeted search answers the quest
|
|
|
76
83
|
grep to locate then read_file just that region with offset/limit — not the whole file. After a successful
|
|
77
84
|
edit_file/write_file do NOT re-read the file to verify — the tool already applied and diffed the change;
|
|
78
85
|
re-reading a big file after every edit is the slowest habit an agent can have.
|
|
86
|
+
When editing an existing user artifact, including a DOCX, keep its original path as the canonical output
|
|
87
|
+
and replace it in place by default. Do not invent suffix copies such as "完整版", "简版", or "new" unless
|
|
88
|
+
the user explicitly asks to save a separate version. For one-shot Python library work such as python-docx,
|
|
89
|
+
call the python tool with source directly; never write a durable helper .py file and then run it. If an
|
|
90
|
+
atomic binary save needs a temporary output, remove it in finally/on failure and leave only the requested
|
|
91
|
+
document when the task completes.
|
|
79
92
|
When an attempt FAILS, never repeat it unchanged — read the error, form a hypothesis about the cause, and
|
|
80
93
|
change something (arguments / approach / tool) before trying again. After two failed variants of the same
|
|
81
94
|
approach, stop: re-plan from what you learned, or ask the user, stating concisely what you tried and what
|
package/dist/config.js
CHANGED
|
@@ -76,7 +76,7 @@ export function providerCatalog() {
|
|
|
76
76
|
...(PROVIDER_DEFAULTS[id].baseURL ? { defaultBaseURL: PROVIDER_DEFAULTS[id].baseURL } : {}),
|
|
77
77
|
}));
|
|
78
78
|
}
|
|
79
|
-
export const CONFIG_KEYS = ["provider", "apiKey", "model", "baseURL", "approval", "sandbox", "theme", "evolve", "assetCapture", "computerUse", "computerApps", "visionModel", "visionBaseURL", "visionApiKey", "embedProvider", "embedModel", "embedBaseURL", "embedApiKey", "routeModel", "routeBaseURL", "routeApiKey", "guardian", "notify", "runTimeoutMs", "maxAgentRounds", "vimMode", "autoCompact", "fileCheckpoints", "updateCheck", "proxy", "fallbackModel", "fallbackProvider", "fallbackBaseURL", "fallbackApiKey", "reasoningEffort"];
|
|
79
|
+
export const CONFIG_KEYS = ["provider", "apiKey", "model", "baseURL", "approval", "sandbox", "theme", "evolve", "assetCapture", "computerUse", "computerApps", "visionModel", "visionBaseURL", "visionApiKey", "embedProvider", "embedModel", "embedBaseURL", "embedApiKey", "routeModel", "routeBaseURL", "routeApiKey", "guardian", "notify", "runTimeoutMs", "maxAgentRounds", "vimMode", "autoCompact", "fileCheckpoints", "updateCheck", "proxy", "packageRegistry", "fallbackModel", "fallbackProvider", "fallbackBaseURL", "fallbackApiKey", "reasoningEffort"];
|
|
80
80
|
export const REASONING_EFFORTS = ["off", "low", "medium", "high", "max"];
|
|
81
81
|
export const APPROVAL_MODES = ["suggest", "auto-edit", "full-auto"];
|
|
82
82
|
export const SANDBOX_MODES = ["off", "workspace-write", "read-only"];
|
|
@@ -493,6 +493,8 @@ export function loadConfig(opts = {}) {
|
|
|
493
493
|
const fileCheckpoints = !(process.env.HARA_CHECKPOINTS === "0" || merged.fileCheckpoints === false || merged.fileCheckpoints === "false"); // default ON
|
|
494
494
|
const updateCheck = !(process.env.HARA_UPDATE_CHECK === "0" || merged.updateCheck === false || merged.updateCheck === "false"); // default ON
|
|
495
495
|
const proxy = typeof merged.proxy === "string" && merged.proxy.trim() ? merged.proxy.trim() : undefined;
|
|
496
|
+
const packageRegistry = nonBlankEnv(process.env.HARA_PACKAGE_REGISTRY)
|
|
497
|
+
?? (typeof merged.packageRegistry === "string" && merged.packageRegistry.trim() ? merged.packageRegistry.trim() : undefined);
|
|
496
498
|
const fallbackModel = nonBlankEnv(process.env.HARA_FALLBACK_MODEL) ?? merged.fallbackModel;
|
|
497
499
|
const requestedFallbackProvider = nonBlankEnv(process.env.HARA_FALLBACK_PROVIDER) ?? merged.fallbackProvider;
|
|
498
500
|
const fallbackProvider = isProviderId(requestedFallbackProvider) ? requestedFallbackProvider : undefined;
|
|
@@ -502,7 +504,7 @@ export function loadConfig(opts = {}) {
|
|
|
502
504
|
const reasoningEffort = reasoningRaw && ["off", "low", "medium", "high", "max"].includes(reasoningRaw)
|
|
503
505
|
? reasoningRaw
|
|
504
506
|
: undefined;
|
|
505
|
-
return { provider, apiKey, model, baseURL, approval, sandbox, theme, evolve, assetCapture, computerUse, computerApps, visionModel, visionBaseURL, visionApiKey, modelVision, embedProvider, embedModel, embedBaseURL, embedApiKey, routeModel, routeBaseURL, routeApiKey, guardian, hooks, notify, runTimeoutMs, maxAgentRounds, vimMode, autoCompact, fileCheckpoints, updateCheck, proxy, fallbackModel, fallbackProvider, fallbackBaseURL, fallbackApiKey, reasoningEffort, mcpServers, cwd: effectiveCwd };
|
|
507
|
+
return { provider, apiKey, model, baseURL, approval, sandbox, theme, evolve, assetCapture, computerUse, computerApps, visionModel, visionBaseURL, visionApiKey, modelVision, embedProvider, embedModel, embedBaseURL, embedApiKey, routeModel, routeBaseURL, routeApiKey, guardian, hooks, notify, runTimeoutMs, maxAgentRounds, vimMode, autoCompact, fileCheckpoints, updateCheck, proxy, packageRegistry, fallbackModel, fallbackProvider, fallbackBaseURL, fallbackApiKey, reasoningEffort, mcpServers, cwd: effectiveCwd };
|
|
506
508
|
}
|
|
507
509
|
export function providerEnvKey(provider) {
|
|
508
510
|
return (PROVIDER_DEFAULTS[provider] ?? PROVIDER_DEFAULTS.anthropic).envKey;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Project-context loading (AGENTS.md) — the cross-tool standard read by Codex/Claude Code/OpenClaw.
|
|
2
2
|
// Walks up from cwd to the project root, concatenates AGENTS.md files, caps total size.
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join, dirname, resolve } from "node:path";
|
|
5
5
|
import { readModelContextBytePrefixSync } from "../fs-read.js";
|
|
6
6
|
import { homeWorkspaceGuidance, isHomeWorkspace } from "./workspace-scope.js";
|
|
@@ -110,6 +110,23 @@ export function hasAgentsMd(cwd) {
|
|
|
110
110
|
const root = findProjectRoot(cwd);
|
|
111
111
|
return FILENAMES.some((n) => existsSync(join(root, n)));
|
|
112
112
|
}
|
|
113
|
+
/** An empty directory has nothing useful for the init agent to analyze. Do not interrupt first launch with
|
|
114
|
+
* an AGENTS.md offer until the user has created a project marker or at least one visible project file. */
|
|
115
|
+
export function hasProjectContent(cwd) {
|
|
116
|
+
const root = findProjectRoot(cwd);
|
|
117
|
+
try {
|
|
118
|
+
return readdirSync(root, { withFileTypes: true }).some((entry) => {
|
|
119
|
+
if (ROOT_MARKERS.includes(entry.name))
|
|
120
|
+
return true;
|
|
121
|
+
if (entry.name === ".DS_Store" || entry.name === ".gitkeep")
|
|
122
|
+
return false;
|
|
123
|
+
return !entry.name.startsWith(".");
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
113
130
|
/** Prompt hara runs against itself to analyze the repo and write AGENTS.md. */
|
|
114
131
|
export const INIT_PROMPT = "Explore this repository to understand it, then write a concise AGENTS.md at the project root.\n" +
|
|
115
132
|
"Use read_file, bash (e.g. `ls`, `git ls-files`, `cat`), and write_file.\n" +
|
package/dist/exec/jobs.js
CHANGED
|
@@ -50,7 +50,7 @@ function ensureExitCleanup() {
|
|
|
50
50
|
process.on("exit", killAllJobs); // sync; runs on normal quit so background jobs don't orphan
|
|
51
51
|
}
|
|
52
52
|
/** Start a background shell job; returns its id immediately. Output is captured to a capped tail buffer. */
|
|
53
|
-
export function startJob(command, cwd, mode) {
|
|
53
|
+
export function startJob(command, cwd, mode, env = {}) {
|
|
54
54
|
ensureExitCleanup();
|
|
55
55
|
const running = [...jobs.values()].filter((job) => job.status === "running" || job.terminationPending).length;
|
|
56
56
|
if (running >= MAX_RUNNING_JOBS) {
|
|
@@ -59,7 +59,7 @@ export function startJob(command, cwd, mode) {
|
|
|
59
59
|
pruneFinishedJobs(1);
|
|
60
60
|
const { cmd, args } = shellCommand(command, cwd, mode);
|
|
61
61
|
const processGroup = platform() !== "win32";
|
|
62
|
-
const child = spawn(cmd, args, { cwd, env: toolSubprocessEnv(), detached: processGroup });
|
|
62
|
+
const child = spawn(cmd, args, { cwd, env: toolSubprocessEnv(process.env, env), detached: processGroup });
|
|
63
63
|
const job = {
|
|
64
64
|
id: "j" + ++seq,
|
|
65
65
|
// The raw command may contain an inline --token/KEY=value. It is used only for spawning above; every
|
package/dist/fs-walk.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Shared filesystem walker — powers @file completion and the grep/glob tools.
|
|
2
2
|
// Walks a directory tree, skipping noise dirs, capped so huge trees stay responsive.
|
|
3
|
-
import { readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { lstatSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import { opendir } from "node:fs/promises";
|
|
5
|
-
import { join, relative, sep } from "node:path";
|
|
5
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { execFile, execSync } from "node:child_process";
|
|
7
7
|
import { fuzzyRank } from "./fuzzy.js";
|
|
8
8
|
import { isSensitiveFilePath } from "./security/sensitive-files.js";
|
|
@@ -22,6 +22,37 @@ const DEFAULT_ASYNC_TIMEOUT_MS = 2_000;
|
|
|
22
22
|
// interruptible. Keep them on a much tighter wall budget; agent/tool paths use the async APIs below.
|
|
23
23
|
const DEFAULT_SYNC_TIMEOUT_MS = 250;
|
|
24
24
|
const DEFAULT_YIELD_EVERY = 128;
|
|
25
|
+
function gitMarkerOnPath(start) {
|
|
26
|
+
let dir = resolve(start);
|
|
27
|
+
for (let depth = 0; depth < 128; depth++) {
|
|
28
|
+
try {
|
|
29
|
+
lstatSync(join(dir, ".git"));
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// An unreadable or otherwise suspicious marker must still take the Git path. Only a definite
|
|
34
|
+
// absence permits the filesystem fallback to skip spawning Git.
|
|
35
|
+
if (error?.code !== "ENOENT")
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const parent = dirname(dir);
|
|
39
|
+
if (parent === dir)
|
|
40
|
+
return false;
|
|
41
|
+
dir = parent;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
function gitMarkerAbove(start) {
|
|
46
|
+
if (gitMarkerOnPath(start))
|
|
47
|
+
return true;
|
|
48
|
+
// Preserve worktree semantics when callers reach a repository through a symlinked project path.
|
|
49
|
+
try {
|
|
50
|
+
return gitMarkerOnPath(realpathSync.native(start));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
25
56
|
function finiteLimit(value, fallback) {
|
|
26
57
|
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback;
|
|
27
58
|
}
|
|
@@ -228,6 +259,12 @@ export function listProjectFiles(root, cap = 8000) {
|
|
|
228
259
|
if (recursiveRootContainsHome(root))
|
|
229
260
|
return [];
|
|
230
261
|
const startedAt = Date.now();
|
|
262
|
+
// On a non-repository directory, a contended `git ls-files` can consume the entire synchronous
|
|
263
|
+
// completion budget before the real filesystem walk starts. A bounded marker scan is sufficient to
|
|
264
|
+
// prove that Git cannot be authoritative here, leaving the budget for the user's actual files.
|
|
265
|
+
if (!gitMarkerAbove(root)) {
|
|
266
|
+
return walkFiles(root, cap, { timeoutMs: Math.max(0, DEFAULT_SYNC_TIMEOUT_MS - (Date.now() - startedAt)) });
|
|
267
|
+
}
|
|
231
268
|
try {
|
|
232
269
|
const out = execSync("git ls-files --cached --others --exclude-standard", {
|
|
233
270
|
cwd: root,
|
|
@@ -288,6 +325,19 @@ export async function listProjectFilesAsync(root, options = {}) {
|
|
|
288
325
|
if (cfg.maxEntries === 0)
|
|
289
326
|
return result([], 0, 0, "entry_limit");
|
|
290
327
|
const remaining = () => cfg.timeoutMs - (Date.now() - startedAt);
|
|
328
|
+
if (!gitMarkerAbove(root)) {
|
|
329
|
+
const timeoutMs = remaining();
|
|
330
|
+
if (timeoutMs <= 0)
|
|
331
|
+
return result([], 0, 0, "time_limit");
|
|
332
|
+
return walkFilesAsync(root, {
|
|
333
|
+
maxFiles: cfg.maxFiles,
|
|
334
|
+
maxDirectories: cfg.maxDirectories,
|
|
335
|
+
maxEntries: cfg.maxEntries,
|
|
336
|
+
timeoutMs,
|
|
337
|
+
signal: cfg.signal,
|
|
338
|
+
yieldEvery: cfg.yieldEvery,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
291
341
|
try {
|
|
292
342
|
if (remaining() <= 0)
|
|
293
343
|
return result([], 0, 0, "time_limit");
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ import { listJobs, tailJob, killJob } from "./exec/jobs.js";
|
|
|
56
56
|
import { readModelContextFileSync } from "./fs-read.js";
|
|
57
57
|
import { MIN_NODE_VERSION, unsupportedNodeMessage } from "./runtime.js";
|
|
58
58
|
import { redactKnownSecrets } from "./security/secrets.js";
|
|
59
|
+
import { normalizePackageRegistry } from "./package-registry.js";
|
|
59
60
|
/** Render the background-job list for /jobs (user-facing view of what the agent has running in the
|
|
60
61
|
* background — dev servers, watchers, long tasks). Mirrors codex/Claude-Code process visibility. */
|
|
61
62
|
function renderBgJobs() {
|
|
@@ -71,7 +72,7 @@ function renderBgJobs() {
|
|
|
71
72
|
return `Background jobs — /jobs tail <id> · /jobs kill <id>:\n${rows.join("\n")}`;
|
|
72
73
|
}
|
|
73
74
|
import { qwenDeviceLogin, loadQwenToken } from "./providers/qwen-oauth.js";
|
|
74
|
-
import { loadAgentContext, hasAgentsMd, INIT_PROMPT, findProjectRoot } from "./context/agents-md.js";
|
|
75
|
+
import { loadAgentContext, hasAgentsMd, hasProjectContent, INIT_PROMPT, findProjectRoot } from "./context/agents-md.js";
|
|
75
76
|
import { homeWorkspaceActionError, discoverProjectWorkspaces, isUnsafeProjectWorkspace, resolveWorkspaceSwitch, suggestedProjectWorkspace, } from "./context/workspace-scope.js";
|
|
76
77
|
import { getEmbedder } from "./search/embed.js";
|
|
77
78
|
import { collectRepoChunksAsync, collectDirChunksAsync, buildIndex, indexPath, indexExists } from "./search/semindex.js";
|
|
@@ -96,7 +97,7 @@ import { scaffoldAssets, assetsDir, assetSearchRoots } from "./recall.js";
|
|
|
96
97
|
import { c, out, statusLine } from "./ui.js";
|
|
97
98
|
import * as bar from "./statusbar.js";
|
|
98
99
|
import { nearest } from "./fuzzy.js";
|
|
99
|
-
import "./tools/builtin.js"; // register read_file/write_file/bash
|
|
100
|
+
import "./tools/builtin.js"; // register read_file/write_file/python/bash/job
|
|
100
101
|
import "./tools/runtime.js"; // register tool_search/tool_result_read
|
|
101
102
|
import "./tools/edit.js"; // register edit_file
|
|
102
103
|
import "./tools/search.js"; // register grep/glob/ls
|
|
@@ -104,7 +105,7 @@ import "./tools/patch.js"; // register apply_patch
|
|
|
104
105
|
import "./tools/web.js"; // register web_fetch
|
|
105
106
|
import "./tools/agent.js"; // register agent (subagent spawn)
|
|
106
107
|
import "./tools/memory.js"; // register memory_search/get/write/forget/skill_create
|
|
107
|
-
import "./tools/session-search.js"; // register
|
|
108
|
+
import { automaticSessionRecall } from "./tools/session-search.js"; // register + deterministic explicit-cue recall
|
|
108
109
|
import "./tools/skill.js"; // register the skill loader tool
|
|
109
110
|
import "./tools/codebase.js"; // register codebase_search (repo as a knowledge base)
|
|
110
111
|
import "./tools/todo.js"; // register todo_write (inline task checklist)
|
|
@@ -1396,6 +1397,9 @@ program
|
|
|
1396
1397
|
.option("--profile <id>", "use this identity profile for this run (personal / org id) — see `hara profile list`")
|
|
1397
1398
|
.option("--overlay <name>", "apply a named config overlay from ~/.hara/config.json (legacy: --profile)")
|
|
1398
1399
|
.option("--cwd <dir>", "run from this explicit project directory (alternative to cd)")
|
|
1400
|
+
.option("--proxy <url>", "HTTP(S) proxy for web_fetch/web_search in this run (credentials belong in config/env)")
|
|
1401
|
+
.option("--registry <url>", "package registry for installs in this run: npmjs, npmmirror, or an HTTP(S) URL")
|
|
1402
|
+
.option("--lang <tag>", "reply language for this run, for example zh-CN or en (default: follow latest message)")
|
|
1399
1403
|
.option("-c, --continue", "resume the most recent session in this directory")
|
|
1400
1404
|
.option("--resume <id>", "resume a specific session by id")
|
|
1401
1405
|
.option("--sandbox <mode>", "sandbox the shell: off | workspace-write | read-only");
|
|
@@ -1419,6 +1423,46 @@ program.hook("preAction", (thisCmd) => {
|
|
|
1419
1423
|
process.exit(2);
|
|
1420
1424
|
}
|
|
1421
1425
|
}
|
|
1426
|
+
const proxyFlag = thisCmd.opts().proxy;
|
|
1427
|
+
if (proxyFlag) {
|
|
1428
|
+
try {
|
|
1429
|
+
const parsed = new URL(proxyFlag);
|
|
1430
|
+
if (!(parsed.protocol === "http:" || parsed.protocol === "https:")
|
|
1431
|
+
|| parsed.pathname !== "/"
|
|
1432
|
+
|| parsed.search
|
|
1433
|
+
|| parsed.hash
|
|
1434
|
+
|| parsed.username
|
|
1435
|
+
|| parsed.password)
|
|
1436
|
+
throw new Error("invalid or credential-bearing proxy URL");
|
|
1437
|
+
process.env.HARA_WEB_PROXY = parsed.origin;
|
|
1438
|
+
}
|
|
1439
|
+
catch {
|
|
1440
|
+
out(c.red("Cannot use --proxy: provide an HTTP(S) origin such as http://127.0.0.1:7890. Put authenticated proxy URLs in `hara config set proxy …` or HTTPS_PROXY so credentials do not enter the process list.\n"));
|
|
1441
|
+
process.exit(2);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
const languageFlag = thisCmd.opts().lang;
|
|
1445
|
+
if (languageFlag) {
|
|
1446
|
+
if (languageFlag === "auto")
|
|
1447
|
+
delete process.env.HARA_REPLY_LANGUAGE;
|
|
1448
|
+
else if (/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8}){0,3}$/u.test(languageFlag)) {
|
|
1449
|
+
process.env.HARA_REPLY_LANGUAGE = languageFlag;
|
|
1450
|
+
}
|
|
1451
|
+
else {
|
|
1452
|
+
out(c.red("Cannot use --lang: provide a language tag such as zh-CN or en, or use auto.\n"));
|
|
1453
|
+
process.exit(2);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const registryFlag = thisCmd.opts().registry;
|
|
1457
|
+
if (registryFlag) {
|
|
1458
|
+
try {
|
|
1459
|
+
process.env.HARA_PACKAGE_REGISTRY = normalizePackageRegistry(registryFlag);
|
|
1460
|
+
}
|
|
1461
|
+
catch {
|
|
1462
|
+
out(c.red("Cannot use --registry: provide npmjs, npmmirror, or an HTTP(S) registry URL without credentials/query/fragment.\n"));
|
|
1463
|
+
process.exit(2);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1422
1466
|
const flag = thisCmd.opts().profile;
|
|
1423
1467
|
if (!flag)
|
|
1424
1468
|
return;
|
|
@@ -3173,6 +3217,17 @@ config
|
|
|
3173
3217
|
process.exit(1);
|
|
3174
3218
|
}
|
|
3175
3219
|
}
|
|
3220
|
+
if (key === "packageRegistry") {
|
|
3221
|
+
try {
|
|
3222
|
+
value = normalizePackageRegistry(value) ?? "";
|
|
3223
|
+
if (!value)
|
|
3224
|
+
throw new Error("empty registry");
|
|
3225
|
+
}
|
|
3226
|
+
catch {
|
|
3227
|
+
out(c.red("Invalid package registry. Use npmjs, npmmirror, or an HTTP(S) URL without credentials, query parameters, or a fragment.\n"));
|
|
3228
|
+
process.exit(1);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3176
3231
|
if (key === "runTimeoutMs") {
|
|
3177
3232
|
const parsed = parseAgentRunTimeoutMs(value);
|
|
3178
3233
|
if (parsed === undefined || parsed < MIN_AGENT_RUN_TIMEOUT_MS || parsed > MAX_AGENT_RUN_TIMEOUT_MS) {
|
|
@@ -3667,7 +3722,13 @@ program.action(async (opts) => {
|
|
|
3667
3722
|
// Inbound images (gateway): the platform downloaded the user's photo(s) and passed their paths via env.
|
|
3668
3723
|
// Let the agent actually SEE them — attached inline for a vision-capable main model, else described via the
|
|
3669
3724
|
// visionModel sidecar and folded into the message (text-only models can't take image blocks).
|
|
3670
|
-
const
|
|
3725
|
+
const printText = String(opts.print);
|
|
3726
|
+
const automaticRecall = meta
|
|
3727
|
+
? await automaticSessionRecall(printText, { cwd, sessionId: meta.id })
|
|
3728
|
+
: "";
|
|
3729
|
+
const userText = (automaticRecall ? `${automaticRecall}\n\n---\n\n` : "")
|
|
3730
|
+
+ await expandMentionsAsync(printText, cwd)
|
|
3731
|
+
+ (schemaObj ? STRUCTURED_INSTRUCTION : "");
|
|
3671
3732
|
const inboundImgs = (process.env.HARA_GATEWAY_IMAGES ?? "")
|
|
3672
3733
|
.split("\n")
|
|
3673
3734
|
.map((s) => s.trim())
|
|
@@ -3910,7 +3971,7 @@ program.action(async (opts) => {
|
|
|
3910
3971
|
// First-run AGENTS.md offer — classic REPL only. In TUI mode we must NOT call rl.question before ink
|
|
3911
3972
|
// mounts: a readline question puts stdin in a state ink can't read from, leaving the input box dead
|
|
3912
3973
|
// (the TUI shows a `/init` tip instead, below). See the `tip` in the runTui header.
|
|
3913
|
-
if (!homeWorkspace && !hasAgentsMd(cwd) && !useTui) {
|
|
3974
|
+
if (!homeWorkspace && !hasAgentsMd(cwd) && hasProjectContent(cwd) && !useTui) {
|
|
3914
3975
|
const ans = (await rl.question(`${c.dim("No AGENTS.md here — analyze this project and create one?")} ${c.dim("[Y/n]")} `)).trim().toLowerCase();
|
|
3915
3976
|
if (ans === "" || ans.startsWith("y")) {
|
|
3916
3977
|
out(c.dim("Analyzing project…\n"));
|
|
@@ -4565,7 +4626,7 @@ program.action(async (opts) => {
|
|
|
4565
4626
|
// First-run AGENTS.md offer — via a tiny ink prompt, NOT readline. A readline question before the
|
|
4566
4627
|
// main TUI leaves stdin unreadable by ink (dead input box); ink cleans up on unmount, so the TUI
|
|
4567
4628
|
// mounted right after gets working input. Runs before mount, like the classic path.
|
|
4568
|
-
if (!homeWorkspace && !hasAgentsMd(cwd)) {
|
|
4629
|
+
if (!homeWorkspace && !hasAgentsMd(cwd) && hasProjectContent(cwd)) {
|
|
4569
4630
|
if (await askConfirm("No AGENTS.md here — analyze this project and create one?")) {
|
|
4570
4631
|
out(c.dim("Analyzing project…\n"));
|
|
4571
4632
|
try {
|
|
@@ -5183,7 +5244,9 @@ program.action(async (opts) => {
|
|
|
5183
5244
|
const planImg = await resolveImages(images, h);
|
|
5184
5245
|
if (planImg.skip)
|
|
5185
5246
|
return;
|
|
5186
|
-
const
|
|
5247
|
+
const automaticRecall = await automaticSessionRecall(line, { cwd, sessionId: meta.id, signal: h.signal });
|
|
5248
|
+
const recallPrefix = [recalledContext, automaticRecall].filter(Boolean).join("\n\n");
|
|
5249
|
+
const planContent = (recallPrefix ? `${recallPrefix}\n\n---\n\n` : "") + await expandMentionsAsync(line, cwd, { signal: h.signal }) + (planImg.extraText ?? "");
|
|
5187
5250
|
const executionContext = beginExecution(line);
|
|
5188
5251
|
if (!executionContext)
|
|
5189
5252
|
return;
|
|
@@ -5299,7 +5362,9 @@ program.action(async (opts) => {
|
|
|
5299
5362
|
const ri = await resolveImages(images, h);
|
|
5300
5363
|
if (ri.skip)
|
|
5301
5364
|
return;
|
|
5302
|
-
const
|
|
5365
|
+
const automaticRecall = await automaticSessionRecall(line, { cwd, sessionId: meta.id, signal: h.signal });
|
|
5366
|
+
const recallPrefix = [recalledContext, automaticRecall].filter(Boolean).join("\n\n");
|
|
5367
|
+
const userContent = (recallPrefix ? `${recallPrefix}\n\n---\n\n` : "") + await expandMentionsAsync(line, cwd, { signal: h.signal }) + (ri.extraText ?? "");
|
|
5303
5368
|
recalledContext = "";
|
|
5304
5369
|
const executionContext = beginExecution(line);
|
|
5305
5370
|
if (!executionContext)
|
|
@@ -5440,7 +5505,9 @@ program.action(async (opts) => {
|
|
|
5440
5505
|
continue;
|
|
5441
5506
|
}
|
|
5442
5507
|
line = inlineLeadingPath(line, existsSync); // leading dropped file path → @-mention so it's read in
|
|
5443
|
-
const
|
|
5508
|
+
const automaticRecall = await automaticSessionRecall(line, { cwd, sessionId: meta.id });
|
|
5509
|
+
const recallPrefix = [recalledContext, automaticRecall].filter(Boolean).join("\n\n");
|
|
5510
|
+
const userContent = (recallPrefix ? `${recallPrefix}\n\n---\n\n` : "") + await expandMentionsAsync(line, cwd);
|
|
5444
5511
|
recalledContext = "";
|
|
5445
5512
|
const recoveredClassicSteering = consumePendingTaskSteering(task);
|
|
5446
5513
|
if (recoveredClassicSteering) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Explicit package-registry routing for npm/pnpm/yarn/bun installs. Registry switching is user-selected:
|
|
2
|
+
* silently replaying an install against a public mirror can break private scopes and changes the software
|
|
3
|
+
* supply-chain trust boundary. */
|
|
4
|
+
export function normalizePackageRegistry(value) {
|
|
5
|
+
const raw = String(value ?? "").trim();
|
|
6
|
+
if (!raw)
|
|
7
|
+
return undefined;
|
|
8
|
+
const expanded = raw === "npmjs"
|
|
9
|
+
? "https://registry.npmjs.org/"
|
|
10
|
+
: raw === "npmmirror"
|
|
11
|
+
? "https://registry.npmmirror.com/"
|
|
12
|
+
: raw;
|
|
13
|
+
let url;
|
|
14
|
+
try {
|
|
15
|
+
url = new URL(expanded);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error("package registry must be npmjs, npmmirror, or an absolute HTTP(S) URL");
|
|
19
|
+
}
|
|
20
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:")
|
|
21
|
+
|| !url.hostname
|
|
22
|
+
|| url.username
|
|
23
|
+
|| url.password
|
|
24
|
+
|| url.search
|
|
25
|
+
|| url.hash) {
|
|
26
|
+
throw new Error("package registry must be an HTTP(S) URL without credentials, query parameters, or a fragment");
|
|
27
|
+
}
|
|
28
|
+
if (!url.pathname.endsWith("/"))
|
|
29
|
+
url.pathname += "/";
|
|
30
|
+
return url.href;
|
|
31
|
+
}
|
|
32
|
+
export function commandHasPackageRegistry(command) {
|
|
33
|
+
return /(?:^|\s)--registry(?:=|\s|$)/iu.test(command);
|
|
34
|
+
}
|
|
35
|
+
/** The common npm config variable is honored by npm and pnpm; explicit Yarn/Bun variables cover their
|
|
36
|
+
* modern clients too. Values are injected as environment, never interpolated into a shell command. */
|
|
37
|
+
export function packageRegistryEnv(registry) {
|
|
38
|
+
return {
|
|
39
|
+
NPM_CONFIG_REGISTRY: registry,
|
|
40
|
+
YARN_NPM_REGISTRY_SERVER: registry,
|
|
41
|
+
BUN_CONFIG_REGISTRY: registry,
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/tools/all.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// imports itself; library entries (serve/server.ts, future embedders) import THIS so the registry is
|
|
3
3
|
// never empty when runAgent plans a turn — an unregistered tool is silently unplannable, which shows up
|
|
4
4
|
// as "the model called write_file and nothing happened".
|
|
5
|
-
import "./builtin.js"; // read_file / write_file / bash / job
|
|
5
|
+
import "./builtin.js"; // read_file / write_file / python / bash / job
|
|
6
6
|
import "./runtime.js"; // tool_search / tool_result_read
|
|
7
7
|
import "./edit.js"; // edit_file
|
|
8
8
|
import "./search.js"; // grep / glob / ls
|