@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.
- package/README.md +102 -0
- package/bin/webstew-bridge +2 -0
- package/dist/auth.d.ts +25 -0
- package/dist/auth.js +142 -0
- package/dist/claude-runner.d.ts +10 -0
- package/dist/claude-runner.js +532 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +162 -0
- package/dist/http.d.ts +14 -0
- package/dist/http.js +56 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -0
- package/dist/protocol.d.ts +151 -0
- package/dist/protocol.js +50 -0
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +113 -0
- package/package.json +27 -0
|
@@ -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