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.
- package/.mcp.json +11 -0
- package/claude-mem +0 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +64 -0
- package/dist/commands/compress.d.ts +2 -0
- package/dist/commands/compress.js +59 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +372 -0
- package/dist/commands/load-context.d.ts +2 -0
- package/dist/commands/load-context.js +330 -0
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +41 -0
- package/dist/commands/migrate.d.ts +9 -0
- package/dist/commands/migrate.js +174 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +159 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +105 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +33 -0
- package/dist/constants.d.ts +516 -0
- package/dist/constants.js +522 -0
- package/dist/error-handler.d.ts +17 -0
- package/dist/error-handler.js +103 -0
- package/dist/mcp-server-cli.d.ts +34 -0
- package/dist/mcp-server-cli.js +158 -0
- package/dist/mcp-server.d.ts +103 -0
- package/dist/mcp-server.js +269 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.js +78 -0
- package/dist/utils/HookDetector.d.ts +64 -0
- package/dist/utils/HookDetector.js +213 -0
- package/dist/utils/PathResolver.d.ts +16 -0
- package/dist/utils/PathResolver.js +55 -0
- package/dist/utils/SettingsManager.d.ts +63 -0
- package/dist/utils/SettingsManager.js +133 -0
- package/dist/utils/TranscriptCompressor.d.ts +111 -0
- package/dist/utils/TranscriptCompressor.js +486 -0
- package/dist/utils/common.d.ts +29 -0
- package/dist/utils/common.js +14 -0
- package/dist/utils/error-utils.d.ts +93 -0
- package/dist/utils/error-utils.js +238 -0
- package/dist/utils/index.d.ts +19 -0
- package/dist/utils/index.js +26 -0
- package/dist/utils/logger.d.ts +19 -0
- package/dist/utils/logger.js +42 -0
- package/dist/utils/mcp-client-factory.d.ts +51 -0
- package/dist/utils/mcp-client-factory.js +115 -0
- package/dist/utils/mcp-client.d.ts +75 -0
- package/dist/utils/mcp-client.js +120 -0
- package/dist/utils/memory-mcp-client.d.ts +135 -0
- package/dist/utils/memory-mcp-client.js +490 -0
- package/dist/utils/weaviate-mcp-adapter.d.ts +102 -0
- package/dist/utils/weaviate-mcp-adapter.js +587 -0
- package/package.json +3 -2
- 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
|
+
}
|