Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

<p align="center"><b>Live page:</b> <a href="https://open-design.ai/html-anything/"><b>open-design.ai/html-anything/</b></a> — overview, surface modes, and showcase before you clone.</p>

> **Markdown is the draft. HTML is what humans read. Your local agent writes it.** The agentic HTML editor — in the agentic era, you don't hand-edit docs anymore, so the output format should be what the reader actually wants: HTML. Local-first, zero API key, reuses the CLI session you already have logged in — **9 coding-agent CLIs** auto-detected on your `PATH` (Claude Code · Cursor Agent · Codex · Gemini CLI · GitHub Copilot CLI · OpenCode · Qwen Coder · Aider · IBM Bob), driven by **75 composable skill templates** across **9 deliverable surfaces** (magazine articles · keynote decks · résumés · posters · Xiaohongshu cards · tweet cards · web prototypes · data reports · Hyperframes videos). One-click export to WeChat / X / Zhihu, or download `.html` / `.png`.
> **Markdown is the draft. HTML is what humans read. Your local agent writes it.** The agentic HTML editor — in the agentic era, you don't hand-edit docs anymore, so the output format should be what the reader actually wants: HTML. Local-first, zero API key, reuses the CLI session you already have logged in — **10 coding-agent CLIs** auto-detected on your `PATH` (Claude Code · Cursor Agent · Codex · Gemini CLI · GitHub Copilot CLI · OpenCode · Qwen Coder · Aider · IBM Bob · Antigravity), driven by **75 composable skill templates** across **9 deliverable surfaces** (magazine articles · keynote decks · résumés · posters · Xiaohongshu cards · tweet cards · web prototypes · data reports · Hyperframes videos). One-click export to WeChat / X / Zhihu, or download `.html` / `.png`.

<p align="center">
<img src="docs/assets/banner.png" alt="HTML Anything — the agentic HTML editor, on your laptop" width="100%" />
</p>

