opentology 0.2.0 → 0.2.1

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,6 +1,6 @@
1
1
  # OpenTology
2
2
 
3
- > CLI-managed RDF/SPARQL infrastructure with RDFS reasoning and SHACL validation -- Supabase for knowledge graphs
3
+ > CLI-managed RDF/SPARQL infrastructure with RDFS reasoning, SHACL validation, and interactive graph visualization -- Supabase for knowledge graphs
4
4
 
5
5
  [English](#english) | [한국어](#한국어)
6
6
 
@@ -22,7 +22,7 @@ graph TB
22
22
  end
23
23
 
24
24
  subgraph MCP["MCP Server"]
25
- Tools[18 Tools]
25
+ Tools[20 Tools]
26
26
  Resource["opentology://schema"]
27
27
  end
28
28
 
@@ -42,7 +42,7 @@ graph TB
42
42
  Pipeline --> Adapter
43
43
  ```
44
44
 
45
- Existing ontology tools have terrible developer experience. OpenTology gives you managed RDF with a simple CLI -- initialize a project, write Turtle, validate with SHACL, push with automatic RDFS inference, and query, all from your terminal. It ships an MCP server so AI assistants can manage your knowledge graph directly. It runs in embedded mode with zero Docker dependency, or connects to an Oxigraph server over HTTP.
45
+ Existing ontology tools have terrible developer experience. OpenTology gives you managed RDF with a simple CLI -- initialize a project, write Turtle, validate with SHACL, push with automatic RDFS inference, query, and visualize your graph in an interactive web UI, all from your terminal. It ships an MCP server so AI assistants can manage your knowledge graph directly. It runs in embedded mode with zero Docker dependency, or connects to an Oxigraph server over HTTP.
46
46
 
47
47
  ### System Requirements
48
48
 
@@ -97,7 +97,7 @@ graph LR
97
97
  | Docker required | No (embedded mode) | Yes | Yes |
98
98
  | RDFS reasoning | Automatic on push | Manual SPARQL CONSTRUCT | Not native |
99
99
  | SHACL validation | Built-in | Manual tooling | N/A |
100
- | AI integration | MCP server with 18 tools | None | Plugin ecosystem |
100
+ | AI integration | MCP server with 20 tools | None | Plugin ecosystem |
101
101
  | Query language | SPARQL (auto-prefixed) | SPARQL (raw) | Cypher |
102
102
  | Data format | Turtle files | Turtle/N-Triples | Property graph |
103
103
  | Project scoping | Automatic named graphs | Manual | Database-level |
@@ -106,7 +106,7 @@ graph LR
106
106
 
107
107
  **Core**
108
108
 
109
- - 14 CLI commands covering the full RDF lifecycle
109
+ - 16 CLI commands covering the full RDF lifecycle
110
110
  - Project-level configuration with `.opentology.json`
111
111
  - Named graph scoping -- queries are automatically scoped to your project
112
112
  - Two modes: HTTP (Oxigraph server) and embedded (WASM, zero Docker)
@@ -126,10 +126,16 @@ graph LR
126
126
 
127
127
  **AI Integration**
128
128
 
129
- - MCP server with 18 tools and 1 resource
129
+ - MCP server with 20 tools and 1 resource
130
130
  - `opentology://schema` resource auto-loads ontology overview
131
131
  - Works with Claude Code, Cursor, and any MCP-compatible client
132
132
 
133
+ **Visualization**
134
+
135
+ - Interactive graph visualization web UI (`opentology context graph`)
136
+ - Explore classes, instances, and relationships visually with vis-network
137
+ - SPARQL query editor, node filtering, search, and focus mode
138
+
133
139
  ### Two Modes
134
140
 
135
141
  | | HTTP Mode | Embedded Mode |
@@ -175,6 +181,8 @@ For embedded mode, no additional setup is needed.
175
181
  | `opentology infer` | Run RDFS materialization (`--clear` to remove inferred triples) |
176
182
  | `opentology graph` | List, create, or drop named graphs |
177
183
  | `opentology prefix` | List, add, or remove project prefixes |
184
+ | `opentology context` | Project context management (`init`, `load`, `status`, `scan`, `graph`) |
185
+ | `opentology viz` | Visualize ontology schema (`schema`) |
178
186
  | `opentology mcp` | Start the MCP server |
179
187
 
180
188
  ### MCP Integration
@@ -192,7 +200,7 @@ Add to your MCP client configuration (`.mcp.json`):
192
200
  }
193
201
  ```
194
202
 
195
- **18 Tools:**
203
+ **20 Tools:**
196
204
 
197
205
  | Tool | Description |
198
206
  |------|-------------|
@@ -210,10 +218,12 @@ Add to your MCP client configuration (`.mcp.json`):
210
218
  | `opentology_graph_list` | List named graphs |
211
219
  | `opentology_graph_create` | Create a named graph |
212
220
  | `opentology_graph_drop` | Drop a named graph |
221
+ | `opentology_visualize` | Generate schema visualization (Mermaid/DOT) |
213
222
  | `opentology_context_init` | Initialize project context graph |
214
223
  | `opentology_context_load` | Load project context |
215
224
  | `opentology_context_status` | Check context initialization status |
216
225
  | `opentology_context_scan` | Scan codebase (module or symbol-level) |
226
+ | `opentology_context_graph` | Start interactive graph visualization web UI |
217
227
 
218
228
  **1 Resource:**
219
229
 
@@ -324,8 +334,8 @@ npm install web-tree-sitter tree-sitter-wasms
324
334
 
325
335
  ### Roadmap
326
336
 
327
- - [x] CLI with 14 commands
328
- - [x] MCP server with 18 tools and 1 resource
337
+ - [x] CLI with 16 commands
338
+ - [x] MCP server with 20 tools and 1 resource
329
339
  - [x] Schema introspection (MCP resource + tool)
330
340
  - [x] Complete CRUD (push --replace, drop, delete)
331
341
  - [x] SHACL validation (shape constraints on push)
@@ -338,7 +348,7 @@ npm install web-tree-sitter tree-sitter-wasms
338
348
  - [ ] OWL reasoning (owl:sameAs, owl:inverseOf)
339
349
  - [ ] Remote ontology import
340
350
  - [ ] Version control for ontology snapshots
341
- - [ ] Web UI for graph exploration
351
+ - [x] Interactive graph visualization web UI
342
352
 
343
353
  ### Contributing
344
354
 
@@ -369,7 +379,7 @@ graph TB
369
379
  end
370
380
 
371
381
  subgraph MCP["MCP Server"]
372
- Tools[18 Tools]
382
+ Tools[20 Tools]
373
383
  Resource["opentology://schema"]
374
384
  end
375
385
 
@@ -389,7 +399,7 @@ graph TB
389
399
  Pipeline --> Adapter
390
400
  ```
391
401
 
392
- 온톨로지 도구들은 개발자 경험이 열악합니다. OpenTology는 터미널에서 RDF의 전체 생애주기를 관리합니다. 프로젝트 초기화, Turtle 작성, SHACL 검증, RDFS 추론이 포함된 푸시, SPARQL 쿼리까지 CLI 하나로 처리합니다. MCP 서버를 내장하고 있어 AI 어시스턴트가 지식 그래프를 직접 다룰 수 있고, Docker 없이 임베디드 모드로 바로 시작할 수 있습니다.
402
+ 온톨로지 도구들은 개발자 경험이 열악합니다. OpenTology는 터미널에서 RDF의 전체 생애주기를 관리합니다. 프로젝트 초기화, Turtle 작성, SHACL 검증, RDFS 추론이 포함된 푸시, SPARQL 쿼리, 인터랙티브 그래프 시각화까지 CLI 하나로 처리합니다. MCP 서버를 내장하고 있어 AI 어시스턴트가 지식 그래프를 직접 다룰 수 있고, Docker 없이 임베디드 모드로 바로 시작할 수 있습니다.
393
403
 
394
404
  ### 시스템 요구사항
395
405
 
@@ -444,7 +454,7 @@ graph LR
444
454
  | Docker 필수 | 아니오 (임베디드 모드) | 예 | 예 |
445
455
  | RDFS 추론 | 푸시 시 자동 | SPARQL CONSTRUCT 수동 작성 | 네이티브 미지원 |
446
456
  | SHACL 검증 | 내장 | 별도 도구 필요 | 해당 없음 |
