keryx 0.20.8 → 0.21.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/index.ts CHANGED
@@ -37,6 +37,13 @@ export { deepMerge, deepMergeDefaults, loadFromEnvIfSet } from "./util/config";
37
37
  export { getValidTypes } from "./util/generate";
38
38
  export { globLoader } from "./util/glob";
39
39
  export { type PaginatedResult, paginate } from "./util/pagination";
40
+ export type { JSONSchema } from "./util/swaggerSchemaGenerator";
41
+ export {
42
+ computeActionsHash,
43
+ generateSwaggerSchemas,
44
+ loadCachedSchemas,
45
+ writeSchemasCache,
46
+ } from "./util/swaggerSchemaGenerator";
40
47
  export { toMarkdown } from "./util/toMarkdown";
41
48
  export type { DbOrTransaction, Transaction } from "./util/transaction";
42
49
  export { withTransaction } from "./util/transaction";
@@ -1,8 +1,12 @@
1
- import { mkdir } from "fs/promises";
2
- import path from "path";
3
- import { Project, Type, ts } from "ts-morph";
4
1
  import { api, logger } from "../api";
5
2
  import { Initializer } from "../classes/Initializer";
3
+ import type { JSONSchema } from "../util/swaggerSchemaGenerator";
4
+ import {
5
+ computeActionsHash,
6
+ generateSwaggerSchemas,
7
+ loadCachedSchemas,
8
+ writeSchemasCache,
9
+ } from "../util/swaggerSchemaGenerator";
6
10
 
7
11
  const namespace = "swagger";
8
12
 
@@ -12,163 +16,6 @@ declare module "../classes/API" {
12
16
  }
13
17
  }
14
18
 