<p align="center">
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square" /></a>
<a href="#supported-coding-agents"><img alt="Agents" src="https://img.shields.io/badge/agents-9%20CLIs-black?style=flat-square" /></a>
<a href="#supported-coding-agents"><img alt="Agents" src="https://img.shields.io/badge/agents-10%20CLIs-black?style=flat-square" /></a>
<a href="#skills"><img alt="Skills" src="https://img.shields.io/badge/skills-75-orange?style=flat-square" /></a>
<a href="#export-targets"><img alt="Export" src="https://img.shields.io/badge/export-WeChat%20%C2%B7%20X%20%C2%B7%20Zhihu%20%C2%B7%20PNG-9b59b6?style=flat-square" /></a>
<a href="#quickstart"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-30%20seconds-green?style=flat-square" /></a>
Expand Down Expand Up @@ -203,6 +203,7 @@ On startup we scan `PATH` (including `~/.local/bin`, `~/.bun/bin`, `/opt/homebre
| **Qwen Coder** | `qwen` | `qwen --yolo -` |
| **Aider** | `aider` | `aider --no-pretty --no-stream --yes-always --message-file -` |
| **IBM Bob** | `bob` | `bob --output-format stream-json --hide-intermediary-output` |
| **Antigravity** | `agy` | `agy --output-format stream-json --dangerously-skip-permissions --disable-slash-commands --effort medium -p <prompt>` |

> The detection strategy and per-CLI adapter shape are borrowed directly from [`nexu-io/open-design`](https://github.com/nexu-io/open-design) and [`multica-ai/multica`](https://github.com/multica-ai/multica): one privileged process spawns CLIs, JSON-line is the wire protocol, every CLI gets a thin adapter in [`next/src/lib/agents/argv.ts`](next/src/lib/agents/argv.ts).

Expand Down
5 changes: 3 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

<p align="center"><b>项目主页:</b> <a href="https://open-design.ai/html-anything/"><b>open-design.ai/html-anything/</b></a> —— 不用 clone 也能先看看 HTML Anything 长什么样、能干啥。</p>

> **Markdown 是草稿, HTML 才是给人读的成品 —— 让本地 agent 直接写 HTML。** Agent 时代的 HTML 编辑器 —— 既然你已经不亲手改文档、全都让 Claude 改了, 那 agent 的输出就该是读者真正想看的 HTML, 而不是中间态的 markdown。本地优先、零 API Key、复用你已经登录好的 CLI session —— **9 个 coding-agent CLI** 在 `PATH` 上自动识别(Claude Code · Cursor Agent · Codex · Gemini CLI · GitHub Copilot CLI · OpenCode · Qwen Coder · Aider · IBM Bob),驱动 **75 套 skill 模板** 和 **9 类可交付场景**(杂志文章 · Keynote PPT · 简历 · 海报 · 小红书 · 推特卡 · Web 原型 · 数据报告 · Hyperframes 视频)。一键复制到公众号 / 推特 / 知乎,或者下载 `.html` / `.png`。
> **Markdown 是草稿, HTML 才是给人读的成品 —— 让本地 agent 直接写 HTML。** Agent 时代的 HTML 编辑器 —— 既然你已经不亲手改文档、全都让 Claude 改了, 那 agent 的输出就该是读者真正想看的 HTML, 而不是中间态的 markdown。本地优先、零 API Key、复用你已经登录好的 CLI session —— **10 个 coding-agent CLI** 在 `PATH` 上自动识别(Claude Code · Cursor Agent · Codex · Gemini CLI · GitHub Copilot CLI · OpenCode · Qwen Coder · Aider · IBM Bob · Antigravity),驱动 **75 套 skill 模板** 和 **9 类可交付场景**(杂志文章 · Keynote PPT · 简历 · 海报 · 小红书 · 推特卡 · Web 原型 · 数据报告 · Hyperframes 视频)。一键复制到公众号 / 推特 / 知乎,或者下载 `.html` / `.png`。

<p align="center">
<img src="docs/assets/banner.png" alt="HTML Anything — agent 时代的 HTML 编辑器,在你的笔记本上" width="100%" />
</p>

<p align="center">
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square" /></a>
<a href="#-自动识别本地-agent"><img alt="Agents" src="https://img.shields.io/badge/agents-9%20CLIs-black?style=flat-square" /></a>
<a href="#-自动识别本地-agent"><img alt="Agents" src="https://img.shields.io/badge/agents-10%20CLIs-black?style=flat-square" /></a>
<a href="#-skills"><img alt="Skills" src="https://img.shields.io/badge/skills-75-orange?style=flat-square" /></a>
<a href="#一键发布到平台"><img alt="Export" src="https://img.shields.io/badge/export-WeChat%20%C2%B7%20X%20%C2%B7%20Zhihu%20%C2%B7%20PNG-9b59b6?style=flat-square" /></a>
<a href="#-30-秒上手"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-30%20seconds-green?style=flat-square" /></a>
Expand Down Expand Up @@ -203,6 +203,7 @@ pnpm -F @html-anything/e2e test
| **Qwen Coder** | `qwen` | `qwen --yolo -` |
| **Aider** | `aider` | `aider --no-pretty --no-stream --yes-always --message-file -` |
| **IBM Bob** | `bob` | `bob --output-format stream-json --hide-intermediary-output` |
| **Antigravity** | `agy` | `agy --output-format stream-json --dangerously-skip-permissions --disable-slash-commands --effort medium -p <prompt>` |

> 这一层的设计直接借鉴了 [`nexu-io/open-design`](https://github.com/nexu-io/open-design) 和 [`multica-ai/multica`](https://github.com/multica-ai/multica) 的 agent 检测策略:唯一被 spawn 子进程的进程是 server route,业务进程不直接 spawn;CLI 的 stdin / stdout 用 JSON-line 协议复用,每个 CLI 一个轻 adapter,统一在 [`next/src/lib/agents/argv.ts`](next/src/lib/agents/argv.ts)。

Expand Down
24 changes: 24 additions & 0 deletions cli/src/__tests__/agents-invoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,4 +527,28 @@ describe("invokeAgent", () => {
});

});

describe("antigravity", () => {
it("passes the prompt right after -p and parses agy step_update/result lines", async () => {
const lines = [
JSON.stringify({ event: "init", conversation_id: "conv-1", init: { cwd: "/tmp" } }),
JSON.stringify({ event: "step_update", step_update: { text_delta: "<p>hi</p>" } }),
JSON.stringify({ event: "result", result: { status: "SUCCESS", response: "<p>hi</p>" } }),
].join("\n") + "\n";

const events = await driveInvoke(
{ agent: "antigravity", prompt: "make html", binOverride: BIN_OVERRIDE },
lines,
0,
);

const argv = mockSpawn.mock.calls.at(-1)![1] as string[];
expect(argv.slice(-2)).toEqual(["-p", "make html"]);
expect(argv).toContain("--disable-slash-commands");

// result.response must not duplicate the streamed delta
const deltas = events.filter((e) => e.type === "delta");
expect(deltas).toEqual([{ type: "delta", text: "<p>hi</p>" }]);
});
});
});
10 changes: 10 additions & 0 deletions cli/src/agents-detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ export const AGENTS: AgentDef[] = [
{ id: "gpt-5", label: "gpt-5" },
],
},
{
id: "antigravity",
label: "Antigravity",
bin: "agy",
envOverride: "ANTIGRAVITY_BIN",
vendor: "Google",
protocol: "argv",
// Model ids come from the user's agy config; no curated list yet.
fallbackModels: [DEFAULT_MODEL],
},
{
id: "gemini",
label: "Gemini CLI",
Expand Down
96 changes: 96 additions & 0 deletions cli/src/agents-invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ function buildArgv(agent: string, opts: AgentArgvOpts = {}): string[] {
"--trust",
...(model ? ["--model", model] : []),
];
case "antigravity":
// `-p` must stay last: invoke appends the prompt right after it
// (`protocol: "argv"`). Effort is pinned so runs don't depend on the
// user's local agy default.
return [
"--output-format",
"stream-json",
"--dangerously-skip-permissions",
"--disable-slash-commands",
"--effort",
"medium",
...(model ? ["--model", model] : []),
"-p",
];
case "gemini":
return [
"--output-format",
Expand Down Expand Up @@ -209,6 +223,7 @@ function rescueHtmlFromToolUse(
name !== "createfile" &&
name !== "writefile" &&
name !== "write_file" &&
name !== "write_to_file" &&
name !== "filewrite"
)
continue;
Expand All @@ -229,6 +244,20 @@ function rescueHtmlFromToolUse(
return parts.join("");
}

/** agy reports `cache_read_tokens`; the UI reads Claude-style usage keys. */
function agyUsage(u: Record<string, number>): AgentParse {
return {
kind: "meta",
key: "usage",
value: {
input_tokens: u.input_tokens,
output_tokens: u.output_tokens,
cache_read_input_tokens: u.cache_read_tokens ?? 0,
cache_creation_input_tokens: 0,
},
};
}

function parseLineWithState(agent: string, line: string, state: ParseState): AgentParse[] {
const trimmed = line.trim();
if (!trimmed) return [];
Expand All @@ -246,6 +275,73 @@ function parseLineWithState(agent: string, line: string, state: ParseState): Age
if (!parsed || typeof parsed !== "object") return [];
const obj = parsed as Record<string, unknown>;
const out: AgentParse[] = [];
if (agent === "antigravity") {
// agy stream-json: one envelope per line, keyed by `event`
// (init / step_update / result).
if (obj.event === "init") {
if (typeof obj.conversation_id === "string") {
out.push({ kind: "meta", key: "session", value: obj.conversation_id });
}
const initObj = obj.init as Record<string, unknown> | undefined;
if (typeof initObj?.cwd === "string") {
out.push({ kind: "meta", key: "cwd", value: initObj.cwd });
}
}

const step = obj.step_update as Record<string, unknown> | undefined;
if (step && typeof step === "object") {
// agy writes files via `write_to_file { TargetFile, CodeContent }`.
// Map it onto the shared tool_use shape so the rescue rules live in
// one place.
const toolInfo = step.tool_info as
| { name?: string; parameters?: Record<string, unknown> }
| undefined;
const params = toolInfo?.parameters;
if (params && (toolInfo?.name ?? step.tool_name) === "write_to_file") {
const toolHtml = rescueHtmlFromToolUse([
{
type: "tool_use",
name: "write_to_file",
input: { file_path: params.TargetFile, content: params.CodeContent },
},
]);
if (toolHtml) {
out.push({ kind: "html", text: toolHtml });
state.sawStreamEventText = true;
}
}
if (typeof step.text_delta === "string" && step.text_delta) {
state.sawStreamEventText = true;
out.push({ kind: "delta", text: step.text_delta });
}
if (typeof step.thinking_delta === "string" && step.thinking_delta) {
out.push({ kind: "meta", key: "thinking", value: step.thinking_delta });
}
if (step.usage && typeof step.usage === "object") {
out.push(agyUsage(step.usage as Record<string, number>));
}
}

const result = obj.result as Record<string, unknown> | undefined;
if (result && typeof result === "object") {
if (!state.sawStreamEventText && typeof result.response === "string" && result.response) {
out.push({ kind: "delta", text: result.response });
}
if (result.usage && typeof result.usage === "object") {
out.push(agyUsage(result.usage as Record<string, number>));
}
if (typeof result.duration_seconds === "number") {
out.push({
kind: "meta",
key: "duration_ms",
value: Math.round(result.duration_seconds * 1000),
});
}
if (typeof result.status === "string") {
out.push({ kind: "meta", key: "result", value: result.status });
}
}
}

if (agent === "claude") {
if (obj.type === "system" && obj.subtype === "init") {
Expand Down
Loading