@ztffn/presentation-generator-plugin 1.4.6 → 1.5.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "presentation-generator",
3
3
  "description": "Generate complete graph-based presentations from natural language briefs and project documents. Pipeline: content extraction, narrative design, deterministic graph generation, and visual styling.",
4
- "version": "1.4.6",
4
+ "version": "1.5.0",
5
5
  "author": {
6
6
  "name": "Huma"
7
7
  },
package/DEFERRED-FIXES.md CHANGED
@@ -4,6 +4,33 @@ Issues that require architectural changes and should be addressed in targeted, f
4
4
 
5
5
  ---
6
6
 
7
+ ## v1.5.0 fixes ✓ resolved
8
+
9
+ **Bug: Style agent always launched as `undefined`**
10
+ `enforce-style-schema.sh` returned `updatedInput: { prompt: "..." }` which replaced
11
+ the entire tool_input, dropping `subagent_type`. Fixed by merging the modified prompt
12
+ into the original tool_input: `updatedInput: ($orig + {prompt: $prompt})`.
13
+
14
+ **Bug: Content brief triggered presentation validator**
15
+ Hook patterns `*_temp/presentation*.json` matched `_temp/presentation-content-brief.json`,
16
+ causing the validator to reject it. Removed the broad arm — only `*_temp/presentation-draft.json`
17
+ needs validation now.
18
+
19
+ **Bug: Bare agent names caused first-attempt failures**
20
+ SKILL.md used `presentation-content` etc; Task tool requires fully-qualified
21
+ `presentation-generator:presentation-content`. Fixed in all three phases (2, 3, 6).
22
+
23
+ **UX: Visual intent annotations leaking into slide content**
24
+ Narrative agents sometimes write `**Visual intent:** bookend` inside `**Content:**` blocks.
25
+ Added `**Visual intent:**` as a recognized field marker in `extract_fields()` so it acts
26
+ as a boundary, preventing bleed into `data.content`. Documented in `outline-format/SKILL.md`.
27
+
28
+ **UX: Intimidating inline scripts in permission prompts**
29
+ Phase 1 multi-line if/elif replaced with a single `find` one-liner.
30
+ Phase 5 Python heredoc extracted into `scripts/verify_node_count.py`.
31
+
32
+ ---
33
+
7
34
  ## Issue 3 — `topic` badge truncation ✓ resolved
8
35
 
9
36
  **Was:** Badge clips at ~180px wide when `topic` strings are long.
