mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
6e32b2052462cbb90d680bc0d0aca18d61b71900
* feat(web): add UriConfirmDialog component
Add a Radix Dialog-based confirmation modal for custom URI scheme
navigation. Follows the RenameSessionDialog pattern.
- UriConfirmDialog: shows URI, scheme label, Cancel/Open/Always-allow buttons
- i18n keys: dialog.uri.{title,description,open,alwaysAllow}
* feat(web): autolink non-https URI schemes in markdown
Add a remark plugin that converts raw `scheme://...` text nodes into
link nodes for non-http(s) schemes. GFM already handles http/https;
this plugin handles the remainder (obsidian://, vscode://, slack://, etc.).
- No scheme allowlist: every `scheme://` pattern is converted; the
sanitize layer (urlTransform) and onClick layer (classifyScheme) handle
blocking/confirmation downstream.
- Runs before remarkStripCjkAutolink so the CJK-strip plugin sees the
new link nodes and can trim trailing CJK punctuation from them.
- Trailing punctuation (.,;!?) stripped from matched URIs.
- Unit tests: conversion, partial-match, escape, explicit link bypass,
code-block bypass, trailing-punct trimming.
* feat(web): linkify custom URI schemes via markdown <a> handler
Wire up 4-layer URI security policy in the markdown renderer:
1. URL sanitize (deny-only): urlTransform strips javascript:/data:/vbscript:/file:
using classifyScheme as single source of truth (handles percent-encoding,
case-insensitive, whitespace-prefix bypass patterns).
2. onClick intercept: custom <A> component classifies each href —
- IANA safe (https/http/irc/ircs/mailto/xmpp): navigate directly.
- Deny (javascript/data/vbscript/file): preventDefault silently.
- Custom (obsidian/vscode/slack/…): preventDefault + open UriConfirmDialog.
3. UriConfirmProvider: one dialog lifted to each markdown root (MarkdownText,
Reasoning, MarkdownRenderer). Shared isAllowed state across all <a> tags in
the subtree — "Always allow" click updates every link in one React commit.
4. Intra-tab cross-provider sync (P7e.1): module-level schemeListeners Set so
sibling UriConfirmProviders (MarkdownText + Reasoning in AssistantMessage)
receive allowed-scheme updates synchronously without waiting for the window
storage event (which only fires in other tabs). Cross-tab sync continues via
the existing window storage event listener.
5. "Always allow" persisted to localStorage (hapi-allowed-schemes). Custom
schemes once allowed navigate directly on subsequent clicks, no dialog gate.
href="#" in DOM for unallowed custom schemes prevents middle-click bypass.
Deny-scheme href="" prevents any navigation even if localStorage tampered.
Security: classifyScheme decodes percent-encoding before scheme extraction,
blocking %6Aavascript:, jav%61script:, javascript%3A (single-encoded colon)
and double-encoded variants. DENY_SCHEMES checked after localStorage lookup so
tampered allowed-list cannot promote deny schemes.
Tests: classifyScheme 6-axis security bypass, denyOnlyTransform, localStorage
roundtrip, cross-tab storage event, <A> click handler cases.
* fix(web): block control-char-spliced deny schemes in classifyScheme
Browsers silently strip ASCII control characters (\t, \n, \r) and
whitespace from URL scheme names during navigation. A scheme like
`java\nscript:alert(1)` was navigated as `javascript:` while our
literal string comparison classified it as 'custom', allowing it
past the deny list and into window.open().
Introduce normalizedScheme() that:
- applies 2 rounds of decodeURIComponent so double-encoded schemes
(javascript%253A → javascript%3A → javascript:) are fully unwrapped
before comparison
- strips [\x00-\x1F\x7F\s] from the extracted scheme name, matching
the browser's own normalization
classifyScheme() now delegates to normalizedScheme() so both the
denyOnlyTransform (urlTransform) path and the <A> onClick path benefit
from the same normalization.
Tests added for \n / \t / \r / space spliced into scheme, and verify
that double-encoded colon is now caught via scheme-match (not just
the no-colon fallback).
* fix(web): preserve relative markdown links from being blocked
Relative / no-scheme hrefs (/settings, ./foo, #section, ?q=1) were
silently preventDefault'd in <A>'s onClick handler. denyOnlyTransform
correctly passed them through (no colon → not a scheme URL), but the
click handler called classifyScheme(href) which returned 'deny' for
any input with no valid scheme separator — then the deny branch fired.
Add hasScheme(href): checks whether the first ':' appears before any
path/query/fragment boundary ('/', '?', '#'). When hasScheme is false
the href is treated as 'iana' so the browser or SPA router can navigate
normally with no dialog and no preventDefault.
Also wrap renderA() with <I18nProvider> so the UriConfirmDialog that
UriConfirmProvider may render does not throw outside its translation
context during tests.
Fixes a regression that broke all relative-path markdown links once the
custom-URI-scheme onClick handler was added.
* test(web): cover percent-encoded scheme control char + protocol-relative href
Round-5 internal hostile review noted two coverage gaps on the bot-fixup commits:
- `java%0Ascript:alert(1)` (percent-encoded newline in the scheme name) takes the
same decode→strip code path as the literal `java\nscript:` case but was only
tested literally. Add an explicit test so a future refactor that drops the
decode-then-strip ordering would be caught.
- Protocol-relative URLs (`//host/path`) have no colon, so `hasScheme` returns
false and `<A>` treats them as scheme-less — browsers then navigate them as
the current origin's protocol. Existing relative-href tests covered absolute
paths, hashes, queries, and colon-in-path, but not the protocol-relative
variant. Add one assertion.
Also extend the `hasScheme` JSDoc to note that protocol-relative URLs are
intentionally treated as scheme-less.
* fix(web): preserve balanced parens/brackets in autolinked URIs
The trailing-punctuation strip used to drop every `)` / `]` from the end
of a matched URI, even when the URL body had an unmatched opener. So a
URI like `obsidian://open?file=Note(1)` was rendered with href
`obsidian://open?file=Note(1` plus a separate `)` text node, opening a
broken deep link.
Match the GFM autolink-literal behaviour: when the trailing character is
`)` or `]`, keep it iff the URL body has more opening counterparts than
closers (so the trailing closer balances an earlier opener and belongs
to the URL). Other trailing punctuation (`.,;!?:>'"`) and unmatched
closers still strip as before.
Add tests for the balanced cases (`Note(1)`, `Note[1]`, nested
`(a(b)c)`), the "balanced URL followed by a period" case, and a
regression test that an unmatched `).` after a URL is still stripped.
HAPI
Run official Claude Code / Codex / Gemini / OpenCode sessions locally and control them remotely through a Web / PWA / Telegram Mini App.
Why HAPI? HAPI is a local-first alternative to Happy. See Why Not Happy? for the key differences.
Features
- Seamless Handoff - Work locally, switch to remote when needed, switch back anytime. No context loss, no session restart.
- Native First - HAPI wraps your AI agent instead of replacing it. Same terminal, same experience, same muscle memory.
- AFK Without Stopping - Step away from your desk? Approve AI requests from your phone with one tap.
- Your AI, Your Choice - Claude Code, Codex, Cursor Agent, Gemini, OpenCode—different models, one unified workflow.
- Terminal Anywhere - Run commands from your phone or browser, directly connected to the working machine.
- Voice Control - Talk to your AI agent hands-free using the built-in voice assistant.
- Workspace Browser - Opt-in via one or more
hapi runner start --workspace-root <path>flags: browse scoped file trees from the web and start sessions in allowed subdirectories.
Demo
https://github.com/user-attachments/assets/38230353-94c6-4dbe-9c29-b2a2cc457546
Getting Started
npx @twsxtd/hapi hub --relay # start hub with E2E encrypted relay
npx @twsxtd/hapi # run claude code
hapi server remains supported as an alias.
The terminal will display a URL and QR code. Scan the QR code with your phone or open the URL to access.
The relay uses WireGuard + TLS for end-to-end encryption. Your data is encrypted from your device to your machine.
For self-hosted options (Cloudflare Tunnel, Tailscale), see Installation
Docs
Build from source
bun install
bun run build:single-exe
Credits
HAPI means "哈皮" a Chinese transliteration of Happy. Great credit to the original project.
Languages
TypeScript
99.5%
CSS
0.2%
JavaScript
0.2%
HTML
0.1%