ast-mcp-server 0.4.0 → 0.5.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/CHANGELOG.md +23 -0
- package/README.md +14 -6
- package/dist/batch/runner.js +2 -2
- package/dist/batch/schema.js +5 -1
- package/dist/server.js +6 -1
- package/dist/services/agent-setup.js +1 -0
- package/dist/services/operation-plan-file.js +1 -1
- package/dist/services/operations.js +172 -17
- package/dist/services/pagination.js +16 -10
- package/dist/services/scaffold.js +241 -0
- package/dist/services/symbols.js +16 -0
- package/dist/tools/apply_operation.js +1 -1
- package/dist/tools/find_references.js +30 -7
- package/dist/tools/operation-schema.js +1 -1
- package/dist/tools/scaffold_class.js +145 -0
- package/dist/tools/search_symbols.js +77 -16
- package/package.json +3 -2
- package/skills/structural-code-editing/SKILL.md +15 -5
- package/benchmark/README.md +0 -118
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { LanguageVariant, Node, Scope, ScriptTarget, SyntaxKind, ts, } from "ts-morph";
|
|
2
|
+
import { buildFileOutline } from "./outline.js";
|
|
3
|
+
function assertIdentifier(value, label) {
|
|
4
|
+
const scanner = ts.createScanner(ScriptTarget.Latest, false, LanguageVariant.Standard, value);
|
|
5
|
+
const valid = scanner.scan() === SyntaxKind.Identifier && scanner.scan() === SyntaxKind.EndOfFileToken;
|
|
6
|
+
if (!valid)
|
|
7
|
+
throw new Error(`${label} must be a valid TypeScript identifier.`);
|
|
8
|
+
}
|
|
9
|
+
function assertBoundedText(value, label, maximum = 10_000) {
|
|
10
|
+
if (value.trim().length === 0)
|
|
11
|
+
throw new Error(`${label} must not be empty.`);
|
|
12
|
+
if (value.length > maximum)
|
|
13
|
+
throw new Error(`${label} exceeds ${maximum} characters.`);
|
|
14
|
+
}
|
|
15
|
+
function parseFragment(sourceText, label) {
|
|
16
|
+
const sourceFile = ts.createSourceFile("__ast_scaffold_fragment.ts", sourceText, ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
17
|
+
const diagnostics = sourceFile.parseDiagnostics;
|
|
18
|
+
if (diagnostics.length > 0) {
|
|
19
|
+
throw new Error(`${label} is not valid TypeScript in its declaration position.`);
|
|
20
|
+
}
|
|
21
|
+
return sourceFile;
|
|
22
|
+
}
|
|
23
|
+
function assertTypeFragment(value, label) {
|
|
24
|
+
assertBoundedText(value, label);
|
|
25
|
+
const sourceFile = parseFragment(`type __ScaffoldType = ${value};`, label);
|
|
26
|
+
const declaration = sourceFile.statements[0];
|
|
27
|
+
if (sourceFile.statements.length !== 1 ||
|
|
28
|
+
!declaration ||
|
|
29
|
+
!ts.isTypeAliasDeclaration(declaration) ||
|
|
30
|
+
declaration.type.getText(sourceFile) !== value.trim()) {
|
|
31
|
+
throw new Error(`${label} must contain exactly one TypeScript type.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function assertHeritageFragment(value, label, token) {
|
|
35
|
+
assertBoundedText(value, label);
|
|
36
|
+
const keyword = token === SyntaxKind.ExtendsKeyword ? "extends" : "implements";
|
|
37
|
+
const sourceFile = parseFragment(`class __Scaffold ${keyword} ${value} {}`, label);
|
|
38
|
+
const declaration = sourceFile.statements[0];
|
|
39
|
+
const clause = declaration && ts.isClassDeclaration(declaration)
|
|
40
|
+
? declaration.heritageClauses?.find((item) => item.token === token)
|
|
41
|
+
: undefined;
|
|
42
|
+
if (sourceFile.statements.length !== 1 ||
|
|
43
|
+
!clause ||
|
|
44
|
+
clause.types.length !== 1 ||
|
|
45
|
+
clause.types[0]?.getText(sourceFile) !== value.trim()) {
|
|
46
|
+
throw new Error(`${label} must contain exactly one TypeScript heritage expression.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function assertInitializerFragment(value, label) {
|
|
50
|
+
assertBoundedText(value, label);
|
|
51
|
+
const sourceFile = parseFragment(`class __Scaffold { value = ${value}; }`, label);
|
|
52
|
+
const declaration = sourceFile.statements[0];
|
|
53
|
+
const members = declaration && ts.isClassDeclaration(declaration) ? declaration.members : [];
|
|
54
|
+
const property = members[0];
|
|
55
|
+
if (sourceFile.statements.length !== 1 ||
|
|
56
|
+
members.length !== 1 ||
|
|
57
|
+
!property ||
|
|
58
|
+
!ts.isPropertyDeclaration(property) ||
|
|
59
|
+
property.initializer?.getText(sourceFile) !== value.trim()) {
|
|
60
|
+
throw new Error(`${label} must contain exactly one TypeScript expression.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function scopeStructure(modifier) {
|
|
64
|
+
switch (modifier) {
|
|
65
|
+
case "public":
|
|
66
|
+
return { scope: Scope.Public };
|
|
67
|
+
case "public readonly":
|
|
68
|
+
return { scope: Scope.Public, isReadonly: true };
|
|
69
|
+
case "protected":
|
|
70
|
+
return { scope: Scope.Protected };
|
|
71
|
+
case "protected readonly":
|
|
72
|
+
return { scope: Scope.Protected, isReadonly: true };
|
|
73
|
+
case "private":
|
|
74
|
+
return { scope: Scope.Private };
|
|
75
|
+
case "private readonly":
|
|
76
|
+
return { scope: Scope.Private, isReadonly: true };
|
|
77
|
+
default:
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function decoratorStructure(value, label) {
|
|
82
|
+
assertBoundedText(value, label);
|
|
83
|
+
const sourceFile = parseFragment(`${value.trim()}\nclass __Scaffold {}`, label);
|
|
84
|
+
const declaration = sourceFile.statements[0];
|
|
85
|
+
const decorators = declaration && ts.isClassDeclaration(declaration) && ts.canHaveDecorators(declaration)
|
|
86
|
+
? ts.getDecorators(declaration)
|
|
87
|
+
: undefined;
|
|
88
|
+
if (sourceFile.statements.length !== 1 ||
|
|
89
|
+
decorators?.length !== 1 ||
|
|
90
|
+
decorators[0]?.getText(sourceFile) !== value.trim()) {
|
|
91
|
+
throw new Error(`${label} must contain exactly one TypeScript decorator.`);
|
|
92
|
+
}
|
|
93
|
+
const match = /^@([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)(?:\(([\s\S]*)\))?$/.exec(value.trim());
|
|
94
|
+
if (!match) {
|
|
95
|
+
throw new Error(`${label} must use @Name or @Name(arguments) syntax.`);
|
|
96
|
+
}
|
|
97
|
+
const argumentText = match[2]?.trim();
|
|
98
|
+
return {
|
|
99
|
+
name: match[1],
|
|
100
|
+
arguments: argumentText ? [argumentText] : [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function validateSpec(spec) {
|
|
104
|
+
assertIdentifier(spec.className, "class_name");
|
|
105
|
+
if (spec.methods.length === 0)
|
|
106
|
+
throw new Error("methods must contain at least one method.");
|
|
107
|
+
const methodNames = new Set();
|
|
108
|
+
for (const [index, method] of spec.methods.entries()) {
|
|
109
|
+
assertIdentifier(method.name, `methods[${index}].name`);
|
|
110
|
+
if (method.name === "constructor") {
|
|
111
|
+
throw new Error('Method name "constructor" is reserved for the generated constructor.');
|
|
112
|
+
}
|
|
113
|
+
if (methodNames.has(method.name)) {
|
|
114
|
+
throw new Error("Method names must be unique so pending method selectors are unambiguous.");
|
|
115
|
+
}
|
|
116
|
+
methodNames.add(method.name);
|
|
117
|
+
assertTypeFragment(method.returnType, `methods[${index}].return_type`);
|
|
118
|
+
const parameterNames = new Set();
|
|
119
|
+
for (const [paramIndex, parameter] of method.params.entries()) {
|
|
120
|
+
assertIdentifier(parameter.name, `methods[${index}].params[${paramIndex}].name`);
|
|
121
|
+
if (parameterNames.has(parameter.name)) {
|
|
122
|
+
throw new Error(`methods[${index}] parameter names must be unique.`);
|
|
123
|
+
}
|
|
124
|
+
parameterNames.add(parameter.name);
|
|
125
|
+
assertTypeFragment(parameter.type, `methods[${index}].params[${paramIndex}].type`);
|
|
126
|
+
}
|
|
127
|
+
for (const [docsIndex, docs] of (method.docs ?? []).entries()) {
|
|
128
|
+
assertBoundedText(docs, `methods[${index}].docs[${docsIndex}]`);
|
|
129
|
+
if (docs.includes("*/")) {
|
|
130
|
+
throw new Error(`methods[${index}].docs[${docsIndex}] must not close its doc comment.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const constructorParameterNames = new Set();
|
|
135
|
+
for (const [index, parameter] of spec.constructorParams.entries()) {
|
|
136
|
+
assertIdentifier(parameter.name, `constructor_params[${index}].name`);
|
|
137
|
+
if (constructorParameterNames.has(parameter.name)) {
|
|
138
|
+
throw new Error("Constructor parameter names must be unique.");
|
|
139
|
+
}
|
|
140
|
+
constructorParameterNames.add(parameter.name);
|
|
141
|
+
assertTypeFragment(parameter.type, `constructor_params[${index}].type`);
|
|
142
|
+
}
|
|
143
|
+
const propertyNames = new Set();
|
|
144
|
+
for (const [index, property] of spec.properties.entries()) {
|
|
145
|
+
assertIdentifier(property.name, `properties[${index}].name`);
|
|
146
|
+
if (propertyNames.has(property.name))
|
|
147
|
+
throw new Error("Property names must be unique.");
|
|
148
|
+
if (methodNames.has(property.name)) {
|
|
149
|
+
throw new Error("Property names must not collide with generated method names.");
|
|
150
|
+
}
|
|
151
|
+
propertyNames.add(property.name);
|
|
152
|
+
assertTypeFragment(property.type, `properties[${index}].type`);
|
|
153
|
+
if (property.initializer === undefined) {
|
|
154
|
+
throw new Error(`properties[${index}] requires an initializer.`);
|
|
155
|
+
}
|
|
156
|
+
assertInitializerFragment(property.initializer, `properties[${index}].initializer`);
|
|
157
|
+
}
|
|
158
|
+
for (const [index, declaration] of spec.imports.entries()) {
|
|
159
|
+
assertBoundedText(declaration.from, `imports[${index}].from`, 500);
|
|
160
|
+
if ([...declaration.from].some((character) => {
|
|
161
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
162
|
+
return codePoint <= 31 || codePoint === 127;
|
|
163
|
+
})) {
|
|
164
|
+
throw new Error(`imports[${index}].from must not contain control characters.`);
|
|
165
|
+
}
|
|
166
|
+
if (declaration.default !== undefined) {
|
|
167
|
+
assertIdentifier(declaration.default, `imports[${index}].default`);
|
|
168
|
+
}
|
|
169
|
+
for (const [nameIndex, name] of (declaration.named ?? []).entries()) {
|
|
170
|
+
assertIdentifier(name, `imports[${index}].named[${nameIndex}]`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
spec.decorators.forEach((decorator, index) => decoratorStructure(decorator, `decorators[${index}]`));
|
|
174
|
+
spec.methods.forEach((method, methodIndex) => (method.decorators ?? []).forEach((decorator, decoratorIndex) => decoratorStructure(decorator, `methods[${methodIndex}].decorators[${decoratorIndex}]`)));
|
|
175
|
+
if (spec.extends !== undefined) {
|
|
176
|
+
assertHeritageFragment(spec.extends, "extends", SyntaxKind.ExtendsKeyword);
|
|
177
|
+
}
|
|
178
|
+
spec.implements.forEach((value, index) => assertHeritageFragment(value, `implements[${index}]`, SyntaxKind.ImplementsKeyword));
|
|
179
|
+
}
|
|
180
|
+
export function buildClassScaffold(project, filePath, spec) {
|
|
181
|
+
validateSpec(spec);
|
|
182
|
+
const sourceFile = project.createSourceFile(filePath, "", { overwrite: false });
|
|
183
|
+
sourceFile.addImportDeclarations(spec.imports.map((declaration) => ({
|
|
184
|
+
moduleSpecifier: declaration.from,
|
|
185
|
+
...(declaration.default ? { defaultImport: declaration.default } : {}),
|
|
186
|
+
...(declaration.named?.length ? { namedImports: declaration.named } : {}),
|
|
187
|
+
})));
|
|
188
|
+
sourceFile.addClass({
|
|
189
|
+
name: spec.className,
|
|
190
|
+
isExported: true,
|
|
191
|
+
...(spec.extends ? { extends: spec.extends } : {}),
|
|
192
|
+
...(spec.implements.length > 0 ? { implements: spec.implements } : {}),
|
|
193
|
+
decorators: spec.decorators.map((decorator, index) => decoratorStructure(decorator, `decorators[${index}]`)),
|
|
194
|
+
ctors: spec.constructorParams.length > 0
|
|
195
|
+
? [
|
|
196
|
+
{
|
|
197
|
+
parameters: spec.constructorParams.map((parameter) => ({
|
|
198
|
+
name: parameter.name,
|
|
199
|
+
type: parameter.type,
|
|
200
|
+
...scopeStructure(parameter.accessModifier),
|
|
201
|
+
})),
|
|
202
|
+
statements: [],
|
|
203
|
+
},
|
|
204
|
+
]
|
|
205
|
+
: [],
|
|
206
|
+
properties: spec.properties.map((property) => ({
|
|
207
|
+
name: property.name,
|
|
208
|
+
type: property.type,
|
|
209
|
+
...scopeStructure(property.accessModifier),
|
|
210
|
+
...(property.initializer !== undefined ? { initializer: property.initializer } : {}),
|
|
211
|
+
})),
|
|
212
|
+
methods: spec.methods.map((method, methodIndex) => ({
|
|
213
|
+
name: method.name,
|
|
214
|
+
isAsync: method.isAsync,
|
|
215
|
+
returnType: method.returnType,
|
|
216
|
+
...scopeStructure(method.accessModifier),
|
|
217
|
+
parameters: method.params.map((parameter) => ({
|
|
218
|
+
name: parameter.name,
|
|
219
|
+
type: parameter.type,
|
|
220
|
+
})),
|
|
221
|
+
decorators: (method.decorators ?? []).map((decorator, decoratorIndex) => decoratorStructure(decorator, `methods[${methodIndex}].decorators[${decoratorIndex}]`)),
|
|
222
|
+
docs: method.docs ?? [],
|
|
223
|
+
statements: [`throw new Error("Not implemented: ${spec.className}.${method.name}");`],
|
|
224
|
+
})),
|
|
225
|
+
});
|
|
226
|
+
const classes = sourceFile.getClasses();
|
|
227
|
+
const allowedStatements = sourceFile
|
|
228
|
+
.getStatements()
|
|
229
|
+
.every((statement) => Node.isImportDeclaration(statement) || Node.isClassDeclaration(statement));
|
|
230
|
+
if (!allowedStatements ||
|
|
231
|
+
classes.length !== 1 ||
|
|
232
|
+
classes[0]?.getName() !== spec.className ||
|
|
233
|
+
classes[0].getMethods().length !== spec.methods.length) {
|
|
234
|
+
throw new Error("Generated scaffold failed structural postcondition validation.");
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
sourceFile,
|
|
238
|
+
pendingMethods: spec.methods.map((method) => `${spec.className}.${method.name}`),
|
|
239
|
+
outline: buildFileOutline(sourceFile).text,
|
|
240
|
+
};
|
|
241
|
+
}
|
package/dist/services/symbols.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { Node, } from "ts-morph";
|
|
2
|
+
export function symbolMatchRank(query, symbol) {
|
|
3
|
+
const normalizedQuery = query.toLowerCase();
|
|
4
|
+
const normalizedPath = symbol.symbolPath.toLowerCase();
|
|
5
|
+
const normalizedName = symbol.name.toLowerCase();
|
|
6
|
+
const normalizedSelector = `${normalizedPath}@${symbol.line}`;
|
|
7
|
+
if (normalizedSelector === normalizedQuery)
|
|
8
|
+
return 0;
|
|
9
|
+
if (normalizedPath === normalizedQuery)
|
|
10
|
+
return 1;
|
|
11
|
+
if (normalizedName === normalizedQuery)
|
|
12
|
+
return 2;
|
|
13
|
+
if (normalizedPath.startsWith(normalizedQuery) || normalizedName.startsWith(normalizedQuery)) {
|
|
14
|
+
return 3;
|
|
15
|
+
}
|
|
16
|
+
return 4;
|
|
17
|
+
}
|
|
2
18
|
function namedNode(node) {
|
|
3
19
|
if (Node.isConstructorDeclaration(node))
|
|
4
20
|
return "constructor";
|
|
@@ -7,7 +7,7 @@ const AstApplyOperationInputSchema = z.object({
|
|
|
7
7
|
});
|
|
8
8
|
const AstApplyOperationOutputSchema = z.object({
|
|
9
9
|
operation_id: z.string().uuid(),
|
|
10
|
-
kind: z.enum(["rename_symbol", "replace_symbol_body"]),
|
|
10
|
+
kind: z.enum(["rename_symbol", "replace_symbol_body", "scaffold_class"]),
|
|
11
11
|
status: z.literal("applied"),
|
|
12
12
|
applied_at: z.string(),
|
|
13
13
|
affected_files: z.array(z.string()),
|
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { PaginationInputSchema, PaginationOutputSchema, paginate } from "../services/pagination.js";
|
|
5
5
|
import { findDeclarationByName, getSourceFileOrThrow, withProject } from "../services/project.js";
|
|
6
6
|
import { errorResult, formattedResult, ToolOutputFormatInputSchema } from "./result.js";
|
|
7
|
+
const ReferenceDetailSchema = z.enum(["locations", "context"]).default("locations");
|
|
7
8
|
const AstFindReferencesInputSchema = z.object({
|
|
8
9
|
project_root: z
|
|
9
10
|
.string()
|
|
@@ -11,10 +12,11 @@ const AstFindReferencesInputSchema = z.object({
|
|
|
11
12
|
file_path: z.string().describe("File containing the declaration."),
|
|
12
13
|
symbol_path: z.string().describe("Exact symbol path returned by an outline or symbol search."),
|
|
13
14
|
include_declaration: z.boolean().default(true),
|
|
15
|
+
detail: ReferenceDetailSchema.describe("Result detail: locations omits source lines; context adds bounded source-line text."),
|
|
14
16
|
...ToolOutputFormatInputSchema,
|
|
15
17
|
...PaginationInputSchema,
|
|
16
18
|
});
|
|
17
|
-
const
|
|
19
|
+
const ContextReferenceSchema = z.object({
|
|
18
20
|
file: z.string(),
|
|
19
21
|
line: z.number().int().positive(),
|
|
20
22
|
column: z.number().int().positive(),
|
|
@@ -22,14 +24,25 @@ const ReferenceSchema = z.object({
|
|
|
22
24
|
is_declaration: z.boolean(),
|
|
23
25
|
context: z.string(),
|
|
24
26
|
});
|
|
25
|
-
const
|
|
27
|
+
const LocationReferenceSchema = ContextReferenceSchema.omit({ context: true });
|
|
28
|
+
const ReferenceOutputBase = {
|
|
26
29
|
symbol: z.string(),
|
|
30
|
+
include_declaration: z.boolean(),
|
|
27
31
|
declaration_count: z.number().int().min(0),
|
|
28
32
|
reference_count: z.number().int().min(0),
|
|
29
33
|
affected_files: z.array(z.string()),
|
|
30
|
-
references: z.array(ReferenceSchema),
|
|
31
34
|
...PaginationOutputSchema,
|
|
32
|
-
}
|
|
35
|
+
};
|
|
36
|
+
const ReferenceOutputSchemas = {
|
|
37
|
+
locations: z.object({
|
|
38
|
+
...ReferenceOutputBase,
|
|
39
|
+
references: z.array(LocationReferenceSchema),
|
|
40
|
+
}),
|
|
41
|
+
context: z.object({
|
|
42
|
+
...ReferenceOutputBase,
|
|
43
|
+
references: z.array(ContextReferenceSchema),
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
33
46
|
export function registerFindReferences(server) {
|
|
34
47
|
server.registerTool("ast_find_references", {
|
|
35
48
|
title: "Find semantic symbol references",
|
|
@@ -41,7 +54,7 @@ export function registerFindReferences(server) {
|
|
|
41
54
|
idempotentHint: true,
|
|
42
55
|
openWorldHint: false,
|
|
43
56
|
},
|
|
44
|
-
}, async ({ project_root, file_path, symbol_path, include_declaration, output_format, offset, limit, }) => {
|
|
57
|
+
}, async ({ project_root, file_path, symbol_path, include_declaration, detail, output_format, offset, limit, }) => {
|
|
45
58
|
try {
|
|
46
59
|
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
47
60
|
const sourceFile = getSourceFileOrThrow(project, file_path);
|
|
@@ -81,14 +94,24 @@ export function registerFindReferences(server) {
|
|
|
81
94
|
const { items, ...metadata } = page;
|
|
82
95
|
return {
|
|
83
96
|
symbol: symbol_path,
|
|
97
|
+
include_declaration,
|
|
84
98
|
declaration_count: 1,
|
|
85
99
|
reference_count: references.length,
|
|
86
100
|
affected_files: affectedFiles,
|
|
87
|
-
references:
|
|
101
|
+
references: detail === "context"
|
|
102
|
+
? items
|
|
103
|
+
: items.map(({ file, line, column, kind, is_declaration }) => ({
|
|
104
|
+
file,
|
|
105
|
+
line,
|
|
106
|
+
column,
|
|
107
|
+
kind,
|
|
108
|
+
is_declaration,
|
|
109
|
+
})),
|
|
88
110
|
...metadata,
|
|
89
111
|
};
|
|
90
112
|
});
|
|
91
|
-
|
|
113
|
+
const outputSchema = ReferenceOutputSchemas[detail];
|
|
114
|
+
return formattedResult(outputSchema, structuredContent, output_format);
|
|
92
115
|
}
|
|
93
116
|
catch (error) {
|
|
94
117
|
return errorResult(error);
|
|
@@ -10,7 +10,7 @@ export const NormalizedDiagnosticSchema = z.object({
|
|
|
10
10
|
export const PreparedOperationOutputSchema = z.object({
|
|
11
11
|
operation_id: z.string().uuid(),
|
|
12
12
|
plan_hash: z.string().length(64),
|
|
13
|
-
kind: z.enum(["rename_symbol", "replace_symbol_body"]),
|
|
13
|
+
kind: z.enum(["rename_symbol", "replace_symbol_body", "scaffold_class"]),
|
|
14
14
|
status: z.literal("prepared"),
|
|
15
15
|
project_root: z.string(),
|
|
16
16
|
created_at: z.string(),
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { prepareScaffoldClass } from "../services/operations.js";
|
|
3
|
+
import { errorResult, structuredResult } from "./result.js";
|
|
4
|
+
import { PreparedOperationOutputSchema, serializePreparedOperation } from "./operation-schema.js";
|
|
5
|
+
const IdentifierSchema = z.string().min(1).max(200);
|
|
6
|
+
const TypeTextSchema = z.string().min(1).max(10_000);
|
|
7
|
+
const AccessSchema = z.enum(["public", "protected", "private"]);
|
|
8
|
+
const ConstructorAccessSchema = z.enum([
|
|
9
|
+
"public",
|
|
10
|
+
"protected",
|
|
11
|
+
"private",
|
|
12
|
+
"public readonly",
|
|
13
|
+
"protected readonly",
|
|
14
|
+
"private readonly",
|
|
15
|
+
]);
|
|
16
|
+
const ImportSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
from: z.string().min(1).max(500),
|
|
19
|
+
named: z.array(IdentifierSchema).max(100).default([]),
|
|
20
|
+
default: IdentifierSchema.optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
const ParameterSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
name: IdentifierSchema,
|
|
26
|
+
type: TypeTextSchema,
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
const ConstructorParameterSchema = ParameterSchema.extend({
|
|
30
|
+
access_modifier: ConstructorAccessSchema,
|
|
31
|
+
}).strict();
|
|
32
|
+
const PropertySchema = z
|
|
33
|
+
.object({
|
|
34
|
+
name: IdentifierSchema,
|
|
35
|
+
type: TypeTextSchema,
|
|
36
|
+
access: AccessSchema.default("private"),
|
|
37
|
+
initializer: z.string().max(10_000).optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
const MethodSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
name: IdentifierSchema,
|
|
43
|
+
is_async: z.boolean().default(false),
|
|
44
|
+
return_type: TypeTextSchema,
|
|
45
|
+
access: AccessSchema.default("public"),
|
|
46
|
+
parameters: z.array(ParameterSchema).max(100).default([]),
|
|
47
|
+
decorators: z.array(z.string().min(1).max(10_000)).max(50).default([]),
|
|
48
|
+
docs: z.array(z.string().min(1).max(10_000)).max(50).default([]),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
export const ScaffoldClassInputSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
project_root: z.string().min(1),
|
|
54
|
+
file_path: z.string().min(1).max(1_000),
|
|
55
|
+
class_name: IdentifierSchema,
|
|
56
|
+
extends: TypeTextSchema.optional(),
|
|
57
|
+
implements: z.array(TypeTextSchema).max(50).default([]),
|
|
58
|
+
decorators: z.array(z.string().min(1).max(10_000)).max(50).default([]),
|
|
59
|
+
imports: z.array(ImportSchema).max(100).default([]),
|
|
60
|
+
constructor_params: z.array(ConstructorParameterSchema).max(100).default([]),
|
|
61
|
+
properties: z.array(PropertySchema).max(200).default([]),
|
|
62
|
+
methods: z.array(MethodSchema).min(1).max(200),
|
|
63
|
+
dry_run: z.literal(true).default(true),
|
|
64
|
+
allow_new_errors: z.boolean().default(false),
|
|
65
|
+
})
|
|
66
|
+
.strict();
|
|
67
|
+
const ScaffoldClassOutputSchema = PreparedOperationOutputSchema.extend({
|
|
68
|
+
file: z.string(),
|
|
69
|
+
class_name: z.string(),
|
|
70
|
+
outline: z.string(),
|
|
71
|
+
pending_methods: z.array(z.string()),
|
|
72
|
+
});
|
|
73
|
+
function toSpec(args) {
|
|
74
|
+
return {
|
|
75
|
+
className: args.class_name,
|
|
76
|
+
...(args.extends ? { extends: args.extends } : {}),
|
|
77
|
+
implements: args.implements,
|
|
78
|
+
decorators: args.decorators,
|
|
79
|
+
imports: args.imports.map((item) => ({
|
|
80
|
+
from: item.from,
|
|
81
|
+
named: item.named,
|
|
82
|
+
...(item.default ? { default: item.default } : {}),
|
|
83
|
+
})),
|
|
84
|
+
constructorParams: args.constructor_params.map((parameter) => ({
|
|
85
|
+
name: parameter.name,
|
|
86
|
+
type: parameter.type,
|
|
87
|
+
accessModifier: parameter.access_modifier,
|
|
88
|
+
})),
|
|
89
|
+
properties: args.properties.map((property) => ({
|
|
90
|
+
name: property.name,
|
|
91
|
+
type: property.type,
|
|
92
|
+
accessModifier: property.access,
|
|
93
|
+
...(property.initializer !== undefined ? { initializer: property.initializer } : {}),
|
|
94
|
+
})),
|
|
95
|
+
methods: args.methods.map((method) => ({
|
|
96
|
+
name: method.name,
|
|
97
|
+
isAsync: method.is_async,
|
|
98
|
+
returnType: method.return_type,
|
|
99
|
+
accessModifier: method.access,
|
|
100
|
+
params: method.parameters.map((parameter) => ({
|
|
101
|
+
name: parameter.name,
|
|
102
|
+
type: parameter.type,
|
|
103
|
+
})),
|
|
104
|
+
decorators: method.decorators,
|
|
105
|
+
docs: method.docs,
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function registerScaffoldClass(server) {
|
|
110
|
+
// The SDK compatibility overload exceeds TypeScript's instantiation depth for this schema.
|
|
111
|
+
// Keep the complete runtime schema and isolate only the registrar type.
|
|
112
|
+
const registerTool = server.registerTool.bind(server);
|
|
113
|
+
registerTool("ast_scaffold_class", {
|
|
114
|
+
title: "Prepare Class Scaffold",
|
|
115
|
+
description: "Prepares a hash-bound new TypeScript class file without writing it. Each method body throws an explicit not-implemented error. Review the diff, then apply the returned operation_id and plan_hash with ast_apply_operation.",
|
|
116
|
+
inputSchema: ScaffoldClassInputSchema,
|
|
117
|
+
outputSchema: ScaffoldClassOutputSchema.shape,
|
|
118
|
+
annotations: {
|
|
119
|
+
readOnlyHint: true,
|
|
120
|
+
destructiveHint: false,
|
|
121
|
+
idempotentHint: false,
|
|
122
|
+
openWorldHint: false,
|
|
123
|
+
},
|
|
124
|
+
}, async (args) => {
|
|
125
|
+
try {
|
|
126
|
+
const prepared = await prepareScaffoldClass({
|
|
127
|
+
projectRoot: args.project_root,
|
|
128
|
+
filePath: args.file_path,
|
|
129
|
+
spec: toSpec(args),
|
|
130
|
+
allowNewErrors: args.allow_new_errors,
|
|
131
|
+
});
|
|
132
|
+
const structuredContent = {
|
|
133
|
+
...serializePreparedOperation(prepared.operation),
|
|
134
|
+
file: prepared.file,
|
|
135
|
+
class_name: prepared.className,
|
|
136
|
+
outline: prepared.outline,
|
|
137
|
+
pending_methods: prepared.pendingMethods,
|
|
138
|
+
};
|
|
139
|
+
return structuredResult(structuredContent);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return errorResult(error);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { buildFileOutline } from "../services/outline.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createPaginationInputSchema, PaginationOutputSchema, paginate, } from "../services/pagination.js";
|
|
5
5
|
import { withProject } from "../services/project.js";
|
|
6
|
-
import { collectSymbols } from "../services/symbols.js";
|
|
6
|
+
import { collectSymbols, symbolMatchRank } from "../services/symbols.js";
|
|
7
7
|
import { errorResult, formattedResult, ToolOutputFormatInputSchema } from "./result.js";
|
|
8
|
+
const SEARCH_DEFAULT_LIMIT = 20;
|
|
9
|
+
const SearchDetailSchema = z.enum(["selectors", "summary", "full"]).default("summary");
|
|
8
10
|
const AstSearchSymbolsInputSchema = z.object({
|
|
9
11
|
project_root: z
|
|
10
12
|
.string()
|
|
@@ -21,10 +23,11 @@ const AstSearchSymbolsInputSchema = z.object({
|
|
|
21
23
|
.string()
|
|
22
24
|
.optional()
|
|
23
25
|
.describe("Optional case-insensitive substring matched against project-relative file paths."),
|
|
26
|
+
detail: SearchDetailSchema.describe("Result detail: selectors for routing, summary adds signatures, full adds redundant declaration fields."),
|
|
24
27
|
...ToolOutputFormatInputSchema,
|
|
25
|
-
...
|
|
28
|
+
...createPaginationInputSchema(SEARCH_DEFAULT_LIMIT),
|
|
26
29
|
});
|
|
27
|
-
const
|
|
30
|
+
const FullSearchSymbolSchema = z.object({
|
|
28
31
|
file: z.string(),
|
|
29
32
|
symbol_path: z.string(),
|
|
30
33
|
selector: z.string(),
|
|
@@ -33,15 +36,51 @@ const SearchSymbolSchema = z.object({
|
|
|
33
36
|
line: z.number().int().positive(),
|
|
34
37
|
signature: z.string(),
|
|
35
38
|
});
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const SelectorSearchSymbolSchema = FullSearchSymbolSchema.pick({
|
|
40
|
+
file: true,
|
|
41
|
+
selector: true,
|
|
42
|
+
kind: true,
|
|
40
43
|
});
|
|
44
|
+
const SummarySearchSymbolSchema = FullSearchSymbolSchema.pick({
|
|
45
|
+
file: true,
|
|
46
|
+
selector: true,
|
|
47
|
+
kind: true,
|
|
48
|
+
signature: true,
|
|
49
|
+
});
|
|
50
|
+
const SearchOutputSchemas = {
|
|
51
|
+
selectors: z.object({
|
|
52
|
+
symbols: z.array(SelectorSearchSymbolSchema),
|
|
53
|
+
duration_ms: z.number().min(0),
|
|
54
|
+
...PaginationOutputSchema,
|
|
55
|
+
}),
|
|
56
|
+
summary: z.object({
|
|
57
|
+
symbols: z.array(SummarySearchSymbolSchema),
|
|
58
|
+
duration_ms: z.number().min(0),
|
|
59
|
+
...PaginationOutputSchema,
|
|
60
|
+
}),
|
|
61
|
+
full: z.object({
|
|
62
|
+
symbols: z.array(FullSearchSymbolSchema),
|
|
63
|
+
duration_ms: z.number().min(0),
|
|
64
|
+
...PaginationOutputSchema,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
function projectSymbols(detail, symbols) {
|
|
68
|
+
if (detail === "full")
|
|
69
|
+
return symbols;
|
|
70
|
+
if (detail === "summary") {
|
|
71
|
+
return symbols.map(({ file, selector, kind, signature }) => ({
|
|
72
|
+
file,
|
|
73
|
+
selector,
|
|
74
|
+
kind,
|
|
75
|
+
signature,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
return symbols.map(({ file, selector, kind }) => ({ file, selector, kind }));
|
|
79
|
+
}
|
|
41
80
|
export function registerSearchSymbols(server) {
|
|
42
81
|
server.registerTool("ast_search_symbols", {
|
|
43
82
|
title: "Search project symbols",
|
|
44
|
-
description: "
|
|
83
|
+
description: "Ranks structural declarations and returns exact file/symbol selectors for downstream AST tools.",
|
|
45
84
|
inputSchema: AstSearchSymbolsInputSchema,
|
|
46
85
|
annotations: {
|
|
47
86
|
readOnlyHint: true,
|
|
@@ -49,7 +88,7 @@ export function registerSearchSymbols(server) {
|
|
|
49
88
|
idempotentHint: true,
|
|
50
89
|
openWorldHint: false,
|
|
51
90
|
},
|
|
52
|
-
}, async ({ project_root, query, kinds, file_filter, output_format, offset, limit }) => {
|
|
91
|
+
}, async ({ project_root, query, kinds, file_filter, detail, output_format, offset, limit }) => {
|
|
53
92
|
try {
|
|
54
93
|
const structuredContent = await withProject(project_root, ({ project, projectRoot }) => {
|
|
55
94
|
const startedAt = performance.now();
|
|
@@ -62,9 +101,13 @@ export function registerSearchSymbols(server) {
|
|
|
62
101
|
if (normalizedFileFilter && !file.toLowerCase().includes(normalizedFileFilter)) {
|
|
63
102
|
continue;
|
|
64
103
|
}
|
|
65
|
-
const matchingSymbols = collectSymbols(sourceFile).filter((symbol) =>
|
|
66
|
-
|
|
67
|
-
|
|
104
|
+
const matchingSymbols = collectSymbols(sourceFile).filter((symbol) => {
|
|
105
|
+
const selector = `${symbol.symbolPath}@${symbol.line}`.toLowerCase();
|
|
106
|
+
return ((!kindSet || kindSet.has(symbol.kind)) &&
|
|
107
|
+
(symbol.name.toLowerCase().includes(normalizedQuery) ||
|
|
108
|
+
symbol.symbolPath.toLowerCase().includes(normalizedQuery) ||
|
|
109
|
+
selector.includes(normalizedQuery)));
|
|
110
|
+
});
|
|
68
111
|
if (matchingSymbols.length === 0)
|
|
69
112
|
continue;
|
|
70
113
|
const signatures = new Map(buildFileOutline(sourceFile).symbols.map((symbol) => [
|
|
@@ -83,16 +126,34 @@ export function registerSearchSymbols(server) {
|
|
|
83
126
|
});
|
|
84
127
|
}
|
|
85
128
|
}
|
|
86
|
-
matches.sort((left, right) =>
|
|
129
|
+
matches.sort((left, right) => {
|
|
130
|
+
const rank = symbolMatchRank(query, {
|
|
131
|
+
symbolPath: left.symbol_path,
|
|
132
|
+
name: left.name,
|
|
133
|
+
line: left.line,
|
|
134
|
+
}) -
|
|
135
|
+
symbolMatchRank(query, {
|
|
136
|
+
symbolPath: right.symbol_path,
|
|
137
|
+
name: right.name,
|
|
138
|
+
line: right.line,
|
|
139
|
+
});
|
|
140
|
+
if (rank !== 0)
|
|
141
|
+
return rank;
|
|
142
|
+
return (left.file.localeCompare(right.file) ||
|
|
143
|
+
left.line - right.line ||
|
|
144
|
+
left.symbol_path.localeCompare(right.symbol_path) ||
|
|
145
|
+
left.kind.localeCompare(right.kind));
|
|
146
|
+
});
|
|
87
147
|
const page = paginate(matches, offset, limit);
|
|
88
148
|
const { items, ...metadata } = page;
|
|
89
149
|
return {
|
|
90
|
-
symbols: items,
|
|
150
|
+
symbols: projectSymbols(detail, items),
|
|
91
151
|
duration_ms: performance.now() - startedAt,
|
|
92
152
|
...metadata,
|
|
93
153
|
};
|
|
94
154
|
});
|
|
95
|
-
|
|
155
|
+
const outputSchema = SearchOutputSchemas[detail];
|
|
156
|
+
return formattedResult(outputSchema, structuredContent, output_format);
|
|
96
157
|
}
|
|
97
158
|
catch (error) {
|
|
98
159
|
return errorResult(error);
|