@tycoworks/bon 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/SKILL.md ADDED
@@ -0,0 +1,218 @@
1
+ ---
2
+ name: slides
3
+ description: >
4
+ Use this skill any time the user wants to create branded slides, presentations,
5
+ pitch decks, or sales collateral as a .pptx. Trigger whenever the user mentions "deck," "slides,"
6
+ "presentation," "pitch," or .pptx output. Also trigger when the user says "build me a deck,"
7
+ "make slides about X," or "turn this into a presentation."
8
+ ---
9
+
10
+ # slides
11
+
12
+ This skill builds on-brand decks from a deck file. The theme provides slide layouts that control design. Your job: pick the right layouts, fill them with content, and build. You never restyle the layout; the engine clones the real slides, so brand, layout, fonts, and chrome come for free.
13
+
14
+ For brand voice and naming guidelines, read `brand.md` if it exists alongside this skill.
15
+
16
+ ## Quick Reference
17
+
18
+ | Task | Guide |
19
+ |------|-------|
20
+ | Discover layouts and assets | Read `manifest.json` |
21
+ | Write a deck (structure, slots, assets) | See [Creating Slides](#creating-slides) below |
22
+ | Fix build errors | See [QA](#qa-required) below |
23
+
24
+ ---
25
+
26
+ ## Layout Discovery
27
+
28
+ Before writing anything, read `manifest.json`. It contains:
29
+
30
+ - **layouts** -- for each: `name`, `description`, `contentSlots` (with `type` and `limit`), `assetSlots` (with `accepts` and `required`), and documentation (`whenToUse`, `whenNotToUse`)
31
+ - **assets** -- brand logos, client logos, illustrations, and icons (`description`, `whenToUse`)
32
+
33
+ Study each layout's `whenToUse` and `limit`s before writing any slides. These are your primary guide for matching content to layouts.
34
+
35
+ ---
36
+
37
+ ## Creating Slides
38
+
39
+ Write a deck file: a plain JSON file with an `output` name and an ordered list of `steps`. Each step names a `layout`, fills its `content` slots, and optionally swaps `assets`.
40
+
41
+ ```json
42
+ {
43
+ "output": "my-deck.pptx",
44
+ "steps": [
45
+ { "layout": "Title",
46
+ "content": { "title": "...", "name": "Presenter Name", "jobTitle": "Their Role" } },
47
+ { "layout": "Quote dark",
48
+ "content": { "quote": "...", "attribution": ["Name", "Role, Company"] },
49
+ "assets": { "logo": "assets/clients/acme.png" } }
50
+ ]
51
+ }
52
+ ```
53
+
54
+ `content` keys must match the layout's `contentSlot` keys; `assets` keys must match its `assetSlot` keys. Asset paths are relative to the repo root.
55
+
56
+ ### Asset slots (`assetSlots`)
57
+ Each asset slot declares:
58
+ - **`accepts`** — a **sizing constraint**, not a semantic label:
59
+ - `icon` — simple, scalable graphics that read at any size: glyphs and logos.
60
+ - `image` — detailed rasters that only read large: illustrations and schematics.
61
+ - **Match the kind exactly — the engine enforces it both ways.**
62
+ - An `image` asset in an `icon` slot is unreadable at card/grid size (detail turns to noise).
63
+ - An `icon` asset in an `image` slot blows up oversized filling a half-slide area and looks stupid.
64
+ - Small slots take icons; large/full-bleed `image` slots take dense `image` assets. The asset's `type` in `manifest.json` tells you which it is. The semantic role ("the client's logo here") lives in the slot's `key`/description, not the type.
65
+ - **`required: true`** — you MUST supply this asset; the slide has no usable default and the build fails without it (e.g. team-member photos, icon-grid icons, the quote logo). If you don't have a suitable image, ask the user for one.
66
+ - Slots without `required` are optional — omit them to keep the layout's built-in default (e.g. the full-bleed background, the two-column split image).
67
+
68
+ ### Slot types (in `manifest.json`)
69
+
70
+ - **`text`** — a single line. Pass a string. (Use it for stat values too — a stat can be textual.)
71
+ - **`lines`** — a fixed set of styled lines (e.g. name + job title). Pass an array of strings, one per line.
72
+ - **`prose`** — a body block, **written as markdown**. Pass a markdown string, or an array of lines:
73
+ - `- ` (or `* `) starts a **bullet**; indent **2 spaces per level** to nest (` - ` = level 1).
74
+ - A line with no marker is a **paragraph** (a lead-in / prose line).
75
+ - Blank lines are ignored.
76
+ - Do NOT put headings in a `prose` body — the heading is the slide's `title` slot, and a subheading is the `subtitle` slot on subtitle layouts. Prose is paragraphs + bullets only.
77
+ - Some slots fill *part* of a shared box (they have a `paragraph` selector in the layout definition), e.g. a cover's `name` and `jobTitle` are two slots in one box. Nothing special to do: just fill each key. Check the layout's `contentSlots` for the exact keys.
78
+
79
+ ```json
80
+ "body": [
81
+ "Every option falls short:",
82
+ "- OLTP databases are siloed.",
83
+ "- Lakehouses lack freshness.",
84
+ " - and cost-efficiency."
85
+ ]
86
+ ```
87
+
88
+ Build:
89
+
90
+ ```bash
91
+ # Run the command from manifest.json's build.command
92
+ # e.g.: node src/run-spec.ts deck.json my-deck.pptx
93
+ ```
94
+
95
+ The deck is written to your current working directory (not inside the skill).
96
+
97
+ ---
98
+
99
+ ## Layout Selection
100
+
101
+ **Don't create boring decks.** Repeating the same layout on every slide makes a forgettable presentation. Use variety and match content shape to layout purpose.
102
+
103
+ ### Before Starting
104
+
105
+ 1. **Read the manifest thoroughly.** Each layout has `whenToUse`, `whenNotToUse`, and `limit`s. Respect all three.
106
+ 2. **Match content shape to layout purpose.** A comparison belongs in a two/three-column layout, quantified proof belongs in stat blocks, a customer voice belongs in a quote or testimonial layout. Don't force content into the wrong layout.
107
+ 3. **Plan narrative arc first.** Decide the sequence of ideas before picking layouts. Then assign each idea to its best-fit layout from the manifest. Keep one variant (all dark or all light) across the deck.
108
+
109
+ ### For Each Slide
110
+
111
+ **Every slide communicates one idea.** If you're writing more than 5 bullets or 3 paragraphs, split into two slides.
112
+
113
+ Check each layout's `limit` in the manifest for content density constraints. When content overflows, split across slides.
114
+
115
+ ### Avoid (Common Mistakes)
116
+
117
+ - **Don't repeat the same layout** -- vary layouts for visual rhythm
118
+ - **Don't dump all content on one slide** -- two clear slides beat one crowded slide
119
+ - **Don't ignore layout limits** -- if a slot says max 4 stats, use 4 or fewer
120
+ - **Don't open with a body/content layout** -- use the Title layout for impact
121
+ - **Don't skip section dividers** -- for decks over 5 slides, use Section title layouts to group sections
122
+ - **Don't restyle the layout** -- the theme owns all design; you only fill slots
123
+ - **Don't invent layout or asset names** -- only use what exists in the manifest
124
+ - **Don't leave required slots empty** -- and don't leave a placeholder logo or dummy text in an asset slot you care about
125
+ - **Don't mix dark and light** -- keep one variant across the deck
126
+
127
+ ---
128
+
129
+ ## QA (Required)
130
+
131
+ **Assume the first build will fail. Your job is to fix it.**
132
+
133
+ Your first draft almost never comes out clean. Approach QA as a debugging session, not a confirmation step. If you haven't run at least one build-fix cycle, you're not done.
134
+
135
+ ### Build
136
+
137
+ Run the command from `manifest.json`'s `build.command`:
138
+
139
+ ```bash
140
+ # e.g.: node src/run-spec.ts deck.json my-deck.pptx
141
+ ```
142
+
143
+ Read output carefully. Common errors and fixes:
144
+
145
+ | Error | Fix |
146
+ |-------|-----|
147
+ | `Unknown layout: 'xyz'` | Check layout names in `manifest.json` |
148
+ | A slot didn't fill | Use the `contentSlot` key names declared by the layout |
149
+ | An image didn't swap / placeholder remains | Use the `assetSlot` key, and an asset path that exists in `manifest.json` |
150
+ | `Cannot read ... JSON` / parse error | Fix the JSON syntax in the deck file |
151
+ | `Skipped setting relation target` | The asset image couldn't be placed; check the path and file |
152
+
153
+ ### Verification Loop
154
+
155
+ 1. Write the deck file → Build
156
+ 2. **Read every error** -- fix all of them
157
+ 3. Rebuild
158
+ 4. **If content overflows**: reduce content or split into two slides
159
+ 5. Repeat until the build exits cleanly
160
+
161
+ **Do not declare success until you've completed at least one build-fix cycle.**
162
+
163
+ ### Visual Check
164
+
165
+ After a clean build, render the `.pptx` to PNGs and inspect them:
166
+
167
+ ```bash
168
+ soffice --headless --convert-to pdf --outdir . <deck>.pptx
169
+ pdftoppm -png -r 96 <deck>.pdf <name>
170
+ ```
171
+
172
+ Read each slide image and check for:
173
+
174
+ - **Word wrapping** — text that breaks mid-word or overflows its container
175
+ - **Cramped text** — content too dense for the slide area
176
+ - **Leftover placeholders** — dummy text ("Lorem ipsum", "Firstname Lastname") or a placeholder logo that should have been swapped
177
+ - **Cut-off content** — text or images clipped at slide edges
178
+
179
+ If you spot issues, reduce content, switch layouts, or split into multiple slides. Rebuild and re-check.
180
+
181
+ ### Content Review (Use Subagents)
182
+
183
+ **Use subagents for review** -- even for short decks. You've been staring at the content and will see what you expect, not what's there. Subagents have fresh eyes.
184
+
185
+ After a successful build, spawn a subagent:
186
+
187
+ ```
188
+ Review this deck. Assume there are issues -- find them.
189
+
190
+ Check for:
191
+ - Slides that are too dense (>7 bullets, >5 paragraphs, too many stats/rows)
192
+ - Same layout repeated multiple times with no variety
193
+ - Content that doesn't match layout purpose (check whenToUse in manifest.json)
194
+ - Narrative that doesn't flow logically
195
+ - Missing opening (Title) or closing (Thank you) slide
196
+ - Slides that are too sparse (a single bullet doesn't need its own slide)
197
+ - Leftover placeholder logos or dummy text in the rendered images
198
+
199
+ For each issue, suggest a specific fix.
200
+
201
+ Read: /path/to/deck.json and the rendered PNGs in the working directory
202
+ Also read: manifest.json (for layout documentation)
203
+ ```
204
+
205
+ If the subagent finds issues, fix them and rebuild.
206
+
207
+ ---
208
+
209
+ ## Core Commands
210
+
211
+ ```bash
212
+ # Build a deck (use command from manifest.json's build.command)
213
+ # e.g.: node src/run-spec.ts deck.json my-deck.pptx
214
+
215
+ # Render to images for visual QA
216
+ soffice --headless --convert-to pdf --outdir . <deck>.pptx
217
+ pdftoppm -png -r 96 <deck>.pdf <name>
218
+ ```
package/bin/bon.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node --experimental-strip-types --no-warnings
2
+ import("../src/cli.ts");
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@tycoworks/bon",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "bin": {
7
+ "bon": "bin/bon.js"
8
+ },
9
+ "scripts": {
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@tycoworks/bon-core": "^0.1.0",
14
+ "commander": "^15.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^22.0.0",
18
+ "typescript": "^5.8.0"
19
+ }
20
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,98 @@
1
+ import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { Config, ThemeConfig } from "@tycoworks/bon-core";
5
+ import { generate } from "@tycoworks/bon-core";
6
+ import { Command } from "commander";
7
+ import { generateManifest } from "./manifest.ts";
8
+ import { smokeSteps } from "./smoke.ts";
9
+
10
+ const DEFAULT_CONFIG = "theme.config.ts";
11
+ const DEFAULT_SMOKE_OUTPUT = "smoke-all.pptx";
12
+ const SKILL_DIR = "skills/slides";
13
+ const MANIFEST_FILE = "manifest.json";
14
+ const SKILL_FILE = "SKILL.md";
15
+ const BUILD_COMMAND = "npx bon build";
16
+
17
+ const sdkDir = dirname(fileURLToPath(import.meta.url));
18
+ const skillMdPath = resolve(sdkDir, "..", SKILL_FILE);
19
+
20
+ async function loadConfig(configPath: string): Promise<Config> {
21
+ const abs = resolve(process.cwd(), configPath);
22
+ let mod: Record<string, unknown>;
23
+ try {
24
+ mod = await import(abs);
25
+ } catch {
26
+ throw new Error(`Config file not found: ${configPath}`);
27
+ }
28
+ const raw = (mod.default ?? mod.config) as ThemeConfig | undefined;
29
+ if (!raw) {
30
+ throw new Error(`${configPath} must export a default or named "config" of type ThemeConfig`);
31
+ }
32
+ return { ...raw, rootDir: dirname(abs) };
33
+ }
34
+
35
+ const program = new Command().name("bon").description("PPTX template engine CLI").version("0.1.0");
36
+
37
+ program
38
+ .command("build")
39
+ .description("Build a PPTX deck from a JSON spec")
40
+ .argument("<deck>", "path to deck JSON file")
41
+ .argument("[output]", "output PPTX filename")
42
+ .option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
43
+ .action(async (deckPath: string, output: string | undefined, opts: { config: string }) => {
44
+ const config = await loadConfig(opts.config);
45
+ let deck: Record<string, unknown>;
46
+ try {
47
+ const raw = readFileSync(resolve(process.cwd(), deckPath), "utf-8");
48
+ deck = JSON.parse(raw);
49
+ } catch (err) {
50
+ throw new Error(`Failed to read deck file: ${deckPath} (${(err as Error).message})`);
51
+ }
52
+ if (output) deck.output = output;
53
+ await generate(deck as any, config);
54
+ });
55
+
56
+ program
57
+ .command("manifest")
58
+ .description("Generate manifest.json from theme config")
59
+ .option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
60
+ .option(`-o, --out <file>`, "write to file instead of stdout")
61
+ .action(async (opts: { config: string; out?: string }) => {
62
+ const config = await loadConfig(opts.config);
63
+ const json = generateManifest(config, { build: { command: BUILD_COMMAND } });
64
+ if (opts.out) {
65
+ writeFileSync(resolve(process.cwd(), opts.out), `${json}\n`);
66
+ console.log(`WROTE ${opts.out}`);
67
+ } else {
68
+ process.stdout.write(`${json}\n`);
69
+ }
70
+ });
71
+
72
+ program
73
+ .command("smoke")
74
+ .description("Generate one smoke-test slide per layout")
75
+ .option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
76
+ .option(`-o, --out <file>`, "output PPTX filename", DEFAULT_SMOKE_OUTPUT)
77
+ .action(async (opts: { config: string; out: string }) => {
78
+ const config = await loadConfig(opts.config);
79
+ const steps = smokeSteps(config);
80
+ await generate({ output: opts.out, steps }, config);
81
+ });
82
+
83
+ program
84
+ .command("plugin")
85
+ .description("Generate plugin package (manifest.json + SKILL.md) for AI agents")
86
+ .option(`-c, --config <path>`, "path to theme config file", DEFAULT_CONFIG)
87
+ .action(async (opts: { config: string }) => {
88
+ const config = await loadConfig(opts.config);
89
+ const outDir = resolve(process.cwd(), SKILL_DIR);
90
+ mkdirSync(outDir, { recursive: true });
91
+ const json = generateManifest(config, { build: { command: BUILD_COMMAND } });
92
+ writeFileSync(resolve(outDir, MANIFEST_FILE), `${json}\n`);
93
+ console.log(`WROTE ${SKILL_DIR}/${MANIFEST_FILE}`);
94
+ copyFileSync(skillMdPath, resolve(outDir, SKILL_FILE));
95
+ console.log(`WROTE ${SKILL_DIR}/${SKILL_FILE}`);
96
+ });
97
+
98
+ await program.parseAsync(process.argv);
package/src/config.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { ThemeConfig } from "@tycoworks/bon-core";
2
+
3
+ export function defineConfig(config: ThemeConfig): ThemeConfig {
4
+ return config;
5
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export type {
2
+ AssetCatalog,
3
+ AssetEntry,
4
+ AssetSlot,
5
+ Config,
6
+ ContentSlot,
7
+ Deck,
8
+ DeckStep,
9
+ Layout,
10
+ ParagraphSelector,
11
+ ThemeConfig,
12
+ } from "@tycoworks/bon-core";
13
+ export { AssetType, Fit, generate, SlotType } from "@tycoworks/bon-core";
14
+ export { defineConfig } from "./config.ts";
15
+ export type { ManifestOptions } from "./manifest.ts";
16
+ export { generateManifest } from "./manifest.ts";
17
+ export { smokeSteps } from "./smoke.ts";
@@ -0,0 +1,106 @@
1
+ import type { Config } from "@tycoworks/bon-core";
2
+
3
+ export type ManifestOptions = {
4
+ build?: { command: string };
5
+ };
6
+
7
+ type ManifestContentSlot = {
8
+ key: string;
9
+ type: string;
10
+ limit?: { maxChars?: number; maxLines?: number; maxItems?: number };
11
+ };
12
+
13
+ type ManifestAssetSlot = {
14
+ key: string;
15
+ accepts: string;
16
+ required?: true;
17
+ };
18
+
19
+ type ManifestLayout = {
20
+ name: string;
21
+ description: string;
22
+ whenToUse: string;
23
+ whenNotToUse: string;
24
+ contentSlots: ManifestContentSlot[];
25
+ assetSlots: ManifestAssetSlot[];
26
+ };
27
+
28
+ type ManifestAssetEntry = {
29
+ path: string;
30
+ type: string;
31
+ description: string;
32
+ whenToUse?: string;
33
+ };
34
+
35
+ type Manifest = {
36
+ version: 1;
37
+ layouts: ManifestLayout[];
38
+ assets: Record<string, Record<string, ManifestAssetEntry>>;
39
+ build: {
40
+ command: string;
41
+ deckFormat: "json";
42
+ deckSchema: {
43
+ output: "string";
44
+ steps: "array of { layout, content, assets }";
45
+ };
46
+ };
47
+ };
48
+
49
+ function stripContentSlot(slot: { key: string; type: string; limit?: object }): ManifestContentSlot {
50
+ const result: ManifestContentSlot = { key: slot.key, type: slot.type };
51
+ if (slot.limit) {
52
+ result.limit = slot.limit;
53
+ }
54
+ return result;
55
+ }
56
+
57
+ function stripAssetSlot(slot: { key: string; accepts: string; required?: boolean }): ManifestAssetSlot {
58
+ const result: ManifestAssetSlot = { key: slot.key, accepts: slot.accepts };
59
+ if (slot.required) {
60
+ result.required = true;
61
+ }
62
+ return result;
63
+ }
64
+
65
+ export function generateManifest(config: Config, options?: ManifestOptions): string {
66
+ const layouts: ManifestLayout[] = config.layouts.map((layout) => ({
67
+ name: layout.name,
68
+ description: layout.description,
69
+ whenToUse: layout.whenToUse,
70
+ whenNotToUse: layout.whenNotToUse,
71
+ contentSlots: layout.contentSlots.map(stripContentSlot),
72
+ assetSlots: (layout.assetSlots ?? []).map(stripAssetSlot),
73
+ }));
74
+
75
+ const assets: Record<string, Record<string, ManifestAssetEntry>> = {};
76
+ for (const [category, entries] of Object.entries(config.assets)) {
77
+ assets[category] = {};
78
+ for (const [name, entry] of Object.entries(entries)) {
79
+ const manifestEntry: ManifestAssetEntry = {
80
+ path: entry.path,
81
+ type: entry.type,
82
+ description: entry.description,
83
+ };
84
+ if (entry.whenToUse) {
85
+ manifestEntry.whenToUse = entry.whenToUse;
86
+ }
87
+ assets[category][name] = manifestEntry;
88
+ }
89
+ }
90
+
91
+ const manifest: Manifest = {
92
+ version: 1,
93
+ layouts,
94
+ assets,
95
+ build: {
96
+ command: options?.build?.command ?? "npx bon build",
97
+ deckFormat: "json",
98
+ deckSchema: {
99
+ output: "string",
100
+ steps: "array of { layout, content, assets }",
101
+ },
102
+ },
103
+ };
104
+
105
+ return JSON.stringify(manifest, null, 2);
106
+ }
package/src/smoke.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { Config, DeckStep } from "@tycoworks/bon-core";
2
+ import { SlotType } from "@tycoworks/bon-core";
3
+
4
+ function pickAsset(config: Config, slot: { key: string; accepts: string }): string | undefined {
5
+ for (const group of Object.values(config.assets)) {
6
+ for (const entry of Object.values(group)) {
7
+ if (entry.type === slot.accepts) return entry.path;
8
+ }
9
+ }
10
+ return undefined;
11
+ }
12
+
13
+ export function smokeSteps(config: Config): DeckStep[] {
14
+ return config.layouts.map((layout) => {
15
+ const content: Record<string, string | string[]> = {};
16
+ for (const s of layout.contentSlots) {
17
+ if (s.type === SlotType.Prose) {
18
+ content[s.key] = ["Sample intro line for this block.", "- First point", "- Second point"];
19
+ } else if (s.type === SlotType.Lines) {
20
+ content[s.key] = ["Sample Name", "Sample Role, Company"];
21
+ } else if (s.key.includes("value")) {
22
+ content[s.key] = "42%";
23
+ } else {
24
+ content[s.key] = `Sample ${s.key.replace(/_/g, " ")}`;
25
+ }
26
+ }
27
+ const assets: Record<string, string> = {};
28
+ for (const a of layout.assetSlots ?? []) {
29
+ const path = pickAsset(config, a);
30
+ if (path) assets[a.key] = path;
31
+ }
32
+ return { layout: layout.name, content, assets };
33
+ });
34
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "lib": ["es2023"],
5
+ "module": "nodenext",
6
+ "moduleResolution": "nodenext",
7
+ "allowImportingTsExtensions": true,
8
+ "verbatimModuleSyntax": true,
9
+ "erasableSyntaxOnly": true,
10
+ "declaration": true,
11
+ "declarationDir": "dist",
12
+ "emitDeclarationOnly": true,
13
+ "strict": true,
14
+ "skipLibCheck": true,
15
+ "composite": true
16
+ },
17
+ "include": ["src/**/*.ts"],
18
+ "references": [{ "path": "../core" }]
19
+ }