@aprovan/patchwork 0.1.0 → 0.1.1

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.
Files changed (71) hide show
  1. package/.github/workflows/publish.yml +1 -1
  2. package/.vscode/launch.json +19 -0
  3. package/README.md +24 -0
  4. package/apps/chat/package.json +4 -4
  5. package/apps/chat/vite.config.ts +8 -8
  6. package/docs/specs/directory-sync.md +822 -0
  7. package/docs/specs/patchwork-vscode.md +625 -0
  8. package/package.json +2 -2
  9. package/packages/compiler/package.json +3 -2
  10. package/packages/compiler/src/index.ts +13 -14
  11. package/packages/compiler/src/vfs/backends/http.ts +139 -0
  12. package/packages/compiler/src/vfs/backends/indexeddb.ts +185 -24
  13. package/packages/compiler/src/vfs/backends/memory.ts +166 -0
  14. package/packages/compiler/src/vfs/core/index.ts +26 -0
  15. package/packages/compiler/src/vfs/core/types.ts +93 -0
  16. package/packages/compiler/src/vfs/core/utils.ts +42 -0
  17. package/packages/compiler/src/vfs/core/virtual-fs.ts +120 -0
  18. package/packages/compiler/src/vfs/index.ts +37 -5
  19. package/packages/compiler/src/vfs/project.ts +16 -16
  20. package/packages/compiler/src/vfs/store.ts +183 -19
  21. package/packages/compiler/src/vfs/sync/differ.ts +47 -0
  22. package/packages/compiler/src/vfs/sync/engine.ts +398 -0
  23. package/packages/compiler/src/vfs/sync/index.ts +3 -0
  24. package/packages/compiler/src/vfs/sync/resolver.ts +46 -0
  25. package/packages/compiler/src/vfs/types.ts +1 -8
  26. package/packages/compiler/tsup.config.ts +5 -5
  27. package/packages/editor/package.json +1 -1
  28. package/packages/editor/src/components/CodeBlockExtension.tsx +1 -1
  29. package/packages/editor/src/components/CodePreview.tsx +59 -1
  30. package/packages/editor/src/components/edit/CodeBlockView.tsx +72 -0
  31. package/packages/editor/src/components/edit/EditModal.tsx +169 -28
  32. package/packages/editor/src/components/edit/FileTree.tsx +67 -13
  33. package/packages/editor/src/components/edit/MediaPreview.tsx +106 -0
  34. package/packages/editor/src/components/edit/SaveConfirmDialog.tsx +60 -0
  35. package/packages/editor/src/components/edit/fileTypes.ts +125 -0
  36. package/packages/editor/src/components/edit/index.ts +4 -0
  37. package/packages/editor/src/components/edit/types.ts +3 -0
  38. package/packages/editor/src/components/edit/useEditSession.ts +22 -4
  39. package/packages/editor/src/index.ts +17 -0
  40. package/packages/editor/src/lib/diff.ts +2 -1
  41. package/packages/editor/src/lib/vfs.ts +28 -10
  42. package/packages/editor/tsup.config.ts +10 -5
  43. package/packages/stitchery/package.json +5 -3
  44. package/packages/stitchery/src/server/index.ts +57 -57
  45. package/packages/stitchery/src/server/vfs-routes.ts +246 -56
  46. package/packages/stitchery/tsup.config.ts +5 -5
  47. package/packages/utcp/package.json +3 -2
  48. package/packages/utcp/tsconfig.json +6 -2
  49. package/packages/utcp/tsup.config.ts +6 -6
  50. package/packages/vscode/README.md +31 -0
  51. package/packages/vscode/media/outline.png +0 -0
  52. package/packages/vscode/media/outline.svg +70 -0
  53. package/packages/vscode/media/patchwork.png +0 -0
  54. package/packages/vscode/media/patchwork.svg +72 -0
  55. package/packages/vscode/node_modules/.bin/jiti +17 -0
  56. package/packages/vscode/node_modules/.bin/tsc +17 -0
  57. package/packages/vscode/node_modules/.bin/tsserver +17 -0
  58. package/packages/vscode/node_modules/.bin/tsup +17 -0
  59. package/packages/vscode/node_modules/.bin/tsup-node +17 -0
  60. package/packages/vscode/node_modules/.bin/tsx +17 -0
  61. package/packages/vscode/package.json +136 -0
  62. package/packages/vscode/src/extension.ts +612 -0
  63. package/packages/vscode/src/providers/PatchworkFileSystemProvider.ts +205 -0
  64. package/packages/vscode/src/providers/PatchworkTreeProvider.ts +177 -0
  65. package/packages/vscode/src/providers/PreviewPanelProvider.ts +536 -0
  66. package/packages/vscode/src/services/EditService.ts +24 -0
  67. package/packages/vscode/src/services/EmbeddedStitchery.ts +82 -0
  68. package/packages/vscode/tsconfig.json +13 -0
  69. package/packages/vscode/tsup.config.ts +11 -0
  70. package/packages/compiler/src/vfs/backends/local-fs.ts +0 -41
  71. package/packages/compiler/src/vfs/backends/s3.ts +0 -60
