left-skills 0.1.9 → 0.3.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/README.md CHANGED
@@ -2,89 +2,100 @@
2
2
 
3
3
  # left-skills
4
4
 
5
- **shift-left for skills** — skill 投入使用前就度量它用没用,而不是等用户撞 bug。
5
+ **shift-left for skills** — measure how your AI skills are used before users hit bugs.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/left-skills)](https://www.npmjs.com/package/left-skills)
8
8
  [![license](https://img.shields.io/npm/l/left-skills)](./LICENSE)
9
9
  [![stars](https://img.shields.io/github/stars/cuikexi/left-skills)](https://github.com/cuikexi/left-skills)
10
10
 
11
+ [中文](./README.zh.md)
12
+
11
13
  </div>
12
14
 
13
- ## 命名由来
15
+ ## Why "left-skills"?
14
16
 
15
- `left-skills` = **shift-left for skills**。把质量/度量左移到"投入使用前"——你写完 skill,先看它用没用、写得好不好,而不是等用户撞 bug 才发现。
17
+ `left-skills` = **shift-left for skills**. Move quality/measurement "left" — to *before* a skill is put into use — instead of waiting for users to hit bugs. Know which skills never get called, fix them early.
16
18
 
17
19
  ## How It Works
18
20
 
19
21
  ```
20
- /skill AI skill
21
- │ Claude Code hook 触发
22
+ type /skill or AI invokes a skill
23
+ │ Claude Code hook fires
22
24
 
23
- left-skills hook(三数据源)
24
- - UserPromptExpansion.command_name (手动 /slash)
25
- - PreToolUse.tool_input.skill (AI Skill 工具)
26
- - UserPromptSubmit.prompt (自然语言提及)
27
- 解析 payload,取 skill
25
+ left-skills hook (3 sources)
26
+ - UserPromptExpansion.command_name (manual /slash)
27
+ - PreToolUse.tool_input.skill (AI invokes Skill tool)
28
+ - UserPromptSubmit.prompt (natural-language mention)
29
+ parse payload, extract skill name
28
30
 
29
- ~/.left-skills/usage.json (append 记录)
31
+ ~/.left-skills/usage.json (append records)
30
32
 
31
33
 
32
- left-skills usage 报告(手动/AI/提及 分开 + 从未调用 ⚠)
34
+ left-skills usage report (manual / AI / mention split + never-called ⚠)
33
35
  ```
34
36
 
35
37
  ## Installation
36
38
 
37
39
  ```bash
38
40
  npm i -g left-skills
39
- left-skills install # 输出 hook 片段,复制进 ~/.claude/settings.json hooks 字段
41
+ left-skills install --write # auto-config hooks into ~/.claude/settings.json (merge + backup .bak)
40
42
  ```
41
43
 
42
- 详见 [docs/install.md](docs/install.md)
44
+ > Don't want auto-write? `left-skills install` prints a snippet to add manually. See [docs/install.md](docs/install.md).
45
+
46
+ ## Uninstall
47
+
48
+ ```bash
49
+ left-skills uninstall # remove hooks (backup .bak)
50
+ npm uninstall -g left-skills # remove binary
51
+ ```
43
52
 
44
53
  ## Quick Start
45
54
 
46
55
  ```bash
47
- left-skills usage # 人看报告
48
- left-skills usage --json # AI (JSON)
49
- left-skills usage --since 7 # 7
56
+ left-skills usage # human-readable report
57
+ left-skills usage --json # for AI (JSON)
58
+ left-skills usage --since 7 # last 7 days
59
+ left-skills doctor # diagnose install (✓/✗ + fix)
60
+ left-skills report --markdown > report.md # export report
50
61
  ```
51
62
 
52
- 报告示例:
63
+ Example:
53
64
 
54
65
  ```
55
- skill 调用报告(14 个 skill)
66
+ skill usage report (14 skills)
56
67
  ────────────────────────────────────
57
- 3 grill-me (手动1 + AI1 + 提及1,最近 今天)
58
- 0 gitlab-ci-generate ⚠ 从未调用
68
+ 3 grill-me (manual1 + AI1 + mention1, last today)
69
+ 0 gitlab-ci-generate ⚠ never called
59
70
  ```
60
71
 
61
72
  ## Highlights
62
73
 
63
- - **三数据源 hook 埋点**:手动 /slashAI Skill、自然语言提及,分开记
64
- - **诚实报告**:手动/AI/提及 分开标(不混"调用次数"),从未调用
65
- - **给 AI 用**:`--json` 输出,AI 能解析决策(改/删 skill)
66
- - **单文件 bundle**:TS 打包单 `.js`,随 skill 生态分发
74
+ - **3-source hook tracking**: manual `/slash`, AI Skill-tool invoke, natural-language mention — tracked separately
75
+ - **Honest reporting**: manual/AI/mention split (no fake "call count"), never-called
76
+ - **For AI use**: `--json` output, AI can parse to decide (improve/delete skill)
77
+ - **Single-file bundle**: TS bundled to one `.js`, ships via skill ecosystem
67
78
 
68
79
  ## Limitations
69
80
 
70
- - **Claude Code**(Cursor / Codex 后扩)
71
- - **AI progressive disclosure 自主激活**(不走 Skill 工具)抓不到——只手动 /slash + AI Skill 工具 + 自然语言提及
72
- - 需装 hook 配置(用户加 settings.json)
73
- - MVP (早期,API 可能变)
81
+ - **Claude Code only** (Cursor / Codex later)
82
+ - **AI pure progressive-disclosure auto-activation** (not via Skill tool) can't be tracked — only manual `/slash` + AI Skill-tool + natural-language mention
83
+ - Requires hook config (user adds to settings.json)
84
+ - MVP stage (early, API may change)
74
85
 
75
86
  ## Contributing
76
87
 
77
- PR 欢迎。开发:
88
+ PRs welcome. Dev setup:
78
89
 
79
90
  ```bash
80
91
  git clone https://github.com/cuikexi/left-skills
81
92
  cd left-skills
82
93
  npm install
83
94
  npm run build
84
- npm link # 本地 left-skills PATH
95
+ npm link # local left-skills into PATH
85
96
  ```
86
97
 
87
- PR 前 `npm run build` 确保 dist 最新。理念 / roadmap [docs/roadmap.md](docs/roadmap.md)
98
+ Run `npm run build` before PR to ensure dist is fresh. Philosophy / roadmap see [docs/roadmap.md](docs/roadmap.md).
88
99
 
89
100
  ## Star History
90
101
 
package/README.zh.md ADDED
@@ -0,0 +1,106 @@
1
+ <div align="center">
2
+
3
+ # left-skills
4
+
5
+ **shift-left for skills** — 在 skill 投入使用前就度量它用没用,而不是等用户撞 bug。
6
+
7
+ [![npm version](https://img.shields.io/npm/v/left-skills)](https://www.npmjs.com/package/left-skills)
8
+ [![license](https://img.shields.io/npm/l/left-skills)](./LICENSE)
9
+ [![stars](https://img.shields.io/github/stars/cuikexi/left-skills)](https://github.com/cuikexi/left-skills)
10
+
11
+ [English](./README.md)
12
+
13
+ </div>
14
+
15
+ ## 命名由来
16
+
17
+ `left-skills` = **shift-left for skills**。把质量/度量左移到"投入使用前"——你写完 skill,先看它用没用、写得好不好,而不是等用户撞 bug 才发现。
18
+
19
+ ## How It Works
20
+
21
+ ```
22
+ 打 /skill 或 AI 调 skill
23
+ │ Claude Code hook 触发
24
+
25
+ left-skills hook(三数据源)
26
+ - UserPromptExpansion.command_name (手动 /slash)
27
+ - PreToolUse.tool_input.skill (AI 调 Skill 工具)
28
+ - UserPromptSubmit.prompt (自然语言提及)
29
+ │ 解析 payload,取 skill 名
30
+
31
+ ~/.left-skills/usage.json (append 记录)
32
+
33
+
34
+ left-skills usage 报告(手动/AI/提及 分开 + 从未调用 ⚠)
35
+ ```
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm i -g left-skills
41
+ left-skills install --write # 自动配 hook 到 ~/.claude/settings.json(合并去重 + 备份 .bak)
42
+ ```
43
+
44
+ > 不想自动写?`left-skills install` 输出片段手动加。详见 [docs/install.md](docs/install.md)。
45
+
46
+ ## 卸载
47
+
48
+ ```bash
49
+ left-skills uninstall # 删 hook(备份 .bak)
50
+ npm uninstall -g left-skills # 卸 binary
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ```bash
56
+ left-skills usage # 人看报告
57
+ left-skills usage --json # AI 用(JSON)
58
+ left-skills usage --since 7 # 近 7 天
59
+ left-skills doctor # 诊断安装(✓/✗ + 建议)
60
+ left-skills report --markdown > report.md # 导出报告
61
+ ```
62
+
63
+ 报告示例:
64
+
65
+ ```
66
+ skill 调用报告(14 个 skill)
67
+ ────────────────────────────────────
68
+ 3 grill-me (手动1 + AI1 + 提及1,最近 今天)
69
+ 0 gitlab-ci-generate ⚠ 从未调用
70
+ ```
71
+
72
+ ## Highlights
73
+
74
+ - **三数据源 hook 埋点**:手动 `/slash`、AI 调 Skill、自然语言提及,分开记
75
+ - **诚实报告**:手动/AI/提及 分开标(不混"调用次数"),从未调用 ⚠
76
+ - **给 AI 用**:`--json` 输出,AI 能解析决策(改/删 skill)
77
+ - **单文件 bundle**:TS 打包单 `.js`,随 skill 生态分发
78
+
79
+ ## Limitations
80
+
81
+ - 只 **Claude Code**(Cursor / Codex 后扩)
82
+ - **AI 纯 progressive disclosure 自主激活**(不走 Skill 工具)抓不到——只手动 `/slash` + AI 调 Skill 工具 + 自然语言提及
83
+ - 需装 hook 配置(用户加 settings.json)
84
+ - MVP 期(早期,API 可能变)
85
+
86
+ ## Contributing
87
+
88
+ PR 欢迎。开发:
89
+
90
+ ```bash
91
+ git clone https://github.com/cuikexi/left-skills
92
+ cd left-skills
93
+ npm install
94
+ npm run build
95
+ npm link # 本地 left-skills 进 PATH
96
+ ```
97
+
98
+ 提 PR 前 `npm run build` 确保 dist 最新。理念 / roadmap 见 [docs/roadmap.md](docs/roadmap.md)。
99
+
100
+ ## Star History
101
+
102
+ [![Star History Chart](https://api.star-history.com/svg?repos=cuikexi/left-skills&type=Date)](https://star-history.com/#cuikexi/left-skills&date)
103
+
104
+ ## License
105
+
106
+ [MIT](./LICENSE)
package/dist/cli.js CHANGED
@@ -3201,6 +3201,22 @@ function relativeTime(iso) {
3201
3201
  if (days === 1) return "1 \u5929\u524D";
3202
3202
  return `${days} \u5929\u524D`;
3203
3203
  }
3204
+ function formatMarkdown(report) {
3205
+ const lines = [];
3206
+ lines.push(`# left-skills usage report`);
3207
+ lines.push("");
3208
+ lines.push(`Generated: ${report.generated_at} | Skills: ${report.skills.length}`);
3209
+ lines.push("");
3210
+ lines.push("| skill | manual | AI | mention | total | last |");
3211
+ lines.push("|---|---|---|---|---|---|");
3212
+ for (const s of report.skills) {
3213
+ const total = s.manual + s.ai + s.mention;
3214
+ const last = s.last_used ? relativeTime(s.last_used) : "-";
3215
+ const mark = total === 0 ? " \u26A0" : "";
3216
+ lines.push(`| ${s.name}${mark} | ${s.manual} | ${s.ai} | ${s.mention} | ${total} | ${last} |`);
3217
+ }
3218
+ return lines.join("\n");
3219
+ }
3204
3220
 
3205
3221
  // src/install.ts
3206
3222
  var import_node_fs3 = require("fs");
@@ -3248,14 +3264,88 @@ function writeHooksToSettings(settingsPath) {
3248
3264
  }
3249
3265
  (0, import_node_fs3.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
3250
3266
  }
3267
+ function removeHooksFromSettings(settingsPath) {
3268
+ if (!(0, import_node_fs3.existsSync)(settingsPath)) return;
3269
+ (0, import_node_fs3.copyFileSync)(settingsPath, settingsPath + ".bak");
3270
+ let settings = {};
3271
+ try {
3272
+ settings = JSON.parse((0, import_node_fs3.readFileSync)(settingsPath, "utf-8"));
3273
+ } catch {
3274
+ return;
3275
+ }
3276
+ if (!settings.hooks) return;
3277
+ for (const event of Object.keys(settings.hooks)) {
3278
+ settings.hooks[event] = settings.hooks[event].filter((entry) => {
3279
+ entry.hooks = (entry.hooks || []).filter((h) => !(h.command && h.command.includes("left-skills")));
3280
+ return entry.hooks.length > 0;
3281
+ });
3282
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
3283
+ }
3284
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
3285
+ (0, import_node_fs3.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
3286
+ }
3251
3287
  function globalSettingsPath() {
3252
3288
  return (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".claude", "settings.json");
3253
3289
  }
3254
3290
 
3291
+ // src/doctor.ts
3292
+ var import_node_fs4 = require("fs");
3293
+ var import_node_os4 = require("os");
3294
+ var import_node_path4 = require("path");
3295
+ var import_node_child_process = require("child_process");
3296
+ function checkBinary() {
3297
+ try {
3298
+ const path = (0, import_node_child_process.execSync)("which left-skills", { encoding: "utf-8" }).trim();
3299
+ return { name: "binary in PATH", ok: true, detail: path };
3300
+ } catch {
3301
+ return { name: "binary in PATH", ok: false, detail: "left-skills \u547D\u4EE4\u627E\u4E0D\u5230", fix: "npm i -g left-skills" };
3302
+ }
3303
+ }
3304
+ function checkHook() {
3305
+ const settingsPath = (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".claude", "settings.json");
3306
+ if (!(0, import_node_fs4.existsSync)(settingsPath)) {
3307
+ return { name: "hook in settings", ok: false, detail: "~/.claude/settings.json \u4E0D\u5B58\u5728", fix: "left-skills install --write" };
3308
+ }
3309
+ try {
3310
+ const settings = JSON.parse((0, import_node_fs4.readFileSync)(settingsPath, "utf-8"));
3311
+ const hooks = settings.hooks || {};
3312
+ const hasLeft = Object.values(hooks).some(
3313
+ (entries) => entries.some((e) => (e.hooks || []).some((h) => h.command && h.command.includes("left-skills")))
3314
+ );
3315
+ return hasLeft ? { name: "hook in settings", ok: true, detail: "~/.claude/settings.json \u6709 left-skills hook" } : { name: "hook in settings", ok: false, detail: "settings.json \u65E0 left-skills hook", fix: "left-skills install --write" };
3316
+ } catch {
3317
+ return { name: "hook in settings", ok: false, detail: "settings.json \u89E3\u6790\u5931\u8D25", fix: "\u68C0\u67E5 ~/.claude/settings.json JSON" };
3318
+ }
3319
+ }
3320
+ function checkUsageFile() {
3321
+ const usagePath = (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".left-skills", "usage.json");
3322
+ return (0, import_node_fs4.existsSync)(usagePath) ? { name: "usage.json", ok: true, detail: "~/.left-skills/usage.json \u5B58\u5728" } : { name: "usage.json", ok: false, detail: "~/.left-skills/usage.json \u4E0D\u5B58\u5728(\u9996\u6B21\u7528\u540E\u521B\u5EFA)", fix: "\u6253 /skill \u6216 AI \u8C03 skill \u89E6\u53D1 hook \u8BB0\u5F55" };
3323
+ }
3324
+ function checkHookCanRun() {
3325
+ try {
3326
+ (0, import_node_child_process.execSync)('echo "{}" | left-skills hook UserPromptExpansion', { encoding: "utf-8", stdio: "pipe" });
3327
+ return { name: "hook command runs", ok: true, detail: "left-skills hook \u80FD\u6267\u884C" };
3328
+ } catch {
3329
+ return { name: "hook command runs", ok: false, detail: "left-skills hook \u547D\u4EE4\u6267\u884C\u5931\u8D25", fix: "\u68C0\u67E5 left-skills binary / node" };
3330
+ }
3331
+ }
3332
+ function checkNodeVersion() {
3333
+ try {
3334
+ const ver = (0, import_node_child_process.execSync)("node --version", { encoding: "utf-8" }).trim();
3335
+ const major = parseInt(ver.slice(1).split(".")[0], 10);
3336
+ return major >= 18 ? { name: "node version", ok: true, detail: `${ver} (\u226518)` } : { name: "node version", ok: false, detail: `${ver} (<18)`, fix: "\u5347\u7EA7 node \u5230 18+" };
3337
+ } catch {
3338
+ return { name: "node version", ok: false, detail: "node \u547D\u4EE4\u627E\u4E0D\u5230", fix: "\u88C5 node 18+" };
3339
+ }
3340
+ }
3341
+ function runDoctor() {
3342
+ return [checkBinary(), checkHook(), checkUsageFile(), checkHookCanRun(), checkNodeVersion()];
3343
+ }
3344
+
3255
3345
  // package.json
3256
3346
  var package_default = {
3257
3347
  name: "left-skills",
3258
- version: "0.1.9",
3348
+ version: "0.3.0",
3259
3349
  description: "\u7ED9 AI \u7528\u7684 skill \u751F\u547D\u5468\u671F\u7BA1\u7406\u5DE5\u5177 \u2014 MVP: skill \u8C03\u7528\u4F7F\u7528\u7EDF\u8BA1",
3260
3350
  bin: {
3261
3351
  "left-skills": "./dist/cli.js"
@@ -3309,6 +3399,19 @@ program2.command("usage").description("skill \u8C03\u7528\u4F7F\u7528\u62A5\u544
3309
3399
  console.log(formatHuman(report));
3310
3400
  }
3311
3401
  });
3402
+ program2.command("report").description("\u5BFC\u51FA usage \u62A5\u544A markdown(\u53EF > report.md \u5206\u4EAB)").option("--markdown", "\u8F93\u51FA markdown(\u9ED8\u8BA4\u5373 markdown)").option("--since <days>", "\u65F6\u95F4\u7A97\u53E3(\u5929,\u9ED8\u8BA4 30)", "30").action((opts) => {
3403
+ const since = parseInt(opts.since, 10) || 30;
3404
+ const report = buildReport(since);
3405
+ console.log(formatMarkdown(report));
3406
+ });
3407
+ program2.command("doctor").description("\u8BCA\u65AD left-skills \u5B89\u88C5/hook \u914D\u7F6E(\u2713/\u2717 + \u4FEE\u590D\u5EFA\u8BAE)").action(() => {
3408
+ const results = runDoctor();
3409
+ for (const r of results) {
3410
+ const mark = r.ok ? "\u2713" : "\u2717";
3411
+ console.log(`${mark} ${r.name}: ${r.detail}`);
3412
+ if (r.fix) console.log(` \u2192 \u4FEE\u590D: ${r.fix}`);
3413
+ }
3414
+ });
3312
3415
  program2.command("install").description("\u8F93\u51FA hook \u914D\u7F6E\u7247\u6BB5(\u9ED8\u8BA4)\u6216 --write \u81EA\u52A8\u5199\u8FDB ~/.claude/settings.json(\u5408\u5E76+\u5907\u4EFD .bak)").option("--write", "\u81EA\u52A8\u5199 hook \u5230 settings.json(\u5408\u5E76\u53BB\u91CD + \u5907\u4EFD .bak)", false).action((opts) => {
3313
3416
  if (opts.write) {
3314
3417
  const path = globalSettingsPath();
@@ -3320,6 +3423,12 @@ program2.command("install").description("\u8F93\u51FA hook \u914D\u7F6E\u7247\u6
3320
3423
  console.log("\n# \u628A\u4E0A\u9762\u7247\u6BB5\u52A0\u8FDB ~/.claude/settings.json \u7684 hooks \u5B57\u6BB5,\u6216\u8DD1 left-skills install --write \u81EA\u52A8\u5199");
3321
3424
  }
3322
3425
  });
3426
+ program2.command("uninstall").description("\u5220 ~/.claude/settings.json \u7684 left-skills hook(\u5E72\u51C0\u5378\u8F7D,\u5907\u4EFD .bak)").action(() => {
3427
+ const path = globalSettingsPath();
3428
+ removeHooksFromSettings(path);
3429
+ console.log(`\u2713 left-skills hook \u5DF2\u4ECE ${path} \u5220\u9664(\u5DF2\u5907\u4EFD .bak)`);
3430
+ console.log(" \u518D\u8DD1 npm uninstall -g left-skills \u5378\u8F7D binary");
3431
+ });
3323
3432
  program2.command("hook <event>").description("hook \u5165\u53E3(\u8BFB stdin payload)").action(async (event) => {
3324
3433
  const payload = await readStdinPayload();
3325
3434
  if (!payload) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "left-skills",
3
- "version": "0.1.9",
3
+ "version": "0.3.0",
4
4
  "description": "给 AI 用的 skill 生命周期管理工具 — MVP: skill 调用使用统计",
5
5
  "bin": {
6
6
  "left-skills": "./dist/cli.js"