@tycoworks/tycoslide 0.12.0 → 0.13.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
@@ -54,7 +54,7 @@ npx tycoslide build deck.md # → deck.pptx
54
54
  ```bash
55
55
  npx tycoslide build deck.md # markdown → PPTX (theme resolved from deck frontmatter)
56
56
  npx tycoslide build deck.md --no-notes # omit speaker notes from the output
57
- npx tycoslide package # regenerate skill.md/syntax.md/manifest.json + zip the theme into a self-contained <package-name>.zip
57
+ npx tycoslide package # regenerate the skill files + zip the theme into a self-contained <package-name>.zip
58
58
  ```
59
59
 
60
60
  ## Theme Structure
@@ -70,14 +70,16 @@ my-theme/
70
70
  package.json
71
71
  skill.md # generated by `tycoslide package`
72
72
  syntax.md # generated by `tycoslide package`
73
- manifest.json # generated by `tycoslide package`
73
+ manifest.json # generated by `tycoslide package` -- the theme's layouts
74
+ assets.json # generated by `tycoslide package` -- the theme's picture catalog
74
75
  <package-name>.zip # uploadable Agent Skill bundle
75
76
  ```
76
77
 
77
78
  **Template** — the PPTX file with named shapes that tycoslide fills.
78
79
  **Layout** — a slide pattern in the template (Title, Body, Quote, etc.).
79
80
  **Theme** — the directory that bundles a template, assets, and config.
80
- **Manifest** — a machine-readable catalog of layouts and assets for AI agents.
81
+ **Manifest** — the machine-readable list of layouts, for an AI agent to read.
82
+ **Catalog** — the machine-readable list of assets, for an AI agent to search.
81
83
 
82
84
  ## Diagrams
83
85
 
package/SKILL.md CHANGED
@@ -26,7 +26,8 @@ This skill builds on-brand decks from a markdown deck file. The theme provides s
26
26
 
27
27
  | Task | Guide |
28
28
  |------|-------|
29
- | Discover layouts and assets | Read `manifest.json` |
29
+ | Discover layouts | Read `manifest.json` |
30
+ | Find a logo, illustration or icon | Search `assets.json` |
30
31
  | Write a deck (structure, slots, assets) | See [Creating Slides](#creating-slides) below |
31
32
  | Fix build errors | See [QA](#qa-required) below |
32
33
 
@@ -34,10 +35,9 @@ This skill builds on-brand decks from a markdown deck file. The theme provides s
34
35
 
35
36
  ## Layout Discovery
36
37
 
37
- Before writing anything, read `manifest.json`. It contains:
38
+ Before writing anything, read `manifest.json`. It lists the theme's **layouts** -- for each: `name`, `description`, `parameters` (frontmatter inputs) and `slots` (body regions). A layout is identified by its `name`; every parameter and slot by its `key`. Parameters carry a `type`, slots carry `accepts`, and either may be `required`.
38
39
 
39
- - **layouts** -- for each: `name`, `description`, `parameters` (frontmatter inputs) and `slots` (body regions). A layout is identified by its `name`; every parameter and slot by its `key`. Parameters carry a `type`, slots carry `accepts`, and either may be `required`
40
- - **assets** -- brand logos, client logos, illustrations, and icons (`description`)
40
+ Pictures live in `assets.json`: every logo, illustration and icon the theme offers, keyed by category and name. **Search it, do not read it whole** -- an icon set alone can run to thousands of entries. Grep for the concept you want (`grep -i "arrow" assets.json`) and use the `$category.name` you find.
41
41
 
42
42
  A layout's inputs split two ways (see [syntax.md](syntax.md) for details):
43
43
  - **parameters** -- one value on a frontmatter line. Fill by putting a value under the parameter's key in the slide frontmatter.
@@ -142,7 +142,7 @@ Keep each slot's content to what its region comfortably holds. When content over
142
142
  - **Don't overstuff a slot** -- keep content to what its region comfortably holds; split across slides when there's too much
143
143
  - **Don't restyle the layout** -- the theme owns all design; you only fill slots
144
144
  - **Don't use an image that's wrong for the slot** -- a small slot wants a simple icon, not a dense illustration. If you get a `shrunk to X%` warning, look at the rendered slide: if the image is now too small to make out, use a simpler one.
145
- - **Don't invent layout or asset names** -- only use what exists in the manifest
145
+ - **Don't invent layout or asset names** -- only use layouts from `manifest.json` and assets from `assets.json`
146
146
  - **Don't leave required parameters or slots empty** -- and don't leave a placeholder logo or dummy text in an image slot you care about
147
147
 
148
148
  ---
@@ -159,7 +159,7 @@ Build the deck again ([Build](#build)) and read the output carefully. Common err
159
159
  |-------|-----|
160
160
  | `unknown layout "xyz"` | Check layout names in `manifest.json` |
161
161
  | A parameter or slot didn't fill | Use the key names the layout declares -- parameters in frontmatter, slots as body regions |
162
- | An image didn't swap / placeholder remains | Write a `::key::` region using the image slot's key, containing `![]($category.name)` from `manifest.json` |
162
+ | An image didn't swap / placeholder remains | Write a `::key::` region using the image slot's key, containing `![]($category.name)` from `assets.json` |
163
163
  | YAML parse error | Fix the YAML syntax in the slide's frontmatter |
164
164
  | `Skipped setting relation target` | The asset image couldn't be placed; check the path and file |
165
165
  | `forbidden style directive` | Remove `style`, `classDef`, `linkStyle`, or `%%{init}` from your mermaid block -- use `class` for grouping instead |
@@ -211,7 +211,7 @@ Check for:
211
211
  For each issue, suggest a specific fix.
212
212
 
213
213
  Read: /path/to/deck.md and the rendered PNGs in the working directory
214
- Also read: manifest.json (for layout documentation)
214
+ Also read: manifest.json (for layout documentation); search assets.json for pictures
215
215
  ```
