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 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
+ }