context-mcp-server 1.0.3 → 1.0.5

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 CHANGED
@@ -371,7 +371,10 @@ Available to web clients (Claude.ai, ChatGPT) only — local AI clients use thei
371
371
 
372
372
  All file and git operations are sandboxed to the registered project root. Enable git tools with `--access-git` or `access_git: true` in config.
373
373
 
374
- ### CodeGraph
374
+ ### ContextGraph (CodeGraph)
375
+
376
+ > Also referred to as **ContextGraph** — the MCP tools use the `codegraph_*` prefix but both names mean the same thing.
377
+
375
378
  - `codegraph_build` — AST scan using tree-sitter: functions, classes, imports, edges. Runs locally, no API cost.
376
379
  - `codegraph_query` — fetch any details about the codebase using natural language: find functions, classes, files, dependencies, callers
377
380
  - `codegraph_explain` — single node: type, file location, all direct connections (depends_on, used_by)
@@ -381,15 +384,25 @@ All file and git operations are sandboxed to the registered project root. Enable
381
384
 
382
385
  ### Multi-AI Support
383
386
 
384
- | AI | Config File | Instruction File |
385
- |----|------------|-----------------|
386
- | Claude Code | `.claude/mcp.json` | `CLAUDE.md` |
387
- | VS Code Copilot | `.vscode/mcp.json` | `CLAUDE.md` |
388
- | Cursor | `.cursor/mcp.json` | `.cursor/rules/context-mcp.mdc` |
389
- | Gemini CLI | `.gemini/settings.json` | `GEMINI.md` |
390
- | Codex CLI | `.codex/config.toml` | `AGENTS.md` |
391
- | Windsurf | `~/.codeium/windsurf/mcp_config.json` | `.windsurf/rules/context-mcp.md` |
392
- | Claude.ai / ChatGPT | HTTP (`ctx online`) | — |
387
+ | AI | Config File | Instructions | Slash Commands |
388
+ |----|------------|--------------|----------------|
389
+ | Claude Code | `.claude/mcp.json` | Skill → `~/.claude/skills/context-mcp/` (global) | ✓ (3 commands) |
390
+ | Cursor | `.cursor/mcp.json` | Rule → `.cursor/rules/context-mcp.mdc` | — |
391
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` | Rule → `.windsurf/rules/context-mcp.md` | — |
392
+ | Gemini CLI | `.gemini/settings.json` | `GEMINI.md` | — |
393
+ | Codex CLI | `.codex/config.toml` | `AGENTS.md` | — |
394
+ | VS Code Copilot | `.vscode/mcp.json` | — | — |
395
+ | Claude.ai / ChatGPT | HTTP (`ctx online`) | — | — |
396
+
397
+ > Claude Code installs context-mcp as a **skill** (`~/.claude/skills/context-mcp/SKILL.md`) — available globally across all projects, not just the current one. Cursor and Windsurf use their native rules system. Gemini and Codex use plain instruction files since they have no skill/rules system.
398
+
399
+ `ctx install --claude` also writes slash commands into `.claude/commands/`:
400
+
401
+ | Command | What it does |
402
+ |---------|-------------|
403
+ | `/context-resume` | Resume context for the current project |
404
+ | `/graph-build` | Build or rebuild the ContextGraph |
405
+ | `/save-context` | Save a note, decision, or bug to context |
393
406
 
394
407
  > The context store lives at `~/.context-mcp/` — not inside any tool, IDE, or session. A decision saved in Claude Code is visible in Cursor. A bug logged from Gemini CLI shows up when you resume in Codex.
395
408
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mcp-server",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Persistent AI memory + codebase knowledge graph MCP server. Works across Claude Code, Cursor, Gemini CLI, Codex, Windsurf, VS Code Copilot, Claude.ai, and ChatGPT.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,8 @@
21
21
  "files": [
22
22
  "src/",
23
23
  "codegraph/",
24
+ "pyproject.toml",
25
+ "uv.lock",
24
26
  "README.md"
25
27
  ],
26
28
  "keywords": [
package/pyproject.toml ADDED
@@ -0,0 +1,69 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "codegraph-mcp"
7
+ version = "1.0.5"
8
+ description = "Codebase knowledge graph MCP server — AST extraction, graph queries, community detection"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ keywords = ["mcp", "ai", "codegraph", "knowledge-graph", "ast"]
13
+ dependencies = [
14
+ "mcp>=1.0.0",
15
+ "networkx>=3.0",
16
+ "pymupdf>=1.24",
17
+ "tree-sitter>=0.23.0",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ treesitter = [
22
+ "tree-sitter>=0.23.0",
23
+ "tree-sitter-python",
24
+ "tree-sitter-javascript",
25
+ "tree-sitter-typescript",
26
+ "tree-sitter-go",
27
+ "tree-sitter-rust",
28
+ "tree-sitter-java",
29
+ "tree-sitter-c",
30
+ "tree-sitter-cpp",
31
+ "tree-sitter-c-sharp",
32
+ "tree-sitter-ruby",
33
+ "tree-sitter-php",
34
+ "tree-sitter-swift",
35
+ "tree-sitter-lua",
36
+ "tree-sitter-kotlin",
37
+ ]
38
+
39
+ [project.scripts]
40
+ codegraph-mcp = "codegraph.server:main"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["codegraph"]
44
+
45
+ # ── uv ────────────────────────────────────────────────────────────────────────
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "pytest>=8.0",
50
+ "pytest-asyncio>=0.23",
51
+ "ruff>=0.4",
52
+ "mypy>=1.10",
53
+ ]
54
+
55
+ # ── ruff ─────────────────────────────────────────────────────────────────────
56
+
57
+ [tool.ruff]
58
+ target-version = "py311"
59
+ line-length = 100
60
+
61
+ [tool.ruff.lint]
62
+ select = ["E", "F", "I"]
63
+ ignore = ["E501"]
64
+
65
+ # ── pytest ────────────────────────────────────────────────────────────────────
66
+
67
+ [tool.pytest.ini_options]
68
+ asyncio_mode = "auto"
69
+ testpaths = ["tests"]
package/src/cli.js CHANGED
@@ -106,28 +106,46 @@ function printSection(title, meta = '') {
106
106
 
107
107
  function printUsage() {
108
108
  printBanner();
109
- printSection('Commands');
109
+
110
+ // Terminal commands (ctx ...)
111
+ printSection('Terminal commands', 'run from your shell');
110
112
  const cmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
111
- cmd('ctx', 'open interactive mode');
112
- cmd('ctx list [project]', 'list entries + discussions + graphs');
113
- cmd('ctx search <query>', 'keyword → semantic fallback search');
114
- cmd('ctx add', 'add entry interactively');
115
- cmd('ctx delete <id-prefix>', 'delete one entry');
116
- cmd('ctx delete project <name|id>', 'delete all entries for a project (by name or id)');
117
- cmd('ctx summary [project]', 'summarize recent entries');
118
- cmd('ctx projects', 'show all projects + graphs');
119
- cmd('ctx discuss [project]', 'show discussions');
120
- cmd('ctx benchmark', 'token savings report (memory + graph)');
113
+ cmd('ctx', 'open interactive mode');
114
+ cmd('ctx list [project]', 'list entries + discussions + graphs');
115
+ cmd('ctx search <query>', 'keyword → semantic fallback search');
116
+ cmd('ctx add', 'add entry interactively');
117
+ cmd('ctx delete <id-prefix>', 'delete one entry');
118
+ cmd('ctx delete project <name|id>', 'delete all entries for a project');
119
+ cmd('ctx summary [project]', 'summarize recent entries');
120
+ cmd('ctx projects', 'show all projects + graphs');
121
+ cmd('ctx discuss [project]', 'show discussions');
122
+ cmd('ctx benchmark', 'token savings report (memory + graph)');
121
123
  console.log('');
122
- cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps');
123
- cmd('ctx install --<platform>', 'write MCP config + instruction file for an AI platform');
124
+ cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps');
125
+ cmd('ctx install --<platform>', 'write MCP config + skill/rules for an AI platform');
124
126
  cmd('ctx install --all', 'install for all platforms at once');
125
- cmd('ctx online [--port N]', 'start HTTP server + show credentials for Claude.ai / ChatGPT');
127
+ cmd('ctx online [--port N]', 'start HTTP server for Claude.ai / ChatGPT');
126
128
  cmd('ctx online --close', 'stop the running HTTP server');
127
129
  cmd('ctx settings', 'view and edit config (port, host, client id/secret)');
128
- console.log('');
130
+ cmd('ctx update', 'check for and apply latest version');
129
131
  cmd('ctx help', 'show this screen');
130
132
  console.log('');
133
+
134
+ // Interactive mode commands (no ctx prefix)
135
+ printSection('Interactive mode', 'type these inside ctx (no "ctx" prefix)');
136
+ const icmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
137
+ icmd('list [project]', 'list entries');
138
+ icmd('search <query>', 'search context');
139
+ icmd('add', 'add entry');
140
+ icmd('projects', 'show all projects');
141
+ icmd('discuss [project]', 'show discussions');
142
+ icmd('summary [project]', 'summarize recent entries');
143
+ icmd('benchmark', 'token savings report');
144
+ icmd('install --<platform>', 'install for a platform');
145
+ icmd('settings', 'edit config');
146
+ icmd('clear', 'clear screen');
147
+ icmd('exit / quit / q', 'exit interactive mode');
148
+ console.log('');
131
149
  }
132
150
  function clearScreen() {
133
151
  // \x1b[2J = clear screen, \x1b[3J = clear scrollback, \x1b[H = cursor home
@@ -531,65 +549,163 @@ function _writeFile(filePath, content, label) {
531
549
  console.log(` ${ok('✓')} ${label.padEnd(28)} ${faint(filePath.replace(/\\/g, '/'))}`);
532
550
  }
533
551
 
552
+ // Entries ctx install writes into project roots — add to user's global gitignore if one exists
553
+ const _GLOBAL_GITIGNORE_ENTRIES = [
554
+ // Installed instruction files
555
+ 'CLAUDE.md',
556
+ 'GEMINI.md',
557
+ 'AGENTS.md',
558
+ // AI/IDE platform config folders (context-mcp specific — safe to ignore globally)
559
+ '.claude/',
560
+ '.cursor/',
561
+ '.vscode/',
562
+ '.gemini/',
563
+ '.codex/',
564
+ '.windsurf/',
565
+ // Build outputs and session artifacts
566
+ 'codegraph-cache/',
567
+ '.mcp.json',
568
+ ];
569
+
570
+ function _updateGlobalGitignore() {
571
+ // Resolve global gitignore path: git config > ~/.gitignore_global > ~/.gitignore
572
+ let giPath = null;
573
+ const gitCfg = spawnSync('git', ['config', '--global', 'core.excludesFile'], { encoding: 'utf8' });
574
+ if (gitCfg.status === 0 && gitCfg.stdout.trim()) {
575
+ const resolved = gitCfg.stdout.trim().replace(/^~/, homedir());
576
+ if (existsSync(resolved)) giPath = resolved;
577
+ }
578
+ if (!giPath) {
579
+ for (const candidate of [join(homedir(), '.gitignore_global'), join(homedir(), '.gitignore')]) {
580
+ if (existsSync(candidate)) { giPath = candidate; break; }
581
+ }
582
+ }
583
+ if (!giPath) return; // no global gitignore — skip silently
584
+
585
+ const existing = readFileSync(giPath, 'utf8');
586
+ const lines = existing.split(/\r?\n/);
587
+ const missing = _GLOBAL_GITIGNORE_ENTRIES.filter(e => !lines.includes(e));
588
+ if (!missing.length) return;
589
+
590
+ const block = '\n# context-mcp — written by ctx install\n' + missing.join('\n') + '\n';
591
+ writeFileSync(giPath, existing.trimEnd() + block, 'utf8');
592
+ console.log(` ${ok('✓')} ${'global gitignore'.padEnd(28)} ${faint(giPath.replace(/\\/g, '/'))}`);
593
+ for (const e of missing) console.log(` ${faint('+ ' + e)}`);
594
+ }
595
+
596
+ function _writeCommands(baseDir) {
597
+ const cmdsDir = join(TPLS, 'commands');
598
+ const destDir = join(baseDir, '.claude', 'commands');
599
+ for (const [name, label] of [
600
+ ['context-resume.md', '/context-resume'],
601
+ ['graph-build.md', '/graph-build'],
602
+ ['save-context.md', '/save-context'],
603
+ ]) {
604
+ const src = join(cmdsDir, name);
605
+ if (existsSync(src)) {
606
+ _writeFile(join(destDir, name), readFileSync(src, 'utf8'), label);
607
+ }
608
+ }
609
+ }
610
+
611
+ const MCP_SERVER_CMD = { command: 'npx', args: ['-y', 'context-mcp-server@latest'] };
612
+
534
613
  const PLATFORMS = {
535
614
  claude: {
536
615
  label: 'Claude Code',
537
- install(cwd) {
538
- const mcpJson = JSON.stringify({
539
- mcpServers: { 'context-mcp': { command: 'npx', args: ['-y', 'context-mcp-server@latest'] } },
540
- }, null, 2);
541
- _writeFile(join(cwd, '.claude', 'mcp.json'), mcpJson, '.claude/mcp.json');
542
- const md = _tpl('CLAUDE.md');
543
- if (md) _writeFile(join(cwd, 'CLAUDE.md'), md, 'CLAUDE.md');
616
+ restartNote: 'Type /context-resume in Claude Code to start using context-mcp.',
617
+ install(dir, scope) {
618
+ // Install as a skill (global, works across all projects) instead of a flat CLAUDE.md
619
+ const skillSrc = join(TPLS, 'skills', 'SKILL.md');
620
+ const skillDest = join(homedir(), '.claude', 'skills', 'context-mcp', 'SKILL.md');
621
+ if (existsSync(skillSrc)) {
622
+ _writeFile(skillDest, readFileSync(skillSrc, 'utf8'), '~/.claude/skills/context-mcp/');
623
+ }
624
+ // Slash commands are always user-global (not project-scoped)
625
+ _writeCommands(homedir());
626
+ // Register via claude CLI so the server is trusted immediately (no manual trust prompt)
627
+ const scopeFlag = scope === 'global' ? 'user' : 'project';
628
+ const reg = spawnSync(
629
+ 'claude', ['mcp', 'add', '--scope', scopeFlag, 'context-mcp', '--', 'npx', '-y', 'context-mcp-server@latest'],
630
+ { encoding: 'utf8', shell: true },
631
+ );
632
+ if (reg.status === 0) {
633
+ console.log(` ${ok('✓')} ${'registered via claude mcp add'.padEnd(28)} ${faint('scope: ' + scopeFlag)}`);
634
+ } else {
635
+ console.log(` ${faint('ℹ')} claude CLI not found — open Claude Code and trust context-mcp when prompted`);
636
+ }
544
637
  },
545
638
  },
546
639
  cursor: {
547
640
  label: 'Cursor',
548
- install(cwd) {
549
- const mcpJson = JSON.stringify({
550
- mcpServers: { 'context-mcp': { command: 'npx', args: ['-y', 'context-mcp-server@latest'] } },
551
- }, null, 2);
552
- _writeFile(join(cwd, '.cursor', 'mcp.json'), mcpJson, '.cursor/mcp.json');
553
- const mdc = _tpl('cursor-rules.mdc');
554
- if (mdc) _writeFile(join(cwd, '.cursor', 'rules', 'context-mcp.mdc'), mdc, '.cursor/rules/context-mcp.mdc');
641
+ restartNote: 'Restart Cursor to load the new MCP server.',
642
+ install(dir, scope) {
643
+ const mcpJson = JSON.stringify({ mcpServers: { 'context-mcp': MCP_SERVER_CMD } }, null, 2);
644
+ _writeFile(join(dir, '.cursor', 'mcp.json'), mcpJson, '.cursor/mcp.json');
645
+ if (scope === 'project') {
646
+ const mdc = _tpl('cursor-rules.mdc');
647
+ if (mdc) _writeFile(join(dir, '.cursor', 'rules', 'context-mcp.mdc'), mdc, '.cursor/rules/context-mcp.mdc');
648
+ }
649
+ // Try to enable via cursor CLI (enable/disable only — no add command)
650
+ const reg = spawnSync(
651
+ 'cursor', ['agent', 'mcp', 'enable', 'context-mcp'],
652
+ { encoding: 'utf8', shell: true },
653
+ );
654
+ if (reg.status === 0) {
655
+ console.log(` ${ok('✓')} ${'enabled via cursor agent mcp'.padEnd(28)}`);
656
+ }
555
657
  },
556
658
  },
557
659
  vscode: {
558
660
  label: 'VS Code Copilot',
559
- install(cwd) {
661
+ restartNote: 'Reload VS Code window (Ctrl+Shift+P → "Reload Window").',
662
+ install(dir) {
560
663
  const mcpJson = JSON.stringify({
561
- servers: { 'context-mcp': { type: 'stdio', command: 'npx', args: ['-y', 'context-mcp-server@latest'] } },
664
+ servers: { 'context-mcp': { type: 'stdio', ...MCP_SERVER_CMD } },
562
665
  }, null, 2);
563
- _writeFile(join(cwd, '.vscode', 'mcp.json'), mcpJson, '.vscode/mcp.json');
564
- const md = _tpl('CLAUDE.md');
565
- if (md) _writeFile(join(cwd, 'CLAUDE.md'), md, 'CLAUDE.md');
666
+ _writeFile(join(dir, '.vscode', 'mcp.json'), mcpJson, '.vscode/mcp.json');
566
667
  },
567
668
  },
568
669
  gemini: {
569
670
  label: 'Gemini CLI',
570
- install(cwd) {
571
- const cfg = JSON.stringify({
572
- mcpServers: { 'context-mcp': { command: 'npx', args: ['-y', 'context-mcp-server@latest'] } },
573
- }, null, 2);
574
- _writeFile(join(cwd, '.gemini', 'settings.json'), cfg, '.gemini/settings.json');
575
- const md = _tpl('GEMINI.md');
576
- if (md) _writeFile(join(cwd, 'GEMINI.md'), md, 'GEMINI.md');
671
+ restartNote: 'Restart your Gemini CLI session.',
672
+ install(dir, scope) {
673
+ const cfg = JSON.stringify({ mcpServers: { 'context-mcp': MCP_SERVER_CMD } }, null, 2);
674
+ _writeFile(join(dir, '.gemini', 'settings.json'), cfg, '.gemini/settings.json');
675
+ if (scope === 'project') {
676
+ const md = _tpl('GEMINI.md');
677
+ if (md) _writeFile(join(dir, 'GEMINI.md'), md, 'GEMINI.md');
678
+ }
577
679
  },
578
680
  },
579
681
  codex: {
580
682
  label: 'Codex CLI',
581
- install(cwd) {
683
+ restartNote: 'Restart your Codex CLI session.',
684
+ install(dir, scope) {
582
685
  const toml = `[[mcp_servers]]\nname = "context-mcp"\ncommand = "npx"\nargs = ["-y", "context-mcp-server@latest"]\n`;
583
- _writeFile(join(cwd, '.codex', 'config.toml'), toml, '.codex/config.toml');
584
- const md = _tpl('AGENTS.md');
585
- if (md) _writeFile(join(cwd, 'AGENTS.md'), md, 'AGENTS.md');
686
+ _writeFile(join(dir, '.codex', 'config.toml'), toml, '.codex/config.toml');
687
+ if (scope === 'project') {
688
+ const md = _tpl('AGENTS.md');
689
+ if (md) _writeFile(join(dir, 'AGENTS.md'), md, 'AGENTS.md');
690
+ }
691
+ // Register via codex CLI so server is active immediately
692
+ const reg = spawnSync(
693
+ 'codex', ['mcp', 'add', 'context-mcp', '--', 'npx', '-y', 'context-mcp-server@latest'],
694
+ { encoding: 'utf8', shell: true },
695
+ );
696
+ if (reg.status === 0) {
697
+ console.log(` ${ok('✓')} ${'registered via codex mcp add'.padEnd(28)}`);
698
+ } else {
699
+ console.log(` ${faint('ℹ')} codex CLI not found — server will load on next Codex session`);
700
+ }
586
701
  },
587
702
  },
588
703
  windsurf: {
589
704
  label: 'Windsurf',
590
- install(cwd) {
705
+ restartNote: 'Restart Windsurf to load the updated MCP config.',
706
+ install(dir) {
591
707
  const rules = _tpl('windsurf-rules.md');
592
- if (rules) _writeFile(join(cwd, '.windsurf', 'rules', 'context-mcp.md'), rules, '.windsurf/rules/context-mcp.md');
708
+ if (rules) _writeFile(join(dir, '.windsurf', 'rules', 'context-mcp.md'), rules, '.windsurf/rules/context-mcp.md');
593
709
  const globalCfgPath = join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
594
710
  let existing = {};
595
711
  try { existing = JSON.parse(readFileSync(globalCfgPath, 'utf8')); } catch {}
@@ -600,7 +716,7 @@ const PLATFORMS = {
600
716
  },
601
717
  };
602
718
 
603
- function cmdInstall(args) {
719
+ async function cmdInstall(args) {
604
720
  const flags = new Set(args.map(a => a.replace(/^--/, '')));
605
721
  const all = flags.has('all');
606
722
  const initial = flags.has('initial');
@@ -637,7 +753,7 @@ function cmdInstall(args) {
637
753
  const venvPath = join(pkgRootInit, '.venv');
638
754
  spawnSync('cmd', ['/c', 'rmdir', '/s', '/q', venvPath], { encoding: 'utf8' });
639
755
  }
640
- const sync2 = spawnSync('uv', ['sync', '--no-dev'], { cwd: pkgRootInit, encoding: 'utf8', shell: true });
756
+ const sync2 = spawnSync('uv', ['--directory', pkgRootInit, 'sync', '--no-dev'], { encoding: 'utf8' });
641
757
  if (sync2.status !== 0) {
642
758
  console.log(` ${bad('✗')} uv sync failed:\n${faint((sync2.stderr || sync2.stdout || '').trim())}`);
643
759
  } else {
@@ -665,22 +781,56 @@ function cmdInstall(args) {
665
781
  return;
666
782
  }
667
783
 
668
- const cwd = process.cwd();
669
- printSection('Install', keys.map(k => PLATFORMS[k].label).join(', '));
670
- console.log('');
784
+ // ── Scope prompt (skip for windsurf-only installs — it's always global) ──────
785
+ const nonWindsurf = keys.filter(k => k !== 'windsurf');
786
+ let scope = 'project';
787
+ let baseDir = process.cwd();
788
+
789
+ if (nonWindsurf.length > 0) {
790
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
791
+ const ask = q => new Promise(resolve => rl.question(` ${accent('›')} ${muted(q)} `, resolve));
792
+
793
+ printSection('Install', keys.map(k => PLATFORMS[k].label).join(', '));
794
+ console.log('');
795
+ console.log(` ${muted('Install scope:')}`);
796
+ console.log(` ${accent('1.')} For this project ${faint('(writes config into current directory)')}`);
797
+ console.log(` ${accent('2.')} Globally ${faint('(writes config to your home directory)')}`);
798
+ console.log('');
799
+ const answer = (await ask('Choose (1/2) [1]:')).trim();
800
+ rl.close();
801
+ console.log('');
802
+
803
+ if (answer === '2') {
804
+ scope = 'global';
805
+ baseDir = homedir();
806
+ }
807
+ } else {
808
+ printSection('Install', keys.map(k => PLATFORMS[k].label).join(', '));
809
+ console.log('');
810
+ }
671
811
 
672
812
  for (const key of keys) {
673
- console.log(` ${bold(lblue(PLATFORMS[key].label))}`);
813
+ const platform = PLATFORMS[key];
814
+ console.log(` ${bold(lblue(platform.label))}`);
815
+ const dir = key === 'windsurf' ? process.cwd() : baseDir;
674
816
  try {
675
- PLATFORMS[key].install(cwd);
817
+ platform.install(dir, scope);
676
818
  } catch (err) {
677
819
  console.log(` ${bad('✗')} failed: ${err.message}`);
678
820
  }
821
+ if (platform.restartNote) {
822
+ console.log(` ${faint('→ ' + platform.restartNote)}`);
823
+ }
679
824
  console.log('');
680
825
  }
681
826
 
827
+ const destLabel = scope === 'global' ? homedir().replace(/\\/g, '/') : process.cwd().replace(/\\/g, '/');
682
828
  console.log(line());
683
- console.log(faint(` ${keys.length} platform(s) installed into ${cwd.replace(/\\/g, '/')}`));
829
+ console.log(faint(` ${keys.length} platform(s) installed · scope: ${scope} · ${destLabel}`));
830
+ console.log('');
831
+
832
+ // ── Global gitignore — add context-mcp runtime files if global gitignore exists ──
833
+ _updateGlobalGitignore();
684
834
  console.log('');
685
835
 
686
836
  // ── Python / uv setup (codegraph) ─────────────────────────────────────────
@@ -696,7 +846,7 @@ function cmdInstall(args) {
696
846
  // Package root is one level up from src/
697
847
  const __dirname_cli = dirname(fileURLToPath(import.meta.url));
698
848
  const pkgRoot = join(__dirname_cli, '..');
699
- const sync = spawnSync('uv', ['sync', '--no-dev'], { cwd: pkgRoot, encoding: 'utf8' });
849
+ const sync = spawnSync('uv', ['--directory', pkgRoot, 'sync', '--no-dev'], { encoding: 'utf8' });
700
850
  if (sync.status !== 0) {
701
851
  console.log(` ${bad('✗')} uv sync failed:\n${faint((sync.stderr || sync.stdout || '').trim())}`);
702
852
  } else {
@@ -853,10 +1003,10 @@ async function cmdSettings(existingRl) {
853
1003
  console.log('');
854
1004
 
855
1005
  const choice = (await ask('Edit field (1-' + FIELDS.length + '):')).trim();
856
- if (!existingRl) rl.close();
857
1006
 
858
1007
  const idx = parseInt(choice) - 1;
859
1008
  if (isNaN(idx) || idx < 0 || idx >= FIELDS.length) {
1009
+ if (!existingRl) rl.close();
860
1010
  console.log(` ${faint('no changes made')}`);
861
1011
  return;
862
1012
  }
@@ -1007,7 +1157,7 @@ async function interactive() {
1007
1157
  case 'benchmark': case 'bench':
1008
1158
  clearScreen(); printCompactHeader('benchmark'); cmdBenchmark(); break;
1009
1159
  case 'install':
1010
- clearScreen(); printCompactHeader('install'); cmdInstall(rest); break;
1160
+ clearScreen(); printCompactHeader('install'); await cmdInstall(rest); break;
1011
1161
  case 'online':
1012
1162
  clearScreen(); printCompactHeader('online'); cmdOnline(rest); break;
1013
1163
  case 'settings': case 'config':
@@ -1033,6 +1183,25 @@ function printBye() {
1033
1183
  console.log(`\n ${ok('✓')} ${bold(lblue('goodbye'))} ${faint('keep building')}\n`);
1034
1184
  }
1035
1185
 
1186
+ // ── Update check ──────────────────────────────────────────────────────────────
1187
+
1188
+ async function checkForUpdate() {
1189
+ try {
1190
+ const result = spawnSync(
1191
+ 'npm', ['view', 'context-mcp-server', 'version', '--json'],
1192
+ { encoding: 'utf8', timeout: 3000, shell: true },
1193
+ );
1194
+ if (result.status !== 0 || !result.stdout) return;
1195
+ const parsed = JSON.parse(result.stdout.trim());
1196
+ const latest = typeof parsed === 'string' ? parsed : String(parsed);
1197
+ const current = pkg.version;
1198
+ if (latest && latest !== current) {
1199
+ console.log(` ${warn('↑')} ${bold('Update available')} ${faint(current)} ${accent('→')} ${ok(latest)} ${faint('run:')} ${accent('npm i -g context-mcp-server@latest')}`);
1200
+ console.log('');
1201
+ }
1202
+ } catch {}
1203
+ }
1204
+
1036
1205
  // ── CLI entry point ───────────────────────────────────────────────────────────
1037
1206
 
1038
1207
  (async () => {
@@ -1052,7 +1221,24 @@ function printBye() {
1052
1221
  case 'benchmark': case 'bench':
1053
1222
  cmdBenchmark(); break;
1054
1223
  case 'install':
1055
- cmdInstall(rest); break;
1224
+ await cmdInstall(rest);
1225
+ process.exit(0);
1226
+ break;
1227
+ case 'update': {
1228
+ printSection('Update');
1229
+ console.log('');
1230
+ const upd = spawnSync(
1231
+ 'npm', ['install', '-g', 'context-mcp-server@latest'],
1232
+ { encoding: 'utf8', shell: true, stdio: 'inherit' },
1233
+ );
1234
+ if (upd.status === 0) {
1235
+ console.log(`\n ${ok('✓')} ${bold('context-mcp updated to latest')}`);
1236
+ } else {
1237
+ console.log(`\n ${bad('✗')} update failed — try: ${accent('npm i -g context-mcp-server@latest')}`);
1238
+ }
1239
+ console.log('');
1240
+ break;
1241
+ }
1056
1242
  case 'online':
1057
1243
  cmdOnline(rest); break;
1058
1244
  case 'settings': case 'config':
@@ -1062,10 +1248,13 @@ function printBye() {
1062
1248
  case 'delete': case 'del': case 'rm':
1063
1249
  cmdDelete(rest); break;
1064
1250
  case 'help': case '--help': case '-h':
1065
- printUsage(); break;
1251
+ await checkForUpdate();
1252
+ printUsage();
1253
+ break;
1066
1254
  case '--version': case '-v':
1067
1255
  console.log(pkg.version); break;
1068
1256
  default:
1257
+ await checkForUpdate();
1069
1258
  await interactive();
1070
1259
  }
1071
1260
  })();
@@ -20,22 +20,40 @@ Then:
20
20
 
21
21
  ---
22
22
 
23
- ## 2. During the Conversation
23
+ ## 2. When to Auto-Save Context
24
24
 
25
- | Situation | Action |
26
- |-----------|--------|
27
- | Decision made | `context.save` type: `"decision"` |
28
- | Bug found/fixed | `context.save` type: `"bug"` |
29
- | Architecture understood | `context.save` type: `"architecture"` |
30
- | User says "save/remember this" | `context.save` immediately |
31
- | Feature spans sessions | `discussion.save` status: `"active"` |
32
- | Need past info | `search` before asking user |
25
+ **After graph build or rebuild** — every time `codegraph_build` completes:
26
+ ```
27
+ context.save type: "architecture" title: "ContextGraph built — <project>"
28
+ content: "nodes: X | edges: Y | communities: Z"
29
+ ```
30
+
31
+ **User explicitly asks** "save this", "remember this", "note that" → save immediately.
33
32
 
34
- Always pass `project`. Auto-compact fires at >50 entries.
33
+ **During plan / implementation / discussion / research** — save only when genuinely valuable:
34
+
35
+ | What happened | Type |
36
+ |--------------|------|
37
+ | Approach / library / pattern decided | `decision` |
38
+ | Bug found (root cause known) or fixed | `bug` |
39
+ | System structure understood | `architecture` |
40
+ | Gotcha, constraint, non-obvious behavior | `note` |
41
+ | Config / env var / secret key discovered | `config` |
42
+ | External API or service integration learned | `note` |
43
+ | Performance insight (why something is slow/fast) | `note` |
44
+ | How to run tests / test pattern discovered | `note` |
45
+ | Deploy / release step discovered | `note` |
46
+ | Milestone / feature / task completed | `note` |
47
+
48
+ Do NOT save: routine reads, search results, temporary debugging dead-ends.
49
+ Feature spans sessions → `discussion.save` status: `"active"`.
50
+ Need past info → `search` before asking. Always pass `project`.
35
51
 
36
52
  ---
37
53
 
38
- ## 3. CodeGraph Pipeline
54
+ ## 3. ContextGraph Pipeline
55
+
56
+ > The knowledge graph is also called **ContextGraph**. The MCP tools use the `codegraph_*` prefix — both names refer to the same thing.
39
57
 
40
58
  ### Step 1 — Build (once, fast, local)
41
59
  ```