teamai-cli 0.16.2 → 0.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,326 @@
1
+ # TeamAI — The team harness for AI agents
2
+
3
+ > [English](README.md) | [简体中文](README.zh-CN.md)
4
+
5
+ [![CI](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml)
6
+ [![npm version](https://img.shields.io/npm/v/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli)
7
+ [![npm downloads](https://img.shields.io/npm/dm/teamai-cli.svg)](https://www.npmjs.com/package/teamai-cli)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+
10
+ 让每个 AI 编程助手都按同一套标准工作。
11
+
12
+ 通过 Git 统一管理 skills、rules、docs,驾驭 20+ 种 AI 工具——一个人也能用,团队用更强。
13
+
14
+ **支持:** Claude Code、Codex、Cursor、CodeBuddy IDE,以及 Gemini CLI、Windsurf、Trae、Aider、Amp、OpenClaw 等 20+ 种 AI 编程工具(skills 同步)。
15
+
16
+ > 📖 **完整使用指南**:[docs/usage-guide.md](docs/usage-guide.md) — 涵盖从团队创建到日常使用的全流程。
17
+
18
+ > 📚 **Provider 说明**:[docs/providers.md](docs/providers.md) — GitHub / TGit 差异与认证配置。
19
+
20
+ 如有问题或建议,欢迎提交 PR 或 Issue,一起共建这个项目。
21
+
22
+ ## 安装
23
+
24
+ ```bash
25
+ npm install -g teamai-cli
26
+ ```
27
+
28
+ <details>
29
+ <summary>腾讯内部用户:通过 tnpm 安装 <code>@tencent/teamai-cli</code></summary>
30
+
31
+ ```bash
32
+ npm install -g @tencent/teamai-cli --registry=http://r.tnpm.oa.com
33
+ ```
34
+
35
+ 两个包的代码内容一致,`@tencent/teamai-cli` 只是公网 `teamai-cli` 的内网镜像。
36
+ </details>
37
+
38
+ ## 快速开始
39
+
40
+ ### 团队成员
41
+
42
+ ```bash
43
+ # 用户级初始化(默认,资源安装到 ~/)
44
+ teamai init --repo yourteam/yourproject
45
+
46
+ # 项目级初始化(资源安装到项目目录下)
47
+ cd /path/to/my-project
48
+ teamai init --repo yourteam/yourproject --scope project
49
+
50
+ # 非交互模式(适合 CI/CD 或 AI agent 自动化)
51
+ teamai init --repo yourteam/yourproject --scope user --role hai_dev --force
52
+ ```
53
+
54
+ ### 管理员
55
+
56
+ 先在 git 托管平台上创建好团队共享经验的仓库(默认 GitHub;TGit 也支持),并把所有团队成员加入到该仓库的 write 权限。
57
+
58
+ - **GitHub**:用 `gh repo create yourorg/yourproject --private` 创建,或在 UI 上建。然后用 Settings → Collaborators 把成员加进来,并把 master/main 设置为默认分支。
59
+ - **TGit(腾讯工蜂)**:在 [git.woa.com](https://git.woa.com/) 上创建,通过 user group 批量添加 master 权限。
60
+
61
+ CLI 会根据用户传入的 repo URL 自动选择 provider:
62
+
63
+ - `yourorg/yourrepo` 或 `https://github.com/yourorg/yourrepo` → GitHub
64
+ - `https://git.woa.com/yourteam/yourrepo` → TGit
65
+
66
+ ## 命令
67
+
68
+ | 命令 | 说明 |
69
+ |------|------|
70
+ | `teamai init [--scope <user\|project>] [--role <id>] [--force]` | 初始化(自动安装 gf CLI、OAuth 登录、关联仓库、注册成员、配置 reviewers、注入 hooks) |
71
+ | `teamai push [--all] [--role <id>]` | 推送本地新资源到独立分支并创建 Merge Request;新 skill 交互式选择目标命名空间,可用 `--role` 覆盖 |
72
+ | `teamai pull [--silent]` | 拉取团队资源并注入到本地 AI 工具(支持双 scope 依次拉取) |
73
+ | `teamai status` | 查看本地 vs 团队仓库差异 |
74
+ | `teamai list [type] [--source repo\|local\|all] [--agent <id>]` | 列出资源(skills\|rules\|docs\|env\|wiki);`--source local` 或 `all` 时会扫描已安装 AI agent 下的 skills 目录,并标注每个 skill 的来源 (`[team]` / `[builtin]` / `[source:<name>]` / `[local-only]`) |
75
+ | `teamai skill [list\|show <name>]` | 默认列出全部 skill;`show <name>` 输出指定 skill 的来源、贡献者、已安装的 agent 列表与描述摘要 |
76
+ | `teamai members` | 列出已注册的团队成员 |
77
+ | `teamai remove <type> <name>` | 从团队仓库和本地删除资源并创建 MR(skills\|rules\|wiki) |
78
+ | `teamai roles` | 管理团队角色(`init`/`list`/`set`/`add`/`remove`/`update`) |
79
+ | `teamai source` | 管理跨团队 skill 订阅源(`add`/`remove`/`list`/`browse`) |
80
+ | `teamai contribute --file <path> [--scope <user\|project>]` | 将 AI 生成的经验文档推送到团队仓库 |
81
+ | `teamai recall <query>` | 搜索团队知识库,自动合并 user + project 双 scope 结果 |
82
+ | `teamai digest` | 生成团队 AI 使用周报(skill 排行、新增/更新 skill、session 摘要) |
83
+ | `teamai hooks` | 管理 AI 工具 hooks(list / inject / remove) |
84
+ | `teamai uninstall [--force]` | 卸载 teamai:移除 hooks、rules、skills、env、docs、~/.teamai/ |
85
+ | `teamai doctor` | 诊断配置问题 |
86
+
87
+ 全局选项:
88
+ - `--dry-run` — 预览模式,不做实际变更
89
+ - `--verbose, -v` — 详细输出
90
+
91
+ ## 工作原理
92
+
93
+ ```
94
+ 成员 A 成员 B
95
+ 创建 skill / 写规则 同上
96
+ │ │
97
+ ▼ ▼
98
+ teamai push teamai push
99
+ │ │
100
+ ▼ ▼
101
+ 创建分支 + MR 创建分支 + MR
102
+ │ │
103
+ └──────► 团队git仓库 ◄──────────────┘
104
+ │ ▲
105
+ │ │ reviewer 审批合并 MR
106
+
107
+ SessionStart hook → teamai pull
108
+ 自动拉取到所有成员本地
109
+ ```
110
+
111
+ - `teamai push` 会创建独立分支(`teamai/push/<user>/<timestamp>`),推送后自动创建 Merge Request 并指派 reviewers
112
+ - `teamai init` 初始化时可配置默认 reviewers(记录在 `teamai.yaml` 的 `reviewers` 字段)
113
+ - `teamai init` 会自动注入与各工具格式对齐的 hooks(含 `SessionStart`、`Stop`、`PostToolUse`、`UserPromptSubmit` 等),会话中会执行 `teamai pull`、`teamai update`、追踪与仪表盘等(支持 Claude Code、Codex、Claude Code Internal、Codex Internal、Cursor、CodeBuddy IDE、OpenClaw、WorkBuddy)
114
+ - Skills 同步到 `~/.claude/skills/`、`~/.codex/skills/`、`~/.codex-internal/skills/`、`~/.claude-internal/skills/`、`~/.cursor/skills/`、`~/.codebuddy/skills/`
115
+ - Rules 同步到各工具的 rules 目录,并通过标记注释合并到 `CLAUDE.md`(支持 claude、claude-internal、codebuddy)
116
+ - Knowledge 同步到 `~/.teamai/docs/`
117
+ - Learnings 同步到 `~/.teamai/learnings/`,并基于该目录构建 recall 索引(全团队共享,不按角色拆分)
118
+ - Culture 同步团队文化文件(`culture.md`),编译 frontmatter 和 body 后注入到各 AI 工具的 `CLAUDE.md`
119
+
120
+ ## 角色化 Skills
121
+
122
+ 当团队资源仓库启用角色化目录后,Skills 按角色 namespace 组织,CLI 在 `teamai init` 时要求选择 `primaryRole` 和可选的 `additionalRoles`,并写入本地 `config.yaml`。
123
+
124
+ 远端仓库目录约定:
125
+
126
+ ```text
127
+ manifest/roles.yaml # 角色定义
128
+ skills/<namespace>/<skill>/ # 按 namespace 组织的 skills
129
+ rules/ # 全局,不做角色拆分
130
+ ```
131
+
132
+ - `teamai pull` 读取 `manifest/roles.yaml`,只同步 `primaryRole + additionalRoles` 对应 namespace 中的 skills(同时保留 tag 过滤的并集)。
133
+ - Skills 从 `skills/<namespace>/<skill-name>/` 拍平安装到本地 `<tool>/skills/<skill-name>/`,用户无感知 namespace 结构。
134
+ - 如果激活 namespace 中出现同名 skill,`pull` 会直接失败,避免隐式覆盖。
135
+ - 不在激活 namespace 中、也不在 tag 过滤结果中的 skills 会被自动清理。
136
+ - `rules/`、`docs/`、`learnings/` 仍然保持原有逻辑,不做角色拆分(learnings 全团队共享)。
137
+
138
+ 配置示例:
139
+
140
+ ```yaml
141
+ primaryRole: hai
142
+ additionalRoles:
143
+ - pm
144
+ resourceProfileVersion: 1
145
+ ```
146
+
147
+ 这会同步 `skills/common/`、`skills/hai/`、`skills/pm/` 三个 namespace 中的所有 skills。
148
+
149
+ ## 角色化推送
150
+
151
+ 角色化仓库下,推送新 skill 时 CLI 会自动检测可用的命名空间并提供交互式选择:
152
+
153
+ ```bash
154
+ # 交互式选择命名空间(推荐)
155
+ teamai push
156
+ # 输出:
157
+ # Which namespace should new skills be pushed to?
158
+ # 1. common
159
+ # 2. hai
160
+ # 3. pm
161
+ # Choose namespace [1-3] (default: 1 = common):
162
+
163
+ # 显式指定目标 namespace
164
+ teamai push --role pm
165
+ ```
166
+
167
+ - 有 `primaryRole` 时,从 `manifest/roles.yaml` 展开可用 namespace 列表
168
+ - 无 `primaryRole` 时,自动扫描团队仓库目录结构中的 namespace
169
+ - 单一命名空间时自动选中,无需交互
170
+ - `--role <id>` 可临时覆盖目标 namespace
171
+ - 修改已有 skill 时自动保持原 namespace,无需重新选择
172
+
173
+ 推送时 CLI 会自动检查 `SKILL.md` 的 YAML frontmatter(`name`/`description`),缺失则自动补全,无需手动维护。
174
+
175
+ ## 团队文化(Culture)
176
+
177
+ 在团队仓库根目录创建 `culture.md`,用 YAML frontmatter 定义公司和团队信息,body 部分写团队文化指引:
178
+
179
+ ```markdown
180
+ ---
181
+ company:
182
+ name: Acme Corp
183
+ mission: Build great things
184
+ values:
185
+ - Innovation
186
+ - Integrity
187
+ team:
188
+ name: Platform
189
+ mission: Enable developers
190
+ goals:
191
+ - Ship v2.0
192
+ - Improve test coverage
193
+ ---
194
+
195
+ ## 编码准则
196
+
197
+ - 所有 PR 必须有至少一个 reviewer 审批
198
+ - 禁止直接 push master
199
+ - 测试覆盖率不低于 80%
200
+ ```
201
+
202
+ `teamai pull` 时会自动将 culture.md 编译为结构化内容,注入到各 AI 工具的 `CLAUDE.md` 中(`<!-- [teamai:culture:start] -->` / `<!-- [teamai:culture:end] -->` 标记之间)。AI 编码助手在每次会话中都能感知团队文化。
203
+
204
+ ## 跨团队 Skill 订阅
205
+
206
+ 通过 `teamai source` 订阅其他团队的公共 skill 仓库,pull 时自动同步订阅源的 skills:
207
+
208
+ ```bash
209
+ # 添加订阅源
210
+ teamai source add https://git.woa.com/other-team/teamai-public.git --name other-team
211
+
212
+ # 查看已订阅的源
213
+ teamai source list
214
+
215
+ # 浏览订阅源的 skills
216
+ teamai source browse other-team
217
+
218
+ # 移除订阅(同时清理其 skills)
219
+ teamai source remove other-team
220
+ ```
221
+
222
+ 订阅源的 skills 在 `teamai pull` 时自动同步到本地,与团队自有 skills 共存。
223
+
224
+ ## Scope(作用域)
225
+
226
+ TeamAI 支持两种 scope,可以共存:
227
+
228
+ | 维度 | User Scope(默认) | Project Scope |
229
+ |------|-------------------|---------------|
230
+ | **资源安装位置** | `~/` 下(如 `~/.claude/skills/`) | 项目目录下(如 `<project>/.claude/skills/`) |
231
+ | **配置文件** | `~/.teamai/config.yaml` | `<project>/.teamai/config.yaml` |
232
+ | **适用场景** | 通用团队规范、跨项目技能 | 项目特定的技能和规则 |
233
+ | **初始化** | `teamai init --repo <group>/<repo>` | `cd <project> && teamai init --repo <group>/<repo> --scope project` |
234
+
235
+ **双 scope 协同:**
236
+ - `teamai pull` 会依次拉取 user + project 两个 scope 的资源,互不冲突
237
+ - `teamai contribute --scope user/project` 可显式选择推送到哪个仓库
238
+ - `teamai recall` 自动合并两个 scope 的知识库,统一搜索排序,结果标注来源 `[user]`/`[project]`
239
+ - 远端 `teamai.yaml` 的 `scope` 字段锁定仓库类型,成员 init 时必须匹配
240
+
241
+ ## 经验自动分享
242
+
243
+ 当一次 AI coding session 结束时,系统会通过 Stop hook 智能评估 session 价值并提示分享:
244
+
245
+ ```
246
+ AI coding session (持续工作中...)
247
+
248
+ ▼ PostToolUse hook 持续追踪工具调用和 skill 使用
249
+
250
+ ▼ 会话结束(Stop hook 触发)
251
+
252
+ ├─ 智能评分:工具调用数量 + 工具多样性 + skill 使用 + 错误重试 + session 时长
253
+ │ (从 dashboard events.jsonl 提取,一次性评估,满分 100)
254
+
255
+ ├─ 分数 < 35 → 不打扰(工具调用少或缺乏多样性,没有总结价值)
256
+
257
+ ▼ 分数 ≥ 35
258
+
259
+ AI 提示:"本次 session 内容丰富,建议运行 /teamai-share-learnings 分享经验"
260
+
261
+ ▼ 用户同意
262
+
263
+ /teamai-share-learnings (AI sub-agent)
264
+ ├─ AI 总结本次 session 的经验
265
+ ├─ 生成 Markdown 文档
266
+ └─ teamai contribute --file <path> → 直接 push 到团队仓库 learnings/
267
+ ```
268
+
269
+ - `/teamai-share-learnings` 是 CLI 内置 skill,随 `teamai pull/init` 自动部署到本地
270
+ - 每个 session 最多提示一次(去重),用户可以忽略
271
+ - 文档直接 push 到 `learnings/` 目录,团队成员下次 pull 时可见
272
+
273
+ ## 团队知识回忆
274
+
275
+ `teamai recall` 实现知识飞轮的"读出路径"——AI 可以自动搜索团队积累的经验文档:
276
+
277
+ ```
278
+ contribute(写入) → pull(同步+索引) → recall(搜索) → upvote(投票) → 排序优化
279
+ ```
280
+
281
+ ```bash
282
+ $ teamai recall "fuse 端口"
283
+ [1/2] MR 审查发现 FUSE 端口冲突 Bug ★1 [user]
284
+ Author: jeffyxu | Score: 18.5 | Tags: troubleshooting, fuse, k8s
285
+
286
+ [2/2] FUSE 部署配置最佳实践 [project]
287
+ Author: alice | Score: 12.0 | Tags: fuse, deploy
288
+ ```
289
+
290
+ - **双 scope 合并搜索**:自动合并 user 和 project scope 的知识库,结果标注来源
291
+ - Hybrid 中英文搜索(Intl.Segmenter + CJK bigrams)
292
+ - 搜索自动投票,好文档自然浮到顶部
293
+ - 投票按 scope 分别写入各自的 repo,归属正确
294
+
295
+ ## 更新
296
+
297
+ ```bash
298
+ teamai update # 自动检测并升级到最新版
299
+ npm update -g teamai-cli # 或手动触发 npm 升级
300
+ ```
301
+
302
+ `teamai update` 会根据当前安装的包名自动选择 registry:
303
+
304
+ - `teamai-cli` → 公网 npm (`https://registry.npmjs.org`)
305
+ - `@tencent/teamai-cli` → 内网 tnpm (`http://r.tnpm.oa.com`)
306
+
307
+ 如需手动覆盖 registry,可以设置环境变量 `TEAMAI_NPM_REGISTRY=<url>`。
308
+
309
+ ### 自动更新控制
310
+
311
+ 自动更新通过 Stop hook 在会话结束时执行,可在两个层级控制:
312
+
313
+ | 配置层级 | 文件 | 字段 | 可选值 |
314
+ |---------|------|------|-------|
315
+ | 团队默认 | `teamai.yaml` | `autoUpdate` | `true`(默认)/ `false` |
316
+ | 用户覆盖 | `~/.teamai/config.yaml` | `updatePolicy` | `auto` / `prompt` / `skip` |
317
+
318
+ 用户级 `updatePolicy` 始终优先于团队级 `autoUpdate`。
319
+
320
+ ## 许可证
321
+
322
+ [MIT](LICENSE)
323
+
324
+ ## 贡献
325
+
326
+ 欢迎 PR!请先阅读 [CONTRIBUTING.md](.github/CONTRIBUTING.md)。
package/dist/index.js CHANGED
@@ -508,7 +508,8 @@ var init_fs = __esm({
508
508
  "__pycache__",
509
509
  ".pyc",
510
510
  ".DS_Store",
511
- "node_modules"
511
+ "node_modules",
512
+ ".git"
512
513
  ]);
513
514
  }
514
515
  });
@@ -4306,77 +4307,60 @@ var init_wiki = __esm({
4306
4307
  return relativePath.replace(/\.md$/, "");
4307
4308
  }
4308
4309
  /**
4309
- * Scan local AI tool wiki directories for pages that are new or modified
4310
+ * Get the shared wiki directory for the current scope.
4311
+ * - User scope → ~/.teamai/wiki/
4312
+ * - Project scope → <projectRoot>/.teamai/wiki/
4313
+ */
4314
+ static getSharedWikiDir(localConfig) {
4315
+ const teamaiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot);
4316
+ return path18.join(teamaiHome, "wiki");
4317
+ }
4318
+ /**
4319
+ * Scan the shared wiki directory for pages that are new or modified
4310
4320
  * compared to the team repo.
4311
4321
  */
4312
4322
  async scanLocalForPush(teamConfig, localConfig) {
4313
4323
  const teamWikiDir = path18.join(localConfig.repo.localPath, "wiki");
4314
4324
  const teamPages = /* @__PURE__ */ new Map();
4315
4325
  if (await pathExists(teamWikiDir)) {
4316
- const files = await listFilesRecursive(teamWikiDir);
4317
- for (const file of files) {
4326
+ const files2 = await listFilesRecursive(teamWikiDir);
4327
+ for (const file of files2) {
4318
4328
  if (_WikiHandler.isWikiPage(file)) {
4319
4329
  const name = _WikiHandler.pathToName(file);
4320
4330
  teamPages.set(name, path18.join(teamWikiDir, file));
4321
4331
  }
4322
4332
  }
4323
4333
  }
4334
+ const sharedWikiDir = _WikiHandler.getSharedWikiDir(localConfig);
4335
+ if (!await pathExists(sharedWikiDir)) return [];
4324
4336
  const tombstones = await this.readTombstones(localConfig);
4325
- const candidates = /* @__PURE__ */ new Map();
4326
- for (const [__, toolPath] of Object.entries(teamConfig.toolPaths)) {
4327
- if (!toolPath.wiki) continue;
4328
- const wikiDir = path18.join(resolveBaseDir(localConfig), toolPath.wiki);
4329
- if (!await pathExists(wikiDir)) continue;
4330
- const files = await listFilesRecursive(wikiDir);
4331
- for (const file of files) {
4332
- if (!_WikiHandler.isWikiPage(file)) continue;
4333
- const name = _WikiHandler.pathToName(file);
4334
- if (tombstones.has(name)) continue;
4335
- const localFilePath = path18.join(wikiDir, file);
4336
- if (teamPages.has(name)) {
4337
- const teamFilePath = teamPages.get(name);
4338
- const equal = await fileContentEqual(localFilePath, teamFilePath);
4339
- if (equal) continue;
4340
- const mtime = await getFileMtime(localFilePath);
4341
- const existing = candidates.get(name);
4342
- if (!existing || mtime > existing.mtime) {
4343
- candidates.set(name, {
4344
- sourcePath: localFilePath,
4345
- mtime,
4346
- status: "modified"
4347
- });
4348
- }
4349
- } else {
4350
- const existing = candidates.get(name);
4351
- if (!existing) {
4352
- const mtime = await getFileMtime(localFilePath);
4353
- candidates.set(name, {
4354
- sourcePath: localFilePath,
4355
- mtime,
4356
- status: "new"
4357
- });
4358
- } else if (existing.status === "new") {
4359
- const mtime = await getFileMtime(localFilePath);
4360
- if (mtime > existing.mtime) {
4361
- candidates.set(name, {
4362
- sourcePath: localFilePath,
4363
- mtime,
4364
- status: "new"
4365
- });
4366
- }
4367
- }
4368
- }
4369
- }
4370
- }
4371
4337
  const items = [];
4372
- for (const [name, candidate] of candidates) {
4373
- items.push({
4374
- name,
4375
- type: "wiki",
4376
- sourcePath: candidate.sourcePath,
4377
- relativePath: `wiki/${name}.md`,
4378
- status: candidate.status
4379
- });
4338
+ const files = await listFilesRecursive(sharedWikiDir);
4339
+ for (const file of files) {
4340
+ if (!_WikiHandler.isWikiPage(file)) continue;
4341
+ const name = _WikiHandler.pathToName(file);
4342
+ if (tombstones.has(name)) continue;
4343
+ const localFilePath = path18.join(sharedWikiDir, file);
4344
+ if (teamPages.has(name)) {
4345
+ const teamFilePath = teamPages.get(name);
4346
+ const equal = await fileContentEqual(localFilePath, teamFilePath);
4347
+ if (equal) continue;
4348
+ items.push({
4349
+ name,
4350
+ type: "wiki",
4351
+ sourcePath: localFilePath,
4352
+ relativePath: `wiki/${name}.md`,
4353
+ status: "modified"
4354
+ });
4355
+ } else {
4356
+ items.push({
4357
+ name,
4358
+ type: "wiki",
4359
+ sourcePath: localFilePath,
4360
+ relativePath: `wiki/${name}.md`,
4361
+ status: "new"
4362
+ });
4363
+ }
4380
4364
  }
4381
4365
  return items;
4382
4366
  }
@@ -4404,57 +4388,47 @@ var init_wiki = __esm({
4404
4388
  log.debug(`Copied wiki page ${item.name} \u2192 team repo`);
4405
4389
  }
4406
4390
  /**
4407
- * Pull a wiki page from team repo to all configured AI tool wiki directories.
4391
+ * Pull a wiki page from team repo to the shared wiki directory.
4408
4392
  */
4409
- async pullItem(item, teamConfig, localConfig) {
4410
- const baseDir = resolveBaseDir(localConfig);
4411
- for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) {
4412
- if (!toolPath.wiki) continue;
4413
- if (!await ResourceHandler.isToolInstalled(toolPath.wiki, baseDir)) {
4414
- log.debug(`Skipping wiki sync for ${tool}: tool not installed`);
4415
- continue;
4416
- }
4417
- const dest = path18.join(baseDir, toolPath.wiki, `${item.name}.md`);
4418
- await ensureDir(path18.dirname(dest));
4419
- try {
4420
- await copyFile(item.sourcePath, dest);
4421
- log.debug(`Synced wiki page ${item.name} \u2192 ${tool}`);
4422
- } catch (e) {
4423
- log.warn(
4424
- `Failed to sync wiki page ${item.name} to ${tool}: ${e.message}`
4425
- );
4426
- }
4393
+ async pullItem(item, _teamConfig, localConfig) {
4394
+ const sharedWikiDir = _WikiHandler.getSharedWikiDir(localConfig);
4395
+ const dest = path18.join(sharedWikiDir, `${item.name}.md`);
4396
+ await ensureDir(path18.dirname(dest));
4397
+ try {
4398
+ await copyFile(item.sourcePath, dest);
4399
+ log.debug(`Synced wiki page ${item.name} \u2192 shared wiki`);
4400
+ } catch (e) {
4401
+ log.warn(
4402
+ `Failed to sync wiki page ${item.name}: ${e.message}`
4403
+ );
4427
4404
  }
4428
4405
  }
4429
4406
  /**
4430
- * Remove a wiki page from the team repo and all local AI tool wiki directories.
4407
+ * Remove a wiki page from the team repo and the shared wiki directory.
4431
4408
  */
4432
- async removeItem(name, teamConfig, localConfig) {
4409
+ async removeItem(name, _teamConfig, localConfig) {
4433
4410
  const removed = [];
4434
- const baseDir = resolveBaseDir(localConfig);
4435
- const fileName = `${name}.md`;
4436
- const teamFile = path18.join(localConfig.repo.localPath, "wiki", fileName);
4411
+ const teamFile = path18.join(localConfig.repo.localPath, "wiki", `${name}.md`);
4437
4412
  if (await pathExists(teamFile)) {
4438
4413
  await remove(teamFile);
4439
4414
  removed.push(teamFile);
4440
4415
  }
4441
4416
  await this.addTombstone(name, localConfig);
4442
- for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) {
4443
- if (!toolPath.wiki) continue;
4444
- const filePath = path18.join(baseDir, toolPath.wiki, fileName);
4445
- if (await pathExists(filePath)) {
4446
- await remove(filePath);
4447
- removed.push(filePath);
4448
- log.debug(`Removed wiki page ${name} from ${tool}`);
4449
- }
4417
+ const sharedWikiDir = _WikiHandler.getSharedWikiDir(localConfig);
4418
+ const sharedFile = path18.join(sharedWikiDir, `${name}.md`);
4419
+ if (await pathExists(sharedFile)) {
4420
+ await remove(sharedFile);
4421
+ removed.push(sharedFile);
4422
+ log.debug(`Removed wiki page ${name} from shared wiki`);
4450
4423
  }
4451
4424
  return removed;
4452
4425
  }
4453
4426
  /**
4454
- * Rebuild _metadata.json from wiki pages on disk.
4427
+ * Rebuild _metadata.json from wiki pages in the shared wiki directory.
4455
4428
  * Called after pull to reconstruct local metadata.
4456
4429
  */
4457
- static async rebuildMetadata(wikiDir) {
4430
+ static async rebuildMetadata(localConfig) {
4431
+ const wikiDir = _WikiHandler.getSharedWikiDir(localConfig);
4458
4432
  if (!await pathExists(wikiDir)) return;
4459
4433
  const files = await listFilesRecursive(wikiDir);
4460
4434
  const pages = {};
@@ -5865,6 +5839,7 @@ __export(pull_exports, {
5865
5839
  cleanupInactiveNamespaceSkills: () => cleanupInactiveNamespaceSkills,
5866
5840
  compileClaudemd: () => compileClaudemd,
5867
5841
  compileCulture: () => compileCulture,
5842
+ filterRulesByKnowledgeNamespaces: () => filterRulesByKnowledgeNamespaces,
5868
5843
  pull: () => pull,
5869
5844
  scanRoleAwareSkills: () => scanRoleAwareSkills
5870
5845
  });
@@ -5914,6 +5889,15 @@ async function buildRolePullContext(localConfig) {
5914
5889
  }
5915
5890
  return { activeNamespaces, activeSkillNames, inactiveSkillNames };
5916
5891
  }
5892
+ function filterRulesByKnowledgeNamespaces(rules, knowledgeNamespaces) {
5893
+ if (!knowledgeNamespaces) return rules;
5894
+ return rules.filter((rule) => {
5895
+ const slashIndex = rule.name.indexOf("/");
5896
+ if (slashIndex === -1) return true;
5897
+ const namespace = rule.name.slice(0, slashIndex);
5898
+ return knowledgeNamespaces.includes(namespace);
5899
+ });
5900
+ }
5917
5901
  async function scanRoleAwareSkills(localConfig, namespaces) {
5918
5902
  const items = /* @__PURE__ */ new Map();
5919
5903
  for (const namespace of namespaces.skills) {
@@ -6040,7 +6024,9 @@ async function pullForScope(localConfig, options) {
6040
6024
  if (type === "rules") {
6041
6025
  const rulesHandler = handler;
6042
6026
  const allItems = await rulesHandler.scanTeamForPull(freshConfig, localConfig);
6043
- const { included: items2, skipped } = filterByTags(allItems, tagsConfig, subscribedTags, "rules");
6027
+ const knowledgeNs = roleContext ? roleContext.activeNamespaces.knowledge : null;
6028
+ const roleFiltered = filterRulesByKnowledgeNamespaces(allItems, knowledgeNs);
6029
+ const { included: items2, skipped } = filterByTags(roleFiltered, tagsConfig, subscribedTags, "rules");
6044
6030
  if (items2.length > 0) {
6045
6031
  if (options.dryRun) {
6046
6032
  log.info(`[${scopeLabel}] [dry-run] Would sync ${items2.length} rule(s)${skipped.length > 0 ? ` (skipped ${skipped.length} by tags)` : ""}`);
@@ -6132,8 +6118,7 @@ async function pullForScope(localConfig, options) {
6132
6118
  if (!options.dryRun) {
6133
6119
  const tombstoneTypes = [
6134
6120
  { type: "rules", ext: ".md", toolPathField: "rules" },
6135
- { type: "skills", toolPathField: "skills" },
6136
- { type: "wiki", ext: ".md", toolPathField: "wiki" }
6121
+ { type: "skills", toolPathField: "skills" }
6137
6122
  ];
6138
6123
  const baseDir = resolveBaseDir(localConfig);
6139
6124
  for (const { type, ext, toolPathField } of tombstoneTypes) {
@@ -6153,6 +6138,23 @@ async function pullForScope(localConfig, options) {
6153
6138
  }
6154
6139
  }
6155
6140
  }
6141
+ try {
6142
+ const wikiHandler = getHandler("wiki");
6143
+ const wikiTombstones = await wikiHandler.readTombstones(localConfig);
6144
+ if (wikiTombstones.size > 0) {
6145
+ const teamaiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot);
6146
+ const wikiDir = path26.join(teamaiHome, "wiki");
6147
+ for (const name of wikiTombstones) {
6148
+ const wikiPath = path26.join(wikiDir, `${name}.md`);
6149
+ if (await pathExists(wikiPath)) {
6150
+ await remove(wikiPath);
6151
+ log.debug(`[${scopeLabel}] Cleaned up tombstoned wiki ${name} from shared wiki`);
6152
+ }
6153
+ }
6154
+ }
6155
+ } catch (e) {
6156
+ log.debug(`[${scopeLabel}] Wiki tombstone cleanup skipped: ${e.message}`);
6157
+ }
6156
6158
  if (roleContext) {
6157
6159
  await cleanupInactiveNamespaceSkills(
6158
6160
  freshConfig,
@@ -11072,7 +11074,7 @@ import { Command } from "commander";
11072
11074
  var require2 = createRequire2(import.meta.url);
11073
11075
  var { version } = require2("../package.json");
11074
11076
  var program = new Command();
11075
- program.name("teamai").description("TeamAI \u2014 \u56E2\u961F AI \u7ECF\u9A8C\u5171\u4EAB\u6846\u67B6").version(version).option("--dry-run", "Preview mode, no changes made").option("-v, --verbose", "Verbose output").hook("preAction", (thisCommand) => {
11077
+ program.name("teamai").description("TeamAI \u2014 The team harness for AI agents").version(version).option("--dry-run", "Preview mode, no changes made").option("-v, --verbose", "Verbose output").hook("preAction", (thisCommand) => {
11076
11078
  const opts = thisCommand.opts();
11077
11079
  if (opts.verbose) setVerbose(true);
11078
11080
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "teamai-cli",
3
- "version": "0.16.2",
4
- "description": "TeamAI — team AI experience sharing framework (skills, rules, docs, env sync across Claude Code, Cursor, Codex, etc.)",
3
+ "version": "0.16.4",
4
+ "description": "TeamAI — the team harness for AI agents (skill sync + shared knowledge base, powered by Git)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "teamai": "dist/index.js"