@aprovan/stitchery 0.1.0-dev.ba8f277 → 0.1.0-dev.c680591

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.
@@ -8,7 +8,7 @@
8
8
  * Provides unified interface for calling services and exposing metadata.
9
9
  */
10
10
 
11
- import { jsonSchema, type Tool } from 'ai';
11
+ import { jsonSchema, type Tool } from "ai";
12
12
 
13
13
  /**
14
14
  * Service backend interface - abstracts service call mechanisms
@@ -30,8 +30,10 @@ export interface ServiceToolInfo {
30
30
  procedure: string;
31
31
  /** Tool description */
32
32
  description?: string;
33
- /** Parameter schema */
33
+ /** Parameter schema (inputs) */
34
34
  parameters?: Record<string, unknown>;
35
+ /** Response schema (outputs) */
36
+ outputs?: Record<string, unknown>;
35
37
  /** TypeScript interface definition (optional, for search results) */
36
38
  typescriptInterface?: string;
37
39
  }
@@ -70,7 +72,7 @@ export class ServiceRegistry {
70
72
  this.tools.set(name, tool);
71
73
 
72
74
  // Parse namespace and procedure from the full name using '.' separator
73
- const dotIndex = name.indexOf('.');
75
+ const dotIndex = name.indexOf(".");
74
76
  const ns = dotIndex > 0 ? name.substring(0, dotIndex) : name;
75
77
  const procedure = dotIndex > 0 ? name.substring(dotIndex + 1) : name;
76
78
 
@@ -97,13 +99,22 @@ export class ServiceRegistry {
97
99
  this.backends.push(backend);
98
100
  if (toolInfos) {
99
101
  for (const info of toolInfos) {
100
- this.toolInfo.set(info.name, info);
102
+ // Generate TypeScript interface including response type if outputs schema provided
103
+ const infoWithInterface: ServiceToolInfo = {
104
+ ...info,
105
+ typescriptInterface: this.generateTypeScriptInterfaceFromSchema(
106
+ info.name,
107
+ info.parameters,
108
+ info.outputs,
109
+ ),
110
+ };
111
+ this.toolInfo.set(info.name, infoWithInterface);
101
112
 
102
113
  // Create a callable Tool object for LLM use
103
114
  const tool: Tool = {
104
115
  description: info.description,
105
116
  inputSchema: jsonSchema(
106
- info.parameters ?? { type: 'object', properties: {} },
117
+ info.parameters ?? { type: "object", properties: {} },
107
118
  ),
108
119
  execute: async (args: unknown) => {
109
120
  return backend.call(info.namespace, info.procedure, [args]);
@@ -119,34 +130,87 @@ export class ServiceRegistry {
119
130
  */
120
131
  private generateTypeScriptInterface(name: string, tool: Tool): string {
121
132
  const schema = tool.inputSchema as Record<string, unknown> | undefined;
122
- const props = (schema?.properties ?? {}) as Record<
133
+ return this.generateTypeScriptInterfaceFromSchema(name, schema);
134
+ }
135
+
136
+ /**
137
+ * Convert JSON Schema type to TypeScript type
138
+ */
139
+ private schemaTypeToTs(schema: Record<string, unknown>): string {
140
+ const type = schema.type as string | undefined;
141
+ const items = schema.items as Record<string, unknown> | undefined;
142
+ const properties = schema.properties as
143
+ | Record<string, Record<string, unknown>>
144
+ | undefined;
145
+ const required = (schema.required ?? []) as string[];
146
+
147
+ if (type === "number" || type === "integer") return "number";
148
+ if (type === "boolean") return "boolean";
149
+ if (type === "null") return "null";
150
+ if (type === "array") {
151
+ if (items) {
152
+ return `${this.schemaTypeToTs(items)}[]`;
153
+ }
154
+ return "unknown[]";
155
+ }
156
+ if (type === "object" && properties) {
157
+ const props = Object.entries(properties)
158
+ .map(([key, val]) => {
159
+ const optional = !required.includes(key) ? "?" : "";
160
+ const propType = this.schemaTypeToTs(val);
161
+ return `${key}${optional}: ${propType}`;
162
+ })
163
+ .join("; ");
164
+ return `{ ${props} }`;
165
+ }
166
+ if (type === "object") return "Record<string, unknown>";
167
+ return "string";
168
+ }
169
+
170
+ /**
171
+ * Generate TypeScript interface from JSON Schema (supports both inputs and outputs)
172
+ */
173
+ private generateTypeScriptInterfaceFromSchema(
174
+ name: string,
175
+ inputSchema?: Record<string, unknown>,
176
+ outputSchema?: Record<string, unknown>,
177
+ ): string {
178
+ const safeName = name.replace(/[^a-zA-Z0-9]/g, "_");
179
+
180
+ // Generate Args interface
181
+ const inputProps = (inputSchema?.properties ?? {}) as Record<
123
182
  string,
124
183
  { type?: string; description?: string }
125
184
  >;
126
- const required = (schema?.required ?? []) as string[];
185
+ const inputRequired = (inputSchema?.required ?? []) as string[];
127
186
 
128
- const params = Object.entries(props)
187
+ const inputParams = Object.entries(inputProps)
129
188
  .map(([key, val]) => {
130
- const optional = !required.includes(key) ? '?' : '';
189
+ const optional = !inputRequired.includes(key) ? "?" : "";
131
190
  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}` : '';
191
+ val.type === "number"
192
+ ? "number"
193
+ : val.type === "boolean"
194
+ ? "boolean"
195
+ : val.type === "array"
196
+ ? "unknown[]"
197
+ : val.type === "object"
198
+ ? "Record<string, unknown>"
199
+ : "string";
200
+ const comment = val.description ? ` // ${val.description}` : "";
142
201
  return ` ${key}${optional}: ${type};${comment}`;
143
202
  })
144
- .join('\n');
203
+ .join("\n");
145
204
 
146
- return `interface ${name.replace(
147
- /[^a-zA-Z0-9]/g,
148
- '_',
149
- )}Args {\n${params}\n}`;
205
+ let result = `interface ${safeName}Args {\n${inputParams}\n}`;
206
+
207
+ // Generate Response interface if outputs schema provided
208
+ if (outputSchema && Object.keys(outputSchema).length > 0) {
209
+ const responseType = this.schemaTypeToTs(outputSchema);
210
+ result += `\n\ntype ${safeName}Response = ${responseType};`;
211
+ }
212
+
213
+ return result;
150
214
  }
151
215
 
152
216
  /**
@@ -154,7 +218,7 @@ export class ServiceRegistry {
154
218
  * OpenAI-compatible APIs require tool names to match ^[a-zA-Z0-9_-]+$
155
219
  */
156
220
  private toLLMToolName(internalName: string): string {
157
- return internalName.replace(/\./g, '_');
221
+ return internalName.replace(/\./g, "_");
158
222
  }
159
223
 
160
224
  /**
@@ -169,11 +233,11 @@ export class ServiceRegistry {
169
233
  }
170
234
  }
171
235
  // Fallback: convert first underscore to dot
172
- const underscoreIndex = llmName.indexOf('_');
236
+ const underscoreIndex = llmName.indexOf("_");
173
237
  if (underscoreIndex > 0) {
174
238
  return (
175
239
  llmName.substring(0, underscoreIndex) +
176
- '.' +
240
+ "." +
177
241
  llmName.substring(underscoreIndex + 1)
178
242
  );
179
243
  }
@@ -231,7 +295,7 @@ export class ServiceRegistry {
231
295
  .map((info) => {
232
296
  const searchText = `${info.name} ${info.namespace} ${
233
297
  info.procedure
234
- } ${info.description ?? ''}`.toLowerCase();
298
+ } ${info.description ?? ""}`.toLowerCase();
235
299
  const matchCount = keywords.filter((kw) =>
236
300
  searchText.includes(kw),
237
301
  ).length;
@@ -304,7 +368,7 @@ export class ServiceRegistry {
304
368
  this.tools.keys(),
305
369
  )
306
370
  .slice(0, 10)
307
- .join(', ')}`,
371
+ .join(", ")}`,
308
372
  );
309
373
  }
310
374
 
@@ -342,7 +406,7 @@ export class ServiceRegistry {
342
406
  */
343
407
  export function generateServicesPrompt(registry: ServiceRegistry): string {
344
408
  const namespaces = registry.getNamespaces();
345
- if (namespaces.length === 0) return '';
409
+ if (namespaces.length === 0) return "";
346
410
 
347
411
  const services = registry.getServiceInfo();
348
412
  const byNamespace = new Map<string, ServiceToolInfo[]>();
@@ -353,7 +417,7 @@ export function generateServicesPrompt(registry: ServiceRegistry): string {
353
417
  byNamespace.set(service.namespace, existing);
354
418
  }
355
419
 
356
- let prompt = `## Available Services\n\nThe following services are available for generated widgets to call:\n\n`;
420
+ let prompt = `## Services\n\nThe following services are available for generated widgets to call:\n\n`;
357
421
 
358
422
  for (const [ns, tools] of byNamespace) {
359
423
  prompt += `### \`${ns}\`\n`;
@@ -362,16 +426,16 @@ export function generateServicesPrompt(registry: ServiceRegistry): string {
362
426
  if (tool.description) {
363
427
  prompt += `: ${tool.description}`;
364
428
  }
365
- prompt += '\n';
429
+ prompt += "\n";
366
430
  }
367
- prompt += '\n';
431
+ prompt += "\n";
368
432
  }
369
433
 
370
434
  prompt += `**Usage in widgets:**
371
435
  \`\`\`tsx
372
436
  // Services are available as global namespaces
373
- const result = await ${namespaces[0] ?? 'service'}.${
374
- byNamespace.get(namespaces[0] ?? '')?.[0]?.procedure ?? 'example'
437
+ const result = await ${namespaces[0] ?? "service"}.${
438
+ byNamespace.get(namespaces[0] ?? "")?.[0]?.procedure ?? "example"
375
439
  }({ /* args */ });
376
440
  \`\`\`
377
441
 
@@ -40,7 +40,7 @@ async function listAllFiles(
40
40
  relPath: string,
41
41
  ): Promise<string[]> {
42
42
  const targetPath = resolvePath(rootDir, relPath);
43
- let entries: Awaited<ReturnType<typeof readdir>> = [];
43
+ let entries;
44
44
  try {
45
45
  entries = await readdir(targetPath, { withFileTypes: true });
46
46
  } catch {
@@ -49,7 +49,7 @@ async function listAllFiles(
49
49
 
50
50
  const results: string[] = [];
51
51
  for (const entry of entries) {
52
- const entryRelPath = joinRelPath(relPath, entry.name);
52
+ const entryRelPath = joinRelPath(relPath, String(entry.name));
53
53
  if (entry.isDirectory()) {
54
54
  results.push(...(await listAllFiles(rootDir, entryRelPath)));
55
55
  } else {