@kopikocappu/mycelium 0.2.2 → 0.2.4

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 +923 -39
  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,
@@ -15960,7 +16030,51 @@ var EMBEDDED_VIEWER = `<!DOCTYPE html>
15960
16030
  flex-direction: column;
15961
16031
  z-index: 10;
15962
16032
  overflow: hidden;
16033
+ position: relative;
16034
+ }
16035
+
16036
+ .sidebar-tab {
16037
+ flex: 1;
16038
+ padding: 8px 4px;
16039
+ background: none;
16040
+ border: none;
16041
+ border-bottom: 2px solid transparent;
16042
+ color: var(--text-dim);
16043
+ cursor: pointer;
16044
+ font-family: inherit;
16045
+ font-size: 11px;
16046
+ letter-spacing: 0.05em;
16047
+ transition: color 0.15s, border-color 0.15s;
16048
+ }
16049
+ .sidebar-tab:hover { color: var(--text); }
16050
+ .sidebar-tab.active { color: var(--text); border-bottom-color: var(--accent); }
16051
+
16052
+ .tab-content { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
16053
+
16054
+ /* \u2500\u2500 History items \u2500\u2500 */
16055
+ .history-item {
16056
+ padding: 10px 12px;
16057
+ border-bottom: 1px solid var(--border);
16058
+ font-size: 11px;
15963
16059
  }
16060
+ .history-item:hover { background: rgba(255,255,255,0.02); }
16061
+ .history-file { color: var(--text); margin-bottom: 3px; font-family: 'SF Mono', monospace; }
16062
+ .history-meta { color: var(--text-dim); display: flex; gap: 8px; flex-wrap: wrap; }
16063
+ .history-task { color: var(--accent2); }
16064
+ .history-agent { color: var(--accent3); }
16065
+
16066
+ /* \u2500\u2500 Ignore list items \u2500\u2500 */
16067
+ .ignore-item {
16068
+ display: flex; align-items: center; justify-content: space-between;
16069
+ padding: 4px 0; gap: 8px;
16070
+ }
16071
+ .ignore-item:hover { background: rgba(255,255,255,0.03); }
16072
+ .ignore-pattern { font-size: 11px; color: var(--text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: 'SF Mono', monospace; }
16073
+ .ignore-pattern.removed { color: var(--text-dim); text-decoration: line-through; }
16074
+ .ignore-pattern.custom { color: #6B8AFF; }
16075
+ .ignore-btn { background: none; border: 1px solid var(--border); color: var(--text-dim); cursor: pointer; font-family: inherit; font-size: 10px; padding: 2px 7px; flex-shrink: 0; }
16076
+ .ignore-btn:hover { color: var(--text); }
16077
+ .ignore-btn.danger:hover { color: #F28B82; border-color: #F28B82; }
15964
16078
 
15965
16079
  #sidebar-header {
15966
16080
  padding: 16px;
@@ -16338,6 +16452,184 @@ var EMBEDDED_VIEWER = `<!DOCTYPE html>
16338
16452
  box-shadow: 0 4px 20px rgba(0,0,0,0.4);
16339
16453
  }
16340
16454
 
