@a13xu/lucid 1.9.2 → 1.9.5
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/README.md +16 -1
- package/build/index.js +2 -2
- package/build/indexer/ast.js +28 -0
- package/build/indexer/file.d.ts +2 -0
- package/build/indexer/file.js +46 -8
- package/build/indexer/project.js +79 -27
- package/build/tools/sync.js +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# @a13xu/lucid
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@a13xu/lucid)
|
|
4
|
+
[](https://www.npmjs.com/package/@a13xu/lucid)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
> **MCP server for Claude Code** — persistent memory, smart code indexing, and code quality validation. Works out of the box with zero configuration.
|
|
8
|
+
|
|
3
9
|
Token-efficient memory, code indexing, and validation for Claude Code agents — backed by **SQLite + FTS5**.
|
|
4
10
|
|
|
5
|
-
Stores a persistent knowledge graph (entities, relations, observations), indexes source files as compressed binary with change detection, retrieves minimal relevant context via TF-IDF or Qdrant, and validates code for LLM drift patterns.
|
|
11
|
+
Stores a persistent knowledge graph (entities, relations, observations), indexes source files as compressed binary with change detection, retrieves minimal relevant context via TF-IDF or Qdrant, and validates code for LLM drift patterns. Supports TypeScript, JavaScript, Python, **Vue, Nuxt**.
|
|
6
12
|
|
|
7
13
|
## Install
|
|
8
14
|
|
|
@@ -226,6 +232,15 @@ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},
|
|
|
226
232
|
|
|
227
233
|
In Claude Code: run `/mcp` — you should see `lucid` with 20 tools.
|
|
228
234
|
|
|
235
|
+
## Contributing
|
|
236
|
+
|
|
237
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/a13xu/lucid).
|
|
238
|
+
|
|
239
|
+
1. Fork the repo
|
|
240
|
+
2. `npm install` → `npm run build`
|
|
241
|
+
3. Test locally: `claude mcp add --transport stdio lucid-dev -- node /path/to/lucid/build/index.js`
|
|
242
|
+
4. Open a PR
|
|
243
|
+
|
|
229
244
|
## Tech stack
|
|
230
245
|
|
|
231
246
|
- **Runtime:** Node.js 18+, TypeScript, ES modules
|
package/build/index.js
CHANGED
|
@@ -52,7 +52,7 @@ else {
|
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
// MCP Server
|
|
54
54
|
// ---------------------------------------------------------------------------
|
|
55
|
-
const server = new Server({ name: "lucid", version: "1.9.
|
|
55
|
+
const server = new Server({ name: "lucid", version: "1.9.5" }, { capabilities: { tools: {} } });
|
|
56
56
|
// ---------------------------------------------------------------------------
|
|
57
57
|
// Tool definitions
|
|
58
58
|
// ---------------------------------------------------------------------------
|
|
@@ -178,7 +178,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
178
178
|
type: "object",
|
|
179
179
|
properties: {
|
|
180
180
|
pattern: { type: "string", description: "Regex pattern to search for." },
|
|
181
|
-
language: { type: "string", enum: ["python", "javascript", "typescript", "generic"], description: "Filter by language." },
|
|
181
|
+
language: { type: "string", enum: ["python", "javascript", "typescript", "vue", "generic"], description: "Filter by language." },
|
|
182
182
|
context: { type: "number", description: "Lines of context before/after each match (0-10, default 2)." },
|
|
183
183
|
},
|
|
184
184
|
required: ["pattern"],
|
package/build/indexer/ast.js
CHANGED
|
@@ -108,6 +108,32 @@ function skeletonPython(source) {
|
|
|
108
108
|
return { imports, exports, todos, summary };
|
|
109
109
|
}
|
|
110
110
|
// ---------------------------------------------------------------------------
|
|
111
|
+
// Vue SFC
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
function skeletonVue(source) {
|
|
114
|
+
// Extract <script> or <script setup> block and run TS skeleton on it
|
|
115
|
+
const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
116
|
+
const sk = scriptMatch ? skeletonTS(scriptMatch[1]) : { imports: [], exports: [], todos: [], summary: "" };
|
|
117
|
+
// Prepend Vue macro signatures (defineProps, defineEmits, defineExpose)
|
|
118
|
+
const scriptContent = scriptMatch?.[1] ?? "";
|
|
119
|
+
for (const macro of ["defineProps", "defineEmits", "defineExpose"]) {
|
|
120
|
+
const m = scriptContent.match(new RegExp(`${macro}[\\s\\S]*?(?=\\n\\n|\\n[^\\s]|$)`, "m"));
|
|
121
|
+
if (m)
|
|
122
|
+
sk.exports.unshift(m[0].split("\n")[0].slice(0, 120));
|
|
123
|
+
}
|
|
124
|
+
// HTML comment as summary fallback
|
|
125
|
+
if (!sk.summary) {
|
|
126
|
+
const htmlComment = source.match(/<!--\s*([\s\S]*?)\s*-->/)?.[1];
|
|
127
|
+
if (htmlComment)
|
|
128
|
+
sk.summary = htmlComment.replace(/\n/g, " ").trim().slice(0, 150);
|
|
129
|
+
}
|
|
130
|
+
// Also note top-level template structure (first tag inside <template>)
|
|
131
|
+
const templateMatch = source.match(/<template[^>]*>\s*<(\w[\w-]*)/);
|
|
132
|
+
if (templateMatch)
|
|
133
|
+
sk.exports.unshift(`<template> root: <${templateMatch[1]}>`);
|
|
134
|
+
return sk;
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
111
137
|
// Generic (markdown, yaml, json, etc.)
|
|
112
138
|
// ---------------------------------------------------------------------------
|
|
113
139
|
function skeletonGeneric(source) {
|
|
@@ -135,6 +161,8 @@ export function extractSkeleton(source, language) {
|
|
|
135
161
|
return skeletonTS(source);
|
|
136
162
|
case "python":
|
|
137
163
|
return skeletonPython(source);
|
|
164
|
+
case "vue":
|
|
165
|
+
return skeletonVue(source);
|
|
138
166
|
default:
|
|
139
167
|
return skeletonGeneric(source);
|
|
140
168
|
}
|
package/build/indexer/file.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface FileIndex {
|
|
|
6
6
|
todos: string[];
|
|
7
7
|
language: string;
|
|
8
8
|
}
|
|
9
|
+
/** Build a FileIndex from already-read source — no IO. */
|
|
10
|
+
export declare function buildFileIndex(filepath: string, source: string): FileIndex;
|
|
9
11
|
export declare function indexFile(filepath: string): FileIndex | null;
|
|
10
12
|
export interface UpsertResult {
|
|
11
13
|
observations: string[];
|
package/build/indexer/file.js
CHANGED
|
@@ -36,6 +36,36 @@ function extractPython(source) {
|
|
|
36
36
|
}
|
|
37
37
|
return { exports, description, todos };
|
|
38
38
|
}
|
|
39
|
+
function extractVue(source) {
|
|
40
|
+
const exports = [];
|
|
41
|
+
const todos = [];
|
|
42
|
+
// Extract <script> or <script setup> block
|
|
43
|
+
const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
44
|
+
const scriptContent = scriptMatch?.[1] ?? "";
|
|
45
|
+
// defineExpose({ foo, bar }) — symbols available to parent components
|
|
46
|
+
const exposeMatch = scriptContent.match(/defineExpose\(\s*\{([^}]+)\}/);
|
|
47
|
+
if (exposeMatch) {
|
|
48
|
+
for (const m of exposeMatch[1].matchAll(/\b([a-zA-Z_]\w*)\b/g)) {
|
|
49
|
+
if (!["true", "false", "null", "undefined"].includes(m[1])) {
|
|
50
|
+
exports.push(m[1]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Regular named exports inside <script> (non-setup composables, types, etc.)
|
|
55
|
+
for (const m of scriptContent.matchAll(/export\s+(?:async\s+)?(?:function|class|const|type|interface|enum)\s+(\w+)/g)) {
|
|
56
|
+
exports.push(m[1]);
|
|
57
|
+
}
|
|
58
|
+
// TODOs from entire SFC (template + script + style)
|
|
59
|
+
for (const m of source.matchAll(/\/\/\s*(TODO|FIXME|HACK)[:\s]+(.+)/gi)) {
|
|
60
|
+
todos.push(`${m[1]}: ${m[2].trim()}`);
|
|
61
|
+
}
|
|
62
|
+
// Description: first HTML comment, then first JSDoc in script
|
|
63
|
+
const htmlComment = source.match(/<!--\s*([\s\S]*?)\s*-->/)?.[1] ?? "";
|
|
64
|
+
const jsdoc = scriptContent.match(/^\/\*\*([\s\S]*?)\*\//m)?.[1] ?? "";
|
|
65
|
+
const raw = htmlComment || jsdoc;
|
|
66
|
+
const description = raw.replace(/\s*\*\s*/g, " ").trim().slice(0, 200);
|
|
67
|
+
return { exports, description, todos };
|
|
68
|
+
}
|
|
39
69
|
function extractGeneric(source) {
|
|
40
70
|
const todos = [];
|
|
41
71
|
for (const m of source.matchAll(/(?:\/\/|#)\s*(TODO|FIXME|HACK)[:\s]+(.+)/gi)) {
|
|
@@ -43,14 +73,8 @@ function extractGeneric(source) {
|
|
|
43
73
|
}
|
|
44
74
|
return { exports: [], description: "", todos };
|
|
45
75
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
source = readFileSync(filepath, { encoding: "utf-8" });
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
76
|
+
/** Build a FileIndex from already-read source — no IO. */
|
|
77
|
+
export function buildFileIndex(filepath, source) {
|
|
54
78
|
const ext = extname(filepath).toLowerCase();
|
|
55
79
|
const module = filepath.replace(/\\/g, "/");
|
|
56
80
|
let extracted;
|
|
@@ -63,12 +87,26 @@ export function indexFile(filepath) {
|
|
|
63
87
|
extracted = extractPython(source);
|
|
64
88
|
language = "python";
|
|
65
89
|
}
|
|
90
|
+
else if (ext === ".vue") {
|
|
91
|
+
extracted = extractVue(source);
|
|
92
|
+
language = "vue";
|
|
93
|
+
}
|
|
66
94
|
else {
|
|
67
95
|
extracted = extractGeneric(source);
|
|
68
96
|
language = "generic";
|
|
69
97
|
}
|
|
70
98
|
return { module, language, ...extracted };
|
|
71
99
|
}
|
|
100
|
+
export function indexFile(filepath) {
|
|
101
|
+
let source;
|
|
102
|
+
try {
|
|
103
|
+
source = readFileSync(filepath, { encoding: "utf-8" });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return buildFileIndex(filepath, source);
|
|
109
|
+
}
|
|
72
110
|
export function upsertFileIndex(index, source, stmts) {
|
|
73
111
|
const fileHash = sha256(source);
|
|
74
112
|
// Change detection — skip everything se hash-ul e identic
|
package/build/indexer/project.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
2
2
|
import { join, extname, basename } from "path";
|
|
3
|
+
import { buildFileIndex, upsertFileIndex } from "./file.js";
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// Helpers
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
@@ -181,40 +182,74 @@ function indexLogicGuardianYaml(path, stmts, results) {
|
|
|
181
182
|
results.push({ entity: `${patternMatches.length} drift patterns`, type: "pattern", observations: patternMatches.length, source: "logic-guardian.yaml" });
|
|
182
183
|
}
|
|
183
184
|
}
|
|
185
|
+
function indexNuxtConfig(path, projectName, stmts, results) {
|
|
186
|
+
const content = readFile(path);
|
|
187
|
+
if (!content)
|
|
188
|
+
return;
|
|
189
|
+
const obs = [`nuxt config: ${basename(path)}`];
|
|
190
|
+
// Modules list: modules: ['@nuxtjs/...', ...]
|
|
191
|
+
const modulesMatch = content.match(/modules\s*:\s*\[([^\]]+)\]/);
|
|
192
|
+
if (modulesMatch) {
|
|
193
|
+
const modules = [...modulesMatch[1].matchAll(/['"`](@?[\w/@-]+)['"`]/g)].map((m) => m[1]);
|
|
194
|
+
if (modules.length > 0)
|
|
195
|
+
obs.push(`nuxt modules: ${modules.join(", ")}`);
|
|
196
|
+
}
|
|
197
|
+
// extends (Nuxt layers)
|
|
198
|
+
const extendsMatch = content.match(/extends\s*:\s*\[?['"`]([^'"`]+)['"`]/);
|
|
199
|
+
if (extendsMatch)
|
|
200
|
+
obs.push(`extends layer: ${extendsMatch[1]}`);
|
|
201
|
+
// ssr setting
|
|
202
|
+
const ssrMatch = content.match(/ssr\s*:\s*(true|false)/);
|
|
203
|
+
if (ssrMatch)
|
|
204
|
+
obs.push(`ssr: ${ssrMatch[1]}`);
|
|
205
|
+
// runtimeConfig public keys
|
|
206
|
+
const rtMatch = content.match(/runtimeConfig\s*:\s*\{([\s\S]*?)(?=\n\s{0,4}\w|\n\})/);
|
|
207
|
+
if (rtMatch) {
|
|
208
|
+
const keys = [...rtMatch[1].matchAll(/^\s{2,}(\w+)\s*:/gm)].map((m) => m[1]);
|
|
209
|
+
if (keys.length > 0)
|
|
210
|
+
obs.push(`runtimeConfig keys: ${keys.slice(0, 10).join(", ")}`);
|
|
211
|
+
}
|
|
212
|
+
upsert(stmts, projectName, "project", obs);
|
|
213
|
+
results.push({ entity: projectName, type: "project", observations: obs.length, source: basename(path) });
|
|
214
|
+
}
|
|
184
215
|
// Source file indexing — extrage exporturi, clase, funcții principale
|
|
185
|
-
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
186
|
-
const SKIP_DIRS = new Set([
|
|
216
|
+
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".py", ".go", ".rs"]);
|
|
217
|
+
const SKIP_DIRS = new Set([
|
|
218
|
+
"node_modules", ".git",
|
|
219
|
+
// Generic build output
|
|
220
|
+
"build", "dist",
|
|
221
|
+
// Python
|
|
222
|
+
"__pycache__", "venv", ".venv",
|
|
223
|
+
// Rust
|
|
224
|
+
"target",
|
|
225
|
+
// Next.js
|
|
226
|
+
".next",
|
|
227
|
+
// Nuxt
|
|
228
|
+
".nuxt", ".output",
|
|
229
|
+
// General
|
|
230
|
+
".cache", "coverage", ".nyc_output",
|
|
231
|
+
]);
|
|
187
232
|
const MAX_SOURCE_FILES = 10_000;
|
|
188
233
|
function indexSourceFile(filepath, rootDir, projectName, stmts) {
|
|
189
234
|
const content = readFile(filepath);
|
|
190
235
|
if (!content)
|
|
191
|
-
return [];
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (lang === ".py") {
|
|
202
|
-
for (const m of content.matchAll(/^(?:def|class|async def)\s+(\w+)/gm)) {
|
|
203
|
-
if (!m[1].startsWith("_"))
|
|
204
|
-
exports.push(m[1]);
|
|
205
|
-
}
|
|
236
|
+
return { exports: [], stored: false };
|
|
237
|
+
// Build structured index (language-aware, single read)
|
|
238
|
+
const fileIdx = buildFileIndex(filepath, content);
|
|
239
|
+
// Store compressed content in source file index → enables get_context() + grep_code()
|
|
240
|
+
const result = upsertFileIndex(fileIdx, content, stmts);
|
|
241
|
+
// Add exports to knowledge graph (for recall())
|
|
242
|
+
if (fileIdx.exports.length > 0) {
|
|
243
|
+
const relPath = filepath.replace(/\\/g, "/").replace(rootDir.replace(/\\/g, "/") + "/", "");
|
|
244
|
+
const obs = [`exports from ${relPath}: ${fileIdx.exports.slice(0, 10).join(", ")}`];
|
|
245
|
+
upsert(stmts, projectName, "project", obs);
|
|
206
246
|
}
|
|
207
|
-
|
|
208
|
-
return [];
|
|
209
|
-
// Cale relativă față de rădăcina proiectului
|
|
210
|
-
const relPath = filepath.replace(/\\/g, "/").replace(rootDir.replace(/\\/g, "/") + "/", "");
|
|
211
|
-
const obs = [`exports from ${relPath}: ${exports.slice(0, 10).join(", ")}`];
|
|
212
|
-
upsert(stmts, projectName, "project", obs);
|
|
213
|
-
return exports;
|
|
247
|
+
return { exports: fileIdx.exports, stored: result.stored };
|
|
214
248
|
}
|
|
215
249
|
function scanSources(dir, projectName, stmts, results) {
|
|
216
250
|
const rootDir = dir.replace(/\\/g, "/");
|
|
217
251
|
let fileCount = 0;
|
|
252
|
+
let storedCount = 0;
|
|
218
253
|
const exportedSymbols = [];
|
|
219
254
|
function walk(d) {
|
|
220
255
|
if (fileCount >= MAX_SOURCE_FILES)
|
|
@@ -241,9 +276,11 @@ function scanSources(dir, projectName, stmts, results) {
|
|
|
241
276
|
walk(full);
|
|
242
277
|
}
|
|
243
278
|
else if (SOURCE_EXTS.has(extname(entry).toLowerCase())) {
|
|
244
|
-
const syms = indexSourceFile(full, rootDir, projectName, stmts);
|
|
279
|
+
const { exports: syms, stored } = indexSourceFile(full, rootDir, projectName, stmts);
|
|
245
280
|
exportedSymbols.push(...syms);
|
|
246
281
|
fileCount++;
|
|
282
|
+
if (stored)
|
|
283
|
+
storedCount++;
|
|
247
284
|
if (fileCount >= MAX_SOURCE_FILES)
|
|
248
285
|
return;
|
|
249
286
|
}
|
|
@@ -255,7 +292,7 @@ function scanSources(dir, projectName, stmts, results) {
|
|
|
255
292
|
entity: projectName,
|
|
256
293
|
type: "project",
|
|
257
294
|
observations: fileCount,
|
|
258
|
-
source: `${fileCount} source files (${exportedSymbols.length} exports)`,
|
|
295
|
+
source: `${fileCount} source files (${storedCount} compressed, ${exportedSymbols.length} exports)`,
|
|
259
296
|
});
|
|
260
297
|
}
|
|
261
298
|
}
|
|
@@ -314,7 +351,22 @@ export function indexProject(directory, stmts) {
|
|
|
314
351
|
if (existsSync(join(dir, "logic-guardian.yaml"))) {
|
|
315
352
|
indexLogicGuardianYaml(join(dir, "logic-guardian.yaml"), stmts, results);
|
|
316
353
|
}
|
|
317
|
-
// 7.
|
|
354
|
+
// 7. Nuxt config files (nuxt.config.ts/.js, app.config.ts)
|
|
355
|
+
for (const p of ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mts"]) {
|
|
356
|
+
const full = join(dir, p);
|
|
357
|
+
if (existsSync(full)) {
|
|
358
|
+
indexNuxtConfig(full, projectName, stmts, results);
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
for (const p of ["app.config.ts", "app.config.js"]) {
|
|
363
|
+
const full = join(dir, p);
|
|
364
|
+
if (existsSync(full)) {
|
|
365
|
+
indexNuxtConfig(full, projectName, stmts, results);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// 8. Surse
|
|
318
370
|
scanSources(dir, projectName, stmts, results);
|
|
319
371
|
return results;
|
|
320
372
|
}
|
package/build/tools/sync.js
CHANGED
|
@@ -6,7 +6,7 @@ import { indexProject } from "../indexer/project.js";
|
|
|
6
6
|
import { computeDiff } from "../retrieval/context.js";
|
|
7
7
|
import { decompress } from "../store/content.js";
|
|
8
8
|
import { implicitRewardFromSync } from "../memory/experience.js";
|
|
9
|
-
const SUPPORTED_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
9
|
+
const SUPPORTED_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".py", ".go", ".rs"]);
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// sync_file
|
|
12
12
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a13xu/lucid",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
4
4
|
"description": "Token-efficient memory, code indexing, and validation for Claude Code agents — SQLite + FTS5, TF-IDF + Qdrant retrieval, AST skeleton pruning, diff-aware context, Logic Guardian drift detection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,17 +39,20 @@
|
|
|
39
39
|
"url": "https://github.com/a13xu/lucid.git"
|
|
40
40
|
},
|
|
41
41
|
"homepage": "https://github.com/a13xu/lucid#readme",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
42
45
|
"engines": {
|
|
43
46
|
"node": ">=18"
|
|
44
47
|
},
|
|
45
48
|
"dependencies": {
|
|
46
49
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
47
|
-
"better-sqlite3": "^
|
|
50
|
+
"better-sqlite3": "^12.0.0",
|
|
48
51
|
"zod": "^3.23.8"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
51
54
|
"@types/better-sqlite3": "^7.6.11",
|
|
52
|
-
"@types/node": "^
|
|
55
|
+
"@types/node": "^22.0.0",
|
|
53
56
|
"typescript": "^5.4.0"
|
|
54
57
|
}
|
|
55
58
|
}
|