@punk6529/playbook 0.2.4 → 0.2.8

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
@@ -24,7 +24,7 @@
24
24
  - `/playbook-advisor`:会话内主动顾问,自动按需查询知识库
25
25
  - `/playbook-query`:手动按标签检索经验
26
26
  - `/playbook-case`:引导式记录新 case,并更新索引
27
- - 一套稳定的 CLI 工作流(init / global init / update / query / promote / hit),便于持续演进而不破坏现有内容
27
+ - 一套稳定的 CLI 工作流(init / global init / update / query / promote / show / hit / reindex),便于持续演进而不破坏现有内容
28
28
 
29
29
  ## 典型工作流
30
30
 
@@ -166,6 +166,16 @@ playbook query docker --json # JSON 格式输出
166
166
 
167
167
  这个命令是 AI skill 的底层工具。`/playbook-advisor` 和 `/playbook-query` skill 在内部调用它来搜索知识库,节省 token 开销。
168
168
 
169
+ ### `playbook show`
170
+
171
+ 输出指定案例的完整内容。AI skill 用它查看案例详情(L2),无需知道文件路径。
172
+
173
+ ```bash
174
+ playbook show case-001-docker-bind # 输出案例文件内容
175
+ ```
176
+
177
+ 先查项目 INDEX,再查全局 INDEX,找到后输出对应文件内容到 stdout。
178
+
169
179
  ### `playbook hit`
170
180
 
171
181
  记录一次案例阅读命中,用于追踪案例的实际使用频率。
@@ -176,6 +186,17 @@ playbook hit case-001-docker-bind # 记录一次命中
176
186
 
177
187
  AI advisor 每次阅读案例详情后自动调用。命中次数在 `playbook query` 输出中以 `hits:N` 显示,帮助识别高价值案例。
178
188
 
189
+ ### `playbook reindex`
190
+
191
+ 从文件 front matter 重建 INDEX.json。当 INDEX 被清空、损坏或需要同步时使用。
192
+
193
+ ```bash
194
+ playbook reindex # 重建项目 INDEX
195
+ playbook reindex --scope global # 重建全局 INDEX
196
+ ```
197
+
198
+ 扫描 `cases/`、`patterns/`、`checklists/` 下的 `.md` 文件,解析 YAML front matter(title, tags, created),重建 INDEX.json。保留已有的 hits 统计。
199
+
179
200
  ## 错误处理
180
201
 
181
202
  - 未知命令或无效选项会给出可操作的提示信息
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punk6529/playbook",
3
- "version": "0.2.4",
3
+ "version": "0.2.8",
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,11 +1,16 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
1
4
  const { CliError } = require("./errors");
2
- const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs, parsePromoteArgs, parseHitArgs } = require("./options");
5
+ const { parseProjectCommandArgs, parseGlobalInitArgs, parseQueryArgs, parsePromoteArgs, parseHitArgs, parseShowArgs, parseReindexArgs } = require("./options");
3
6
  const { initProject } = require("./commands/init-project");
4
7
  const { initGlobal } = require("./commands/global-init");
5
8
  const { updateProject } = require("./commands/update-project");
6
9
  const { queryIndex, aggregateTags, formatPlainText, formatJson, formatTagsOnly } = require("./commands/query");
7
10
  const { promoteEntries } = require("./commands/promote");
8
11
  const { hitEntry } = require("./commands/hit");
12
+ const { showEntry } = require("./commands/show");
13
+ const { reindexPlaybook } = require("./commands/reindex");
9
14
  const { printSummary } = require("./reporting");
10
15
 
