@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 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.132.3 — 2026-07-22 — immediate prompt-key routing and verified release carrier
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. This patch carries the complete 0.132 feature set into the native
16
- and Desktop release train. Upgrade with `npm i -g @nanhara/hara@0.132.3`.
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), `bash`, and
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
@@ -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. Reply in the same language as the user's latest message unless they explicitly
68
- ask for another language; keep code, commands, paths, and technical identifiers unchanged. Use the
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 bounded cross-session transcript recall
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 userText = await expandMentionsAsync(String(opts.print), cwd) + (schemaObj ? STRUCTURED_INSTRUCTION : "");
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 planContent = (recalledContext ? `${recalledContext}\n\n---\n\n` : "") + await expandMentionsAsync(line, cwd, { signal: h.signal }) + (planImg.extraText ?? "");
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 userContent = (recalledContext ? `${recalledContext}\n\n---\n\n` : "") + await expandMentionsAsync(line, cwd, { signal: h.signal }) + (ri.extraText ?? "");
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 userContent = (recalledContext ? `${recalledContext}\n\n---\n\n` : "") + await expandMentionsAsync(line, cwd);
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