@punk6529/playbook 0.2.0 → 0.2.2

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
@@ -1,6 +1,38 @@
1
1
  # @punk6529/playbook
2
2
 
3
- 结构化工程知识库 CLI —— 初始化、更新和查询项目 Playbook 资产。
3
+ 把团队踩过的坑,沉淀成 AI 可读、可查、可复用的工程记忆。
4
+
5
+ `@punk6529/playbook` 是一个结构化工程知识库 CLI,用于在项目中快速建立并维护 Playbook 资产(cases / patterns / checklists + INDEX)。
6
+
7
+ 它不只是“记笔记工具”,而是把“个人经验”变成“团队可复用资产”的最小基础设施:
8
+
9
+ - 解决问题后可立即沉淀为 case,避免同类问题反复排查
10
+ - AI 可以按 tag 精准检索历史经验,而不是每次从零猜测
11
+ - 支持项目级和全局级知识库,单项目沉淀可逐步升级为跨项目最佳实践
12
+ - 通过结构化索引 + 按需读取,控制上下文体积,降低 token 浪费
13
+
14
+ ## 这个工具适合谁
15
+
16
+ - 希望减少重复踩坑的个人开发者
17
+ - 需要把故障处置经验标准化的技术团队
18
+ - 在 Codex / Claude 工作流中,希望让 AI“带着历史经验工作”的团队
19
+
20
+ ## 你会获得什么
21
+
22
+ - 一套开箱即用的知识库目录结构(项目级 + 全局级)
23
+ - 三个可直接使用的 AI skills:
24
+ - `/playbook-advisor`:会话内主动顾问,自动按需查询知识库
25
+ - `/playbook-query`:手动按标签检索经验
26
+ - `/playbook-case`:引导式记录新 case,并更新索引
27
+ - 一套稳定的 CLI 工作流(init / global init / update / query),便于持续演进而不破坏现有内容
28
+
29
+ ## 典型工作流
30
+
31
+ 1. `playbook init` 初始化项目知识库与 AI skills (新项目一次即可)
32
+ 2. `playbook global init` 初始化全局知识库(一次即可)
33
+ 3. 新会话开始先调用 `/playbook-advisor`,AI 会列出现有 tags;后续遇到问题时,会按相关 tags 查询案例 summary,并选取最相关案例查看详情
34
+ 4. 问题解决后,用 `/playbook-case` 沉淀经验并更新索引
35
+ 5. 在多个项目间通过全局仓库复用高价值模式
4
36
 
5
37
  ## 安装
6
38
 
@@ -14,65 +46,44 @@ npm install -g @punk6529/playbook
14
46
  npx @punk6529/playbook --help
