@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.
- package/.github/workflows/publish.yml +1 -1
- package/.vscode/launch.json +19 -0
- package/README.md +24 -0
- package/apps/chat/package.json +4 -4
- package/apps/chat/vite.config.ts +8 -8
- package/docs/specs/directory-sync.md +822 -0
- package/docs/specs/patchwork-vscode.md +625 -0
- package/package.json +2 -2
- package/packages/compiler/package.json +3 -2
- package/packages/compiler/src/index.ts +13 -14
- package/packages/compiler/src/vfs/backends/http.ts +139 -0
- package/packages/compiler/src/vfs/backends/indexeddb.ts +185 -24
- package/packages/compiler/src/vfs/backends/memory.ts +166 -0
- package/packages/compiler/src/vfs/core/index.ts +26 -0
- package/packages/compiler/src/vfs/core/types.ts +93 -0
- package/packages/compiler/src/vfs/core/utils.ts +42 -0
- package/packages/compiler/src/vfs/core/virtual-fs.ts +120 -0
- package/packages/compiler/src/vfs/index.ts +37 -5
- package/packages/compiler/src/vfs/project.ts +16 -16
- package/packages/compiler/src/vfs/store.ts +183 -19
- package/packages/compiler/src/vfs/sync/differ.ts +47 -0
- package/packages/compiler/src/vfs/sync/engine.ts +398 -0
- package/packages/compiler/src/vfs/sync/index.ts +3 -0
- package/packages/compiler/src/vfs/sync/resolver.ts +46 -0
- package/packages/compiler/src/vfs/types.ts +1 -8
- package/packages/compiler/tsup.config.ts +5 -5
- package/packages/editor/package.json +1 -1
- package/packages/editor/src/components/CodeBlockExtension.tsx +1 -1
- package/packages/editor/src/components/CodePreview.tsx +59 -1
- package/packages/editor/src/components/edit/CodeBlockView.tsx +72 -0
- package/packages/editor/src/components/edit/EditModal.tsx +169 -28
- package/packages/editor/src/components/edit/FileTree.tsx +67 -13
- package/packages/editor/src/components/edit/MediaPreview.tsx +106 -0
- package/packages/editor/src/components/edit/SaveConfirmDialog.tsx +60 -0
- package/packages/editor/src/components/edit/fileTypes.ts +125 -0
- package/packages/editor/src/components/edit/index.ts +4 -0
- package/packages/editor/src/components/edit/types.ts +3 -0
- package/packages/editor/src/components/edit/useEditSession.ts +22 -4
- package/packages/editor/src/index.ts +17 -0
- package/packages/editor/src/lib/diff.ts +2 -1
- package/packages/editor/src/lib/vfs.ts +28 -10
- package/packages/editor/tsup.config.ts +10 -5
- package/packages/stitchery/package.json +5 -3
- package/packages/stitchery/src/server/index.ts +57 -57
- package/packages/stitchery/src/server/vfs-routes.ts +246 -56
- package/packages/stitchery/tsup.config.ts +5 -5
- package/packages/utcp/package.json +3 -2
- package/packages/utcp/tsconfig.json +6 -2
- package/packages/utcp/tsup.config.ts +6 -6
- package/packages/vscode/README.md +31 -0
- package/packages/vscode/media/outline.png +0 -0
- package/packages/vscode/media/outline.svg +70 -0
- package/packages/vscode/media/patchwork.png +0 -0
- package/packages/vscode/media/patchwork.svg +72 -0
- package/packages/vscode/node_modules/.bin/jiti +17 -0
- package/packages/vscode/node_modules/.bin/tsc +17 -0
- package/packages/vscode/node_modules/.bin/tsserver +17 -0
- package/packages/vscode/node_modules/.bin/tsup +17 -0
- package/packages/vscode/node_modules/.bin/tsup-node +17 -0
- package/packages/vscode/node_modules/.bin/tsx +17 -0
- package/packages/vscode/package.json +136 -0
- package/packages/vscode/src/extension.ts +612 -0
- package/packages/vscode/src/providers/PatchworkFileSystemProvider.ts +205 -0
- package/packages/vscode/src/providers/PatchworkTreeProvider.ts +177 -0
- package/packages/vscode/src/providers/PreviewPanelProvider.ts +536 -0
- package/packages/vscode/src/services/EditService.ts +24 -0
- package/packages/vscode/src/services/EmbeddedStitchery.ts +82 -0
- package/packages/vscode/tsconfig.json +13 -0
- package/packages/vscode/tsup.config.ts +11 -0
- package/packages/compiler/src/vfs/backends/local-fs.ts +0 -41
- 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
|
+
}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
44
|
+
* Store uses HttpBackend to persist to the stitchery server.
|
|
45
45
|
*/
|
|
46
46
|
export function getVFSStore(): VFSStore {
|
|
47
47
|
if (!storeInstance) {
|
|
48
|
-
const
|
|
49
|
-
storeInstance = new VFSStore(
|
|
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(
|
|
94
|
+
export async function saveFile(path: string, content: string): Promise<void> {
|
|
92
95
|
const store = getVFSStore();
|
|
93
|
-
await store.
|
|
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
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
|
-
entry: [
|
|
5
|
-
format: [
|
|
6
|
-
dts:
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["esm"],
|
|
6
|
+
dts: false,
|
|
7
7
|
clean: true,
|
|
8
|
-
external: [
|
|
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.
|
|
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
|
|
2
|
-
import { createMCPClient } from
|
|
3
|
-
import { Experimental_StdioMCPTransport } from
|
|
4
|
-
import { createUtcpBackend } from
|
|
5
|
-
import { jsonSchema, type Tool } from
|
|
6
|
-
import type { ServerConfig, McpServerConfig } from
|
|
7
|
-
import { handleChat, handleEdit, type RouteContext } from
|
|
8
|
-
import { handleLocalPackages } from
|
|
9
|
-
import { handleVFS, type VFSContext } from
|
|
10
|
-
import { ServiceRegistry, generateServicesPrompt } from
|
|
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:
|
|
36
|
+
type: "object",
|
|
37
37
|
properties: {
|
|
38
38
|
query: {
|
|
39
|
-
type:
|
|
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:
|
|
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:
|
|
50
|
-
description:
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Get detailed info about a specific tool by name",
|
|
51
51
|
},
|
|
52
52
|
limit: {
|
|
53
|
-
type:
|
|
54
|
-
description:
|
|
53
|
+
type: "number",
|
|
54
|
+
description: "Maximum number of results to return",
|
|
55
55
|
default: 10,
|
|
56
56
|
},
|
|
57
57
|
include_interfaces: {
|
|
58
|
-
type:
|
|
59
|
-
description:
|
|
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(
|
|
116
|
+
function parseBody<T>(req: import("node:http").IncomingMessage): Promise<T> {
|
|
117
117
|
return new Promise((resolve, reject) => {
|
|
118
|
-
let body =
|
|
119
|
-
req.on(
|
|
120
|
-
req.on(
|
|
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(
|
|
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 =
|
|
137
|
-
copilotProxyUrl =
|
|
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(
|
|
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(
|
|
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(
|
|
160
|
-
log(
|
|
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((
|
|
170
|
+
toolInfos.map((tool: { name: string }) => tool.name).join(", "),
|
|
171
171
|
);
|
|
172
172
|
} catch (err) {
|
|
173
|
-
console.error(
|
|
173
|
+
console.error("[stitchery] Failed to initialize UTCP backend:", err);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
log(
|
|
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(
|
|
202
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
203
203
|
res.setHeader(
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
"Access-Control-Allow-Methods",
|
|
205
|
+
"GET, POST, PUT, DELETE, HEAD, OPTIONS",
|
|
206
206
|
);
|
|
207
207
|
res.setHeader(
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
"Access-Control-Allow-Headers",
|
|
209
|
+
"Content-Type, Authorization",
|
|
210
210
|
);
|
|
211
211
|
|
|
212
|
-
if (req.method ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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(
|
|
256
|
-
res.setHeader(
|
|
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 :
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
315
|
-
res.setHeader(
|
|
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 ===
|
|
327
|
-
res.setHeader(
|
|
326
|
+
if (url === "/health" || url === "/") {
|
|
327
|
+
res.setHeader("Content-Type", "application/json");
|
|
328
328
|
res.writeHead(200);
|
|
329
|
-
res.end(JSON.stringify({ status:
|
|
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(
|
|
336
|
+
log("Error:", err);
|
|
337
337
|
res.writeHead(500);
|
|
338
|
-
res.end(err instanceof Error ? err.message :
|
|
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(
|
|
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 });
|