@nanhara/hara 0.70.0 → 0.95.2

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +395 -0
  2. package/README.md +7 -2
  3. package/dist/agent/compact.js +10 -0
  4. package/dist/agent/context-report.js +33 -0
  5. package/dist/agent/failover.js +53 -0
  6. package/dist/agent/loop.js +120 -13
  7. package/dist/agent/rewind.js +20 -0
  8. package/dist/agent/route.js +47 -0
  9. package/dist/checkpoints.js +91 -0
  10. package/dist/config.js +33 -9
  11. package/dist/context/subdir-hints.js +76 -0
  12. package/dist/cron/runner.js +7 -4
  13. package/dist/exec/jobs.js +82 -0
  14. package/dist/gateway/dingtalk.js +209 -0
  15. package/dist/gateway/discord.js +164 -0
  16. package/dist/gateway/feishu.js +144 -0
  17. package/dist/gateway/matrix.js +188 -0
  18. package/dist/gateway/mattermost.js +206 -0
  19. package/dist/gateway/serve.js +339 -0
  20. package/dist/gateway/sessions.js +89 -0
  21. package/dist/gateway/signal.js +220 -0
  22. package/dist/gateway/slack.js +190 -0
  23. package/dist/gateway/telegram.js +115 -0
  24. package/dist/gateway/tmux-routes.js +135 -0
  25. package/dist/gateway/tts.js +100 -0
  26. package/dist/gateway/wecom.js +383 -0
  27. package/dist/gateway/weixin.js +590 -0
  28. package/dist/index.js +1073 -120
  29. package/dist/org-fleet/enroll.js +28 -5
  30. package/dist/plugins/plugins.js +49 -1
  31. package/dist/profile/profile.js +436 -0
  32. package/dist/providers/anthropic.js +28 -1
  33. package/dist/providers/openai.js +16 -1
  34. package/dist/sandbox.js +15 -12
  35. package/dist/security/external-content.js +57 -0
  36. package/dist/security/permissions.js +211 -0
  37. package/dist/session/session-model.js +36 -0
  38. package/dist/statusbar.js +12 -3
  39. package/dist/tools/builtin.js +49 -2
  40. package/dist/tools/external_agent.js +118 -0
  41. package/dist/tools/send.js +35 -0
  42. package/dist/tools/skill.js +6 -2
  43. package/dist/tools/todo.js +65 -8
  44. package/dist/tools/web.js +4 -3
  45. package/dist/tui/App.js +241 -30
  46. package/dist/tui/run.js +36 -1
  47. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,401 @@ 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.94.1 — unreleased (gateway: relay is on-inbound, not a noisy push)
