@gmickel/gno 0.11.0 → 0.13.0
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 +46 -10
- package/assets/screenshots/webui-doc-view.jpg +0 -0
- package/assets/screenshots/webui-editor.jpg +0 -0
- package/assets/screenshots/webui-graph.jpg +0 -0
- package/assets/skill/SKILL.md +15 -12
- package/assets/skill/cli-reference.md +83 -0
- package/assets/skill/examples.md +48 -0
- package/package.json +2 -1
- package/src/cli/commands/embed.ts +20 -0
- package/src/cli/commands/graph.ts +187 -0
- package/src/cli/commands/links.ts +891 -0
- package/src/cli/commands/ref-parser.ts +1 -1
- package/src/cli/commands/vec.ts +287 -0
- package/src/cli/options.ts +9 -0
- package/src/cli/program.ts +279 -1
- package/src/core/links.ts +429 -0
- package/src/embed/backlog.ts +19 -1
- package/src/ingestion/position.ts +119 -0
- package/src/ingestion/strip.ts +162 -0
- package/src/ingestion/sync.ts +58 -1
- package/src/mcp/tools/index.ts +61 -0
- package/src/mcp/tools/links.ts +722 -0
- package/src/serve/public/app.tsx +6 -1
- package/src/serve/public/components/BacklinksPanel.tsx +329 -0
- package/src/serve/public/components/OutgoingLinksPanel.tsx +336 -0
- package/src/serve/public/components/RelatedNotesSidebar.tsx +490 -0
- package/src/serve/public/components/WikiLinkAutocomplete.tsx +554 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Dashboard.tsx +11 -1
- package/src/serve/public/pages/DocView.tsx +344 -259
- package/src/serve/public/pages/DocumentEditor.tsx +69 -10
- package/src/serve/public/pages/GraphView.tsx +700 -0
- package/src/serve/routes/graph.ts +162 -0
- package/src/serve/routes/links.ts +500 -0
- package/src/serve/server.ts +49 -0
- package/src/store/migrations/004-doc-links.ts +64 -0
- package/src/store/migrations/005-graph-indexes.ts +50 -0
- package/src/store/migrations/index.ts +3 -1
- package/src/store/sqlite/adapter.ts +1075 -0
- package/src/store/types.ts +213 -0
- package/src/store/vector/sqlite-vec.ts +62 -45
- package/src/store/vector/types.ts +2 -0
package/README.md
CHANGED
|
@@ -27,6 +27,25 @@ GNO is a local knowledge engine for privacy-conscious developers and AI agents.
|
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
+
## What's New in v0.13
|
|
31
|
+
|
|
32
|
+
- **Knowledge Graph**: Interactive force-directed visualization of document connections
|
|
33
|
+
- **Graph with Similarity**: See semantic similarity as golden edges (not just wiki/markdown links)
|
|
34
|
+
- **CLI**: `gno graph` command with collection filtering and similarity options
|
|
35
|
+
- **Web UI**: `/graph` page with zoom, pan, collection filter, similarity toggle
|
|
36
|
+
- **MCP**: `gno_graph` tool for AI agents to explore document relationships
|
|
37
|
+
- **REST API**: `/api/graph` endpoint with full query parameters
|
|
38
|
+
|
|
39
|
+
### v0.12
|
|
40
|
+
|
|
41
|
+
- **Note Linking**: Wiki-style `[[links]]`, backlinks, and AI-powered related notes
|
|
42
|
+
- **Tag System**: Filter searches by frontmatter tags with `--tags-any`/`--tags-all`
|
|
43
|
+
- **Web UI**: Outgoing links panel, backlinks panel, related notes sidebar
|
|
44
|
+
- **CLI**: `gno links`, `gno backlinks`, `gno similar` commands
|
|
45
|
+
- **MCP**: `gno_links`, `gno_backlinks`, `gno_similar` tools
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
30
49
|
## Quick Start
|
|
31
50
|
|
|
32
51
|
```bash
|
|
@@ -144,16 +163,20 @@ Connect GNO to Claude Desktop, Cursor, Raycast, and more:
|
|
|
144
163
|
|
|
145
164
|

|
|
146
165
|
|
|
147
|
-
GNO exposes
|
|
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
|
-
| `
|
|
166
|
+
GNO exposes tools via [Model Context Protocol](https://modelcontextprotocol.io):
|
|
167
|
+
|
|
168
|
+
| Tool | Description |
|
|
169
|
+
| :-------------- | :------------------------------------ |
|
|
170
|
+
| `gno_search` | BM25 keyword search |
|
|
171
|
+
| `gno_vsearch` | Vector semantic search |
|
|
172
|
+
| `gno_query` | Hybrid search (recommended) |
|
|
173
|
+
| `gno_get` | Retrieve document by ID |
|
|
174
|
+
| `gno_multi_get` | Batch document retrieval |
|
|
175
|
+
| `gno_links` | Get outgoing links from document |
|
|
176
|
+
| `gno_backlinks` | Get documents linking TO document |
|
|
177
|
+
| `gno_similar` | Find semantically similar documents |
|
|
178
|
+
| `gno_graph` | Get knowledge graph (nodes and edges) |
|
|
179
|
+
| `gno_status` | Index health check |
|
|
157
180
|
|
|
158
181
|
**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
182
|
|
|
@@ -201,6 +224,18 @@ Full-featured markdown editor with:
|
|
|
201
224
|
| **Keyboard Shortcuts** | ⌘S save, ⌘B bold, ⌘I italic, ⌘K link |
|
|
202
225
|
| **Quick Capture** | ⌘N creates new note from anywhere |
|
|
203
226
|
|
|
227
|
+
### Document Viewer
|
|
228
|
+
|
|
229
|
+

|
|
230
|
+
|
|
231
|
+
View documents with full context: outgoing links, backlinks, and AI-powered related notes sidebar.
|
|
232
|
+
|
|
233
|
+
### Knowledge Graph
|
|
234
|
+
|
|
235
|
+

|
|
236
|
+
|
|
237
|
+
Interactive visualization of document connections. Wiki links, markdown links, and optional similarity edges rendered as a navigable constellation.
|
|
238
|
+
|
|
204
239
|
### Collections Management
|
|
205
240
|
|
|
206
241
|

|
|
@@ -315,6 +350,7 @@ graph TD
|
|
|
315
350
|
| **MCP Server** | Works with Claude Desktop, Cursor, Zed, + 8 more |
|
|
316
351
|
| **Collections** | Organize sources with patterns, excludes, contexts |
|
|
317
352
|
| **Tag Filtering** | Frontmatter tags with hierarchical paths, filter via `--tags-any`/`--tags-all` |
|
|
353
|
+
| **Note Linking** | Wiki links, backlinks, related notes, cross-collection navigation |
|
|
318
354
|
| **Multilingual** | 30+ languages, auto-detection, cross-lingual search |
|
|
319
355
|
| **Incremental** | SHA-256 tracking, only changed files re-indexed |
|
|
320
356
|
| **Keyboard First** | ⌘N capture, ⌘K search, ⌘/ shortcuts, ⌘S save |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/assets/skill/SKILL.md
CHANGED
|
@@ -20,6 +20,8 @@ Fast local semantic search. Index once, search instantly. No cloud, no API keys.
|
|
|
20
20
|
- User wants a **web UI** to browse/search documents
|
|
21
21
|
- User asks to **get AI answers** from their documents
|
|
22
22
|
- User wants to **tag, categorize, or filter** documents
|
|
23
|
+
- User asks about **backlinks, wiki links, or related notes**
|
|
24
|
+
- User wants to **visualize document connections** or see a **knowledge graph**
|
|
23
25
|
|
|
24
26
|
## Quick Start
|
|
25
27
|
|
|
@@ -32,18 +34,19 @@ gno search "your query" # BM25 keyword search
|
|
|
32
34
|
|
|
33
35
|
## Command Overview
|
|
34
36
|
|
|
35
|
-
| Category | Commands | Description
|
|
36
|
-
| ------------ | ---------------------------------------------------------------- |
|
|
37
|
-
| **Search** | `search`, `vsearch`, `query`, `ask` | Find documents by keywords, meaning, or get AI answers
|
|
38
|
-
| **
|
|
39
|
-
| **
|
|
40
|
-
| **
|
|
41
|
-
| **
|
|
42
|
-
| **
|
|
43
|
-
| **
|
|
44
|
-
| **
|
|
45
|
-
| **
|
|
46
|
-
| **
|
|
37
|
+
| Category | Commands | Description |
|
|
38
|
+
| ------------ | ---------------------------------------------------------------- | --------------------------------------------------------- |
|
|
39
|
+
| **Search** | `search`, `vsearch`, `query`, `ask` | Find documents by keywords, meaning, or get AI answers |
|
|
40
|
+
| **Links** | `links`, `backlinks`, `similar`, `graph` | Navigate document relationships and visualize connections |
|
|
41
|
+
| **Retrieve** | `get`, `multi-get`, `ls` | Fetch document content by URI or ID |
|
|
42
|
+
| **Index** | `init`, `collection add/list/remove`, `index`, `update`, `embed` | Set up and maintain document index |
|
|
43
|
+
| **Tags** | `tags`, `tags add`, `tags rm` | Organize and filter documents |
|
|
44
|
+
| **Context** | `context add/list/rm/check` | Add hints to improve search relevance |
|
|
45
|
+
| **Models** | `models list/use/pull/clear/path` | Manage local AI models |
|
|
46
|
+
| **Serve** | `serve` | Web UI for browsing and searching |
|
|
47
|
+
| **MCP** | `mcp`, `mcp install/uninstall/status` | AI assistant integration |
|
|
48
|
+
| **Skill** | `skill install/uninstall/show/paths` | Install skill for AI agents |
|
|
49
|
+
| **Admin** | `status`, `doctor`, `cleanup`, `reset`, `vec`, `completion` | Maintenance and diagnostics |
|
|
47
50
|
|
|
48
51
|
## Search Modes
|
|
49
52
|
|
|
@@ -234,6 +234,76 @@ Validate context configuration.
|
|
|
234
234
|
gno context check [--json]
|
|
235
235
|
```
|
|
236
236
|
|
|
237
|
+
## Note Linking
|
|
238
|
+
|
|
239
|
+
### gno links
|
|
240
|
+
|
|
241
|
+
List outgoing links from a document.
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
gno links <ref> [options]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
| Option | Description |
|
|
248
|
+
| ------------ | ------------------------------------ |
|
|
249
|
+
| `--type` | Filter: `wiki`, `markdown`, or `all` |
|
|
250
|
+
| `--resolved` | Only show resolved links |
|
|
251
|
+
| `--broken` | Only show broken links |
|
|
252
|
+
| `--json` | JSON output |
|
|
253
|
+
|
|
254
|
+
### gno backlinks
|
|
255
|
+
|
|
256
|
+
Find documents linking TO a target.
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
gno backlinks <ref> [options]
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
| Option | Description |
|
|
263
|
+
| -------- | ------------------------- |
|
|
264
|
+
| `-n` | Max results (default: 20) |
|
|
265
|
+
| `--json` | JSON output |
|
|
266
|
+
|
|
267
|
+
### gno similar
|
|
268
|
+
|
|
269
|
+
Find semantically similar documents.
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
gno similar <ref> [options]
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
| Option | Description |
|
|
276
|
+
| -------------------- | ----------------------------- |
|
|
277
|
+
| `-n` | Max results (default: 5) |
|
|
278
|
+
| `--threshold` | Min similarity (0-1) |
|
|
279
|
+
| `--cross-collection` | Search across all collections |
|
|
280
|
+
| `--json` | JSON output |
|
|
281
|
+
|
|
282
|
+
**Requirements**: Embeddings must exist for source and target documents.
|
|
283
|
+
|
|
284
|
+
### gno graph
|
|
285
|
+
|
|
286
|
+
Generate knowledge graph of document connections.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
gno graph [options]
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
| Option | Default | Description |
|
|
293
|
+
| ------------------ | ------- | ------------------------------ |
|
|
294
|
+
| `-c, --collection` | all | Filter to single collection |
|
|
295
|
+
| `--limit` | 2000 | Max nodes |
|
|
296
|
+
| `--edge-limit` | 10000 | Max edges |
|
|
297
|
+
| `--similar` | false | Include similarity edges |
|
|
298
|
+
| `--threshold` | 0.7 | Similarity threshold (0-1) |
|
|
299
|
+
| `--linked-only` | true | Exclude isolated nodes |
|
|
300
|
+
| `--similar-top-k` | 5 | Similar docs per node (max 20) |
|
|
301
|
+
| `--json` | - | JSON output |
|
|
302
|
+
|
|
303
|
+
**Edge types**: `wiki` (wiki links), `markdown` (md links), `similar` (vector similarity).
|
|
304
|
+
|
|
305
|
+
**Web UI**: Access interactive graph at `http://localhost:3000/graph` via `gno serve`.
|
|
306
|
+
|
|
237
307
|
## Tags
|
|
238
308
|
|
|
239
309
|
### gno tags
|
|
@@ -324,6 +394,19 @@ gno doctor [--json|--md]
|
|
|
324
394
|
gno cleanup
|
|
325
395
|
```
|
|
326
396
|
|
|
397
|
+
### gno vec
|
|
398
|
+
|
|
399
|
+
Vector index maintenance. Use when `gno similar` returns empty despite embeddings.
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
gno vec sync # Fast incremental sync
|
|
403
|
+
gno vec rebuild # Full rebuild
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
| Option | Description |
|
|
407
|
+
| -------- | ----------- |
|
|
408
|
+
| `--json` | JSON output |
|
|
409
|
+
|
|
327
410
|
## MCP Server
|
|
328
411
|
|
|
329
412
|
### gno mcp
|
package/assets/skill/examples.md
CHANGED
|
@@ -206,6 +206,54 @@ gno models use quality
|
|
|
206
206
|
gno models pull
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
+
## Note Linking
|
|
210
|
+
|
|
211
|
+
### View outgoing links
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# List all links from a document
|
|
215
|
+
gno links gno://notes/readme.md
|
|
216
|
+
|
|
217
|
+
# Filter by type
|
|
218
|
+
gno links gno://notes/readme.md --type wiki
|
|
219
|
+
gno links gno://notes/readme.md --type markdown
|
|
220
|
+
|
|
221
|
+
# Show only broken links
|
|
222
|
+
gno links gno://notes/readme.md --broken
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Find backlinks
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Find all documents linking TO this one
|
|
229
|
+
gno backlinks gno://notes/api-design.md
|
|
230
|
+
|
|
231
|
+
# More results
|
|
232
|
+
gno backlinks gno://notes/api-design.md -n 50
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Find related notes
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# Find semantically similar documents
|
|
239
|
+
gno similar gno://notes/auth.md
|
|
240
|
+
|
|
241
|
+
# Stricter threshold (default: 0.7)
|
|
242
|
+
gno similar gno://notes/auth.md --threshold 0.85
|
|
243
|
+
|
|
244
|
+
# Search across all collections
|
|
245
|
+
gno similar gno://notes/auth.md --cross-collection
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Wiki link syntax
|
|
249
|
+
|
|
250
|
+
```markdown
|
|
251
|
+
# In your documents:
|
|
252
|
+
See [[API Design]] for details.
|
|
253
|
+
Check [[work:Project Plan]] for cross-collection link.
|
|
254
|
+
Read [[Security#OAuth]] for specific section.
|
|
255
|
+
```
|
|
256
|
+
|
|
209
257
|
## Tagging
|
|
210
258
|
|
|
211
259
|
### List and manage tags
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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",
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"picocolors": "^1.1.1",
|
|
102
102
|
"react": "^19.2.3",
|
|
103
103
|
"react-dom": "^19.2.3",
|
|
104
|
+
"react-force-graph-2d": "^1.29.0",
|
|
104
105
|
"react-markdown": "^10.1.0",
|
|
105
106
|
"rehype-sanitize": "^6.0.0",
|
|
106
107
|
"remark-gfm": "^4.0.1",
|
|
@@ -372,6 +372,26 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
|
|
|
372
372
|
return { success: false, error: result.error };
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
// Sync vec index if any vec0 writes failed (matches embedBacklog behavior)
|
|
376
|
+
if (vectorIndex.vecDirty) {
|
|
377
|
+
const syncResult = await vectorIndex.syncVecIndex();
|
|
378
|
+
if (syncResult.ok) {
|
|
379
|
+
const { added, removed } = syncResult.value;
|
|
380
|
+
if (added > 0 || removed > 0) {
|
|
381
|
+
if (!options.json) {
|
|
382
|
+
process.stdout.write(
|
|
383
|
+
`\n[vec] Synced index: +${added} -${removed}\n`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
vectorIndex.vecDirty = false;
|
|
388
|
+
} else if (!options.json) {
|
|
389
|
+
process.stdout.write(
|
|
390
|
+
`\n[vec] Sync failed: ${syncResult.error.message}\n`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
375
395
|
return {
|
|
376
396
|
success: true,
|
|
377
397
|
embedded: result.embedded,
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gno graph command implementation.
|
|
3
|
+
* Outputs knowledge graph of document links.
|
|
4
|
+
*
|
|
5
|
+
* @module src/cli/commands/graph
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { GetGraphOptions, GraphResult } from "../../store/types";
|
|
9
|
+
|
|
10
|
+
import { initStore } from "./shared";
|
|
11
|
+
|
|
12
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
// Types
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface GraphOptions {
|
|
17
|
+
/** Override config path */
|
|
18
|
+
configPath?: string;
|
|
19
|
+
/** Filter by collection */
|
|
20
|
+
collection?: string;
|
|
21
|
+
/** Max nodes (default 2000) */
|
|
22
|
+
limitNodes?: number;
|
|
23
|
+
/** Max edges (default 10000) */
|
|
24
|
+
limitEdges?: number;
|
|
25
|
+
/** Include similarity edges */
|
|
26
|
+
includeSimilar?: boolean;
|
|
27
|
+
/** Similarity threshold (0-1) */
|
|
28
|
+
threshold?: number;
|
|
29
|
+
/** Include isolated nodes */
|
|
30
|
+
includeIsolated?: boolean;
|
|
31
|
+
/** Similar top-K per node */
|
|
32
|
+
similarTopK?: number;
|
|
33
|
+
/** Output format: json (default), dot, mermaid */
|
|
34
|
+
format?: "json" | "dot" | "mermaid";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type GraphCommandResult =
|
|
38
|
+
| { success: true; data: GraphResult }
|
|
39
|
+
| { success: false; error: string; isValidation?: boolean };
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// Command: graph
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Execute gno graph command.
|
|
47
|
+
* Returns knowledge graph of document links.
|
|
48
|
+
*/
|
|
49
|
+
export async function graph(
|
|
50
|
+
options: GraphOptions = {}
|
|
51
|
+
): Promise<GraphCommandResult> {
|
|
52
|
+
const initResult = await initStore({ configPath: options.configPath });
|
|
53
|
+
if (!initResult.ok) {
|
|
54
|
+
return { success: false, error: initResult.error };
|
|
55
|
+
}
|
|
56
|
+
const { store } = initResult;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const storeOptions: GetGraphOptions = {
|
|
60
|
+
collection: options.collection,
|
|
61
|
+
limitNodes: options.limitNodes,
|
|
62
|
+
limitEdges: options.limitEdges,
|
|
63
|
+
includeSimilar: options.includeSimilar,
|
|
64
|
+
threshold: options.threshold,
|
|
65
|
+
linkedOnly: !options.includeIsolated,
|
|
66
|
+
similarTopK: options.similarTopK,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const result = await store.getGraph(storeOptions);
|
|
70
|
+
if (!result.ok) {
|
|
71
|
+
return { success: false, error: result.error.message };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { success: true, data: result.value };
|
|
75
|
+
} finally {
|
|
76
|
+
await store.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// Formatters
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/** Escape string for DOT format (double quotes, newlines) */
|
|
85
|
+
function escapeDot(s: string): string {
|
|
86
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Escape string for Mermaid (quotes, brackets, newlines) */
|
|
90
|
+
function escapeMermaid(s: string): string {
|
|
91
|
+
return s
|
|
92
|
+
.replace(/\\/g, "\\\\")
|
|
93
|
+
.replace(/"/g, "#quot;")
|
|
94
|
+
.replace(/\[/g, "#lbrack;")
|
|
95
|
+
.replace(/\]/g, "#rbrack;")
|
|
96
|
+
.replace(/\n/g, " ");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format graph result as DOT (Graphviz).
|
|
101
|
+
*/
|
|
102
|
+
export function formatDot(result: GraphResult): string {
|
|
103
|
+
const lines: string[] = ["digraph G {"];
|
|
104
|
+
lines.push(" rankdir=LR;");
|
|
105
|
+
lines.push(
|
|
106
|
+
' node [shape=box, style="rounded,filled", fillcolor="#f0f0f0"];'
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Nodes
|
|
110
|
+
for (const node of result.nodes) {
|
|
111
|
+
const label = escapeDot(node.title ?? node.id);
|
|
112
|
+
lines.push(` "${escapeDot(node.id)}" [label="${label}"];`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Edges
|
|
116
|
+
for (const link of result.links) {
|
|
117
|
+
const style =
|
|
118
|
+
link.type === "similar"
|
|
119
|
+
? ' [style=dashed, color="#888888", dir=none]'
|
|
120
|
+
: "";
|
|
121
|
+
lines.push(
|
|
122
|
+
` "${escapeDot(link.source)}" -> "${escapeDot(link.target)}"${style};`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
lines.push("}");
|
|
127
|
+
return lines.join("\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Format graph result as Mermaid.
|
|
132
|
+
*/
|
|
133
|
+
export function formatMermaid(result: GraphResult): string {
|
|
134
|
+
const lines: string[] = ["graph LR"];
|
|
135
|
+
|
|
136
|
+
// Build node ID map (Mermaid needs simple IDs)
|
|
137
|
+
const nodeIds = new Map<string, string>();
|
|
138
|
+
result.nodes.forEach((node, i) => {
|
|
139
|
+
nodeIds.set(node.id, `n${i}`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Nodes with labels
|
|
143
|
+
for (const node of result.nodes) {
|
|
144
|
+
const mermaidId = nodeIds.get(node.id) ?? node.id;
|
|
145
|
+
const label = escapeMermaid(node.title ?? node.id);
|
|
146
|
+
lines.push(` ${mermaidId}["${label}"]`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Edges
|
|
150
|
+
for (const link of result.links) {
|
|
151
|
+
const sourceId = nodeIds.get(link.source) ?? link.source;
|
|
152
|
+
const targetId = nodeIds.get(link.target) ?? link.target;
|
|
153
|
+
const arrow = link.type === "similar" ? "---" : "-->";
|
|
154
|
+
lines.push(` ${sourceId} ${arrow} ${targetId}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return lines.join("\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Format graph result for output.
|
|
162
|
+
*/
|
|
163
|
+
export function formatGraph(
|
|
164
|
+
result: GraphCommandResult,
|
|
165
|
+
options: GraphOptions
|
|
166
|
+
): string {
|
|
167
|
+
if (!result.success) {
|
|
168
|
+
if (options.format === "json" || !options.format) {
|
|
169
|
+
return JSON.stringify({
|
|
170
|
+
error: { code: "GRAPH_FAILED", message: result.error },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return `Error: ${result.error}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { data } = result;
|
|
177
|
+
|
|
178
|
+
switch (options.format) {
|
|
179
|
+
case "dot":
|
|
180
|
+
return formatDot(data);
|
|
181
|
+
case "mermaid":
|
|
182
|
+
return formatMermaid(data);
|
|
183
|
+
case "json":
|
|
184
|
+
default:
|
|
185
|
+
return JSON.stringify(data, null, 2);
|
|
186
|
+
}
|
|
187
|
+
}
|