Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ddc1071
feat: add support for local LLM provider and integrate Google OAuth flow
reikernodd May 8, 2026
98fb5d3
feat: implement Gemini OAuth 2.0 flow with automated credential loadi…
reikernodd May 9, 2026
b732ae4
chore: update hello-agent language to English and refine .gitignore p…
reikernodd May 9, 2026
ae7e1fa
test: add useLayoutEffect mock to ConsoleOAuthFlow tests
reikernodd May 10, 2026
0195760
Merge branch 'main' into main
reikernodd May 10, 2026
421429d
refactor: improve type safety, add environment variable support for A…
reikernodd May 12, 2026
75c230d
chore: add .files directory to .gitignore
reikernodd May 12, 2026
a8d1e83
Merge branch 'claude-code-best:main' into main
reikernodd May 12, 2026
942a7f9
docs: reformat README markdown tables and fix whitespace inconsistencies
reikernodd May 13, 2026
20aee68
refactor: import getAPIProvider and normalize arrow function syntax i…
reikernodd May 13, 2026
5134e9a
test: increase test timeouts and improve mock consistency across comp…
reikernodd May 15, 2026
db805ed
Merge branch 'claude-code-best:main' into main
reikernodd May 15, 2026
4d13822
chore: update .gitignore to ignore OAuth.json within .files directory
reikernodd May 15, 2026
0e3c232
Merge branch 'main' into main
reikernodd May 17, 2026
a817a76
Merge branch 'claude-code-best:main' into main
reikernodd May 30, 2026
079986d
Merge branch 'main' into main
reikernodd Jun 2, 2026
eaeefaf
Refactor tests and improve mocking for better isolation
reikernodd Jun 3, 2026
9d71b8e
Merge branch 'main' of https://github.com/reikernodd/claude-code
reikernodd Jun 3, 2026
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
185 changes: 185 additions & 0 deletions .files/test_fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Test Isolation Fixes

Two bugs. Two files touched.

---

## 1. `packages/modifiers-napi/src/__tests__/index.test.ts`

**Problem:** `mock.module('bun:ffi', ...)` is declared at module scope with no cleanup.
Bun reuses workers across files in a full suite run, so the mock bleeds into other
files that import anything touching `bun:ffi`. When `index.test.ts` runs _after_
another file has already loaded `bun:ffi` into the module registry, the module-level
mock declaration loses the race and the test gets the wrong module — or nothing at all.

**Fix:** Add `afterAll(() => mock.restore())`.

```typescript
// packages/modifiers-napi/src/__tests__/index.test.ts

import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test'

let ffiShouldThrow = false
let nativeFlags = 0
let dlopenCalls = 0

mock.module('bun:ffi', () => ({
FFIType: {
i32: 0,
u64: 0,
},
dlopen: () => {
dlopenCalls++
if (ffiShouldThrow) {
throw new Error('ffi load failed')
}
return {
symbols: {
CGEventSourceFlagsState: () => nativeFlags,
},
}
},
}))

// ✅ NEW: restore the bun:ffi mock after this file finishes
// so it doesn't bleed into other files running in the same worker
afterAll(() => {
mock.restore()
})

beforeEach(() => {
ffiShouldThrow = false
nativeFlags = 0
dlopenCalls = 0
})

// ... rest of file unchanged
```

---

## 2. `src/utils/staticRender.tsx`

**Problem:** `await instance.waitUntilExit()` hangs indefinitely when Ink's reconciler
commit phase doesn't complete. `waitUntilExit()` only resolves when `exit()` is called
inside `useLayoutEffect` — but if `@anthropic/ink`'s `wrappedRender` has stale
process-level state from a previous test file (singleton reconciler, leaked
`process.on('exit')` handlers, a cached fiber root), React never commits, `useLayoutEffect`
never fires, and the whole render silently hangs until the 10s test timeout kills it.

This only surfaces in the full suite because Bun shares workers across files sequentially.
Running the file alone gives it a clean worker with no prior Ink state.

