@aprovan/stitchery 0.1.0-dev.03aaf5b
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 +36 -0
- package/LICENSE +373 -0
- package/dist/cli.cjs +1400 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1389 -0
- package/dist/cli.js.map +1 -0
- package/dist/index-DNQY1UAP.d.cts +195 -0
- package/dist/index-DNQY1UAP.d.ts +195 -0
- package/dist/index.cjs +1337 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1308 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.cjs +1327 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +3 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +1304 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +42 -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 +332 -0
- package/src/types.ts +59 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export interface LocalPackagesContext {
|
|
6
|
+
localPackages: Record<string, string>;
|
|
7
|
+
log: (...args: unknown[]) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function handleLocalPackages(
|
|
11
|
+
req: IncomingMessage,
|
|
12
|
+
res: ServerResponse,
|
|
13
|
+
ctx: LocalPackagesContext,
|
|
14
|
+
): boolean {
|
|
15
|
+
const rawUrl = req.url || '';
|
|
16
|
+
|
|
17
|
+
// Only handle /_local-packages routes
|
|
18
|
+
if (!rawUrl.startsWith('/_local-packages')) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const urlWithoutPrefix = rawUrl.replace('/_local-packages', '');
|
|
23
|
+
|
|
24
|
+
// Strip query string (bundlers add ?import to dynamic imports)
|
|
25
|
+
const url = urlWithoutPrefix.split('?')[0] || '';
|
|
26
|
+
|
|
27
|
+
// Parse the package name from URL (handles scoped packages like @scope/name)
|
|
28
|
+
const match = url.match(/^\/@([^/]+)\/([^/@]+)(.*)$/);
|
|
29
|
+
if (!match) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const [, scope, name, restPath] = match;
|
|
34
|
+
const packageName = `@${scope}/${name}`;
|
|
35
|
+
const localPath = ctx.localPackages[packageName];
|
|
36
|
+
|
|
37
|
+
if (!localPath) {
|
|
38
|
+
res.writeHead(404);
|
|
39
|
+
res.end(`Package ${packageName} not found in local overrides`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Determine what file to serve
|
|
44
|
+
const rest = restPath || '';
|
|
45
|
+
let filePath: string;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
if (rest === '/package.json') {
|
|
49
|
+
filePath = path.join(localPath, 'package.json');
|
|
50
|
+
} else if (rest === '' || rest === '/') {
|
|
51
|
+
const pkgJson = JSON.parse(
|
|
52
|
+
fs.readFileSync(path.join(localPath, 'package.json'), 'utf-8'),
|
|
53
|
+
);
|
|
54
|
+
const mainEntry = pkgJson.main || 'dist/index.js';
|
|
55
|
+
filePath = path.join(localPath, mainEntry);
|
|
56
|
+
} else {
|
|
57
|
+
const normalizedPath = rest.startsWith('/') ? rest.slice(1) : rest;
|
|
58
|
+
const distPath = path.join(localPath, 'dist', normalizedPath);
|
|
59
|
+
const rootPath = path.join(localPath, normalizedPath);
|
|
60
|
+
filePath = fs.existsSync(distPath) ? distPath : rootPath;
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
ctx.log('Error resolving file path:', err);
|
|
64
|
+
res.writeHead(500);
|
|
65
|
+
res.end(`Error resolving path for ${packageName}: ${err}`);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
ctx.log(`Serving ${filePath}`);
|
|
71
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
72
|
+
const ext = path.extname(filePath);
|
|
73
|
+
const contentType =
|
|
74
|
+
ext === '.json'
|
|
75
|
+
? 'application/json'
|
|
76
|
+
: ext === '.js'
|
|
77
|
+
? 'application/javascript'
|
|
78
|
+
: ext === '.ts'
|
|
79
|
+
? 'application/typescript'
|
|
80
|
+
: 'text/plain';
|
|
81
|
+
res.setHeader('Content-Type', contentType);
|
|
82
|
+
res.writeHead(200);
|
|
83
|
+
res.end(content);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
ctx.log('Error serving file:', filePath, err);
|
|
86
|
+
res.writeHead(404);
|
|
87
|
+
res.end(`File not found: ${filePath}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
3
|
+
import {
|
|
4
|
+
streamText,
|
|
5
|
+
convertToModelMessages,
|
|
6
|
+
stepCountIs,
|
|
7
|
+
type UIMessage,
|
|
8
|
+
type Tool,
|
|
9
|
+
} from 'ai';
|
|
10
|
+
import { PATCHWORK_PROMPT, EDIT_PROMPT } from '../prompts.js';
|
|
11
|
+
import type { ServiceRegistry } from './services.js';
|
|
12
|
+
|
|
13
|
+
export interface RouteContext {
|
|
14
|
+
copilotProxyUrl: string;
|
|
15
|
+
tools: Record<string, Tool>;
|
|
16
|
+
registry: ServiceRegistry;
|
|
17
|
+
servicesPrompt: string;
|
|
18
|
+
log: (...args: unknown[]) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseBody<T>(req: IncomingMessage): Promise<T> {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
let body = '';
|
|
24
|
+
req.on('data', (chunk) => (body += chunk));
|
|
25
|
+
req.on('end', () => {
|
|
26
|
+
try {
|
|
27
|
+
resolve(JSON.parse(body));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
reject(err);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
req.on('error', reject);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function handleChat(
|
|
37
|
+
req: IncomingMessage,
|
|
38
|
+
res: ServerResponse,
|
|
39
|
+
ctx: RouteContext,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const {
|
|
42
|
+
messages,
|
|
43
|
+
metadata,
|
|
44
|
+
}: {
|
|
45
|
+
messages: UIMessage[];
|
|
46
|
+
metadata?: { patchwork?: { compilers?: string[] } };
|
|
47
|
+
} = await parseBody(req);
|
|
48
|
+
|
|
49
|
+
const normalizedMessages = messages.map((msg) => ({
|
|
50
|
+
...msg,
|
|
51
|
+
parts: msg.parts ?? [{ type: 'text' as const, text: '' }],
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const provider = createOpenAICompatible({
|
|
55
|
+
name: 'copilot-proxy',
|
|
56
|
+
baseURL: ctx.copilotProxyUrl,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = streamText({
|
|
60
|
+
model: provider('claude-sonnet-4'),
|
|
61
|
+
system: `---\npatchwork:\n compilers: ${
|
|
62
|
+
(metadata?.patchwork?.compilers ?? []).join(',') ?? '[]'
|
|
63
|
+
}\n services: ${ctx.registry
|
|
64
|
+
.getNamespaces()
|
|
65
|
+
.join(',')}\n---\n\n${PATCHWORK_PROMPT}\n\n${ctx.servicesPrompt}`,
|
|
66
|
+
messages: await convertToModelMessages(normalizedMessages),
|
|
67
|
+
stopWhen: stepCountIs(5),
|
|
68
|
+
tools: ctx.tools,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const response = result.toUIMessageStreamResponse();
|
|
72
|
+
response.headers.forEach((value: string, key: string) =>
|
|
73
|
+
res.setHeader(key, value),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (!response.body) {
|
|
77
|
+
res.end();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const reader = response.body.getReader();
|
|
82
|
+
const pump = async () => {
|
|
83
|
+
const { done, value } = await reader.read();
|
|
84
|
+
if (done) {
|
|
85
|
+
res.end();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
res.write(value);
|
|
89
|
+
await pump();
|
|
90
|
+
};
|
|
91
|
+
await pump();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function handleEdit(
|
|
95
|
+
req: IncomingMessage,
|
|
96
|
+
res: ServerResponse,
|
|
97
|
+
ctx: RouteContext,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const { code, prompt }: { code: string; prompt: string } = await parseBody(
|
|
100
|
+
req,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const provider = createOpenAICompatible({
|
|
104
|
+
name: 'copilot-proxy',
|
|
105
|
+
baseURL: ctx.copilotProxyUrl,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const result = streamText({
|
|
109
|
+
model: provider('claude-opus-4.5'),
|
|
110
|
+
system: `Current component code:\n\`\`\`tsx\n${code}\n\`\`\`\n\n${EDIT_PROMPT}`,
|
|
111
|
+
messages: [{ role: 'user', content: prompt }],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
115
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
116
|
+
res.writeHead(200);
|
|
117
|
+
|
|
118
|
+
for await (const chunk of result.textStream) {
|
|
119
|
+
res.write(chunk);
|
|
120
|
+
}
|
|
121
|
+
res.end();
|
|
122
|
+
}
|
|
@@ -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
|
+
}
|