@andespindola/brainlink 0.1.0-beta.30 → 0.1.0-beta.32

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
@@ -553,6 +553,7 @@ blink server --host 127.0.0.1 --port 4321 --watch
553
553
  ```
554
554
 
555
555
  By default, the server uses `$HOME/.brainlink/vault`. Pass `--vault ./vault` only when you want to inspect a custom vault.
556
+ By default, `blink server` also opens the graph URL in your default browser. Use `--no-open` to keep it headless.
556
557
 
557
558
  The graph UI shows:
558
559
 
@@ -838,9 +839,11 @@ Watches Markdown files and rebuilds the index when notes change.
838
839
  ```bash
839
840
  blink server --watch
840
841
  blink server --vault ./vault --watch
842
+ blink server --vault ./vault --watch --no-open
841
843
  ```
842
844
 
843
845
  Starts the local read-only graph UI and HTTP API.
846
+ By default, it opens the graph URL in your default browser. Use `--no-open` to skip that behavior.
844
847
 
845
848
  The HTTP server only binds to loopback hosts such as `127.0.0.1`, `localhost` or `::1`.
846
849
 
@@ -23,6 +23,7 @@ const state = {
23
23
  nodeDetails: new Map(),
24
24
  transform: { x: 0, y: 0, scale: 1 },
25
25
  pointer: { x: 0, y: 0, down: false, dragNode: null, moved: false },
26
+ cursor: { x: 0, y: 0, inCanvas: false },
26
27
  graphSignature: '',
27
28
  graphStatus: '',
28
29
  last: performance.now()
@@ -687,6 +688,30 @@ const wheelZoomFactor = event => {
687
688
  return event.deltaY < 0 ? 1 + adjustedStep : 1 / (1 + adjustedStep)
688
689
  }
689
690
 
691
+ const isScreenPointInsideCanvas = (screenX, screenY) => {
692
+ const rect = canvas.getBoundingClientRect()
693
+
694
+ return screenX >= rect.left && screenX <= rect.right && screenY >= rect.top && screenY <= rect.bottom
695
+ }
696
+
697
+ const handleWheelZoom = event => {
698
+ if (!isScreenPointInsideCanvas(event.clientX, event.clientY)) {
699
+ return
700
+ }
701
+
702
+ event.preventDefault()
703
+ const rect = canvas.getBoundingClientRect()
704
+ const cursorX = event.clientX - rect.left
705
+ const cursorY = event.clientY - rect.top
706
+ const factor = wheelZoomFactor(event)
707
+
708
+ if (!Number.isFinite(factor) || factor <= 0 || factor === 1) {
709
+ return
710
+ }
711
+
712
+ zoomAtPoint(cursorX, cursorY, factor)
713
+ }
714
+
690
715
  const bindEvents = () => {
691
716
  window.addEventListener('resize', resize)
692
717
  elements.search.addEventListener('input', event => {
@@ -730,14 +755,7 @@ const bindEvents = () => {
730
755
  }
731
756
  if (event.target === elements.contentDialog) elements.contentDialog.close()
732
757
  })
733
- canvas.addEventListener('wheel', event => {
734
- event.preventDefault()
735
- const rect = canvas.getBoundingClientRect()
736
- const cursorX = event.clientX - rect.left
737
- const cursorY = event.clientY - rect.top
738
- const factor = wheelZoomFactor(event)
739
- zoomAtPoint(cursorX, cursorY, factor)
740
- }, { passive: false })
758
+ window.addEventListener('wheel', handleWheelZoom, { passive: false })
741
759
  canvas.addEventListener('dblclick', event => {
742
760
  const rect = canvas.getBoundingClientRect()
743
761
  const cursorX = event.clientX - rect.left
@@ -757,6 +775,7 @@ const bindEvents = () => {
757
775
  canvas.addEventListener('pointermove', event => {
758
776
  const point = worldPoint(event)
759
777
  state.hovered = hitNode(point)
778
+ state.cursor = { x: event.clientX, y: event.clientY, inCanvas: true }
760
779
  if (!state.pointer.down) return
761
780
  const dx = event.clientX - state.pointer.x
762
781
  const dy = event.clientY - state.pointer.y
@@ -780,6 +799,12 @@ const bindEvents = () => {
780
799
  canvas.addEventListener('pointercancel', () => {
781
800
  state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
782
801
  })
802
+ canvas.addEventListener('pointerenter', event => {
803
+ state.cursor = { x: event.clientX, y: event.clientY, inCanvas: true }
804
+ })
805
+ canvas.addEventListener('pointerleave', event => {
806
+ state.cursor = { x: event.clientX, y: event.clientY, inCanvas: false }
807
+ })
783
808
  window.addEventListener('keydown', event => {
784
809
  if (event.key === '+' || event.key === '=') {
785
810
  event.preventDefault()
@@ -1,6 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { dirname, relative, resolve } from 'node:path';
4
+ import { platform } from 'node:os';
5
+ import { spawn } from 'node:child_process';
4
6
  import { addNote } from '../../application/add-note.js';
5
7
  import { buildContextPackage } from '../../application/build-context.js';
6
8
  import { importLegacySqliteDatabase } from '../../application/import-legacy-sqlite.js';
@@ -24,6 +26,29 @@ const resolveAddContent = (options) => {
24
26
  }
25
27
  return readFileSync(options.contentFile, 'utf8');
26
28
  };
29
+ const openUrlInBrowser = (url) => {
30
+ const openDisabled = process.env.BRAINLINK_NO_BROWSER === '1' ||
31
+ process.env.BRAINLINK_NO_BROWSER === 'true' ||
32
+ process.env.CI === 'true';
33
+ if (openDisabled) {
34
+ return;
35
+ }
36
+ try {
37
+ if (platform() === 'darwin') {
38
+ const child = spawn('open', [url], { detached: true, stdio: 'ignore' });
39
+ child.unref();
40
+ return;
41
+ }
42
+ if (platform() === 'win32') {
43
+ const child = spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' });
44
+ child.unref();
45
+ return;
46
+ }
47
+ const child = spawn('xdg-open', [url], { detached: true, stdio: 'ignore' });
48
+ child.unref();
49
+ }
50
+ catch { }
51
+ };
27
52
  export const registerWriteCommands = (program) => {
28
53
  program
29
54
  .command('init')
@@ -213,6 +238,7 @@ export const registerWriteCommands = (program) => {
213
238
  .option('-h, --host <host>', 'server host', '127.0.0.1')
214
239
  .option('-p, --port <port>', 'server port', '4321')
215
240
  .option('--no-index', 'skip indexing before starting the server')
241
+ .option('--no-open', 'do not open the graph UI in the default browser')
216
242
  .option('-w, --watch', 'watch markdown files and reindex on changes')
217
243
  .option('--json', 'print machine-readable JSON')
218
244
  .description('start a local web UI for the knowledge graph')
@@ -225,7 +251,10 @@ export const registerWriteCommands = (program) => {
225
251
  shouldIndex: options.index,
226
252
  shouldWatch: Boolean(options.watch)
227
253
  });
228
- print(options.json, { url: server.url, watch: Boolean(options.watch), readonly: true }, () => `Brainlink graph server running at ${server.url}`);
254
+ if (options.open !== false) {
255
+ openUrlInBrowser(server.url);
256
+ }
257
+ print(options.json, { url: server.url, watch: Boolean(options.watch), readonly: true, openedInBrowser: options.open !== false }, () => `Brainlink graph server running at ${server.url}${options.open !== false ? ' (opened in browser)' : ''}`);
229
258
  });
230
259
  program
231
260
  .command('quickstart')
@@ -536,9 +536,11 @@ shared: 30 documents
536
536
  ```bash
537
537
  blink server --host 127.0.0.1 --port 4321
538
538
  blink server --vault ./vault --host 127.0.0.1 --port 4321
539
+ blink server --vault ./vault --host 127.0.0.1 --port 4321 --no-open
539
540
  ```
540
541
 
541
542
  This starts a local frontend for inspecting the knowledge graph.
543
+ By default it opens the graph URL in your default browser. Use `--no-open` to keep the server headless.
542
544
 
543
545
  Without `--vault`, the graph UI serves `$HOME/.brainlink/vault`.
544
546
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.30",
3
+ "version": "0.1.0-beta.32",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",