15
47
  ```
16
48
 
17
- ## 命令
49
+ ## 用户命令
18
50
 
19
- ```bash
20
- playbook init [path] [--tools all|none|codex,claude] [--force]
21
- playbook global init [--force]
22
- playbook update [path] [--tools all|none|codex,claude] [--force]
23
- playbook query [tags...] [--limit N] [--scope project|global|both] [--json] [--tags-only]
24
- ```
51
+ 日常使用的命令:
25
52
 
26
- ## 作用范围
53
+ ### `playbook init`
27
54
 
28
- - `playbook init` 项目级:
29
- - 创建 `docs/playbook/{cases,patterns,checklists}` 和 `docs/playbook/INDEX.json`
30
- - 根据 `--tools` 参数写入 AI skill 模板:
31
- - Codex: `.codex/skills/{playbook-query,playbook-case,playbook-advisor}/SKILL.md`
32
- - Claude: `.claude/skills/{playbook-query,playbook-case,playbook-advisor}/SKILL.md`
33
- - `playbook global init` 全局级:
34
- - 创建 `~/.playbook/repo/{cases,patterns,checklists}`
35
- - 创建 `~/.playbook/repo/INDEX.json` 和 `~/.playbook/repo/tags.yml`
36
- - 不支持 `--tools`
37
- - `playbook update` 项目级:
38
- - 仅更新托管模板文件
39
- - 不修改用户业务内容(如 case markdown 文件)
40
- - V1 暂不支持 `playbook global update`
55
+ 在项目中初始化知识库。新项目首次使用时运行一次。
41
56
 
42
- ## Skill 交付
57
+ ```bash
58
+ playbook init # 当前目录,所有 AI 工具
59
+ playbook init ./my-project # 指定目录
60
+ playbook init --tools claude # 仅安装 Claude skill
61
+ playbook init --tools none # 不安装任何 AI skill
62
+ ```
43
63
 
44
- CLI 为支持的 AI 工具(`codex`、`claude`)安装以下 skill:
64
+ 创建 `docs/playbook/{cases,patterns,checklists}`、`INDEX.json`,以及 AI skill 模板。
45
65
 
46
- | Skill | 用途 |
47
- |-------|------|
48
- | `playbook-query` | 手动查询知识库 |
49
- | `playbook-case` | 引导式 case 创建 |
50
- | `playbook-advisor` | 主动知识应用(自动感知 tag,AI 自主查询) |
66
+ ### `playbook global init`
51
67
 
52
- CLI 只负责文件交付(`init`/`update`),skill 的运行时执行由宿主 AI 工具处理。
68
+ 初始化全局知识库(跨项目共享的经验)。只需运行一次。
53
69
 
54
- ## 冲突策略
70
+ ```bash
71
+ playbook global init
72
+ ```
55
73
 
56
- - `playbook init` 和 `playbook global init` 默认非破坏性:
57
- - 有冲突的托管文件会跳过并报告为 `conflict`
58
- - `playbook update` 采用分级策略:
59
- - skill 模板:内容不同时自动刷新
60
- - `docs/playbook/INDEX.json`:保护不覆盖,除非使用 `--force`
61
- - 使用 `--force` 可强制覆盖受保护的托管文件
74
+ 创建 `~/.playbook/repo/{cases,patterns,checklists}`、`INDEX.json` 和 `tags.yml`。
62
75
 
63
- ## 查询命令
76
+ ### `playbook update`
64
77
 
65
- `playbook query` tag 搜索 INDEX.json 条目(OR 逻辑),同时搜索项目和全局范围。
78
+ 升级 playbook CLI 版本后,更新项目中的 skill 模板到最新版。
66
79
 
67
80
  ```bash
68
- playbook query docker env # 匹配 "docker" 或 "env" 的条目
69
- playbook query # 所有条目
70
- playbook query --tags-only # 仅输出 tag 概览:docker(3) env(5) ci(2)
71
- playbook query --scope project # 仅项目范围
72
- playbook query --limit 0 # 不限数量
73
- playbook query docker --json # JSON 格式输出
81
+ playbook update # 更新当前项目
82
+ playbook update --force # 强制覆盖(含 INDEX.json)
74
83
  ```
75
84
 
85
+ 仅刷新 skill 模板,不修改用户业务内容(case/pattern/checklist 文件)。`INDEX.json` 默认受保护,需 `--force` 才会覆盖。
86
+
76
87
  ## 在 AI 中使用
77
88
 
78
89
  `playbook init` 之后,AI 工具中有三个 skill 可用:
@@ -123,6 +134,25 @@ AI:(引导你填写 问题/原因/解决/教训 四个部分)
123
134
  AI:(生成 case 文件,更新 INDEX.json)
124
135
  ```
125
136
 