11
16
  function usageText() {
@@ -18,6 +23,9 @@ function usageText() {
18
23
  " playbook promote <entry-id...> [--force]",
19
24
  " playbook promote --all [--force]",
20
25
  " playbook hit <entry-id>",
26
+ " playbook show <entry-id>",
27
+ " playbook reindex [--scope project|global]",
28
+ " playbook root [--scope project|global]",
21
29
  "",
22
30
  "Notes:",
23
31
  " - `init` and `update` are project-scoped commands.",
@@ -178,6 +186,83 @@ function runHit(args, io) {
178
186
  return 0;
179
187
  }
180
188
 
189
+ function showHelpText() {
190
+ return [
191
+ "Usage: playbook show <entry-id>",
192
+ "",
193
+ "Display the full content of a playbook entry.",
194
+ "Searches project INDEX first, then global INDEX.",
195
+ "",
196
+ "Options:",
197
+ " -h, --help Show this help",
198
+ ].join("\n");
199
+ }
200
+
201
+ function runShow(args, io) {
202
+ const options = parseShowArgs(args);
203
+ if (options.help) {
204
+ writeStdout(io, showHelpText());
205
+ return 0;
206
+ }
207
+
208
+ const result = showEntry(options);
209
+ writeStdout(io, result.content);
210
+ return 0;
211
+ }
212
+
213
+ function reindexHelpText() {
214
+ return [
215
+ "Usage: playbook reindex [options]",
216
+ "",
217
+ "Rebuild INDEX.json by scanning playbook files and reading front matter.",
218
+ "",
219
+ "Options:",
220
+ " --scope X Scope: project or global (default: project)",
221
+ " -h, --help Show this help",
222
+ ].join("\n");
223
+ }
224
+
225
+ function runReindex(args, io) {
226
+ const options = parseReindexArgs(args);
227
+ if (options.help) {
228
+ writeStdout(io, reindexHelpText());
229
+ return 0;
230
+ }
231
+
232
+ const result = reindexPlaybook(options);
233
+ writeStdout(io, `Reindexed ${result.scope}: ${result.count} ${result.count === 1 ? "entry" : "entries"} found.`);
234
+ return 0;
235
+ }
236
+
237
+ function runRoot(args, io) {
238
+ let scope = "project";
239
+ for (const arg of args) {
240
+ if (arg === "-h" || arg === "--help") {
241
+ writeStdout(io, "Usage: playbook root [--scope project|global]\n\nOutput the absolute path of the playbook directory.");
242
+ return 0;
243
+ }
244
+ if (arg === "--scope" || arg.startsWith("--scope=")) {
245
+ const value = arg === "--scope" ? args[args.indexOf(arg) + 1] : arg.slice("--scope=".length);
246
+ if (value === "global" || value === "project") scope = value;
247
+ }
248
+ }
249
+
250
+ if (scope === "global") {
251
+ const globalRoot = path.join(os.homedir(), ".playbook/repo");
252
+ if (!fs.existsSync(globalRoot)) {
253
+ throw new CliError("Global playbook not found. Run 'playbook global init' first.");
254
+ }
255
+ writeStdout(io, globalRoot);
256
+ } else {
257
+ const projectRoot = path.resolve("docs/playbook");
258
+ if (!fs.existsSync(projectRoot)) {
259
+ throw new CliError("Project playbook not found. Run 'playbook init' first.");
260
+ }
261
+ writeStdout(io, projectRoot);
262
+ }
263
+ return 0;
264
+ }
265
+
181
266
  function runGlobal(args, io) {
182
267
  const subcommand = args[0];
183
268
  if (!subcommand) {
@@ -235,11 +320,23 @@ async function runCli(argv, io = process) {
235
320
  return runHit(argv.slice(1), io);
236
321
  }
237
322
 
323
+ if (command === "show") {
324
+ return runShow(argv.slice(1), io);
325
+ }
326
+
327
+ if (command === "reindex") {
328
+ return runReindex(argv.slice(1), io);
329
+ }
330
+
331
+ if (command === "root") {
332
+ return runRoot(argv.slice(1), io);
333
+ }
334
+
238
335
  if (command === "global") {
239
336
  return runGlobal(argv.slice(1), io);
240
337
  }
241
338
 
242
- throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query, promote, hit`);
339
+ throw new CliError(`Unknown command: ${command}. Supported: init, global, update, query, promote, hit, show, reindex, root`);
243
340
  } catch (error) {
244
341
  if (error instanceof CliError) {
245
342
  writeStderr(io, `Error: ${error.message}`);
@@ -92,11 +92,28 @@ function formatPlainText(result) {
92
92
  return lines.join("\n");
93
93
  }
94
94
 
95
+ function readTagsRegistry(tagsYmlPath) {
96
+ try {
97
+ const content = fs.readFileSync(tagsYmlPath, "utf8");
98
+ const ids = [];
99
+ for (const line of content.split("\n")) {
100
+ const match = line.match(/^\s*-\s*id:\s*(.+)/);
101
+ if (match) {
102
+ ids.push(match[1].trim());
103
+ }
104
+ }
105
+ return ids;
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
95
111
  function aggregateTags(options) {
96
112
  const { scope = "both", targetPath = "." } = options;
97
113
 
98
114
  const projectIndexPath = path.resolve(targetPath, "docs/playbook/INDEX.json");
99
115
  const globalIndexPath = path.join(os.homedir(), ".playbook/repo/INDEX.json");
116
+ const tagsYmlPath = path.join(os.homedir(), ".playbook/repo/tags.yml");
100
117
 
101
118
  const tagCounts = {};
102
119
  let anyIndexFound = false;
@@ -125,6 +142,17 @@ function aggregateTags(options) {
125
142
  }
126
143
  }
127
144
 
145
+ // Merge tags.yml registry — ensures registered tags appear even with count 0
146
+ const registryTags = readTagsRegistry(tagsYmlPath);
147
+ if (registryTags) {
148
+ anyIndexFound = true;
149
+ for (const tag of registryTags) {
150
+ if (!(tag in tagCounts)) {
151
+ tagCounts[tag] = 0;
152
+ }
153
+ }
154
+ }
155
+
128
156
  if (!anyIndexFound) {
129
157
  return { tags: {}, found: false };
130
158
  }
@@ -0,0 +1,98 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const { CliError } = require("../errors");
5
+ const { parseFrontMatter } = require("../frontmatter");
6
+
7
+ const TYPE_DIRS = {
8
+ cases: "case",
9
+ patterns: "pattern",
10
+ checklists: "checklist",
11
+ };
12
+
13
+ function readIndex(indexPath) {
14
+ try {
15
+ const content = fs.readFileSync(indexPath, "utf8");
16
+ return JSON.parse(content);
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ function scanDir(baseDir) {
23
+ const entries = {};
24
+
25
+ for (const [dirName, type] of Object.entries(TYPE_DIRS)) {
26
+ const dirPath = path.join(baseDir, dirName);
27
+ if (!fs.existsSync(dirPath)) continue;
28
+
29
+ const files = fs.readdirSync(dirPath).filter((f) => f.endsWith(".md"));
30
+ for (const file of files) {
31
+ const filePath = path.join(dirPath, file);
32
+ const content = fs.readFileSync(filePath, "utf8");
33
+ const fm = parseFrontMatter(content);
34
+
35
+ if (!fm.title) continue; // skip files without valid front matter
36
+
37
+ const id = file.replace(/\.md$/, "");
38
+ const relativePath = `${dirName}/${file}`;
39
+
40
+ entries[id] = {
41
+ type,
42
+ title: fm.title,
43
+ tags: fm.tags || [],
44
+ path: relativePath,
45
+ created: fm.created || new Date().toISOString().slice(0, 10),
46
+ };
47
+ }
48
+ }
49
+
50
+ return entries;
51
+ }
52
+
53
+ function reindexPlaybook(options) {
54
+ const { scope = "project", targetPath = "." } = options;
55
+
56
+ let baseDir;
57
+ let indexPath;
58
+
59
+ if (scope === "global") {
60
+ baseDir = path.join(os.homedir(), ".playbook/repo");
61
+ indexPath = path.join(baseDir, "INDEX.json");
62
+ } else {
63
+ baseDir = path.resolve(targetPath, "docs/playbook");
64
+ indexPath = path.join(baseDir, "INDEX.json");
65
+ }
66
+
67
+ if (!fs.existsSync(baseDir)) {
68
+ throw new CliError(
69
+ scope === "global"
70
+ ? "Global playbook not found. Run 'playbook global init' first."
71
+ : "Project playbook not found. Run 'playbook init' first."
72
+ );
73
+ }
74
+
75
+ // Read existing index to preserve hits
76
+ const oldIndex = readIndex(indexPath) || {};
77
+
78
+ // Scan files and build new index
79
+ const newEntries = scanDir(baseDir);
80
+
81
+ // Preserve hits from old index
82
+ for (const [id, entry] of Object.entries(newEntries)) {
83
+ if (oldIndex[id] && oldIndex[id].hits) {
84
+ entry.hits = oldIndex[id].hits;
85
+ }
86
+ }
87
+
88
+ // Write new index
89
+ fs.writeFileSync(indexPath, JSON.stringify(newEntries, null, 2) + "\n", "utf8");
90
+
91
+ return {
92
+ scope,
93
+ count: Object.keys(newEntries).length,
94
+ entries: newEntries,
95
+ };
96
+ }
97
+
98
+ module.exports = { reindexPlaybook, scanDir };
@@ -0,0 +1,57 @@
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 showEntry(options) {
16
+ const { entryId, targetPath = "." } = options;
17
+
18
+ const projectIndexPath = path.resolve(targetPath, "docs/playbook/INDEX.json");
19
+ const globalIndexPath = path.join(os.homedir(), ".playbook/repo/INDEX.json");
20
+
21
+ // Try project first
22
+ const projectIndex = readIndex(projectIndexPath);
23
+ if (projectIndex && projectIndex[entryId]) {
24
+ const entryPath = projectIndex[entryId].path;
25
+ const fullPath = path.resolve(targetPath, "docs/playbook", entryPath);
26
+ const content = readFile(fullPath, entryId);
27
+ return { entryId, scope: "project", content };
28
+ }
29
+
30
+ // Fall back to global
31
+ const globalIndex = readIndex(globalIndexPath);
32
+ if (globalIndex && globalIndex[entryId]) {
33
+ const entryPath = globalIndex[entryId].path;
34
+ const fullPath = path.join(os.homedir(), ".playbook/repo", entryPath);
35
+ const content = readFile(fullPath, entryId);
36
+ return { entryId, scope: "global", content };
37
+ }
38
+
39
+ // Not found
40
+ if (!projectIndex && !globalIndex) {
41
+ throw new CliError("No INDEX.json found. Run 'playbook init' or 'playbook global init' first.");
42
+ }
43
+
44
+ throw new CliError(`Entry '${entryId}' not found in project or global INDEX.`);
45
+ }
46
+
47
+ function readFile(fullPath, entryId) {
48
+ try {
49
+ return fs.readFileSync(fullPath, "utf8");
50
+ } catch {
51
+ throw new CliError(`File not found for entry '${entryId}': ${fullPath}`);
52
+ }
53
+ }
54
+
55
+ module.exports = {
56
+ showEntry,
57
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Parse YAML front matter from markdown content.
3
+ * Expects format:
4
+ * ---
5
+ * title: Some title
6
+ * tags: [tag1, tag2]
7
+ * created: 2026-02-01
8
+ * ---
9
+ * body content here
10
+ */
11
+ function parseFrontMatter(content) {
12
+ const trimmed = content.trimStart();
13
+ if (!trimmed.startsWith("---")) {
14
+ return { title: null, tags: null, created: null, body: content };
15
+ }
16
+
17
+ const endIndex = trimmed.indexOf("\n---", 3);
18
+ if (endIndex === -1) {
19
+ return { title: null, tags: null, created: null, body: content };
20
+ }
21
+
22
+ const fmBlock = trimmed.slice(4, endIndex);
23
+ const body = trimmed.slice(endIndex + 4).replace(/^\n/, "");
24
+
25
+ let title = null;
26
+ let tags = null;
27
+ let created = null;
28
+
29
+ for (const line of fmBlock.split("\n")) {
30
+ const stripped = line.trim();
31
+ if (!stripped || stripped.startsWith("#")) continue;
32
+
33
+ const colonIndex = stripped.indexOf(":");
34
+ if (colonIndex === -1) continue;
35
+
36
+ const key = stripped.slice(0, colonIndex).trim();
37
+ const value = stripped.slice(colonIndex + 1).trim();
38
+
39
+ if (key === "title") {
40
+ title = value;
41
+ } else if (key === "tags") {
42
+ tags = parseTagsValue(value);
43
+ } else if (key === "created") {
44
+ created = value;
45
+ }
46
+ }
47
+
48
+ return { title, tags, created, body };
49
+ }
50
+
51
+ function parseTagsValue(value) {
52
+ // Handle [tag1, tag2] format
53
+ const match = value.match(/^\[(.*)?\]$/);
54
+ if (match) {
55
+ if (!match[1] || !match[1].trim()) return [];
56
+ return match[1]
57
+ .split(",")
58
+ .map((t) => t.trim())
59
+ .filter(Boolean);
60
+ }
61
+ // Handle bare value as single tag
62
+ if (value) return [value];
63
+ return [];
64
+ }
65
+
66
+ module.exports = { parseFrontMatter };
package/src/options.js CHANGED
@@ -240,11 +240,69 @@ function parseHitArgs(args) {
240
240
  return { help: false, entryId };
241
241
  }
242
242
 
243
+ function parseShowArgs(args) {
244
+ for (const arg of args) {
245
+ if (arg === "-h" || arg === "--help") {
246
+ return { help: true };
247
+ }
248
+ }
249
+
250
+ const entryId = args[0];
251
+ if (!entryId || entryId.startsWith("-")) {
252
+ throw new CliError("playbook show requires an entry ID");
253
+ }
254
+
255
+ if (args.length > 1) {
256
+ throw new CliError("playbook show accepts exactly one entry ID");
257
+ }
258
+
259
+ return { help: false, entryId };
260
+ }
261
+
262
+ function parseReindexArgs(args) {
263
+ let scope = "project";
264
+
265
+ for (let i = 0; i < args.length; i += 1) {
266
+ const arg = args[i];
267
+
268
+ if (arg === "-h" || arg === "--help") {
269
+ return { help: true };
270
+ }
271
+
272
+ if (arg === "--scope") {
273
+ const next = args[i + 1];
274
+ if (!next || !["project", "global"].includes(next)) {
275
+ throw new CliError("--scope must be one of: project, global");
276
+ }
277
+ scope = next;
278
+ i += 1;
279
+ continue;
280
+ }
281
+
282
+ if (arg.startsWith("--scope=")) {
283
+ const value = arg.slice("--scope=".length);
284
+ if (!["project", "global"].includes(value)) {
285
+ throw new CliError("--scope must be one of: project, global");
286
+ }
287
+ scope = value;
288
+ continue;
289
+ }
290
+
291
+ if (arg.startsWith("-")) {
292
+ throw new CliError(`Unknown option for playbook reindex: ${arg}`);
293
+ }
294
+ }
295
+
296
+ return { help: false, scope };
297
+ }
298
+
243
299
  module.exports = {
244
300
  parseProjectCommandArgs,
245
301
  parseGlobalInitArgs,
246
302
  parseQueryArgs,
247
303
  parsePromoteArgs,
248
304
  parseHitArgs,
305
+ parseShowArgs,
306
+ parseReindexArgs,
249
307
  parseToolsValue,
250
308
  };
@@ -43,11 +43,12 @@ Review the titles in the output. Only proceed to Step 2 for entries whose titles
43
43
 
44
44
  ### Step 2 — Read specific cases (L2)
45
45
 
46
- For relevant entries only:
47
- - Project entries: read `docs/playbook/<path>`
48
- - Global entries: read `~/.playbook/repo/<path>`
46
+ For relevant entries only, run:
47
+ ```bash
48
+ playbook show <entry-id>
49
+ ```
49
50
 
50
- Do NOT read all matched entries. Only read the ones that look relevant based on title.
51
+ This outputs the full content of the case file. Do NOT read all matched entries. Only show the ones that look relevant based on title.
51
52
 
52
53
  When multiple entries have similar titles, prefer entries with higher `hits` count — they have been used more often and are likely more effective.
53
54
 
@@ -13,10 +13,11 @@ Use this skill when the user asks to create, review, or inspect case entries.
13
13
  - For new cases, enforce this guided flow:
14
14
  1. If the user has not provided a concrete problem statement, ask first:
15
15
  "你想把之前遇到的哪个问题沉淀为 case?请一句话描述。"
16
- 2. After problem statement is available, suggest tags from registry/index context first:
17
- - Prefer project index: `docs/playbook/INDEX.json`
18
- - Use global index only when relevant: `~/.playbook/repo/INDEX.json`
19
- - Ask user to choose existing tags or explicitly confirm a new tag
16
+ 2. After problem statement is available, get available tags by running:
17
+ ```bash
18
+ playbook query --tags-only
19
+ ```
20
+ Suggest relevant existing tags from the output. Ask user to choose existing tags or explicitly confirm a new tag.
20
21
  3. If scope is not confirmed, ask for scope confirmation:
21
22
  - default is `project`
22
23
  - use `global` only when user explicitly confirms
@@ -65,9 +66,51 @@ Format: `{type}-{NNN}-{slug}`
65
66
  - `NNN`: zero-padded 3-digit sequence number (check existing entries to determine next number)
66
67
  - `slug`: lowercase hyphenated descriptor derived from the title
67
68
 
69
+ ### Case File Format
70
+
71
+ Case files MUST include YAML front matter with title, tags, and created:
72
+
73
+ ```markdown
74
+ ---
75
+ title: OAuth token refresh race condition under concurrency
76
+ tags: [auth, api]
77
+ created: 2026-02-10
78
+ ---
79
+
80
+ ## Problem
81
+ ...
82
+
83
+ ## Context
84
+ ...
85
+
86
+ ## Solution
87
+ ...
88
+
89
+ ## Takeaway
90
+ ...
91
+ ```
92
+
93
+ The front matter is the source of truth for metadata. INDEX.json is rebuilt from front matter via `playbook reindex`.
94
+
95
+ ### File Paths
96
+
97
+ Write files to the correct location based on scope:
98
+
99
+ - **Project scope:** case file → `<project-root>/docs/playbook/<path>`, INDEX → `<project-root>/docs/playbook/INDEX.json`
100
+ - **Global scope:** case file → `~/.playbook/repo/<path>`, INDEX → `~/.playbook/repo/INDEX.json`
101
+
102
+ **IMPORTANT:** Always use absolute paths when writing files. Do NOT use relative paths — the current working directory may not be the project root.
103
+
104
+ Get the playbook directory first:
105
+ ```bash
106
+ playbook root # project scope
107
+ playbook root --scope global # global scope
108
+ ```
109
+ Then use the output to construct absolute paths, e.g. `<root>/cases/case-001-xxx.md` and `<root>/INDEX.json`.
110
+
68
111
  ### Draft Package Output
69
112
 
70
- When producing the draft package, output the INDEX entry as a ready-to-merge JSON snippet:
113
+ When producing the draft package, output:
71
114
 
72
115
  ```
73
116
  **Entry ID:** case-001-oauth-token-race
@@ -84,3 +127,11 @@ Add this entry to INDEX.json:
84
127
  }
85
128
  }
86
129
  ```
130
+
131
+ ### Write Procedure
132
+
133
+ After user confirms the draft:
134
+
135
+ 1. Write the case file to the scope-specific path
136
+ 2. Read the current INDEX.json, merge the new entry, write back
137
+ 3. Run `playbook hit <entry-id>` is NOT needed — hit is only for reads
@@ -30,14 +30,15 @@ Output format (one line per match):
30
30
  (N matches: X project, Y global)
31
31
  ```
32
32
 
33
- ### Expansion (L2): Read individual case files
33
+ ### Expansion (L2): Show entry content
34
34
 
35
35
  When the user asks to see details of a specific entry, or when you need full context for a task:
36
36
 
37
- - For project entries: read `docs/playbook/<path>` where `<path>` is from the query result
38
- - For global entries: read `~/.playbook/repo/<path>`
37
+ ```bash
38
+ playbook show <entry-id>
39
+ ```
39
40
 
40
- Only expand entries that are actually needed. Do not read all matched entries at once.
41
+ This outputs the full content of the entry file. Only expand entries that are actually needed. Do not show all matched entries at once.
41
42
 
42
43
  ### Edge Cases
43
44