teamai-cli 0.16.8 → 0.16.9
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.
- package/README.md +41 -0
- package/README.zh-CN.md +41 -0
- package/dist/index.js +841 -95
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/teamai-cli-logo.svg" alt="teamai-cli" width="430">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# TeamAI — The team harness for AI agents
|
|
2
6
|
|
|
3
7
|
> [English](README.md) | [简体中文](README.zh-CN.md)
|
|
@@ -89,6 +93,7 @@ The CLI picks a provider automatically from the repo URL:
|
|
|
89
93
|
| `teamai domains drift [url] [--apply \| --lock \| --apply-all]` | Inspect and resolve domain-drift signals; `--apply` reassigns the repo to the recommended domain and refreshes the aggregate views |
|
|
90
94
|
| `teamai digest` | Generate a team AI usage weekly digest (skill leaderboard, new/updated skills, session summaries) |
|
|
91
95
|
| `teamai hooks` | Manage AI-tool hooks (list / inject / remove) |
|
|
96
|
+
| `teamai ci extract-mr --url <url> [--mode comment\|write\|both] [--individual-comments]` | CI pipeline integration: extract knowledge from MR/PR, post as comments, and write to team repo after merge. With `--individual-comments`, each suggestion is posted separately with reaction/reject support (GitHub 👎 / TGit ☝️) |
|
|
92
97
|
| `teamai uninstall [--force]` | Uninstall teamai: remove hooks, rules, skills, env, docs, and `~/.teamai/` |
|
|
93
98
|
| `teamai doctor` | Diagnose configuration problems |
|
|
94
99
|
|
|
@@ -361,6 +366,42 @@ Auto-update runs on the Stop hook at the end of a session. It can be controlled
|
|
|
361
366
|
|
|
362
367
|
The user-level `updatePolicy` always wins over the team-level `autoUpdate`.
|
|
363
368
|
|
|
369
|
+
## CI Integration
|
|
370
|
+
|
|
371
|
+
TeamAI can integrate into your CI pipeline to automatically extract knowledge from every MR/PR:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
MR opened/updated → CI extracts learning + codebase suggestions → posts as comments
|
|
375
|
+
→ Reviewer rejects unwanted suggestions (GitHub 👎 / TGit ☝️)
|
|
376
|
+
→ MR merged → CI writes approved items to team knowledge repo
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Quick Start
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
# Comment mode: post suggestions to MR (run on PR open/update)
|
|
383
|
+
teamai ci extract-mr --url "$MR_URL" --mode comment --individual-comments
|
|
384
|
+
|
|
385
|
+
# Write mode: write approved items to knowledge repo (run after merge)
|
|
386
|
+
teamai ci extract-mr --url "$MR_URL" --mode write --team-repo ./team-repo --individual-comments
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### CI Templates
|
|
390
|
+
|
|
391
|
+
Ready-to-use templates in `examples/ci/`:
|
|
392
|
+
|
|
393
|
+
| File | Platform |
|
|
394
|
+
|------|----------|
|
|
395
|
+
| `github-actions-mr-extract.yml` | GitHub Actions |
|
|
396
|
+
| `coding-ci-mr-extract.yaml` | Coding CI (TGit + ZhiYan QCI) |
|
|
397
|
+
|
|
398
|
+
### Reject Interaction
|
|
399
|
+
|
|
400
|
+
| Platform | How to reject | Default |
|
|
401
|
+
|----------|--------------|---------|
|
|
402
|
+
| GitHub | Add 👎 reaction to the suggestion comment | Write all |
|
|
403
|
+
| TGit | Add ☝️ emoji to the suggestion note | Write all |
|
|
404
|
+
|
|
364
405
|
## License
|
|
365
406
|
|
|
366
407
|
[MIT](LICENSE)
|
package/README.zh-CN.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/teamai-cli-logo.svg" alt="teamai-cli" width="430">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# TeamAI — The team harness for AI agents
|
|
2
6
|
|
|
3
7
|
> [English](README.md) | [简体中文](README.zh-CN.md)
|
|
@@ -89,6 +93,7 @@ CLI 会根据用户传入的 repo URL 自动选择 provider:
|
|
|
89
93
|
| `teamai domains drift [url] [--apply \| --lock \| --apply-all]` | 浏览并处理域漂移信号;`--apply` 把仓库重新归类到推荐域并刷新聚合视图 |
|
|
90
94
|
| `teamai digest` | 生成团队 AI 使用周报(skill 排行、新增/更新 skill、session 摘要) |
|
|
91
95
|
| `teamai hooks` | 管理 AI 工具 hooks(list / inject / remove) |
|
|
96
|
+
| `teamai ci extract-mr --url <url> [--mode comment\|write\|both] [--individual-comments]` | CI 流水线集成:从 MR/PR 中提取知识,发布为评论,合并后写入团队知识仓库。使用 `--individual-comments` 时每条建议单独发布,支持 reaction/reject 交互(GitHub 👎 / TGit ☝️) |
|
|
92
97
|
| `teamai uninstall [--force]` | 卸载 teamai:移除 hooks、rules、skills、env、docs、~/.teamai/ |
|
|
93
98
|
| `teamai doctor` | 诊断配置问题 |
|
|
94
99
|
|
|
@@ -361,6 +366,42 @@ npm update -g teamai-cli # 或手动触发 npm 升级
|
|
|
361
366
|
|
|
362
367
|
用户级 `updatePolicy` 始终优先于团队级 `autoUpdate`。
|
|
363
368
|
|
|
369
|
+
## CI 集成
|
|
370
|
+
|
|
371
|
+
TeamAI 可以集成到 CI 流水线中,从每次 MR/PR 自动提取知识:
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
MR 创建/更新 → CI 提取 learning + codebase 建议 → 以评论形式发布
|
|
375
|
+
→ Reviewer 拒绝不需要的建议(GitHub 👎 / TGit ☝️)
|
|
376
|
+
→ MR 合并 → CI 将已通过的条目写入团队知识仓库
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 快速开始
|
|
380
|
+
|
|
381
|
+
```bash
|
|
382
|
+
# Comment 模式:将建议发布到 MR(在 PR 打开/更新时运行)
|
|
383
|
+
teamai ci extract-mr --url "$MR_URL" --mode comment --individual-comments
|
|
384
|
+
|
|
385
|
+
# Write 模式:将已通过的条目写入知识仓库(在合并后运行)
|
|
386
|
+
teamai ci extract-mr --url "$MR_URL" --mode write --team-repo ./team-repo --individual-comments
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### CI 模板
|
|
390
|
+
|
|
391
|
+
`examples/ci/` 目录下提供了开箱即用的模板:
|
|
392
|
+
|
|
393
|
+
| 文件 | 平台 |
|
|
394
|
+
|------|------|
|
|
395
|
+
| `github-actions-mr-extract.yml` | GitHub Actions |
|
|
396
|
+
| `coding-ci-mr-extract.yaml` | Coding CI(TGit + 智研 QCI) |
|
|
397
|
+
|
|
398
|
+
### 拒绝交互
|
|
399
|
+
|
|
400
|
+
| 平台 | 拒绝方式 | 默认行为 |
|
|
401
|
+
|------|---------|---------|
|
|
402
|
+
| GitHub | 对建议评论添加 👎 reaction | 全部写入 |
|
|
403
|
+
| TGit | 对建议 note 添加 ☝️ emoji | 全部写入 |
|
|
404
|
+
|
|
364
405
|
## 许可证
|
|
365
406
|
|
|
366
407
|
[MIT](LICENSE)
|
package/dist/index.js
CHANGED
|
@@ -2354,6 +2354,13 @@ var init_repo_url2 = __esm({
|
|
|
2354
2354
|
});
|
|
2355
2355
|
|
|
2356
2356
|
// src/providers/github/index.ts
|
|
2357
|
+
var github_exports = {};
|
|
2358
|
+
__export(github_exports, {
|
|
2359
|
+
GitHubProvider: () => GitHubProvider,
|
|
2360
|
+
ghGetOAuthToken: () => ghGetOAuthToken,
|
|
2361
|
+
ghIsAuthenticated: () => ghIsAuthenticated,
|
|
2362
|
+
isGhInstalled: () => isGhInstalled
|
|
2363
|
+
});
|
|
2357
2364
|
var GitHubProvider;
|
|
2358
2365
|
var init_github = __esm({
|
|
2359
2366
|
"src/providers/github/index.ts"() {
|
|
@@ -4385,6 +4392,9 @@ var init_docs = __esm({
|
|
|
4385
4392
|
import path17 from "path";
|
|
4386
4393
|
import { z as z3 } from "zod";
|
|
4387
4394
|
import YAML5 from "yaml";
|
|
4395
|
+
function shellQuoteValue(value) {
|
|
4396
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
4397
|
+
}
|
|
4388
4398
|
var EnvVariableSchema, EnvYamlSchema, EnvHandler;
|
|
4389
4399
|
var init_env = __esm({
|
|
4390
4400
|
"src/resources/env.ts"() {
|
|
@@ -4518,9 +4528,15 @@ var init_env = __esm({
|
|
|
4518
4528
|
}
|
|
4519
4529
|
/**
|
|
4520
4530
|
* Generate the content of ~/.teamai/env.sh with export statements.
|
|
4531
|
+
*
|
|
4532
|
+
* Values are single-quoted so shell metacharacters in an env value (quotes,
|
|
4533
|
+
* `$`, backticks, `\`, …) are taken literally and cannot break or inject into
|
|
4534
|
+
* the sourced script. An embedded single quote is encoded with the standard
|
|
4535
|
+
* `'\''` sequence. env.sh is sourced from every team member's shell profile,
|
|
4536
|
+
* so values (which originate from the team repo's env/env.yaml) must be safe.
|
|
4521
4537
|
*/
|
|
4522
4538
|
generateEnvFile(variables) {
|
|
4523
|
-
const lines = variables.map((v) => `export ${v.key}
|
|
4539
|
+
const lines = variables.map((v) => `export ${v.key}=${shellQuoteValue(v.value)}`);
|
|
4524
4540
|
return lines.join("\n") + "\n";
|
|
4525
4541
|
}
|
|
4526
4542
|
/**
|
|
@@ -5773,8 +5789,8 @@ async function push(options) {
|
|
|
5773
5789
|
process.exitCode = 2;
|
|
5774
5790
|
return;
|
|
5775
5791
|
}
|
|
5776
|
-
const
|
|
5777
|
-
const skillPath = options.skill.startsWith("~") ? path24.join(
|
|
5792
|
+
const os7 = await import("os");
|
|
5793
|
+
const skillPath = options.skill.startsWith("~") ? path24.join(os7.homedir(), options.skill.slice(1)) : path24.resolve(options.skill);
|
|
5778
5794
|
let matchedItem;
|
|
5779
5795
|
for (const item of allItems) {
|
|
5780
5796
|
if (item.type !== "skills") continue;
|
|
@@ -5786,7 +5802,7 @@ async function push(options) {
|
|
|
5786
5802
|
matchedItem = item;
|
|
5787
5803
|
break;
|
|
5788
5804
|
}
|
|
5789
|
-
const skillInput = options.skill.replace(/^~/,
|
|
5805
|
+
const skillInput = options.skill.replace(/^~/, os7.homedir());
|
|
5790
5806
|
if (item.sourcePath.endsWith(skillInput) || item.sourcePath.includes(path24.sep + skillInput)) {
|
|
5791
5807
|
matchedItem = item;
|
|
5792
5808
|
break;
|
|
@@ -8811,6 +8827,20 @@ async function doctor(options) {
|
|
|
8811
8827
|
fix: "Run `teamai init` to authenticate via gf auth login"
|
|
8812
8828
|
}
|
|
8813
8829
|
);
|
|
8830
|
+
} else if (providerName === "github") {
|
|
8831
|
+
const { isGhInstalled: isGhInstalled2, ghIsAuthenticated: ghIsAuthenticated2 } = await Promise.resolve().then(() => (init_github(), github_exports));
|
|
8832
|
+
checks.push(
|
|
8833
|
+
{
|
|
8834
|
+
name: "gh CLI is installed",
|
|
8835
|
+
check: async () => isGhInstalled2(),
|
|
8836
|
+
fix: "Install from https://cli.github.com/ or run `brew install gh`"
|
|
8837
|
+
},
|
|
8838
|
+
{
|
|
8839
|
+
name: "gh CLI is authenticated",
|
|
8840
|
+
check: async () => ghIsAuthenticated2(),
|
|
8841
|
+
fix: "Run `gh auth login` to authenticate"
|
|
8842
|
+
}
|
|
8843
|
+
);
|
|
8814
8844
|
}
|
|
8815
8845
|
checks.push(
|
|
8816
8846
|
{
|
|
@@ -9739,7 +9769,7 @@ async function buildRemovalPlan(localConfig, teamConfig) {
|
|
|
9739
9769
|
if (toolPath.claudemd) {
|
|
9740
9770
|
const claudeMdPath = path38.join(baseDir, toolPath.claudemd);
|
|
9741
9771
|
const content = await readFileSafe(claudeMdPath);
|
|
9742
|
-
if (content && content.includes(
|
|
9772
|
+
if (content && CLAUDEMD_MARKER_PAIRS.some(([start]) => content.includes(start))) {
|
|
9743
9773
|
plan.claudeMdFiles.push(claudeMdPath);
|
|
9744
9774
|
}
|
|
9745
9775
|
}
|
|
@@ -9843,18 +9873,21 @@ async function executeRemoval(plan) {
|
|
|
9843
9873
|
}
|
|
9844
9874
|
for (const claudeMdPath of plan.claudeMdFiles) {
|
|
9845
9875
|
try {
|
|
9846
|
-
const
|
|
9847
|
-
if (!
|
|
9848
|
-
|
|
9849
|
-
const
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9876
|
+
const raw = await readFileSafe(claudeMdPath);
|
|
9877
|
+
if (!raw) continue;
|
|
9878
|
+
let content = raw;
|
|
9879
|
+
for (const [startMarker, endMarker] of CLAUDEMD_MARKER_PAIRS) {
|
|
9880
|
+
const startIdx = content.indexOf(startMarker);
|
|
9881
|
+
const endIdx = content.indexOf(endMarker);
|
|
9882
|
+
if (startIdx === -1 || endIdx === -1) continue;
|
|
9883
|
+
const before = content.substring(0, startIdx).replace(/\n+$/, "\n");
|
|
9884
|
+
const after = content.substring(endIdx + endMarker.length).replace(/^\n+/, "\n");
|
|
9885
|
+
content = (before + after).trim();
|
|
9886
|
+
}
|
|
9887
|
+
if (content.length === 0) {
|
|
9855
9888
|
await remove(claudeMdPath);
|
|
9856
9889
|
} else {
|
|
9857
|
-
await writeFile(claudeMdPath,
|
|
9890
|
+
await writeFile(claudeMdPath, content + "\n");
|
|
9858
9891
|
}
|
|
9859
9892
|
log.success(`\u6E05\u7406 CLAUDE.md: ${claudeMdPath}`);
|
|
9860
9893
|
} catch (e) {
|
|
@@ -9980,6 +10013,7 @@ async function uninstall(opts) {
|
|
|
9980
10013
|
}
|
|
9981
10014
|
}
|
|
9982
10015
|
}
|
|
10016
|
+
var CLAUDEMD_MARKER_PAIRS;
|
|
9983
10017
|
var init_uninstall = __esm({
|
|
9984
10018
|
"src/uninstall.ts"() {
|
|
9985
10019
|
"use strict";
|
|
@@ -9990,6 +10024,12 @@ var init_uninstall = __esm({
|
|
|
9990
10024
|
init_fs();
|
|
9991
10025
|
init_logger();
|
|
9992
10026
|
init_prompt();
|
|
10027
|
+
CLAUDEMD_MARKER_PAIRS = [
|
|
10028
|
+
[TEAMAI_RULES_START, TEAMAI_RULES_END],
|
|
10029
|
+
[TEAMAI_CULTURE_START, TEAMAI_CULTURE_END],
|
|
10030
|
+
[TEAMAI_CLAUDEMD_START, TEAMAI_CLAUDEMD_END],
|
|
10031
|
+
[TEAMAI_RECALL_RULES_START, TEAMAI_RECALL_RULES_END]
|
|
10032
|
+
];
|
|
9993
10033
|
}
|
|
9994
10034
|
});
|
|
9995
10035
|
|
|
@@ -10121,22 +10161,19 @@ __export(hooks_cmd_exports, {
|
|
|
10121
10161
|
hooksRemove: () => hooksRemove
|
|
10122
10162
|
});
|
|
10123
10163
|
import path40 from "path";
|
|
10124
|
-
|
|
10125
|
-
const
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
if (localConfig.scope === "project") {
|
|
10129
|
-
const userBaseDir = process.env.HOME ?? "";
|
|
10130
|
-
await injectHooksToAllTools(teamConfig.toolPaths, userBaseDir);
|
|
10164
|
+
function resolveHookBaseDirs(localConfig) {
|
|
10165
|
+
const baseDir = resolveBaseDir(localConfig) ?? "";
|
|
10166
|
+
if (localConfig.scope !== "project") {
|
|
10167
|
+
return [baseDir];
|
|
10131
10168
|
}
|
|
10132
|
-
|
|
10133
|
-
|
|
10169
|
+
const userBaseDir = process.env.HOME ?? "";
|
|
10170
|
+
if (!userBaseDir || userBaseDir === baseDir) {
|
|
10171
|
+
return [baseDir];
|
|
10134
10172
|
}
|
|
10173
|
+
return [baseDir, userBaseDir];
|
|
10135
10174
|
}
|
|
10136
|
-
async function
|
|
10137
|
-
const
|
|
10138
|
-
const baseDir = resolveBaseDir(localConfig);
|
|
10139
|
-
for (const [tool, paths] of Object.entries(teamConfig.toolPaths)) {
|
|
10175
|
+
async function removeHooksFromAllTools(toolPaths, baseDir) {
|
|
10176
|
+
for (const [tool, paths] of Object.entries(toolPaths)) {
|
|
10140
10177
|
if (paths.settings) {
|
|
10141
10178
|
const settingsPath = path40.join(baseDir, paths.settings);
|
|
10142
10179
|
try {
|
|
@@ -10146,6 +10183,21 @@ async function hooksRemove(_options) {
|
|
|
10146
10183
|
}
|
|
10147
10184
|
}
|
|
10148
10185
|
}
|
|
10186
|
+
}
|
|
10187
|
+
async function hooksInject(options) {
|
|
10188
|
+
const { localConfig, teamConfig } = await autoDetectInit();
|
|
10189
|
+
for (const baseDir of resolveHookBaseDirs(localConfig)) {
|
|
10190
|
+
await injectHooksToAllTools(teamConfig.toolPaths, baseDir);
|
|
10191
|
+
}
|
|
10192
|
+
if (!options.silent) {
|
|
10193
|
+
log.success("Hooks injected into all AI tool settings");
|
|
10194
|
+
}
|
|
10195
|
+
}
|
|
10196
|
+
async function hooksRemove(_options) {
|
|
10197
|
+
const { localConfig, teamConfig } = await autoDetectInit();
|
|
10198
|
+
for (const baseDir of resolveHookBaseDirs(localConfig)) {
|
|
10199
|
+
await removeHooksFromAllTools(teamConfig.toolPaths, baseDir);
|
|
10200
|
+
}
|
|
10149
10201
|
log.success("Hooks removed from all AI tool settings");
|
|
10150
10202
|
}
|
|
10151
10203
|
var init_hooks_cmd = __esm({
|
|
@@ -10544,6 +10596,27 @@ var init_digest = __esm({
|
|
|
10544
10596
|
}
|
|
10545
10597
|
});
|
|
10546
10598
|
|
|
10599
|
+
// src/utils/session-id.ts
|
|
10600
|
+
function deriveSessionId(data, options = {}) {
|
|
10601
|
+
if (typeof data.session_id === "string" && data.session_id) {
|
|
10602
|
+
return data.session_id;
|
|
10603
|
+
}
|
|
10604
|
+
if (process.env.CLAUDE_SESSION_ID) {
|
|
10605
|
+
return process.env.CLAUDE_SESSION_ID;
|
|
10606
|
+
}
|
|
10607
|
+
const ppid = process.ppid ?? process.pid;
|
|
10608
|
+
if (options.includeCwd) {
|
|
10609
|
+
const cwd = typeof data.cwd === "string" ? data.cwd : process.cwd();
|
|
10610
|
+
return `pid-${ppid}-${cwd}`;
|
|
10611
|
+
}
|
|
10612
|
+
return `pid-${ppid}`;
|
|
10613
|
+
}
|
|
10614
|
+
var init_session_id = __esm({
|
|
10615
|
+
"src/utils/session-id.ts"() {
|
|
10616
|
+
"use strict";
|
|
10617
|
+
}
|
|
10618
|
+
});
|
|
10619
|
+
|
|
10547
10620
|
// src/pid-monitor.ts
|
|
10548
10621
|
import fs11 from "fs";
|
|
10549
10622
|
import { execSync as execSync4 } from "child_process";
|
|
@@ -10684,17 +10757,6 @@ async function readLastAssistantOutput(transcriptPath) {
|
|
|
10684
10757
|
return "";
|
|
10685
10758
|
}
|
|
10686
10759
|
}
|
|
10687
|
-
function deriveSessionId(hookData) {
|
|
10688
|
-
if (typeof hookData.session_id === "string" && hookData.session_id) {
|
|
10689
|
-
return hookData.session_id;
|
|
10690
|
-
}
|
|
10691
|
-
if (process.env.CLAUDE_SESSION_ID) {
|
|
10692
|
-
return process.env.CLAUDE_SESSION_ID;
|
|
10693
|
-
}
|
|
10694
|
-
const cwd = typeof hookData.cwd === "string" ? hookData.cwd : process.cwd();
|
|
10695
|
-
const ppid = process.ppid ?? process.pid;
|
|
10696
|
-
return `pid-${ppid}-${cwd}`;
|
|
10697
|
-
}
|
|
10698
10760
|
function mapEventType(hookEventName) {
|
|
10699
10761
|
switch (hookEventName) {
|
|
10700
10762
|
case "SessionStart":
|
|
@@ -10729,7 +10791,7 @@ async function parseHookEvent(raw, tool) {
|
|
|
10729
10791
|
log.debug(`dashboard-collector: unknown hook event: ${hookEventName}`);
|
|
10730
10792
|
return null;
|
|
10731
10793
|
}
|
|
10732
|
-
const sessionId = deriveSessionId(hookData);
|
|
10794
|
+
const sessionId = deriveSessionId(hookData, { includeCwd: true });
|
|
10733
10795
|
const cwd = typeof hookData.cwd === "string" ? hookData.cwd : void 0;
|
|
10734
10796
|
const event = {
|
|
10735
10797
|
type: eventType,
|
|
@@ -10918,6 +10980,7 @@ var init_dashboard_collector = __esm({
|
|
|
10918
10980
|
"src/dashboard-collector.ts"() {
|
|
10919
10981
|
"use strict";
|
|
10920
10982
|
init_logger();
|
|
10983
|
+
init_session_id();
|
|
10921
10984
|
init_fs();
|
|
10922
10985
|
init_pid_monitor();
|
|
10923
10986
|
init_types();
|
|
@@ -11945,12 +12008,14 @@ var init_recall = __esm({
|
|
|
11945
12008
|
var auto_recall_exports = {};
|
|
11946
12009
|
__export(auto_recall_exports, {
|
|
11947
12010
|
autoRecall: () => autoRecall,
|
|
12011
|
+
autoRecallFromInput: () => autoRecallFromInput,
|
|
11948
12012
|
containsError: () => containsError,
|
|
11949
12013
|
extractGrepQuery: () => extractGrepQuery,
|
|
11950
12014
|
extractQuery: () => extractQuery,
|
|
11951
12015
|
extractWebFetchQuery: () => extractWebFetchQuery,
|
|
11952
12016
|
extractWebSearchQuery: () => extractWebSearchQuery,
|
|
11953
12017
|
isReadOnlyCommand: () => isReadOnlyCommand,
|
|
12018
|
+
parseHookInput: () => parseHookInput,
|
|
11954
12019
|
readRecallQuality: () => readRecallQuality,
|
|
11955
12020
|
readStdin: () => readStdin3,
|
|
11956
12021
|
shouldSkipQuery: () => shouldSkipQuery
|
|
@@ -12101,6 +12166,18 @@ function shouldSkipQuery(sessionId, query) {
|
|
|
12101
12166
|
writeCache(sessionId, updated);
|
|
12102
12167
|
return false;
|
|
12103
12168
|
}
|
|
12169
|
+
function parseHookInput(data) {
|
|
12170
|
+
const toolName = typeof data.tool_name === "string" ? data.tool_name : "";
|
|
12171
|
+
if (!toolName) return null;
|
|
12172
|
+
const rawInput = data.tool_input;
|
|
12173
|
+
const toolInput = rawInput !== null && typeof rawInput === "object" && !Array.isArray(rawInput) ? rawInput : {};
|
|
12174
|
+
const toolResponse = data.tool_response;
|
|
12175
|
+
const toolOutput = typeof data.tool_output === "string" ? data.tool_output : typeof data.tool_result === "string" ? data.tool_result : toolResponse ? [
|
|
12176
|
+
typeof toolResponse.stdout === "string" ? toolResponse.stdout : "",
|
|
12177
|
+
typeof toolResponse.stderr === "string" ? toolResponse.stderr : ""
|
|
12178
|
+
].filter(Boolean).join("\n") : "";
|
|
12179
|
+
return { toolName, toolInput, toolOutput, sessionId: deriveSessionId(data) };
|
|
12180
|
+
}
|
|
12104
12181
|
async function readStdin3() {
|
|
12105
12182
|
if (process.stdin.isTTY) return null;
|
|
12106
12183
|
const chunks = [];
|
|
@@ -12111,44 +12188,30 @@ async function readStdin3() {
|
|
|
12111
12188
|
if (!raw.trim()) return null;
|
|
12112
12189
|
try {
|
|
12113
12190
|
const data = JSON.parse(raw);
|
|
12114
|
-
|
|
12115
|
-
const rawInput = data.tool_input;
|
|
12116
|
-
const toolInput = rawInput !== null && typeof rawInput === "object" && !Array.isArray(rawInput) ? rawInput : {};
|
|
12117
|
-
const toolResponse = data.tool_response;
|
|
12118
|
-
const toolOutput = typeof data.tool_output === "string" ? data.tool_output : typeof data.tool_result === "string" ? data.tool_result : toolResponse ? [
|
|
12119
|
-
typeof toolResponse.stdout === "string" ? toolResponse.stdout : "",
|
|
12120
|
-
typeof toolResponse.stderr === "string" ? toolResponse.stderr : ""
|
|
12121
|
-
].filter(Boolean).join("\n") : "";
|
|
12122
|
-
const sessionId = typeof data.session_id === "string" && data.session_id || process.env.CLAUDE_SESSION_ID || `pid-${process.ppid ?? process.pid}`;
|
|
12123
|
-
return { toolName, toolInput, toolOutput, sessionId };
|
|
12191
|
+
return parseHookInput(data);
|
|
12124
12192
|
} catch {
|
|
12125
12193
|
return null;
|
|
12126
12194
|
}
|
|
12127
12195
|
}
|
|
12128
|
-
async function
|
|
12196
|
+
async function autoRecallFromInput(input) {
|
|
12129
12197
|
if (process.env.TEAMAI_RECALL_DISABLED === "1") {
|
|
12130
|
-
return;
|
|
12131
|
-
}
|
|
12132
|
-
const input = await readStdin3();
|
|
12133
|
-
if (!input) {
|
|
12134
|
-
log.debug("auto-recall: no STDIN data");
|
|
12135
|
-
return;
|
|
12198
|
+
return null;
|
|
12136
12199
|
}
|
|
12137
12200
|
const { toolName, toolInput, toolOutput, sessionId } = input;
|
|
12138
12201
|
if (!RECALL_TOOLS.has(toolName)) {
|
|
12139
|
-
return;
|
|
12202
|
+
return null;
|
|
12140
12203
|
}
|
|
12141
12204
|
let query = "";
|
|
12142
12205
|
if (toolName === "Bash") {
|
|
12143
12206
|
const command = typeof toolInput.command === "string" ? toolInput.command : "";
|
|
12144
12207
|
if (isReadOnlyCommand(command)) {
|
|
12145
|
-
return;
|
|
12208
|
+
return null;
|
|
12146
12209
|
}
|
|
12147
12210
|
if (toolOutput.includes("[teamai:")) {
|
|
12148
|
-
return;
|
|
12211
|
+
return null;
|
|
12149
12212
|
}
|
|
12150
12213
|
if (!containsError(toolOutput)) {
|
|
12151
|
-
return;
|
|
12214
|
+
return null;
|
|
12152
12215
|
}
|
|
12153
12216
|
query = extractQuery(toolOutput);
|
|
12154
12217
|
} else if (toolName === "Grep") {
|
|
@@ -12160,11 +12223,11 @@ async function autoRecall() {
|
|
|
12160
12223
|
}
|
|
12161
12224
|
if (!query) {
|
|
12162
12225
|
log.debug(`auto-recall: no query extracted from ${toolName}`);
|
|
12163
|
-
return;
|
|
12226
|
+
return null;
|
|
12164
12227
|
}
|
|
12165
12228
|
if (shouldSkipQuery(sessionId, query)) {
|
|
12166
12229
|
log.debug(`auto-recall: skipping duplicate/rate-limited query: ${query.slice(0, 50)}`);
|
|
12167
|
-
return;
|
|
12230
|
+
return null;
|
|
12168
12231
|
}
|
|
12169
12232
|
const { loadIndex: loadIndex2, search: search2 } = await Promise.resolve().then(() => (init_search_index(), search_index_exports));
|
|
12170
12233
|
const { formatResults: formatResults2 } = await Promise.resolve().then(() => (init_recall(), recall_exports));
|
|
@@ -12188,7 +12251,7 @@ async function autoRecall() {
|
|
|
12188
12251
|
missCount: 0
|
|
12189
12252
|
};
|
|
12190
12253
|
writeCache(sessionId, { ...cache, missCount: cache.missCount + 1, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
12191
|
-
return;
|
|
12254
|
+
return null;
|
|
12192
12255
|
}
|
|
12193
12256
|
const searchStart = Date.now();
|
|
12194
12257
|
const results = search2(query, index, 3);
|
|
@@ -12232,7 +12295,7 @@ async function autoRecall() {
|
|
|
12232
12295
|
}
|
|
12233
12296
|
if (results.length === 0) {
|
|
12234
12297
|
log.debug(`auto-recall: no results for query: ${query.slice(0, 50)}`);
|
|
12235
|
-
return;
|
|
12298
|
+
return null;
|
|
12236
12299
|
}
|
|
12237
12300
|
const titles = results.map((r) => r.entry.title).join(", ");
|
|
12238
12301
|
log.debug(`auto-recall: [${toolName}] query="${query.slice(0, 60)}" \u2192 ${results.length} results: ${titles}`);
|
|
@@ -12248,7 +12311,6 @@ async function autoRecall() {
|
|
|
12248
12311
|
additionalContext: context
|
|
12249
12312
|
}
|
|
12250
12313
|
});
|
|
12251
|
-
process.stdout.write(hookOutput + "\n");
|
|
12252
12314
|
try {
|
|
12253
12315
|
const { autoUpvote: autoUpvote2 } = await Promise.resolve().then(() => (init_recall(), recall_exports));
|
|
12254
12316
|
const { requireInit: requireInit3 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
@@ -12256,6 +12318,21 @@ async function autoRecall() {
|
|
|
12256
12318
|
await autoUpvote2(results, localConfig.username, localConfig.repo.localPath);
|
|
12257
12319
|
} catch {
|
|
12258
12320
|
}
|
|
12321
|
+
return hookOutput;
|
|
12322
|
+
}
|
|
12323
|
+
async function autoRecall() {
|
|
12324
|
+
if (process.env.TEAMAI_RECALL_DISABLED === "1") {
|
|
12325
|
+
return;
|
|
12326
|
+
}
|
|
12327
|
+
const input = await readStdin3();
|
|
12328
|
+
if (!input) {
|
|
12329
|
+
log.debug("auto-recall: no STDIN data");
|
|
12330
|
+
return;
|
|
12331
|
+
}
|
|
12332
|
+
const output = await autoRecallFromInput(input);
|
|
12333
|
+
if (output) {
|
|
12334
|
+
process.stdout.write(output + "\n");
|
|
12335
|
+
}
|
|
12259
12336
|
}
|
|
12260
12337
|
function readRecallQuality(sessionId) {
|
|
12261
12338
|
const cache = readCache(sessionId);
|
|
@@ -12273,6 +12350,7 @@ var init_auto_recall = __esm({
|
|
|
12273
12350
|
"use strict";
|
|
12274
12351
|
init_logger();
|
|
12275
12352
|
init_types();
|
|
12353
|
+
init_session_id();
|
|
12276
12354
|
RECALL_TOOLS = /* @__PURE__ */ new Set(["Bash", "Grep", "WebSearch", "WebFetch"]);
|
|
12277
12355
|
READ_ONLY_COMMANDS = ["cat", "head", "tail", "less", "more", "bat", "batcat"];
|
|
12278
12356
|
ERROR_PATTERNS = [
|
|
@@ -12540,7 +12618,7 @@ async function readStdinAndDeriveSession() {
|
|
|
12540
12618
|
if (!raw.trim()) return null;
|
|
12541
12619
|
try {
|
|
12542
12620
|
const hookData = JSON.parse(raw);
|
|
12543
|
-
const sessionId =
|
|
12621
|
+
const sessionId = deriveSessionId(hookData, { includeCwd: true });
|
|
12544
12622
|
const cwd = typeof hookData.cwd === "string" ? hookData.cwd : void 0;
|
|
12545
12623
|
return { sessionId, cwd };
|
|
12546
12624
|
} catch {
|
|
@@ -12664,6 +12742,7 @@ var init_contribute_check = __esm({
|
|
|
12664
12742
|
init_fs();
|
|
12665
12743
|
init_dashboard_collector();
|
|
12666
12744
|
init_auto_recall();
|
|
12745
|
+
init_session_id();
|
|
12667
12746
|
init_types();
|
|
12668
12747
|
STALE_SESSION_MS = 24 * 60 * 60 * 1e3;
|
|
12669
12748
|
}
|
|
@@ -12830,8 +12909,7 @@ async function readStdin4() {
|
|
|
12830
12909
|
try {
|
|
12831
12910
|
const data = JSON.parse(raw);
|
|
12832
12911
|
const toolName = typeof data.tool_name === "string" ? data.tool_name : "";
|
|
12833
|
-
|
|
12834
|
-
return { toolName, sessionId };
|
|
12912
|
+
return { toolName, sessionId: deriveSessionId(data) };
|
|
12835
12913
|
} catch {
|
|
12836
12914
|
return null;
|
|
12837
12915
|
}
|
|
@@ -12873,6 +12951,7 @@ var init_todowrite_hint = __esm({
|
|
|
12873
12951
|
"src/todowrite-hint.ts"() {
|
|
12874
12952
|
"use strict";
|
|
12875
12953
|
init_logger();
|
|
12954
|
+
init_session_id();
|
|
12876
12955
|
CACHE_TTL_MS3 = 24 * 60 * 60 * 1e3;
|
|
12877
12956
|
}
|
|
12878
12957
|
});
|
|
@@ -14126,7 +14205,7 @@ function parseGitHubPRUrl(url) {
|
|
|
14126
14205
|
}
|
|
14127
14206
|
return { owner: match[1], repo: match[2], number: match[3] };
|
|
14128
14207
|
}
|
|
14129
|
-
async function githubApiGet(
|
|
14208
|
+
async function githubApiGet(path69) {
|
|
14130
14209
|
return new Promise((resolve, reject) => {
|
|
14131
14210
|
const token = process.env["GITHUB_TOKEN"];
|
|
14132
14211
|
const headers = {
|
|
@@ -14135,7 +14214,7 @@ async function githubApiGet(path68) {
|
|
|
14135
14214
|
};
|
|
14136
14215
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
14137
14216
|
const req = https2.request(
|
|
14138
|
-
{ hostname: "api.github.com", path:
|
|
14217
|
+
{ hostname: "api.github.com", path: path69, headers },
|
|
14139
14218
|
(res) => {
|
|
14140
14219
|
const chunks = [];
|
|
14141
14220
|
res.on("data", (c) => chunks.push(c));
|
|
@@ -14254,12 +14333,16 @@ async function fetchTGitMRViaApi(group, project, mrIid) {
|
|
|
14254
14333
|
const encodedPath = encodeURIComponent(`${group}/${project}`);
|
|
14255
14334
|
const baseUrl = `https://git.woa.com/api/v3/projects/${encodedPath}`;
|
|
14256
14335
|
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
14257
|
-
const
|
|
14258
|
-
if (!
|
|
14259
|
-
throw new Error(`TGit API \u8FD4\u56DE\u9519\u8BEF ${
|
|
14336
|
+
const listResp = await fetch(`${baseUrl}/merge_requests?iid=${mrIid}`, { headers });
|
|
14337
|
+
if (!listResp.ok) {
|
|
14338
|
+
throw new Error(`TGit API \u8FD4\u56DE\u9519\u8BEF ${listResp.status}\uFF1A${await listResp.text()}`);
|
|
14339
|
+
}
|
|
14340
|
+
const mrList = await listResp.json();
|
|
14341
|
+
const mr = mrList.find((m) => String(m.id) !== void 0);
|
|
14342
|
+
if (!mr) {
|
|
14343
|
+
throw new Error(`TGit MR !${mrIid} \u4E0D\u5B58\u5728`);
|
|
14260
14344
|
}
|
|
14261
|
-
const
|
|
14262
|
-
const diffResp = await fetch(`${baseUrl}/merge_requests/${mrIid}/changes`, { headers });
|
|
14345
|
+
const diffResp = await fetch(`${baseUrl}/merge_requests/${mr.id}/changes`, { headers });
|
|
14263
14346
|
let diff = "";
|
|
14264
14347
|
if (diffResp.ok) {
|
|
14265
14348
|
const diffData = await diffResp.json();
|
|
@@ -14671,6 +14754,10 @@ async function importFromMR(opts) {
|
|
|
14671
14754
|
aiSpinner.fail("AI \u5206\u6790\u5931\u8D25");
|
|
14672
14755
|
throw err;
|
|
14673
14756
|
}
|
|
14757
|
+
const frontmatterStart = learningContent.indexOf("---");
|
|
14758
|
+
if (frontmatterStart > 0) {
|
|
14759
|
+
learningContent = learningContent.slice(frontmatterStart);
|
|
14760
|
+
}
|
|
14674
14761
|
const parsed = matter6(learningContent);
|
|
14675
14762
|
const learningTitle = parsed.data["title"] ?? mr.title;
|
|
14676
14763
|
const draftKeywords = extractKeywords(learningContent);
|
|
@@ -19615,6 +19702,7 @@ var PULL_TIMEOUT_MS, UPDATE_TIMEOUT_MS, TRACK_TIMEOUT_MS, DASHBOARD_TIMEOUT_MS,
|
|
|
19615
19702
|
var init_hook_handlers = __esm({
|
|
19616
19703
|
"src/hook-handlers.ts"() {
|
|
19617
19704
|
"use strict";
|
|
19705
|
+
init_session_id();
|
|
19618
19706
|
PULL_TIMEOUT_MS = 6e4;
|
|
19619
19707
|
UPDATE_TIMEOUT_MS = 1e4;
|
|
19620
19708
|
TRACK_TIMEOUT_MS = 5e3;
|
|
@@ -19712,23 +19800,10 @@ var init_hook_handlers = __esm({
|
|
|
19712
19800
|
autoRecallHandler = {
|
|
19713
19801
|
name: "auto-recall",
|
|
19714
19802
|
async execute(stdin, _tool) {
|
|
19715
|
-
const {
|
|
19716
|
-
|
|
19717
|
-
|
|
19718
|
-
|
|
19719
|
-
if (typeof chunk === "string") {
|
|
19720
|
-
capturedOutput = chunk;
|
|
19721
|
-
} else if (Buffer.isBuffer(chunk)) {
|
|
19722
|
-
capturedOutput = chunk.toString();
|
|
19723
|
-
}
|
|
19724
|
-
return true;
|
|
19725
|
-
});
|
|
19726
|
-
try {
|
|
19727
|
-
await autoRecall2();
|
|
19728
|
-
} finally {
|
|
19729
|
-
process.stdout.write = originalWrite;
|
|
19730
|
-
}
|
|
19731
|
-
return capturedOutput;
|
|
19803
|
+
const { autoRecallFromInput: autoRecallFromInput2, parseHookInput: parseHookInput2 } = await Promise.resolve().then(() => (init_auto_recall(), auto_recall_exports));
|
|
19804
|
+
const input = parseHookInput2(stdin);
|
|
19805
|
+
if (!input) return null;
|
|
19806
|
+
return autoRecallFromInput2(input);
|
|
19732
19807
|
}
|
|
19733
19808
|
};
|
|
19734
19809
|
todowriteHintHandler = {
|
|
@@ -19738,8 +19813,7 @@ var init_hook_handlers = __esm({
|
|
|
19738
19813
|
const toolName = typeof stdin.tool_name === "string" ? stdin.tool_name : "";
|
|
19739
19814
|
if (toolName !== "TodoWrite") return null;
|
|
19740
19815
|
const { shouldSkipTodoWriteHint: shouldSkipTodoWriteHint2, buildHintMessage: buildHintMessage3 } = await Promise.resolve().then(() => (init_todowrite_hint(), todowrite_hint_exports));
|
|
19741
|
-
|
|
19742
|
-
if (shouldSkipTodoWriteHint2(sessionId)) return null;
|
|
19816
|
+
if (shouldSkipTodoWriteHint2(deriveSessionId(stdin))) return null;
|
|
19743
19817
|
return JSON.stringify({
|
|
19744
19818
|
hookSpecificOutput: {
|
|
19745
19819
|
hookEventName: "PostToolUse",
|
|
@@ -19801,6 +19875,672 @@ var init_hook_dispatch_cli = __esm({
|
|
|
19801
19875
|
}
|
|
19802
19876
|
});
|
|
19803
19877
|
|
|
19878
|
+
// src/ci/mr-comment.ts
|
|
19879
|
+
function parseMrUrl(url) {
|
|
19880
|
+
const ghMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
19881
|
+
if (ghMatch) {
|
|
19882
|
+
return { provider: "github", owner: ghMatch[1], repo: ghMatch[2], number: ghMatch[3] };
|
|
19883
|
+
}
|
|
19884
|
+
const tgitMatch = url.match(/git\.woa\.com\/(.+)\/([^/]+)\/merge_requests\/(\d+)/);
|
|
19885
|
+
if (tgitMatch) {
|
|
19886
|
+
return { provider: "tgit", owner: tgitMatch[1], repo: tgitMatch[2], number: tgitMatch[3] };
|
|
19887
|
+
}
|
|
19888
|
+
throw new Error(`\u65E0\u6CD5\u89E3\u6790 MR URL: ${url}\uFF0C\u4EC5\u652F\u6301 GitHub \u548C TGit`);
|
|
19889
|
+
}
|
|
19890
|
+
function formatComment(learning, suggestions, marker) {
|
|
19891
|
+
const lines = [marker, "", "## TeamAI \u77E5\u8BC6\u63D0\u70BC", ""];
|
|
19892
|
+
if (learning) {
|
|
19893
|
+
lines.push("### Learning", "");
|
|
19894
|
+
lines.push(`**${learning.title}**`, "");
|
|
19895
|
+
lines.push("<details>", "<summary>\u5C55\u5F00\u5B8C\u6574\u5185\u5BB9</summary>", "");
|
|
19896
|
+
lines.push("```markdown");
|
|
19897
|
+
lines.push(learning.content);
|
|
19898
|
+
lines.push("```");
|
|
19899
|
+
lines.push("", "</details>", "");
|
|
19900
|
+
}
|
|
19901
|
+
if (suggestions && suggestions.length > 0) {
|
|
19902
|
+
lines.push("### Codebase.md \u66F4\u65B0\u5EFA\u8BAE", "");
|
|
19903
|
+
lines.push("| \u7AE0\u8282 | \u64CD\u4F5C | \u5185\u5BB9\u9884\u89C8 |");
|
|
19904
|
+
lines.push("|------|------|---------|");
|
|
19905
|
+
for (const s of suggestions) {
|
|
19906
|
+
const preview = s.content.replace(/\n/g, " ").slice(0, 80);
|
|
19907
|
+
lines.push(`| ${s.section} | ${s.action} | ${preview} |`);
|
|
19908
|
+
}
|
|
19909
|
+
lines.push("");
|
|
19910
|
+
}
|
|
19911
|
+
if (!learning && (!suggestions || suggestions.length === 0)) {
|
|
19912
|
+
lines.push("> \u672C\u6B21 MR \u65E0\u53EF\u63D0\u70BC\u7684\u77E5\u8BC6\u5185\u5BB9\u3002", "");
|
|
19913
|
+
}
|
|
19914
|
+
lines.push("---");
|
|
19915
|
+
lines.push("> MR \u5408\u5165\u540E\u5C06\u81EA\u52A8\u5E94\u7528\u4EE5\u4E0A\u5EFA\u8BAE\u5230\u56E2\u961F\u77E5\u8BC6\u5E93\u3002");
|
|
19916
|
+
lines.push("> _Auto-generated by `teamai ci extract-mr`_");
|
|
19917
|
+
return lines.join("\n");
|
|
19918
|
+
}
|
|
19919
|
+
async function githubRequest(path69, method, body) {
|
|
19920
|
+
const token = process.env["GITHUB_TOKEN"];
|
|
19921
|
+
if (!token) throw new Error("\u672A\u8BBE\u7F6E GITHUB_TOKEN \u73AF\u5883\u53D8\u91CF");
|
|
19922
|
+
const url = `https://api.github.com${path69}`;
|
|
19923
|
+
const headers = {
|
|
19924
|
+
Authorization: `Bearer ${token}`,
|
|
19925
|
+
Accept: "application/vnd.github+json",
|
|
19926
|
+
"User-Agent": "teamai-cli"
|
|
19927
|
+
};
|
|
19928
|
+
if (body) headers["Content-Type"] = "application/json";
|
|
19929
|
+
return fetch(url, {
|
|
19930
|
+
method,
|
|
19931
|
+
headers,
|
|
19932
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
19933
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
19934
|
+
});
|
|
19935
|
+
}
|
|
19936
|
+
async function findGitHubComment(owner, repo, prNumber, marker) {
|
|
19937
|
+
const resp = await githubRequest(
|
|
19938
|
+
`/repos/${owner}/${repo}/issues/${prNumber}/comments?per_page=100`,
|
|
19939
|
+
"GET"
|
|
19940
|
+
);
|
|
19941
|
+
if (!resp.ok) return null;
|
|
19942
|
+
const comments = await resp.json();
|
|
19943
|
+
return comments.find((c) => c.body.includes(marker)) ?? null;
|
|
19944
|
+
}
|
|
19945
|
+
async function postGitHubComment(owner, repo, prNumber, body) {
|
|
19946
|
+
const resp = await githubRequest(
|
|
19947
|
+
`/repos/${owner}/${repo}/issues/${prNumber}/comments`,
|
|
19948
|
+
"POST",
|
|
19949
|
+
{ body }
|
|
19950
|
+
);
|
|
19951
|
+
if (!resp.ok) {
|
|
19952
|
+
const errText = await resp.text();
|
|
19953
|
+
throw new Error(`GitHub comment \u521B\u5EFA\u5931\u8D25 (${resp.status}): ${errText}`);
|
|
19954
|
+
}
|
|
19955
|
+
const data = await resp.json();
|
|
19956
|
+
return { created: true, url: data.html_url };
|
|
19957
|
+
}
|
|
19958
|
+
async function updateGitHubComment(owner, repo, commentId, body) {
|
|
19959
|
+
const resp = await githubRequest(
|
|
19960
|
+
`/repos/${owner}/${repo}/issues/comments/${commentId}`,
|
|
19961
|
+
"PATCH",
|
|
19962
|
+
{ body }
|
|
19963
|
+
);
|
|
19964
|
+
if (!resp.ok) {
|
|
19965
|
+
const errText = await resp.text();
|
|
19966
|
+
throw new Error(`GitHub comment \u66F4\u65B0\u5931\u8D25 (${resp.status}): ${errText}`);
|
|
19967
|
+
}
|
|
19968
|
+
const data = await resp.json();
|
|
19969
|
+
return { created: false, url: data.html_url };
|
|
19970
|
+
}
|
|
19971
|
+
function getTGitToken() {
|
|
19972
|
+
const envToken = process.env["TAI_PAT_TOKEN"];
|
|
19973
|
+
if (envToken) return envToken;
|
|
19974
|
+
const oauthToken = gfGetOAuthToken();
|
|
19975
|
+
if (oauthToken) return oauthToken;
|
|
19976
|
+
throw new Error("\u672A\u8BBE\u7F6E TAI_PAT_TOKEN \u73AF\u5883\u53D8\u91CF\uFF0C\u4E14\u65E0\u6CD5\u4ECE gf credential \u83B7\u53D6 token");
|
|
19977
|
+
}
|
|
19978
|
+
async function tgitRequest(path69, method, body) {
|
|
19979
|
+
const token = getTGitToken();
|
|
19980
|
+
const url = `https://git.woa.com/api/v3${path69}`;
|
|
19981
|
+
const headers = {
|
|
19982
|
+
Authorization: `Bearer ${token}`,
|
|
19983
|
+
"Content-Type": "application/json"
|
|
19984
|
+
};
|
|
19985
|
+
return fetch(url, {
|
|
19986
|
+
method,
|
|
19987
|
+
headers,
|
|
19988
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
19989
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
19990
|
+
});
|
|
19991
|
+
}
|
|
19992
|
+
async function getMrGlobalId(projectId, mrIid) {
|
|
19993
|
+
const resp = await tgitRequest(
|
|
19994
|
+
`/projects/${projectId}/merge_requests?iid=${mrIid}`,
|
|
19995
|
+
"GET"
|
|
19996
|
+
);
|
|
19997
|
+
if (!resp.ok) {
|
|
19998
|
+
throw new Error(`TGit \u67E5\u8BE2 MR \u5931\u8D25 (${resp.status})`);
|
|
19999
|
+
}
|
|
20000
|
+
const mrs = await resp.json();
|
|
20001
|
+
const mr = mrs.find((m) => String(m.iid) === mrIid);
|
|
20002
|
+
if (!mr) {
|
|
20003
|
+
throw new Error(`TGit MR !${mrIid} \u4E0D\u5B58\u5728`);
|
|
20004
|
+
}
|
|
20005
|
+
return mr.id;
|
|
20006
|
+
}
|
|
20007
|
+
async function findTGitComment(projectId, mrGlobalId, marker) {
|
|
20008
|
+
const resp = await tgitRequest(
|
|
20009
|
+
`/projects/${projectId}/merge_requests/${mrGlobalId}/notes?per_page=100`,
|
|
20010
|
+
"GET"
|
|
20011
|
+
);
|
|
20012
|
+
if (!resp.ok) return null;
|
|
20013
|
+
const notes = await resp.json();
|
|
20014
|
+
return notes.find((n) => n.body.includes(marker)) ?? null;
|
|
20015
|
+
}
|
|
20016
|
+
async function postTGitComment(projectId, mrGlobalId, body) {
|
|
20017
|
+
const resp = await tgitRequest(
|
|
20018
|
+
`/projects/${projectId}/merge_requests/${mrGlobalId}/notes`,
|
|
20019
|
+
"POST",
|
|
20020
|
+
{ body }
|
|
20021
|
+
);
|
|
20022
|
+
if (!resp.ok) {
|
|
20023
|
+
const errText = await resp.text();
|
|
20024
|
+
throw new Error(`TGit comment \u521B\u5EFA\u5931\u8D25 (${resp.status}): ${errText}`);
|
|
20025
|
+
}
|
|
20026
|
+
return { created: true };
|
|
20027
|
+
}
|
|
20028
|
+
async function updateTGitComment(projectId, mrGlobalId, noteId, body) {
|
|
20029
|
+
const resp = await tgitRequest(
|
|
20030
|
+
`/projects/${projectId}/merge_requests/${mrGlobalId}/notes/${noteId}`,
|
|
20031
|
+
"PUT",
|
|
20032
|
+
{ body }
|
|
20033
|
+
);
|
|
20034
|
+
if (!resp.ok) {
|
|
20035
|
+
const errText = await resp.text();
|
|
20036
|
+
throw new Error(`TGit comment \u66F4\u65B0\u5931\u8D25 (${resp.status}): ${errText}`);
|
|
20037
|
+
}
|
|
20038
|
+
return { created: false };
|
|
20039
|
+
}
|
|
20040
|
+
async function postOrUpdateMrComment(mrUrl, learning, suggestions, marker, dryRun) {
|
|
20041
|
+
const effectiveMarker = marker ?? DEFAULT_MARKER;
|
|
20042
|
+
const body = formatComment(learning, suggestions, effectiveMarker);
|
|
20043
|
+
const parsed = parseMrUrl(mrUrl);
|
|
20044
|
+
if (dryRun) {
|
|
20045
|
+
log.info("dry-run: \u4EE5\u4E0B\u4E3A\u5C06\u53D1\u5E03\u7684 comment \u5185\u5BB9:");
|
|
20046
|
+
console.log(body);
|
|
20047
|
+
return { created: true };
|
|
20048
|
+
}
|
|
20049
|
+
if (parsed.provider === "github") {
|
|
20050
|
+
const existing2 = await findGitHubComment(
|
|
20051
|
+
parsed.owner,
|
|
20052
|
+
parsed.repo,
|
|
20053
|
+
parsed.number,
|
|
20054
|
+
effectiveMarker
|
|
20055
|
+
);
|
|
20056
|
+
if (existing2) {
|
|
20057
|
+
log.debug(`\u53D1\u73B0\u5DF2\u6709 comment #${existing2.id}\uFF0C\u66F4\u65B0\u4E2D...`);
|
|
20058
|
+
return updateGitHubComment(parsed.owner, parsed.repo, existing2.id, body);
|
|
20059
|
+
}
|
|
20060
|
+
return postGitHubComment(parsed.owner, parsed.repo, parsed.number, body);
|
|
20061
|
+
}
|
|
20062
|
+
const projectId = encodeURIComponent(`${parsed.owner}/${parsed.repo}`);
|
|
20063
|
+
const mrGlobalId = await getMrGlobalId(projectId, parsed.number);
|
|
20064
|
+
const existing = await findTGitComment(projectId, mrGlobalId, effectiveMarker);
|
|
20065
|
+
if (existing) {
|
|
20066
|
+
log.debug(`\u53D1\u73B0\u5DF2\u6709 note #${existing.id}\uFF0C\u66F4\u65B0\u4E2D...`);
|
|
20067
|
+
return updateTGitComment(projectId, mrGlobalId, existing.id, body);
|
|
20068
|
+
}
|
|
20069
|
+
return postTGitComment(projectId, mrGlobalId, body);
|
|
20070
|
+
}
|
|
20071
|
+
function formatIndividualComment(type, index, content, provider) {
|
|
20072
|
+
const markerId = type === "learning" ? "learning" : `suggestion:${index}`;
|
|
20073
|
+
const marker = `<!-- teamai:ci-extract:${markerId} -->`;
|
|
20074
|
+
const lines = [marker, ""];
|
|
20075
|
+
if (type === "learning") {
|
|
20076
|
+
lines.push(`## TeamAI Learning`, "");
|
|
20077
|
+
lines.push(`**${content.title}**`, "");
|
|
20078
|
+
if (content.body) {
|
|
20079
|
+
lines.push("<details>", "<summary>\u5C55\u5F00\u5B8C\u6574\u5185\u5BB9</summary>", "");
|
|
20080
|
+
lines.push("```markdown");
|
|
20081
|
+
lines.push(content.body);
|
|
20082
|
+
lines.push("```");
|
|
20083
|
+
lines.push("", "</details>");
|
|
20084
|
+
}
|
|
20085
|
+
} else {
|
|
20086
|
+
lines.push(`## TeamAI Codebase \u5EFA\u8BAE #${index}`, "");
|
|
20087
|
+
lines.push(`**\u7AE0\u8282**: ${content.section} | **\u64CD\u4F5C**: ${content.action}`, "");
|
|
20088
|
+
if (content.preview) {
|
|
20089
|
+
lines.push("```");
|
|
20090
|
+
lines.push(content.preview.slice(0, 300));
|
|
20091
|
+
lines.push("```");
|
|
20092
|
+
}
|
|
20093
|
+
}
|
|
20094
|
+
lines.push("");
|
|
20095
|
+
if (provider === "github") {
|
|
20096
|
+
lines.push("> \u{1F4A1} \u4E0D\u5199\u5165\u6B64\u6761\uFF1F\u8BF7\u5BF9\u672C\u6761 comment \u52A0 \u{1F44E} reaction");
|
|
20097
|
+
} else {
|
|
20098
|
+
lines.push("> \u{1F4A1} \u4E0D\u5199\u5165\u6B64\u6761\uFF1F\u8BF7\u5BF9\u672C\u6761\u8BC4\u8BBA\u52A0 \u261D\uFE0F emoji reaction");
|
|
20099
|
+
}
|
|
20100
|
+
lines.push("");
|
|
20101
|
+
lines.push("_Auto-generated by `teamai ci extract-mr`_");
|
|
20102
|
+
return lines.join("\n");
|
|
20103
|
+
}
|
|
20104
|
+
async function postIndividualComments(mrUrl, learning, suggestions, dryRun) {
|
|
20105
|
+
const parsed = parseMrUrl(mrUrl);
|
|
20106
|
+
let posted = 0;
|
|
20107
|
+
const items = [];
|
|
20108
|
+
if (learning) {
|
|
20109
|
+
const body = formatIndividualComment(
|
|
20110
|
+
"learning",
|
|
20111
|
+
0,
|
|
20112
|
+
{ title: learning.title, body: learning.content },
|
|
20113
|
+
parsed.provider
|
|
20114
|
+
);
|
|
20115
|
+
items.push({ markerId: "learning", body });
|
|
20116
|
+
}
|
|
20117
|
+
if (suggestions) {
|
|
20118
|
+
for (let i = 0; i < suggestions.length; i++) {
|
|
20119
|
+
const s = suggestions[i];
|
|
20120
|
+
const body = formatIndividualComment(
|
|
20121
|
+
"suggestion",
|
|
20122
|
+
i + 1,
|
|
20123
|
+
{ section: s.section, action: s.action, preview: s.content },
|
|
20124
|
+
parsed.provider
|
|
20125
|
+
);
|
|
20126
|
+
items.push({ markerId: `suggestion:${i + 1}`, body });
|
|
20127
|
+
}
|
|
20128
|
+
}
|
|
20129
|
+
if (items.length === 0) return { posted: 0 };
|
|
20130
|
+
if (dryRun) {
|
|
20131
|
+
for (const item of items) {
|
|
20132
|
+
log.info(`dry-run [${item.markerId}]:`);
|
|
20133
|
+
console.log(item.body);
|
|
20134
|
+
console.log("---");
|
|
20135
|
+
}
|
|
20136
|
+
return { posted: items.length };
|
|
20137
|
+
}
|
|
20138
|
+
if (parsed.provider === "github") {
|
|
20139
|
+
for (const item of items) {
|
|
20140
|
+
const marker = `<!-- teamai:ci-extract:${item.markerId} -->`;
|
|
20141
|
+
const existing = await findGitHubComment(parsed.owner, parsed.repo, parsed.number, marker);
|
|
20142
|
+
if (existing) {
|
|
20143
|
+
await updateGitHubComment(parsed.owner, parsed.repo, existing.id, item.body);
|
|
20144
|
+
} else {
|
|
20145
|
+
await postGitHubComment(parsed.owner, parsed.repo, parsed.number, item.body);
|
|
20146
|
+
}
|
|
20147
|
+
posted++;
|
|
20148
|
+
}
|
|
20149
|
+
} else {
|
|
20150
|
+
const projectId = encodeURIComponent(`${parsed.owner}/${parsed.repo}`);
|
|
20151
|
+
const mrGlobalId = await getMrGlobalId(projectId, parsed.number);
|
|
20152
|
+
const mrListResp = await tgitRequest(
|
|
20153
|
+
`/projects/${projectId}/merge_requests?iid=${parsed.number}`,
|
|
20154
|
+
"GET"
|
|
20155
|
+
);
|
|
20156
|
+
let anchorFile = "README.md";
|
|
20157
|
+
if (mrListResp.ok) {
|
|
20158
|
+
const mrList = await mrListResp.json();
|
|
20159
|
+
if (mrList.length > 0) {
|
|
20160
|
+
const { source_branch, target_branch } = mrList[0];
|
|
20161
|
+
const compareResp = await tgitRequest(
|
|
20162
|
+
`/projects/${projectId}/repository/compare?from=${target_branch}&to=${source_branch}`,
|
|
20163
|
+
"GET"
|
|
20164
|
+
);
|
|
20165
|
+
if (compareResp.ok) {
|
|
20166
|
+
const compareData = await compareResp.json();
|
|
20167
|
+
if (compareData.diffs && compareData.diffs.length > 0) {
|
|
20168
|
+
anchorFile = compareData.diffs[0].new_path;
|
|
20169
|
+
}
|
|
20170
|
+
}
|
|
20171
|
+
}
|
|
20172
|
+
}
|
|
20173
|
+
for (const item of items) {
|
|
20174
|
+
const marker = `<!-- teamai:ci-extract:${item.markerId} -->`;
|
|
20175
|
+
const existingNote = await findTGitComment(projectId, mrGlobalId, marker);
|
|
20176
|
+
if (existingNote) {
|
|
20177
|
+
await updateTGitComment(projectId, mrGlobalId, existingNote.id, item.body);
|
|
20178
|
+
} else {
|
|
20179
|
+
const resp = await tgitRequest(
|
|
20180
|
+
`/projects/${projectId}/merge_requests/${mrGlobalId}/notes`,
|
|
20181
|
+
"POST",
|
|
20182
|
+
{ body: item.body, path: anchorFile, line: 1, line_type: "new" }
|
|
20183
|
+
);
|
|
20184
|
+
if (!resp.ok) {
|
|
20185
|
+
await postTGitComment(projectId, mrGlobalId, item.body);
|
|
20186
|
+
}
|
|
20187
|
+
}
|
|
20188
|
+
posted++;
|
|
20189
|
+
}
|
|
20190
|
+
}
|
|
20191
|
+
log.success(`\u5DF2\u53D1\u5E03 ${posted} \u6761\u72EC\u7ACB\u5EFA\u8BAE`);
|
|
20192
|
+
return { posted };
|
|
20193
|
+
}
|
|
20194
|
+
var DEFAULT_MARKER, API_TIMEOUT_MS;
|
|
20195
|
+
var init_mr_comment = __esm({
|
|
20196
|
+
"src/ci/mr-comment.ts"() {
|
|
20197
|
+
"use strict";
|
|
20198
|
+
init_gf_cli();
|
|
20199
|
+
init_logger();
|
|
20200
|
+
DEFAULT_MARKER = "<!-- teamai:ci-extract -->";
|
|
20201
|
+
API_TIMEOUT_MS = 15e3;
|
|
20202
|
+
}
|
|
20203
|
+
});
|
|
20204
|
+
|
|
20205
|
+
// src/ci/read-rejections.ts
|
|
20206
|
+
function extractMarkerId(body) {
|
|
20207
|
+
const match = body.match(MARKER_REGEX);
|
|
20208
|
+
return match ? match[1] : null;
|
|
20209
|
+
}
|
|
20210
|
+
async function githubRequest2(path69) {
|
|
20211
|
+
const token = process.env["GITHUB_TOKEN"];
|
|
20212
|
+
if (!token) throw new Error("\u672A\u8BBE\u7F6E GITHUB_TOKEN");
|
|
20213
|
+
return fetch(`https://api.github.com${path69}`, {
|
|
20214
|
+
headers: {
|
|
20215
|
+
Authorization: `Bearer ${token}`,
|
|
20216
|
+
Accept: "application/vnd.github+json",
|
|
20217
|
+
"User-Agent": "teamai-cli"
|
|
20218
|
+
},
|
|
20219
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS2)
|
|
20220
|
+
});
|
|
20221
|
+
}
|
|
20222
|
+
async function readGitHubRejections(owner, repo, prNumber) {
|
|
20223
|
+
const result = { rejectedIds: /* @__PURE__ */ new Set(), approvedIds: /* @__PURE__ */ new Set(), allIds: /* @__PURE__ */ new Set() };
|
|
20224
|
+
const resp = await githubRequest2(`/repos/${owner}/${repo}/issues/${prNumber}/comments?per_page=100`);
|
|
20225
|
+
if (!resp.ok) return result;
|
|
20226
|
+
const comments = await resp.json();
|
|
20227
|
+
for (const comment of comments) {
|
|
20228
|
+
const markerId = extractMarkerId(comment.body);
|
|
20229
|
+
if (!markerId) continue;
|
|
20230
|
+
result.allIds.add(markerId);
|
|
20231
|
+
const reactResp = await githubRequest2(
|
|
20232
|
+
`/repos/${owner}/${repo}/issues/comments/${comment.id}/reactions?per_page=100`
|
|
20233
|
+
);
|
|
20234
|
+
if (!reactResp.ok) {
|
|
20235
|
+
result.approvedIds.add(markerId);
|
|
20236
|
+
continue;
|
|
20237
|
+
}
|
|
20238
|
+
const reactions = await reactResp.json();
|
|
20239
|
+
const hasThumbsDown = reactions.some((r) => r.content === "-1");
|
|
20240
|
+
if (hasThumbsDown) {
|
|
20241
|
+
result.rejectedIds.add(markerId);
|
|
20242
|
+
} else {
|
|
20243
|
+
result.approvedIds.add(markerId);
|
|
20244
|
+
}
|
|
20245
|
+
}
|
|
20246
|
+
return result;
|
|
20247
|
+
}
|
|
20248
|
+
function getTGitToken2() {
|
|
20249
|
+
const envToken = process.env["TAI_PAT_TOKEN"];
|
|
20250
|
+
if (envToken) return envToken;
|
|
20251
|
+
const oauthToken = gfGetOAuthToken();
|
|
20252
|
+
if (oauthToken) return oauthToken;
|
|
20253
|
+
throw new Error("\u672A\u8BBE\u7F6E TAI_PAT_TOKEN \u4E14\u65E0\u6CD5\u83B7\u53D6 OAuth token");
|
|
20254
|
+
}
|
|
20255
|
+
async function tgitRequest2(path69) {
|
|
20256
|
+
const token = getTGitToken2();
|
|
20257
|
+
return fetch(`https://git.woa.com/api/v3${path69}`, {
|
|
20258
|
+
headers: {
|
|
20259
|
+
Authorization: `Bearer ${token}`,
|
|
20260
|
+
"Content-Type": "application/json"
|
|
20261
|
+
},
|
|
20262
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS2)
|
|
20263
|
+
});
|
|
20264
|
+
}
|
|
20265
|
+
async function getMrGlobalId2(projectId, mrIid) {
|
|
20266
|
+
const resp = await tgitRequest2(`/projects/${projectId}/merge_requests?iid=${mrIid}`);
|
|
20267
|
+
if (!resp.ok) throw new Error(`TGit \u67E5\u8BE2 MR \u5931\u8D25 (${resp.status})`);
|
|
20268
|
+
const mrs = await resp.json();
|
|
20269
|
+
const mr = mrs.find((m) => String(m.iid) === mrIid);
|
|
20270
|
+
if (!mr) throw new Error(`TGit MR !${mrIid} \u4E0D\u5B58\u5728`);
|
|
20271
|
+
return mr.id;
|
|
20272
|
+
}
|
|
20273
|
+
async function readTGitRejections(owner, repo, mrIid) {
|
|
20274
|
+
const result = { rejectedIds: /* @__PURE__ */ new Set(), approvedIds: /* @__PURE__ */ new Set(), allIds: /* @__PURE__ */ new Set() };
|
|
20275
|
+
const projectId = encodeURIComponent(`${owner}/${repo}`);
|
|
20276
|
+
const mrGlobalId = await getMrGlobalId2(projectId, mrIid);
|
|
20277
|
+
const resp = await tgitRequest2(`/projects/${projectId}/merge_requests/${mrGlobalId}/notes?per_page=100`);
|
|
20278
|
+
if (!resp.ok) return result;
|
|
20279
|
+
const notes = await resp.json();
|
|
20280
|
+
for (const note of notes) {
|
|
20281
|
+
const markerId = extractMarkerId(note.body);
|
|
20282
|
+
if (!markerId) continue;
|
|
20283
|
+
result.allIds.add(markerId);
|
|
20284
|
+
const hasRejectEmoji = (note.comments ?? []).some((c) => c.comment === TGIT_REJECT_EMOJI);
|
|
20285
|
+
if (hasRejectEmoji) {
|
|
20286
|
+
result.rejectedIds.add(markerId);
|
|
20287
|
+
} else {
|
|
20288
|
+
result.approvedIds.add(markerId);
|
|
20289
|
+
}
|
|
20290
|
+
}
|
|
20291
|
+
return result;
|
|
20292
|
+
}
|
|
20293
|
+
async function readRejections(mrUrl) {
|
|
20294
|
+
const parsed = parseMrUrl(mrUrl);
|
|
20295
|
+
if (parsed.provider === "github") {
|
|
20296
|
+
log.debug("\u8BFB\u53D6 GitHub reactions...");
|
|
20297
|
+
return readGitHubRejections(parsed.owner, parsed.repo, parsed.number);
|
|
20298
|
+
}
|
|
20299
|
+
log.debug("\u8BFB\u53D6 TGit emoji reactions...");
|
|
20300
|
+
return readTGitRejections(parsed.owner, parsed.repo, parsed.number);
|
|
20301
|
+
}
|
|
20302
|
+
function shouldWrite(markerId, rejections, _provider) {
|
|
20303
|
+
return !rejections.rejectedIds.has(markerId);
|
|
20304
|
+
}
|
|
20305
|
+
var API_TIMEOUT_MS2, MARKER_REGEX, TGIT_REJECT_EMOJI;
|
|
20306
|
+
var init_read_rejections = __esm({
|
|
20307
|
+
"src/ci/read-rejections.ts"() {
|
|
20308
|
+
"use strict";
|
|
20309
|
+
init_mr_comment();
|
|
20310
|
+
init_gf_cli();
|
|
20311
|
+
init_logger();
|
|
20312
|
+
API_TIMEOUT_MS2 = 15e3;
|
|
20313
|
+
MARKER_REGEX = /<!-- teamai:ci-extract:(\S+) -->/;
|
|
20314
|
+
TGIT_REJECT_EMOJI = 8;
|
|
20315
|
+
}
|
|
20316
|
+
});
|
|
20317
|
+
|
|
20318
|
+
// src/ci/extract-mr.ts
|
|
20319
|
+
var extract_mr_exports = {};
|
|
20320
|
+
__export(extract_mr_exports, {
|
|
20321
|
+
ciExtractMr: () => ciExtractMr
|
|
20322
|
+
});
|
|
20323
|
+
import fs36 from "fs/promises";
|
|
20324
|
+
import path68 from "path";
|
|
20325
|
+
import os6 from "os";
|
|
20326
|
+
async function configureGitUser2(repoPath, provider) {
|
|
20327
|
+
const { execFileSync: execFileSync4 } = await import("child_process");
|
|
20328
|
+
let name = "teamai-ci";
|
|
20329
|
+
let email = "teamai-ci@noreply";
|
|
20330
|
+
try {
|
|
20331
|
+
if (provider === "github") {
|
|
20332
|
+
const token = process.env["GITHUB_TOKEN"];
|
|
20333
|
+
if (token) {
|
|
20334
|
+
const resp = await fetch("https://api.github.com/user", {
|
|
20335
|
+
headers: { Authorization: `Bearer ${token}`, "User-Agent": "teamai-cli" },
|
|
20336
|
+
signal: AbortSignal.timeout(8e3)
|
|
20337
|
+
});
|
|
20338
|
+
if (resp.ok) {
|
|
20339
|
+
const user = await resp.json();
|
|
20340
|
+
name = user.login;
|
|
20341
|
+
email = user.email ?? `${user.login}@users.noreply.github.com`;
|
|
20342
|
+
}
|
|
20343
|
+
}
|
|
20344
|
+
} else {
|
|
20345
|
+
const token = process.env["TAI_PAT_TOKEN"];
|
|
20346
|
+
if (token) {
|
|
20347
|
+
const resp = await fetch("https://git.woa.com/api/v3/user", {
|
|
20348
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
20349
|
+
signal: AbortSignal.timeout(8e3)
|
|
20350
|
+
});
|
|
20351
|
+
if (resp.ok) {
|
|
20352
|
+
const user = await resp.json();
|
|
20353
|
+
name = user.username;
|
|
20354
|
+
email = user.email;
|
|
20355
|
+
}
|
|
20356
|
+
}
|
|
20357
|
+
}
|
|
20358
|
+
} catch {
|
|
20359
|
+
log.debug("\u65E0\u6CD5\u83B7\u53D6\u7528\u6237\u4FE1\u606F\uFF0C\u4F7F\u7528\u9ED8\u8BA4 git user");
|
|
20360
|
+
}
|
|
20361
|
+
try {
|
|
20362
|
+
execFileSync4("git", ["config", "user.name", name], { cwd: repoPath, stdio: "ignore" });
|
|
20363
|
+
execFileSync4("git", ["config", "user.email", email], { cwd: repoPath, stdio: "ignore" });
|
|
20364
|
+
log.debug(`Git user: ${name} <${email}>`);
|
|
20365
|
+
} catch {
|
|
20366
|
+
log.debug("git config \u5931\u8D25\uFF08\u975E git \u4ED3\u5E93\uFF09\uFF0C\u8DF3\u8FC7");
|
|
20367
|
+
}
|
|
20368
|
+
}
|
|
20369
|
+
async function writeKnowledgeToRepo(teamRepo, learning, suggestions, writeMode, mrUrl, dryRun) {
|
|
20370
|
+
const changedFiles = [];
|
|
20371
|
+
if (learning) {
|
|
20372
|
+
const safeTitle = learning.title.replace(/[^a-zA-Z0-9一-鿿_-]/g, "-").replace(/-+/g, "-").slice(0, 50);
|
|
20373
|
+
const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
20374
|
+
const filename = `${dateStr}-${safeTitle}.md`;
|
|
20375
|
+
const learningsDir = path68.join(teamRepo, "learnings");
|
|
20376
|
+
const learningPath = path68.join(learningsDir, filename);
|
|
20377
|
+
if (!dryRun) {
|
|
20378
|
+
await fs36.mkdir(learningsDir, { recursive: true });
|
|
20379
|
+
await fs36.writeFile(learningPath, learning.content, "utf-8");
|
|
20380
|
+
}
|
|
20381
|
+
log.success(`Learning \u5199\u5165: learnings/${filename}`);
|
|
20382
|
+
changedFiles.push(`learnings/${filename}`);
|
|
20383
|
+
}
|
|
20384
|
+
if (suggestions && suggestions.length > 0) {
|
|
20385
|
+
if (writeMode === "direct") {
|
|
20386
|
+
const codebasePath = path68.join(teamRepo, "docs", "codebase.md");
|
|
20387
|
+
try {
|
|
20388
|
+
const existing = await fs36.readFile(codebasePath, "utf-8");
|
|
20389
|
+
const updated = await applyCodebaseSuggestions(existing, suggestions);
|
|
20390
|
+
if (!dryRun) {
|
|
20391
|
+
await fs36.writeFile(codebasePath, updated, "utf-8");
|
|
20392
|
+
}
|
|
20393
|
+
log.success("Codebase.md \u5DF2\u66F4\u65B0");
|
|
20394
|
+
changedFiles.push("docs/codebase.md");
|
|
20395
|
+
} catch {
|
|
20396
|
+
log.warn("docs/codebase.md \u4E0D\u5B58\u5728\u6216\u8BFB\u53D6\u5931\u8D25\uFF0C\u8DF3\u8FC7 codebase \u66F4\u65B0");
|
|
20397
|
+
}
|
|
20398
|
+
} else {
|
|
20399
|
+
for (const s of suggestions) {
|
|
20400
|
+
if (!dryRun) {
|
|
20401
|
+
await appendPendingReview(teamRepo, {
|
|
20402
|
+
kind: "codebase-section",
|
|
20403
|
+
target: { file: "docs/codebase.md", section: s.section },
|
|
20404
|
+
payload: { content: s.content, action: s.action },
|
|
20405
|
+
source: `ci:extract-mr:${mrUrl}`
|
|
20406
|
+
});
|
|
20407
|
+
}
|
|
20408
|
+
}
|
|
20409
|
+
log.success(`${suggestions.length} \u6761\u5EFA\u8BAE\u5DF2\u52A0\u5165 pending-review \u961F\u5217`);
|
|
20410
|
+
changedFiles.push(".teamai/pending-review.jsonl");
|
|
20411
|
+
}
|
|
20412
|
+
}
|
|
20413
|
+
if (!dryRun && changedFiles.length > 0) {
|
|
20414
|
+
try {
|
|
20415
|
+
const provider = mrUrl.includes("github.com") ? "github" : "tgit";
|
|
20416
|
+
await configureGitUser2(teamRepo, provider);
|
|
20417
|
+
await pushRepoDirectly(teamRepo, `[teamai] CI extract knowledge from MR`, changedFiles);
|
|
20418
|
+
log.success("\u5DF2\u63A8\u9001\u5230\u56E2\u961F\u4ED3\u5E93");
|
|
20419
|
+
} catch (err) {
|
|
20420
|
+
log.warn(`\u63A8\u9001\u5931\u8D25: ${err.message}\uFF0C\u6587\u4EF6\u5DF2\u5199\u5165\u672C\u5730`);
|
|
20421
|
+
}
|
|
20422
|
+
}
|
|
20423
|
+
}
|
|
20424
|
+
async function writeArtifacts(outputDir, learning, suggestions) {
|
|
20425
|
+
await fs36.mkdir(outputDir, { recursive: true });
|
|
20426
|
+
if (learning) {
|
|
20427
|
+
await fs36.writeFile(path68.join(outputDir, "learning.md"), learning.content, "utf-8");
|
|
20428
|
+
}
|
|
20429
|
+
if (suggestions && suggestions.length > 0) {
|
|
20430
|
+
await fs36.writeFile(
|
|
20431
|
+
path68.join(outputDir, "codebase-suggestions.json"),
|
|
20432
|
+
JSON.stringify(suggestions, null, 2),
|
|
20433
|
+
"utf-8"
|
|
20434
|
+
);
|
|
20435
|
+
}
|
|
20436
|
+
}
|
|
20437
|
+
async function ciExtractMr(opts) {
|
|
20438
|
+
if ((opts.mode === "write" || opts.mode === "both") && !opts.teamRepo) {
|
|
20439
|
+
throw new Error("write \u6A21\u5F0F\u9700\u8981 --team-repo \u53C2\u6570");
|
|
20440
|
+
}
|
|
20441
|
+
let existingCodebaseMd;
|
|
20442
|
+
if (opts.existingCodebase) {
|
|
20443
|
+
try {
|
|
20444
|
+
existingCodebaseMd = await fs36.readFile(opts.existingCodebase, "utf-8");
|
|
20445
|
+
} catch {
|
|
20446
|
+
log.warn(`\u65E0\u6CD5\u8BFB\u53D6 --existing-codebase: ${opts.existingCodebase}`);
|
|
20447
|
+
}
|
|
20448
|
+
} else if (opts.teamRepo) {
|
|
20449
|
+
const codebasePath = path68.join(opts.teamRepo, "docs", "codebase.md");
|
|
20450
|
+
try {
|
|
20451
|
+
existingCodebaseMd = await fs36.readFile(codebasePath, "utf-8");
|
|
20452
|
+
} catch {
|
|
20453
|
+
}
|
|
20454
|
+
}
|
|
20455
|
+
const tmpDir = await fs36.mkdtemp(path68.join(os6.tmpdir(), "teamai-ci-extract-"));
|
|
20456
|
+
let learning;
|
|
20457
|
+
let suggestions;
|
|
20458
|
+
try {
|
|
20459
|
+
const result = await importFromMR({
|
|
20460
|
+
url: opts.url,
|
|
20461
|
+
all: true,
|
|
20462
|
+
outputDir: tmpDir,
|
|
20463
|
+
existingCodebaseMd,
|
|
20464
|
+
dryRun: true
|
|
20465
|
+
// 不让 importFromMR 自己写文件,我们自己控制写入
|
|
20466
|
+
});
|
|
20467
|
+
learning = result.learning;
|
|
20468
|
+
suggestions = result.codebaseSuggestions;
|
|
20469
|
+
} finally {
|
|
20470
|
+
await fs36.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
20471
|
+
});
|
|
20472
|
+
}
|
|
20473
|
+
if (opts.mode === "comment" || opts.mode === "both") {
|
|
20474
|
+
if (opts.individualComments) {
|
|
20475
|
+
const { posted } = await postIndividualComments(opts.url, learning, suggestions, opts.dryRun);
|
|
20476
|
+
log.success(`\u5DF2\u53D1\u5E03 ${posted} \u6761\u72EC\u7ACB\u5EFA\u8BAE comment`);
|
|
20477
|
+
} else {
|
|
20478
|
+
const result = await postOrUpdateMrComment(
|
|
20479
|
+
opts.url,
|
|
20480
|
+
learning,
|
|
20481
|
+
suggestions,
|
|
20482
|
+
opts.commentMarker,
|
|
20483
|
+
opts.dryRun
|
|
20484
|
+
);
|
|
20485
|
+
if (result.created) {
|
|
20486
|
+
log.success("MR comment \u5DF2\u53D1\u5E03");
|
|
20487
|
+
} else {
|
|
20488
|
+
log.success("MR comment \u5DF2\u66F4\u65B0");
|
|
20489
|
+
}
|
|
20490
|
+
if (result.url) {
|
|
20491
|
+
log.info(`Comment URL: ${result.url}`);
|
|
20492
|
+
}
|
|
20493
|
+
}
|
|
20494
|
+
}
|
|
20495
|
+
if (opts.mode === "write" || opts.mode === "both") {
|
|
20496
|
+
let filteredLearning = learning;
|
|
20497
|
+
let filteredSuggestions = suggestions;
|
|
20498
|
+
if (opts.individualComments && !opts.dryRun) {
|
|
20499
|
+
const parsed = parseMrUrl(opts.url);
|
|
20500
|
+
const rejections = await readRejections(opts.url);
|
|
20501
|
+
if (rejections.allIds.size > 0) {
|
|
20502
|
+
if (learning && !shouldWrite("learning", rejections, parsed.provider)) {
|
|
20503
|
+
log.info("Learning \u88AB reject\uFF0C\u8DF3\u8FC7\u5199\u5165");
|
|
20504
|
+
filteredLearning = void 0;
|
|
20505
|
+
}
|
|
20506
|
+
if (suggestions) {
|
|
20507
|
+
filteredSuggestions = suggestions.filter(
|
|
20508
|
+
(_, i) => shouldWrite(`suggestion:${i + 1}`, rejections, parsed.provider)
|
|
20509
|
+
);
|
|
20510
|
+
const rejected = suggestions.length - filteredSuggestions.length;
|
|
20511
|
+
if (rejected > 0) {
|
|
20512
|
+
log.info(`${rejected} \u6761 codebase \u5EFA\u8BAE\u88AB reject\uFF0C\u5DF2\u6392\u9664`);
|
|
20513
|
+
}
|
|
20514
|
+
}
|
|
20515
|
+
}
|
|
20516
|
+
}
|
|
20517
|
+
await writeKnowledgeToRepo(
|
|
20518
|
+
opts.teamRepo,
|
|
20519
|
+
filteredLearning,
|
|
20520
|
+
filteredSuggestions,
|
|
20521
|
+
opts.writeMode ?? "direct",
|
|
20522
|
+
opts.url,
|
|
20523
|
+
opts.dryRun
|
|
20524
|
+
);
|
|
20525
|
+
}
|
|
20526
|
+
if (opts.output) {
|
|
20527
|
+
await writeArtifacts(opts.output, learning, suggestions);
|
|
20528
|
+
log.success(`Artifacts \u5DF2\u8F93\u51FA\u5230: ${opts.output}`);
|
|
20529
|
+
}
|
|
20530
|
+
}
|
|
20531
|
+
var init_extract_mr = __esm({
|
|
20532
|
+
"src/ci/extract-mr.ts"() {
|
|
20533
|
+
"use strict";
|
|
20534
|
+
init_import_mr();
|
|
20535
|
+
init_codebase();
|
|
20536
|
+
init_review_store();
|
|
20537
|
+
init_git();
|
|
20538
|
+
init_logger();
|
|
20539
|
+
init_mr_comment();
|
|
20540
|
+
init_read_rejections();
|
|
20541
|
+
}
|
|
20542
|
+
});
|
|
20543
|
+
|
|
19804
20544
|
// src/index.ts
|
|
19805
20545
|
init_logger();
|
|
19806
20546
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -20123,5 +20863,11 @@ program.command("hook-dispatch <event>").description("Unified hook dispatcher \u
|
|
|
20123
20863
|
const { hookDispatchCli: hookDispatchCli2 } = await Promise.resolve().then(() => (init_hook_dispatch_cli(), hook_dispatch_cli_exports));
|
|
20124
20864
|
await hookDispatchCli2(event, cmdOpts.tool ?? "claude", cmdOpts.matcher ?? "*");
|
|
20125
20865
|
});
|
|
20866
|
+
var ciCmd = program.command("ci").description("CI pipeline integration commands");
|
|
20867
|
+
ciCmd.command("extract-mr").description("Extract knowledge from MR/PR and post as comment or write to team repo").requiredOption("--url <url>", "MR/PR web URL").option("--mode <mode>", "Operation mode: comment | write | both", "comment").option("--team-repo <path>", "Team knowledge repo path (required for write mode)").option("--existing-codebase <path>", "Existing codebase.md for style consistency").option("--comment-marker <marker>", "HTML comment anchor for idempotent updates", "<!-- teamai:ci-extract -->").option("--write-mode <mode>", "Write strategy: direct | pending-review", "direct").option("--output <dir>", "Write artifacts to directory").option("--individual-comments", "Post each suggestion as separate comment with reaction/resolve support").action(async (cmdOpts) => {
|
|
20868
|
+
const globalOpts = program.opts();
|
|
20869
|
+
const { ciExtractMr: ciExtractMr2 } = await Promise.resolve().then(() => (init_extract_mr(), extract_mr_exports));
|
|
20870
|
+
await ciExtractMr2({ ...globalOpts, ...cmdOpts });
|
|
20871
|
+
});
|
|
20126
20872
|
program.parse();
|
|
20127
20873
|
//# sourceMappingURL=index.js.map
|