coder-config 0.40.1
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/LICENSE +21 -0
- package/README.md +553 -0
- package/cli.js +431 -0
- package/config-loader.js +294 -0
- package/hooks/activity-track.sh +56 -0
- package/hooks/codex-workstream.sh +44 -0
- package/hooks/gemini-workstream.sh +44 -0
- package/hooks/workstream-inject.sh +20 -0
- package/lib/activity.js +283 -0
- package/lib/apply.js +344 -0
- package/lib/cli.js +267 -0
- package/lib/config.js +171 -0
- package/lib/constants.js +55 -0
- package/lib/env.js +114 -0
- package/lib/index.js +47 -0
- package/lib/init.js +122 -0
- package/lib/mcps.js +139 -0
- package/lib/memory.js +201 -0
- package/lib/projects.js +138 -0
- package/lib/registry.js +83 -0
- package/lib/utils.js +129 -0
- package/lib/workstreams.js +652 -0
- package/package.json +80 -0
- package/scripts/capture-screenshots.js +142 -0
- package/scripts/postinstall.js +122 -0
- package/scripts/release.sh +71 -0
- package/scripts/sync-version.js +77 -0
- package/scripts/tauri-prepare.js +328 -0
- package/shared/mcp-registry.json +76 -0
- package/ui/dist/assets/index-DbZ3_HBD.js +3204 -0
- package/ui/dist/assets/index-DjLdm3Mr.css +32 -0
- package/ui/dist/icons/icon-192.svg +16 -0
- package/ui/dist/icons/icon-512.svg +16 -0
- package/ui/dist/index.html +39 -0
- package/ui/dist/manifest.json +25 -0
- package/ui/dist/sw.js +24 -0
- package/ui/dist/tutorial/claude-settings.png +0 -0
- package/ui/dist/tutorial/header.png +0 -0
- package/ui/dist/tutorial/mcp-registry.png +0 -0
- package/ui/dist/tutorial/memory-view.png +0 -0
- package/ui/dist/tutorial/permissions.png +0 -0
- package/ui/dist/tutorial/plugins-view.png +0 -0
- package/ui/dist/tutorial/project-explorer.png +0 -0
- package/ui/dist/tutorial/projects-view.png +0 -0
- package/ui/dist/tutorial/sidebar.png +0 -0
- package/ui/dist/tutorial/tutorial-view.png +0 -0
- package/ui/dist/tutorial/workstreams-view.png +0 -0
- package/ui/routes/activity.js +58 -0
- package/ui/routes/commands.js +74 -0
- package/ui/routes/configs.js +329 -0
- package/ui/routes/env.js +40 -0
- package/ui/routes/file-explorer.js +668 -0
- package/ui/routes/index.js +41 -0
- package/ui/routes/mcp-discovery.js +235 -0
- package/ui/routes/memory.js +385 -0
- package/ui/routes/package.json +3 -0
- package/ui/routes/plugins.js +466 -0
- package/ui/routes/projects.js +198 -0
- package/ui/routes/registry.js +30 -0
- package/ui/routes/rules.js +74 -0
- package/ui/routes/search.js +125 -0
- package/ui/routes/settings.js +381 -0
- package/ui/routes/subprojects.js +208 -0
- package/ui/routes/tool-sync.js +127 -0
- package/ui/routes/updates.js +339 -0
- package/ui/routes/workstreams.js +224 -0
- package/ui/server.cjs +773 -0
- package/ui/terminal-server.cjs +160 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server Discovery Routes
|
|
3
|
+
*
|
|
4
|
+
* Connects to MCP servers and discovers their available tools.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
// Cache for discovered tools (serverName -> { tools, timestamp })
|
|
12
|
+
const toolsCache = new Map();
|
|
13
|
+
const CACHE_TTL = 60000; // 1 minute cache
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Send a JSON-RPC message to an MCP server via stdio
|
|
17
|
+
*/
|
|
18
|
+
function sendMessage(proc, method, params = {}, id = 1) {
|
|
19
|
+
const message = {
|
|
20
|
+
jsonrpc: '2.0',
|
|
21
|
+
id,
|
|
22
|
+
method,
|
|
23
|
+
params
|
|
24
|
+
};
|
|
25
|
+
proc.stdin.write(JSON.stringify(message) + '\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse JSON-RPC responses from stdout
|
|
30
|
+
*/
|
|
31
|
+
function parseResponses(data) {
|
|
32
|
+
const responses = [];
|
|
33
|
+
const lines = data.toString().split('\n').filter(l => l.trim());
|
|
34
|
+
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(line);
|
|
38
|
+
if (parsed.jsonrpc === '2.0') {
|
|
39
|
+
responses.push(parsed);
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Skip non-JSON lines (could be server logs)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return responses;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Expand environment variables in a string
|
|
51
|
+
*/
|
|
52
|
+
function expandEnv(str, env = {}) {
|
|
53
|
+
if (typeof str !== 'string') return str;
|
|
54
|
+
|
|
55
|
+
// Combine process env with custom env
|
|
56
|
+
const fullEnv = { ...process.env, ...env };
|
|
57
|
+
|
|
58
|
+
return str.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, p1, p2) => {
|
|
59
|
+
const varName = p1 || p2;
|
|
60
|
+
return fullEnv[varName] || match;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Discover tools from a stdio-based MCP server
|
|
66
|
+
*/
|
|
67
|
+
async function discoverStdioTools(serverName, config) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const timeout = setTimeout(() => {
|
|
70
|
+
proc.kill();
|
|
71
|
+
reject(new Error('Timeout waiting for server response'));
|
|
72
|
+
}, 10000); // 10 second timeout
|
|
73
|
+
|
|
74
|
+
// Expand environment variables in command and args
|
|
75
|
+
const command = expandEnv(config.command, config.env);
|
|
76
|
+
const args = (config.args || []).map(arg => expandEnv(arg, config.env));
|
|
77
|
+
|
|
78
|
+
// Prepare environment
|
|
79
|
+
const env = { ...process.env };
|
|
80
|
+
if (config.env) {
|
|
81
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
82
|
+
env[key] = expandEnv(value, config.env);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const proc = spawn(command, args, {
|
|
87
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
88
|
+
env,
|
|
89
|
+
cwd: config.cwd || os.homedir(),
|
|
90
|
+
shell: process.platform === 'win32'
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
let stdout = '';
|
|
94
|
+
let stderr = '';
|
|
95
|
+
let initialized = false;
|
|
96
|
+
let toolsReceived = false;
|
|
97
|
+
|
|
98
|
+
proc.stdout.on('data', (data) => {
|
|
99
|
+
stdout += data.toString();
|
|
100
|
+
const responses = parseResponses(data);
|
|
101
|
+
|
|
102
|
+
for (const response of responses) {
|
|
103
|
+
if (response.id === 1 && !initialized) {
|
|
104
|
+
// Initialize response received, now request tools
|
|
105
|
+
initialized = true;
|
|
106
|
+
sendMessage(proc, 'tools/list', {}, 2);
|
|
107
|
+
} else if (response.id === 2 && !toolsReceived) {
|
|
108
|
+
// Tools list response
|
|
109
|
+
toolsReceived = true;
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
proc.kill();
|
|
112
|
+
|
|
113
|
+
const tools = response.result?.tools || [];
|
|
114
|
+
resolve(tools.map(t => ({
|
|
115
|
+
name: t.name,
|
|
116
|
+
description: t.description || '',
|
|
117
|
+
inputSchema: t.inputSchema
|
|
118
|
+
})));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
proc.stderr.on('data', (data) => {
|
|
124
|
+
stderr += data.toString();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
proc.on('error', (err) => {
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
reject(new Error(`Failed to spawn server: ${err.message}`));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
proc.on('close', (code) => {
|
|
133
|
+
if (!toolsReceived) {
|
|
134
|
+
clearTimeout(timeout);
|
|
135
|
+
reject(new Error(`Server exited with code ${code}. stderr: ${stderr.slice(0, 200)}`));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Send initialize request
|
|
140
|
+
sendMessage(proc, 'initialize', {
|
|
141
|
+
protocolVersion: '2024-11-05',
|
|
142
|
+
capabilities: {},
|
|
143
|
+
clientInfo: {
|
|
144
|
+
name: 'claude-config-ui',
|
|
145
|
+
version: '1.0.0'
|
|
146
|
+
}
|
|
147
|
+
}, 1);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Discover tools from an SSE-based MCP server
|
|
153
|
+
*/
|
|
154
|
+
async function discoverSseTools(serverName, config) {
|
|
155
|
+
// SSE servers are more complex - they require establishing an event stream
|
|
156
|
+
// For now, return empty and note that SSE discovery is not yet supported
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get tools for a specific MCP server
|
|
162
|
+
*/
|
|
163
|
+
async function getServerTools(manager, serverName) {
|
|
164
|
+
// Check cache first
|
|
165
|
+
const cached = toolsCache.get(serverName);
|
|
166
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
167
|
+
return { serverName, tools: cached.tools, cached: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Get registry
|
|
171
|
+
const registry = manager.loadJson(manager.registryPath) || { mcpServers: {} };
|
|
172
|
+
const config = registry.mcpServers?.[serverName];
|
|
173
|
+
|
|
174
|
+
if (!config) {
|
|
175
|
+
return { serverName, error: 'Server not found in registry' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
let tools;
|
|
180
|
+
|
|
181
|
+
if (config.command) {
|
|
182
|
+
// stdio server
|
|
183
|
+
tools = await discoverStdioTools(serverName, config);
|
|
184
|
+
} else if (config.url) {
|
|
185
|
+
// SSE server
|
|
186
|
+
tools = await discoverSseTools(serverName, config);
|
|
187
|
+
} else {
|
|
188
|
+
return { serverName, error: 'Unknown server type' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Cache the result
|
|
192
|
+
toolsCache.set(serverName, { tools, timestamp: Date.now() });
|
|
193
|
+
|
|
194
|
+
return { serverName, tools, cached: false };
|
|
195
|
+
} catch (err) {
|
|
196
|
+
return { serverName, error: err.message };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get tools for all MCP servers
|
|
202
|
+
*/
|
|
203
|
+
async function getAllServerTools(manager) {
|
|
204
|
+
const registry = manager.loadJson(manager.registryPath) || { mcpServers: {} };
|
|
205
|
+
const serverNames = Object.keys(registry.mcpServers || {});
|
|
206
|
+
|
|
207
|
+
const results = await Promise.all(
|
|
208
|
+
serverNames.map(name => getServerTools(manager, name))
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return results.reduce((acc, result) => {
|
|
212
|
+
acc[result.serverName] = result.error
|
|
213
|
+
? { error: result.error }
|
|
214
|
+
: { tools: result.tools, cached: result.cached };
|
|
215
|
+
return acc;
|
|
216
|
+
}, {});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Clear the tools cache
|
|
221
|
+
*/
|
|
222
|
+
function clearCache(serverName = null) {
|
|
223
|
+
if (serverName) {
|
|
224
|
+
toolsCache.delete(serverName);
|
|
225
|
+
} else {
|
|
226
|
+
toolsCache.clear();
|
|
227
|
+
}
|
|
228
|
+
return { success: true };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
getServerTools,
|
|
233
|
+
getAllServerTools,
|
|
234
|
+
clearCache
|
|
235
|
+
};
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Routes
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get all memory files (global + project + sync)
|
|
11
|
+
*/
|
|
12
|
+
function getMemory(projectDir) {
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
const globalMemoryDir = path.join(home, '.claude', 'memory');
|
|
15
|
+
const projectMemoryDir = path.join(projectDir, '.claude', 'memory');
|
|
16
|
+
const syncDir = path.join(home, '.claude', 'sync');
|
|
17
|
+
const templatesDir = path.join(home, '.claude', 'templates', 'project-memory');
|
|
18
|
+
|
|
19
|
+
const result = {
|
|
20
|
+
global: {
|
|
21
|
+
dir: globalMemoryDir,
|
|
22
|
+
files: []
|
|
23
|
+
},
|
|
24
|
+
project: {
|
|
25
|
+
dir: projectMemoryDir,
|
|
26
|
+
files: [],
|
|
27
|
+
initialized: false
|
|
28
|
+
},
|
|
29
|
+
sync: {
|
|
30
|
+
dir: syncDir,
|
|
31
|
+
state: null,
|
|
32
|
+
history: []
|
|
33
|
+
},
|
|
34
|
+
templates: {
|
|
35
|
+
dir: templatesDir,
|
|
36
|
+
available: fs.existsSync(templatesDir)
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Global memory files
|
|
41
|
+
const globalFiles = ['index.md', 'preferences.md', 'corrections.md', 'facts.md'];
|
|
42
|
+
for (const file of globalFiles) {
|
|
43
|
+
const filePath = path.join(globalMemoryDir, file);
|
|
44
|
+
result.global.files.push({
|
|
45
|
+
name: file,
|
|
46
|
+
path: filePath,
|
|
47
|
+
exists: fs.existsSync(filePath),
|
|
48
|
+
type: file.replace('.md', '')
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Project memory files
|
|
53
|
+
const projectFiles = ['context.md', 'patterns.md', 'decisions.md', 'issues.md', 'history.md'];
|
|
54
|
+
result.project.initialized = fs.existsSync(projectMemoryDir);
|
|
55
|
+
for (const file of projectFiles) {
|
|
56
|
+
const filePath = path.join(projectMemoryDir, file);
|
|
57
|
+
result.project.files.push({
|
|
58
|
+
name: file,
|
|
59
|
+
path: filePath,
|
|
60
|
+
exists: fs.existsSync(filePath),
|
|
61
|
+
type: file.replace('.md', '')
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Sync state
|
|
66
|
+
const stateJsonPath = path.join(syncDir, 'state.json');
|
|
67
|
+
const stateMdPath = path.join(syncDir, 'state.md');
|
|
68
|
+
if (fs.existsSync(stateJsonPath)) {
|
|
69
|
+
try {
|
|
70
|
+
result.sync.state = JSON.parse(fs.readFileSync(stateJsonPath, 'utf8'));
|
|
71
|
+
result.sync.stateMd = fs.existsSync(stateMdPath) ? fs.readFileSync(stateMdPath, 'utf8') : null;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
result.sync.state = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Sync history
|
|
78
|
+
const historyDir = path.join(syncDir, 'history');
|
|
79
|
+
if (fs.existsSync(historyDir)) {
|
|
80
|
+
try {
|
|
81
|
+
const files = fs.readdirSync(historyDir)
|
|
82
|
+
.filter(f => f.endsWith('.json'))
|
|
83
|
+
.sort()
|
|
84
|
+
.reverse()
|
|
85
|
+
.slice(0, 10);
|
|
86
|
+
result.sync.history = files.map(f => ({
|
|
87
|
+
name: f,
|
|
88
|
+
path: path.join(historyDir, f)
|
|
89
|
+
}));
|
|
90
|
+
} catch (e) {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get a specific memory file content
|
|
98
|
+
*/
|
|
99
|
+
function getMemoryFile(filePath, projectDir) {
|
|
100
|
+
if (!filePath) {
|
|
101
|
+
return { error: 'Path required' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const home = os.homedir();
|
|
105
|
+
const normalizedPath = path.resolve(filePath);
|
|
106
|
+
const isGlobalMemory = normalizedPath.startsWith(path.join(home, '.claude'));
|
|
107
|
+
const isProjectMemory = normalizedPath.startsWith(path.join(projectDir, '.claude'));
|
|
108
|
+
|
|
109
|
+
if (!isGlobalMemory && !isProjectMemory) {
|
|
110
|
+
return { error: 'Access denied: path must be within .claude directory' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
114
|
+
return { content: '', exists: false };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const content = fs.readFileSync(normalizedPath, 'utf8');
|
|
119
|
+
return { content, exists: true, path: normalizedPath };
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return { error: e.message };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Save a memory file
|
|
127
|
+
*/
|
|
128
|
+
function saveMemoryFile(body, projectDir) {
|
|
129
|
+
const { path: filePath, content } = body;
|
|
130
|
+
|
|
131
|
+
if (!filePath) {
|
|
132
|
+
return { error: 'Path required' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const home = os.homedir();
|
|
136
|
+
const normalizedPath = path.resolve(filePath);
|
|
137
|
+
const isGlobalMemory = normalizedPath.startsWith(path.join(home, '.claude'));
|
|
138
|
+
const isProjectMemory = normalizedPath.startsWith(path.join(projectDir, '.claude'));
|
|
139
|
+
|
|
140
|
+
if (!isGlobalMemory && !isProjectMemory) {
|
|
141
|
+
return { error: 'Access denied: path must be within .claude directory' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const dir = path.dirname(normalizedPath);
|
|
146
|
+
if (!fs.existsSync(dir)) {
|
|
147
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fs.writeFileSync(normalizedPath, content, 'utf8');
|
|
151
|
+
return { success: true, path: normalizedPath };
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return { error: e.message };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Add a memory entry to the appropriate file
|
|
159
|
+
*/
|
|
160
|
+
function addMemoryEntry(body, projectDir) {
|
|
161
|
+
const { type, content, scope = 'global' } = body;
|
|
162
|
+
|
|
163
|
+
if (!type || !content) {
|
|
164
|
+
return { error: 'Type and content required' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const typeToFile = {
|
|
168
|
+
preference: { file: 'preferences.md', dir: 'global' },
|
|
169
|
+
correction: { file: 'corrections.md', dir: 'global' },
|
|
170
|
+
fact: { file: 'facts.md', dir: 'global' },
|
|
171
|
+
pattern: { file: 'patterns.md', dir: 'project' },
|
|
172
|
+
decision: { file: 'decisions.md', dir: 'project' },
|
|
173
|
+
issue: { file: 'issues.md', dir: 'project' },
|
|
174
|
+
history: { file: 'history.md', dir: 'project' },
|
|
175
|
+
context: { file: 'context.md', dir: 'project' }
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const mapping = typeToFile[type];
|
|
179
|
+
if (!mapping) {
|
|
180
|
+
return { error: `Unknown type: ${type}. Valid types: ${Object.keys(typeToFile).join(', ')}` };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const home = os.homedir();
|
|
184
|
+
let targetDir;
|
|
185
|
+
if (mapping.dir === 'global' || scope === 'global') {
|
|
186
|
+
targetDir = path.join(home, '.claude', 'memory');
|
|
187
|
+
} else {
|
|
188
|
+
targetDir = path.join(projectDir, '.claude', 'memory');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const targetPath = path.join(targetDir, mapping.file);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
if (!fs.existsSync(targetDir)) {
|
|
195
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let existing = '';
|
|
199
|
+
if (fs.existsSync(targetPath)) {
|
|
200
|
+
existing = fs.readFileSync(targetPath, 'utf8');
|
|
201
|
+
} else {
|
|
202
|
+
existing = `# ${type.charAt(0).toUpperCase() + type.slice(1)}s\n\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
206
|
+
const entry = `\n## [${timestamp}]\n${content}\n`;
|
|
207
|
+
|
|
208
|
+
fs.writeFileSync(targetPath, existing + entry, 'utf8');
|
|
209
|
+
|
|
210
|
+
return { success: true, path: targetPath, type };
|
|
211
|
+
} catch (e) {
|
|
212
|
+
return { error: e.message };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Initialize project memory from templates
|
|
218
|
+
*/
|
|
219
|
+
function initProjectMemory(dir, projectDir) {
|
|
220
|
+
const targetDir = dir || projectDir;
|
|
221
|
+
const home = os.homedir();
|
|
222
|
+
const templatesDir = path.join(home, '.claude', 'templates', 'project-memory');
|
|
223
|
+
const memoryDir = path.join(targetDir, '.claude', 'memory');
|
|
224
|
+
|
|
225
|
+
if (!fs.existsSync(templatesDir)) {
|
|
226
|
+
const defaultTemplates = {
|
|
227
|
+
'context.md': `# Project Context\n\n## Overview\n[Describe what this project does]\n\n## Tech Stack\n- \n\n## Key Conventions\n- \n`,
|
|
228
|
+
'patterns.md': `# Code Patterns\n\n## Common Patterns\n[Document recurring patterns in this codebase]\n`,
|
|
229
|
+
'decisions.md': `# Architecture Decisions\n\n## ADRs\n[Record important decisions and their rationale]\n`,
|
|
230
|
+
'issues.md': `# Known Issues\n\n## Current Issues\n[Track bugs, limitations, and workarounds]\n`,
|
|
231
|
+
'history.md': `# Session History\n\n[Chronological log of significant work]\n`
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
236
|
+
for (const [file, content] of Object.entries(defaultTemplates)) {
|
|
237
|
+
fs.writeFileSync(path.join(templatesDir, file), content, 'utf8');
|
|
238
|
+
}
|
|
239
|
+
} catch (e) {
|
|
240
|
+
return { error: `Failed to create templates: ${e.message}` };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (fs.existsSync(memoryDir)) {
|
|
245
|
+
return { error: 'Project memory already exists', dir: memoryDir };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
250
|
+
const templateFiles = fs.readdirSync(templatesDir);
|
|
251
|
+
|
|
252
|
+
for (const file of templateFiles) {
|
|
253
|
+
const src = path.join(templatesDir, file);
|
|
254
|
+
const dest = path.join(memoryDir, file);
|
|
255
|
+
if (fs.statSync(src).isFile()) {
|
|
256
|
+
fs.copyFileSync(src, dest);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { success: true, dir: memoryDir, files: templateFiles };
|
|
261
|
+
} catch (e) {
|
|
262
|
+
return { error: e.message };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Search memory files
|
|
268
|
+
*/
|
|
269
|
+
function searchMemory(query, projectDir) {
|
|
270
|
+
if (!query) {
|
|
271
|
+
return { results: [] };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const home = os.homedir();
|
|
275
|
+
const searchDirs = [
|
|
276
|
+
path.join(home, '.claude', 'memory'),
|
|
277
|
+
path.join(projectDir, '.claude', 'memory')
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const results = [];
|
|
281
|
+
const queryLower = query.toLowerCase();
|
|
282
|
+
|
|
283
|
+
for (const dir of searchDirs) {
|
|
284
|
+
if (!fs.existsSync(dir)) continue;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
288
|
+
|
|
289
|
+
for (const file of files) {
|
|
290
|
+
const filePath = path.join(dir, file);
|
|
291
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
292
|
+
|
|
293
|
+
if (content.toLowerCase().includes(queryLower)) {
|
|
294
|
+
const lines = content.split('\n');
|
|
295
|
+
const matches = [];
|
|
296
|
+
|
|
297
|
+
for (let i = 0; i < lines.length; i++) {
|
|
298
|
+
if (lines[i].toLowerCase().includes(queryLower)) {
|
|
299
|
+
matches.push({
|
|
300
|
+
line: i + 1,
|
|
301
|
+
text: lines[i].trim().substring(0, 200)
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (matches.length > 0) {
|
|
307
|
+
results.push({
|
|
308
|
+
file,
|
|
309
|
+
path: filePath,
|
|
310
|
+
scope: dir.includes(projectDir) ? 'project' : 'global',
|
|
311
|
+
matches: matches.slice(0, 5)
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch (e) {}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { query, results };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get sync state
|
|
324
|
+
*/
|
|
325
|
+
function getSyncState() {
|
|
326
|
+
const home = os.homedir();
|
|
327
|
+
const syncDir = path.join(home, '.claude', 'sync');
|
|
328
|
+
|
|
329
|
+
const result = {
|
|
330
|
+
dir: syncDir,
|
|
331
|
+
state: null,
|
|
332
|
+
stateMd: null,
|
|
333
|
+
history: []
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const stateJsonPath = path.join(syncDir, 'state.json');
|
|
337
|
+
if (fs.existsSync(stateJsonPath)) {
|
|
338
|
+
try {
|
|
339
|
+
result.state = JSON.parse(fs.readFileSync(stateJsonPath, 'utf8'));
|
|
340
|
+
} catch (e) {}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const stateMdPath = path.join(syncDir, 'state.md');
|
|
344
|
+
if (fs.existsSync(stateMdPath)) {
|
|
345
|
+
try {
|
|
346
|
+
result.stateMd = fs.readFileSync(stateMdPath, 'utf8');
|
|
347
|
+
} catch (e) {}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const historyDir = path.join(syncDir, 'history');
|
|
351
|
+
if (fs.existsSync(historyDir)) {
|
|
352
|
+
try {
|
|
353
|
+
const files = fs.readdirSync(historyDir)
|
|
354
|
+
.filter(f => f.endsWith('.json'))
|
|
355
|
+
.sort()
|
|
356
|
+
.reverse()
|
|
357
|
+
.slice(0, 10);
|
|
358
|
+
|
|
359
|
+
result.history = files.map(f => {
|
|
360
|
+
const filePath = path.join(historyDir, f);
|
|
361
|
+
try {
|
|
362
|
+
return {
|
|
363
|
+
name: f,
|
|
364
|
+
path: filePath,
|
|
365
|
+
data: JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
366
|
+
};
|
|
367
|
+
} catch (e) {
|
|
368
|
+
return { name: f, path: filePath, error: e.message };
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
} catch (e) {}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
module.exports = {
|
|
378
|
+
getMemory,
|
|
379
|
+
getMemoryFile,
|
|
380
|
+
saveMemoryFile,
|
|
381
|
+
addMemoryEntry,
|
|
382
|
+
initProjectMemory,
|
|
383
|
+
searchMemory,
|
|
384
|
+
getSyncState,
|
|
385
|
+
};
|