Skip to content
Merged
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
26 changes: 23 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,35 @@ permissions:
contents: read

jobs:
lint:
name: Lint and type-check
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type-check
run: npm run typecheck
- name: Verify package contents
run: npm pack --dry-run

test:
name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
# macOS is the primary developer platform and the one whose paths,
# shell profile handling and node:sqlite build differ from Linux.
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [20, 22, 24]
steps:
- name: Check out repository
Expand All @@ -33,5 +55,3 @@ jobs:
run: npm ci
- name: Run tests
run: npm test
- name: Verify package contents
run: npm pack --dry-run
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
node_modules/
dist/
.claude/
.DS_Store
*.tsbuildinfo
.env
.env.*
coverage/
.worktrees/
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@
npm ci # 按 package-lock.json 安装精确依赖
npm run build # 编译 TypeScript 到 dist/(使用 tsc)
npm test # 先构建,再执行 node --test(在 dist/ 下自动发现测试文件;Windows 的 cmd.exe 不做 glob 展开,故用目录自动发现而非 dist/test/*.test.js)
npm run lint # ESLint(flat config,见 eslint.config.js)
npm run typecheck # tsc --noEmit,同时检查 src/ 与 test/
```

运行单个测试文件:`npm run build && node --test dist/test/catalog.test.js`
运行完整的本地 CI 检查(安装 + 测试 + 包校验):`make check`
运行单个测试文件:`npm run test:one -- dist/test/catalog.test.js`(或 `make test-one F=test/catalog`)
运行完整的本地 CI 检查(安装 + 测试 + lint + 包校验):`make check`

`Makefile` 提供了一些便捷目标,运行 `make` 可查看完整列表。常见的有:`make doctor`、`make test`、`make build`、`make version`。

## 架构

本包声明了 `"type": "module"` 并使用 `rootDir: "."` 编译,因此**所有 import 都使用 `.js` 后缀,且所有路径以仓库根目录为基准**(例如 `import { loadConfig } from "./config.js"`,测试位于 `test/` 下,导入如 `"../src/catalog.js"`)。

`src/` 中不使用 `any`:客户端请求、Provider 响应与 SSE 事件这类无法预先验证的 JSON 一律建模为 `src/json.ts` 的 `JsonRecord`,通过 `recStr`/`recNum`/`recCount`/`recObj`/`recObjs`/`parse` 读取。字段名写错或漏掉非对象检查会直接编译失败,而不是变成一次运行时 undefined。tsconfig 开启了 `noUncheckedIndexedAccess` 等严格标志,ESLint 只保留 tsc 看不见的规则(见 `eslint.config.js`)。

### 两套协议转换路径

适配器对外暴露三个请求端点,在三种 API 格式之间进行转换。本地面向客户端(client-facing)的端点是:
Expand All @@ -34,7 +38,7 @@ npm test # 先构建,再执行 node --test(在 dist/ 下自动发

转换函数集中在 `src/convert/`,按**上游协议**(而非转换方向)拆分为四个文件:

- `shared.ts` — 跨方向 helper:图片/effort/thinking/三套 tool-choice 转换、采样参数、JSON 解析等
- `shared.ts` — 跨方向 helper:图片/effort/thinking/三套 tool-choice 转换、采样参数(JSON 字段的读取统一走 `src/json.ts`)
- `chat.ts` — 上游 = Chat Completions 的全部方向:`toChatRequest` / `fromChatResponse`(Anthropic ↔ Chat Completions)、`toChatCompletionsRequest` / `fromChatResponseToResponses`(Responses ↔ Chat Completions)
- `responses.ts` — 上游 = Responses:`toResponsesRequest` / `fromResponsesResponse`(Anthropic ↔ Responses);`toResponsesRequestFromChat` / `fromResponsesResponseToChat`(本地 Chat Completions ↔ Responses;分别复用 `toResponsesRequest`/`fromResponsesResponse` 加 `anthropic.ts` 的两个新函数拼出,不重复写转换逻辑)
- `anthropic.ts` — 上游 = Anthropic(自定义 Provider 声明 `protocol: "anthropic"` 时专属):`toAnthropicRequest` / `fromAnthropicResponse`(Responses ↔ Anthropic;Codex 只会看到本地 Responses 端点,仍需经此转换才能到达一个原生 Anthropic 上游);`toAnthropicRequestFromChat` / `fromAnthropicResponseToChat`(本地 Chat Completions ↔ Anthropic,供 `/v1/chat/completions` 使用,只映射主流字段,不映射 DeepSeek 的 `thinking`/`reasoning_effort` 扩展)
Expand Down
17 changes: 14 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ CLI := node dist/src/cli.js

.DEFAULT_GOAL := help

.PHONY: help install build test check ci pack install-local link unlink \
.PHONY: help install build test test-one lint typecheck check ci pack install-local link unlink \
claude codex proxy doctor version clean

help: ## Show available targets
Expand All @@ -26,10 +26,21 @@ build: node_modules/.package-lock.json ## Compile TypeScript into dist/
test: node_modules/.package-lock.json ## Build and run the test suite
$(NPM) test

check: test ## Run tests and verify the npm package contents
# make test-one F=test/streaming — a single test file, quoted so a bare
# `make test-one` doesn't expand to every file in the directory.
test-one: node_modules/.package-lock.json ## Build and run one test file (F=test/<name>)
$(NPM) run test:one -- "dist/$(F).test.js"

lint: node_modules/.package-lock.json ## Lint the repository
$(NPM) run lint

typecheck: node_modules/.package-lock.json ## Type-check src/ and test/ without emitting
$(NPM) run typecheck

check: test lint ## Run tests, lint and verify the npm package contents
$(NPM) pack --dry-run

ci: install test ## Reproduce the GitHub Actions verification locally
ci: install lint test ## Reproduce the GitHub Actions verification locally

pack: build ## Build and create an npm tarball
$(NPM) pack
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,16 +511,21 @@ git clone https://github.com/tzzs/agentx.git
cd agentx
npm ci
npm test
npm run lint
npm run build
```