447
- | AI 연동 | MCP 서버 18개 도구 | 없음 | 플러그인 생태계 |
457
+ | AI 연동 | MCP 서버 20개 도구 | 없음 | 플러그인 생태계 |
448
458
  | 쿼리 언어 | SPARQL (접두사 자동 삽입) | SPARQL (수동) | Cypher |
449
459
  | 데이터 형식 | Turtle 파일 | Turtle/N-Triples | 속성 그래프 |
450
460
  | 프로젝트 구분 | Named Graph 자동 | 수동 관리 | 데이터베이스 단위 |
@@ -453,7 +463,7 @@ graph LR
453
463
 
454
464
  **핵심**
455
465
 
456
- - RDF 전체 생애주기를 다루는 14개 CLI 명령어
466
+ - RDF 전체 생애주기를 다루는 16개 CLI 명령어
457
467
  - `.opentology.json` 기반 프로젝트 설정
458
468
  - Named Graph 자동 스코핑 -- 쿼리가 프로젝트 그래프에 자동 한정
459
469
  - HTTP 모드(Oxigraph 서버)와 임베디드 모드(WASM, Docker 불필요) 지원
@@ -473,10 +483,16 @@ graph LR
473
483
 
474
484
  **AI 연동**
475
485
 
476
- - 18개 도구와 1개 리소스를 제공하는 MCP 서버
486
+ - 20개 도구와 1개 리소스를 제공하는 MCP 서버
477
487
  - `opentology://schema` 리소스로 온톨로지 개요 자동 로드
478
488
  - Claude Code, Cursor 등 MCP 호환 클라이언트와 연동
479
489
 
490
+ **시각화**
491
+
492
+ - 인터랙티브 그래프 시각화 웹 UI (`opentology context graph`)
493
+ - vis-network로 클래스, 인스턴스, 관계를 시각적으로 탐색
494
+ - SPARQL 쿼리 편집기, 노드 필터링, 검색, 포커스 모드
495
+
480
496
  ### 두 가지 모드
481
497
 
482
498
  | | HTTP 모드 | 임베디드 모드 |
@@ -522,6 +538,8 @@ docker run -p 7878:7878 ghcr.io/oxigraph/oxigraph \
522
538
  | `opentology infer` | RDFS 물질화 실행 (`--clear`로 추론 트리플 제거) |
523
539
  | `opentology graph` | Named Graph 목록/생성/삭제 |
524
540
  | `opentology prefix` | 프로젝트 접두사 목록/추가/제거 |
541
+ | `opentology context` | 프로젝트 컨텍스트 관리 (`init`, `load`, `status`, `scan`, `graph`) |
542
+ | `opentology viz` | 온톨로지 스키마 시각화 (`schema`) |
525
543
  | `opentology mcp` | MCP 서버 시작 |
526
544
 
527
545
  ### MCP 연동
@@ -539,7 +557,7 @@ MCP 클라이언트 설정 파일(`.mcp.json`)에 추가:
539
557
  }
540
558
  ```
541
559
 
542
- **18개 도구:**
560
+ **20개 도구:**
543
561
 
544
562
  | 도구 | 설명 |
545
563
  |------|------|
@@ -557,10 +575,12 @@ MCP 클라이언트 설정 파일(`.mcp.json`)에 추가:
557
575
  | `opentology_graph_list` | Named Graph 목록 조회 |
558
576
  | `opentology_graph_create` | Named Graph 생성 |
559
577
  | `opentology_graph_drop` | Named Graph 삭제 |
578
+ | `opentology_visualize` | 스키마 시각화 생성 (Mermaid/DOT) |
560
579
  | `opentology_context_init` | 프로젝트 컨텍스트 그래프 초기화 |
561
580
  | `opentology_context_load` | 프로젝트 컨텍스트 로드 |
562
581
  | `opentology_context_status` | 컨텍스트 초기화 상태 확인 |
563
582
  | `opentology_context_scan` | 코드베이스 스캔 (모듈/심볼 수준) |
583
+ | `opentology_context_graph` | 인터랙티브 그래프 시각화 웹 UI 시작 |
564
584
 
565
585
  **1개 리소스:**
566
586
 
@@ -671,8 +691,8 @@ npm install web-tree-sitter tree-sitter-wasms
671
691
 
672
692
  ### 로드맵
673
693
 
674
- - [x] 14개 CLI 명령어
675
- - [x] 18개 도구와 1개 리소스를 갖춘 MCP 서버
694
+ - [x] 16개 CLI 명령어
695
+ - [x] 20개 도구와 1개 리소스를 갖춘 MCP 서버
676
696
  - [x] 스키마 조회 (MCP 리소스 + 도구)
677
697
  - [x] 완전한 CRUD (push --replace, drop, delete)
678
698
  - [x] SHACL 검증 (푸시 시 형상 제약 자동 검증)
@@ -685,7 +705,7 @@ npm install web-tree-sitter tree-sitter-wasms
685
705
  - [ ] OWL 추론 (owl:sameAs, owl:inverseOf)
686
706
  - [ ] 원격 온톨로지 임포트
687
707
  - [ ] 온톨로지 스냅샷 버전 관리
688
- - [ ] 그래프 탐색 웹 UI
708
+ - [x] 인터랙티브 그래프 시각화 웹 UI
689
709
 
690
710
  ### 기여 방법
691
711
 
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, writeFileSync, unlinkSync, readdirSync } from 'n
3
3
  import { join } from 'node:path';
4
4
  import { loadConfig, saveConfig } from '../lib/config.js';
5
5
  import { createReadyAdapter } from '../lib/store-factory.js';
6
+ import { startGraphServer } from '../lib/graph-server.js';
6
7
  import { OTX_BOOTSTRAP_TURTLE } from '../templates/otx-ontology.js';
7
8
  import { generateContextSection, updateClaudeMd } from '../templates/claude-md-context.js';
8
9
  import { generateHookScript } from '../templates/session-start-hook.js';
@@ -366,4 +367,38 @@ export function registerContext(program) {
366
367
  console.log('CLAUDE.md: ' + (hasMarkers ? pc.green('markers present') : pc.yellow('markers missing')));
367
368
  }
368
369
  });
370
+ // --- context graph ---
371
+ context
372
+ .command('graph')
373
+ .description('Open interactive graph visualization in the browser')
374
+ .option('--port <number>', 'Server port (default: auto)', parseInt)
375
+ .action(async (opts) => {
376
+ let config;
377
+ try {
378
+ config = loadConfig();
379
+ }
380
+ catch {
381
+ console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
382
+ process.exit(1);
383
+ }
384
+ const graphs = config.graphs ?? {};
385
+ if (!graphs['context']) {
386
+ console.error(pc.red('Error: Context not initialized. Run `opentology context init` first.'));
387
+ process.exit(1);
388
+ }
389
+ try {
390
+ const { port } = await startGraphServer({ port: opts.port });
391
+ const url = `http://localhost:${port}`;
392
+ console.log(pc.green(`Graph server running at ${url}`));
393
+ // Open browser
394
+ const { exec } = await import('node:child_process');
395
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
396
+ exec(`${cmd} ${url}`);
397
+ console.log(pc.dim('Press Ctrl+C to stop the server.'));
398
+ }
399
+ catch (err) {
400
+ console.error(pc.red(`Error: ${err.message}`));
401
+ process.exit(1);
402
+ }
403
+ });
369
404
  }