package/bin/index.js CHANGED
@@ -264,19 +264,12 @@ function clearInstalledPluginsEntry() {
264
264
  function writeInstalledPluginsJson(version, installDir, scope) {
265
265
  // Claude Code's `claude plugin install` writes stale version/installPath data
266
266
  // (a known bug: https://github.com/anthropics/claude-code/issues/15642).
267
- // We overwrite the entry directly with the correct values after install.
267
+ // We overwrite the entry directly, pointing installPath at installDir so
268
+ // Claude reads from the actual installed location, not a global cache copy.
268
269
  const registryPath = path.join(
269
270
  os.homedir(), ".claude", "plugins", "installed_plugins.json"
270
271
  );
271
272
 
272
- // Ensure the cache dir exists and contains the plugin files.
273
- const cacheDir = path.join(
274
- os.homedir(), ".claude", "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME, version
275
- );
276
- if (!fs.existsSync(cacheDir)) {
277
- execSync(`cp -r "${installDir}/." "${cacheDir}"`, { stdio: "pipe" });
278
- }
279
-
280
273
  let registry = { version: 2, plugins: {} };
281
274
  if (fs.existsSync(registryPath)) {
282
275
  try { registry = JSON.parse(fs.readFileSync(registryPath, "utf8")); } catch {}
@@ -287,7 +280,7 @@ function writeInstalledPluginsJson(version, installDir, scope) {
287
280
  const existing = (registry.plugins[key] || []).find((e) => e.scope === scope);
288
281
  const entry = {
289
282
  scope,
290
- installPath: cacheDir,
283
+ installPath: installDir,
291
284
  version,
292
285
  installedAt: existing?.installedAt || now,
293
286
  lastUpdated: now,
@@ -60,11 +60,17 @@ Your report MUST include the exact validator terminal output."
60
60
  # Build the modified prompt
61
61
  MODIFIED_PROMPT="${PREAMBLE}${ORIGINAL_PROMPT}${SUFFIX}"
62
62
 
63
- # Return updatedInput with the augmented prompt
64
- jq -n --arg prompt "$MODIFIED_PROMPT" '{
65
- hookSpecificOutput: {
66
- hookEventName: "PreToolUse",
67
- permissionDecision: "allow",
68
- updatedInput: { prompt: $prompt }
69
- }
70
- }'
63
+ # Return updatedInput: merge modified prompt into original tool_input so
64
+ # subagent_type (and any other fields) are preserved.
65
+ ORIGINAL_INPUT=$(jq '.tool_input' <<< "$INPUT")
66
+
67
+ jq -n \
68
+ --argjson orig "$ORIGINAL_INPUT" \
69
+ --arg prompt "$MODIFIED_PROMPT" \
70
+ '{
71
+ hookSpecificOutput: {
72
+ hookEventName: "PreToolUse",
73
+ permissionDecision: "allow",
74
+ updatedInput: ($orig + {prompt: $prompt})
75
+ }
76
+ }'
@@ -12,7 +12,7 @@ FILE=$(jq -r '.tool_input.file_path // empty' <<< "$INPUT")
12
12
 
13
13
  # Only validate presentation JSON
14
14
  case "$FILE" in
15
- *presentations/*.json|*_temp/presentation-draft.json|*_temp/presentation*.json) ;;
15
+ *presentations/*.json|*_temp/presentation-draft.json) ;;
16
16
  *) exit 0 ;;
17
17
  esac
18
18
 
@@ -14,7 +14,7 @@ fi
14
14
 
15
15
  # Only validate presentation JSON files
16
16
  case "$FILE" in
17
- *presentations/*.json|*_temp/presentation-draft.json|*_temp/presentation*.json)
17
+ *presentations/*.json|*_temp/presentation-draft.json)
18
18
  ;;
19
19
  *)
20
20
  exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ztffn/presentation-generator-plugin",
3
- "version": "1.4.6",
3
+ "version": "1.5.0",
4
4
  "description": "Claude Code plugin for generating graph-based presentations",
5
5
  "bin": {
6
6
  "presentation-generator-plugin": "bin/index.js"
@@ -155,12 +155,17 @@ def extract_fields(text):
155
155
  """Extract Key message, Content, Speaker notes, Transition from a content block."""
156
156
  fields = {}
157
157
 
158
- # Define field markers in order
158
+ # Define field markers in order.
159
+ # visual_intent is listed between content and speaker_notes so the parser
160
+ # treats it as a boundary — stopping content extraction before the annotation
161
+ # if the narrative agent places it there. The extracted value is stored but
162
+ # not used in the output JSON (it is metadata for the style agent only).
159
163
  markers = [
160
- ("key_message", r"\*\*Key message:\*\*"),
161
- ("content", r"\*\*Content:\*\*"),
164
+ ("key_message", r"\*\*Key message:\*\*"),
165
+ ("content", r"\*\*Content:\*\*"),
166
+ ("visual_intent", r"\*\*Visual intent:\*\*"),
162
167
  ("speaker_notes", r"\*\*Speaker notes:\*\*"),
163
- ("transition", r"\*\*Transition to next:\*\*"),
168
+ ("transition", r"\*\*Transition to next:\*\*"),
164
169
  ]
165
170
 
166
171
  # Find positions of all markers
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ verify_node_count.py — Checks that the JSON node count matches the outline declaration.
4
+ Usage: python3 verify_node_count.py <outline.md> <presentation.json>
5
+ Exit: 0 if counts match, 1 if mismatch.
6
+ """
7
+
8
+ import json
9
+ import re
10
+ import sys
11
+ from pathlib import Path
12
+
13
+
14
+ def main():
15
+ if len(sys.argv) != 3:
16
+ print("Usage: python3 verify_node_count.py <outline.md> <presentation.json>")
17
+ sys.exit(1)
18
+
19
+ outline = Path(sys.argv[1]).read_text(encoding="utf-8")
20
+
21
+ # Scope counts to their respective sections to avoid false matches
22
+ spine_m = re.search(r"^## SPINE\b", outline, re.MULTILINE)
23
+ dd_m = re.search(r"^## DRILL-DOWNS\b", outline, re.MULTILINE)
24
+ cont_m = re.search(r"^## CONTENT PER SLIDE\b", outline, re.MULTILINE)
25
+
26
+ spine_text = outline[spine_m.end():dd_m.start()] if spine_m and dd_m else ""
27
+ dd_text = outline[dd_m.end():cont_m.start()] if dd_m and cont_m else ""
28
+
29
+ spine_count = len(re.findall(r"^\d+\.\s+\*\*", spine_text, re.MULTILINE))
30
+ drill_count = len(re.findall(r"^-\s+\*\*\d+\.\d+\*\*", dd_text, re.MULTILINE))
31
+ expected = spine_count + drill_count
32
+
33
+ data = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
34
+ actual = len(data["nodes"])
35
+
36
+ print(f"Outline declares {expected} nodes "
37
+ f"({spine_count} spine + {drill_count} drill-down), JSON has {actual}")
38
+
39
+ if actual < expected:
40
+ print(f"MISMATCH: {expected - actual} node(s) missing — "
41
+ f"inspect DRILL-DOWNS headers and child entry format in the outline")
42
+ sys.exit(1)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
@@ -100,6 +100,7 @@ One H3 block per slide for every spine and drill-down node declared above:
100
100
  The detailed slide content. Use bullet points and markdown formatting.
101
101
  - Point one with a specific number or named entity
102
102
  - Point two — consequence or contrast
103
+ **Visual intent:** workhorse
103
104
  **Speaker notes:**
104
105
  What the presenter says that is not on screen. Talking points, anticipated questions, timing.
105
106
  **Transition to next:**
@@ -128,7 +129,8 @@ Rules:
128
129
  - Spine header: `### SPINE N: Title`
129
130
  - Drill-down header: `### SPINE N.M: Title` — same N.M notation as DRILL-DOWNS section
130
131
  - Every node declared in SPINE and DRILL-DOWNS must have a corresponding content block
131
- - Field markers must be exactly: `**Key message:**`, `**Content:**`, `**Speaker notes:**`, `**Transition to next:**`
132
+ - Field markers must be exactly: `**Key message:**`, `**Content:**`, `**Visual intent:**` (optional), `**Speaker notes:**`, `**Transition to next:**`
133
+ - `**Visual intent:**` is optional metadata for the style agent. If included, place it **after** `**Content:**` and **before** `**Speaker notes:**` on its own line. The parser treats it as a boundary so it never bleeds into `data.content` in the JSON. Valid values: `bookend`, `workhorse`, `breathing-room`, `chapter-opener`, `impact`, `evidence`.
132
134
  - Do NOT add parenthetical context like `(drill-down under Slide 3)` after the title
133
135
 
134
136
  ---
@@ -18,15 +18,12 @@ You are an orchestrator. Execute these 7 phases in order. Do not skip phases. Do
18
18
  Run this bash command to find the plugin:
19
19
 
20
20
  ```bash
21
- if [ -f ".claude/plugins/presentation-generator/scripts/outline_to_graph.py" ]; then
22
- echo "PLUGIN_ROOT=.claude/plugins/presentation-generator"
23
- elif [ -f "$HOME/.claude/plugins/presentation-generator/scripts/outline_to_graph.py" ]; then
24
- echo "PLUGIN_ROOT=$HOME/.claude/plugins/presentation-generator"
25
- else
26
- echo "ERROR: plugin not found"
27
- fi
21
+ find -L .claude ~/.claude -path "*/presentation-generator/scripts/outline_to_graph.py" 2>/dev/null | head -1 | sed 's|/scripts/outline_to_graph.py||'
28
22
  ```
29
23
 
24
+ The output is the PLUGIN_ROOT path (e.g. `.claude/plugins/presentation-generator`).
25
+ If the output is empty, the plugin is not installed — stop and tell the user.
26
+
30
27
  Save the PLUGIN_ROOT value. You need it for every later phase.
31
28
 
32
29
  Then extract from the user's message:
@@ -46,7 +43,7 @@ Say: **"Phase 2 — Extracting content from documents"**
46
43
 
47
44
  Call the Task tool with exactly these parameters:
48
45
 
49
- - subagent_type: `presentation-content`
46
+ - subagent_type: `presentation-generator:presentation-content`
50
47
  - description: `Extract content brief`
51
48
  - prompt: (copy this template, fill in the bracketed values)
52
49
 
@@ -76,7 +73,7 @@ Say: **"Phase 3 — Designing narrative structure"**
76
73
 
77
74
  Call the Task tool with exactly these parameters:
78
75
 
79
- - subagent_type: `presentation-narrative`
76
+ - subagent_type: `presentation-generator:presentation-narrative`
80
77
  - description: `Design narrative outline`
81
78
  - prompt: (copy this template, fill in the bracketed values)
82
79
 
@@ -136,33 +133,7 @@ If exit code is non-zero, show the errors and stop.
136
133
  After a successful run, verify node count against the outline:
137
134
 
138
135
  ```bash
139
- python3 - <<'EOF'
140
- import json, re, sys
141
-
142
- with open("_temp/presentation-outline.md") as f:
143
- outline = f.read()
144
-
145
- # Scope counts to their respective sections to avoid false matches
146
- spine_m = re.search(r"^## SPINE\b", outline, re.MULTILINE)
147
- dd_m = re.search(r"^## DRILL-DOWNS\b", outline, re.MULTILINE)
148
- cont_m = re.search(r"^## CONTENT PER SLIDE\b", outline, re.MULTILINE)
149
-
150
- spine_text = outline[spine_m.end():dd_m.start()] if spine_m and dd_m else ""
151
- dd_text = outline[dd_m.end():cont_m.start()] if dd_m and cont_m else ""
152
-
153
- spine_count = len(re.findall(r"^\d+\.\s+\*\*", spine_text, re.MULTILINE))
154
- drill_count = len(re.findall(r"^-\s+\*\*\d+\.\d+\*\*", dd_text, re.MULTILINE))
155
- expected = spine_count + drill_count
156
-
157
- with open("presentations/{SLUG}/{SLUG}.json") as f:
158
- data = json.load(f)
159
- actual = len(data["nodes"])
160
-
161
- print(f"Outline declares {expected} nodes ({spine_count} spine + {drill_count} drill-down), JSON has {actual}")
162
- if actual < expected:
163
- print(f"MISMATCH: {expected - actual} node(s) missing — inspect DRILL-DOWNS headers and child entry format in the outline")
164
- sys.exit(1)
165
- EOF
136
+ python3 "{PLUGIN_ROOT}/scripts/verify_node_count.py" _temp/presentation-outline.md "presentations/{SLUG}/{SLUG}.json"
166
137
  ```
167
138
 
168
139
  If this count check fails: read the DRILL-DOWNS section of `_temp/presentation-outline.md`, identify format mismatches against `outline-format/SKILL.md`, and do NOT proceed to Phase 6 until the outline is corrected and Phase 5 re-run.
@@ -175,7 +146,7 @@ Say: **"Phase 6 — Applying visual styling"**
175
146
 
176
147
  Call the Task tool with exactly these parameters:
177
148
 
178
- - subagent_type: `presentation-style`
149
+ - subagent_type: `presentation-generator:presentation-style`
179
150
  - description: `Apply visual treatments`
180
151
  - prompt: (copy this template, fill in the bracketed values)
181
152