**Fix:** Race `waitUntilExit()` against a 3s safety timeout. On timeout, call
`instance.unmount()` to force Ink cleanup and throw a descriptive error instead of
a silent 10s hang.

Also: explicitly remove the stream `data` listener and destroy the stream after render.
Without this, the PassThrough stream stays open and its listener accumulates garbage
across calls in the same process lifetime.

```typescript
// src/utils/staticRender.tsx

import * as React from 'react';
import { useLayoutEffect } from 'react';
import { PassThrough } from 'stream';
import stripAnsi from 'strip-ansi';
import { wrappedRender as render, useApp } from '@anthropic/ink';

function RenderOnceAndExit({ children }: { children: React.ReactNode }): React.ReactNode {
const { exit } = useApp();

useLayoutEffect(() => {
const timer = setTimeout(exit, 0);
return () => clearTimeout(timer);
}, [exit]);

return <>{children}</>;
}

const SYNC_START = '\x1B[?2026h';
const SYNC_END = '\x1B[?2026l';

function extractFirstFrame(output: string): string {
const startIndex = output.indexOf(SYNC_START);
if (startIndex === -1) return output;

const contentStart = startIndex + SYNC_START.length;
const endIndex = output.indexOf(SYNC_END, contentStart);
if (endIndex === -1) return output;

return output.slice(contentStart, endIndex);
}

export async function renderToAnsiString(node: React.ReactNode, columns?: number): Promise<string> {
let output = '';

const stream = new PassThrough();
if (columns !== undefined) {
(stream as unknown as { columns: number }).columns = columns;
}

// ✅ NEW: named handler so we can remove it after render
const dataHandler = (chunk: Buffer) => {
output += chunk.toString();
};
stream.on('data', dataHandler);

const instance = await render(<RenderOnceAndExit>{node}</RenderOnceAndExit>, {
stdout: stream as unknown as NodeJS.WriteStream,
patchConsole: false,
});

// ✅ NEW: race waitUntilExit against a 3s hard limit.
// If Ink's reconciler is stuck (stale worker state from a prior test file),
// this surfaces a real error instead of silently hanging for 10s.
await Promise.race([
instance.waitUntilExit(),
new Promise<void>((_, reject) =>
setTimeout(() => {
instance.unmount();
reject(
new Error(
'[staticRender] Ink render did not exit within 3s — wrappedRender may have stale process state from a prior test file',
),
);
}, 3000),
),
]);

// ✅ NEW: clean up the stream so it doesn't accumulate across calls
stream.off('data', dataHandler);
stream.destroy();

return extractFirstFrame(output);
}

export async function renderToString(node: React.ReactNode, columns?: number): Promise<string> {
const output = await renderToAnsiString(node, columns);
return stripAnsi(output);
}
```

---

## If AutofixProgress tests still hang after this

The 3s safety net will stop the silent timeout and give you a real error message.
If you're still seeing the hang, the root is inside `wrappedRender` from `@anthropic/ink`.
Look for any of these in that file:

- A module-level singleton (cached app instance, fiber root, reconciler)
- `process.on('exit')` / `process.on('SIGTERM')` handlers added on each `render()` call but never removed
- A global `stdout` patch applied once and assumed fresh on every call

Share that file and the exact line can be pinpointed.

---

## Summary

| File | Change | Why |
| ------------------ | ------------------------------------------------------- | ------------------------------------------------ |
| `index.test.ts` | Add `afterAll(() => mock.restore())` | Stops `bun:ffi` mock bleeding into other workers |
| `staticRender.tsx` | Race `waitUntilExit()` with 3s timeout + stream cleanup | Surfaces real errors instead of silent 10s hangs |
10 changes: 6 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ coverage
src/utils/vendor/

