claude-mem 3.0.2 → 3.0.4

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.
Files changed (56) hide show
  1. package/.mcp.json +11 -0
  2. package/claude-mem +0 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +64 -0
  5. package/dist/commands/compress.d.ts +2 -0
  6. package/dist/commands/compress.js +59 -0
  7. package/dist/commands/install.d.ts +2 -0
  8. package/dist/commands/install.js +372 -0
  9. package/dist/commands/load-context.d.ts +2 -0
  10. package/dist/commands/load-context.js +330 -0
  11. package/dist/commands/logs.d.ts +2 -0
  12. package/dist/commands/logs.js +41 -0
  13. package/dist/commands/migrate.d.ts +9 -0
  14. package/dist/commands/migrate.js +174 -0
  15. package/dist/commands/status.d.ts +1 -0
  16. package/dist/commands/status.js +159 -0
  17. package/dist/commands/uninstall.d.ts +2 -0
  18. package/dist/commands/uninstall.js +105 -0
  19. package/dist/config.d.ts +6 -0
  20. package/dist/config.js +33 -0
  21. package/dist/constants.d.ts +516 -0
  22. package/dist/constants.js +522 -0
  23. package/dist/error-handler.d.ts +17 -0
  24. package/dist/error-handler.js +103 -0
  25. package/dist/mcp-server-cli.d.ts +34 -0
  26. package/dist/mcp-server-cli.js +158 -0
  27. package/dist/mcp-server.d.ts +103 -0
  28. package/dist/mcp-server.js +269 -0
  29. package/dist/types.d.ts +148 -0
  30. package/dist/types.js +78 -0
  31. package/dist/utils/HookDetector.d.ts +64 -0
  32. package/dist/utils/HookDetector.js +213 -0
  33. package/dist/utils/PathResolver.d.ts +16 -0
  34. package/dist/utils/PathResolver.js +55 -0
  35. package/dist/utils/SettingsManager.d.ts +63 -0
  36. package/dist/utils/SettingsManager.js +133 -0
  37. package/dist/utils/TranscriptCompressor.d.ts +111 -0
  38. package/dist/utils/TranscriptCompressor.js +486 -0
  39. package/dist/utils/common.d.ts +29 -0
  40. package/dist/utils/common.js +14 -0
  41. package/dist/utils/error-utils.d.ts +93 -0
  42. package/dist/utils/error-utils.js +238 -0
  43. package/dist/utils/index.d.ts +19 -0
  44. package/dist/utils/index.js +26 -0
  45. package/dist/utils/logger.d.ts +19 -0
  46. package/dist/utils/logger.js +42 -0
  47. package/dist/utils/mcp-client-factory.d.ts +51 -0
  48. package/dist/utils/mcp-client-factory.js +115 -0
  49. package/dist/utils/mcp-client.d.ts +75 -0
  50. package/dist/utils/mcp-client.js +120 -0
  51. package/dist/utils/memory-mcp-client.d.ts +135 -0
  52. package/dist/utils/memory-mcp-client.js +490 -0
  53. package/dist/utils/weaviate-mcp-adapter.d.ts +102 -0
  54. package/dist/utils/weaviate-mcp-adapter.js +587 -0
  55. package/package.json +3 -2
  56. package/src/claude-mem.js +0 -859
