@andespindola/brainlink 0.1.0-beta.86 → 0.1.0-beta.88

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
@@ -84,7 +84,7 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
84
84
  - Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
85
85
  - Canvas graph rendering uses the same batched node and edge pipeline for every graph size, reducing per-frame draw calls while keeping selected and hovered items highlighted.
86
86
  - WebGL acceleration is used when available for dense node and edge drawing, with Canvas 2D preserved as the interaction and fallback layer.
87
- - Graph zoom-out renders hierarchical ecosystem subgraphs: macro clusters of up to 1000 notes expand near the user's focus into smaller graph meshes before individual notes are rendered.
87
+ - Graph zoom-out renders hierarchical ecosystem subgraphs only above 1000 notes: the memory hub stays centered, 1000-note groups stay as compact sand-like points, and focused groups gradually expand into smaller graph meshes before individual notes are rendered.
88
88
  - Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
89
89
  - Large-segment layout spacing now grows logarithmically to keep initial visual density consistent between medium and very large vaults (for example, ~1k vs ~50k notes).
90
90
  - Graph coordinates are visually compacted across graph sizes so reset starts from a stable macro mass and zoom-in progressively expands toward local detail.
@@ -602,7 +602,7 @@ The graph UI shows:
602
602
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
603
603
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
604
604
  - graph camera treats hub-centered navigation as structural only when the hub is dominant; diffuse stress graphs reset and zoom around the full graph mass
605
- - graph LOD progression: very low zoom uses connected ecosystem clusters of up to 1000 notes, zoom-in expands only focused clusters into 250-note and 60-note subgraphs with aggregated real links, then progressively raises the focused node budget so local areas keep nearby notes and links visible
605
+ - graph LOD progression: graphs up to 1000 notes render directly; larger graphs use a compact memory-hub-centered mesh of connected 1000-note points, zoom-in spreads only focused clusters into 250-note and 60-note subgraphs with aggregated real links, then progressively raises the focused node budget so local areas keep nearby notes and links visible
606
606
 
607
607
  The server indexes before starting by default. Use `--no-index` to skip that step:
608
608
 
@@ -22,8 +22,10 @@ const worldCoordinateLimit = 5_000_000
22
22
  const transformCoordinateLimit = 20_000_000
23
23
  const hoverHitTestIntervalMs = 64
24
24
  const ecosystemGroupSize = 1000
25
+ const ecosystemActivationNodeThreshold = 1000
25
26
  const ecosystemGroupSizes = [1000, 250, 60]
26
27
  const ecosystemClusterEdgeLimit = 520
28
+ const ecosystemHubEdgeLimit = 120
27
29
  const ecosystemClusterScaleThreshold = 0.32
28
30
  const ecosystemSubgraphScaleThreshold = 0.18
29
31
  const ecosystemMicroScaleThreshold = 0.08
