memtrace 0.3.23 → 0.3.25
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/README.md +1 -1
- package/installer/dist/commands/doctor.js +18 -1
- package/installer/dist/index.js +3 -3
- package/installer/dist/transformers/claude.d.ts +9 -0
- package/installer/dist/transformers/claude.js +107 -21
- package/installer/dist/transformers/codex.d.ts +8 -0
- package/installer/dist/transformers/codex.js +134 -0
- package/installer/dist/transformers/index.d.ts +2 -1
- package/installer/dist/transformers/index.js +3 -2
- package/installer/dist/transformers/types.d.ts +2 -2
- package/installer/skills/commands/memtrace-graph.md +22 -64
- package/installer/skills/workflows/memtrace-codebase-exploration.md +1 -1
- package/installer/skills/workflows/memtrace-continuous-memory.md +104 -0
- package/installer/skills/workflows/memtrace-first.md +40 -2
- package/package.json +5 -4
- package/skills/workflows/memtrace-first.md +4 -0
- package/uninstall.js +89 -13
package/README.md
CHANGED
|
@@ -238,7 +238,7 @@ Uses **Structural Significance Budgeting** to surface the minimum set of changes
|
|
|
238
238
|
For manual setup:
|
|
239
239
|
|
|
240
240
|
```bash
|
|
241
|
-
claude plugin marketplace add syncable-dev/memtrace
|
|
241
|
+
claude plugin marketplace add https://github.com/syncable-dev/memtrace-public.git
|
|
242
242
|
claude plugin install memtrace-skills@memtrace --scope user
|
|
243
243
|
claude mcp add memtrace -- memtrace mcp -e MEMTRACE_ARCADEDB_BOLT_URL=bolt://localhost:7687
|
|
244
244
|
```
|
|
@@ -34,6 +34,12 @@ function mcpHasMemtrace(file) {
|
|
|
34
34
|
return false;
|
|
35
35
|
return Boolean(value.mcpServers && value.mcpServers['memtrace']);
|
|
36
36
|
}
|
|
37
|
+
function codexMcpHasMemtrace(file) {
|
|
38
|
+
if (!fs.existsSync(file))
|
|
39
|
+
return false;
|
|
40
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
41
|
+
return /^\s*\[mcp_servers\.(?:"memtrace"|memtrace)\]\s*$/m.test(raw);
|
|
42
|
+
}
|
|
37
43
|
function checkAgent(agent) {
|
|
38
44
|
if (agent === 'claude') {
|
|
39
45
|
const skillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
@@ -46,6 +52,17 @@ function checkAgent(agent) {
|
|
|
46
52
|
mcpConfigPath,
|
|
47
53
|
};
|
|
48
54
|
}
|
|
55
|
+
if (agent === 'codex') {
|
|
56
|
+
const skillsDir = path.join(os.homedir(), '.agents', 'skills');
|
|
57
|
+
const mcpConfigPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
58
|
+
return {
|
|
59
|
+
agent,
|
|
60
|
+
skillsFound: countMemtraceSkills(skillsDir),
|
|
61
|
+
skillsDir,
|
|
62
|
+
mcpRegistered: codexMcpHasMemtrace(mcpConfigPath),
|
|
63
|
+
mcpConfigPath,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
49
66
|
const skillsDir = path.join(os.homedir(), '.cursor', 'skills');
|
|
50
67
|
const mcpConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
51
68
|
return {
|
|
@@ -60,7 +77,7 @@ export async function runDoctorChecks() {
|
|
|
60
77
|
const binary = await checkBinary();
|
|
61
78
|
return {
|
|
62
79
|
...binary,
|
|
63
|
-
agents: [checkAgent('claude'), checkAgent('cursor')],
|
|
80
|
+
agents: [checkAgent('claude'), checkAgent('cursor'), checkAgent('codex')],
|
|
64
81
|
};
|
|
65
82
|
}
|
|
66
83
|
export function formatReport(r) {
|
package/installer/dist/index.js
CHANGED
|
@@ -13,9 +13,9 @@ function parseOnly(val) {
|
|
|
13
13
|
program
|
|
14
14
|
.command('install')
|
|
15
15
|
.description('Install memtrace skills and register MCP for selected agents')
|
|
16
|
-
.option('--only <agents>', 'comma-separated agent names (claude,cursor)', parseOnly)
|
|
17
|
-
.option('--local', 'install into the current project (./.claude/, ./.cursor/)', false)
|
|
18
|
-
.option('--global', 'install globally (~/.claude/, ~/.cursor/) [default]', false)
|
|
16
|
+
.option('--only <agents>', 'comma-separated agent names (claude,cursor,codex)', parseOnly)
|
|
17
|
+
.option('--local', 'install into the current project (./.claude/, ./.cursor/, ./.agents/)', false)
|
|
18
|
+
.option('--global', 'install globally (~/.claude/, ~/.cursor/, ~/.agents/) [default]', false)
|
|
19
19
|
.option('--skip-mcp', 'write skills only, skip MCP server registration', false)
|
|
20
20
|
.option('--repair', 'alias for install — run after fixing a corrupt settings file')
|
|
21
21
|
.option('-y, --yes', 'non-interactive, accept defaults')
|
|
@@ -23,6 +23,15 @@ export declare function registerMcpInSettingsAt(settingsPath: string, memtraceBi
|
|
|
23
23
|
* Never overwrites a malformed file.
|
|
24
24
|
*/
|
|
25
25
|
export declare function enablePluginInSettingsAt(settingsPath: string): SettingsMutationResult;
|
|
26
|
+
export interface SettingsCleanupResult {
|
|
27
|
+
changed: boolean;
|
|
28
|
+
backupPath?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Remove every Claude settings entry Memtrace may have written across
|
|
32
|
+
* installer versions and Claude marketplace schema changes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function removeClaudeSettingsEntriesAt(settingsPath: string): SettingsCleanupResult;
|
|
26
35
|
/**
|
|
27
36
|
* Full Claude Code plugin installation.
|
|
28
37
|
*
|
|
@@ -5,7 +5,23 @@ import { execCommand, commandExists } from '../utils.js';
|
|
|
5
5
|
import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
|
|
6
6
|
const PLUGIN_NAME = 'memtrace-skills';
|
|
7
7
|
const MARKETPLACE_NAME = 'memtrace';
|
|
8
|
-
const MARKETPLACE_REPO = 'syncable-dev/memtrace';
|
|
8
|
+
const MARKETPLACE_REPO = 'syncable-dev/memtrace-public';
|
|
9
|
+
const MARKETPLACE_GIT_URL = `https://github.com/${MARKETPLACE_REPO}.git`;
|
|
10
|
+
const PLUGIN_KEY = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
11
|
+
const MARKETPLACE_SETTING_KEYS = [
|
|
12
|
+
MARKETPLACE_NAME,
|
|
13
|
+
'syncable-dev-memtrace',
|
|
14
|
+
'syncable-dev-memtrace-public',
|
|
15
|
+
];
|
|
16
|
+
const MARKETPLACE_SETTING_CONTAINERS = [
|
|
17
|
+
'extraKnownMarketplaces',
|
|
18
|
+
'marketplaces',
|
|
19
|
+
'pluginMarketplaces',
|
|
20
|
+
'knownMarketplaces',
|
|
21
|
+
];
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
9
25
|
/**
|
|
10
26
|
* Try to register the memtrace MCP server via `claude mcp add-json`.
|
|
11
27
|
* Returns true on success, false on timeout/error/missing CLI.
|
|
@@ -87,7 +103,7 @@ async function tryClaudeCliInstall() {
|
|
|
87
103
|
if (!hasClaude)
|
|
88
104
|
return false;
|
|
89
105
|
try {
|
|
90
|
-
await execCommand(`claude plugin marketplace add ${
|
|
106
|
+
await execCommand(`claude plugin marketplace add --scope user ${MARKETPLACE_GIT_URL}`);
|
|
91
107
|
}
|
|
92
108
|
catch {
|
|
93
109
|
// Marketplace may already exist
|
|
@@ -101,8 +117,7 @@ async function tryClaudeCliInstall() {
|
|
|
101
117
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
102
118
|
if (fs.existsSync(settingsPath)) {
|
|
103
119
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
104
|
-
|
|
105
|
-
if (settings.enabledPlugins?.[key] === true) {
|
|
120
|
+
if (settings.enabledPlugins?.[PLUGIN_KEY] === true) {
|
|
106
121
|
return true;
|
|
107
122
|
}
|
|
108
123
|
}
|
|
@@ -168,10 +183,10 @@ export function enablePluginInSettingsAt(settingsPath) {
|
|
|
168
183
|
}
|
|
169
184
|
const settings = (value ?? {});
|
|
170
185
|
settings.enabledPlugins = settings.enabledPlugins ?? {};
|
|
171
|
-
settings.enabledPlugins[
|
|
186
|
+
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
172
187
|
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces ?? {};
|
|
173
188
|
settings.extraKnownMarketplaces[MARKETPLACE_NAME] = {
|
|
174
|
-
source: { source: '
|
|
189
|
+
source: { source: 'git', url: MARKETPLACE_GIT_URL },
|
|
175
190
|
};
|
|
176
191
|
writeJsonAtomic(settingsPath, settings);
|
|
177
192
|
return { registered: true };
|
|
@@ -183,6 +198,79 @@ function enablePluginInSettings() {
|
|
|
183
198
|
const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
|
|
184
199
|
enablePluginInSettingsAt(settingsFile);
|
|
185
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Remove every Claude settings entry Memtrace may have written across
|
|
203
|
+
* installer versions and Claude marketplace schema changes.
|
|
204
|
+
*/
|
|
205
|
+
export function removeClaudeSettingsEntriesAt(settingsPath) {
|
|
206
|
+
const { value, corrupted, backupPath } = safeReadJson(settingsPath);
|
|
207
|
+
if (corrupted) {
|
|
208
|
+
console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped Claude settings cleanup.`);
|
|
209
|
+
return { changed: false, backupPath };
|
|
210
|
+
}
|
|
211
|
+
if (!value)
|
|
212
|
+
return { changed: false };
|
|
213
|
+
const settings = value;
|
|
214
|
+
let changed = false;
|
|
215
|
+
const enabledPlugins = settings.enabledPlugins;
|
|
216
|
+
if (isRecord(enabledPlugins)) {
|
|
217
|
+
for (const key of Object.keys(enabledPlugins)) {
|
|
218
|
+
if (key === PLUGIN_KEY || (key.startsWith(`${PLUGIN_NAME}@`) && key.includes('memtrace'))) {
|
|
219
|
+
delete enabledPlugins[key];
|
|
220
|
+
changed = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (Object.keys(enabledPlugins).length === 0)
|
|
224
|
+
delete settings.enabledPlugins;
|
|
225
|
+
}
|
|
226
|
+
const mcpServers = settings.mcpServers;
|
|
227
|
+
if (isRecord(mcpServers) && Object.hasOwn(mcpServers, 'memtrace')) {
|
|
228
|
+
delete mcpServers.memtrace;
|
|
229
|
+
changed = true;
|
|
230
|
+
if (Object.keys(mcpServers).length === 0)
|
|
231
|
+
delete settings.mcpServers;
|
|
232
|
+
}
|
|
233
|
+
for (const containerName of MARKETPLACE_SETTING_CONTAINERS) {
|
|
234
|
+
const container = settings[containerName];
|
|
235
|
+
if (!isRecord(container))
|
|
236
|
+
continue;
|
|
237
|
+
for (const key of MARKETPLACE_SETTING_KEYS) {
|
|
238
|
+
if (Object.hasOwn(container, key)) {
|
|
239
|
+
delete container[key];
|
|
240
|
+
changed = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (Object.keys(container).length === 0)
|
|
244
|
+
delete settings[containerName];
|
|
245
|
+
}
|
|
246
|
+
if (changed)
|
|
247
|
+
writeJsonAtomic(settingsPath, settings);
|
|
248
|
+
return { changed };
|
|
249
|
+
}
|
|
250
|
+
function removeClaudeMarketplaceCacheDirs() {
|
|
251
|
+
const marketplacesRoot = path.join(os.homedir(), '.claude', 'plugins', 'marketplaces');
|
|
252
|
+
for (const entry of MARKETPLACE_SETTING_KEYS) {
|
|
253
|
+
const dir = path.join(marketplacesRoot, entry);
|
|
254
|
+
if (fs.existsSync(dir)) {
|
|
255
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function removeClaudeInstalledPluginMetadata() {
|
|
260
|
+
const installedPluginsFile = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
261
|
+
const { value, corrupted } = safeReadJson(installedPluginsFile);
|
|
262
|
+
if (corrupted || !value || !isRecord(value.plugins))
|
|
263
|
+
return;
|
|
264
|
+
let changed = false;
|
|
265
|
+
for (const key of Object.keys(value.plugins)) {
|
|
266
|
+
if (key === PLUGIN_KEY || (key.startsWith(`${PLUGIN_NAME}@`) && key.includes('memtrace'))) {
|
|
267
|
+
delete value.plugins[key];
|
|
268
|
+
changed = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (changed)
|
|
272
|
+
writeJsonAtomic(installedPluginsFile, value);
|
|
273
|
+
}
|
|
186
274
|
/**
|
|
187
275
|
* Register the memtrace MCP server in Claude Code's settings.
|
|
188
276
|
* This adds the MCP server config so Claude Code can connect to memtrace tools.
|
|
@@ -204,9 +292,13 @@ async function registerMcpServer(memtraceBinaryPath) {
|
|
|
204
292
|
* 3. Register MCP server for memtrace tools
|
|
205
293
|
*/
|
|
206
294
|
export async function installClaudePlugin(skills, memtraceBinaryPath) {
|
|
207
|
-
// Step 1:
|
|
295
|
+
// Step 1: make sure Claude sees the public HTTPS marketplace source before
|
|
296
|
+
// the CLI attempts to clone/update it.
|
|
297
|
+
enablePluginInSettings();
|
|
298
|
+
removeClaudeMarketplaceCacheDirs();
|
|
299
|
+
// Step 2: Try CLI install
|
|
208
300
|
await tryClaudeCliInstall();
|
|
209
|
-
// Step
|
|
301
|
+
// Step 3: Find or create cache dir
|
|
210
302
|
let cacheDir = findCliInstalledCacheDir();
|
|
211
303
|
if (cacheDir) {
|
|
212
304
|
const orphanedFile = path.join(cacheDir, '.orphaned_at');
|
|
@@ -216,7 +308,7 @@ export async function installClaudePlugin(skills, memtraceBinaryPath) {
|
|
|
216
308
|
else {
|
|
217
309
|
cacheDir = getClaudePluginCacheDir();
|
|
218
310
|
}
|
|
219
|
-
// Step
|
|
311
|
+
// Step 4: Clean up old versions
|
|
220
312
|
const pluginRoot = getPluginCacheRoot();
|
|
221
313
|
if (fs.existsSync(pluginRoot)) {
|
|
222
314
|
const activeDirName = path.basename(cacheDir);
|
|
@@ -226,7 +318,7 @@ export async function installClaudePlugin(skills, memtraceBinaryPath) {
|
|
|
226
318
|
}
|
|
227
319
|
}
|
|
228
320
|
}
|
|
229
|
-
// Step
|
|
321
|
+
// Step 5: Write skills
|
|
230
322
|
const skillsDir = path.join(cacheDir, 'skills');
|
|
231
323
|
if (fs.existsSync(skillsDir)) {
|
|
232
324
|
fs.rmSync(skillsDir, { recursive: true });
|
|
@@ -241,9 +333,9 @@ export async function installClaudePlugin(skills, memtraceBinaryPath) {
|
|
|
241
333
|
}
|
|
242
334
|
writePluginManifest(cacheDir);
|
|
243
335
|
enablePluginInSettings();
|
|
244
|
-
// Step
|
|
336
|
+
// Step 6: Register MCP server
|
|
245
337
|
await registerMcpServer(memtraceBinaryPath);
|
|
246
|
-
// Step
|
|
338
|
+
// Step 7: Write user-level skills for SDK-based integrations
|
|
247
339
|
writeUserLevelSkills(skills);
|
|
248
340
|
return { cacheDir, skillCount: skills.length };
|
|
249
341
|
}
|
|
@@ -281,18 +373,12 @@ export async function uninstallClaudePlugin() {
|
|
|
281
373
|
const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
|
|
282
374
|
if (fs.existsSync(settingsFile)) {
|
|
283
375
|
try {
|
|
284
|
-
|
|
285
|
-
const pluginKey = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
286
|
-
if (settings.enabledPlugins && typeof settings.enabledPlugins === 'object') {
|
|
287
|
-
delete settings.enabledPlugins[pluginKey];
|
|
288
|
-
}
|
|
289
|
-
if (settings.mcpServers && typeof settings.mcpServers === 'object') {
|
|
290
|
-
delete settings.mcpServers['memtrace'];
|
|
291
|
-
}
|
|
292
|
-
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
|
|
376
|
+
removeClaudeSettingsEntriesAt(settingsFile);
|
|
293
377
|
}
|
|
294
378
|
catch { /* */ }
|
|
295
379
|
}
|
|
380
|
+
removeClaudeMarketplaceCacheDirs();
|
|
381
|
+
removeClaudeInstalledPluginMetadata();
|
|
296
382
|
// Clean up user-level skills
|
|
297
383
|
const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
298
384
|
if (fs.existsSync(userSkillsDir)) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Transformer, InstallContext } from './types.js';
|
|
2
|
+
export declare function codexConfigPath(ctx: InstallContext): string;
|
|
3
|
+
export declare function stripCodexMcpServer(raw: string): string;
|
|
4
|
+
export interface CodexMcpResult {
|
|
5
|
+
registered: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function registerCodexMcpAt(configFile: string, binary: string): CodexMcpResult;
|
|
8
|
+
export declare const codexTransformer: Transformer;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { commandExists, execCommand } from '../utils.js';
|
|
5
|
+
const MCP_SERVER_NAME = 'memtrace';
|
|
6
|
+
function skillsRoot(ctx) {
|
|
7
|
+
const base = ctx.scope === 'global' ? os.homedir() : ctx.cwd;
|
|
8
|
+
return path.join(base, '.agents', 'skills');
|
|
9
|
+
}
|
|
10
|
+
export function codexConfigPath(ctx) {
|
|
11
|
+
const base = ctx.scope === 'global' ? path.join(os.homedir(), '.codex') : path.join(ctx.cwd, '.codex');
|
|
12
|
+
return path.join(base, 'config.toml');
|
|
13
|
+
}
|
|
14
|
+
function writeSkill(skill, rootDir) {
|
|
15
|
+
const name = skill.filename.replace(/\.md$/, '');
|
|
16
|
+
const outDir = path.join(rootDir, name);
|
|
17
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
18
|
+
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
|
|
19
|
+
const content = `---\nname: ${name}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
|
|
20
|
+
fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
|
|
21
|
+
}
|
|
22
|
+
function tomlString(value) {
|
|
23
|
+
return `"${value
|
|
24
|
+
.replace(/\\/g, '\\\\')
|
|
25
|
+
.replace(/"/g, '\\"')
|
|
26
|
+
.replace(/\n/g, '\\n')
|
|
27
|
+
.replace(/\r/g, '\\r')}"`;
|
|
28
|
+
}
|
|
29
|
+
function writeTextAtomic(filePath, content) {
|
|
30
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
31
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
32
|
+
fs.writeFileSync(tmpPath, content);
|
|
33
|
+
fs.renameSync(tmpPath, filePath);
|
|
34
|
+
}
|
|
35
|
+
function isMemtraceMcpTable(tableName) {
|
|
36
|
+
return tableName === `mcp_servers.${MCP_SERVER_NAME}`
|
|
37
|
+
|| tableName === `mcp_servers."${MCP_SERVER_NAME}"`
|
|
38
|
+
|| tableName.startsWith(`mcp_servers.${MCP_SERVER_NAME}.`)
|
|
39
|
+
|| tableName.startsWith(`mcp_servers."${MCP_SERVER_NAME}".`);
|
|
40
|
+
}
|
|
41
|
+
export function stripCodexMcpServer(raw) {
|
|
42
|
+
const lines = raw.split(/\r?\n/);
|
|
43
|
+
const kept = [];
|
|
44
|
+
let skipping = false;
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
const table = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
47
|
+
if (table) {
|
|
48
|
+
skipping = isMemtraceMcpTable(table[1].trim());
|
|
49
|
+
if (skipping)
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!skipping)
|
|
53
|
+
kept.push(line);
|
|
54
|
+
}
|
|
55
|
+
return kept.join('\n').trimEnd();
|
|
56
|
+
}
|
|
57
|
+
export function registerCodexMcpAt(configFile, binary) {
|
|
58
|
+
const existing = fs.existsSync(configFile) ? fs.readFileSync(configFile, 'utf-8') : '';
|
|
59
|
+
const base = stripCodexMcpServer(existing);
|
|
60
|
+
const block = [
|
|
61
|
+
`[mcp_servers.${MCP_SERVER_NAME}]`,
|
|
62
|
+
`command = ${tomlString(binary)}`,
|
|
63
|
+
`args = [${tomlString('mcp')}]`,
|
|
64
|
+
'',
|
|
65
|
+
].join('\n');
|
|
66
|
+
const next = `${base}${base ? '\n\n' : ''}${block}`;
|
|
67
|
+
writeTextAtomic(configFile, next);
|
|
68
|
+
return { registered: true };
|
|
69
|
+
}
|
|
70
|
+
function shellQuote(value) {
|
|
71
|
+
if (process.platform === 'win32')
|
|
72
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
73
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
74
|
+
}
|
|
75
|
+
async function tryCodexMcpAdd(binary) {
|
|
76
|
+
if (!(await commandExists('codex')))
|
|
77
|
+
return false;
|
|
78
|
+
try {
|
|
79
|
+
await execCommand(`codex mcp add ${MCP_SERVER_NAME} -- ${shellQuote(binary)} mcp`, { timeoutMs: 5_000 });
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export const codexTransformer = {
|
|
87
|
+
name: 'codex',
|
|
88
|
+
async install(skills, ctx) {
|
|
89
|
+
const rootDir = skillsRoot(ctx);
|
|
90
|
+
for (const s of skills)
|
|
91
|
+
writeSkill(s, rootDir);
|
|
92
|
+
let mcpRegistered = false;
|
|
93
|
+
const warnings = [];
|
|
94
|
+
const mcpConfigPath = codexConfigPath(ctx);
|
|
95
|
+
if (!ctx.skipMcp) {
|
|
96
|
+
if (ctx.scope === 'global' && await tryCodexMcpAdd(ctx.memtraceBinary)) {
|
|
97
|
+
mcpRegistered = true;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
registerCodexMcpAt(mcpConfigPath, ctx.memtraceBinary);
|
|
101
|
+
mcpRegistered = true;
|
|
102
|
+
if (ctx.scope === 'global') {
|
|
103
|
+
warnings.push('codex CLI MCP registration was unavailable; wrote ~/.codex/config.toml directly.');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
agent: 'codex',
|
|
109
|
+
skillsWritten: skills.length,
|
|
110
|
+
skillsDir: rootDir,
|
|
111
|
+
mcpConfigPath,
|
|
112
|
+
mcpRegistered,
|
|
113
|
+
warnings,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
async uninstall(ctx) {
|
|
117
|
+
const rootDir = skillsRoot(ctx);
|
|
118
|
+
if (fs.existsSync(rootDir)) {
|
|
119
|
+
for (const entry of fs.readdirSync(rootDir)) {
|
|
120
|
+
if (entry.startsWith('memtrace-')) {
|
|
121
|
+
fs.rmSync(path.join(rootDir, entry), { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const configFile = codexConfigPath(ctx);
|
|
126
|
+
if (fs.existsSync(configFile)) {
|
|
127
|
+
const next = stripCodexMcpServer(fs.readFileSync(configFile, 'utf-8'));
|
|
128
|
+
if (next.trim())
|
|
129
|
+
writeTextAtomic(configFile, `${next}\n`);
|
|
130
|
+
else
|
|
131
|
+
fs.unlinkSync(configFile);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Transformer } from './types.js';
|
|
2
2
|
import { claudeTransformer } from './claude.js';
|
|
3
3
|
import { cursorTransformer } from './cursor.js';
|
|
4
|
+
import { codexTransformer } from './codex.js';
|
|
4
5
|
export declare const ALL_TRANSFORMERS: Transformer[];
|
|
5
6
|
export declare function findTransformer(name: string): Transformer | undefined;
|
|
6
|
-
export { claudeTransformer, cursorTransformer };
|
|
7
|
+
export { claudeTransformer, cursorTransformer, codexTransformer };
|
|
7
8
|
export type { Transformer, InstallContext, InstallResult, TransformResult } from './types.js';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { claudeTransformer } from './claude.js';
|
|
2
2
|
import { cursorTransformer } from './cursor.js';
|
|
3
|
-
|
|
3
|
+
import { codexTransformer } from './codex.js';
|
|
4
|
+
export const ALL_TRANSFORMERS = [claudeTransformer, cursorTransformer, codexTransformer];
|
|
4
5
|
export function findTransformer(name) {
|
|
5
6
|
return ALL_TRANSFORMERS.find(t => t.name === name);
|
|
6
7
|
}
|
|
7
|
-
export { claudeTransformer, cursorTransformer };
|
|
8
|
+
export { claudeTransformer, cursorTransformer, codexTransformer };
|
|
@@ -12,7 +12,7 @@ export interface TransformResult {
|
|
|
12
12
|
* Runtime context passed to every transformer during install/uninstall.
|
|
13
13
|
*/
|
|
14
14
|
export interface InstallContext {
|
|
15
|
-
/** 'global' = write
|
|
15
|
+
/** 'global' = write user-level config; 'local' = write project-level config under cwd. */
|
|
16
16
|
scope: 'global' | 'local';
|
|
17
17
|
/** Used for local scope. Absolute path. */
|
|
18
18
|
cwd: string;
|
|
@@ -33,7 +33,7 @@ export interface InstallResult {
|
|
|
33
33
|
* One transformer per supported AI coding agent.
|
|
34
34
|
*/
|
|
35
35
|
export interface Transformer {
|
|
36
|
-
name: 'claude' | 'cursor';
|
|
36
|
+
name: 'claude' | 'cursor' | 'codex';
|
|
37
37
|
install(skills: Skill[], ctx: InstallContext): Promise<InstallResult>;
|
|
38
38
|
uninstall(ctx: InstallContext): Promise<void>;
|
|
39
39
|
}
|
|
@@ -1,42 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: memtrace-graph
|
|
3
|
-
description: "Use when the user asks about architectural bottlenecks, important symbols, PageRank, centrality, bridge functions, code communities, logical modules, service boundaries, chokepoints, or wants to understand the high-level architecture of a codebase"
|
|
3
|
+
description: "Use when the user asks about architectural bottlenecks, important symbols, PageRank, centrality, bridge functions, code communities, logical modules, service boundaries, chokepoints, dependency paths between symbols, or wants to understand the high-level architecture of a codebase"
|
|
4
4
|
allowed-tools:
|
|
5
5
|
- mcp__memtrace__find_bridge_symbols
|
|
6
6
|
- mcp__memtrace__find_central_symbols
|
|
7
|
+
- mcp__memtrace__find_dependency_path
|
|
7
8
|
- mcp__memtrace__list_communities
|
|
8
9
|
- mcp__memtrace__list_processes
|
|
9
10
|
- mcp__memtrace__get_process_flow
|
|
10
|
-
- mcp__memtrace__execute_cypher
|
|
11
11
|
user-invocable: true
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
## Overview
|
|
15
15
|
|
|
16
|
-
Graph algorithms that reveal the structural architecture of a codebase — community detection (Louvain), centrality ranking (PageRank
|
|
16
|
+
Graph algorithms that reveal the structural architecture of a codebase — community detection (Louvain), centrality ranking (PageRank), bridge symbol identification (Tarjan articulation points), shortest-path discovery, and execution flow tracing.
|
|
17
|
+
|
|
18
|
+
All four algorithm tools (`find_central_symbols`, `find_bridge_symbols`, `find_dependency_path`, `list_communities`) run natively against the MemDB-backed knowledge graph — no Cypher required.
|
|
17
19
|
|
|
18
20
|
## Quick Reference
|
|
19
21
|
|
|
20
22
|
| Tool | Purpose |
|
|
21
23
|
|------|---------|
|
|
22
|
-
| `find_bridge_symbols` | Architectural chokepoints — symbols
|
|
23
|
-
| `find_central_symbols` | Most important symbols by PageRank or degree centrality |
|
|
24
|
+
| `find_bridge_symbols` | Architectural chokepoints — symbols whose removal disconnects the graph (Tarjan articulation points) |
|
|
25
|
+
| `find_central_symbols` | Most important symbols by **PageRank** (default) or degree centrality |
|
|
26
|
+
| `find_dependency_path` | Shortest call/import path between two symbols (BFS over typed edges) |
|
|
24
27
|
| `list_communities` | Louvain-detected logical modules/services |
|
|
25
28
|
| `list_processes` | Execution flows: HTTP handlers, background jobs, CLI commands, event handlers |
|
|
26
29
|
| `get_process_flow` | Trace a single process step-by-step |
|
|
27
|
-
| `execute_cypher` | Direct read-only Cypher queries for custom analysis |
|
|
28
|
-
|
|
29
|
-
## Parameter Types — Read This First
|
|
30
|
-
|
|
31
|
-
All memtrace MCP tools are **strictly typed**. Numbers must be JSON numbers, not strings.
|
|
32
|
-
|
|
33
|
-
| Parameter shape | Correct | Wrong (will fail deserialization) |
|
|
34
|
-
|-----------------|---------|-----------------------------------|
|
|
35
|
-
| Integer/count (`limit`, `min_size`, `depth`) | `limit: 20` | `limit: "20"` |
|
|
36
|
-
| String identifier (`repo_id`, `branch`, `name`) | `repo_id: "my-repo"` | `repo_id: my-repo` |
|
|
37
|
-
| Boolean (`fuzzy`, `include_tests`) | `fuzzy: true` | `fuzzy: "true"` |
|
|
38
|
-
|
|
39
|
-
If you see `MCP error -32602: invalid type: string "N", expected usize`, you passed a string where a number was required. Remove the quotes.
|
|
40
30
|
|
|
41
31
|
## Steps
|
|
42
32
|
|
|
@@ -44,67 +34,34 @@ If you see `MCP error -32602: invalid type: string "N", expected usize`, you pas
|
|
|
44
34
|
|
|
45
35
|
Start with `list_communities` to see how the codebase is naturally partitioned into logical modules. Each community has a name, member count, and representative symbols.
|
|
46
36
|
|
|
47
|
-
**`list_communities` parameters:**
|
|
48
|
-
- `repo_id` — string, required. Repository ID (from `list_indexed_repositories`).
|
|
49
|
-
- `branch` — string, optional. Defaults to `"main"`.
|
|
50
|
-
- `min_size` — **integer**, optional. Minimum community size to include. Default `3`.
|
|
51
|
-
- `limit` — **integer**, optional. Max communities to return. Default `50`, capped at `200`.
|
|
52
|
-
|
|
53
|
-
Example (correct):
|
|
54
|
-
```json
|
|
55
|
-
{ "repo_id": "Memtrace", "limit": 20 }
|
|
56
|
-
```
|
|
57
|
-
Example (WRONG — will fail):
|
|
58
|
-
```json
|
|
59
|
-
{ "repo_id": "Memtrace", "limit": "20" }
|
|
60
|
-
```
|
|
61
|
-
|
|
62
37
|
### 2. Find critical infrastructure
|
|
63
38
|
|
|
64
39
|
Use `find_central_symbols` to identify the most important symbols:
|
|
40
|
+
- `method: "pagerank"` — importance by link structure (default; same algorithm Google uses)
|
|
41
|
+
- `method: "degree"` — importance by direct connection count
|
|
42
|
+
- `limit` — how many to return
|
|
65
43
|
|
|
66
|
-
|
|
67
|
-
- `repo_id` — string, required.
|
|
68
|
-
- `branch` — string, optional. Defaults to `"main"`.
|
|
69
|
-
- `limit` — **integer**, optional. How many to return. Default `20`, capped at `100`.
|
|
70
|
-
- `algorithm` — string, optional. `"pagerank"` (default, via MAGE — falls back to degree if unavailable) or `"degree"` (simple in-degree count, no MAGE required).
|
|
44
|
+
The PageRank pass walks every CALLS / REFERENCES edge in the repo, distributes rank with the standard 0.85 damping factor, and converges on a stable ordering. The output is sorted by score descending, with each entry carrying `name`, `kind`, `file_path`, `score`, and the `in_degree`/`out_degree` it accumulated during the walk.
|
|
71
45
|
|
|
72
46
|
### 3. Find architectural chokepoints
|
|
73
47
|
|
|
74
|
-
Use `find_bridge_symbols` to find symbols that, if removed, would disconnect parts of the graph. These are:
|
|
48
|
+
Use `find_bridge_symbols` to find symbols that, if removed, would disconnect parts of the graph (Tarjan articulation points). These are:
|
|
75
49
|
- **Single points of failure** — if they break, cascading failures occur
|
|
76
50
|
- **Integration points** — good places for interfaces/contracts
|
|
77
51
|
- **Refactoring targets** — often too much responsibility concentrated in one place
|
|
78
52
|
|
|
79
|
-
|
|
80
|
-
- `repo_id` — string, required.
|
|
81
|
-
- `branch` — string, optional. Defaults to `"main"`.
|
|
82
|
-
- `limit` — **integer**, optional. Default `15`, capped at `50`.
|
|
53
|
+
### 4. Discover the path between two symbols
|
|
83
54
|
|
|
84
|
-
|
|
55
|
+
Use `find_dependency_path` to answer "how does symbol A reach symbol B?" — returns the shortest call/import chain via BFS over typed edges. Useful for:
|
|
56
|
+
- "Why does the auth handler depend on the database client?"
|
|
57
|
+
- "How does this CLI command reach the logging subsystem?"
|
|
58
|
+
- "Confirm symbol X actually transitively depends on Y."
|
|
85
59
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
**`list_processes` parameters:**
|
|
89
|
-
- `repo_id` — string, required.
|
|
90
|
-
- `branch` — string, optional. Defaults to `"main"`.
|
|
91
|
-
- `limit` — **integer**, optional. Default `50`.
|
|
92
|
-
|
|
93
|
-
Use `get_process_flow` with a process name to trace a specific flow step-by-step — shows the full call chain from entry point through business logic to data access.
|
|
60
|
+
### 5. Trace execution flows
|
|
94
61
|
|
|
95
|
-
|
|
96
|
-
- `process` — string, required. Process name or entry-point symbol name (from `list_processes`).
|
|
97
|
-
- `repo_id` — string, required.
|
|
98
|
-
- `branch` — string, optional. Defaults to `"main"`.
|
|
99
|
-
|
|
100
|
-
### 5. Custom queries
|
|
101
|
-
|
|
102
|
-
Use `execute_cypher` for advanced graph queries not covered by built-in tools. This is read-only and runs directly against the knowledge graph.
|
|
62
|
+
Use `list_processes` to see all entry points (HTTP handlers, background jobs, CLI commands, event handlers).
|
|
103
63
|
|
|
104
|
-
|
|
105
|
-
- `query` — string, required. A read-only Cypher query. Write keywords (CREATE, MERGE, DELETE, SET, etc.) are forbidden. Use `$repo_id` to scope to a repository.
|
|
106
|
-
- `params` — object, optional. JSON object of parameter bindings.
|
|
107
|
-
- `repo_id` — string, optional. If provided, injected as `$repo_id` into `params`.
|
|
64
|
+
Use `get_process_flow` with a process name to trace a specific flow step-by-step — shows the full call chain from entry point through business logic to data access, ordered by the indexed `step` property on each STEP_IN_PROCESS edge.
|
|
108
65
|
|
|
109
66
|
## Decision Points
|
|
110
67
|
|
|
@@ -113,5 +70,6 @@ Use `execute_cypher` for advanced graph queries not covered by built-in tools. T
|
|
|
113
70
|
| "What are the main modules?" | `list_communities` |
|
|
114
71
|
| "What are the most important functions?" | `find_central_symbols` with method=pagerank |
|
|
115
72
|
| "Where are the bottlenecks?" | `find_bridge_symbols` |
|
|
73
|
+
| "How does symbol A reach symbol B?" | `find_dependency_path` |
|
|
116
74
|
| "How does a request flow through the system?" | `list_processes` → `get_process_flow` |
|
|
117
75
|
| "What's the entry point for feature X?" | `list_processes`, then filter by name |
|
|
@@ -50,7 +50,7 @@ Each community represents a cohesive module — these are the "areas" of the cod
|
|
|
50
50
|
|
|
51
51
|
### 4. Find the most important symbols
|
|
52
52
|
|
|
53
|
-
Call `find_central_symbols` with `method: "pagerank"
|
|
53
|
+
Call `find_central_symbols` with `limit: 15`. It ranks symbols by PageRank over the repo's CALLS / REFERENCES edges (default `method: "pagerank"`, 0.85 damping factor).
|
|
54
54
|
|
|
55
55
|
These are the symbols that the rest of the codebase depends on most heavily. They form the "skeleton" of the architecture.
|
|
56
56
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memtrace-continuous-memory
|
|
3
|
+
description: "Use when the user asks to keep memtrace fresh while editing, watch a directory for live updates, enable incremental indexing, set up always-on memory, or wants their just-saved code to be queryable immediately"
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- mcp__memtrace__watch_directory
|
|
6
|
+
- mcp__memtrace__list_watched_paths
|
|
7
|
+
- mcp__memtrace__unwatch_directory
|
|
8
|
+
- mcp__memtrace__index_directory
|
|
9
|
+
- mcp__memtrace__list_indexed_repositories
|
|
10
|
+
- mcp__memtrace__check_job_status
|
|
11
|
+
user-invocable: true
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Memtrace keeps the knowledge graph live as you edit. Once you call `watch_directory`, every save runs through the **incremental indexing fast-path** — a notify-based file watcher debounces saves, the indexer re-parses only the touched files, and the engine commits the delta in a single WAL transaction. Steady-state latency is **~80 ms from save to queryable** on a typical project.
|
|
17
|
+
|
|
18
|
+
This is what makes "session continuity" actually work: by the time you ask `find_symbol` after a save, the new symbol is already in the graph.
|
|
19
|
+
|
|
20
|
+
## Steps
|
|
21
|
+
|
|
22
|
+
### 1. Confirm the repo is indexed
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
mcp__memtrace__list_indexed_repositories
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If the repo isn't there, run `index_directory` first. The watcher requires an existing repo_id — it never bootstraps from scratch.
|
|
29
|
+
|
|
30
|
+
### 2. Start watching
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
mcp__memtrace__watch_directory(
|
|
34
|
+
path: "/abs/path/to/repo"
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The tool registers a `notify` watcher on the directory tree, debounces save bursts (so a `:wq` that touches a swap file doesn't trigger twice), and routes deltas through the indexer's incremental fast-path. Returns immediately — watching runs in the background.
|
|
39
|
+
|
|
40
|
+
### 3. Confirm it's live
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
mcp__memtrace__list_watched_paths
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Each entry shows the watched root, the bound repo_id, and the last delta's `persist_ms`.
|
|
47
|
+
|
|
48
|
+
### 4. Edit normally — Memtrace catches up
|
|
49
|
+
|
|
50
|
+
After every save the watcher emits a `labels_updated` WebSocket event:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"event": "labels_updated",
|
|
55
|
+
"repo_id": "demo",
|
|
56
|
+
"nodes_changed": 12,
|
|
57
|
+
"persist_ms": 78,
|
|
58
|
+
"timestamp": "2026-04-27T10:42:13Z"
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Dashboards and IDE plugins subscribe to this on `/ws` and refresh themselves. As an agent you don't have to listen — your next `find_symbol` / `find_code` / `get_symbol_context` call will see the new state automatically.
|
|
63
|
+
|
|
64
|
+
### 5. Stop watching
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
mcp__memtrace__unwatch_directory(path: "/abs/path/to/repo")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Idempotent — unwatching an already-unwatched path is a no-op.
|
|
71
|
+
|
|
72
|
+
## When to Use
|
|
73
|
+
|
|
74
|
+
- **Long sessions on the same repo** — keeps `get_symbol_context` accurate without rerunning `index_directory`
|
|
75
|
+
- **Pair programming with an IDE plugin** — the dashboard's WebSocket subscription auto-refreshes panels
|
|
76
|
+
- **Demo / live coding** — every save reflects in the graph within 80–150 ms
|
|
77
|
+
- **Long-running agents** — instead of polling `index_directory`, the watcher pushes deltas
|
|
78
|
+
|
|
79
|
+
## When NOT to Use
|
|
80
|
+
|
|
81
|
+
- **One-shot batch edits** — running `index_directory --incremental` at the end is cheaper than spinning up a watcher
|
|
82
|
+
- **Generated / build output trees** — exclude paths under `target/`, `dist/`, `node_modules/` (the watcher honours common ignore patterns but a noisy build can still saturate the debounce queue)
|
|
83
|
+
- **CI / containerised runs** — file events are unreliable across container boundaries; index explicitly instead
|
|
84
|
+
|
|
85
|
+
## Latency Expectations
|
|
86
|
+
|
|
87
|
+
| Operation | Typical wall time |
|
|
88
|
+
|---|---|
|
|
89
|
+
| File save → watcher fires | < 5 ms |
|
|
90
|
+
| Debounce window | 50 ms |
|
|
91
|
+
| Incremental parse + delta persist | ~80 ms |
|
|
92
|
+
| `labels_updated` broadcast | < 1 ms after persist |
|
|
93
|
+
| Total: save → queryable | ~80–150 ms |
|
|
94
|
+
|
|
95
|
+
If you see `persist_ms` consistently above 500 ms, the saved files are larger than expected (e.g., generated bundles) — narrow the watch root or add ignore patterns.
|
|
96
|
+
|
|
97
|
+
## Common Mistakes
|
|
98
|
+
|
|
99
|
+
| Mistake | Reality |
|
|
100
|
+
|---|---|
|
|
101
|
+
| Calling `watch_directory` on an unindexed repo | Returns an error — run `index_directory` first |
|
|
102
|
+
| Watching `node_modules/` or `target/` | Saturates the watcher with build noise — point at the source root only |
|
|
103
|
+
| Polling `find_symbol` every second to "wait" for indexing | Subscribe to the `labels_updated` WS event, or just call once after the save — the delta is already there |
|
|
104
|
+
| Forgetting to `unwatch_directory` between sessions | Watchers are per-process; restarting `memtrace start` wipes them, but for hosted instances unwatching cleanly avoids leaks |
|
|
@@ -9,12 +9,50 @@ description: "Use when working on any indexed codebase before searching files, r
|
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
IF THE REPO IS INDEXED IN MEMTRACE → USE MEMTRACE TOOLS FIRST.
|
|
12
|
-
|
|
12
|
+
Memtrace returns exact file_path + start_line + end_line for every result.
|
|
13
|
+
Read the file at THAT location. Do not Grep/Glob/Find to "locate" anything
|
|
14
|
+
already in the graph.
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
Memtrace is the memory layer of the codebase. It has the full knowledge graph: every symbol, call, import, community, process, and API — with time dimension. File tools are blind to this structure.
|
|
16
18
|
|
|
17
|
-
**97% better accuracy. 83% fewer wasted tokens. No exceptions.**
|
|
19
|
+
**97% better accuracy. 83% fewer wasted tokens. No exceptions for what's in the graph.**
|
|
20
|
+
|
|
21
|
+
## Value Tracking
|
|
22
|
+
|
|
23
|
+
Do not print usage receipts in normal answers. Memtrace records tool usage, graph facts, file references, and estimated context avoided internally. Users can inspect that in the local UI's Value panel.
|
|
24
|
+
|
|
25
|
+
## What Memtrace actually indexes
|
|
26
|
+
|
|
27
|
+
Memtrace's hybrid search = **BM25 over symbol metadata** (name, signature, file_path, kind) **+ semantic vector search over embedded code bodies** (first ~1500 chars of every Function / Method / Class / Struct / Interface body), fused via Reciprocal Rank Fusion.
|
|
28
|
+
|
|
29
|
+
The semantic side means **string literals, error messages, magic constants, log strings, and any text inside an indexed symbol's body are findable through `find_code`**. The body got embedded; the embedding catches it. You do NOT need `Grep` to hunt for `STRIPE_KEY_FOO_BAR` if it lives inside a function in your indexed codebase.
|
|
30
|
+
|
|
31
|
+
## The narrow exceptions where grep/glob are still right
|
|
32
|
+
|
|
33
|
+
These are the ONLY cases where file tools beat memtrace:
|
|
34
|
+
|
|
35
|
+
- **Files outside the indexed repo.** Vendored deps, system headers, dirs `walker::is_excluded_path` skipped (`.git`, `node_modules`, `target`, `dist`). Memtrace literally cannot see them.
|
|
36
|
+
- **Non-source artifacts.** `.env`, `package.json`, build scripts, top-level `README.md`, raw config files. Memtrace indexes parseable code, not configuration text.
|
|
37
|
+
- **Pure file-inventory questions.** "How many `*.test.ts` files exist", "list every Markdown file in `docs/`". You're asking for a file count, not a symbol search.
|
|
38
|
+
- **Reading at a known path.** Once memtrace has handed you `file_path:start_line:end_line`, use `Read` — never substitute `Grep` for `Read`.
|
|
39
|
+
|
|
40
|
+
For everything else inside the indexed repo, memtrace is the right tool.
|
|
41
|
+
|
|
42
|
+
## The decision rule
|
|
43
|
+
|
|
44
|
+
| Question Claude is asking | Right tool |
|
|
45
|
+
|---|---|
|
|
46
|
+
| "Where is symbol `foo` defined?" | `find_symbol(name="foo")` → file:line. Then `Read` that range. |
|
|
47
|
+
| "What calls `foo`?" | `get_symbol_context(name="foo")` → callers with file:line each. |
|
|
48
|
+
| "How does authentication work?" | `find_code(query="authentication")` → ranked symbols with file:line. |
|
|
49
|
+
| "Find the function that uses `STRIPE_KEY_FOO_BAR`" | `find_code(query="STRIPE_KEY_FOO_BAR")` → semantic finds it inside any embedded body. |
|
|
50
|
+
| "Where's that error message `'connection refused for tenant'`?" | `find_code(query="connection refused for tenant")` → semantic catches it. |
|
|
51
|
+
| "What breaks if I change `foo`?" | `get_impact(name="foo")` → blast radius with file:line. |
|
|
52
|
+
| "What changed in `auth.ts` last week?" | `get_evolution(file_path="auth.ts", from="7d ago")`. |
|
|
53
|
+
| "List all `*.test.ts` files." | `Glob` (file inventory, not symbol search). |
|
|
54
|
+
| "Find this string in my `.env`." | `Grep` (non-source artifact). |
|
|
55
|
+
| "Read file I already have the path of." | `Read` (path is known). |
|
|
18
56
|
|
|
19
57
|
## Parameter Types — Read This Before Calling Any Tool
|
|
20
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memtrace",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.25",
|
|
4
4
|
"description": "Code intelligence graph — MCP server + AI agent skills + visualization UI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"uninstall.js"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
+
"prepack": "node ../../installer/scripts/prepare-npm-package.js",
|
|
31
32
|
"postinstall": "node install.js",
|
|
32
33
|
"preuninstall": "node uninstall.js"
|
|
33
34
|
},
|
|
@@ -36,9 +37,9 @@
|
|
|
36
37
|
"fs-extra": "^11.0.0"
|
|
37
38
|
},
|
|
38
39
|
"optionalDependencies": {
|
|
39
|
-
"@memtrace/darwin-arm64": "0.3.
|
|
40
|
-
"@memtrace/linux-x64": "0.3.
|
|
41
|
-
"@memtrace/win32-x64": "0.3.
|
|
40
|
+
"@memtrace/darwin-arm64": "0.3.25",
|
|
41
|
+
"@memtrace/linux-x64": "0.3.25",
|
|
42
|
+
"@memtrace/win32-x64": "0.3.25"
|
|
42
43
|
},
|
|
43
44
|
"engines": {
|
|
44
45
|
"node": ">=18"
|
|
@@ -18,6 +18,10 @@ Memtrace is the memory layer of the codebase. It has the full knowledge graph: e
|
|
|
18
18
|
|
|
19
19
|
**97% better accuracy. 83% fewer wasted tokens. No exceptions for what's in the graph.**
|
|
20
20
|
|
|
21
|
+
## Value Tracking
|
|
22
|
+
|
|
23
|
+
Do not print usage receipts in normal answers. Memtrace records tool usage, graph facts, file references, and estimated context avoided internally. Users can inspect that in the local UI's Value panel.
|
|
24
|
+
|
|
21
25
|
## What Memtrace actually indexes
|
|
22
26
|
|
|
23
27
|
Memtrace's hybrid search = **BM25 over symbol metadata** (name, signature, file_path, kind) **+ semantic vector search over embedded code bodies** (first ~1500 chars of every Function / Method / Class / Struct / Interface body), fused via Reciprocal Rank Fusion.
|
package/uninstall.js
CHANGED
|
@@ -11,6 +11,18 @@ const { execSync, spawnSync } = require("child_process");
|
|
|
11
11
|
const PLUGIN_NAME = "memtrace-skills";
|
|
12
12
|
const MARKETPLACE_NAME = "memtrace";
|
|
13
13
|
const MCP_SERVER_NAME = "memtrace";
|
|
14
|
+
const PLUGIN_KEY = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
15
|
+
const MARKETPLACE_SETTING_KEYS = [
|
|
16
|
+
MARKETPLACE_NAME,
|
|
17
|
+
"syncable-dev-memtrace",
|
|
18
|
+
"syncable-dev-memtrace-public",
|
|
19
|
+
];
|
|
20
|
+
const MARKETPLACE_SETTING_CONTAINERS = [
|
|
21
|
+
"extraKnownMarketplaces",
|
|
22
|
+
"marketplaces",
|
|
23
|
+
"pluginMarketplaces",
|
|
24
|
+
"knownMarketplaces",
|
|
25
|
+
];
|
|
14
26
|
|
|
15
27
|
const SKILL_NAMES = [
|
|
16
28
|
// Commands
|
|
@@ -71,13 +83,17 @@ function writeSettings(settings) {
|
|
|
71
83
|
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
function isRecord(value) {
|
|
87
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
// ── Remove MCP server ───────────────────────────────────────────────────────
|
|
75
91
|
|
|
76
92
|
function removeMcpServer() {
|
|
77
93
|
const settings = readSettings();
|
|
78
94
|
let changed = false;
|
|
79
95
|
|
|
80
|
-
if (settings.mcpServers && settings.mcpServers[MCP_SERVER_NAME]) {
|
|
96
|
+
if (isRecord(settings.mcpServers) && settings.mcpServers[MCP_SERVER_NAME]) {
|
|
81
97
|
delete settings.mcpServers[MCP_SERVER_NAME];
|
|
82
98
|
if (Object.keys(settings.mcpServers).length === 0) {
|
|
83
99
|
delete settings.mcpServers;
|
|
@@ -94,29 +110,82 @@ function removeMcpServer() {
|
|
|
94
110
|
function removePlugin() {
|
|
95
111
|
const settings = readSettings();
|
|
96
112
|
let changed = false;
|
|
97
|
-
const pluginKey = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
98
113
|
|
|
99
|
-
if (settings.enabledPlugins
|
|
100
|
-
|
|
114
|
+
if (isRecord(settings.enabledPlugins)) {
|
|
115
|
+
for (const key of Object.keys(settings.enabledPlugins)) {
|
|
116
|
+
if (key === PLUGIN_KEY || (key.startsWith(`${PLUGIN_NAME}@`) && key.includes("memtrace"))) {
|
|
117
|
+
delete settings.enabledPlugins[key];
|
|
118
|
+
changed = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
101
121
|
if (Object.keys(settings.enabledPlugins).length === 0) {
|
|
102
122
|
delete settings.enabledPlugins;
|
|
103
123
|
}
|
|
104
|
-
changed
|
|
105
|
-
console.log(`memtrace: removed plugin ${pluginKey}`);
|
|
124
|
+
if (changed) console.log(`memtrace: removed plugin ${PLUGIN_KEY}`);
|
|
106
125
|
}
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
let removedMarketplace = false;
|
|
128
|
+
for (const containerName of MARKETPLACE_SETTING_CONTAINERS) {
|
|
129
|
+
const container = settings[containerName];
|
|
130
|
+
if (!isRecord(container)) continue;
|
|
131
|
+
for (const key of MARKETPLACE_SETTING_KEYS) {
|
|
132
|
+
if (Object.prototype.hasOwnProperty.call(container, key)) {
|
|
133
|
+
delete container[key];
|
|
134
|
+
changed = true;
|
|
135
|
+
removedMarketplace = true;
|
|
136
|
+
}
|
|
112
137
|
}
|
|
113
|
-
|
|
138
|
+
if (Object.keys(container).length === 0) {
|
|
139
|
+
delete settings[containerName];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (removedMarketplace) {
|
|
114
144
|
console.log(`memtrace: removed marketplace ${MARKETPLACE_NAME}`);
|
|
115
145
|
}
|
|
116
146
|
|
|
117
147
|
if (changed) writeSettings(settings);
|
|
118
148
|
}
|
|
119
149
|
|
|
150
|
+
function removeMarketplaceCaches() {
|
|
151
|
+
const marketplacesRoot = path.join(os.homedir(), ".claude", "plugins", "marketplaces");
|
|
152
|
+
for (const entry of MARKETPLACE_SETTING_KEYS) {
|
|
153
|
+
const dir = path.join(marketplacesRoot, entry);
|
|
154
|
+
if (fs.existsSync(dir)) {
|
|
155
|
+
try {
|
|
156
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
157
|
+
console.log(`memtrace: removed marketplace cache ${entry}`);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.warn(`memtrace: failed to remove marketplace cache ${entry}: ${e.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function removeInstalledPluginMetadata() {
|
|
166
|
+
const installedPluginsFile = path.join(os.homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
167
|
+
if (!fs.existsSync(installedPluginsFile)) return;
|
|
168
|
+
let data;
|
|
169
|
+
try {
|
|
170
|
+
data = JSON.parse(fs.readFileSync(installedPluginsFile, "utf-8"));
|
|
171
|
+
} catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (!isRecord(data.plugins)) return;
|
|
175
|
+
|
|
176
|
+
let changed = false;
|
|
177
|
+
for (const key of Object.keys(data.plugins)) {
|
|
178
|
+
if (key === PLUGIN_KEY || (key.startsWith(`${PLUGIN_NAME}@`) && key.includes("memtrace"))) {
|
|
179
|
+
delete data.plugins[key];
|
|
180
|
+
changed = true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (changed) {
|
|
184
|
+
fs.writeFileSync(installedPluginsFile, JSON.stringify(data, null, 2) + "\n");
|
|
185
|
+
console.log("memtrace: removed plugin metadata");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
120
189
|
// ── Try Claude CLI uninstall ────────────────────────────────────────────────
|
|
121
190
|
|
|
122
191
|
function tryClaudeCliUninstall() {
|
|
@@ -140,11 +209,15 @@ function tryClaudeCliUninstall() {
|
|
|
140
209
|
|
|
141
210
|
// ── Main uninstall logic ───────────────────────────────────────────────────
|
|
142
211
|
|
|
143
|
-
function legacyCleanup() {
|
|
212
|
+
function legacyCleanup(options = {}) {
|
|
144
213
|
try { removeSkills(); } catch (e) { console.warn(`memtrace: skill removal failed: ${e.message}`); }
|
|
145
|
-
|
|
214
|
+
if (!options.skipClaudeCli) {
|
|
215
|
+
try { tryClaudeCliUninstall(); } catch { /* silent */ }
|
|
216
|
+
}
|
|
146
217
|
try { removePlugin(); } catch (e) { console.warn(`memtrace: plugin removal failed: ${e.message}`); }
|
|
147
218
|
try { removeMcpServer(); } catch (e) { console.warn(`memtrace: MCP server removal failed: ${e.message}`); }
|
|
219
|
+
try { removeMarketplaceCaches(); } catch (e) { console.warn(`memtrace: marketplace cache cleanup failed: ${e.message}`); }
|
|
220
|
+
try { removeInstalledPluginMetadata(); } catch (e) { console.warn(`memtrace: installed plugin metadata cleanup failed: ${e.message}`); }
|
|
148
221
|
}
|
|
149
222
|
|
|
150
223
|
function run() {
|
|
@@ -159,6 +232,9 @@ function run() {
|
|
|
159
232
|
if (result.status !== 0) {
|
|
160
233
|
console.warn(`memtrace: installer uninstall exited ${result.status}; falling back to legacy cleanup`);
|
|
161
234
|
legacyCleanup();
|
|
235
|
+
} else {
|
|
236
|
+
// Older bundled uninstallers did not remove every marketplace shape.
|
|
237
|
+
legacyCleanup({ skipClaudeCli: true });
|
|
162
238
|
}
|
|
163
239
|
} else {
|
|
164
240
|
// No bundled installer — use legacy JS-only cleanup (Claude-only)
|