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 +39 -19
- package/dist/commands/context.js +35 -0
- package/dist/lib/graph-server.d.ts +7 -0
- package/dist/lib/graph-server.js +531 -0
- package/dist/mcp/server.js +73 -53
- package/dist/templates/session-start-hook.js +2 -2
- package/dist/templates/slash-commands.js +27 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenTology
|
|
2
2
|
|
|
3
|
-
> CLI-managed RDF/SPARQL infrastructure with RDFS reasoning
|
|
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[
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
-
**
|
|
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
|
|
328
|
-
- [x] MCP server with
|
|
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
|
-
- [
|
|
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[
|
|
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
|
|
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 서버
|
|
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 전체 생애주기를 다루는
|
|
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
|
-
-
|
|
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
|
-
**
|
|
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]
|
|
675
|
-
- [x]
|
|
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
|
-
- [
|
|
708
|
+
- [x] 인터랙티브 그래프 시각화 웹 UI
|
|
689
709
|
|
|
690
710
|
### 기여 방법
|
|
691
711
|
|
package/dist/commands/context.js
CHANGED
|
@@ -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,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">▼</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">▼</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
|
+
}
|
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
|
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
|
|
346
|
-
: 'Analyze codebaseSnapshot and push Knowledge triples via
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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 (
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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 '
|
|
1073
|
+
case 'init':
|
|
1060
1074
|
result = await handleInit(args);
|
|
1061
1075
|
break;
|
|
1062
|
-
case '
|
|
1076
|
+
case 'validate':
|
|
1063
1077
|
result = await handleValidate(args);
|
|
1064
1078
|
break;
|
|
1065
|
-
case '
|
|
1079
|
+
case 'push':
|
|
1066
1080
|
result = await handlePush(args);
|
|
1067
1081
|
break;
|
|
1068
|
-
case '
|
|
1082
|
+
case 'query':
|
|
1069
1083
|
result = await handleQuery(args);
|
|
1070
1084
|
break;
|
|
1071
|
-
case '
|
|
1085
|
+
case 'status':
|
|
1072
1086
|
result = await handleStatus(args);
|
|
1073
1087
|
break;
|
|
1074
|
-
case '
|
|
1088
|
+
case 'pull':
|
|
1075
1089
|
result = await handlePull(args);
|
|
1076
1090
|
break;
|
|
1077
|
-
case '
|
|
1091
|
+
case 'schema':
|
|
1078
1092
|
result = await handleSchema(args);
|
|
1079
1093
|
break;
|
|
1080
|
-
case '
|
|
1094
|
+
case 'drop':
|
|
1081
1095
|
result = await handleDrop(args);
|
|
1082
1096
|
break;
|
|
1083
|
-
case '
|
|
1097
|
+
case 'delete':
|
|
1084
1098
|
result = await handleDelete(args);
|
|
1085
1099
|
break;
|
|
1086
|
-
case '
|
|
1100
|
+
case 'diff':
|
|
1087
1101
|
result = await handleDiff(args);
|
|
1088
1102
|
break;
|
|
1089
|
-
case '
|
|
1103
|
+
case 'graph_list':
|
|
1090
1104
|
result = await handleGraphList(args);
|
|
1091
1105
|
break;
|
|
1092
|
-
case '
|
|
1106
|
+
case 'graph_create':
|
|
1093
1107
|
result = await handleGraphCreate(args);
|
|
1094
1108
|
break;
|
|
1095
|
-
case '
|
|
1109
|
+
case 'graph_drop':
|
|
1096
1110
|
result = await handleGraphDrop(args);
|
|
1097
1111
|
break;
|
|
1098
|
-
case '
|
|
1112
|
+
case 'infer':
|
|
1099
1113
|
result = await handleInfer(args);
|
|
1100
1114
|
break;
|
|
1101
|
-
case '
|
|
1115
|
+
case 'context_scan':
|
|
1102
1116
|
result = await handleContextScan(args);
|
|
1103
1117
|
break;
|
|
1104
|
-
case '
|
|
1118
|
+
case 'context_init':
|
|
1105
1119
|
result = await handleContextInit(args);
|
|
1106
1120
|
break;
|
|
1107
|
-
case '
|
|
1121
|
+
case 'context_load':
|
|
1108
1122
|
result = await handleContextLoad();
|
|
1109
1123
|
break;
|
|
1110
|
-
case '
|
|
1124
|
+
case 'context_status':
|
|
1111
1125
|
result = await handleContextStatus();
|
|
1112
1126
|
break;
|
|
1113
|
-
case '
|
|
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
|
|
68
|
-
lines.push('Reminder: At session end, push a session summary using
|
|
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
|
|
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
|
|
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 \`
|
|
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
|
|
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
|
|
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: `
|
|
89
|
+
content: `Before scanning, ask the user which scan depth they want:
|
|
90
90
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
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
|
];
|