216
216
 
217
217
  If the subagent finds issues, fix them and rebuild.
package/dist/cli.js CHANGED
@@ -3,9 +3,9 @@ import { basename, dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { Command } from "commander";
5
5
  import { buildDeck } from "./index.js";
6
- import { generateManifest } from "./manifest.js";
6
+ import { ASSETS_FILE, generateAssetCatalog, generateManifest } from "./manifest.js";
7
7
  import { compileDeck, loadThemeConfig, parseSlideDocument, RESERVED_KEY } from "./markdown/index.js";
8
- import { renameSkill, zipDir } from "./skillZip.js";
8
+ import { renameSkill, skillPackageJson, zipDir } from "./skillZip.js";
9
9
  const DEFAULT_CONFIG = "theme.json";
10
10
  const MANIFEST_FILE = "manifest.json";
11
11
  // The theme skill is written as lowercase skill.md (copied from tycoslide's own
@@ -60,11 +60,12 @@ program
60
60
  if (!themePkg.name) {
61
61
  throw new Error('Cannot name the skill: the theme\'s package.json has no "name" field.');
62
62
  }
63
- // basename drops any npm scope, e.g. "@acme/mz-slides" -> "mz-slides".
63
+ // basename drops any npm scope, e.g. "@acme/acme-slides" -> "acme-slides".
64
64
  const skillName = basename(themePkg.name);
65
- const manifestJson = `${generateManifest(config)}\n`;
66
- writeFileSync(resolve(process.cwd(), MANIFEST_FILE), manifestJson);
65
+ writeFileSync(resolve(process.cwd(), MANIFEST_FILE), `${generateManifest(config)}\n`);
67
66
  console.log(`WROTE ${MANIFEST_FILE}`);
67
+ writeFileSync(resolve(process.cwd(), ASSETS_FILE), `${generateAssetCatalog(config)}\n`);
68
+ console.log(`WROTE ${ASSETS_FILE}`);
68
69
  let skillMd;
69
70
  try {
70
71
  skillMd = renameSkill(readFileSync(skillMdPath, "utf-8"), skillName);
@@ -80,8 +81,9 @@ program
80
81
  // Bundle the WHOLE theme so the skill is self-contained: unzip ->
81
82
  // `npm install` (pulls the engine + its deps) -> `npx tycoslide build`.
82
83
  const zipFile = `${skillName}.zip`;
83
- const generated = [opts.config, MANIFEST_FILE, SKILL_FILE, SYNTAX_FILE];
84
- writeFileSync(resolve(process.cwd(), zipFile), await zipDir(process.cwd(), skillName, config, generated));
84
+ const generated = [opts.config, MANIFEST_FILE, ASSETS_FILE, SKILL_FILE, SYNTAX_FILE];
85
+ const skillPkg = skillPackageJson(themePkg, { name: pkg.name, version: pkg.version });
86
+ writeFileSync(resolve(process.cwd(), zipFile), await zipDir(process.cwd(), skillName, config, generated, skillPkg));
85
87
  console.log(`WROTE ${zipFile}`);
86
88
  });