@@ -0,0 +1,125 @@
1
+ export type FileCategory = 'compilable' | 'text' | 'media' | 'binary';
2
+
3
+ export interface FileTypeInfo {
4
+ category: FileCategory;
5
+ language: string | null;
6
+ mimeType: string;
7
+ }
8
+
9
+ const COMPILABLE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
10
+ const MEDIA_EXTENSIONS = ['.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.mp4', '.mov', '.webm'];
11
+ const TEXT_EXTENSIONS = ['.json', '.yaml', '.yml', '.md', '.txt', '.css', '.html', '.xml', '.toml'];
12
+
13
+ const EXTENSION_TO_LANGUAGE: Record<string, string> = {
14
+ '.tsx': 'tsx',
15
+ '.jsx': 'jsx',
16
+ '.ts': 'typescript',
17
+ '.js': 'javascript',
18
+ '.json': 'json',
19
+ '.yaml': 'yaml',
20
+ '.yml': 'yaml',
21
+ '.md': 'markdown',
22
+ '.txt': 'text',
23
+ '.css': 'css',
24
+ '.html': 'html',
25
+ '.xml': 'xml',
26
+ '.toml': 'toml',
27
+ '.svg': 'xml',
28
+ };
29
+
30
+ const EXTENSION_TO_MIME: Record<string, string> = {
31
+ '.tsx': 'text/typescript-jsx',
32
+ '.jsx': 'text/javascript-jsx',
33
+ '.ts': 'text/typescript',
34
+ '.js': 'text/javascript',
35
+ '.json': 'application/json',
36
+ '.yaml': 'text/yaml',
37
+ '.yml': 'text/yaml',
38
+ '.md': 'text/markdown',
39
+ '.txt': 'text/plain',
40
+ '.css': 'text/css',
41
+ '.html': 'text/html',
42
+ '.xml': 'application/xml',
43
+ '.toml': 'text/toml',
44
+ '.svg': 'image/svg+xml',
45
+ '.png': 'image/png',
46
+ '.jpg': 'image/jpeg',
47
+ '.jpeg': 'image/jpeg',
48
+ '.gif': 'image/gif',
49
+ '.webp': 'image/webp',
50
+ '.mp4': 'video/mp4',
51
+ '.mov': 'video/quicktime',
52
+ '.webm': 'video/webm',
53
+ };
54
+
55
+ function getExtension(path: string): string {
56
+ const lastDot = path.lastIndexOf('.');
57
+ if (lastDot === -1) return '';
58
+ return path.slice(lastDot).toLowerCase();
59
+ }
60
+
61
+ export function getFileType(path: string): FileTypeInfo {
62
+ const ext = getExtension(path);
63
+
64
+ if (COMPILABLE_EXTENSIONS.includes(ext)) {
65
+ return {
66
+ category: 'compilable',
67
+ language: EXTENSION_TO_LANGUAGE[ext] ?? null,
68
+ mimeType: EXTENSION_TO_MIME[ext] ?? 'text/plain',
69
+ };
70
+ }
71
+
72
+ if (TEXT_EXTENSIONS.includes(ext)) {
73
+ return {
74
+ category: 'text',
75
+ language: EXTENSION_TO_LANGUAGE[ext] ?? null,
76
+ mimeType: EXTENSION_TO_MIME[ext] ?? 'text/plain',
77
+ };
78
+ }
79
+
80
+ if (MEDIA_EXTENSIONS.includes(ext)) {
81
+ return {
82
+ category: 'media',
83
+ language: ext === '.svg' ? 'xml' : null,
84
+ mimeType: EXTENSION_TO_MIME[ext] ?? 'application/octet-stream',
85
+ };
86
+ }
87
+
88
+ return {
89
+ category: 'binary',
90
+ language: null,
91
+ mimeType: 'application/octet-stream',
92
+ };
93
+ }
94
+
95
+ export function isCompilable(path: string): boolean {
96
+ return COMPILABLE_EXTENSIONS.includes(getExtension(path));
97
+ }
98
+
99
+ export function isMediaFile(path: string): boolean {
100
+ return MEDIA_EXTENSIONS.includes(getExtension(path));
101
+ }
102
+
103
+ export function isTextFile(path: string): boolean {
104
+ return TEXT_EXTENSIONS.includes(getExtension(path));
105
+ }
106
+
107
+ export function getLanguageFromExt(path: string): string | null {
108
+ const ext = getExtension(path);
109
+ return EXTENSION_TO_LANGUAGE[ext] ?? null;
110
+ }
111
+
112
+ export function getMimeType(path: string): string {
113
+ const ext = getExtension(path);
114
+ return EXTENSION_TO_MIME[ext] ?? 'application/octet-stream';
115
+ }
116
+
117
+ export function isImageFile(path: string): boolean {
118
+ const ext = getExtension(path);
119
+ return ['.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext);
120
+ }
121
+
122
+ export function isVideoFile(path: string): boolean {
123
+ const ext = getExtension(path);
124
+ return ['.mp4', '.mov', '.webm'].includes(ext);
125
+ }
@@ -4,3 +4,7 @@ export * from './useEditSession';
4
4
  export * from './EditHistory';
5
5
  export * from './EditModal';
6
6
  export * from './FileTree';
7
+ export * from './SaveConfirmDialog';
8
+ export * from './fileTypes';
9
+ export * from './CodeBlockView';
10
+ export * from './MediaPreview';
@@ -23,8 +23,11 @@ export interface EditSessionActions {
23
23
  updateActiveFile: (content: string) => void;
24
24
  setActiveFile: (path: string) => void;
25
25
  clearError: () => void;
26
+ replaceFile: (path: string, content: string, encoding?: 'utf8' | 'base64') => void;
26
27
  }
27
28
 
29
+ export type FileEncoding = 'utf8' | 'base64';
30
+
28
31
  // Convenience getters
29
32
  export function getActiveContent(state: EditSessionState): string {
30
33
  return state.project.files.get(state.activeFile)?.content ?? '';
@@ -10,7 +10,8 @@ import type {
10
10
  } from './types';
11
11
 
12
12
  export interface UseEditSessionOptions {
13
- originalCode: string;
13
+ originalCode?: string;
14
+ originalProject?: VirtualProject;
14
15
  compile?: CompileFn;
15
16
  apiEndpoint?: string;
16
17
  }
@@ -25,11 +26,11 @@ function cloneProject(project: VirtualProject): VirtualProject {
25
26
  export function useEditSession(
26
27
  options: UseEditSessionOptions,
27
28
  ): EditSessionState & EditSessionActions {
28
- const { originalCode, compile, apiEndpoint } = options;
29
+ const { originalCode, originalProject: providedProject, compile, apiEndpoint } = options;
29
30
 
30
31
  const originalProject = useMemo(
31
- () => createSingleFileProject(originalCode),
32
- [originalCode],
32
+ () => providedProject ?? createSingleFileProject(originalCode ?? ''),
33
+ [providedProject, originalCode],
33
34
  );
34
35
 
35
36
  const [project, setProject] = useState<VirtualProject>(originalProject);
@@ -142,6 +143,22 @@ export function useEditSession(
142
143
  [activeFile],
143
144
  );
144
145
 
146
+ const replaceFile = useCallback(
147
+ (path: string, content: string, encoding: 'utf8' | 'base64' = 'utf8') => {
148
+ setProject((prev) => {
149
+ const updated = cloneProject(prev);
150
+ const file = updated.files.get(path);
151
+ if (file) {
152
+ updated.files.set(path, { ...file, content, encoding });
153
+ } else {
154
+ updated.files.set(path, { path, content, encoding });
155
+ }
156
+ return updated;
157
+ });
158
+ },
159
+ [],
160
+ );
161
+
145
162
  const clearError = useCallback(() => {
146
163
  setError(null);
147
164
  }, []);
@@ -160,5 +177,6 @@ export function useEditSession(
160
177
  updateActiveFile,
161
178
  setActiveFile,
162
179
  clearError,
180
+ replaceFile,
163
181
  };
164
182
  }
@@ -12,6 +12,9 @@ export {
12
12
  EditModal,
13
13
  EditHistory,
14
14
  FileTree,
15
+ SaveConfirmDialog,
16
+ CodeBlockView,
17
+ MediaPreview,
15
18
  useEditSession,
16
19
  sendEditRequest,
17
20
  type EditModalProps,
@@ -25,8 +28,22 @@ export {
25
28
  type CompileFn,
26
29
  type EditApiOptions,
27
30
  type FileTreeProps,
31
+ type SaveConfirmDialogProps,
32
+ type CodeBlockViewProps,
33
+ type MediaPreviewProps,
34
+ type FileCategory,
35
+ type FileTypeInfo,
36
+ type FileEncoding,
28
37
  getActiveContent,
29
38
  getFiles,
39
+ getFileType,
40
+ isCompilable,
41
+ isMediaFile,
42
+ isTextFile,
43
+ isImageFile,
44
+ isVideoFile,
45
+ getLanguageFromExt,
46
+ getMimeType,
30
47
  } from './components/edit';
31
48
 
32
49
  // Lib utilities
@@ -155,7 +155,6 @@ export interface ParsedEditResponse {
155
155
  /**
156
156
  * Parse progress notes and diffs from an edit response.
157
157
  *
158
- * New format uses tagged attributes on code fences:
159
158
  * ```diff note="Adding handler" path="@/components/Button.tsx"
160
159
  * <<<<<<< SEARCH
161
160
  * exact code
@@ -275,6 +274,8 @@ export function applyDiffs(
275
274
  }
276
275
 
277
276
  export function hasDiffBlocks(text: string): boolean {
277
+ // Reset lastIndex to avoid state issues from the global regex flag
278
+ DIFF_BLOCK_REGEX.lastIndex = 0;
278
279
  return DIFF_BLOCK_REGEX.test(text);
279
280
  }
280
281
 
@@ -1,13 +1,13 @@
1
- import {
2
- VFSStore,
3
- LocalFSBackend,
1
+ import {
2
+ VFSStore,
3
+ HttpBackend,
4
+ type ChangeRecord,
4
5
  type VirtualProject,
5
- type VirtualFile
6
6
  } from '@aprovan/patchwork-compiler';
7
7
 
8
8
  /**
9
9
  * VFS client for persisting virtual projects to the stitchery server.
10
- * Uses LocalFSBackend which makes HTTP requests to /vfs routes.
10
+ * Uses HttpBackend which makes HTTP requests to /vfs routes.
11
11
  */
12
12
 
13
13
  // VFS base URL - points to stitchery server's VFS routes
@@ -41,12 +41,15 @@ let storeInstance: VFSStore | null = null;
41
41
 
42
42
  /**
43
43
  * Get the VFS store instance (creates one if needed).
44
- * Store uses LocalFSBackend to persist to the stitchery server.
44
+ * Store uses HttpBackend to persist to the stitchery server.
45
45
  */
46
46
  export function getVFSStore(): VFSStore {
47
47
  if (!storeInstance) {
48
- const backend = new LocalFSBackend({ baseUrl: VFS_BASE_URL });
49
- storeInstance = new VFSStore(backend);
48
+ const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
49
+ storeInstance = new VFSStore(provider, {
50
+ sync: true,
51
+ conflictStrategy: 'local-wins',
52
+ });
50
53
  }
51
54
  return storeInstance;
52
55
  }
@@ -88,9 +91,24 @@ export async function listProjects(): Promise<string[]> {
88
91
  /**
89
92
  * Save a single file to the VFS.
90
93
  */
91
- export async function saveFile(file: VirtualFile): Promise<void> {
94
+ export async function saveFile(path: string, content: string): Promise<void> {
92
95
  const store = getVFSStore();
93
- await store.putFile(file);
96
+ await store.writeFile(path, content);
97
+ }
98
+
99
+ export async function loadFile(
100
+ path: string,
101
+ encoding?: 'utf8' | 'base64',
102
+ ): Promise<string> {
103
+ const store = getVFSStore();
104
+ return store.readFile(path, encoding);
105
+ }
106
+
107
+ export function subscribeToChanges(
108
+ callback: (record: ChangeRecord) => void,
109
+ ): () => void {
110
+ const store = getVFSStore();
111
+ return store.on('change', callback);
94
112
  }
95
113
 
96
114
  /**
@@ -1,10 +1,15 @@
1
- import { defineConfig } from 'tsup';
1
+ import { defineConfig } from "tsup";
2
2
 
3
3
  export default defineConfig({
4
- entry: ['src/index.ts'],
5
- format: ['esm'],
6
- dts: true,
4
+ entry: ["src/index.ts"],
5
+ format: ["esm"],
6
+ dts: false,
7
7
  clean: true,
8
- external: ['react', 'react-dom'],
8
+ external: [
9
+ "react",
10
+ "react-dom",
11
+ "@aprovan/bobbin",
12
+ "@aprovan/patchwork-compiler",
13
+ ],
9
14
  treeshake: true,
10
15
  });
@@ -3,7 +3,7 @@
3
3
  "version": "0.1.0",
4
4
  "description": "Backend services for LLM-generated artifacts",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
6
+ "main": "./dist/index.cjs",
7
7
  "types": "./dist/index.d.ts",
8
8
  "bin": {
9
9
  "stitchery": "./dist/cli.js"
@@ -11,11 +11,13 @@
11
11
  "exports": {
12
12
  ".": {
13
13
  "types": "./dist/index.d.ts",
14
- "import": "./dist/index.js"
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
15
16
  },
16
17
  "./server": {
17
18
  "types": "./dist/server/index.d.ts",
18
- "import": "./dist/server/index.js"
19
+ "import": "./dist/server/index.js",
20
+ "require": "./dist/server/index.cjs"
19
21
  }
20
22
  },
21
23
  "scripts": {
@@ -1,13 +1,13 @@
1
- import { createServer, type Server } from 'node:http';
2
- import { createMCPClient } from '@ai-sdk/mcp';
3
- import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
4
- import { createUtcpBackend } from '@aprovan/patchwork-utcp';
5
- import { jsonSchema, type Tool } from 'ai';
6
- import type { ServerConfig, McpServerConfig } from '../types.js';
7
- import { handleChat, handleEdit, type RouteContext } from './routes.js';
8
- import { handleLocalPackages } from './local-packages.js';
9
- import { handleVFS, type VFSContext } from './vfs-routes.js';
10
- import { ServiceRegistry, generateServicesPrompt } from './services.js';
1
+ import { createServer, type Server } from "node:http";
2
+ import { createMCPClient } from "@ai-sdk/mcp";
3
+ import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
4
+ import { createUtcpBackend } from "@aprovan/patchwork-utcp";
5
+ import { jsonSchema, type Tool } from "ai";
6
+ import type { ServerConfig, McpServerConfig } from "../types.js";
7
+ import { handleChat, handleEdit, type RouteContext } from "./routes.js";
8
+ import { handleLocalPackages } from "./local-packages.js";
9
+ import { handleVFS, type VFSContext } from "./vfs-routes.js";
10
+ import { ServiceRegistry, generateServicesPrompt } from "./services.js";
11
11
 
12
12
  export interface StitcheryServer {
13
13
  server: Server;
@@ -33,30 +33,30 @@ async function initMcpTools(
33
33
  }
34
34
 
35
35
  const searchServicesSchema = {
36
- type: 'object',
36
+ type: "object",
37
37
  properties: {
38
38
  query: {
39
- type: 'string',
39
+ type: "string",
40
40
  description:
41
41
  'Natural language description of what you want to do (e.g., "get weather forecast", "list github repos")',
42
42
  },
43
43
  namespace: {
44
- type: 'string',
44
+ type: "string",
45
45
  description:
46
46
  'Filter results to a specific service namespace (e.g., "weather", "github")',
47
47
  },
48
48
  tool_name: {
49
- type: 'string',
50
- description: 'Get detailed info about a specific tool by name',
49
+ type: "string",
50
+ description: "Get detailed info about a specific tool by name",
51
51
  },
52
52
  limit: {
53
- type: 'number',
54
- description: 'Maximum number of results to return',
53
+ type: "number",
54
+ description: "Maximum number of results to return",
55
55
  default: 10,
56
56
  },
57
57
  include_interfaces: {
58
- type: 'boolean',
59
- description: 'Include TypeScript interface definitions in results',
58
+ type: "boolean",
59
+ description: "Include TypeScript interface definitions in results",
60
60
  default: true,
61
61
  },
62
62
  },
@@ -113,18 +113,18 @@ Returns matching services with their TypeScript interfaces. Use when:
113
113
  };
114
114
  }
115
115
 
116
- function parseBody<T>(req: import('node:http').IncomingMessage): Promise<T> {
116
+ function parseBody<T>(req: import("node:http").IncomingMessage): Promise<T> {
117
117
  return new Promise((resolve, reject) => {
118
- let body = '';
119
- req.on('data', (chunk) => (body += chunk));
120
- req.on('end', () => {
118
+ let body = "";
119
+ req.on("data", (chunk) => (body += chunk));
120
+ req.on("end", () => {
121
121
  try {
122
122
  resolve(JSON.parse(body));
123
123
  } catch (err) {
124
124
  reject(err);
125
125
  }
126
126
  });
127
- req.on('error', reject);
127
+ req.on("error", reject);
128
128
  });
129
129
  }
130
130
 
@@ -133,8 +133,8 @@ export async function createStitcheryServer(
133
133
  ): Promise<StitcheryServer> {
134
134
  const {
135
135
  port = 6434,
136
- host = '127.0.0.1',
137
- copilotProxyUrl = 'http://127.0.0.1:6433/v1',
136
+ host = "127.0.0.1",
137
+ copilotProxyUrl = "http://127.0.0.1:6433/v1",
138
138
  localPackages = {},
139
139
  mcpServers = [],
140
140
  utcp,
@@ -144,20 +144,20 @@ export async function createStitcheryServer(
144
144
  } = config;
145
145
 
146
146
  const log = verbose
147
- ? (...args: unknown[]) => console.log('[stitchery]', ...args)
147
+ ? (...args: unknown[]) => console.log("[stitchery]", ...args)
148
148
  : () => {};
149
149
 
150
150
  // Create service registry
151
151
  const registry = new ServiceRegistry();
152
152
 
153
- log('Initializing MCP tools...');
153
+ log("Initializing MCP tools...");
154
154
  await initMcpTools(mcpServers, registry);
155
155
  log(`Loaded ${registry.size} tools from ${mcpServers.length} MCP servers`);
156
156
 
157
157
  // Initialize UTCP backend if config provided
158
158
  if (utcp) {
159
- log('Initializing UTCP backend...');
160
- log('UTCP config:', JSON.stringify(utcp, null, 2));
159
+ log("Initializing UTCP backend...");
160
+ log("UTCP config:", JSON.stringify(utcp, null, 2));
161
161
  try {
162
162
  // Cast to unknown since createUtcpBackend uses UtcpClientConfigSerializer to validate
163
163
  const { backend, toolInfos } = await createUtcpBackend(
@@ -167,14 +167,14 @@ export async function createStitcheryServer(
167
167
  registry.registerBackend(backend, toolInfos);
168
168
  log(
169
169
  `Registered UTCP backend with ${toolInfos.length} tools:`,
170
- toolInfos.map((t) => t.name).join(', '),
170
+ toolInfos.map((tool: { name: string }) => tool.name).join(", "),
171
171
  );
172
172
  } catch (err) {
173
- console.error('[stitchery] Failed to initialize UTCP backend:', err);
173
+ console.error("[stitchery] Failed to initialize UTCP backend:", err);
174
174
  }
175
175
  }
176
176
 
177
- log('Local packages:', localPackages);
177
+ log("Local packages:", localPackages);
178
178
 
179
179
  // Create internal tools (search_services, etc.)
180
180
  const internalTools = {
@@ -199,23 +199,23 @@ export async function createStitcheryServer(
199
199
  : null;
200
200
 
201
201
  const server = createServer(async (req, res) => {
202
- res.setHeader('Access-Control-Allow-Origin', '*');
202
+ res.setHeader("Access-Control-Allow-Origin", "*");
203
203
  res.setHeader(
204
- 'Access-Control-Allow-Methods',
205
- 'GET, POST, PUT, DELETE, HEAD, OPTIONS',
204
+ "Access-Control-Allow-Methods",
205
+ "GET, POST, PUT, DELETE, HEAD, OPTIONS",
206
206
  );
207
207
  res.setHeader(
208
- 'Access-Control-Allow-Headers',
209
- 'Content-Type, Authorization',
208
+ "Access-Control-Allow-Headers",
209
+ "Content-Type, Authorization",
210
210
  );
211
211
 
212
- if (req.method === 'OPTIONS') {
212
+ if (req.method === "OPTIONS") {
213
213
  res.writeHead(204);
214
214
  res.end();
215
215
  return;
216
216
  }
217
217
 
218
- const url = req.url || '/';
218
+ const url = req.url || "/";
219
219
  log(`${req.method} ${url}`);
220
220
 
221
221
  try {
@@ -227,19 +227,19 @@ export async function createStitcheryServer(
227
227
  return;
228
228
  }
229
229
 
230
- if (url === '/api/chat' && req.method === 'POST') {
230
+ if (url === "/api/chat" && req.method === "POST") {
231
231
  await handleChat(req, res, routeCtx);
232
232
  return;
233
233
  }
234
234
 
235
- if (url === '/api/edit' && req.method === 'POST') {
235
+ if (url === "/api/edit" && req.method === "POST") {
236
236
  await handleEdit(req, res, routeCtx);
237
237
  return;
238
238
  }
239
239
 
240
240
  // Service proxy endpoint for widgets
241
241
  const proxyMatch = url.match(/^\/api\/proxy\/([^/]+)\/(.+)$/);
242
- if (proxyMatch && req.method === 'POST') {
242
+ if (proxyMatch && req.method === "POST") {
243
243
  const [, namespace, procedure] = proxyMatch;
244
244
  try {
245
245
  const body = await parseBody<{ args?: unknown }>(req);
@@ -248,16 +248,16 @@ export async function createStitcheryServer(
248
248
  procedure!,
249
249
  body.args ?? {},
250
250
  );
251
- res.setHeader('Content-Type', 'application/json');
251
+ res.setHeader("Content-Type", "application/json");
252
252
  res.writeHead(200);
253
253
  res.end(JSON.stringify(result));
254
254
  } catch (err) {
255
- log('Proxy error:', err);
256
- res.setHeader('Content-Type', 'application/json');
255
+ log("Proxy error:", err);
256
+ res.setHeader("Content-Type", "application/json");
257
257
  res.writeHead(500);
258
258
  res.end(
259
259
  JSON.stringify({
260
- error: err instanceof Error ? err.message : 'Service call failed',
260
+ error: err instanceof Error ? err.message : "Service call failed",
261
261
  }),
262
262
  );
263
263
  }
@@ -265,7 +265,7 @@ export async function createStitcheryServer(
265
265
  }
266
266
 
267
267
  // Services search endpoint (POST with body for complex queries)
268
- if (url === '/api/services/search' && req.method === 'POST') {
268
+ if (url === "/api/services/search" && req.method === "POST") {
269
269
  const body = await parseBody<{
270
270
  query?: string;
271
271
  namespace?: string;
@@ -274,7 +274,7 @@ export async function createStitcheryServer(
274
274
  include_interfaces?: boolean;
275
275
  }>(req);
276
276
 
277
- res.setHeader('Content-Type', 'application/json');
277
+ res.setHeader("Content-Type", "application/json");
278
278
  res.writeHead(200);
279
279
 
280
280
  if (body.tool_name) {
@@ -311,8 +311,8 @@ export async function createStitcheryServer(
311
311
  }
312
312
 
313
313
  // Services metadata endpoint
314
- if (url === '/api/services' && req.method === 'GET') {
315
- res.setHeader('Content-Type', 'application/json');
314
+ if (url === "/api/services" && req.method === "GET") {
315
+ res.setHeader("Content-Type", "application/json");
316
316
  res.writeHead(200);
317
317
  res.end(
318
318
  JSON.stringify({
@@ -323,19 +323,19 @@ export async function createStitcheryServer(
323
323
  return;
324
324
  }
325
325
 
326
- if (url === '/health' || url === '/') {
327
- res.setHeader('Content-Type', 'application/json');
326
+ if (url === "/health" || url === "/") {
327
+ res.setHeader("Content-Type", "application/json");
328
328
  res.writeHead(200);
329
- res.end(JSON.stringify({ status: 'ok', service: 'stitchery' }));
329
+ res.end(JSON.stringify({ status: "ok", service: "stitchery" }));
330
330
  return;
331
331
  }
332
332
 
333
333
  res.writeHead(404);
334
334
  res.end(`Not found: ${url}`);
335
335
  } catch (err) {
336
- log('Error:', err);
336
+ log("Error:", err);
337
337
  res.writeHead(500);
338
- res.end(err instanceof Error ? err.message : 'Internal server error');
338
+ res.end(err instanceof Error ? err.message : "Internal server error");
339
339
  }
340
340
  });
341
341
 
@@ -345,7 +345,7 @@ export async function createStitcheryServer(
345
345
 
346
346
  async start() {
347
347
  return new Promise((resolve, reject) => {
348
- server.on('error', reject);
348
+ server.on("error", reject);
349
349
  server.listen(port, host, () => {
350
350
  log(`Server listening on http://${host}:${port}`);
351
351
  resolve({ port, host });