memtrace 0.1.38 → 0.1.47
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 +2 -2
- package/bin/memtrace.js +102 -14
- package/install.js +21 -181
- package/installer/dist/commands/doctor.d.ts +16 -0
- package/installer/dist/commands/doctor.js +86 -0
- package/installer/dist/commands/install.d.ts +9 -0
- package/installer/dist/commands/install.js +104 -0
- package/installer/dist/commands/picker.d.ts +6 -0
- package/installer/dist/commands/picker.js +22 -0
- package/installer/dist/fs-safe.d.ts +21 -0
- package/installer/dist/fs-safe.js +35 -0
- package/installer/dist/index.d.ts +2 -0
- package/installer/dist/index.js +52 -0
- package/installer/dist/skills.d.ts +17 -0
- package/installer/dist/skills.js +64 -0
- package/installer/dist/transformers/claude.d.ts +41 -0
- package/installer/dist/transformers/claude.js +400 -0
- package/installer/dist/transformers/cursor.d.ts +7 -0
- package/installer/dist/transformers/cursor.js +84 -0
- package/installer/dist/transformers/index.d.ts +7 -0
- package/installer/dist/transformers/index.js +7 -0
- package/installer/dist/transformers/types.d.ts +39 -0
- package/installer/dist/transformers/types.js +1 -0
- package/installer/dist/utils.d.ts +5 -0
- package/installer/dist/utils.js +22 -0
- package/installer/package.json +49 -0
- package/installer/skills/commands/memtrace-api-topology.md +65 -0
- package/installer/skills/commands/memtrace-cochange.md +76 -0
- package/installer/skills/commands/memtrace-evolution.md +135 -0
- package/installer/skills/commands/memtrace-graph.md +117 -0
- package/installer/skills/commands/memtrace-impact.md +64 -0
- package/installer/skills/commands/memtrace-index.md +66 -0
- package/installer/skills/commands/memtrace-quality.md +69 -0
- package/installer/skills/commands/memtrace-relationships.md +73 -0
- package/installer/skills/commands/memtrace-search.md +67 -0
- package/installer/skills/workflows/memtrace-change-impact-analysis.md +85 -0
- package/installer/skills/workflows/memtrace-codebase-exploration.md +108 -0
- package/installer/skills/workflows/memtrace-episode-replay.md +100 -0
- package/installer/skills/workflows/memtrace-first.md +120 -0
- package/installer/skills/workflows/memtrace-incident-investigation.md +125 -0
- package/installer/skills/workflows/memtrace-refactoring-guide.md +116 -0
- package/installer/skills/workflows/memtrace-session-continuity.md +98 -0
- package/package.json +10 -5
- package/skills/commands/memtrace-api-topology.md +3 -0
- package/skills/commands/memtrace-cochange.md +3 -0
- package/skills/commands/memtrace-evolution.md +3 -0
- package/skills/commands/memtrace-graph.md +54 -4
- package/skills/commands/memtrace-impact.md +3 -0
- package/skills/commands/memtrace-index.md +3 -0
- package/skills/commands/memtrace-quality.md +3 -0
- package/skills/commands/memtrace-relationships.md +3 -0
- package/skills/commands/memtrace-search.md +18 -13
- package/skills/workflows/memtrace-first.md +12 -0
- package/uninstall.js +22 -28
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface SafeReadResult<T> {
|
|
2
|
+
/** Parsed value, or null if file missing or corrupt. */
|
|
3
|
+
value: T | null;
|
|
4
|
+
/** True if the file existed but failed to parse. */
|
|
5
|
+
corrupted: boolean;
|
|
6
|
+
/** Path to the backup copy, if we made one. */
|
|
7
|
+
backupPath?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read and JSON-parse a file without ever silently losing data.
|
|
11
|
+
* On parse failure, copies the file to <path>.corrupt-<ISO-timestamp>
|
|
12
|
+
* and returns { value: null, corrupted: true, backupPath }.
|
|
13
|
+
* Caller is responsible for deciding whether to proceed with writes.
|
|
14
|
+
*/
|
|
15
|
+
export declare function safeReadJson<T = unknown>(filePath: string): SafeReadResult<T>;
|
|
16
|
+
/**
|
|
17
|
+
* Write JSON atomically: write to a sibling .tmp file, then rename.
|
|
18
|
+
* Rename is atomic on POSIX and good enough on Windows for config files.
|
|
19
|
+
* Creates parent directories as needed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeJsonAtomic(filePath: string, data: unknown): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Read and JSON-parse a file without ever silently losing data.
|
|
5
|
+
* On parse failure, copies the file to <path>.corrupt-<ISO-timestamp>
|
|
6
|
+
* and returns { value: null, corrupted: true, backupPath }.
|
|
7
|
+
* Caller is responsible for deciding whether to proceed with writes.
|
|
8
|
+
*/
|
|
9
|
+
export function safeReadJson(filePath) {
|
|
10
|
+
if (!fs.existsSync(filePath)) {
|
|
11
|
+
return { value: null, corrupted: false };
|
|
12
|
+
}
|
|
13
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
14
|
+
try {
|
|
15
|
+
return { value: JSON.parse(raw), corrupted: false };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
const iso = new Date().toISOString().replace(/[:.]/g, '-');
|
|
19
|
+
const backupPath = `${filePath}.corrupt-${iso}`;
|
|
20
|
+
fs.copyFileSync(filePath, backupPath);
|
|
21
|
+
return { value: null, corrupted: true, backupPath };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Write JSON atomically: write to a sibling .tmp file, then rename.
|
|
26
|
+
* Rename is atomic on POSIX and good enough on Windows for config files.
|
|
27
|
+
* Creates parent directories as needed.
|
|
28
|
+
*/
|
|
29
|
+
export function writeJsonAtomic(filePath, data) {
|
|
30
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
31
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
32
|
+
const payload = JSON.stringify(data, null, 2) + '\n';
|
|
33
|
+
fs.writeFileSync(tmpPath, payload);
|
|
34
|
+
fs.renameSync(tmpPath, filePath);
|
|
35
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { runInstall, runUninstall } from './commands/install.js';
|
|
4
|
+
import { runDoctor } from './commands/doctor.js';
|
|
5
|
+
const program = new Command();
|
|
6
|
+
program
|
|
7
|
+
.name('memtrace-skills')
|
|
8
|
+
.description('Install Memtrace skills and MCP for AI coding agents')
|
|
9
|
+
.version('0.1.0');
|
|
10
|
+
function parseOnly(val) {
|
|
11
|
+
return val.split(',').map(s => s.trim()).filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
program
|
|
14
|
+
.command('install')
|
|
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)
|
|
19
|
+
.option('--skip-mcp', 'write skills only, skip MCP server registration', false)
|
|
20
|
+
.option('--repair', 'alias for install — run after fixing a corrupt settings file')
|
|
21
|
+
.option('-y, --yes', 'non-interactive, accept defaults')
|
|
22
|
+
.action(async (opts) => {
|
|
23
|
+
const options = {
|
|
24
|
+
scope: opts.local ? 'local' : 'global',
|
|
25
|
+
only: opts.only,
|
|
26
|
+
skipMcp: opts.skipMcp,
|
|
27
|
+
yes: opts.yes,
|
|
28
|
+
};
|
|
29
|
+
await runInstall(options);
|
|
30
|
+
});
|
|
31
|
+
program
|
|
32
|
+
.command('doctor')
|
|
33
|
+
.description('Check memtrace install health across all agents')
|
|
34
|
+
.action(async () => {
|
|
35
|
+
process.exit(await runDoctor());
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command('uninstall')
|
|
39
|
+
.description('Remove memtrace skills and MCP registrations')
|
|
40
|
+
.option('--only <agents>', 'comma-separated agent names', parseOnly)
|
|
41
|
+
.option('--local', 'uninstall from the current project', false)
|
|
42
|
+
.action(async (opts) => {
|
|
43
|
+
await runUninstall({
|
|
44
|
+
scope: opts.local ? 'local' : 'global',
|
|
45
|
+
only: opts.only,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
// Default: `memtrace-skills` with no subcommand → install with defaults (global, all agents)
|
|
49
|
+
program.action(async () => {
|
|
50
|
+
await runInstall({ scope: 'global' });
|
|
51
|
+
});
|
|
52
|
+
program.parse();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SkillFrontmatter {
|
|
2
|
+
name?: string;
|
|
3
|
+
description: string;
|
|
4
|
+
'allowed-tools'?: string[];
|
|
5
|
+
'user-invocable'?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface Skill {
|
|
8
|
+
filename: string;
|
|
9
|
+
category: 'commands' | 'workflows';
|
|
10
|
+
frontmatter: SkillFrontmatter;
|
|
11
|
+
body: string;
|
|
12
|
+
raw: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Load all skills from a skills directory (expects commands/ and workflows/ subdirs).
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadSkills(skillsDir: string): Skill[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Parse YAML-ish frontmatter from a skill markdown file.
|
|
5
|
+
* Handles the subset of YAML used in skill files (no nested objects).
|
|
6
|
+
*/
|
|
7
|
+
function parseFrontmatter(raw) {
|
|
8
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
9
|
+
if (!match)
|
|
10
|
+
return null;
|
|
11
|
+
const frontmatterRaw = match[1];
|
|
12
|
+
const body = match[2];
|
|
13
|
+
const frontmatter = {};
|
|
14
|
+
// Parse description
|
|
15
|
+
const descMatch = frontmatterRaw.match(/^description:\s*"?(.*?)"?\s*$/m);
|
|
16
|
+
if (descMatch)
|
|
17
|
+
frontmatter.description = descMatch[1];
|
|
18
|
+
// Parse name
|
|
19
|
+
const nameMatch = frontmatterRaw.match(/^name:\s*(.+)$/m);
|
|
20
|
+
if (nameMatch)
|
|
21
|
+
frontmatter.name = nameMatch[1].trim();
|
|
22
|
+
// Parse user-invocable
|
|
23
|
+
const invocableMatch = frontmatterRaw.match(/^user-invocable:\s*(.+)$/m);
|
|
24
|
+
if (invocableMatch)
|
|
25
|
+
frontmatter['user-invocable'] = invocableMatch[1].trim() === 'true';
|
|
26
|
+
// Parse allowed-tools (YAML list)
|
|
27
|
+
const toolsMatch = frontmatterRaw.match(/^allowed-tools:\n((?:\s+-\s+.+\n?)+)/m);
|
|
28
|
+
if (toolsMatch) {
|
|
29
|
+
frontmatter['allowed-tools'] = toolsMatch[1]
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map(line => line.replace(/^\s+-\s+/, '').trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
return { frontmatter, body };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Load all skills from a skills directory (expects commands/ and workflows/ subdirs).
|
|
38
|
+
*/
|
|
39
|
+
export function loadSkills(skillsDir) {
|
|
40
|
+
const skills = [];
|
|
41
|
+
for (const category of ['commands', 'workflows']) {
|
|
42
|
+
const categoryDir = path.join(skillsDir, category);
|
|
43
|
+
if (!fs.existsSync(categoryDir))
|
|
44
|
+
continue;
|
|
45
|
+
for (const file of fs.readdirSync(categoryDir)) {
|
|
46
|
+
if (!file.endsWith('.md'))
|
|
47
|
+
continue;
|
|
48
|
+
const raw = fs.readFileSync(path.join(categoryDir, file), 'utf-8');
|
|
49
|
+
const parsed = parseFrontmatter(raw);
|
|
50
|
+
if (!parsed || !parsed.frontmatter.description) {
|
|
51
|
+
console.warn(`Warning: could not parse frontmatter for ${file}, skipping`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
skills.push({
|
|
55
|
+
filename: file,
|
|
56
|
+
category,
|
|
57
|
+
frontmatter: parsed.frontmatter,
|
|
58
|
+
body: parsed.body,
|
|
59
|
+
raw,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return skills;
|
|
64
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Skill } from '../skills.js';
|
|
2
|
+
import { Transformer, TransformResult } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Transform a skill into Claude Code plugin format.
|
|
5
|
+
* Each skill becomes a directory with SKILL.md inside skills/<skill-name>/
|
|
6
|
+
*/
|
|
7
|
+
export declare function transformForClaude(skill: Skill): TransformResult[];
|
|
8
|
+
/**
|
|
9
|
+
* Fallback cache directory when the CLI install didn't create one.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getClaudePluginCacheDir(): string;
|
|
12
|
+
export interface SettingsMutationResult {
|
|
13
|
+
registered: boolean;
|
|
14
|
+
backupPath?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Merge-add the memtrace MCP server into a Claude settings.json file.
|
|
18
|
+
* Never overwrites a malformed file — backs it up and returns registered=false.
|
|
19
|
+
*/
|
|
20
|
+
export declare function registerMcpInSettingsAt(settingsPath: string, memtraceBinary: string): SettingsMutationResult;
|
|
21
|
+
/**
|
|
22
|
+
* Merge-add plugin + marketplace entries into a Claude settings.json file.
|
|
23
|
+
* Never overwrites a malformed file.
|
|
24
|
+
*/
|
|
25
|
+
export declare function enablePluginInSettingsAt(settingsPath: string): SettingsMutationResult;
|
|
26
|
+
/**
|
|
27
|
+
* Full Claude Code plugin installation.
|
|
28
|
+
*
|
|
29
|
+
* 1. Try `claude plugin marketplace add` + `claude plugin install`
|
|
30
|
+
* 2. Fall back to manual: write cache files + update settings.json
|
|
31
|
+
* 3. Register MCP server for memtrace tools
|
|
32
|
+
*/
|
|
33
|
+
export declare function installClaudePlugin(skills: Skill[], memtraceBinaryPath: string): Promise<{
|
|
34
|
+
cacheDir: string;
|
|
35
|
+
skillCount: number;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Remove the Claude Code plugin and MCP server registration.
|
|
39
|
+
*/
|
|
40
|
+
export declare function uninstallClaudePlugin(): Promise<void>;
|
|
41
|
+
export declare const claudeTransformer: Transformer;
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { execCommand, commandExists } from '../utils.js';
|
|
5
|
+
import { safeReadJson, writeJsonAtomic } from '../fs-safe.js';
|
|
6
|
+
const PLUGIN_NAME = 'memtrace-skills';
|
|
7
|
+
const MARKETPLACE_NAME = 'memtrace';
|
|
8
|
+
const MARKETPLACE_REPO = 'syncable-dev/memtrace';
|
|
9
|
+
/**
|
|
10
|
+
* Try to register the memtrace MCP server via `claude mcp add-json`.
|
|
11
|
+
* Returns true on success, false on timeout/error/missing CLI.
|
|
12
|
+
* 5-second timeout — we fall back to direct JSON merge fast.
|
|
13
|
+
*/
|
|
14
|
+
async function tryMcpAddJson(memtraceBinary) {
|
|
15
|
+
if (!(await commandExists('claude')))
|
|
16
|
+
return false;
|
|
17
|
+
const config = JSON.stringify({
|
|
18
|
+
command: memtraceBinary,
|
|
19
|
+
args: ['mcp'],
|
|
20
|
+
env: { MEMGRAPH_URL: 'bolt://localhost:7687' },
|
|
21
|
+
});
|
|
22
|
+
// Shell-quote the JSON. Use single quotes; escape any embedded single quote.
|
|
23
|
+
const escaped = config.replace(/'/g, `'\\''`);
|
|
24
|
+
try {
|
|
25
|
+
await execCommand(`claude mcp add-json --scope user memtrace '${escaped}'`, { timeoutMs: 5_000 });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Transform a skill into Claude Code plugin format.
|
|
34
|
+
* Each skill becomes a directory with SKILL.md inside skills/<skill-name>/
|
|
35
|
+
*/
|
|
36
|
+
export function transformForClaude(skill) {
|
|
37
|
+
const skillName = skill.filename.replace(/\.md$/, '');
|
|
38
|
+
const safeDesc = skill.frontmatter.description
|
|
39
|
+
.replace(/"/g, '\\"')
|
|
40
|
+
.trim();
|
|
41
|
+
const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
|
|
42
|
+
return [{ relativePath: `skills/${skillName}/SKILL.md`, content }];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Root directory for all cached versions of this plugin.
|
|
46
|
+
*/
|
|
47
|
+
function getPluginCacheRoot() {
|
|
48
|
+
return path.join(os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME, PLUGIN_NAME);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Find the cache directory that Claude Code's CLI created (from `claude plugin install`).
|
|
52
|
+
*/
|
|
53
|
+
function findCliInstalledCacheDir() {
|
|
54
|
+
const root = getPluginCacheRoot();
|
|
55
|
+
if (!fs.existsSync(root))
|
|
56
|
+
return null;
|
|
57
|
+
for (const entry of fs.readdirSync(root)) {
|
|
58
|
+
const dir = path.join(root, entry);
|
|
59
|
+
if (!fs.statSync(dir).isDirectory())
|
|
60
|
+
continue;
|
|
61
|
+
return dir;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Fallback cache directory when the CLI install didn't create one.
|
|
67
|
+
*/
|
|
68
|
+
export function getClaudePluginCacheDir() {
|
|
69
|
+
return path.join(getPluginCacheRoot(), '0.0.0');
|
|
70
|
+
}
|
|
71
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
72
|
+
// Installation strategy (in priority order):
|
|
73
|
+
//
|
|
74
|
+
// 1. `claude plugin marketplace add` + `claude plugin install`
|
|
75
|
+
// The official flow. Registers the marketplace, clones the plugin,
|
|
76
|
+
// caches it, AND auto-enables it in settings.
|
|
77
|
+
//
|
|
78
|
+
// 2. Manual write: cache files + enabledPlugins in settings.json
|
|
79
|
+
// If the CLI is unavailable, write plugin files directly to the
|
|
80
|
+
// cache directory AND register in ~/.claude/settings.json.
|
|
81
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
/**
|
|
83
|
+
* Try to install the plugin via the Claude Code CLI.
|
|
84
|
+
*/
|
|
85
|
+
async function tryClaudeCliInstall() {
|
|
86
|
+
const hasClaude = await commandExists('claude');
|
|
87
|
+
if (!hasClaude)
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
await execCommand(`claude plugin marketplace add ${MARKETPLACE_REPO}`);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Marketplace may already exist
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await execCommand(`claude plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME} --scope user`);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
try {
|
|
101
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
102
|
+
if (fs.existsSync(settingsPath)) {
|
|
103
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
104
|
+
const key = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
105
|
+
if (settings.enabledPlugins?.[key] === true) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Fall through to manual
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Write the plugin.json manifest inside the cache directory.
|
|
118
|
+
*/
|
|
119
|
+
function writePluginManifest(cacheDir) {
|
|
120
|
+
const manifestDir = path.join(cacheDir, '.claude-plugin');
|
|
121
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
122
|
+
const version = path.basename(cacheDir);
|
|
123
|
+
const manifest = {
|
|
124
|
+
name: PLUGIN_NAME,
|
|
125
|
+
description: 'Memtrace skills for codebase exploration, code search, relationship analysis, temporal evolution, blast radius impact, code quality, graph algorithms, API topology, and multi-step workflows.',
|
|
126
|
+
version,
|
|
127
|
+
author: {
|
|
128
|
+
name: 'Syncable',
|
|
129
|
+
email: 'support@syncable.dev',
|
|
130
|
+
},
|
|
131
|
+
homepage: 'https://memtrace.io',
|
|
132
|
+
repository: `https://github.com/${MARKETPLACE_REPO}`,
|
|
133
|
+
license: 'SEE LICENSE IN LICENSE',
|
|
134
|
+
keywords: ['memtrace', 'code-intelligence', 'knowledge-graph', 'mcp', 'temporal-analysis'],
|
|
135
|
+
};
|
|
136
|
+
fs.writeFileSync(path.join(manifestDir, 'plugin.json'), JSON.stringify(manifest, null, 2));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Merge-add the memtrace MCP server into a Claude settings.json file.
|
|
140
|
+
* Never overwrites a malformed file — backs it up and returns registered=false.
|
|
141
|
+
*/
|
|
142
|
+
export function registerMcpInSettingsAt(settingsPath, memtraceBinary) {
|
|
143
|
+
const { value, corrupted, backupPath } = safeReadJson(settingsPath);
|
|
144
|
+
if (corrupted) {
|
|
145
|
+
console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}. Skipped MCP registration for Claude.`);
|
|
146
|
+
console.warn(`memtrace: fix the file and run 'memtrace install --repair'.`);
|
|
147
|
+
return { registered: false, backupPath };
|
|
148
|
+
}
|
|
149
|
+
const settings = (value ?? {});
|
|
150
|
+
settings.mcpServers = settings.mcpServers ?? {};
|
|
151
|
+
settings.mcpServers['memtrace'] = {
|
|
152
|
+
command: memtraceBinary,
|
|
153
|
+
args: ['mcp'],
|
|
154
|
+
env: { MEMGRAPH_URL: 'bolt://localhost:7687' },
|
|
155
|
+
};
|
|
156
|
+
writeJsonAtomic(settingsPath, settings);
|
|
157
|
+
return { registered: true };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Merge-add plugin + marketplace entries into a Claude settings.json file.
|
|
161
|
+
* Never overwrites a malformed file.
|
|
162
|
+
*/
|
|
163
|
+
export function enablePluginInSettingsAt(settingsPath) {
|
|
164
|
+
const { value, corrupted, backupPath } = safeReadJson(settingsPath);
|
|
165
|
+
if (corrupted) {
|
|
166
|
+
console.warn(`memtrace: ${settingsPath} is malformed; backed up to ${backupPath}.`);
|
|
167
|
+
return { registered: false, backupPath };
|
|
168
|
+
}
|
|
169
|
+
const settings = (value ?? {});
|
|
170
|
+
settings.enabledPlugins = settings.enabledPlugins ?? {};
|
|
171
|
+
settings.enabledPlugins[`${PLUGIN_NAME}@${MARKETPLACE_NAME}`] = true;
|
|
172
|
+
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces ?? {};
|
|
173
|
+
settings.extraKnownMarketplaces[MARKETPLACE_NAME] = {
|
|
174
|
+
source: { source: 'github', repo: MARKETPLACE_REPO },
|
|
175
|
+
};
|
|
176
|
+
writeJsonAtomic(settingsPath, settings);
|
|
177
|
+
return { registered: true };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Enable the plugin in ~/.claude/settings.json.
|
|
181
|
+
*/
|
|
182
|
+
function enablePluginInSettings() {
|
|
183
|
+
const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
|
|
184
|
+
enablePluginInSettingsAt(settingsFile);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Register the memtrace MCP server in Claude Code's settings.
|
|
188
|
+
* This adds the MCP server config so Claude Code can connect to memtrace tools.
|
|
189
|
+
*/
|
|
190
|
+
async function registerMcpServer(memtraceBinaryPath) {
|
|
191
|
+
// Strategy 1 (preferred): claude CLI's own add-json path
|
|
192
|
+
const viaCli = await tryMcpAddJson(memtraceBinaryPath);
|
|
193
|
+
if (viaCli)
|
|
194
|
+
return;
|
|
195
|
+
// Strategy 2 (fallback): direct settings.json merge (safe + atomic)
|
|
196
|
+
const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
|
|
197
|
+
registerMcpInSettingsAt(settingsFile, memtraceBinaryPath);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Full Claude Code plugin installation.
|
|
201
|
+
*
|
|
202
|
+
* 1. Try `claude plugin marketplace add` + `claude plugin install`
|
|
203
|
+
* 2. Fall back to manual: write cache files + update settings.json
|
|
204
|
+
* 3. Register MCP server for memtrace tools
|
|
205
|
+
*/
|
|
206
|
+
export async function installClaudePlugin(skills, memtraceBinaryPath) {
|
|
207
|
+
// Step 1: Try CLI install
|
|
208
|
+
await tryClaudeCliInstall();
|
|
209
|
+
// Step 2: Find or create cache dir
|
|
210
|
+
let cacheDir = findCliInstalledCacheDir();
|
|
211
|
+
if (cacheDir) {
|
|
212
|
+
const orphanedFile = path.join(cacheDir, '.orphaned_at');
|
|
213
|
+
if (fs.existsSync(orphanedFile))
|
|
214
|
+
fs.unlinkSync(orphanedFile);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
cacheDir = getClaudePluginCacheDir();
|
|
218
|
+
}
|
|
219
|
+
// Step 3: Clean up old versions
|
|
220
|
+
const pluginRoot = getPluginCacheRoot();
|
|
221
|
+
if (fs.existsSync(pluginRoot)) {
|
|
222
|
+
const activeDirName = path.basename(cacheDir);
|
|
223
|
+
for (const entry of fs.readdirSync(pluginRoot)) {
|
|
224
|
+
if (entry !== activeDirName && entry !== '.DS_Store') {
|
|
225
|
+
fs.rmSync(path.join(pluginRoot, entry), { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Step 4: Write skills
|
|
230
|
+
const skillsDir = path.join(cacheDir, 'skills');
|
|
231
|
+
if (fs.existsSync(skillsDir)) {
|
|
232
|
+
fs.rmSync(skillsDir, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
for (const skill of skills) {
|
|
235
|
+
const results = transformForClaude(skill);
|
|
236
|
+
for (const { relativePath, content } of results) {
|
|
237
|
+
const fullPath = path.join(cacheDir, relativePath);
|
|
238
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
239
|
+
fs.writeFileSync(fullPath, content);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
writePluginManifest(cacheDir);
|
|
243
|
+
enablePluginInSettings();
|
|
244
|
+
// Step 5: Register MCP server
|
|
245
|
+
await registerMcpServer(memtraceBinaryPath);
|
|
246
|
+
// Step 6: Write user-level skills for SDK-based integrations
|
|
247
|
+
writeUserLevelSkills(skills);
|
|
248
|
+
return { cacheDir, skillCount: skills.length };
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Write skills to ~/.claude/skills/ for SDK-based integrations.
|
|
252
|
+
*/
|
|
253
|
+
function writeUserLevelSkills(skills) {
|
|
254
|
+
const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
255
|
+
for (const skill of skills) {
|
|
256
|
+
const skillName = skill.filename.replace(/\.md$/, '');
|
|
257
|
+
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
|
|
258
|
+
const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
|
|
259
|
+
const outDir = path.join(userSkillsDir, skillName);
|
|
260
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
261
|
+
fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Remove the Claude Code plugin and MCP server registration.
|
|
266
|
+
*/
|
|
267
|
+
export async function uninstallClaudePlugin() {
|
|
268
|
+
const hasClaude = await commandExists('claude');
|
|
269
|
+
if (hasClaude) {
|
|
270
|
+
try {
|
|
271
|
+
await execCommand(`claude plugin uninstall ${PLUGIN_NAME}@${MARKETPLACE_NAME} --scope user`);
|
|
272
|
+
}
|
|
273
|
+
catch { /* fall through */ }
|
|
274
|
+
}
|
|
275
|
+
// Manual cleanup
|
|
276
|
+
const cacheRoot = getPluginCacheRoot();
|
|
277
|
+
if (fs.existsSync(cacheRoot)) {
|
|
278
|
+
fs.rmSync(cacheRoot, { recursive: true });
|
|
279
|
+
}
|
|
280
|
+
// Remove from settings.json
|
|
281
|
+
const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
|
|
282
|
+
if (fs.existsSync(settingsFile)) {
|
|
283
|
+
try {
|
|
284
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
|
|
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));
|
|
293
|
+
}
|
|
294
|
+
catch { /* */ }
|
|
295
|
+
}
|
|
296
|
+
// Clean up user-level skills
|
|
297
|
+
const userSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
298
|
+
if (fs.existsSync(userSkillsDir)) {
|
|
299
|
+
for (const entry of fs.readdirSync(userSkillsDir)) {
|
|
300
|
+
if (entry.startsWith('memtrace-')) {
|
|
301
|
+
const entryPath = path.join(userSkillsDir, entry);
|
|
302
|
+
const stat = fs.statSync(entryPath);
|
|
303
|
+
if (stat.isDirectory()) {
|
|
304
|
+
fs.rmSync(entryPath, { recursive: true });
|
|
305
|
+
}
|
|
306
|
+
else if (entry.endsWith('.md')) {
|
|
307
|
+
fs.unlinkSync(entryPath);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Write/merge the memtrace MCP server entry into a project-local .mcp.json.
|
|
315
|
+
* Safe-reads existing config; never overwrites a malformed file.
|
|
316
|
+
*/
|
|
317
|
+
function writeClaudeLocalMcp(mcpPath, binary) {
|
|
318
|
+
const { value, corrupted, backupPath } = safeReadJson(mcpPath);
|
|
319
|
+
if (corrupted) {
|
|
320
|
+
console.warn(`memtrace: ${mcpPath} is malformed; backed up to ${backupPath}. Skipped MCP registration.`);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
const cfg = (value ?? {});
|
|
324
|
+
cfg.mcpServers = cfg.mcpServers ?? {};
|
|
325
|
+
cfg.mcpServers['memtrace'] = {
|
|
326
|
+
command: binary,
|
|
327
|
+
args: ['mcp'],
|
|
328
|
+
env: { MEMGRAPH_URL: 'bolt://localhost:7687' },
|
|
329
|
+
};
|
|
330
|
+
writeJsonAtomic(mcpPath, cfg);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
export const claudeTransformer = {
|
|
334
|
+
name: 'claude',
|
|
335
|
+
async install(skills, ctx) {
|
|
336
|
+
if (ctx.scope === 'global') {
|
|
337
|
+
const { skillCount } = await installClaudePlugin(skills, ctx.memtraceBinary);
|
|
338
|
+
return {
|
|
339
|
+
agent: 'claude',
|
|
340
|
+
skillsWritten: skillCount,
|
|
341
|
+
skillsDir: path.join(os.homedir(), '.claude', 'skills'),
|
|
342
|
+
mcpConfigPath: path.join(os.homedir(), '.claude', 'settings.json'),
|
|
343
|
+
mcpRegistered: !ctx.skipMcp,
|
|
344
|
+
warnings: [],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
// Local scope: write skills into <cwd>/.claude/skills/
|
|
348
|
+
const skillsDir = path.join(ctx.cwd, '.claude', 'skills');
|
|
349
|
+
let count = 0;
|
|
350
|
+
for (const skill of skills) {
|
|
351
|
+
const skillName = skill.filename.replace(/\.md$/, '');
|
|
352
|
+
const outDir = path.join(skillsDir, skillName);
|
|
353
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
354
|
+
const safeDesc = skill.frontmatter.description.replace(/"/g, '\\"').trim();
|
|
355
|
+
const content = `---\nname: ${skillName}\ndescription: "${safeDesc}"\n---\n\n${skill.body}`;
|
|
356
|
+
fs.writeFileSync(path.join(outDir, 'SKILL.md'), content);
|
|
357
|
+
count++;
|
|
358
|
+
}
|
|
359
|
+
// MCP for local scope: .mcp.json at project root (Claude's project-level convention)
|
|
360
|
+
const mcpConfigPath = path.join(ctx.cwd, '.mcp.json');
|
|
361
|
+
let mcpRegistered = false;
|
|
362
|
+
if (!ctx.skipMcp) {
|
|
363
|
+
mcpRegistered = writeClaudeLocalMcp(mcpConfigPath, ctx.memtraceBinary);
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
agent: 'claude',
|
|
367
|
+
skillsWritten: count,
|
|
368
|
+
skillsDir,
|
|
369
|
+
mcpConfigPath,
|
|
370
|
+
mcpRegistered,
|
|
371
|
+
warnings: [],
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
async uninstall(ctx) {
|
|
375
|
+
if (ctx.scope === 'global') {
|
|
376
|
+
await uninstallClaudePlugin();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// Local: remove <cwd>/.claude/skills/memtrace-* and the memtrace entry from <cwd>/.mcp.json
|
|
380
|
+
const skillsDir = path.join(ctx.cwd, '.claude', 'skills');
|
|
381
|
+
if (fs.existsSync(skillsDir)) {
|
|
382
|
+
for (const entry of fs.readdirSync(skillsDir)) {
|
|
383
|
+
if (entry.startsWith('memtrace-')) {
|
|
384
|
+
fs.rmSync(path.join(skillsDir, entry), { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const mcpPath = path.join(ctx.cwd, '.mcp.json');
|
|
389
|
+
const { value, corrupted } = safeReadJson(mcpPath);
|
|
390
|
+
if (!corrupted && value?.mcpServers?.['memtrace']) {
|
|
391
|
+
delete value.mcpServers['memtrace'];
|
|
392
|
+
if (Object.keys(value.mcpServers).length === 0)
|
|
393
|
+
delete value.mcpServers;
|
|
394
|
+
if (Object.keys(value).length === 0)
|
|
395
|
+
fs.unlinkSync(mcpPath);
|
|
396
|
+
else
|
|
397
|
+
writeJsonAtomic(mcpPath, value);
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Transformer } from './types.js';
|
|
2
|
+
export interface CursorMcpResult {
|
|
3
|
+
registered: boolean;
|
|
4
|
+
backupPath?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function registerCursorMcpAt(mcpFile: string, binary: string): CursorMcpResult;
|
|
7
|
+
export declare const cursorTransformer: Transformer;
|