@lyra-ai/toolkit 0.0.1 → 0.1.0
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/QUICKSTART.md +91 -0
- package/README.md +65 -41
- package/bin/toolkit.mjs +114 -0
- package/package.json +3 -1
- package/src/config-cmd.mjs +100 -0
- package/src/flush.mjs +284 -0
- package/src/hook.mjs +31 -0
- package/src/install.mjs +101 -0
- package/src/status.mjs +67 -0
- package/src/uninstall.mjs +50 -0
package/QUICKSTART.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @lyra-ai/toolkit Quickstart
|
|
2
|
+
|
|
3
|
+
面向 AI agent 的零依赖禅道 CLI。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @lyra-ai/toolkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
验证:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
zentao --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 初始化
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
zentao init
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
按提示填入禅道地址、用户名、密码,末尾自动验证连通性。
|
|
24
|
+
|
|
25
|
+
脚本 / CI 环境用非交互模式:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
zentao init --url https://zentao.example.com --username myname --password mypass
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 开始使用
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 查看项目与产品
|
|
35
|
+
zentao project list --pretty
|
|
36
|
+
zentao product list --pretty
|
|
37
|
+
|
|
38
|
+
# 同步产品 bug 到本地缓存(现有产品版本17.x API不支持直接筛选)
|
|
39
|
+
zentao bug sync --product 财司核心7.0 # 支持名称前缀,也可用数字 ID
|
|
40
|
+
|
|
41
|
+
# 查 bug(全走本地缓存,先 sync 后查)
|
|
42
|
+
zentao bug list --mine --status active
|
|
43
|
+
zentao bug list --title 124网银
|
|
44
|
+
|
|
45
|
+
# 看详情
|
|
46
|
+
zentao bug get 42 --pretty
|
|
47
|
+
|
|
48
|
+
# 解决 / 指派
|
|
49
|
+
zentao bug resolve 42 --resolution fixed --comment "已修复"
|
|
50
|
+
zentao bug assign 42 --to zhangsan
|
|
51
|
+
|
|
52
|
+
# 配置管理
|
|
53
|
+
zentao config # 查看所有配置
|
|
54
|
+
zentao config set ZENTAO_DEFAULT_PROJECT 1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Bug 工作流
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
sync(拉取) → list/search(检索) → get(详情) → resolve/assign(操作)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- **`bug sync`** 拉产品 bug 到本地缓存(`~/.zentao-cli/cache/`)
|
|
64
|
+
- **`bug list`** 本地检索,支持标题/状态/严重程度/指派/日期/排序等
|
|
65
|
+
- **`bug search`** 与 `bug list` 等价(隐藏别名)
|
|
66
|
+
- **`bug get`** 实时拉取单条最新详情
|
|
67
|
+
|
|
68
|
+
## Agent 集成要点
|
|
69
|
+
|
|
70
|
+
- 默认输出 **JSON → stdout**,错误 → stderr
|
|
71
|
+
- 退出码:`0` 成功 / `2` 参数错误 / `3` 认证失败 / `4` 网络错误 / `5` API 错误
|
|
72
|
+
- `--pretty` 输出 Markdown 表格(人类可读)
|
|
73
|
+
- `--product` 支持 ID 或**产品名前缀**(如 `财司` → 匹配"财司存款")
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Agent 典型用法
|
|
77
|
+
zentao bug sync --product 财司
|
|
78
|
+
zentao bug list --status active --mine --limit 10
|
|
79
|
+
zentao bug get 42
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 配置参考
|
|
83
|
+
|
|
84
|
+
| 变量 | 必填 | 说明 |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `ZENTAO_URL` | ✅ | 禅道地址 |
|
|
87
|
+
| `ZENTAO_USERNAME` | ✅ | 用户名 |
|
|
88
|
+
| `ZENTAO_PASSWORD` | ✅ | 密码 |
|
|
89
|
+
| `ZENTAO_DEFAULT_PROJECT` | | 默认产品/项目 ID |
|
|
90
|
+
| `ZENTAO_TOKEN_TTL` | | Token 有效期(秒,默认 3600) |
|
|
91
|
+
| `ZENTAO_CACHE_STALE_MS` | | 缓存过期阈值(ms,默认 3600000) |
|
package/README.md
CHANGED
|
@@ -1,57 +1,81 @@
|
|
|
1
1
|
# @lyra-ai/toolkit
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
当前工具:**zentao**(禅道 REST v1)。预留 `log-query`、`skill-hub` 等扩展位。
|
|
3
|
+
组织级研发工具集。面向研发团队的效能分析、评估与持续改进。
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
> **适用场景**:组织内部已部署配套的分析平台和技能中心。不适用于个人开发者独立使用。
|
|
6
|
+
|
|
7
|
+
纯 JS(`.mjs`,Node 18+,无编译、无运行时依赖)。
|
|
8
|
+
|
|
9
|
+
## 功能
|
|
10
|
+
|
|
11
|
+
### 个人编码效能看板
|
|
12
|
+
|
|
13
|
+
安装后,你的 AI 编码数据自动进入组织分析平台,生成专属的多维度效能看板。
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
toolkit install claudecode --registry http://your-platform:38000
|
|
7
17
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
|
|
19
|
+
- 会话结束后自动同步,无需手动操作
|
|
20
|
+
- 支持多项目,按项目自动分组
|
|
21
|
+
- 只处理新会话,不重复
|
|
22
|
+
|
|
23
|
+
### 管理命令
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
toolkit status # 安装状态 + 已同步/待同步会话数
|
|
27
|
+
toolkit flush # 手动触发一次同步
|
|
28
|
+
toolkit config # 查看全部配置
|
|
29
|
+
toolkit config set registry <url> # 修改平台地址
|
|
30
|
+
toolkit config set project-filter <关键词> # 按项目名关键词过滤(逗号分隔),留空=全部
|
|
31
|
+
toolkit config unset project-filter # 清除过滤,恢复全部
|
|
32
|
+
toolkit uninstall # 卸载
|
|
33
|
+
toolkit uninstall --purge # 卸载并清除本地数据
|
|
14
34
|
```
|
|
15
|
-
|
|
35
|
+
|
|
36
|
+
### --registry
|
|
37
|
+
|
|
38
|
+
组织平台根地址,自动派生子系统路径:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
http://your-host:38000
|
|
42
|
+
/ai-metrics 效能分析平台
|
|
43
|
+
/skillhub 技能中心(预留)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### 禅道 CLI(zentao)
|
|
49
|
+
|
|
50
|
+
禅道 REST v1 的命令行工具,支持 bug 查询/创建/解决、项目管理、配置管理。
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
zentao ping # 连通性测试
|
|
54
|
+
zentao init # 交互式配置
|
|
55
|
+
zentao project list # 项目列表
|
|
56
|
+
zentao product list # 产品列表
|
|
57
|
+
zentao bug list [过滤选项] # bug 检索(本地缓存)
|
|
58
|
+
zentao bug sync [--product ID] [--mine] # 同步 bug 到本地
|
|
59
|
+
zentao bug get <id> # bug 详情
|
|
60
|
+
zentao bug create --title T # 创建 bug
|
|
61
|
+
zentao bug resolve <id> --resolution fixed # 解决 bug
|
|
62
|
+
zentao config set <key> <value> # 配置管理
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
默认输出 JSON(stdout),错误走 stderr。退出码:0 成功 / 2 参数 / 3 认证 / 4 网络 / 5 API。
|
|
66
|
+
|
|
67
|
+
---
|
|
16
68
|
|
|
17
69
|
## 安装
|
|
70
|
+
|
|
18
71
|
```bash
|
|
19
72
|
npm install -g @lyra-ai/toolkit # 发布后
|
|
20
73
|
# 或本地开发:
|
|
21
|
-
cd <本目录> && npm link
|
|
74
|
+
cd <本目录> && npm link
|
|
22
75
|
```
|
|
23
|
-
配置:复制 `.env.example` 为 `~/.zentao-cli/.env` 或 cwd `.env`,填 `ZENTAO_URL/USERNAME/PASSWORD`。
|
|
24
|
-
或用 `zentao config set` 命令行管理配置,或 `zentao init` 向导式初始化。
|
|
25
|
-
|
|
26
|
-
## 命令速查(agent 直接用)
|
|
27
|
-
- `zentao ping` → `{"ok":true,"url":...}`
|
|
28
|
-
- `zentao init` → 交互式向导(填 URL/用户/密码,末了验证连通性)
|
|
29
|
-
- `zentao init --url <url> --username <u> --password <p> [--project <id>] [--ping]` → 非交互式(脚本/AI)
|
|
30
|
-
- `zentao project list [--status active|closed|all] [--limit N] [--offset N]`
|
|
31
|
-
- `zentao product list [--status active|closed|all] [--limit N] [--offset N]`
|
|
32
|
-
- `zentao config` → 列出所有配置(密码脱敏为 `***`)
|
|
33
|
-
- `zentao config get <key>` → 取单个值(密码需 `--raw` 才显原文)
|
|
34
|
-
- `zentao config set <key> <value>` → 写入 `~/.zentao-cli/.env`
|
|
35
|
-
- `zentao config unset <key>` → 删除配置项
|
|
36
|
-
- `zentao config path` → 输出配置文件路径
|
|
37
|
-
- `zentao bug list [--title <kw>] [--status S] [--severity 1-4] [--priority 1-4] [--assigned-to U] [--opened-by U] [--mine] [--product <ID|名称前缀>] [--project ID] [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--order id_desc|...] [--limit N] [--offset N]` → 本地缓存检索(需先 sync);`bug search` 等价别名
|
|
38
|
-
- `zentao bug get <id>` → Bug 详情 JSON(live)
|
|
39
|
-
- `zentao bug create --title T [--product ID --severity 1 --priority 2 --assigned-to U]`(`--project` 为兼容别名,作用域为产品)
|
|
40
|
-
- `zentao bug resolve <id> --resolution fixed [--comment ...]`
|
|
41
|
-
- `zentao bug assign <id> --to <user>`
|
|
42
|
-
|
|
43
|
-
### 本地 bug 索引(先 sync 再 list/search,统一为缓存检索)
|
|
44
|
-
- `zentao bug sync [--product ID] [--status <browseType>] [--mine]` → 一次性同步某产品 bug 到本地(默认 `unclosed`);`--mine` 只缓存指派给我的
|
|
45
|
-
- `zentao bug browse-types` → 列出 `sync --status` 的合法 browseType(`all`/`unclosed`/`assigntome`/...)
|
|
46
|
-
|
|
47
|
-
> 缓存位于 `~/.zentao-cli/cache/bugs-<productId>.json`;关键判断仍走 `bug get`(live)。
|
|
48
|
-
> `bug list` 的 `--product` 支持产品 ID 或**产品名前缀**(如 `--product 财司`,内部调用 products API 解析)。
|
|
49
|
-
> `--mine` 一律本地按 `ZENTAO_USERNAME` 过滤,可与任意过滤叠加。
|
|
50
|
-
> 缓存 >1h(`ZENTAO_CACHE_STALE_MS` 可配)时 search 会在 stderr 提示过期。
|
|
51
|
-
|
|
52
|
-
默认输出 JSON(stdout),错误走 stderr。退出码:0 成功 / 2 参数配置 / 3 认证 / 4 网络 / 5 API。
|
|
53
76
|
|
|
54
77
|
## JSON 示例
|
|
78
|
+
|
|
55
79
|
```json
|
|
56
80
|
{"total":1,"count":1,"bugs":[{"id":"9","title":"t","severity":"1","priority":"2","status":"active","assignedTo":"a"}]}
|
|
57
81
|
```
|
package/bin/toolkit.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* toolkit — 研发工具集 CLI 入口
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* toolkit install claudecode --registry <url> 安装插件
|
|
7
|
+
* toolkit status 查看状态
|
|
8
|
+
* toolkit flush 手动上传
|
|
9
|
+
* toolkit uninstall [--purge] 卸载
|
|
10
|
+
* toolkit help 帮助
|
|
11
|
+
*
|
|
12
|
+
* 其他子命令(zentao 等)保留向后兼容。
|
|
13
|
+
*/
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const srcDir = path.resolve(__dirname, '..', 'src');
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const cmd = args[0];
|
|
22
|
+
|
|
23
|
+
const HELP = `toolkit — 研发工具集
|
|
24
|
+
|
|
25
|
+
命令:
|
|
26
|
+
install claudecode --registry <url> 安装 Claude Code 插件
|
|
27
|
+
status 查看安装状态 + 待同步会话
|
|
28
|
+
flush 手动触发一次同步
|
|
29
|
+
config 配置管理(registry / include)
|
|
30
|
+
uninstall [--purge] 卸载插件(--purge 同时删数据)
|
|
31
|
+
help 显示此帮助
|
|
32
|
+
|
|
33
|
+
其他:
|
|
34
|
+
zentao <subcommand> 禅道 CLI(向后兼容)
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
// 无参数 → help
|
|
39
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
40
|
+
console.log(HELP);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// toolkit install claudecode --registry <url>
|
|
45
|
+
if (cmd === 'install') {
|
|
46
|
+
const target = args[1]; // claudecode
|
|
47
|
+
if (target !== 'claudecode') {
|
|
48
|
+
console.error('目前只支持: toolkit install claudecode');
|
|
49
|
+
console.error(' 用法: toolkit install claudecode --registry <url>');
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
// 解析 --registry
|
|
53
|
+
const regIdx = args.indexOf('--registry');
|
|
54
|
+
let registry = regIdx >= 0 ? args[regIdx + 1] : null;
|
|
55
|
+
if (!registry) {
|
|
56
|
+
console.error('ERROR: 缺少 --registry <url>');
|
|
57
|
+
console.error(' 用法: toolkit install claudecode --registry http://your-platform');
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
const { install } = await import(pathToFileURL(path.join(srcDir, 'install.mjs')).href);
|
|
61
|
+
install(registry);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// toolkit status
|
|
66
|
+
if (cmd === 'status') {
|
|
67
|
+
const { status } = await import(pathToFileURL(path.join(srcDir, 'status.mjs')).href);
|
|
68
|
+
status();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// toolkit flush
|
|
73
|
+
if (cmd === 'flush') {
|
|
74
|
+
const flushScript = path.join(srcDir, 'flush.mjs');
|
|
75
|
+
// 同步运行 flush(显示输出)
|
|
76
|
+
const child = spawn(process.execPath, [flushScript], {
|
|
77
|
+
stdio: 'inherit',
|
|
78
|
+
env: { ...process.env, TOOLKIT_VERBOSE: '1' },
|
|
79
|
+
});
|
|
80
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// toolkit uninstall [--purge]
|
|
85
|
+
if (cmd === 'uninstall') {
|
|
86
|
+
const purge = args.includes('--purge');
|
|
87
|
+
const { uninstall } = await import(pathToFileURL(path.join(srcDir, 'uninstall.mjs')).href);
|
|
88
|
+
uninstall({ purge });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// toolkit config [get|set|unset|list]
|
|
93
|
+
if (cmd === 'config') {
|
|
94
|
+
const { configCmd } = await import(pathToFileURL(path.join(srcDir, 'config-cmd.mjs')).href);
|
|
95
|
+
configCmd(args.slice(1));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 未知命令 → 尝试 zentao 兼容
|
|
100
|
+
// 注意:'config' 已被 toolkit 自己处理,不在 zentao 转发列表中
|
|
101
|
+
const zentaoSubcmds = ['project', 'product', 'bug', 'ping', 'init'];
|
|
102
|
+
if (zentaoSubcmds.includes(cmd)) {
|
|
103
|
+
const mod = await import(pathToFileURL(path.join(srcDir, 'zentao.mjs')).href);
|
|
104
|
+
const zcode = mod.main();
|
|
105
|
+
if (zcode != null) process.exit(zcode);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.error(`未知命令: ${cmd}\n`);
|
|
110
|
+
console.log(HELP);
|
|
111
|
+
process.exit(2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyra-ai/toolkit",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "面向 AI agent 的零依赖开发工具集",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"toolkit": "./bin/toolkit.mjs",
|
|
7
8
|
"zentao": "./bin/zentao.mjs"
|
|
8
9
|
},
|
|
9
10
|
"engines": { "node": ">=18" },
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
"bin",
|
|
12
13
|
"src",
|
|
13
14
|
"README.md",
|
|
15
|
+
"QUICKSTART.md",
|
|
14
16
|
"LICENSE",
|
|
15
17
|
".env.example"
|
|
16
18
|
],
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* config-cmd.mjs — `toolkit config [get|set|unset|list]`
|
|
4
|
+
* 管理 ~/.toolkit/config.json(registry / project-filter)。
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
|
|
10
|
+
const TOOLKIT_DIR = path.join(os.homedir(), '.toolkit');
|
|
11
|
+
const CONFIG_PATH = path.join(TOOLKIT_DIR, 'config.json');
|
|
12
|
+
|
|
13
|
+
const HELP = `toolkit config — 配置管理
|
|
14
|
+
|
|
15
|
+
用法:
|
|
16
|
+
toolkit config 显示全部配置
|
|
17
|
+
toolkit config get <key> 查看单个配置项
|
|
18
|
+
toolkit config set <key> <value> 设置配置项
|
|
19
|
+
toolkit config unset <key> 删除配置项
|
|
20
|
+
|
|
21
|
+
可用配置项:
|
|
22
|
+
registry 平台根地址(如 http://your-host:38000)
|
|
23
|
+
project-filter 项目过滤关键词,逗号分隔(如 my-project,backend);留空 = 全部上传
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
function load() {
|
|
27
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); }
|
|
28
|
+
catch { console.error('配置文件不存在。先运行: toolkit install claudecode --registry <url>'); process.exit(1); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function save(config) {
|
|
32
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function configCmd(args) {
|
|
36
|
+
const sub = args[0];
|
|
37
|
+
|
|
38
|
+
if (!sub || sub === 'list' || sub === 'help' || sub === '--help') {
|
|
39
|
+
const config = load();
|
|
40
|
+
const pf = config['project-filter'];
|
|
41
|
+
console.log('Toolkit 配置 (' + CONFIG_PATH + ')');
|
|
42
|
+
console.log('════════════════════════════════════════');
|
|
43
|
+
console.log(` registry: ${config.registry || '(未设置)'}`);
|
|
44
|
+
console.log(` project-filter: ${pf && pf.length ? pf.join(', ') : '(空 = 全部上传)'}`);
|
|
45
|
+
console.log(` uid: ${config.uid?.slice(0, 12)}...`);
|
|
46
|
+
console.log('════════════════════════════════════════');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (sub === 'get') {
|
|
51
|
+
const key = args[1];
|
|
52
|
+
if (!key) { console.error('用法: toolkit config get <key>'); process.exit(2); }
|
|
53
|
+
const config = load();
|
|
54
|
+
if (key === 'project-filter') {
|
|
55
|
+
const pf = config['project-filter'];
|
|
56
|
+
console.log(pf && pf.length ? pf.join(', ') : '');
|
|
57
|
+
} else {
|
|
58
|
+
console.log(config[key] ?? '');
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (sub === 'set') {
|
|
64
|
+
const key = args[1];
|
|
65
|
+
const value = args.slice(2).join(' ');
|
|
66
|
+
if (!key || !value) { console.error('用法: toolkit config set <key> <value>'); process.exit(2); }
|
|
67
|
+
const config = load();
|
|
68
|
+
if (key === 'project-filter') {
|
|
69
|
+
config['project-filter'] = value.split(',').map(s => s.trim()).filter(Boolean);
|
|
70
|
+
} else if (key === 'registry') {
|
|
71
|
+
config.registry = value.replace(/\/$/, '');
|
|
72
|
+
} else {
|
|
73
|
+
config[key] = value;
|
|
74
|
+
}
|
|
75
|
+
save(config);
|
|
76
|
+
const display = key === 'project-filter'
|
|
77
|
+
? (config['project-filter'].join(', ') || '(空)')
|
|
78
|
+
: config[key];
|
|
79
|
+
console.log(`✅ ${key} = ${display}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (sub === 'unset') {
|
|
84
|
+
const key = args[1];
|
|
85
|
+
if (!key) { console.error('用法: toolkit config unset <key>'); process.exit(2); }
|
|
86
|
+
const config = load();
|
|
87
|
+
if (key === 'project-filter') {
|
|
88
|
+
config['project-filter'] = [];
|
|
89
|
+
} else {
|
|
90
|
+
delete config[key];
|
|
91
|
+
}
|
|
92
|
+
save(config);
|
|
93
|
+
console.log(`✅ ${key} 已清除`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.error(`未知子命令: ${sub}\n`);
|
|
98
|
+
console.log(HELP);
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
package/src/flush.mjs
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* flush.mjs — 扫描 Claude Code 会话 JSONL → 零依赖 zip → POST 平台。
|
|
4
|
+
* 被 hook.mjs(SessionEnd)detached spawn 调用,或 `toolkit flush` 手动调用。
|
|
5
|
+
* 自包含:零依赖,只用 Node 内置模块。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { deflateRawSync } from 'node:zlib';
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const HOME = os.homedir();
|
|
16
|
+
const TOOLKIT_DIR = path.join(HOME, '.toolkit');
|
|
17
|
+
const CONFIG_PATH = path.join(TOOLKIT_DIR, 'config.json');
|
|
18
|
+
const UPLOADED_PATH = path.join(TOOLKIT_DIR, 'uploaded.json');
|
|
19
|
+
const LOG_DIR = path.join(TOOLKIT_DIR, 'logs');
|
|
20
|
+
const CLAUDE_PROJECTS = path.join(HOME, '.claude', 'projects');
|
|
21
|
+
|
|
22
|
+
// ── 日志 ──────────────────────────────────────────────
|
|
23
|
+
function log(...args) {
|
|
24
|
+
const ts = new Date().toISOString();
|
|
25
|
+
const line = `[${ts}] ${args.join(' ')}\n`;
|
|
26
|
+
try {
|
|
27
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
28
|
+
fs.appendFileSync(path.join(LOG_DIR, `${ts.slice(0, 10)}.log`), line);
|
|
29
|
+
} catch {}
|
|
30
|
+
if (process.env.TOOLKIT_VERBOSE) process.stderr.write(line);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── 读配置 ────────────────────────────────────────────
|
|
34
|
+
function loadConfig() {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
37
|
+
} catch {
|
|
38
|
+
log('ERROR: config.json not found. Run `toolkit install` first.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadUploaded() {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(fs.readFileSync(UPLOADED_PATH, 'utf-8'));
|
|
46
|
+
} catch {
|
|
47
|
+
return { uploaded_sids: [], last_flush: null };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function saveUploaded(state) {
|
|
52
|
+
state.last_flush = new Date().toISOString();
|
|
53
|
+
// 只保留最近 500 个 sid(防膨胀)
|
|
54
|
+
if (state.uploaded_sids.length > 500) {
|
|
55
|
+
state.uploaded_sids = state.uploaded_sids.slice(-500);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
fs.writeFileSync(UPLOADED_PATH, JSON.stringify(state, null, 2));
|
|
59
|
+
} catch (e) {
|
|
60
|
+
log('WARN: cannot save uploaded.json:', e.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 扫描新会话 ────────────────────────────────────────
|
|
65
|
+
function scanNewSessions(uploadedSet, includeKeywords) {
|
|
66
|
+
const found = [];
|
|
67
|
+
if (!fs.existsSync(CLAUDE_PROJECTS)) return found;
|
|
68
|
+
for (const projDir of fs.readdirSync(CLAUDE_PROJECTS)) {
|
|
69
|
+
// project-filter:配了关键词时,项目目录名必须命中至少一个(子串,大小写不敏感)
|
|
70
|
+
if (includeKeywords && includeKeywords.length > 0) {
|
|
71
|
+
const lower = projDir.toLowerCase();
|
|
72
|
+
if (!includeKeywords.some(kw => lower.includes(kw.toLowerCase()))) continue;
|
|
73
|
+
}
|
|
74
|
+
const projPath = path.join(CLAUDE_PROJECTS, projDir);
|
|
75
|
+
let st;
|
|
76
|
+
try { st = fs.statSync(projPath); } catch { continue; }
|
|
77
|
+
if (!st.isDirectory()) continue;
|
|
78
|
+
for (const file of fs.readdirSync(projPath)) {
|
|
79
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
80
|
+
const sid = file.slice(0, -6); // 去掉 .jsonl
|
|
81
|
+
if (uploadedSet.has(sid)) continue;
|
|
82
|
+
const fp = path.join(projPath, file);
|
|
83
|
+
// 跳过空文件(<100 字节,可能是刚创建的)
|
|
84
|
+
try { if (fs.statSync(fp).size < 100) continue; } catch { continue; }
|
|
85
|
+
found.push({ sid, path: fp, project: projDir, name: file });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── 零依赖 zip ────────────────────────────────────────
|
|
92
|
+
// CRC32 查表法
|
|
93
|
+
const CRC_TABLE = (() => {
|
|
94
|
+
const t = new Uint32Array(256);
|
|
95
|
+
for (let i = 0; i < 256; i++) {
|
|
96
|
+
let c = i;
|
|
97
|
+
for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
|
98
|
+
t[i] = c;
|
|
99
|
+
}
|
|
100
|
+
return t;
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
function crc32(buf) {
|
|
104
|
+
let crc = 0xFFFFFFFF;
|
|
105
|
+
for (let i = 0; i < buf.length; i++) {
|
|
106
|
+
crc = CRC_TABLE[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
|
|
107
|
+
}
|
|
108
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createZip(files) {
|
|
112
|
+
// files: [{ name: string, data: Buffer }]
|
|
113
|
+
const localParts = [];
|
|
114
|
+
const centralParts = [];
|
|
115
|
+
let offset = 0;
|
|
116
|
+
|
|
117
|
+
for (const file of files) {
|
|
118
|
+
const data = Buffer.isBuffer(file.data) ? file.data : Buffer.from(file.data);
|
|
119
|
+
const compressed = deflateRawSync(data);
|
|
120
|
+
const crc = crc32(data);
|
|
121
|
+
const nameBuf = Buffer.from(file.name);
|
|
122
|
+
|
|
123
|
+
// Local file header (30 bytes)
|
|
124
|
+
const lh = Buffer.alloc(30);
|
|
125
|
+
lh.writeUInt32LE(0x04034b50, 0);
|
|
126
|
+
lh.writeUInt16LE(20, 4); // version needed
|
|
127
|
+
lh.writeUInt16LE(0, 6); // flags
|
|
128
|
+
lh.writeUInt16LE(8, 8); // compression: deflate
|
|
129
|
+
lh.writeUInt16LE(0, 10); // mod time
|
|
130
|
+
lh.writeUInt16LE(0x0021, 12); // mod date (1980-01-01 safe)
|
|
131
|
+
lh.writeUInt32LE(crc, 14);
|
|
132
|
+
lh.writeUInt32LE(compressed.length, 18);
|
|
133
|
+
lh.writeUInt32LE(data.length, 22);
|
|
134
|
+
lh.writeUInt16LE(nameBuf.length, 26);
|
|
135
|
+
lh.writeUInt16LE(0, 28); // extra length
|
|
136
|
+
|
|
137
|
+
const localPart = Buffer.concat([lh, nameBuf, compressed]);
|
|
138
|
+
localParts.push(localPart);
|
|
139
|
+
|
|
140
|
+
// Central directory header (46 bytes)
|
|
141
|
+
const ch = Buffer.alloc(46);
|
|
142
|
+
ch.writeUInt32LE(0x02014b50, 0);
|
|
143
|
+
ch.writeUInt16LE(20, 4);
|
|
144
|
+
ch.writeUInt16LE(20, 6);
|
|
145
|
+
ch.writeUInt16LE(0, 8);
|
|
146
|
+
ch.writeUInt16LE(8, 10);
|
|
147
|
+
ch.writeUInt16LE(0, 12);
|
|
148
|
+
ch.writeUInt16LE(0x0021, 14);
|
|
149
|
+
ch.writeUInt32LE(crc, 16);
|
|
150
|
+
ch.writeUInt32LE(compressed.length, 20);
|
|
151
|
+
ch.writeUInt32LE(data.length, 24);
|
|
152
|
+
ch.writeUInt16LE(nameBuf.length, 28);
|
|
153
|
+
ch.writeUInt16LE(0, 30); // extra
|
|
154
|
+
ch.writeUInt16LE(0, 32); // comment
|
|
155
|
+
ch.writeUInt16LE(0, 34); // disk
|
|
156
|
+
ch.writeUInt16LE(0, 36); // internal attrs
|
|
157
|
+
ch.writeUInt32LE(0, 38); // external attrs
|
|
158
|
+
ch.writeUInt32LE(offset, 42); // local header offset
|
|
159
|
+
centralParts.push(Buffer.concat([ch, nameBuf]));
|
|
160
|
+
|
|
161
|
+
offset += localPart.length;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// End of central directory (22 bytes)
|
|
165
|
+
const centralBuf = Buffer.concat(centralParts);
|
|
166
|
+
const eocd = Buffer.alloc(22);
|
|
167
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
168
|
+
eocd.writeUInt16LE(0, 4);
|
|
169
|
+
eocd.writeUInt16LE(0, 6);
|
|
170
|
+
eocd.writeUInt16LE(files.length, 8);
|
|
171
|
+
eocd.writeUInt16LE(files.length, 10);
|
|
172
|
+
eocd.writeUInt32LE(centralBuf.length, 12);
|
|
173
|
+
eocd.writeUInt32LE(offset, 16); // central dir offset
|
|
174
|
+
eocd.writeUInt16LE(0, 20);
|
|
175
|
+
|
|
176
|
+
return Buffer.concat([...localParts, centralBuf, eocd]);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── HTTP POST(带重试)────────────────────────────────
|
|
180
|
+
async function postZip(url, zipBuf, uid) {
|
|
181
|
+
const delays = [2000, 4000, 8000]; // 重试间隔
|
|
182
|
+
for (let attempt = 0; attempt <= delays.length; attempt++) {
|
|
183
|
+
try {
|
|
184
|
+
const controller = new AbortController();
|
|
185
|
+
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
186
|
+
const res = await fetch(url, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: { 'Content-Type': 'application/zip', 'X-Uid': uid },
|
|
189
|
+
body: zipBuf,
|
|
190
|
+
signal: controller.signal,
|
|
191
|
+
});
|
|
192
|
+
clearTimeout(timeout);
|
|
193
|
+
if (res.ok) {
|
|
194
|
+
const j = await res.json().catch(() => ({}));
|
|
195
|
+
return j;
|
|
196
|
+
}
|
|
197
|
+
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
|
|
198
|
+
throw new Error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`HTTP ${res.status}`);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
if (attempt < delays.length) {
|
|
203
|
+
const jitter = (Math.random() - 0.5) * delays[attempt] * 0.5;
|
|
204
|
+
const wait = Math.max(1000, delays[attempt] + jitter);
|
|
205
|
+
log(`attempt ${attempt + 1} failed: ${e.message}, retry in ${Math.round(wait)}ms`);
|
|
206
|
+
await new Promise(r => setTimeout(r, wait));
|
|
207
|
+
} else {
|
|
208
|
+
throw e;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── 主流程 ────────────────────────────────────────────
|
|
215
|
+
async function main() {
|
|
216
|
+
const config = loadConfig();
|
|
217
|
+
const { registry, uid } = config;
|
|
218
|
+
const metricsBase = `${registry.replace(/\/$/, '')}${config.contexts?.metrics || '/ai-metrics'}`;
|
|
219
|
+
const uploadUrl = `${metricsBase}/api/upload`;
|
|
220
|
+
|
|
221
|
+
log(`flush start: registry=${registry} uid=${uid.slice(0, 8)}...`);
|
|
222
|
+
|
|
223
|
+
const uploaded = loadUploaded();
|
|
224
|
+
const uploadedSet = new Set(uploaded.uploaded_sids);
|
|
225
|
+
const includeKeywords = config['project-filter'] || [];
|
|
226
|
+
const newSessions = scanNewSessions(uploadedSet, includeKeywords);
|
|
227
|
+
|
|
228
|
+
if (newSessions.length === 0) {
|
|
229
|
+
log('no new sessions to upload');
|
|
230
|
+
saveUploaded(uploaded);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
log(`found ${newSessions.length} new session(s)`);
|
|
235
|
+
|
|
236
|
+
// 按项目分组,每组一次上传(平台按项目分)
|
|
237
|
+
const byProject = new Map();
|
|
238
|
+
for (const s of newSessions) {
|
|
239
|
+
if (!byProject.has(s.project)) byProject.set(s.project, []);
|
|
240
|
+
byProject.get(s.project).push(s);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let totalUploaded = 0;
|
|
244
|
+
for (const [project, sessions] of byProject) {
|
|
245
|
+
log(`uploading project=${project}: ${sessions.length} session(s)`);
|
|
246
|
+
|
|
247
|
+
// 读文件 + 创建 zip
|
|
248
|
+
const zipFiles = [];
|
|
249
|
+
for (const s of sessions) {
|
|
250
|
+
try {
|
|
251
|
+
const data = fs.readFileSync(s.path);
|
|
252
|
+
zipFiles.push({ name: `${project}/${s.name}`, data });
|
|
253
|
+
} catch (e) {
|
|
254
|
+
log(`WARN: cannot read ${s.path}: ${e.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (zipFiles.length === 0) continue;
|
|
258
|
+
|
|
259
|
+
const zipBuf = createZip(zipFiles);
|
|
260
|
+
log(`zip created: ${zipFiles.length} files, ${Math.round(zipBuf.length / 1024)}KB`);
|
|
261
|
+
|
|
262
|
+
// 上传
|
|
263
|
+
try {
|
|
264
|
+
const result = await postZip(uploadUrl, zipBuf, uid);
|
|
265
|
+
log(`upload OK: dataset=${result.dataset_id}, sessions=${result.files || '?'}`);
|
|
266
|
+
for (const s of sessions) {
|
|
267
|
+
uploadedSet.add(s.sid);
|
|
268
|
+
uploaded.uploaded_sids = [...uploadedSet];
|
|
269
|
+
}
|
|
270
|
+
totalUploaded += sessions.length;
|
|
271
|
+
} catch (e) {
|
|
272
|
+
log(`upload FAILED for ${project}: ${e.message}`);
|
|
273
|
+
// 不更新 uploaded — 下次重试
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
saveUploaded(uploaded);
|
|
278
|
+
log(`flush done: ${totalUploaded}/${newSessions.length} uploaded`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
main().catch(e => {
|
|
282
|
+
log(`FATAL: ${e.message}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
});
|
package/src/hook.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hook.mjs — Claude Code SessionEnd hook 插件入口。
|
|
4
|
+
* 职责:detached spawn flush.mjs → exit(0)。
|
|
5
|
+
* 不读 stdin、不开网、不写文件。<10ms,远在 1.5s 超时内。
|
|
6
|
+
*
|
|
7
|
+
* Claude Code 通过 hooks.json (exec form) 调用:
|
|
8
|
+
* command: "node"
|
|
9
|
+
* args: ["${CLAUDE_PLUGIN_ROOT}/hook.mjs"]
|
|
10
|
+
*
|
|
11
|
+
* ${CLAUDE_PLUGIN_ROOT} 被 Claude Code 替换为插件目录路径,
|
|
12
|
+
* 同时 export 为环境变量 process.env.CLAUDE_PLUGIN_ROOT。
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const flushScript = path.join(__dirname, 'flush.mjs');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
spawn(process.execPath, [flushScript], {
|
|
23
|
+
detached: true,
|
|
24
|
+
stdio: 'ignore',
|
|
25
|
+
env: { ...process.env, TOOLKIT_AUTO: '1' },
|
|
26
|
+
}).unref();
|
|
27
|
+
} catch {
|
|
28
|
+
// 永不抛错:flusher 没起来,下次 SessionEnd 或手动 toolkit flush 补传
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process.exit(0);
|
package/src/install.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* install.mjs — `toolkit install claudecode --registry <url>`
|
|
4
|
+
* 创建 Claude Code 插件(~/.claude/plugins/toolkit/)+ 配置(~/.toolkit/)+ 启用。
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const HOME = os.homedir();
|
|
14
|
+
const PLUGIN_DIR = path.join(HOME, '.claude', 'plugins', 'toolkit');
|
|
15
|
+
const TOOLKIT_DIR = path.join(HOME, '.toolkit');
|
|
16
|
+
const SETTINGS_PATH = path.join(HOME, '.claude', 'settings.json');
|
|
17
|
+
const PLUGIN_KEY = 'toolkit@local';
|
|
18
|
+
|
|
19
|
+
const PLUGIN_JSON = {
|
|
20
|
+
name: 'toolkit',
|
|
21
|
+
version: '0.1.0',
|
|
22
|
+
description: '研发工具集 — 会话度量、技能同步等',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const HOOKS_JSON = {
|
|
26
|
+
hooks: {
|
|
27
|
+
SessionEnd: [{
|
|
28
|
+
hooks: [{
|
|
29
|
+
type: 'command',
|
|
30
|
+
command: 'node',
|
|
31
|
+
args: ['${CLAUDE_PLUGIN_ROOT}/hook.mjs'],
|
|
32
|
+
async: false,
|
|
33
|
+
}],
|
|
34
|
+
}],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function install(registry, opts = {}) {
|
|
39
|
+
// 1. 检测 Claude Code
|
|
40
|
+
const claudeDir = path.join(HOME, '.claude');
|
|
41
|
+
if (!fs.existsSync(claudeDir)) {
|
|
42
|
+
console.error('ERROR: ~/.claude not found. Is Claude Code installed?');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. 创建插件目录
|
|
47
|
+
fs.mkdirSync(path.join(PLUGIN_DIR, '.claude-plugin'), { recursive: true });
|
|
48
|
+
fs.mkdirSync(path.join(PLUGIN_DIR, 'hooks'), { recursive: true });
|
|
49
|
+
|
|
50
|
+
// 3. 写插件文件
|
|
51
|
+
fs.writeFileSync(
|
|
52
|
+
path.join(PLUGIN_DIR, '.claude-plugin', 'plugin.json'),
|
|
53
|
+
JSON.stringify(PLUGIN_JSON, null, 2)
|
|
54
|
+
);
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(PLUGIN_DIR, 'hooks', 'hooks.json'),
|
|
57
|
+
JSON.stringify(HOOKS_JSON, null, 2)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// 4. 复制 hook.mjs + flush.mjs 到插件目录(自包含)
|
|
61
|
+
const srcDir = __dirname;
|
|
62
|
+
fs.copyFileSync(path.join(srcDir, 'hook.mjs'), path.join(PLUGIN_DIR, 'hook.mjs'));
|
|
63
|
+
fs.copyFileSync(path.join(srcDir, 'flush.mjs'), path.join(PLUGIN_DIR, 'flush.mjs'));
|
|
64
|
+
|
|
65
|
+
// 5. 创建 ~/.toolkit/ 配置
|
|
66
|
+
fs.mkdirSync(TOOLKIT_DIR, { recursive: true });
|
|
67
|
+
fs.mkdirSync(path.join(TOOLKIT_DIR, 'logs'), { recursive: true });
|
|
68
|
+
|
|
69
|
+
// 读取已有 config(保留 uid)或新建
|
|
70
|
+
const configPath = path.join(TOOLKIT_DIR, 'config.json');
|
|
71
|
+
let config = {};
|
|
72
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch {}
|
|
73
|
+
if (!config.uid) config.uid = randomUUID();
|
|
74
|
+
config.registry = registry.replace(/\/$/, '');
|
|
75
|
+
config.contexts = { metrics: '/ai-metrics', skills: '/skillhub' };
|
|
76
|
+
config.installed_at = new Date().toISOString();
|
|
77
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
78
|
+
|
|
79
|
+
// 6. uploaded.json 初始化(如果不存在)
|
|
80
|
+
const uploadedPath = path.join(TOOLKIT_DIR, 'uploaded.json');
|
|
81
|
+
if (!fs.existsSync(uploadedPath)) {
|
|
82
|
+
fs.writeFileSync(uploadedPath, JSON.stringify({ uploaded_sids: [], last_flush: null }, null, 2));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 7. 启用插件(settings.json)
|
|
86
|
+
let settings = {};
|
|
87
|
+
try { settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
88
|
+
if (!settings.enabledPlugins) settings.enabledPlugins = {};
|
|
89
|
+
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
90
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
91
|
+
|
|
92
|
+
// 8. 输出
|
|
93
|
+
console.log('✅ 安装完成。\n');
|
|
94
|
+
console.log(` 插件目录: ${PLUGIN_DIR}`);
|
|
95
|
+
console.log(` 配置文件: ${configPath}`);
|
|
96
|
+
console.log(` 身份 UID: ${config.uid.slice(0, 12)}...`);
|
|
97
|
+
console.log(` 平台地址: ${config.registry}${config.contexts.metrics}/`);
|
|
98
|
+
console.log(`\n Claude Code 会话结束后,数据自动上报到上述地址。`);
|
|
99
|
+
console.log(` 手动触发: toolkit flush`);
|
|
100
|
+
console.log(` 查看状态: toolkit status`);
|
|
101
|
+
}
|
package/src/status.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* status.mjs — `toolkit status`
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
|
|
9
|
+
const HOME = os.homedir();
|
|
10
|
+
const PLUGIN_DIR = path.join(HOME, '.claude', 'plugins', 'toolkit');
|
|
11
|
+
const TOOLKIT_DIR = path.join(HOME, '.toolkit');
|
|
12
|
+
|
|
13
|
+
export function status() {
|
|
14
|
+
const pluginExists = fs.existsSync(path.join(PLUGIN_DIR, '.claude-plugin', 'plugin.json'));
|
|
15
|
+
const configExists = fs.existsSync(path.join(TOOLKIT_DIR, 'config.json'));
|
|
16
|
+
|
|
17
|
+
console.log('Toolkit Status');
|
|
18
|
+
console.log('════════════════════════════════════════');
|
|
19
|
+
|
|
20
|
+
if (!pluginExists && !configExists) {
|
|
21
|
+
console.log(' ❌ 未安装。运行: toolkit install claudecode --registry <url>');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 插件状态
|
|
26
|
+
console.log(` 插件: ${pluginExists ? '✅ 已安装' : '❌ 缺失'}`);
|
|
27
|
+
if (pluginExists) {
|
|
28
|
+
const pj = JSON.parse(fs.readFileSync(path.join(PLUGIN_DIR, '.claude-plugin', 'plugin.json'), 'utf-8'));
|
|
29
|
+
console.log(` 版本: ${pj.version}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 配置
|
|
33
|
+
if (configExists) {
|
|
34
|
+
const config = JSON.parse(fs.readFileSync(path.join(TOOLKIT_DIR, 'config.json'), 'utf-8'));
|
|
35
|
+
console.log(` 平台地址: ${config.registry}${config.contexts?.metrics || '/ai-metrics'}/`);
|
|
36
|
+
console.log(` 身份 UID: ${config.uid?.slice(0, 12)}...`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 上传状态
|
|
40
|
+
const uploadedPath = path.join(TOOLKIT_DIR, 'uploaded.json');
|
|
41
|
+
try {
|
|
42
|
+
const up = JSON.parse(fs.readFileSync(uploadedPath, 'utf-8'));
|
|
43
|
+
console.log(` 已传会话: ${up.uploaded_sids?.length || 0} 个`);
|
|
44
|
+
console.log(` 上次上传: ${up.last_flush || '从未'}`);
|
|
45
|
+
} catch {
|
|
46
|
+
console.log(' 上传状态: 未初始化');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 待传会话计数
|
|
50
|
+
const projectsDir = path.join(HOME, '.claude', 'projects');
|
|
51
|
+
let pending = 0;
|
|
52
|
+
if (fs.existsSync(projectsDir) && configExists) {
|
|
53
|
+
const up = JSON.parse(fs.readFileSync(uploadedPath, 'utf-8'));
|
|
54
|
+
const uploadedSet = new Set(up.uploaded_sids || []);
|
|
55
|
+
for (const projDir of fs.readdirSync(projectsDir)) {
|
|
56
|
+
const pp = path.join(projectsDir, projDir);
|
|
57
|
+
try {
|
|
58
|
+
if (!fs.statSync(pp).isDirectory()) continue;
|
|
59
|
+
for (const f of fs.readdirSync(pp)) {
|
|
60
|
+
if (f.endsWith('.jsonl') && !uploadedSet.has(f.slice(0, -6))) pending++;
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.log(` 待传会话: ${pending} 个`);
|
|
66
|
+
console.log('════════════════════════════════════════');
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* uninstall.mjs — `toolkit uninstall`
|
|
4
|
+
* 删插件目录 + 移除 settings.json enabledPlugins 条目 + 可选保留 ~/.toolkit/ 数据。
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
|
|
10
|
+
const HOME = os.homedir();
|
|
11
|
+
const PLUGIN_DIR = path.join(HOME, '.claude', 'plugins', 'toolkit');
|
|
12
|
+
const SETTINGS_PATH = path.join(HOME, '.claude', 'settings.json');
|
|
13
|
+
const PLUGIN_KEY = 'toolkit@local';
|
|
14
|
+
|
|
15
|
+
function rmrf(dir) {
|
|
16
|
+
if (!fs.existsSync(dir)) return;
|
|
17
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
18
|
+
const p = path.join(dir, entry);
|
|
19
|
+
try {
|
|
20
|
+
if (fs.statSync(p).isDirectory()) rmrf(p);
|
|
21
|
+
else fs.unlinkSync(p);
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
try { fs.rmdirSync(dir); } catch {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function uninstall(opts = {}) {
|
|
28
|
+
// 1. 删插件目录
|
|
29
|
+
rmrf(PLUGIN_DIR);
|
|
30
|
+
console.log('✅ 插件目录已删除');
|
|
31
|
+
|
|
32
|
+
// 2. 移除 settings.json enabledPlugins
|
|
33
|
+
try {
|
|
34
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
35
|
+
if (settings.enabledPlugins?.[PLUGIN_KEY] !== undefined) {
|
|
36
|
+
delete settings.enabledPlugins[PLUGIN_KEY];
|
|
37
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
38
|
+
console.log('✅ settings.json enabledPlugins 已移除');
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
|
|
42
|
+
// 3. 数据(~/.toolkit/)
|
|
43
|
+
if (opts.purge) {
|
|
44
|
+
rmrf(path.join(HOME, '.toolkit'));
|
|
45
|
+
console.log('✅ ~/.toolkit/ 数据已清除');
|
|
46
|
+
} else {
|
|
47
|
+
console.log('ℹ️ ~/.toolkit/ 数据保留(含 UID + 上传记录)。');
|
|
48
|
+
console.log(' 彻底清除: toolkit uninstall --purge');
|
|
49
|
+
}
|
|
50
|
+
}
|