137
+ ## 内部命令
138
+
139
+ 以下命令主要由 AI skill 内部调用,用户一般不需要直接使用:
140
+
141
+ ### `playbook query`
142
+
143
+ 按 tag 搜索 INDEX.json 条目(OR 逻辑),同时搜索项目和全局范围。
144
+
145
+ ```bash
146
+ playbook query docker env # 匹配 "docker" 或 "env" 的条目
147
+ playbook query # 所有条目
148
+ playbook query --tags-only # 仅输出 tag 概览:docker(3) env(5) ci(2)
149
+ playbook query --scope project # 仅项目范围
150
+ playbook query --limit 0 # 不限数量
151
+ playbook query docker --json # JSON 格式输出
152
+ ```
153
+
154
+ 这个命令是 AI skill 的底层工具。`/playbook-advisor` 和 `/playbook-query` skill 在内部调用它来搜索知识库,节省 token 开销。
155
+
126
156
  ## 错误处理
127
157
 
128
158
  - 未知命令或无效选项会给出可操作的提示信息
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punk6529/playbook",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for initializing and updating Playbook & Case workflows.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,9 +1,10 @@
1
1
  const { CliError } = require("./errors");
2
- const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs } = require("./options");
2
+ const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs, parsePromoteArgs } = require("./options");
3
3
  const { initProject } = require("./commands/init-project");
4
4
  const { initGlobal } = require("./commands/global-init");
5
5
  const { updateProject } = require("./commands/update-project");
6
6
  const { queryIndex, aggregateTags, formatPlainText, formatJson, formatTagsOnly } = require("./commands/query");
7
+ const { promoteEntries } = require("./commands/promote");
7
8
  const { printSummary } = require("./reporting");
8
9
 