9
+
10
+ - **Fix the bind output relay to be quiet + platform-correct.** 0.94.0's continuous 3s timer-push flooded chat
11
+ (a message every few seconds) and hit iLink's `ret=-2` (its bot model is passive-reply, no continuous push).
12
+ Replaced with **on-inbound relay**: when you message a bound/registered pane, the daemon captures the pane,
13
+ injects your text, waits ~3s, and replies **once** with the session's NEW output (`🖥 <pane>\n<delta>`). One
14
+ reply per message — no spam, and it fits iLink's "one inbound → one reply" model. Send `?` to peek again.
15
+ - `tmux-routes.ts`: `pickPaneForReply()` (pick+consume, no inject); the timer loop is gone.
16
+ - `serve.ts` onMessage: capture-before → inject → settle → reply with `outputDelta`.
17
+
18
+ ## 0.94.0 — unreleased (gateway: bind output relay — two-way remote terminal)
19
+
20
+ - **Output relay for `hara remote bind`** — the daemon now polls each bound tmux pane and pushes its NEW output
21
+ back to chat once it settles, so a session you drive from your phone is **two-way**: your replies inject in
22
+ (existing), and you SEE the session's output come back (`🖥 <pane>\n<delta>`). Only bound (`bind`) panes; the
23
+ first sighting is baselined (no dump of the pre-existing screen); best-effort send (iLink cold-push may drop a
24
+ message when the chat is cold). Pure `outputDelta` (append / unchanged / scroll-anchor / tail) unit-tested.
25
+ - `src/gateway/tmux-routes.ts`: `boundRoutes`, `capturePane`, `outputDelta`.
26
+ - `src/gateway/serve.ts`: a 3s relay loop in `runGateway` (capture → settle → send delta), cleared on abort.
27
+ - Caveat: a full-screen TUI session (e.g. Claude Code's ink UI) relays the rendered frame; a non-TUI /
28
+ `HARA_TUI=0` / `-p`-style session relays cleaner. 285 tests.
29
+ ## 0.93.0 — unreleased (hara remote — universal chat-driven HITL for any tmux session)
30
+
31
+ - **`hara remote`** — a first-class, agent-agnostic command so ANY terminal session in tmux (Claude Code, codex,
32
+ hara, a plain REPL) can be driven from chat, not just via the Claude-Code wechat-send skill:
33
+ - `hara remote ask "<q>"` — register this pane (one-shot) + push the question to WeChat; your reply injects back.
34
+ - `hara remote bind` — **persistent bind**: every WeChat reply injects into this pane until `unbind` / `/detach`
35
+ (drive a whole session from your phone while you're out, many messages, not just one).
36
+ - `hara remote unbind` · `hara remote status`.
37
+ - Route store gains `mode: "once" | "bind"`; the daemon consumes "once" routes but keeps "bind" ones
38
+ (`pickRoute`/`deliverToTmux`). New `/detach` chat command unbinds all persistent panes from your phone.
39
+ - Generic by design: the injection is just `tmux send-keys`, so it works for any program reading stdin in the
40
+ pane — the agent only needs to call `hara remote ask` when it wants your input (the confirm-loop pattern).
41
+ - 284 tests (+ bind-mode `pickRoute`); persistent-bind verified live (two replies → same pane, route persists).
42
+
43
+ ## 0.92.0 — unreleased (gateway: reply-into-tmux — two-way HITL for any running session)
44
+
45
+ - **Reply routing into an already-running tmux session** — a session you started yourself (Claude Code / codex /
46
+ hara, in tmux) can ping you on WeChat and, while you're away, your reply gets **injected back into that exact
47
+ session** so it continues. No need to launch it under a supervisor or use a blocking tool — borrows the ccgram
48
+ keystroke-injection pattern (the only way to retrofit a session the daemon doesn't own).
49
+ - `src/gateway/tmux-routes.ts`: a route store (`~/.hara/gateway/tmux-routes.json`) + `tmux send-keys` injection.
50
+ The gateway daemon, on an owner reply, injects it into the **oldest live registered pane** (`deliverToTmux`),
51
+ one-shot per ask, dead panes pruned. Pure `pickRoute` + `paneAlive` (list-panes membership) unit-tested.
52
+ - `serve.ts` `onMessage`: a non-slash reply is routed to a waiting pane (and the task isn't re-run) — owner-gated
53
+ by the existing allowlist; only panes that **opted in** are ever touched.
54
+ - The `wechat-send` skill gains `--ask`: send a question + register this tmux pane for the reply.
55
+ - Live-verified the injection path (send-keys → pane received the line); fixed `paneAlive` (display-message was
56
+ too lenient → use `list-panes -a` membership). 283 tests.
57
+
58
+ ## 0.91.0 — unreleased (external_agent: delegate to Claude Code / Codex)
59
+
60
+ - **`external_agent` tool** — hand a self-contained task to an EXTERNAL coding agent (**`claude`** / **`codex`**)
61
+ running headless in the current dir, and get its result back. Zero new deps — drives each agent's native
62
+ headless flag (`claude -p … --output-format text --permission-mode …`, `codex exec … --cd … --sandbox …`)
63
+ over `node:child_process`, not openclaw's heavier ACP/acpx stack. Pick the best engine per task.
64
+ - **Gated**: `kind:"exec"` → inherits the approval flow; and because read-only fan-out sub-agents only get the
65
+ `READONLY_TOOLS` allow-list, this privileged tool is **never** exposed to them.
66
+ - **Trust tiers** `externalAgentTrust` / `HARA_EXTERNAL_AGENT_TRUST` = `off | gated (default) | full`. `gated`
67
+ runs the external agent in its safe sub-mode (`claude --permission-mode plan/acceptEdits`,
68
+ `codex --sandbox read-only/workspace-write`); the dangerous bypass/full-access sub-modes are only reachable at
69
+ `full`. Backend allow-list (claude/codex), timeout + output cap. Pure `buildExternalArgv` unit-tested.
70
+
71
+ ## 0.90.0 — unreleased (gateway: WeCom + Signal → 10 platforms)
72
+
73
+ - **WeCom (企业微信)** — connects out to WeCom's AI-Bot WebSocket gateway (no public webhook). `HARA_WECOM_BOT_ID`
74
+ + `HARA_WECOM_SECRET`. Two-way: inbound text + images (incl. AES-decrypted attachments → `~/.hara/wecom/media`),
75
+ outbound text/image/file. Zero new deps (native WebSocket + node:crypto).
76
+ - **Signal** — talks to a local **signal-cli** daemon (JSON-RPC). `HARA_SIGNAL_RPC_URL` + `HARA_SIGNAL_NUMBER`.
77
+ Inbound text + image attachments, outbound text/file; phone numbers redacted in logs. signal-cli is an external
78
+ daemon the user runs (documented). Zero new npm deps.
79
+ - Pure parsers `parseWecomMessage` / `parseSignalMessage` unit-tested. docs/gateway.md + README updated to 10
80
+ platforms. Ported from the openclaw + hermes adapters.
81
+
82
+ ## 0.89.0 — unreleased (gateway: Slack · Mattermost · Matrix · DingTalk + docs)
83
+
84
+ - **Four more platforms**, all zero-new-dep (native WebSocket / `fetch`), same `ChatAdapter` seam:
85
+ - **Slack** — Socket Mode (connects out, no public URL). `HARA_SLACK_APP_TOKEN` (xapp-) + `HARA_SLACK_BOT_TOKEN`
86
+ (xoxb-). Inbound text + image files (downloaded via the bot token); outbound text + file upload.
87
+ - **Mattermost** — v4 WebSocket + REST. `HARA_MATTERMOST_URL` + `HARA_MATTERMOST_TOKEN` (bot/PAT). Two-way images.
88
+ - **Matrix** — `/sync` long-poll. `HARA_MATRIX_HOMESERVER` + `HARA_MATRIX_TOKEN` + `HARA_MATRIX_USER_ID`.
89
+ Two-way images; **unencrypted rooms only in v1** (no E2EE).
90
+ - **DingTalk (钉钉)** — Stream Mode (connects out). `HARA_DINGTALK_CLIENT_ID` + `HARA_DINGTALK_CLIENT_SECRET`.
91
+ Text in/out via the per-message `sessionWebhook`; **v1: no file send, inbound images arrive as `[图片]`**.
92
+ - Each platform's wire parser (`parseSlackEvent` / `parseMattermostPost` / `parseMatrixEvent` / `parseMxc` /
93
+ `parseDingtalkMessage`) is pure + unit-tested.
94
+ - **Docs** — new **[docs/gateway.md](docs/gateway.md)** documents all 8 platforms (Telegram · WeChat · Discord ·
95
+ Feishu/Lark · Slack · Mattermost · Matrix · DingTalk): a capabilities table, common config
96
+ (`HARA_GATEWAY_ALLOWED`, `--cwd`, slash commands, two-way images), and per-platform setup. Linked from README.
97
+ - Ported by studying the openclaw + hermes adapter implementations; live-tested per platform as tokens become
98
+ available.
99
+
100
+ ## 0.88.0 — unreleased (gateway: Feishu/Lark adapter)
101
+
102
+ - **Feishu/Lark** — `hara gateway --platform feishu` via the official `@larksuiteoapi/node-sdk` (the one new
103
+ dependency): a **WSClient long-connection** for inbound (no public webhook needed — fits the local daemon) and
104
+ the REST Client for outbound. Inbound text / rich-text post / image / file / audio (media downloaded to
105
+ `~/.hara/feishu/media` → the agent SEES images); outbound text + `sendFile` (image upload → image message,
106
+ else file upload → file message) so `send_file` / `/send` work. Creds via `HARA_FEISHU_APP_ID` /
107
+ `HARA_FEISHU_APP_SECRET` (+ `HARA_FEISHU_DOMAIN=lark` for larksuite.com), users via `HARA_GATEWAY_ALLOWED`
108
+ (open_id). v1 = p2p DMs; group support is a fast-follow. `parseFeishuContent` / `flattenPost` are pure + tested.
109
+
110
+ ## 0.87.0 — unreleased (gateway: Discord adapter)
111
+
112
+ - **Discord** — `hara gateway --platform discord` connects to the Discord gateway over Node's native global
113
+ WebSocket (zero new dep on Node ≥ 22): HELLO→heartbeat→IDENTIFY, dispatches MESSAGE_CREATE, auto-reconnects.
114
+ Inbound text + image attachments (downloaded to `~/.hara/discord/media` → the agent SEES them); outbound text
115
+ (2000-char chunks) and `sendFile` (multipart) so `send_file` / `/send` work. Token via `HARA_DISCORD_TOKEN`,
116
+ users via `HARA_GATEWAY_ALLOWED` (Discord user ids). Needs the bot's privileged Message Content Intent.
117
+ Same `ChatAdapter` seam as Telegram/WeChat — all cross-platform gateway logic worked unchanged.
118
+
119
+ ## 0.86.1 — unreleased (gateway: Telegram reaches image parity with WeChat)
120
+
121
+ - **Telegram two-way images** — `parseTelegramUpdate` now accepts photo messages (caption or a `[图片]` marker)
122
+ and the receive loop downloads the largest photo to `~/.hara/telegram/media` → `InboundMsg.images`, so the
123
+ agent sees it just like on WeChat. Added `sendFile` (sendPhoto for images, sendDocument otherwise) so `send_file`
124
+ and `/send` work on Telegram too. All the cross-platform gateway plumbing (send_file tool, in-chat system
125
+ context, stuck-guard, `-p` image attach/describe) was already platform-agnostic — only the adapter changed.
126
+
127
+ ## 0.86.0 — unreleased (gateway: the agent SEES inbound images)
128
+
129
+ - **Inbound image understanding** — a photo you send in chat now reaches the model as a real image, not just a
130
+ `[图片: /path]` text breadcrumb. The WeChat adapter routes downloaded images into `InboundMsg.images`; the
131
+ gateway forwards their paths to the headless run (`HARA_GATEWAY_IMAGES`); the `-p` handler attaches them inline
132
+ for a vision-capable main model, or describes them via the configured `visionModel` sidecar and folds the
133
+ description into the message for a text-only model (e.g. glm-5 main + qwen3.7-plus vision). Together with 0.85's
134
+ `send_file`, the chat is now two-way for images. Verified live: glm-5 correctly described a sent picture via the
135
+ qwen3.7-plus sidecar.
136
+
137
+ ## 0.85.0 — unreleased (gateway: agent sends files in conversation + stuck-guard)
138
+
139
+ - **`send_file` tool** — the agent can now deliver a file/image to the chat *conversationally* ("生成 X 发我"),
140
+ not just via the manual `/send` command. Self-gates on `HARA_GATEWAY`, so it appears only inside `hara gateway`.
141
+ It queues the path to a per-message outbox file; the daemon drains it after the headless run and delivers each
142
+ file via the platform adapter (`sendMediaFile` — images inline, others as attachments). This closes the gap
143
+ where hara, asked to "send an image", would generate it fine but then try to UI-automate the desktop WeChat
144
+ client with the `computer` tool (wrong surface, and blind without a vision model) and silently deliver nothing.
145
+ - **In-chat system context** — when running under the gateway, the agent is told it's in a `${platform}` chat:
146
+ deliver files via `send_file` (the only channel that reaches the peer), do **not** drive the desktop client /
147
+ `computer` tool, and never claim a file was sent unless `send_file` succeeded.
148
+ - **Stuck-guard (gateway only)** — once per run, if the agent keeps repeating one non-read tool (≥5×) or acts
149
+ blind (≥2 screenshots it can't read), a one-shot self-check nudge is injected so it steps back and picks a
150
+ working path instead of grinding forever (no human there to hit Esc). Off outside the gateway.
151
+
152
+ ## 0.84.0 — unreleased (gateway: file + image send/receive)
153
+
154
+ - **Receive** — a file / image / (untranscribed) voice you send is now downloaded + AES-decrypted to a local
155
+ file under `~/.hara/weixin/media/`, and hara is handed a `[图片|文件 name|语音: /path]` reference so it can
156
+ read/process it (images → a vision-capable model). Ported iLink's inbound media path (CDN download →
157
+ AES-128-ECB decrypt) including the image `aeskey` hex-hack, the `encrypt_query_param` vs `encrypted_query_param`
158
+ naming, the conditional PKCS7 strip, the CDN-host SSRF allowlist, and a 128 MiB cap. Media-only messages
159
+ (no text) are now processed too.
160
+ - **Send** — `sendMediaFile` sends any local file: images go **inline** (`image_item`), everything else
161
+ (zip / pdf / doc / audio / …) as a **file attachment** (`file_item`) carrying the filename. New `/send <path>`
162
+ command (absolute / `~` / relative to the chat's dir). Voice replies + `/say` now ride the same generic path.
163
+ - `ChatAdapter.sendAudio?` → **`sendFile?`** (generic seam). Live-validated end-to-end: ZIP attachment + inline
164
+ image delivered. 265 tests.
165
+
166
+ ## 0.83.0 — unreleased (gateway: voice replies — pluggable TTS, /voice + /say)
167
+
168
+ - The gateway can now **reply with voice** — a WeChat audio-file attachment (iLink's native voice bubble is
169
+ unreliable). `/voice` toggles spoken replies for a chat (each reply is also sent as audio); `/say <text>`
170
+ speaks one message. Ported iLink's media-upload path (getuploadurl → AES-128-ECB → CDN → `file_item`) into
171
+ `weixin.ts` byte-exact (incl. the dual AES-key encoding: hex in getuploadurl, base64-of-hex in sendmessage)
172
+ + live-validated end-to-end.
173
+ - **Pluggable TTS** (`src/gateway/tts.ts`) — config-driven via env, nothing vendor-hardcoded, mirrors the video
174
+ project's provider design; both API and local:
175
+ - `say` (default) — local macOS, ~0.5s, Chinese voices (Tingting…), zero config → m4a.
176
+ - `openai` — any OpenAI-compatible `/audio/speech` endpoint (point `HARA_TTS_BASE_URL` at Aliyun DashScope or
177
+ a local TTS server); reuses the existing `openai` dep, no new dependency.
178
+ - `cmd` — a configurable local command (point `HARA_TTS_CMD` at VoxCPM or any local TTS; text on stdin).
179
+ - Select via `HARA_TTS_PROVIDER` (+ `HARA_TTS_VOICE`/`MODEL`/`BASE_URL`/`API_KEY`/`CMD`); a failed API
180
+ provider falls back to local `say`. New optional `ChatAdapter.sendAudio?` seam (weixin implements it).
181
+
182
+ ## 0.82.2 — unreleased (gateway: fix the voice tag that made hara disclaim)
183
+
184
+ - 0.82.1's bare `[voice message]` prefix backfired — hara read it as raw audio to process and replied "I can't
185
+ handle voice," even though the message was already transcribed text (verified: the transcription reaches hara
186
+ correctly). Replaced with an explicit note ("…already transcribed to text below — just reply to it normally,
187
+ you don't have or need the audio") so hara answers the content instead of disclaiming.
188
+
189
+ ## 0.82.1 — unreleased (gateway: tag transcribed WeChat voice messages)
190
+
191
+ - WeChat **voice input already works** — iLink transcribes voice server-side (`voice_item.text`) and the gateway
192
+ reads it. But the text reached hara unlabeled, so when the spoken words referenced "this voice," hara replied
193
+ as if it only got text. Now a transcribed voice message is tagged `[voice message] <text>`, so hara knows the
194
+ input came from voice. (Sending voice back is deferred — iLink's native voice bubble is unreliable per the
195
+ Hermes reference; only audio-file attachments are possible, and that needs the media-upload + TTS path.)
196
+
197
+ ## 0.82.0 — unreleased (gateway: roam projects + threads from chat — /cd, /pwd, project-scoped /sessions)
198
+
199
+ - A chat can now **switch working directory at runtime**, and its session follows: `/cd <dir>` (absolute, `~`,
200
+ or relative to the current dir) moves the chat into that project and opens that project's own thread; `/pwd`
201
+ shows the current dir + thread; `/sessions` lists the **current dir's** threads; `/new` forks the current
202
+ dir's thread; `/resume <id>` jumps to a thread and adopts its dir so it runs in the right place. Each
203
+ (chat, dir) pair gets its own stable, resumable session id (`<platform>-<chatId>-<cwdTag>[-fork]`) — so one
204
+ gateway roams across projects while keeping per-project history, and switching back resumes. Foundation for
205
+ a future desktop client.
206
+ - The chat-session store (`~/.hara/gateway/chats.json`) is now cwd-aware (`chatContext`/`chatCd`); old entries
207
+ migrate in place (keep their existing thread). Tested.
208
+
209
+ ## 0.81.6 — unreleased (headless `-p --resume`: auto-compact long sessions)
210
+
211
+ - The `hara -p … --resume <id>` / `--continue` path (used by the chat gateway and cron) now runs
212
+ `maybeAutoCompact` before saving, so a long chat/cron thread **auto-compacts** (summarizes old turns)
213
+ instead of growing until it overflows the model's context window. Silent in headless mode (no notify) so
214
+ nothing leaks into a captured reply. Opt-out via `autoCompact: false` / `HARA_AUTO_COMPACT=0`. Previously
215
+ auto-compaction was wired only into the interactive/TUI loops, so gateway/cron sessions could overflow.
216
+
217
+ ## 0.81.5 — unreleased (gateway: default workspace = ~/.hara/workspace — dir-free, Hermes-style)
218
+
219
+ - `hara gateway` (no `--cwd`) now operates in a dedicated **`~/.hara/workspace`** (created + seeded on first
220
+ run) instead of the launch directory — so the chat bot is dir-free + safe-by-default (like Hermes' own
221
+ `~/.hermes`), never landing full-auto on whatever repo you happened to launch from. `--cwd <dir>` still
222
+ targets a real project.
223
+ - This only sets where *files* are created. hara's **global memory (`~/.hara/memory`) and roles
224
+ (`~/.hara/roles` + B-end `~/.hara/org-roles`) are cwd-independent and always loaded**, so a chat session
225
+ shares the same global brain as the terminal CLI regardless of workspace. `~/.hara/` is hara's home:
226
+ memory · roles · sessions · skills · checkpoints · config · per-platform gateway state.
227
+
228
+ ## 0.81.4 — unreleased (gateway: clean chat replies — strip MCP logs + token footer)
229
+
230
+ - The chat gateway scraped the `hara -p` subprocess output and sent it verbatim, so WeChat/Telegram replies
231
+ were wrapped in CLI chrome — `mcp: … → N tool(s)`, `mcp: … failed …`, and the `model · ↑N ↓N tok` footer.
232
+ New `cleanReply()` strips those, leaving just the assistant's answer. (Tool/diff streaming for multi-step
233
+ tasks is untouched; this only removes the always-present MCP-startup + token-footer noise.) Tested.
234
+
235
+ ## 0.81.3 — unreleased (fix: gateway/cron subprocess spawn when hara runs via the `hara` bin symlink)
236
+
237
+ - `selfArgv()` (used by the chat gateway and the cron tick to spawn a fresh `hara` per task) **dropped the
238
+ script path** when hara was invoked via the installed `hara` bin symlink: that path has no `.js` extension,
239
+ so the old extension-based heuristic returned just `[node]`, making the spawn `node -p <text> --approval …`
240
+ — which node rejected with `bad option: --approval` (the gateway replied with that error instead of running).
241
+ Now keyed on whether `execPath` is node (the real compiled-binary discriminator): it re-invokes the entry
242
+ (`dist/index.js` **or** the bin symlink) under node, and only re-invokes `execPath` directly for a true
243
+ single-binary build. Test covers all three cases.
244
+
245
+ ## 0.81.2 — unreleased (gateway: `--cwd` flag to point at a workspace without `cd`)
246
+
247
+ - `hara gateway [--platform …] --cwd <dir>` sets the directory hara operates in for each incoming message,
248
+ so you can launch the daemon against a chosen workspace without `cd`-ing first (resolved to an absolute
249
+ path). Defaults to the current dir as before. Recommended pattern: a dedicated safe scratch dir (e.g.
250
+ `~/work/projects/tools/hara`) rather than a sensitive repo, since each message runs `--approval full-auto`.
251
+
252
+ ## 0.81.1 — unreleased (WeChat gateway: auto-allowlist the bot owner)
253
+
254
+ - `hara gateway --platform weixin` now **auto-allows the bot owner** (the iLink `user_id` from login — i.e.
255
+ whoever scanned the QR), since on a personal-WeChat bot that id equals the `from_user_id` on your own
256
+ messages. Removes the "run once with an empty allowlist to discover your wxid, then re-run" dance — just
257
+ log in and run the daemon. Additional ids can still be added via `HARA_GATEWAY_ALLOWED`. (Validated the full
258
+ receive→reply round-trip against the live iLink server.)
259
+
260
+ ## 0.81.0 — unreleased (WeChat (iLink) gateway adapter — drive your local hara from personal WeChat)
261
+
262
+ - **`hara gateway --platform weixin`** adds **WeChat (personal)** as a second chat channel, via Tencent's
263
+ official iLink bot API (`ilinkai.weixin.qq.com`, no ban risk) — the same `ChatAdapter` seam as Telegram,
264
+ so per-chat resumable sessions / allowlist / `/new` `/sessions` `/resume` all carry over. Text DMs (v1):
265
+ the bot reads & edits files and runs bash in the gateway's cwd, and replies in WeChat.
266
+ - **`hara gateway --platform weixin --login`** — interactive QR login (scan with WeChat); saves
267
+ `{account_id, token, base_url}` to `~/.hara/weixin/creds.json`. The per-peer `context_token` and the
268
+ long-poll cursor are persisted, so replies route correctly and a restart resumes mid-stream. Handles
269
+ iLink's `-14` / stale-`-2` session-expiry (tokenless retry on send; re-login prompt on poll). Built-in
270
+ fetch; the login QR renders via the **optional** `qrcode-terminal` dep (falls back to printing the URL).
271
+ No crypto needed on the text path.
272
+ - The allowlist now logs the sender id of unauthorized messages, so you can discover your WeChat id to add
273
+ to `HARA_GATEWAY_ALLOWED`. 256 tests.
274
+
275
+ ## 0.80.0 — unreleased (chat gateway — drive your local hara from Telegram; + headless session continuity)
276
+
277
+ - **`hara gateway`** (opt-in daemon, hara's first long-running process) lets you drive your **local** hara from
278
+ a chat app — **Telegram** first ("message your bot → hara reads/edits files, runs bash, replies"). Each chat
279
+ is a **continuous, resumable session**: `/new` forks a fresh thread · `/sessions` lists · `/resume <id>`
280
+ jumps to one — backed by the stored sessions. Access is **allowlist-gated** (`HARA_GATEWAY_ALLOWED` user ids;
281
+ empty = nobody, never wide-open); token from `HARA_TELEGRAM_TOKEN`. Generic `ChatAdapter` shape → WeChat-iLink
282
+ / Feishu are same-interface fast-follows. **Zero new dep** (built-in `fetch` long-poll). `src/gateway/` + tests.
283
+ - **`hara -p "<task>" --resume <id>` / `--continue`** now does **headless session continuity** — loads the
284
+ session, appends the prompt, runs, saves it back (a `--resume <id>` with no match is created with that id);
285
+ plain `hara -p` stays stateless. Useful for cron, scripts, and the gateway's per-chat threads.
286
+ - This is the chat layer of the multi-terminal plan; the ACP server (for a custom App / editors) is deferred
287
+ until that App exists. 249 tests.
288
+
289
+ ## 0.79.0 — unreleased (app-level failover — retry an errored turn on a fallback model)
290
+
291
+ - **`fallbackModel`** (opt-in): when a turn ends in a *recoverable* provider error — overload (529/503),
292
+ rate-limit (429), timeout, transient 5xx, or context-overflow — hara retries it **once on the fallback
293
+ model** instead of dying (a different model may not be overloaded / may have a larger window). `auth` errors
294
+ and user interrupts are never auto-retried. The SDK's transient `maxRetries:4` still handles the first line;
295
+ this is the app layer for what's left. `fallbackBaseURL`/`fallbackApiKey` default to the primary's.
296
+ - Errors now also carry a short **actionable hint** by kind (auth → check key · overloaded → set fallbackModel
297
+ · context-overflow → /compact). Classification handles DashScope/GLM/Qwen **Chinese** error strings too. New
298
+ `src/agent/failover.ts` (pure classify + decide, fully tested); runAgent just executes the decision, guarded
299
+ to one retry. Tests (244 total). Completes the Sprint-2 spine (⑤ rewind + ⑥ failover).
300
+
301
+ ## 0.78.0 — unreleased (file-state checkpoints — shadow-git "undo the agent's edits")
302
+
303
+ - **Durable file checkpoints** complete the rewind story (beyond the edit-only in-memory undo, which missed
304
+ `bash`-made changes). Before each turn, hara snapshots the whole working tree into a **shadow git repo** kept
305
+ OUTSIDE the project (`~/.hara/checkpoints/<hash>`, `GIT_DIR` there + `GIT_WORK_TREE` = project root) — so it
306
+ captures everything (incl. `bash`), **never touches your real `.git`/index**, and the model never sees it.
307
+ **`/checkpoint`** lists them; **`/checkpoint restore <n>`** reverts files to one.
308
+ - **Safe by construction**: restore snapshots the current state first (so it's undoable) and only reverts
309
+ changed/deleted files — it **never deletes files created since** the checkpoint. Heavy/derived dirs
310
+ (`node_modules`, `.git`, `dist`, …) are excluded; only the first snapshot is a full scan (git's index makes
311
+ the rest incremental). Default on; opt out with `fileCheckpoints:false` / `HARA_CHECKPOINTS=0`.
312
+ `src/checkpoints.ts` + tests (241 total).
313
+
314
+ ## 0.77.0 — unreleased (/rewind — fork the conversation back to an earlier turn)
315
+
316
+ - New **`/rewind`** — `/rewind` lists recent user turns; `/rewind <n>` forks the conversation back to before
317
+ one, so when the agent goes down a wrong path you can snip back to a good turn and re-steer, instead of
318
+ `/clear` (lose everything) or living with a poisoned context (codex's backtrack). **Conversation only —
319
+ file edits are NOT reverted** (durable file-state checkpoints via shadow-git are the planned heavier
320
+ follow-up). Both UIs; pure in-memory + session store. `src/agent/rewind.ts` + tests (239 total).
321
+
322
+ ## 0.76.0 — unreleased (/context — see what's filling the context window)
323
+
324
+ - New **`/context`** command: a token-spend breakdown of the conversation — which tool's output, assistant
325
+ text, and your messages are using the window (biggest first, with the share of the model's window) — so on a
326
+ long session you can see *why* you're near the limit, not just the `ctx%` number. Pairs with auto-compaction.
327
+ chars/4 estimate, zero-dep, both UIs. `src/agent/context-report.ts` + tests (237 total).
328
+
329
+ ## 0.75.0 — unreleased (lazy subdirectory AGENTS.md / CLAUDE.md — monorepo-local conventions reach the model)
330
+
331
+ - When a tool touches a directory not seen yet this session, hara loads that directory's **`AGENTS.md` /
332
+ `CLAUDE.md`** (the local conventions for that package) and appends it to the tool result — so in a monorepo,
333
+ `packages/api/AGENTS.md` or `growth/CLAUDE.md` reaches the model exactly when work moves there, not just the
334
+ root doc loaded at startup. Each directory loads once per session; only dirs **under cwd** (startup already
335
+ covers cwd→root); paths are taken from a tool's `path` or path-like tokens in a `bash` command. Zero-dep,
336
+ additive (never removes context). `src/context/subdir-hints.ts` + tests (235 total).
337
+
338
+ ## 0.74.1 — unreleased (bash output keeps head + tail, not just head)
339
+
340
+ - Long command output (build / test logs) is now truncated **keeping both the head and the tail** instead of
341
+ only the first 100k chars — so the model still sees the **end**, where the error/result usually is (plain
342
+ head-truncation cut exactly the part that matters). `read_file` truncation is unchanged. New `capHeadTail`
343
+ used on `bash` success + failure output; + test (233 total).
344
+
345
+ ## 0.74.0 — unreleased (auto-compaction — summarize before the context overflows, like Claude Code)
346
+
347
+ - **Auto-compaction**: when a turn fills the model's context past ~85%, hara now **summarizes the conversation
348
+ and continues automatically** (a "✻ Auto-compacting conversation…" notice) instead of only warning — so a
349
+ long session no longer dead-ends at the context limit. Opt out with `autoCompact: false` (or
350
+ `HARA_AUTO_COMPACT=0`); below the threshold the ≥80% warning still shows. Works in the TUI **and** the classic
351
+ REPL.
352
+ - The summarize-and-replace logic is now a single shared `compactConversation` (manual `/compact` and auto both
353
+ use it — no drift); it keeps the working-memory notes that survive the wipe and resets the context gauge off
354
+ the (small) summary. New `src/agent/compact.ts` (`shouldAutoCompact` trigger) + test (232 total).
355
+ - The two distinct controls (unchanged, just clarified): **`/compact`** summarizes → replaces history to free
356
+ tokens while keeping the thread; **`/clear`** (= `/reset`) wipes the conversation for a fresh start.
357
+
358
+ ## 0.73.0 — unreleased (Sprint 2: background shell jobs — run dev servers / watchers without blocking)
359
+
360
+ - **`bash {background: true}`** starts a long-lived command (dev server, `tsc --watch`, a long build) as a
361
+ background job and returns a job id immediately — the agent keeps working instead of blocking on a command
362
+ that never exits. New **`job` tool** (`list` / `tail` / `kill`) manages them; output is captured to a capped
363
+ tail buffer. Background jobs reuse `bash`'s exact **sandbox write-confinement** (shared `shellCommand`) and
364
+ pass the same permission gate when started; they're the agent's own children and are **terminated when hara
365
+ exits** (no orphaned dev servers). `src/exec/jobs.ts` + tests (231 total). First slice of the exec-subsystem
366
+ gap from the 4-expert analysis — persistent/interactive PTY + docker/ssh backends come next.
367
+
368
+ ## 0.72.0 — unreleased (per-turn model routing: strong model for code, cheap/general for trivial turns)
369
+
370
+ - **Opt-in per-turn model routing** — the answer to "use a coding model for real work, a cheap/general model
371
+ for trivial chat" *without* splitting hara into two tools. Set `routeModel` (+ optional `routeBaseURL` /
372
+ `routeApiKey`, which default to the primary's) and each turn routes by its latest user message: trivial,
373
+ non-coding turns (short · single-line · no code / URL · no action keyword) go to `routeModel`; anything with
374
+ a coding/action signal stays on the primary `model`. **Conservative by design** — a coding tool errs toward
375
+ the strong model, so routing only fires on clear Q&A / chit-chat.
376
+ - Implemented as a transparent provider wrapper at the single main-chat entry (`withRouting`), so every
377
+ interactive / `-p` / TUI turn gets it while role / review / sub-agent providers stay untouched. The decision
378
+ reads the last `role:"user"` message (tool results are `role:"tool"`), so it's stable across a turn's tool
379
+ rounds. New `src/agent/route.ts` + tests. Env: `HARA_ROUTE_MODEL` / `HARA_ROUTE_BASE_URL` / `HARA_ROUTE_API_KEY`.
380
+
381
+ ## 0.71.0 — unreleased (Sprint 1: governance + safety — command permissions · untrusted-content wrapping · structured compaction)
382
+
383
+ Three zero-dep items distilled from a 4-expert (codex/cc-haha/hermes/openclaw) C-end gap analysis; all reinforce hara's governance moat.
384
+
385
+ - **Command-level permission rules for `bash`** (`~/.hara/permissions.json` + project `.hara/permissions.json`):
386
+ an `allow` / `deny` + read-only-autorun policy that **composes with** approval modes. A **deny** rule blocks a
387
+ command even in `--full-auto`; an **allow** rule (or a recognized **read-only** command — `ls`/`grep`/`git
388
+ status`…) auto-runs even in `suggest` mode — ending the "confirm every command vs. unguarded full-auto" false
389
+ choice. Commands are canonicalized (unwrap `bash -lc`, strip `NODE_ENV=…`/`timeout` wrappers) so approving
390
+ `npm test` once sticks across phrasings; a compound command (`&&`/`||`/`;`/`|`) takes its **strictest** part's
391
+ decision; anything unparseable (`$()`/backticks/unbalanced quotes) fails **closed** to a prompt. New
392
+ `hara permissions` (list · `--init [--project]`). `src/security/permissions.ts` + tests.
393
+ - **Untrusted-content wrapping** for `web_fetch` / `web_search`: external page/search text is wrapped in a
394
+ "treat as DATA, not instructions" notice with a **random per-call boundary id**, and homoglyph / zero-width
395
+ tricks (fullwidth/CJK/math angle brackets, ZWSP/BOM/soft-hyphen) are defanged so a hostile page can't forge
396
+ the boundary or smuggle hidden instructions — closing the realistic indirect-prompt-injection vector for an
397
+ agent that holds `bash`. `src/security/external-content.ts` + tests.
398
+ - **Structured `/compact` template**: replaces the one-line summary with a 6-section brief (goal · key decisions
399
+ · files & code · errors & fixes · current state · next step) that **quotes the user's most recent request
400
+ verbatim** and drafts in an `<analysis>` scratchpad first — so resuming after a compaction doesn't drift or
401
+ drop the error→fix history.
402
+
8
403
  ## 0.70.0 — unreleased (B-end: devices pull their governed org-role bundle)
9
404
 
10
405
  - An enrolled device now **syncs its digital-employee roles from hara-control** — `GET {gateway}/v1/roles`
package/README.md CHANGED
@@ -2,17 +2,21 @@
2
2
 
3
3
  **A coding agent CLI that runs like an engineering org.**
4
4
 
5
+ ![hara — your coding agent, driven from 10 chat platforms](docs/assets/hara-gateway-hero.png)
6
+
5
7
  > Think "Claude Code, but it operates as a configurable, governed *organization* of role-agents" —
6
8
  > with routing boundaries, a dispatcher, a single source-of-truth data layer, human-in-the-loop
7
9
  > approvals, and cron autonomy.
8
10
 
9
- 🚧 **v0.33** · TypeScript · local-first · Apache-2.0
11
+ 🚧 **v0.89** · TypeScript · local-first · Apache-2.0
10
12
 
11
13
  **Highlights**
12
14
  - **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.
15
+ - **Drive it from chat** — `hara gateway` runs your local hara from **Telegram · WeChat · Discord · Feishu/Lark · Slack · Mattermost · Matrix · DingTalk · WeCom · Signal** (10 platforms), with **two-way images** (send a photo → it sees it; ask for a file → it sends one), per-chat resumable sessions, and project roaming. Connects out — no public webhook. See **[docs/gateway.md](docs/gateway.md)**.
13
16
  - **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
17
  - **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
18
  - **Multi-provider, all streamed** — Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) with live Markdown + visible reasoning.
19
+ - **Delegate to other agents** — the **`external_agent`** tool hands a self-contained task to **Claude Code** or **Codex** running headless, and returns the result — so you pick the best engine per task. Gated by approval, trust-tiered, and never exposed to read-only sub-agents.
16
20
  - **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
21
 
18
22
  Track it: https://github.com/hara-cli/hara · https://hara.run
@@ -240,10 +244,11 @@ turn, or **`/undo`** to revert the last edit. In-session **`/diff`**, **`/review
240
244
  - **Project context**: auto-loads `AGENTS.md` (the cross-tool standard) walking up to the repo root; `hara init` writes one by analyzing the repo.
241
245
  - **`@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.
242
246
  - **Multi-provider**: Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) — **all streamed live**.
247
+ - **Chat gateway**: drive your local hara from a chat app — **Telegram · WeChat · Discord · Feishu/Lark · Slack · Mattermost · Matrix · DingTalk**. The daemon connects out (no public webhook), with per-chat sessions, project roaming (`/cd`), and **two-way images** (send a photo → it sees it; ask for a file → it sends one). Setup per platform: **[docs/gateway.md](docs/gateway.md)**.
243
248
 
244
249
  ### Roadmap
245
250
 
246
- **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** · **Docker image** · `/compact` context management.
251
+ **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`)** · **chat gateway (8 platforms, two-way images)** · **single-binary distribution** · **Docker image** · `/compact` context management.
247
252
  **Next:** SSOT data authority · an enterprise control-plane (fleet + central token management).
248
253
 
249
254
  ## Security
@@ -0,0 +1,10 @@
1
+ // Auto-compaction trigger — the small, testable decision behind "compact the conversation before it overflows"
2
+ // (à la Claude Code's auto-compact). The actual summarize-and-replace I/O lives in index.ts (compactConversation),
3
+ // which reuses the manual /compact path; this just decides *when* to fire.
4
+ /** Auto-compact once the last turn used ≥ this % of the model's context window. */
5
+ export const AUTO_COMPACT_PCT = 85;
6
+ /** Whether to auto-compact now: enabled, the history is substantial enough to be worth summarizing, and the
7
+ * last turn filled the context past the threshold (so the NEXT turn would risk overflow). */
8
+ export function shouldAutoCompact(ctxPct, historyLen, autoCompact, threshold = AUTO_COMPACT_PCT) {
9
+ return autoCompact && historyLen >= 4 && ctxPct >= threshold;
10
+ }
@@ -0,0 +1,33 @@
1
+ import { contextWindow } from "../statusbar.js";
2
+ const est = (s) => Math.ceil((s?.length ?? 0) / 4);
3
+ /** Estimate token spend by category (your messages / assistant / each tool's output) across the history. */
4
+ export function analyzeContext(history) {
5
+ const by = new Map();
6
+ const add = (k, n) => {
7
+ by.set(k, (by.get(k) ?? 0) + n);
8
+ };
9
+ for (const m of history) {
10
+ if (m.role === "user")
11
+ add("your messages", est(m.content));
12
+ else if (m.role === "assistant")
13
+ add("assistant", est(m.text));
14
+ else if (m.role === "tool")
15
+ for (const r of m.results)
16
+ add(`tool: ${r.name}`, est(r.content));
17
+ }
18
+ const total = [...by.values()].reduce((a, b) => a + b, 0);
19
+ const rows = [...by.entries()]
20
+ .map(([label, tokens]) => ({ label, tokens, pct: Math.round((tokens / (total || 1)) * 100) }))
21
+ .sort((a, b) => b.tokens - a.tokens);
22
+ return { total, rows };
23
+ }
24
+ /** Human-readable breakdown, biggest first, with the share of the model's window used + a trim hint. */
25
+ export function formatContextReport(history, model) {
26
+ const { total, rows } = analyzeContext(history);
27
+ if (!rows.length)
28
+ return "Context is empty.";
29
+ const pctWin = Math.min(99, Math.round((total / contextWindow(model)) * 100));
30
+ const lines = rows.slice(0, 8).map((r) => ` ${String(r.pct).padStart(3)}% ${r.label} (~${r.tokens.toLocaleString()} tok)`);
31
+ const tip = pctWin >= 80 ? "\n → near the limit; /compact to summarize or /clear to reset." : "";
32
+ return `Context ~${total.toLocaleString()} tok (~${pctWin}% of ${model}'s window):\n${lines.join("\n")}${tip}`;
33
+ }
@@ -0,0 +1,53 @@
1
+ // App-level failover — what to do when a provider turn ENDS in an error (the SDK already retried transient
2
+ // 429/5xx via maxRetries; this handles what's left). Two recoveries: a context-overflow → compact + retry,
3
+ // and a persistent/overloaded error → retry once on a configured FALLBACK model. The decision is pure +
4
+ // tested here; runAgent just executes it (and guards each recovery to once, so no retry loops).
5
+ /** Classify a provider error from its message (+ HTTP status if known). Message patterns include the
6
+ * Chinese strings DashScope/GLM/Qwen return, since hara targets those endpoints. */
7
+ export function classifyError(msg, status) {
8
+ const m = (msg || "").toLowerCase();
9
+ if (m === "interrupted")
10
+ return "interrupted";
11
+ if (status === 401 || status === 403 || /unauthor|invalid api key|invalid.*key|forbidden|permission denied|无效|鉴权/.test(m))
12
+ return "auth";
13
+ if (status === 429 || /rate.?limit|too many requests|\b429\b|请求过于频繁|限流/.test(m))
14
+ return "rate_limit";
15
+ if (status === 529 || status === 503 || /overload|capacity|service unavailable|temporarily unavailable|\b503\b|\b529\b|繁忙|过载/.test(m))
16
+ return "overloaded";
17
+ if (/context length|context window|maximum context|maximum.*token|too long|reduce the length|超过最大长度|上下文长度|输入过长/.test(m))
18
+ return "context_overflow";
19
+ if (/timeout|timed out|etimedout|econnreset|socket hang up|network/.test(m))
20
+ return "timeout";
21
+ if (typeof status === "number" && status >= 500)
22
+ return "transient";
23
+ return "unknown";
24
+ }
25
+ // Error kinds where retrying on a DIFFERENT model can plausibly help (it may not be overloaded / may differ).
26
+ const FALLBACKABLE = new Set(["overloaded", "rate_limit", "timeout", "transient", "context_overflow", "unknown"]);
27
+ /** Decide the recovery for an errored turn: retry once on the fallback model, or fail. Never auto-recovers
28
+ * `auth` (a config problem) or `interrupted` (the user). Context-overflow IS fallback-able — a
29
+ * larger-context fallback model may fit (and preemptive auto-compaction already prevents most overflows). */
30
+ export function failoverAction(kind, s) {
31
+ if (kind === "interrupted" || kind === "auth")
32
+ return "fail";
33
+ if (s.hasFallback && !s.triedFallback && FALLBACKABLE.has(kind))
34
+ return "fallback";
35
+ return "fail";
36
+ }
37
+ /** A short actionable hint appended to the surfaced error message. */
38
+ export function errorHint(kind) {
39
+ switch (kind) {
40
+ case "auth":
41
+ return " — check your API key / auth (try `hara setup`)";
42
+ case "rate_limit":
43
+ return " — rate-limited; wait a moment, or set `fallbackModel` to auto-switch";
44
+ case "overloaded":
45
+ return " — provider overloaded; set `fallbackModel` to auto-switch on errors";
46
+ case "context_overflow":
47
+ return " — context too long; `/compact` (or enable `autoCompact`)";
48
+ case "timeout":
49
+ return " — network timeout; check connectivity";
50
+ default:
51
+ return "";
52
+ }
53
+ }