The project uses TypeScript, native Node.js `fetch`, Node.js ESM, and the built-in `node:test` runner. Tests are compiled into `dist/test` before execution.

`npm run lint` runs ESLint (flat config in `eslint.config.js`), `npm run typecheck` runs `tsc --noEmit` over `src/` and `test/`, and `make check` runs both plus the package check. `src/` is free of `any`: wire payloads are read through the typed accessors in `src/json.ts`.

To run one test file: `npm run test:one -- dist/test/catalog.test.js` (or `make test-one F=test/catalog`).

The test suite covers request/response conversion, system instructions, streaming events, tool calls, provider routing, chat-completion conversion, token usage adapters, storage, and the usage query API. Tests do not require an API key or network access.

## CI and Publishing

GitHub Actions runs the build, tests, and package dry-run for every push and pull request against `main`. Release Please is configured in `.github/workflows/release-please.yml` and creates a release PR from conventional commits. Publishing is configured in `.github/workflows/publish.yml`:
GitHub Actions runs lint and type-check once per push, and the test suite on Node.js 20/22/24 across Linux, macOS and Windows, for every push and pull request against `main`. Release Please is configured in `.github/workflows/release-please.yml` and creates a release PR from conventional commits. Publishing is configured in `.github/workflows/publish.yml`:

1. Add an `NPM_TOKEN` secret to the `npm` GitHub environment.
2. Push a tag matching `v*.*.*`, or manually run **Publish package**.
Expand All @@ -538,7 +543,7 @@ Install Claude Code and ensure `claude` is available in the same shell's `PATH`,

**`Codex not found: the "codex" command is not installed or not on PATH`**

