@gmickel/gno 0.9.0 → 0.9.2

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
@@ -1,13 +1,13 @@
1
1
  # GNO
2
2
 
3
- **Your Local Second Brain** Index, search, and synthesize your entire digital life.
3
+ **Your Local Second Brain**: Index, search, and synthesize your entire digital life.
4
4
 
5
5
  [![npm](./assets/badges/npm.svg)](https://www.npmjs.com/package/@gmickel/gno)
6
6
  [![MIT License](./assets/badges/license.svg)](./LICENSE)
7
7
  [![Website](./assets/badges/website.svg)](https://gno.sh)
8
8
  [![Twitter](./assets/badges/twitter.svg)](https://twitter.com/gmickel)
9
9
 
10
- GNO is a local knowledge engine for privacy-conscious developers and AI agents. Index your notes, code, PDFs, and Office docs. Get hybrid search (BM25 + vector + reranking) and AI-powered answersall running 100% on your machine.
10
+ GNO is a local knowledge engine for privacy-conscious developers and AI agents. Index your notes, code, PDFs, and Office docs. Get hybrid search (BM25 + vector + reranking) and AI-powered answers, all running 100% on your machine.
11
11
 
12
12
  ---
13
13
 
@@ -16,9 +16,9 @@ GNO is a local knowledge engine for privacy-conscious developers and AI agents.
16
16
  - [Quick Start](#quick-start)
17
17
  - [Installation](#installation)
18
18
  - [Search Modes](#search-modes)
19
+ - [Agent Integration](#agent-integration)
19
20
  - [Web UI](#web-ui)
20
21
  - [REST API](#rest-api)
21
- - [Agent Integration](#agent-integration)
22
22
  - [How It Works](#how-it-works)
23
23
  - [Features](#features)
24
24
  - [Local Models](#local-models)
@@ -85,7 +85,7 @@ Check status: `gno mcp status`
85
85
 
86
86
  #### Skills (Claude Code, Codex, OpenCode)
87
87
 
88
- Skills integrate via CLIno MCP overhead:
88
+ Skills integrate via CLI with no MCP overhead:
89
89
 
90
90
  ```bash
91
91
  gno skill install --scope user # User-wide
@@ -106,7 +106,7 @@ gno skill install --target all # Both Claude + Codex
106
106
  | `gno query` | Hybrid | Best accuracy (BM25 + vector + reranking) |
107
107
  | `gno ask --answer` | RAG | Direct answers with citations |
108
108
 
109
- **BM25** indexes full documents (not chunks) with Snowball stemming"running" matches "run".
109
+ **BM25** indexes full documents (not chunks) with Snowball stemming, so "running" matches "run".
110
110
  **Vector** embeds chunks with document titles for context awareness.
111
111
 
112
112
  ```bash
@@ -120,29 +120,76 @@ Output formats: `--json`, `--files`, `--csv`, `--md`, `--xml`
120
120
 
121
121
  ---
122
122
 
123
+ ## Agent Integration
124
+
125
+ Give your local LLM agents a long-term memory. GNO integrates as a Claude Code skill or MCP server, allowing agents to search, read, and cite your local files.
126
+
127
+ ### Skills
128
+
129
+ Skills add GNO search to Claude Code/Codex without MCP protocol overhead:
130
+
131
+ ```bash
132
+ gno skill install --scope user
133
+ ```
134
+
135
+ ![GNO Skill in Claude Code](./assets/screenshots/claudecodeskill.jpg)
136
+
137
+ Then ask your agent: _"Search my notes for the auth discussion"_
138
+
139
+ [Skill setup guide →](https://gno.sh/docs/integrations/skills/)
140
+
141
+ ### MCP Server
142
+
143
+ Connect GNO to Claude Desktop, Cursor, Raycast, and more:
144
+
145
+ ![GNO MCP](./assets/screenshots/mcp.jpg)
146
+
147
+ GNO exposes 6 tools via [Model Context Protocol](https://modelcontextprotocol.io):
148
+
149
+ | Tool | Description |
150
+ | :-------------- | :-------------------------- |
151
+ | `gno_search` | BM25 keyword search |
152
+ | `gno_vsearch` | Vector semantic search |
153
+ | `gno_query` | Hybrid search (recommended) |
154
+ | `gno_get` | Retrieve document by ID |
155
+ | `gno_multi_get` | Batch document retrieval |
156
+ | `gno_status` | Index health check |
157
+
158
+ **Design**: MCP tools are retrieval-only. Your AI assistant (Claude, GPT-4) synthesizes answers from retrieved context. Best retrieval (GNO) + best reasoning (your LLM).
159
+
160
+ [MCP setup guide →](https://gno.sh/docs/MCP/)
161
+
162
+ ---
163
+
123
164
  ## Web UI
124
165
 
125
- Visual dashboard for search, browsing, editing, and AI answers—right in your browser.
166
+ Visual dashboard for search, browsing, editing, and AI answers. Right in your browser.
126
167
 
127
168
  ```bash
128
169
  gno serve # Start on port 3000
129
170
  gno serve --port 8080 # Custom port
130
171
  ```
131
172
 
132
- ![GNO Web UI](./assets/screenshots/webui-home.png)
173
+ ![GNO Web UI](./assets/screenshots/webui-home.jpg)
133
174
 
134
175
  Open `http://localhost:3000` to:
135
176
 
136
- - **Search** BM25, vector, or hybrid modes with visual results
137
- - **Browse** Paginated document list, filter by collection
138
- - **Edit** Create, edit, and delete documents with live preview
139
- - **Ask** AI-powered Q&A with citations
140
- - **Manage Collections** Add, remove, and re-index collections
141
- - **Switch presets** Change models live without restart
177
+ - **Search**: BM25, vector, or hybrid modes with visual results
178
+ - **Browse**: Paginated document list, filter by collection
179
+ - **Edit**: Create, edit, and delete documents with live preview
180
+ - **Ask**: AI-powered Q&A with citations
181
+ - **Manage Collections**: Add, remove, and re-index collections
182
+ - **Switch presets**: Change models live without restart
183
+
184
+ ### Search
185
+
186
+ ![GNO Search](./assets/screenshots/webui-search.jpg)
187
+
188
+ Three retrieval modes: BM25 (keyword), Vector (semantic), or Hybrid (best of both). Adjust search depth for speed vs thoroughness.
142
189
 
143
190
  ### Document Editing
144
191
 
145
- ![GNO Document Editor](./assets/screenshots/webui-editor.png)
192
+ ![GNO Document Editor](./assets/screenshots/webui-editor.jpg)
146
193
 
147
194
  Full-featured markdown editor with:
148
195
 
@@ -156,7 +203,7 @@ Full-featured markdown editor with:
156
203
 
157
204
  ### Collections Management
158
205
 
159
- ![GNO Collections](./assets/screenshots/webui-collections.png)
206
+ ![GNO Collections](./assets/screenshots/webui-collections.jpg)
160
207
 
161
208
  - Add collections with folder path input
162
209
  - View document count, chunk count, embedding status
@@ -165,9 +212,9 @@ Full-featured markdown editor with:
165
212
 
166
213
  ### AI Answers
167
214
 
168
- ![GNO AI Answers](./assets/screenshots/webui-ask-answer.png)
215
+ ![GNO AI Answers](./assets/screenshots/webui-ask-answer.jpg)
169
216
 
170
- Ask questions in natural languageGNO searches your documents and synthesizes answers with inline citations linking to sources.
217
+ Ask questions in natural language. GNO searches your documents and synthesizes answers with inline citations linking to sources.
171
218
 
172
219
  Everything runs locally. No cloud, no accounts, no data leaving your machine.
173
220
 
@@ -219,41 +266,6 @@ No authentication. No rate limits. Build custom tools, automate workflows, integ
219
266
 
220
267
  ---
221
268
 
222
- ## Agent Integration
223
-
224
- ### MCP Server
225
-
226
- ![GNO MCP](./assets/screenshots/mcp.jpg)
227
-
228
- GNO exposes 6 tools via [Model Context Protocol](https://modelcontextprotocol.io):
229
-
230
- | Tool | Description |
231
- | :-------------- | :-------------------------- |
232
- | `gno_search` | BM25 keyword search |
233
- | `gno_vsearch` | Vector semantic search |
234
- | `gno_query` | Hybrid search (recommended) |
235
- | `gno_get` | Retrieve document by ID |
236
- | `gno_multi_get` | Batch document retrieval |
237
- | `gno_status` | Index health check |
238
-
239
- **Design**: MCP tools are retrieval-only. Your AI assistant (Claude, GPT-4) synthesizes answers from retrieved context—best retrieval (GNO) + best reasoning (your LLM).
240
-
241
- ### Skills
242
-
243
- Skills add GNO search to Claude Code/Codex without MCP protocol overhead:
244
-
245
- ```bash
246
- gno skill install --scope user
247
- ```
248
-
249
- ![GNO Skill in Claude Code](./assets/screenshots/claudecodeskill.jpg)
250
-
251
- Then ask your agent: _"Search my notes for the auth discussion"_
252
-
253
- > **Detailed docs**: [MCP Integration](https://gno.sh/docs/MCP/) · [Use Cases](https://gno.sh/docs/USE-CASES/)
254
-
255
- ---
256
-
257
269
  ## How It Works
258
270
 
259
271
  ```mermaid
@@ -279,11 +291,11 @@ graph TD
279
291
  M --> N[Final Results]
280
292
  ```
281
293
 
282
- 0. **Strong Signal Check** Skip expansion if BM25 has confident match (saves 1-3s)
283
- 1. **Query Expansion** LLM generates lexical variants, semantic rephrases, and a [HyDE](https://arxiv.org/abs/2212.10496) passage
284
- 2. **Parallel Retrieval** Document-level BM25 + chunk-level vector search on all variants
285
- 3. **Fusion** RRF with 2× weight for original query, tiered bonus for top ranks
286
- 4. **Reranking** Qwen3-Reranker scores full documents (32K context), blended with fusion
294
+ 0. **Strong Signal Check**: Skip expansion if BM25 has confident match (saves 1-3s)
295
+ 1. **Query Expansion**: LLM generates lexical variants, semantic rephrases, and a [HyDE](https://arxiv.org/abs/2212.10496) passage
296
+ 2. **Parallel Retrieval**: Document-level BM25 + chunk-level vector search on all variants
297
+ 3. **Fusion**: RRF with 2× weight for original query, tiered bonus for top ranks
298
+ 4. **Reranking**: Qwen3-Reranker scores full documents (32K context), blended with fusion
287
299
 
288
300
  > **Deep dive**: [How Search Works](https://gno.sh/docs/HOW-SEARCH-WORKS/)
289
301
 
@@ -298,12 +310,12 @@ graph TD
298
310
  | **Web UI** | Visual dashboard for search, browse, edit, and AI Q&A |
299
311
  | **REST API** | HTTP API for custom tools and integrations |
300
312
  | **Multi-Format** | Markdown, PDF, DOCX, XLSX, PPTX, plain text |
301
- | **Local LLM** | AI answers via llama.cppno API keys |
313
+ | **Local LLM** | AI answers via llama.cpp, no API keys |
302
314
  | **Privacy First** | 100% offline, zero telemetry, your data stays yours |
303
315
  | **MCP Server** | Works with Claude Desktop, Cursor, Zed, + 8 more |
304
316
  | **Collections** | Organize sources with patterns, excludes, contexts |
305
317
  | **Multilingual** | 30+ languages, auto-detection, cross-lingual search |
306
- | **Incremental** | SHA-256 trackingonly changed files re-indexed |
318
+ | **Incremental** | SHA-256 tracking, only changed files re-indexed |
307
319
  | **Keyboard First** | ⌘N capture, ⌘K search, ⌘/ shortcuts, ⌘S save |
308
320
 
309
321
  ---
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -100,6 +100,7 @@
100
100
  "react-dom": "^19.2.3",
101
101
  "react-markdown": "^10.1.0",
102
102
  "rehype-sanitize": "^6.0.0",
103
+ "remark-gfm": "^4.0.1",
103
104
  "shiki": "^3.20.0",
104
105
  "sqlite-vec": "^0.1.7-alpha.2",
105
106
  "streamdown": "^1.6.10",
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Path resolution for skill installation.
3
3
  * Supports Claude Code and Codex targets with project/user scopes.
4
+ * Note: OpenCode and Amp use the same .claude path as Claude Code.
4
5
  *
5
6
  * @module src/cli/commands/skill/paths
6
7
  */
@@ -28,6 +29,8 @@ export const ENV_CODEX_SKILLS_DIR = "CODEX_SKILLS_DIR";
28
29
  export type SkillScope = "project" | "user";
29
30
  export type SkillTarget = "claude" | "codex";
30
31
 
32
+ export const SKILL_TARGETS: SkillTarget[] = ["claude", "codex"];
33
+
31
34
  export interface SkillPathOptions {
32
35
  scope: SkillScope;
33
36
  target: SkillTarget;
@@ -53,13 +56,27 @@ export interface SkillPaths {
53
56
  /** Skill name for the gno skill directory */
54
57
  export const SKILL_NAME = "gno";
55
58
 
56
- /** Directory name for skills within agent config */
57
- const SKILLS_SUBDIR = "skills";
59
+ /** Path configuration per target */
60
+ interface TargetPathConfig {
61
+ projectBase: string; // e.g., ".claude"
62
+ userBase: string; // e.g., ".claude" (joined with homedir) or ".config/opencode"
63
+ skillsSubdir: string; // e.g., "skills" or "skill"
64
+ envVar: string;
65
+ }
58
66
 
59
- /** Agent config directory names */
60
- const AGENT_DIRS: Record<SkillTarget, string> = {
61
- claude: ".claude",
62
- codex: ".codex",
67
+ const TARGET_CONFIGS: Record<SkillTarget, TargetPathConfig> = {
68
+ claude: {
69
+ projectBase: ".claude",
70
+ userBase: ".claude",
71
+ skillsSubdir: "skills",
72
+ envVar: ENV_CLAUDE_SKILLS_DIR,
73
+ },
74
+ codex: {
75
+ projectBase: ".codex",
76
+ userBase: ".codex",
77
+ skillsSubdir: "skills",
78
+ envVar: ENV_CODEX_SKILLS_DIR,
79
+ },
63
80
  };
64
81
 
65
82
  // ─────────────────────────────────────────────────────────────────────────────
@@ -71,19 +88,15 @@ const AGENT_DIRS: Record<SkillTarget, string> = {
71
88
  */
72
89
  export function resolveSkillPaths(opts: SkillPathOptions): SkillPaths {
73
90
  const { scope, target, cwd, homeDir } = opts;
91
+ const config = TARGET_CONFIGS[target];
74
92
 
75
93
  // Check for env overrides first
76
- const envOverride =
77
- target === "claude"
78
- ? process.env[ENV_CLAUDE_SKILLS_DIR]
79
- : process.env[ENV_CODEX_SKILLS_DIR];
94
+ const envOverride = process.env[config.envVar];
80
95
 
81
96
  if (envOverride) {
82
97
  // Require absolute path for security
83
98
  if (!isAbsolute(envOverride)) {
84
- throw new Error(
85
- `${target === "claude" ? ENV_CLAUDE_SKILLS_DIR : ENV_CODEX_SKILLS_DIR} must be an absolute path`
86
- );
99
+ throw new Error(`${config.envVar} must be an absolute path`);
87
100
  }
88
101
  const skillsDir = normalize(envOverride);
89
102
  return {
@@ -94,18 +107,17 @@ export function resolveSkillPaths(opts: SkillPathOptions): SkillPaths {
94
107
  }
95
108
 
96
109
  // Resolve base directory
97
- const agentDir = AGENT_DIRS[target];
98
110
  let base: string;
99
111
 
100
112
  if (scope === "user") {
101
113
  const home = homeDir ?? process.env[ENV_SKILLS_HOME_OVERRIDE] ?? homedir();
102
- base = join(home, agentDir);
114
+ base = join(home, config.userBase);
103
115
  } else {
104
116
  const projectRoot = cwd ?? process.cwd();
105
- base = join(projectRoot, agentDir);
117
+ base = join(projectRoot, config.projectBase);
106
118
  }
107
119
 
108
- const skillsDir = join(base, SKILLS_SUBDIR);
120
+ const skillsDir = join(base, config.skillsSubdir);
109
121
  const gnoDir = join(skillsDir, SKILL_NAME);
110
122
 
111
123
  return { base, skillsDir, gnoDir };
@@ -120,8 +132,7 @@ export function resolveAllPaths(
120
132
  overrides?: { cwd?: string; homeDir?: string }
121
133
  ): Array<{ scope: SkillScope; target: SkillTarget; paths: SkillPaths }> {
122
134
  const scopes: SkillScope[] = scope === "all" ? ["project", "user"] : [scope];
123
- const targets: SkillTarget[] =
124
- target === "all" ? ["claude", "codex"] : [target];
135
+ const targets: SkillTarget[] = target === "all" ? SKILL_TARGETS : [target];
125
136
 
126
137
  const results: Array<{
127
138
  scope: SkillScope;
@@ -147,11 +158,16 @@ export function resolveAllPaths(
147
158
  // ─────────────────────────────────────────────────────────────────────────────
148
159
 
149
160
  /**
150
- * Expected path suffix for gno skill directory.
151
- * Platform-aware (handles Windows backslash).
161
+ * Get expected path suffixes for gno skill directory.
162
+ * Returns all valid suffixes since different targets use different subdir names.
152
163
  */
153
- function getExpectedSuffix(): string {
154
- return `${sep}${SKILLS_SUBDIR}${sep}${SKILL_NAME}`;
164
+ function getExpectedSuffixes(): string[] {
165
+ const subdirs = new Set(
166
+ Object.values(TARGET_CONFIGS).map((c) => c.skillsSubdir)
167
+ );
168
+ return Array.from(subdirs).map(
169
+ (subdir) => `${sep}${subdir}${sep}${SKILL_NAME}`
170
+ );
155
171
  }
156
172
 
157
173
  /**
@@ -164,11 +180,14 @@ export function validatePathForDeletion(
164
180
  ): string | null {
165
181
  const normalized = normalize(destDir);
166
182
  const normalizedBase = normalize(base);
167
- const expectedSuffix = getExpectedSuffix();
168
-
169
- // Must end with /skills/gno (or \skills\gno on Windows)
170
- if (!normalized.endsWith(expectedSuffix)) {
171
- return `Path does not end with expected suffix (${expectedSuffix})`;
183
+ const expectedSuffixes = getExpectedSuffixes();
184
+
185
+ // Must end with /skills/gno or /skill/gno (platform-aware)
186
+ const hasValidSuffix = expectedSuffixes.some((suffix) =>
187
+ normalized.endsWith(suffix)
188
+ );
189
+ if (!hasValidSuffix) {
190
+ return `Path does not end with expected suffix (${expectedSuffixes.join(" or ")})`;
172
191
  }
173
192
 
174
193
  // Minimum length sanity check
@@ -21,6 +21,8 @@ export interface CodeMirrorEditorProps {
21
21
  initialContent: string;
22
22
  /** Called when content changes */
23
23
  onChange: (content: string) => void;
24
+ /** Called when scroll position changes (0-1 percentage) */
25
+ onScroll?: (scrollPercent: number) => void;
24
26
  /** Additional CSS classes */
25
27
  className?: string;
26
28
  }
@@ -36,21 +38,28 @@ export interface CodeMirrorEditorRef {
36
38
  wrapSelection: (prefix: string, suffix: string) => void;
37
39
  /** Insert text at cursor position */
38
40
  insertAtCursor: (text: string) => void;
41
+ /** Scroll to percentage position (0-1). Returns true if scroll actually changed. */
42
+ scrollToPercent: (percent: number) => boolean;
39
43
  }
40
44
 
41
45
  function CodeMirrorEditorInner(
42
- { initialContent, onChange, className }: CodeMirrorEditorProps,
46
+ { initialContent, onChange, onScroll, className }: CodeMirrorEditorProps,
43
47
  ref: ForwardedRef<CodeMirrorEditorRef>
44
48
  ) {
45
49
  const containerRef = useRef<HTMLDivElement>(null);
46
50
  const viewRef = useRef<EditorView | null>(null);
47
51
  const onChangeRef = useRef(onChange);
52
+ const onScrollRef = useRef(onScroll);
48
53
 
49
- // Keep onChange ref current to avoid recreating editor on callback change
54
+ // Keep callback refs current to avoid recreating editor
50
55
  useEffect(() => {
51
56
  onChangeRef.current = onChange;
52
57
  }, [onChange]);
53
58
 
59
+ useEffect(() => {
60
+ onScrollRef.current = onScroll;
61
+ }, [onScroll]);
62
+
54
63
  // Initialize CodeMirror
55
64
  useEffect(() => {
56
65
  if (!containerRef.current) return;
@@ -81,8 +90,28 @@ function CodeMirrorEditorInner(
81
90
  parent: containerRef.current,
82
91
  });
83
92
 
93
+ // Attach scroll listener directly to scrollDOM for reliable event capture
94
+ // (scroll events don't bubble, so domEventHandlers may miss them)
95
+ const scroller = view.scrollDOM;
96
+ const handleScroll = () => {
97
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
98
+ if (maxScroll > 0 && onScrollRef.current) {
99
+ const percent = Math.max(
100
+ 0,
101
+ Math.min(1, scroller.scrollTop / maxScroll)
102
+ );
103
+ if (Number.isFinite(percent)) {
104
+ onScrollRef.current(percent);
105
+ }
106
+ }
107
+ };
108
+ scroller.addEventListener("scroll", handleScroll, { passive: true });
109
+
84
110
  viewRef.current = view;
85
- return () => view.destroy();
111
+ return () => {
112
+ scroller.removeEventListener("scroll", handleScroll);
113
+ view.destroy();
114
+ };
86
115
  // Only run on mount - initialContent should not trigger re-creation
87
116
  // eslint-disable-next-line react-hooks/exhaustive-deps
88
117
  }, []);
@@ -134,6 +163,23 @@ function CodeMirrorEditorInner(
134
163
  });
135
164
  view.focus();
136
165
  },
166
+ scrollToPercent: (percent: number): boolean => {
167
+ const view = viewRef.current;
168
+ if (!view || !Number.isFinite(percent)) return false;
169
+
170
+ const clamped = Math.max(0, Math.min(1, percent));
171
+ const scroller = view.scrollDOM;
172
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
173
+ if (maxScroll > 0) {
174
+ const targetScroll = clamped * maxScroll;
175
+ // Only scroll if position actually changes (avoids unnecessary events)
176
+ if (Math.abs(scroller.scrollTop - targetScroll) > 0.5) {
177
+ scroller.scrollTop = targetScroll;
178
+ return true;
179
+ }
180
+ }
181
+ return false;
182
+ },
137
183
  }));
138
184
 
139
185
  return <div ref={containerRef} className={className} />;