@kalera/munin-runtime 0.1.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/.turbo/turbo-build.log +4 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +71 -0
- package/dist/mcp-server.d.ts +1 -0
- package/dist/mcp-server.js +139 -0
- package/package.json +22 -0
- package/src/index.ts +99 -0
- package/src/mcp-server.ts +152 -0
- package/tsconfig.json +8 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ParsedCliArgs {
|
|
2
|
+
action: string;
|
|
3
|
+
payload: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export interface CliEnv {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
project: string;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
retries: number;
|
|
11
|
+
backoffMs: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseCliArgs(argv: string[], usage: string): ParsedCliArgs;
|
|
14
|
+
export declare function loadCliEnv(): CliEnv;
|
|
15
|
+
export declare function executeWithRetry<T>(task: () => Promise<T>, retries: number, backoffMs: number): Promise<T>;
|
|
16
|
+
export declare function safeError(error: unknown): Record<string, unknown>;
|
|
17
|
+
export * from "./mcp-server.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const REDACT_KEYS = ["apiKey", "authorization", "token", "secret", "password"];
|
|
2
|
+
export function parseCliArgs(argv, usage) {
|
|
3
|
+
const [action, payloadRaw] = argv;
|
|
4
|
+
if (!action) {
|
|
5
|
+
throw new Error(usage);
|
|
6
|
+
}
|
|
7
|
+
if (!payloadRaw) {
|
|
8
|
+
return { action, payload: {} };
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
return {
|
|
12
|
+
action,
|
|
13
|
+
payload: JSON.parse(payloadRaw),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error("Payload must be valid JSON");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function loadCliEnv() {
|
|
21
|
+
const baseUrl = process.env.MUNIN_BASE_URL;
|
|
22
|
+
if (!baseUrl) {
|
|
23
|
+
throw new Error("MUNIN_BASE_URL is required");
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
baseUrl,
|
|
27
|
+
project: process.env.MUNIN_PROJECT ?? "default",
|
|
28
|
+
apiKey: process.env.MUNIN_API_KEY,
|
|
29
|
+
timeoutMs: Number(process.env.MUNIN_TIMEOUT_MS ?? 15000),
|
|
30
|
+
retries: Number(process.env.MUNIN_RETRIES ?? 3),
|
|
31
|
+
backoffMs: Number(process.env.MUNIN_BACKOFF_MS ?? 300),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function executeWithRetry(task, retries, backoffMs) {
|
|
35
|
+
let attempt = 0;
|
|
36
|
+
let lastError;
|
|
37
|
+
while (attempt <= retries) {
|
|
38
|
+
try {
|
|
39
|
+
return await task();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
lastError = error;
|
|
43
|
+
if (attempt === retries) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
const jitter = Math.floor(Math.random() * 100);
|
|
47
|
+
await sleep(backoffMs * 2 ** attempt + jitter);
|
|
48
|
+
attempt += 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw lastError;
|
|
52
|
+
}
|
|
53
|
+
export function safeError(error) {
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
return {
|
|
56
|
+
name: error.name,
|
|
57
|
+
message: redactText(error.message),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { message: redactText(String(error)) };
|
|
61
|
+
}
|
|
62
|
+
function redactText(text) {
|
|
63
|
+
return REDACT_KEYS.reduce((acc, key) => {
|
|
64
|
+
const pattern = new RegExp(`${key}\\s*[:=]\\s*[^\\s,]+`, "gi");
|
|
65
|
+
return acc.replace(pattern, `${key}=[REDACTED]`);
|
|
66
|
+
}, text);
|
|
67
|
+
}
|
|
68
|
+
function sleep(ms) {
|
|
69
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
+
}
|
|
71
|
+
export * from "./mcp-server.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startMcpServer(): Promise<void>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { MuninClient } from "@kalera/munin-sdk";
|
|
5
|
+
import { loadCliEnv, safeError } from "./index.js";
|
|
6
|
+
export async function startMcpServer() {
|
|
7
|
+
const env = loadCliEnv();
|
|
8
|
+
const client = new MuninClient({
|
|
9
|
+
baseUrl: env.baseUrl,
|
|
10
|
+
project: env.project,
|
|
11
|
+
apiKey: env.apiKey,
|
|
12
|
+
timeoutMs: env.timeoutMs,
|
|
13
|
+
});
|
|
14
|
+
const server = new Server({
|
|
15
|
+
name: "munin-mcp-server",
|
|
16
|
+
version: "0.1.0",
|
|
17
|
+
}, {
|
|
18
|
+
capabilities: {
|
|
19
|
+
tools: {},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
23
|
+
return {
|
|
24
|
+
tools: [
|
|
25
|
+
{
|
|
26
|
+
name: "munin_store_memory",
|
|
27
|
+
description: "Store or update a memory in Munin. Requires a unique key and the content.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
key: { type: "string", description: "Unique identifier for this memory" },
|
|
32
|
+
content: { type: "string", description: "The content to remember" },
|
|
33
|
+
title: { type: "string", description: "Optional title" },
|
|
34
|
+
tags: {
|
|
35
|
+
type: "array",
|
|
36
|
+
items: { type: "string" },
|
|
37
|
+
description: "List of tags, e.g. ['planning', 'frontend']"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
required: ["key", "content"],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "munin_retrieve_memory",
|
|
45
|
+
description: "Retrieve a memory by its unique key.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
key: { type: "string", description: "Unique identifier" },
|
|
50
|
+
},
|
|
51
|
+
required: ["key"],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "munin_search_memories",
|
|
56
|
+
description: "Search for memories using semantic search or keywords.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
query: { type: "string", description: "Search query" },
|
|
61
|
+
tags: { type: "array", items: { type: "string" } },
|
|
62
|
+
limit: { type: "number", description: "Max results (default: 10)" },
|
|
63
|
+
},
|
|
64
|
+
required: ["query"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "munin_list_memories",
|
|
69
|
+
description: "List all memories with pagination.",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
limit: { type: "number" },
|
|
74
|
+
offset: { type: "number" },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "munin_recent_memories",
|
|
80
|
+
description: "Get the most recently updated memories.",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
limit: { type: "number" },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
92
|
+
try {
|
|
93
|
+
const args = request.params.arguments || {};
|
|
94
|
+
let result;
|
|
95
|
+
switch (request.params.name) {
|
|
96
|
+
case "munin_store_memory":
|
|
97
|
+
result = await client.store(args);
|
|
98
|
+
break;
|
|
99
|
+
case "munin_retrieve_memory":
|
|
100
|
+
result = await client.retrieve(args);
|
|
101
|
+
break;
|
|
102
|
+
case "munin_search_memories":
|
|
103
|
+
result = await client.search(args);
|
|
104
|
+
break;
|
|
105
|
+
case "munin_list_memories":
|
|
106
|
+
result = await client.list(args);
|
|
107
|
+
break;
|
|
108
|
+
case "munin_recent_memories":
|
|
109
|
+
result = await client.recent(args);
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
content: [
|
|
116
|
+
{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: JSON.stringify(result, null, 2),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
const errObj = safeError(error);
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: `Error executing tool: ${JSON.stringify(errObj)}`,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
isError: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const transport = new StdioServerTransport();
|
|
137
|
+
await server.connect(transport);
|
|
138
|
+
console.error("Munin MCP Server running on stdio");
|
|
139
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kalera/munin-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"typescript": "^5.9.2"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
15
|
+
"@kalera/munin-sdk": "0.1.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
20
|
+
"test": "echo 'adapter-runtime tests: pending'"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface ParsedCliArgs {
|
|
2
|
+
action: string;
|
|
3
|
+
payload: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface CliEnv {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
project: string;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
timeoutMs: number;
|
|
11
|
+
retries: number;
|
|
12
|
+
backoffMs: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const REDACT_KEYS = ["apiKey", "authorization", "token", "secret", "password"];
|
|
16
|
+
|
|
17
|
+
export function parseCliArgs(argv: string[], usage: string): ParsedCliArgs {
|
|
18
|
+
const [action, payloadRaw] = argv;
|
|
19
|
+
if (!action) {
|
|
20
|
+
throw new Error(usage);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!payloadRaw) {
|
|
24
|
+
return { action, payload: {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return {
|
|
29
|
+
action,
|
|
30
|
+
payload: JSON.parse(payloadRaw) as Record<string, unknown>,
|
|
31
|
+
};
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error("Payload must be valid JSON");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function loadCliEnv(): CliEnv {
|
|
38
|
+
const baseUrl = process.env.MUNIN_BASE_URL;
|
|
39
|
+
if (!baseUrl) {
|
|
40
|
+
throw new Error("MUNIN_BASE_URL is required");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
baseUrl,
|
|
45
|
+
project: process.env.MUNIN_PROJECT ?? "default",
|
|
46
|
+
apiKey: process.env.MUNIN_API_KEY,
|
|
47
|
+
timeoutMs: Number(process.env.MUNIN_TIMEOUT_MS ?? 15000),
|
|
48
|
+
retries: Number(process.env.MUNIN_RETRIES ?? 3),
|
|
49
|
+
backoffMs: Number(process.env.MUNIN_BACKOFF_MS ?? 300),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function executeWithRetry<T>(
|
|
54
|
+
task: () => Promise<T>,
|
|
55
|
+
retries: number,
|
|
56
|
+
backoffMs: number,
|
|
57
|
+
): Promise<T> {
|
|
58
|
+
let attempt = 0;
|
|
59
|
+
let lastError: unknown;
|
|
60
|
+
|
|
61
|
+
while (attempt <= retries) {
|
|
62
|
+
try {
|
|
63
|
+
return await task();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
lastError = error;
|
|
66
|
+
if (attempt === retries) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const jitter = Math.floor(Math.random() * 100);
|
|
70
|
+
await sleep(backoffMs * 2 ** attempt + jitter);
|
|
71
|
+
attempt += 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw lastError;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function safeError(error: unknown): Record<string, unknown> {
|
|
79
|
+
if (error instanceof Error) {
|
|
80
|
+
return {
|
|
81
|
+
name: error.name,
|
|
82
|
+
message: redactText(error.message),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { message: redactText(String(error)) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function redactText(text: string): string {
|
|
90
|
+
return REDACT_KEYS.reduce((acc, key) => {
|
|
91
|
+
const pattern = new RegExp(`${key}\\s*[:=]\\s*[^\\s,]+`, "gi");
|
|
92
|
+
return acc.replace(pattern, `${key}=[REDACTED]`);
|
|
93
|
+
}, text);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sleep(ms: number): Promise<void> {
|
|
97
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
98
|
+
}
|
|
99
|
+
export * from "./mcp-server.js";
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { MuninClient } from "@kalera/munin-sdk";
|
|
8
|
+
import { loadCliEnv, safeError } from "./index.js";
|
|
9
|
+
|
|
10
|
+
export async function startMcpServer() {
|
|
11
|
+
const env = loadCliEnv();
|
|
12
|
+
|
|
13
|
+
const client = new MuninClient({
|
|
14
|
+
baseUrl: env.baseUrl,
|
|
15
|
+
project: env.project,
|
|
16
|
+
apiKey: env.apiKey,
|
|
17
|
+
timeoutMs: env.timeoutMs,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const server = new Server(
|
|
21
|
+
{
|
|
22
|
+
name: "munin-mcp-server",
|
|
23
|
+
version: "0.1.0",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
capabilities: {
|
|
27
|
+
tools: {},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
33
|
+
return {
|
|
34
|
+
tools: [
|
|
35
|
+
{
|
|
36
|
+
name: "munin_store_memory",
|
|
37
|
+
description: "Store or update a memory in Munin. Requires a unique key and the content.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
key: { type: "string", description: "Unique identifier for this memory" },
|
|
42
|
+
content: { type: "string", description: "The content to remember" },
|
|
43
|
+
title: { type: "string", description: "Optional title" },
|
|
44
|
+
tags: {
|
|
45
|
+
type: "array",
|
|
46
|
+
items: { type: "string" },
|
|
47
|
+
description: "List of tags, e.g. ['planning', 'frontend']"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
required: ["key", "content"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "munin_retrieve_memory",
|
|
55
|
+
description: "Retrieve a memory by its unique key.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
key: { type: "string", description: "Unique identifier" },
|
|
60
|
+
},
|
|
61
|
+
required: ["key"],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "munin_search_memories",
|
|
66
|
+
description: "Search for memories using semantic search or keywords.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
query: { type: "string", description: "Search query" },
|
|
71
|
+
tags: { type: "array", items: { type: "string" } },
|
|
72
|
+
limit: { type: "number", description: "Max results (default: 10)" },
|
|
73
|
+
},
|
|
74
|
+
required: ["query"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "munin_list_memories",
|
|
79
|
+
description: "List all memories with pagination.",
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
limit: { type: "number" },
|
|
84
|
+
offset: { type: "number" },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "munin_recent_memories",
|
|
90
|
+
description: "Get the most recently updated memories.",
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
limit: { type: "number" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
103
|
+
try {
|
|
104
|
+
const args = request.params.arguments || {};
|
|
105
|
+
let result;
|
|
106
|
+
|
|
107
|
+
switch (request.params.name) {
|
|
108
|
+
case "munin_store_memory":
|
|
109
|
+
result = await client.store(args);
|
|
110
|
+
break;
|
|
111
|
+
case "munin_retrieve_memory":
|
|
112
|
+
result = await client.retrieve(args);
|
|
113
|
+
break;
|
|
114
|
+
case "munin_search_memories":
|
|
115
|
+
result = await client.search(args);
|
|
116
|
+
break;
|
|
117
|
+
case "munin_list_memories":
|
|
118
|
+
result = await client.list(args);
|
|
119
|
+
break;
|
|
120
|
+
case "munin_recent_memories":
|
|
121
|
+
result = await client.recent(args);
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
content: [
|
|
129
|
+
{
|
|
130
|
+
type: "text",
|
|
131
|
+
text: JSON.stringify(result, null, 2),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const errObj = safeError(error);
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: `Error executing tool: ${JSON.stringify(errObj)}`,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
isError: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const transport = new StdioServerTransport();
|
|
150
|
+
await server.connect(transport);
|
|
151
|
+
console.error("Munin MCP Server running on stdio");
|
|
152
|
+
}
|