Skip to content

Commit aea7498

Browse files
authored
add IBM Bob agent adapter (#102)
* add IBM Bob agent adapter Add support for IBM Bob Shell CLI as a new coding agent: - Add bob agent definition in detect.ts with IBM vendor info - Add bob case in buildArgv() to use --output-format stream-json - Add bob parsing logic in parseLineWithState() to handle text/content/message fields - Add prompt echo suppression in invoke.ts to avoid duplicating system prompts - Update README.md and README.zh-CN.md agent tables and badge counts (8 → 9 CLIs) Bob is invoked via stdin with stream-json output format, similar to other agents. The adapter reuses the existing JSON-line parser infrastructure. * address review feedback: add --hide-intermediary-output flag Add --hide-intermediary-output flag to Bob adapter to suppress thinking and reasoning output, preventing intermediary text from polluting the final HTML/markdown document. Changes: - Add --hide-intermediary-output to buildArgv() for bob agent - Add regression tests in argv.test.ts covering Bob stream-json parsing - Update README.md and README.zh-CN.md with correct invocation This addresses the review feedback from nettee about blocking issue where Bob's reasoning could be appended into generated artifacts.
1 parent 5941e53 commit aea7498

6 files changed

Lines changed: 89 additions & 4 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44

55
<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>
66

7-
> **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 — **8 coding-agent CLIs** auto-detected on your `PATH` (Claude Code · Cursor Agent · Codex · Gemini CLI · GitHub Copilot CLI · OpenCode · Qwen Coder · Aider), 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`.
7+
> **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`.
88
99
<p align="center">
1010
<img src="docs/assets/banner.png" alt="HTML Anything — the agentic HTML editor, on your laptop" width="100%" />
1111
</p>
1212

1313
<p align="center">
1414
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square" /></a>
15-
<a href="#supported-coding-agents"><img alt="Agents" src="https://img.shields.io/badge/agents-8%20CLIs-black?style=flat-square" /></a>
15+
<a href="#supported-coding-agents"><img alt="Agents" src="https://img.shields.io/badge/agents-9%20CLIs-black?style=flat-square" /></a>
1616
<a href="#skills"><img alt="Skills" src="https://img.shields.io/badge/skills-75-orange?style=flat-square" /></a>
1717
<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>
1818
<a href="#quickstart"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-30%20seconds-green?style=flat-square" /></a>
@@ -202,6 +202,7 @@ On startup we scan `PATH` (including `~/.local/bin`, `~/.bun/bin`, `/opt/homebre
202202
| **OpenCode** | `opencode-cli` / `opencode` | `opencode run --format json --dangerously-skip-permissions -` |
203203
| **Qwen Coder** | `qwen` | `qwen --yolo -` |
204204
| **Aider** | `aider` | `aider --no-pretty --no-stream --yes-always --message-file -` |
205+
| **IBM Bob** | `bob` | `bob --output-format stream-json --hide-intermediary-output` |
205206

206207
> 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).
207208

README.zh-CN.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44

55
<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>
66

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

1313
<p align="center">
1414
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square" /></a>
15-
<a href="#-自动识别本地-agent"><img alt="Agents" src="https://img.shields.io/badge/agents-8%20CLIs-black?style=flat-square" /></a>
15+
<a href="#-自动识别本地-agent"><img alt="Agents" src="https://img.shields.io/badge/agents-9%20CLIs-black?style=flat-square" /></a>
1616
<a href="#-skills"><img alt="Skills" src="https://img.shields.io/badge/skills-75-orange?style=flat-square" /></a>
1717
<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>
1818
<a href="#-30-秒上手"><img alt="Quickstart" src="https://img.shields.io/badge/quickstart-30%20seconds-green?style=flat-square" /></a>
@@ -202,6 +202,7 @@ pnpm -F @html-anything/e2e test
202202
| **OpenCode** | `opencode-cli` / `opencode` | `opencode run --format json --dangerously-skip-permissions -` |
203203
| **Qwen Coder** | `qwen` | `qwen --yolo -` |
204204
| **Aider** | `aider` | `aider --no-pretty --no-stream --yes-always --message-file -` |
205+
| **IBM Bob** | `bob` | `bob --output-format stream-json --hide-intermediary-output` |
205206

206207
> 这一层的设计直接借鉴了 [`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)
207208

next/src/lib/agents/__tests__/argv.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,60 @@ describe("parseLine opencode", () => {
162162
]);
163163
});
164164
});
165+
166+
describe("parseLine bob", () => {
167+
it("extracts text from stream-json output", () => {
168+
const line = JSON.stringify({
169+
text: "<html><body>Hello</body></html>",
170+
});
171+
172+
expect(parseLine("bob", line)).toEqual([
173+
{
174+
kind: "delta",
175+
text: "<html><body>Hello</body></html>",
176+
},
177+
]);
178+
});
179+
180+
it("extracts content field when present", () => {
181+
const line = JSON.stringify({
182+
content: "<html><body>World</body></html>",
183+
});
184+
185+
expect(parseLine("bob", line)).toEqual([
186+
{
187+
kind: "delta",
188+
text: "<html><body>World</body></html>",
189+
},
190+
]);
191+
});
192+
193+
it("extracts message field when present", () => {
194+
const line = JSON.stringify({
195+
message: "<html><body>Test</body></html>",
196+
});
197+
198+
expect(parseLine("bob", line)).toEqual([
199+
{
200+
kind: "delta",
201+
text: "<html><body>Test</body></html>",
202+
},
203+
]);
204+
});
205+
206+
it("handles final answer after thinking when --hide-intermediary-output is used", () => {
207+
// When --hide-intermediary-output is enabled, Bob only emits the final answer.
208+
// This test verifies that the parser correctly handles the final completion.
209+
const finalAnswer = JSON.stringify({
210+
text: "<html><body>Final result</body></html>",
211+
});
212+
213+
expect(parseLine("bob", finalAnswer)).toEqual([
214+
{
215+
kind: "delta",
216+
text: "<html><body>Final result</body></html>",
217+
},
218+
]);
219+
});
220+
});
221+

