npm package: @snack-kit/porygon
Description: Unified programmatic interface for LLM Agent CLI tools
Latest version: 0.10.0

# Porygon

统一的 LLM Agent CLI 编程接口框架。通过 Facade + Adapter 模式，将 Claude Code、OpenCode 等 CLI 工具封装为一致的 TypeScript API。

## 安装

```bash
npm add @snack-kit/porygon
```

**前置条件**：需要安装至少一个 CLI 后端：
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (`claude` 命令)
- [OpenCode](https://github.com/opencode-ai/opencode) (`opencode` 命令)

## 快速上手

```ts
import { createPorygon } from "@snack-kit/porygon";

const porygon = createPorygon();

// 简单调用：发送 prompt，返回最终文本
const answer = await porygon.run({ prompt: "什么是闭包？" });
console.log(answer);

// 释放资源
await porygon.dispose();
```

## API 速查

### `createPorygon(config?): Porygon`

工厂函数，创建 Porygon 实例。

```ts
const porygon = createPorygon({
  defaultBackend: "claude",           // "claude" | "opencode"
  backends: {
    claude: {
      model: "sonnet",
      interactive: false,             // false = --dangerously-skip-permissions
      cliPath: "/usr/local/bin/claude", // 自定义 CLI 路径
      appendSystemPrompt: "用中文回答",
      cwd: "/path/to/project",
      proxy: { url: "http://127.0.0.1:7897" },
    },
    opencode: {
      serverUrl: "http://localhost:39393",
      apiKey: "sk-xxx",
    },
  },
  defaults: {
    timeoutMs: 300_000,
    maxTurns: 50,
    appendSystemPrompt: "请简洁回答",
    disallowedTools: ["WebSearch"],  // 全局禁用的工具
  },
  proxy: { url: "http://proxy:8080", noProxy: "localhost" },
});
```

**PorygonConfig 字段：**

| 字段 | 类型 | 说明 |
|------|------|------|
| `defaultBackend` | `string` | 默认后端名称 |
| `backends` | `Record<string, BackendConfig>` | 各后端独立配置 |
| `defaults` | `{ appendSystemPrompt?, timeoutMs?, maxTurns?, disallowedTools? }` | 全局默认参数 |
| `proxy` | `{ url: string, noProxy?: string }` | 全局代理（后端级 proxy 优先） |

**BackendConfig 字段：**

| 字段 | 类型 | 说明 |
|------|------|------|
| `model` | `string` | 模型名称 |
| `interactive` | `boolean` | 是否交互模式，`false` 时 Claude 添加 `--dangerously-skip-permissions` |
| `cliPath` | `string` | CLI 可执行文件路径（如 Claude CLI 的自定义安装路径） |
| `serverUrl` | `string` | 远程服务地址（OpenCode serve 模式） |
| `apiKey` | `string` | API Key 认证 |
| `appendSystemPrompt` | `string` | 追加系统提示词 |
| `proxy` | `ProxyConfig` | 后端专属代理 |
| `cwd` | `string` | 工作目录 |
| `disallowedTools` | `string[]` | 禁止使用的工具黑名单（后端级） |
| `streamPartialMessages` | `boolean` | 是否启用 token 级流式输出（Claude CLI `--include-partial-messages`），默认 `true` |
| `options` | `Record<string, unknown>` | 透传给后端的额外选项（向后兼容，推荐使用上方的显式字段） |

---

### `porygon.run(request): Promise<string>`

发送 prompt，等待完成，返回最终结果文本。适合简单的一次性调用。

```ts
const result = await porygon.run({
  prompt: "写一个 HTTP 服务器",
  backend: "claude",
  model: "opus",
});
```

---

### `porygon.query(request): AsyncGenerator<AgentMessage>`

流式查询，逐条 yield `AgentMessage`。适合需要实时输出或监控工具调用的场景。

```ts
for await (const msg of porygon.query({ prompt: "解释快速排序" })) {
  switch (msg.type) {
    case "system":
      console.log("模型:", msg.model);
      break;
    case "stream_chunk":
      process.stdout.write(msg.text); // 增量文本，实时输出
      break;
    case "assistant":
      // turnComplete=true: 与前面 stream_chunk 内容重复，流式消费者应跳过
      // turnComplete=false/undefined: 独立文本（如 run() 模式），需要处理
      if (!msg.turnComplete) {
        console.log("回复:", msg.text);
      }
      break;
    case "tool_use":
      console.log("工具调用:", msg.toolName, msg.input);
      if (msg.output) console.log("工具结果:", msg.output);
      break;
    case "result":
      console.log("完成", { cost: msg.costUsd, tokens: msg.inputTokens });
      break;
    case "error":
      console.error("错误:", msg.message);
      break;
  }
}
```

**消息流顺序**（两个后端统一）：

```
system → stream_chunk* → assistant(turnComplete) → tool_use* → ... → result
```

- `stream_chunk` — 增量文本片段，用于实时展示
- `assistant` — 单个 turn 的完整文本汇总。`turnComplete: true` 表示内容与 `stream_chunk` 重复
- `tool_use` — 工具调用；若带 `output` 字段则为工具执行结果
- `result` — 最终结果，包含完整文本和用量统计

---

### PromptRequest（请求参数）

`run()` 和 `query()` 共用同一参数类型：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `prompt` | `string` | 是 | 提示词 |
| `backend` | `string` | 否 | 后端名称（默认取 config.defaultBackend） |
| `model` | `string` | 否 | 模型名称 |
| `resume` | `string` | 否 | 恢复会话的 session ID |
| `systemPrompt` | `string` | 否 | 系统提示词（替换模式，设置后 appendSystemPrompt 失效） |
| `appendSystemPrompt` | `string` | 否 | 系统提示词（追加模式，与 config 中的追加内容合并） |
| `onlyTools` | `string[]` | 否 | 仅允许使用的工具白名单（内部通过 getTools() 差集转为黑名单，与 disallowedTools 互斥，优先级更高） |
| `disallowedTools` | `string[]` | 否 | 禁止使用的工具黑名单 |
| `maxTurns` | `number` | 否 | 最大对话轮次 |
| `timeoutMs` | `number` | 否 | 超时毫秒数 |
| `cwd` | `string` | 否 | 工作目录 |
| `envVars` | `Record<string, string>` | 否 | 注入子进程的环境变量 |
| `mcpServers` | `Record<string, McpServerConfig>` | 否 | MCP 服务器配置 |
| `backendOptions` | `Record<string, unknown>` | 否 | 透传给后端的额外选项 |

**配置合并策略**（`mergeRequest`）：

| 字段 | 策略 |
|------|------|
| `model` | request > backendConfig > 不设置 |
| `timeoutMs` | request > defaults |
| `maxTurns` | request > defaults |
| `cwd` | request > backendConfig |
| `onlyTools` | 通过 getTools() 获取全量列表，差集计算转为 disallowedTools（设置后忽略 disallowedTools） |
| `disallowedTools` | defaults + backendConfig + request 三层叠加去重 |
| `appendSystemPrompt` | defaults + backendConfig + request 三层追加拼接（换行分隔）。若 `systemPrompt` 已设置则忽略全部 append |

---

### AgentMessage（消息类型）

`porygon.query()` 返回的 `AgentMessage` 是一个联合类型。所有消息均包含 `timestamp: number`，可选 `sessionId?: string` 和 `raw?: unknown`（后端原始数据）。

| type | 接口 | 关键字段 |
|------|------|----------|
| `"system"` | `AgentSystemMessage` | `model?`, `tools?`, `cwd?` |
| `"assistant"` | `AgentAssistantMessage` | `text`, `turnComplete?` |
| `"tool_use"` | `AgentToolUseMessage` | `toolName`, `input`, `output?` |
| `"stream_chunk"` | `AgentStreamChunkMessage` | `text` |
| `"result"` | `AgentResultMessage` | `text`, `durationMs?`, `costUsd?`, `inputTokens?`, `outputTokens?` |
| `"error"` | `AgentErrorMessage` | `message`, `code?` |

---

### `porygon.checkBackend(backend, options?): Promise<HealthCheckResult>`

检查单个后端的健康状态。支持两种模式：

- **轻量检测**（默认）：仅检查 CLI 是否存在 + 版本兼容性
- **深度检测**（`deep: true`）：额外向模型发送一条测试消息，验证 Token/模型/配额是否真正可用

```ts
// 轻量检测：仅检查 CLI 存在性和版本
const result = await porygon.checkBackend("claude");
// { available: true, version: "1.2.0", supported: true }

// 深度检测：真实调用模型验证可用性
const deep = await porygon.checkBackend("claude", { deep: true, model: "haiku" });
// { available: true, version: "1.2.0", supported: true, modelVerified: true }
// { available: true, version: "1.2.0", supported: true, modelVerified: false, warnings: ["模型验证失败: ..."] }
```

**CheckBackendOptions：**

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `deep` | `boolean` | `false` | 启用深度检测，向模型发送测试消息 |
| `model` | `string?` | 后端默认 | 深度检测使用的模型 |
| `timeoutMs` | `number` | `15000` | 深度检测超时（毫秒） |
| `envVars` | `Record<string, string>` | - | 注入到深度检测子进程的环境变量（用于隔离第三方后端凭证） |

**HealthCheckResult：**

| 字段 | 类型 | 说明 |
|------|------|------|
| `available` | `boolean` | 后端是否可用 |
| `version` | `string?` | CLI 版本号 |
| `supported` | `boolean?` | 版本是否在测试范围内 |
| `warnings` | `string[]?` | 兼容性警告 |
| `error` | `string?` | 错误信息 |
| `modelVerified` | `boolean?` | 深度检测时模型是否真正响应 |

---

### `porygon.healthCheck(): Promise<Record<string, HealthCheckResult>>`

对所有已注册后端进行健康检查（并行执行）。

```ts
const health = await porygon.healthCheck();
// { claude: { available: true, version: "1.2.0", supported: true }, opencode: { available: false, error: "..." } }
```

---

### `porygon.use(direction, fn): () => void`

注册拦截器，返回取消注册函数。

**direction**: `"input"` | `"output"`

**InterceptorFn 签名：**

```ts
type InterceptorFn = (
  text: string,
  context: InterceptorContext
) => string | boolean | undefined | Promise<string | boolean | undefined>;
```

- 返回 `string`：替换文本
- 返回 `false`：拒绝消息（抛出 `InterceptorRejectedError`）
- 返回 `true` / `undefined`：不修改，传递原始文本

**示例：**

```ts
// 输入拦截：追加指令
const unuse = porygon.use("input", (text) => text + "\n请用 Markdown 格式回答");

// 输出拦截：过滤敏感信息
porygon.use("output", (text) => text.replace(/sk-[a-zA-Z0-9]{48}/g, "[REDACTED]"));

// 拒绝过长输入
porygon.use("input", (text) => { if (text.length > 10000) return false; });

// 取消注册
unuse();
```

---

### 防护拦截器

内置的 prompt 注入防护工具函数。

#### `createInputGuard(options?): InterceptorFn`

检测并阻止常见 prompt 注入攻击（内置 13 条中英文模式）。

```ts
import { createInputGuard } from "@snack-kit/porygon";

// 使用内置规则
porygon.use("input", createInputGuard());

// 自定义配置
porygon.use("input", createInputGuard({
  blockedPatterns: [/我是管理员/, /sudo mode/i],
  action: "reject",       // "reject"（拒绝）| "redact"（替换为 [REDACTED]）
  backends: ["claude"],    // 仅对指定后端生效
  customCheck: (text) => text.includes("bypass"),
}));
```

#### `createOutputGuard(options?): InterceptorFn`

检测输出中的敏感关键词泄露。

```ts
import { createOutputGuard } from "@snack-kit/porygon";

porygon.use("output", createOutputGuard({
  sensitiveKeywords: ["内部API密钥", "sk-xxxx"],
  action: "redact",
}));
```

**GuardOptions：**

| 字段 | 类型 | 说明 |
|------|------|------|
| `blockedPatterns` | `RegExp[]` | 额外阻止模式（输入侧） |
| `sensitiveKeywords` | `string[]` | 敏感关键词列表（输出侧） |
| `action` | `"reject"` \| `"redact"` | 触发时的处理动作，输入默认 reject，输出默认 redact |
| `customCheck` | `(text, context) => boolean` | 自定义判定函数 |
| `backends` | `string[]` | 仅对指定后端生效 |

---

### `porygon.listSessions(backend?, options?): Promise<SessionInfo[]>`

列出指定后端的历史会话。

```ts
const sessions = await porygon.listSessions("claude", { limit: 10 });
// SessionInfo: { sessionId, backend, summary?, lastModified, cwd?, metadata? }
```

---

### `porygon.abort(backend, sessionId): void`

中止正在运行的查询。

---

### `porygon.settings(backend, newSettings?): Promise<Record<string, unknown>>`

读取或更新后端设置。

```ts
const settings = await porygon.settings("claude");
await porygon.settings("claude", { model: "opus" });
```

---

### `porygon.listModels(backend?): Promise<ModelInfo[]>`

获取后端可用模型列表。

```ts
const models = await porygon.listModels("claude");
// ModelInfo: { id: string, name?: string, provider?: string }
```

---

### `porygon.getTools(force?): Promise<string[]>`

获取当前可用的工具列表。首次调用通过发起最小化对话（`maxTurns: 1`）从 `system` 事件中提取，结果会被缓存，后续调用直接返回缓存。

```ts
const tools = await porygon.getTools();
console.log(tools);
// ["Bash", "Read", "Edit", "Write", "Glob", "Grep", "mcp__xxx__yyy", ...]

// 强制刷新缓存
const fresh = await porygon.getTools(true);
```

> 仅 Claude 后端支持此能力，其他后端返回空数组。

---

### `porygon.getCapabilities(backend?): AdapterCapabilities`

获取后端能力声明。

```ts
const caps = porygon.getCapabilities("claude");
// {
//   features: Set<"streaming" | "session-resume" | "system-prompt" | "tool-restriction" | "mcp" | ...>,
//   streamingMode: "delta" | "chunked",
//   outputFormats: string[],
//   testedVersionRange: string,
// }
```

**`streamingMode`：**
- `"delta"` — 后端原生产生增量 `stream_chunk` 事件（OpenCode；Claude 启用 `streamPartialMessages` 时也为此模式）
- `"chunked"` — 适配器将完整 assistant 拆分为 `stream_chunk` + `assistant`（Claude 关闭 `streamPartialMessages` 时）

---

### `porygon.dispose(): Promise<void>`

终止所有进程、中止所有查询、清空缓存。使用完毕后必须调用。

---

## 错误处理

所有错误继承自 `PorygonError`，包含 `code` 字段：

```ts
import { PorygonError, AdapterNotFoundError } from "@snack-kit/porygon";

try {
  await porygon.run({ prompt: "hi", backend: "nonexistent" });
} catch (err) {
  if (err instanceof AdapterNotFoundError) {
    console.log(err.code); // "ADAPTER_NOT_FOUND"
  }
}
```

| 错误类 | code | 场景 |
|--------|------|------|
| `AdapterNotFoundError` | `ADAPTER_NOT_FOUND` | 后端未注册 |
| `AdapterNotAvailableError` | `ADAPTER_NOT_AVAILABLE` | CLI 不可用 |
| `AdapterIncompatibleError` | `ADAPTER_INCOMPATIBLE` | 版本不兼容 |
| `SessionNotFoundError` | `SESSION_NOT_FOUND` | 会话不存在 |
| `InterceptorRejectedError` | `INTERCEPTOR_REJECTED` | 拦截器拒绝 |
| `AgentExecutionError` | `AGENT_EXECUTION_ERROR` | 执行异常 |
| `AgentTimeoutError` | `AGENT_TIMEOUT` | 超时 |
| `ConfigValidationError` | `CONFIG_VALIDATION_ERROR` | 配置校验失败 |

## 事件

Porygon 继承自 EventEmitter：

```ts
porygon.on("health:degraded", (backend: string, warning: string) => {
  console.warn(`${backend} 降级: ${warning}`);
});
```

| 事件 | 参数 | 说明 |
|------|------|------|
| `health:degraded` | `(backend, warning)` | 后端版本不兼容时触发 |

## 完整导出列表

```ts
// 核心
export { Porygon, createPorygon } from "@snack-kit/porygon";
export type { PorygonEvents, HealthCheckResult, CheckBackendOptions } from "@snack-kit/porygon";

// 类型
export type {
  PorygonConfig, BackendConfig, ProxyConfig,
  PromptRequest, McpServerConfig,
  AgentMessage, AgentSystemMessage, AgentAssistantMessage,
  AgentToolUseMessage, AgentStreamChunkMessage, AgentResultMessage, AgentErrorMessage,
  AdapterCapabilities, CompatibilityResult, SessionInfo, SessionListOptions, ModelInfo,
  InterceptorFn, InterceptorDirection, InterceptorContext,
  GuardOptions, GuardAction,
  IAgentAdapter,
} from "@snack-kit/porygon";

// 错误
export {
  PorygonError, AdapterNotFoundError, AdapterNotAvailableError,
  AdapterIncompatibleError, SessionNotFoundError, InterceptorRejectedError,
  AgentExecutionError, AgentTimeoutError, ConfigValidationError,
} from "@snack-kit/porygon";

// 防护拦截器
export { createInputGuard, createOutputGuard } from "@snack-kit/porygon";

// 交互式会话
export { InteractiveSession } from "@snack-kit/porygon";

// 适配器（自定义扩展用）
export { AbstractAgentAdapter, ClaudeAdapter, OpenCodeAdapter } from "@snack-kit/porygon";
```

## 典型使用模式

### 带防护的流式调用

```ts
import { createPorygon, createInputGuard, createOutputGuard } from "@snack-kit/porygon";

const porygon = createPorygon({
  defaultBackend: "claude",
  backends: {
    claude: { model: "sonnet", interactive: false },
  },
});

porygon.use("input", createInputGuard());
porygon.use("output", createOutputGuard({ sensitiveKeywords: ["SECRET"] }));

for await (const msg of porygon.query({ prompt: "解释 async/await" })) {
  if (msg.type === "stream_chunk") process.stdout.write(msg.text);
  if (msg.type === "result") console.log("\n完成", msg.costUsd);
}

await porygon.dispose();
```

### 恢复会话

```ts
const sessions = await porygon.listSessions("claude", { limit: 1 });
if (sessions.length > 0) {
  const answer = await porygon.run({
    prompt: "继续上次的工作",
    resume: sessions[0].sessionId,
  });
}
```

### 注入环境变量与 MCP 服务器

```ts
const answer = await porygon.run({
  prompt: "查找相关文档后回答",
  envVars: { CLAUDE_CODE_OAUTH_TOKEN: "token-xxx" },
  mcpServers: {
    "context7": { command: "npx", args: ["-y", "@context7/mcp"] },
  },
});
```

### 工具控制

```ts
// 查询所有可用工具
const tools = await porygon.getTools();
console.log(tools); // ["Bash", "Read", "Edit", ...]

// 黑名单：禁用危险工具
await porygon.run({
  prompt: "分析代码",
  disallowedTools: ["Bash", "Edit", "Write"],
});

// 白名单：只允许只读工具（内部自动转为黑名单，getTools 结果有缓存）
await porygon.run({
  prompt: "阅读代码并回答",
  onlyTools: ["Read", "Grep", "Glob"],
});

// 配置级黑名单：全局 + 后端级 + 请求级三层叠加
const porygon = createPorygon({
  defaults: { disallowedTools: ["WebSearch"] },
  backends: {
    claude: {
      interactive: false,
      disallowedTools: ["Bash"],
    },
  },
});
// 此时 disallowedTools = ["WebSearch", "Bash"]，无需每次请求重复传入
```

---

### 健康检查后选择可用后端

```ts
// 轻量检查：CLI 存在性 + 版本
const result = await porygon.checkBackend("claude");
if (!result.available) console.error(result.error);

// 深度检查：验证模型真正可用（Token/配额/模型名）
const deep = await porygon.checkBackend("claude", { deep: true, model: "haiku" });
if (deep.modelVerified) console.log("模型验证通过");

// 批量检查所有后端
const health = await porygon.healthCheck();
const backend = health.claude?.available ? "claude" : "opencode";
const answer = await porygon.run({ prompt: "hello", backend });
```

## 架构

```
Porygon (Facade)
├── InterceptorManager     # 输入/输出拦截器流水线
├── ProcessManager         # 子进程生命周期管理
├── SessionManager         # 会话缓存与委托
└── Adapters
    ├── ClaudeAdapter      # claude -p --output-format stream-json
    └── OpenCodeAdapter    # opencode serve + REST API + SSE
```

**适配器能力对比：**

| 能力 | Claude | OpenCode |
|------|--------|----------|
| streaming | `chunked` | `delta` |
| session-resume | ✓ | ✓ |
| system-prompt | ✓ | ✓ |
| tool-restriction | ✓ | - |
| mcp | ✓ | - |
| subagents | ✓ | - |
| worktree | ✓ | - |
| serve-mode | - | ✓ |

## 开发

```bash
npm install
npm run build       # 构建（ESM + CJS + DTS）
npm test            # 运行测试
npm run dev         # watch 模式构建
npm run playground  # 启动 Playground
```

---

## Changelog

### v0.10.0

#### 新特性

- **`deepCheck()` 适配器级 API 凭证验证** — `ClaudeAdapter` 新增 `deepCheck()` 方法，直接调用 Anthropic Messages API 验证凭证有效性，无需启动 CLI 子进程。支持 `ANTHROPIC_AUTH_TOKEN`、`ANTHROPIC_API_KEY` 两种凭证类型，OAuth token 和无显式凭证时自动 fallback 到 CLI 方式
- **`CheckBackendOptions.envVars`** — 深度检测支持注入环境变量，用于隔离第三方后端凭证验证场景

#### 改进

- **Claude 环境变量隔离增强** — 子进程环境变量构建时，除 `CLAUDECODE` 外额外清除 `ANTHROPIC_*` 和 `CLAUDE_CODE_*` 前缀变量，确保认证只来自显式传入的 `envVars`，避免宿主机凭证泄漏到第三方后端
- **`envVars` 空字符串覆盖支持** — `request.envVars` 中值为空字符串时也会写入环境变量（之前被忽略），允许通过空值覆盖系统环境变量实现凭证隔离
- **`IAgentAdapter` 接口扩展** — 新增可选 `deepCheck()` 方法定义，自定义适配器可实现此方法提供免 CLI 的凭证验证

---

### v0.9.0

#### 新特性

- **Claude token 级流式输出** — 新增 `BackendConfig.streamPartialMessages` 配置项（默认 `true`），启用后 Claude CLI 添加 `--include-partial-messages` 参数，实现真正的逐 token 流式输出（`stream_event` → `text_delta`）。此前 Claude 后端的文本输出需等整个 turn 完成后才一次性返回，对于长文本生成（如代码生成）场景体验较差，启用后文本会以 `stream_chunk` 消息逐步到达，用户可实时看到生成过程

#### 改进

- Claude 适配器的 `streamingMode` 在启用 `streamPartialMessages` 时等效于 `"delta"` 模式，与 OpenCode 适配器行为一致

---

### v0.8.0

#### 改进

- **`timeoutMs` 语义变更为空闲超时** — `timeoutMs` 不再表示进程的绝对超时时间，而是"空闲超时"：进程无任何输出（stdout/stderr）超过此时间才触发终止。每次收到输出会重置计时器，更适合 LLM 调用耗时不可预测的场景
- **默认空闲超时调整** — 默认值从 `300_000`（5 分钟）调整为 `120_000`（2 分钟）

#### 破坏性变更

- **`timeoutMs` 语义变更** — 从绝对超时改为空闲超时（idle timeout）。如果依赖旧的绝对超时行为，需要调整使用方式。对于模型思考时间可能较长的场景，建议适当调大此值
- **默认超时时间缩短** — 从 5 分钟缩短至 2 分钟。如需更长超时，请在 `defaults.timeoutMs` 或请求参数中显式设置

---

### v0.7.0

#### 新特性

- **`InteractiveSession` 多轮对话封装** — 新增 `InteractiveSession` 类，封装 per-turn spawn + `--resume` 模式，提供透明的多轮对话体验。通过 `porygon.session()` 创建，调用 `session.send(prompt)` 自动管理 sessionId 续接
- **`EphemeralProcess` 超时错误明确化** — 进程超时终止后抛出明确的 `Process timed out after ${timeoutMs}ms` 错误，替代之前模糊的退出码错误信息

#### 改进

- `EphemeralProcess` 内部新增 `timedOut` 状态标记，区分正常退出与超时终止场景

---

### v0.6.0

#### 改进

- **Windows 跨平台兼容** — 使用 `cross-spawn` 替换原生 `child_process.spawn`，解决 Windows 上因 PATH 解析、PATHEXT 扩展名（`.exe`、`.cmd` 等）导致的 `ENOENT` 错误。所有进程启动（`EphemeralProcess`、`PersistentProcess`）均已适配
- **`isAvailable()` Windows 适配** — Claude adapter 的 CLI 存在性检测在 Windows 上使用 `where` 命令替代 `which`

#### 新增依赖

- `cross-spawn` — Node.js 生态标准的跨平台进程启动库，正确处理 Windows 上的命令解析、参数转义和 PATHEXT 查找

---

### v0.5.0

#### 新特性

- **`porygon.getTools(force?)`** — 获取当前可用的工具列表（包括内置工具和 MCP 插件工具）。首次调用通过最小化对话获取并缓存，后续直接返回缓存，传入 `force: true` 强制刷新
- **`IAgentAdapter.getTools?(force?)`** — 适配器接口新增可选方法，支持缓存与强制刷新
- **`onlyTools`（请求参数）** — 仅允许使用的工具白名单，内部通过 `getTools()` 获取全量列表后差集计算转为 `disallowedTools`，工具缓存避免重复对话开销
- **配置级工具黑名单** — `BackendConfig` 和 `defaults` 新增 `disallowedTools` 字段，支持全局或后端级统一禁用工具，三层叠加去重
- **`checkBackend(backend, options?)` 深度检测模式** — 新增第二个参数 `CheckBackendOptions`，传入 `{ deep: true }` 时在基础 CLI/版本检查通过后，向模型发送一条极短测试消息（`maxTurns: 1`），验证 Token、模型、配额是否真正可用
- **`CheckBackendOptions`** — 新增选项类型，支持 `deep`（启用深度检测）、`model`（指定验证模型）、`timeoutMs`（深度检测超时，默认 15s）
- **`HealthCheckResult.modelVerified`** — 新增字段，深度检测时标识模型是否真正响应

#### 破坏性变更

- **`PromptRequest.allowedTools` 移除** — Claude CLI 的 `--allowedTools` 实际语义为"免确认"而非"限制范围"，已移除避免误用。替代方案：`onlyTools`（白名单，自动转黑名单）或 `disallowedTools`（黑名单）
- **`porygon.session()` 返回类型变更** — 从 `InteractiveSession` 改为 `Promise<InteractiveSession>`，调用方需 `await`

#### Bug Fixes

- **Claude adapter 类型定义**: 修复 `ClaudeResultEvent` 缺少 `total_cost_usd` 字段导致的 TS2322 类型错误

### v0.4.0

#### Bug Fixes

- **Claude adapter**: 修复 `result` 事件中 `inputTokens` 和 `outputTokens` 始终为 0 的问题。Claude CLI 的 token 统计位于 `event.usage.input_tokens` / `event.usage.output_tokens`，而非顶层的 `event.input_tokens` / `event.output_tokens`
- **Claude adapter**: 修复 `costUsd` 始终为 `undefined` 的问题。Claude CLI 使用 `total_cost_usd` 字段名，而非 `cost_usd`

### v0.3.0

#### Bug Fixes

- **Claude adapter**: 将 prompt 从 CLI 参数 (`-p <prompt>`) 改为通过 stdin 传递 (`--print` + stdin pipe)，修复超长 prompt 导致的偶发 403 "Request not allowed" 错误

#### 改进

- `SpawnOptions` 新增 `stdinData` 字段，`EphemeralProcess.executeStreaming` 支持向子进程 stdin 写入数据

### v0.2.0

#### 新特性

- **`checkBackend(backend)`** — 新增单后端健康检查方法，无需检查全部后端
- **`HealthCheckResult`** — 健康检查返回扁平化结构 `{ available, version?, supported?, warnings?, error? }`
- **`BackendConfig.cliPath`** — Claude CLI 自定义路径，有类型提示
- **`BackendConfig.interactive`** — 布尔值控制是否跳过权限确认
- **`BackendConfig.serverUrl`** — OpenCode 远程服务地址（顶级字段）
- **`BackendConfig.apiKey`** — API Key 认证（顶级字段）
- **`AgentAssistantMessage.turnComplete`** — 标记 assistant 消息为 turn 完整文本汇总，流式消费者可据此跳过重复文本
- **`AdapterCapabilities.streamingMode`** — 声明后端的流式模式（`"delta"` 或 `"chunked"`）
- **`IAgentAdapter.deleteSession?`** — 可选的会话删除方法（接口已定义，适配器待实现）
- **`tool_result` 映射** — Claude 的 `tool_result` 内容块现在映射为带 `output` 字段的 `tool_use` 消息
- **`session_id` 自动提取** — Claude 原始事件中的 `session_id` 自动映射到 `AgentMessage.sessionId`
- **CJS 输出** — 同时构建 ESM 和 CJS 格式，支持 Electron 等 CJS 环境

#### 破坏性变更

- **`mapClaudeEvent()` 返回类型变更** — 从 `AgentMessage | null` 改为 `AgentMessage[]`（仅影响直接调用 mapper 的代码）
- **Claude 消息流变更** — `assistant` 事件拆分为 `stream_chunk[]` + `assistant(turnComplete: true)`。之前同时累加 `assistant` 和 `stream_chunk` 文本的代码需要调整：只累加 `stream_chunk`，或检查 `turnComplete` 标记
- **`healthCheck()` 返回类型变更** — 从 `{ available, compatibility: CompatibilityResult | null, error? }` 改为 `HealthCheckResult`（`{ available, version?, supported?, warnings?, error? }`），移除嵌套的 `compatibility` 字段
- **`AdapterCapabilities` 新增必填字段** — `streamingMode: "delta" | "chunked"` 为必填，自定义适配器需添加

#### 改进

- `mergeRequest()` 添加详细 JSDoc 注释说明配置合并策略
- `mapAssistantContent()` 支持多内容块拆分，不再丢失 tool_use 块

### v0.1.0 — 初始版本

- 基础 Facade + Adapter 架构
- Claude Code CLI 适配器（`claude -p --output-format stream-json`）
- OpenCode 适配器（`opencode serve` + REST API + SSE）
- 输入/输出拦截器流水线
- 防护拦截器（prompt 注入检测、敏感信息过滤）
- 进程生命周期管理（EphemeralProcess / PersistentProcess）
- 会话管理与恢复
- Zod 配置校验
- ESM 输出