claude-presentation-builder 1.0.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/LICENSE +21 -0
- package/README.md +273 -0
- package/index.js +152 -0
- package/package.json +30 -0
- package/src/loop.js +92 -0
- package/src/tools.js +123 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Byrne Reese
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# claude-presentation-builder
|
|
2
|
+
|
|
3
|
+
An agentic Node.js module that generates branded PowerPoint presentations using Claude. You provide a prompt describing what you want and a skill file defining your brand rules — Claude runs a full agentic loop to generate gradient assets, write a `pptxgenjs` build script, execute it, and deliver a finished `.pptx` file.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
The module drives a multi-turn conversation with the Claude API. In each turn Claude can call three tools — `bash`, `write_file`, and `read_file` — to do real work: install packages, generate background images, write and run a Node.js build script. Your app handles none of that; it just waits for the `.pptx` to land at `outputPath`.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Your app Claude
|
|
13
|
+
│ │
|
|
14
|
+
│── generatePresentation() ────>│
|
|
15
|
+
│ │── write_file: gen_gradients.py
|
|
16
|
+
│<── tool execution ────────────│
|
|
17
|
+
│── result ────────────────────>│
|
|
18
|
+
│ │── bash: python gen_gradients.py
|
|
19
|
+
│<── tool execution ────────────│
|
|
20
|
+
│── result ────────────────────>│
|
|
21
|
+
│ │── write_file: build.js
|
|
22
|
+
│ │── bash: node build.js
|
|
23
|
+
│<── tool execution ────────────│
|
|
24
|
+
│── result ────────────────────>│
|
|
25
|
+
│ │── end_turn
|
|
26
|
+
│<── { outputPath, summary } ───│
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install claude-presentation-builder
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires Node.js 18 or later.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { generatePresentation } from "claude-presentation-builder";
|
|
45
|
+
import { readFileSync } from "fs";
|
|
46
|
+
|
|
47
|
+
const skillContent = readFileSync("./my-brand.skill.md", "utf8");
|
|
48
|
+
|
|
49
|
+
const { outputPath } = await generatePresentation({
|
|
50
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
51
|
+
prompt: "Create a 5-slide executive summary for our Q3 roadmap",
|
|
52
|
+
skillContent,
|
|
53
|
+
outputPath: "./decks/q3-roadmap.pptx",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log("Saved to:", outputPath);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## API Reference
|
|
62
|
+
|
|
63
|
+
### `generatePresentation(options)`
|
|
64
|
+
|
|
65
|
+
Returns `Promise<{ outputPath: string, summary: string }>`.
|
|
66
|
+
|
|
67
|
+
#### Parameters
|
|
68
|
+
|
|
69
|
+
| Parameter | Type | Required | Description |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| `apiKey` | `string` | ✅ | Your Anthropic API key. Use `process.env.ANTHROPIC_API_KEY` — never hardcode. |
|
|
72
|
+
| `prompt` | `string` | ✅ | Natural language description of the presentation to build. Be as specific as you like — number of slides, audience, key messages, data to include. |
|
|
73
|
+
| `skillContent` | `string` | ✅ | The full text of your brand/template skill file. This tells Claude exactly how to style the presentation. See [Writing a skill file](#writing-a-skill-file). |
|
|
74
|
+
| `outputPath` | `string` | — | Where to write the finished `.pptx`. Accepts absolute or relative paths. Defaults to `./presentation-<timestamp>.pptx` in the current working directory. |
|
|
75
|
+
| `workDir` | `string` | — | Directory Claude uses for intermediate files (gradient PNGs, build scripts, etc.). Defaults to a unique folder inside `os.tmpdir()`. Useful to set explicitly if you want to inspect intermediate outputs or cache gradient assets between runs. |
|
|
76
|
+
| `model` | `string` | — | Claude model to use. Defaults to `"claude-opus-4-6"`. |
|
|
77
|
+
| `maxTokens` | `number` | — | `max_tokens` per API call. Defaults to `8096`. Increase if Claude is truncating long build scripts. |
|
|
78
|
+
| `onProgress` | `function` | — | Progress callback invoked throughout the build. Receives a single [event object](#progress-events). Useful for logging, UI progress bars, or debugging. |
|
|
79
|
+
|
|
80
|
+
#### Return value
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
{
|
|
84
|
+
outputPath: string, // Absolute path to the written .pptx file
|
|
85
|
+
summary: string, // Claude's final text response — a short description of what was built
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Errors
|
|
90
|
+
|
|
91
|
+
| Condition | Error message |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `apiKey` not provided | `claude-presentation-builder: apiKey is required` |
|
|
94
|
+
| `prompt` not provided | `claude-presentation-builder: prompt is required` |
|
|
95
|
+
| `skillContent` not provided | `claude-presentation-builder: skillContent is required. Pass the text of your brand/template skill file.` |
|
|
96
|
+
| Claude completes without producing a file | `claude-presentation-builder: Claude did not produce output.pptx in <workDir>. Check onProgress logs for build errors.` |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### Progress events
|
|
101
|
+
|
|
102
|
+
The `onProgress` callback receives one of these event shapes on each update:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// An API call is being made
|
|
106
|
+
{ type: "api_call", round: number }
|
|
107
|
+
|
|
108
|
+
// Claude returned a stop reason
|
|
109
|
+
{ type: "stop_reason", value: "tool_use" | "end_turn", round: number }
|
|
110
|
+
|
|
111
|
+
// Claude is invoking a tool
|
|
112
|
+
{ type: "tool_call", tool: "bash" | "write_file" | "read_file", input: object }
|
|
113
|
+
|
|
114
|
+
// A tool has returned its result
|
|
115
|
+
{ type: "tool_result", tool: "bash" | "write_file" | "read_file", output: string }
|
|
116
|
+
|
|
117
|
+
// Claude finished — build is complete
|
|
118
|
+
{ type: "complete", text: string }
|
|
119
|
+
|
|
120
|
+
// Internal log line from the tool executor
|
|
121
|
+
{ type: "log", message: string }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Example — simple console logger:**
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const { outputPath } = await generatePresentation({
|
|
128
|
+
apiKey, prompt, skillContent,
|
|
129
|
+
onProgress: (e) => {
|
|
130
|
+
if (e.type === "tool_call") console.log(`⚙ ${e.tool}:`, JSON.stringify(e.input).slice(0, 80));
|
|
131
|
+
if (e.type === "tool_result") console.log(` → ${String(e.output).slice(0, 100)}`);
|
|
132
|
+
if (e.type === "complete") console.log("✅ Done");
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Example — structured logging:**
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
import pino from "pino";
|
|
141
|
+
const log = pino();
|
|
142
|
+
|
|
143
|
+
onProgress: (e) => log.info(e, "presentation-builder")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Writing a skill file
|
|
149
|
+
|
|
150
|
+
A skill file is a plain Markdown (`.md`) text file that tells Claude how to build your presentation. You load it yourself and pass its content as the `skillContent` parameter. There is no required format — Claude reads it as instructions.
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
import { readFileSync } from "fs";
|
|
154
|
+
|
|
155
|
+
const skillContent = readFileSync("./acme-brand.skill.md", "utf8");
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
A well-written skill file typically covers:
|
|
159
|
+
|
|
160
|
+
- **Background/theme** — how to generate or load slide backgrounds (colors, gradients, images)
|
|
161
|
+
- **Typography** — font names, sizes, weights, and colors for each text role
|
|
162
|
+
- **Layout library** — named slide layouts with exact positioning and dimensions
|
|
163
|
+
- **Brand assets** — where logo and icon files live and how to use them
|
|
164
|
+
- **Build workflow** — the step-by-step process Claude must follow (e.g. generate PNGs first, then write the pptxgenjs script, then run it)
|
|
165
|
+
- **Critical rules** — hard constraints that must never be violated
|
|
166
|
+
|
|
167
|
+
**Minimal example skill file:**
|
|
168
|
+
|
|
169
|
+
```markdown
|
|
170
|
+
# Acme Corp Presentation Template
|
|
171
|
+
|
|
172
|
+
Build presentations using pptxgenjs (npm install pptxgenjs).
|
|
173
|
+
|
|
174
|
+
## Background
|
|
175
|
+
All slides use a solid #1A1A2E background.
|
|
176
|
+
|
|
177
|
+
## Typography
|
|
178
|
+
- Titles: 32pt, bold, white (#FFFFFF), font "Helvetica Neue"
|
|
179
|
+
- Body: 14pt, normal, light gray (#CCCCCC), font "Helvetica Neue"
|
|
180
|
+
|
|
181
|
+
## Footer
|
|
182
|
+
Every slide: company name bottom-right, slide number bottom-left. 9pt, #888888.
|
|
183
|
+
|
|
184
|
+
## Build steps
|
|
185
|
+
1. Write build.js using pptxgenjs.
|
|
186
|
+
2. Run: npm install pptxgenjs && node build.js
|
|
187
|
+
3. Output must be written to output.pptx.
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
> **Tip:** The more precise your skill file, the more consistent the output. Define exact hex codes, font sizes in points, and element positions in inches (pptxgenjs uses inches for all coordinates on a 10" × 5.625" canvas).
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Advanced usage
|
|
195
|
+
|
|
196
|
+
### Inspect intermediate build files
|
|
197
|
+
|
|
198
|
+
Set `workDir` to a stable path to keep Claude's working files after the build completes — useful for debugging or caching gradient assets.
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
const { outputPath } = await generatePresentation({
|
|
202
|
+
apiKey, prompt, skillContent,
|
|
203
|
+
outputPath: "./output.pptx",
|
|
204
|
+
workDir: "./build-cache",
|
|
205
|
+
});
|
|
206
|
+
// ./build-cache/ will contain gen_gradients.py, bg_warm.png, build.js, output.pptx, etc.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Use a different Claude model
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
const { outputPath } = await generatePresentation({
|
|
213
|
+
apiKey, prompt, skillContent,
|
|
214
|
+
model: "claude-sonnet-4-6", // faster and cheaper; may need a more explicit skill file
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Load skill content from multiple files
|
|
219
|
+
|
|
220
|
+
Compose your skill from reusable parts:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
import { readFileSync } from "fs";
|
|
224
|
+
|
|
225
|
+
const skillContent = [
|
|
226
|
+
readFileSync("./skills/brand-colors.md", "utf8"),
|
|
227
|
+
readFileSync("./skills/slide-layouts.md", "utf8"),
|
|
228
|
+
readFileSync("./skills/build-workflow.md", "utf8"),
|
|
229
|
+
].join("\n\n---\n\n");
|
|
230
|
+
|
|
231
|
+
const { outputPath } = await generatePresentation({
|
|
232
|
+
apiKey, prompt, skillContent,
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Batch generation
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
import { generatePresentation } from "claude-presentation-builder";
|
|
240
|
+
import { readFileSync } from "fs";
|
|
241
|
+
|
|
242
|
+
const skillContent = readFileSync("./my-brand.skill.md", "utf8");
|
|
243
|
+
|
|
244
|
+
const decks = [
|
|
245
|
+
{ prompt: "Q1 business review — 8 slides", output: "./q1-review.pptx" },
|
|
246
|
+
{ prompt: "Product roadmap overview — 6 slides", output: "./roadmap.pptx" },
|
|
247
|
+
{ prompt: "New hire onboarding deck — 10 slides", output: "./onboarding.pptx" },
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
for (const deck of decks) {
|
|
251
|
+
const { outputPath } = await generatePresentation({
|
|
252
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
253
|
+
prompt: deck.prompt,
|
|
254
|
+
skillContent,
|
|
255
|
+
outputPath: deck.output,
|
|
256
|
+
});
|
|
257
|
+
console.log("Built:", outputPath);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Requirements
|
|
264
|
+
|
|
265
|
+
- Node.js 18+
|
|
266
|
+
- An [Anthropic API key](https://console.anthropic.com/)
|
|
267
|
+
- Python 3 with `Pillow` and `numpy` available on `PATH` (required if your skill generates gradient backgrounds — install with `pip install Pillow numpy`)
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-presentation-builder
|
|
3
|
+
*
|
|
4
|
+
* Agentic AI-powered presentation builder. Pass a prompt describing what
|
|
5
|
+
* you want and a skill file defining your brand/template rules — Claude
|
|
6
|
+
* drives a full agentic loop to generate, build, and write a .pptx file.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
*
|
|
10
|
+
* import { generatePresentation } from "claude-presentation-builder";
|
|
11
|
+
* import { readFileSync } from "fs";
|
|
12
|
+
*
|
|
13
|
+
* const skillContent = readFileSync("./my-brand.skill.md", "utf8");
|
|
14
|
+
*
|
|
15
|
+
* const { outputPath } = await generatePresentation({
|
|
16
|
+
* apiKey: process.env.ANTHROPIC_API_KEY,
|
|
17
|
+
* prompt: "Create a 5-slide deck about our Q3 roadmap",
|
|
18
|
+
* skillContent,
|
|
19
|
+
* outputPath: "./decks/q3-roadmap.pptx",
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* console.log("Saved to:", outputPath);
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
26
|
+
import fs from "fs";
|
|
27
|
+
import path from "path";
|
|
28
|
+
import os from "os";
|
|
29
|
+
import { TOOL_DEFINITIONS, createToolExecutor } from "./src/tools.js";
|
|
30
|
+
import { runAgenticLoop } from "./src/loop.js";
|
|
31
|
+
|
|
32
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate a branded PowerPoint presentation using an agentic Claude loop.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} options
|
|
38
|
+
* @param {string} options.apiKey Anthropic API key (required)
|
|
39
|
+
* @param {string} options.prompt What the presentation should be about (required)
|
|
40
|
+
* @param {string} options.skillContent Brand/template instructions for Claude (required).
|
|
41
|
+
* Typically the text content of a Markdown skill file.
|
|
42
|
+
* @param {string} [options.outputPath] Where to save the finished .pptx file.
|
|
43
|
+
* Defaults to ./presentation-<timestamp>.pptx
|
|
44
|
+
* @param {string} [options.workDir] Directory for intermediate build files (gradients,
|
|
45
|
+
* scripts, etc.). Defaults to a temp folder under
|
|
46
|
+
* os.tmpdir() that is unique per call.
|
|
47
|
+
* @param {string} [options.model] Claude model to use.
|
|
48
|
+
* Defaults to "claude-opus-4-6".
|
|
49
|
+
* @param {number} [options.maxTokens] max_tokens per API call. Defaults to 8096.
|
|
50
|
+
* @param {Function} [options.onProgress] Progress callback invoked throughout the build.
|
|
51
|
+
* Receives a single event object — see Event Types.
|
|
52
|
+
*
|
|
53
|
+
* @returns {Promise<{ outputPath: string, summary: string }>}
|
|
54
|
+
* outputPath — absolute path to the written .pptx file
|
|
55
|
+
* summary — Claude's final text response (a short description of what was built)
|
|
56
|
+
*
|
|
57
|
+
* @throws {Error} if apiKey, prompt, or skillContent are missing
|
|
58
|
+
* @throws {Error} if Claude completes without writing output.pptx to workDir
|
|
59
|
+
*/
|
|
60
|
+
export async function generatePresentation({
|
|
61
|
+
apiKey,
|
|
62
|
+
prompt,
|
|
63
|
+
skillContent,
|
|
64
|
+
outputPath,
|
|
65
|
+
workDir,
|
|
66
|
+
model = "claude-opus-4-6",
|
|
67
|
+
maxTokens = 8096,
|
|
68
|
+
onProgress = () => {},
|
|
69
|
+
}) {
|
|
70
|
+
// ── Validate ──────────────────────────────────────────────────────────────
|
|
71
|
+
if (!apiKey)
|
|
72
|
+
throw new Error("claude-presentation-builder: apiKey is required");
|
|
73
|
+
if (!prompt)
|
|
74
|
+
throw new Error("claude-presentation-builder: prompt is required");
|
|
75
|
+
if (!skillContent)
|
|
76
|
+
throw new Error(
|
|
77
|
+
"claude-presentation-builder: skillContent is required. " +
|
|
78
|
+
"Pass the text of your brand/template skill file. " +
|
|
79
|
+
"Example: readFileSync('./my-brand.skill.md', 'utf8')"
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// ── Resolve paths ─────────────────────────────────────────────────────────
|
|
83
|
+
const resolvedWorkDir =
|
|
84
|
+
workDir ?? path.join(os.tmpdir(), `cpb-${Date.now()}`);
|
|
85
|
+
|
|
86
|
+
const resolvedOutputPath =
|
|
87
|
+
outputPath ?? path.resolve(`presentation-${Date.now()}.pptx`);
|
|
88
|
+
|
|
89
|
+
const absoluteOutputPath = path.isAbsolute(resolvedOutputPath)
|
|
90
|
+
? resolvedOutputPath
|
|
91
|
+
: path.resolve(resolvedOutputPath);
|
|
92
|
+
|
|
93
|
+
fs.mkdirSync(resolvedWorkDir, { recursive: true });
|
|
94
|
+
fs.mkdirSync(path.dirname(absoluteOutputPath), { recursive: true });
|
|
95
|
+
|
|
96
|
+
// ── Wire up tools ─────────────────────────────────────────────────────────
|
|
97
|
+
const executeTools = createToolExecutor(resolvedWorkDir, (msg) =>
|
|
98
|
+
onProgress({ type: "log", message: msg })
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// ── Run the agentic loop ──────────────────────────────────────────────────
|
|
102
|
+
const client = new Anthropic({ apiKey });
|
|
103
|
+
|
|
104
|
+
const summary = await runAgenticLoop({
|
|
105
|
+
client,
|
|
106
|
+
model,
|
|
107
|
+
maxTokens,
|
|
108
|
+
systemPrompt: buildSystemPrompt(skillContent, resolvedWorkDir),
|
|
109
|
+
userMessage: prompt,
|
|
110
|
+
tools: TOOL_DEFINITIONS,
|
|
111
|
+
executeTools,
|
|
112
|
+
onProgress,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── Copy output to the requested destination ──────────────────────────────
|
|
116
|
+
const builtFile = path.join(resolvedWorkDir, "output.pptx");
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(builtFile)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`claude-presentation-builder: Claude did not produce output.pptx in ${resolvedWorkDir}.\n` +
|
|
121
|
+
`Check onProgress logs for build errors.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fs.copyFileSync(builtFile, absoluteOutputPath);
|
|
126
|
+
|
|
127
|
+
return { outputPath: absoluteOutputPath, summary };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
function buildSystemPrompt(skillContent, workDir) {
|
|
133
|
+
return `\
|
|
134
|
+
You are a presentation builder. Build a PowerPoint presentation by following
|
|
135
|
+
the brand and template instructions below exactly.
|
|
136
|
+
|
|
137
|
+
You have three tools:
|
|
138
|
+
- bash → run shell commands (Python, Node.js, npm install, etc.)
|
|
139
|
+
- write_file → write a file to disk
|
|
140
|
+
- read_file → read a file from disk
|
|
141
|
+
|
|
142
|
+
Working directory: ${workDir}
|
|
143
|
+
The finished presentation MUST be written to: ${workDir}/output.pptx
|
|
144
|
+
|
|
145
|
+
IMPORTANT: Do NOT narrate steps or output tool-call syntax as text.
|
|
146
|
+
Actually USE the tools to execute every step.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
${skillContent}
|
|
151
|
+
`;
|
|
152
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-presentation-builder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Agentic AI-powered presentation builder — bring your own brand skill, get back a .pptx",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@anthropic-ai/sdk": "^0.39.0"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"presentation",
|
|
22
|
+
"pptx",
|
|
23
|
+
"powerpoint",
|
|
24
|
+
"claude",
|
|
25
|
+
"anthropic",
|
|
26
|
+
"ai",
|
|
27
|
+
"agent"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|
package/src/loop.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runAgenticLoop
|
|
3
|
+
*
|
|
4
|
+
* Drives a multi-turn Claude conversation that can call tools. Keeps running
|
|
5
|
+
* until Claude returns stop_reason === "end_turn" (no more tool calls).
|
|
6
|
+
*
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {object} opts.client Anthropic SDK client instance
|
|
9
|
+
* @param {string} opts.model Model string, e.g. "claude-opus-4-6"
|
|
10
|
+
* @param {number} opts.maxTokens max_tokens for each API call
|
|
11
|
+
* @param {string} opts.systemPrompt System prompt passed to every call
|
|
12
|
+
* @param {string} opts.userMessage The initial user message
|
|
13
|
+
* @param {Array} opts.tools Tool definition array (TOOL_DEFINITIONS)
|
|
14
|
+
* @param {Function} opts.executeTools (toolName, toolInput) => string
|
|
15
|
+
* @param {Function} [opts.onProgress] Optional (event) => void progress callback
|
|
16
|
+
* event: { type, tool?, input?, output?, text? }
|
|
17
|
+
*
|
|
18
|
+
* @returns {Promise<string>} The final text response from Claude
|
|
19
|
+
*/
|
|
20
|
+
export async function runAgenticLoop({
|
|
21
|
+
client,
|
|
22
|
+
model,
|
|
23
|
+
maxTokens,
|
|
24
|
+
systemPrompt,
|
|
25
|
+
userMessage,
|
|
26
|
+
tools,
|
|
27
|
+
executeTools,
|
|
28
|
+
onProgress = () => {},
|
|
29
|
+
}) {
|
|
30
|
+
const messages = [{ role: "user", content: userMessage }];
|
|
31
|
+
let round = 0;
|
|
32
|
+
|
|
33
|
+
while (true) {
|
|
34
|
+
round++;
|
|
35
|
+
onProgress({ type: "api_call", round });
|
|
36
|
+
|
|
37
|
+
const response = await client.messages.create({
|
|
38
|
+
model,
|
|
39
|
+
max_tokens: maxTokens,
|
|
40
|
+
system: systemPrompt,
|
|
41
|
+
tools,
|
|
42
|
+
messages,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
onProgress({ type: "stop_reason", value: response.stop_reason, round });
|
|
46
|
+
|
|
47
|
+
// Append Claude's reply to history
|
|
48
|
+
messages.push({ role: "assistant", content: response.content });
|
|
49
|
+
|
|
50
|
+
// ── Done ──────────────────────────────────────────────────────────────
|
|
51
|
+
if (response.stop_reason === "end_turn") {
|
|
52
|
+
const finalText = response.content
|
|
53
|
+
.filter((b) => b.type === "text")
|
|
54
|
+
.map((b) => b.text)
|
|
55
|
+
.join("\n")
|
|
56
|
+
.trim();
|
|
57
|
+
|
|
58
|
+
onProgress({ type: "complete", text: finalText });
|
|
59
|
+
return finalText;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Tool calls ────────────────────────────────────────────────────────
|
|
63
|
+
if (response.stop_reason === "tool_use") {
|
|
64
|
+
const toolResults = [];
|
|
65
|
+
|
|
66
|
+
for (const block of response.content) {
|
|
67
|
+
if (block.type !== "tool_use") continue;
|
|
68
|
+
|
|
69
|
+
onProgress({ type: "tool_call", tool: block.name, input: block.input });
|
|
70
|
+
|
|
71
|
+
const output = executeTools(block.name, block.input);
|
|
72
|
+
|
|
73
|
+
onProgress({ type: "tool_result", tool: block.name, output });
|
|
74
|
+
|
|
75
|
+
toolResults.push({
|
|
76
|
+
type: "tool_result",
|
|
77
|
+
tool_use_id: block.id,
|
|
78
|
+
content: String(output),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Send results back and loop
|
|
83
|
+
messages.push({ role: "user", content: toolResults });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Unexpected ────────────────────────────────────────────────────────
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Unexpected stop_reason "${response.stop_reason}" in round ${round}`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
// ── Tool definitions sent to the Claude API ───────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export const TOOL_DEFINITIONS = [
|
|
8
|
+
{
|
|
9
|
+
name: "bash",
|
|
10
|
+
description:
|
|
11
|
+
"Execute a bash command in the working directory. Use this to: run Python scripts " +
|
|
12
|
+
"(e.g. generate gradient PNGs), install npm packages, run Node.js scripts to build " +
|
|
13
|
+
"the .pptx file, and inspect the filesystem. Always prefer writing scripts to disk " +
|
|
14
|
+
"first via write_file, then executing them with bash.",
|
|
15
|
+
input_schema: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
command: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "The bash command to execute.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ["command"],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "write_file",
|
|
28
|
+
description:
|
|
29
|
+
"Write text content to a file. Use this to create Python scripts, Node.js scripts, " +
|
|
30
|
+
"and any other files needed during the build. Paths are relative to the working directory " +
|
|
31
|
+
"unless absolute.",
|
|
32
|
+
input_schema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
path: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "File path (relative to working directory, or absolute).",
|
|
38
|
+
},
|
|
39
|
+
content: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Full text content to write.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
required: ["path", "content"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "read_file",
|
|
49
|
+
description:
|
|
50
|
+
"Read the contents of a file. Use this to inspect generated files, check for errors, " +
|
|
51
|
+
"or verify output.",
|
|
52
|
+
input_schema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
path: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "File path (relative to working directory, or absolute).",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
required: ["path"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// ── Tool executor factory ─────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns an executeTools(name, input) function bound to a specific workDir.
|
|
69
|
+
* Any error is caught and returned as a string so Claude can self-correct.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} workDir Absolute path to the working directory
|
|
72
|
+
* @param {Function} [log] Optional log(message) callback for progress output
|
|
73
|
+
*/
|
|
74
|
+
export function createToolExecutor(workDir, log = () => {}) {
|
|
75
|
+
fs.mkdirSync(workDir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
return function executeTools(toolName, toolInput) {
|
|
78
|
+
log(`[tool:${toolName}] ${JSON.stringify(toolInput).slice(0, 120)}`);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// ── bash ──────────────────────────────────────────────────────────────
|
|
82
|
+
if (toolName === "bash") {
|
|
83
|
+
const output = execSync(toolInput.command, {
|
|
84
|
+
cwd: workDir,
|
|
85
|
+
timeout: 120_000, // 2 min per command
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
88
|
+
});
|
|
89
|
+
const trimmed = (output || "(no output)").slice(0, 4000);
|
|
90
|
+
log(`[output] ${trimmed.slice(0, 200)}`);
|
|
91
|
+
return trimmed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── write_file ────────────────────────────────────────────────────────
|
|
95
|
+
if (toolName === "write_file") {
|
|
96
|
+
const filePath = path.isAbsolute(toolInput.path)
|
|
97
|
+
? toolInput.path
|
|
98
|
+
: path.join(workDir, toolInput.path);
|
|
99
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
100
|
+
fs.writeFileSync(filePath, toolInput.content, "utf8");
|
|
101
|
+
log(`[written] ${filePath}`);
|
|
102
|
+
return `File written: ${filePath}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── read_file ─────────────────────────────────────────────────────────
|
|
106
|
+
if (toolName === "read_file") {
|
|
107
|
+
const filePath = path.isAbsolute(toolInput.path)
|
|
108
|
+
? toolInput.path
|
|
109
|
+
: path.join(workDir, toolInput.path);
|
|
110
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
111
|
+
log(`[read] ${filePath} (${content.length} chars)`);
|
|
112
|
+
return content.slice(0, 8000); // cap to avoid blowing context
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return `Unknown tool: ${toolName}`;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// Return error text — lets Claude read it and self-correct
|
|
118
|
+
const msg = `ERROR: ${err.message}\n${err.stderr ?? ""}`.trim();
|
|
119
|
+
log(`[error] ${msg.slice(0, 300)}`);
|
|
120
|
+
return msg;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|