@aprovan/stitchery 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +24 -0
- package/LICENSE +373 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1224 -0
- package/dist/cli.js.map +1 -0
- package/dist/index-DNQY1UAP.d.ts +195 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1143 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +1139 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +40 -0
- package/src/cli.ts +116 -0
- package/src/index.ts +16 -0
- package/src/prompts.ts +326 -0
- package/src/server/index.ts +365 -0
- package/src/server/local-packages.ts +91 -0
- package/src/server/routes.ts +122 -0
- package/src/server/services.ts +382 -0
- package/src/server/vfs-routes.ts +142 -0
- package/src/types.ts +59 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Registry - Tracks available services for widget calls
|
|
3
|
+
*
|
|
4
|
+
* Services can be registered from:
|
|
5
|
+
* - MCP servers (via --mcp CLI)
|
|
6
|
+
* - External backends (UTCP, HTTP, etc.)
|
|
7
|
+
*
|
|
8
|
+
* Provides unified interface for calling services and exposing metadata.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { jsonSchema, type Tool } from 'ai';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Service backend interface - abstracts service call mechanisms
|
|
15
|
+
* Backends can be UTCP, HTTP proxies, direct MCP, etc.
|
|
16
|
+
*/
|
|
17
|
+
export interface ServiceBackend {
|
|
18
|
+
call(service: string, procedure: string, args: unknown[]): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Service tool metadata for prompt generation
|
|
23
|
+
*/
|
|
24
|
+
export interface ServiceToolInfo {
|
|
25
|
+
/** Full tool name (e.g., 'weather.get_forecast') */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Namespace (e.g., 'weather') */
|
|
28
|
+
namespace: string;
|
|
29
|
+
/** Procedure name (e.g., 'get_forecast') */
|
|
30
|
+
procedure: string;
|
|
31
|
+
/** Tool description */
|
|
32
|
+
description?: string;
|
|
33
|
+
/** Parameter schema */
|
|
34
|
+
parameters?: Record<string, unknown>;
|
|
35
|
+
/** TypeScript interface definition (optional, for search results) */
|
|
36
|
+
typescriptInterface?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Search options for tool discovery
|
|
41
|
+
*/
|
|
42
|
+
export interface SearchServicesOptions {
|
|
43
|
+
/** Natural language task description to search for */
|
|
44
|
+
query?: string;
|
|
45
|
+
/** Filter by namespace */
|
|
46
|
+
namespace?: string;
|
|
47
|
+
/** Maximum results to return */
|
|
48
|
+
limit?: number;
|
|
49
|
+
/** Include full TypeScript interfaces in results */
|
|
50
|
+
includeInterfaces?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Service registry that tracks available services
|
|
55
|
+
*/
|
|
56
|
+
export class ServiceRegistry {
|
|
57
|
+
private tools: Map<string, Tool> = new Map();
|
|
58
|
+
private toolInfo: Map<string, ServiceToolInfo> = new Map();
|
|
59
|
+
private backends: ServiceBackend[] = [];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Register tools from MCP or other sources
|
|
63
|
+
* @param tools - Record of tool name to Tool
|
|
64
|
+
* @param namespace - Optional namespace to prefix all tools (e.g., MCP server name)
|
|
65
|
+
*/
|
|
66
|
+
registerTools(tools: Record<string, Tool>, namespace?: string): void {
|
|
67
|
+
for (const [toolName, tool] of Object.entries(tools)) {
|
|
68
|
+
// Build the full name: namespace.toolName or just toolName
|
|
69
|
+
const name = namespace ? `${namespace}.${toolName}` : toolName;
|
|
70
|
+
this.tools.set(name, tool);
|
|
71
|
+
|
|
72
|
+
// Parse namespace and procedure from the full name using '.' separator
|
|
73
|
+
const dotIndex = name.indexOf('.');
|
|
74
|
+
const ns = dotIndex > 0 ? name.substring(0, dotIndex) : name;
|
|
75
|
+
const procedure = dotIndex > 0 ? name.substring(dotIndex + 1) : name;
|
|
76
|
+
|
|
77
|
+
this.toolInfo.set(name, {
|
|
78
|
+
name,
|
|
79
|
+
namespace: ns,
|
|
80
|
+
procedure,
|
|
81
|
+
description: tool.description,
|
|
82
|
+
parameters: (tool.inputSchema ?? {}) as Record<string, unknown>,
|
|
83
|
+
typescriptInterface: this.generateTypeScriptInterface(name, tool),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Register a service backend (UTCP, HTTP, etc.)
|
|
90
|
+
* Creates callable Tool objects for each procedure so the LLM can invoke them directly.
|
|
91
|
+
* Backends are tried in order of registration, first success wins.
|
|
92
|
+
*/
|
|
93
|
+
registerBackend(
|
|
94
|
+
backend: ServiceBackend,
|
|
95
|
+
toolInfos?: ServiceToolInfo[],
|
|
96
|
+
): void {
|
|
97
|
+
this.backends.push(backend);
|
|
98
|
+
if (toolInfos) {
|
|
99
|
+
for (const info of toolInfos) {
|
|
100
|
+
this.toolInfo.set(info.name, info);
|
|
101
|
+
|
|
102
|
+
// Create a callable Tool object for LLM use
|
|
103
|
+
const tool: Tool = {
|
|
104
|
+
description: info.description,
|
|
105
|
+
inputSchema: jsonSchema(
|
|
106
|
+
info.parameters ?? { type: 'object', properties: {} },
|
|
107
|
+
),
|
|
108
|
+
execute: async (args: unknown) => {
|
|
109
|
+
return backend.call(info.namespace, info.procedure, [args]);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
this.tools.set(info.name, tool);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate TypeScript interface from tool schema
|
|
119
|
+
*/
|
|
120
|
+
private generateTypeScriptInterface(name: string, tool: Tool): string {
|
|
121
|
+
const schema = tool.inputSchema as Record<string, unknown> | undefined;
|
|
122
|
+
const props = (schema?.properties ?? {}) as Record<
|
|
123
|
+
string,
|
|
124
|
+
{ type?: string; description?: string }
|
|
125
|
+
>;
|
|
126
|
+
const required = (schema?.required ?? []) as string[];
|
|
127
|
+
|
|
128
|
+
const params = Object.entries(props)
|
|
129
|
+
.map(([key, val]) => {
|
|
130
|
+
const optional = !required.includes(key) ? '?' : '';
|
|
131
|
+
const type =
|
|
132
|
+
val.type === 'number'
|
|
133
|
+
? 'number'
|
|
134
|
+
: val.type === 'boolean'
|
|
135
|
+
? 'boolean'
|
|
136
|
+
: val.type === 'array'
|
|
137
|
+
? 'unknown[]'
|
|
138
|
+
: val.type === 'object'
|
|
139
|
+
? 'Record<string, unknown>'
|
|
140
|
+
: 'string';
|
|
141
|
+
const comment = val.description ? ` // ${val.description}` : '';
|
|
142
|
+
return ` ${key}${optional}: ${type};${comment}`;
|
|
143
|
+
})
|
|
144
|
+
.join('\n');
|
|
145
|
+
|
|
146
|
+
return `interface ${name.replace(
|
|
147
|
+
/[^a-zA-Z0-9]/g,
|
|
148
|
+
'_',
|
|
149
|
+
)}Args {\n${params}\n}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Convert internal tool name (namespace.procedure) to LLM-safe name (namespace_procedure)
|
|
154
|
+
* OpenAI-compatible APIs require tool names to match ^[a-zA-Z0-9_-]+$
|
|
155
|
+
*/
|
|
156
|
+
private toLLMToolName(internalName: string): string {
|
|
157
|
+
return internalName.replace(/\./g, '_');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Convert LLM tool name (namespace_procedure) back to internal name (namespace.procedure)
|
|
162
|
+
* Only converts the first underscore after the namespace prefix
|
|
163
|
+
*/
|
|
164
|
+
private fromLLMToolName(llmName: string): string {
|
|
165
|
+
// Find the tool by checking if any registered tool converts to this LLM name
|
|
166
|
+
for (const internalName of this.tools.keys()) {
|
|
167
|
+
if (this.toLLMToolName(internalName) === llmName) {
|
|
168
|
+
return internalName;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Fallback: convert first underscore to dot
|
|
172
|
+
const underscoreIndex = llmName.indexOf('_');
|
|
173
|
+
if (underscoreIndex > 0) {
|
|
174
|
+
return (
|
|
175
|
+
llmName.substring(0, underscoreIndex) +
|
|
176
|
+
'.' +
|
|
177
|
+
llmName.substring(underscoreIndex + 1)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return llmName;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get all tools for LLM usage with LLM-safe names (underscores instead of dots)
|
|
185
|
+
*/
|
|
186
|
+
getTools(): Record<string, Tool> {
|
|
187
|
+
const result: Record<string, Tool> = {};
|
|
188
|
+
for (const [name, tool] of this.tools) {
|
|
189
|
+
result[this.toLLMToolName(name)] = tool;
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get service metadata for prompt generation
|
|
196
|
+
*/
|
|
197
|
+
getServiceInfo(): ServiceToolInfo[] {
|
|
198
|
+
return Array.from(this.toolInfo.values());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get unique namespaces
|
|
203
|
+
*/
|
|
204
|
+
getNamespaces(): string[] {
|
|
205
|
+
const namespaces = new Set<string>();
|
|
206
|
+
for (const info of this.toolInfo.values()) {
|
|
207
|
+
namespaces.add(info.namespace);
|
|
208
|
+
}
|
|
209
|
+
return Array.from(namespaces);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Search for services by query, namespace, or list all
|
|
214
|
+
*/
|
|
215
|
+
searchServices(options: SearchServicesOptions = {}): ServiceToolInfo[] {
|
|
216
|
+
const { query, namespace, limit = 20, includeInterfaces = false } = options;
|
|
217
|
+
|
|
218
|
+
let results = Array.from(this.toolInfo.values());
|
|
219
|
+
|
|
220
|
+
// Filter by namespace
|
|
221
|
+
if (namespace) {
|
|
222
|
+
results = results.filter((info) => info.namespace === namespace);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Search by query (simple keyword matching)
|
|
226
|
+
if (query) {
|
|
227
|
+
const queryLower = query.toLowerCase();
|
|
228
|
+
const keywords = queryLower.split(/\s+/).filter(Boolean);
|
|
229
|
+
|
|
230
|
+
results = results
|
|
231
|
+
.map((info) => {
|
|
232
|
+
const searchText = `${info.name} ${info.namespace} ${
|
|
233
|
+
info.procedure
|
|
234
|
+
} ${info.description ?? ''}`.toLowerCase();
|
|
235
|
+
const matchCount = keywords.filter((kw) =>
|
|
236
|
+
searchText.includes(kw),
|
|
237
|
+
).length;
|
|
238
|
+
return { info, score: matchCount / keywords.length };
|
|
239
|
+
})
|
|
240
|
+
.filter(({ score }) => score > 0)
|
|
241
|
+
.sort((a, b) => b.score - a.score)
|
|
242
|
+
.map(({ info }) => info);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Apply limit
|
|
246
|
+
results = results.slice(0, limit);
|
|
247
|
+
|
|
248
|
+
// Optionally include TypeScript interfaces
|
|
249
|
+
if (!includeInterfaces) {
|
|
250
|
+
results = results.map(({ typescriptInterface: _, ...rest }) => rest);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get detailed info about a specific tool
|
|
258
|
+
*/
|
|
259
|
+
getToolInfo(toolName: string): ServiceToolInfo | undefined {
|
|
260
|
+
return this.toolInfo.get(toolName);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* List all tool names
|
|
265
|
+
*/
|
|
266
|
+
listToolNames(): string[] {
|
|
267
|
+
return Array.from(this.toolInfo.keys());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Call a service procedure
|
|
272
|
+
*/
|
|
273
|
+
async call(
|
|
274
|
+
namespace: string,
|
|
275
|
+
procedure: string,
|
|
276
|
+
args: unknown,
|
|
277
|
+
): Promise<unknown> {
|
|
278
|
+
// Try registered backends first (in order)
|
|
279
|
+
for (const backend of this.backends) {
|
|
280
|
+
try {
|
|
281
|
+
return await backend.call(namespace, procedure, [args]);
|
|
282
|
+
} catch {
|
|
283
|
+
// Try next backend
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Fall back to MCP tools - use dot separator
|
|
288
|
+
const exactKey = `${namespace}.${procedure}`;
|
|
289
|
+
let tool = this.tools.get(exactKey);
|
|
290
|
+
|
|
291
|
+
if (!tool) {
|
|
292
|
+
// Try finding by namespace prefix match
|
|
293
|
+
for (const [name, t] of this.tools) {
|
|
294
|
+
if (name.startsWith(`${namespace}.`) && name.endsWith(procedure)) {
|
|
295
|
+
tool = t;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!tool) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Service not found: ${namespace}.${procedure}. Available: ${Array.from(
|
|
304
|
+
this.tools.keys(),
|
|
305
|
+
)
|
|
306
|
+
.slice(0, 10)
|
|
307
|
+
.join(', ')}`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Execute the tool
|
|
312
|
+
if (!tool.execute) {
|
|
313
|
+
throw new Error(`Tool ${namespace}.${procedure} has no execute function`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const result = await tool.execute(args as Record<string, unknown>, {
|
|
317
|
+
toolCallId: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
318
|
+
messages: [],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Check if a service exists
|
|
326
|
+
*/
|
|
327
|
+
has(namespace: string, procedure: string): boolean {
|
|
328
|
+
const key = `${namespace}.${procedure}`;
|
|
329
|
+
return this.tools.has(key);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get count of registered tools
|
|
334
|
+
*/
|
|
335
|
+
get size(): number {
|
|
336
|
+
return this.toolInfo.size;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Generate a services description for prompts
|
|
342
|
+
*/
|
|
343
|
+
export function generateServicesPrompt(registry: ServiceRegistry): string {
|
|
344
|
+
const namespaces = registry.getNamespaces();
|
|
345
|
+
if (namespaces.length === 0) return '';
|
|
346
|
+
|
|
347
|
+
const services = registry.getServiceInfo();
|
|
348
|
+
const byNamespace = new Map<string, ServiceToolInfo[]>();
|
|
349
|
+
|
|
350
|
+
for (const service of services) {
|
|
351
|
+
const existing = byNamespace.get(service.namespace) || [];
|
|
352
|
+
existing.push(service);
|
|
353
|
+
byNamespace.set(service.namespace, existing);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let prompt = `## Available Services\n\nThe following services are available for generated widgets to call:\n\n`;
|
|
357
|
+
|
|
358
|
+
for (const [ns, tools] of byNamespace) {
|
|
359
|
+
prompt += `### \`${ns}\`\n`;
|
|
360
|
+
for (const tool of tools) {
|
|
361
|
+
prompt += `- \`${ns}.${tool.procedure}()\``;
|
|
362
|
+
if (tool.description) {
|
|
363
|
+
prompt += `: ${tool.description}`;
|
|
364
|
+
}
|
|
365
|
+
prompt += '\n';
|
|
366
|
+
}
|
|
367
|
+
prompt += '\n';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
prompt += `**Usage in widgets:**
|
|
371
|
+
\`\`\`tsx
|
|
372
|
+
// Services are available as global namespaces
|
|
373
|
+
const result = await ${namespaces[0] ?? 'service'}.${
|
|
374
|
+
byNamespace.get(namespaces[0] ?? '')?.[0]?.procedure ?? 'example'
|
|
375
|
+
}({ /* args */ });
|
|
376
|
+
\`\`\`
|
|
377
|
+
|
|
378
|
+
Make sure to handle loading states and errors when calling services.
|
|
379
|
+
`;
|
|
380
|
+
|
|
381
|
+
return prompt;
|
|
382
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFile,
|
|
3
|
+
writeFile,
|
|
4
|
+
unlink,
|
|
5
|
+
readdir,
|
|
6
|
+
stat,
|
|
7
|
+
mkdir,
|
|
8
|
+
} from 'node:fs/promises';
|
|
9
|
+
import { join, relative, dirname } from 'node:path';
|
|
10
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
11
|
+
|
|
12
|
+
export interface VFSContext {
|
|
13
|
+
rootDir: string;
|
|
14
|
+
usePaths: boolean;
|
|
15
|
+
log: (...args: unknown[]) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function listFilesRecursive(dir: string): Promise<string[]> {
|
|
19
|
+
try {
|
|
20
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
21
|
+
const files: string[] = [];
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const full = join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
files.push(...(await listFilesRecursive(full)));
|
|
26
|
+
} else {
|
|
27
|
+
files.push(full);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return files;
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function ensureDir(filePath: string): Promise<void> {
|
|
37
|
+
const dir = dirname(filePath);
|
|
38
|
+
await mkdir(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function handleVFS(
|
|
42
|
+
req: IncomingMessage,
|
|
43
|
+
res: ServerResponse,
|
|
44
|
+
ctx: VFSContext,
|
|
45
|
+
): boolean {
|
|
46
|
+
const url = req.url || '/';
|
|
47
|
+
const method = req.method || 'GET';
|
|
48
|
+
|
|
49
|
+
if (!url.startsWith('/vfs')) return false;
|
|
50
|
+
|
|
51
|
+
// Handle config endpoint
|
|
52
|
+
if (url === '/vfs/config' && method === 'GET') {
|
|
53
|
+
res.setHeader('Content-Type', 'application/json');
|
|
54
|
+
res.writeHead(200);
|
|
55
|
+
res.end(JSON.stringify({ usePaths: ctx.usePaths }));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handleRequest = async () => {
|
|
60
|
+
const path = url.slice(4).split('?')[0] || '';
|
|
61
|
+
const query = new URL(url, 'http://localhost').searchParams;
|
|
62
|
+
|
|
63
|
+
if (path === '' || path === '/') {
|
|
64
|
+
if (method === 'GET') {
|
|
65
|
+
const prefix = query.get('prefix') || '';
|
|
66
|
+
const files = await listFilesRecursive(join(ctx.rootDir, prefix));
|
|
67
|
+
const relativePaths = files.map((f) => relative(ctx.rootDir, f));
|
|
68
|
+
res.setHeader('Content-Type', 'application/json');
|
|
69
|
+
res.writeHead(200);
|
|
70
|
+
res.end(JSON.stringify(relativePaths));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const filePath = join(ctx.rootDir, path.slice(1));
|
|
76
|
+
|
|
77
|
+
switch (method) {
|
|
78
|
+
case 'GET': {
|
|
79
|
+
try {
|
|
80
|
+
const content = await readFile(filePath, 'utf-8');
|
|
81
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
82
|
+
res.writeHead(200);
|
|
83
|
+
res.end(content);
|
|
84
|
+
} catch {
|
|
85
|
+
res.writeHead(404);
|
|
86
|
+
res.end('Not found');
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case 'PUT': {
|
|
91
|
+
let body = '';
|
|
92
|
+
req.on('data', (chunk) => (body += chunk));
|
|
93
|
+
req.on('end', async () => {
|
|
94
|
+
try {
|
|
95
|
+
await ensureDir(filePath);
|
|
96
|
+
await writeFile(filePath, body, 'utf-8');
|
|
97
|
+
res.writeHead(200);
|
|
98
|
+
res.end('ok');
|
|
99
|
+
} catch (err) {
|
|
100
|
+
ctx.log('VFS PUT error:', err);
|
|
101
|
+
res.writeHead(500);
|
|
102
|
+
res.end('Write failed');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
case 'DELETE': {
|
|
108
|
+
try {
|
|
109
|
+
await unlink(filePath);
|
|
110
|
+
res.writeHead(200);
|
|
111
|
+
res.end('ok');
|
|
112
|
+
} catch {
|
|
113
|
+
res.writeHead(404);
|
|
114
|
+
res.end('Not found');
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
case 'HEAD': {
|
|
119
|
+
try {
|
|
120
|
+
await stat(filePath);
|
|
121
|
+
res.writeHead(200);
|
|
122
|
+
res.end();
|
|
123
|
+
} catch {
|
|
124
|
+
res.writeHead(404);
|
|
125
|
+
res.end();
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
default:
|
|
130
|
+
res.writeHead(405);
|
|
131
|
+
res.end('Method not allowed');
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
handleRequest().catch((err) => {
|
|
136
|
+
ctx.log('VFS error:', err);
|
|
137
|
+
res.writeHead(500);
|
|
138
|
+
res.end('Internal error');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return true;
|
|
142
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTCP service configuration
|
|
3
|
+
* Used to register services via UTCP protocol
|
|
4
|
+
*/
|
|
5
|
+
export interface UtcpConfig {
|
|
6
|
+
/** Working directory for UTCP operations */
|
|
7
|
+
cwd?: string;
|
|
8
|
+
/** Manual call templates (service definitions) */
|
|
9
|
+
manual_call_templates?: Array<{
|
|
10
|
+
name: string;
|
|
11
|
+
call_template_type: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
http_method?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}>;
|
|
16
|
+
/** Additional UTCP options */
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ServerConfig {
|
|
21
|
+
port: number;
|
|
22
|
+
host: string;
|
|
23
|
+
copilotProxyUrl: string;
|
|
24
|
+
localPackages: Record<string, string>;
|
|
25
|
+
mcpServers: McpServerConfig[];
|
|
26
|
+
/** UTCP configuration for registering services */
|
|
27
|
+
utcp?: UtcpConfig;
|
|
28
|
+
/** Directory for virtual file system storage */
|
|
29
|
+
vfsDir?: string;
|
|
30
|
+
/** Use file paths from code blocks instead of UUIDs */
|
|
31
|
+
vfsUsePaths?: boolean;
|
|
32
|
+
verbose: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface McpServerConfig {
|
|
36
|
+
name: string;
|
|
37
|
+
command: string;
|
|
38
|
+
args: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ChatRequest {
|
|
42
|
+
messages: UIMessage[];
|
|
43
|
+
metadata?: {
|
|
44
|
+
patchwork?: {
|
|
45
|
+
compilers?: string[];
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface EditRequest {
|
|
51
|
+
code: string;
|
|
52
|
+
prompt: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UIMessage {
|
|
56
|
+
role: string;
|
|
57
|
+
content: string;
|
|
58
|
+
parts?: Array<{ type: string; text: string }>;
|
|
59
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"moduleResolution": "bundler"
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*"],
|
|
12
|
+
"exclude": ["node_modules", "dist"]
|
|
13
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
index: 'src/index.ts',
|
|
6
|
+
cli: 'src/cli.ts',
|
|
7
|
+
'server/index': 'src/server/index.ts',
|
|
8
|
+
},
|
|
9
|
+
format: ['esm'],
|
|
10
|
+
dts: true,
|
|
11
|
+
clean: true,
|
|
12
|
+
sourcemap: true,
|
|
13
|
+
splitting: false,
|
|
14
|
+
shims: true,
|
|
15
|
+
});
|