@aiready/ast-mcp-server 0.1.6 → 0.3.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/dist/{chunk-KTDNJP4B.js → chunk-FQP7SHLM.js} +4 -10
- package/dist/chunk-FQP7SHLM.js.map +1 -0
- package/dist/index.cjs +61 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +61 -37
- package/dist/index.js.map +1 -1
- package/dist/search-code-63QOEFQN.js +8 -0
- package/package.json +1 -1
- package/dist/chunk-KTDNJP4B.js.map +0 -1
- package/dist/search-code-NPNOCF3R.js +0 -8
- /package/dist/{search-code-NPNOCF3R.js.map → search-code-63QOEFQN.js.map} +0 -0
|
@@ -27,15 +27,9 @@ function validateWorkspacePath(inputPath) {
|
|
|
27
27
|
|
|
28
28
|
// src/tools/search-code.ts
|
|
29
29
|
var execFileAsync = promisify(execFile);
|
|
30
|
-
async function searchCode(pattern, searchPath, filePattern, limit = 50, regex = true) {
|
|
30
|
+
async function searchCode(pattern, searchPath, filePattern, limit = 50, regex = true, offset = 0) {
|
|
31
31
|
const safePath = validateWorkspacePath(searchPath);
|
|
32
|
-
const args = [
|
|
33
|
-
"--json",
|
|
34
|
-
"--max-count",
|
|
35
|
-
limit.toString(),
|
|
36
|
-
"--max-columns",
|
|
37
|
-
"500"
|
|
38
|
-
];
|
|
32
|
+
const args = ["--json", "--max-columns", "500"];
|
|
39
33
|
if (!regex) {
|
|
40
34
|
args.push("--fixed-strings");
|
|
41
35
|
}
|
|
@@ -66,7 +60,7 @@ async function searchCode(pattern, searchPath, filePattern, limit = 50, regex =
|
|
|
66
60
|
}
|
|
67
61
|
}
|
|
68
62
|
}
|
|
69
|
-
return results.slice(
|
|
63
|
+
return results.slice(offset, offset + limit);
|
|
70
64
|
} catch (error) {
|
|
71
65
|
if (error.code === 1) {
|
|
72
66
|
return [];
|
|
@@ -79,4 +73,4 @@ export {
|
|
|
79
73
|
validateWorkspacePath,
|
|
80
74
|
searchCode
|
|
81
75
|
};
|
|
82
|
-
//# sourceMappingURL=chunk-
|
|
76
|
+
//# sourceMappingURL=chunk-FQP7SHLM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/search-code.ts","../src/security.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport { promisify } from 'util';\nimport { rgPath } from '@vscode/ripgrep';\nimport { validateWorkspacePath } from '../security.js';\n\nconst execFileAsync = promisify(execFile);\n\nexport interface SearchResult {\n file: string;\n line: number;\n column: number;\n text: string;\n}\n\nexport async function searchCode(\n pattern: string,\n searchPath: string,\n filePattern?: string,\n limit: number = 50,\n regex: boolean = true,\n offset: number = 0\n): Promise<SearchResult[]> {\n const safePath = validateWorkspacePath(searchPath);\n\n const args = ['--json', '--max-columns', '500'];\n\n if (!regex) {\n args.push('--fixed-strings');\n }\n\n args.push(pattern, safePath);\n\n if (filePattern) {\n args.push('--glob', filePattern);\n }\n\n // Common exclusions\n args.push('--glob', '!**/node_modules/**');\n args.push('--glob', '!**/dist/**');\n args.push('--glob', '!**/.git/**');\n\n try {\n const { stdout } = await execFileAsync(rgPath, args);\n const lines = stdout.split('\\n').filter(Boolean);\n const results: SearchResult[] = [];\n\n for (const line of lines) {\n const data = JSON.parse(line);\n if (data.type === 'match') {\n const file = data.data.path.text;\n const lineNumber = data.data.line_number;\n const submatches = data.data.submatches;\n\n for (const submatch of submatches) {\n results.push({\n file,\n line: lineNumber,\n column: submatch.start,\n text: data.data.lines.text.trim(),\n });\n }\n }\n }\n\n return results.slice(offset, offset + limit);\n } catch (error: any) {\n if (error.code === 1) {\n // rg returns 1 if no matches found\n return [];\n }\n throw error;\n }\n}\n","import path from 'path';\nimport fs from 'fs';\n\nexport function resolveWorkspaceRoot(): string {\n return process.env.AST_WORKSPACE_ROOT || process.cwd();\n}\n\nexport function validateWorkspacePath(inputPath: string): string {\n const root = resolveWorkspaceRoot();\n const resolved = path.resolve(root, inputPath);\n const normalized = path.normalize(resolved);\n\n // Reject path traversal\n if (!normalized.startsWith(root)) {\n throw new Error(\n `Path traversal detected: ${inputPath} escapes workspace root`\n );\n }\n\n // Reject null bytes\n if (normalized.includes('\\0')) {\n throw new Error('Path contains null bytes');\n }\n\n return normalized;\n}\n\nexport function validateFileExists(filePath: string): string {\n const safe = validateWorkspacePath(filePath);\n if (!fs.existsSync(safe)) {\n throw new Error(`File not found: ${filePath}`);\n }\n if (!fs.statSync(safe).isFile()) {\n throw new Error(`Not a file: ${filePath}`);\n }\n return safe;\n}\n"],"mappings":";;;AAAA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAC1B,SAAS,cAAc;;;ACFvB,OAAO,UAAU;AAGV,SAAS,uBAA+B;AAC7C,SAAO,QAAQ,IAAI,sBAAsB,QAAQ,IAAI;AACvD;AAEO,SAAS,sBAAsB,WAA2B;AAC/D,QAAM,OAAO,qBAAqB;AAClC,QAAM,WAAW,KAAK,QAAQ,MAAM,SAAS;AAC7C,QAAM,aAAa,KAAK,UAAU,QAAQ;AAG1C,MAAI,CAAC,WAAW,WAAW,IAAI,GAAG;AAChC,UAAM,IAAI;AAAA,MACR,4BAA4B,SAAS;AAAA,IACvC;AAAA,EACF;AAGA,MAAI,WAAW,SAAS,IAAI,GAAG;AAC7B,UAAM,IAAI,MAAM,0BAA0B;AAAA,EAC5C;AAEA,SAAO;AACT;;;ADpBA,IAAM,gBAAgB,UAAU,QAAQ;AASxC,eAAsB,WACpB,SACA,YACA,aACA,QAAgB,IAChB,QAAiB,MACjB,SAAiB,GACQ;AACzB,QAAM,WAAW,sBAAsB,UAAU;AAEjD,QAAM,OAAO,CAAC,UAAU,iBAAiB,KAAK;AAE9C,MAAI,CAAC,OAAO;AACV,SAAK,KAAK,iBAAiB;AAAA,EAC7B;AAEA,OAAK,KAAK,SAAS,QAAQ;AAE3B,MAAI,aAAa;AACf,SAAK,KAAK,UAAU,WAAW;AAAA,EACjC;AAGA,OAAK,KAAK,UAAU,qBAAqB;AACzC,OAAK,KAAK,UAAU,aAAa;AACjC,OAAK,KAAK,UAAU,aAAa;AAEjC,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,cAAc,QAAQ,IAAI;AACnD,UAAM,QAAQ,OAAO,MAAM,IAAI,EAAE,OAAO,OAAO;AAC/C,UAAM,UAA0B,CAAC;AAEjC,eAAW,QAAQ,OAAO;AACxB,YAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,UAAI,KAAK,SAAS,SAAS;AACzB,cAAM,OAAO,KAAK,KAAK,KAAK;AAC5B,cAAM,aAAa,KAAK,KAAK;AAC7B,cAAM,aAAa,KAAK,KAAK;AAE7B,mBAAW,YAAY,YAAY;AACjC,kBAAQ,KAAK;AAAA,YACX;AAAA,YACA,MAAM;AAAA,YACN,QAAQ,SAAS;AAAA,YACjB,MAAM,KAAK,KAAK,MAAM,KAAK,KAAK;AAAA,UAClC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,WAAO,QAAQ,MAAM,QAAQ,SAAS,KAAK;AAAA,EAC7C,SAAS,OAAY;AACnB,QAAI,MAAM,SAAS,GAAG;AAEpB,aAAO,CAAC;AAAA,IACV;AACA,UAAM;AAAA,EACR;AACF;","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -73,15 +73,9 @@ var search_code_exports = {};
|
|
|
73
73
|
__export(search_code_exports, {
|
|
74
74
|
searchCode: () => searchCode
|
|
75
75
|
});
|
|
76
|
-
async function searchCode(pattern, searchPath, filePattern, limit = 50, regex = true) {
|
|
76
|
+
async function searchCode(pattern, searchPath, filePattern, limit = 50, regex = true, offset = 0) {
|
|
77
77
|
const safePath = validateWorkspacePath(searchPath);
|
|
78
|
-
const args = [
|
|
79
|
-
"--json",
|
|
80
|
-
"--max-count",
|
|
81
|
-
limit.toString(),
|
|
82
|
-
"--max-columns",
|
|
83
|
-
"500"
|
|
84
|
-
];
|
|
78
|
+
const args = ["--json", "--max-columns", "500"];
|
|
85
79
|
if (!regex) {
|
|
86
80
|
args.push("--fixed-strings");
|
|
87
81
|
}
|
|
@@ -112,7 +106,7 @@ async function searchCode(pattern, searchPath, filePattern, limit = 50, regex =
|
|
|
112
106
|
}
|
|
113
107
|
}
|
|
114
108
|
}
|
|
115
|
-
return results.slice(
|
|
109
|
+
return results.slice(offset, offset + limit);
|
|
116
110
|
} catch (error) {
|
|
117
111
|
if (error.code === 1) {
|
|
118
112
|
return [];
|
|
@@ -172,7 +166,8 @@ var SearchCodeSchema = import_zod.z.object({
|
|
|
172
166
|
pattern: import_zod.z.string().describe("Search pattern (regex by default)"),
|
|
173
167
|
path: import_zod.z.string().describe("Directory to search in"),
|
|
174
168
|
filePattern: import_zod.z.string().optional().describe('Glob filter (e.g., "*.ts")'),
|
|
175
|
-
limit: import_zod.z.number().optional().default(50).describe("Max matches to return"),
|
|
169
|
+
limit: import_zod.z.number().optional().default(50).describe("Max matches to return (default 50)"),
|
|
170
|
+
offset: import_zod.z.number().optional().default(0).describe("Pagination offset (default 0)"),
|
|
176
171
|
regex: import_zod.z.boolean().optional().default(true).describe("Use regex mode (default true)")
|
|
177
172
|
});
|
|
178
173
|
var GetSymbolDocsSchema = import_zod.z.object({
|
|
@@ -190,6 +185,7 @@ init_cjs_shims();
|
|
|
190
185
|
init_cjs_shims();
|
|
191
186
|
var import_ts_morph3 = require("ts-morph");
|
|
192
187
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
188
|
+
var import_path5 = __toESM(require("path"), 1);
|
|
193
189
|
|
|
194
190
|
// src/project-manager.ts
|
|
195
191
|
init_cjs_shims();
|
|
@@ -462,6 +458,9 @@ var SymbolIndex = class {
|
|
|
462
458
|
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
463
459
|
return this.computeStats(cache, duration, Math.round(memoryUsage));
|
|
464
460
|
}
|
|
461
|
+
isInitialized() {
|
|
462
|
+
return Object.keys(this.index).length > 0;
|
|
463
|
+
}
|
|
465
464
|
lookup(name) {
|
|
466
465
|
return this.index[name] || [];
|
|
467
466
|
}
|
|
@@ -552,8 +551,23 @@ var TypeScriptAdapter = class {
|
|
|
552
551
|
const poolSize = parseInt(process.env.AST_WORKER_POOL_SIZE || "2");
|
|
553
552
|
this.pool = new WorkerPool(poolSize);
|
|
554
553
|
}
|
|
555
|
-
async
|
|
556
|
-
|
|
554
|
+
async ensureIndex(p) {
|
|
555
|
+
if (!symbolIndex.isInitialized()) {
|
|
556
|
+
const absolutePath = import_path5.default.resolve(p);
|
|
557
|
+
let searchPath = absolutePath;
|
|
558
|
+
if (!import_fs3.default.statSync(absolutePath).isDirectory()) {
|
|
559
|
+
const tsconfig = await projectManager.findNearestTsConfig(absolutePath);
|
|
560
|
+
searchPath = tsconfig ? import_path5.default.dirname(tsconfig) : import_path5.default.dirname(absolutePath);
|
|
561
|
+
}
|
|
562
|
+
console.error(
|
|
563
|
+
`[AST] Index not initialized. Building for ${searchPath}...`
|
|
564
|
+
);
|
|
565
|
+
await symbolIndex.buildIndex(searchPath);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async resolveDefinition(symbolName, path6) {
|
|
569
|
+
validateWorkspacePath(path6);
|
|
570
|
+
await this.ensureIndex(path6);
|
|
557
571
|
const indexHits = symbolIndex.lookup(symbolName);
|
|
558
572
|
if (indexHits.length > 0) {
|
|
559
573
|
const results = [];
|
|
@@ -581,32 +595,33 @@ var TypeScriptAdapter = class {
|
|
|
581
595
|
}
|
|
582
596
|
return results;
|
|
583
597
|
}
|
|
584
|
-
if (import_fs3.default.statSync(
|
|
598
|
+
if (import_fs3.default.statSync(path6).isDirectory()) {
|
|
585
599
|
return [];
|
|
586
600
|
}
|
|
587
|
-
const tsconfig = await projectManager.findNearestTsConfig(
|
|
601
|
+
const tsconfig = await projectManager.findNearestTsConfig(path6);
|
|
588
602
|
if (!tsconfig) return [];
|
|
589
603
|
try {
|
|
590
604
|
const result = await this.pool.execute(
|
|
591
605
|
"resolve_definition",
|
|
592
606
|
{
|
|
593
607
|
tsconfig,
|
|
594
|
-
file:
|
|
608
|
+
file: path6,
|
|
595
609
|
symbol: symbolName
|
|
596
610
|
}
|
|
597
611
|
);
|
|
598
612
|
return result;
|
|
599
613
|
} catch {
|
|
600
614
|
const project = projectManager.ensureProject(tsconfig);
|
|
601
|
-
const sourceFile = project.addSourceFileAtPathIfExists(
|
|
615
|
+
const sourceFile = project.addSourceFileAtPathIfExists(path6);
|
|
602
616
|
if (!sourceFile) return [];
|
|
603
617
|
const exported = sourceFile.getExportedDeclarations().get(symbolName);
|
|
604
618
|
if (!exported) return [];
|
|
605
619
|
return exported.map((decl) => this.mapToDefinitionLocation(decl));
|
|
606
620
|
}
|
|
607
621
|
}
|
|
608
|
-
async findReferences(symbolName,
|
|
609
|
-
validateWorkspacePath(
|
|
622
|
+
async findReferences(symbolName, path6, limit = 50, offset = 0) {
|
|
623
|
+
validateWorkspacePath(path6);
|
|
624
|
+
await this.ensureIndex(path6);
|
|
610
625
|
const hits = symbolIndex.lookup(symbolName);
|
|
611
626
|
if (hits.length === 0) return { references: [], total_count: 0 };
|
|
612
627
|
const hit = hits[0];
|
|
@@ -623,7 +638,7 @@ var TypeScriptAdapter = class {
|
|
|
623
638
|
const { searchCode: searchCode2 } = await Promise.resolve().then(() => (init_search_code(), search_code_exports));
|
|
624
639
|
const searchResults = await searchCode2(
|
|
625
640
|
symbolName,
|
|
626
|
-
|
|
641
|
+
path6,
|
|
627
642
|
"*.{ts,tsx,js,jsx}",
|
|
628
643
|
1e3,
|
|
629
644
|
false
|
|
@@ -655,8 +670,9 @@ var TypeScriptAdapter = class {
|
|
|
655
670
|
total_count: unique.length
|
|
656
671
|
};
|
|
657
672
|
}
|
|
658
|
-
async findImplementations(symbolName,
|
|
659
|
-
validateWorkspacePath(
|
|
673
|
+
async findImplementations(symbolName, path6, limit = 50, offset = 0) {
|
|
674
|
+
validateWorkspacePath(path6);
|
|
675
|
+
await this.ensureIndex(path6);
|
|
660
676
|
const hits = symbolIndex.lookup(symbolName);
|
|
661
677
|
if (hits.length === 0) return { implementations: [], total_count: 0 };
|
|
662
678
|
const hit = hits[0];
|
|
@@ -687,7 +703,7 @@ var TypeScriptAdapter = class {
|
|
|
687
703
|
const { searchCode: searchCode2 } = await Promise.resolve().then(() => (init_search_code(), search_code_exports));
|
|
688
704
|
const searchResults = await searchCode2(
|
|
689
705
|
symbolName,
|
|
690
|
-
|
|
706
|
+
path6,
|
|
691
707
|
"*.{ts,tsx,js,jsx}",
|
|
692
708
|
1e3,
|
|
693
709
|
false
|
|
@@ -866,22 +882,22 @@ var TypeScriptAdapter = class {
|
|
|
866
882
|
var typescriptAdapter = new TypeScriptAdapter();
|
|
867
883
|
|
|
868
884
|
// src/tools/resolve-definition.ts
|
|
869
|
-
async function resolveDefinition(symbol,
|
|
870
|
-
return await typescriptAdapter.resolveDefinition(symbol,
|
|
885
|
+
async function resolveDefinition(symbol, path6) {
|
|
886
|
+
return await typescriptAdapter.resolveDefinition(symbol, path6);
|
|
871
887
|
}
|
|
872
888
|
|
|
873
889
|
// src/tools/find-references.ts
|
|
874
890
|
init_cjs_shims();
|
|
875
|
-
async function findReferences(symbol,
|
|
876
|
-
return await typescriptAdapter.findReferences(symbol,
|
|
891
|
+
async function findReferences(symbol, path6, limit = 50, offset = 0) {
|
|
892
|
+
return await typescriptAdapter.findReferences(symbol, path6, limit, offset);
|
|
877
893
|
}
|
|
878
894
|
|
|
879
895
|
// src/tools/find-implementations.ts
|
|
880
896
|
init_cjs_shims();
|
|
881
|
-
async function findImplementations(symbol,
|
|
897
|
+
async function findImplementations(symbol, path6, limit = 50, offset = 0) {
|
|
882
898
|
return await typescriptAdapter.findImplementations(
|
|
883
899
|
symbol,
|
|
884
|
-
|
|
900
|
+
path6,
|
|
885
901
|
limit,
|
|
886
902
|
offset
|
|
887
903
|
);
|
|
@@ -928,8 +944,8 @@ async function getSymbolDocs(symbol, filePath) {
|
|
|
928
944
|
|
|
929
945
|
// src/tools/build-symbol-index.ts
|
|
930
946
|
init_cjs_shims();
|
|
931
|
-
async function buildSymbolIndex(
|
|
932
|
-
return await symbolIndex.buildIndex(
|
|
947
|
+
async function buildSymbolIndex(path6) {
|
|
948
|
+
return await symbolIndex.buildIndex(path6);
|
|
933
949
|
}
|
|
934
950
|
|
|
935
951
|
// src/index.ts
|
|
@@ -1024,6 +1040,7 @@ var ASTExplorerServer = class {
|
|
|
1024
1040
|
path: { type: "string", description: "Directory to search" },
|
|
1025
1041
|
filePattern: { type: "string", description: "Glob filter" },
|
|
1026
1042
|
limit: { type: "number", default: 50 },
|
|
1043
|
+
offset: { type: "number", default: 0 },
|
|
1027
1044
|
regex: { type: "boolean", default: true }
|
|
1028
1045
|
},
|
|
1029
1046
|
required: ["pattern", "path"]
|
|
@@ -1060,8 +1077,8 @@ var ASTExplorerServer = class {
|
|
|
1060
1077
|
try {
|
|
1061
1078
|
switch (name) {
|
|
1062
1079
|
case "resolve_definition": {
|
|
1063
|
-
const { symbol, path:
|
|
1064
|
-
const results = await resolveDefinition(symbol,
|
|
1080
|
+
const { symbol, path: path6 } = ResolveDefinitionSchema.parse(args);
|
|
1081
|
+
const results = await resolveDefinition(symbol, path6);
|
|
1065
1082
|
return {
|
|
1066
1083
|
content: [
|
|
1067
1084
|
{ type: "text", text: JSON.stringify(results, null, 2) }
|
|
@@ -1069,8 +1086,8 @@ var ASTExplorerServer = class {
|
|
|
1069
1086
|
};
|
|
1070
1087
|
}
|
|
1071
1088
|
case "find_references": {
|
|
1072
|
-
const { symbol, path:
|
|
1073
|
-
const results = await findReferences(symbol,
|
|
1089
|
+
const { symbol, path: path6, limit, offset } = FindReferencesSchema.parse(args);
|
|
1090
|
+
const results = await findReferences(symbol, path6, limit, offset);
|
|
1074
1091
|
return {
|
|
1075
1092
|
content: [
|
|
1076
1093
|
{ type: "text", text: JSON.stringify(results, null, 2) }
|
|
@@ -1078,10 +1095,10 @@ var ASTExplorerServer = class {
|
|
|
1078
1095
|
};
|
|
1079
1096
|
}
|
|
1080
1097
|
case "find_implementations": {
|
|
1081
|
-
const { symbol, path:
|
|
1098
|
+
const { symbol, path: path6, limit, offset } = FindImplementationsSchema.parse(args);
|
|
1082
1099
|
const results = await findImplementations(
|
|
1083
1100
|
symbol,
|
|
1084
|
-
|
|
1101
|
+
path6,
|
|
1085
1102
|
limit,
|
|
1086
1103
|
offset
|
|
1087
1104
|
);
|
|
@@ -1101,13 +1118,14 @@ var ASTExplorerServer = class {
|
|
|
1101
1118
|
};
|
|
1102
1119
|
}
|
|
1103
1120
|
case "search_code": {
|
|
1104
|
-
const { pattern, path:
|
|
1121
|
+
const { pattern, path: path6, filePattern, limit, offset, regex } = SearchCodeSchema.parse(args);
|
|
1105
1122
|
const results = await searchCode(
|
|
1106
1123
|
pattern,
|
|
1107
|
-
|
|
1124
|
+
path6,
|
|
1108
1125
|
filePattern,
|
|
1109
1126
|
limit,
|
|
1110
|
-
regex
|
|
1127
|
+
regex,
|
|
1128
|
+
offset
|
|
1111
1129
|
);
|
|
1112
1130
|
return {
|
|
1113
1131
|
content: [
|
|
@@ -1116,15 +1134,15 @@ var ASTExplorerServer = class {
|
|
|
1116
1134
|
};
|
|
1117
1135
|
}
|
|
1118
1136
|
case "get_symbol_docs": {
|
|
1119
|
-
const { symbol, path:
|
|
1120
|
-
const docs = await getSymbolDocs(symbol,
|
|
1137
|
+
const { symbol, path: path6 } = GetSymbolDocsSchema.parse(args);
|
|
1138
|
+
const docs = await getSymbolDocs(symbol, path6);
|
|
1121
1139
|
return {
|
|
1122
1140
|
content: [{ type: "text", text: JSON.stringify(docs, null, 2) }]
|
|
1123
1141
|
};
|
|
1124
1142
|
}
|
|
1125
1143
|
case "build_symbol_index": {
|
|
1126
|
-
const { path:
|
|
1127
|
-
const stats = await buildSymbolIndex(
|
|
1144
|
+
const { path: path6 } = BuildSymbolIndexSchema.parse(args);
|
|
1145
|
+
const stats = await buildSymbolIndex(path6);
|
|
1128
1146
|
return {
|
|
1129
1147
|
content: [{ type: "text", text: JSON.stringify(stats, null, 2) }]
|
|
1130
1148
|
};
|