When a client executable is missing, AgentX explains the problem and prints the recommended install command (for example, `npm install -g @openai/codex`). In an interactive terminal it also offers to run that command for you and relaunches the client after a verified install. Decline to install manually; re-run the same `agentx <client>` command afterwards.
When a client executable is missing, AgentX explains the problem and prints the recommended install command (for example, `npm install -g @openai/codex`), then exits. AgentX never installs clients for you — run the command yourself, then re-run the same `agentx <client>` command.

**The port is busy**

Expand Down
9 changes: 7 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,16 +496,21 @@ git clone https://github.com/tzzs/agentx.git
cd agentx
npm ci
npm test
npm run lint
npm run build
```

项目使用 TypeScript、Node.js 原生 `fetch`、Node.js ESM 和内置 `node:test` 测试运行器。测试会先编译到 `dist/test`,再执行编译后的测试。

`npm run lint` 运行 ESLint(flat config 位于 `eslint.config.js`),`npm run typecheck` 对 `src/` 与 `test/` 执行 `tsc --noEmit`,`make check` 则会连同包校验一起跑完这些检查。`src/` 中不使用 `any`:wire payload 统一通过 `src/json.ts` 的带类型访问器读取。

只跑单个测试文件:`npm run test:one -- dist/test/catalog.test.js`(或 `make test-one F=test/catalog`)。

测试覆盖请求/响应转换、system instructions、流式事件、工具调用、Provider 路由、Chat Completions 转换,以及 Token 用量适配器、存储和用量查询 API。测试不需要 API Key,也不依赖网络。

## CI 与发布

GitHub Actions 会在每次 push 和针对 `main` 的 Pull Request 中运行构建、测试和 npm 包内容检查。`.github/workflows/release-please.yml` 会根据 Conventional Commits 创建版本发布 PR。发布配置位于 `.github/workflows/publish.yml`:
GitHub Actions 会在每次 push 和针对 `main` 的 Pull Request 中运行一次 lint 与类型检查,并在 Linux、macOS、Windows 三套系统的 Node.js 20/22/24 矩阵上跑测试。`.github/workflows/release-please.yml` 会根据 Conventional Commits 创建版本发布 PR。发布配置位于 `.github/workflows/publish.yml`:

1. 在 GitHub 的 `npm` environment 中添加 `NPM_TOKEN` Secret。
2. 推送匹配 `v*.*.*` 的版本标签,或手动运行 **Publish package** 工作流。
Expand All @@ -523,7 +528,7 @@ GitHub Actions 会在每次 push 和针对 `main` 的 Pull Request 中运行构

**`Codex not found: the "codex" command is not installed or not on PATH`**

当客户端可执行文件缺失时,AgentX 会说明问题并给出推荐的安装命令(例如 `npm install -g @openai/codex`)。在交互式终端中还会询问是否立即执行该命令,并在确认安装成功后自动重新启动客户端。也可以选择跳过、手动安装,之后再次运行相同的 `agentx <client>` 命令。
当客户端可执行文件缺失时,AgentX 会说明问题并给出推荐的安装命令(例如 `npm install -g @openai/codex`),然后退出。AgentX 不会代为执行安装——请自行运行该命令,之后再次运行相同的 `agentx <client>` 命令。

**端口被占用**

Expand Down
9 changes: 7 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ Claude Code (Anthropic Messages API)
└─ 流式: streaming/anthropic-passthrough.ts, streaming/anthropic-to-responses.ts
```

`src/convert/` 是转换函数所在的模块化目录:`shared.ts`(跨方向 helper:图片/effort/thinking/tool-choice 映射、采样参数、JSON 解析)、`chat.ts`(上游 = Chat Completions 的全部方向)、`responses.ts`(上游 = Responses)、`anthropic.ts`(上游 = Anthropic,自定义 Provider 专属)、`index.ts`(barrel 导出,公共函数名不变)。`src/catalog.ts` 只保留 `providers`/`providerFor`/`honorRequestedModel` 路由函数。
`src/convert/` 是转换函数所在的模块化目录:`shared.ts`(跨方向 helper:图片/effort/thinking/tool-choice 映射、采样参数)、`chat.ts`(上游 = Chat Completions 的全部方向)、`responses.ts`(上游 = Responses)、`anthropic.ts`(上游 = Anthropic,自定义 Provider 专属)、`index.ts`(barrel 导出,公共函数名不变)。`src/catalog.ts` 只保留 `providers`/`providerFor`/`honorRequestedModel` 路由函数。