@@ -73,6 +75,8 @@ const state = {
73
75
  visibleEdgeByNode: new Map(),
74
76
  ecosystemClusters: [],
75
77
  ecosystemClustersBySize: new Map(),
78
+ ecosystemNodeClusterBySize: new Map(),
79
+ ecosystemHubCluster: null,
76
80
  macroCenter: { x: 0, y: 0 },
77
81
  macroRepresentative: null,
78
82
  primaryHub: null,
@@ -569,11 +573,6 @@ const recomputeVisibility = () => {
569
573
  state.visibleEdges = limitedEdges
570
574
  state.visibleNodeSpatial = createSpatialIndex(nodes)
571
575
  state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
572
- const ecosystemGraph = nodes.length > 1
573
- ? buildEcosystemGraph(nodes)
574
- : { clusters: [], clustersBySize: new Map() }
575
- state.ecosystemClusters = ecosystemGraph.clusters
576
- state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
577
576
  const primaryHub = rankedHubNodes()[0] ?? null
578
577
  state.primaryHub = primaryHub
579
578
  state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
@@ -585,6 +584,13 @@ const recomputeVisibility = () => {
585
584
  y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
586
585
  }
587
586
  : { x: 0, y: 0 }
587
+ const ecosystemGraph = nodes.length > ecosystemActivationNodeThreshold
588
+ ? buildEcosystemGraph(nodes, state.macroCenter, primaryHub)
589
+ : { clusters: [], clustersBySize: new Map(), nodeClusterBySize: new Map(), hubCluster: null }
590
+ state.ecosystemClusters = ecosystemGraph.clusters
591
+ state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
592
+ state.ecosystemNodeClusterBySize = ecosystemGraph.nodeClusterBySize
593
+ state.ecosystemHubCluster = ecosystemGraph.hubCluster
588
594
  state.macroRepresentative = resolveMacroRepresentative(nodes)
589
595
  markRenderDirty()
590
596
  }
@@ -729,18 +735,32 @@ const selectEcosystemRepresentative = nodes => {
729
735
  return representative
730
736
  }
731
737
 
732
- const buildEcosystemCluster = (nodes, index) => {
738
+ const ecosystemLayoutSpacingForSize = size => {
739
+ if (size >= 1000) return 360
740
+ if (size >= 250) return 92
741
+ return 28
742
+ }
743
+
744
+ const ecosystemCompactPoint = (index, total, center, spacing) => {
745
+ if (total <= 1) {
746
+ return { x: center.x, y: center.y }
747
+ }
748
+ const angle = index * 2.399963229728653
749
+ const radius = spacing * Math.sqrt(index + 1)
750
+ return {
751
+ x: center.x + Math.cos(angle) * radius,
752
+ y: center.y + Math.sin(angle) * radius
753
+ }
754
+ }
755
+
756
+ const buildEcosystemCluster = (nodes, index, point) => {
733
757
  const count = Math.max(nodes.length, 1)
734
- const sum = nodes.reduce((accumulator, node) => ({
735
- x: accumulator.x + node.x,
736
- y: accumulator.y + node.y
737
- }), { x: 0, y: 0 })
738
758
  const representative = selectEcosystemRepresentative(nodes)
739
759
 
740
760
  return {
741
761
  id: 'ecosystem-' + index,
742
- x: sum.x / count,
743
- y: sum.y / count,
762
+ x: point.x,
763
+ y: point.y,
744
764
  count,
745
765
  nodeIds: nodes.map(node => node.id),
746
766
  representative,
@@ -748,18 +768,47 @@ const buildEcosystemCluster = (nodes, index) => {
748
768
  }
749
769
  }
750
770
 
751
- const buildEcosystemLevel = (sortedNodes, size, parentLookup) => {
771
+ const buildEcosystemHubCluster = (hub, center) => hub
772
+ ? {
773
+ id: 'ecosystem-hub',
774
+ x: center.x,
775
+ y: center.y,
776
+ count: 1,
777
+ size: 1,
778
+ nodeIds: [hub.id],
779
+ representative: hub,
780
+ label: hub.title || 'Memory Hub',
781
+ parentId: null,
782
+ parentX: null,
783
+ parentY: null,
784
+ isHub: true
785
+ }
786
+ : null
787
+
788
+ const buildEcosystemLevel = (sortedNodes, size, parentLookup, center) => {
752
789
  const clusters = []
753
790
  const clusterByNodeId = new Map()
791
+ const parentChildIndex = new Map()
754
792
 
755
793
  for (let offset = 0; offset < sortedNodes.length; offset += size) {
756
794
  const clusterNodes = sortedNodes.slice(offset, offset + size)
757
795
  const parentCluster = parentLookup?.get(clusterNodes[0]?.id)
796
+ const siblingIndex = parentCluster
797
+ ? (parentChildIndex.get(parentCluster.id) ?? 0)
798
+ : clusters.length
799
+ if (parentCluster) {
800
+ parentChildIndex.set(parentCluster.id, siblingIndex + 1)
801
+ }
802
+ const point = parentCluster
803
+ ? ecosystemCompactPoint(siblingIndex, Math.ceil((parentCluster.count || size) / size), parentCluster, ecosystemLayoutSpacingForSize(size))
804
+ : ecosystemCompactPoint(clusters.length, Math.ceil(sortedNodes.length / size), center, ecosystemLayoutSpacingForSize(size))
758
805
  const cluster = {
759
- ...buildEcosystemCluster(clusterNodes, clusters.length),
806
+ ...buildEcosystemCluster(clusterNodes, clusters.length, point),
760
807
  id: 'ecosystem-' + size + '-' + clusters.length,
761
808
  size,
762
- parentId: parentCluster?.id ?? null
809
+ parentId: parentCluster?.id ?? null,
810
+ parentX: parentCluster?.x ?? null,
811
+ parentY: parentCluster?.y ?? null
763
812
  }
764
813
  clusters.push(cluster)
765
814
  for (let index = 0; index < clusterNodes.length; index += 1) {
@@ -770,25 +819,32 @@ const buildEcosystemLevel = (sortedNodes, size, parentLookup) => {
770
819
  return { clusters, clusterByNodeId }
771
820
  }
772
821
 
773
- const buildEcosystemGraph = (nodes) => {
822
+ const buildEcosystemGraph = (nodes, center, hub) => {
774
823
  if (nodes.length === 0) {
775
- return { clusters: [], clustersBySize: new Map() }
824
+ return { clusters: [], clustersBySize: new Map(), nodeClusterBySize: new Map(), hubCluster: null }
776
825
  }
777
826
 
778
- const sortedNodes = [...nodes].sort(compareNodesForEcosystem)
827
+ const hubCluster = buildEcosystemHubCluster(hub, center)
828
+ const sortedNodes = nodes
829
+ .filter(node => node.id !== hub?.id)
830
+ .sort(compareNodesForEcosystem)
779
831
  const clustersBySize = new Map()
832
+ const nodeClusterBySize = new Map()
780
833
  let parentLookup = null
781
834
 
782
835
  for (let index = 0; index < ecosystemGroupSizes.length; index += 1) {
783
836
  const size = ecosystemGroupSizes[index]
784
- const level = buildEcosystemLevel(sortedNodes, size, parentLookup)
837
+ const level = buildEcosystemLevel(sortedNodes, size, parentLookup, center)
785
838
  clustersBySize.set(size, level.clusters)
839
+ nodeClusterBySize.set(size, level.clusterByNodeId)
786
840
  parentLookup = level.clusterByNodeId
787
841
  }
788
842
 
789
843
  return {
790
844
  clusters: clustersBySize.get(ecosystemGroupSize) ?? [],
791
- clustersBySize
845
+ clustersBySize,
846
+ nodeClusterBySize,
847
+ hubCluster
792
848
  }
793
849
  }
794
850
 
@@ -821,61 +877,124 @@ const nearestEcosystemParentIds = (clusters, focusPoint, limit) =>
821
877
  .slice(0, limit)
822
878
  .map(item => item.cluster.id)
823
879
 
824
- const ecosystemPlanForScale = scale => {
825
- if (scale <= ecosystemMicroScaleThreshold) {
826
- return { baseSize: 1000, childSize: null }
880
+ const smoothStep = value => {
881
+ const clamped = Math.max(0, Math.min(1, value))
882
+ return clamped * clamped * (3 - clamped * 2)
883
+ }
884
+
885
+ const zoomProgress = (scale, start, end) =>
886
+ smoothStep((scale - start) / Math.max(end - start, 0.0001))
887
+
888
+ const expandFocusedClusters = (parentClusters, childSize, spread, viewport) => {
889
+ const focusPoint = ecosystemFocusPoint()
890
+ const expandedParentIds = new Set(nearestEcosystemParentIds(
891
+ parentClusters,
892
+ focusPoint,
893
+ ecosystemFocusedParentLimit
894
+ ))
895
+ const childClusters = state.ecosystemClustersBySize.get(childSize) ?? []
896
+ const visibleChildClusters = childClusters
897
+ .filter(cluster => expandedParentIds.has(cluster.parentId))
898
+ .map(cluster => spreadChildClusterFromParent(cluster, spread))
899
+ .filter(cluster => isClusterInViewport(cluster, viewport))
900
+
901
+ return {
902
+ expandedParentIds,
903
+ childClusters: visibleChildClusters
827
904
  }
828
- if (scale <= ecosystemSubgraphScaleThreshold) {
829
- return { baseSize: 1000, childSize: 250 }
905
+ }
906
+
907
+ const spreadChildClusterFromParent = (cluster, spread) => {
908
+ if (!Number.isFinite(cluster.parentX) || !Number.isFinite(cluster.parentY)) {
909
+ return cluster
910
+ }
911
+
912
+ return {
913
+ ...cluster,
914
+ x: cluster.parentX + (cluster.x - cluster.parentX) * spread,
915
+ y: cluster.parentY + (cluster.y - cluster.parentY) * spread
830
916
  }
831
- return { baseSize: 250, childSize: 60 }
832
917
  }
833
918
 
834
919
  const selectHierarchicalEcosystemClusters = viewport => {
835
- const plan = ecosystemPlanForScale(state.transform.scale)
836
- const baseClusters = state.ecosystemClustersBySize.get(plan.baseSize) ?? state.ecosystemClusters
920
+ const baseClusters = state.ecosystemClustersBySize.get(ecosystemGroupSize) ?? state.ecosystemClusters
837
921
  const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
922
+ const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
838
923
 
839
- if (!plan.childSize) {
840
- return visibleBaseClusters
924
+ if (state.transform.scale <= ecosystemMicroScaleThreshold) {
925
+ return [...hubClusters, ...visibleBaseClusters]
841
926
  }
842
927
 
843
- const focusPoint = ecosystemFocusPoint()
844
- const expandedParentIds = new Set(nearestEcosystemParentIds(
928
+ const midExpansion = expandFocusedClusters(
845
929
  visibleBaseClusters,
846
- focusPoint,
847
- ecosystemFocusedParentLimit
848
- ))
849
- const childClusters = state.ecosystemClustersBySize.get(plan.childSize) ?? []
850
- const visibleChildClusters = childClusters.filter(cluster =>
851
- expandedParentIds.has(cluster.parentId) &&
852
- isClusterInViewport(cluster, viewport)
930
+ 250,
931
+ state.transform.scale <= ecosystemSubgraphScaleThreshold
932
+ ? zoomProgress(state.transform.scale, ecosystemMicroScaleThreshold, ecosystemSubgraphScaleThreshold)
933
+ : 1,
934
+ viewport
853
935
  )
936
+ if (midExpansion.childClusters.length === 0) {
937
+ return [...hubClusters, ...visibleBaseClusters]
938
+ }
854
939
 
855
- if (visibleChildClusters.length === 0) {
856
- return visibleBaseClusters
940
+ const remainingBaseClusters = visibleBaseClusters.filter(cluster => !midExpansion.expandedParentIds.has(cluster.id))
941
+ if (state.transform.scale <= ecosystemSubgraphScaleThreshold) {
942
+ return [
943
+ ...hubClusters,
944
+ ...remainingBaseClusters,
945
+ ...midExpansion.childClusters
946
+ ]
947
+ }
948
+
949
+ const microExpansion = expandFocusedClusters(
950
+ midExpansion.childClusters,
951
+ 60,
952
+ zoomProgress(state.transform.scale, ecosystemSubgraphScaleThreshold, ecosystemClusterScaleThreshold),
953
+ viewport
954
+ )
955
+ if (microExpansion.childClusters.length === 0) {
956
+ return [
957
+ ...hubClusters,
958
+ ...remainingBaseClusters,
959
+ ...midExpansion.childClusters
960
+ ]
857
961
  }
858
962
 
859
963
  return [
860
- ...visibleBaseClusters.filter(cluster => !expandedParentIds.has(cluster.id)),
861
- ...visibleChildClusters
964
+ ...hubClusters,
965
+ ...remainingBaseClusters,
966
+ ...midExpansion.childClusters.filter(cluster => !microExpansion.expandedParentIds.has(cluster.id)),
967
+ ...microExpansion.childClusters
862
968
  ]
863
969
  }
864
970
 
865
971
  const ecosystemEdgesForClusters = clusters => {
866
- const clusterByNodeId = new Map()
867
- for (let clusterIndex = 0; clusterIndex < clusters.length; clusterIndex += 1) {
868
- const cluster = clusters[clusterIndex]
869
- for (let nodeIndex = 0; nodeIndex < cluster.nodeIds.length; nodeIndex += 1) {
870
- clusterByNodeId.set(cluster.nodeIds[nodeIndex], cluster)
972
+ const clusterById = new Map(clusters.map(cluster => [cluster.id, cluster]))
973
+ const clusterIds = new Set(clusterById.keys())
974
+ const levelBySize = new Map()
975
+ for (let index = 0; index < clusters.length; index += 1) {
976
+ const cluster = clusters[index]
977
+ if (!cluster.size || cluster.isHub) continue
978
+ levelBySize.set(cluster.size, state.ecosystemNodeClusterBySize.get(cluster.size) ?? new Map())
979
+ }
980
+ const resolveClusterForNode = nodeId => {
981
+ if (state.ecosystemHubCluster?.nodeIds.includes(nodeId) && clusterIds.has(state.ecosystemHubCluster.id)) {
982
+ return state.ecosystemHubCluster
983
+ }
984
+ for (const [size, lookup] of levelBySize) {
985
+ const cluster = lookup.get(nodeId)
986
+ if (cluster && clusterIds.has(cluster.id)) {
987
+ return clusterById.get(cluster.id) ?? cluster
988
+ }
871
989
  }
990
+ return null
872
991
  }
873
992
 
874
993
  const edgeByClusterPair = new Map()
875
994
  for (let index = 0; index < state.visibleEdges.length; index += 1) {
876
995
  const edge = state.visibleEdges[index]
877
- const sourceCluster = clusterByNodeId.get(edge.source)
878
- const targetCluster = clusterByNodeId.get(edge.target)
996
+ const sourceCluster = resolveClusterForNode(edge.source)
997
+ const targetCluster = resolveClusterForNode(edge.target)
879
998
  if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
880
999
  continue
881
1000
  }
@@ -898,9 +1017,34 @@ const ecosystemEdgesForClusters = clusters => {
898
1017
  })
899
1018
  }
900
1019
 
901
- return Array.from(edgeByClusterPair.values())
1020
+ const edges = Array.from(edgeByClusterPair.values())
902
1021
  .sort((left, right) => right.weight - left.weight)
903
1022
  .slice(0, ecosystemClusterEdgeLimit)
1023
+ const hubCluster = state.ecosystemHubCluster && clusterIds.has(state.ecosystemHubCluster.id)
1024
+ ? state.ecosystemHubCluster
1025
+ : null
1026
+ if (!hubCluster) {
1027
+ return edges
1028
+ }
1029
+
1030
+ const existingHubTargets = new Set(edges.flatMap(edge =>
1031
+ edge.sourceCluster.id === hubCluster.id
1032
+ ? [edge.targetCluster.id]
1033
+ : edge.targetCluster.id === hubCluster.id
1034
+ ? [edge.sourceCluster.id]
1035
+ : []
1036
+ ))
1037
+ const syntheticHubEdges = clusters
1038
+ .filter(cluster => cluster.id !== hubCluster.id && !existingHubTargets.has(cluster.id))
1039
+ .slice(0, ecosystemHubEdgeLimit)
1040
+ .map(cluster => ({
1041
+ id: hubCluster.id + ':' + cluster.id,
1042
+ sourceCluster: hubCluster,
1043
+ targetCluster: cluster,
1044
+ weight: 1,
1045
+ inferred: true
1046
+ }))
1047
+ return edges.concat(syntheticHubEdges)
904
1048
  }
905
1049
 
906
1050
  const edgeBudgetForCurrentFrame = () => {
@@ -2337,6 +2481,20 @@ const baseNodeRadius = node => {
2337
2481
 
2338
2482
  const nodeRadius = node => Math.max(baseNodeRadius(node), minNodePixelRadius / Math.max(state.transform.scale, 0.0001))
2339
2483
 
2484
+ const clusterRadiusPx = cluster => {
2485
+ if (cluster.id === 'macro-galaxy') {
2486
+ return 10
2487
+ }
2488
+ if (cluster.isHub) {
2489
+ return 5.2
2490
+ }
2491
+ if (String(cluster.id).startsWith('ecosystem-')) {
2492
+ const base = cluster.size >= 1000 ? 2.4 : cluster.size >= 250 ? 2.1 : 1.8
2493
+ return Math.max(1.8, Math.min(4.2, base + Math.log10(cluster.count + 1) * 0.28))
2494
+ }
2495
+ return Math.max(8, Math.min(28, 8 + Math.log2(cluster.count + 1) * 3))
2496
+ }
2497
+
2340
2498
  const worldViewportBounds = () => {
2341
2499
  const rect = canvas.getBoundingClientRect()
2342
2500
  const width = Math.max(rect.width, 320)
@@ -2498,7 +2656,11 @@ const computeRenderVisibility = () => {
2498
2656
  return
2499
2657
  }
2500
2658
 
2501
- if (state.transform.scale <= ecosystemClusterScaleThreshold && state.ecosystemClusters.length > 0) {
2659
+ if (
2660
+ state.visibleNodes.length > ecosystemActivationNodeThreshold &&
2661
+ state.transform.scale <= ecosystemClusterScaleThreshold &&
2662
+ state.ecosystemClusters.length > 0
2663
+ ) {
2502
2664
  const clusters = selectHierarchicalEcosystemClusters(viewport)
2503
2665
  .sort((left, right) => right.count - left.count)
2504
2666
  state.renderClusters = clusters
@@ -2760,20 +2922,22 @@ const render = now => {
2760
2922
  }
2761
2923
  state.renderClusters.forEach(cluster => {
2762
2924
  const isMacro = cluster.id === 'macro-galaxy'
2763
- const radiusPx = isMacro
2764
- ? 10
2765
- : Math.max(8, Math.min(28, 8 + Math.log2(cluster.count + 1) * 3))
2925
+ const isEcosystem = String(cluster.id).startsWith('ecosystem-')
2926
+ const isHub = Boolean(cluster.isHub)
2927
+ const radiusPx = clusterRadiusPx(cluster)
2766
2928
  const radius = radiusPx / safeScale
2767
- const haloRadius = (radiusPx + (isMacro ? 8 : 4)) / safeScale
2768
- ctx.beginPath()
2769
- ctx.arc(cluster.x, cluster.y, haloRadius, 0, Math.PI * 2)
2770
- ctx.fillStyle = isMacro ? 'rgba(243, 247, 251, 0.28)' : graphTheme.nodeHalo
2771
- ctx.fill()
2929
+ const haloRadius = (radiusPx + (isMacro ? 8 : isHub ? 4 : isEcosystem ? 1.1 : 4)) / safeScale
2930
+ if (isHub || !isEcosystem || state.transform.scale >= ecosystemSubgraphScaleThreshold) {
2931
+ ctx.beginPath()
2932
+ ctx.arc(cluster.x, cluster.y, haloRadius, 0, Math.PI * 2)
2933
+ ctx.fillStyle = isMacro ? 'rgba(243, 247, 251, 0.28)' : graphTheme.nodeHalo
2934
+ ctx.fill()
2935
+ }
2772
2936
  ctx.beginPath()
2773
2937
  ctx.arc(cluster.x, cluster.y, radius, 0, Math.PI * 2)
2774
2938
  ctx.fillStyle = isMacro ? '#f3f7fb' : graphTheme.node
2775
2939
  ctx.fill()
2776
- ctx.lineWidth = 1.4 / safeScale
2940
+ ctx.lineWidth = (isEcosystem && !isHub ? 0.7 : 1.4) / safeScale
2777
2941
  ctx.strokeStyle = isMacro ? '#ffffff' : graphTheme.nodeStroke
2778
2942
  ctx.stroke()
2779
2943
  if (isMacro && cluster.representative?.title) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.86",
3
+ "version": "0.1.0-beta.88",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",