next/src/lib/agents/argv.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ export function buildArgv(agent: string, _opts: AgentArgvOpts = {}): string[] {
8383
"json",
8484
...(model ? ["--model", model] : []),
8585
];
86+
case "bob":
87+
// Bob's `-p`/`--prompt` flag expects an argument; we stream the prompt via
88+
// stdin, so omit `-p` and just request stream-json output.
89+
// --hide-intermediary-output suppresses thinking/reasoning, leaving only
90+
// the final completion in stdout.
91+
return ["--output-format", "stream-json", "--hide-intermediary-output"];
8692
case "opencode":
8793
return [
8894
"run",
@@ -416,6 +422,12 @@ function parseLineWithState(agent: string, line: string, state: ParseState): Age
416422
if (typeof obj.message === "string") out.push({ kind: "delta", text: obj.message });
417423
}
418424

425+
if (agent === "bob") {
426+
if (typeof obj.text === "string") out.push({ kind: "delta", text: obj.text });
427+
if (typeof obj.content === "string") out.push({ kind: "delta", text: obj.content });
428+
if (typeof obj.message === "string") out.push({ kind: "delta", text: obj.message });
429+
}
430+
419431
if (agent === "qoder") {
420432
// Qoder's stream-json output mirrors claude's envelope shape (init/system,
421433
// stream_event with content_block_delta/text_delta, assistant message,

next/src/lib/agents/detect.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ export const AGENTS: AgentDef[] = [
139139
{ id: "gpt-5.2", label: "GPT-5.2" },
140140
],
141141
},
142+
{
143+
id: "bob",
144+
label: "IBM Bob Shell",
145+
bin: "bob",
146+
envOverride: "BOB_BIN",
147+
vendor: "IBM",
148+
fallbackModels: [DEFAULT_MODEL],
149+
},
142150
{
143151
id: "opencode",
144152
label: "OpenCode",

next/src/lib/agents/invoke.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ export function invokeAgent(opts: InvokeOpts): ReadableStream<InvokeEvent> {
215215
stdoutBuf = stdoutBuf.slice(nl + 1);
216216
if (!line) continue;
217217
for (const part of parse(line)) {
218+
// Some agents (bob) may echo the entire prompt back as the first
219+
// streamed delta. Suppress that to avoid polluting the user-facing
220+
// output with the system prompt.
221+
if (opts.agent === "bob" && part.kind === "delta") {
222+
if (part.text.trim() === opts.prompt.trim()) continue;
223+
}
218224
if (part.kind === "delta") safeEnqueue({ type: "delta", text: part.text });
219225
else if (part.kind === "html") safeEnqueue({ type: "html", text: part.text });
220226
else if (part.kind === "meta") safeEnqueue({ type: "meta", key: part.key, value: part.value });

0 commit comments

Comments
 (0)