所有 wire payload(客户端请求、Provider 响应、SSE 事件)经 `src/json.ts` 的 `JsonRecord` + 字段访问器(`recStr`/`recNum`/`recCount`/`recObj`/`recObjs`/`parse`)读取,代码库里不使用 `any`:字段名写错或漏掉非对象检查由编译器拦下,而不是留成一次 undefined 运行时错误。

`src/streaming/` 同样是拆分后的模块化目录(`common.ts` 收敛公共 SSE 写入/heartbeat/usage capture 逻辑,每条协议转换路径各占一个文件),不是单一的 `streaming.ts`。

Expand Down Expand Up @@ -119,12 +121,15 @@ Claude Code (Anthropic Messages API)
| `src/server.ts` | HTTP Adapter、认证、路由、重试、端口回退 |
| `src/catalog.ts` | 模型路由(`providers`/`providerFor`/`honorRequestedModel`),不含转换函数 |
| `src/convert/` | 协议转换函数,按上游协议拆分:`shared.ts`(跨方向 helper)、`chat.ts`(上游 = Chat Completions)、`responses.ts`(上游 = Responses)、`anthropic.ts`(上游 = Anthropic,自定义 Provider 专属)、`index.ts`(barrel) |
| `src/json.ts` | wire payload 的类型词汇表:`JsonRecord` 与字段访问器,替代 `any` |
| `src/streaming/` | SSE 流式协议转换(按上游协议拆分成多个文件) |
| `src/fsutil.ts` | `atomicWriteFile`:临时文件 + rename,避免崩溃留下截断的状态文件 |
| `src/usage/` | `TokenUsage` 类型、存储后端(sqlite/json/memory)、统计渲染 |
| `src/providers/registry.ts` + `types.ts` | Provider 目录、模型注册表、自定义 Provider 注册/移除 |
| `src/process.ts` | 子进程启动/环境注入/stdio 转发/清理/`--native` 环境清洗 |
| `src/credentials.ts` | 凭据查找(CLI > `AGENTX_` 前缀环境变量 > 旧环境变量 > 交互输入) |
| `src/runtime.ts` | 非敏感运行时状态持久化(默认 runtime、last model、自定义 Provider 元数据) |
| `src/quota.ts` | Provider 远程额度查询(`agentx quota`) |
| `src/doctor.ts` / `src/ui.ts` | 诊断 / 交互式启动器(含自定义 Provider 的 Add/Remove 流程) |

> 注:相比 agentx.md 中建议的 `src/commands/`、`src/proxy/`、`src/runtime/` 目录结构,实际实现是扁平化的 `src/*.ts`(server.ts 取代了 proxy/anthropic/responses/chat-completions/auth 分层,转换逻辑集中在 `src/convert/`,按上游协议而非方向拆分)。
> 注:相比 `docs/plans/initial-product-spec.md` 中建议的 `src/commands/`、`src/proxy/`、`src/runtime/` 目录结构,实际实现是扁平化的 `src/*.ts`(server.ts 取代了 proxy/anthropic/responses/chat-completions/auth 分层,转换逻辑集中在 `src/convert/`,按上游协议而非方向拆分)。
8 changes: 7 additions & 1 deletion agentx.md → docs/plans/initial-product-spec.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# 项目:agentx
# Plan: AgentX 初始产品规格

Status: Shipped — 这是项目从零开始时的规格说明,保留作为设计初衷的历史记录;
与现状的偏差(命令集、目录结构、provider 范围)以 `docs/architecture.md` 为准。
Date: 2026-08-18