16455
+ /* \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 */
16456
+ [data-tip] { position: relative; }
16457
+ [data-tip]::after {
16458
+ content: attr(data-tip);
16459
+ position: absolute;
16460
+ bottom: calc(100% + 8px);
16461
+ left: 50%;
16462
+ transform: translateX(-50%);
16463
+ background: #1a1a1a;
16464
+ border: 1px solid rgba(255,255,255,0.15);
16465
+ color: rgba(255,255,255,0.85);
16466
+ font-family: var(--font-code);
16467
+ font-size: 11px;
16468
+ white-space: pre;
16469
+ padding: 6px 10px;
16470
+ line-height: 1.5;
16471
+ pointer-events: none;
16472
+ opacity: 0;
16473
+ transition: opacity 0.15s;
16474
+ z-index: 50;
16475
+ text-align: left;
16476
+ min-width: 160px;
16477
+ }
16478
+ [data-tip]:hover::after { opacity: 1; }
16479
+
16480
+ /* \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 */
16481
+ #settings-panel {
16482
+ display: none;
16483
+ position: absolute;
16484
+ top: 0; left: 0; right: 0; bottom: 0;
16485
+ background: var(--bg);
16486
+ z-index: 30;
16487
+ overflow-y: auto;
16488
+ padding: 20px;
16489
+ }
16490
+
16491
+ #settings-panel.open { display: block; }
16492
+
16493
+ .settings-header {
16494
+ display: flex;
16495
+ align-items: center;
16496
+ justify-content: space-between;
16497
+ margin-bottom: 20px;
16498
+ padding-bottom: 12px;
16499
+ border-bottom: 1px solid var(--border);
16500
+ }
16501
+
16502
+ .settings-title {
16503
+ font-family: var(--font-mono);
16504
+ font-size: 13px;
16505
+ font-weight: 700;
16506
+ color: var(--text);
16507
+ }
16508
+
16509
+ .settings-close {
16510
+ background: none;
16511
+ border: 1px solid var(--border);
16512
+ color: var(--text-dim);
16513
+ cursor: pointer;
16514
+ font-size: 14px;
16515
+ padding: 2px 8px;
16516
+ font-family: var(--font-code);
16517
+ }
16518
+
16519
+ .settings-close:hover { color: var(--text); background: rgba(255,255,255,0.05); }
16520
+
16521
+ .settings-section {
16522
+ margin-bottom: 20px;
16523
+ }
16524
+
16525
+ .settings-section-title {
16526
+ font-family: var(--font-code);
16527
+ font-size: 10px;
16528
+ letter-spacing: 0.12em;
16529
+ text-transform: uppercase;
16530
+ color: var(--text-dim);
16531
+ margin-bottom: 10px;
16532
+ }
16533
+
16534
+ .ignore-item {
16535
+ display: flex;
16536
+ align-items: center;
16537
+ justify-content: space-between;
16538
+ padding: 5px 8px;
16539
+ border-radius: 3px;
16540
+ gap: 8px;
16541
+ }
16542
+
16543
+ .ignore-item:hover { background: rgba(255,255,255,0.04); }
16544
+
16545
+ .ignore-pattern {
16546
+ font-family: var(--font-code);
16547
+ font-size: 11px;
16548
+ color: var(--text);
16549
+ flex: 1;
16550
+ overflow: hidden;
16551
+ text-overflow: ellipsis;
16552
+ white-space: nowrap;
16553
+ }
16554
+
16555
+ .ignore-pattern.removed { color: var(--text-dim); text-decoration: line-through; }
16556
+ .ignore-pattern.custom { color: #6B8AFF; }
16557
+
16558
+ .ignore-btn {
16559
+ background: none;
16560
+ border: 1px solid var(--border);
16561
+ color: var(--text-dim);
16562
+ cursor: pointer;
16563
+ font-family: var(--font-code);
16564
+ font-size: 10px;
16565
+ padding: 2px 7px;
16566
+ flex-shrink: 0;
16567
+ transition: all 0.12s;
16568
+ }
16569
+
16570
+ .ignore-btn:hover { color: var(--text); border-color: rgba(255,255,255,0.3); }
16571
+ .ignore-btn.danger:hover { color: #F28B82; border-color: #F28B82; }
16572
+
16573
+ .ignore-add-row {
16574
+ display: flex;
16575
+ gap: 8px;
16576
+ margin-top: 10px;
16577
+ }
16578
+
16579
+ .ignore-input {
16580
+ flex: 1;
16581
+ background: var(--surface2);
16582
+ border: 1px solid var(--border);
16583
+ color: var(--text);
16584
+ font-family: var(--font-code);
16585
+ font-size: 12px;
16586
+ padding: 7px 10px;
16587
+ outline: none;
16588
+ }
16589
+
16590
+ .ignore-input:focus { border-color: rgba(255,255,255,0.3); }
16591
+ .ignore-input::placeholder { color: var(--text-dim); }
16592
+
16593
+ .ignore-add-btn {
16594
+ background: rgba(107,138,255,0.15);
16595
+ border: 1px solid rgba(107,138,255,0.3);
16596
+ color: #6B8AFF;
16597
+ cursor: pointer;
16598
+ font-family: var(--font-code);
16599
+ font-size: 11px;
16600
+ padding: 7px 14px;
16601
+ transition: all 0.12s;
16602
+ }
16603
+
16604
+ .ignore-add-btn:hover { background: rgba(107,138,255,0.25); }
16605
+
16606
+ .settings-note {
16607
+ font-family: var(--font-code);
16608
+ font-size: 10px;
16609
+ color: var(--text-dim);
16610
+ margin-top: 12px;
16611
+ line-height: 1.6;
16612
+ padding: 8px;
16613
+ border: 1px solid var(--border);
16614
+ background: rgba(255,255,255,0.02);
16615
+ }
16616
+
16617
+ .rescan-btn {
16618
+ width: 100%;
16619
+ margin-top: 16px;
16620
+ padding: 10px;
16621
+ background: rgba(107,138,255,0.1);
16622
+ border: 1px solid rgba(107,138,255,0.25);
16623
+ color: #6B8AFF;
16624
+ font-family: var(--font-code);
16625
+ font-size: 12px;
16626
+ cursor: pointer;
16627
+ transition: all 0.12s;
16628
+ letter-spacing: 0.05em;
16629
+ }
16630
+
16631
+ .rescan-btn:hover { background: rgba(107,138,255,0.2); }
16632
+
16341
16633
  #tooltip .tip-name { font-weight: 700; font-size: 13px; margin-bottom: 4px; color: var(--text); }
16342
16634
  #tooltip .tip-desc { color: var(--text-dim); font-size: 11px; line-height: 1.6; margin-top: 4px; }
16343
16635
  #tooltip .tip-tags { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 3px; }
@@ -16360,12 +16652,12 @@ var EMBEDDED_VIEWER = `<!DOCTYPE html>
16360
16652
 
16361
16653
  .link {
16362
16654
  fill: none;
16363
- stroke: rgba(255,255,255,0.4);
16364
- stroke-opacity: 0.7;
16655
+ stroke: #4a8fff;
16656
+ stroke-opacity: 0.6;
16365
16657
  }
16366
16658
 
16367
- .link.imports-edge { stroke: rgba(255,255,255,0.35); stroke-width: 1; }
16368
- .link.calls-edge { stroke: rgba(255,255,255,0.18); stroke-width: 1; stroke-dasharray: 4,4; }
16659
+ .link.imports-edge { stroke: #4a8fff; stroke-width: 1.5; stroke-opacity: 0.6; }
16660
+ .link.calls-edge { stroke: #f7916a; stroke-width: 1.5; stroke-dasharray: 5,4; stroke-opacity: 0.5; }
16369
16661
  .link.contains-edge { stroke: #333344; stroke-width: 0.5; stroke-dasharray: 3,3; }
16370
16662
 
16371
16663
  .link.highlighted { stroke-opacity: 1 !important; stroke-width: 3 !important; }
@@ -16391,41 +16683,136 @@ var EMBEDDED_VIEWER = `<!DOCTYPE html>
16391
16683
 
16392
16684
  <!-- Sidebar -->
16393
16685
  <div id="sidebar">
16394
- <div id="sidebar-header">
16686
+ <div id="sidebar-header" style="display:flex;align-items:center;justify-content:space-between;padding:0 12px;">
16395
16687
  <div class="logo">mycelium<span>.dev</span></div>
16688
+ <button id="settings-btn" title="Manage ignore list" style="background:none;border:1px solid var(--border);color:var(--text-dim);cursor:pointer;font-size:13px;padding:3px 8px;font-family:inherit;border-radius:3px;" onmouseenter="this.style.color='var(--text)'" onmouseleave="this.style.color='var(--text-dim)'">\u2699</button>
16396
16689
  </div>
16397
- <div id="search-wrap">
16398
- <span class="search-icon">\u2315</span>
16399
- <input id="search" type="text" placeholder="Search files, functions, tags\u2026" autocomplete="off">
16690
+
16691
+ <!-- Tab bar -->
16692
+ <div style="display:flex;border-bottom:1px solid var(--border);flex-shrink:0;">
16693
+ <button class="sidebar-tab active" id="tab-graph" onclick="switchTab('graph')">Graph</button>
16694
+ <button class="sidebar-tab" id="tab-history" onclick="switchTab('history')">History</button>
16695
+ <button class="sidebar-tab" id="tab-help" onclick="switchTab('help')">? Help</button>
16400
16696
  </div>
16401
- <div id="controls">
16402
- <div class="control-row">
16403
- <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>
16406
- <button class="toggle-btn" id="toggle-fns">Functions</button>
16697
+
16698
+ <!-- GRAPH TAB -->
16699
+ <div id="tab-content-graph" class="tab-content">
16700
+ <div id="search-wrap">
16701
+ <span class="search-icon">\u2315</span>
16702
+ <input id="search" type="text" placeholder="Search files, functions, tags\u2026" autocomplete="off">
16407
16703
  </div>
16408
- <div class="control-row">
16409
- <span>Zoom level:</span>
16410
- <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>
16704
+ <div id="controls">
16705
+ <div class="control-row">
16706
+ <span>Edges:</span>
16707
+ <button class="toggle-btn active" id="toggle-imports" data-tip="Show/hide import edges&#10;Blue solid lines&#10;File A imports from File B">Imports</button>
16708
+ <button class="toggle-btn active" id="toggle-calls" data-tip="Show/hide call edges&#10;Orange dashed lines&#10;Function A calls Function B&#10;(needs codebase-memory-mcp)">Calls</button>
16709
+ <button class="toggle-btn" id="toggle-functions" data-tip="Show function nodes&#10;Symbols within each file&#10;Circles attached to parent file">Functions</button>
16710
+ </div>
16711
+ <div class="control-row">
16712
+ <span>Zoom level:</span>
16713
+ <div class="zoom-level-btns">
16714
+ <button class="toggle-btn active" id="zoom-files" data-tip="Files only (default)&#10;One dot = one file&#10;Best for codebase structure">Files</button>
16715
+ <button class="toggle-btn" id="zoom-symbols" data-tip="Show symbols&#10;Functions and classes appear&#10;orbiting their parent file">Symbols</button>
16716
+ </div>
16413
16717
  </div>
16718
+ <div class="control-row">
16719
+ <button class="toggle-btn" id="toggle-clusters" data-tip="Directory grouping&#10;Dashed outlines show&#10;which folder each node is in">Directory clusters</button>
16720
+ </div>
16721
+ </div>
16722
+ <div id="node-info">
16723
+ <div class="node-info-empty" style="line-height:1.8">Click any dot to see<br>what it imports, exports,<br>and connects to</div>
16414
16724
  </div>
16415
- <div class="control-row">
16416
- <button class="toggle-btn" id="toggle-clusters">Directory clusters</button>
16725
+ <div id="legend">
16726
+ <div id="dir-legend"></div>
16727
+ <div class="legend-item">
16728
+ <div class="legend-line" style="background:#4a8fff;height:2px;"></div>
16729
+ <span style="font-size:10px;">Import (blue)</span>
16730
+ </div>
16731
+ <div class="legend-item" style="margin-top:4px;">
16732
+ <div style="width:24px;height:2px;background:repeating-linear-gradient(90deg,#f7916a 0,#f7916a 4px,transparent 4px,transparent 8px);flex-shrink:0;"></div>
16733
+ <span style="font-size:10px;">Call (orange dashed)</span>
16734
+ </div>
16417
16735
  </div>
16418
16736
  </div>
16419
- <div id="node-info">
16420
- <div class="node-info-empty" style="line-height:1.8">Click any dot to see<br>what it imports, exports,<br>and connects to</div>
16737
+
16738
+ <!-- HISTORY TAB -->
16739
+ <div id="tab-content-history" class="tab-content" style="display:none;overflow-y:auto;flex:1;">
16740
+ <div style="padding:12px;border-bottom:1px solid var(--border);">
16741
+ <div style="font-size:10px;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-dim);margin-bottom:4px;">Active task</div>
16742
+ <div id="history-task" style="font-size:13px;color:var(--text);">Loading\u2026</div>
16743
+ </div>
16744
+ <div id="history-list" style="padding:8px 0;"></div>
16745
+ <div style="padding:12px;border-top:1px solid var(--border);">
16746
+ <button onclick="loadHistory()" style="background:none;border:1px solid var(--border);color:var(--text-dim);cursor:pointer;font-family:inherit;font-size:11px;padding:5px 10px;width:100%;">\u21BB Refresh</button>
16747
+ </div>
16748
+ </div>
16749
+
16750
+ <!-- HELP TAB -->
16751
+ <div id="tab-content-help" class="tab-content" style="display:none;overflow-y:auto;flex:1;padding:14px;font-size:12px;line-height:1.7;">
16752
+ <div style="font-family:inherit;font-weight:700;font-size:13px;margin-bottom:12px;color:var(--text);">What Mycelium does</div>
16753
+
16754
+ <div style="color:var(--text-dim);margin-bottom:16px;">One command gives AI agents persistent, queryable memory of your entire codebase. Agents call /preflight before touching any file \u2014 instead of reading 40 files, they read 4.</div>
16755
+
16756
+ <div style="font-size:10px;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-dim);margin-bottom:8px;">Graph viewer</div>
16757
+ <div style="margin-bottom:14px;">
16758
+ <div style="margin-bottom:5px;"><span style="color:#4a8fff;">\u25CF</span> <strong>Blue solid lines</strong> \u2014 import edges. File A imports from File B.</div>
16759
+ <div style="margin-bottom:5px;"><span style="color:#f7916a;">\u25CF</span> <strong>Orange dashed lines</strong> \u2014 call edges. Function A calls Function B. Requires codebase-memory-mcp.</div>
16760
+ <div style="margin-bottom:5px;"><span style="color:#ffffff;opacity:0.5;">\u25CF</span> <strong>Node size</strong> \u2014 scales with file size (line count).</div>
16761
+ <div style="margin-bottom:5px;"><span style="color:#ffffff;opacity:0.5;">\u25CF</span> <strong>Node color</strong> \u2014 one color per directory.</div>
16762
+ <div style="margin-bottom:5px;"><strong>Click a node</strong> \u2014 see AI description, imports, callers.</div>
16763
+ <div style="margin-bottom:5px;"><strong>Scroll</strong> \u2014 zoom. <strong>Drag</strong> \u2014 pan. <strong>\u22A1</strong> \u2014 fit to screen.</div>
16764
+ </div>
16765
+
16766
+ <div style="font-size:10px;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-dim);margin-bottom:8px;">Agent API endpoints</div>
16767
+ <div style="margin-bottom:14px;">
16768
+ <div style="margin-bottom:6px;"><code style="color:#6af7c4;">/preflight?task=</code><br><span style="color:var(--text-dim);">Natural language task \u2192 exact files to read. Saves 7k+ tokens per agent task.</span></div>
16769
+ <div style="margin-bottom:6px;"><code style="color:#6af7c4;">/search?q=</code><br><span style="color:var(--text-dim);">Semantic search across all file descriptions and tags.</span></div>
16770
+ <div style="margin-bottom:6px;"><code style="color:#6af7c4;">/dependencies?file=</code><br><span style="color:var(--text-dim);">What a file imports and what imports it.</span></div>
16771
+ <div style="margin-bottom:6px;"><code style="color:#6af7c4;">/xref?file=&fn=</code><br><span style="color:var(--text-dim);">Every caller and callee of a function. Know the blast radius before changing anything.</span></div>
16772
+ <div style="margin-bottom:6px;"><code style="color:#6af7c4;">/history</code><br><span style="color:var(--text-dim);">Every file save by task and agent identity.</span></div>
16773
+ <div style="margin-bottom:6px;"><code style="color:#6af7c4;">/graph</code><br><span style="color:var(--text-dim);">Full graph \u2014 all nodes and edges.</span></div>
16774
+ </div>
16775
+
16776
+ <div style="font-size:10px;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-dim);margin-bottom:8px;">CLI commands</div>
16777
+ <div style="margin-bottom:14px;">
16778
+ <div style="margin-bottom:4px;"><code style="color:#6af7c4;">mycelium init</code> \u2014 scan, summarize, serve, watch</div>
16779
+ <div style="margin-bottom:4px;"><code style="color:#6af7c4;">mycelium serve</code> \u2014 start server from existing graph</div>
16780
+ <div style="margin-bottom:4px;"><code style="color:#6af7c4;">mycelium scan</code> \u2014 re-scan without starting server</div>
16781
+ <div style="margin-bottom:4px;"><code style="color:#6af7c4;">mycelium search</code> \u2014 search from terminal</div>
16782
+ <div style="margin-bottom:4px;"><code style="color:#6af7c4;">mycelium status</code> \u2014 graph stats</div>
16783
+ <div style="margin-bottom:4px;"><code style="color:#6af7c4;">mycelium key</code> \u2014 save Anthropic/OpenAI keys</div>
16784
+ <div style="margin-bottom:4px;"><code style="color:#6af7c4;">mycelium embed</code> \u2014 generate semantic embeddings</div>
16785
+ <div style="margin-bottom:4px;"><code style="color:#6af7c4;">mycelium history</code> \u2014 show change log</div>
16786
+ <div style="margin-bottom:4px;"><code style="color:#6af7c4;">mycelium task</code> \u2014 set active task for history</div>
16787
+ <div style="margin-bottom:4px;"><code style="color:#6af7c4;">mycelium ignore</code> \u2014 manage scan ignore list</div>
16788
+ </div>
16789
+
16790
+ <div style="font-size:10px;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-dim);margin-bottom:8px;">CLAUDE.md</div>
16791
+ <div style="color:var(--text-dim);margin-bottom:16px;">Mycelium writes a 5-step checklist into CLAUDE.md automatically. Claude Code reads it and calls /preflight before touching any file.</div>
16792
+
16793
+ <div style="border-top:1px solid var(--border);padding-top:12px;">
16794
+ <a href="https://github.com/KopikoCappu/Mycelium" target="_blank" style="color:#6B8AFF;text-decoration:none;font-size:11px;">GitHub \u2197</a>
16795
+ &nbsp;&nbsp;
16796
+ <a href="https://www.npmjs.com/package/@kopikocappu/mycelium" target="_blank" style="color:#6B8AFF;text-decoration:none;font-size:11px;">npm \u2197</a>
16797
+ </div>
16421
16798
  </div>
16422
- <div id="legend">
16423
- <div id="dir-legend"></div>
16424
- <div class="legend-item">
16425
- <div class="legend-line" style="background:rgba(255,255,255,0.35)"></div>Import
16799
+
16800
+ <!-- SETTINGS PANEL (slides over everything) -->
16801
+ <div id="settings-panel" style="display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:var(--surface);z-index:50;overflow-y:auto;padding:16px;flex-direction:column;">
16802
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid var(--border);">
16803
+ <span style="font-weight:700;font-size:13px;">\u2699 Ignore List</span>
16804
+ <button id="settings-close" style="background:none;border:1px solid var(--border);color:var(--text-dim);cursor:pointer;font-size:12px;padding:3px 10px;font-family:inherit;">\u2715</button>
16805
+ </div>
16806
+ <div style="font-size:10px;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-dim);margin-bottom:8px;">Default patterns</div>
16807
+ <div id="ignore-default-list"></div>
16808
+ <div style="font-size:10px;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-dim);margin-top:14px;margin-bottom:8px;">Custom patterns</div>
16809
+ <div id="ignore-custom-list"><div style="font-size:11px;color:var(--text-dim);padding:4px 0;">None added yet</div></div>
16810
+ <div style="display:flex;gap:8px;margin-top:10px;">
16811
+ <input id="ignore-input" placeholder="e.g. android/** or **/*.test.ts" style="flex:1;background:var(--surface2);border:1px solid var(--border);color:var(--text);font-family:inherit;font-size:12px;padding:7px 10px;outline:none;" />
16812
+ <button id="ignore-add-btn" style="background:rgba(107,138,255,0.15);border:1px solid rgba(107,138,255,0.3);color:#6B8AFF;cursor:pointer;font-family:inherit;font-size:11px;padding:7px 14px;">+ Add</button>
16426
16813
  </div>
16427
- <div class="legend-item">
16428
- <div class="legend-line" style="background:rgba(255,255,255,0.25)"></div>Call
16814
+ <div style="font-size:10px;color:var(--text-dim);margin-top:12px;padding:8px;border:1px solid var(--border);line-height:1.6;">
16815
+ Changes apply on next scan.<br>Run <code>mycelium scan</code> or re-run <code>mycelium init</code>.
16429
16816
  </div>
16430
16817
  </div>
16431
16818
  </div>
@@ -16562,7 +16949,12 @@ function buildVis() {
16562
16949
  // Filter nodes/links based on current view settings
16563
16950
  visNodes = allNodes.filter(n => {
16564
16951
  if (n.kind === 'file') return true;
16565
- if ((n.kind === 'function' || n.kind === 'class') && zoomLevel === 'symbols') return showFunctions;
16952
+ if (zoomLevel === 'symbols' && showFunctions) {
16953
+ if (n.kind === 'function' || n.kind === 'class' || n.kind === 'type' || n.kind === 'interface') {
16954
+ // Only show named, non-anonymous symbols
16955
+ return n.name && n.name.length > 1 && !n.name.includes('anonymous') && !n.name.startsWith('_');
16956
+ }
16957
+ }
16566
16958
  return false;
16567
16959
  });
16568
16960
 
@@ -16571,7 +16963,9 @@ function buildVis() {
16571
16963
  if (!nodeIds.has(l.from) || !nodeIds.has(l.to)) return false;
16572
16964
  if (l.kind === 'imports' && !showImports) return false;
16573
16965
  if (l.kind === 'calls' && !showCalls) return false;
16574
- if (l.kind === 'contains') return zoomLevel === 'symbols';
16966
+ // Contains edges anchor symbols to their parent file
16967
+ // Critical: without these, symbols have no edges and scatter everywhere
16968
+ if (l.kind === 'contains') return zoomLevel === 'symbols' && showFunctions;
16575
16969
  return true;
16576
16970
  });
16577
16971
 
@@ -16620,7 +17014,11 @@ function buildVis() {
16620
17014
  .id(d => d.id)
16621
17015
  .distance(d => d.kind === 'contains' ? 40 : d.kind === 'calls' ? 80 : 100)
16622
17016
  .strength(d => d.kind === 'contains' ? 0.8 : 0.3))
16623
- .force('charge', d3.forceManyBody().strength(d => d.kind === 'file' ? -250 : -80))
17017
+ .force('charge', d3.forceManyBody()
17018
+ // Symbols get very weak repulsion so they cluster near parent files via contains edges
17019
+ // Without this, 600+ unconnected symbols explode across the entire screen
17020
+ .strength(d => d.kind === 'file' ? -180 : -8)
17021
+ .distanceMax(300))
16624
17022
  .force('center', d3.forceCenter(W / 2, H / 2))
16625
17023
  .force('collision', d3.forceCollide().radius(d => nodeRadius(d) + 8))
16626
17024
  .force('cluster', clusterForce(dirGroups, 0.08))
@@ -17016,6 +17414,12 @@ document.getElementById('toggle-clusters').addEventListener('click', function()
17016
17414
  });
17017
17415
 
17018
17416
  document.getElementById('zoom-files').addEventListener('click', function() {
17417
+ if (zoomLevel === 'symbols') {
17418
+ // Reset positions \u2014 symbols simulation spreads files far apart
17419
+ // Without this, files stay in their spread-out positions after toggling back
17420
+ allNodes.forEach(n => { n.x = undefined; n.y = undefined; n.vx = 0; n.vy = 0; });
17421
+ _initialFitDone = false;
17422
+ }
17019
17423
  zoomLevel = 'files';
17020
17424
  this.classList.add('active');
17021
17425
  document.getElementById('zoom-symbols').classList.remove('active');
@@ -17190,6 +17594,202 @@ function hideLoading() {
17190
17594
  initMinimap();
17191
17595
  loadGraph();
17192
17596
 
17597
+ // \u2500\u2500 Tab switching \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
17598
+ function switchTab(name) {
17599
+ ['graph','history','help'].forEach(t => {
17600
+ document.getElementById('tab-content-' + t).style.display = t === name ? 'flex' : 'none';
17601
+ const btn = document.getElementById('tab-' + t);
17602
+ if (btn) btn.classList.toggle('active', t === name);
17603
+ });
17604
+ if (name === 'history') loadHistory();
17605
+ }
17606
+
17607
+ // \u2500\u2500 History tab \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
17608
+ async function loadHistory() {
17609
+ const listEl = document.getElementById('history-list');
17610
+ const taskEl = document.getElementById('history-task');
17611
+ listEl.innerHTML = '<div style="padding:12px;font-size:11px;color:var(--text-dim)">Loading\u2026</div>';
17612
+ try {
17613
+ const res = await fetch(\`\${API_BASE}/history\`);
17614
+ const data = await res.json();
17615
+ taskEl.textContent = data.activeTask || 'General development';
17616
+ const changes = data.recentChanges || [];
17617
+ if (changes.length === 0) {
17618
+ listEl.innerHTML = '<div style="padding:12px;font-size:11px;color:var(--text-dim)">No changes yet.<br>Mycelium logs every file save during active sessions.</div>';
17619
+ return;
17620
+ }
17621
+ listEl.innerHTML = changes.slice(0, 50).map(c => {
17622
+ const time = new Date(c.timestamp || c.at || Date.now()).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
17623
+ const file = (c.file || c.path || '').split(/[\\/]/).pop();
17624
+ return \`<div class="history-item">
17625
+ <div class="history-file">\${file || c.file || '\u2014'}</div>
17626
+ <div class="history-meta">
17627
+ <span>\${time}</span>
17628
+ \${c.task ? '<span class="history-task">' + c.task + '</span>' : ''}
17629
+ \${c.agent ? '<span class="history-agent">' + c.agent + '</span>' : ''}
17630
+ </div>
17631
+ </div>\`;
17632
+ }).join('');
17633
+ } catch(e) {
17634
+ listEl.innerHTML = '<div style="padding:12px;font-size:11px;color:var(--text-dim)">Could not load history.<br>Is the server running?</div>';
17635
+ }
17636
+ }
17637
+
17638
+ // \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
17639
+ document.getElementById('settings-btn').addEventListener('click', () => {
17640
+ document.getElementById('settings-panel').style.display = 'flex';
17641
+ loadIgnoreConfig();
17642
+ });
17643
+ document.getElementById('settings-close').addEventListener('click', () => {
17644
+ document.getElementById('settings-panel').style.display = 'none';
17645
+ });
17646
+
17647
+ async function loadIgnoreConfig() {
17648
+ try {
17649
+ const res = await fetch(\`\${API_BASE}/config\`);
17650
+ const data = await res.json();
17651
+ renderIgnoreList(data);
17652
+ } catch(e) {
17653
+ document.getElementById('ignore-default-list').innerHTML =
17654
+ '<div style="font-size:11px;color:var(--text-dim);padding:4px 0">Server not reachable</div>';
17655
+ }
17656
+ }
17657
+
17658
+ function renderIgnoreList(data) {
17659
+ const { defaultIgnore = [], userIgnore = [], userUnignore = [] } = data;
17660
+ document.getElementById('ignore-default-list').innerHTML = defaultIgnore.map(p => {
17661
+ const removed = userUnignore.includes(p);
17662
+ return \`<div class="ignore-item">
17663
+ <span class="ignore-pattern \${removed ? 'removed' : ''}">\${p}</span>
17664
+ \${removed
17665
+ ? \`<button class="ignore-btn" onclick="ignoreAction('restore','\${p}')">restore</button>\`
17666
+ : \`<button class="ignore-btn danger" onclick="ignoreAction('remove','\${p}')">remove</button>\`}
17667
+ </div>\`;
17668
+ }).join('');
17669
+ const customEl = document.getElementById('ignore-custom-list');
17670
+ customEl.innerHTML = userIgnore.length === 0
17671
+ ? '<div style="font-size:11px;color:var(--text-dim);padding:4px 0;">None added yet</div>'
17672
+ : userIgnore.map(p => \`<div class="ignore-item">
17673
+ <span class="ignore-pattern custom">\${p}</span>
17674
+ <button class="ignore-btn danger" onclick="ignoreAction('remove','\${p}')">remove</button>
17675
+ </div>\`).join('');
17676
+ }
17677
+
17678
+ async function ignoreAction(action, pattern) {
17679
+ try {
17680
+ await fetch(\`\${API_BASE}/config\`, {
17681
+ method: 'POST',
17682
+ headers: {'Content-Type':'application/json'},
17683
+ body: JSON.stringify({action, pattern}),
17684
+ });
17685
+ await loadIgnoreConfig();
17686
+ } catch(e) { alert('Could not update config.'); }
17687
+ }
17688
+
17689
+ document.getElementById('ignore-add-btn').addEventListener('click', async () => {
17690
+ const input = document.getElementById('ignore-input');
17691
+ const pattern = input.value.trim();
17692
+ if (!pattern) return;
17693
+ await ignoreAction('add', pattern);
17694
+ input.value = '';
17695
+ });
17696
+ document.getElementById('ignore-input').addEventListener('keydown', e => {
17697
+ if (e.key === 'Enter') document.getElementById('ignore-add-btn').click();
17698
+ });
17699
+
17700
+ // \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
17701
+ const settingsBtn = document.getElementById('settings-btn');
17702
+ const settingsPanel = document.getElementById('settings-panel');
17703
+ const settingsClose = document.getElementById('settings-close');
17704
+
17705
+ settingsBtn.addEventListener('click', () => {
17706
+ settingsPanel.classList.add('open');
17707
+ loadIgnoreConfig();
17708
+ });
17709
+
17710
+ settingsClose.addEventListener('click', () => {
17711
+ settingsPanel.classList.remove('open');
17712
+ });
17713
+
17714
+ // Hover effect for gear button
17715
+ settingsBtn.addEventListener('mouseenter', () => {
17716
+ settingsBtn.style.color = 'var(--text)';
17717
+ settingsBtn.style.borderColor = 'rgba(255,255,255,0.3)';
17718
+ });
17719
+ settingsBtn.addEventListener('mouseleave', () => {
17720
+ settingsBtn.style.color = 'var(--text-dim)';
17721
+ settingsBtn.style.borderColor = 'var(--border)';
17722
+ });
17723
+
17724
+ async function loadIgnoreConfig() {
17725
+ try {
17726
+ const res = await fetch(\`\${API_BASE}/config\`);
17727
+ const data = await res.json();
17728
+ renderIgnoreList(data);
17729
+ } catch (e) {
17730
+ document.getElementById('ignore-default-list').innerHTML =
17731
+ '<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>';
17732
+ }
17733
+ }
17734
+
17735
+ function renderIgnoreList(data) {
17736
+ const { defaultIgnore, userIgnore, userUnignore } = data;
17737
+
17738
+ // Default patterns
17739
+ const defaultEl = document.getElementById('ignore-default-list');
17740
+ defaultEl.innerHTML = defaultIgnore.map(pattern => {
17741
+ const isRemoved = userUnignore.includes(pattern);
17742
+ return \`<div class="ignore-item">
17743
+ <span class="ignore-pattern \${isRemoved ? 'removed' : ''}">\${pattern}</span>
17744
+ \${isRemoved
17745
+ ? \`<button class="ignore-btn" onclick="restorePattern('\${pattern}')">restore</button>\`
17746
+ : \`<button class="ignore-btn danger" onclick="removePattern('\${pattern}')">remove</button>\`
17747
+ }
17748
+ </div>\`;
17749
+ }).join('');
17750
+
17751
+ // Custom patterns
17752
+ const customEl = document.getElementById('ignore-custom-list');
17753
+ if (userIgnore.length === 0) {
17754
+ customEl.innerHTML = '<div style="font-family:var(--font-code);font-size:11px;color:var(--text-dim);padding:4px 0;">None added yet</div>';
17755
+ } else {
17756
+ customEl.innerHTML = userIgnore.map(pattern =>
17757
+ \`<div class="ignore-item">
17758
+ <span class="ignore-pattern custom">\${pattern}</span>
17759
+ <button class="ignore-btn danger" onclick="removePattern('\${pattern}')">remove</button>
17760
+ </div>\`
17761
+ ).join('');
17762
+ }
17763
+ }
17764
+
17765
+ async function postIgnoreAction(action, pattern) {
17766
+ try {
17767
+ await fetch(\`\${API_BASE}/config\`, {
17768
+ method: 'POST',
17769
+ headers: { 'Content-Type': 'application/json' },
17770
+ body: JSON.stringify({ action, pattern }),
17771
+ });
17772
+ await loadIgnoreConfig();
17773
+ } catch (e) {
17774
+ alert('Could not update config. Is the server running?');
17775
+ }
17776
+ }
17777
+
17778
+ function removePattern(pattern) { postIgnoreAction('remove', pattern); }
17779
+ function restorePattern(pattern) { postIgnoreAction('restore', pattern); }
17780
+
17781
+ document.getElementById('ignore-add-btn').addEventListener('click', async () => {
17782
+ const input = document.getElementById('ignore-input');
17783
+ const pattern = input.value.trim();
17784
+ if (!pattern) return;
17785
+ await postIgnoreAction('add', pattern);
17786
+ input.value = '';
17787
+ });
17788
+
17789
+ document.getElementById('ignore-input').addEventListener('keydown', e => {
17790
+ if (e.key === 'Enter') document.getElementById('ignore-add-btn').click();
17791
+ });
17792
+
17193
17793
  function updateDirLegend() {
17194
17794
  const el = document.getElementById('dir-legend');
17195
17795
  if (!el) return;
@@ -17220,8 +17820,62 @@ var RateLimiter = class {
17220
17820
  return true;
17221
17821
  }
17222
17822
  };
17823
+ var DEFAULT_IGNORE_LIST = [
17824
+ "node_modules/**",
17825
+ "vendor/**",
17826
+ ".pnp/**",
17827
+ "dist/**",
17828
+ "build/**",
17829
+ "out/**",
17830
+ ".next/**",
17831
+ ".nuxt/**",
17832
+ ".output/**",
17833
+ "coverage/**",
17834
+ "android/**",
17835
+ "ios/**",
17836
+ ".expo/**",
17837
+ "Pods/**",
17838
+ "assets/**",
17839
+ "public/**",
17840
+ "static/**",
17841
+ "**/*.png",
17842
+ "**/*.jpg",
17843
+ "**/*.jpeg",
17844
+ "**/*.gif",
17845
+ "**/*.svg",
17846
+ "**/*.ico",
17847
+ "**/*.woff",
17848
+ "**/*.woff2",
17849
+ "**/*.ttf",
17850
+ "**/*.mp4",
17851
+ "**/*.mp3",
17852
+ ".mycelium/**",
17853
+ ".graphmem/**",
17854
+ ".cache/**",
17855
+ ".turbo/**",
17856
+ "generated/**",
17857
+ "__generated__/**",
17858
+ "**/*.generated.ts",
17859
+ "**/*.generated.js",
17860
+ "**/*.d.ts",
17861
+ "**/*.min.js",
17862
+ "**/*.min.css",
17863
+ "**/*.map",
17864
+ "**/*.test.ts",
17865
+ "**/*.test.tsx",
17866
+ "**/*.spec.ts",
17867
+ "**/*.spec.tsx",
17868
+ "**/__tests__/**",
17869
+ "**/__mocks__/**",
17870
+ "**/*.stories.ts",
17871
+ "**/*.stories.tsx",
17872
+ "docs/**",
17873
+ "fixtures/**",
17874
+ "e2e/**"
17875
+ ];
17223
17876
  var McpServer = class {
17224
17877
  constructor(store, changeLogger, config) {
17878
+ this.projectRoot = null;
17225
17879
  this.rateLimiter = new RateLimiter();
17226
17880
  this.server = null;
17227
17881
  this.store = store;
@@ -17299,6 +17953,10 @@ var McpServer = class {
17299
17953
  if (pathname === "/xref") {
17300
17954
  return this.handleXref(url, res);
17301
17955
  }
17956
+ if (pathname === "/config") {
17957
+ if (req.method === "GET") return this.handleConfigGet(res);
17958
+ if (req.method === "POST") return this.handleConfigPost(req, res);
17959
+ }
17302
17960
  if (pathname === "/preflight") {
17303
17961
  return this.handlePreflight(url, res);
17304
17962
  }
@@ -17503,6 +18161,75 @@ var McpServer = class {
17503
18161
  this.handleKeywordPreflight(task, teamName, lens, res);
17504
18162
  }
17505
18163
  }
18164
+ handleConfigGet(res) {
18165
+ const configPath = path4.join(this.projectRoot ?? process.cwd(), ".mycelium", "config.json");
18166
+ let saved = {};
18167
+ try {
18168
+ if (fs3.existsSync(configPath)) saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
18169
+ } catch {
18170
+ }
18171
+ const userIgnore = saved?.parser?.userIgnore ?? [];
18172
+ const userUnignore = saved?.parser?.userUnignore ?? [];
18173
+ const defaultIgnore = DEFAULT_IGNORE_LIST;
18174
+ const activeIgnore = [
18175
+ ...defaultIgnore.filter((p) => !userUnignore.includes(p)),
18176
+ ...userIgnore
18177
+ ];
18178
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
18179
+ res.end(JSON.stringify({
18180
+ defaultIgnore,
18181
+ userIgnore,
18182
+ userUnignore,
18183
+ activeIgnore,
18184
+ totalActive: activeIgnore.length
18185
+ }));
18186
+ }
18187
+ handleConfigPost(req, res) {
18188
+ let body = "";
18189
+ req.on("data", (chunk) => {
18190
+ body += chunk.toString();
18191
+ });
18192
+ req.on("end", () => {
18193
+ try {
18194
+ const { action, pattern } = JSON.parse(body);
18195
+ const configPath = path4.join(this.projectRoot ?? process.cwd(), ".mycelium", "config.json");
18196
+ let saved = {};
18197
+ try {
18198
+ if (fs3.existsSync(configPath)) saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
18199
+ } catch {
18200
+ }
18201
+ if (!saved.parser) saved.parser = {};
18202
+ let userIgnore = saved.parser.userIgnore ?? [];
18203
+ let userUnignore = saved.parser.userUnignore ?? [];
18204
+ if (action === "add" && pattern) {
18205
+ if (!userIgnore.includes(pattern)) {
18206
+ userIgnore = [...userIgnore, pattern];
18207
+ userUnignore = userUnignore.filter((p) => p !== pattern);
18208
+ }
18209
+ } else if (action === "remove" && pattern) {
18210
+ const isDefault = DEFAULT_IGNORE_LIST.includes(pattern);
18211
+ if (isDefault) {
18212
+ if (!userUnignore.includes(pattern)) userUnignore = [...userUnignore, pattern];
18213
+ } else {
18214
+ userIgnore = userIgnore.filter((p) => p !== pattern);
18215
+ }
18216
+ } else if (action === "restore" && pattern) {
18217
+ userUnignore = userUnignore.filter((p) => p !== pattern);
18218
+ } else if (action === "reset") {
18219
+ userIgnore = [];
18220
+ userUnignore = [];
18221
+ }
18222
+ saved.parser.userIgnore = userIgnore;
18223
+ saved.parser.userUnignore = userUnignore;
18224
+ fs3.writeFileSync(configPath, JSON.stringify(saved, null, 2));
18225
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
18226
+ res.end(JSON.stringify({ ok: true, userIgnore, userUnignore }));
18227
+ } catch (e) {
18228
+ res.writeHead(400);
18229
+ res.end(JSON.stringify({ error: e.message }));
18230
+ }
18231
+ });
18232
+ }
17506
18233
  async handleXref(url, res) {
17507
18234
  const file = (url.searchParams.get("file") || "").slice(0, 500);
17508
18235
  const fn = (url.searchParams.get("fn") || "").slice(0, 500);
@@ -18073,6 +18800,68 @@ ${GRAPHMEM_SECTION_END}`;
18073
18800
  }
18074
18801
 
18075
18802
  // src/graph/schema.ts
18803
+ var DEFAULT_IGNORE = [
18804
+ // Package managers & deps
18805
+ "node_modules/**",
18806
+ "vendor/**",
18807
+ ".pnp/**",
18808
+ // Build output
18809
+ "dist/**",
18810
+ "build/**",
18811
+ "out/**",
18812
+ ".next/**",
18813
+ ".nuxt/**",
18814
+ ".output/**",
18815
+ "coverage/**",
18816
+ // Native mobile (usually auto-generated)
18817
+ "android/**",
18818
+ "ios/**",
18819
+ ".expo/**",
18820
+ "Pods/**",
18821
+ // Assets & static
18822
+ "assets/**",
18823
+ "public/**",
18824
+ "static/**",
18825
+ "**/*.png",
18826
+ "**/*.jpg",
18827
+ "**/*.jpeg",
18828
+ "**/*.gif",
18829
+ "**/*.svg",
18830
+ "**/*.ico",
18831
+ "**/*.woff",
18832
+ "**/*.woff2",
18833
+ "**/*.ttf",
18834
+ "**/*.mp4",
18835
+ "**/*.mp3",
18836
+ // Generated & cache
18837
+ ".mycelium/**",
18838
+ ".graphmem/**",
18839
+ ".cache/**",
18840
+ ".turbo/**",
18841
+ "generated/**",
18842
+ "__generated__/**",
18843
+ "**/*.generated.ts",
18844
+ "**/*.generated.js",
18845
+ // Config & declaration files
18846
+ "**/*.d.ts",
18847
+ "**/*.min.js",
18848
+ "**/*.min.css",
18849
+ "**/*.map",
18850
+ // Tests (optional — users may want these)
18851
+ "**/*.test.ts",
18852
+ "**/*.test.tsx",
18853
+ "**/*.spec.ts",
18854
+ "**/*.spec.tsx",
18855
+ "**/__tests__/**",
18856
+ "**/__mocks__/**",
18857
+ // Stories
18858
+ "**/*.stories.ts",
18859
+ "**/*.stories.tsx",
18860
+ // Docs & fixtures
18861
+ "docs/**",
18862
+ "fixtures/**",
18863
+ "e2e/**"
18864
+ ];
18076
18865
  var DEFAULT_CONFIG = {
18077
18866
  teams: {
18078
18867
  core: { name: "core", includeTags: [], includeAll: true }
@@ -18082,8 +18871,9 @@ var DEFAULT_CONFIG = {
18082
18871
  batchSize: 10
18083
18872
  },
18084
18873
  parser: {
18085
- include: ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
18086
- exclude: ["node_modules/**", "dist/**", ".graphmem/**", "**/*.test.*", "**/*.spec.*"]
18874
+ include: [],
18875
+ // empty = auto-detect from tsconfig or directory structure
18876
+ exclude: DEFAULT_IGNORE
18087
18877
  },
18088
18878
  mcp: {
18089
18879
  port: 47821,
@@ -18123,13 +18913,41 @@ function resolveRoot(input) {
18123
18913
  }
18124
18914
  function loadProjectConfig(root) {
18125
18915
  const cfgPath = path8.join(root, ".mycelium", "config.json");
18916
+ let saved = {};
18917
+ if (fs8.existsSync(cfgPath)) {
18918
+ try {
18919
+ saved = JSON.parse(fs8.readFileSync(cfgPath, "utf8"));
18920
+ } catch {
18921
+ }
18922
+ }
18923
+ const merged = { ...DEFAULT_CONFIG, ...saved };
18924
+ const userIgnore = saved.parser?.userIgnore ?? [];
18925
+ const userUnignore = saved.parser?.userUnignore ?? [];
18926
+ merged.parser = {
18927
+ ...DEFAULT_CONFIG.parser,
18928
+ ...saved.parser ?? {},
18929
+ exclude: [
18930
+ ...DEFAULT_IGNORE.filter((p) => !userUnignore.includes(p)),
18931
+ ...userIgnore
18932
+ ],
18933
+ userIgnore,
18934
+ userUnignore
18935
+ };
18936
+ return merged;
18937
+ }
18938
+ function saveProjectConfig(root, update) {
18939
+ const cfgPath = path8.join(root, ".mycelium", "config.json");
18940
+ let existing = {};
18126
18941
  if (fs8.existsSync(cfgPath)) {
18127
18942
  try {
18128
- return { ...DEFAULT_CONFIG, ...JSON.parse(fs8.readFileSync(cfgPath, "utf8")) };
18943
+ existing = JSON.parse(fs8.readFileSync(cfgPath, "utf8"));
18129
18944
  } catch {
18130
18945
  }
18131
18946
  }
18132
- return DEFAULT_CONFIG;
18947
+ if (!existing.parser) existing.parser = {};
18948
+ if (update.userIgnore !== void 0) existing.parser.userIgnore = update.userIgnore;
18949
+ if (update.userUnignore !== void 0) existing.parser.userUnignore = update.userUnignore;
18950
+ fs8.writeFileSync(cfgPath, JSON.stringify(existing, null, 2));
18133
18951
  }
18134
18952
  function initProjectDir(root) {
18135
18953
  const dir = path8.join(root, ".mycelium");
@@ -18307,7 +19125,7 @@ program2.command("init [path]").description("Scan codebase, build graph, start s
18307
19125
  ok(".mcp.json written");
18308
19126
  if (opts.serve) {
18309
19127
  const server = new McpServer(store, logger, config);
18310
- server.start();
19128
+ server.start(root);
18311
19129
  log("");
18312
19130
  log(` ${C.bold}Graph view${C.reset} ${C.cyan}http://localhost:${config.mcp.port}/ui${C.reset}`);
18313
19131
  log(` ${C.bold}MCP server${C.reset} ${C.gray}http://localhost:${config.mcp.port}${C.reset}`);
@@ -18338,7 +19156,7 @@ program2.command("serve [path]").description("Start MCP server and file watcher
18338
19156
  }
18339
19157
  ok(`Loaded graph: ${stats.fileCount} files, ${stats.edgeCount} edges`);
18340
19158
  const server = new McpServer(store, logger, config);
18341
- server.start();
19159
+ server.start(root);
18342
19160
  log("");
18343
19161
  log(` ${C.bold}Graph view${C.reset} ${C.cyan}http://localhost:${config.mcp.port}/ui${C.reset}`);
18344
19162
  log(` ${C.bold}MCP server${C.reset} ${C.gray}http://localhost:${config.mcp.port}${C.reset}`);
@@ -18529,6 +19347,72 @@ program2.command("embed [path]").description("Generate semantic embeddings for a
18529
19347
  if (pruned > 0) ok(`${pruned} stale embeddings removed`);
18530
19348
  ok(`Semantic search now active on /search and /preflight`);
18531
19349
  });
19350
+ 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) => {
19351
+ header();
19352
+ const root = resolveRoot(targetPath);
19353
+ const config = loadProjectConfig(root);
19354
+ const userIgnore = config.parser.userIgnore ?? [];
19355
+ const userUnignore = config.parser.userUnignore ?? [];
19356
+ if (opts.reset) {
19357
+ saveProjectConfig(root, { userIgnore: [], userUnignore: [] });
19358
+ ok("Ignore list reset to defaults");
19359
+ return;
19360
+ }
19361
+ if (opts.add) {
19362
+ const pattern = opts.add.trim();
19363
+ if (userIgnore.includes(pattern)) {
19364
+ warn(`Already ignored: ${pattern}`);
19365
+ return;
19366
+ }
19367
+ const newUnignore = userUnignore.filter((p) => p !== pattern);
19368
+ saveProjectConfig(root, { userIgnore: [...userIgnore, pattern], userUnignore: newUnignore });
19369
+ ok(`Added to ignore list: ${C.gray}${pattern}${C.reset}`);
19370
+ ok("Run mycelium scan to apply changes");
19371
+ return;
19372
+ }
19373
+ if (opts.remove) {
19374
+ const pattern = opts.remove.trim();
19375
+ const isDefault = DEFAULT_IGNORE.includes(pattern);
19376
+ const isCustom = userIgnore.includes(pattern);
19377
+ if (!isDefault && !isCustom) {
19378
+ warn(`Pattern not found in ignore list: ${pattern}`);
19379
+ return;
19380
+ }
19381
+ if (isDefault) {
19382
+ if (!userUnignore.includes(pattern)) {
19383
+ saveProjectConfig(root, { userIgnore, userUnignore: [...userUnignore, pattern] });
19384
+ }
19385
+ } else {
19386
+ saveProjectConfig(root, { userIgnore: userIgnore.filter((p) => p !== pattern), userUnignore });
19387
+ }
19388
+ ok(`Removed from ignore list: ${C.gray}${pattern}${C.reset}`);
19389
+ ok("Run mycelium scan to apply changes");
19390
+ return;
19391
+ }
19392
+ const activeIgnore = [
19393
+ ...DEFAULT_IGNORE.filter((p) => !userUnignore.includes(p)),
19394
+ ...userIgnore
19395
+ ];
19396
+ log(`
19397
+ ${C.bold}Default patterns${C.reset} ${C.gray}(${DEFAULT_IGNORE.length - userUnignore.length} active)${C.reset}`);
19398
+ for (const p of DEFAULT_IGNORE) {
19399
+ const removed = userUnignore.includes(p);
19400
+ log(` ${removed ? C.gray + "\u2717 " : " "}${p}${removed ? " (removed)" : ""}${C.reset}`);
19401
+ }
19402
+ if (userIgnore.length > 0) {
19403
+ log(`
19404
+ ${C.bold}Custom patterns${C.reset} ${C.gray}(${userIgnore.length})${C.reset}`);
19405
+ for (const p of userIgnore) {
19406
+ log(` ${C.cyan}+${C.reset} ${p}`);
19407
+ }
19408
+ }
19409
+ log(`
19410
+ ${C.gray}Total active: ${activeIgnore.length} patterns${C.reset}`);
19411
+ log(` ${C.gray}Add: mycelium ignore --add "android/**"${C.reset}`);
19412
+ log(` ${C.gray}Remove: mycelium ignore --remove "**/*.test.ts"${C.reset}`);
19413
+ log(` ${C.gray}Reset: mycelium ignore --reset${C.reset}
19414
+ `);
19415
+ });
18532
19416
  program2.parse(process.argv);
18533
19417
  /*! Bundled license information:
18534
19418
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kopikocappu/mycelium",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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"