87
89
  // Everything below the CLI throws plain Errors carrying a written-for-humans
package/dist/index.d.ts CHANGED
@@ -34,6 +34,6 @@ export declare function toEngineConfig(config: CompilerConfig): Config;
34
34
  export declare function buildDeck(deck: CompilerDeck, config: CompilerConfig, options?: GenerateOptions): Promise<void>;
35
35
  export type { Config, Deck, DeckStep, GenerateOptions, ImageFill, Layout, Slot, StyledParagraph, TableFill, TextFill, TextRun, ThemeConfig, } from "./engine/index.js";
36
36
  export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
37
- export { generateManifest } from "./manifest.js";
37
+ export { ASSETS_FILE, generateAssetCatalog, generateManifest } from "./manifest.js";
38
38
  export type { AssetCatalog, AssetEntry, CompilerBlock, CompilerConfig, CompilerDeck, CompilerDeckStep, CompilerLayout, CompilerParameter, CompilerSlot, CompilerThemeConfig, EngineFill, MermaidConfig, MermaidVariant, ParsedDocument, RawSlide, } from "./markdown/index.js";
39
39
  export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
package/dist/index.js CHANGED
@@ -118,6 +118,6 @@ export async function buildDeck(deck, config, options = {}) {
118
118
  // Engine — primitives-only public surface.
119
119
  export { fillImage, fillTable, fillTemplate, fillText, generate, SlotType } from "./engine/index.js";
120
120
  // Authoring
121
- export { generateManifest } from "./manifest.js";
121
+ export { ASSETS_FILE, generateAssetCatalog, generateManifest } from "./manifest.js";
122
122
  // Markdown / Compiler
123
123
  export { AcceptType, compileMarkdownDeck, loadThemeConfig, parseThemeConfig } from "./markdown/index.js";
@@ -1,2 +1,7 @@
1
1
  import type { CompilerConfig } from "./markdown/types.js";
2
+ /** Filename of the searchable asset catalog, named by the manifest that points at it. */
3
+ export declare const ASSETS_FILE = "assets.json";
4
+ /** The layouts document: read whole, so it carries no open-ended list. */
2
5
  export declare function generateManifest(config: CompilerConfig): string;
6
+ /** The catalog document: searched by name, never read whole. */
7
+ export declare function generateAssetCatalog(config: CompilerConfig): string;
package/dist/manifest.js CHANGED
@@ -18,6 +18,9 @@ function stripSlot(slot) {
18
18
  result.required = true;
19
19
  return result;
20
20
  }
21
+ /** Filename of the searchable asset catalog, named by the manifest that points at it. */
22
+ export const ASSETS_FILE = "assets.json";
23
+ /** The layouts document: read whole, so it carries no open-ended list. */
21
24
  export function generateManifest(config) {
22
25
  const layouts = config.layouts.map((layout) => {
23
26
  const ml = {
@@ -30,18 +33,17 @@ export function generateManifest(config) {
30
33
  ml.description = layout.description;
31
34
  return ml;
32
35
  });
36
+ const manifest = { layouts, assets: ASSETS_FILE };
37
+ return JSON.stringify(manifest, null, 2);
38
+ }
39
+ /** The catalog document: searched by name, never read whole. */
40
+ export function generateAssetCatalog(config) {
33
41
  const assets = {};
34
42
  for (const [category, entries] of Object.entries(config.assets)) {
35
43
  assets[category] = {};
36
44
  for (const [name, entry] of Object.entries(entries)) {
37
- const manifestEntry = {
38
- path: entry.path,
39
- type: entry.type,
40
- description: entry.description,
41
- };
42
- assets[category][name] = manifestEntry;
45
+ assets[category][name] = { path: entry.path, type: entry.type, description: entry.description };
43
46
  }
44
47
  }
45
- const manifest = { layouts, assets };
46
- return JSON.stringify(manifest, null, 2);
48
+ return JSON.stringify(assets, null, 2);
47
49
  }
@@ -257,7 +257,8 @@ export async function compileDeck(doc, config) {
257
257
  const [, category, name] = match;
258
258
  const entry = assets[category]?.[name];
259
259
  if (!entry) {
260
- // Every asset in the catalog is far too many to read (mz-slides has 126).
260
+ // Every asset in the catalog is far too many to read: a theme's icon set
261
+ // alone can run to thousands.
261
262
  // A known category narrows it to that category's names, which is what the
262
263
  // author is choosing between; an unknown one lists the categories instead.
263
264
  const group = assets[category];
@@ -43,8 +43,8 @@ export declare const ThemeConfigSchema: z.ZodObject<{
43
43
  assets: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
44
44
  path: z.ZodString;
45
45
  type: z.ZodEnum<{
46
- icon: "icon";
47
46
  image: "image";
47
+ icon: "icon";
48
48
  background: "background";
49
49
  }>;
50
50
  description: z.ZodString;
@@ -6,11 +6,30 @@ import type { CompilerThemeConfig } from "./markdown/types.js";
6
6
  * Throws if there is no frontmatter or no `name:` line — the caller names the file.
7
7
  */
8
8
  export declare function renameSkill(md: string, name: string): string;
9
+ /**
10
+ * The `package.json` a packaged skill installs from — deliberately NOT the theme's
11
+ * own. A theme repo's manifest is a development document: it carries the script
12
+ * that regenerates the skill, and lists the engine as a devDependency because the
13
+ * repo builds with it rather than shipping it.
14
+ *
15
+ * Copying that verbatim breaks the consumer twice. The build script runs as a
16
+ * postinstall inside their container, so anything it touches that is read-only
17
+ * fails their whole `npm install`. And under `--omit=dev` the engine is never
18
+ * installed, so neither the postinstall nor `npx tycoslide build` can find it.
19
+ *
20
+ * What ships instead declares only what the skill needs to RUN: the theme's own
21
+ * dependencies plus the engine, as runtime dependencies, and no scripts at all.
22
+ */
23
+ export declare function skillPackageJson(theme: Record<string, unknown>, engine: {
24
+ name: string;
25
+ version: string;
26
+ }): string;
9
27
  /**
10
28
  * Zip a theme into an uploadable Agent Skill archive whose entries all live
11
- * under a single root folder (e.g. `mz-slides/theme.json`), matching Anthropic's
29
+ * under a single root folder (e.g. `acme-slides/theme.json`), matching Anthropic's
12
30
  * custom-skill format. `generated` names the files the caller just wrote (the
13
- * config, manifest, skill.md, syntax.md). Optional support files are skipped
14
- * when absent; anything the config declares but that is missing is an error.
31
+ * config, manifest, skill.md, syntax.md); `packageJson` is the authored manifest
32
+ * from `skillPackageJson`. Optional support files are skipped when absent;
33
+ * anything the config declares but that is missing is an error.
15
34
  */
16
- export declare function zipDir(rootDir: string, folderName: string, config: CompilerThemeConfig, generated: string[]): Promise<Buffer>;
35
+ export declare function zipDir(rootDir: string, folderName: string, config: CompilerThemeConfig, generated: string[], packageJson: string): Promise<Buffer>;
package/dist/skillZip.js CHANGED
@@ -18,13 +18,42 @@ export function renameSkill(md, name) {
18
18
  throw new Error('skill.md frontmatter has no "name:" line');
19
19
  return md.replace(block[0], block[0].replace(NAME_LINE, `name: ${name}`));
20
20
  }
21
+ /** The manifest a packaged skill installs from, authored rather than copied. */
22
+ const PACKAGE_JSON = "package.json";
21
23
  /**
22
- * Files a packaged skill needs beyond the theme's own declarations. `package.json`
23
- * matters most: the unzip flow is `npm install` -> `npx tycoslide build`, so it
24
- * restores the engine and any npm-resolved brand fonts. The lockfile is taken
25
- * when present so that install is reproducible.
24
+ * Files a packaged skill needs beyond the theme's own declarations. Only the
25
+ * lockfile: `package.json` is authored by `skillPackageJson` rather than taken
26
+ * from the theme directory.
26
27
  */
27
- const SUPPORT_FILES = ["package.json", "package-lock.json"];
28
+ const SUPPORT_FILES = ["package-lock.json"];
29
+ /**
30
+ * The `package.json` a packaged skill installs from — deliberately NOT the theme's
31
+ * own. A theme repo's manifest is a development document: it carries the script
32
+ * that regenerates the skill, and lists the engine as a devDependency because the
33
+ * repo builds with it rather than shipping it.
34
+ *
35
+ * Copying that verbatim breaks the consumer twice. The build script runs as a
36
+ * postinstall inside their container, so anything it touches that is read-only
37
+ * fails their whole `npm install`. And under `--omit=dev` the engine is never
38
+ * installed, so neither the postinstall nor `npx tycoslide build` can find it.
39
+ *
40
+ * What ships instead declares only what the skill needs to RUN: the theme's own
41
+ * dependencies plus the engine, as runtime dependencies, and no scripts at all.
42
+ */
43
+ export function skillPackageJson(theme, engine) {
44
+ const dependencies = {
45
+ ...(theme.dependencies ?? {}),
46
+ [engine.name]: `^${engine.version}`,
47
+ };
48
+ const skill = {
49
+ name: theme.name,
50
+ version: theme.version,
51
+ description: theme.description,
52
+ private: true,
53
+ dependencies: Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))),
54
+ };
55
+ return `${JSON.stringify(skill, null, 2)}\n`;
56
+ }
28
57
  /**
29
58
  * Every path a packaged theme needs, relative to `rootDir` and POSIX-separated.
30
59
  *
@@ -40,18 +69,20 @@ function skillPaths(config, generated) {
40
69
  }
41
70
  /**
42
71
  * Zip a theme into an uploadable Agent Skill archive whose entries all live
43
- * under a single root folder (e.g. `mz-slides/theme.json`), matching Anthropic's
72
+ * under a single root folder (e.g. `acme-slides/theme.json`), matching Anthropic's
44
73
  * custom-skill format. `generated` names the files the caller just wrote (the
45
- * config, manifest, skill.md, syntax.md). Optional support files are skipped
46
- * when absent; anything the config declares but that is missing is an error.
74
+ * config, manifest, skill.md, syntax.md); `packageJson` is the authored manifest
75
+ * from `skillPackageJson`. Optional support files are skipped when absent;
76
+ * anything the config declares but that is missing is an error.
47
77
  */
48
- export async function zipDir(rootDir, folderName, config, generated) {
78
+ export async function zipDir(rootDir, folderName, config, generated, packageJson) {
49
79
  const zip = new JSZip();
50
80
  const folder = zip.folder(folderName);
51
81
  if (!folder)
52
82
  throw new Error(`Failed to create zip folder: ${folderName}`);
83
+ folder.file(PACKAGE_JSON, packageJson);
53
84
  const optional = new Set(SUPPORT_FILES);
54
- let count = 0;
85
+ let count = 1;
55
86
  for (const rel of skillPaths(config, generated)) {
56
87
  const abs = join(rootDir, ...rel.split("/"));
57
88
  if (!existsSync(abs)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tycoworks/tycoslide",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Create editable, on-brand PowerPoint slides from markdown.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/syntax.md CHANGED
@@ -140,7 +140,7 @@ Fill a slot by writing a `::key::` region in the body; the marker maps to the sl
140
140
 
141
141
  ![]($brand.primaryDarkWordmark)
142
142
  ```
143
- The categories and names are listed in `manifest.json`. How the picture is scaled and cropped comes from the **asset's `type`** in the catalog: `icon` never enlarges past native and never crops, `image` fits the whole picture without cropping, `background` fills the frame and center-crops. A fenced `mermaid` block also fills an image slot, rendering to a themed PNG (see below).
143
+ The categories and names are catalogued in `assets.json`. How the picture is scaled and cropped comes from the **asset's `type`** in the catalog: `icon` never enlarges past native and never crops, `image` fits the whole picture without cropping, `background` fills the frame and center-crops. A fenced `mermaid` block also fills an image slot, rendering to a themed PNG (see below).
144
144
 
145
145
  ---
146
146