package/dist/types.js ADDED
@@ -0,0 +1,78 @@
1
+ export class HookError extends Error {
2
+ hookType;
3
+ payload;
4
+ code;
5
+ constructor(message, hookType, payload, code) {
6
+ super(message);
7
+ this.hookType = hookType;
8
+ this.payload = payload;
9
+ this.code = code;
10
+ this.name = 'HookError';
11
+ }
12
+ }
13
+ export class CompressionError extends Error {
14
+ transcriptPath;
15
+ stage;
16
+ constructor(message, transcriptPath, stage) {
17
+ super(message);
18
+ this.transcriptPath = transcriptPath;
19
+ this.stage = stage;
20
+ this.name = 'CompressionError';
21
+ }
22
+ }
23
+ export class FileLogger {
24
+ logFile;
25
+ enableDebug;
26
+ constructor(logFile, enableDebug = false) {
27
+ this.logFile = logFile;
28
+ this.enableDebug = enableDebug;
29
+ }
30
+ info(message, meta) {
31
+ this.log('INFO', message, meta);
32
+ }
33
+ warn(message, meta) {
34
+ this.log('WARN', message, meta);
35
+ }
36
+ error(message, error, meta) {
37
+ const errorMeta = error ? { error: error.message, stack: error.stack } : {};
38
+ this.log('ERROR', message, { ...meta, ...errorMeta });
39
+ }
40
+ debug(message, meta) {
41
+ if (this.enableDebug) {
42
+ this.log('DEBUG', message, meta);
43
+ }
44
+ }
45
+ log(level, message, meta) {
46
+ const timestamp = new Date().toISOString();
47
+ const metaStr = meta ? ` ${JSON.stringify(meta)}` : '';
48
+ const logLine = `[${timestamp}] ${level}: ${message}${metaStr}\n`;
49
+ console.error(logLine);
50
+ }
51
+ }
52
+ export function validateHookPayload(payload, expectedType) {
53
+ if (!payload || typeof payload !== 'object') {
54
+ throw new HookError(`Invalid payload: expected object, got ${typeof payload}`, expectedType);
55
+ }
56
+ const hookPayload = payload;
57
+ if (!hookPayload.session_id || typeof hookPayload.session_id !== 'string') {
58
+ throw new HookError('Missing or invalid session_id', expectedType, hookPayload);
59
+ }
60
+ if (!hookPayload.transcript_path ||
61
+ typeof hookPayload.transcript_path !== 'string') {
62
+ throw new HookError('Missing or invalid transcript_path', expectedType, hookPayload);
63
+ }
64
+ return hookPayload;
65
+ }
66
+ export function createSuccessResponse(additionalData) {
67
+ return {
68
+ continue: true,
69
+ ...additionalData,
70
+ };
71
+ }
72
+ export function createErrorResponse(reason, additionalData) {
73
+ return {
74
+ continue: false,
75
+ stopReason: reason,
76
+ ...additionalData,
77
+ };
78
+ }
@@ -0,0 +1,64 @@
1
+ import { ClaudeSettings } from './SettingsManager.js';
2
+ export type HookType = 'PreCompact' | 'SessionStart' | 'SessionEnd';
3
+ export interface HookDetectionResult {
4
+ hasPreCompact: boolean;
5
+ hasSessionStart: boolean;
6
+ hasSessionEnd: boolean;
7
+ hasAny: boolean;
8
+ }
9
+ export interface HookInfo {
10
+ name: string;
11
+ path: string;
12
+ exists: boolean;
13
+ executable: boolean;
14
+ }
15
+ /**
16
+ * Utility for detecting and managing claude-mem hooks in Claude settings
17
+ */
18
+ export declare class HookDetector {
19
+ /**
20
+ * Check if settings contain any of our hooks
21
+ */
22
+ static hasOurHooks(settings: ClaudeSettings): HookDetectionResult;
23
+ /**
24
+ * Check if settings contain our hook for a specific hook type
25
+ */
26
+ static hasOurHookType(settings: ClaudeSettings, hookType: HookType): boolean;
27
+ /**
28
+ * Filter out our hooks from the settings
29
+ */
30
+ static filterOurHooks(settings: ClaudeSettings): ClaudeSettings;
31
+ /**
32
+ * Add our hooks to the settings with the correct configuration format
33
+ */
34
+ static addOurHooks(settings: ClaudeSettings, hooks: {
35
+ preCompactScript?: string;
36
+ sessionStartScript?: string;
37
+ sessionEndScript?: string;
38
+ }): ClaudeSettings;
39
+ /**
40
+ * Get hook configuration for display purposes
41
+ */
42
+ static getHookInfo(settings: ClaudeSettings): {
43
+ totalHookTypes: number;
44
+ hookTypesList: string[];
45
+ hasPreCompact: boolean;
46
+ hasSessionStart: boolean;
47
+ hasSessionEnd: boolean;
48
+ hasAny: boolean;
49
+ };
50
+ /**
51
+ * Get hook file info including existence and executable status
52
+ */
53
+ static getHookFileInfo(hookName: string): HookInfo;
54
+ /**
55
+ * Check all hook file statuses
56
+ */
57
+ static getAllHookFileStatuses(): {
58
+ [key: string]: HookInfo;
59
+ };
60
+ /**
61
+ * Check if settings contain our hooks (legacy method for compatibility)
62
+ */
63
+ static hasOurHooksLegacy(settings: any, hookType: string): boolean;
64
+ }
@@ -0,0 +1,213 @@
1
+ import { existsSync, statSync } from 'fs';
2
+ import { PathResolver } from './PathResolver.js';
3
+ import { PACKAGE_NAME } from '../config.js';
4
+ /**
5
+ * Utility for detecting and managing claude-mem hooks in Claude settings
6
+ */
7
+ export class HookDetector {
8
+ /**
9
+ * Check if settings contain any of our hooks
10
+ */
11
+ static hasOurHooks(settings) {
12
+ const hasPreCompact = this.hasOurHookType(settings, 'PreCompact');
13
+ const hasSessionStart = this.hasOurHookType(settings, 'SessionStart');
14
+ const hasSessionEnd = this.hasOurHookType(settings, 'SessionEnd');
15
+ return {
16
+ hasPreCompact,
17
+ hasSessionStart,
18
+ hasSessionEnd,
19
+ hasAny: hasPreCompact || hasSessionStart || hasSessionEnd
20
+ };
21
+ }
22
+ /**
23
+ * Check if settings contain our hook for a specific hook type
24
+ */
25
+ static hasOurHookType(settings, hookType) {
26
+ const configs = settings.hooks?.[hookType];
27
+ if (!configs)
28
+ return false;
29
+ // Natural iteration - check each hook command directly
30
+ for (const config of configs) {
31
+ if (!config.hooks)
32
+ continue;
33
+ for (const hook of config.hooks) {
34
+ const command = hook.command;
35
+ if (command && (command.includes(PACKAGE_NAME) ||
36
+ command.includes('pre-compact.js') ||
37
+ command.includes('session-start.js') ||
38
+ command.includes('session-end.js'))) {
39
+ return true;
40
+ }
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+ /**
46
+ * Filter out our hooks from the settings
47
+ */
48
+ static filterOurHooks(settings) {
49
+ const filteredSettings = { ...settings };
50
+ if (!filteredSettings.hooks) {
51
+ return filteredSettings;
52
+ }
53
+ // Filter each hook type
54
+ const hookTypes = ['PreCompact', 'SessionStart', 'SessionEnd'];
55
+ for (const hookType of hookTypes) {
56
+ if (filteredSettings.hooks[hookType]) {
57
+ filteredSettings.hooks[hookType] = filteredSettings.hooks[hookType].filter((config) => {
58
+ // Natural check - if no hooks, keep the config
59
+ if (!config.hooks)
60
+ return true;
61
+ // Check if any hook contains our commands
62
+ for (const hook of config.hooks) {
63
+ const command = hook.command;
64
+ if (command && (command.includes(PACKAGE_NAME) ||
65
+ command.includes('pre-compact.js') ||
66
+ command.includes('session-start.js') ||
67
+ command.includes('session-end.js'))) {
68
+ return false; // Filter out this config
69
+ }
70
+ }
71
+ return true; // Keep this config
72
+ });
73
+ // Remove the hook type array if it's empty
74
+ if (filteredSettings.hooks[hookType].length === 0) {
75
+ delete filteredSettings.hooks[hookType];
76
+ }
77
+ }
78
+ }
79
+ return filteredSettings;
80
+ }
81
+ /**
82
+ * Add our hooks to the settings with the correct configuration format
83
+ */
84
+ static addOurHooks(settings, hooks) {
85
+ const updatedSettings = { ...settings };
86
+ if (!updatedSettings.hooks) {
87
+ updatedSettings.hooks = {};
88
+ }
89
+ // Add PreCompact hook if script provided
90
+ if (hooks.preCompactScript) {
91
+ if (!updatedSettings.hooks.PreCompact) {
92
+ updatedSettings.hooks.PreCompact = [];
93
+ }
94
+ // Add echo command first to show message, then actual hook
95
+ updatedSettings.hooks.PreCompact.push({
96
+ hooks: [
97
+ {
98
+ type: "command",
99
+ command: `echo '{"systemMessage": "Compressing memories..."}'`
100
+ },
101
+ {
102
+ type: "command",
103
+ command: hooks.preCompactScript,
104
+ timeout: 180000
105
+ }
106
+ ]
107
+ });
108
+ }
109
+ // Add SessionStart hook if script provided
110
+ if (hooks.sessionStartScript) {
111
+ if (!updatedSettings.hooks.SessionStart) {
112
+ updatedSettings.hooks.SessionStart = [];
113
+ }
114
+ // Add echo command first to show message, then actual hook
115
+ updatedSettings.hooks.SessionStart.push({
116
+ hooks: [
117
+ {
118
+ type: "command",
119
+ command: `echo '{"systemMessage": "Remembering..."}'`
120
+ },
121
+ {
122
+ type: "command",
123
+ command: hooks.sessionStartScript,
124
+ timeout: 30000
125
+ }
126
+ ]
127
+ });
128
+ }
129
+ // Add SessionEnd hook if script provided
130
+ if (hooks.sessionEndScript) {
131
+ if (!updatedSettings.hooks.SessionEnd) {
132
+ updatedSettings.hooks.SessionEnd = [];
133
+ }
134
+ updatedSettings.hooks.SessionEnd.push({
135
+ hooks: [{
136
+ type: "command",
137
+ command: hooks.sessionEndScript,
138
+ timeout: 180000
139
+ }]
140
+ });
141
+ }
142
+ return updatedSettings;
143
+ }
144
+ /**
145
+ * Get hook configuration for display purposes
146
+ */
147
+ static getHookInfo(settings) {
148
+ const detection = this.hasOurHooks(settings);
149
+ const hookTypes = Object.keys(settings.hooks || {});
150
+ return {
151
+ ...detection,
152
+ totalHookTypes: hookTypes.length,
153
+ hookTypesList: hookTypes
154
+ };
155
+ }
156
+ /**
157
+ * Get hook file info including existence and executable status
158
+ */
159
+ static getHookFileInfo(hookName) {
160
+ const path = PathResolver.resolveHookPath(hookName);
161
+ const exists = existsSync(path);
162
+ let executable = false;
163
+ if (exists) {
164
+ try {
165
+ const stats = statSync(path);
166
+ executable = (stats.mode & 0o111) !== 0;
167
+ }
168
+ catch (error) {
169
+ // Ignore stat errors
170
+ }
171
+ }
172
+ return {
173
+ name: hookName,
174
+ path,
175
+ exists,
176
+ executable
177
+ };
178
+ }
179
+ /**
180
+ * Check all hook file statuses
181
+ */
182
+ static getAllHookFileStatuses() {
183
+ const hooks = ['pre-compact.js', 'session-start.js', 'session-end.js'];
184
+ const statuses = {};
185
+ hooks.forEach(hookName => {
186
+ statuses[hookName] = this.getHookFileInfo(hookName);
187
+ });
188
+ return statuses;
189
+ }
190
+ /**
191
+ * Check if settings contain our hooks (legacy method for compatibility)
192
+ */
193
+ static hasOurHooksLegacy(settings, hookType) {
194
+ const matchers = settings.hooks?.[hookType];
195
+ if (!matchers)
196
+ return false;
197
+ // Natural iteration - check each hook command directly
198
+ for (const matcher of matchers) {
199
+ if (!matcher.hooks)
200
+ continue;
201
+ for (const hook of matcher.hooks) {
202
+ const command = hook.command;
203
+ if (command && (command.includes('pre-compact.js') ||
204
+ command.includes('session-start.js') ||
205
+ command.includes('session-end.js') ||
206
+ command.includes(PACKAGE_NAME))) {
207
+ return true;
208
+ }
209
+ }
210
+ }
211
+ return false;
212
+ }
213
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * PathResolver utility for managing claude-mem file system paths
3
+ */
4
+ export declare class PathResolver {
5
+ private baseDir;
6
+ constructor();
7
+ getConfigDir(): string;
8
+ getIndexDir(): string;
9
+ getIndexPath(): string;
10
+ getArchiveDir(): string;
11
+ getProjectArchiveDir(projectName: string): string;
12
+ getLogsDir(): string;
13
+ static ensureDirectory(dirPath: string): void;
14
+ static ensureDirectories(dirPaths: string[]): void;
15
+ static extractProjectName(transcriptPath: string): string;
16
+ }
@@ -0,0 +1,55 @@
1
+ import { join } from 'path';
2
+ import { homedir } from 'os';
3
+ import { existsSync, mkdirSync } from 'fs';
4
+ /**
5
+ * PathResolver utility for managing claude-mem file system paths
6
+ */
7
+ export class PathResolver {
8
+ baseDir;
9
+ constructor() {
10
+ this.baseDir = join(homedir(), '.claude-mem');
11
+ }
12
+ getConfigDir() {
13
+ return this.baseDir;
14
+ }
15
+ getIndexDir() {
16
+ return this.baseDir;
17
+ }
18
+ getIndexPath() {
19
+ return join(this.baseDir, 'index.jsonl');
20
+ }
21
+ getArchiveDir() {
22
+ return join(this.baseDir, 'archives');
23
+ }
24
+ getProjectArchiveDir(projectName) {
25
+ return join(this.getArchiveDir(), projectName);
26
+ }
27
+ getLogsDir() {
28
+ return join(this.baseDir, 'logs');
29
+ }
30
+ static ensureDirectory(dirPath) {
31
+ if (!existsSync(dirPath)) {
32
+ mkdirSync(dirPath, { recursive: true });
33
+ }
34
+ }
35
+ static ensureDirectories(dirPaths) {
36
+ dirPaths.forEach(dirPath => PathResolver.ensureDirectory(dirPath));
37
+ }
38
+ static extractProjectName(transcriptPath) {
39
+ // Try to extract project name from path
40
+ const pathParts = transcriptPath.split('/');
41
+ // Look for common project indicators
42
+ const projectIndicators = ['src', 'lib', 'app', 'project', 'workspace'];
43
+ for (let i = pathParts.length - 1; i >= 0; i--) {
44
+ if (projectIndicators.includes(pathParts[i]) && i > 0) {
45
+ return pathParts[i - 1];
46
+ }
47
+ }
48
+ // Fallback to directory name containing the transcript
49
+ if (pathParts.length > 1) {
50
+ return pathParts[pathParts.length - 2];
51
+ }
52
+ // Ultimate fallback
53
+ return 'unknown-project';
54
+ }
55
+ }
@@ -0,0 +1,63 @@
1
+ import { type SettingsLocation } from './PathResolver.js';
2
+ export interface ClaudeSettings {
3
+ hooks?: {
4
+ [hookType: string]: Array<{
5
+ hooks: Array<{
6
+ type?: string;
7
+ command: string;
8
+ args?: string[];
9
+ timeout?: number;
10
+ }>;
11
+ matcher?: any;
12
+ }>;
13
+ };
14
+ [key: string]: any;
15
+ }
16
+ export interface SettingsResult {
17
+ settings: ClaudeSettings;
18
+ location: SettingsLocation;
19
+ existed: boolean;
20
+ }
21
+ /**
22
+ * Utility for managing Claude settings files
23
+ */
24
+ export declare class SettingsManager {
25
+ /**
26
+ * Read settings from the specified location
27
+ */
28
+ static readSettings(options: {
29
+ user?: boolean;
30
+ global?: boolean;
31
+ project?: boolean;
32
+ local?: boolean;
33
+ }): SettingsResult;
34
+ /**
35
+ * Write settings to the specified location
36
+ */
37
+ static writeSettings(settings: ClaudeSettings, location: SettingsLocation): void;
38
+ /**
39
+ * Read settings from all possible locations for status checking
40
+ */
41
+ static readAllSettings(): SettingsResult[];
42
+ /**
43
+ * Update settings by applying a transformation function
44
+ */
45
+ static updateSettings(options: {
46
+ user?: boolean;
47
+ global?: boolean;
48
+ project?: boolean;
49
+ local?: boolean;
50
+ }, updateFn: (settings: ClaudeSettings) => ClaudeSettings): SettingsResult;
51
+ /**
52
+ * Simple settings file reader with error handling (path-based)
53
+ */
54
+ static readSettingsFromPath(settingsPath: string): any;
55
+ /**
56
+ * Simple settings file writer with proper formatting (path-based)
57
+ */
58
+ static writeSettingsToPath(settingsPath: string, settings: any): void;
59
+ /**
60
+ * Merge settings objects safely with deep merge
61
+ */
62
+ static mergeSettings(base: any, updates: any): any;
63
+ }
@@ -0,0 +1,133 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { dirname } from 'path';
3
+ import { PathResolver } from './PathResolver.js';
4
+ import { log } from './logger.js';
5
+ /**
6
+ * Utility for managing Claude settings files
7
+ */
8
+ export class SettingsManager {
9
+ /**
10
+ * Read settings from the specified location
11
+ */
12
+ static readSettings(options) {
13
+ const location = PathResolver.getSettingsLocation(options);
14
+ let settings = {};
15
+ let existed = false;
16
+ if (existsSync(location.fullPath)) {
17
+ existed = true;
18
+ try {
19
+ const content = readFileSync(location.fullPath, 'utf8');
20
+ settings = JSON.parse(content);
21
+ }
22
+ catch (error) {
23
+ log.warning(`Creating new settings file (could not parse existing): ${error.message}`);
24
+ settings = {};
25
+ }
26
+ }
27
+ // Ensure hooks structure exists
28
+ if (!settings.hooks) {
29
+ settings.hooks = {};
30
+ }
31
+ return {
32
+ settings,
33
+ location,
34
+ existed
35
+ };
36
+ }
37
+ /**
38
+ * Write settings to the specified location
39
+ */
40
+ static writeSettings(settings, location) {
41
+ // Ensure directory exists
42
+ PathResolver.ensureDirectory(location.directory);
43
+ // Write settings file
44
+ writeFileSync(location.fullPath, JSON.stringify(settings, null, 2));
45
+ }
46
+ /**
47
+ * Read settings from all possible locations for status checking
48
+ */
49
+ static readAllSettings() {
50
+ const locations = PathResolver.getAllSettingsLocations();
51
+ return locations.map(location => {
52
+ let settings = {};
53
+ let existed = false;
54
+ if (existsSync(location.fullPath)) {
55
+ existed = true;
56
+ try {
57
+ const content = readFileSync(location.fullPath, 'utf8');
58
+ settings = JSON.parse(content);
59
+ }
60
+ catch (error) {
61
+ // Don't log errors for status checking, just mark as unreadable
62
+ settings = { _error: error.message };
63
+ }
64
+ }
65
+ return {
66
+ settings,
67
+ location,
68
+ existed
69
+ };
70
+ });
71
+ }
72
+ /**
73
+ * Update settings by applying a transformation function
74
+ */
75
+ static updateSettings(options, updateFn) {
76
+ const result = this.readSettings(options);
77
+ const updatedSettings = updateFn(result.settings);
78
+ this.writeSettings(updatedSettings, result.location);
79
+ return {
80
+ settings: updatedSettings,
81
+ location: result.location,
82
+ existed: result.existed
83
+ };
84
+ }
85
+ /**
86
+ * Simple settings file reader with error handling (path-based)
87
+ */
88
+ static readSettingsFromPath(settingsPath) {
89
+ if (!existsSync(settingsPath)) {
90
+ return {};
91
+ }
92
+ try {
93
+ const content = readFileSync(settingsPath, 'utf8');
94
+ return JSON.parse(content);
95
+ }
96
+ catch (error) {
97
+ log.warning(`Could not parse settings file: ${error}`);
98
+ return {};
99
+ }
100
+ }
101
+ /**
102
+ * Simple settings file writer with proper formatting (path-based)
103
+ */
104
+ static writeSettingsToPath(settingsPath, settings) {
105
+ const settingsDir = dirname(settingsPath);
106
+ // Ensure directory exists
107
+ if (!existsSync(settingsDir)) {
108
+ mkdirSync(settingsDir, { recursive: true });
109
+ }
110
+ try {
111
+ const content = JSON.stringify(settings, null, 2);
112
+ writeFileSync(settingsPath, content, 'utf8');
113
+ }
114
+ catch (error) {
115
+ throw new Error(`Failed to write settings file: ${error}`);
116
+ }
117
+ }
118
+ /**
119
+ * Merge settings objects safely with deep merge
120
+ */
121
+ static mergeSettings(base, updates) {
122
+ const merged = { ...base };
123
+ Object.keys(updates).forEach(key => {
124
+ if (updates[key] && typeof updates[key] === 'object' && !Array.isArray(updates[key])) {
125
+ merged[key] = this.mergeSettings(merged[key] || {}, updates[key]);
126
+ }
127
+ else {
128
+ merged[key] = updates[key];
129
+ }
130
+ });
131
+ return merged;
132
+ }
133
+ }