@@ -0,0 +1,7 @@
1
+ export interface GraphServerOptions {
2
+ port?: number;
3
+ }
4
+ export declare function startGraphServer(opts?: GraphServerOptions): Promise<{
5
+ port: number;
6
+ close: () => void;
7
+ }>;
@@ -0,0 +1,531 @@
1
+ import { createServer } from 'node:http';
2
+ import { loadConfig } from './config.js';
3
+ import { createReadyAdapter } from './store-factory.js';
4
+ function html(config) {
5
+ const projectId = config.projectId;
6
+ return `<!DOCTYPE html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="UTF-8" />
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
11
+ <title>OpenTology — ${projectId}</title>
12
+ <script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
13
+ <style>
14
+ * { margin: 0; padding: 0; box-sizing: border-box; }
15
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: #ffffff; color: #1f2328; }
16
+ #app { display: flex; height: 100vh; overflow: hidden; }
17
+ #sidebar { width: 320px; min-width: 320px; background: #f6f8fa; border-right: 1px solid #d1d9e0; display: flex; flex-direction: column; overflow: hidden; }
18
+ #sidebar h1 { padding: 16px; font-size: 16px; border-bottom: 1px solid #d1d9e0; color: #0550ae; }
19
+ #graph-list { padding: 8px 16px; border-bottom: 1px solid #d1d9e0; }
20
+ #graph-list select { width: 100%; padding: 6px 8px; background: #ffffff; color: #1f2328; border: 1px solid #d1d9e0; border-radius: 6px; font-size: 13px; }
21
+ #query-box { padding: 12px 16px; border-bottom: 1px solid #d1d9e0; }
22
+ #query-box textarea { width: 100%; height: 80px; padding: 8px; background: #ffffff; color: #1f2328; border: 1px solid #d1d9e0; border-radius: 6px; font-family: 'SF Mono', Monaco, monospace; font-size: 12px; resize: vertical; }
23
+ #query-box button { margin-top: 8px; padding: 6px 16px; background: #1a7f37; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
24
+ #query-box button:hover { background: #218739; }
25
+ #details { flex: 1; overflow-y: auto; padding: 12px 16px; font-size: 13px; }
26
+ #details h3 { color: #0550ae; margin-bottom: 8px; }
27
+ #details table { width: 100%; border-collapse: collapse; }
28
+ #details td { padding: 4px 6px; border-bottom: 1px solid #d1d9e0; word-break: break-all; }
29
+ #details td:first-child { color: #656d76; width: 90px; }
30
+ #results-table { display: none; padding: 12px 16px; overflow: auto; max-height: 300px; border-bottom: 1px solid #d1d9e0; }
31
+ #results-table table { width: 100%; border-collapse: collapse; font-size: 12px; }
32
+ #results-table th { text-align: left; padding: 6px 8px; background: #eef1f5; border-bottom: 2px solid #d1d9e0; color: #0550ae; position: sticky; top: 0; }
33
+ #results-table td { padding: 4px 8px; border-bottom: 1px solid #d1d9e0; word-break: break-all; }
34
+ #results-table tr:hover td { background: #f0f4ff; }
35
+ #center { flex: 1; display: flex; flex-direction: column; position: relative; min-height: 0; overflow: hidden; }
36
+ #network-wrap { flex: 1; position: relative; min-height: 0; overflow: hidden; }
37
+ #network { position: absolute; top: 0; left: 0; right: 0; bottom: 0; }
38
+ #focus-bar { display: none; padding: 8px 16px; background: #ddf4ff; border-bottom: 1px solid #54aeff; font-size: 13px; color: #0550ae; align-items: center; gap: 8px; }
39
+ #focus-bar button { padding: 4px 12px; background: #0550ae; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; }
40
+ #focus-bar button:hover { background: #0969da; }
41
+ #filter-panel { width: 280px; min-width: 280px; background: #f6f8fa; border-left: 1px solid #d1d9e0; display: flex; flex-direction: column; overflow: hidden; }
42
+ .panel-section { border-bottom: 1px solid #d1d9e0; }
43
+ .panel-header { padding: 10px 16px; font-size: 13px; font-weight: 600; color: #1f2328; cursor: pointer; display: flex; justify-content: space-between; align-items: center; user-select: none; }
44
+ .panel-header:hover { background: #eef1f5; }
45
+ .panel-header .arrow { font-size: 10px; color: #656d76; transition: transform 0.15s; }
46
+ .panel-header .arrow.collapsed { transform: rotate(-90deg); }
47
+ .panel-body { padding: 4px 16px 10px; }
48
+ .panel-body.collapsed { display: none; }
49
+ .filter-item { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: 12px; cursor: pointer; }
50
+ .filter-item:hover { color: #0550ae; }
51
+ .filter-item input { margin: 0; cursor: pointer; }
52
+ .filter-count { color: #656d76; margin-left: auto; font-size: 11px; }
53
+ .filter-actions { display: flex; gap: 8px; padding: 6px 0 2px; }
54
+ .filter-actions button { padding: 2px 8px; background: none; border: 1px solid #d1d9e0; border-radius: 4px; cursor: pointer; font-size: 11px; color: #656d76; }
55
+ .filter-actions button:hover { background: #eef1f5; color: #1f2328; }
56
+ #search-box { padding: 10px 16px; border-bottom: 1px solid #d1d9e0; }
57
+ #search-box input { width: 100%; padding: 6px 10px; background: #ffffff; color: #1f2328; border: 1px solid #d1d9e0; border-radius: 6px; font-size: 13px; outline: none; }
58
+ #search-box input:focus { border-color: #0969da; box-shadow: 0 0 0 3px rgba(9,105,218,0.15); }
59
+ #search-results { max-height: 200px; overflow-y: auto; padding: 0 16px; }
60
+ .search-item { padding: 5px 8px; font-size: 12px; cursor: pointer; border-radius: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
61
+ .search-item:hover { background: #ddf4ff; color: #0550ae; }
62
+ .filter-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
63
+ .legend { position: absolute; bottom: 16px; left: 16px; background: #f6f8faee; padding: 12px 16px; border-radius: 8px; border: 1px solid #d1d9e0; font-size: 12px; color: #1f2328; }
64
+ .legend-item { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
65
+ .legend-dot { width: 12px; height: 12px; border-radius: 50%; }
66
+ .stats { padding: 8px 16px; border-bottom: 1px solid #d1d9e0; font-size: 12px; color: #656d76; }
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <div id="app">
71
+ <div id="sidebar">
72
+ <h1>OpenTology — ${projectId}</h1>
73
+ <div id="graph-list">
74
+ <select id="graphSelect"><option value="">Loading graphs...</option></select>
75
+ </div>
76
+ <div class="stats" id="stats"></div>
77
+ <div id="query-box">
78
+ <textarea id="sparql" placeholder="SPARQL query...">SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 100</textarea>
79
+ <button onclick="runQuery()">Run Query</button>
80
+ </div>
81
+ <div id="results-table"></div>
82
+ <div id="details"></div>
83
+ </div>
84
+ <div id="center">
85
+ <div id="focus-bar">
86
+ <span id="focus-label"></span>
87
+ <button onclick="exitFocus()">Show All</button>
88
+ </div>
89
+ <div id="network-wrap">
90
+ <div id="network">
91
+ <div class="legend">
92
+ <div class="legend-item"><div class="legend-dot" style="background:#0550ae"></div> Class</div>
93
+ <div class="legend-item"><div class="legend-dot" style="background:#cf222e"></div> Instance</div>
94
+ <div class="legend-item"><div class="legend-dot" style="background:#1a7f37"></div> Property</div>
95
+ <div class="legend-item"><div class="legend-dot" style="background:#ffffff;border:1px solid #d1d9e0"></div> Literal</div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ <div id="filter-panel">
101
+ <div id="search-box">
102
+ <input type="text" id="searchInput" placeholder="Search nodes..." oninput="onSearch(this.value)" />
103
+ </div>
104
+ <div id="search-results"></div>
105
+ <div class="panel-section">
106
+ <div class="panel-header" onclick="toggleSection(this)">Node Types <span class="arrow">&#9660;</span></div>
107
+ <div class="panel-body" id="nodeTypeFilters"></div>
108
+ </div>
109
+ <div class="panel-section">
110
+ <div class="panel-header" onclick="toggleSection(this)">Classes (rdf:type) <span class="arrow">&#9660;</span></div>
111
+ <div class="panel-body" id="classFilters"></div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ <script>
116
+ const COLORS = {
117
+ class: '#0550ae', instance: '#cf222e', property: '#1a7f37',
118
+ literal: '#ffffff', edge: '#d1d9e0', edgeLabel: '#656d76'
119
+ };
120
+ const NODE_TYPE_COLORS = { Class: COLORS.class, Instance: COLORS.instance, Property: COLORS.property, Literal: COLORS.literal };
121
+ const PREFIXES = {
122
+ 'http://www.w3.org/1999/02/22-rdf-syntax-ns#': 'rdf:',
123
+ 'http://www.w3.org/2000/01/rdf-schema#': 'rdfs:',
124
+ 'http://www.w3.org/2002/07/owl#': 'owl:',
125
+ 'http://www.w3.org/2001/XMLSchema#': 'xsd:',
126
+ 'https://opentology.dev/vocab#': 'otx:',
127
+ };
128
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
129
+
130
+ function shorten(uri) {
131
+ if (!uri) return '';
132
+ for (const [full, prefix] of Object.entries(PREFIXES)) {
133
+ if (uri.startsWith(full)) return prefix + uri.slice(full.length);
134
+ }
135
+ const hash = uri.lastIndexOf('#');
136
+ if (hash > 0) return uri.slice(hash + 1);
137
+ const slash = uri.lastIndexOf('/');
138
+ if (slash > 0) return uri.slice(slash + 1);
139
+ return uri;
140
+ }
141
+
142
+ let network, nodesDS, edgesDS, classSet = new Set();
143
+ // Full graph data for filtering
144
+ let allNodes = new Map(), allEdges = [];
145
+ // rdf:type map: nodeId -> Set of class URIs
146
+ let nodeTypeMap = new Map();
147
+ // Filter state
148
+ let activeNodeTypes = new Set(['Class', 'Instance', 'Property', 'Literal']);
149
+ let activeClasses = new Set();
150
+ let allClasses = new Map(); // classUri -> count
151
+ let focusNodeId = null;
152
+
153
+ function getNodeType(id) {
154
+ if (id.includes('|')) return 'Literal';
155
+ if (classSet.has(id)) return 'Class';
156
+ if (id.startsWith('http://www.w3.org/') || id.startsWith('https://opentology.dev/vocab#')) return 'Property';
157
+ return 'Instance';
158
+ }
159
+
160
+ async function init() {
161
+ const gs = await fetch('/api/graphs').then(r => r.json());
162
+ const sel = document.getElementById('graphSelect');
163
+ sel.innerHTML = '';
164
+ for (const g of gs) {
165
+ sel.innerHTML += '<option value="' + g.uri + '">' + g.name + ' (' + g.triples + ')</option>';
166
+ }
167
+ sel.onchange = () => { exitFocus(); loadGraph(sel.value); };
168
+
169
+ try {
170
+ const schema = await fetch('/api/schema').then(r => r.json());
171
+ (schema.classes || []).forEach(c => classSet.add(c));
172
+ } catch {}
173
+
174
+ nodesDS = new vis.DataSet();
175
+ edgesDS = new vis.DataSet();
176
+ network = new vis.Network(document.getElementById('network'), { nodes: nodesDS, edges: edgesDS }, {
177
+ physics: { solver: 'forceAtlas2Based', forceAtlas2Based: { gravitationalConstant: -80, springLength: 120 } },
178
+ nodes: { shape: 'dot', font: { color: '#1f2328', size: 12 }, borderWidth: 0 },
179
+ edges: { arrows: 'to', color: { color: COLORS.edge, highlight: '#0550ae' }, font: { color: COLORS.edgeLabel, size: 10, strokeWidth: 0 }, smooth: { type: 'curvedCW', roundness: 0.15 } },
180
+ interaction: { hover: true, tooltipDelay: 100 },
181
+ });
182
+ network.on('click', params => {
183
+ if (params.nodes.length) {
184
+ const nid = params.nodes[0];
185
+ showNodeDetails(nid);
186
+ enterFocus(nid);
187
+ }
188
+ });
189
+ loadGraph(sel.value);
190
+ }
191
+
192
+ async function loadGraph(graphUri) {
193
+ const q = graphUri
194
+ ? 'SELECT ?s ?p ?o WHERE { GRAPH <' + graphUri + '> { ?s ?p ?o } } LIMIT 500'
195
+ : 'SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 500';
196
+ document.getElementById('sparql').value = q;
197
+ await runQuery();
198
+ }
199
+
200
+ async function runQuery() {
201
+ const sparql = document.getElementById('sparql').value;
202
+ const res = await fetch('/api/query?sparql=' + encodeURIComponent(sparql) + '&raw=true').then(r => r.json());
203
+ const bindings = res.results?.bindings || [];
204
+ const vars = res.head?.vars || [];
205
+ const isSPO = vars.includes('s') && vars.includes('p') && vars.includes('o');
206
+ const tableEl = document.getElementById('results-table');
207
+ if (isSPO) {
208
+ tableEl.style.display = 'none';
209
+ buildGraphData(bindings);
210
+ applyFilters();
211
+ buildFilterPanel();
212
+ } else {
213
+ renderTable(vars, bindings);
214
+ nodesDS.clear(); edgesDS.clear();
215
+ }
216
+ document.getElementById('stats').textContent = bindings.length + ' results';
217
+ }
218
+
219
+ function renderTable(vars, bindings) {
220
+ const tableEl = document.getElementById('results-table');
221
+ if (!bindings.length) {
222
+ tableEl.style.display = 'block';
223
+ tableEl.innerHTML = '<p style="color:#656d76">No results</p>';
224
+ return;
225
+ }
226
+ let h = '<table><thead><tr>';
227
+ for (const v of vars) h += '<th>' + v + '</th>';
228
+ h += '</tr></thead><tbody>';
229
+ for (const b of bindings) {
230
+ h += '<tr>';
231
+ for (const v of vars) {
232
+ const cell = b[v];
233
+ const val = cell ? (cell.type === 'uri' ? shorten(cell.value) : cell.value) : '';
234
+ h += '<td>' + val + '</td>';
235
+ }
236
+ h += '</tr>';
237
+ }
238
+ h += '</tbody></table>';
239
+ tableEl.style.display = 'block';
240
+ tableEl.innerHTML = h;
241
+ }
242
+
243
+ function nodeColor(uri) {
244
+ if (classSet.has(uri)) return COLORS.class;
245
+ if (uri.startsWith('http://www.w3.org/') || uri.startsWith('https://opentology.dev/vocab#')) return COLORS.property;
246
+ return COLORS.instance;
247
+ }
248
+
249
+ function buildGraphData(bindings) {
250
+ allNodes = new Map();
251
+ allEdges = [];
252
+ nodeTypeMap = new Map();
253
+ allClasses = new Map();
254
+
255
+ // First pass: collect rdf:type relationships
256
+ for (const b of bindings) {
257
+ if (b.p && b.p.value === RDF_TYPE && b.o && (b.o.type === 'uri' || b.o.type === 'bnode')) {
258
+ if (!nodeTypeMap.has(b.s.value)) nodeTypeMap.set(b.s.value, new Set());
259
+ nodeTypeMap.get(b.s.value).add(b.o.value);
260
+ }
261
+ }
262
+
263
+ // Second pass: build nodes and edges
264
+ for (const b of bindings) {
265
+ const ks = Object.keys(b);
266
+ if (!ks.includes('s') || !ks.includes('p') || !ks.includes('o')) continue;
267
+ const s = b.s, p = b.p, o = b.o;
268
+ if (!allNodes.has(s.value)) {
269
+ allNodes.set(s.value, { id: s.value, label: shorten(s.value), color: nodeColor(s.value), size: 14, title: s.value });
270
+ }
271
+ if (o.type === 'uri' || o.type === 'bnode') {
272
+ if (!allNodes.has(o.value)) {
273
+ allNodes.set(o.value, { id: o.value, label: shorten(o.value), color: nodeColor(o.value), size: 12, title: o.value });
274
+ }
275
+ allEdges.push({ from: s.value, to: o.value, label: shorten(p.value), title: p.value });
276
+ } else {
277
+ const litId = s.value + '|' + p.value + '|' + o.value;
278
+ const litLabel = o.value.length > 40 ? o.value.slice(0, 40) + '...' : o.value;
279
+ if (!allNodes.has(litId)) {
280
+ allNodes.set(litId, { id: litId, label: litLabel, color: { background: COLORS.literal, border: '#d1d9e0' }, borderWidth: 1, size: 8, shape: 'box', font: { size: 10, color: '#1f2328' }, title: o.value });
281
+ }
282
+ allEdges.push({ from: s.value, to: litId, label: shorten(p.value), title: p.value });
283
+ }
284
+ }
285
+
286
+ // Build class counts
287
+ for (const [nodeId, types] of nodeTypeMap) {
288
+ for (const cls of types) {
289
+ allClasses.set(cls, (allClasses.get(cls) || 0) + 1);
290
+ }
291
+ }
292
+ activeClasses = new Set(allClasses.keys());
293
+ }
294
+
295
+ function applyFilters() {
296
+ const visibleNodes = new Set();
297
+ for (const [id, node] of allNodes) {
298
+ const ntype = getNodeType(id);
299
+ if (!activeNodeTypes.has(ntype)) continue;
300
+ // For instances, check class filter
301
+ if (ntype === 'Instance' && nodeTypeMap.has(id)) {
302
+ const types = nodeTypeMap.get(id);
303
+ let anyActive = false;
304
+ for (const t of types) { if (activeClasses.has(t)) { anyActive = true; break; } }
305
+ if (!anyActive) continue;
306
+ }
307
+ visibleNodes.add(id);
308
+ }
309
+
310
+ // If focus mode, restrict to neighbors
311
+ if (focusNodeId && visibleNodes.has(focusNodeId)) {
312
+ const neighbors = new Set([focusNodeId]);
313
+ for (const e of allEdges) {
314
+ if (e.from === focusNodeId && visibleNodes.has(e.to)) neighbors.add(e.to);
315
+ if (e.to === focusNodeId && visibleNodes.has(e.from)) neighbors.add(e.from);
316
+ }
317
+ for (const id of [...visibleNodes]) {
318
+ if (!neighbors.has(id)) visibleNodes.delete(id);
319
+ }
320
+ }
321
+
322
+ const filteredEdges = allEdges.filter(e => visibleNodes.has(e.from) && visibleNodes.has(e.to));
323
+
324
+ nodesDS.clear(); edgesDS.clear();
325
+ const nodeArr = [];
326
+ for (const id of visibleNodes) {
327
+ const n = { ...allNodes.get(id) };
328
+ if (focusNodeId === id) { n.size = 22; n.borderWidth = 3; n.color = { background: n.color.background || n.color, border: '#0969da' }; }
329
+ nodeArr.push(n);
330
+ }
331
+ nodesDS.add(nodeArr);
332
+ edgesDS.add(filteredEdges);
333
+ network.fit();
334
+ }
335
+
336
+ function buildFilterPanel() {
337
+ // Node types
338
+ const ntEl = document.getElementById('nodeTypeFilters');
339
+ let ntHtml = '<div class="filter-actions"><button onclick="toggleAll(\\'nodeType\\', true)">All</button><button onclick="toggleAll(\\'nodeType\\', false)">None</button></div>';
340
+ for (const t of ['Class', 'Instance', 'Property', 'Literal']) {
341
+ const c = NODE_TYPE_COLORS[t];
342
+ const bg = t === 'Literal' ? '#ffffff;border:1px solid #d1d9e0' : c;
343
+ let count = 0;
344
+ for (const [id] of allNodes) { if (getNodeType(id) === t) count++; }
345
+ ntHtml += '<label class="filter-item"><input type="checkbox" ' + (activeNodeTypes.has(t) ? 'checked' : '') + ' onchange="toggleNodeType(\\'' + t + '\\', this.checked)"><div class="filter-dot" style="background:' + bg + '"></div>' + t + '<span class="filter-count">' + count + '</span></label>';
346
+ }
347
+ ntEl.innerHTML = ntHtml;
348
+
349
+ // Classes
350
+ const clEl = document.getElementById('classFilters');
351
+ const sorted = [...allClasses.entries()].sort((a, b) => b[1] - a[1]);
352
+ let clHtml = '<div class="filter-actions"><button onclick="toggleAll(\\'class\\', true)">All</button><button onclick="toggleAll(\\'class\\', false)">None</button></div>';
353
+ if (!sorted.length) {
354
+ clHtml += '<div style="color:#656d76;font-size:12px;padding:4px 0">No typed instances</div>';
355
+ }
356
+ for (const [cls, count] of sorted) {
357
+ clHtml += '<label class="filter-item"><input type="checkbox" ' + (activeClasses.has(cls) ? 'checked' : '') + ' onchange="toggleClass(\\'' + cls.replace(/'/g, "\\\\'") + '\\', this.checked)">' + shorten(cls) + '<span class="filter-count">' + count + '</span></label>';
358
+ }
359
+ clEl.innerHTML = clHtml;
360
+ }
361
+
362
+ function toggleNodeType(t, on) {
363
+ if (on) activeNodeTypes.add(t); else activeNodeTypes.delete(t);
364
+ applyFilters();
365
+ }
366
+ function toggleClass(cls, on) {
367
+ if (on) activeClasses.add(cls); else activeClasses.delete(cls);
368
+ applyFilters();
369
+ }
370
+ function toggleAll(kind, on) {
371
+ if (kind === 'nodeType') {
372
+ activeNodeTypes = on ? new Set(['Class', 'Instance', 'Property', 'Literal']) : new Set();
373
+ } else {
374
+ activeClasses = on ? new Set(allClasses.keys()) : new Set();
375
+ }
376
+ applyFilters();
377
+ buildFilterPanel();
378
+ }
379
+
380
+ function toggleSection(header) {
381
+ const body = header.nextElementSibling;
382
+ const arrow = header.querySelector('.arrow');
383
+ body.classList.toggle('collapsed');
384
+ arrow.classList.toggle('collapsed');
385
+ }
386
+
387
+ // Search
388
+ function onSearch(query) {
389
+ const el = document.getElementById('search-results');
390
+ if (!query || query.length < 2) { el.innerHTML = ''; return; }
391
+ const q = query.toLowerCase();
392
+ const matches = [];
393
+ for (const [id, node] of allNodes) {
394
+ if (node.label.toLowerCase().includes(q) || id.toLowerCase().includes(q)) {
395
+ matches.push({ id, label: node.label });
396
+ if (matches.length >= 20) break;
397
+ }
398
+ }
399
+ el.innerHTML = matches.map(m =>
400
+ '<div class="search-item" onclick="focusNode(\\'' + m.id.replace(/'/g, "\\\\'") + '\\')">' + m.label + '</div>'
401
+ ).join('');
402
+ }
403
+
404
+ // Focus mode
405
+ function focusNode(nodeId) {
406
+ enterFocus(nodeId);
407
+ showNodeDetails(nodeId);
408
+ document.getElementById('searchInput').value = '';
409
+ document.getElementById('search-results').innerHTML = '';
410
+ }
411
+
412
+ function enterFocus(nodeId) {
413
+ if (!allNodes.has(nodeId)) return;
414
+ focusNodeId = nodeId;
415
+ const bar = document.getElementById('focus-bar');
416
+ document.getElementById('focus-label').textContent = 'Focused: ' + shorten(nodeId);
417
+ bar.style.display = 'flex';
418
+ applyFilters();
419
+ }
420
+
421
+ function exitFocus() {
422
+ focusNodeId = null;
423
+ document.getElementById('focus-bar').style.display = 'none';
424
+ applyFilters();
425
+ }
426
+
427
+ async function showNodeDetails(nodeId) {
428
+ const cleanId = nodeId.includes('|') ? nodeId.split('|')[0] : nodeId;
429
+ const q = 'SELECT ?p ?o WHERE { <' + cleanId + '> ?p ?o }';
430
+ const res = await fetch('/api/query?sparql=' + encodeURIComponent(q) + '&raw=true').then(r => r.json());
431
+ const bindings = res.results?.bindings || [];
432
+ let html = '<h3>' + shorten(cleanId) + '</h3><table>';
433
+ for (const b of bindings) {
434
+ html += '<tr><td>' + shorten(b.p.value) + '</td><td>' + (b.o.type === 'uri' ? shorten(b.o.value) : b.o.value) + '</td></tr>';
435
+ }
436
+ html += '</table>';
437
+ document.getElementById('details').innerHTML = html;
438
+ }
439
+
440
+ init();
441
+ </script>
442
+ </body>
443
+ </html>`;
444
+ }
445
+ function cors(res) {
446
+ res.setHeader('Access-Control-Allow-Origin', '*');
447
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
448
+ }
449
+ function json(res, data) {
450
+ cors(res);
451
+ res.setHeader('Content-Type', 'application/json');
452
+ res.end(JSON.stringify(data));
453
+ }
454
+ export async function startGraphServer(opts = {}) {
455
+ const config = loadConfig();
456
+ const adapter = await createReadyAdapter(config);
457
+ const server = createServer(async (req, res) => {
458
+ const url = new URL(req.url ?? '/', `http://localhost`);
459
+ const path = url.pathname;
460
+ if (req.method === 'OPTIONS') {
461
+ cors(res);
462
+ res.end();
463
+ return;
464
+ }
465
+ try {
466
+ if (path === '/' || path === '/index.html') {
467
+ cors(res);
468
+ res.setHeader('Content-Type', 'text/html');
469
+ res.end(html(config));
470
+ return;
471
+ }
472
+ if (path === '/api/graphs') {
473
+ const graphs = config.graphs ?? {};
474
+ const result = [];
475
+ for (const [name, uri] of Object.entries(graphs)) {
476
+ const triples = await adapter.getGraphTripleCount(uri).catch(() => 0);
477
+ result.push({ name, uri, triples });
478
+ }
479
+ // Also include default graph
480
+ const defaultTriples = await adapter.getGraphTripleCount(config.graphUri).catch(() => 0);
481
+ result.unshift({ name: 'default', uri: config.graphUri, triples: defaultTriples });
482
+ json(res, result);
483
+ return;
484
+ }
485
+ if (path === '/api/schema') {
486
+ const graphUri = url.searchParams.get('graphUri') ?? config.graphUri;
487
+ const overview = await adapter.getSchemaOverview(graphUri);
488
+ json(res, overview);
489
+ return;
490
+ }
491
+ if (path === '/api/query') {
492
+ const sparql = url.searchParams.get('sparql');
493
+ if (!sparql) {
494
+ res.statusCode = 400;
495
+ json(res, { error: 'Missing sparql parameter' });
496
+ return;
497
+ }
498
+ const raw = url.searchParams.get('raw') === 'true';
499
+ let query = sparql;
500
+ if (!raw) {
501
+ const { hasGraphScope, autoScopeQuery } = await import('./sparql-utils.js');
502
+ if (!hasGraphScope(query)) {
503
+ const scoped = autoScopeQuery(query, config.graphUri);
504
+ if (scoped)
505
+ query = scoped;
506
+ }
507
+ }
508
+ const result = await adapter.sparqlQuery(query);
509
+ json(res, result);
510
+ return;
511
+ }
512
+ res.statusCode = 404;
513
+ json(res, { error: 'Not found' });
514
+ }
515
+ catch (err) {
516
+ res.statusCode = 500;
517
+ json(res, { error: err.message });
518
+ }
519
+ });
520
+ const port = opts.port ?? 0; // 0 = auto-assign
521
+ return new Promise((resolve) => {
522
+ server.listen(port, '127.0.0.1', () => {
523
+ const addr = server.address();
524
+ const assignedPort = typeof addr === 'object' && addr ? addr.port : port;
525
+ resolve({
526
+ port: assignedPort,
527
+ close: () => server.close(),
528
+ });
529
+ });
530
+ });
531
+ }
@@ -17,6 +17,7 @@ import { generateContextSection, updateClaudeMd } from '../templates/claude-md-c
17
17
  import { generateHookScript } from '../templates/session-start-hook.js';