# AI tool runtime directories
.agents/
.claude/
.omx/
.docs/task/
.agents/*
.claude/*
.omx/*
.docs/task/*
.files/OAuth.json

# Binary / screenshot files (root only)
/*.png
*.bmp
Expand Down
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
| **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 |
| **Channels 频道通知** | MCP 服务器推送外部消息到会话(飞书/Slack/Discord/微信等),`--channels plugin:name@marketplace` 启用 | [文档](https://ccb.agent-aura.top/docs/features/channels) |
| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok 兼容 (`/login`) | [文档](https://ccb.agent-aura.top/docs/features/all-features-guide) |
| **本地 LLM (Ollama/Local)** | 支持 Ollama, LM Studio, Jan.ai, LocalAI。支持在 `/login` 中一键拉取模型、检查硬件状态、本地优先运行。 | /login 选择 Local LLM |
| Voice Mode | 语音输入,支持豆包语言输入(`/voice doubao`) | [文档](https://ccb.agent-aura.top/docs/features/voice-mode) |
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |
Expand Down Expand Up @@ -144,10 +145,17 @@ bun run build

### 👤 新人配置 /login

首次运行后,在 REPL 中输入 `/login` 命令进入登录配置界面,选择 **Anthropic Compatible** 即可对接第三方 API 兼容服务(无需 Anthropic 官方账号)。
选择 OpenAI 和 Gemini 对应的栏目都是支持相应协议的
首次运行后,在 REPL 中输入 `/login` 命令进入登录配置界面:

需要填写的字段:
1. **Anthropic Compatible**: 对接第三方 API 兼容服务(OpenRouter、AWS Bedrock 代理等)。
2. **OpenAI / Gemini / Grok**: 对应各自协议的云端服务。
- **Gemini (Google Auth)**: 支持交互式浏览器登录。
1. 在 Google Cloud Console 的 **APIs & Services > OAuth consent screen** 中配置 OAuth 客户端(User Type 设为 External)。
2. 下载 credentials JSON 文件并保存到项目根目录的 `.files/OAuth.json`。 3. 在 `/login` 配置界面中留空 API Key 并按 Enter,程序将自动拉起浏览器完成授权并自动拉取模型列表。
3. **Local LLM**: **(推荐)** 使用本地运行的模型。
- 支持 **Ollama**, **LM Studio**, **Jan.ai**, **LocalAI**。
- **Ollama 深度集成**: 可直接在 CLI 中查看已安装模型,或输入模型名(如 `llama3.1`)一键拉取(Pull)。支持模型列表交互切换和硬件状态自动检测。
- 自动检测本地运行状态和默认端口。

| 📌 字段 | 📝 说明 | 💡 示例 |
| ------------ | ------------- | ---------------------------- |
Expand All @@ -157,9 +165,22 @@ bun run build
| Sonnet Model | 均衡模型 ID | `claude-sonnet-4-6` |
| Opus Model | 高性能模型 ID | `claude-opus-4-6` |

- ⌨️ **Tab / Shift+Tab** 切换字段,**Enter** 确认并跳到下一个,最后一个字段按 Enter 保存
### 🩺 系统诊断 /doctor

> ℹ️ 支持所有 Anthropic API 兼容服务(如 OpenRouter、AWS Bedrock 代理等),只要接口兼容 Messages API 即可。
如果你在使用过程中遇到环境问题(尤其是本地模型运行缓慢或无法连接),可以使用 `/doctor` 命令进行全方位诊断:

- **硬件负载**: 自动显示当前 CPU 型号、核心数、剩余内存 (RAM) 以及系统架构。
- **本地环境**: 检查 Ollama 等本地 Runner 是否正在运行,并列出所有可用模型。
- **配置校验**: 检查环境变量(如 `LOCAL_BASE_URL`)和权限设置。
- **故障排查**: 识别多个重复安装的版本、过期的版本锁或权限不足的更新。

## 环境变量 (Environment Variables)

除了交互式 `/login` 配置外,你也可以通过环境变量直接配置:

- `LOCAL_BASE_URL`: 本地 LLM 运行地址 (例如 `http://localhost:11434`)。
- `LOCAL_MODEL`: 本地 LLM 模型名称 (例如 `llama3.1`)。用于 `local` provider 时覆盖默认模型。
- `GEMINI_BASE_URL`: Gemini API 的自定义基础地址。

## Feature Flags

Expand Down
Loading