Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ All notable changes to this project will be documented in this file. See [standa

### 🐛 Bug Fixes

- `teamai pull` now deletes a tombstoned agent under all three render extensions, so the Codex `.toml` and Kiro `.json` copies of a removed agent no longer survive on other machines. The cleanup also runs when the team repo rev is unchanged, so an upgrade reaches machines that already pulled the tombstone with an older CLI. `teamai remove agents <name>` also honours `enabledAgents` and no longer deletes from excluded tools. Fixes [#576](https://github.com/Tencent/teamai-cli/issues/576).
- `teamai remove rules <name>` and `teamai remove skills <name>` now honour `enabledAgents` and leave excluded tools untouched, matching the whitelist `teamai pull` already applies when it cleans up a tombstoned resource. Fixes [#590](https://github.com/Tencent/teamai-cli/issues/590).
- `teamai import --cache-status` and `--cache-gc` now expose their existing JSON output through the CLI `--json` option.
- Course-correction matching normalizes prompts and keywords to Unicode NFC, so composed and decomposed accents match. Stored prompt summaries and the 60-second correction window are unchanged. Fixes [#573](https://github.com/Tencent/teamai-cli/issues/573).
- Course-correction detection matches keywords in space-separated scripts as whole words, so Spanish "segundo" no longer counts as `undo` (for [#564](https://github.com/Tencent/teamai-cli/issues/564)).
- `teamai doctor` no longer assumes TGit before initialization and now exits with code 1 when any diagnostic check fails.
- MCP `requires` is resolved from `PATH` (including Windows `PATHEXT`), so `teamai mcp inject` no longer skips servers such as `uvx` on Windows ([#540](https://github.com/Tencent/teamai-cli/pull/540), for [#539](https://github.com/Tencent/teamai-cli/issues/539)).
- The GitHub and CNB providers resolve their CLI to a launchable absolute path and start it through cross-spawn, so on Windows they no longer answer "installed" while every call fails silently ([#520](https://github.com/Tencent/teamai-cli/pull/520)).
- `enabledAgents` now also gates CLI builtin deploy, CLAUDE.md-class injects, and last-pull skip-sync targets, so an already-installed tool outside the whitelist is not written to ([#510](https://github.com/Tencent/teamai-cli/issues/510)).
- `teamai status` counts rule files in subdirectories recursively ([#437](https://github.com/Tencent/teamai-cli/pull/437)).
- Codex Stop-phase contribution hints are deferred to the next prompt, so the host no longer rejects `additionalContext` ([#441](https://github.com/Tencent/teamai-cli/pull/441)).
- Agent version detection launches the agent CLI through cross-spawn, so on Windows an npm-installed agent CLI such as `codebuddy`, `claude` or `openclaw` (a `.cmd` shim) reports its version instead of an empty `agent_version`.

### 📝 Documentation

Expand Down
8 changes: 5 additions & 3 deletions docs/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ The existing SessionStart hook runs `teamai pull`. When the `packages` declarati
```bash
teamai packages # Install every team declaration
teamai packages --dry-run # Preview native commands without installing or writing files
teamai doctor # Check runtimes and declared package/marketplace/plugin status
teamai doctor # Check runtimes and declared package/marketplace/plugin status; exits 1 when any check fails
```

After a successful install, TeamAI writes a local snapshot to `teamai.lock` under the active scope's `.teamai` directory. The lock records installed versions and the declaration hash used by the SessionStart hint; it is not stored in the team repository. In user scope, machine-wide npm tools and Claude plugins are acknowledged once, while project npm dependencies are acknowledged separately for each working directory so installing in one repository cannot silence another repository's hint.
Expand Down Expand Up @@ -1390,7 +1390,7 @@ roles:
agents: [common, frontend] # optional; omitted = root-level agents only
```

`teamai pull` copies these into each Tier-1 tool's `agents/` directory (e.g. `~/.claude/agents/`), flattened by file name, so two active namespaces must not define the same agent name (pull reports the collision and skips the scope). When a member changes role, agents of the namespaces that stopped being active are removed on the next pull, unless the deployed copy was edited locally, in which case it is kept with a warning. Without a configured role, every agent syncs. `teamai push` resolves the source using the same active role and project namespaces as pull. It writes edits to that source and skips ambiguous destinations with a warning; an agent with only inactive sources is also skipped. Skipped agents do not block other resources in the same push. A new agent lands at the root. Cleanup checks each tool separately, respecting YAML `targets` and legacy format support. An active same-named agent protects a deployed file only when it targets that tool and output file. The CLI's built-in `teamai-recall.md` is deployed alongside team agents but is not uploaded by `teamai push`.
`teamai pull` copies these into each Tier-1 tool's `agents/` directory (e.g. `~/.claude/agents/`), flattened by file name, so two active namespaces must not define the same agent name (pull reports the collision and skips the scope). `teamai pull` writes `<name>.toml` for Codex tools, `<name>.json` for Kiro, and `<name>.md` for every other tool. When a member changes role, agents of the namespaces that stopped being active are removed on the next pull, unless the deployed copy was edited locally, in which case it is kept with a warning. Without a configured role, every agent syncs. `teamai push` resolves the source using the same active role and project namespaces as pull. It writes edits to that source and skips ambiguous destinations with a warning; an agent with only inactive sources is also skipped. Skipped agents do not block other resources in the same push. A new agent lands at the root. Cleanup checks each tool separately, respecting YAML `targets` and legacy format support. An active same-named agent protects a deployed file only when it targets that tool and output file. `teamai remove agents <name>` records a tombstone. The next pull on every other machine deletes `<name>.md`, `<name>.toml` and `<name>.json` from each synced tool's agents directory. That cleanup also runs when the pull finds the team repo unchanged. The CLI's built-in `teamai-recall.md` is deployed alongside team agents but is not uploaded by `teamai push`.

### OpenCode

Expand Down Expand Up @@ -1460,6 +1460,8 @@ teamai remove agents <name>
teamai remove mcp <name>
```

`teamai doctor` exits with code 0 only when every check passes, and code 1 when any check fails. Before initialization, it reports the missing configuration without assuming a Git provider.

Auto-update runs in the Stop hook and is controlled by two tiers:

| Tier | File | Field | Value |
Expand Down Expand Up @@ -1664,7 +1666,7 @@ Shared resources (the env block, docs directory, and `~/.teamai/`) are removed *

The exclusion is durable: `uninstall --agent <tool>` drops the tool from `enabledAgents` and records it in `disabledAgents`, so a later `pull` (or another tool's session-start hook) will not resurrect its skills, rules, agents, CLAUDE.md block, or hooks. Running `init --agent <tool>` again clears the exclusion and re-enables sync for that tool.

The same `enabledAgents` whitelist (from `init --agent`) also gates CLI built-in skills/rules/agents and CLAUDE.md-class injects: an already-installed tool outside the list is not written to, even if its root directory already exists. Editing `enabledAgents` without `init` still invalidates the last-pull skip cache for newly added tools.
The same `enabledAgents` whitelist (from `init --agent`) also gates CLI built-in skills/rules/agents and CLAUDE.md-class injects: an already-installed tool outside the list is neither written to nor deleted from, even if its root directory already exists. `teamai remove` respects the same whitelist for agents, rules, and skills. Editing `enabledAgents` without `init` still invalidates the last-pull skip cache for newly added tools.

To rejoin after uninstalling:

Expand Down
8 changes: 5 additions & 3 deletions docs/usage-guide.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ Claude 插件 target 使用 `plugin@marketplace` 格式。`claude-plugins-offici
```bash
teamai packages # 安装团队声明的全部包和插件
teamai packages --dry-run # 预览底层命令,不安装也不写文件
teamai doctor # 检查运行环境及声明的包、marketplace、插件状态
teamai doctor # 检查运行环境及声明的包、marketplace、插件状态;任一检查失败时退出码为 1
```

安装成功后,TeamAI 会在当前 scope 的 `.teamai` 目录下写入本地快照 `teamai.lock`。该文件记录已安装版本,以及供 SessionStart 提示比对的声明哈希,不会写入团队仓库。在 user scope 下,全局 npm 工具和 Claude 插件只需确认一次;项目 npm 依赖会按工作目录分别确认,避免在一个仓库安装后错误关闭另一个仓库的提示。
Expand Down Expand Up @@ -1354,7 +1354,7 @@ roles:
agents: [common, frontend] # 可选;省略 = 只同步根目录 agents
```

`teamai pull` 会将它们按文件名拍平复制到每个 Tier-1 工具的 `agents/` 目录(如 `~/.claude/agents/`),因此两个活跃 namespace 不能定义同名 agent(pull 会报告冲突并跳过该 scope)。成员切换角色后,不再活跃的 namespace 中的 agents 会在下一次 pull 时被移除;若本地副本已被手动修改,则保留并给出警告。未配置角色时同步全部 agents。`teamai push` 使用与 pull 相同的活跃角色和项目 namespace 来确定源文件,并将修改写回该源文件;若存在多个候选目标,则跳过并给出警告。若源文件均不活跃,也会跳过。跳过的 agent 不会阻止同一次 push 中的其他资源。新 agent 落在根目录。清理会逐个工具检查 YAML 的 `targets` 和旧格式支持;只有活跃的同名 agent 会写入该工具的同一输出文件时,才保留该文件。CLI 内置的 `teamai-recall.md` 与团队 agents 并列部署,但不会被 `teamai push` 上传。
`teamai pull` 会将它们按文件名拍平复制到每个 Tier-1 工具的 `agents/` 目录(如 `~/.claude/agents/`),因此两个活跃 namespace 不能定义同名 agent(pull 会报告冲突并跳过该 scope)。`teamai pull` 为 Codex 系工具写入 `<name>.toml`,为 Kiro 写入 `<name>.json`,其余工具写入 `<name>.md`。成员切换角色后,不再活跃的 namespace 中的 agents 会在下一次 pull 时被移除;若本地副本已被手动修改,则保留并给出警告。未配置角色时同步全部 agents。`teamai push` 使用与 pull 相同的活跃角色和项目 namespace 来确定源文件,并将修改写回该源文件;若存在多个候选目标,则跳过并给出警告。若源文件均不活跃,也会跳过。跳过的 agent 不会阻止同一次 push 中的其他资源。新 agent 落在根目录。清理会逐个工具检查 YAML 的 `targets` 和旧格式支持;只有活跃的同名 agent 会写入该工具的同一输出文件时,才保留该文件。`teamai remove agents <name>` 会记录 tombstone。其他机器下一次 pull 时,会从每个同步中的工具的 agents 目录删除 `<name>.md`、`<name>.toml` 和 `<name>.json`。即使该次 pull 发现团队仓库没有变化,也会执行清理。CLI 内置的 `teamai-recall.md` 与团队 agents 并列部署,但不会被 `teamai push` 上传。

### OpenCode

Expand Down Expand Up @@ -1424,6 +1424,8 @@ teamai remove agents <name>
teamai remove mcp <name>
```

仅当所有检查通过时,`teamai doctor` 才以状态码 0 退出;任一检查失败时以状态码 1 退出。尚未初始化时,它只报告缺少配置,不会臆测 Git 托管平台。

自动更新在 Stop hook 中执行,可通过两层控制:

| 层级 | 文件 | 字段 | 值 |
Expand Down Expand Up @@ -1620,7 +1622,7 @@ teamai uninstall --agent claude

该排除是持久的:`uninstall --agent <tool>` 会把该工具从 `enabledAgents` 移除并记入 `disabledAgents`,因此之后的 `pull`(或其他工具的 session-start hook)不会再把它的 skills、rules、agents、CLAUDE.md 块或 hooks 重新装回。重新执行 `init --agent <tool>` 会清除该排除、恢复对该工具的同步。

同一套 `enabledAgents` 白名单(来自 `init --agent`)也约束 CLI 内置 skills/rules/agents 以及 CLAUDE.md 类注入:即使工具根目录已经存在,白名单外的已安装工具也不会被写入。不经过 `init` 直接把工具加进 `enabledAgents` 时,last-pull 跳过缓存会对新加入的工具失效。
同一套 `enabledAgents` 白名单(来自 `init --agent`)也约束 CLI 内置 skills/rules/agents 以及 CLAUDE.md 类注入:即使工具根目录已经存在,白名单外的已安装工具也不会被写入或删除。`teamai remove` 对 agents、rules 和 skills 同样遵守该白名单。不经过 `init` 直接把工具加进 `enabledAgents` 时,last-pull 跳过缓存会对新加入的工具失效。

卸载后如需重新加入:

Expand Down
32 changes: 32 additions & 0 deletions src/__tests__/agent-version.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execFile } from 'node:child_process';
import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { getAgentVersion, clearVersionCache, _readPlistVersion } from '../agent-version.js';

beforeEach(() => {
Expand Down Expand Up @@ -69,6 +72,35 @@ describe('getAgentVersion', () => {
});
});

describe('getAgentVersion with an npm-installed CLI', () => {
let binDir: string;
let savedPath: string | undefined;

beforeEach(async () => {
binDir = await mkdtemp(path.join(os.tmpdir(), 'teamai-agent-version-'));
savedPath = process.env.PATH;
// npm writes only `codebuddy.cmd` / `codebuddy.ps1` on Windows, so the
// bare name has no `.exe` for CreateProcess to find.
if (process.platform === 'win32') {
await writeFile(path.join(binDir, 'codebuddy.cmd'), '@echo 2.5.0\r\n');
} else {
const script = path.join(binDir, 'codebuddy');
await writeFile(script, '#!/bin/sh\necho 2.5.0\n');
await chmod(script, 0o755);
}
process.env.PATH = `${binDir}${path.delimiter}${savedPath ?? ''}`;
});

afterEach(async () => {
process.env.PATH = savedPath;
await rm(binDir, { recursive: true, force: true });
});

it('reads the version through the platform launcher', async () => {
expect(await getAgentVersion('codebuddy')).toBe('2.5.0');
});
});

describe('_readPlistVersion', () => {
it('returns empty string for non-existent path', async () => {
const ver = await _readPlistVersion('/nonexistent/App.app');
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,18 @@ projects:
expect(tombstone.split('\n').map((l) => l.trim())).toContain('old');
});

it('removeItem leaves agents of an excluded tool alone', async () => {
await fse.writeFile(path.join(repoPath, 'agents', 'old.md'), 'old');
await fse.writeFile(path.join(homeDir, '.claude/agents', 'old.md'), 'old');
await fse.writeFile(path.join(homeDir, '.codebuddy/agents', 'old.md'), 'old');

// enabledAgents whitelists claude only, so codebuddy is not ours to touch.
await handler.removeItem('old', teamConfig, { ...localConfig, enabledAgents: ['claude'] });

expect(await fse.pathExists(path.join(homeDir, '.claude/agents', 'old.md'))).toBe(false);
expect(await fse.pathExists(path.join(homeDir, '.codebuddy/agents', 'old.md'))).toBe(true);
});

it('removeItem deletes a namespaced agent from the team repo and tombstones it', async () => {
await fse.ensureDir(path.join(repoPath, 'agents', 'devops'));
await fse.writeFile(path.join(repoPath, 'agents', 'devops', 'tf.yaml'), 'name: tf\n');
Expand Down
Loading