18
18
  import { generateSlashCommands } from '../templates/slash-commands.js';
19
19
  import { scanCodebase } from '../lib/codebase-scanner.js';
20
+ import { startGraphServer } from '../lib/graph-server.js';
20
21
  export const MAX_TRIPLES_PER_PUSH = 100;
21
22
  export function assertTripleLimit(tripleCount) {
22
23
  if (tripleCount > MAX_TRIPLES_PER_PUSH) {
@@ -333,7 +334,7 @@ async function handleContextScan(args) {
333
334
  _experimental: true,
334
335
  _hint: pushStats
335
336
  ? `Symbol triples pushed: ${pushStats.triplesInserted} triples in ${pushStats.batchCount} batches. Query with: SELECT ?c WHERE { ?c a otx:Class ; otx:definedIn <urn:module:...> }`
336
- : 'Deep scan completed but triple push failed. Use opentology_push manually with the generated triples.',
337
+ : 'Deep scan completed but triple push failed. Use push manually with the generated triples.',
337
338
  };
338
339
  }
339
340
  // Default: module-level scan (existing behavior)
@@ -342,8 +343,8 @@ async function handleContextScan(args) {
342
343
  return {
343
344
  codebaseSnapshot: snapshot,
344
345
  _hint: snapshot.dependencyGraph && snapshot.dependencyGraph.modules.length > 0
345
- ? 'Analyze codebaseSnapshot and push Knowledge triples via opentology_push. Module dependency triples (otx:Module + otx:dependsOn) are available in dependencyGraph — push them to the context graph as-is.'
346
- : 'Analyze codebaseSnapshot and push Knowledge triples via opentology_push. No dependency graph was auto-extracted (non-JS/TS project or parsing issue). Inspect key source files manually and push otx:Module + otx:dependsOn triples for the important modules you identify.',
346
+ ? 'Analyze codebaseSnapshot and push Knowledge triples via push. Module dependency triples (otx:Module + otx:dependsOn) are available in dependencyGraph — push them to the context graph as-is.'
347
+ : 'Analyze codebaseSnapshot and push Knowledge triples via push. No dependency graph was auto-extracted (non-JS/TS project or parsing issue). Inspect key source files manually and push otx:Module + otx:dependsOn triples for the important modules you identify.',
347
348
  };
348
349
  }
349
350
  async function handleContextInit(args) {
@@ -461,7 +462,7 @@ async function handleContextLoad() {
461
462
  const config = loadConfig();
462
463
  const graphs = config.graphs ?? {};
463
464
  if (!graphs['context'] || !graphs['sessions']) {
464
- throw new Error('Context not initialized. Use opentology_context_init first.');
465
+ throw new Error('Context not initialized. Use context_init first.');
465
466
  }
466
467
  const contextUri = graphs['context'];
467
468
  const sessionsUri = graphs['sessions'];
@@ -669,7 +670,7 @@ export async function startMcpServer() {
669
670
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
670
671
  tools: [
671
672
  {
672
- name: 'opentology_init',
673
+ name: 'init',
673
674
  description: 'Initialize a new OpenTology project. Creates .opentology.json config with the project ID, SPARQL endpoint, and named graph URI. Must be run before other tools if no config exists.',
674
675
  inputSchema: {
675
676
  type: 'object',
@@ -692,7 +693,7 @@ export async function startMcpServer() {
692
693
  },
693
694
  },
694
695
  {
695
- name: 'opentology_validate',
696
+ name: 'validate',
696
697
  description: 'Validate Turtle (RDF) content for syntax correctness. Returns triple count and prefixes if valid, or an error message if invalid. Use before pushing to catch errors early. When shacl is true, also validates against SHACL shapes in shapes/ directory.',
697
698
  inputSchema: {
698
699
  type: 'object',
@@ -710,7 +711,7 @@ export async function startMcpServer() {
710
711
  },
711
712
  },
712
713
  {
713
- name: 'opentology_push',
714
+ name: 'push',
714
715
  description: 'Validate and insert Turtle (RDF) triples into the project graph. Validates syntax first, then pushes to the SPARQL endpoint. Auto-validates against SHACL shapes when shapes/ directory exists. Returns success status and triple count. IMPORTANT: Maximum 100 triples per call. For larger datasets, split into multiple pushes of 20-50 triples each.',
715
716
  inputSchema: {
716
717
  type: 'object',
@@ -733,7 +734,7 @@ export async function startMcpServer() {
733
734
  },
734
735
  graph: {
735
736
  type: 'string',
736
- description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
737
+ description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
737
738
  },
738
739
  graphUri: {
739
740
  type: 'string',
@@ -744,7 +745,7 @@ export async function startMcpServer() {
744
745
  },
745
746
  },
746
747
  {
747
- name: 'opentology_query',
748
+ name: 'query',
748
749
  description: 'Execute a SPARQL query against the project graph. Automatically scopes unscoped queries to the project named graph unless raw mode is enabled.',
749
750
  inputSchema: {
750
751
  type: 'object',
@@ -755,7 +756,7 @@ export async function startMcpServer() {
755
756
  },
756
757
  graph: {
757
758
  type: 'string',
758
- description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
759
+ description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
759
760
  },
760
761
  graphUri: {
761
762
  type: 'string',
@@ -770,14 +771,14 @@ export async function startMcpServer() {
770
771
  },
771
772
  },
772
773
  {
773
- name: 'opentology_status',
774
+ name: 'status',
774
775
  description: 'Get the current status of the OpenTology project, including project ID, endpoint, graph URI, and the number of triples stored in the graph.',
775
776
  inputSchema: {
776
777
  type: 'object',
777
778
  properties: {
778
779
  graph: {
779
780
  type: 'string',
780
- description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
781
+ description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
781
782
  },
782
783
  graphUri: {
783
784
  type: 'string',
@@ -787,14 +788,14 @@ export async function startMcpServer() {
787
788
  },
788
789
  },
789
790
  {
790
- name: 'opentology_pull',
791
+ name: 'pull',
791
792
  description: 'Export the entire project graph as Turtle (RDF). Returns all triples from the named graph serialized in Turtle format.',
792
793
  inputSchema: {
793
794
  type: 'object',
794
795
  properties: {
795
796
  graph: {
796
797
  type: 'string',
797
- description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
798
+ description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
798
799
  },
799
800
  graphUri: {
800
801
  type: 'string',
@@ -804,7 +805,7 @@ export async function startMcpServer() {
804
805
  },
805
806
  },
806
807
  {
807
- name: 'opentology_schema',
808
+ name: 'schema',
808
809
  description: 'Inspect the ontology schema. Without parameters, returns all classes and properties (same as opentology://schema resource). With a class parameter, returns detailed info: instance count, properties used by that class, and sample triples.',
809
810
  inputSchema: {
810
811
  type: 'object',
@@ -815,7 +816,7 @@ export async function startMcpServer() {
815
816
  },
816
817
  graph: {
817
818
  type: 'string',
818
- description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
819
+ description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
819
820
  },
820
821
  graphUri: {
821
822
  type: 'string',
@@ -825,7 +826,7 @@ export async function startMcpServer() {
825
826
  },
826
827
  },
827
828
  {
828
- name: 'opentology_drop',
829
+ name: 'drop',
829
830
  description: 'Drop (delete) the entire project graph. Requires confirm: true to prevent accidental deletion.',
830
831
  inputSchema: {
831
832
  type: 'object',
@@ -836,7 +837,7 @@ export async function startMcpServer() {
836
837
  },
837
838
  graph: {
838
839
  type: 'string',
839
- description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
840
+ description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
840
841
  },
841
842
  graphUri: {
842
843
  type: 'string',
@@ -847,7 +848,7 @@ export async function startMcpServer() {
847
848
  },
848
849
  },
849
850
  {
850
- name: 'opentology_delete',
851
+ name: 'delete',
851
852
  description: 'Delete specific triples. Provide Turtle content to remove those exact triples, or a SPARQL WHERE pattern for pattern-based deletion.',
852
853
  inputSchema: {
853
854
  type: 'object',
@@ -862,7 +863,7 @@ export async function startMcpServer() {
862
863
  },
863
864
  graph: {
864
865
  type: 'string',
865
- description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
866
+ description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
866
867
  },
867
868
  graphUri: {
868
869
  type: 'string',
@@ -872,7 +873,7 @@ export async function startMcpServer() {
872
873
  },
873
874
  },
874
875
  {
875
- name: 'opentology_diff',
876
+ name: 'diff',
876
877
  description: 'Compare local Turtle content against the remote graph. Returns added triples (in local but not remote), removed triples (in remote but not local), and count of unchanged triples.',
877
878
  inputSchema: {
878
879
  type: 'object',
@@ -883,7 +884,7 @@ export async function startMcpServer() {
883
884
  },
884
885
  graph: {
885
886
  type: 'string',
886
- description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
887
+ description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
887
888
  },
888
889
  graphUri: {
889
890
  type: 'string',
@@ -894,7 +895,7 @@ export async function startMcpServer() {
894
895
  },
895
896
  },
896
897
  {
897
- name: 'opentology_graph_list',
898
+ name: 'graph_list',
898
899
  description: 'List all named graphs for the project. Shows graph name, URI, and triple count.',
899
900
  inputSchema: {
900
901
  type: 'object',
@@ -902,7 +903,7 @@ export async function startMcpServer() {
902
903
  },
903
904
  },
904
905
  {
905
- name: 'opentology_graph_create',
906
+ name: 'graph_create',
906
907
  description: 'Create a new named graph. Generates a URI based on the project graph URI and registers it in the config.',
907
908
  inputSchema: {
908
909
  type: 'object',
@@ -916,7 +917,7 @@ export async function startMcpServer() {
916
917
  },
917
918
  },
918
919
  {
919
- name: 'opentology_graph_drop',
920
+ name: 'graph_drop',
920
921
  description: 'Drop a named graph and remove it from config. Requires confirm: true to prevent accidental deletion.',
921
922
  inputSchema: {
922
923
  type: 'object',
@@ -934,7 +935,7 @@ export async function startMcpServer() {
934
935
  },
935
936
  },
936
937
  {
937
- name: 'opentology_infer',
938
+ name: 'infer',
938
939
  description: 'Run RDFS inference on the project graph, materializing inferred triples into the main graph (so queries work naturally). A bookkeeping copy is kept in the inference graph for status reporting and clear support. With clear: true, removes inferred triples from both graphs.',
939
940
  inputSchema: {
940
941
  type: 'object',
@@ -945,7 +946,7 @@ export async function startMcpServer() {
945
946
  },
946
947
  graph: {
947
948
  type: 'string',
948
- description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
949
+ description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
949
950
  },
950
951
  graphUri: {
951
952
  type: 'string',
@@ -955,7 +956,7 @@ export async function startMcpServer() {
955
956
  },
956
957
  },
957
958
  {
958
- name: 'opentology_context_init',
959
+ name: 'context_init',
959
960
  description: 'Initialize project context graph for session-based knowledge management. Creates context/sessions named graphs, bootstraps otx ontology vocabulary, generates a Claude Code SessionStart hook script, and updates CLAUDE.md. Idempotent — safe to call multiple times. Use force: true to regenerate hook and CLAUDE.md.',
960
961
  inputSchema: {
961
962
  type: 'object',
@@ -968,15 +969,15 @@ export async function startMcpServer() {
968
969
  },
969
970
  },
970
971
  {
971
- name: 'opentology_context_load',
972
- description: 'Load project context: recent sessions (last 3), open issues (up to 10), and recent decisions (last 3) from the context graph. Returns structured JSON. Call this at the start of a session to understand project state. Requires context to be initialized first (opentology_context_init).',
972
+ name: 'context_load',
973
+ description: 'Load project context: recent sessions (last 3), open issues (up to 10), and recent decisions (last 3) from the context graph. Returns structured JSON. Call this at the start of a session to understand project state. Requires context to be initialized first (context_init).',
973
974
  inputSchema: {
974
975
  type: 'object',
975
976
  properties: {},
976
977
  },
977
978
  },
978
979
  {
979
- name: 'opentology_context_status',
980
+ name: 'context_status',
980
981
  description: 'Check whether project context is initialized. Shows graph triple counts, hook script presence, and CLAUDE.md marker status.',
981
982
  inputSchema: {
982
983
  type: 'object',
@@ -984,7 +985,7 @@ export async function startMcpServer() {
984
985
  },
985
986
  },
986
987
  {
987
- name: 'opentology_context_scan',
988
+ name: 'context_scan',
988
989
  description: 'Scan the current project codebase. depth="module" (default) returns a structured snapshot with file-level dependency graph. depth="symbol" (experimental) extracts class/interface/method-level dependencies and auto-pushes OTX triples to the context graph. Supports TypeScript (ts-morph), Python, Go, Rust, Java, Swift (Tree-sitter).',
989
990
  inputSchema: {
990
991
  type: 'object',
@@ -1023,7 +1024,7 @@ export async function startMcpServer() {
1023
1024
  },
1024
1025
  },
1025
1026
  {
1026
- name: 'opentology_visualize',
1027
+ name: 'visualize',
1027
1028
  description: 'Generate a visual diagram of the graph schema. Returns Mermaid or DOT text showing classes, properties, and their relationships (subClassOf, domain/range).',
1028
1029
  inputSchema: {
1029
1030
  type: 'object',
@@ -1040,7 +1041,7 @@ export async function startMcpServer() {
1040
1041
  },
1041
1042
  graph: {
1042
1043
  type: 'string',
1043
- description: 'Logical graph name (as created by opentology_graph_create). Resolves to a graph URI via config.',
1044
+ description: 'Logical graph name (as created by graph_create). Resolves to a graph URI via config.',
1044
1045
  },
1045
1046
  graphUri: {
1046
1047
  type: 'string',
@@ -1049,6 +1050,19 @@ export async function startMcpServer() {
1049
1050
  },
1050
1051
  },
1051
1052
  },
1053
+ {
1054
+ name: 'context_graph',
1055
+ description: 'Start an interactive graph visualization web server. Opens a local web UI where you can explore classes, instances, and relationships visually. Returns the server URL. The server runs until the process exits.',
1056
+ inputSchema: {
1057
+ type: 'object',
1058
+ properties: {
1059
+ port: {
1060
+ type: 'number',
1061
+ description: 'Server port (default: auto-assigned)',
1062
+ },
1063
+ },
1064
+ },
1065
+ },
1052
1066
  ],
1053
1067
  }));
1054
1068
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -1056,63 +1070,69 @@ export async function startMcpServer() {
1056
1070
  try {
1057
1071
  let result;
1058
1072
  switch (name) {
1059
- case 'opentology_init':
1073
+ case 'init':
1060
1074
  result = await handleInit(args);
1061
1075
  break;
1062
- case 'opentology_validate':
1076
+ case 'validate':
1063
1077
  result = await handleValidate(args);
1064
1078
  break;
1065
- case 'opentology_push':
1079
+ case 'push':
1066
1080
  result = await handlePush(args);
1067
1081
  break;
1068
- case 'opentology_query':
1082
+ case 'query':
1069
1083
  result = await handleQuery(args);
1070
1084
  break;
1071
- case 'opentology_status':
1085
+ case 'status':
1072
1086
  result = await handleStatus(args);
1073
1087
  break;
1074
- case 'opentology_pull':
1088
+ case 'pull':
1075
1089
  result = await handlePull(args);
1076
1090
  break;
1077
- case 'opentology_schema':
1091
+ case 'schema':
1078
1092
  result = await handleSchema(args);
1079
1093
  break;
1080
- case 'opentology_drop':
1094
+ case 'drop':
1081
1095
  result = await handleDrop(args);
1082
1096
  break;
1083
- case 'opentology_delete':
1097
+ case 'delete':
1084
1098
  result = await handleDelete(args);
1085
1099
  break;
1086
- case 'opentology_diff':
1100
+ case 'diff':
1087
1101
  result = await handleDiff(args);
1088
1102
  break;
1089
- case 'opentology_graph_list':
1103
+ case 'graph_list':
1090
1104
  result = await handleGraphList(args);
1091
1105
  break;
1092
- case 'opentology_graph_create':
1106
+ case 'graph_create':
1093
1107
  result = await handleGraphCreate(args);
1094
1108
  break;
1095
- case 'opentology_graph_drop':
1109
+ case 'graph_drop':
1096
1110
  result = await handleGraphDrop(args);
1097
1111
  break;
1098
- case 'opentology_infer':
1112
+ case 'infer':
1099
1113
  result = await handleInfer(args);
1100
1114
  break;
1101
- case 'opentology_context_scan':
1115
+ case 'context_scan':
1102
1116
  result = await handleContextScan(args);
1103
1117
  break;
1104
- case 'opentology_context_init':
1118
+ case 'context_init':
1105
1119
  result = await handleContextInit(args);
1106
1120
  break;
1107
- case 'opentology_context_load':
1121
+ case 'context_load':
1108
1122
  result = await handleContextLoad();
1109
1123
  break;
1110
- case 'opentology_context_status':
1124
+ case 'context_status':
1111
1125
  result = await handleContextStatus();
1112
1126
  break;
1113
- case 'opentology_visualize':
1127
+ case 'visualize':
1114
1128
  result = await handleVisualize(args);
1115
1129
  break;
1130
+ case 'context_graph': {
1131
+ const port = args.port;
1132
+ const srv = await startGraphServer({ port });
1133
+ result = { url: `http://localhost:${srv.port}`, port: srv.port, message: `Graph server running at http://localhost:${srv.port}` };
1134
+ break;
1135
+ }
1116
1136
  default:
1117
1137
  throw new Error(`Unknown tool: ${name}`);
1118
1138
  }
@@ -64,8 +64,8 @@ function formatOutput(data) {
64
64
 
65
65
  lines.push('---');
66
66
  lines.push('Dependency graph (otx:Module + otx:dependsOn) is available in the context graph.');
67
- lines.push('If significant code changes occurred since last scan, consider running opentology_context_scan to update dependency triples.');
68
- lines.push('Reminder: At session end, push a session summary using opentology_push to the sessions graph.');
67
+ lines.push('If significant code changes occurred since last scan, consider running context_scan to update dependency triples.');
68
+ lines.push('Reminder: At session end, push a session summary using push to the sessions graph.');
69
69
 
70
70
  return lines.join('\\n');
71
71
  }
@@ -2,7 +2,7 @@ export function generateSlashCommands() {
2
2
  return [
3
3
  {
4
4
  filename: 'context-init.md',
5
- content: `Use the opentology_context_init MCP tool to initialize the project context graph.
5
+ content: `Use the context_init MCP tool to initialize the project context graph.
6
6
 
7
7
  After initialization:
8
8
  1. Report what was created (graphs, hook script, CLAUDE.md, slash commands).
@@ -10,9 +10,9 @@ After initialization:
10
10
 
11
11
  ## Codebase Analysis
12
12
 
13
- After context_init completes, call opentology_context_scan to get a codebase snapshot.
13
+ After context_init completes, call context_scan to get a codebase snapshot.
14
14
 
15
- Analyze the \`codebaseSnapshot\` field from the response and create Knowledge triples to push via \`opentology_push\`.
15
+ Analyze the \`codebaseSnapshot\` field from the response and create Knowledge triples to push via \`push\`.
16
16
 
17
17
  ### What to extract:
18
18
  - **Project triple** (\`otx:Project\`): name, description, tech stack (\`otx:stack\`), status "active"
@@ -52,7 +52,7 @@ Analyze the \`codebaseSnapshot\` field from the response and create Knowledge tr
52
52
  },
53
53
  {
54
54
  filename: 'context-load.md',
55
- content: `Use the opentology_context_load MCP tool to load the current project context.
55
+ content: `Use the context_load MCP tool to load the current project context.
56
56
 
57
57
  Display the results in a readable format:
58
58
  - Recent sessions with dates and next todos
@@ -68,7 +68,7 @@ If context is not initialized, suggest running /context-init first.
68
68
 
69
69
  Ask the user what was accomplished in this session, or summarize the conversation so far.
70
70
 
71
- Then use opentology_push to insert a session record:
71
+ Then use push to insert a session record:
72
72
 
73
73
  \`\`\`turtle
74
74
  @prefix otx: <https://opentology.dev/vocab#> .
@@ -86,11 +86,14 @@ Push to the sessions graph (use graph name "sessions").
86
86
  },
87
87
  {
88
88
  filename: 'context-scan.md',
89
- content: `Use the opentology_context_scan MCP tool to scan the current project codebase.
89
+ content: `Before scanning, ask the user which scan depth they want:
90
90
 
91
- The tool returns a structured \`codebaseSnapshot\` containing package.json, directory tree, entry points, detected imports, and README.
91
+ 1. **Module scan** (default) fast, file-level analysis. Returns directory tree, entry points, detected imports, and dependency graph. Good for a quick project overview.
92
+ 2. **Deep scan** (symbol) — slower, extracts classes, interfaces, functions, and method calls using ts-morph (TypeScript) or Tree-sitter (Go, Python, Rust, Java, Swift). Auto-pushes symbol triples to the context graph. Use when you need detailed architectural understanding.
92
93
 
93
- Analyze the snapshot and create Knowledge triples to push via \`opentology_push\`:
94
+ Once the user chooses (or accepts the default), call the context_scan MCP tool with the appropriate \`depth\` parameter ("module" or "symbol").
95
+
96
+ After the scan completes, analyze the results and create Knowledge triples to push via \`push\`:
94
97
  - **Project triple** (\`otx:Project\`): name, description, tech stack, status "active"
95
98
  - **Knowledge triples** (\`otx:Knowledge\`): architectural patterns, framework choices, build setup, project structure
96
99
 
@@ -99,9 +102,24 @@ Push to the **context** graph (use graph name "context"). Keep each push under 1
99
102
  },
100
103
  {
101
104
  filename: 'context-status.md',
102
- content: `Use the opentology_context_status MCP tool to check the project context initialization status.
105
+ content: `Use the context_status MCP tool to check the project context initialization status.
103
106
 
104
107
  Display the results clearly: whether context is initialized, graph triple counts, hook presence, and CLAUDE.md status.
108
+ `,
109
+ },
110
+ {
111
+ filename: 'context-graph.md',
112
+ content: `Use the context_graph MCP tool to start an interactive graph visualization web server.
113
+
114
+ The tool starts a local web server and returns a URL. Tell the user:
115
+ 1. The URL to open in their browser (e.g. http://localhost:PORT)
116
+ 2. They can explore classes, instances, and relationships visually
117
+ 3. The sidebar has a SPARQL query box for custom queries
118
+ 4. Click any node to see its properties
119
+ 5. Use the graph selector dropdown to switch between named graphs
120
+ 6. Press Ctrl+C in the terminal to stop the server
121
+
122
+ If context is not initialized, suggest running /context-init first.
105
123
  `,
106
124
  },
107
125
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentology",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI-managed RDF/SPARQL infrastructure — Supabase for RDF",
5
5
  "type": "module",
6
6
  "bin": {