9
10
  function usageText() {
@@ -13,6 +14,8 @@ function usageText() {
13
14
  " playbook global init [--force]",
14
15
  " playbook update [path] [--tools all|none|codex,claude] [--force]",
15
16
  " playbook query [tags...] [--limit N] [--scope project|global|both] [--json] [--tags-only]",
17
+ " playbook promote <entry-id...> [--force]",
18
+ " playbook promote --all [--force]",
16
19
  "",
17
20
  "Notes:",
18
21
  " - `init` and `update` are project-scoped commands.",
@@ -100,6 +103,55 @@ function runQuery(args, io) {
100
103
  return 0;
101
104
  }
102
105
 
106
+ function promoteHelpText() {
107
+ return [
108
+ "Usage: playbook promote <entry-id...> [options]",
109
+ " playbook promote --all [options]",
110
+ "",
111
+ "Move project entries to global knowledge base.",
112
+ "",
113
+ "Options:",
114
+ " --all Promote all project entries",
115
+ " --force Overwrite if entry already exists in global",
116
+ " -h, --help Show this help",
117
+ ].join("\n");
118
+ }
119
+
120
+ function runPromote(args, io) {
121
+ const options = parsePromoteArgs(args);
122
+ if (options.help) {
123
+ writeStdout(io, promoteHelpText());
124
+ return 0;
125
+ }
126
+
127
+ const result = promoteEntries(options);
128
+
129
+ if (result.message) {
130
+ writeStdout(io, result.message);
131
+ return 0;
132
+ }
133
+
134
+ let hasError = false;
135
+ for (const r of result.results) {
136
+ if (r.action === "promoted") {
137
+ writeStdout(io, `[promoted] ${r.entryId}\t${r.title}`);
138
+ } else if (r.action === "conflict") {
139
+ writeStderr(io, `[conflict] ${r.entryId}\t${r.reason}`);
140
+ hasError = true;
141
+ } else if (r.action === "error") {
142
+ writeStderr(io, `[error] ${r.entryId}\t${r.reason}`);
143
+ hasError = true;
144
+ }
145
+ }
146
+
147
+ const promoted = result.results.filter((r) => r.action === "promoted").length;
148
+ if (promoted > 0) {
149
+ writeStdout(io, `(${promoted} ${promoted === 1 ? "entry" : "entries"} promoted to global)`);
150
+ }
151
+
152
+ return hasError ? 1 : 0;
153
+ }
154
+
103
155
  function runGlobal(args, io) {
104
156
  const subcommand = args[0];
105
157
  if (!subcommand) {
@@ -149,11 +201,15 @@ async function runCli(argv, io = process) {
149
201
  return runQuery(argv.slice(1), io);
150
202
  }
151
203
 
204
+ if (command === "promote") {
205
+ return runPromote(argv.slice(1), io);
206
+ }
207
+
152
208
  if (command === "global") {
153
209
  return runGlobal(argv.slice(1), io);
154
210
  }
155
211
 
156
- throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query`);
212
+ throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query, promote`);
157
213
  } catch (error) {
158
214
  if (error instanceof CliError) {
159
215
  writeStderr(io, `Error: ${error.message}`);
@@ -0,0 +1,96 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const { CliError } = require("../errors");
5
+
6
+ function readIndex(indexPath) {
7
+ try {
8
+ const content = fs.readFileSync(indexPath, "utf8");
9
+ return JSON.parse(content);
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ function writeIndex(indexPath, data) {
16
+ fs.writeFileSync(indexPath, JSON.stringify(data, null, 2) + "\n", "utf8");
17
+ }
18
+
19
+ function promoteEntry(entryId, projectIndex, globalIndex, projectBase, globalBase, force) {
20
+ const entry = projectIndex[entryId];
21
+ if (!entry) {
22
+ return { action: "error", entryId, reason: `Entry '${entryId}' not found in project INDEX` };
23
+ }
24
+
25
+ if (globalIndex[entryId] && !force) {
26
+ return {
27
+ action: "conflict",
28
+ entryId,
29
+ reason: `Entry '${entryId}' already exists in global INDEX (use --force to overwrite)`,
30
+ };
31
+ }
32
+
33
+ const srcFile = path.join(projectBase, entry.path);
34
+ const destFile = path.join(globalBase, entry.path);
35
+
36
+ // Write to global first (safety: write before delete)
37
+ try {
38
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
39
+ fs.copyFileSync(srcFile, destFile);
40
+ } catch (err) {
41
+ return { action: "error", entryId, reason: `Failed to copy file to global: ${err.message}` };
42
+ }
43
+
44
+ globalIndex[entryId] = entry;
45
+
46
+ // Now safe to remove from project
47
+ delete projectIndex[entryId];
48
+ try {
49
+ fs.unlinkSync(srcFile);
50
+ } catch {
51
+ // File may not exist, that's ok — INDEX entry already removed
52
+ }
53
+
54
+ return { action: "promoted", entryId, title: entry.title };
55
+ }
56
+
57
+ function promoteEntries(options) {
58
+ const { entryIds = [], all = false, force = false, targetPath = "." } = options;
59
+
60
+ const projectBase = path.resolve(targetPath, "docs/playbook");
61
+ const globalBase = path.join(os.homedir(), ".playbook/repo");
62
+
63
+ const projectIndexPath = path.join(projectBase, "INDEX.json");
64
+ const globalIndexPath = path.join(globalBase, "INDEX.json");
65
+
66
+ const projectIndex = readIndex(projectIndexPath);
67
+ if (!projectIndex) {
68
+ throw new CliError("Project INDEX.json not found. Run 'playbook init' first.");
69
+ }
70
+
71
+ const globalIndex = readIndex(globalIndexPath);
72
+ if (!globalIndex) {
73
+ throw new CliError("Global INDEX.json not found. Run 'playbook global init' first.");
74
+ }
75
+
76
+ const ids = all ? Object.keys(projectIndex) : entryIds;
77
+
78
+ if (all && ids.length === 0) {
79
+ return { results: [], message: "No entries in project INDEX to promote." };
80
+ }
81
+
82
+ const results = [];
83
+ for (const id of ids) {
84
+ results.push(promoteEntry(id, projectIndex, globalIndex, projectBase, globalBase, force));
85
+ }
86
+
87
+ // Write updated indices
88
+ writeIndex(projectIndexPath, projectIndex);
89
+ writeIndex(globalIndexPath, globalIndex);
90
+
91
+ return { results };
92
+ }
93
+
94
+ module.exports = {
95
+ promoteEntries,
96
+ };
package/src/manifest.js CHANGED
@@ -57,6 +57,18 @@ const PROJECT_MANAGED_FILES = [
57
57
  tool: "claude",
58
58
  overwriteOnUpdate: true,
59
59
  },
60
+ {
61
+ relativePath: ".codex/skills/playbook-promote/SKILL.md",
62
+ templatePath: "project/skills/playbook-promote/SKILL.md",
63
+ tool: "codex",
64
+ overwriteOnUpdate: true,
65
+ },
66
+ {
67
+ relativePath: ".claude/skills/playbook-promote/SKILL.md",
68
+ templatePath: "project/skills/playbook-promote/SKILL.md",
69
+ tool: "claude",
70
+ overwriteOnUpdate: true,
71
+ },
60
72
  ];
61
73
 
62
74
  const GLOBAL_SKELETON_DIRS = ["cases", "patterns", "checklists"];
package/src/options.js CHANGED
@@ -181,9 +181,50 @@ function parseQueryArgs(args) {
181
181
  return { help: false, tags, limit, scope, json, tagsOnly };
182
182
  }
183
183
 
184
+ function parsePromoteArgs(args) {
185
+ let force = false;
186
+ let all = false;
187
+ const entryIds = [];
188
+
189
+ for (let i = 0; i < args.length; i += 1) {
190
+ const arg = args[i];
191
+
192
+ if (arg === "-h" || arg === "--help") {
193
+ return { help: true };
194
+ }
195
+
196
+ if (arg === "--force") {
197
+ force = true;
198
+ continue;
199
+ }
200
+
201
+ if (arg === "--all") {
202
+ all = true;
203
+ continue;
204
+ }
205
+
206
+ if (arg.startsWith("-")) {
207
+ throw new CliError(`Unknown option for playbook promote: ${arg}`);
208
+ }
209
+
210
+ entryIds.push(arg);
211
+ }
212
+
213
+ if (!all && entryIds.length === 0) {
214
+ throw new CliError("playbook promote requires entry IDs or --all");
215
+ }
216
+
217
+ if (all && entryIds.length > 0) {
218
+ throw new CliError("Cannot use --all with specific entry IDs");
219
+ }
220
+
221
+ return { help: false, force, all, entryIds };
222
+ }
223
+
184
224
  module.exports = {
185
225
  parseProjectCommandArgs,
186
226
  parseGlobalInitArgs,
187
227
  parseQueryArgs,
228
+ parsePromoteArgs,
188
229
  parseToolsValue,
189
230
  };
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: playbook-promote
3
+ description: Guide when to promote project-level playbook entries to global knowledge base.
4
+ license: MIT
5
+ ---
6
+
7
+ Use this skill to suggest promoting valuable project experiences to the global knowledge base for cross-project reuse.
8
+
9
+ ## When to Suggest Promote
10
+
11
+ Suggest `playbook promote <entry-id>` to the user when:
12
+ - A project case was successfully used to resolve a problem and the lesson is not project-specific
13
+ - A pattern or checklist has proven useful and would benefit other projects
14
+ - The user explicitly asks about sharing knowledge across projects
15
+
16
+ ## When NOT to Suggest
17
+
18
+ Do not suggest promote when:
19
+ - The entry references project-specific configuration, paths, or dependencies
20
+ - The entry is still being refined or hasn't been validated
21
+ - The user is focused on other work (don't interrupt)
22
+
23
+ ## How to Promote
24
+
25
+ ```bash
26
+ playbook promote <entry-id> # promote a single entry
27
+ playbook promote <id1> <id2> # promote multiple entries
28
+ playbook promote --all # promote all project entries
29
+ playbook promote <entry-id> --force # overwrite if already exists in global
30
+ ```
31
+
32
+ The command moves entries from project to global: copies the file, updates both INDEX.json files, and removes the project copy.