From e08a87192ba65e9a5f6f0c6812a59171dfeaf5fe Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Sat, 9 May 2026 14:49:47 +0800 Subject: [PATCH 1/8] docs: make English the default README for GitHub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename README.md → README.zh-CN.md (Chinese) Rename README.en.md → README.md (English, now default) Update language switch links in both files. --- README.en.md | 323 ------------------------------------------------ README.md | 312 +++++++++++++++++++++++----------------------- README.zh-CN.md | 323 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 479 insertions(+), 479 deletions(-) delete mode 100644 README.en.md create mode 100644 README.zh-CN.md diff --git a/README.en.md b/README.en.md deleted file mode 100644 index a71a02b..0000000 --- a/README.en.md +++ /dev/null @@ -1,323 +0,0 @@ -# TeamAI — The team harness for AI agents - -> [English](README.en.md) | [简体中文](README.md) - -[![CI](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml) -[![npm version](https://img.shields.io/npm/v/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli) -[![npm downloads](https://img.shields.io/npm/dm/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) - -Make every AI coding agent work by the same harness. Git-native management of skills, rules, and docs across 20+ AI tools — for you or your whole team. - -**Supports:** Claude Code, Codex, Cursor, CodeBuddy IDE, as well as Gemini CLI, Windsurf, Trae, Aider, Amp, OpenClaw, and 20+ other AI coding tools (skills sync). - -> 📖 **Full usage guide:** [docs/usage-guide.md](docs/usage-guide.md) — covers everything from team creation to day-to-day use. -> 📚 **Provider notes:** [docs/providers.md](docs/providers.md) — GitHub / TGit differences and auth setup. - -Questions or suggestions are welcome — please open a PR or an Issue and help build this project together. - -## Install - -```bash -npm install -g teamai-cli -``` - -
-Tencent internal users: install @tencent/teamai-cli via tnpm - -```bash -npm install -g @tencent/teamai-cli --registry=http://r.tnpm.oa.com -``` - -The two packages share identical source code; `@tencent/teamai-cli` is just the internal mirror of the public `teamai-cli`. -
- -## Quick Start - -### Team members - -```bash -# User-scope init (default, resources installed under ~/) -teamai init --repo yourteam/yourproject - -# Project-scope init (resources installed under the project directory) -cd /path/to/my-project -teamai init --repo yourteam/yourproject --scope project - -# Non-interactive mode (for CI/CD or AI-agent automation) -teamai init --repo yourteam/yourproject --scope user --role hai_dev --force -``` - -### Admins - -First create the shared-experience repo on your git host (GitHub by default; TGit also supported) and grant write access to every team member. - -- **GitHub:** create with `gh repo create yourorg/yourproject --private` or via the UI. Then use Settings → Collaborators to add members, and set `master`/`main` as the default branch. -- **TGit (Tencent Gongfeng):** create on [git.woa.com](https://git.woa.com/) and grant master permissions in bulk via user groups. - -The CLI picks a provider automatically from the repo URL: - -- `yourorg/yourrepo` or `https://github.com/yourorg/yourrepo` → GitHub -- `https://git.woa.com/yourteam/yourrepo` → TGit - -## Commands - -| Command | Description | -|---------|-------------| -| `teamai init [--scope ] [--role ] [--force]` | Initialize (auto-installs gf CLI, OAuth login, links repo, registers member, configures reviewers, injects hooks) | -| `teamai push [--all] [--role ]` | Push local new resources to a dedicated branch and open a Merge Request; new skills prompt interactively for a target namespace (override with `--role`) | -| `teamai pull [--silent]` | Pull team resources and inject them into local AI tools (both scopes pulled sequentially) | -| `teamai status` | Show the diff between local and the team repo | -| `teamai list [type] [--source repo\|local\|all] [--agent ]` | List resources (skills\|rules\|docs\|env\|wiki). With `--source local` or `all`, scans skills directories of installed AI agents and tags each skill's origin (`[team]` / `[builtin]` / `[source:]` / `[local-only]`) | -| `teamai skill [list\|show ]` | List all skills by default; `show ` prints the skill's origin, contributors, installed-agent list, and description summary | -| `teamai members` | List registered team members | -| `teamai remove ` | Remove a resource from both the team repo and local, then open an MR (skills\|rules\|wiki) | -| `teamai roles` | Manage team roles (`init`/`list`/`set`/`add`/`remove`/`update`) | -| `teamai source` | Manage cross-team skill subscription sources (`add`/`remove`/`list`/`browse`) | -| `teamai contribute --file [--scope ]` | Push an AI-generated experience document to the team repo | -| `teamai recall ` | Search the team knowledge base, automatically merging user + project scope results | -| `teamai digest` | Generate a team AI usage weekly digest (skill leaderboard, new/updated skills, session summaries) | -| `teamai hooks` | Manage AI-tool hooks (list / inject / remove) | -| `teamai uninstall [--force]` | Uninstall teamai: remove hooks, rules, skills, env, docs, and `~/.teamai/` | -| `teamai doctor` | Diagnose configuration problems | - -Global options: -- `--dry-run` — preview mode, no real changes -- `--verbose, -v` — verbose output - -## How It Works - -``` -Member A Member B - create skill / write rules same - │ │ - ▼ ▼ - teamai push teamai push - │ │ - ▼ ▼ - create branch + MR create branch + MR - │ │ - └──────► team git repo ◄─────────────┘ - │ ▲ - │ │ reviewer approves + merges MR - ▼ - SessionStart hook → teamai pull - auto-synced to every member's local -``` - -- `teamai push` creates a dedicated branch (`teamai/push//`), pushes it, then opens a Merge Request and assigns reviewers automatically. -- `teamai init` lets you configure default reviewers (stored in the `reviewers` field of `teamai.yaml`). -- `teamai init` injects hooks tailored to each tool's format (`SessionStart`, `Stop`, `PostToolUse`, `UserPromptSubmit`, etc.). During sessions the hooks run `teamai pull`, `teamai update`, tracking, dashboard updates, and so on (supports Claude Code, Codex, Claude Code Internal, Codex Internal, Cursor, CodeBuddy IDE, OpenClaw, WorkBuddy). -- Skills sync to `~/.claude/skills/`, `~/.codex/skills/`, `~/.codex-internal/skills/`, `~/.claude-internal/skills/`, `~/.cursor/skills/`, `~/.codebuddy/skills/`. -- Rules sync to each tool's rules directory and are merged into `CLAUDE.md` via marker comments (supported for claude, claude-internal, codebuddy). -- Knowledge syncs to `~/.teamai/docs/`. -- Learnings sync to `~/.teamai/learnings/` and back the recall index (shared team-wide, not partitioned by role). -- Culture syncs the team culture file (`culture.md`): its frontmatter and body are compiled and injected into every AI tool's `CLAUDE.md`. - -## Role-scoped Skills - -When the team resource repo enables role-scoped directories, skills are organized under role namespaces. During `teamai init`, the CLI asks you to pick a `primaryRole` and optional `additionalRoles` and writes them to your local `config.yaml`. - -Remote repo layout convention: - -```text -manifest/roles.yaml # role definitions -skills/// # skills organized by namespace -rules/ # global, not role-scoped -``` - -- `teamai pull` reads `manifest/roles.yaml` and only syncs skills under `primaryRole + additionalRoles` namespaces (unioned with tag-filter results). -- Skills install flat from `skills///` into `/skills//` — the namespace layout is invisible to users. -- If two activated namespaces contain a skill with the same name, `pull` fails outright to prevent silent overrides. -- Skills outside both activated namespaces and tag-filter results are cleaned up automatically. -- `rules/`, `docs/`, `learnings/` keep their original behavior and are not role-scoped (learnings are shared team-wide). - -Example config: - -```yaml -primaryRole: hai -additionalRoles: - - pm -resourceProfileVersion: 1 -``` - -This syncs every skill from `skills/common/`, `skills/hai/`, and `skills/pm/`. - -## Role-scoped Pushing - -In a role-scoped repo, when you push a new skill the CLI auto-detects available namespaces and prompts: - -```bash -# Interactive namespace selection (recommended) -teamai push -# Output: -# Which namespace should new skills be pushed to? -# 1. common -# 2. hai -# 3. pm -# Choose namespace [1-3] (default: 1 = common): - -# Explicit target namespace -teamai push --role pm -``` - -- With a `primaryRole`, the list expands from `manifest/roles.yaml`. -- Without a `primaryRole`, namespaces are discovered by scanning the team repo's directory structure. -- When only one namespace exists, it's selected automatically — no prompt. -- `--role ` temporarily overrides the target namespace. -- Modifying an existing skill keeps its original namespace — no reselection needed. - -On push, the CLI checks `SKILL.md`'s YAML frontmatter (`name`/`description`) and auto-fills anything missing, so you don't have to maintain it by hand. - -## Team Culture - -Create `culture.md` at the root of the team repo. Use YAML frontmatter for company/team info and the body for cultural guidelines: - -```markdown ---- -company: - name: Acme Corp - mission: Build great things - values: - - Innovation - - Integrity -team: - name: Platform - mission: Enable developers - goals: - - Ship v2.0 - - Improve test coverage ---- - -## Coding Guidelines - -- Every PR needs at least one reviewer approval -- Direct pushes to master are forbidden -- Test coverage must stay above 80% -``` - -`teamai pull` compiles `culture.md` into structured content and injects it into every AI tool's `CLAUDE.md` (between `` and ``). AI coding assistants pick up the team culture on every session. - -## Cross-team Skill Subscription - -Use `teamai source` to subscribe to other teams' public skill repos. Their skills sync automatically on `pull`: - -```bash -# Add a subscription source -teamai source add https://github.com/other-team/teamai-public.git --name other-team - -# List subscribed sources -teamai source list - -# Browse skills from a source -teamai source browse other-team - -# Remove a subscription (and clean up its skills) -teamai source remove other-team -``` - -Subscribed skills sync to your local machine on `teamai pull` and coexist with your own team's skills. - -## Scope - -TeamAI supports two scopes that can coexist: - -| Dimension | User Scope (default) | Project Scope | -|-----------|---------------------|---------------| -| **Install location** | under `~/` (e.g. `~/.claude/skills/`) | under the project (e.g. `/.claude/skills/`) | -| **Config file** | `~/.teamai/config.yaml` | `/.teamai/config.yaml` | -| **Use case** | general team norms, cross-project skills | project-specific skills and rules | -| **Init** | `teamai init --repo /` | `cd && teamai init --repo / --scope project` | - -**Dual-scope cooperation:** -- `teamai pull` pulls user and project scopes sequentially; they don't conflict. -- `teamai contribute --scope user/project` lets you pick which repo to push to. -- `teamai recall` merges knowledge bases from both scopes into a single ranking and tags each result with its origin `[user]` / `[project]`. -- The `scope` field in the remote `teamai.yaml` locks the repo's type; member init must match. - -## Automatic Experience Sharing - -When an AI coding session ends, the Stop hook evaluates session value and prompts you to share: - -``` -AI coding session (ongoing...) - │ - ▼ PostToolUse hook continuously tracks tool calls and skill usage - │ - ▼ session ends (Stop hook fires) - │ - ├─ Smart scoring: tool-call count + tool diversity + skill usage + error retries + session duration - │ (extracted from dashboard events.jsonl, one-shot, out of 100) - │ - ├─ Score < 35 → stay silent (too few or too uniform calls, not worth summarizing) - │ - ▼ Score ≥ 35 - │ - AI: "This session was productive — consider running /teamai-share-learnings to share." - │ - ▼ user accepts - │ - /teamai-share-learnings (AI sub-agent) - ├─ AI summarizes the session's lessons - ├─ Generates a Markdown document - └─ teamai contribute --file → pushes directly to the team repo's learnings/ -``` - -- `/teamai-share-learnings` is a built-in CLI skill, deployed locally by `teamai pull/init`. -- Each session is prompted at most once (de-duplicated); you can always ignore it. -- The document lands directly in `learnings/` and is visible to teammates on their next `pull`. - -## Team Knowledge Recall - -`teamai recall` implements the "read" side of the knowledge flywheel — the AI can search across accumulated team experience docs: - -``` -contribute (write) → pull (sync + index) → recall (search) → upvote (vote) → better ranking -``` - -```bash -$ teamai recall "fuse port" -[1/2] MR review caught a FUSE port-conflict bug ★1 [user] -Author: jeffyxu | Score: 18.5 | Tags: troubleshooting, fuse, k8s - -[2/2] FUSE deployment configuration best practices [project] -Author: alice | Score: 12.0 | Tags: fuse, deploy -``` - -- **Dual-scope merged search:** automatically merges user and project scope knowledge bases, each result tagged with its origin. -- Hybrid CJK + English search (Intl.Segmenter + CJK bigrams). -- Searches implicitly upvote matched docs; good docs naturally float up over time. -- Votes are written to each scope's own repo, so attribution stays correct. - -## Update - -```bash -teamai update # auto-detect and upgrade to latest -npm update -g teamai-cli # or trigger an npm upgrade manually -``` - -`teamai update` picks the registry based on the installed package name: - -- `teamai-cli` → public npm (`https://registry.npmjs.org`) -- `@tencent/teamai-cli` → internal tnpm (`http://r.tnpm.oa.com`) - -To override the registry manually, set `TEAMAI_NPM_REGISTRY=`. - -### Auto-update Control - -Auto-update runs on the Stop hook at the end of a session. It can be controlled at two layers: - -| Layer | File | Field | Allowed values | -|-------|------|-------|----------------| -| Team default | `teamai.yaml` | `autoUpdate` | `true` (default) / `false` | -| User override | `~/.teamai/config.yaml` | `updatePolicy` | `auto` / `prompt` / `skip` | - -The user-level `updatePolicy` always wins over the team-level `autoUpdate`. - -## License - -[MIT](LICENSE) - -## Contributing - -PRs are welcome! Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) first. diff --git a/README.md b/README.md index cec6456..8a6d618 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,138 @@ # TeamAI — The team harness for AI agents -> [English](README.en.md) | [简体中文](README.md) +> [English](README.md) | [简体中文](README.zh-CN.md) [![CI](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli) [![npm downloads](https://img.shields.io/npm/dm/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -让每个 AI 编程助手都按同一套标准工作。通过 Git 统一管理 skills、rules、docs,驾驭 20+ 种 AI 工具——一个人也能用,团队用更强。 +Make every AI coding agent work by the same harness. Git-native management of skills, rules, and docs across 20+ AI tools — for you or your whole team. -**支持:** Claude Code、Codex、Cursor、CodeBuddy IDE,以及 Gemini CLI、Windsurf、Trae、Aider、Amp、OpenClaw 等 20+ 种 AI 编程工具(skills 同步)。 +**Supports:** Claude Code, Codex, Cursor, CodeBuddy IDE, as well as Gemini CLI, Windsurf, Trae, Aider, Amp, OpenClaw, and 20+ other AI coding tools (skills sync). -> 📖 **完整使用指南**:[docs/usage-guide.md](docs/usage-guide.md) — 涵盖从团队创建到日常使用的全流程。 -> 📚 **Provider 说明**:[docs/providers.md](docs/providers.md) — GitHub / TGit 差异与认证配置。 +> 📖 **Full usage guide:** [docs/usage-guide.md](docs/usage-guide.md) — covers everything from team creation to day-to-day use. +> 📚 **Provider notes:** [docs/providers.md](docs/providers.md) — GitHub / TGit differences and auth setup. -如有问题或建议,欢迎提交 PR 或 Issue,一起共建这个项目。 +Questions or suggestions are welcome — please open a PR or an Issue and help build this project together. -## 安装 +## Install ```bash npm install -g teamai-cli ```
-腾讯内部用户:通过 tnpm 安装 @tencent/teamai-cli +Tencent internal users: install @tencent/teamai-cli via tnpm ```bash npm install -g @tencent/teamai-cli --registry=http://r.tnpm.oa.com ``` -两个包的代码内容一致,`@tencent/teamai-cli` 只是公网 `teamai-cli` 的内网镜像。 +The two packages share identical source code; `@tencent/teamai-cli` is just the internal mirror of the public `teamai-cli`.
-## 快速开始 +## Quick Start -### 团队成员 +### Team members ```bash -# 用户级初始化(默认,资源安装到 ~/) +# User-scope init (default, resources installed under ~/) teamai init --repo yourteam/yourproject -# 项目级初始化(资源安装到项目目录下) +# Project-scope init (resources installed under the project directory) cd /path/to/my-project teamai init --repo yourteam/yourproject --scope project -# 非交互模式(适合 CI/CD 或 AI agent 自动化) +# Non-interactive mode (for CI/CD or AI-agent automation) teamai init --repo yourteam/yourproject --scope user --role hai_dev --force ``` -### 管理员 +### Admins -先在 git 托管平台上创建好团队共享经验的仓库(默认 GitHub;TGit 也支持),并把所有团队成员加入到该仓库的 write 权限。 +First create the shared-experience repo on your git host (GitHub by default; TGit also supported) and grant write access to every team member. -- **GitHub**:用 `gh repo create yourorg/yourproject --private` 创建,或在 UI 上建。然后用 Settings → Collaborators 把成员加进来,并把 master/main 设置为默认分支。 -- **TGit(腾讯工蜂)**:在 [git.woa.com](https://git.woa.com/) 上创建,通过 user group 批量添加 master 权限。 +- **GitHub:** create with `gh repo create yourorg/yourproject --private` or via the UI. Then use Settings → Collaborators to add members, and set `master`/`main` as the default branch. +- **TGit (Tencent Gongfeng):** create on [git.woa.com](https://git.woa.com/) and grant master permissions in bulk via user groups. -CLI 会根据用户传入的 repo URL 自动选择 provider: +The CLI picks a provider automatically from the repo URL: -- `yourorg/yourrepo` 或 `https://github.com/yourorg/yourrepo` → GitHub +- `yourorg/yourrepo` or `https://github.com/yourorg/yourrepo` → GitHub - `https://git.woa.com/yourteam/yourrepo` → TGit -## 命令 - -| 命令 | 说明 | -|------|------| -| `teamai init [--scope ] [--role ] [--force]` | 初始化(自动安装 gf CLI、OAuth 登录、关联仓库、注册成员、配置 reviewers、注入 hooks) | -| `teamai push [--all] [--role ]` | 推送本地新资源到独立分支并创建 Merge Request;新 skill 交互式选择目标命名空间,可用 `--role` 覆盖 | -| `teamai pull [--silent]` | 拉取团队资源并注入到本地 AI 工具(支持双 scope 依次拉取) | -| `teamai status` | 查看本地 vs 团队仓库差异 | -| `teamai list [type] [--source repo\|local\|all] [--agent ]` | 列出资源(skills\|rules\|docs\|env\|wiki);`--source local` 或 `all` 时会扫描已安装 AI agent 下的 skills 目录,并标注每个 skill 的来源 (`[team]` / `[builtin]` / `[source:]` / `[local-only]`) | -| `teamai skill [list\|show ]` | 默认列出全部 skill;`show ` 输出指定 skill 的来源、贡献者、已安装的 agent 列表与描述摘要 | -| `teamai members` | 列出已注册的团队成员 | -| `teamai remove ` | 从团队仓库和本地删除资源并创建 MR(skills\|rules\|wiki) | -| `teamai roles` | 管理团队角色(`init`/`list`/`set`/`add`/`remove`/`update`) | -| `teamai source` | 管理跨团队 skill 订阅源(`add`/`remove`/`list`/`browse`) | -| `teamai contribute --file [--scope ]` | 将 AI 生成的经验文档推送到团队仓库 | -| `teamai recall ` | 搜索团队知识库,自动合并 user + project 双 scope 结果 | -| `teamai digest` | 生成团队 AI 使用周报(skill 排行、新增/更新 skill、session 摘要) | -| `teamai hooks` | 管理 AI 工具 hooks(list / inject / remove) | -| `teamai uninstall [--force]` | 卸载 teamai:移除 hooks、rules、skills、env、docs、~/.teamai/ | -| `teamai doctor` | 诊断配置问题 | - -全局选项: -- `--dry-run` — 预览模式,不做实际变更 -- `--verbose, -v` — 详细输出 - -## 工作原理 +## Commands + +| Command | Description | +|---------|-------------| +| `teamai init [--scope ] [--role ] [--force]` | Initialize (auto-installs gf CLI, OAuth login, links repo, registers member, configures reviewers, injects hooks) | +| `teamai push [--all] [--role ]` | Push local new resources to a dedicated branch and open a Merge Request; new skills prompt interactively for a target namespace (override with `--role`) | +| `teamai pull [--silent]` | Pull team resources and inject them into local AI tools (both scopes pulled sequentially) | +| `teamai status` | Show the diff between local and the team repo | +| `teamai list [type] [--source repo\|local\|all] [--agent ]` | List resources (skills\|rules\|docs\|env\|wiki). With `--source local` or `all`, scans skills directories of installed AI agents and tags each skill's origin (`[team]` / `[builtin]` / `[source:]` / `[local-only]`) | +| `teamai skill [list\|show ]` | List all skills by default; `show ` prints the skill's origin, contributors, installed-agent list, and description summary | +| `teamai members` | List registered team members | +| `teamai remove ` | Remove a resource from both the team repo and local, then open an MR (skills\|rules\|wiki) | +| `teamai roles` | Manage team roles (`init`/`list`/`set`/`add`/`remove`/`update`) | +| `teamai source` | Manage cross-team skill subscription sources (`add`/`remove`/`list`/`browse`) | +| `teamai contribute --file [--scope ]` | Push an AI-generated experience document to the team repo | +| `teamai recall ` | Search the team knowledge base, automatically merging user + project scope results | +| `teamai digest` | Generate a team AI usage weekly digest (skill leaderboard, new/updated skills, session summaries) | +| `teamai hooks` | Manage AI-tool hooks (list / inject / remove) | +| `teamai uninstall [--force]` | Uninstall teamai: remove hooks, rules, skills, env, docs, and `~/.teamai/` | +| `teamai doctor` | Diagnose configuration problems | + +Global options: +- `--dry-run` — preview mode, no real changes +- `--verbose, -v` — verbose output + +## How It Works ``` -成员 A 成员 B - 创建 skill / 写规则 同上 +Member A Member B + create skill / write rules same │ │ ▼ ▼ teamai push teamai push │ │ ▼ ▼ - 创建分支 + MR 创建分支 + MR + create branch + MR create branch + MR │ │ - └──────► 团队git仓库 ◄──────────────┘ + └──────► team git repo ◄─────────────┘ │ ▲ - │ │ reviewer 审批合并 MR + │ │ reviewer approves + merges MR ▼ SessionStart hook → teamai pull - 自动拉取到所有成员本地 + auto-synced to every member's local ``` -- `teamai push` 会创建独立分支(`teamai/push//`),推送后自动创建 Merge Request 并指派 reviewers -- `teamai init` 初始化时可配置默认 reviewers(记录在 `teamai.yaml` 的 `reviewers` 字段) -- `teamai init` 会自动注入与各工具格式对齐的 hooks(含 `SessionStart`、`Stop`、`PostToolUse`、`UserPromptSubmit` 等),会话中会执行 `teamai pull`、`teamai update`、追踪与仪表盘等(支持 Claude Code、Codex、Claude Code Internal、Codex Internal、Cursor、CodeBuddy IDE、OpenClaw、WorkBuddy) -- Skills 同步到 `~/.claude/skills/`、`~/.codex/skills/`、`~/.codex-internal/skills/`、`~/.claude-internal/skills/`、`~/.cursor/skills/`、`~/.codebuddy/skills/` -- Rules 同步到各工具的 rules 目录,并通过标记注释合并到 `CLAUDE.md`(支持 claude、claude-internal、codebuddy) -- Knowledge 同步到 `~/.teamai/docs/` -- Learnings 同步到 `~/.teamai/learnings/`,并基于该目录构建 recall 索引(全团队共享,不按角色拆分) -- Culture 同步团队文化文件(`culture.md`),编译 frontmatter 和 body 后注入到各 AI 工具的 `CLAUDE.md` +- `teamai push` creates a dedicated branch (`teamai/push//`), pushes it, then opens a Merge Request and assigns reviewers automatically. +- `teamai init` lets you configure default reviewers (stored in the `reviewers` field of `teamai.yaml`). +- `teamai init` injects hooks tailored to each tool's format (`SessionStart`, `Stop`, `PostToolUse`, `UserPromptSubmit`, etc.). During sessions the hooks run `teamai pull`, `teamai update`, tracking, dashboard updates, and so on (supports Claude Code, Codex, Claude Code Internal, Codex Internal, Cursor, CodeBuddy IDE, OpenClaw, WorkBuddy). +- Skills sync to `~/.claude/skills/`, `~/.codex/skills/`, `~/.codex-internal/skills/`, `~/.claude-internal/skills/`, `~/.cursor/skills/`, `~/.codebuddy/skills/`. +- Rules sync to each tool's rules directory and are merged into `CLAUDE.md` via marker comments (supported for claude, claude-internal, codebuddy). +- Knowledge syncs to `~/.teamai/docs/`. +- Learnings sync to `~/.teamai/learnings/` and back the recall index (shared team-wide, not partitioned by role). +- Culture syncs the team culture file (`culture.md`): its frontmatter and body are compiled and injected into every AI tool's `CLAUDE.md`. -## 角色化 Skills +## Role-scoped Skills -当团队资源仓库启用角色化目录后,Skills 按角色 namespace 组织,CLI 在 `teamai init` 时要求选择 `primaryRole` 和可选的 `additionalRoles`,并写入本地 `config.yaml`。 +When the team resource repo enables role-scoped directories, skills are organized under role namespaces. During `teamai init`, the CLI asks you to pick a `primaryRole` and optional `additionalRoles` and writes them to your local `config.yaml`. -远端仓库目录约定: +Remote repo layout convention: ```text -manifest/roles.yaml # 角色定义 -skills/// # 按 namespace 组织的 skills -rules/ # 全局,不做角色拆分 +manifest/roles.yaml # role definitions +skills/// # skills organized by namespace +rules/ # global, not role-scoped ``` -- `teamai pull` 读取 `manifest/roles.yaml`,只同步 `primaryRole + additionalRoles` 对应 namespace 中的 skills(同时保留 tag 过滤的并集)。 -- Skills 从 `skills///` 拍平安装到本地 `/skills//`,用户无感知 namespace 结构。 -- 如果激活 namespace 中出现同名 skill,`pull` 会直接失败,避免隐式覆盖。 -- 不在激活 namespace 中、也不在 tag 过滤结果中的 skills 会被自动清理。 -- `rules/`、`docs/`、`learnings/` 仍然保持原有逻辑,不做角色拆分(learnings 全团队共享)。 +- `teamai pull` reads `manifest/roles.yaml` and only syncs skills under `primaryRole + additionalRoles` namespaces (unioned with tag-filter results). +- Skills install flat from `skills///` into `/skills//` — the namespace layout is invisible to users. +- If two activated namespaces contain a skill with the same name, `pull` fails outright to prevent silent overrides. +- Skills outside both activated namespaces and tag-filter results are cleaned up automatically. +- `rules/`, `docs/`, `learnings/` keep their original behavior and are not role-scoped (learnings are shared team-wide). -配置示例: +Example config: ```yaml primaryRole: hai @@ -141,37 +141,37 @@ additionalRoles: resourceProfileVersion: 1 ``` -这会同步 `skills/common/`、`skills/hai/`、`skills/pm/` 三个 namespace 中的所有 skills。 +This syncs every skill from `skills/common/`, `skills/hai/`, and `skills/pm/`. -## 角色化推送 +## Role-scoped Pushing -角色化仓库下,推送新 skill 时 CLI 会自动检测可用的命名空间并提供交互式选择: +In a role-scoped repo, when you push a new skill the CLI auto-detects available namespaces and prompts: ```bash -# 交互式选择命名空间(推荐) +# Interactive namespace selection (recommended) teamai push -# 输出: +# Output: # Which namespace should new skills be pushed to? # 1. common # 2. hai # 3. pm # Choose namespace [1-3] (default: 1 = common): -# 显式指定目标 namespace +# Explicit target namespace teamai push --role pm ``` -- 有 `primaryRole` 时,从 `manifest/roles.yaml` 展开可用 namespace 列表 -- 无 `primaryRole` 时,自动扫描团队仓库目录结构中的 namespace -- 单一命名空间时自动选中,无需交互 -- `--role ` 可临时覆盖目标 namespace -- 修改已有 skill 时自动保持原 namespace,无需重新选择 +- With a `primaryRole`, the list expands from `manifest/roles.yaml`. +- Without a `primaryRole`, namespaces are discovered by scanning the team repo's directory structure. +- When only one namespace exists, it's selected automatically — no prompt. +- `--role ` temporarily overrides the target namespace. +- Modifying an existing skill keeps its original namespace — no reselection needed. -推送时 CLI 会自动检查 `SKILL.md` 的 YAML frontmatter(`name`/`description`),缺失则自动补全,无需手动维护。 +On push, the CLI checks `SKILL.md`'s YAML frontmatter (`name`/`description`) and auto-fills anything missing, so you don't have to maintain it by hand. -## 团队文化(Culture) +## Team Culture -在团队仓库根目录创建 `culture.md`,用 YAML frontmatter 定义公司和团队信息,body 部分写团队文化指引: +Create `culture.md` at the root of the team repo. Use YAML frontmatter for company/team info and the body for cultural guidelines: ```markdown --- @@ -189,135 +189,135 @@ team: - Improve test coverage --- -## 编码准则 +## Coding Guidelines -- 所有 PR 必须有至少一个 reviewer 审批 -- 禁止直接 push master -- 测试覆盖率不低于 80% +- Every PR needs at least one reviewer approval +- Direct pushes to master are forbidden +- Test coverage must stay above 80% ``` -`teamai pull` 时会自动将 culture.md 编译为结构化内容,注入到各 AI 工具的 `CLAUDE.md` 中(`` / `` 标记之间)。AI 编码助手在每次会话中都能感知团队文化。 +`teamai pull` compiles `culture.md` into structured content and injects it into every AI tool's `CLAUDE.md` (between `` and ``). AI coding assistants pick up the team culture on every session. -## 跨团队 Skill 订阅 +## Cross-team Skill Subscription -通过 `teamai source` 订阅其他团队的公共 skill 仓库,pull 时自动同步订阅源的 skills: +Use `teamai source` to subscribe to other teams' public skill repos. Their skills sync automatically on `pull`: ```bash -# 添加订阅源 -teamai source add https://git.woa.com/other-team/teamai-public.git --name other-team +# Add a subscription source +teamai source add https://github.com/other-team/teamai-public.git --name other-team -# 查看已订阅的源 +# List subscribed sources teamai source list -# 浏览订阅源的 skills +# Browse skills from a source teamai source browse other-team -# 移除订阅(同时清理其 skills) +# Remove a subscription (and clean up its skills) teamai source remove other-team ``` -订阅源的 skills 在 `teamai pull` 时自动同步到本地,与团队自有 skills 共存。 +Subscribed skills sync to your local machine on `teamai pull` and coexist with your own team's skills. -## Scope(作用域) +## Scope -TeamAI 支持两种 scope,可以共存: +TeamAI supports two scopes that can coexist: -| 维度 | User Scope(默认) | Project Scope | -|------|-------------------|---------------| -| **资源安装位置** | `~/` 下(如 `~/.claude/skills/`) | 项目目录下(如 `/.claude/skills/`) | -| **配置文件** | `~/.teamai/config.yaml` | `/.teamai/config.yaml` | -| **适用场景** | 通用团队规范、跨项目技能 | 项目特定的技能和规则 | -| **初始化** | `teamai init --repo /` | `cd && teamai init --repo / --scope project` | +| Dimension | User Scope (default) | Project Scope | +|-----------|---------------------|---------------| +| **Install location** | under `~/` (e.g. `~/.claude/skills/`) | under the project (e.g. `/.claude/skills/`) | +| **Config file** | `~/.teamai/config.yaml` | `/.teamai/config.yaml` | +| **Use case** | general team norms, cross-project skills | project-specific skills and rules | +| **Init** | `teamai init --repo /` | `cd && teamai init --repo / --scope project` | -**双 scope 协同:** -- `teamai pull` 会依次拉取 user + project 两个 scope 的资源,互不冲突 -- `teamai contribute --scope user/project` 可显式选择推送到哪个仓库 -- `teamai recall` 自动合并两个 scope 的知识库,统一搜索排序,结果标注来源 `[user]`/`[project]` -- 远端 `teamai.yaml` 的 `scope` 字段锁定仓库类型,成员 init 时必须匹配 +**Dual-scope cooperation:** +- `teamai pull` pulls user and project scopes sequentially; they don't conflict. +- `teamai contribute --scope user/project` lets you pick which repo to push to. +- `teamai recall` merges knowledge bases from both scopes into a single ranking and tags each result with its origin `[user]` / `[project]`. +- The `scope` field in the remote `teamai.yaml` locks the repo's type; member init must match. -## 经验自动分享 +## Automatic Experience Sharing -当一次 AI coding session 结束时,系统会通过 Stop hook 智能评估 session 价值并提示分享: +When an AI coding session ends, the Stop hook evaluates session value and prompts you to share: ``` -AI coding session (持续工作中...) +AI coding session (ongoing...) │ - ▼ PostToolUse hook 持续追踪工具调用和 skill 使用 + ▼ PostToolUse hook continuously tracks tool calls and skill usage │ - ▼ 会话结束(Stop hook 触发) + ▼ session ends (Stop hook fires) │ - ├─ 智能评分:工具调用数量 + 工具多样性 + skill 使用 + 错误重试 + session 时长 - │ (从 dashboard events.jsonl 提取,一次性评估,满分 100) + ├─ Smart scoring: tool-call count + tool diversity + skill usage + error retries + session duration + │ (extracted from dashboard events.jsonl, one-shot, out of 100) │ - ├─ 分数 < 35 → 不打扰(工具调用少或缺乏多样性,没有总结价值) + ├─ Score < 35 → stay silent (too few or too uniform calls, not worth summarizing) │ - ▼ 分数 ≥ 35 + ▼ Score ≥ 35 │ - AI 提示:"本次 session 内容丰富,建议运行 /teamai-share-learnings 分享经验" + AI: "This session was productive — consider running /teamai-share-learnings to share." │ - ▼ 用户同意 + ▼ user accepts │ /teamai-share-learnings (AI sub-agent) - ├─ AI 总结本次 session 的经验 - ├─ 生成 Markdown 文档 - └─ teamai contribute --file → 直接 push 到团队仓库 learnings/ + ├─ AI summarizes the session's lessons + ├─ Generates a Markdown document + └─ teamai contribute --file → pushes directly to the team repo's learnings/ ``` -- `/teamai-share-learnings` 是 CLI 内置 skill,随 `teamai pull/init` 自动部署到本地 -- 每个 session 最多提示一次(去重),用户可以忽略 -- 文档直接 push 到 `learnings/` 目录,团队成员下次 pull 时可见 +- `/teamai-share-learnings` is a built-in CLI skill, deployed locally by `teamai pull/init`. +- Each session is prompted at most once (de-duplicated); you can always ignore it. +- The document lands directly in `learnings/` and is visible to teammates on their next `pull`. -## 团队知识回忆 +## Team Knowledge Recall -`teamai recall` 实现知识飞轮的"读出路径"——AI 可以自动搜索团队积累的经验文档: +`teamai recall` implements the "read" side of the knowledge flywheel — the AI can search across accumulated team experience docs: ``` -contribute(写入) → pull(同步+索引) → recall(搜索) → upvote(投票) → 排序优化 +contribute (write) → pull (sync + index) → recall (search) → upvote (vote) → better ranking ``` ```bash -$ teamai recall "fuse 端口" -[1/2] MR 审查发现 FUSE 端口冲突 Bug ★1 [user] +$ teamai recall "fuse port" +[1/2] MR review caught a FUSE port-conflict bug ★1 [user] Author: jeffyxu | Score: 18.5 | Tags: troubleshooting, fuse, k8s -[2/2] FUSE 部署配置最佳实践 [project] +[2/2] FUSE deployment configuration best practices [project] Author: alice | Score: 12.0 | Tags: fuse, deploy ``` -- **双 scope 合并搜索**:自动合并 user 和 project scope 的知识库,结果标注来源 -- Hybrid 中英文搜索(Intl.Segmenter + CJK bigrams) -- 搜索自动投票,好文档自然浮到顶部 -- 投票按 scope 分别写入各自的 repo,归属正确 +- **Dual-scope merged search:** automatically merges user and project scope knowledge bases, each result tagged with its origin. +- Hybrid CJK + English search (Intl.Segmenter + CJK bigrams). +- Searches implicitly upvote matched docs; good docs naturally float up over time. +- Votes are written to each scope's own repo, so attribution stays correct. -## 更新 +## Update ```bash -teamai update # 自动检测并升级到最新版 -npm update -g teamai-cli # 或手动触发 npm 升级 +teamai update # auto-detect and upgrade to latest +npm update -g teamai-cli # or trigger an npm upgrade manually ``` -`teamai update` 会根据当前安装的包名自动选择 registry: +`teamai update` picks the registry based on the installed package name: -- `teamai-cli` → 公网 npm (`https://registry.npmjs.org`) -- `@tencent/teamai-cli` → 内网 tnpm (`http://r.tnpm.oa.com`) +- `teamai-cli` → public npm (`https://registry.npmjs.org`) +- `@tencent/teamai-cli` → internal tnpm (`http://r.tnpm.oa.com`) -如需手动覆盖 registry,可以设置环境变量 `TEAMAI_NPM_REGISTRY=`。 +To override the registry manually, set `TEAMAI_NPM_REGISTRY=`. -### 自动更新控制 +### Auto-update Control -自动更新通过 Stop hook 在会话结束时执行,可在两个层级控制: +Auto-update runs on the Stop hook at the end of a session. It can be controlled at two layers: -| 配置层级 | 文件 | 字段 | 可选值 | -|---------|------|------|-------| -| 团队默认 | `teamai.yaml` | `autoUpdate` | `true`(默认)/ `false` | -| 用户覆盖 | `~/.teamai/config.yaml` | `updatePolicy` | `auto` / `prompt` / `skip` | +| Layer | File | Field | Allowed values | +|-------|------|-------|----------------| +| Team default | `teamai.yaml` | `autoUpdate` | `true` (default) / `false` | +| User override | `~/.teamai/config.yaml` | `updatePolicy` | `auto` / `prompt` / `skip` | -用户级 `updatePolicy` 始终优先于团队级 `autoUpdate`。 +The user-level `updatePolicy` always wins over the team-level `autoUpdate`. -## 许可证 +## License [MIT](LICENSE) -## 贡献 +## Contributing -欢迎 PR!请先阅读 [CONTRIBUTING.md](.github/CONTRIBUTING.md)。 +PRs are welcome! Please read [CONTRIBUTING.md](.github/CONTRIBUTING.md) first. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..b192a26 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,323 @@ +# TeamAI — The team harness for AI agents + +> [English](README.md) | [简体中文](README.zh-CN.md) + +[![CI](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml) +[![npm version](https://img.shields.io/npm/v/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli) +[![npm downloads](https://img.shields.io/npm/dm/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + +让每个 AI 编程助手都按同一套标准工作。通过 Git 统一管理 skills、rules、docs,驾驭 20+ 种 AI 工具——一个人也能用,团队用更强。 + +**支持:** Claude Code、Codex、Cursor、CodeBuddy IDE,以及 Gemini CLI、Windsurf、Trae、Aider、Amp、OpenClaw 等 20+ 种 AI 编程工具(skills 同步)。 + +> 📖 **完整使用指南**:[docs/usage-guide.md](docs/usage-guide.md) — 涵盖从团队创建到日常使用的全流程。 +> 📚 **Provider 说明**:[docs/providers.md](docs/providers.md) — GitHub / TGit 差异与认证配置。 + +如有问题或建议,欢迎提交 PR 或 Issue,一起共建这个项目。 + +## 安装 + +```bash +npm install -g teamai-cli +``` + +
+腾讯内部用户:通过 tnpm 安装 @tencent/teamai-cli + +```bash +npm install -g @tencent/teamai-cli --registry=http://r.tnpm.oa.com +``` + +两个包的代码内容一致,`@tencent/teamai-cli` 只是公网 `teamai-cli` 的内网镜像。 +
+ +## 快速开始 + +### 团队成员 + +```bash +# 用户级初始化(默认,资源安装到 ~/) +teamai init --repo yourteam/yourproject + +# 项目级初始化(资源安装到项目目录下) +cd /path/to/my-project +teamai init --repo yourteam/yourproject --scope project + +# 非交互模式(适合 CI/CD 或 AI agent 自动化) +teamai init --repo yourteam/yourproject --scope user --role hai_dev --force +``` + +### 管理员 + +先在 git 托管平台上创建好团队共享经验的仓库(默认 GitHub;TGit 也支持),并把所有团队成员加入到该仓库的 write 权限。 + +- **GitHub**:用 `gh repo create yourorg/yourproject --private` 创建,或在 UI 上建。然后用 Settings → Collaborators 把成员加进来,并把 master/main 设置为默认分支。 +- **TGit(腾讯工蜂)**:在 [git.woa.com](https://git.woa.com/) 上创建,通过 user group 批量添加 master 权限。 + +CLI 会根据用户传入的 repo URL 自动选择 provider: + +- `yourorg/yourrepo` 或 `https://github.com/yourorg/yourrepo` → GitHub +- `https://git.woa.com/yourteam/yourrepo` → TGit + +## 命令 + +| 命令 | 说明 | +|------|------| +| `teamai init [--scope ] [--role ] [--force]` | 初始化(自动安装 gf CLI、OAuth 登录、关联仓库、注册成员、配置 reviewers、注入 hooks) | +| `teamai push [--all] [--role ]` | 推送本地新资源到独立分支并创建 Merge Request;新 skill 交互式选择目标命名空间,可用 `--role` 覆盖 | +| `teamai pull [--silent]` | 拉取团队资源并注入到本地 AI 工具(支持双 scope 依次拉取) | +| `teamai status` | 查看本地 vs 团队仓库差异 | +| `teamai list [type] [--source repo\|local\|all] [--agent ]` | 列出资源(skills\|rules\|docs\|env\|wiki);`--source local` 或 `all` 时会扫描已安装 AI agent 下的 skills 目录,并标注每个 skill 的来源 (`[team]` / `[builtin]` / `[source:]` / `[local-only]`) | +| `teamai skill [list\|show ]` | 默认列出全部 skill;`show ` 输出指定 skill 的来源、贡献者、已安装的 agent 列表与描述摘要 | +| `teamai members` | 列出已注册的团队成员 | +| `teamai remove ` | 从团队仓库和本地删除资源并创建 MR(skills\|rules\|wiki) | +| `teamai roles` | 管理团队角色(`init`/`list`/`set`/`add`/`remove`/`update`) | +| `teamai source` | 管理跨团队 skill 订阅源(`add`/`remove`/`list`/`browse`) | +| `teamai contribute --file [--scope ]` | 将 AI 生成的经验文档推送到团队仓库 | +| `teamai recall ` | 搜索团队知识库,自动合并 user + project 双 scope 结果 | +| `teamai digest` | 生成团队 AI 使用周报(skill 排行、新增/更新 skill、session 摘要) | +| `teamai hooks` | 管理 AI 工具 hooks(list / inject / remove) | +| `teamai uninstall [--force]` | 卸载 teamai:移除 hooks、rules、skills、env、docs、~/.teamai/ | +| `teamai doctor` | 诊断配置问题 | + +全局选项: +- `--dry-run` — 预览模式,不做实际变更 +- `--verbose, -v` — 详细输出 + +## 工作原理 + +``` +成员 A 成员 B + 创建 skill / 写规则 同上 + │ │ + ▼ ▼ + teamai push teamai push + │ │ + ▼ ▼ + 创建分支 + MR 创建分支 + MR + │ │ + └──────► 团队git仓库 ◄──────────────┘ + │ ▲ + │ │ reviewer 审批合并 MR + ▼ + SessionStart hook → teamai pull + 自动拉取到所有成员本地 +``` + +- `teamai push` 会创建独立分支(`teamai/push//`),推送后自动创建 Merge Request 并指派 reviewers +- `teamai init` 初始化时可配置默认 reviewers(记录在 `teamai.yaml` 的 `reviewers` 字段) +- `teamai init` 会自动注入与各工具格式对齐的 hooks(含 `SessionStart`、`Stop`、`PostToolUse`、`UserPromptSubmit` 等),会话中会执行 `teamai pull`、`teamai update`、追踪与仪表盘等(支持 Claude Code、Codex、Claude Code Internal、Codex Internal、Cursor、CodeBuddy IDE、OpenClaw、WorkBuddy) +- Skills 同步到 `~/.claude/skills/`、`~/.codex/skills/`、`~/.codex-internal/skills/`、`~/.claude-internal/skills/`、`~/.cursor/skills/`、`~/.codebuddy/skills/` +- Rules 同步到各工具的 rules 目录,并通过标记注释合并到 `CLAUDE.md`(支持 claude、claude-internal、codebuddy) +- Knowledge 同步到 `~/.teamai/docs/` +- Learnings 同步到 `~/.teamai/learnings/`,并基于该目录构建 recall 索引(全团队共享,不按角色拆分) +- Culture 同步团队文化文件(`culture.md`),编译 frontmatter 和 body 后注入到各 AI 工具的 `CLAUDE.md` + +## 角色化 Skills + +当团队资源仓库启用角色化目录后,Skills 按角色 namespace 组织,CLI 在 `teamai init` 时要求选择 `primaryRole` 和可选的 `additionalRoles`,并写入本地 `config.yaml`。 + +远端仓库目录约定: + +```text +manifest/roles.yaml # 角色定义 +skills/// # 按 namespace 组织的 skills +rules/ # 全局,不做角色拆分 +``` + +- `teamai pull` 读取 `manifest/roles.yaml`,只同步 `primaryRole + additionalRoles` 对应 namespace 中的 skills(同时保留 tag 过滤的并集)。 +- Skills 从 `skills///` 拍平安装到本地 `/skills//`,用户无感知 namespace 结构。 +- 如果激活 namespace 中出现同名 skill,`pull` 会直接失败,避免隐式覆盖。 +- 不在激活 namespace 中、也不在 tag 过滤结果中的 skills 会被自动清理。 +- `rules/`、`docs/`、`learnings/` 仍然保持原有逻辑,不做角色拆分(learnings 全团队共享)。 + +配置示例: + +```yaml +primaryRole: hai +additionalRoles: + - pm +resourceProfileVersion: 1 +``` + +这会同步 `skills/common/`、`skills/hai/`、`skills/pm/` 三个 namespace 中的所有 skills。 + +## 角色化推送 + +角色化仓库下,推送新 skill 时 CLI 会自动检测可用的命名空间并提供交互式选择: + +```bash +# 交互式选择命名空间(推荐) +teamai push +# 输出: +# Which namespace should new skills be pushed to? +# 1. common +# 2. hai +# 3. pm +# Choose namespace [1-3] (default: 1 = common): + +# 显式指定目标 namespace +teamai push --role pm +``` + +- 有 `primaryRole` 时,从 `manifest/roles.yaml` 展开可用 namespace 列表 +- 无 `primaryRole` 时,自动扫描团队仓库目录结构中的 namespace +- 单一命名空间时自动选中,无需交互 +- `--role ` 可临时覆盖目标 namespace +- 修改已有 skill 时自动保持原 namespace,无需重新选择 + +推送时 CLI 会自动检查 `SKILL.md` 的 YAML frontmatter(`name`/`description`),缺失则自动补全,无需手动维护。 + +## 团队文化(Culture) + +在团队仓库根目录创建 `culture.md`,用 YAML frontmatter 定义公司和团队信息,body 部分写团队文化指引: + +```markdown +--- +company: + name: Acme Corp + mission: Build great things + values: + - Innovation + - Integrity +team: + name: Platform + mission: Enable developers + goals: + - Ship v2.0 + - Improve test coverage +--- + +## 编码准则 + +- 所有 PR 必须有至少一个 reviewer 审批 +- 禁止直接 push master +- 测试覆盖率不低于 80% +``` + +`teamai pull` 时会自动将 culture.md 编译为结构化内容,注入到各 AI 工具的 `CLAUDE.md` 中(`` / `` 标记之间)。AI 编码助手在每次会话中都能感知团队文化。 + +## 跨团队 Skill 订阅 + +通过 `teamai source` 订阅其他团队的公共 skill 仓库,pull 时自动同步订阅源的 skills: + +```bash +# 添加订阅源 +teamai source add https://git.woa.com/other-team/teamai-public.git --name other-team + +# 查看已订阅的源 +teamai source list + +# 浏览订阅源的 skills +teamai source browse other-team + +# 移除订阅(同时清理其 skills) +teamai source remove other-team +``` + +订阅源的 skills 在 `teamai pull` 时自动同步到本地,与团队自有 skills 共存。 + +## Scope(作用域) + +TeamAI 支持两种 scope,可以共存: + +| 维度 | User Scope(默认) | Project Scope | +|------|-------------------|---------------| +| **资源安装位置** | `~/` 下(如 `~/.claude/skills/`) | 项目目录下(如 `/.claude/skills/`) | +| **配置文件** | `~/.teamai/config.yaml` | `/.teamai/config.yaml` | +| **适用场景** | 通用团队规范、跨项目技能 | 项目特定的技能和规则 | +| **初始化** | `teamai init --repo /` | `cd && teamai init --repo / --scope project` | + +**双 scope 协同:** +- `teamai pull` 会依次拉取 user + project 两个 scope 的资源,互不冲突 +- `teamai contribute --scope user/project` 可显式选择推送到哪个仓库 +- `teamai recall` 自动合并两个 scope 的知识库,统一搜索排序,结果标注来源 `[user]`/`[project]` +- 远端 `teamai.yaml` 的 `scope` 字段锁定仓库类型,成员 init 时必须匹配 + +## 经验自动分享 + +当一次 AI coding session 结束时,系统会通过 Stop hook 智能评估 session 价值并提示分享: + +``` +AI coding session (持续工作中...) + │ + ▼ PostToolUse hook 持续追踪工具调用和 skill 使用 + │ + ▼ 会话结束(Stop hook 触发) + │ + ├─ 智能评分:工具调用数量 + 工具多样性 + skill 使用 + 错误重试 + session 时长 + │ (从 dashboard events.jsonl 提取,一次性评估,满分 100) + │ + ├─ 分数 < 35 → 不打扰(工具调用少或缺乏多样性,没有总结价值) + │ + ▼ 分数 ≥ 35 + │ + AI 提示:"本次 session 内容丰富,建议运行 /teamai-share-learnings 分享经验" + │ + ▼ 用户同意 + │ + /teamai-share-learnings (AI sub-agent) + ├─ AI 总结本次 session 的经验 + ├─ 生成 Markdown 文档 + └─ teamai contribute --file → 直接 push 到团队仓库 learnings/ +``` + +- `/teamai-share-learnings` 是 CLI 内置 skill,随 `teamai pull/init` 自动部署到本地 +- 每个 session 最多提示一次(去重),用户可以忽略 +- 文档直接 push 到 `learnings/` 目录,团队成员下次 pull 时可见 + +## 团队知识回忆 + +`teamai recall` 实现知识飞轮的"读出路径"——AI 可以自动搜索团队积累的经验文档: + +``` +contribute(写入) → pull(同步+索引) → recall(搜索) → upvote(投票) → 排序优化 +``` + +```bash +$ teamai recall "fuse 端口" +[1/2] MR 审查发现 FUSE 端口冲突 Bug ★1 [user] +Author: jeffyxu | Score: 18.5 | Tags: troubleshooting, fuse, k8s + +[2/2] FUSE 部署配置最佳实践 [project] +Author: alice | Score: 12.0 | Tags: fuse, deploy +``` + +- **双 scope 合并搜索**:自动合并 user 和 project scope 的知识库,结果标注来源 +- Hybrid 中英文搜索(Intl.Segmenter + CJK bigrams) +- 搜索自动投票,好文档自然浮到顶部 +- 投票按 scope 分别写入各自的 repo,归属正确 + +## 更新 + +```bash +teamai update # 自动检测并升级到最新版 +npm update -g teamai-cli # 或手动触发 npm 升级 +``` + +`teamai update` 会根据当前安装的包名自动选择 registry: + +- `teamai-cli` → 公网 npm (`https://registry.npmjs.org`) +- `@tencent/teamai-cli` → 内网 tnpm (`http://r.tnpm.oa.com`) + +如需手动覆盖 registry,可以设置环境变量 `TEAMAI_NPM_REGISTRY=`。 + +### 自动更新控制 + +自动更新通过 Stop hook 在会话结束时执行,可在两个层级控制: + +| 配置层级 | 文件 | 字段 | 可选值 | +|---------|------|------|-------| +| 团队默认 | `teamai.yaml` | `autoUpdate` | `true`(默认)/ `false` | +| 用户覆盖 | `~/.teamai/config.yaml` | `updatePolicy` | `auto` / `prompt` / `skip` | + +用户级 `updatePolicy` 始终优先于团队级 `autoUpdate`。 + +## 许可证 + +[MIT](LICENSE) + +## 贡献 + +欢迎 PR!请先阅读 [CONTRIBUTING.md](.github/CONTRIBUTING.md)。 From c75b4e98d031ed9f2c780d3faa9baa412a595b19 Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Sat, 9 May 2026 14:53:46 +0800 Subject: [PATCH 2/8] docs: add spacing between blockquote lines in README Separate the two blockquote lines (usage guide / provider notes) with a blank line so they render as distinct blocks with proper spacing. --- README.md | 1 + README.zh-CN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 8a6d618..f3b76ea 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Make every AI coding agent work by the same harness. Git-native management of sk **Supports:** Claude Code, Codex, Cursor, CodeBuddy IDE, as well as Gemini CLI, Windsurf, Trae, Aider, Amp, OpenClaw, and 20+ other AI coding tools (skills sync). > 📖 **Full usage guide:** [docs/usage-guide.md](docs/usage-guide.md) — covers everything from team creation to day-to-day use. + > 📚 **Provider notes:** [docs/providers.md](docs/providers.md) — GitHub / TGit differences and auth setup. Questions or suggestions are welcome — please open a PR or an Issue and help build this project together. diff --git a/README.zh-CN.md b/README.zh-CN.md index b192a26..2e74637 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -12,6 +12,7 @@ **支持:** Claude Code、Codex、Cursor、CodeBuddy IDE,以及 Gemini CLI、Windsurf、Trae、Aider、Amp、OpenClaw 等 20+ 种 AI 编程工具(skills 同步)。 > 📖 **完整使用指南**:[docs/usage-guide.md](docs/usage-guide.md) — 涵盖从团队创建到日常使用的全流程。 + > 📚 **Provider 说明**:[docs/providers.md](docs/providers.md) — GitHub / TGit 差异与认证配置。 如有问题或建议,欢迎提交 PR 或 Issue,一起共建这个项目。 From f1d565db9f0948985f17ea3e68581259ea363395 Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Sat, 9 May 2026 14:57:51 +0800 Subject: [PATCH 3/8] docs: split tagline and description into separate paragraphs --- README.md | 4 +++- README.zh-CN.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f3b76ea..2c29215 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ [![npm downloads](https://img.shields.io/npm/dm/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -Make every AI coding agent work by the same harness. Git-native management of skills, rules, and docs across 20+ AI tools — for you or your whole team. +Make every AI coding agent work by the same harness. + +Git-native management of skills, rules, and docs across 20+ AI tools — for you or your whole team. **Supports:** Claude Code, Codex, Cursor, CodeBuddy IDE, as well as Gemini CLI, Windsurf, Trae, Aider, Amp, OpenClaw, and 20+ other AI coding tools (skills sync). diff --git a/README.zh-CN.md b/README.zh-CN.md index 2e74637..536f933 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -7,7 +7,9 @@ [![npm downloads](https://img.shields.io/npm/dm/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -让每个 AI 编程助手都按同一套标准工作。通过 Git 统一管理 skills、rules、docs,驾驭 20+ 种 AI 工具——一个人也能用,团队用更强。 +让每个 AI 编程助手都按同一套标准工作。 + +通过 Git 统一管理 skills、rules、docs,驾驭 20+ 种 AI 工具——一个人也能用,团队用更强。 **支持:** Claude Code、Codex、Cursor、CodeBuddy IDE,以及 Gemini CLI、Windsurf、Trae、Aider、Amp、OpenClaw 等 20+ 种 AI 编程工具(skills 同步)。 From 834f20de549ddc85fab73c3f48b1638d585521cf Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Sat, 9 May 2026 09:31:42 +0000 Subject: [PATCH 4/8] fix(e2e): stabilize E2E tests for GitHub Actions CI (merge request !185) Squash merge branch 'fix/e2e-ci-stability' into 'master' ## Summary - Disable file-level parallelism in vitest E2E config (`fileParallelism: false`) to prevent intermittent "Cannot find module dist/index.js" race conditions on GitHub Actions - Add `--role common` to init project-scope test (required in non-interactive mode when roles manifest is present) - Add `GIT_AUTHOR_*`/`GIT_COMMITTER_*` env vars to init test (HOME override hides global .gitconfig) - Add diagnostic output to init assertion for future debugging ## Test plan - [x] GitHub Actions E2E passes with these changes - [x] Local `npx vitest run --config vitest.e2e.config.ts` still passes --- src/__tests__/e2e/e2e.test.ts | 10 ++++++++-- vitest.e2e.config.ts | 10 ++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/__tests__/e2e/e2e.test.ts b/src/__tests__/e2e/e2e.test.ts index cb2160b..77fc55f 100644 --- a/src/__tests__/e2e/e2e.test.ts +++ b/src/__tests__/e2e/e2e.test.ts @@ -668,19 +668,25 @@ describe('init project scope (sandboxed)', () => { const stdinAllBlanks = '\n\n\n\n\n'; const result = await runCLIWithEnv( - ['init', '--scope', 'project', '--repo', fullUrl, '--force'], + ['init', '--scope', 'project', '--repo', fullUrl, '--role', 'common', '--force'], { // GitHub: gh CLI / REST honors GITHUB_TOKEN // TGit: gf CLI honors TGIT_TOKEN GITHUB_TOKEN: process.env.TEAMAI_TEST_TOKEN ?? '', TGIT_TOKEN: process.env.TEAMAI_TEST_TOKEN ?? '', HOME: sandbox, // isolate from user-scope ~/.teamai + // Git identity must be set explicitly because HOME override + // hides the global .gitconfig written by CI setup steps. + GIT_AUTHOR_NAME: 'TeamAI CI', + GIT_AUTHOR_EMAIL: 'ci@teamai.test', + GIT_COMMITTER_NAME: 'TeamAI CI', + GIT_COMMITTER_EMAIL: 'ci@teamai.test', }, stdinAllBlanks, sandbox, // cwd = sandbox, so .teamai/ lands here ); - expect(result.code).toBe(0); + expect(result.code, `init failed with output:\n${result.output}`).toBe(0); expect(fs.existsSync(path.join(sandbox, '.teamai', 'config.yaml'))).toBe(true); // Verify the project-scope config has the expected shape diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index 5308c21..2f1f69e 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -8,10 +8,12 @@ export default defineConfig({ ], testTimeout: 60_000, hookTimeout: 30_000, - // E2E tests spawn child processes and touch the real filesystem; a few - // (notably auto-recall-e2e) are inherently slightly flaky on CI runners - // due to timing/state. Retry once: flaky tests recover, real bugs stay - // failed (since they'll fail both runs). + // E2E tests spawn child processes and touch the real filesystem. + // Run test files sequentially to avoid race conditions (parallel + // file-level execution causes intermittent "Cannot find module + // dist/index.js" on GitHub Actions CI runners). + fileParallelism: false, + // Retry once: flaky tests recover, real bugs stay failed. retry: 1, }, }); From 1085f5652fb91ba4f3fbab3b9d4480acc79d13aa Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Wed, 13 May 2026 13:00:25 +0000 Subject: [PATCH 5/8] =?UTF-8?q?refactor:=20wiki=20=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E4=BB=8E=E5=90=84=E5=B7=A5=E5=85=B7=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E7=BB=9F=E4=B8=80=E5=88=B0=20~/.teamai/wiki/=20(merge?= =?UTF-8?q?=20request=20!186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash merge branch 'feature/wiki-shared-location' into 'master' ## Summary - Wiki 页面不再复制到各 AI 工具目录(~/.claude/wiki/、~/.cursor/wiki/ 等),改为统一存储在 `~/.teamai/wiki/`(user scope)或 `/.teamai/wiki/`(project scope) - 移除了多工具目录遍历和 mtime 竞选逻辑,代码精简约 20 行 - pull/push/remove/tombstone 清理全部指向共享目录 ## Why Wiki 是团队共享知识库,不是工具配置,没必要每个 agent 目录都复制一份。放在 `~/.teamai/wiki/` 更合理。 ## Test plan - [x] 939/939 单元测试全部通过 - [x] TypeScript 类型检查通过 - [x] 手动验证 `teamai pull` wiki 同步到 `~/.teamai/wiki/` - [x] 手动验证 `teamai push` 从 `~/.teamai/wiki/` 读取 --- src/__tests__/wiki-bugfix.test.ts | 95 +++++++-------- src/__tests__/wiki-handler.test.ts | 40 +++--- src/pull.ts | 22 +++- src/resources/wiki.ts | 189 ++++++++++++----------------- 4 files changed, 162 insertions(+), 184 deletions(-) diff --git a/src/__tests__/wiki-bugfix.test.ts b/src/__tests__/wiki-bugfix.test.ts index 71998a4..39e5fff 100644 --- a/src/__tests__/wiki-bugfix.test.ts +++ b/src/__tests__/wiki-bugfix.test.ts @@ -12,6 +12,11 @@ * `REMOVABLE_TYPES` didn't include 'wiki'. * BUG #4 — `teamai pull` didn't honor wiki tombstones: a `wiki/.removed` * entry in the team repo never deleted the local copy. + * + * NOTE (refactored for shared wiki location): + * With the wiki refactoring, BUG #4 no longer iterates through per-tool + * wiki directories. Instead, pull.ts has a dedicated wiki tombstone cleanup + * block that removes pages from the shared wiki location (~/.teamai/wiki/). */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; @@ -38,10 +43,9 @@ vi.mock('../utils/logger.js', () => ({ import { filterExistingTopLevelPaths } from '../push.js'; import { WikiHandler } from '../resources/wiki.js'; -import { ResourceHandler } from '../resources/base.js'; -import { resolveBaseDir } from '../types.js'; import { remove as removeFile } from '../utils/fs.js'; -import type { TeamaiConfig, LocalConfig, ResourceType } from '../types.js'; +import { getTeamaiHome } from '../types.js'; +import type { TeamaiConfig, LocalConfig } from '../types.js'; // ───────────────────────────────────────────────────────────────────────── // BUG #1 — filterExistingTopLevelPaths @@ -107,6 +111,8 @@ describe('remove.ts REMOVABLE_TYPES (BUG #3 regression)', () => { try { const repoPath = path.join(tmpDir, 'team-repo'); await fse.ensureDir(path.join(repoPath, 'wiki')); + const teamaiHome = path.join(tmpDir, '.teamai'); + await fse.ensureDir(path.join(teamaiHome, 'wiki')); const teamConfig: TeamaiConfig = { team: 'test', @@ -124,7 +130,6 @@ describe('remove.ts REMOVABLE_TYPES (BUG #3 regression)', () => { claude: { skills: '.claude/skills', rules: '.claude/rules', - wiki: '.claude/wiki', }, }, } as TeamaiConfig; @@ -133,7 +138,8 @@ describe('remove.ts REMOVABLE_TYPES (BUG #3 regression)', () => { username: 'tester', updatePolicy: 'auto', additionalRoles: [], - scope: 'user', + scope: 'project', + projectRoot: tmpDir, } as LocalConfig; const removed = await handler.removeItem('entities/alpha', teamConfig, localConfig); @@ -153,18 +159,18 @@ describe('remove.ts REMOVABLE_TYPES (BUG #3 regression)', () => { // ───────────────────────────────────────────────────────────────────────── describe('pull tombstone cleanup for wiki (BUG #4 regression)', () => { let tmpDir: string; - let homeDir: string; let repoPath: string; + let teamaiHome: string; let teamConfig: TeamaiConfig; let localConfig: LocalConfig; beforeEach(async () => { tmpDir = await fse.mkdtemp(path.join(os.tmpdir(), 'teamai-pullts-')); - homeDir = path.join(tmpDir, 'home'); repoPath = path.join(tmpDir, 'team-repo'); + teamaiHome = path.join(tmpDir, '.teamai'); + await fse.ensureDir(path.join(repoPath, 'wiki')); - await fse.ensureDir(path.join(homeDir, '.claude', 'wiki', 'entities')); - vi.stubEnv('HOME', homeDir); + await fse.ensureDir(path.join(teamaiHome, 'wiki', 'entities')); teamConfig = { team: 'test', @@ -182,7 +188,6 @@ describe('pull tombstone cleanup for wiki (BUG #4 regression)', () => { claude: { skills: '.claude/skills', rules: '.claude/rules', - wiki: '.claude/wiki', }, }, } as TeamaiConfig; @@ -192,20 +197,20 @@ describe('pull tombstone cleanup for wiki (BUG #4 regression)', () => { username: 'tester', updatePolicy: 'auto', additionalRoles: [], - scope: 'user', + scope: 'project', + projectRoot: tmpDir, } as LocalConfig; }); afterEach(async () => { - vi.unstubAllEnvs(); await fse.remove(tmpDir); }); /** - * Reproduces the exact loop pull.ts runs after the fix (tombstoneTypes - * now includes wiki with toolPathField='wiki'). We keep the check at - * this layer instead of end-to-end because pullForScope touches git, - * spinners, and file-system fetches that are costly to mock. + * Reproduces the new wiki tombstone cleanup logic in pull.ts. + * With the refactoring, wiki tombstones are cleaned up from the shared + * ~/.teamai/wiki/ location (or /.teamai/wiki/ for project scope) + * instead of iterating through per-tool directories. */ it('removes local wiki pages that are listed in wiki/.removed', async () => { // Simulate a team member running `teamai remove wiki entities/alpha`: @@ -214,36 +219,21 @@ describe('pull tombstone cleanup for wiki (BUG #4 regression)', () => { path.join(repoPath, 'wiki', '.removed'), 'entities/alpha\n', ); - // Local copy that should be cleaned up. - const localAlpha = path.join(homeDir, '.claude', 'wiki', 'entities', 'alpha.md'); + // Local copy in shared wiki location that should be cleaned up. + const localAlpha = path.join(teamaiHome, 'wiki', 'entities', 'alpha.md'); await fse.writeFile(localAlpha, '# alpha'); - // Mimic pull.ts tombstone loop with the fixed config - const tombstoneTypes: { - type: ResourceType; - ext?: string; - toolPathField: 'rules' | 'skills' | 'wiki'; - }[] = [ - { type: 'rules', ext: '.md', toolPathField: 'rules' }, - { type: 'skills', toolPathField: 'skills' }, - { type: 'wiki', ext: '.md', toolPathField: 'wiki' }, - ]; - + // Mimic pull.ts wiki tombstone cleanup with the new architecture const handler = new WikiHandler(); - const baseDir = resolveBaseDir(localConfig); + const wikiTombstones = await handler.readTombstones(localConfig); - for (const { type, ext, toolPathField } of tombstoneTypes) { - if (type !== 'wiki') continue; - const tombstones = await handler.readTombstones(localConfig); - for (const [_tool, toolPath] of Object.entries(teamConfig.toolPaths)) { - const dir = toolPath[toolPathField]; - if (!dir) continue; - if (!await ResourceHandler.isToolInstalled(dir, baseDir)) continue; - for (const name of tombstones) { - const p = path.join(baseDir, dir, ext ? `${name}${ext}` : name); - if (await fse.pathExists(p)) { - await removeFile(p); - } + if (wikiTombstones.size > 0) { + const sharedWikiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot); + const wikiDir = path.join(sharedWikiHome, 'wiki'); + for (const name of wikiTombstones) { + const wikiPath = path.join(wikiDir, `${name}.md`); + if (await fse.pathExists(wikiPath)) { + await removeFile(wikiPath); } } } @@ -256,21 +246,22 @@ describe('pull tombstone cleanup for wiki (BUG #4 regression)', () => { path.join(repoPath, 'wiki', '.removed'), 'entities/alpha\n', ); - const localAlpha = path.join(homeDir, '.claude', 'wiki', 'entities', 'alpha.md'); - const localBeta = path.join(homeDir, '.claude', 'wiki', 'entities', 'beta.md'); + const localAlpha = path.join(teamaiHome, 'wiki', 'entities', 'alpha.md'); + const localBeta = path.join(teamaiHome, 'wiki', 'entities', 'beta.md'); await fse.writeFile(localAlpha, '# alpha'); await fse.writeFile(localBeta, '# beta'); const handler = new WikiHandler(); - const baseDir = resolveBaseDir(localConfig); - const tombstones = await handler.readTombstones(localConfig); + const wikiTombstones = await handler.readTombstones(localConfig); - for (const [_tool, toolPath] of Object.entries(teamConfig.toolPaths)) { - if (!toolPath.wiki) continue; - if (!await ResourceHandler.isToolInstalled(toolPath.wiki, baseDir)) continue; - for (const name of tombstones) { - const p = path.join(baseDir, toolPath.wiki, `${name}.md`); - if (await fse.pathExists(p)) await removeFile(p); + if (wikiTombstones.size > 0) { + const sharedWikiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot); + const wikiDir = path.join(sharedWikiHome, 'wiki'); + for (const name of wikiTombstones) { + const wikiPath = path.join(wikiDir, `${name}.md`); + if (await fse.pathExists(wikiPath)) { + await removeFile(wikiPath); + } } } diff --git a/src/__tests__/wiki-handler.test.ts b/src/__tests__/wiki-handler.test.ts index 03c1d70..cd5c63e 100644 --- a/src/__tests__/wiki-handler.test.ts +++ b/src/__tests__/wiki-handler.test.ts @@ -19,20 +19,18 @@ import type { TeamaiConfig, LocalConfig } from '../types.js'; describe('WikiHandler', () => { let tmpDir: string; - let homeDir: string; + let teamaiHome: string; let handler: WikiHandler; let teamConfig: TeamaiConfig; let localConfig: LocalConfig; beforeEach(async () => { tmpDir = await fse.mkdtemp(path.join(os.tmpdir(), 'teamai-wiki-test-')); - homeDir = path.join(tmpDir, 'home'); + teamaiHome = path.join(tmpDir, '.teamai'); const repoPath = path.join(tmpDir, 'team-repo'); await fse.ensureDir(path.join(repoPath, 'wiki')); - await fse.ensureDir(path.join(homeDir, '.claude-internal', 'wiki')); - - vi.stubEnv('HOME', homeDir); + await fse.ensureDir(path.join(teamaiHome, 'wiki')); handler = new WikiHandler(); @@ -49,14 +47,14 @@ describe('WikiHandler', () => { env: { injectShellProfile: true }, }, toolPaths: { - 'claude-internal': { - skills: '.claude-internal/skills', - rules: '.claude-internal/rules', - wiki: '.claude-internal/wiki', + claude: { + skills: '.claude/skills', + rules: '.claude/rules', }, }, }; + // Use project scope with projectRoot so getTeamaiHome returns the test tmpDir localConfig = { repo: { localPath: repoPath, @@ -65,19 +63,19 @@ describe('WikiHandler', () => { username: 'testuser', updatePolicy: 'auto', additionalRoles: [], - scope: 'user', + scope: 'project', + projectRoot: tmpDir, }; }); afterEach(async () => { - vi.unstubAllEnvs(); await fse.remove(tmpDir); }); describe('scanLocalForPush', () => { it('detects new wiki page not in team repo', async () => { - // Create local wiki page - const localWiki = path.join(homeDir, '.claude-internal', 'wiki'); + // Create local wiki page in shared location + const localWiki = path.join(teamaiHome, 'wiki'); await fse.ensureDir(path.join(localWiki, 'entities')); await fse.writeFile( path.join(localWiki, 'entities', 'test-module.md'), @@ -102,7 +100,7 @@ describe('WikiHandler', () => { ); // Create same page locally with different content - const localWiki = path.join(homeDir, '.claude-internal', 'wiki'); + const localWiki = path.join(teamaiHome, 'wiki'); await fse.ensureDir(path.join(localWiki, 'entities')); await fse.writeFile( path.join(localWiki, 'entities', 'test-module.md'), @@ -124,7 +122,7 @@ describe('WikiHandler', () => { await fse.ensureDir(path.join(teamWiki, 'entities')); await fse.writeFile(path.join(teamWiki, 'entities', 'test-module.md'), content); - const localWiki = path.join(homeDir, '.claude-internal', 'wiki'); + const localWiki = path.join(teamaiHome, 'wiki'); await fse.ensureDir(path.join(localWiki, 'entities')); await fse.writeFile(path.join(localWiki, 'entities', 'test-module.md'), content); @@ -134,7 +132,7 @@ describe('WikiHandler', () => { }); it('excludes _metadata.json from push', async () => { - const localWiki = path.join(homeDir, '.claude-internal', 'wiki'); + const localWiki = path.join(teamaiHome, 'wiki'); await fse.writeFile( path.join(localWiki, '_metadata.json'), '{"version":1}', @@ -185,7 +183,7 @@ describe('WikiHandler', () => { describe('pushItem', () => { it('copies wiki page to team repo', async () => { - const localWiki = path.join(homeDir, '.claude-internal', 'wiki'); + const localWiki = path.join(teamaiHome, 'wiki'); await fse.ensureDir(path.join(localWiki, 'entities')); const sourcePath = path.join(localWiki, 'entities', 'test.md'); await fse.writeFile(sourcePath, '# Test'); @@ -209,7 +207,7 @@ describe('WikiHandler', () => { }); describe('pullItem', () => { - it('copies wiki page to local tool directory', async () => { + it('copies wiki page to shared wiki location', async () => { const teamWiki = path.join(localConfig.repo.localPath, 'wiki'); await fse.ensureDir(path.join(teamWiki, 'entities')); const sourcePath = path.join(teamWiki, 'entities', 'test.md'); @@ -226,7 +224,7 @@ describe('WikiHandler', () => { localConfig, ); - const dest = path.join(homeDir, '.claude-internal', 'wiki', 'entities', 'test.md'); + const dest = path.join(teamaiHome, 'wiki', 'entities', 'test.md'); expect(await fse.pathExists(dest)).toBe(true); expect(await fse.readFile(dest, 'utf-8')).toBe('# Test from team'); }); @@ -234,7 +232,7 @@ describe('WikiHandler', () => { describe('rebuildMetadata', () => { it('rebuilds metadata from wiki pages', async () => { - const wikiDir = path.join(tmpDir, 'test-wiki'); + const wikiDir = path.join(teamaiHome, 'wiki'); await fse.ensureDir(path.join(wikiDir, 'entities')); await fse.writeFile( path.join(wikiDir, 'entities', 'foo.md'), @@ -245,7 +243,7 @@ describe('WikiHandler', () => { '---\ntitle: Bar\ncategory: entity\ntags: [util]\nupdated: 2026-04-08\n---\n\n# Bar\n\n## Related\n\n## Backlinks\n', ); - await WikiHandler.rebuildMetadata(wikiDir); + await WikiHandler.rebuildMetadata(localConfig); const metadataPath = path.join(wikiDir, '_metadata.json'); expect(await fse.pathExists(metadataPath)).toBe(true); diff --git a/src/pull.ts b/src/pull.ts index ea004b4..259c228 100644 --- a/src/pull.ts +++ b/src/pull.ts @@ -417,11 +417,10 @@ async function pullForScope( const tombstoneTypes: { type: ResourceType; ext?: string; - toolPathField: 'rules' | 'skills' | 'wiki'; + toolPathField: 'rules' | 'skills'; }[] = [ { type: 'rules', ext: '.md', toolPathField: 'rules' }, { type: 'skills', toolPathField: 'skills' }, - { type: 'wiki', ext: '.md', toolPathField: 'wiki' }, ]; const baseDir = resolveBaseDir(localConfig); @@ -445,6 +444,25 @@ async function pullForScope( } } + // Wiki tombstone cleanup: wiki is now in shared location, not per-tool + try { + const wikiHandler = getHandler('wiki'); + const wikiTombstones = await wikiHandler.readTombstones(localConfig); + if (wikiTombstones.size > 0) { + const teamaiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot); + const wikiDir = path.join(teamaiHome, 'wiki'); + for (const name of wikiTombstones) { + const wikiPath = path.join(wikiDir, `${name}.md`); + if (await pathExists(wikiPath)) { + await remove(wikiPath); + log.debug(`[${scopeLabel}] Cleaned up tombstoned wiki ${name} from shared wiki`); + } + } + } + } catch (e) { + log.debug(`[${scopeLabel}] Wiki tombstone cleanup skipped: ${(e as Error).message}`); + } + if (roleContext) { await cleanupInactiveNamespaceSkills( freshConfig, diff --git a/src/resources/wiki.ts b/src/resources/wiki.ts index 43df0d8..3973f85 100644 --- a/src/resources/wiki.ts +++ b/src/resources/wiki.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import { ResourceHandler } from './base.js'; import type { ResourceItem, ResourceItemStatus, TeamaiConfig, LocalConfig } from '../types.js'; -import { resolveBaseDir } from '../types.js'; +import { resolveBaseDir, getTeamaiHome } from '../types.js'; import { listFilesRecursive, pathExists, @@ -9,7 +9,6 @@ import { remove, ensureDir, readFileSafe, - getFileMtime, fileContentEqual, } from '../utils/fs.js'; import { log } from '../utils/logger.js'; @@ -18,13 +17,20 @@ import { log } from '../utils/logger.js'; /** * Wiki resource handler. * - * Unlike skills (directory-based), wiki pages are individual .md files - * stored in category subdirectories (entities/, concepts/, etc.). - * The handler treats the entire wiki/ tree as flat files keyed by - * their relative path within wiki/ (e.g. "entities/message-builder"). + * Wiki pages are now stored in a centralized shared location: + * - User scope → ~/.teamai/wiki/ + * - Project scope → /.teamai/wiki/ + * + * Individual pages are stored as .md files in category subdirectories + * (entities/, concepts/, etc.). The handler treats the entire wiki/ tree + * as flat files keyed by their relative path within wiki/. * * _metadata.json is NOT synced via push/pull — it is rebuilt locally * by the /wiki skill after each pull. + * + * BREAKING CHANGE: Wiki is no longer distributed to individual tool + * directories (e.g., ~/.claude/wiki/, ~/.cursor/wiki/). All tools now + * reference the single shared wiki at ~/.teamai/wiki/. */ export class WikiHandler extends ResourceHandler { readonly type = 'wiki' as const; @@ -55,7 +61,17 @@ export class WikiHandler extends ResourceHandler { } /** - * Scan local AI tool wiki directories for pages that are new or modified + * Get the shared wiki directory for the current scope. + * - User scope → ~/.teamai/wiki/ + * - Project scope → /.teamai/wiki/ + */ + private static getSharedWikiDir(localConfig: LocalConfig): string { + const teamaiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot); + return path.join(teamaiHome, 'wiki'); + } + + /** + * Scan the shared wiki directory for pages that are new or modified * compared to the team repo. */ async scanLocalForPush( @@ -76,76 +92,44 @@ export class WikiHandler extends ResourceHandler { } } - const tombstones = await this.readTombstones(localConfig); + const sharedWikiDir = WikiHandler.getSharedWikiDir(localConfig); + if (!await pathExists(sharedWikiDir)) return []; - // Collect best candidate for each page across all tool directories - const candidates = new Map< - string, - { sourcePath: string; mtime: number; status: ResourceItemStatus } - >(); + const tombstones = await this.readTombstones(localConfig); + const items: ResourceItem[] = []; - for (const [__, toolPath] of Object.entries(teamConfig.toolPaths)) { - if (!toolPath.wiki) continue; - const wikiDir = path.join(resolveBaseDir(localConfig), toolPath.wiki); - if (!await pathExists(wikiDir)) continue; + const files = await listFilesRecursive(sharedWikiDir); + for (const file of files) { + if (!WikiHandler.isWikiPage(file)) continue; - const files = await listFilesRecursive(wikiDir); - for (const file of files) { - if (!WikiHandler.isWikiPage(file)) continue; - - const name = WikiHandler.pathToName(file); - if (tombstones.has(name)) continue; - - const localFilePath = path.join(wikiDir, file); - - if (teamPages.has(name)) { - const teamFilePath = teamPages.get(name)!; - const equal = await fileContentEqual(localFilePath, teamFilePath); - if (equal) continue; - - const mtime = await getFileMtime(localFilePath); - const existing = candidates.get(name); - if (!existing || mtime > existing.mtime) { - candidates.set(name, { - sourcePath: localFilePath, - mtime, - status: 'modified', - }); - } - } else { - const existing = candidates.get(name); - if (!existing) { - const mtime = await getFileMtime(localFilePath); - candidates.set(name, { - sourcePath: localFilePath, - mtime, - status: 'new', - }); - } else if (existing.status === 'new') { - const mtime = await getFileMtime(localFilePath); - if (mtime > existing.mtime) { - candidates.set(name, { - sourcePath: localFilePath, - mtime, - status: 'new', - }); - } - } - } + const name = WikiHandler.pathToName(file); + if (tombstones.has(name)) continue; + + const localFilePath = path.join(sharedWikiDir, file); + + if (teamPages.has(name)) { + const teamFilePath = teamPages.get(name)!; + const equal = await fileContentEqual(localFilePath, teamFilePath); + if (equal) continue; + + items.push({ + name, + type: 'wiki', + sourcePath: localFilePath, + relativePath: `wiki/${name}.md`, + status: 'modified', + }); + } else { + items.push({ + name, + type: 'wiki', + sourcePath: localFilePath, + relativePath: `wiki/${name}.md`, + status: 'new', + }); } } - const items: ResourceItem[] = []; - for (const [name, candidate] of candidates) { - items.push({ - name, - type: 'wiki', - sourcePath: candidate.sourcePath, - relativePath: `wiki/${name}.md`, - status: candidate.status, - }); - } - return items; } @@ -185,50 +169,38 @@ export class WikiHandler extends ResourceHandler { } /** - * Pull a wiki page from team repo to all configured AI tool wiki directories. + * Pull a wiki page from team repo to the shared wiki directory. */ async pullItem( item: ResourceItem, - teamConfig: TeamaiConfig, + _teamConfig: TeamaiConfig, localConfig: LocalConfig, ): Promise { - const baseDir = resolveBaseDir(localConfig); - - for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) { - if (!toolPath.wiki) continue; - - if (!await ResourceHandler.isToolInstalled(toolPath.wiki, baseDir)) { - log.debug(`Skipping wiki sync for ${tool}: tool not installed`); - continue; - } - - const dest = path.join(baseDir, toolPath.wiki, `${item.name}.md`); - await ensureDir(path.dirname(dest)); - try { - await copyFile(item.sourcePath, dest); - log.debug(`Synced wiki page ${item.name} → ${tool}`); - } catch (e) { - log.warn( - `Failed to sync wiki page ${item.name} to ${tool}: ${(e as Error).message}`, - ); - } + const sharedWikiDir = WikiHandler.getSharedWikiDir(localConfig); + const dest = path.join(sharedWikiDir, `${item.name}.md`); + await ensureDir(path.dirname(dest)); + try { + await copyFile(item.sourcePath, dest); + log.debug(`Synced wiki page ${item.name} → shared wiki`); + } catch (e) { + log.warn( + `Failed to sync wiki page ${item.name}: ${(e as Error).message}`, + ); } } /** - * Remove a wiki page from the team repo and all local AI tool wiki directories. + * Remove a wiki page from the team repo and the shared wiki directory. */ async removeItem( name: string, - teamConfig: TeamaiConfig, + _teamConfig: TeamaiConfig, localConfig: LocalConfig, ): Promise { const removed: string[] = []; - const baseDir = resolveBaseDir(localConfig); - const fileName = `${name}.md`; // Remove from team repo - const teamFile = path.join(localConfig.repo.localPath, 'wiki', fileName); + const teamFile = path.join(localConfig.repo.localPath, 'wiki', `${name}.md`); if (await pathExists(teamFile)) { await remove(teamFile); removed.push(teamFile); @@ -236,25 +208,24 @@ export class WikiHandler extends ResourceHandler { await this.addTombstone(name, localConfig); - // Remove from each tool's wiki directory - for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) { - if (!toolPath.wiki) continue; - const filePath = path.join(baseDir, toolPath.wiki, fileName); - if (await pathExists(filePath)) { - await remove(filePath); - removed.push(filePath); - log.debug(`Removed wiki page ${name} from ${tool}`); - } + // Remove from shared wiki directory + const sharedWikiDir = WikiHandler.getSharedWikiDir(localConfig); + const sharedFile = path.join(sharedWikiDir, `${name}.md`); + if (await pathExists(sharedFile)) { + await remove(sharedFile); + removed.push(sharedFile); + log.debug(`Removed wiki page ${name} from shared wiki`); } return removed; } /** - * Rebuild _metadata.json from wiki pages on disk. + * Rebuild _metadata.json from wiki pages in the shared wiki directory. * Called after pull to reconstruct local metadata. */ - static async rebuildMetadata(wikiDir: string): Promise { + static async rebuildMetadata(localConfig: LocalConfig): Promise { + const wikiDir = WikiHandler.getSharedWikiDir(localConfig); if (!await pathExists(wikiDir)) return; const files = await listFilesRecursive(wikiDir); From 9d9bf70cf11673762df2f38fcd6114ffbedf4678 Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Tue, 19 May 2026 02:14:06 +0000 Subject: [PATCH 6/8] fix(push): exclude .git from copyDir to prevent submodule gitlink (merge request !187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash merge branch 'fix/push-exclude-git-tgit' into 'master' ## Summary - When a skill directory is itself a git repository (e.g. user cloned a repo into `.claude/skills/`), `copyDir` would copy the `.git` directory into the team repo - Git then treats the nested `.git` as a gitlink (mode 160000 / submodule reference), causing `git add` to record only a commit hash instead of actual file content - The resulting MR appears to contain only `Subproject commit ` — no real files **Fix:** Add `.git` to the `IGNORED_NAMES` filter in `src/utils/fs.ts` ## Test plan - [x] Added unit test: `should NOT copy .git directory when skill source is a git repo` - [x] All 936 existing tests pass - [x] Type check passes (`tsc --noEmit`) Fixes https://github.com/Tencent/teamai-cli/issues/10 --- src/__tests__/skills.test.ts | 24 ++++++++++++++++++++++++ src/utils/fs.ts | 1 + 2 files changed, 25 insertions(+) diff --git a/src/__tests__/skills.test.ts b/src/__tests__/skills.test.ts index dceb482..4c21df1 100644 --- a/src/__tests__/skills.test.ts +++ b/src/__tests__/skills.test.ts @@ -654,6 +654,30 @@ scope: 'user', const content = await fse.readFile(contribPath, 'utf-8'); expect(content).toBe('testuser\n'); }); + + it('should NOT copy .git directory when skill source is a git repo', async () => { + const localSkillDir = path.join(homeDir, '.claude/skills', 'git-skill'); + await fse.ensureDir(localSkillDir); + await fse.writeFile(path.join(localSkillDir, 'SKILL.md'), '# Git Skill\nContent here'); + await fse.writeFile(path.join(localSkillDir, 'helper.py'), 'print("hello")'); + // Simulate a .git directory (as if skill was cloned from a git repo) + await fse.ensureDir(path.join(localSkillDir, '.git', 'objects')); + await fse.writeFile(path.join(localSkillDir, '.git', 'HEAD'), 'ref: refs/heads/main'); + + const item = { + name: 'git-skill', + type: 'skills' as const, + sourcePath: localSkillDir, + relativePath: 'skills/git-skill', + }; + + await handler.pushItem(item, teamConfig, localConfig); + + const destDir = path.join(localConfig.repo.localPath, 'skills', 'git-skill'); + expect(await fse.pathExists(path.join(destDir, 'SKILL.md'))).toBe(true); + expect(await fse.pathExists(path.join(destDir, 'helper.py'))).toBe(true); + expect(await fse.pathExists(path.join(destDir, '.git'))).toBe(false); + }); }); describe('SkillsHandler.readContributors', () => { diff --git a/src/utils/fs.ts b/src/utils/fs.ts index 6243f08..ff44fac 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -8,6 +8,7 @@ const IGNORED_NAMES = new Set([ '.pyc', '.DS_Store', 'node_modules', + '.git', ]); function isIgnored(name: string): boolean { From 883d0aea0ac40709ff248242122c157c79755e68 Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Tue, 19 May 2026 10:17:04 +0800 Subject: [PATCH 7/8] 0.16.4 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index d2e577f..3749bf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "teamai-cli", - "version": "0.16.3", + "version": "0.16.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 82bb01f..81021f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "teamai-cli", - "version": "0.16.3", + "version": "0.16.4", "description": "TeamAI — the team harness for AI agents (skill sync + shared knowledge base, powered by Git)", "type": "module", "bin": { From b09b01c251c3b2fb64b0aa3dcf517f73fe0ecf1e Mon Sep 17 00:00:00 2001 From: jeffyxu Date: Tue, 19 May 2026 17:09:48 +0800 Subject: [PATCH 8/8] fix: align wiki path to ~/.teamai/wiki/ for teamai push compatibility Fixes #13 --- skills/teamai-wiki/SKILL.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/skills/teamai-wiki/SKILL.md b/skills/teamai-wiki/SKILL.md index a78e3fe..3885b97 100644 --- a/skills/teamai-wiki/SKILL.md +++ b/skills/teamai-wiki/SKILL.md @@ -243,10 +243,7 @@ When invoked, first determine the subcommand (init/ingest/query/lint/status/expo #### Step 1 — Parse arguments -- `WIKI_DIR`: wiki 目录路径。按以下顺序检测: - 1. team repo 中的 `wiki/` 目录(如果当前项目已通过 `teamai init` 配置)→ 首选 - 2. `~/.claude-internal/wiki/` 或 `~/.claude/wiki/`(本地 AI 工具 wiki 目录) - 3. 当前目录的 `./wiki/`(fallback) +- `WIKI_DIR`: 固定为 `~/.teamai/wiki/`(teamai push/pull 同步的标准路径) - `SOURCE_DIR`: 可选的 `dir` 参数 如果 `WIKI_DIR` 已经存在且包含 `_metadata.json`,提示用户已经初始化过,询问是否要重新初始化。 @@ -1009,7 +1006,7 @@ Wiki exported to: 8. **_metadata.json 是真相来源** — 页面列表、文件哈希、链接图都以此为准。 9. **命名一致性** — 文件名 kebab-case,标题 Title Case 或人类可读中文。 10. **幂等性** — 重复 ingest 同一源目录应产生相同结果(不会重复创建页面)。 -11. **wiki 路径推断** — 优先使用 team repo 的 wiki/ 目录(通过 `teamai pull` 同步到本地);其次检查 `~/.claude-internal/wiki/` 或 `~/.claude/wiki/`;最后 fallback 到 `./wiki/`。 +11. **wiki 路径** — 固定使用 `~/.teamai/wiki/`,与 teamai push/pull 对齐。 12. **Obsidian 兼容** — 所有 `[[links]]` 使用 Obsidian 格式,方便用户在 Obsidian 中直接浏览。 13. **语言** — Wiki 页面内容默认使用中文撰写,技术术语保持英文原文。 14. **智能分类** — LLM 根据内容自动判断页面归属哪个分类目录,无需用户指定。