## 原始规格

请实现一个生产可用的 npm CLI 工具 **`agentx`**。

Expand Down
25 changes: 3 additions & 22 deletions docs/remaining-simplification-todos.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,16 @@
# AgentX 剩余精简待办

> 更新时间:2026-08-30
> 背景:已完成 runtime/profile 状态合并、Codex catalog 收窄、移除 `--model auto` 隐式路由、模型目录按需刷新、凭据 shell profile 自动写入移除、`doctor` 按 client/`--offline` 范围控制、`streaming.ts` 拆分为模块化目录、cost estimation 删除、`agentx quota` 从 `usage` 拆分、未接入的 Google usage/pricing 及整个 capabilities 抽象删除。本文档记录后续建议继续执行的精简项——已完成的条目直接从列表移除,不在此保留变更记录(改动历史见 git log)。
> 更新时间:2026-09-20
> 背景:已完成 runtime/profile 状态合并、Codex catalog 收窄、移除 `--model auto` 隐式路由、模型目录按需刷新、凭据 shell profile 自动写入移除、`doctor` 按 client/`--offline` 范围控制、`streaming.ts` 拆分为模块化目录、cost estimation 删除、`agentx quota` 从 `usage` 拆分、未接入的 Google usage/pricing 及整个 capabilities 抽象删除、客户端自动安装流程移除(改为纯提示)。本文档记录后续建议继续执行的精简项——已完成的条目直接从列表移除,不在此保留变更记录(改动历史见 git log)。
>
> 同一批改造里还新增了自定义 Provider(含原生 Anthropic Messages API 协议支持)。这是功能增补,不是本文档要精简的对象;`Anthropic` 的 usage 适配器(`src/providers/usage/anthropic.ts`)因此从死代码变为活代码,不再适用于"未接入协议应删除"的判断。

## 优先级总览

| 级别 | 待办 | 主要收益 |
| --- | --- | --- |
| G | 删除客户端自动安装流程 | 缩小 launcher 职责 |
| K | 同账号多实例请求协调 | 减少并发排队造成的感知延迟 |

## G. 删除客户端自动安装流程

找不到 `claude` / `codex` 时,launcher 会交互式询问是否执行全局 npm 安装。这个功能方便,但会让 AgentX 额外承担外部安装器的职责,也增加错误处理路径。

### 建议方案

退化为清晰的提示:

```bash
✗ Codex not found.
Install:
npm install -g @openai/codex
Then re-run:
agentx codex
```

保留 `doctor` 检测客户端是否可用即可。

---

## K. 同账号多实例请求协调
Expand All @@ -48,4 +29,4 @@ Then re-run:

## 建议下一批执行顺序

G 和 H 都是需要用户拍板的方向性选择(是保留便利功能、收窄支持范围,还是移除),不建议在没有明确意见的情况下单方面执行。K 已经结论为"暂不实施",除非诊断层面的轻量方案被明确需要。
K 是唯一剩余的待办,且已结论为"暂不实施",除非诊断层面的轻量方案被明确需要。
27 changes: 27 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import tseslint from "typescript-eslint";

/**
* The type checker carries most of the safety here (strict mode plus
* noUncheckedIndexedAccess), so ESLint keeps only what tsc can't see: an
* accidental `any` reintroduced at a wire boundary, and dead code.
*/
export default tseslint.config(
{ ignores: ["dist/**", "node_modules/**"] },
...tseslint.configs.recommended,
{
files: ["**/*.ts"],
rules: {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": ["error", { args: "all", argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
eqeqeq: ["error", "always", { null: "ignore" }],
"no-var": "error",
"prefer-const": "error",
},
},
{
// Tests stub providers with loosely-shaped JSON, and an assertion's shape
// is exactly what a test is allowed to leave imprecise.
files: ["test/**/*.ts"],
rules: { "@typescript-eslint/no-explicit-any": "off" },
},
);
Loading
Loading