@webstew/bridge 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.
@@ -0,0 +1,532 @@
1
+ "use strict";
2
+ // Invoke the local `claude` CLI for one agent turn, stream events back
3
+ // to the caller as BridgeResponse chunks.
4
+ //
5
+ // Why CLI instead of @anthropic-ai/claude-agent-sdk:
6
+ // - User already has `claude` on PATH if they have Pro/Max — zero extra
7
+ // install for the bridge to work.
8
+ // - The CLI's `--output-format stream-json` emits one JSON event per
9
+ // line which is trivial to parse line-buffered.
10
+ // - Same auth path as Claude Code (subscription tokens, OAuth refresh,
11
+ // --resume sessions) — the bridge inherits whatever the user already
12
+ // authenticated.
13
+ //
14
+ // Flow per request:
15
+ // 1. Materialize the project VFS into a per-project workspace dir
16
+ // (write each file, mkdir -p as needed).
17
+ // 2. Snapshot the dir (path → contents map) so we can diff after.
18
+ // 3. Spawn `claude --print "..." --output-format stream-json [...]`
19
+ // with cwd = workspace dir.
20
+ // 4. Read its stdout line-by-line, parse each JSON event, map to a
21
+ // BridgeResponse chunk, hand to the onEvent callback.
22
+ // 5. On process exit, diff dir vs snapshot — emit file_update for
23
+ // changed/new files, file_delete for removed.
24
+ // 6. Emit `done` (or `error` on non-zero exit).
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.runClaudeOnce = runClaudeOnce;
30
+ const node_child_process_1 = require("node:child_process");
31
+ const node_fs_1 = __importDefault(require("node:fs"));
32
+ const node_path_1 = __importDefault(require("node:path"));
33
+ const node_readline_1 = __importDefault(require("node:readline"));
34
+ const auth_1 = require("./auth");
35
+ // CLI flag mapping. Versions of `claude` may differ — these are the
36
+ // stable v0.x flags. If a flag is unsupported the CLI errors out; we
37
+ // surface the error in the BridgeResponse so the user sees it in chat.
38
+ function pickModelFlag(model) {
39
+ // Only pass --model when it's a real Claude Code model identifier
40
+ // (claude-opus-4-x, claude-sonnet-4-x, claude-haiku-4-x, etc.).
41
+ // Webstew's selector also has 'auto' (server router), 'gpt-4o', etc.
42
+ // — those would make claude exit 1 with no stderr. Skip them, let
43
+ // claude use its own default (which is whatever the user picked in
44
+ // Claude Code settings).
45
+ if (!model)
46
+ return [];
47
+ if (!/^claude-(opus|sonnet|haiku)-/i.test(model))
48
+ return [];
49
+ return ['--model', model];
50
+ }
51
+ async function runClaudeOnce(opts) {
52
+ const { request, requestId, onEvent } = opts;
53
+ const projectId = request.projectId || '_unscoped_';
54
+ const workspaceDir = (0, auth_1.workspaceDirFor)(projectId);
55
+ // 1a. Drop a CLAUDE.md so claude knows it's working on a Webstew
56
+ // project and behaves like the in-app agent does (Edit over
57
+ // Bash, /api/media for images, preserve scope, etc.). Re-written
58
+ // every request so the file stays in sync with prompt evolutions.
59
+ writeClaudeMd(workspaceDir, request.target);
60
+ // 1b. Materialize the VFS to disk.
61
+ writeVfs(workspaceDir, request.files || {});
62
+ // 2. Snapshot for diffing after the run.
63
+ const before = snapshotDir(workspaceDir);
64
+ // 3. Spawn claude.
65
+ // Flag rationale:
66
+ // --print non-interactive (no REPL)
67
+ // --output-format stream-json one JSON event per stdout line
68
+ // --verbose REQUIRED with --print + stream-json; CLI
69
+ // errors otherwise
70
+ // --permission-mode acceptEdits
71
+ // critical: without this, claude won't write
72
+ // files (no human at the TTY to grant
73
+ // permission). The user invoking the chat
74
+ // already authorized changes by asking.
75
+ // --dangerously-skip-permissions
76
+ // fallback for older CLI versions that
77
+ // don't recognize --permission-mode; the
78
+ // CLI ignores unknown flags before the
79
+ // positional prompt arg.
80
+ // Pass --mcp-config so claude loads the webstew MCP server (switch_target,
81
+ // cms, integrations, etc.). ensureMcpConfig() writes ~/.webstew/mcp.json if
82
+ // it doesn't exist yet and returns the path. The file is written once; later
83
+ // calls are a no-op so users can extend it manually.
84
+ const mcpConfigPath = (0, auth_1.ensureMcpConfig)();
85
+ const args = [
86
+ '--print',
87
+ request.prompt,
88
+ '--output-format',
89
+ 'stream-json',
90
+ '--verbose',
91
+ // bypassPermissions: skips permission prompts entirely (no TTY in
92
+ // --print mode anyway). acceptEdits only auto-approves the Edit
93
+ // tool — MCP tools were getting silently denied, which is why
94
+ // chef text-pretended to call them instead of actually calling.
95
+ // The user already authorized everything by initiating the chat.
96
+ '--permission-mode',
97
+ 'bypassPermissions',
98
+ '--mcp-config',
99
+ mcpConfigPath,
100
+ ...pickModelFlag(request.model),
101
+ ];
102
+ if (request.maxIterations) {
103
+ args.push('--max-turns', String(request.maxIterations));
104
+ }
105
+ const child = (0, node_child_process_1.spawn)(opts.claudeBin || 'claude', args, {
106
+ cwd: workspaceDir,
107
+ env: process.env,
108
+ stdio: ['ignore', 'pipe', 'pipe'],
109
+ });
110
+ // Hard-kill the child when the runtime aborts (BridgeCancelled).
111
+ // SIGTERM first; SIGKILL after 1s if it hasn't exited (claude is
112
+ // usually mid-API-call so SIGTERM is enough). The readline loop
113
+ // below will see stdin EOF and exit naturally once the child dies.
114
+ let aborted = false;
115
+ if (opts.signal) {
116
+ const onAbort = () => {
117
+ aborted = true;
118
+ try {
119
+ child.kill('SIGTERM');
120
+ }
121
+ catch { }
122
+ setTimeout(() => {
123
+ if (!child.killed) {
124
+ try {
125
+ child.kill('SIGKILL');
126
+ }
127
+ catch { }
128
+ }
129
+ }, 1000);
130
+ };
131
+ if (opts.signal.aborted)
132
+ onAbort();
133
+ else
134
+ opts.signal.addEventListener('abort', onAbort, { once: true });
135
+ }
136
+ let stderr = '';
137
+ let stdoutTail = '';
138
+ child.stderr.on('data', (b) => { stderr += b.toString(); });
139
+ // Mirror stdout to a tail buffer too — when claude exits 1 with no
140
+ // stderr, the error is often a single non-JSON line on stdout that
141
+ // never makes it through our line-by-line parser before the process
142
+ // closes. Capturing the tail lets us surface it on failure.
143
+ child.stdout.on('data', (b) => {
144
+ stdoutTail = (stdoutTail + b.toString()).slice(-2000);
145
+ });
146
+ // 4. Line-by-line JSON event stream.
147
+ const rl = node_readline_1.default.createInterface({ input: child.stdout });
148
+ for await (const line of rl) {
149
+ const trimmed = line.trim();
150
+ if (!trimmed)
151
+ continue;
152
+ let evt;
153
+ try {
154
+ evt = JSON.parse(trimmed);
155
+ }
156
+ catch {
157
+ // Non-JSON line — surface as a text event so the user sees
158
+ // whatever claude printed.
159
+ await onEvent({ requestId, kind: 'text', data: { text: trimmed } });
160
+ continue;
161
+ }
162
+ await emitEventFromClaude(evt, requestId, onEvent);
163
+ }
164
+ // 5. Process exit.
165
+ const exitCode = await new Promise((res) => {
166
+ child.on('close', (c) => res(c ?? 1));
167
+ });
168
+ // 6. Diff filesystem and emit file_update / file_delete.
169
+ const after = snapshotDir(workspaceDir);
170
+ for (const [p, contents] of after) {
171
+ if (before.get(p) !== contents) {
172
+ await onEvent({ requestId, kind: 'file_update', data: { path: p, contents } });
173
+ }
174
+ }
175
+ for (const p of before.keys()) {
176
+ if (!after.has(p)) {
177
+ await onEvent({ requestId, kind: 'file_delete', data: { path: p } });
178
+ }
179
+ }
180
+ if (exitCode !== 0) {
181
+ // Don't surface an "error" event when the user explicitly aborted
182
+ // — that path already emits its own cancelled signal up the chain.
183
+ if (aborted)
184
+ return;
185
+ process.stderr.write(`\n[claude-runner] exit ${exitCode}\n` +
186
+ `[claude-runner] cwd: ${workspaceDir}\n` +
187
+ `[claude-runner] bin: ${opts.claudeBin || 'claude'}\n` +
188
+ `[claude-runner] args: ${JSON.stringify(args)}\n` +
189
+ `[claude-runner] stderr:\n${stderr || '(empty)'}\n` +
190
+ `[claude-runner] stdout tail:\n${stdoutTail || '(empty)'}\n\n`);
191
+ const detail = stderr.trim().slice(-400) ||
192
+ stdoutTail.trim().slice(-400) ||
193
+ 'No stderr or stdout — likely an invalid CLI flag combination.';
194
+ await onEvent({
195
+ requestId,
196
+ kind: 'error',
197
+ data: { message: `claude exited with code ${exitCode}. ${detail}` },
198
+ });
199
+ return;
200
+ }
201
+ await onEvent({ requestId, kind: 'done', data: { summary: 'Done.', iterations: 0 } });
202
+ }
203
+ // Map one claude stream-json event to our BridgeResponse schema. The
204
+ // claude CLI's event shapes (as of v0.x): { type: 'system'|'assistant'|
205
+ // 'user'|'result'|... , ... }. We translate to our subset; unknown
206
+ // types are skipped silently — better to under-emit than to spam the
207
+ // chat with internal noise.
208
+ async function emitEventFromClaude(evt, requestId, onEvent) {
209
+ // Assistant text token / message
210
+ if (evt.type === 'assistant' && evt.message?.content) {
211
+ const blocks = Array.isArray(evt.message.content) ? evt.message.content : [];
212
+ for (const block of blocks) {
213
+ if (block.type === 'text' && block.text) {
214
+ await onEvent({ requestId, kind: 'text', data: { text: block.text } });
215
+ }
216
+ else if (block.type === 'tool_use') {
217
+ await onEvent({
218
+ requestId,
219
+ kind: 'tool_use',
220
+ data: { id: block.id, name: block.name, input: block.input },
221
+ });
222
+ }
223
+ }
224
+ return;
225
+ }
226
+ // Tool result echoed back into the conversation
227
+ if (evt.type === 'user' && Array.isArray(evt.message?.content)) {
228
+ for (const block of evt.message.content) {
229
+ if (block.type === 'tool_result') {
230
+ const c = typeof block.content === 'string'
231
+ ? block.content
232
+ : JSON.stringify(block.content).slice(0, 2000);
233
+ await onEvent({
234
+ requestId,
235
+ kind: 'tool_result',
236
+ data: {
237
+ tool_use_id: block.tool_use_id,
238
+ ok: !block.is_error,
239
+ content: c,
240
+ },
241
+ });
242
+ }
243
+ }
244
+ return;
245
+ }
246
+ // Final result row — claude emits this last. We ignore here because
247
+ // we synthesize our own `done` event after diffing the filesystem,
248
+ // which carries the post-run file state. Letting claude's `result`
249
+ // through would race the diff.
250
+ if (evt.type === 'result')
251
+ return;
252
+ // System / model setup events — drop. Plumbing chatter not useful in
253
+ // the workspace chat thread.
254
+ }
255
+ // ── Filesystem helpers ────────────────────────────────────────────────
256
+ function writeVfs(root, files) {
257
+ for (const [rel, contents] of Object.entries(files)) {
258
+ const safe = sanitizeRel(rel);
259
+ if (!safe)
260
+ continue;
261
+ const full = node_path_1.default.join(root, safe);
262
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(full), { recursive: true });
263
+ node_fs_1.default.writeFileSync(full, contents);
264
+ }
265
+ }
266
+ // Filenames the bridge writes for its own bookkeeping. Excluded from
267
+ // snapshot diff so they don't surface as user project changes.
268
+ const BRIDGE_OWNED_FILES = new Set(['CLAUDE.md']);
269
+ function snapshotDir(root) {
270
+ const out = new Map();
271
+ if (!node_fs_1.default.existsSync(root))
272
+ return out;
273
+ const walk = (dir) => {
274
+ for (const ent of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
275
+ if (ent.name.startsWith('.'))
276
+ continue; // skip dotfiles like .git
277
+ const full = node_path_1.default.join(dir, ent.name);
278
+ if (ent.isDirectory()) {
279
+ walk(full);
280
+ }
281
+ else if (ent.isFile()) {
282
+ const rel = node_path_1.default.relative(root, full);
283
+ if (BRIDGE_OWNED_FILES.has(rel))
284
+ continue;
285
+ try {
286
+ out.set(rel, node_fs_1.default.readFileSync(full, 'utf8'));
287
+ }
288
+ catch {
289
+ // Binary or unreadable — skip; the workspace shouldn't contain
290
+ // binaries in v1 (HTML/CSS/JS/MD only).
291
+ }
292
+ }
293
+ }
294
+ };
295
+ walk(root);
296
+ return out;
297
+ }
298
+ function sanitizeRel(p) {
299
+ // Reject absolute paths and traversal — same rule the agent route
300
+ // applies on the server side.
301
+ if (!p || p.startsWith('/') || p.includes('..'))
302
+ return null;
303
+ return p;
304
+ }
305
+ // CLAUDE.md written into the workspace dir so claude has the same
306
+ // project context the direct-Anthropic agent route's system prompt
307
+ // gives. Without this, bridge claude is a generic Claude Code session
308
+ // that doesn't know it's editing a live Webstew site preview, defaults
309
+ // to Bash where Edit would do, and ignores Webstew's image proxy.
310
+ function writeClaudeMd(root, target) {
311
+ const targetLabel = target === 'nextjs' ? 'Next.js app'
312
+ : target === 'react' ? 'Vite + React app'
313
+ : target === 'astro' ? 'Astro site'
314
+ : target === 'expo' ? 'React Native / Expo app'
315
+ : 'single-page HTML website';
316
+ const md = `# Webstew project — Chef context
317
+
318
+ You are the **Chef** — the AI cook inside a live Webstew workspace
319
+ session. The user sees a real-time browser preview of this directory.
320
+ Every file you write here ships to their preview iframe immediately,
321
+ and (when they're on a saved project) persists to Mongo so it survives
322
+ a refresh. Your subscription is paying for this call — be efficient
323
+ but generous.
324
+
325
+ ## What Webstew is
326
+ Webstew (https://webstew.net) is an AI website + app builder. One
327
+ prompt turns into a production-ready site, store, or mobile app the
328
+ user can deploy. They picked you (their local Claude Code) as the brain
329
+ so their subscription handles the bill instead of API credits.
330
+
331
+ ## What the user has at their disposal in this workspace
332
+
333
+ **Build targets** — the user can scaffold any of these from the same chat:
334
+ - **website** — single \`index.html\`, Tailwind via CDN, vanilla JS. ← THIS PROJECT
335
+ - **nextjs** — full Next.js 14 app router, Tailwind, TypeScript
336
+ - **astro** — content-focused static sites, MDX, islands
337
+ - **react** — Vite + React + TypeScript SPA
338
+ - **expo** — React Native mobile app (iOS + Android), Expo Router
339
+
340
+ **CMS** — every project can have content collections (blog posts,
341
+ services, team members, products). Use the MCP tools:
342
+ \`webstew_list_cms_collections\` → see what exists, then
343
+ \`webstew_list_cms_items(collection)\` → see items, then
344
+ \`webstew_create_cms_item(collection, slug, fields)\` → write new items.
345
+ Field names must match the collection's schema. Default status is
346
+ "published" — pass status: "draft" to stage.
347
+
348
+ **Integrations** (Composio): Gmail, Slack, HubSpot, Salesforce, Google
349
+ Sheets, Drive, Calendar, Microsoft Teams, Intercom, Zendesk, Jira,
350
+ Monday, Stripe, Shopify, LinkedIn, Facebook, GitHub, QuickBooks,
351
+ Google Ads. Three-step flow:
352
+ 1. \`webstew_list_integrations\` → see what's connected. If the user's
353
+ requested toolkit isn't there, tell them to connect it at
354
+ /integrations (you can offer to open that panel via
355
+ \`webstew_open_panel("integrations", ...)\`).
356
+ 2. \`webstew_list_integration_actions(toolkit)\` → see available verbs.
357
+ NEVER guess action slugs — always confirm here first.
358
+ 3. \`webstew_run_integration_action(action, args)\` → do the thing.
359
+
360
+ **Image pipeline** — Two paths:
361
+ - For images you ADD: write \`<img src="/api/media?q=KEYWORDS&w=W&h=H">\`
362
+ directly into HTML (Webstew proxies Pexels). No tool call needed.
363
+ - For user-supplied images they want stored permanently: call
364
+ \`webstew_upload_image(sourceUrl)\` → returns a Cloudinary URL.
365
+
366
+ **Workspace navigation** — \`webstew_open_panel(panel, reason)\` opens
367
+ any sidebar panel for the user (build, templates, projects, images,
368
+ video, integrations, env, console, deploy, webstew). User sees an
369
+ Approve/Deny modal. Use when their task is better done in a panel
370
+ (e.g. "open integrations so you can connect Slack").
371
+
372
+ **Deploy** — Render integration (auto-deploy from generated project),
373
+ GitHub push, custom domains. Use \`webstew_open_panel("deploy", ...)\`
374
+ to surface the deploy UI for them.
375
+
376
+ **Grader** — \`webstew_grade_site(url)\` runs SEO + AI-visibility scoring
377
+ on any public URL and returns issues. After grading, fix the top 2-3
378
+ actionable items yourself instead of pasting the report back.
379
+
380
+ **Marketplace** — Users can publish their sites as templates others
381
+ buy (Stripe Connect, 30% platform fee). Read-only from chat.
382
+
383
+ ## This project: ${targetLabel}
384
+
385
+ Working dir: this dir IS the project. No \`cd\`.
386
+ ${target === 'website' || !target
387
+ ? '- One file: `index.html`. Tailwind via CDN, vanilla JS. Edit in place.'
388
+ : '- Multi-file. Run `Glob \"**/*\"` once if you don\'t know the layout.'}
389
+ ${target === 'expo' ? `
390
+ ## ⚠ EXPO — FILE WRITES ONLY, NO SHELL COMMANDS
391
+
392
+ The workspace preview renders the VFS files directly — local npm installs,
393
+ npx commands, and package manager operations do NOTHING here and will time
394
+ out the session. NEVER run:
395
+ - npm install / npm ci / yarn / pnpm install
396
+ - npx create-expo-app or any scaffolding CLI
397
+ - npx expo start / npx expo build
398
+ - Any Bash command that installs, builds, or starts a dev server
399
+
400
+ **Write JSX/TSX files directly.** Assume these are pre-installed and can
401
+ be imported without any install step:
402
+ - react, react-native (View, Text, StyleSheet, TouchableOpacity, ScrollView,
403
+ Image, TextInput, FlatList, etc.)
404
+ - expo (Expo.* APIs)
405
+ - @expo/vector-icons (Ionicons, MaterialIcons, etc.)
406
+ - expo-router (Link, Stack, Tabs — use file-based routing under app/)
407
+ - expo-linear-gradient, expo-blur, expo-status-bar
408
+
409
+ **package.json — required web script:**
410
+ Every expo project MUST include this script so the browser preview can start:
411
+ \`\`\`json
412
+ {
413
+ "scripts": {
414
+ "web": "expo start --web",
415
+ "start": "expo start",
416
+ "android": "expo run:android",
417
+ "ios": "expo run:ios"
418
+ }
419
+ }
420
+ \`\`\`
421
+ If you create or modify package.json, always include \`"web": "expo start --web"\`.
422
+
423
+ **First response in a new expo project — minimal files only:**
424
+ Write these files and nothing else:
425
+ 1. \`package.json\` — with the scripts above + minimal deps: react, react-native, expo, expo-status-bar
426
+ 2. \`app/index.tsx\` (expo-router) OR \`App.js\` (bare) — one complete screen
427
+
428
+ No app.json changes, no babel.config, no tsconfig, no additional screens
429
+ unless the user explicitly asked for them. Start lean — iterate from there.
430
+ ` : ''}
431
+
432
+ ## ⚠ TARGET MISMATCH — handle via tool call, not by asking the user
433
+
434
+ The user is currently on the **${targetLabel}** target. If their
435
+ request clearly implies a DIFFERENT target, call the
436
+ **\`mcp__webstew__webstew_switch_target\`** tool. The user gets an
437
+ Approve/Deny modal automatically — do NOT ask them to switch manually
438
+ in the sidebar.
439
+
440
+ Flow:
441
+ 1. Detect mismatch (see triggers below).
442
+ 2. Call \`mcp__webstew__webstew_switch_target\` with \`target\` + \`reason\` args
443
+ — e.g. \`{target: "expo", reason: "You asked for a mobile app — the current workspace is HTML."}\`.
444
+ 3. The tool blocks while the user clicks Approve / Deny.
445
+ 4. **Approved** → tool returns success. End this turn with a one-liner
446
+ ("Switched to Expo. Tell me about the app — features, screens, vibe.")
447
+ and STOP. Don't try to scaffold the new project in the same turn —
448
+ the user's next message lands in the new target.
449
+ 5. **Declined** → tool returns "declined". Acknowledge ("Staying here
450
+ then.") and offer the closest thing you CAN do in the current
451
+ target (e.g. responsive HTML).
452
+
453
+ Trigger examples:
454
+ - "Build me a mobile app" / "iOS / Android / native / Expo / push notifications" → \`expo\`
455
+ - "Build a Next.js site" / "server components" / "API routes" / "SSR" → \`nextjs\`
456
+ - "Build a blog with MDX" / "static site generator" / "content collections" → \`astro\`
457
+ - "Build a React SPA" / "Vite" / "client-side routing" → \`react\`
458
+
459
+ If the request *could* work in the current target (e.g. "make my site
460
+ mobile-responsive" on the website target — yes, responsive CSS works),
461
+ just do it. Mismatch only applies when the OUTPUT FORMAT requires a
462
+ different runtime.
463
+
464
+ If \`mcp__webstew__webstew_switch_target\` isn't in your tool list, the
465
+ MCP server didn't load — tell them to run \`webstew-bridge connect\`
466
+ (no code needed) and retry. Don't ask them to switch manually.
467
+
468
+ ## Your toolkit
469
+
470
+ **Standard Claude Code tools** (always available):
471
+ Read, Write, Edit, Bash, Glob, Grep, plus whatever MCP servers the
472
+ user has installed in their own Claude Code config (\`~/.claude/\`).
473
+
474
+ **Webstew MCP tools** (loaded at startup via \`@webstew/agent-tools\`).
475
+ Tool names are prefixed \`mcp__webstew__webstew_*\`. Call them directly —
476
+ no schema-loading step required in this environment.
477
+
478
+ - \`mcp__webstew__webstew_switch_target(target, reason)\` — switch workspace target (website / nextjs / astro / react / expo). User approval in chat.
479
+ - \`mcp__webstew__webstew_open_panel(panel, reason)\` — open sidebar panel. User approval in chat.
480
+ - \`mcp__webstew__webstew_list_cms_collections\` / \`webstew_list_cms_items\` / \`webstew_create_cms_item\` — CMS read/write.
481
+ - \`mcp__webstew__webstew_upload_image(sourceUrl)\` — store image in Cloudinary.
482
+ - \`mcp__webstew__webstew_grade_site(url)\` — SEO + AI-visibility scoring.
483
+ - \`mcp__webstew__webstew_list_integrations\` / \`webstew_list_integration_actions(toolkit)\` / \`webstew_run_integration_action(action, args)\` — Slack, Gmail, HubSpot, Stripe, Shopify, etc.
484
+
485
+ If you don't see any \`mcp__webstew__*\` tools in your tool list, the
486
+ MCP server didn't load — tell the user to run \`webstew-bridge connect\`
487
+ (no code needed if already paired) and retry.
488
+
489
+ ## How to edit (THE MOST IMPORTANT RULES)
490
+ - **Make the SMALLEST change that satisfies the literal request.** If
491
+ the user says "change the title", change the \`<title>\` tag (and
492
+ \`<h1>\` if clearly implied) and NOTHING else. Same image URLs, same
493
+ copy, same classes, same comments, same whitespace.
494
+ - **Prefer Edit over Write** for narrow changes. Edit is surgical;
495
+ Write/full-rewrites drift unrelated content.
496
+ - **Never touch \`background-image\`, \`src="..."\`, \`url(...)\` URLs**
497
+ when changing colors or theme. Hero bg-images disappearing is a
498
+ recurring bug from blanket rewrites — don't be that bug.
499
+ - One sentence of prose max before tool calls. No "let me start by…"
500
+ preambles. Get cooking.
501
+
502
+ ## Images
503
+ - For images you ADD: \`<img src="/api/media?q=KEYWORDS&w=W&h=H">\` —
504
+ Webstew's Pexels-backed proxy. Keywords describe content, not feature
505
+ names. Example: \`q=modern+startup+team\` not \`q=hero1\`.
506
+ - DO NOT emit \`picsum.photos\`, \`source.unsplash.com\`, or
507
+ \`loremflickr.com\` — they're dead or rate-limited.
508
+
509
+ ## Empty workspace
510
+ If you arrive and the directory is empty (no index.html), the user
511
+ hasn't generated a site yet. Don't pretend they have one — offer to
512
+ generate a starter (write a complete index.html based on what they
513
+ describe), OR direct them to click "Build" in the workspace and pick
514
+ a template.
515
+
516
+ ## Selected-element prompts
517
+ When the user's prompt starts with \`User has selected this element in
518
+ the live preview:\` followed by a code block, edit ONLY that exact
519
+ node. Locate it by literal substring; touch nothing outside it.
520
+
521
+ ## Conversational asks
522
+ If they say "hi" or ask a meta-question, answer briefly chef-style
523
+ ("Ready to cook — what are we making?") and ask what they want to
524
+ build/change. Don't invent edits.
525
+ `;
526
+ try {
527
+ node_fs_1.default.writeFileSync(node_path_1.default.join(root, 'CLAUDE.md'), md);
528
+ }
529
+ catch {
530
+ // Non-fatal — claude works without it, just less Webstew-aware.
531
+ }
532
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env -S npx --yes tsx
2
+ export {};