Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ All notable changes to this project will be documented in this file. See [standa
- Multi-project management: `role` and `project` together resolve resource namespaces, and project-private learnings are isolated ([#426](https://github.com/Tencent/teamai-cli/pull/426), for [#375](https://github.com/Tencent/teamai-cli/issues/375)).
- Data partitions auto-migrate a legacy `.teamai`, resume interrupted migrations, smoke-check the clone, and keep a git-ignored backup ([#439](https://github.com/Tencent/teamai-cli/pull/439), for [#374](https://github.com/Tencent/teamai-cli/issues/374)).
- Teams add their own course-correction words via `sharing.intervention.correctionKeywords` in `teamai.yaml`. The built-in list still covers only Chinese, English and Japanese, so corrections typed in other languages count only once the team configures them. The `UserPromptSubmit` hook now stores a `correction` flag on each dashboard prompt event (for [#564](https://github.com/Tencent/teamai-cli/issues/564)).
- `teamai init <repo> --provider <name>` uses the named provider instead of detecting one, so a member of a team on self-hosted GitLab can join with `--provider git` and their existing Git authentication, without `GITLAB_TOKEN`. The choice is saved in that machine's local config and takes precedence over the team's `teamai.yaml` `provider` for PR/MR creation and for `teamai doctor`'s provider checks; an existing `teamai.yaml` is unchanged, so other members keep the team's provider, and a `teamai.yaml` that `init` creates records the provider `init` would detect without the flag rather than `git`, and stops with a `GITLAB_URL` hint on an unconfigured self-hosted GitLab. `--provider gitlab` on a host that is not the configured `GITLAB_URL` or `TEAMAI_GITLAB_HOST` stops with a hint instead of sending the token to gitlab.com. With `git`, `teamai push` pushes the branch and leaves the merge request to be opened on the Git host, exiting non-zero as it does for a `provider: git` team repo. Re-running `init` without `--provider` returns to auto-detection. The value must be one of `tgit`, `github`, `cnb`, `gitlab`, `gitcode` or `git`, and `--provider` cannot be combined with `--http` (for [#789](https://github.com/Tencent/teamai-cli/issues/789)).

### 🐛 Bug Fixes

- When `TEAMAI_GITLAB_HOST` and `GITLAB_URL` name different hosts, GitLab commands stop with an error naming both, before any request. A repo on `TEAMAI_GITLAB_HOST` was detected as GitLab while every API call, the token included, went to `GITLAB_URL`. An invalid `GITLAB_URL` is now reported as such by `init` instead of as a failed GitLab login (for [#789](https://github.com/Tencent/teamai-cli/issues/789)).
- A misspelled top-level key in `mcp/mcp.yaml` or `hooks/hooks.yaml` (`server:` for `servers:`, `hook:` for `hooks:`) no longer removes every installed team MCP server or hook: such a file read as empty. It now fails like a file that does not parse, so pull keeps what is installed, and pull and `teamai doctor` name the file, the keys found and the key expected. An extra top-level key beside `servers:` or `hooks:` is still ignored (for [#822](https://github.com/Tencent/teamai-cli/issues/822)).
- The closing line of `teamai recall` output is in English (for [#822](https://github.com/Tencent/teamai-cli/issues/822)).
- A broken team file no longer wipes or downgrades what a member has installed. An `mcp/mcp.yaml` or `hooks/hooks.yaml` that did not parse reconciled to an empty set and removed every team MCP server or hook from every tool, and two active namespaces defining one skill or agent aborted the pull for the whole scope, skipping rules, env, docs and cleanup. Now a file in the active set that does not parse or cannot be read, a name repeated inside one file, or one name in two active namespaces stops only that resource type for the run: env keeps `env.sh`, hooks and MCP keep their entries (the built-in hooks, with the session-start pull, are still installed where missing: with the root hooks file's `builtin:` overrides when it parses, and otherwise with their defaults only in a tool that has none yet; `teamai init` says the team hooks were not installed), model profiles leave switched agents alone, skills and agents keep what is installed, and every other type still syncs. The warning names the file or both files and the fix, and for env, hooks, MCP and models is also written to `~/.teamai/debug.log` for session-start pulls (for [#707](https://github.com/Tencent/teamai-cli/issues/707)).
Expand Down
12 changes: 10 additions & 2 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ git@git.example.com:group/repo.git → 检查 GitLab,未确认则 git

已知 host 和显式配置的 GitLab 实例优先。对于未知 host,`init` 会匿名探测 GitLab 登录页;确认是未配置的 GitLab 实例时,先提示设置 `GITLAB_URL` 和 `GITLAB_TOKEN` 后重试,不会直接把探测结果写入配置。未确认则继续使用 `git`。

初始化成功后,provider 选择会写入 team 仓库的 `teamai.yaml` 的 `provider` 字段,后续 `push` / `pull` 都按这个值来。探测不会自动修改已有的 provider。
初始化成功后,provider 选择会写入 team 仓库的 `teamai.yaml` 的 `provider` 字段,后续 `push` / `pull` 都按这个值来;成员用 `--provider` 保存在本机的选择优先于它(见下节)。探测不会自动修改已有的 provider。

### 手动指定 provider(`--provider`)

`teamai init <input> --provider <name>` 跳过上面的自动检测(包括 GitLab 探测),直接使用指定的 provider,取值与 `teamai.yaml` 的 `provider` 相同:`tgit`、`github`、`cnb`、`gitlab`、`gitcode`、`git`。典型用法是团队仓库在自建 GitLab 上、但成员只需要普通 Git:`--provider git` 不做平台登录、不检查 `GITLAB_TOKEN`,clone/pull/push 走已有的 Git 凭据。

该选择写入成员本机的本地配置(`provider` 字段),只影响这台机器:创建 PR/MR(`push`、`remove` 等)和 `doctor` 的 provider 检查优先使用它,已有的 `teamai.yaml` 不变。`init` 新建 `teamai.yaml`(空仓库,或单仓库模式首次初始化)时,`--provider git` 写入的仍是不带该参数时检测到的 provider(包括 GitLab 探测);探测到尚未配置的自建 GitLab 时 `init` 会停止并提示设置 `GITLAB_URL`,不会把 `git` 写成团队默认值。其他值按指定值写入。不带 `--provider` 重新运行 `init` 即恢复自动检测。

自建 GitLab 使用 `--provider gitlab` 时仍需设置 `GITLAB_URL`(以及 `GITLAB_TOKEN`)。GitLab API 地址取自 `GITLAB_URL`,未设置时指向 gitlab.com,所以检测无法识别该 host 时 `init` 会直接报错退出,不会把 token 发往别处。

## 通用 Git Provider(自建/私有仓库)

Expand Down Expand Up @@ -229,7 +237,7 @@ export GITLAB_TOKEN=glpat-xxx
### 自托管实例检测

- **公有 gitlab.com**:URL host 直接命中,自动选择 gitlab provider。
- **自托管实例**:设置 `GITLAB_URL` 后,URL host 与 `GITLAB_URL` 的 host 相同时自动识别为 gitlab;也可用 `TEAMAI_GITLAB_HOST` 直接指定 host。仓库参数需使用完整 HTTP(S) 或 SSH URL:
- **自托管实例**:设置 `GITLAB_URL` 后,URL host 与 `GITLAB_URL` 的 host 相同时自动识别为 gitlab;也可用 `TEAMAI_GITLAB_HOST` 直接指定 host,未设 `GITLAB_URL` 时 API 指向 `https://<该 host>`。两者同时设置但 host 不同时,teamai 会在发送 token 前报错停止。仓库参数需使用完整 HTTP(S) 或 SSH URL:
```bash
export GITLAB_URL=https://git.example.com
teamai init https://git.example.com/yourgroup/yourrepo # → gitlab
Expand Down
18 changes: 18 additions & 0 deletions docs/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,12 @@ export GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxx
teamai init https://git.example.com/yourgroup/yourrepo
```

`TEAMAI_GITLAB_HOST=git.example.com` also works without `GITLAB_URL`: the API then goes to `https://git.example.com`. When both are set they must name the same host, otherwise teamai stops before sending the token.

For an unknown host, `init` makes an anonymous GitLab sign-in page check with a three-second total timeout. If confirmed as GitLab, it stops before authentication, cloning, or writing configuration and asks you to set the instance URL and token, then retry. The check does not send tokens or follow redirects. If it cannot confirm GitLab, initialization continues with the generic `git` provider, which supports Git transport but cannot create repos or PRs/MRs automatically. Set `GITLAB_URL` explicitly for instances behind SSO, deployed under a subpath, or otherwise inaccessible to the check.

Members who only sync and never need the CLI to open merge requests can skip the token: see [plain Git with `--provider git`](#member-onboarding).

**Already initialized with `provider: git`?** Set the variables above and change `provider` to `gitlab` in the team repo's `teamai.yaml`. Setting the environment variables alone does not change an existing provider selection. A failed `teamai push` may already have pushed the branch; if its diagnostic detects GitLab, it prints these recovery steps. See [provider configuration](providers.md#gitlab-provider含自托管).

### Project Scope (default)
Expand Down Expand Up @@ -541,6 +545,20 @@ npm install -g teamai-cli
teamai init https://github.com/yourorg/yourrepo --scope user
```

**Plain Git, no platform token (`--provider git`):**

When the team repo is on a platform whose provider needs a token (for example self-hosted GitLab and `GITLAB_TOKEN`), a member who never needs the CLI to open PRs/MRs can use their existing Git authentication (SSH key or credential helper) instead:

```bash
teamai init https://gitlab.example.com/yourgroup/yourrepo --provider git
```

- `--provider` skips auto-detection and uses the named provider: `tgit`, `github`, `cnb`, `gitlab`, `gitcode`, or `git`. `git` runs no platform login or token check.
- The choice is saved in this machine's local config only. An existing `teamai.yaml` is not changed, so other members keep the team's provider. When `init` creates a new `teamai.yaml`, `--provider git` still records the provider `init` would detect without it. If the host is a self-hosted GitLab that is not configured, `init` stops and asks for `GITLAB_URL` rather than record `git` as the team default.
- `--provider gitlab` on a self-hosted instance still needs `GITLAB_URL` or `TEAMAI_GITLAB_HOST` (and `GITLAB_TOKEN`). Without either `init` stops, because the GitLab API would otherwise target gitlab.com.
- `pull` works as usual. `push` pushes the branch but cannot open a PR/MR, so open it on the Git host yourself; the command exits non-zero because that step did not run.
- Re-running `teamai init` without `--provider` returns to auto-detection.

**HTTP mode (read-only consumer):**

For users or agents that don't need git access and only consume skills/rules:
Expand Down
18 changes: 18 additions & 0 deletions docs/usage-guide.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,12 @@ export GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxx
teamai init https://git.example.com/yourgroup/yourrepo
```

也可以不设 `GITLAB_URL`,只设 `TEAMAI_GITLAB_HOST=git.example.com`:此时 API 指向 `https://git.example.com`。两者同时设置时必须是同一个 host,否则 teamai 会在发送 token 前停止。

对于未知 host,`init` 会匿名检查 GitLab 登录页,总超时为三秒。确认是 GitLab 后,会在认证、克隆或写入配置前停止,提示设置实例地址和 token 后重试。探测不发送 token,也不跟随重定向。无法确认时,初始化继续使用通用 `git` provider;它支持 Git 传输,但不能自动建仓或创建 PR/MR。实例若由 SSO 遮蔽、部署在子路径下,或无法被探测访问,请显式设置 `GITLAB_URL`。

只同步资源、从不需要 CLI 创建 MR 的成员可以不配 token:见[成员接入](#成员接入)中的 `--provider git`。

**已经初始化为 `provider: git`?** 设置上述环境变量,并把团队仓库 `teamai.yaml` 中的 `provider` 改为 `gitlab`。仅设置环境变量不会改变已有 provider 选择。失败的 `teamai push` 可能已经推送了分支;若其诊断探测到 GitLab,会输出这些修复步骤。详见 [Provider 配置](providers.md#gitlab-provider含自托管)。

### 项目级(Project Scope,默认)
Expand Down Expand Up @@ -482,6 +486,20 @@ npm install -g teamai-cli
teamai init https://github.com/yourorg/yourrepo --scope user
```

**纯 Git、无需平台 token(`--provider git`):**

团队仓库所在平台的 provider 需要 token 时(例如自建 GitLab 需要 `GITLAB_TOKEN`),从不需要 CLI 创建 PR/MR 的成员可以改用已有的 Git 认证(SSH Key 或 Credential Helper):

```bash
teamai init https://gitlab.example.com/yourgroup/yourrepo --provider git
```

- `--provider` 跳过自动检测,直接使用指定的 provider:`tgit`、`github`、`cnb`、`gitlab`、`gitcode` 或 `git`。`git` 不做平台登录,也不检查 token。
- 该选择只保存在本机的本地配置中。已有的 `teamai.yaml` 不变,其他成员仍使用团队的 provider。`init` 新建 `teamai.yaml` 时,`--provider git` 写入的仍是 `init` 不带该参数时检测到的 provider;若 host 是尚未配置的自建 GitLab,`init` 会停止并提示设置 `GITLAB_URL`,而不是写入 `git`。
- 自建 GitLab 使用 `--provider gitlab` 时仍需设置 `GITLAB_URL` 或 `TEAMAI_GITLAB_HOST`(以及 `GITLAB_TOKEN`)。两者都未设置时 `init` 会直接停止,否则 GitLab API 会指向 gitlab.com。
- `pull` 照常工作。`push` 会推送分支,但无法创建 PR/MR,需要到 Git 平台上手动创建;由于这一步没有完成,命令以非零退出码结束。
- 不带 `--provider` 重新运行 `teamai init` 即恢复自动检测。

**HTTP 模式(只读消费者):**

无需 git 访问、仅消费 skills/rules 的用户或 agent:
Expand Down
1 change: 1 addition & 0 deletions skill-data/core/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Generated: do not edit by hand. Regenerate with
- `teamai init [repo]` — Initialize teamai (configure Git provider, clone repo, register member)
- `--repo <repo>` — Team repo (alias of the positional argument)
- `--http <url>` — Git-free HTTP team repo (read-only consumer; only needs an API key)
- `--provider <name>` — Git provider for the team repo on this machine: tgit, github, cnb, gitlab, gitcode, or git. Skips auto-detection. `git` uses your existing Git auth and needs no platform token, but opens no PR/MR.
- `--self` — Single-repo mode: the current git repo is the team repo (equivalent to `teamai init .`). Knowledge lives on main under .teamai/; reports go to the teamai-reports orphan branch.
- `--token <key>` — API key for HTTP team repo / status reporting (stored 0600, never committed). Also reads TEAMAI_API_TOKEN.
- `--scope <scope>` — Install scope: project (default, <cwd>/.teamai + <cwd>/.claude) or user (~/.teamai + ~/.claude)
Expand Down
3 changes: 3 additions & 0 deletions skill-data/core/references/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ read -rs GITLAB_TOKEN && export GITLAB_TOKEN # paste when prompted; api scope
teamai init https://git.example.com/yourgroup/yourrepo
```

A member who only syncs and never needs the CLI to open merge requests can skip
both: `teamai init <url> --provider git` uses their existing Git authentication.

## Which tools actually get hooks

`teamai hooks inject` prints **"Hooks injected into all AI tool settings"** even
Expand Down
6 changes: 5 additions & 1 deletion skill-data/setup/references/join-member.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ Match the login to the URL's host (do NOT create a second repo):
before continuing. (Headless/CI only: pre-set `GITHUB_TOKEN` — a token with `repo`
scope — instead.)
- **`gitlab.com/...`** or self-hosted GitLab → set `GITLAB_TOKEN` (and `GITLAB_URL`
for self-hosted, with `api` scope)
for self-hosted, with `api` scope). **Exception:** a member who only syncs and
never needs the CLI to open merge requests (typical for non-developers) can skip
the token: add `--provider git` to the `init` in Step 4. Git then uses their
existing SSH key or credential helper, and `push` leaves the MR for them to open
on the web.

If they have no account on that platform, they register there, then ask the admin
to add them to the repo.
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,19 @@ describe('doctor — hook checks', () => {
expect(allPassed).toBe(false);
});

// #789: a member on `init --provider git` is not asked for the team
// provider's CLI or token.
it('checks the member\'s provider instead of the team\'s', async () => {
mockedLoadLocalConfig.mockResolvedValue({ ...mockLocalConfig, provider: 'git' });

await doctor({});

const allLines = consoleSpy.mock.calls.map((c) => String(c[0]));
expect(allLines.some((line) => line.includes('gf CLI'))).toBe(false);
expect(mockedIsGfInstalled).not.toHaveBeenCalled();
expect(mockedGfIsAuthenticated).not.toHaveBeenCalled();
});

it('checks hooks only for enabled agents', async () => {
mockedLoadLocalConfig.mockResolvedValue({
...mockLocalConfig,
Expand Down
53 changes: 53 additions & 0 deletions src/__tests__/gitlab-detection-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,56 @@ describe('self-hosted GitLab detection through the built CLI', () => {
expect(requests).toHaveLength(before);
});
});

describe('GitLab token requests through the built CLI stay on the repository host', () => {
let sandbox: string;
let stub: string;

beforeAll(() => {
expect(fs.existsSync(CLI), 'Run npm run build first').toBe(true);
sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-gitlab-host-'));
// Preloaded into the CLI: records each request host and answers 401, so no
// request leaves the machine.
stub = path.join(sandbox, 'stub-fetch.mjs');
fs.writeFileSync(stub, [
"import fs from 'node:fs';",
'globalThis.fetch = async (input) => {',
" fs.appendFileSync(process.env.FETCH_LOG, new URL(String(input)).host + '\\n');",
" return new Response('{}', { status: 401 });",
'};',
].join('\n'));
});

afterAll(() => {
if (sandbox) fs.rmSync(sandbox, { recursive: true, force: true });
});

async function runInit(extraArgs: string[], env: Record<string, string>) {
const dir = fs.mkdtempSync(path.join(sandbox, 'run-'));
const log = path.join(dir, 'fetch.log');
const result = await runCLI(
['init', 'https://gitlab.corp/team/repo.git', '--agent', 'claude', '--force', ...extraArgs],
dir, dir, { NODE_OPTIONS: `--import=${stub}`, FETCH_LOG: log, GITLAB_TOKEN: 'corp-token', ...env },
);
const hosts = fs.existsSync(log) ? fs.readFileSync(log, 'utf8').trim().split('\n') : [];
return { ...result, hosts };
}

for (const [label, extraArgs] of [['auto-detected', []], ['--provider gitlab', ['--provider', 'gitlab']]] as const) {
it(`${label}: TEAMAI_GITLAB_HOST without GITLAB_URL sends the token only to that host`, async () => {
const result = await runInit([...extraArgs], { TEAMAI_GITLAB_HOST: 'gitlab.corp' });
expect(result.hosts.length, result.output).toBeGreaterThan(0);
expect(new Set(result.hosts)).toEqual(new Set(['gitlab.corp']));
});

it(`${label}: TEAMAI_GITLAB_HOST and GITLAB_URL naming different hosts sends no request`, async () => {
const result = await runInit([...extraArgs], {
TEAMAI_GITLAB_HOST: 'gitlab.corp', GITLAB_URL: 'https://gitlab.com',
});
expect(result.code, result.output).toBe(1);
expect(result.hosts).toEqual([]);
expect(result.output).toContain('TEAMAI_GITLAB_HOST');
expect(result.output).not.toContain('corp-token');
});
}
});
Loading
Loading