@aprovan/stitchery 0.1.0-dev.6bd527d

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/dist/cli.cjs ADDED
@@ -0,0 +1,1400 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_path2 = __toESM(require("path"), 1);
28
+ var import_fs2 = __toESM(require("fs"), 1);
29
+ var import_commander = require("commander");
30
+
31
+ // src/server/index.ts
32
+ var import_node_http = require("http");
33
+ var import_mcp = require("@ai-sdk/mcp");
34
+ var import_mcp_stdio = require("@ai-sdk/mcp/mcp-stdio");
35
+ var import_patchwork_utcp = require("@aprovan/patchwork-utcp");
36
+ var import_ai3 = require("ai");
37
+
38
+ // src/server/routes.ts
39
+ var import_openai_compatible = require("@ai-sdk/openai-compatible");
40
+ var import_ai = require("ai");
41
+
42
+ // src/prompts.ts
43
+ var PATCHWORK_PROMPT = `
44
+ You are a friendly assistant! When responding to the user, you _must_ respond with JSX files!
45
+
46
+ Look at 'patchwork.compilers' to see what specific runtime components and libraries are supported. (e.g. '['@aprovan/patchwork-image-shadcn' supports React, Tailwind, & ShadCN components). If there are no compilers, respond as you normally would. If compilers are available, ALWAYS respond with a component following [Component Generation](#component-generation).
47
+
48
+ Look at 'patchwork.services' to see what services are available for widgets to call. If services are listed, you can generate widgets that make service calls using global namespace objects.
49
+
50
+ **IMPORTANT: If you need to discover available services or get details about a specific service, use the \`search_services\` tool.**
51
+
52
+ ## Component Generation
53
+
54
+ Respond with code blocks using tagged attributes on the fence line. The \`note\` attribute (optional but encouraged) provides a brief description visible in the UI. The \`path\` attribute specifies the virtual file path.
55
+
56
+ ### Code Block Format
57
+
58
+ \`\`\`tsx note="Main component" path="components/weather/main.tsx"
59
+ export default function WeatherWidget() {
60
+ // component code
61
+ }
62
+ \`\`\`
63
+
64
+ ### Attribute Order
65
+ Put \`note\` first so it's available soonest in streaming UI.
66
+
67
+ ### Multi-File Generation
68
+
69
+ When generating complex widgets, you can output multiple files. Use the \`@/\` prefix for virtual file system paths. ALWAYS prefer to generate visible components before metadata.
70
+
71
+ **Example multi-file widget:**
72
+
73
+ \`\`\`json note="Widget configuration" path="components/dashboard/package.json"
74
+ {
75
+ "description": "Interactive dashboard widget",
76
+ "patchwork": {
77
+ "inputs": {
78
+ "type": "object",
79
+ "properties": {
80
+ "title": { "type": "string" }
81
+ }
82
+ },
83
+ "services": {
84
+ "analytics": ["getMetrics", "getChartData"]
85
+ }
86
+ }
87
+ }
88
+ \`\`\`
89
+
90
+ \`\`\`tsx note="Main widget component" path="components/dashboard/main.tsx"
91
+ import { Card } from './Card';
92
+ import { Chart } from './Chart';
93
+
94
+ export default function Dashboard({ title = "Dashboard" }) {
95
+ return (
96
+ <div>
97
+ <h1>{title}</h1>
98
+ <Card />
99
+ <Chart />
100
+ </div>
101
+ );
102
+ }
103
+ \`\`\`
104
+
105
+ \`\`\`tsx note="Card subcomponent" path="components/dashboard/Card.tsx"
106
+ export function Card() {
107
+ return <div className="p-4 rounded border">Card content</div>;
108
+ }
109
+ \`\`\`
110
+
111
+ ### Requirements
112
+ - DO think heavily about correctness of code and syntax
113
+ - DO keep things simple and self-contained
114
+ - ALWAYS include the \`path\` attribute specifying the file location. Be generic with the name and describe the general component's use
115
+ - ALWAYS output the COMPLETE code block with opening \`\`\`tsx and closing \`\`\` markers
116
+ - Use \`note\` attribute to describe what each code block does (optional but encouraged)
117
+ - NEVER truncate or cut off code - finish the entire component before stopping
118
+ - If the component is complex, simplify it rather than leaving it incomplete
119
+ - Do NOT include: a heading/title
120
+
121
+ ### Visual Design Guidelines
122
+ Create professional, polished interfaces that present information **spatially** rather than as vertical lists:
123
+ - Use **cards, grids, and flexbox layouts** to organize related data into visual groups
124
+ - Leverage **icons** (from lucide-react) alongside text to communicate meaning at a glance
125
+ - Apply **visual hierarchy** through typography scale, weight, and color contrast
126
+ - Use **whitespace strategically** to create breathing room and separation
127
+ - Prefer **horizontal arrangements** where data fits naturally (e.g., stats in a row, badges inline)
128
+ - Group related metrics into **compact visual clusters** rather than separate line items
129
+ - Use **subtle backgrounds, borders, and shadows** to define sections without heavy dividers
130
+
131
+ ### Root Element Constraints
132
+ The component will be rendered inside a parent container that handles positioning. Your root element should:
133
+ - \u2705 Use intrinsic sizing (let content determine dimensions)
134
+ - \u2705 Handle internal padding (e.g., \`p-4\`, \`p-6\`)
135
+ - \u274C NEVER add centering utilities (\`items-center\`, \`justify-center\`) to position itself
136
+ - \u274C NEVER add viewport-relative sizing (\`min-h-screen\`, \`h-screen\`, \`w-screen\`)
137
+ - \u274C NEVER add flex/grid on root just for self-centering
138
+
139
+ ### Using Services in Widgets (CRITICAL)
140
+
141
+ **MANDATORY workflow - you must follow these steps IN ORDER:**
142
+
143
+ 1. **Use \`search_services\`** to discover the service schema
144
+ 2. **STOP. Make an actual call to the service tool itself** (e.g., \`weather_get_forecast\`, \`github_get_repo\`) with real arguments. This is NOT optional. Do NOT skip this step.
145
+ 3. **Observe the response** - verify it succeeded and note the exact data structure
146
+ 4. **Only then generate the widget** that fetches the same data at runtime
147
+
148
+ **\`search_services\` is NOT a substitute for calling the actual service.** It only returns documentation. You MUST invoke the real service tool to validate your arguments work.
149
+
150
+ **Tool naming:** Service tools use underscores, not dots. For example: \`weather_get_forecast\`, \`github_list_repos\`.
151
+
152
+ **Example workflow for a weather widget:**
153
+ \`\`\`
154
+ Step 1: search_services({ query: "weather" })
155
+ \u2192 Learn that weather_get_current_conditions exists with params: { latitude, longitude }
156
+
157
+ Step 2: weather_get_current_conditions({ latitude: 29.7604, longitude: -95.3698 }) \u2190 REQUIRED!
158
+ \u2192 Verify it returns { temp: 72, humidity: 65, ... }
159
+
160
+ Step 3: Generate widget that calls weather.get_current_conditions at runtime
161
+ \`\`\`
162
+
163
+ **If you skip Step 2, you will generate broken widgets.** Arguments that look correct in the schema may fail at runtime due to validation rules, required formats, or service-specific constraints.
164
+
165
+ **NEVER embed static data directly in the component.**
166
+
167
+ \u274C **WRONG** - Embedding data directly:
168
+ \`\`\`tsx path="components/weather/bad-example.tsx"
169
+ // DON'T DO THIS - calling tool, then embedding the response as static data
170
+ export default function WeatherWidget() {
171
+ // Static data embedded at generation time - BAD!
172
+ const weather = { temp: 72, condition: "sunny", humidity: 45 };
173
+ return <div>Temperature: {weather.temp}\xB0F</div>;
174
+ }
175
+ \`\`\`
176
+
177
+ \u2705 **CORRECT** - Fetching data at runtime:
178
+ \`\`\`tsx note="Weather widget with runtime data" path="components/weather/main.tsx"
179
+ export default function WeatherWidget() {
180
+ const [data, setData] = useState(null);
181
+ const [loading, setLoading] = useState(true);
182
+ const [error, setError] = useState(null);
183
+
184
+ useEffect(() => {
185
+ // Fetch data at runtime - GOOD!
186
+ weather.get_forecast({ latitude: 48.8566, longitude: 2.3522 })
187
+ .then(setData)
188
+ .catch(setError)
189
+ .finally(() => setLoading(false));
190
+ }, []);
191
+
192
+ if (loading) return <Skeleton className="h-32 w-full" />;
193
+ if (error) return <Alert variant="destructive">{error.message}</Alert>;
194
+
195
+ return <div>Temperature: {data.temp}\xB0F</div>;
196
+ }
197
+ \`\`\`
198
+
199
+ **Why this matters:**
200
+ - Widgets with runtime service calls show **live data** that updates when refreshed
201
+ - Static embedded data becomes **stale immediately** after generation
202
+ - The proxy pattern allows widgets to be **reusable** across different contexts
203
+ - Error handling and loading states improve **user experience**
204
+
205
+ **Service call pattern:**
206
+ \`\`\`tsx
207
+ // Services are available as global namespace objects
208
+ // Call format: namespace.procedure_name({ ...args })
209
+
210
+ const [data, setData] = useState(null);
211
+ const [loading, setLoading] = useState(true);
212
+ const [error, setError] = useState(null);
213
+
214
+ useEffect(() => {
215
+ serviceName.procedure_name({ param1: value1 })
216
+ .then(setData)
217
+ .catch(setError)
218
+ .finally(() => setLoading(false));
219
+ }, [/* dependencies */]);
220
+ \`\`\`
221
+
222
+ **Required for service-using widgets:**
223
+ - Always show loading indicators (Skeleton, Loader2 spinner, etc.)
224
+ - Always handle errors gracefully with user-friendly messages
225
+ - Use appropriate React hooks (useState, useEffect) for async data
226
+ - Services are injected as globals - NO imports needed
227
+
228
+ ### Validating Service Calls (CRITICAL - READ CAREFULLY)
229
+
230
+ **Calling \`search_services\` multiple times is NOT validation.** You must call the actual service tool.
231
+
232
+ \u274C **WRONG workflow (will produce broken widgets):**
233
+ \`\`\`
234
+ 1. search_services({ query: "weather" }) \u2190 Only gets schema
235
+ 2. search_services({ query: "location" }) \u2190 Still only schema
236
+ 3. Generate widget \u2190 BROKEN - never tested the actual service!
237
+ \`\`\`
238
+
239
+ \u2705 **CORRECT workflow:**
240
+ \`\`\`
241
+ 1. search_services({ query: "weather" }) \u2190 Get schema
242
+ 2. weather_get_forecast({ latitude: 29.76, longitude: -95.37 }) \u2190 ACTUALLY CALL IT
243
+ 3. Observe response: { temp: 72, conditions: "sunny", ... }
244
+ 4. Generate widget that calls weather.get_forecast at runtime
245
+ \`\`\`
246
+
247
+ **The service tool (e.g., \`weather_get_forecast\`, \`github_list_repos\`) is a DIFFERENT tool from \`search_services\`.** You have access to both. Use both.
248
+
249
+ **Only after a successful test call to the actual service should you generate the widget.**
250
+
251
+ ### Component Parameterization (IMPORTANT)
252
+
253
+ **Widgets should accept props for dynamic values instead of hardcoding:**
254
+
255
+ \u274C **WRONG** - Hardcoded values:
256
+ \`\`\`tsx path="components/weather/bad-example.tsx"
257
+ export default function WeatherWidget() {
258
+ // Location hardcoded - BAD!
259
+ const [lat, lon] = [48.8566, 2.3522]; // Paris
260
+ // ...
261
+ }
262
+ \`\`\`
263
+
264
+ \u2705 **CORRECT** - Parameterized with props and defaults:
265
+ \`\`\`tsx note="Parameterized weather widget" path="components/weather/main.tsx"
266
+ interface WeatherWidgetProps {
267
+ location?: string; // e.g., "Paris, France"
268
+ latitude?: number; // Direct coordinates (optional)
269
+ longitude?: number;
270
+ }
271
+
272
+ export default function WeatherWidget({
273
+ location = "Paris, France",
274
+ latitude,
275
+ longitude
276
+ }: WeatherWidgetProps) {
277
+ // Use provided coordinates or look up from location name
278
+ // ...
279
+ }
280
+ \`\`\`
281
+
282
+ **Why parameterize:**
283
+ - Components become **reusable** across different contexts
284
+ - Users can **customize behavior** without editing code
285
+ - Enables **composition** - parent components can pass different values
286
+ - Supports **testing** with various inputs
287
+
288
+ **What to parameterize:**
289
+ - Location names, coordinates, IDs
290
+ - Search queries and filters
291
+ - Display options (count, format, theme)
292
+ - API-specific identifiers (usernames, repo names, etc.)
293
+
294
+ ### Anti-patterns to Avoid
295
+ - \u274C Bulleted or numbered lists of key-value pairs
296
+ - \u274C Vertical stacks where horizontal layouts would fit
297
+ - \u274C Plain text labels without visual treatment
298
+ - \u274C Uniform styling that doesn't distinguish primary from secondary information
299
+ - \u274C Wrapping components in centering containers (parent handles this)
300
+ - \u274C **Embedding API response data directly in components instead of fetching at runtime**
301
+ - \u274C **Calling a tool, then putting the response as static JSX/JSON in the generated code**
302
+ - \u274C **Hardcoding values that should be component props**
303
+ - \u274C **Calling \`search_services\` multiple times instead of calling the actual service tool**
304
+ - \u274C **Generating a widget without first making a real call to the service with your intended arguments**
305
+ - \u274C **Treating schema documentation as proof that a service call will work**
306
+ - \u274C **Omitting the \`path\` attribute on code blocks**
307
+ `;
308
+ var EDIT_PROMPT = `
309
+ You are editing an existing JSX component. The user will provide the current code and describe the changes they want.
310
+
311
+ ## Response Format
312
+
313
+ Use code fences with tagged attributes. The \`note\` attribute (optional but encouraged) provides a brief description visible in the UI. The \`path\` attribute specifies the target file.
314
+
315
+ \`\`\`diff note="Brief description of this change" path="@/components/Button.tsx"
316
+ <<<<<<< SEARCH
317
+ exact code to find
318
+ =======
319
+ replacement code
320
+ >>>>>>> REPLACE
321
+ \`\`\`
322
+
323
+ ### Attribute Order
324
+ Put \`note\` first so it's available soonest in streaming UI.
325
+
326
+ ### Multi-File Edits
327
+ When editing multiple files, use the \`path\` attribute with virtual paths (\`@/\` prefix for generated files):
328
+
329
+ \`\`\`diff note="Update button handler" path="@/components/Button.tsx"
330
+ <<<<<<< SEARCH
331
+ onClick={() => {}}
332
+ =======
333
+ onClick={() => handleClick()}
334
+ >>>>>>> REPLACE
335
+ \`\`\`
336
+
337
+ \`\`\`diff note="Add utility function" path="@/lib/utils.ts"
338
+ <<<<<<< SEARCH
339
+ export const formatDate = ...
340
+ =======
341
+ export const formatDate = ...
342
+
343
+ export const handleClick = () => console.log('clicked');
344
+ >>>>>>> REPLACE
345
+ \`\`\`
346
+
347
+ ## Rules
348
+ - SEARCH block must match the existing code EXACTLY (whitespace, indentation, everything)
349
+ - You can include multiple diff blocks for multiple changes
350
+ - Each diff block should have its own \`note\` attribute annotation
351
+ - Keep changes minimal and targeted
352
+ - Do NOT output the full file - only the diffs
353
+ - If clarification is needed, ask briefly before any diffs
354
+
355
+ ## CRITICAL: Diff Marker Safety
356
+ - NEVER include the strings "<<<<<<< SEARCH", "=======", or ">>>>>>> REPLACE" inside your replacement code
357
+ - These are reserved markers for parsing the diff format
358
+ - If you need to show diff-like content, use alternative notation (e.g., "// old code" / "// new code")
359
+ - Malformed diff markers will cause the edit to fail
360
+
361
+ ## Summary
362
+ After all diffs, provide a brief markdown summary of the changes made. Use formatting like:
363
+ - **Bold** for emphasis on key changes
364
+ - Bullet points for listing multiple changes
365
+ - Keep it concise (2-4 lines max)
366
+ - Do NOT include: a heading/title
367
+ `;
368
+
369
+ // src/server/routes.ts
370
+ function parseBody(req) {
371
+ return new Promise((resolve2, reject) => {
372
+ let body = "";
373
+ req.on("data", (chunk) => body += chunk);
374
+ req.on("end", () => {
375
+ try {
376
+ resolve2(JSON.parse(body));
377
+ } catch (err) {
378
+ reject(err);
379
+ }
380
+ });
381
+ req.on("error", reject);
382
+ });
383
+ }
384
+ async function handleChat(req, res, ctx) {
385
+ const {
386
+ messages,
387
+ metadata
388
+ } = await parseBody(req);
389
+ const normalizedMessages = messages.map((msg) => ({
390
+ ...msg,
391
+ parts: msg.parts ?? [{ type: "text", text: "" }]
392
+ }));
393
+ const provider = (0, import_openai_compatible.createOpenAICompatible)({
394
+ name: "copilot-proxy",
395
+ baseURL: ctx.copilotProxyUrl
396
+ });
397
+ const result = (0, import_ai.streamText)({
398
+ model: provider("claude-sonnet-4"),
399
+ system: `---
400
+ patchwork:
401
+ compilers: ${(metadata?.patchwork?.compilers ?? []).join(",") ?? "[]"}
402
+ services: ${ctx.registry.getNamespaces().join(",")}
403
+ ---
404
+
405
+ ${PATCHWORK_PROMPT}
406
+
407
+ ${ctx.servicesPrompt}`,
408
+ messages: await (0, import_ai.convertToModelMessages)(normalizedMessages),
409
+ stopWhen: (0, import_ai.stepCountIs)(5),
410
+ tools: ctx.tools
411
+ });
412
+ const response = result.toUIMessageStreamResponse();
413
+ response.headers.forEach(
414
+ (value, key) => res.setHeader(key, value)
415
+ );
416
+ if (!response.body) {
417
+ res.end();
418
+ return;
419
+ }
420
+ const reader = response.body.getReader();
421
+ const pump = async () => {
422
+ const { done, value } = await reader.read();
423
+ if (done) {
424
+ res.end();
425
+ return;
426
+ }
427
+ res.write(value);
428
+ await pump();
429
+ };
430
+ await pump();
431
+ }
432
+ async function handleEdit(req, res, ctx) {
433
+ const { code, prompt } = await parseBody(
434
+ req
435
+ );
436
+ const provider = (0, import_openai_compatible.createOpenAICompatible)({
437
+ name: "copilot-proxy",
438
+ baseURL: ctx.copilotProxyUrl
439
+ });
440
+ const result = (0, import_ai.streamText)({
441
+ model: provider("claude-opus-4.5"),
442
+ system: `Current component code:
443
+ \`\`\`tsx
444
+ ${code}
445
+ \`\`\`
446
+
447
+ ${EDIT_PROMPT}`,
448
+ messages: [{ role: "user", content: prompt }]
449
+ });
450
+ res.setHeader("Content-Type", "text/plain");
451
+ res.setHeader("Transfer-Encoding", "chunked");
452
+ res.writeHead(200);
453
+ for await (const chunk of result.textStream) {
454
+ res.write(chunk);
455
+ }
456
+ res.end();
457
+ }
458
+
459
+ // src/server/local-packages.ts
460
+ var import_fs = __toESM(require("fs"), 1);
461
+ var import_path = __toESM(require("path"), 1);
462
+ function handleLocalPackages(req, res, ctx) {
463
+ const rawUrl = req.url || "";
464
+ if (!rawUrl.startsWith("/_local-packages")) {
465
+ return false;
466
+ }
467
+ const urlWithoutPrefix = rawUrl.replace("/_local-packages", "");
468
+ const url = urlWithoutPrefix.split("?")[0] || "";
469
+ const match = url.match(/^\/@([^/]+)\/([^/@]+)(.*)$/);
470
+ if (!match) {
471
+ return false;
472
+ }
473
+ const [, scope, name, restPath] = match;
474
+ const packageName = `@${scope}/${name}`;
475
+ const localPath = ctx.localPackages[packageName];
476
+ if (!localPath) {
477
+ res.writeHead(404);
478
+ res.end(`Package ${packageName} not found in local overrides`);
479
+ return true;
480
+ }
481
+ const rest = restPath || "";
482
+ let filePath;
483
+ try {
484
+ if (rest === "/package.json") {
485
+ filePath = import_path.default.join(localPath, "package.json");
486
+ } else if (rest === "" || rest === "/") {
487
+ const pkgJson = JSON.parse(
488
+ import_fs.default.readFileSync(import_path.default.join(localPath, "package.json"), "utf-8")
489
+ );
490
+ const mainEntry = pkgJson.main || "dist/index.js";
491
+ filePath = import_path.default.join(localPath, mainEntry);
492
+ } else {
493
+ const normalizedPath = rest.startsWith("/") ? rest.slice(1) : rest;
494
+ const distPath = import_path.default.join(localPath, "dist", normalizedPath);
495
+ const rootPath = import_path.default.join(localPath, normalizedPath);
496
+ filePath = import_fs.default.existsSync(distPath) ? distPath : rootPath;
497
+ }
498
+ } catch (err) {
499
+ ctx.log("Error resolving file path:", err);
500
+ res.writeHead(500);
501
+ res.end(`Error resolving path for ${packageName}: ${err}`);
502
+ return true;
503
+ }
504
+ try {
505
+ ctx.log(`Serving ${filePath}`);
506
+ const content = import_fs.default.readFileSync(filePath, "utf-8");
507
+ const ext = import_path.default.extname(filePath);
508
+ const contentType = ext === ".json" ? "application/json" : ext === ".js" ? "application/javascript" : ext === ".ts" ? "application/typescript" : "text/plain";
509
+ res.setHeader("Content-Type", contentType);
510
+ res.writeHead(200);
511
+ res.end(content);
512
+ } catch (err) {
513
+ ctx.log("Error serving file:", filePath, err);
514
+ res.writeHead(404);
515
+ res.end(`File not found: ${filePath}`);
516
+ }
517
+ return true;
518
+ }
519
+
520
+ // src/server/vfs-routes.ts
521
+ var import_promises = require("fs/promises");
522
+ var import_node_fs = require("fs");
523
+ var import_node_path = require("path");
524
+ function normalizeRelPath(path3) {
525
+ const decoded = decodeURIComponent(path3).replace(/\\/g, "/");
526
+ return decoded.replace(/^\/+|\/+$/g, "");
527
+ }
528
+ function resolvePath(rootDir, relPath) {
529
+ const root = (0, import_node_path.resolve)(rootDir);
530
+ const full = (0, import_node_path.resolve)(root, relPath);
531
+ if (full !== root && !full.startsWith(`${root}${import_node_path.sep}`)) {
532
+ throw new Error("Invalid path");
533
+ }
534
+ return full;
535
+ }
536
+ function joinRelPath(base, name) {
537
+ return base ? `${base}/${name}` : name;
538
+ }
539
+ async function listAllFiles(rootDir, relPath) {
540
+ const targetPath = resolvePath(rootDir, relPath);
541
+ let entries = [];
542
+ try {
543
+ entries = await (0, import_promises.readdir)(targetPath, { withFileTypes: true });
544
+ } catch {
545
+ return [];
546
+ }
547
+ const results = [];
548
+ for (const entry of entries) {
549
+ const entryRelPath = joinRelPath(relPath, entry.name);
550
+ if (entry.isDirectory()) {
551
+ results.push(...await listAllFiles(rootDir, entryRelPath));
552
+ } else {
553
+ results.push(entryRelPath);
554
+ }
555
+ }
556
+ return results.sort((a, b) => a.localeCompare(b));
557
+ }
558
+ async function ensureDir(filePath) {
559
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(filePath), { recursive: true });
560
+ }
561
+ function sendJson(res, status, body) {
562
+ res.setHeader("Content-Type", "application/json");
563
+ res.writeHead(status);
564
+ res.end(JSON.stringify(body));
565
+ }
566
+ function handleVFS(req, res, ctx) {
567
+ const url = req.url || "/";
568
+ const method = req.method || "GET";
569
+ if (!url.startsWith("/vfs")) return false;
570
+ if (url === "/vfs/config" && method === "GET") {
571
+ res.setHeader("Content-Type", "application/json");
572
+ res.writeHead(200);
573
+ res.end(JSON.stringify({ usePaths: ctx.usePaths }));
574
+ return true;
575
+ }
576
+ const handleRequest = async () => {
577
+ const urlObj = new URL(url, "http://localhost");
578
+ const query = urlObj.searchParams;
579
+ const rawPath = urlObj.pathname.slice(4);
580
+ const relPath = normalizeRelPath(rawPath);
581
+ if (query.has("watch")) {
582
+ if (method !== "GET") {
583
+ res.writeHead(405);
584
+ res.end("Method not allowed");
585
+ return;
586
+ }
587
+ const watchPath = normalizeRelPath(query.get("watch") || "");
588
+ const fullWatchPath = resolvePath(ctx.rootDir, watchPath);
589
+ let watchStats;
590
+ try {
591
+ watchStats = await (0, import_promises.stat)(fullWatchPath);
592
+ } catch {
593
+ res.writeHead(404);
594
+ res.end("Not found");
595
+ return;
596
+ }
597
+ res.setHeader("Content-Type", "text/event-stream");
598
+ res.setHeader("Cache-Control", "no-cache");
599
+ res.setHeader("Connection", "keep-alive");
600
+ res.writeHead(200);
601
+ const watcher = (0, import_node_fs.watch)(
602
+ fullWatchPath,
603
+ { recursive: watchStats.isDirectory() },
604
+ async (eventType, filename) => {
605
+ const eventPath = normalizeRelPath(
606
+ [watchPath, filename ? filename.toString() : ""].filter(Boolean).join("/")
607
+ );
608
+ const fullEventPath = resolvePath(ctx.rootDir, eventPath);
609
+ let type = "update";
610
+ if (eventType === "rename") {
611
+ try {
612
+ await (0, import_promises.stat)(fullEventPath);
613
+ type = "create";
614
+ } catch {
615
+ type = "delete";
616
+ }
617
+ }
618
+ res.write("event: change\n");
619
+ res.write(
620
+ `data: ${JSON.stringify({
621
+ type,
622
+ path: eventPath,
623
+ mtime: (/* @__PURE__ */ new Date()).toISOString()
624
+ })}
625
+
626
+ `
627
+ );
628
+ }
629
+ );
630
+ req.on("close", () => watcher.close());
631
+ return;
632
+ }
633
+ if (method === "HEAD" && !relPath) {
634
+ res.writeHead(200);
635
+ res.end();
636
+ return;
637
+ }
638
+ if (method === "GET" && !relPath && !query.toString()) {
639
+ const files = await listAllFiles(ctx.rootDir, "");
640
+ sendJson(res, 200, files);
641
+ return;
642
+ }
643
+ if (!relPath && method !== "GET" && method !== "HEAD") {
644
+ res.writeHead(400);
645
+ res.end("Invalid path");
646
+ return;
647
+ }
648
+ const targetPath = resolvePath(ctx.rootDir, relPath);
649
+ if (method === "GET" && query.get("stat") === "true") {
650
+ try {
651
+ const stats = await (0, import_promises.stat)(targetPath);
652
+ sendJson(res, 200, {
653
+ size: stats.size,
654
+ mtime: stats.mtime.toISOString(),
655
+ isFile: stats.isFile(),
656
+ isDirectory: stats.isDirectory()
657
+ });
658
+ } catch {
659
+ res.writeHead(404);
660
+ res.end("Not found");
661
+ }
662
+ return;
663
+ }
664
+ if (method === "GET" && query.get("readdir") === "true") {
665
+ try {
666
+ const entries = await (0, import_promises.readdir)(targetPath, { withFileTypes: true });
667
+ const mapped = entries.map((entry) => ({
668
+ name: entry.name,
669
+ isDirectory: entry.isDirectory()
670
+ })).sort((a, b) => a.name.localeCompare(b.name));
671
+ sendJson(res, 200, mapped);
672
+ } catch (err) {
673
+ const code = err.code;
674
+ if (code === "ENOTDIR") {
675
+ res.writeHead(409);
676
+ res.end("Not a directory");
677
+ return;
678
+ }
679
+ res.writeHead(404);
680
+ res.end("Not found");
681
+ }
682
+ return;
683
+ }
684
+ if (method === "POST" && query.get("mkdir") === "true") {
685
+ const recursive = query.get("recursive") === "true";
686
+ try {
687
+ await (0, import_promises.mkdir)(targetPath, { recursive });
688
+ res.writeHead(200);
689
+ res.end("ok");
690
+ } catch {
691
+ res.writeHead(500);
692
+ res.end("Mkdir failed");
693
+ }
694
+ return;
695
+ }
696
+ switch (method) {
697
+ case "GET": {
698
+ try {
699
+ const content = await (0, import_promises.readFile)(targetPath, "utf-8");
700
+ res.setHeader("Content-Type", "text/plain");
701
+ res.writeHead(200);
702
+ res.end(content);
703
+ } catch {
704
+ res.writeHead(404);
705
+ res.end("Not found");
706
+ }
707
+ return;
708
+ }
709
+ case "PUT": {
710
+ let body = "";
711
+ req.on("data", (chunk) => body += chunk);
712
+ req.on("end", async () => {
713
+ try {
714
+ await ensureDir(targetPath);
715
+ await (0, import_promises.writeFile)(targetPath, body, "utf-8");
716
+ res.writeHead(200);
717
+ res.end("ok");
718
+ } catch (err) {
719
+ ctx.log("VFS PUT error:", err);
720
+ res.writeHead(500);
721
+ res.end("Write failed");
722
+ }
723
+ });
724
+ return;
725
+ }
726
+ case "DELETE": {
727
+ const recursive = query.get("recursive") === "true";
728
+ let stats;
729
+ try {
730
+ stats = await (0, import_promises.stat)(targetPath);
731
+ } catch {
732
+ res.writeHead(404);
733
+ res.end("Not found");
734
+ return;
735
+ }
736
+ if (stats.isDirectory()) {
737
+ if (!recursive) {
738
+ try {
739
+ const entries = await (0, import_promises.readdir)(targetPath);
740
+ if (entries.length > 0) {
741
+ res.writeHead(409);
742
+ res.end("Directory not empty");
743
+ return;
744
+ }
745
+ await (0, import_promises.rm)(targetPath, { recursive: false });
746
+ res.writeHead(200);
747
+ res.end("ok");
748
+ } catch {
749
+ res.writeHead(500);
750
+ res.end("Delete failed");
751
+ }
752
+ return;
753
+ }
754
+ try {
755
+ await (0, import_promises.rm)(targetPath, { recursive: true });
756
+ res.writeHead(200);
757
+ res.end("ok");
758
+ } catch {
759
+ res.writeHead(500);
760
+ res.end("Delete failed");
761
+ }
762
+ return;
763
+ }
764
+ try {
765
+ await (0, import_promises.unlink)(targetPath);
766
+ res.writeHead(200);
767
+ res.end("ok");
768
+ } catch {
769
+ res.writeHead(404);
770
+ res.end("Not found");
771
+ }
772
+ return;
773
+ }
774
+ case "HEAD": {
775
+ try {
776
+ await (0, import_promises.stat)(targetPath);
777
+ res.writeHead(200);
778
+ res.end();
779
+ } catch {
780
+ res.writeHead(404);
781
+ res.end();
782
+ }
783
+ return;
784
+ }
785
+ default:
786
+ res.writeHead(405);
787
+ res.end("Method not allowed");
788
+ }
789
+ };
790
+ handleRequest().catch((err) => {
791
+ ctx.log("VFS error:", err);
792
+ res.writeHead(500);
793
+ res.end("Internal error");
794
+ });
795
+ return true;
796
+ }
797
+
798
+ // src/server/services.ts
799
+ var import_ai2 = require("ai");
800
+ var ServiceRegistry = class {
801
+ tools = /* @__PURE__ */ new Map();
802
+ toolInfo = /* @__PURE__ */ new Map();
803
+ backends = [];
804
+ /**
805
+ * Register tools from MCP or other sources
806
+ * @param tools - Record of tool name to Tool
807
+ * @param namespace - Optional namespace to prefix all tools (e.g., MCP server name)
808
+ */
809
+ registerTools(tools, namespace) {
810
+ for (const [toolName, tool] of Object.entries(tools)) {
811
+ const name = namespace ? `${namespace}.${toolName}` : toolName;
812
+ this.tools.set(name, tool);
813
+ const dotIndex = name.indexOf(".");
814
+ const ns = dotIndex > 0 ? name.substring(0, dotIndex) : name;
815
+ const procedure = dotIndex > 0 ? name.substring(dotIndex + 1) : name;
816
+ this.toolInfo.set(name, {
817
+ name,
818
+ namespace: ns,
819
+ procedure,
820
+ description: tool.description,
821
+ parameters: tool.inputSchema ?? {},
822
+ typescriptInterface: this.generateTypeScriptInterface(name, tool)
823
+ });
824
+ }
825
+ }
826
+ /**
827
+ * Register a service backend (UTCP, HTTP, etc.)
828
+ * Creates callable Tool objects for each procedure so the LLM can invoke them directly.
829
+ * Backends are tried in order of registration, first success wins.
830
+ */
831
+ registerBackend(backend, toolInfos) {
832
+ this.backends.push(backend);
833
+ if (toolInfos) {
834
+ for (const info of toolInfos) {
835
+ this.toolInfo.set(info.name, info);
836
+ const tool = {
837
+ description: info.description,
838
+ inputSchema: (0, import_ai2.jsonSchema)(
839
+ info.parameters ?? { type: "object", properties: {} }
840
+ ),
841
+ execute: async (args) => {
842
+ return backend.call(info.namespace, info.procedure, [args]);
843
+ }
844
+ };
845
+ this.tools.set(info.name, tool);
846
+ }
847
+ }
848
+ }
849
+ /**
850
+ * Generate TypeScript interface from tool schema
851
+ */
852
+ generateTypeScriptInterface(name, tool) {
853
+ const schema = tool.inputSchema;
854
+ const props = schema?.properties ?? {};
855
+ const required = schema?.required ?? [];
856
+ const params = Object.entries(props).map(([key, val]) => {
857
+ const optional = !required.includes(key) ? "?" : "";
858
+ const type = val.type === "number" ? "number" : val.type === "boolean" ? "boolean" : val.type === "array" ? "unknown[]" : val.type === "object" ? "Record<string, unknown>" : "string";
859
+ const comment = val.description ? ` // ${val.description}` : "";
860
+ return ` ${key}${optional}: ${type};${comment}`;
861
+ }).join("\n");
862
+ return `interface ${name.replace(
863
+ /[^a-zA-Z0-9]/g,
864
+ "_"
865
+ )}Args {
866
+ ${params}
867
+ }`;
868
+ }
869
+ /**
870
+ * Convert internal tool name (namespace.procedure) to LLM-safe name (namespace_procedure)
871
+ * OpenAI-compatible APIs require tool names to match ^[a-zA-Z0-9_-]+$
872
+ */
873
+ toLLMToolName(internalName) {
874
+ return internalName.replace(/\./g, "_");
875
+ }
876
+ /**
877
+ * Convert LLM tool name (namespace_procedure) back to internal name (namespace.procedure)
878
+ * Only converts the first underscore after the namespace prefix
879
+ */
880
+ fromLLMToolName(llmName) {
881
+ for (const internalName of this.tools.keys()) {
882
+ if (this.toLLMToolName(internalName) === llmName) {
883
+ return internalName;
884
+ }
885
+ }
886
+ const underscoreIndex = llmName.indexOf("_");
887
+ if (underscoreIndex > 0) {
888
+ return llmName.substring(0, underscoreIndex) + "." + llmName.substring(underscoreIndex + 1);
889
+ }
890
+ return llmName;
891
+ }
892
+ /**
893
+ * Get all tools for LLM usage with LLM-safe names (underscores instead of dots)
894
+ */
895
+ getTools() {
896
+ const result = {};
897
+ for (const [name, tool] of this.tools) {
898
+ result[this.toLLMToolName(name)] = tool;
899
+ }
900
+ return result;
901
+ }
902
+ /**
903
+ * Get service metadata for prompt generation
904
+ */
905
+ getServiceInfo() {
906
+ return Array.from(this.toolInfo.values());
907
+ }
908
+ /**
909
+ * Get unique namespaces
910
+ */
911
+ getNamespaces() {
912
+ const namespaces = /* @__PURE__ */ new Set();
913
+ for (const info of this.toolInfo.values()) {
914
+ namespaces.add(info.namespace);
915
+ }
916
+ return Array.from(namespaces);
917
+ }
918
+ /**
919
+ * Search for services by query, namespace, or list all
920
+ */
921
+ searchServices(options = {}) {
922
+ const { query, namespace, limit = 20, includeInterfaces = false } = options;
923
+ let results = Array.from(this.toolInfo.values());
924
+ if (namespace) {
925
+ results = results.filter((info) => info.namespace === namespace);
926
+ }
927
+ if (query) {
928
+ const queryLower = query.toLowerCase();
929
+ const keywords = queryLower.split(/\s+/).filter(Boolean);
930
+ results = results.map((info) => {
931
+ const searchText = `${info.name} ${info.namespace} ${info.procedure} ${info.description ?? ""}`.toLowerCase();
932
+ const matchCount = keywords.filter(
933
+ (kw) => searchText.includes(kw)
934
+ ).length;
935
+ return { info, score: matchCount / keywords.length };
936
+ }).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score).map(({ info }) => info);
937
+ }
938
+ results = results.slice(0, limit);
939
+ if (!includeInterfaces) {
940
+ results = results.map(({ typescriptInterface: _, ...rest }) => rest);
941
+ }
942
+ return results;
943
+ }
944
+ /**
945
+ * Get detailed info about a specific tool
946
+ */
947
+ getToolInfo(toolName) {
948
+ return this.toolInfo.get(toolName);
949
+ }
950
+ /**
951
+ * List all tool names
952
+ */
953
+ listToolNames() {
954
+ return Array.from(this.toolInfo.keys());
955
+ }
956
+ /**
957
+ * Call a service procedure
958
+ */
959
+ async call(namespace, procedure, args) {
960
+ for (const backend of this.backends) {
961
+ try {
962
+ return await backend.call(namespace, procedure, [args]);
963
+ } catch {
964
+ }
965
+ }
966
+ const exactKey = `${namespace}.${procedure}`;
967
+ let tool = this.tools.get(exactKey);
968
+ if (!tool) {
969
+ for (const [name, t] of this.tools) {
970
+ if (name.startsWith(`${namespace}.`) && name.endsWith(procedure)) {
971
+ tool = t;
972
+ break;
973
+ }
974
+ }
975
+ }
976
+ if (!tool) {
977
+ throw new Error(
978
+ `Service not found: ${namespace}.${procedure}. Available: ${Array.from(
979
+ this.tools.keys()
980
+ ).slice(0, 10).join(", ")}`
981
+ );
982
+ }
983
+ if (!tool.execute) {
984
+ throw new Error(`Tool ${namespace}.${procedure} has no execute function`);
985
+ }
986
+ const result = await tool.execute(args, {
987
+ toolCallId: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
988
+ messages: []
989
+ });
990
+ return result;
991
+ }
992
+ /**
993
+ * Check if a service exists
994
+ */
995
+ has(namespace, procedure) {
996
+ const key = `${namespace}.${procedure}`;
997
+ return this.tools.has(key);
998
+ }
999
+ /**
1000
+ * Get count of registered tools
1001
+ */
1002
+ get size() {
1003
+ return this.toolInfo.size;
1004
+ }
1005
+ };
1006
+ function generateServicesPrompt(registry) {
1007
+ const namespaces = registry.getNamespaces();
1008
+ if (namespaces.length === 0) return "";
1009
+ const services = registry.getServiceInfo();
1010
+ const byNamespace = /* @__PURE__ */ new Map();
1011
+ for (const service of services) {
1012
+ const existing = byNamespace.get(service.namespace) || [];
1013
+ existing.push(service);
1014
+ byNamespace.set(service.namespace, existing);
1015
+ }
1016
+ let prompt = `## Available Services
1017
+
1018
+ The following services are available for generated widgets to call:
1019
+
1020
+ `;
1021
+ for (const [ns, tools] of byNamespace) {
1022
+ prompt += `### \`${ns}\`
1023
+ `;
1024
+ for (const tool of tools) {
1025
+ prompt += `- \`${ns}.${tool.procedure}()\``;
1026
+ if (tool.description) {
1027
+ prompt += `: ${tool.description}`;
1028
+ }
1029
+ prompt += "\n";
1030
+ }
1031
+ prompt += "\n";
1032
+ }
1033
+ prompt += `**Usage in widgets:**
1034
+ \`\`\`tsx
1035
+ // Services are available as global namespaces
1036
+ const result = await ${namespaces[0] ?? "service"}.${byNamespace.get(namespaces[0] ?? "")?.[0]?.procedure ?? "example"}({ /* args */ });
1037
+ \`\`\`
1038
+
1039
+ Make sure to handle loading states and errors when calling services.
1040
+ `;
1041
+ return prompt;
1042
+ }
1043
+
1044
+ // src/server/index.ts
1045
+ async function initMcpTools(configs, registry) {
1046
+ for (const config of configs) {
1047
+ const client = await (0, import_mcp.createMCPClient)({
1048
+ transport: new import_mcp_stdio.Experimental_StdioMCPTransport({
1049
+ command: config.command,
1050
+ args: config.args
1051
+ })
1052
+ });
1053
+ registry.registerTools(await client.tools(), config.name);
1054
+ }
1055
+ }
1056
+ var searchServicesSchema = {
1057
+ type: "object",
1058
+ properties: {
1059
+ query: {
1060
+ type: "string",
1061
+ description: 'Natural language description of what you want to do (e.g., "get weather forecast", "list github repos")'
1062
+ },
1063
+ namespace: {
1064
+ type: "string",
1065
+ description: 'Filter results to a specific service namespace (e.g., "weather", "github")'
1066
+ },
1067
+ tool_name: {
1068
+ type: "string",
1069
+ description: "Get detailed info about a specific tool by name"
1070
+ },
1071
+ limit: {
1072
+ type: "number",
1073
+ description: "Maximum number of results to return",
1074
+ default: 10
1075
+ },
1076
+ include_interfaces: {
1077
+ type: "boolean",
1078
+ description: "Include TypeScript interface definitions in results",
1079
+ default: true
1080
+ }
1081
+ }
1082
+ };
1083
+ function createSearchServicesTool(registry) {
1084
+ return {
1085
+ description: `Search for available services/tools. Use this to discover what APIs are available for widgets to call.
1086
+
1087
+ Returns matching services with their TypeScript interfaces. Use when:
1088
+ - You need to find a service to accomplish a task
1089
+ - You want to explore available APIs in a namespace
1090
+ - You need the exact interface/parameters for a service call`,
1091
+ inputSchema: (0, import_ai3.jsonSchema)(searchServicesSchema),
1092
+ execute: async (args) => {
1093
+ if (args.tool_name) {
1094
+ const info = registry.getToolInfo(args.tool_name);
1095
+ if (!info) {
1096
+ return {
1097
+ success: false,
1098
+ error: `Tool '${args.tool_name}' not found`
1099
+ };
1100
+ }
1101
+ return { success: true, tool: info };
1102
+ }
1103
+ const results = registry.searchServices({
1104
+ query: args.query,
1105
+ namespace: args.namespace,
1106
+ limit: args.limit ?? 10,
1107
+ includeInterfaces: args.include_interfaces ?? true
1108
+ });
1109
+ return {
1110
+ success: true,
1111
+ count: results.length,
1112
+ tools: results,
1113
+ namespaces: registry.getNamespaces()
1114
+ };
1115
+ }
1116
+ };
1117
+ }
1118
+ function parseBody2(req) {
1119
+ return new Promise((resolve2, reject) => {
1120
+ let body = "";
1121
+ req.on("data", (chunk) => body += chunk);
1122
+ req.on("end", () => {
1123
+ try {
1124
+ resolve2(JSON.parse(body));
1125
+ } catch (err) {
1126
+ reject(err);
1127
+ }
1128
+ });
1129
+ req.on("error", reject);
1130
+ });
1131
+ }
1132
+ async function createStitcheryServer(config = {}) {
1133
+ const {
1134
+ port = 6434,
1135
+ host = "127.0.0.1",
1136
+ copilotProxyUrl = "http://127.0.0.1:6433/v1",
1137
+ localPackages = {},
1138
+ mcpServers = [],
1139
+ utcp,
1140
+ vfsDir,
1141
+ vfsUsePaths = false,
1142
+ verbose = false
1143
+ } = config;
1144
+ const log = verbose ? (...args) => console.log("[stitchery]", ...args) : () => {
1145
+ };
1146
+ const registry = new ServiceRegistry();
1147
+ log("Initializing MCP tools...");
1148
+ await initMcpTools(mcpServers, registry);
1149
+ log(`Loaded ${registry.size} tools from ${mcpServers.length} MCP servers`);
1150
+ if (utcp) {
1151
+ log("Initializing UTCP backend...");
1152
+ log("UTCP config:", JSON.stringify(utcp, null, 2));
1153
+ try {
1154
+ const { backend, toolInfos } = await (0, import_patchwork_utcp.createUtcpBackend)(
1155
+ utcp,
1156
+ utcp.cwd
1157
+ );
1158
+ registry.registerBackend(backend, toolInfos);
1159
+ log(
1160
+ `Registered UTCP backend with ${toolInfos.length} tools:`,
1161
+ toolInfos.map((tool) => tool.name).join(", ")
1162
+ );
1163
+ } catch (err) {
1164
+ console.error("[stitchery] Failed to initialize UTCP backend:", err);
1165
+ }
1166
+ }
1167
+ log("Local packages:", localPackages);
1168
+ const internalTools = {
1169
+ search_services: createSearchServicesTool(registry)
1170
+ };
1171
+ const allTools = { ...registry.getTools(), ...internalTools };
1172
+ const routeCtx = {
1173
+ copilotProxyUrl,
1174
+ tools: allTools,
1175
+ registry,
1176
+ servicesPrompt: generateServicesPrompt(registry),
1177
+ log
1178
+ };
1179
+ const localPkgCtx = { localPackages, log };
1180
+ const vfsCtx = vfsDir ? { rootDir: vfsDir, usePaths: vfsUsePaths, log } : null;
1181
+ const server = (0, import_node_http.createServer)(async (req, res) => {
1182
+ res.setHeader("Access-Control-Allow-Origin", "*");
1183
+ res.setHeader(
1184
+ "Access-Control-Allow-Methods",
1185
+ "GET, POST, PUT, DELETE, HEAD, OPTIONS"
1186
+ );
1187
+ res.setHeader(
1188
+ "Access-Control-Allow-Headers",
1189
+ "Content-Type, Authorization"
1190
+ );
1191
+ if (req.method === "OPTIONS") {
1192
+ res.writeHead(204);
1193
+ res.end();
1194
+ return;
1195
+ }
1196
+ const url = req.url || "/";
1197
+ log(`${req.method} ${url}`);
1198
+ try {
1199
+ if (handleLocalPackages(req, res, localPkgCtx)) {
1200
+ return;
1201
+ }
1202
+ if (vfsCtx && handleVFS(req, res, vfsCtx)) {
1203
+ return;
1204
+ }
1205
+ if (url === "/api/chat" && req.method === "POST") {
1206
+ await handleChat(req, res, routeCtx);
1207
+ return;
1208
+ }
1209
+ if (url === "/api/edit" && req.method === "POST") {
1210
+ await handleEdit(req, res, routeCtx);
1211
+ return;
1212
+ }
1213
+ const proxyMatch = url.match(/^\/api\/proxy\/([^/]+)\/(.+)$/);
1214
+ if (proxyMatch && req.method === "POST") {
1215
+ const [, namespace, procedure] = proxyMatch;
1216
+ try {
1217
+ const body = await parseBody2(req);
1218
+ const result = await registry.call(
1219
+ namespace,
1220
+ procedure,
1221
+ body.args ?? {}
1222
+ );
1223
+ res.setHeader("Content-Type", "application/json");
1224
+ res.writeHead(200);
1225
+ res.end(JSON.stringify(result));
1226
+ } catch (err) {
1227
+ log("Proxy error:", err);
1228
+ res.setHeader("Content-Type", "application/json");
1229
+ res.writeHead(500);
1230
+ res.end(
1231
+ JSON.stringify({
1232
+ error: err instanceof Error ? err.message : "Service call failed"
1233
+ })
1234
+ );
1235
+ }
1236
+ return;
1237
+ }
1238
+ if (url === "/api/services/search" && req.method === "POST") {
1239
+ const body = await parseBody2(req);
1240
+ res.setHeader("Content-Type", "application/json");
1241
+ res.writeHead(200);
1242
+ if (body.tool_name) {
1243
+ const info = registry.getToolInfo(body.tool_name);
1244
+ if (!info) {
1245
+ res.end(
1246
+ JSON.stringify({
1247
+ success: false,
1248
+ error: `Tool '${body.tool_name}' not found`
1249
+ })
1250
+ );
1251
+ } else {
1252
+ res.end(JSON.stringify({ success: true, tool: info }));
1253
+ }
1254
+ return;
1255
+ }
1256
+ const results = registry.searchServices({
1257
+ query: body.query,
1258
+ namespace: body.namespace,
1259
+ limit: body.limit ?? 20,
1260
+ includeInterfaces: body.include_interfaces ?? false
1261
+ });
1262
+ res.end(
1263
+ JSON.stringify({
1264
+ success: true,
1265
+ count: results.length,
1266
+ tools: results,
1267
+ namespaces: registry.getNamespaces()
1268
+ })
1269
+ );
1270
+ return;
1271
+ }
1272
+ if (url === "/api/services" && req.method === "GET") {
1273
+ res.setHeader("Content-Type", "application/json");
1274
+ res.writeHead(200);
1275
+ res.end(
1276
+ JSON.stringify({
1277
+ namespaces: registry.getNamespaces(),
1278
+ services: registry.getServiceInfo()
1279
+ })
1280
+ );
1281
+ return;
1282
+ }
1283
+ if (url === "/health" || url === "/") {
1284
+ res.setHeader("Content-Type", "application/json");
1285
+ res.writeHead(200);
1286
+ res.end(JSON.stringify({ status: "ok", service: "stitchery" }));
1287
+ return;
1288
+ }
1289
+ res.writeHead(404);
1290
+ res.end(`Not found: ${url}`);
1291
+ } catch (err) {
1292
+ log("Error:", err);
1293
+ res.writeHead(500);
1294
+ res.end(err instanceof Error ? err.message : "Internal server error");
1295
+ }
1296
+ });
1297
+ return {
1298
+ server,
1299
+ registry,
1300
+ async start() {
1301
+ return new Promise((resolve2, reject) => {
1302
+ server.on("error", reject);
1303
+ server.listen(port, host, () => {
1304
+ log(`Server listening on http://${host}:${port}`);
1305
+ resolve2({ port, host });
1306
+ });
1307
+ });
1308
+ },
1309
+ async stop() {
1310
+ return new Promise((resolve2, reject) => {
1311
+ server.close((err) => {
1312
+ if (err) reject(err);
1313
+ else resolve2();
1314
+ });
1315
+ });
1316
+ }
1317
+ };
1318
+ }
1319
+
1320
+ // src/cli.ts
1321
+ var program = new import_commander.Command();
1322
+ program.name("stitchery").description("Backend services for LLM-generated artifacts").version("0.1.0");
1323
+ program.command("serve").description("Start the stitchery server").option("-p, --port <port>", "Port to listen on", "6434").option("-h, --host <host>", "Host to bind to", "127.0.0.1").option(
1324
+ "--copilot-proxy-url <url>",
1325
+ "Copilot proxy URL",
1326
+ "http://127.0.0.1:6433/v1"
1327
+ ).option(
1328
+ "--mcp <servers...>",
1329
+ "MCP server commands (format: name:command:arg1,arg2)"
1330
+ ).option("--utcp-config <path>", "Load UTCP configuration from JSON file").option(
1331
+ "--local-package <packages...>",
1332
+ "Local package overrides (format: name:path)"
1333
+ ).option(
1334
+ "--vfs-dir <path>",
1335
+ "Directory for virtual file system storage",
1336
+ ".working/widgets"
1337
+ ).option(
1338
+ "--vfs-use-paths",
1339
+ "Use file paths from code blocks instead of UUIDs for VFS storage"
1340
+ ).option("-v, --verbose", "Enable verbose logging").action(async (options) => {
1341
+ if (options.verbose) {
1342
+ console.log("[stitchery] CLI options:", JSON.stringify(options, null, 2));
1343
+ }
1344
+ const mcpServers = (options.mcp ?? []).map((spec) => {
1345
+ const [name, command, ...rest] = spec.split(":");
1346
+ const rawArgs = rest.join(":").split(",").filter(Boolean);
1347
+ const args = rawArgs.map(
1348
+ (arg) => arg.startsWith(".") ? import_path2.default.resolve(process.cwd(), arg) : arg
1349
+ );
1350
+ return { name, command, args };
1351
+ });
1352
+ const localPackages = {};
1353
+ for (const spec of options.localPackage ?? []) {
1354
+ const [name, ...pathParts] = spec.split(":");
1355
+ const pkgPath = pathParts.join(":");
1356
+ localPackages[name] = import_path2.default.resolve(process.cwd(), pkgPath);
1357
+ }
1358
+ let utcpConfig;
1359
+ if (options.utcpConfig) {
1360
+ const utcpPath = import_path2.default.resolve(process.cwd(), options.utcpConfig);
1361
+ if (!import_fs2.default.existsSync(utcpPath)) {
1362
+ console.error(`UTCP config file not found: ${utcpPath}`);
1363
+ process.exit(1);
1364
+ }
1365
+ try {
1366
+ const content = import_fs2.default.readFileSync(utcpPath, "utf-8");
1367
+ utcpConfig = JSON.parse(content);
1368
+ if (options.verbose) {
1369
+ console.log("[stitchery] Loaded UTCP config from:", utcpPath);
1370
+ }
1371
+ } catch (err) {
1372
+ console.error(`Failed to parse UTCP config: ${err}`);
1373
+ process.exit(1);
1374
+ }
1375
+ }
1376
+ const vfsDir = options.vfsDir ? import_path2.default.resolve(process.cwd(), options.vfsDir) : void 0;
1377
+ if (vfsDir && options.verbose) {
1378
+ console.log("[stitchery] VFS directory:", vfsDir);
1379
+ }
1380
+ const server = await createStitcheryServer({
1381
+ port: parseInt(options.port, 10),
1382
+ host: options.host,
1383
+ copilotProxyUrl: options.copilotProxyUrl,
1384
+ mcpServers,
1385
+ localPackages,
1386
+ utcp: utcpConfig,
1387
+ vfsDir,
1388
+ vfsUsePaths: options.vfsUsePaths ?? false,
1389
+ verbose: options.verbose
1390
+ });
1391
+ const { port, host } = await server.start();
1392
+ console.log(`Stitchery server running at http://${host}:${port}`);
1393
+ process.on("SIGINT", async () => {
1394
+ console.log("\nShutting down...");
1395
+ await server.stop();
1396
+ process.exit(0);
1397
+ });
1398
+ });
1399
+ program.parse();
1400
+ //# sourceMappingURL=cli.cjs.map