@kopikocappu/mycelium 0.2.2 → 0.2.3

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.
Files changed (2) hide show
  1. package/dist/cli.js +657 -15
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -15698,7 +15698,7 @@ function getEngine() {
15698
15698
  var import_child_process = require("child_process");
15699
15699
  var import_crypto2 = __toESM(require("crypto"));
15700
15700
  var import_path2 = __toESM(require("path"));
15701
- var CbmAdapter = class {
15701
+ var CbmAdapter = class _CbmAdapter {
15702
15702
  constructor() {
15703
15703
  this.cbmBin = this.findBinary();
15704
15704
  }
@@ -15844,6 +15844,71 @@ var CbmAdapter = class {
15844
15844
  return null;
15845
15845
  }
15846
15846
  }
15847
+ static {
15848
+ // Directories/patterns that are never application source code
15849
+ // This is a blacklist approach — custom dirs are included automatically
15850
+ this.NEVER_SOURCE = [
15851
+ "node_modules",
15852
+ ".expo",
15853
+ ".git",
15854
+ "dist",
15855
+ "build",
15856
+ "out",
15857
+ ".next",
15858
+ ".nuxt",
15859
+ ".output",
15860
+ "coverage",
15861
+ "android",
15862
+ "ios",
15863
+ "Pods",
15864
+ "assets",
15865
+ "public",
15866
+ "static",
15867
+ ".mycelium",
15868
+ ".graphmem",
15869
+ ".cache",
15870
+ ".turbo",
15871
+ "generated",
15872
+ "__generated__",
15873
+ "docs",
15874
+ "fixtures",
15875
+ "e2e",
15876
+ ".vscode",
15877
+ ".idea"
15878
+ ];
15879
+ }
15880
+ static {
15881
+ this.NEVER_EXTENSIONS = [
15882
+ ".d.ts",
15883
+ ".min.js",
15884
+ ".min.css",
15885
+ ".map",
15886
+ ".png",
15887
+ ".jpg",
15888
+ ".jpeg",
15889
+ ".gif",
15890
+ ".svg",
15891
+ ".ico",
15892
+ ".woff",
15893
+ ".woff2",
15894
+ ".ttf",
15895
+ ".mp4",
15896
+ ".mp3"
15897
+ ];
15898
+ }
15899
+ isSourceFile(filePath) {
15900
+ const parts = filePath.split("/");
15901
+ for (const part of parts) {
15902
+ if (_CbmAdapter.NEVER_SOURCE.includes(part)) return false;
15903
+ }
15904
+ for (const ext2 of _CbmAdapter.NEVER_EXTENSIONS) {
15905
+ if (filePath.endsWith(ext2)) return false;
15906
+ }
15907
+ if (filePath.includes(".test.") || filePath.includes(".spec.")) return false;
15908
+ if (filePath.includes(".stories.")) return false;
15909
+ if (filePath.includes("__tests__") || filePath.includes("__mocks__")) return false;
15910
+ return true;
15911
+ }
15847
15912
  convertNodes(cbmNodes, repoPath) {
15848
15913
  const now = Date.now();
15849
15914
  const nodes = [];
@@ -15851,6 +15916,11 @@ var CbmAdapter = class {
15851
15916
  const label = n.label?.toLowerCase() ?? "file";
15852
15917
  const kind = this.mapLabel(label);
15853
15918
  const filePath = n.file ? import_path2.default.relative(repoPath, n.file).replace(/\\/g, "/") : n.name;
15919
+ if (kind === "file" && !this.isSourceFile(filePath)) continue;
15920
+ if (kind !== "file") {
15921
+ const parentFile = filePath.split("::")[0];
15922
+ if (!this.isSourceFile(parentFile)) continue;
15923
+ }
15854
15924
  const id = kind === "file" ? filePath : `${filePath}::${n.qualified_name ?? n.name}`;
15855
15925
  nodes.push({
15856
15926
  id,
@@ -16338,6 +16408,184 @@ var EMBEDDED_VIEWER = `<!DOCTYPE html>
16338
16408
  box-shadow: 0 4px 20px rgba(0,0,0,0.4);
16339
16409
  }
16340
16410
 
16411
+ /* \u2500\u2500 Button tooltips \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
16412
+ [data-tip] { position: relative; }
16413
+ [data-tip]::after {
16414
+ content: attr(data-tip);
16415
+ position: absolute;
16416
+ bottom: calc(100% + 8px);
16417
+ left: 50%;
16418
+ transform: translateX(-50%);
16419
+ background: #1a1a1a;
16420
+ border: 1px solid rgba(255,255,255,0.15);
16421
+ color: rgba(255,255,255,0.85);
16422
+ font-family: var(--font-code);
16423
+ font-size: 11px;
16424
+ white-space: pre;
16425
+ padding: 6px 10px;
16426
+ line-height: 1.5;
16427
+ pointer-events: none;
16428
+ opacity: 0;
16429
+ transition: opacity 0.15s;
16430
+ z-index: 50;
16431
+ text-align: left;
16432
+ min-width: 160px;
16433
+ }
16434
+ [data-tip]:hover::after { opacity: 1; }
16435
+
16436
+ /* \u2500\u2500 Settings panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
16437
+ #settings-panel {
16438
+ display: none;
16439
+ position: absolute;
16440
+ top: 0; left: 0; right: 0; bottom: 0;
16441
+ background: var(--bg);
16442
+ z-index: 30;
16443
+ overflow-y: auto;
16444
+ padding: 20px;
16445
+ }
16446
+
16447
+ #settings-panel.open { display: block; }
16448
+
16449
+ .settings-header {
16450
+ display: flex;
16451
+ align-items: center;
16452
+ justify-content: space-between;
16453
+ margin-bottom: 20px;
16454
+ padding-bottom: 12px;
16455
+ border-bottom: 1px solid var(--border);
16456
+ }
16457
+
16458
+ .settings-title {
16459
+ font-family: var(--font-mono);
16460
+ font-size: 13px;
16461
+ font-weight: 700;
16462
+ color: var(--text);
16463
+ }
16464
+
16465
+ .settings-close {
16466
+ background: none;
16467
+ border: 1px solid var(--border);
16468
+ color: var(--text-dim);
16469
+ cursor: pointer;
16470
+ font-size: 14px;
16471
+ padding: 2px 8px;
16472
+ font-family: var(--font-code);
16473
+ }
16474
+
16475
+ .settings-close:hover { color: var(--text); background: rgba(255,255,255,0.05); }
16476
+
16477
+ .settings-section {
16478
+ margin-bottom: 20px;
16479
+ }
16480
+
16481
+ .settings-section-title {
16482
+ font-family: var(--font-code);
16483
+ font-size: 10px;
16484
+ letter-spacing: 0.12em;
16485
+ text-transform: uppercase;
16486
+ color: var(--text-dim);
16487
+ margin-bottom: 10px;
16488
+ }
16489
+
16490
+ .ignore-item {
16491
+ display: flex;
16492
+ align-items: center;
16493
+ justify-content: space-between;
16494
+ padding: 5px 8px;
16495
+ border-radius: 3px;
16496
+ gap: 8px;
16497
+ }
16498
+
16499
+ .ignore-item:hover { background: rgba(255,255,255,0.04); }
16500
+
16501
+ .ignore-pattern {
16502
+ font-family: var(--font-code);
16503
+ font-size: 11px;
16504
+ color: var(--text);
16505
+ flex: 1;
16506
+ overflow: hidden;
16507
+ text-overflow: ellipsis;
16508
+ white-space: nowrap;
16509
+ }
16510
+
16511
+ .ignore-pattern.removed { color: var(--text-dim); text-decoration: line-through; }
16512
+ .ignore-pattern.custom { color: #6B8AFF; }
16513
+
16514
+ .ignore-btn {
16515
+ background: none;
16516
+ border: 1px solid var(--border);
16517
+ color: var(--text-dim);
16518
+ cursor: pointer;
16519
+ font-family: var(--font-code);
16520
+ font-size: 10px;
16521
+ padding: 2px 7px;
16522
+ flex-shrink: 0;
16523
+ transition: all 0.12s;
16524
+ }
16525
+
16526
+ .ignore-btn:hover { color: var(--text); border-color: rgba(255,255,255,0.3); }
16527
+ .ignore-btn.danger:hover { color: #F28B82; border-color: #F28B82; }
16528
+
16529
+ .ignore-add-row {
16530
+ display: flex;
16531
+ gap: 8px;
16532
+ margin-top: 10px;
16533
+ }
16534
+
16535
+ .ignore-input {
16536
+ flex: 1;
16537
+ background: var(--surface2);
16538
+ border: 1px solid var(--border);
16539
+ color: var(--text);
16540
+ font-family: var(--font-code);
16541
+ font-size: 12px;
16542
+ padding: 7px 10px;
16543
+ outline: none;
16544
+ }
16545
+
16546
+ .ignore-input:focus { border-color: rgba(255,255,255,0.3); }
16547
+ .ignore-input::placeholder { color: var(--text-dim); }
16548
+
16549
+ .ignore-add-btn {
16550
+ background: rgba(107,138,255,0.15);
16551
+ border: 1px solid rgba(107,138,255,0.3);
16552
+ color: #6B8AFF;
16553
+ cursor: pointer;
16554
+ font-family: var(--font-code);
16555
+ font-size: 11px;
16556
+ padding: 7px 14px;
16557
+ transition: all 0.12s;
16558
+ }
16559
+
16560
+ .ignore-add-btn:hover { background: rgba(107,138,255,0.25); }
16561
+
16562
+ .settings-note {
16563
+ font-family: var(--font-code);
16564
+ font-size: 10px;
16565
+ color: var(--text-dim);
16566
+ margin-top: 12px;
16567
+ line-height: 1.6;
16568
+ padding: 8px;
16569
+ border: 1px solid var(--border);
16570
+ background: rgba(255,255,255,0.02);
16571
+ }
16572
+
16573
+ .rescan-btn {
16574
+ width: 100%;
16575
+ margin-top: 16px;
16576
+ padding: 10px;
16577
+ background: rgba(107,138,255,0.1);
16578
+ border: 1px solid rgba(107,138,255,0.25);
16579
+ color: #6B8AFF;
16580
+ font-family: var(--font-code);
16581
+ font-size: 12px;
16582
+ cursor: pointer;
16583
+ transition: all 0.12s;
16584
+ letter-spacing: 0.05em;
16585
+ }
16586
+
16587
+ .rescan-btn:hover { background: rgba(107,138,255,0.2); }
16588
+
16341
16589
  #tooltip .tip-name { font-weight: 700; font-size: 13px; margin-bottom: 4px; color: var(--text); }
16342
16590
  #tooltip .tip-desc { color: var(--text-dim); font-size: 11px; line-height: 1.6; margin-top: 4px; }
16343
16591
  #tooltip .tip-tags { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 3px; }
@@ -16401,19 +16649,19 @@ var EMBEDDED_VIEWER = `<!DOCTYPE html>
16401
16649
  <div id="controls">
16402
16650
  <div class="control-row">
16403
16651
  <span>Edges:</span>
16404
- <button class="toggle-btn active" id="toggle-imports">Imports</button>
16405
- <button class="toggle-btn active" id="toggle-calls">Calls</button>
16652
+ <button class="toggle-btn active" id="toggle-imports" data-tip="Show/hide import edges&#10;Blue lines \u2014 file A imports file B&#10;Shows dependency direction">Imports</button>
16653
+ <button class="toggle-btn active" id="toggle-calls" data-tip="Show/hide call edges&#10;Dashed lines \u2014 function A calls function B&#10;Requires codebase-memory-mcp">Calls</button>
16406
16654
  <button class="toggle-btn" id="toggle-fns">Functions</button>
16407
16655
  </div>
16408
16656
  <div class="control-row">
16409
16657
  <span>Zoom level:</span>
16410
16658
  <div class="zoom-level-btns">
16411
- <button class="toggle-btn active" id="zoom-files">Files</button>
16412
- <button class="toggle-btn" id="zoom-symbols">Symbols</button>
16659
+ <button class="toggle-btn active" id="zoom-files" data-tip="File view (default)&#10;One dot per file&#10;Best for understanding structure">Files</button>
16660
+ <button class="toggle-btn" id="zoom-symbols" data-tip="Symbol view&#10;Shows functions and classes&#10;Symbols orbit their parent file">Symbols</button>
16413
16661
  </div>
16414
16662
  </div>
16415
16663
  <div class="control-row">
16416
- <button class="toggle-btn" id="toggle-clusters">Directory clusters</button>
16664
+ <button class="toggle-btn" id="toggle-clusters" data-tip="Show/hide directory boundaries&#10;Dashed outlines group files&#10;by their folder">Directory clusters</button>
16417
16665
  </div>
16418
16666
  </div>
16419
16667
  <div id="node-info">
@@ -16562,7 +16810,12 @@ function buildVis() {
16562
16810
  // Filter nodes/links based on current view settings
16563
16811
  visNodes = allNodes.filter(n => {
16564
16812
  if (n.kind === 'file') return true;
16565
- if ((n.kind === 'function' || n.kind === 'class') && zoomLevel === 'symbols') return showFunctions;
16813
+ if (zoomLevel === 'symbols' && showFunctions) {
16814
+ if (n.kind === 'function' || n.kind === 'class' || n.kind === 'type' || n.kind === 'interface') {
16815
+ // Only show named, non-anonymous symbols
16816
+ return n.name && n.name.length > 1 && !n.name.includes('anonymous') && !n.name.startsWith('_');
16817
+ }
16818
+ }
16566
16819
  return false;
16567
16820
  });
16568
16821
 
@@ -16571,7 +16824,9 @@ function buildVis() {
16571
16824
  if (!nodeIds.has(l.from) || !nodeIds.has(l.to)) return false;
16572
16825
  if (l.kind === 'imports' && !showImports) return false;
16573
16826
  if (l.kind === 'calls' && !showCalls) return false;
16574
- if (l.kind === 'contains') return zoomLevel === 'symbols';
16827
+ // Contains edges anchor symbols to their parent file
16828
+ // Critical: without these, symbols have no edges and scatter everywhere
16829
+ if (l.kind === 'contains') return zoomLevel === 'symbols' && showFunctions;
16575
16830
  return true;
16576
16831
  });
16577
16832
 
@@ -16620,7 +16875,11 @@ function buildVis() {
16620
16875
  .id(d => d.id)
16621
16876
  .distance(d => d.kind === 'contains' ? 40 : d.kind === 'calls' ? 80 : 100)
16622
16877
  .strength(d => d.kind === 'contains' ? 0.8 : 0.3))
16623
- .force('charge', d3.forceManyBody().strength(d => d.kind === 'file' ? -250 : -80))
16878
+ .force('charge', d3.forceManyBody()
16879
+ // Symbols get very weak repulsion so they cluster near parent files via contains edges
16880
+ // Without this, 600+ unconnected symbols explode across the entire screen
16881
+ .strength(d => d.kind === 'file' ? -180 : -8)
16882
+ .distanceMax(300))
16624
16883
  .force('center', d3.forceCenter(W / 2, H / 2))
16625
16884
  .force('collision', d3.forceCollide().radius(d => nodeRadius(d) + 8))
16626
16885
  .force('cluster', clusterForce(dirGroups, 0.08))
@@ -17016,6 +17275,12 @@ document.getElementById('toggle-clusters').addEventListener('click', function()
17016
17275
  });
17017
17276
 
17018
17277
  document.getElementById('zoom-files').addEventListener('click', function() {
17278
+ if (zoomLevel === 'symbols') {
17279
+ // Reset positions \u2014 symbols simulation spreads files far apart
17280
+ // Without this, files stay in their spread-out positions after toggling back
17281
+ allNodes.forEach(n => { n.x = undefined; n.y = undefined; n.vx = 0; n.vy = 0; });
17282
+ _initialFitDone = false;
17283
+ }
17019
17284
  zoomLevel = 'files';
17020
17285
  this.classList.add('active');
17021
17286
  document.getElementById('zoom-symbols').classList.remove('active');
@@ -17190,6 +17455,99 @@ function hideLoading() {
17190
17455
  initMinimap();
17191
17456
  loadGraph();
17192
17457
 
17458
+ // \u2500\u2500 Settings / Ignore Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
17459
+ const settingsBtn = document.getElementById('settings-btn');
17460
+ const settingsPanel = document.getElementById('settings-panel');
17461
+ const settingsClose = document.getElementById('settings-close');
17462
+
17463
+ settingsBtn.addEventListener('click', () => {
17464
+ settingsPanel.classList.add('open');
17465
+ loadIgnoreConfig();
17466
+ });
17467
+
17468
+ settingsClose.addEventListener('click', () => {
17469
+ settingsPanel.classList.remove('open');
17470
+ });
17471
+
17472
+ // Hover effect for gear button
17473
+ settingsBtn.addEventListener('mouseenter', () => {
17474
+ settingsBtn.style.color = 'var(--text)';
17475
+ settingsBtn.style.borderColor = 'rgba(255,255,255,0.3)';
17476
+ });
17477
+ settingsBtn.addEventListener('mouseleave', () => {
17478
+ settingsBtn.style.color = 'var(--text-dim)';
17479
+ settingsBtn.style.borderColor = 'var(--border)';
17480
+ });
17481
+
17482
+ async function loadIgnoreConfig() {
17483
+ try {
17484
+ const res = await fetch(\`\${API_BASE}/config\`);
17485
+ const data = await res.json();
17486
+ renderIgnoreList(data);
17487
+ } catch (e) {
17488
+ document.getElementById('ignore-default-list').innerHTML =
17489
+ '<div style="font-family:var(--font-code);font-size:11px;color:var(--text-dim);padding:4px 0">Could not load config \u2014 is the server running?</div>';
17490
+ }
17491
+ }
17492
+
17493
+ function renderIgnoreList(data) {
17494
+ const { defaultIgnore, userIgnore, userUnignore } = data;
17495
+
17496
+ // Default patterns
17497
+ const defaultEl = document.getElementById('ignore-default-list');
17498
+ defaultEl.innerHTML = defaultIgnore.map(pattern => {
17499
+ const isRemoved = userUnignore.includes(pattern);
17500
+ return \`<div class="ignore-item">
17501
+ <span class="ignore-pattern \${isRemoved ? 'removed' : ''}">\${pattern}</span>
17502
+ \${isRemoved
17503
+ ? \`<button class="ignore-btn" onclick="restorePattern('\${pattern}')">restore</button>\`
17504
+ : \`<button class="ignore-btn danger" onclick="removePattern('\${pattern}')">remove</button>\`
17505
+ }
17506
+ </div>\`;
17507
+ }).join('');
17508
+
17509
+ // Custom patterns
17510
+ const customEl = document.getElementById('ignore-custom-list');
17511
+ if (userIgnore.length === 0) {
17512
+ customEl.innerHTML = '<div style="font-family:var(--font-code);font-size:11px;color:var(--text-dim);padding:4px 0;">None added yet</div>';
17513
+ } else {
17514
+ customEl.innerHTML = userIgnore.map(pattern =>
17515
+ \`<div class="ignore-item">
17516
+ <span class="ignore-pattern custom">\${pattern}</span>
17517
+ <button class="ignore-btn danger" onclick="removePattern('\${pattern}')">remove</button>
17518
+ </div>\`
17519
+ ).join('');
17520
+ }
17521
+ }
17522
+
17523
+ async function postIgnoreAction(action, pattern) {
17524
+ try {
17525
+ await fetch(\`\${API_BASE}/config\`, {
17526
+ method: 'POST',
17527
+ headers: { 'Content-Type': 'application/json' },
17528
+ body: JSON.stringify({ action, pattern }),
17529
+ });
17530
+ await loadIgnoreConfig();
17531
+ } catch (e) {
17532
+ alert('Could not update config. Is the server running?');
17533
+ }
17534
+ }
17535
+
17536
+ function removePattern(pattern) { postIgnoreAction('remove', pattern); }
17537
+ function restorePattern(pattern) { postIgnoreAction('restore', pattern); }
17538
+
17539
+ document.getElementById('ignore-add-btn').addEventListener('click', async () => {
17540
+ const input = document.getElementById('ignore-input');
17541
+ const pattern = input.value.trim();
17542
+ if (!pattern) return;
17543
+ await postIgnoreAction('add', pattern);
17544
+ input.value = '';
17545
+ });
17546
+
17547
+ document.getElementById('ignore-input').addEventListener('keydown', e => {
17548
+ if (e.key === 'Enter') document.getElementById('ignore-add-btn').click();
17549
+ });
17550
+
17193
17551
  function updateDirLegend() {
17194
17552
  const el = document.getElementById('dir-legend');
17195
17553
  if (!el) return;
@@ -17220,8 +17578,62 @@ var RateLimiter = class {
17220
17578
  return true;
17221
17579
  }
17222
17580
  };
17581
+ var DEFAULT_IGNORE_LIST = [
17582
+ "node_modules/**",
17583
+ "vendor/**",
17584
+ ".pnp/**",
17585
+ "dist/**",
17586
+ "build/**",
17587
+ "out/**",
17588
+ ".next/**",
17589
+ ".nuxt/**",
17590
+ ".output/**",
17591
+ "coverage/**",
17592
+ "android/**",
17593
+ "ios/**",
17594
+ ".expo/**",
17595
+ "Pods/**",
17596
+ "assets/**",
17597
+ "public/**",
17598
+ "static/**",
17599
+ "**/*.png",
17600
+ "**/*.jpg",
17601
+ "**/*.jpeg",
17602
+ "**/*.gif",
17603
+ "**/*.svg",
17604
+ "**/*.ico",
17605
+ "**/*.woff",
17606
+ "**/*.woff2",
17607
+ "**/*.ttf",
17608
+ "**/*.mp4",
17609
+ "**/*.mp3",
17610
+ ".mycelium/**",
17611
+ ".graphmem/**",
17612
+ ".cache/**",
17613
+ ".turbo/**",
17614
+ "generated/**",
17615
+ "__generated__/**",
17616
+ "**/*.generated.ts",
17617
+ "**/*.generated.js",
17618
+ "**/*.d.ts",
17619
+ "**/*.min.js",
17620
+ "**/*.min.css",
17621
+ "**/*.map",
17622
+ "**/*.test.ts",
17623
+ "**/*.test.tsx",
17624
+ "**/*.spec.ts",
17625
+ "**/*.spec.tsx",
17626
+ "**/__tests__/**",
17627
+ "**/__mocks__/**",
17628
+ "**/*.stories.ts",
17629
+ "**/*.stories.tsx",
17630
+ "docs/**",
17631
+ "fixtures/**",
17632
+ "e2e/**"
17633
+ ];
17223
17634
  var McpServer = class {
17224
17635
  constructor(store, changeLogger, config) {
17636
+ this.projectRoot = null;
17225
17637
  this.rateLimiter = new RateLimiter();
17226
17638
  this.server = null;
17227
17639
  this.store = store;
@@ -17299,6 +17711,10 @@ var McpServer = class {
17299
17711
  if (pathname === "/xref") {
17300
17712
  return this.handleXref(url, res);
17301
17713
  }
17714
+ if (pathname === "/config") {
17715
+ if (req.method === "GET") return this.handleConfigGet(res);
17716
+ if (req.method === "POST") return this.handleConfigPost(req, res);
17717
+ }
17302
17718
  if (pathname === "/preflight") {
17303
17719
  return this.handlePreflight(url, res);
17304
17720
  }
@@ -17503,6 +17919,75 @@ var McpServer = class {
17503
17919
  this.handleKeywordPreflight(task, teamName, lens, res);
17504
17920
  }
17505
17921
  }
17922
+ handleConfigGet(res) {
17923
+ const configPath = path4.join(this.projectRoot ?? process.cwd(), ".mycelium", "config.json");
17924
+ let saved = {};
17925
+ try {
17926
+ if (fs3.existsSync(configPath)) saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
17927
+ } catch {
17928
+ }
17929
+ const userIgnore = saved?.parser?.userIgnore ?? [];
17930
+ const userUnignore = saved?.parser?.userUnignore ?? [];
17931
+ const defaultIgnore = DEFAULT_IGNORE_LIST;
17932
+ const activeIgnore = [
17933
+ ...defaultIgnore.filter((p) => !userUnignore.includes(p)),
17934
+ ...userIgnore
17935
+ ];
17936
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
17937
+ res.end(JSON.stringify({
17938
+ defaultIgnore,
17939
+ userIgnore,
17940
+ userUnignore,
17941
+ activeIgnore,
17942
+ totalActive: activeIgnore.length
17943
+ }));
17944
+ }
17945
+ handleConfigPost(req, res) {
17946
+ let body = "";
17947
+ req.on("data", (chunk) => {
17948
+ body += chunk.toString();
17949
+ });
17950
+ req.on("end", () => {
17951
+ try {
17952
+ const { action, pattern } = JSON.parse(body);
17953
+ const configPath = path4.join(this.projectRoot ?? process.cwd(), ".mycelium", "config.json");
17954
+ let saved = {};
17955
+ try {
17956
+ if (fs3.existsSync(configPath)) saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
17957
+ } catch {
17958
+ }
17959
+ if (!saved.parser) saved.parser = {};
17960
+ let userIgnore = saved.parser.userIgnore ?? [];
17961
+ let userUnignore = saved.parser.userUnignore ?? [];
17962
+ if (action === "add" && pattern) {
17963
+ if (!userIgnore.includes(pattern)) {
17964
+ userIgnore = [...userIgnore, pattern];
17965
+ userUnignore = userUnignore.filter((p) => p !== pattern);
17966
+ }
17967
+ } else if (action === "remove" && pattern) {
17968
+ const isDefault = DEFAULT_IGNORE_LIST.includes(pattern);
17969
+ if (isDefault) {
17970
+ if (!userUnignore.includes(pattern)) userUnignore = [...userUnignore, pattern];
17971
+ } else {
17972
+ userIgnore = userIgnore.filter((p) => p !== pattern);
17973
+ }
17974
+ } else if (action === "restore" && pattern) {
17975
+ userUnignore = userUnignore.filter((p) => p !== pattern);
17976
+ } else if (action === "reset") {
17977
+ userIgnore = [];
17978
+ userUnignore = [];
17979
+ }
17980
+ saved.parser.userIgnore = userIgnore;
17981
+ saved.parser.userUnignore = userUnignore;
17982
+ fs3.writeFileSync(configPath, JSON.stringify(saved, null, 2));
17983
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
17984
+ res.end(JSON.stringify({ ok: true, userIgnore, userUnignore }));
17985
+ } catch (e) {
17986
+ res.writeHead(400);
17987
+ res.end(JSON.stringify({ error: e.message }));
17988
+ }
17989
+ });
17990
+ }
17506
17991
  async handleXref(url, res) {
17507
17992
  const file = (url.searchParams.get("file") || "").slice(0, 500);
17508
17993
  const fn = (url.searchParams.get("fn") || "").slice(0, 500);
@@ -18073,6 +18558,68 @@ ${GRAPHMEM_SECTION_END}`;
18073
18558
  }
18074
18559
 
18075
18560
  // src/graph/schema.ts
18561
+ var DEFAULT_IGNORE = [
18562
+ // Package managers & deps
18563
+ "node_modules/**",
18564
+ "vendor/**",
18565
+ ".pnp/**",
18566
+ // Build output
18567
+ "dist/**",
18568
+ "build/**",
18569
+ "out/**",
18570
+ ".next/**",
18571
+ ".nuxt/**",
18572
+ ".output/**",
18573
+ "coverage/**",
18574
+ // Native mobile (usually auto-generated)
18575
+ "android/**",
18576
+ "ios/**",
18577
+ ".expo/**",
18578
+ "Pods/**",
18579
+ // Assets & static
18580
+ "assets/**",
18581
+ "public/**",
18582
+ "static/**",
18583
+ "**/*.png",
18584
+ "**/*.jpg",
18585
+ "**/*.jpeg",
18586
+ "**/*.gif",
18587
+ "**/*.svg",
18588
+ "**/*.ico",
18589
+ "**/*.woff",
18590
+ "**/*.woff2",
18591
+ "**/*.ttf",
18592
+ "**/*.mp4",
18593
+ "**/*.mp3",
18594
+ // Generated & cache
18595
+ ".mycelium/**",
18596
+ ".graphmem/**",
18597
+ ".cache/**",
18598
+ ".turbo/**",
18599
+ "generated/**",
18600
+ "__generated__/**",
18601
+ "**/*.generated.ts",
18602
+ "**/*.generated.js",
18603
+ // Config & declaration files
18604
+ "**/*.d.ts",
18605
+ "**/*.min.js",
18606
+ "**/*.min.css",
18607
+ "**/*.map",
18608
+ // Tests (optional — users may want these)
18609
+ "**/*.test.ts",
18610
+ "**/*.test.tsx",
18611
+ "**/*.spec.ts",
18612
+ "**/*.spec.tsx",
18613
+ "**/__tests__/**",
18614
+ "**/__mocks__/**",
18615
+ // Stories
18616
+ "**/*.stories.ts",
18617
+ "**/*.stories.tsx",
18618
+ // Docs & fixtures
18619
+ "docs/**",
18620
+ "fixtures/**",
18621
+ "e2e/**"
18622
+ ];
18076
18623
  var DEFAULT_CONFIG = {
18077
18624
  teams: {
18078
18625
  core: { name: "core", includeTags: [], includeAll: true }
@@ -18082,8 +18629,9 @@ var DEFAULT_CONFIG = {
18082
18629
  batchSize: 10
18083
18630
  },
18084
18631
  parser: {
18085
- include: ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
18086
- exclude: ["node_modules/**", "dist/**", ".graphmem/**", "**/*.test.*", "**/*.spec.*"]
18632
+ include: [],
18633
+ // empty = auto-detect from tsconfig or directory structure
18634
+ exclude: DEFAULT_IGNORE
18087
18635
  },
18088
18636
  mcp: {
18089
18637
  port: 47821,
@@ -18123,13 +18671,41 @@ function resolveRoot(input) {
18123
18671
  }
18124
18672
  function loadProjectConfig(root) {
18125
18673
  const cfgPath = path8.join(root, ".mycelium", "config.json");
18674
+ let saved = {};
18126
18675
  if (fs8.existsSync(cfgPath)) {
18127
18676
  try {
18128
- return { ...DEFAULT_CONFIG, ...JSON.parse(fs8.readFileSync(cfgPath, "utf8")) };
18677
+ saved = JSON.parse(fs8.readFileSync(cfgPath, "utf8"));
18129
18678
  } catch {
18130
18679
  }
18131
18680
  }
18132
- return DEFAULT_CONFIG;
18681
+ const merged = { ...DEFAULT_CONFIG, ...saved };
18682
+ const userIgnore = saved.parser?.userIgnore ?? [];
18683
+ const userUnignore = saved.parser?.userUnignore ?? [];
18684
+ merged.parser = {
18685
+ ...DEFAULT_CONFIG.parser,
18686
+ ...saved.parser ?? {},
18687
+ exclude: [
18688
+ ...DEFAULT_IGNORE.filter((p) => !userUnignore.includes(p)),
18689
+ ...userIgnore
18690
+ ],
18691
+ userIgnore,
18692
+ userUnignore
18693
+ };
18694
+ return merged;
18695
+ }
18696
+ function saveProjectConfig(root, update) {
18697
+ const cfgPath = path8.join(root, ".mycelium", "config.json");
18698
+ let existing = {};
18699
+ if (fs8.existsSync(cfgPath)) {
18700
+ try {
18701
+ existing = JSON.parse(fs8.readFileSync(cfgPath, "utf8"));
18702
+ } catch {
18703
+ }
18704
+ }
18705
+ if (!existing.parser) existing.parser = {};
18706
+ if (update.userIgnore !== void 0) existing.parser.userIgnore = update.userIgnore;
18707
+ if (update.userUnignore !== void 0) existing.parser.userUnignore = update.userUnignore;
18708
+ fs8.writeFileSync(cfgPath, JSON.stringify(existing, null, 2));
18133
18709
  }
18134
18710
  function initProjectDir(root) {
18135
18711
  const dir = path8.join(root, ".mycelium");
@@ -18307,7 +18883,7 @@ program2.command("init [path]").description("Scan codebase, build graph, start s
18307
18883
  ok(".mcp.json written");
18308
18884
  if (opts.serve) {
18309
18885
  const server = new McpServer(store, logger, config);
18310
- server.start();
18886
+ server.start(root);
18311
18887
  log("");
18312
18888
  log(` ${C.bold}Graph view${C.reset} ${C.cyan}http://localhost:${config.mcp.port}/ui${C.reset}`);
18313
18889
  log(` ${C.bold}MCP server${C.reset} ${C.gray}http://localhost:${config.mcp.port}${C.reset}`);
@@ -18338,7 +18914,7 @@ program2.command("serve [path]").description("Start MCP server and file watcher
18338
18914
  }
18339
18915
  ok(`Loaded graph: ${stats.fileCount} files, ${stats.edgeCount} edges`);
18340
18916
  const server = new McpServer(store, logger, config);
18341
- server.start();
18917
+ server.start(root);
18342
18918
  log("");
18343
18919
  log(` ${C.bold}Graph view${C.reset} ${C.cyan}http://localhost:${config.mcp.port}/ui${C.reset}`);
18344
18920
  log(` ${C.bold}MCP server${C.reset} ${C.gray}http://localhost:${config.mcp.port}${C.reset}`);
@@ -18529,6 +19105,72 @@ program2.command("embed [path]").description("Generate semantic embeddings for a
18529
19105
  if (pruned > 0) ok(`${pruned} stale embeddings removed`);
18530
19106
  ok(`Semantic search now active on /search and /preflight`);
18531
19107
  });
19108
+ program2.command("ignore [path]").description("View and manage the scan ignore list").option("--add <pattern>", "Add a pattern to the ignore list").option("--remove <pattern>", "Remove a pattern (default or custom) from the ignore list").option("--reset", "Reset to default ignore list").action((targetPath, opts) => {
19109
+ header();
19110
+ const root = resolveRoot(targetPath);
19111
+ const config = loadProjectConfig(root);
19112
+ const userIgnore = config.parser.userIgnore ?? [];
19113
+ const userUnignore = config.parser.userUnignore ?? [];
19114
+ if (opts.reset) {
19115
+ saveProjectConfig(root, { userIgnore: [], userUnignore: [] });
19116
+ ok("Ignore list reset to defaults");
19117
+ return;
19118
+ }
19119
+ if (opts.add) {
19120
+ const pattern = opts.add.trim();
19121
+ if (userIgnore.includes(pattern)) {
19122
+ warn(`Already ignored: ${pattern}`);
19123
+ return;
19124
+ }
19125
+ const newUnignore = userUnignore.filter((p) => p !== pattern);
19126
+ saveProjectConfig(root, { userIgnore: [...userIgnore, pattern], userUnignore: newUnignore });
19127
+ ok(`Added to ignore list: ${C.gray}${pattern}${C.reset}`);
19128
+ ok("Run mycelium scan to apply changes");
19129
+ return;
19130
+ }
19131
+ if (opts.remove) {
19132
+ const pattern = opts.remove.trim();
19133
+ const isDefault = DEFAULT_IGNORE.includes(pattern);
19134
+ const isCustom = userIgnore.includes(pattern);
19135
+ if (!isDefault && !isCustom) {
19136
+ warn(`Pattern not found in ignore list: ${pattern}`);
19137
+ return;
19138
+ }
19139
+ if (isDefault) {
19140
+ if (!userUnignore.includes(pattern)) {
19141
+ saveProjectConfig(root, { userIgnore, userUnignore: [...userUnignore, pattern] });
19142
+ }
19143
+ } else {
19144
+ saveProjectConfig(root, { userIgnore: userIgnore.filter((p) => p !== pattern), userUnignore });
19145
+ }
19146
+ ok(`Removed from ignore list: ${C.gray}${pattern}${C.reset}`);
19147
+ ok("Run mycelium scan to apply changes");
19148
+ return;
19149
+ }
19150
+ const activeIgnore = [
19151
+ ...DEFAULT_IGNORE.filter((p) => !userUnignore.includes(p)),
19152
+ ...userIgnore
19153
+ ];
19154
+ log(`
19155
+ ${C.bold}Default patterns${C.reset} ${C.gray}(${DEFAULT_IGNORE.length - userUnignore.length} active)${C.reset}`);
19156
+ for (const p of DEFAULT_IGNORE) {
19157
+ const removed = userUnignore.includes(p);
19158
+ log(` ${removed ? C.gray + "\u2717 " : " "}${p}${removed ? " (removed)" : ""}${C.reset}`);
19159
+ }
19160
+ if (userIgnore.length > 0) {
19161
+ log(`
19162
+ ${C.bold}Custom patterns${C.reset} ${C.gray}(${userIgnore.length})${C.reset}`);
19163
+ for (const p of userIgnore) {
19164
+ log(` ${C.cyan}+${C.reset} ${p}`);
19165
+ }
19166
+ }
19167
+ log(`
19168
+ ${C.gray}Total active: ${activeIgnore.length} patterns${C.reset}`);
19169
+ log(` ${C.gray}Add: mycelium ignore --add "android/**"${C.reset}`);
19170
+ log(` ${C.gray}Remove: mycelium ignore --remove "**/*.test.ts"${C.reset}`);
19171
+ log(` ${C.gray}Reset: mycelium ignore --reset${C.reset}
19172
+ `);
19173
+ });
18532
19174
  program2.parse(process.argv);
18533
19175
  /*! Bundled license information:
18534
19176
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kopikocappu/mycelium",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Codebase memory for AI coding agents. Natural language preflight, graph viewer, and agent history in one command.",
5
5
  "bin": {
6
6
  "mycelium": "dist/cli.js"