15
- type JSONSchema = {
16
- type?: string;
17
- properties?: Record<string, JSONSchema>;
18
- items?: JSONSchema;
19
- oneOf?: JSONSchema[];
20
- required?: string[];
21
- additionalProperties?: boolean | JSONSchema;
22
- enum?: (string | number | boolean | null)[];
23
- const?: unknown;
24
- $ref?: string;
25
- description?: string;
26
- };
27
-
28
- /**
29
- * Convert a ts-morph Type to JSON Schema format
30
- */
31
- function typeToJsonSchema(
32
- type: Type,
33
- visited: Set<string> = new Set(),
34
- ): JSONSchema {
35
- const typeText = type.getText();
36
-
37
- // Prevent infinite recursion for circular types
38
- if (visited.has(typeText)) {
39
- return { type: "object", additionalProperties: true };
40
- }
41
-
42
- // Handle Promise<T> - unwrap to T
43
- if (
44
- type.getSymbol()?.getName() === "Promise" ||
45
- typeText.startsWith("Promise<")
46
- ) {
47
- const typeArgs = type.getTypeArguments();
48
- if (typeArgs.length > 0) {
49
- return typeToJsonSchema(typeArgs[0], visited);
50
- }
51
- }
52
-
53
- // Handle primitives
54
- if (type.isString() || type.isStringLiteral()) {
55
- if (type.isStringLiteral()) {
56
- return { type: "string", const: type.getLiteralValue() };
57
- }
58
- return { type: "string" };
59
- }
60
-
61
- if (type.isNumber() || type.isNumberLiteral()) {
62
- if (type.isNumberLiteral()) {
63
- return { type: "number", const: type.getLiteralValue() };
64
- }
65
- return { type: "number" };
66
- }
67
-
68
- if (type.isBoolean() || type.isBooleanLiteral()) {
69
- if (type.isBooleanLiteral()) {
70
- return { type: "boolean", const: type.getLiteralValue() };
71
- }
72
- return { type: "boolean" };
73
- }
74
-
75
- if (type.isNull()) {
76
- return { type: "null" as any };
77
- }
78
-
79
- if (type.isUndefined()) {
80
- return { type: "undefined" as any };
81
- }
82
-
83
- // Handle arrays
84
- if (type.isArray()) {
85
- const elementType = type.getArrayElementType();
86
- if (elementType) {
87
- return {
88
- type: "array",
89
- items: typeToJsonSchema(elementType, visited),
90
- };
91
- }
92
- return { type: "array" };
93
- }
94
-
95
- // Handle unions (but not boolean which is true | false)
96
- if (type.isUnion() && !type.isBoolean()) {
97
- const unionTypes = type.getUnionTypes();
98
- // Filter out undefined for optional properties
99
- const nonUndefinedTypes = unionTypes.filter((t) => !t.isUndefined());
100
- if (nonUndefinedTypes.length === 1) {
101
- return typeToJsonSchema(nonUndefinedTypes[0], visited);
102
- }
103
- return {
104
- oneOf: nonUndefinedTypes.map((t) => typeToJsonSchema(t, visited)),
105
- };
106
- }
107
-
108
- // Handle objects/interfaces
109
- if (type.isObject()) {
110
- // Check if it's a Date
111
- if (type.getSymbol()?.getName() === "Date") {
112
- return { type: "string", description: "ISO 8601 date string" };
113
- }
114
-
115
- // Add to visited set to prevent recursion
116
- const newVisited = new Set(visited);
117
- newVisited.add(typeText);
118
-
119
- const properties: Record<string, JSONSchema> = {};
120
- const required: string[] = [];
121
-
122
- const typeProperties = type.getProperties();
123
- for (const prop of typeProperties) {
124
- const propName = prop.getName();
125
- // Skip internal properties
126
- if (propName.startsWith("_")) continue;
127
-
128
- // Try to get the type - use getTypeAtLocation if declaration exists, otherwise use getDeclaredType
129
- let propType: Type | undefined;
130
- const valueDecl = prop.getValueDeclaration();
131
- if (valueDecl) {
132
- propType = prop.getTypeAtLocation(valueDecl);
133
- } else {
134
- // For computed types without a direct declaration, get the type from declarations
135
- const declarations = prop.getDeclarations();
136
- if (declarations.length > 0) {
137
- propType = prop.getTypeAtLocation(declarations[0]);
138
- }
139
- }
140
-
141
- if (!propType) continue;
142
-
143
- properties[propName] = typeToJsonSchema(propType, newVisited);
144
-
145
- // Check if property is optional
146
- if (!prop.isOptional()) {
147
- required.push(propName);
148
- }
149
- }
150
-
151
- const schema: JSONSchema = {
152
- type: "object",
153
- properties,
154
- };
155
-
156
- if (required.length > 0) {
157
- schema.required = required;
158
- }
159
-
160
- return schema;
161
- }
162
-
163
- // Handle any/unknown
164
- if (type.isAny() || type.isUnknown()) {
165
- return { type: "object", additionalProperties: true };
166
- }
167
-
168
- // Fallback for complex types
169
- return { type: "object", additionalProperties: true };
170
- }
171
-
172
19
  export class SwaggerInitializer extends Initializer {
173
20
  constructor() {
174
21
  super(namespace);
@@ -176,137 +23,44 @@ export class SwaggerInitializer extends Initializer {
176
23
  }
177
24
 
178
25
  async initialize() {
179
- const cacheDir = path.join(api.rootDir, ".cache");
180
- const cacheFile = path.join(cacheDir, "swagger-schemas.json");
181
-
182
- // Hash action source files to detect changes
183
- const actionsDirs = [path.join(api.rootDir, "actions")];
184
- const glob = new Bun.Glob("**/*.ts");
185
- const hasher = new Bun.CryptoHasher("sha256");
186
- for (const actionsDir of actionsDirs) {
187
- try {
188
- const actionFiles = Array.from(glob.scanSync(actionsDir)).sort();
189
- for (const file of actionFiles) {
190
- const content = await Bun.file(path.join(actionsDir, file)).text();
191
- hasher.update(content);
192
- }
193
- } catch {
194
- // Directory may not exist
195
- }
196
- }
197
- const hash = hasher.digest("hex") as string;
26
+ const hash = await computeActionsHash(api.rootDir);
198
27
 
199
28
  // Check cache
200
- const cacheFileHandle = Bun.file(cacheFile);
201
- if (await cacheFileHandle.exists()) {
202
- try {
203
- const cached = await cacheFileHandle.json();
204
- if (cached.hash === hash) {
205
- logger.debug(
206
- `Loaded ${Object.keys(cached.responseSchemas).length} OpenAPI response schemas from cache`,
207
- );
208
- return { responseSchemas: cached.responseSchemas };
209
- }
210
- } catch {
211
- // Cache file corrupted, regenerate
212
- }
29
+ const cached = await loadCachedSchemas(api.rootDir);
30
+ if (cached && cached.hash === hash) {
31
+ logger.debug(
32
+ `Loaded ${Object.keys(cached.responseSchemas).length} OpenAPI response schemas from cache`,
33
+ );
34
+ return { responseSchemas: cached.responseSchemas };
213
35
  }
214
36
 
215
- const responseSchemas: Record<string, JSONSchema> = {};
216
-
37
+ // Generate schemas via ts-morph
38
+ let responseSchemas: Record<string, JSONSchema> = {};
217
39
  try {
218
- const tsConfigPath = path.join(api.rootDir, "tsconfig.json");
219
- const hasTsConfig = await Bun.file(tsConfigPath).exists();
220
-
221
- const project = new Project({
222
- ...(hasTsConfig ? { tsConfigFilePath: tsConfigPath } : {}),
223
- skipAddingFilesFromTsConfig: true,
224
- compilerOptions: {
225
- strict: true,
226
- target: ts.ScriptTarget.ESNext,
227
- module: ts.ModuleKind.ESNext,
228
- moduleResolution: ts.ModuleResolutionKind.Bundler,
229
- },
40
+ const result = await generateSwaggerSchemas({
41
+ rootDir: api.rootDir,
42
+ packageDir: api.packageDir,
230
43
  });
231
-
232
- // Add all source files so types can be resolved across the codebase
233
- project.addSourceFilesAtPaths(path.join(api.packageDir, "**/*.ts"));
234
- if (api.rootDir !== api.packageDir) {
235
- project.addSourceFilesAtPaths(path.join(api.rootDir, "**/*.ts"));
236
- }
237
- // Exclude test files
238
- for (const sourceFile of project.getSourceFiles()) {
239
- if (sourceFile.getFilePath().includes("__tests__")) {
240
- project.removeSourceFile(sourceFile);
241
- }
242
- }
243
-
244
- // Process each source file
245
- for (const sourceFile of project.getSourceFiles()) {
246
- const classes = sourceFile.getClasses();
247
-
248
- for (const classDecl of classes) {
249
- // Check if class implements Action (has name property and run method)
250
- const nameProperty = classDecl.getProperty("name");
251
- const runMethod =
252
- classDecl.getMethod("run") || classDecl.getProperty("run"); // run can be a property with arrow function
253
-
254
- if (!nameProperty || !runMethod) continue;
255
-
256
- // Get action name from the name property initializer
257
- const nameInitializer = nameProperty.getInitializer();
258
- if (!nameInitializer) continue;
259
-
260
- let actionName = nameInitializer.getText();
261
- // Remove quotes from string literal
262
- actionName = actionName.replace(/^["']|["']$/g, "");
263
-
264
- // Get return type of run method
265
- let returnType: Type | undefined;
266
-
267
- if (runMethod.getKind() === ts.SyntaxKind.MethodDeclaration) {
268
- // It's a method
269
- const method = classDecl.getMethod("run");
270
- if (method) {
271
- returnType = method.getReturnType();
272
- }
273
- } else {
274
- // It's a property (arrow function)
275
- const prop = classDecl.getProperty("run");
276
- if (prop) {
277
- const propType = prop.getType();
278
- // Get the return type from the function type
279
- const callSignatures = propType.getCallSignatures();
280
- if (callSignatures.length > 0) {
281
- returnType = callSignatures[0].getReturnType();
282
- }
283
- }
284
- }
285
-
286
- if (returnType) {
287
- const schema = typeToJsonSchema(returnType);
288
- responseSchemas[actionName] = schema;
289
- logger.debug(`Generated response schema for action: ${actionName}`);
290
- }
291
- }
292
- }
44
+ responseSchemas = result.responseSchemas;
293
45
 
294
46
  logger.info(
295
47
  `Generated ${Object.keys(responseSchemas).length} response schemas for swagger`,
296
48
  );
49
+
50
+ // Write cache
51
+ try {
52
+ await writeSchemasCache(api.rootDir, {
53
+ hash,
54
+ responseSchemas,
55
+ });
56
+ } catch (error) {
57
+ logger.warn(`Failed to write swagger schema cache: ${error}`);
58
+ }
297
59
  } catch (error) {
298
60
  logger.error(`Failed to generate swagger response schemas: ${error}`);
299
- }
300
-
301
- // Write cache
302
- try {
303
- await mkdir(cacheDir, { recursive: true });
304
- await Bun.write(
305
- cacheFile,
306
- JSON.stringify({ hash, responseSchemas }, null, 2),
61
+ logger.warn(
62
+ "Swagger response schemas are unavailable. Run `keryx build` at build time to pre-generate them.",
307
63
  );
308
- } catch (error) {
309
- logger.warn(`Failed to write swagger schema cache: ${error}`);
310
64
  }
311
65
 
312
66
  return { responseSchemas };
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "keryx",
3
- "version": "0.20.8",
4
- "mcpName": "io.github.actionhero/keryx",
3
+ "version": "0.21.1",
5
4
  "module": "index.ts",
6
5
  "type": "module",
7
6
  "license": "MIT",
@@ -56,8 +55,7 @@
56
55
  "keryx.ts",
57
56
  "migrations.ts",
58
57
  "tsconfig.json",
59
- "LICENSE",
60
- "server.json"
58
+ "LICENSE"
61
59
  ],
62
60
  "bin": {
63
61
  "keryx": "keryx.ts"
package/util/cli.ts CHANGED
@@ -95,6 +95,33 @@ export async function buildProgram(opts: {
95
95
  }
96
96
  });
97
97
 
98
+ program
99
+ .command("build")
100
+ .summary("Pre-generate swagger schemas for production")
101
+ .description(
102
+ "Run build-time code generation. Analyzes Action return types via ts-morph\n" +
103
+ "and writes .cache/swagger-schemas.json so the server can skip this step at\n" +
104
+ "startup. Recommended for memory-constrained environments (e.g., Docker).\n\n" +
105
+ "Example Dockerfile usage:\n" +
106
+ " COPY . .\n" +
107
+ " RUN bun keryx.ts build\n" +
108
+ ' CMD ["bun", "keryx.ts", "start"]',
109
+ )
110
+ .action(async () => {
111
+ const { generateSwaggerSchemas, writeSchemasCache } = await import(
112
+ "./swaggerSchemaGenerator"
113
+ );
114
+ const result = await generateSwaggerSchemas({
115
+ rootDir: api.rootDir,
116
+ packageDir: api.packageDir,
117
+ });
118
+ await writeSchemasCache(api.rootDir, result);
119
+ console.log(
120
+ `Generated ${Object.keys(result.responseSchemas).length} swagger response schemas`,
121
+ );
122
+ process.exit(0);
123
+ });
124
+
98
125
  program
99
126
  .command("generate <type> <name>")
100
127
  .alias("g")
@@ -0,0 +1,330 @@
1
+ import { mkdir } from "fs/promises";
2
+ import path from "path";
3
+ import type { Type } from "ts-morph";
4
+
5
+ export type JSONSchema = {
6
+ type?: string;
7
+ properties?: Record<string, JSONSchema>;
8
+ items?: JSONSchema;
9
+ oneOf?: JSONSchema[];
10
+ required?: string[];
11
+ additionalProperties?: boolean | JSONSchema;
12
+ enum?: (string | number | boolean | null)[];
13
+ const?: unknown;
14
+ $ref?: string;
15
+ description?: string;
16
+ };
17
+
18
+ /**
19
+ * Convert a ts-morph Type to JSON Schema format.
20
+ *
21
+ * @param type - The ts-morph Type to convert
22
+ * @param visited - Set of type text strings already visited (prevents infinite recursion for circular types)
23
+ * @returns A JSON Schema representation of the type
24
+ */
25
+ export function typeToJsonSchema(
26
+ type: Type,
27
+ visited: Set<string> = new Set(),
28
+ ): JSONSchema {
29
+ const typeText = type.getText();
30
+
31
+ // Prevent infinite recursion for circular types
32
+ if (visited.has(typeText)) {
33
+ return { type: "object", additionalProperties: true };
34
+ }
35
+
36
+ // Handle Promise<T> - unwrap to T
37
+ if (
38
+ type.getSymbol()?.getName() === "Promise" ||
39
+ typeText.startsWith("Promise<")
40
+ ) {
41
+ const typeArgs = type.getTypeArguments();
42
+ if (typeArgs.length > 0) {
43
+ return typeToJsonSchema(typeArgs[0], visited);
44
+ }
45
+ }
46
+
47
+ // Handle primitives
48
+ if (type.isString() || type.isStringLiteral()) {
49
+ if (type.isStringLiteral()) {
50
+ return { type: "string", const: type.getLiteralValue() };
51
+ }
52
+ return { type: "string" };
53
+ }
54
+
55
+ if (type.isNumber() || type.isNumberLiteral()) {
56
+ if (type.isNumberLiteral()) {
57
+ return { type: "number", const: type.getLiteralValue() };
58
+ }
59
+ return { type: "number" };
60
+ }
61
+
62
+ if (type.isBoolean() || type.isBooleanLiteral()) {
63
+ if (type.isBooleanLiteral()) {
64
+ return { type: "boolean", const: type.getLiteralValue() };
65
+ }
66
+ return { type: "boolean" };
67
+ }
68
+
69
+ if (type.isNull()) {
70
+ return { type: "null" as any };
71
+ }
72
+
73
+ if (type.isUndefined()) {
74
+ return { type: "undefined" as any };
75
+ }
76
+
77
+ // Handle arrays
78
+ if (type.isArray()) {
79
+ const elementType = type.getArrayElementType();
80
+ if (elementType) {
81
+ return {
82
+ type: "array",
83
+ items: typeToJsonSchema(elementType, visited),
84
+ };
85
+ }
86
+ return { type: "array" };
87
+ }
88
+
89
+ // Handle unions (but not boolean which is true | false)
90
+ if (type.isUnion() && !type.isBoolean()) {
91
+ const unionTypes = type.getUnionTypes();
92
+ // Filter out undefined for optional properties
93
+ const nonUndefinedTypes = unionTypes.filter((t) => !t.isUndefined());
94
+ if (nonUndefinedTypes.length === 1) {
95
+ return typeToJsonSchema(nonUndefinedTypes[0], visited);
96
+ }
97
+ return {
98
+ oneOf: nonUndefinedTypes.map((t) => typeToJsonSchema(t, visited)),
99
+ };
100
+ }
101
+
102
+ // Handle objects/interfaces
103
+ if (type.isObject()) {
104
+ // Check if it's a Date
105
+ if (type.getSymbol()?.getName() === "Date") {
106
+ return { type: "string", description: "ISO 8601 date string" };
107
+ }
108
+
109
+ // Add to visited set to prevent recursion
110
+ const newVisited = new Set(visited);
111
+ newVisited.add(typeText);
112
+
113
+ const properties: Record<string, JSONSchema> = {};
114
+ const required: string[] = [];
115
+
116
+ const typeProperties = type.getProperties();
117
+ for (const prop of typeProperties) {
118
+ const propName = prop.getName();
119
+ // Skip internal properties
120
+ if (propName.startsWith("_")) continue;
121
+
122
+ // Try to get the type - use getTypeAtLocation if declaration exists, otherwise use getDeclaredType
123
+ let propType: Type | undefined;
124
+ const valueDecl = prop.getValueDeclaration();
125
+ if (valueDecl) {
126
+ propType = prop.getTypeAtLocation(valueDecl);
127
+ } else {
128
+ // For computed types without a direct declaration, get the type from declarations
129
+ const declarations = prop.getDeclarations();
130
+ if (declarations.length > 0) {
131
+ propType = prop.getTypeAtLocation(declarations[0]);
132
+ }
133
+ }
134
+
135
+ if (!propType) continue;
136
+
137
+ properties[propName] = typeToJsonSchema(propType, newVisited);
138
+
139
+ // Check if property is optional
140
+ if (!prop.isOptional()) {
141
+ required.push(propName);
142
+ }
143
+ }
144
+
145
+ const schema: JSONSchema = {
146
+ type: "object",
147
+ properties,
148
+ };
149
+
150
+ if (required.length > 0) {
151
+ schema.required = required;
152
+ }
153
+
154
+ return schema;
155
+ }
156
+
157
+ // Handle any/unknown
158
+ if (type.isAny() || type.isUnknown()) {
159
+ return { type: "object", additionalProperties: true };
160
+ }
161
+
162
+ // Fallback for complex types
163
+ return { type: "object", additionalProperties: true };
164
+ }
165
+
166
+ /**
167
+ * Compute a SHA256 hash of all TypeScript action source files.
168
+ * Used to detect when actions have changed and the cache needs regeneration.
169
+ *
170
+ * @param rootDir - The application root directory containing the `actions/` folder
171
+ * @returns Hex-encoded SHA256 hash string
172
+ */
173
+ export async function computeActionsHash(rootDir: string): Promise<string> {
174
+ const actionsDirs = [path.join(rootDir, "actions")];
175
+ const glob = new Bun.Glob("**/*.ts");
176
+ const hasher = new Bun.CryptoHasher("sha256");
177
+ for (const actionsDir of actionsDirs) {
178
+ try {
179
+ const actionFiles = Array.from(glob.scanSync(actionsDir)).sort();
180
+ for (const file of actionFiles) {
181
+ const content = await Bun.file(path.join(actionsDir, file)).text();
182
+ hasher.update(content);
183
+ }
184
+ } catch {
185
+ // Directory may not exist
186
+ }
187
+ }
188
+ return hasher.digest("hex") as string;
189
+ }
190
+
191
+ /**
192
+ * Load cached swagger schemas from disk.
193
+ *
194
+ * @param rootDir - The application root directory containing the `.cache/` folder
195
+ * @returns The cached data (hash + responseSchemas) or null if cache is missing/corrupted
196
+ */
197
+ export async function loadCachedSchemas(rootDir: string): Promise<{
198
+ hash: string;
199
+ responseSchemas: Record<string, JSONSchema>;
200
+ } | null> {
201
+ const cacheFile = path.join(rootDir, ".cache", "swagger-schemas.json");
202
+ const cacheFileHandle = Bun.file(cacheFile);
203
+ if (await cacheFileHandle.exists()) {
204
+ try {
205
+ const cached = await cacheFileHandle.json();
206
+ if (cached.hash && cached.responseSchemas) {
207
+ return cached;
208
+ }
209
+ } catch {
210
+ // Cache file corrupted
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+
216
+ /**
217
+ * Run the full ts-morph analysis to generate JSON Schema representations
218
+ * of all Action return types.
219
+ *
220
+ * @param opts - rootDir and packageDir paths for source file discovery
221
+ * @param opts.rootDir - The application root directory
222
+ * @param opts.packageDir - The keryx framework package directory
223
+ * @returns Hash of action files and generated response schemas
224
+ */
225
+ export async function generateSwaggerSchemas(opts: {
226
+ rootDir: string;
227
+ packageDir: string;
228
+ }): Promise<{ hash: string; responseSchemas: Record<string, JSONSchema> }> {
229
+ const { rootDir, packageDir } = opts;
230
+ const hash = await computeActionsHash(rootDir);
231
+ const responseSchemas: Record<string, JSONSchema> = {};
232
+
233
+ // Dynamic import so ts-morph is only loaded when actually generating
234
+ const { Project, ts } = await import("ts-morph");
235
+
236
+ const tsConfigPath = path.join(rootDir, "tsconfig.json");
237
+ const hasTsConfig = await Bun.file(tsConfigPath).exists();
238
+
239
+ const project = new Project({
240
+ ...(hasTsConfig ? { tsConfigFilePath: tsConfigPath } : {}),
241
+ skipAddingFilesFromTsConfig: true,
242
+ compilerOptions: {
243
+ strict: true,
244
+ target: ts.ScriptTarget.ESNext,
245
+ module: ts.ModuleKind.ESNext,
246
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
247
+ },
248
+ });
249
+
250
+ // Add all source files so types can be resolved across the codebase
251
+ project.addSourceFilesAtPaths(path.join(packageDir, "**/*.ts"));
252
+ if (rootDir !== packageDir) {
253
+ project.addSourceFilesAtPaths(path.join(rootDir, "**/*.ts"));
254
+ }
255
+ // Exclude test files
256
+ for (const sourceFile of project.getSourceFiles()) {
257
+ if (sourceFile.getFilePath().includes("__tests__")) {
258
+ project.removeSourceFile(sourceFile);
259
+ }
260
+ }
261
+
262
+ // Process each source file
263
+ for (const sourceFile of project.getSourceFiles()) {
264
+ const classes = sourceFile.getClasses();
265
+
266
+ for (const classDecl of classes) {
267
+ // Check if class implements Action (has name property and run method)
268
+ const nameProperty = classDecl.getProperty("name");
269
+ const runMethod =
270
+ classDecl.getMethod("run") || classDecl.getProperty("run"); // run can be a property with arrow function
271
+
272
+ if (!nameProperty || !runMethod) continue;
273
+
274
+ // Get action name from the name property initializer
275
+ const nameInitializer = nameProperty.getInitializer();
276
+ if (!nameInitializer) continue;
277
+
278
+ let actionName = nameInitializer.getText();
279
+ // Remove quotes from string literal
280
+ actionName = actionName.replace(/^["']|["']$/g, "");
281
+
282
+ // Get return type of run method
283
+ let returnType: Type | undefined;
284
+
285
+ if (runMethod.getKind() === ts.SyntaxKind.MethodDeclaration) {
286
+ // It's a method
287
+ const method = classDecl.getMethod("run");
288
+ if (method) {
289
+ returnType = method.getReturnType();
290
+ }
291
+ } else {
292
+ // It's a property (arrow function)
293
+ const prop = classDecl.getProperty("run");
294
+ if (prop) {
295
+ const propType = prop.getType();
296
+ // Get the return type from the function type
297
+ const callSignatures = propType.getCallSignatures();
298
+ if (callSignatures.length > 0) {
299
+ returnType = callSignatures[0].getReturnType();
300
+ }
301
+ }
302
+ }
303
+
304
+ if (returnType) {
305
+ const schema = typeToJsonSchema(returnType);
306
+ responseSchemas[actionName] = schema;
307
+ }
308
+ }
309
+ }
310
+
311
+ return { hash, responseSchemas };
312
+ }
313
+
314
+ /**
315
+ * Write swagger schema cache to disk.
316
+ *
317
+ * @param rootDir - The application root directory where `.cache/` will be created
318
+ * @param data - The hash and response schemas to cache
319
+ */
320
+ export async function writeSchemasCache(
321
+ rootDir: string,
322
+ data: { hash: string; responseSchemas: Record<string, JSONSchema> },
323
+ ): Promise<void> {
324
+ const cacheDir = path.join(rootDir, ".cache");
325
+ await mkdir(cacheDir, { recursive: true });
326
+ await Bun.write(
327
+ path.join(cacheDir, "swagger-schemas.json"),
328
+ JSON.stringify(data, null, 2),
329
+ );
330
+ }
package/server.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
- "name": "io.github.actionhero/keryx",
4
- "description": "Fullstack TypeScript framework for MCP and APIs — one action serves HTTP, WS, CLI, and MCP.",
5
- "repository": {
6
- "url": "https://github.com/actionhero/keryx",
7
- "source": "github"
8
- },
9
- "version": "0.20.8",
10
- "packages": [
11
- {
12
- "registryType": "npm",
13
- "identifier": "keryx",
14
- "version": "0.20.8",
15
- "transport": { "type": "http" }
16
- }
17
- ]
18
- }