@nanhara/hara 0.48.0 → 0.62.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 +245 -0
- package/README.md +28 -7
- package/SECURITY.md +54 -0
- package/dist/agent/loop.js +21 -4
- package/dist/completions.js +49 -0
- package/dist/config.js +19 -7
- package/dist/cron/install.js +112 -0
- package/dist/cron/runner.js +109 -0
- package/dist/cron/schedule.js +147 -0
- package/dist/cron/store.js +87 -0
- package/dist/hooks.js +64 -0
- package/dist/index.js +410 -13
- package/dist/mcp/server.js +56 -0
- package/dist/memory/store.js +44 -6
- package/dist/notify.js +42 -0
- package/dist/org/review-chain.js +91 -0
- package/dist/org/roles.js +11 -0
- package/dist/plugins/plugins.js +14 -0
- package/dist/providers/anthropic.js +21 -11
- package/dist/providers/qwen-oauth.js +9 -2
- package/dist/sandbox.js +25 -3
- package/dist/search/semindex.js +9 -2
- package/dist/session/store.js +12 -2
- package/dist/tools/computer.js +9 -4
- package/dist/tools/patch.js +31 -12
- package/dist/tools/todo.js +51 -0
- package/dist/tools/web.js +178 -8
- package/dist/tui/App.js +17 -4
- package/dist/tui/InputBox.js +37 -3
- package/dist/tui/vim.js +115 -0
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,251 @@ 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.62.0 — unreleased (shell completions)
|
|
9
|
+
|
|
10
|
+
- **`hara completions bash|zsh|fish`** prints a completion script (eval it in your shell rc) that
|
|
11
|
+
tab-completes the top-level subcommands and the subcommands of each group (`cron`, `memory`, `plugin`,
|
|
12
|
+
`roles`, `skills`, `config`), falling back to file completion. Generated from the live command tree so it
|
|
13
|
+
never drifts; hand-rolled (no new dependency).
|
|
14
|
+
|
|
15
|
+
## 0.61.3 — unreleased (audit follow-through: session robustness + SECURITY.md)
|
|
16
|
+
|
|
17
|
+
- **Corrupt/hand-edited session files no longer crash** `--resume` or `/sessions` (audit M4): `loadSession`
|
|
18
|
+
validates the shape (meta object + history array), `deriveTitle` tolerates a non-string, and `listSessions`
|
|
19
|
+
skips metaless files instead of throwing.
|
|
20
|
+
- New **`SECURITY.md`** — the threat model, the controls (approval gate, read-only sub-agents, write-confinement
|
|
21
|
+
sandbox, `web_fetch` SSRF guard, 0600 secrets, plugin trust), what is deliberately *not* a security boundary,
|
|
22
|
+
and how to report a vulnerability. Captures the posture from the two audit passes.
|
|
23
|
+
|
|
24
|
+
## 0.61.2 — unreleased (security hardening — second audit: SSRF, RPA, secrets)
|
|
25
|
+
|
|
26
|
+
A second audit (RPA / network / auth / search) found more real issues; fixed:
|
|
27
|
+
- **`web_fetch` SSRF (critical).** It would fetch any host — incl. `169.254.169.254` (cloud metadata),
|
|
28
|
+
`localhost`/`127.0.0.1` internal services, and private ranges — and followed redirects blindly. Now it
|
|
29
|
+
**refuses private/loopback/link-local/CGNAT targets** (resolving the hostname first), **re-checks on every
|
|
30
|
+
redirect hop** (manual redirects), and reads the body under a **byte ceiling** (no multi-GB / bomb body).
|
|
31
|
+
- **`computer` "don't ask again" defeated the per-action grant (high).** Screen control is supposed to
|
|
32
|
+
confirm every action; the shared "always" approval silently auto-approved all future clicks/types. Now
|
|
33
|
+
`computer` is **never** satisfied by a prior "always" — it always prompts.
|
|
34
|
+
- **Key blocklist bypassable (high).** It only caught spelled-out combos, so Windows SendKeys `%{F4}`/`^w`
|
|
35
|
+
and Linux `XF86LogOff`/`XF86PowerOff` slipped through. Now caught on all three platforms (bare editing
|
|
36
|
+
keys like Delete stay allowed).
|
|
37
|
+
- **Secrets could be embedded into the semantic index (medium).** The asset/skill/memory dirs aren't
|
|
38
|
+
`.gitignore`-filtered, so a stray `credentials.json`/`secrets.yaml` there could be POSTed to the embedding
|
|
39
|
+
provider + persisted. Now secret-named files are skipped in both index collectors.
|
|
40
|
+
- **Token/config files were world-readable (medium).** `~/.hara/qwen-oauth.json` (access+refresh tokens)
|
|
41
|
+
and `~/.hara/config.json` (`apiKey`) are now written **0600** (and tightened on save).
|
|
42
|
+
- **RPA app allowlist was substring-matched (low).** `"Notes"` matched `"Notes - Evil"`; now an exact
|
|
43
|
+
(case-insensitive) frontmost-app match.
|
|
44
|
+
|
|
45
|
+
The RPA + clipboard shell-outs were confirmed injection-safe (argv arrays, JSON-quoted scripts). 198 tests
|
|
46
|
+
(2 new: the SSRF private-IP guard + the widened key blocklist).
|
|
47
|
+
|
|
48
|
+
## 0.61.1 — unreleased (security + correctness hardening — core audit)
|
|
49
|
+
|
|
50
|
+
A security/correctness audit of the core (sandbox, confirmation gate, file tools, MCP client) found real
|
|
51
|
+
issues; fixed:
|
|
52
|
+
- **Confirmation-gate bypass via sub-agents (critical).** The read-kind `agent` tool never prompts, yet
|
|
53
|
+
spawned sub-agents ran **full-auto, unconfirmed** — so a role granting `edit_file`/`bash` let a fan-out
|
|
54
|
+
sub-agent mutate files / run shell with no approval, even in `suggest` mode. Sub-agents are now **always
|
|
55
|
+
read-only** (a role may narrow further but can never grant write/exec — `subagentToolFilter`). Write-capable
|
|
56
|
+
roles run in the main loop via `hara org`, behind the gate.
|
|
57
|
+
- **`apply_patch` wasn't actually atomic (critical / data-loss).** It claimed all-or-nothing but Phase 2 wrote
|
|
58
|
+
files sequentially — a mid-way failure left a half-patched tree with no undo. Now it **rolls back** every
|
|
59
|
+
applied write on any failure (restores updated/deleted, removes created), so it's truly all-or-nothing.
|
|
60
|
+
- **Sandbox honesty.** It's **file-write confinement only** (not reads/network/exec; `/private/tmp` stays
|
|
61
|
+
writable) — clarified in the header, `--sandbox` docs, and label so it no longer oversells containment.
|
|
62
|
+
- The non-macOS "runs unsandboxed" warning now fires from `runShell` (every entry point: `-p`, org, cron),
|
|
63
|
+
not just the REPL; a runaway `bash` whose output exceeds `maxBuffer` is now **killed** (not streamed to
|
|
64
|
+
the timeout); and `hara plugin add` now **shows the commands a plugin will run** on every launch (its MCP
|
|
65
|
+
servers + hooks are arbitrary code — surface the trust surface).
|
|
66
|
+
|
|
67
|
+
196 tests (2 new: the sub-agent read-only guard + the apply_patch rollback). The edit tools, hooks matcher,
|
|
68
|
+
and sandbox profile-injection safety were audited and confirmed solid.
|
|
69
|
+
|
|
70
|
+
## 0.61.0 — unreleased (`hara memory` — inspect + distill durable memory)
|
|
71
|
+
|
|
72
|
+
- New **`hara memory`** command group, giving memory a CLI surface it lacked:
|
|
73
|
+
- **`hara memory show`** — print the digest injected at session start (what the agent actually sees).
|
|
74
|
+
- **`hara memory init`** — scaffold the global + project memory dirs/seed files.
|
|
75
|
+
- **`hara memory distill [--days N] [--scope global|project|all]`** — **promote short-term → long-term**:
|
|
76
|
+
consolidate recent daily logs (`log/YYYY-MM-DD.md`) into durable `MEMORY.md`/`USER.md`, deduped against
|
|
77
|
+
what's already there, skipping the ephemeral. This closes the one tiering gap the PAI/hermes study
|
|
78
|
+
surfaced (the daily-log tier was previously write-only). The agent routes each fact to the right
|
|
79
|
+
target/scope (user pref → `USER.md`, project fact → project memory). Verified live with glm-5.
|
|
80
|
+
- `.hara/` is now gitignored in this repo so dogfooding doesn't leave runtime state (memory/roles/plans).
|
|
81
|
+
|
|
82
|
+
## 0.60.2 — unreleased (memory digest: per-source budgets)
|
|
83
|
+
|
|
84
|
+
- After studying the PAI and hermes memory systems (both lexical-first; both treat vectors as an *optional*
|
|
85
|
+
optimization, not a requirement — which validates hara's design), tightened the frozen-snapshot digest:
|
|
86
|
+
the old `slice(0, 4000)` on the **concatenated** sources could cut an entry mid-line and let a large
|
|
87
|
+
project `MEMORY.md` **crowd `USER.md` out entirely**. Each source (project MEMORY / global MEMORY / USER)
|
|
88
|
+
now gets its **own** budget and is truncated at a **line boundary**, so high-value user prefs are always
|
|
89
|
+
injected and no entry is split. The rest stays reachable via `memory_search` (which is already hybrid
|
|
90
|
+
lexical + opt-in semantic). No behavior change when memory is small.
|
|
91
|
+
|
|
92
|
+
## 0.60.1 — unreleased (cron hardening — from a code-review pass)
|
|
93
|
+
|
|
94
|
+
A review of the fast-built `hara cron` module surfaced real bugs; fixed:
|
|
95
|
+
- **Malformed cron expressions were silently accepted** (`Number("")===0` etc.) — `"0 9 * * 1,"`, `"/5 * * * *"`, `"5/"` parsed as valid jobs that fire at the wrong time. Now strictly validated and rejected; `N/step` correctly extends to max (Vixie semantics).
|
|
96
|
+
- **`hara cron install` could emit a broken plist/crontab** when a path contained `&`/`<`/`>` (launchd XML) or a space/metacharacter (crontab shell line). Now XML-escaped / shell-quoted, and an install is refused if a path contains a newline.
|
|
97
|
+
- **Per-job logs grew unbounded** — capped to the last ~256KB once over ~1MB.
|
|
98
|
+
- **The tick lock could poison the scheduler for 30 min after a crash, or double-fire a long job** — now keyed on PID liveness (a dead owner is taken over within one tick; a live owner is respected for long runs).
|
|
99
|
+
- **An ambiguous id-prefix silently deleted/toggled the *first* match** — `cron remove/enable/disable/run/logs` now error on an ambiguous prefix instead of guessing.
|
|
100
|
+
|
|
101
|
+
The vim reducer, type-ahead steering, Anthropic message coalescing, the MCP allowlist, and the binary build were reviewed and confirmed clean. 192 tests (4 new hardening cases).
|
|
102
|
+
|
|
103
|
+
## 0.60.0 — unreleased (single-binary distribution)
|
|
104
|
+
|
|
105
|
+
- **Standalone binaries** — hara can now be a single self-contained executable (no Node required):
|
|
106
|
+
`curl -fsSL .../install.sh | sh`. Built with `bun build --compile` (`npm run build:binary`, or
|
|
107
|
+
`build:binaries` to cross-compile darwin-arm64/x64 + linux-x64/arm64 from one machine). A tagged
|
|
108
|
+
release (`.github/workflows/release.yml`) builds + attaches them; `install.sh` grabs the right one.
|
|
109
|
+
- Build fixes for the bundled binary: a Bun plugin **stubs ink's dev-only `react-devtools-core`** (lazy-
|
|
110
|
+
imported under `DEV`, never in production) so it bundles clean; the version is **baked in via a build
|
|
111
|
+
define** (a compiled binary has no `package.json` to read at runtime); and cron's self-reinvoke now
|
|
112
|
+
detects script-vs-binary mode (`selfArgv`) so `hara cron` works from the binary too. The 60 MB binaries
|
|
113
|
+
are kept out of the npm tarball (`!dist/bin`), which stays ~140 kB.
|
|
114
|
+
|
|
115
|
+
## 0.59.0 — unreleased (vim keybindings in the input box)
|
|
116
|
+
|
|
117
|
+
- **Vim mode** (opt-in: `hara config set vimMode true`, or `HARA_VIM=1`). The TUI prompt becomes modal —
|
|
118
|
+
**Esc** → normal, **i/a/I/A/o** → insert. Normal-mode motions `h l 0 $ w b e` (+ `gg`/`G`), edits
|
|
119
|
+
`x D C dd cc dw cw`, and paste `p`/`P` with a delete/yank register. A distinct prompt marker (`◆` yellow)
|
|
120
|
+
+ a `-- NORMAL -- / -- INSERT --` hint show the mode. Off by default (normal typing is unchanged). The
|
|
121
|
+
editing logic is a pure reducer (`src/tui/vim.ts`), fully unit-tested; `hara doctor` shows the input mode.
|
|
122
|
+
|
|
123
|
+
## 0.58.0 — unreleased (`hara cron` — scheduled tasks)
|
|
124
|
+
|
|
125
|
+
- **Scheduled tasks.** `hara cron add "<schedule>" "<task>"` runs a task on a schedule — the fired job is a
|
|
126
|
+
fresh `hara` session (the run *is* the agent, like openclaw/hermes). Schedules: a 5-field **cron expr**
|
|
127
|
+
(`"0 9 * * 1-5"`), an **interval** (`"every 30m"`), or a **one-shot** (`"in 2h"` / an ISO timestamp).
|
|
128
|
+
`--org` routes it through the role org instead of a plain prompt.
|
|
129
|
+
- **Fires via your OS, no daemon to babysit.** `hara cron install` registers a per-minute `hara cron tick`
|
|
130
|
+
with **launchd** (macOS) or **crontab** (Linux); `tick` runs whatever's due (lock-guarded so a slow job
|
|
131
|
+
doesn't double-fire) and logs each run. Manage with `hara cron list / run <id> / enable / disable /
|
|
132
|
+
remove / logs / uninstall`. Jobs persist atomically in `~/.hara/cron/jobs.json`; `hara doctor` shows the
|
|
133
|
+
count + scheduler status. Cron matching is hand-rolled (no new dependency), minute-granular, local-time.
|
|
134
|
+
|
|
135
|
+
## 0.57.0 — unreleased (in-session `/diff`, `/review`, `/commit` in the TUI)
|
|
136
|
+
|
|
137
|
+
- The default TUI now wires three more slash commands so the **change → review → commit** loop happens
|
|
138
|
+
in-session instead of dropping to a subcommand (they used to print "isn't wired into the TUI yet"):
|
|
139
|
+
- **`/diff`** — show the working-tree diff vs HEAD (`/diff staged` for the index), rendered as a colored diff block. No model call.
|
|
140
|
+
- **`/review`** — a senior-reviewer pass over `git diff HEAD` (read-only), streamed inline.
|
|
141
|
+
- **`/commit`** — stage everything and commit with an AI-written message (reuses the review→commit machinery).
|
|
142
|
+
Reuses existing, already-verified pieces (`autoCommit`, `REVIEW_SYSTEM`, `runShell`). Other subcommands
|
|
143
|
+
(`init`/`index`/`plan`/`org`/…) still point you to `hara <cmd>` or `HARA_TUI=0`.
|
|
144
|
+
|
|
145
|
+
## 0.56.0 — unreleased (review → commit capstone + robust verdict parsing)
|
|
146
|
+
|
|
147
|
+
- **`hara org --review --commit`** closes the loop: once the reviewer approves, hara stages the work and
|
|
148
|
+
commits it with an AI-written message (reusing `hara commit`'s generation). **Guarded** — it only
|
|
149
|
+
auto-commits when the working tree was **clean before the run** (so it captures this run's work, never
|
|
150
|
+
pre-existing WIP), and with `--review` only **after approval** (a review that doesn't pass leaves the
|
|
151
|
+
changes in your tree, uncommitted). `--commit` works without `--review` too (commit the implementer's
|
|
152
|
+
result). Verified live end-to-end: implement → review → approve → `✓ committed`.
|
|
153
|
+
- **Robust verdict parsing** (hardening v0.55, found via live smokes). Real models don't emit the literal
|
|
154
|
+
`VERDICT: APPROVED` token — across runs glm-5 wrote `**VERDICT**: No issues found`, `**VERDICT**: PASS`,
|
|
155
|
+
and `VERDICT: LGTM`. The parser now anchors on a markdown-tolerant `VERDICT` marker and **classifies the
|
|
156
|
+
phrase after it** (approve vs changes synonyms), with a changes-signal veto and an ambiguous-→-not-approved
|
|
157
|
+
safe default (worst case is one extra review round, never a bad auto-commit). `not approved` correctly
|
|
158
|
+
vetoes despite containing "approv". Unit tests now cover the exact shapes seen in live runs.
|
|
159
|
+
|
|
160
|
+
## 0.55.0 — unreleased (multi-role review chain — `hara org --review`)
|
|
161
|
+
|
|
162
|
+
- **Review chains** — `hara org --review "<task>"` runs the org like an actual engineering team: the owning
|
|
163
|
+
role implements, then a **reviewer** role inspects the diff and either **approves** or sends it back with
|
|
164
|
+
concrete fixes, looping implement → review → fix until approved or a round cap (`--rounds`, default 3).
|
|
165
|
+
This is hara's differentiation — not "one agent + temp sub-agents" but roles that hold each other to a
|
|
166
|
+
bar. The reviewer is read-only (uses your `reviewer` role if defined, else a built-in persona) and ends
|
|
167
|
+
with a machine-parseable `VERDICT: APPROVED | CHANGES_REQUESTED`; on changes-requested the issues feed
|
|
168
|
+
back into the implementer's own conversation so it keeps context. New `src/org/review-chain.ts` (verdict
|
|
169
|
+
parsing, non-destructive `git diff HEAD` capture, prompts) — all unit-tested. **Verified live end-to-end**
|
|
170
|
+
(implementer edits a file → reviewer approves → loop exits).
|
|
171
|
+
|
|
172
|
+
## 0.54.0 — unreleased (`hara mcp` — run hara as an MCP server)
|
|
173
|
+
|
|
174
|
+
- **MCP server mode** — `hara mcp` runs hara as an MCP server over stdio, so other MCP clients (Claude
|
|
175
|
+
Desktop, Cursor, another hara…) can call its tools. hara was already an MCP *client*; this completes
|
|
176
|
+
the loop. The high-value one is **`codebase_search`** — point any MCP client at a repo and it gets
|
|
177
|
+
hara's semantic/lexical code search, plus `read_file`/`grep`/`glob`/`ls`/`web_fetch`/`web_search`.
|
|
178
|
+
**Read-only by default** — no `edit_file`/`bash`/`computer`, so an external client can't mutate your
|
|
179
|
+
machine through hara; override the exposed set with `HARA_MCP_TOOLS=a,b,c` at your own risk. Reuses
|
|
180
|
+
hara's tool registry (`src/mcp/server.ts`, built on `@modelcontextprotocol/sdk` — already a dep).
|
|
181
|
+
Verified end-to-end (a real MCP client lists the tools + calls `ls`/`codebase_search`). `hara doctor`
|
|
182
|
+
now shows both the client (servers connected) and serve (tools exposed) sides.
|
|
183
|
+
|
|
184
|
+
```jsonc
|
|
185
|
+
// e.g. in a client's mcpServers config:
|
|
186
|
+
"hara": { "command": "hara", "args": ["mcp"] } // run from the repo you want searchable
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 0.53.0 — unreleased (task-done notifications + steering in plan mode)
|
|
190
|
+
|
|
191
|
+
- **Notifications** — get pinged when a turn finishes so you can walk away during a long run
|
|
192
|
+
(codex/Claude-Code parity). `hara config set notify bell` rings the terminal BEL; `notify system` fires
|
|
193
|
+
an OS notification (macOS `osascript` / Linux `notify-send`) plus the bell; default `off`. Gated on
|
|
194
|
+
elapsed time (≥8s) so quick turns you were watching stay silent. Wired into the TUI turn, plan-mode
|
|
195
|
+
execute, and the plain REPL; `hara doctor` shows the setting. New `src/notify.ts` (`notifyDone`).
|
|
196
|
+
- **Type-ahead steering now covers plan mode too.** v0.52 wired steering into the regular turn only;
|
|
197
|
+
the `pendingInput` builder is now hoisted so plan-mode *investigation* and *execution* also fold in
|
|
198
|
+
messages you type mid-turn (previously they fell back to the old wait-for-turn-end behavior — an
|
|
199
|
+
inconsistency). All three turn paths now steer.
|
|
200
|
+
|
|
201
|
+
## 0.52.0 — unreleased (type-ahead steering — mid-turn messages course-correct the live task)
|
|
202
|
+
|
|
203
|
+
- **Type-ahead now *steers* the running turn** instead of waiting for it to finish. Previously a message
|
|
204
|
+
typed while hara worked was held and replayed as a brand-new turn once the turn ended — so a
|
|
205
|
+
supplement ("also handle the error case", "use TS not JS") arrived *after* the task had already
|
|
206
|
+
finished on the old understanding, becoming rework. Now, studying how **codex** does it (its
|
|
207
|
+
`pending_input` drains at the next model-call boundary *inside* the same turn) vs **cc-haha/Claude
|
|
208
|
+
Code** (waits for full completion), hara adopts the codex model: queued messages are **folded into the
|
|
209
|
+
next model call** (drained after each tool round), so the model course-corrects mid-task. Each shows
|
|
210
|
+
inline in the transcript at the point it's folded in. Messages typed during the *final* step (no more
|
|
211
|
+
tool rounds) still start a fresh turn; **Esc** drops the queue and stops.
|
|
212
|
+
- New `RunOpts.pendingInput` (the loop drains it before each model call; unused outside the TUI = zero
|
|
213
|
+
change for `-p`/sub-agents/plain REPL). The TUI hands the queue through `Helpers.drainQueue`.
|
|
214
|
+
- **`toAnthropic` now coalesces consecutive `user` messages** — required since a steered message lands
|
|
215
|
+
right after tool-results (which map to a `user` message) and Anthropic rejects two `user` turns in a
|
|
216
|
+
row. Dormant in normal alternating histories. Unit-tested.
|
|
217
|
+
|
|
218
|
+
## 0.51.0 — unreleased (lifecycle hooks — PreToolUse / PostToolUse)
|
|
219
|
+
|
|
220
|
+
- **Hooks dispatch** — run your own shell commands around every tool call (codex / Claude-Code parity, which
|
|
221
|
+
hara lacked). A **`PreToolUse`** hook runs *before* a tool and can **veto** it (non-zero exit blocks the
|
|
222
|
+
call; its stdout/stderr becomes the denial the model sees) — e.g. forbid `bash rm -rf`, gate edits to a
|
|
223
|
+
path, require a clean tree. A **`PostToolUse`** hook runs *after* (observe-only) — e.g. `prettier` a file
|
|
224
|
+
the agent just wrote, log/notify. The command gets `{tool, payload}` as JSON on stdin + `HARA_TOOL_NAME`
|
|
225
|
+
in its env; each is matched by a `matcher` (regex/literal on the tool name, `*`/omitted = all) with a 30s
|
|
226
|
+
timeout. Configure in `config.json` `"hooks"`; **plugins can contribute hooks** too. `hara doctor` shows
|
|
227
|
+
the active count. No hooks configured = zero overhead (fast no-op).
|
|
228
|
+
|
|
229
|
+
```jsonc
|
|
230
|
+
// ~/.hara/config.json
|
|
231
|
+
"hooks": {
|
|
232
|
+
"PreToolUse": [{ "matcher": "bash", "command": "grep -q 'rm -rf' && { echo 'no rm -rf'; exit 1; } || exit 0" }],
|
|
233
|
+
"PostToolUse": [{ "matcher": "edit_file|write_file", "command": "prettier --write \"$(jq -r .payload.input.path)\" 2>/dev/null; exit 0" }]
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## 0.50.0 — unreleased (web_search — find pages, not just fetch)
|
|
238
|
+
|
|
239
|
+
- New **`web_search`** tool — search the web (title/URL/snippet), then `web_fetch` a result to read it. Closes
|
|
240
|
+
the other codex/cc-haha gap (hara could previously only fetch a *known* URL). **Reliable with a Tavily key**
|
|
241
|
+
(`HARA_SEARCH_API_KEY` / `TAVILY_API_KEY`, free tier); a **keyless DuckDuckGo** fallback works best-effort
|
|
242
|
+
(POST endpoint; may rate-limit). Read-kind, available to sub-agents. Verified live (keyless: "anthropic
|
|
243
|
+
claude" → real results); parser unit-tested (incl. the DDG `uddg` redirect decode).
|
|
244
|
+
|
|
245
|
+
## 0.49.0 — unreleased (inline todo tool — `todo_write`)
|
|
246
|
+
|
|
247
|
+
- New **`todo_write`** tool — the agent maintains a live task checklist during multi-step work (codex's
|
|
248
|
+
`update_plan` / Claude Code's `TodoWrite`, which hara lacked). Plan up front, keep one item `in_progress`,
|
|
249
|
+
flip to `done` as you go; pass the full list each call. Read-kind (never prompts); the system prompt nudges
|
|
250
|
+
its use for multi-step tasks; sub-agents can use it too. Renders a `☐/▶/☑` checklist with a done count.
|
|
251
|
+
*(Gap analysis vs codex + cc-haha: this was the top missing capability.)*
|
|
252
|
+
|
|
8
253
|
## 0.48.0 — unreleased (chrome plugin: drive your real logged-in Chrome)
|
|
9
254
|
|
|
10
255
|
- New first-party **`chrome` plugin** — web automation via **`chrome-devtools-mcp`** against a **real Chrome with
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
**Highlights**
|
|
12
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
|
-
- **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).
|
|
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). Inspect/consolidate it with **`hara memory show`** and **`hara memory distill`** (promote recent daily logs → durable memory). Lexical-first by design — semantic search is opt-in, never required.
|
|
15
15
|
- **Multi-provider, all streamed** — Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) with live Markdown + visible reasoning.
|
|
16
16
|
- **Solid coding core** — `edit_file` / `apply_patch` (atomic multi-file) with colored diffs · `grep`/`glob`/`ls`/`codebase_search` (lexical + optional semantic search over the repo) /`web_fetch` · fuzzy `@file` · `/undo` · `/compact` · **Esc-to-interrupt** · parallel sub-agents · MCP client · macOS sandbox.
|
|
17
17
|
|
|
@@ -23,6 +23,14 @@ Track it: https://github.com/hara-cli/hara · https://hara.run
|
|
|
23
23
|
npm i -g @nanhara/hara
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Or a **standalone binary** (no Node required):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
curl -fsSL https://raw.githubusercontent.com/hara-cli/hara/main/install.sh | sh
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Tab completion (optional): `eval "$(hara completions zsh)"` in your `~/.zshrc` (or `bash`/`fish`).
|
|
33
|
+
|
|
26
34
|
Or from source:
|
|
27
35
|
|
|
28
36
|
```bash
|
|
@@ -171,14 +179,18 @@ vector DB needed, and lexical still works when there's no index. Re-running `har
|
|
|
171
179
|
only changed files re-embed (a full repo rebuild that takes ~a minute re-runs in well under a second).
|
|
172
180
|
|
|
173
181
|
**Approval modes**: `suggest` confirms edits & shell · `auto-edit` auto-applies file edits but confirms shell · `full-auto` runs everything.
|
|
174
|
-
**Sandbox** (macOS): `--sandbox workspace-write|read-only` runs the `bash` tool under Seatbelt (writes confined to the project / blocked).
|
|
182
|
+
**Sandbox** (macOS): `--sandbox workspace-write|read-only` runs the `bash` tool under Seatbelt — **file-write confinement** (writes confined to the project / blocked). It does not restrict reads, network, or process exec; on non-macOS the shell runs unsandboxed (with a warning).
|
|
175
183
|
**Screen control** (opt-in): the `computer` tool drives desktop software (screenshot → click/type), native per OS
|
|
176
184
|
(mac `screencapture`+`cliclick` · Windows PowerShell · Linux `scrot`+`xdotool`). Off by default — enable a tier with
|
|
177
185
|
`hara config set computerUse read|click|full` and allowlist apps with `hara config set computerApps "App, …"`. Guarded
|
|
178
186
|
by the tier, the frontmost-app allowlist, a dangerous-key blocklist, and a once-per-session grant. Screenshots are read via your
|
|
179
187
|
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.
|
|
180
188
|
**Sessions**: conversations are saved automatically — `-c` / `--resume <id>` to continue, `hara sessions` to list.
|
|
181
|
-
**MCP**: add an `mcpServers` map to config (global or project `.hara/config.json`); their tools appear to the agent as `mcp__<server>__<tool>`.
|
|
189
|
+
**MCP**: add an `mcpServers` map to config (global or project `.hara/config.json`); their tools appear to the agent as `mcp__<server>__<tool>`. hara can also **be** an MCP server — `hara mcp` exposes its read/search tools (esp. **`codebase_search`**) over stdio so other clients (Claude Desktop, Cursor, another hara) can use them; read-only by default (`HARA_MCP_TOOLS` to override).
|
|
190
|
+
**Vim mode**: `hara config set vimMode true` makes the prompt modal — Esc → normal, `i/a/A/I` insert, `h l 0 $ w b e` motions, `x D C dd cw p` edits. Off by default.
|
|
191
|
+
**Scheduled tasks**: `hara cron add "0 9 * * 1-5" "<task>"` (or `"every 30m"`, `"in 2h"`) runs a task on a schedule — each run is a fresh hara session. `hara cron install` wires a per-minute tick into launchd/crontab (no daemon); `--org` routes through the role org. Manage with `hara cron list/run/enable/disable/remove/logs`.
|
|
192
|
+
**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.
|
|
193
|
+
**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.
|
|
182
194
|
**Profiles**: add a `profiles` map to `~/.hara/config.json` (`--profile <name>`), or drop a project-level `.hara/config.json` that overrides the global config.
|
|
183
195
|
|
|
184
196
|
### The org — what makes hara different
|
|
@@ -187,7 +199,11 @@ Define role-agents in `.hara/roles/*.md` — each is a persona (the file body) p
|
|
|
187
199
|
(keywords that route a task here), optional `rejects`, `model`, and `allowTools`/`denyTools`. `hara org
|
|
188
200
|
"<task>"` routes the task to the role that **owns** it (keyword match, LLM fallback) and runs that role's
|
|
189
201
|
agent — e.g. a read-only `reviewer` that reports issues vs an `implementer` that edits code. `hara roles`
|
|
190
|
-
lists them, `hara roles init` scaffolds a starter set, and `--role <id>` forces a specific role.
|
|
202
|
+
lists them, `hara roles init` scaffolds a starter set, and `--role <id>` forces a specific role. Add
|
|
203
|
+
**`--review`** and the org works like a team: the owning role implements, then a **reviewer** role inspects
|
|
204
|
+
the diff and either approves or sends it back with fixes — looping implement → review → fix until approved
|
|
205
|
+
(or `--rounds N`). Add **`--commit`** and it commits the approved result with an AI-written message (guarded
|
|
206
|
+
to a clean start tree; a review that doesn't pass leaves the work uncommitted). The
|
|
191
207
|
**`agent`** tool spawns **parallel read-only sub-agents** for fan-out — analyze / review / search
|
|
192
208
|
several things at once (each can take a `role`).
|
|
193
209
|
|
|
@@ -207,15 +223,20 @@ A streaming agentic loop with built-in tools — `read_file`, `write_file`, **`e
|
|
|
207
223
|
read-only **`grep`** / **`glob`** / **`ls`** / **`web_fetch`** — behind a human-in-the-loop confirmation gate on the
|
|
208
224
|
dangerous ones unless `-y`. Read-only tools run in parallel within a turn, and edits print a
|
|
209
225
|
**colored diff** of what changed. Shell output streams live; press **Esc** to interrupt a running
|
|
210
|
-
turn, or **`/undo`** to revert the last edit.
|
|
226
|
+
turn, or **`/undo`** to revert the last edit. In-session **`/diff`**, **`/review`**, and **`/commit`** close the change → review → commit loop without leaving the prompt.
|
|
227
|
+
- **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.
|
|
211
228
|
- **Project context**: auto-loads `AGENTS.md` (the cross-tool standard) walking up to the repo root; `hara init` writes one by analyzing the repo.
|
|
212
229
|
- **`@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.
|
|
213
230
|
- **Multi-provider**: Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) — **all streamed live**.
|
|
214
231
|
|
|
215
232
|
### Roadmap
|
|
216
233
|
|
|
217
|
-
**Shipped:** ink TUI · plan mode · persistent memory + self-evolution · atomization planner · parallel sub-agents · `/compact` context management.
|
|
218
|
-
**Next:**
|
|
234
|
+
**Shipped:** ink TUI · plan mode · persistent memory + self-evolution · atomization planner · parallel plan atoms · **multi-role review chains** · parallel sub-agents · MCP client *and* server · **scheduled tasks (`hara cron`)** · **single-binary distribution** · `/compact` context management.
|
|
235
|
+
**Next:** SSOT data authority · an enterprise control-plane (fleet + central token management).
|
|
236
|
+
|
|
237
|
+
## Security
|
|
238
|
+
|
|
239
|
+
Human-in-the-loop by default, with a layered model (approval gate · read-only sub-agents · write-confinement sandbox · `web_fetch` SSRF guard · `0600` secrets · reviewed plugin trust). Threat model, controls, and how to report a vulnerability: **[SECURITY.md](SECURITY.md)**.
|
|
219
240
|
|
|
220
241
|
## License
|
|
221
242
|
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
hara is a coding agent that reads/writes files, runs shell commands, drives a browser/desktop, and calls
|
|
4
|
+
LLMs — so it takes a layered, **human-in-the-loop-by-default** stance. This documents the threat model, the
|
|
5
|
+
controls, what is deliberately *not* a security boundary, and how to report a vulnerability.
|
|
6
|
+
|
|
7
|
+
## Threat model
|
|
8
|
+
|
|
9
|
+
hara runs on **your** machine under **your** account, on code **you** point it at. It is not a multi-tenant
|
|
10
|
+
sandbox. The adversary we defend against is primarily **the model going wrong** — a bad suggestion, a
|
|
11
|
+
prompt-injected web page or file steering it toward a destructive or exfiltrating action — not a malicious
|
|
12
|
+
local user (who already has your shell).
|
|
13
|
+
|
|
14
|
+
## Controls
|
|
15
|
+
|
|
16
|
+
- **Approval gate.** Every file edit, shell command, and screen action is classified (`read` / `edit` /
|
|
17
|
+
`exec` / `computer`) and gated by an approval mode: `suggest` (confirm edits & commands), `auto-edit`
|
|
18
|
+
(auto-apply edits, confirm commands), `full-auto` (no prompts — opt-in). Read-only tools never prompt.
|
|
19
|
+
- **Screen control is gated on *every* action.** The `computer` tool always asks before each click/type,
|
|
20
|
+
even in `full-auto`, and "don't ask again" never applies to it. Guarded further by a frontmost-app
|
|
21
|
+
**allowlist** (exact match), a dangerous-key **blocklist** (quit/close/logout across macOS/Windows/Linux
|
|
22
|
+
syntaxes), and a per-session grant. Off by default.
|
|
23
|
+
- **Sub-agents are read-only.** The parallel `agent` fan-out tool runs sub-agents that can never edit or run
|
|
24
|
+
shell — a role may *narrow* their tools but never *grant* write/exec. Write-capable roles run in the main
|
|
25
|
+
loop (`hara org`), behind the gate.
|
|
26
|
+
- **Shell sandbox (macOS).** `--sandbox workspace-write|read-only` runs the `bash` tool under Seatbelt —
|
|
27
|
+
**file-write confinement** (see the non-boundary note below). Commands/paths are passed as argv / a profile
|
|
28
|
+
file, not interpolated into a shell string.
|
|
29
|
+
- **`web_fetch` SSRF guard.** Refuses to fetch private / loopback / link-local / CGNAT addresses (resolving
|
|
30
|
+
the hostname first), re-checks on every redirect hop, and reads the body under a byte ceiling — so the
|
|
31
|
+
model can't reach cloud-metadata endpoints or internal services.
|
|
32
|
+
- **Secrets.** `~/.hara/config.json` (API keys) and `~/.hara/qwen-oauth.json` (tokens) are written `0600`.
|
|
33
|
+
The optional semantic index respects `.gitignore` and skips secret-named files, so keys aren't embedded or
|
|
34
|
+
sent to an embedding provider. The memory guard screens secret-shaped strings out of what the agent saves.
|
|
35
|
+
- **Plugins are code you trust.** Installing a plugin (`hara plugin add`) grants its author code execution:
|
|
36
|
+
its MCP servers and hooks run shell commands on launch. `hara plugin add` **prints the exact commands** a
|
|
37
|
+
plugin will run so you can review them; disable with `hara plugin disable <name>`.
|
|
38
|
+
- **Coding-plan keys.** Provider keys you configure are used only to call the model endpoint you set.
|
|
39
|
+
|
|
40
|
+
## What is *not* a security boundary
|
|
41
|
+
|
|
42
|
+
- **The sandbox confines file writes only** — not reads, not network, not process exec; `/private/tmp`
|
|
43
|
+
stays writable. It stops a stray `rm`/overwrite escaping the project, not a determined exfiltration. Treat
|
|
44
|
+
a `full-auto` + network-capable shell as able to read and send anything your account can.
|
|
45
|
+
- **`@file` mentions** read any file *you* name (including outside the project) — that's you attaching
|
|
46
|
+
context, not the model exfiltrating; mentions are expanded on your typed input only, never on model output.
|
|
47
|
+
- **`full-auto` / `-y`** removes the human gate by your explicit choice. Use it on code and in directories
|
|
48
|
+
you trust.
|
|
49
|
+
|
|
50
|
+
## Reporting a vulnerability
|
|
51
|
+
|
|
52
|
+
Please report security issues privately — open a GitHub **security advisory** on `hara-cli/hara`, or email
|
|
53
|
+
the maintainers — rather than a public issue. Include a minimal reproduction and the impact. We'll
|
|
54
|
+
acknowledge, fix, and credit you.
|
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;
|
|
@@ -127,14 +136,16 @@ export async function runAgent(history, opts) {
|
|
|
127
136
|
const preview = String(input.path ?? input.command ?? input.pattern ?? input.url ?? input.task ?? "")
|
|
128
137
|
.replace(/\s+/g, " ")
|
|
129
138
|
.trim();
|
|
130
|
-
|
|
139
|
+
// Screen control is gated on EVERY action — a prior "don't ask again" must never satisfy it.
|
|
140
|
+
const alwaysGate = tool.kind === "computer";
|
|
141
|
+
if (needsConfirm(tool.kind, opts.approval) && (alwaysGate || !opts.autoApprove?.has(tu.name))) {
|
|
131
142
|
const reply = await opts.confirm(`${c.yellow("⚠")} ${c.bold(tu.name)} ${c.dim(preview)} — run?`);
|
|
132
143
|
if (reply === false) {
|
|
133
144
|
plans.push({ tu, tool, denied: "User denied this action." });
|
|
134
145
|
continue;
|
|
135
146
|
}
|
|
136
|
-
if (reply === "always")
|
|
137
|
-
opts.autoApprove?.add(tu.name);
|
|
147
|
+
if (reply === "always" && !alwaysGate)
|
|
148
|
+
opts.autoApprove?.add(tu.name); // computer: treat "always" as one-time yes
|
|
138
149
|
}
|
|
139
150
|
plans.push({ tu, tool });
|
|
140
151
|
if (!opts.quiet) {
|
|
@@ -154,8 +165,14 @@ export async function runAgent(history, opts) {
|
|
|
154
165
|
}
|
|
155
166
|
activity.inc();
|
|
156
167
|
try {
|
|
168
|
+
const pre = runHooks("PreToolUse", p.tu.name, p.tu.input, ctx.cwd); // a hook may veto the call
|
|
169
|
+
if (pre.block) {
|
|
170
|
+
results[idx] = { id: p.tu.id, name: p.tu.name, content: pre.message, isError: true };
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
157
173
|
const res = await p.tool.run(p.tu.input, ctx);
|
|
158
174
|
results[idx] = { id: p.tu.id, name: p.tu.name, content: res };
|
|
175
|
+
runHooks("PostToolUse", p.tu.name, { input: p.tu.input, result: res }, ctx.cwd); // observe-only
|
|
159
176
|
}
|
|
160
177
|
catch (e) {
|
|
161
178
|
results[idx] = { id: p.tu.id, name: p.tu.name, content: `Error: ${e.message}`, isError: true };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Shell completion scripts (bash / zsh / fish), generated from the command tree so they never drift.
|
|
2
|
+
// `hara completions <shell>` prints one; the user evals it in their shell rc. Completes the top-level
|
|
3
|
+
// subcommands, the subcommands of each group (cron/memory/plugin/roles/skills/config), and falls back to
|
|
4
|
+
// file completion otherwise. Hand-rolled (no dependency) — same minimal-deps philosophy as the rest.
|
|
5
|
+
const bash = ({ top, subs }) => {
|
|
6
|
+
const cases = Object.entries(subs)
|
|
7
|
+
.map(([cmd, sub]) => ` ${cmd}) COMPREPLY=( $(compgen -W "${sub.join(" ")}" -- "$cur") ); return;;`)
|
|
8
|
+
.join("\n");
|
|
9
|
+
return `# hara bash completion — add to ~/.bashrc: eval "$(hara completions bash)"
|
|
10
|
+
_hara() {
|
|
11
|
+
local cur prev; cur="\${COMP_WORDS[COMP_CWORD]}"; prev="\${COMP_WORDS[1]}"
|
|
12
|
+
if [ "$COMP_CWORD" -eq 1 ]; then COMPREPLY=( $(compgen -W "${top.join(" ")}" -- "$cur") ); return; fi
|
|
13
|
+
case "$prev" in
|
|
14
|
+
${cases}
|
|
15
|
+
*) COMPREPLY=( $(compgen -f -- "$cur") );;
|
|
16
|
+
esac
|
|
17
|
+
}
|
|
18
|
+
complete -F _hara hara
|
|
19
|
+
`;
|
|
20
|
+
};
|
|
21
|
+
const zsh = ({ top, subs }) => {
|
|
22
|
+
const cases = Object.entries(subs)
|
|
23
|
+
.map(([cmd, sub]) => ` ${cmd}) compadd -- ${sub.join(" ")} ;;`)
|
|
24
|
+
.join("\n");
|
|
25
|
+
return `# hara zsh completion — add to ~/.zshrc: eval "$(hara completions zsh)"
|
|
26
|
+
_hara() {
|
|
27
|
+
if (( CURRENT == 2 )); then compadd -- ${top.join(" ")}; return; fi
|
|
28
|
+
case "\${words[2]}" in
|
|
29
|
+
${cases}
|
|
30
|
+
*) _files ;;
|
|
31
|
+
esac
|
|
32
|
+
}
|
|
33
|
+
compdef _hara hara
|
|
34
|
+
`;
|
|
35
|
+
};
|
|
36
|
+
const fish = ({ top, subs }) => {
|
|
37
|
+
const lines = [
|
|
38
|
+
"# hara fish completion — save to ~/.config/fish/completions/hara.fish: hara completions fish > ~/.config/fish/completions/hara.fish",
|
|
39
|
+
"complete -c hara -f",
|
|
40
|
+
`complete -c hara -n __fish_use_subcommand -a "${top.join(" ")}"`,
|
|
41
|
+
...Object.entries(subs).map(([cmd, sub]) => `complete -c hara -n "__fish_seen_subcommand_from ${cmd}" -a "${sub.join(" ")}"`),
|
|
42
|
+
];
|
|
43
|
+
return lines.join("\n") + "\n";
|
|
44
|
+
};
|
|
45
|
+
/** Render the completion script for a shell, or null if the shell isn't supported. */
|
|
46
|
+
export function completionScript(shell, tree) {
|
|
47
|
+
const gen = { bash, zsh, fish };
|
|
48
|
+
return gen[shell] ? gen[shell](tree) : null;
|
|
49
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join, dirname, resolve } from "node:path";
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "node:fs";
|
|
4
4
|
const PROVIDER_DEFAULTS = {
|
|
5
5
|
anthropic: { model: "claude-opus-4-8", envKey: "ANTHROPIC_API_KEY" },
|
|
6
6
|
qwen: {
|
|
@@ -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", "vimMode"];
|
|
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"];
|
|
@@ -51,12 +51,22 @@ function readProjectConfig(cwd) {
|
|
|
51
51
|
}
|
|
52
52
|
return {};
|
|
53
53
|
}
|
|
54
|
+
/** Write the config 0600 (it can hold `apiKey`) + tighten an existing file. */
|
|
55
|
+
function persistConfig(p, cfg) {
|
|
56
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
57
|
+
writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
58
|
+
try {
|
|
59
|
+
chmodSync(p, 0o600);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* best-effort */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
54
65
|
export function writeConfigValue(key, value) {
|
|
55
66
|
const p = configPath();
|
|
56
67
|
const cfg = readRawConfig();
|
|
57
68
|
cfg[key] = value;
|
|
58
|
-
|
|
59
|
-
writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
69
|
+
persistConfig(p, cfg);
|
|
60
70
|
}
|
|
61
71
|
/** Record (or clear, with cap=null) a confirmed per-model vision capability in `modelVision`. */
|
|
62
72
|
export function setModelVisionOverride(model, cap) {
|
|
@@ -68,8 +78,7 @@ export function setModelVisionOverride(model, cap) {
|
|
|
68
78
|
else
|
|
69
79
|
map[model] = cap;
|
|
70
80
|
cfg.modelVision = map;
|
|
71
|
-
|
|
72
|
-
writeFileSync(p, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
81
|
+
persistConfig(p, cfg);
|
|
73
82
|
}
|
|
74
83
|
/**
|
|
75
84
|
* Effective config. Precedence (high→low): env vars > selected profile >
|
|
@@ -107,7 +116,10 @@ export function loadConfig(opts = {}) {
|
|
|
107
116
|
...(project.mcpServers ?? {}),
|
|
108
117
|
...(profile.mcpServers ?? {}),
|
|
109
118
|
};
|
|
110
|
-
|
|
119
|
+
const hooks = (merged.hooks && typeof merged.hooks === "object" ? merged.hooks : {});
|
|
120
|
+
const notify = (process.env.HARA_NOTIFY ?? merged.notify ?? "off");
|
|
121
|
+
const vimMode = process.env.HARA_VIM === "1" || merged.vimMode === true || merged.vimMode === "true";
|
|
122
|
+
return { provider, apiKey, model, baseURL, approval, sandbox, theme, evolve, assetCapture, computerUse, computerApps, visionModel, visionBaseURL, visionApiKey, modelVision, embedProvider, embedModel, embedBaseURL, embedApiKey, hooks, notify, vimMode, mcpServers, cwd: process.cwd() };
|
|
111
123
|
}
|
|
112
124
|
export function providerEnvKey(provider) {
|
|
113
125
|
return (PROVIDER_DEFAULTS[provider] ?? PROVIDER_DEFAULTS.anthropic).envKey;
|