@nanhara/hara 0.33.0 → 0.53.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,7 +5,222 @@ 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.33.0 — unreleased (semantic recall + memory)
8
+ ## 0.53.0 — unreleased (task-done notifications + steering in plan mode)
9
+
10
+ - **Notifications** — get pinged when a turn finishes so you can walk away during a long run
11
+ (codex/Claude-Code parity). `hara config set notify bell` rings the terminal BEL; `notify system` fires
12
+ an OS notification (macOS `osascript` / Linux `notify-send`) plus the bell; default `off`. Gated on
13
+ elapsed time (≥8s) so quick turns you were watching stay silent. Wired into the TUI turn, plan-mode
14
+ execute, and the plain REPL; `hara doctor` shows the setting. New `src/notify.ts` (`notifyDone`).
15
+ - **Type-ahead steering now covers plan mode too.** v0.52 wired steering into the regular turn only;
16
+ the `pendingInput` builder is now hoisted so plan-mode *investigation* and *execution* also fold in
17
+ messages you type mid-turn (previously they fell back to the old wait-for-turn-end behavior — an
18
+ inconsistency). All three turn paths now steer.
19
+
20
+ ## 0.52.0 — unreleased (type-ahead steering — mid-turn messages course-correct the live task)
21
+
22
+ - **Type-ahead now *steers* the running turn** instead of waiting for it to finish. Previously a message
23
+ typed while hara worked was held and replayed as a brand-new turn once the turn ended — so a
24
+ supplement ("also handle the error case", "use TS not JS") arrived *after* the task had already
25
+ finished on the old understanding, becoming rework. Now, studying how **codex** does it (its
26
+ `pending_input` drains at the next model-call boundary *inside* the same turn) vs **cc-haha/Claude
27
+ Code** (waits for full completion), hara adopts the codex model: queued messages are **folded into the
28
+ next model call** (drained after each tool round), so the model course-corrects mid-task. Each shows
29
+ inline in the transcript at the point it's folded in. Messages typed during the *final* step (no more
30
+ tool rounds) still start a fresh turn; **Esc** drops the queue and stops.
31
+ - New `RunOpts.pendingInput` (the loop drains it before each model call; unused outside the TUI = zero
32
+ change for `-p`/sub-agents/plain REPL). The TUI hands the queue through `Helpers.drainQueue`.
33
+ - **`toAnthropic` now coalesces consecutive `user` messages** — required since a steered message lands
34
+ right after tool-results (which map to a `user` message) and Anthropic rejects two `user` turns in a
35
+ row. Dormant in normal alternating histories. Unit-tested.
36
+
37
+ ## 0.51.0 — unreleased (lifecycle hooks — PreToolUse / PostToolUse)
38
+
39
+ - **Hooks dispatch** — run your own shell commands around every tool call (codex / Claude-Code parity, which
40
+ hara lacked). A **`PreToolUse`** hook runs *before* a tool and can **veto** it (non-zero exit blocks the
41
+ call; its stdout/stderr becomes the denial the model sees) — e.g. forbid `bash rm -rf`, gate edits to a
42
+ path, require a clean tree. A **`PostToolUse`** hook runs *after* (observe-only) — e.g. `prettier` a file
43
+ the agent just wrote, log/notify. The command gets `{tool, payload}` as JSON on stdin + `HARA_TOOL_NAME`
44
+ in its env; each is matched by a `matcher` (regex/literal on the tool name, `*`/omitted = all) with a 30s
45
+ timeout. Configure in `config.json` `"hooks"`; **plugins can contribute hooks** too. `hara doctor` shows
46
+ the active count. No hooks configured = zero overhead (fast no-op).
47
+
48
+ ```jsonc
49
+ // ~/.hara/config.json
50
+ "hooks": {
51
+ "PreToolUse": [{ "matcher": "bash", "command": "grep -q 'rm -rf' && { echo 'no rm -rf'; exit 1; } || exit 0" }],
52
+ "PostToolUse": [{ "matcher": "edit_file|write_file", "command": "prettier --write \"$(jq -r .payload.input.path)\" 2>/dev/null; exit 0" }]
53
+ }
54
+ ```
55
+
56
+ ## 0.50.0 — unreleased (web_search — find pages, not just fetch)
57
+
58
+ - New **`web_search`** tool — search the web (title/URL/snippet), then `web_fetch` a result to read it. Closes
59
+ the other codex/cc-haha gap (hara could previously only fetch a *known* URL). **Reliable with a Tavily key**
60
+ (`HARA_SEARCH_API_KEY` / `TAVILY_API_KEY`, free tier); a **keyless DuckDuckGo** fallback works best-effort
61
+ (POST endpoint; may rate-limit). Read-kind, available to sub-agents. Verified live (keyless: "anthropic
62
+ claude" → real results); parser unit-tested (incl. the DDG `uddg` redirect decode).
63
+
64
+ ## 0.49.0 — unreleased (inline todo tool — `todo_write`)
65
+
66
+ - New **`todo_write`** tool — the agent maintains a live task checklist during multi-step work (codex's
67
+ `update_plan` / Claude Code's `TodoWrite`, which hara lacked). Plan up front, keep one item `in_progress`,
68
+ flip to `done` as you go; pass the full list each call. Read-kind (never prompts); the system prompt nudges
69
+ its use for multi-step tasks; sub-agents can use it too. Renders a `☐/▶/☑` checklist with a done count.
70
+ *(Gap analysis vs codex + cc-haha: this was the top missing capability.)*
71
+
72
+ ## 0.48.0 — unreleased (chrome plugin: drive your real logged-in Chrome)
73
+
74
+ - New first-party **`chrome` plugin** — web automation via **`chrome-devtools-mcp`** against a **real Chrome with
75
+ a persistent-login profile** (sign into a site once, reused across runs), or attach to your running Chrome via
76
+ `--browserUrl http://127.0.0.1:9222`. The "drive my actual sessions" complement to the isolated-Playwright
77
+ `browser` plugin (enable one, not both) — this is the openclaw/cc-haha route.
78
+ - Shipped as an option (not auto-installed — `browser` stays the default). `chrome-devtools-mcp` verified
79
+ resolvable; both plugin manifests validated.
80
+
81
+ ## 0.47.0 — unreleased (browser plugin: reliable web automation via Playwright MCP)
82
+
83
+ - New first-party **`browser` plugin** wires the **Playwright MCP** (`@playwright/mcp`) into hara → the agent gets
84
+ reliable web automation: `mcp__browser__navigate / snapshot / click / type / fill_form …` acting on the page's
85
+ **DOM/accessibility tree** (selectors, auto-waiting), NOT screenshots or pixel coordinates. This is the
86
+ reliable counterpart to the fragile desktop `computer` tool — no permission walls, no coordinate-guessing.
87
+ - Ships a `web-automation` skill (snapshot-driven workflow; notes the `chrome-devtools-mcp` alternative for
88
+ driving your real logged-in Chrome, à la openclaw/cc-haha).
89
+ - Install: `hara plugin add file:<repo>/plugins/browser`; `npx playwright install chromium` once. Verified
90
+ `@playwright/mcp@0.0.76` resolves + the plugin loads (`hara doctor` → plugins: browser).
91
+
92
+ ## 0.46.0 — unreleased (screen control: bounded-failure circuit breaker)
93
+
94
+ - The `computer` tool now **stops after 3 consecutive failures** instead of letting the agent loop forever on a
95
+ broken setup (learned from codex, which bounds Computer Use attempts then gives up). After 3 in a row it
96
+ returns a clear stop + the likely cause (missing Accessibility/Screen Recording permission, or the app isn't
97
+ reachable) + how to fix; resets on any success. Each failure shows the running `[n/3]` count.
98
+
99
+ ## 0.45.1 — unreleased (activate via `open -a`; Accessibility gotcha)
100
+
101
+ - `activateApp` uses `open -a <app>` on macOS — `osascript … to activate` often left another window on top.
102
+ - Documented (gotcha #0 in `computer.ts`) that **cliclick needs the Accessibility permission, separate from
103
+ Screen Recording** — without it, clicks/keys silently no-op (the #1 cause of "it does nothing").
104
+
105
+ ## 0.45.0 — unreleased (screen control: activate, IME-safe typing)
106
+
107
+ - **`activate` action** — bring the target app to the foreground before screenshot/click. Fixes clicks landing
108
+ on the terminal hara runs in (the "Ghostty" problem): the agent must `activate WeChat` *first*.
109
+ - **IME-safe typing** — `type` now sets the clipboard and pastes (Cmd/Ctrl+V) instead of injecting keystrokes,
110
+ which a Chinese input method garbles. Reliable for **CJK + emoji** (verified pbcopy round-trip: `你好 hello 😀`);
111
+ falls back to keystrokes for ASCII if the clipboard set fails.
112
+ - The hard-won **RPA gotchas** (foreground trap, IME, Retina coords, grounding fragility, placeholder text like
113
+ "AAAA") are documented at the top of `computer.ts`.
114
+ - TUI: the type-ahead pool shows each queued line **highlighted** (accent color) above the input — no verbose
115
+ header (per feedback).
116
+
117
+ ## 0.44.0 — unreleased (type-ahead pool: visible + coalesced)
118
+
119
+ - The type-ahead queue is now a **visible pool**: messages typed while the agent works are listed above the
120
+ input (`📥 pool (N) — sent together when this turn finishes`), so Enter visibly *enters the pool* instead of
121
+ appearing to vanish (the reported "回车消失了/没显示在对话池").
122
+ - On turn-end the pool is **coalesced into one turn** — your "also do X" / "and Y" additions reach the agent
123
+ together, in order, rather than as separate sequential turns.
124
+ - Esc still clears the pool (stop means stop). 130 tests (+1 coalesce; existing type-ahead tests updated).
125
+
126
+ ## 0.43.0 — unreleased (grounding for screen control — accurate clicks)
127
+
128
+ - The `computer` tool now **locates UI elements by description** instead of guessing pixels from a text read.
129
+ Pass `target` to `click`/`move` (e.g. "the Send button") — hara screenshots, asks a vision model for the
130
+ element's position (resolution-independent fractions, Retina-safe), and clicks there. New **`find`** action
131
+ returns coordinates without clicking.
132
+ - This is codex's "native computer-use" lesson applied **locally**: codex's `computer_use` is a remote browser
133
+ sandbox; hara grounds against your own screen + apps. Needs a grounding-capable vision model (e.g. a qwen-VL).
134
+ - `screenSize()` per OS converts fractions → click coords; `parseLocate` accepts per-mille/percent/fraction
135
+ replies (tested). cliclick installed → `hara doctor` shows screencapture ✓ + cliclick ✓.
136
+ - **Still requires you to grant macOS Screen Recording + Accessibility** to actually drive the screen — those
137
+ toggles can only be set by you in System Settings.
138
+
139
+ ## 0.42.0 — unreleased (type-ahead: keep typing while the agent works)
140
+
141
+ - You can now **type while the agent is working** — the message enters a **FIFO queue** and is sent
142
+ automatically when the current turn finishes (the input box stays active mid-turn; a "⌨ working — Enter
143
+ queues" hint shows the depth). Fixes the "input does nothing while working" dogfooding feedback.
144
+ - **Esc stops everything** — interrupts the turn AND clears the queue, so a stopped turn never fires queued
145
+ messages. The queue drain is idempotent (guarded against double-send under React StrictMode).
146
+ - Expert-reviewed for queue correctness (FIFO, exactly-once), the Esc/abort UX, and input-handler conflicts.
147
+
148
+ ## 0.41.0 — unreleased (English session names, auto-summarized)
149
+
150
+ - After the first turn a session gets a short **English kebab-case name** summarizing what it's about
151
+ (e.g. `add-semantic-search`) via one tiny model call — replacing the literal first-message title. A non-English
152
+ conversation is translated to an English gist (pinyin only if untranslatable). Names stay short + ASCII.
153
+ - The stable session **id is still the UUID** (unchanged — this only improves the human-friendly name); falls
154
+ back to the lexical title if the naming call fails. New `slugify()` helper (tested).
155
+
156
+ ## 0.40.0 — unreleased (TUI polish: markdown rendering + numbered choices)
157
+
158
+ - The ink TUI now **renders assistant Markdown** (headers, bold, inline code, bullets; code fences kept
159
+ verbatim) instead of showing raw `**`/`##`/backticks. The renderer (`md.ts`) had only been wired into the
160
+ classic REPL; the default TUI showed markdown literally.
161
+ - **Selection prompts are numbered**: each choice shows `1.`, `2.`, … and you can **press the number to pick it
162
+ directly** (in addition to ↑↓ + Enter). The hint reads "↑↓ or 1–N to choose".
163
+
164
+ ## 0.39.0 — unreleased (hara commit — AI commit messages)
165
+
166
+ - **`hara commit`** generates a conventional-commits message from your staged diff, shows it, and commits after
167
+ a `Y/n` confirm. `-a` stages tracked changes first; the global `-y` skips the confirm. Pairs with `hara
168
+ review` (review → commit). Verified live (glm-5): generated `feat(util): add mul function` and committed it.
169
+ - Note: the skip-confirm reuses the global `-y/--yes` (a subcommand `-y` would collide with it — same lesson as
170
+ `hara plan resume`).
171
+
172
+ ## 0.38.0 — unreleased (hara review — review your changes)
173
+
174
+ - **`hara review`** reviews your uncommitted changes (`git diff HEAD`) for correctness bugs, security issues,
175
+ missing error handling, naming, and missing tests — grouped by severity (**Blocker / Should-fix / Nit**) with
176
+ file:line and concrete fixes. **Read-only**: it can read files for context but never edits. `--staged`
177
+ reviews staged changes; `--base <ref>` reviews against a ref (e.g. `main`).
178
+ - Verified live (glm-5): on a planted diff it flagged a hardcoded secret (Blocker), an unguarded divide, and
179
+ dead code, then gave a clear "do not merge" verdict.
180
+ - `codebase_search` added to the read-only tool set (so reviewers / sub-agents can search the repo).
181
+
182
+ ## 0.37.0 — unreleased (task-aware screenshots for screen control)
183
+
184
+ - Screenshots from the `computer` tool are now read with a **screenshot-tuned prompt** aimed at *acting*, not
185
+ transcribing: interactive elements (buttons/fields/menus) with labels and approximate positions, the active
186
+ element, and any errors. A text-only main model driving the desktop gets something it can actually click.
187
+ - New optional **`focus`** on the screenshot action ("the Login button") narrows the read to the current goal.
188
+ - Internal: `describeImages` gains `system`/`hint` options, `SCREENSHOT_SYSTEM` added, `ctx.describeImage`
189
+ takes a hint. (For contrast: codex's `computer_use` is a remote/hosted *browser* MCP plugin with no local
190
+ syscalls — hara stays **native + local** so it can operate your own desktop software.)
191
+
192
+ ## 0.36.0 — unreleased (resumable plans)
193
+
194
+ - **`hara plan resume`** continues the saved plan (`.hara/org/plan.json`): atoms already marked done are
195
+ skipped, pending/failed ones run. When a verify gate stops a plan midway, fix the issue and resume instead
196
+ of starting from scratch. Interrupted atoms (running/failed) reset to pending; works with `--parallel` too.
197
+ - Internal: execution extracted into a shared `executePlan` (skips completed atoms) used by both fresh runs and
198
+ resume; `loadPlan` wired into the CLI. Verified: a half-done plan resumed, skipped the done atom, ran only
199
+ the pending one.
200
+
201
+ ## 0.35.0 — unreleased (parallel plan execution — the org works in parallel)
202
+
203
+ - **`hara plan --parallel`** runs independent atoms concurrently. The planner already builds a dependency DAG;
204
+ now `topoWaves` groups atoms into dependency *waves* (every atom in a wave depends only on earlier waves), and
205
+ each wave's atoms execute at the same time. A diamond plan `a1 → (a2,a3) → a4` runs a2 and a3 together.
206
+ - This is the org differentiator made literal: not one agent stepping through a list, but a team working the
207
+ independent parts at once. Verified live (glm-5): two independent atoms ran in one wave and completed
208
+ out-of-order; both check-gates passed.
209
+ - Sequential remains the default (and is what interactive approval uses, since concurrent atoms can't share a
210
+ prompt). `hara plan` is full-auto, so `--parallel` is safe there. A wave stops the run if any of its atoms fail.
211
+ - Internal: `executeAtom` extracted (shared by both paths); `topoWaves(atoms)` added alongside `topoOrder`.
212
+
213
+ ## 0.34.0 — unreleased (incremental indexing)
214
+
215
+ - **`hara index` is now incremental.** Re-running it re-embeds only the files whose mtime changed since the
216
+ last build; unchanged files keep their existing vectors, and deleted files drop out. A changed embedding
217
+ model still forces a full rebuild. Output reports `(N embedded, M reused)`.
218
+ - Turns indexing from a run-once-and-go-stale command into something you can re-run after every edit. Measured
219
+ on hara's own repo with local `bge-m3`: full build **~68s** → unchanged rebuild **~0.4s** (~150×); editing one
220
+ file re-embeds just that file's chunks.
221
+ - Internal: each chunk records its source file's mtime; `buildIndex` returns `{total, embedded, reused}`.
222
+
223
+ ## 0.33.0 — 2026-06-20 · first public release (semantic recall + memory)
9
224
 
10
225
  - **`recall` and `memory_search` go hybrid too.** The semantic layer added in 0.32 now also powers your
11
226
  code-asset library and durable memory — `hara index --assets` embeds `~/.hara/code-assets`, global skills,
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  🚧 **v0.33** · TypeScript · local-first · Apache-2.0
10
10
 
11
11
  **Highlights**
12
- - **An org, not just an agent** — `hara org "<task>"` routes work to the role that *owns* it; `hara plan "<task>"` decomposes a task into a verified DAG of atoms (frame → atomize → sequence → execute → **verify gate**).
12
+ - **An org, not just an agent** — `hara org "<task>"` routes work to the role that *owns* it; `hara plan "<task>"` decomposes a task into a verified DAG of atoms (frame → atomize → sequence → execute → **verify gate**), and `hara plan --parallel` runs independent atoms concurrently.
13
13
  - **Real terminal UX** — an **ink TUI**: bottom-pinned input box, **plan mode** (read-only → propose a plan → approve → execute), selectable approvals with "don't ask again", windowed reasoning, **paste images** (Ctrl+V) for vision models, light/dark theme.
14
14
  - **Persistent memory + self-evolution** — `memory_*` tools over global/project `MEMORY.md`; the agent recalls before acting, **proactively saves** durable facts, and grows its own playbooks (a lexical guard screens what it writes).
15
15
  - **Multi-provider, all streamed** — Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) with live Markdown + visible reasoning.
@@ -112,6 +112,10 @@ hara doctor # check your setup (auth / model / node / assets / ro
112
112
  hara roles init # scaffold role-agents (implementer / reviewer / docs)
113
113
  hara org "review src/ for bugs" # dispatch a task to the role that owns it (or --role <id>)
114
114
  hara plan "add a /health endpoint with a test" # decompose → sequence (DAG) → run each step + verify
115
+ hara plan --parallel "..." # run independent atoms concurrently · hara plan resume # continue a stopped plan
116
+ hara review # review uncommitted changes for bugs/security/missing tests (--staged · --base main)
117
+ hara commit # AI commit message from staged changes, then commit (-a to stage all · -y to skip confirm)
118
+ hara index # build the semantic search index (after: hara config set embedProvider ollama|qwen)
115
119
  hara -p "summarize @README.md and fix the lint errors in src/" # one-shot; @path attaches a file
116
120
  hara --approval auto-edit # suggest (default) | auto-edit | full-auto (-y = full-auto)
117
121
  hara --sandbox workspace-write # confine shell writes to the project (macOS Seatbelt)
@@ -163,16 +167,20 @@ not just keywords. By default they're lexical (zero setup). Configure an embeddi
163
167
  then `hara index` (repo, for `codebase_search`) / `hara index --assets` (code-assets, skills & memory) / `hara
164
168
  index --all`. A query like "read an image pasted from the clipboard" then surfaces `src/images.ts` even with no
165
169
  shared words. Indexes are rebuildable `.hara/index/` artifacts (self-`.gitignore`d, never committed); no native
166
- vector DB needed, and lexical still works when there's no index.
170
+ vector DB needed, and lexical still works when there's no index. Re-running `hara index` is **incremental** —
171
+ only changed files re-embed (a full repo rebuild that takes ~a minute re-runs in well under a second).
167
172
 
168
173
  **Approval modes**: `suggest` confirms edits & shell · `auto-edit` auto-applies file edits but confirms shell · `full-auto` runs everything.
169
174
  **Sandbox** (macOS): `--sandbox workspace-write|read-only` runs the `bash` tool under Seatbelt (writes confined to the project / blocked).
170
175
  **Screen control** (opt-in): the `computer` tool drives desktop software (screenshot → click/type), native per OS
171
176
  (mac `screencapture`+`cliclick` · Windows PowerShell · Linux `scrot`+`xdotool`). Off by default — enable a tier with
172
177
  `hara config set computerUse read|click|full` and allowlist apps with `hara config set computerApps "App, …"`. Guarded
173
- by the tier, the frontmost-app allowlist, a dangerous-key blocklist, and a once-per-session grant; screenshots are read via your vision model.
178
+ by the tier, the frontmost-app allowlist, a dangerous-key blocklist, and a once-per-session grant. Screenshots are read via your
179
+ vision model into **actionable** output — interactive elements + positions (pass `focus` to target what you're after) — so even a text-only main model can click.
174
180
  **Sessions**: conversations are saved automatically — `-c` / `--resume <id>` to continue, `hara sessions` to list.
175
181
  **MCP**: add an `mcpServers` map to config (global or project `.hara/config.json`); their tools appear to the agent as `mcp__<server>__<tool>`.
182
+ **Notifications**: `hara config set notify bell` (terminal bell) or `notify system` (OS notification) pings you when a turn finishes — handy for long runs you've stepped away from. Gated on elapsed time so quick turns stay quiet; off by default.
183
+ **Hooks**: run your own shell commands around tool calls via a `"hooks"` map in config. A **`PreToolUse`** hook can **veto** a call (non-zero exit blocks it; its output becomes the reason the model sees) — gate `bash`, forbid edits outside a path, require a clean tree. A **`PostToolUse`** hook observes (format/lint a file the agent just wrote, log, notify). Each has a `matcher` (regex/literal on the tool name, `*` = all) and gets `{tool, payload}` on stdin + `HARA_TOOL_NAME` in env. Plugins can contribute hooks too.
176
184
  **Profiles**: add a `profiles` map to `~/.hara/config.json` (`--profile <name>`), or drop a project-level `.hara/config.json` that overrides the global config.
177
185
 
178
186
  ### The org — what makes hara different
@@ -190,7 +198,9 @@ sequences them as a DAG, and executes each step (optionally routed to a role) be
190
198
  **verify gate** — frame → atomize → sequence → execute → verify. Each atom may carry a `check` shell
191
199
  command, so verification is **objective** (e.g. `npm test`, `tsc --noEmit`) rather than a
192
200
  self-assessment. Plan state is the SSOT at `.hara/org/plan.json` (inspectable; execution stops on the
193
- first failed verification).
201
+ first failed verification — fix it and **`hara plan resume`** continues, skipping the atoms already done).
202
+ With **`hara plan --parallel`**, independent atoms (the same dependency wave) run **concurrently** — the org
203
+ works the independent parts at once, not one step at a time.
194
204
 
195
205
  ### What it can do
196
206
 
@@ -200,6 +210,7 @@ read-only **`grep`** / **`glob`** / **`ls`** / **`web_fetch`** — behind a huma
200
210
  dangerous ones unless `-y`. Read-only tools run in parallel within a turn, and edits print a
201
211
  **colored diff** of what changed. Shell output streams live; press **Esc** to interrupt a running
202
212
  turn, or **`/undo`** to revert the last edit.
213
+ - **Type-ahead steering**: keep typing while hara works — your message is held, then **folded into the next model call** (not deferred to a new turn), so a clarification or "also do X" course-corrects the task already in flight (codex-style). Messages typed after the final step start a fresh turn; **Esc** drops the queue and stops.
203
214
  - **Project context**: auto-loads `AGENTS.md` (the cross-tool standard) walking up to the repo root; `hara init` writes one by analyzing the repo.
204
215
  - **`@file` mentions**: attach file contents to a message (`@path`); Tab-completes with a **fuzzy** matcher over the project (subdirs, git-tracked + untracked) — `@idx` → `src/index.ts`. `@<dir>` loads a directory listing, `@src/`+Tab drills into a folder, and mistyped tool/file paths get a "did you mean" suggestion.
205
216
  - **Multi-provider**: Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) — **all streamed live**.
@@ -4,6 +4,7 @@ import { c, out } from "../ui.js";
4
4
  import { activity } from "../activity.js";
5
5
  import { makeRenderer } from "../md.js";
6
6
  import { skillsDigest } from "../skills/skills.js";
7
+ import { runHooks } from "../hooks.js";
7
8
  /** Whether a tool call needs user confirmation under the given approval mode. */
8
9
  export function needsConfirm(kind, mode) {
9
10
  if (kind === "read")
@@ -20,7 +21,9 @@ const HARA_SYSTEM = (cwd) => `You are hara, a coding agent running in the user's
20
21
  Working directory: ${cwd}
21
22
  Be concise and direct. Use the provided tools to read files, edit/write files, and run shell
22
23
  commands. Prefer small, verifiable steps; edit existing files with edit_file rather than rewriting
23
- them whole. You have a persistent memory: use memory_search before answering about prior decisions,
24
+ them whole. For a multi-step task, call \`todo_write\` to plan a short checklist and keep it updated as
25
+ you go (one item in_progress at a time) — skip it for trivial one-step tasks. You have a persistent
26
+ memory: use memory_search before answering about prior decisions,
24
27
  conventions, or the user's preferences, and memory_write to proactively save durable facts you learn.
25
28
  When a task matches one of the Skills listed below, call the \`skill\` tool to load its full instructions
26
29
  before acting; save a reusable how-to as a new skill with skill_create. If you discover a durable project
@@ -38,6 +41,12 @@ function composeSystem(cwd, projectContext, override, memory) {
38
41
  export async function runAgent(history, opts) {
39
42
  const { provider, ctx } = opts;
40
43
  for (;;) {
44
+ // Type-ahead steering: fold in anything the user submitted while the previous step ran, so it
45
+ // reaches the model on this next call (drained after the last tool round; empty on the 1st pass).
46
+ if (opts.pendingInput) {
47
+ for (const m of await opts.pendingInput())
48
+ history.push(m);
49
+ }
41
50
  const specs = opts.toolFilter ? toolSpecs().filter((t) => opts.toolFilter(t.name)) : toolSpecs();
42
51
  const sink = ctx.ui; // TUI mode: route output to ink instead of stdout
43
52
  const tty = stdout.isTTY && !opts.quiet && !sink;
@@ -154,8 +163,14 @@ export async function runAgent(history, opts) {
154
163
  }
155
164
  activity.inc();
156
165
  try {
166
+ const pre = runHooks("PreToolUse", p.tu.name, p.tu.input, ctx.cwd); // a hook may veto the call
167
+ if (pre.block) {
168
+ results[idx] = { id: p.tu.id, name: p.tu.name, content: pre.message, isError: true };
169
+ return;
170
+ }
157
171
  const res = await p.tool.run(p.tu.input, ctx);
158
172
  results[idx] = { id: p.tu.id, name: p.tu.name, content: res };
173
+ runHooks("PostToolUse", p.tu.name, { input: p.tu.input, result: res }, ctx.cwd); // observe-only
159
174
  }
160
175
  catch (e) {
161
176
  results[idx] = { id: p.tu.id, name: p.tu.name, content: `Error: ${e.message}`, isError: true };
package/dist/config.js CHANGED
@@ -11,7 +11,7 @@ const PROVIDER_DEFAULTS = {
11
11
  "qwen-oauth": { model: "coder-model", envKey: "QWEN_OAUTH_TOKEN" },
12
12
  openai: { model: "gpt-4o-mini", envKey: "OPENAI_API_KEY" },
13
13
  };
14
- export const CONFIG_KEYS = ["provider", "apiKey", "model", "baseURL", "approval", "sandbox", "theme", "evolve", "assetCapture", "computerUse", "computerApps", "visionModel", "visionBaseURL", "visionApiKey", "embedProvider", "embedModel", "embedBaseURL", "embedApiKey"];
14
+ export const CONFIG_KEYS = ["provider", "apiKey", "model", "baseURL", "approval", "sandbox", "theme", "evolve", "assetCapture", "computerUse", "computerApps", "visionModel", "visionBaseURL", "visionApiKey", "embedProvider", "embedModel", "embedBaseURL", "embedApiKey", "notify"];
15
15
  export const APPROVAL_MODES = ["suggest", "auto-edit", "full-auto"];
16
16
  export const SANDBOX_MODES = ["off", "workspace-write", "read-only"];
17
17
  const PROJECT_ROOT_MARKERS = [".git", "package.json", "Cargo.toml", "go.mod", "pyproject.toml", ".hg"];
@@ -107,7 +107,9 @@ export function loadConfig(opts = {}) {
107
107
  ...(project.mcpServers ?? {}),
108
108
  ...(profile.mcpServers ?? {}),
109
109
  };
110
- return { provider, apiKey, model, baseURL, approval, sandbox, theme, evolve, assetCapture, computerUse, computerApps, visionModel, visionBaseURL, visionApiKey, modelVision, embedProvider, embedModel, embedBaseURL, embedApiKey, mcpServers, cwd: process.cwd() };
110
+ const hooks = (merged.hooks && typeof merged.hooks === "object" ? merged.hooks : {});
111
+ const notify = (process.env.HARA_NOTIFY ?? merged.notify ?? "off");
112
+ return { provider, apiKey, model, baseURL, approval, sandbox, theme, evolve, assetCapture, computerUse, computerApps, visionModel, visionBaseURL, visionApiKey, modelVision, embedProvider, embedModel, embedBaseURL, embedApiKey, hooks, notify, mcpServers, cwd: process.cwd() };
111
113
  }
112
114
  export function providerEnvKey(provider) {
113
115
  return (PROVIDER_DEFAULTS[provider] ?? PROVIDER_DEFAULTS.anthropic).envKey;
package/dist/hooks.js ADDED
@@ -0,0 +1,64 @@
1
+ // Lifecycle hooks — run user/plugin shell commands around tool calls (codex/Claude-Code parity).
2
+ // PreToolUse runs BEFORE a tool: a non-zero exit BLOCKS the call (its output becomes the denial message).
3
+ // PostToolUse runs AFTER: observe-only (format, log, notify). Configured in config.json `hooks` + contributed
4
+ // by plugins. The command receives {tool, payload} as JSON on stdin + HARA_TOOL_NAME in the env.
5
+ import { spawnSync } from "node:child_process";
6
+ import { loadConfig } from "./config.js";
7
+ import { pluginHooks } from "./plugins/plugins.js";
8
+ let cache = null;
9
+ export function resetHooksCache() {
10
+ cache = null;
11
+ }
12
+ function merged() {
13
+ if (cache)
14
+ return cache;
15
+ const cfg = loadConfig().hooks ?? {};
16
+ const plg = pluginHooks();
17
+ cache = {
18
+ PreToolUse: [...(cfg.PreToolUse ?? []), ...(plg.PreToolUse ?? [])],
19
+ PostToolUse: [...(cfg.PostToolUse ?? []), ...(plg.PostToolUse ?? [])],
20
+ };
21
+ return cache;
22
+ }
23
+ const matches = (m, name) => {
24
+ if (!m || m === "*")
25
+ return true;
26
+ try {
27
+ return new RegExp(m).test(name);
28
+ }
29
+ catch {
30
+ return m === name;
31
+ }
32
+ };
33
+ /** True if any hook is configured (lets the loop skip the work entirely in the common case). */
34
+ export function hasHooks() {
35
+ const h = merged();
36
+ return !!(h.PreToolUse?.length || h.PostToolUse?.length);
37
+ }
38
+ /** Run hooks for an event matching `toolName`. PreToolUse: a non-zero exit BLOCKS (returns the message);
39
+ * PostToolUse: observe-only, never blocks. Sync (hooks are short, opt-in); 30s timeout each. */
40
+ export function runHooks(event, toolName, payload, cwd) {
41
+ for (const h of merged()[event] ?? []) {
42
+ if (!matches(h.matcher, toolName))
43
+ continue;
44
+ let r;
45
+ try {
46
+ r = spawnSync(h.command, {
47
+ shell: true,
48
+ cwd,
49
+ input: JSON.stringify({ tool: toolName, payload }),
50
+ encoding: "utf8",
51
+ timeout: 30_000,
52
+ env: { ...process.env, HARA_TOOL_NAME: toolName },
53
+ });
54
+ }
55
+ catch {
56
+ continue;
57
+ }
58
+ if (event === "PreToolUse" && r.status !== 0 && r.status !== null) {
59
+ const msg = (String(r.stdout ?? "") + String(r.stderr ?? "")).trim();
60
+ return { block: true, message: `⛔ blocked by a PreToolUse hook${msg ? `: ${msg}` : ` (exit ${r.status})`}` };
61
+ }
62
+ }
63
+ return { block: false, message: "" };
64
+ }