@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 +216 -1
- package/README.md +15 -4
- package/dist/agent/loop.js +16 -1
- package/dist/config.js +4 -2
- package/dist/hooks.js +64 -0
- package/dist/index.js +331 -77
- package/dist/notify.js +42 -0
- package/dist/org/planner.js +19 -0
- package/dist/plugins/plugins.js +14 -0
- package/dist/providers/anthropic.js +21 -11
- package/dist/search/semindex.js +62 -11
- package/dist/session/store.js +14 -0
- package/dist/tools/computer.js +156 -16
- package/dist/tools/todo.js +51 -0
- package/dist/tools/web.js +97 -0
- package/dist/tui/App.js +55 -7
- package/dist/tui/InputBox.js +2 -2
- package/dist/vision.js +52 -3
- package/package.json +3 -2
- package/plugins/browser/.hara-plugin/plugin.json +9 -0
- package/plugins/browser/skills/web/SKILL.md +27 -0
- package/plugins/chrome/.hara-plugin/plugin.json +9 -0
- package/plugins/chrome/skills/chrome/SKILL.md +26 -0
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.
|
|
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
|
|
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**.
|
package/dist/agent/loop.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
+
}
|