kiro-memory 1.0.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.
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/servers/mcp-server.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ console.log = (...args) => console.error("[contextkit-mcp]", ...args);
11
+ var WORKER_HOST = process.env.CONTEXTKIT_WORKER_HOST || "127.0.0.1";
12
+ var WORKER_PORT = process.env.CONTEXTKIT_WORKER_PORT || "3001";
13
+ var WORKER_BASE = `http://${WORKER_HOST}:${WORKER_PORT}`;
14
+ async function callWorkerGET(endpoint, params = {}) {
15
+ const url = new URL(endpoint, WORKER_BASE);
16
+ Object.entries(params).forEach(([k, v]) => {
17
+ if (v !== void 0 && v !== null && v !== "") url.searchParams.set(k, v);
18
+ });
19
+ const resp = await fetch(url.toString(), { signal: AbortSignal.timeout(1e4) });
20
+ if (!resp.ok) throw new Error(`Worker ${resp.status}: ${await resp.text()}`);
21
+ return resp.json();
22
+ }
23
+ async function callWorkerPOST(endpoint, body) {
24
+ const url = new URL(endpoint, WORKER_BASE);
25
+ const resp = await fetch(url.toString(), {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify(body),
29
+ signal: AbortSignal.timeout(1e4)
30
+ });
31
+ if (!resp.ok) throw new Error(`Worker ${resp.status}: ${await resp.text()}`);
32
+ return resp.json();
33
+ }
34
+ var TOOLS = [
35
+ {
36
+ name: "search",
37
+ description: "Cerca nella memoria di ContextKit. Restituisce osservazioni e sommari che corrispondono alla query. Usa questo tool per trovare contesto da sessioni precedenti.",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ query: { type: "string", description: "Testo da cercare nelle osservazioni e sommari" },
42
+ project: { type: "string", description: "Filtra per nome progetto (opzionale)" },
43
+ type: { type: "string", description: "Filtra per tipo osservazione: file-write, command, research, tool-use (opzionale)" },
44
+ limit: { type: "number", description: "Numero massimo risultati (default: 20)" }
45
+ },
46
+ required: ["query"]
47
+ }
48
+ },
49
+ {
50
+ name: "timeline",
51
+ description: "Mostra il contesto cronologico attorno a un'osservazione specifica. Utile per capire cosa \xE8 successo prima e dopo un evento.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ anchor: { type: "number", description: "ID dell'osservazione come punto di riferimento" },
56
+ depth_before: { type: "number", description: "Numero di osservazioni prima (default: 5)" },
57
+ depth_after: { type: "number", description: "Numero di osservazioni dopo (default: 5)" }
58
+ },
59
+ required: ["anchor"]
60
+ }
61
+ },
62
+ {
63
+ name: "get_observations",
64
+ description: 'Recupera i dettagli completi di osservazioni specifiche per ID. Usa dopo "search" per ottenere il contenuto completo.',
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ ids: {
69
+ type: "array",
70
+ items: { type: "number" },
71
+ description: "Array di ID osservazioni da recuperare"
72
+ }
73
+ },
74
+ required: ["ids"]
75
+ }
76
+ },
77
+ {
78
+ name: "get_context",
79
+ description: "Recupera il contesto recente per un progetto: osservazioni, sommari e prompt recenti.",
80
+ inputSchema: {
81
+ type: "object",
82
+ properties: {
83
+ project: { type: "string", description: "Nome del progetto" }
84
+ },
85
+ required: ["project"]
86
+ }
87
+ }
88
+ ];
89
+ var handlers = {
90
+ async search(args) {
91
+ const result = await callWorkerGET("/api/search", {
92
+ q: args.query,
93
+ project: args.project || "",
94
+ type: args.type || "",
95
+ limit: String(args.limit || 20)
96
+ });
97
+ const obs = result.observations || [];
98
+ const sums = result.summaries || [];
99
+ if (obs.length === 0 && sums.length === 0) {
100
+ return "Nessun risultato trovato per la query.";
101
+ }
102
+ let output = `## Risultati ricerca: "${args.query}"
103
+
104
+ `;
105
+ if (obs.length > 0) {
106
+ output += `### Osservazioni (${obs.length})
107
+
108
+ `;
109
+ output += "| ID | Tipo | Titolo | Data |\n|---|---|---|---|\n";
110
+ obs.forEach((o) => {
111
+ output += `| ${o.id} | ${o.type} | ${o.title} | ${o.created_at?.split("T")[0] || ""} |
112
+ `;
113
+ });
114
+ output += "\n";
115
+ }
116
+ if (sums.length > 0) {
117
+ output += `### Sommari (${sums.length})
118
+
119
+ `;
120
+ sums.forEach((s) => {
121
+ if (s.learned) output += `- **Appreso**: ${s.learned}
122
+ `;
123
+ if (s.completed) output += `- **Completato**: ${s.completed}
124
+ `;
125
+ });
126
+ }
127
+ return output;
128
+ },
129
+ async timeline(args) {
130
+ const result = await callWorkerGET("/api/timeline", {
131
+ anchor: String(args.anchor),
132
+ depth_before: String(args.depth_before || 5),
133
+ depth_after: String(args.depth_after || 5)
134
+ });
135
+ const entries = result.timeline || result || [];
136
+ if (!Array.isArray(entries) || entries.length === 0) {
137
+ return `Nessun contesto trovato attorno all'osservazione ${args.anchor}.`;
138
+ }
139
+ let output = `## Timeline attorno all'osservazione #${args.anchor}
140
+
141
+ `;
142
+ entries.forEach((e) => {
143
+ const marker = e.id === args.anchor ? "\u2192 " : " ";
144
+ output += `${marker}**#${e.id}** [${e.type}] ${e.title} (${e.created_at?.split("T")[0] || ""})
145
+ `;
146
+ if (e.content) output += ` ${e.content.substring(0, 200)}
147
+ `;
148
+ output += "\n";
149
+ });
150
+ return output;
151
+ },
152
+ async get_observations(args) {
153
+ const result = await callWorkerPOST("/api/observations/batch", { ids: args.ids });
154
+ const obs = result.observations || result || [];
155
+ if (!Array.isArray(obs) || obs.length === 0) {
156
+ return "Nessuna osservazione trovata per gli ID specificati.";
157
+ }
158
+ let output = `## Dettagli Osservazioni
159
+
160
+ `;
161
+ obs.forEach((o) => {
162
+ output += `### #${o.id}: ${o.title}
163
+ `;
164
+ output += `- **Tipo**: ${o.type}
165
+ `;
166
+ output += `- **Progetto**: ${o.project}
167
+ `;
168
+ output += `- **Data**: ${o.created_at}
169
+ `;
170
+ if (o.text) output += `- **Contenuto**: ${o.text}
171
+ `;
172
+ if (o.narrative) output += `- **Narrativa**: ${o.narrative}
173
+ `;
174
+ if (o.concepts) output += `- **Concetti**: ${o.concepts}
175
+ `;
176
+ if (o.files_read) output += `- **File letti**: ${o.files_read}
177
+ `;
178
+ if (o.files_modified) output += `- **File modificati**: ${o.files_modified}
179
+ `;
180
+ output += "\n";
181
+ });
182
+ return output;
183
+ },
184
+ async get_context(args) {
185
+ const result = await callWorkerGET(`/api/context/${encodeURIComponent(args.project)}`);
186
+ const obs = result.observations || [];
187
+ const sums = result.summaries || [];
188
+ let output = `## Contesto: ${args.project}
189
+
190
+ `;
191
+ if (sums.length > 0) {
192
+ output += `### Sommari Recenti
193
+
194
+ `;
195
+ sums.forEach((s) => {
196
+ if (s.request) output += `**Richiesta**: ${s.request}
197
+ `;
198
+ if (s.learned) output += `- Appreso: ${s.learned}
199
+ `;
200
+ if (s.completed) output += `- Completato: ${s.completed}
201
+ `;
202
+ if (s.next_steps) output += `- Prossimi passi: ${s.next_steps}
203
+
204
+ `;
205
+ });
206
+ }
207
+ if (obs.length > 0) {
208
+ output += `### Osservazioni Recenti (${obs.length})
209
+
210
+ `;
211
+ obs.slice(0, 10).forEach((o) => {
212
+ output += `- **${o.title}** [${o.type}]: ${(o.text || "").substring(0, 100)}
213
+ `;
214
+ });
215
+ }
216
+ return output;
217
+ }
218
+ };
219
+ async function main() {
220
+ const server = new Server(
221
+ { name: "contextkit", version: "1.0.0" },
222
+ { capabilities: { tools: {} } }
223
+ );
224
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
225
+ tools: TOOLS
226
+ }));
227
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
228
+ const { name, arguments: args } = request.params;
229
+ const handler = handlers[name];
230
+ if (!handler) {
231
+ return {
232
+ content: [{ type: "text", text: `Tool sconosciuto: ${name}` }],
233
+ isError: true
234
+ };
235
+ }
236
+ try {
237
+ const result = await handler(args || {});
238
+ return {
239
+ content: [{ type: "text", text: result }]
240
+ };
241
+ } catch (error) {
242
+ const msg = error?.message || String(error);
243
+ if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
244
+ return {
245
+ content: [{
246
+ type: "text",
247
+ text: `Worker ContextKit non raggiungibile su ${WORKER_BASE}.
248
+ Avvia il worker con: cd <contextkit-dir> && npm run worker:start`
249
+ }],
250
+ isError: true
251
+ };
252
+ }
253
+ return {
254
+ content: [{ type: "text", text: `Errore: ${msg}` }],
255
+ isError: true
256
+ };
257
+ }
258
+ });
259
+ const transport = new StdioServerTransport();
260
+ await server.connect(transport);
261
+ console.log("ContextKit MCP server avviato su stdio");
262
+ }
263
+ main().catch((err) => {
264
+ console.error("Errore avvio MCP server:", err);
265
+ process.exit(1);
266
+ });
@@ -0,0 +1,357 @@
1
+ // src/services/search/ChromaManager.ts
2
+ import { ChromaClient } from "chromadb";
3
+ import { join as join2 } from "path";
4
+ import { homedir as homedir2 } from "os";
5
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
6
+
7
+ // src/utils/logger.ts
8
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
12
+ LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
13
+ LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
14
+ LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
15
+ LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
16
+ LogLevel2[LogLevel2["SILENT"] = 4] = "SILENT";
17
+ return LogLevel2;
18
+ })(LogLevel || {});
19
+ var DEFAULT_DATA_DIR = join(homedir(), ".contextkit");
20
+ var Logger = class {
21
+ level = null;
22
+ useColor;
23
+ logFilePath = null;
24
+ logFileInitialized = false;
25
+ constructor() {
26
+ this.useColor = process.stdout.isTTY ?? false;
27
+ }
28
+ /**
29
+ * Initialize log file path and ensure directory exists (lazy initialization)
30
+ */
31
+ ensureLogFileInitialized() {
32
+ if (this.logFileInitialized) return;
33
+ this.logFileInitialized = true;
34
+ try {
35
+ const logsDir = join(DEFAULT_DATA_DIR, "logs");
36
+ if (!existsSync(logsDir)) {
37
+ mkdirSync(logsDir, { recursive: true });
38
+ }
39
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
40
+ this.logFilePath = join(logsDir, `contextkit-${date}.log`);
41
+ } catch (error) {
42
+ console.error("[LOGGER] Failed to initialize log file:", error);
43
+ this.logFilePath = null;
44
+ }
45
+ }
46
+ /**
47
+ * Lazy-load log level from settings file
48
+ */
49
+ getLevel() {
50
+ if (this.level === null) {
51
+ try {
52
+ const settingsPath = join(DEFAULT_DATA_DIR, "settings.json");
53
+ if (existsSync(settingsPath)) {
54
+ const settingsData = readFileSync(settingsPath, "utf-8");
55
+ const settings = JSON.parse(settingsData);
56
+ const envLevel = (settings.CONTEXTKIT_LOG_LEVEL || "INFO").toUpperCase();
57
+ this.level = LogLevel[envLevel] ?? 1 /* INFO */;
58
+ } else {
59
+ this.level = 1 /* INFO */;
60
+ }
61
+ } catch (error) {
62
+ this.level = 1 /* INFO */;
63
+ }
64
+ }
65
+ return this.level;
66
+ }
67
+ /**
68
+ * Create correlation ID for tracking an observation through the pipeline
69
+ */
70
+ correlationId(sessionId, observationNum) {
71
+ return `obs-${sessionId}-${observationNum}`;
72
+ }
73
+ /**
74
+ * Create session correlation ID
75
+ */
76
+ sessionId(sessionId) {
77
+ return `session-${sessionId}`;
78
+ }
79
+ /**
80
+ * Format data for logging - create compact summaries instead of full dumps
81
+ */
82
+ formatData(data) {
83
+ if (data === null || data === void 0) return "";
84
+ if (typeof data === "string") return data;
85
+ if (typeof data === "number") return data.toString();
86
+ if (typeof data === "boolean") return data.toString();
87
+ if (typeof data === "object") {
88
+ if (data instanceof Error) {
89
+ return this.getLevel() === 0 /* DEBUG */ ? `${data.message}
90
+ ${data.stack}` : data.message;
91
+ }
92
+ if (Array.isArray(data)) {
93
+ return `[${data.length} items]`;
94
+ }
95
+ const keys = Object.keys(data);
96
+ if (keys.length === 0) return "{}";
97
+ if (keys.length <= 3) {
98
+ return JSON.stringify(data);
99
+ }
100
+ return `{${keys.length} keys: ${keys.slice(0, 3).join(", ")}...}`;
101
+ }
102
+ return String(data);
103
+ }
104
+ /**
105
+ * Format timestamp in local timezone (YYYY-MM-DD HH:MM:SS.mmm)
106
+ */
107
+ formatTimestamp(date) {
108
+ const year = date.getFullYear();
109
+ const month = String(date.getMonth() + 1).padStart(2, "0");
110
+ const day = String(date.getDate()).padStart(2, "0");
111
+ const hours = String(date.getHours()).padStart(2, "0");
112
+ const minutes = String(date.getMinutes()).padStart(2, "0");
113
+ const seconds = String(date.getSeconds()).padStart(2, "0");
114
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
115
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
116
+ }
117
+ /**
118
+ * Core logging method
119
+ */
120
+ log(level, component, message, context, data) {
121
+ if (level < this.getLevel()) return;
122
+ this.ensureLogFileInitialized();
123
+ const timestamp = this.formatTimestamp(/* @__PURE__ */ new Date());
124
+ const levelStr = LogLevel[level].padEnd(5);
125
+ const componentStr = component.padEnd(6);
126
+ let correlationStr = "";
127
+ if (context?.correlationId) {
128
+ correlationStr = `[${context.correlationId}] `;
129
+ } else if (context?.sessionId) {
130
+ correlationStr = `[session-${context.sessionId}] `;
131
+ }
132
+ let dataStr = "";
133
+ if (data !== void 0 && data !== null) {
134
+ if (data instanceof Error) {
135
+ dataStr = this.getLevel() === 0 /* DEBUG */ ? `
136
+ ${data.message}
137
+ ${data.stack}` : ` ${data.message}`;
138
+ } else if (this.getLevel() === 0 /* DEBUG */ && typeof data === "object") {
139
+ dataStr = "\n" + JSON.stringify(data, null, 2);
140
+ } else {
141
+ dataStr = " " + this.formatData(data);
142
+ }
143
+ }
144
+ let contextStr = "";
145
+ if (context) {
146
+ const { sessionId, memorySessionId, correlationId, ...rest } = context;
147
+ if (Object.keys(rest).length > 0) {
148
+ const pairs = Object.entries(rest).map(([k, v]) => `${k}=${v}`);
149
+ contextStr = ` {${pairs.join(", ")}}`;
150
+ }
151
+ }
152
+ const logLine = `[${timestamp}] [${levelStr}] [${componentStr}] ${correlationStr}${message}${contextStr}${dataStr}`;
153
+ if (this.logFilePath) {
154
+ try {
155
+ appendFileSync(this.logFilePath, logLine + "\n", "utf8");
156
+ } catch (error) {
157
+ process.stderr.write(`[LOGGER] Failed to write to log file: ${error}
158
+ `);
159
+ }
160
+ } else {
161
+ process.stderr.write(logLine + "\n");
162
+ }
163
+ }
164
+ // Public logging methods
165
+ debug(component, message, context, data) {
166
+ this.log(0 /* DEBUG */, component, message, context, data);
167
+ }
168
+ info(component, message, context, data) {
169
+ this.log(1 /* INFO */, component, message, context, data);
170
+ }
171
+ warn(component, message, context, data) {
172
+ this.log(2 /* WARN */, component, message, context, data);
173
+ }
174
+ error(component, message, context, data) {
175
+ this.log(3 /* ERROR */, component, message, context, data);
176
+ }
177
+ /**
178
+ * Log data flow: input → processing
179
+ */
180
+ dataIn(component, message, context, data) {
181
+ this.info(component, `\u2192 ${message}`, context, data);
182
+ }
183
+ /**
184
+ * Log data flow: processing → output
185
+ */
186
+ dataOut(component, message, context, data) {
187
+ this.info(component, `\u2190 ${message}`, context, data);
188
+ }
189
+ /**
190
+ * Log successful completion
191
+ */
192
+ success(component, message, context, data) {
193
+ this.info(component, `\u2713 ${message}`, context, data);
194
+ }
195
+ /**
196
+ * Log failure
197
+ */
198
+ failure(component, message, context, data) {
199
+ this.error(component, `\u2717 ${message}`, context, data);
200
+ }
201
+ /**
202
+ * Log timing information
203
+ */
204
+ timing(component, message, durationMs, context) {
205
+ this.info(component, `\u23F1 ${message}`, context, { duration: `${durationMs}ms` });
206
+ }
207
+ /**
208
+ * Happy Path Error - logs when the expected "happy path" fails but we have a fallback
209
+ */
210
+ happyPathError(component, message, context, data, fallback = "") {
211
+ const stack = new Error().stack || "";
212
+ const stackLines = stack.split("\n");
213
+ const callerLine = stackLines[2] || "";
214
+ const callerMatch = callerLine.match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/);
215
+ const location = callerMatch ? `${callerMatch[1].split("/").pop()}:${callerMatch[2]}` : "unknown";
216
+ const enhancedContext = {
217
+ ...context,
218
+ location
219
+ };
220
+ this.warn(component, `[HAPPY-PATH] ${message}`, enhancedContext, data);
221
+ return fallback;
222
+ }
223
+ };
224
+ var logger = new Logger();
225
+
226
+ // src/services/search/ChromaManager.ts
227
+ var VECTOR_DB_DIR = join2(homedir2(), ".contextkit", "vector-db");
228
+ var ChromaManager = class {
229
+ client;
230
+ collection = null;
231
+ isAvailable = false;
232
+ constructor() {
233
+ if (!existsSync2(VECTOR_DB_DIR)) {
234
+ mkdirSync2(VECTOR_DB_DIR, { recursive: true });
235
+ }
236
+ this.client = new ChromaClient({
237
+ path: process.env.CHROMADB_URL || "http://localhost:8000"
238
+ });
239
+ }
240
+ /**
241
+ * Initialize ChromaDB connection and collection
242
+ */
243
+ async initialize() {
244
+ try {
245
+ await this.client.heartbeat();
246
+ this.collection = await this.client.getOrCreateCollection({
247
+ name: "contextkit-observations",
248
+ metadata: { description: "ContextKit observation embeddings" }
249
+ });
250
+ this.isAvailable = true;
251
+ logger.info("CHROMA", "ChromaDB initialized successfully");
252
+ return true;
253
+ } catch (error) {
254
+ logger.warn("CHROMA", "ChromaDB not available, falling back to SQLite search", {}, error);
255
+ this.isAvailable = false;
256
+ return false;
257
+ }
258
+ }
259
+ /**
260
+ * Add observation embedding to ChromaDB
261
+ */
262
+ async addObservation(id, content, metadata) {
263
+ if (!this.isAvailable || !this.collection) {
264
+ logger.debug("CHROMA", "ChromaDB not available, skipping embedding");
265
+ return;
266
+ }
267
+ try {
268
+ await this.collection.add({
269
+ ids: [id],
270
+ documents: [content],
271
+ metadatas: [metadata]
272
+ });
273
+ logger.debug("CHROMA", `Added observation ${id} to vector DB`);
274
+ } catch (error) {
275
+ logger.error("CHROMA", `Failed to add observation ${id}`, {}, error);
276
+ }
277
+ }
278
+ /**
279
+ * Search observations by semantic similarity
280
+ */
281
+ async search(query, options = {}) {
282
+ if (!this.isAvailable || !this.collection) {
283
+ logger.debug("CHROMA", "ChromaDB not available, returning empty results");
284
+ return [];
285
+ }
286
+ try {
287
+ const where = options.project ? { project: options.project } : void 0;
288
+ const results = await this.collection.query({
289
+ queryTexts: [query],
290
+ nResults: options.limit || 10,
291
+ where
292
+ });
293
+ const hits = [];
294
+ if (results.ids && results.ids[0]) {
295
+ for (let i = 0; i < results.ids[0].length; i++) {
296
+ hits.push({
297
+ id: results.ids[0][i],
298
+ content: results.documents?.[0]?.[i] || "",
299
+ metadata: results.metadatas?.[0]?.[i] || {},
300
+ distance: results.distances?.[0]?.[i] || 0
301
+ });
302
+ }
303
+ }
304
+ logger.debug("CHROMA", `Search returned ${hits.length} results`);
305
+ return hits;
306
+ } catch (error) {
307
+ logger.error("CHROMA", "Search failed", {}, error);
308
+ return [];
309
+ }
310
+ }
311
+ /**
312
+ * Delete observation from ChromaDB
313
+ */
314
+ async deleteObservation(id) {
315
+ if (!this.isAvailable || !this.collection) {
316
+ return;
317
+ }
318
+ try {
319
+ await this.collection.delete({ ids: [id] });
320
+ logger.debug("CHROMA", `Deleted observation ${id}`);
321
+ } catch (error) {
322
+ logger.error("CHROMA", `Failed to delete observation ${id}`, {}, error);
323
+ }
324
+ }
325
+ /**
326
+ * Check if ChromaDB is available
327
+ */
328
+ isChromaAvailable() {
329
+ return this.isAvailable;
330
+ }
331
+ /**
332
+ * Get collection stats
333
+ */
334
+ async getStats() {
335
+ if (!this.isAvailable || !this.collection) {
336
+ return { count: 0 };
337
+ }
338
+ try {
339
+ const count = await this.collection.count();
340
+ return { count };
341
+ } catch (error) {
342
+ logger.error("CHROMA", "Failed to get stats", {}, error);
343
+ return { count: 0 };
344
+ }
345
+ }
346
+ };
347
+ var chromaManager = null;
348
+ function getChromaManager() {
349
+ if (!chromaManager) {
350
+ chromaManager = new ChromaManager();
351
+ }
352
+ return chromaManager;
353
+ }
354
+ export {
355
+ ChromaManager,
356
+ getChromaManager
357
+ };