opentology 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -19
- package/dist/commands/context.js +234 -12
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +132 -0
- package/dist/index.js +3 -1
- package/dist/lib/context-sync.d.ts +15 -0
- package/dist/lib/context-sync.js +229 -0
- package/dist/lib/graph-server.d.ts +7 -0
- package/dist/lib/graph-server.js +556 -0
- package/dist/mcp/server.js +254 -64
- package/dist/templates/pre-edit-hook.d.ts +1 -0
- package/dist/templates/pre-edit-hook.js +101 -0
- package/dist/templates/session-start-hook.js +30 -3
- 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[23 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 23 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
|
+
- 18 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 23 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,9 @@ 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`, `impact`, `sync`, `graph`) |
|
|
185
|
+
| `opentology viz` | Visualize ontology schema (`schema`) |
|
|
186
|
+
| `opentology doctor` | Check project health (config, store, context, hooks, dependencies) |
|
|
178
187
|
| `opentology mcp` | Start the MCP server |
|
|
179
188
|
|
|
180
189
|
### MCP Integration
|
|
@@ -192,7 +201,7 @@ Add to your MCP client configuration (`.mcp.json`):
|
|
|
192
201
|
}
|
|
193
202
|
```
|
|
194
203
|
|
|
195
|
-
**
|
|
204
|
+
**23 Tools:**
|
|
196
205
|
|
|
197
206
|
| Tool | Description |
|
|
198
207
|
|------|-------------|
|
|
@@ -210,10 +219,15 @@ Add to your MCP client configuration (`.mcp.json`):
|
|
|
210
219
|
| `opentology_graph_list` | List named graphs |
|
|
211
220
|
| `opentology_graph_create` | Create a named graph |
|
|
212
221
|
| `opentology_graph_drop` | Drop a named graph |
|
|
222
|
+
| `opentology_visualize` | Generate schema visualization (Mermaid/DOT) |
|
|
213
223
|
| `opentology_context_init` | Initialize project context graph |
|
|
214
224
|
| `opentology_context_load` | Load project context |
|
|
215
225
|
| `opentology_context_status` | Check context initialization status |
|
|
216
226
|
| `opentology_context_scan` | Scan codebase (module or symbol-level) |
|
|
227
|
+
| `opentology_context_impact` | Analyze file modification impact (dependents, dependencies, related context) |
|
|
228
|
+
| `opentology_context_sync` | Auto-sync: recover missed sessions from git, rescan module graph |
|
|
229
|
+
| `opentology_context_graph` | Start interactive graph visualization web UI |
|
|
230
|
+
| `opentology_doctor` | Check project health (config, store, hooks, dependencies) |
|
|
217
231
|
|
|
218
232
|
**1 Resource:**
|
|
219
233
|
|
|
@@ -324,8 +338,8 @@ npm install web-tree-sitter tree-sitter-wasms
|
|
|
324
338
|
|
|
325
339
|
### Roadmap
|
|
326
340
|
|
|
327
|
-
- [x] CLI with
|
|
328
|
-
- [x] MCP server with
|
|
341
|
+
- [x] CLI with 18 commands
|
|
342
|
+
- [x] MCP server with 23 tools and 1 resource
|
|
329
343
|
- [x] Schema introspection (MCP resource + tool)
|
|
330
344
|
- [x] Complete CRUD (push --replace, drop, delete)
|
|
331
345
|
- [x] SHACL validation (shape constraints on push)
|
|
@@ -338,7 +352,7 @@ npm install web-tree-sitter tree-sitter-wasms
|
|
|
338
352
|
- [ ] OWL reasoning (owl:sameAs, owl:inverseOf)
|
|
339
353
|
- [ ] Remote ontology import
|
|
340
354
|
- [ ] Version control for ontology snapshots
|
|
341
|
-
- [
|
|
355
|
+
- [x] Interactive graph visualization web UI
|
|
342
356
|
|
|
343
357
|
### Contributing
|
|
344
358
|
|
|
@@ -369,7 +383,7 @@ graph TB
|
|
|
369
383
|
end
|
|
370
384
|
|
|
371
385
|
subgraph MCP["MCP Server"]
|
|
372
|
-
Tools[
|
|
386
|
+
Tools[23 Tools]
|
|
373
387
|
Resource["opentology://schema"]
|
|
374
388
|
end
|
|
375
389
|
|
|
@@ -389,7 +403,7 @@ graph TB
|
|
|
389
403
|
Pipeline --> Adapter
|
|
390
404
|
```
|
|
391
405
|
|
|
392
|
-
온톨로지 도구들은 개발자 경험이 열악합니다. OpenTology는 터미널에서 RDF의 전체 생애주기를 관리합니다. 프로젝트 초기화, Turtle 작성, SHACL 검증, RDFS 추론이 포함된 푸시, SPARQL
|
|
406
|
+
온톨로지 도구들은 개발자 경험이 열악합니다. OpenTology는 터미널에서 RDF의 전체 생애주기를 관리합니다. 프로젝트 초기화, Turtle 작성, SHACL 검증, RDFS 추론이 포함된 푸시, SPARQL 쿼리, 인터랙티브 그래프 시각화까지 CLI 하나로 처리합니다. MCP 서버를 내장하고 있어 AI 어시스턴트가 지식 그래프를 직접 다룰 수 있고, Docker 없이 임베디드 모드로 바로 시작할 수 있습니다.
|
|
393
407
|
|
|
394
408
|
### 시스템 요구사항
|
|
395
409
|
|
|
@@ -444,7 +458,7 @@ graph LR
|
|
|
444
458
|
| Docker 필수 | 아니오 (임베디드 모드) | 예 | 예 |
|
|
445
459
|
| RDFS 추론 | 푸시 시 자동 | SPARQL CONSTRUCT 수동 작성 | 네이티브 미지원 |
|
|
446
460
|
| SHACL 검증 | 내장 | 별도 도구 필요 | 해당 없음 |
|
|
447
|
-
| AI 연동 | MCP 서버
|
|
461
|
+
| AI 연동 | MCP 서버 23개 도구 | 없음 | 플러그인 생태계 |
|
|
448
462
|
| 쿼리 언어 | SPARQL (접두사 자동 삽입) | SPARQL (수동) | Cypher |
|
|
449
463
|
| 데이터 형식 | Turtle 파일 | Turtle/N-Triples | 속성 그래프 |
|
|
450
464
|
| 프로젝트 구분 | Named Graph 자동 | 수동 관리 | 데이터베이스 단위 |
|
|
@@ -453,7 +467,7 @@ graph LR
|
|
|
453
467
|
|
|
454
468
|
**핵심**
|
|
455
469
|
|
|
456
|
-
- RDF 전체 생애주기를 다루는
|
|
470
|
+
- RDF 전체 생애주기를 다루는 18개 CLI 명령어
|
|
457
471
|
- `.opentology.json` 기반 프로젝트 설정
|
|
458
472
|
- Named Graph 자동 스코핑 -- 쿼리가 프로젝트 그래프에 자동 한정
|
|
459
473
|
- HTTP 모드(Oxigraph 서버)와 임베디드 모드(WASM, Docker 불필요) 지원
|
|
@@ -473,10 +487,16 @@ graph LR
|
|
|
473
487
|
|
|
474
488
|
**AI 연동**
|
|
475
489
|
|
|
476
|
-
-
|
|
490
|
+
- 23개 도구와 1개 리소스를 제공하는 MCP 서버
|
|
477
491
|
- `opentology://schema` 리소스로 온톨로지 개요 자동 로드
|
|
478
492
|
- Claude Code, Cursor 등 MCP 호환 클라이언트와 연동
|
|
479
493
|
|
|
494
|
+
**시각화**
|
|
495
|
+
|
|
496
|
+
- 인터랙티브 그래프 시각화 웹 UI (`opentology context graph`)
|
|
497
|
+
- vis-network로 클래스, 인스턴스, 관계를 시각적으로 탐색
|
|
498
|
+
- SPARQL 쿼리 편집기, 노드 필터링, 검색, 포커스 모드
|
|
499
|
+
|
|
480
500
|
### 두 가지 모드
|
|
481
501
|
|
|
482
502
|
| | HTTP 모드 | 임베디드 모드 |
|
|
@@ -522,6 +542,9 @@ docker run -p 7878:7878 ghcr.io/oxigraph/oxigraph \
|
|
|
522
542
|
| `opentology infer` | RDFS 물질화 실행 (`--clear`로 추론 트리플 제거) |
|
|
523
543
|
| `opentology graph` | Named Graph 목록/생성/삭제 |
|
|
524
544
|
| `opentology prefix` | 프로젝트 접두사 목록/추가/제거 |
|
|
545
|
+
| `opentology context` | 프로젝트 컨텍스트 관리 (`init`, `load`, `status`, `scan`, `impact`, `sync`, `graph`) |
|
|
546
|
+
| `opentology viz` | 온톨로지 스키마 시각화 (`schema`) |
|
|
547
|
+
| `opentology doctor` | 프로젝트 건강 진단 (설정, 스토어, 컨텍스트, 훅, 의존성) |
|
|
525
548
|
| `opentology mcp` | MCP 서버 시작 |
|
|
526
549
|
|
|
527
550
|
### MCP 연동
|
|
@@ -539,7 +562,7 @@ MCP 클라이언트 설정 파일(`.mcp.json`)에 추가:
|
|
|
539
562
|
}
|
|
540
563
|
```
|
|
541
564
|
|
|
542
|
-
**
|
|
565
|
+
**23개 도구:**
|
|
543
566
|
|
|
544
567
|
| 도구 | 설명 |
|
|
545
568
|
|------|------|
|
|
@@ -557,10 +580,15 @@ MCP 클라이언트 설정 파일(`.mcp.json`)에 추가:
|
|
|
557
580
|
| `opentology_graph_list` | Named Graph 목록 조회 |
|
|
558
581
|
| `opentology_graph_create` | Named Graph 생성 |
|
|
559
582
|
| `opentology_graph_drop` | Named Graph 삭제 |
|
|
583
|
+
| `opentology_visualize` | 스키마 시각화 생성 (Mermaid/DOT) |
|
|
560
584
|
| `opentology_context_init` | 프로젝트 컨텍스트 그래프 초기화 |
|
|
561
585
|
| `opentology_context_load` | 프로젝트 컨텍스트 로드 |
|
|
562
586
|
| `opentology_context_status` | 컨텍스트 초기화 상태 확인 |
|
|
563
587
|
| `opentology_context_scan` | 코드베이스 스캔 (모듈/심볼 수준) |
|
|
588
|
+
| `opentology_context_impact` | 파일 수정 영향 분석 (의존 모듈, 관련 컨텍스트) |
|
|
589
|
+
| `opentology_context_sync` | 자동 동기화: git에서 누락 세션 복구, 모듈 그래프 재스캔 |
|
|
590
|
+
| `opentology_context_graph` | 인터랙티브 그래프 시각화 웹 UI 시작 |
|
|
591
|
+
| `opentology_doctor` | 프로젝트 건강 진단 (설정, 스토어, 훅, 의존성) |
|
|
564
592
|
|
|
565
593
|
**1개 리소스:**
|
|
566
594
|
|
|
@@ -671,8 +699,8 @@ npm install web-tree-sitter tree-sitter-wasms
|
|
|
671
699
|
|
|
672
700
|
### 로드맵
|
|
673
701
|
|
|
674
|
-
- [x]
|
|
675
|
-
- [x]
|
|
702
|
+
- [x] 18개 CLI 명령어
|
|
703
|
+
- [x] 23개 도구와 1개 리소스를 갖춘 MCP 서버
|
|
676
704
|
- [x] 스키마 조회 (MCP 리소스 + 도구)
|
|
677
705
|
- [x] 완전한 CRUD (push --replace, drop, delete)
|
|
678
706
|
- [x] SHACL 검증 (푸시 시 형상 제약 자동 검증)
|
|
@@ -685,7 +713,7 @@ npm install web-tree-sitter tree-sitter-wasms
|
|
|
685
713
|
- [ ] OWL 추론 (owl:sameAs, owl:inverseOf)
|
|
686
714
|
- [ ] 원격 온톨로지 임포트
|
|
687
715
|
- [ ] 온톨로지 스냅샷 버전 관리
|
|
688
|
-
- [
|
|
716
|
+
- [x] 인터랙티브 그래프 시각화 웹 UI
|
|
689
717
|
|
|
690
718
|
### 기여 방법
|
|
691
719
|
|
package/dist/commands/context.js
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
4
5
|
import { loadConfig, saveConfig } from '../lib/config.js';
|
|
5
6
|
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
7
|
+
import { startGraphServer } from '../lib/graph-server.js';
|
|
8
|
+
import { syncContext } from '../lib/context-sync.js';
|
|
6
9
|
import { OTX_BOOTSTRAP_TURTLE } from '../templates/otx-ontology.js';
|
|
7
10
|
import { generateContextSection, updateClaudeMd } from '../templates/claude-md-context.js';
|
|
8
11
|
import { generateHookScript } from '../templates/session-start-hook.js';
|
|
12
|
+
import { generatePreEditHookScript } from '../templates/pre-edit-hook.js';
|
|
9
13
|
import { generateSlashCommands } from '../templates/slash-commands.js';
|
|
14
|
+
function ask(question) {
|
|
15
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
rl.question(question, (answer) => {
|
|
18
|
+
rl.close();
|
|
19
|
+
resolve(answer.trim().toLowerCase());
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
10
23
|
export function registerContext(program) {
|
|
11
24
|
const context = program
|
|
12
25
|
.command('context')
|
|
@@ -86,6 +99,16 @@ export function registerContext(program) {
|
|
|
86
99
|
else {
|
|
87
100
|
console.log(pc.dim(' Hook script already exists — skipped (use --force to regenerate)'));
|
|
88
101
|
}
|
|
102
|
+
// Step 3b: Write pre-edit hook script
|
|
103
|
+
const preEditHookPath = join(hookDir, 'pre-edit.mjs');
|
|
104
|
+
if (!existsSync(preEditHookPath) || opts.force) {
|
|
105
|
+
mkdirSync(hookDir, { recursive: true });
|
|
106
|
+
writeFileSync(preEditHookPath, generatePreEditHookScript(), 'utf-8');
|
|
107
|
+
console.log(pc.green(` Generated hook script: .opentology/hooks/pre-edit.mjs`));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.log(pc.dim(' Pre-edit hook already exists — skipped'));
|
|
111
|
+
}
|
|
89
112
|
// Step 4: Update CLAUDE.md
|
|
90
113
|
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
91
114
|
const section = generateContextSection(config.projectId, config.graphUri);
|
|
@@ -138,18 +161,51 @@ export function registerContext(program) {
|
|
|
138
161
|
}
|
|
139
162
|
// Step 6: Save config LAST (atomic commit point)
|
|
140
163
|
saveConfig(config);
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
164
|
+
// Step 7: Register hooks in .claude/settings.json
|
|
165
|
+
const settingsDir = join(process.cwd(), '.claude');
|
|
166
|
+
const settingsPath = join(settingsDir, 'settings.json');
|
|
167
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
168
|
+
let settings = {};
|
|
169
|
+
if (existsSync(settingsPath)) {
|
|
170
|
+
try {
|
|
171
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
172
|
+
}
|
|
173
|
+
catch { /* start fresh */ }
|
|
174
|
+
}
|
|
175
|
+
const hooks = (settings.hooks ?? {});
|
|
176
|
+
// Always register SessionStart hook
|
|
177
|
+
const sessionStartCmd = 'node .opentology/hooks/session-start.mjs';
|
|
178
|
+
if (!hooks.SessionStart)
|
|
179
|
+
hooks.SessionStart = [];
|
|
180
|
+
const hasSessionHook = hooks.SessionStart.some((h) => h.command === sessionStartCmd);
|
|
181
|
+
if (!hasSessionHook) {
|
|
182
|
+
hooks.SessionStart.push({ type: 'command', command: sessionStartCmd });
|
|
183
|
+
console.log(pc.green(' Registered SessionStart hook in .claude/settings.json'));
|
|
184
|
+
}
|
|
185
|
+
// Ask about PreToolUse impact hook
|
|
144
186
|
console.log('');
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
187
|
+
const enableImpact = await ask(pc.bold('Enable PreToolUse impact analysis hook? ') +
|
|
188
|
+
pc.dim('(auto-shows dependents before Edit/Write)') +
|
|
189
|
+
' [Y/n] ');
|
|
190
|
+
if (enableImpact !== 'n' && enableImpact !== 'no') {
|
|
191
|
+
const preEditCmd = 'node .opentology/hooks/pre-edit.mjs';
|
|
192
|
+
if (!hooks.PreToolUse)
|
|
193
|
+
hooks.PreToolUse = [];
|
|
194
|
+
const hasPreEditHook = hooks.PreToolUse.some((h) => h.command === preEditCmd);
|
|
195
|
+
if (!hasPreEditHook) {
|
|
196
|
+
hooks.PreToolUse.push({
|
|
197
|
+
type: 'command',
|
|
198
|
+
command: preEditCmd,
|
|
199
|
+
matcher: { tool_name: 'Edit|Write' },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
console.log(pc.green(' Registered PreToolUse impact hook in .claude/settings.json'));
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.log(pc.dim(' Skipped PreToolUse hook registration'));
|
|
206
|
+
}
|
|
207
|
+
settings.hooks = hooks;
|
|
208
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
153
209
|
console.log('');
|
|
154
210
|
console.log(pc.dim('Consider adding .opentology/hooks/ to version control so team members share the hook.'));
|
|
155
211
|
}
|
|
@@ -366,4 +422,170 @@ export function registerContext(program) {
|
|
|
366
422
|
console.log('CLAUDE.md: ' + (hasMarkers ? pc.green('markers present') : pc.yellow('markers missing')));
|
|
367
423
|
}
|
|
368
424
|
});
|
|
425
|
+
// --- context impact ---
|
|
426
|
+
context
|
|
427
|
+
.command('impact')
|
|
428
|
+
.description('Analyze impact of modifying a file (dependents, dependencies, related context)')
|
|
429
|
+
.requiredOption('--file <path>', 'Relative file path to analyze')
|
|
430
|
+
.option('--format <type>', 'Output format: table, json', 'table')
|
|
431
|
+
.action(async (opts) => {
|
|
432
|
+
let config;
|
|
433
|
+
try {
|
|
434
|
+
config = loadConfig();
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
const graphs = config.graphs ?? {};
|
|
441
|
+
if (!graphs['context']) {
|
|
442
|
+
console.error(pc.red('Error: Context not initialized. Run `opentology context init` first.'));
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
const contextUri = graphs['context'];
|
|
446
|
+
const OTX = 'https://opentology.dev/vocab#';
|
|
447
|
+
const moduleUriStr = `urn:module:${opts.file}`;
|
|
448
|
+
try {
|
|
449
|
+
const adapter = await createReadyAdapter(config);
|
|
450
|
+
// Dependents (reverse deps)
|
|
451
|
+
const dependentsResult = await adapter.sparqlQuery(`
|
|
452
|
+
SELECT ?dependent WHERE {
|
|
453
|
+
GRAPH <${contextUri}> {
|
|
454
|
+
?dependent <${OTX}dependsOn> <${moduleUriStr}> .
|
|
455
|
+
}
|
|
456
|
+
}`);
|
|
457
|
+
const dependents = (dependentsResult.results?.bindings ?? []).map((b) => b.dependent?.value?.replace('urn:module:', '') ?? '').filter(Boolean);
|
|
458
|
+
// Dependencies
|
|
459
|
+
const depsResult = await adapter.sparqlQuery(`
|
|
460
|
+
SELECT ?dep WHERE {
|
|
461
|
+
GRAPH <${contextUri}> {
|
|
462
|
+
<${moduleUriStr}> <${OTX}dependsOn> ?dep .
|
|
463
|
+
}
|
|
464
|
+
}`);
|
|
465
|
+
const dependencies = (depsResult.results?.bindings ?? []).map((b) => b.dep?.value?.replace('urn:module:', '') ?? '').filter(Boolean);
|
|
466
|
+
// Related context entries
|
|
467
|
+
let related = [];
|
|
468
|
+
try {
|
|
469
|
+
const relatedResult = await adapter.sparqlQuery(`
|
|
470
|
+
SELECT ?type ?title ?status ?date WHERE {
|
|
471
|
+
GRAPH <${contextUri}> {
|
|
472
|
+
?s <${OTX}body> ?body .
|
|
473
|
+
?s a ?type .
|
|
474
|
+
?s <${OTX}title> ?title .
|
|
475
|
+
OPTIONAL { ?s <${OTX}status> ?status }
|
|
476
|
+
OPTIONAL { ?s <${OTX}date> ?date }
|
|
477
|
+
FILTER(CONTAINS(?body, "${opts.file}"))
|
|
478
|
+
}
|
|
479
|
+
} LIMIT 10`);
|
|
480
|
+
related = (relatedResult.results?.bindings ?? []).map((b) => ({
|
|
481
|
+
type: b.type?.value?.replace(OTX, '') ?? '',
|
|
482
|
+
title: b.title?.value ?? '',
|
|
483
|
+
status: b.status?.value,
|
|
484
|
+
date: b.date?.value,
|
|
485
|
+
}));
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// FILTER/CONTAINS may not be supported
|
|
489
|
+
}
|
|
490
|
+
const impact = dependents.length === 0 ? 'low' : dependents.length <= 3 ? 'medium' : 'high';
|
|
491
|
+
if (opts.format === 'json') {
|
|
492
|
+
console.log(JSON.stringify({ filePath: opts.file, dependents, dependencies, related, impact }, null, 2));
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
console.log(pc.bold(`Impact Analysis: ${opts.file}`));
|
|
496
|
+
console.log(`Impact level: ${impact === 'high' ? pc.red(impact) : impact === 'medium' ? pc.yellow(impact) : pc.green(impact)}`);
|
|
497
|
+
console.log('');
|
|
498
|
+
if (dependents.length > 0) {
|
|
499
|
+
console.log(pc.bold(`Dependents (${dependents.length}):`));
|
|
500
|
+
for (const d of dependents)
|
|
501
|
+
console.log(` ${d}`);
|
|
502
|
+
console.log('');
|
|
503
|
+
}
|
|
504
|
+
if (dependencies.length > 0) {
|
|
505
|
+
console.log(pc.bold(`Dependencies (${dependencies.length}):`));
|
|
506
|
+
for (const d of dependencies)
|
|
507
|
+
console.log(` ${d}`);
|
|
508
|
+
console.log('');
|
|
509
|
+
}
|
|
510
|
+
if (related.length > 0) {
|
|
511
|
+
console.log(pc.bold('Related context:'));
|
|
512
|
+
for (const r of related) {
|
|
513
|
+
console.log(` [${r.type}] ${r.title}${r.status ? ` (${r.status})` : ''}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (dependents.length === 0 && dependencies.length === 0) {
|
|
517
|
+
console.log(pc.dim('No module dependencies found. Run `opentology context scan` to populate.'));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
console.error(pc.red(`Error: ${err.message}`));
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
// --- context sync ---
|
|
527
|
+
context
|
|
528
|
+
.command('sync')
|
|
529
|
+
.description('Auto-sync context graph: recover missed sessions from git log, rescan module dependencies')
|
|
530
|
+
.option('--format <type>', 'Output format: table, json', 'table')
|
|
531
|
+
.action(async (opts) => {
|
|
532
|
+
let config;
|
|
533
|
+
try {
|
|
534
|
+
config = loadConfig();
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const result = await syncContext(config, process.cwd());
|
|
542
|
+
if (opts.format === 'json') {
|
|
543
|
+
console.log(JSON.stringify(result, null, 2));
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
console.log(pc.bold('Context Sync'));
|
|
547
|
+
for (const action of result.actions) {
|
|
548
|
+
console.log(` ${pc.green('•')} ${action}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
console.error(pc.red(`Error: ${err.message}`));
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
// --- context graph ---
|
|
558
|
+
context
|
|
559
|
+
.command('graph')
|
|
560
|
+
.description('Open interactive graph visualization in the browser')
|
|
561
|
+
.option('--port <number>', 'Server port (default: auto)', parseInt)
|
|
562
|
+
.action(async (opts) => {
|
|
563
|
+
let config;
|
|
564
|
+
try {
|
|
565
|
+
config = loadConfig();
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
|
|
569
|
+
process.exit(1);
|
|
570
|
+
}
|
|
571
|
+
const graphs = config.graphs ?? {};
|
|
572
|
+
if (!graphs['context']) {
|
|
573
|
+
console.error(pc.red('Error: Context not initialized. Run `opentology context init` first.'));
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const { port } = await startGraphServer({ port: opts.port });
|
|
578
|
+
const url = `http://localhost:${port}`;
|
|
579
|
+
console.log(pc.green(`Graph server running at ${url}`));
|
|
580
|
+
// Open browser
|
|
581
|
+
const { exec } = await import('node:child_process');
|
|
582
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
583
|
+
exec(`${cmd} ${url}`);
|
|
584
|
+
console.log(pc.dim('Press Ctrl+C to stop the server.'));
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
console.error(pc.red(`Error: ${err.message}`));
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
});
|
|
369
591
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
interface CheckResult {
|
|
3
|
+
name: string;
|
|
4
|
+
status: 'ok' | 'warn' | 'fail';
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function runDoctor(): Promise<CheckResult[]>;
|
|
8
|
+
export declare function registerDoctor(program: Command): void;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { configExists, loadConfig } from '../lib/config.js';
|
|
5
|
+
import { createReadyAdapter } from '../lib/store-factory.js';
|
|
6
|
+
export async function runDoctor() {
|
|
7
|
+
const results = [];
|
|
8
|
+
// 1. Config check
|
|
9
|
+
if (!configExists()) {
|
|
10
|
+
results.push({ name: 'Config', status: 'fail', message: 'No .opentology.json found. Run `opentology init` first.' });
|
|
11
|
+
return results; // Can't proceed without config
|
|
12
|
+
}
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
results.push({ name: 'Config', status: 'ok', message: `Project: ${config.projectId} (${config.mode} mode)` });
|
|
15
|
+
// 2. Store connectivity
|
|
16
|
+
try {
|
|
17
|
+
const adapter = await createReadyAdapter(config);
|
|
18
|
+
const r = await adapter.sparqlQuery(`SELECT (COUNT(*) AS ?c) WHERE { GRAPH <${config.graphUri}> { ?s ?p ?o } }`);
|
|
19
|
+
const count = r.results?.bindings?.[0]?.c?.value ?? '0';
|
|
20
|
+
results.push({ name: 'Store', status: 'ok', message: `Connected — ${count} triples in default graph` });
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
results.push({ name: 'Store', status: 'fail', message: `Cannot connect: ${err.message}` });
|
|
24
|
+
}
|
|
25
|
+
// 3. Context initialization
|
|
26
|
+
const graphs = config.graphs ?? {};
|
|
27
|
+
if (graphs['context'] && graphs['sessions']) {
|
|
28
|
+
results.push({ name: 'Context', status: 'ok', message: `context: ${graphs['context']}` });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
results.push({ name: 'Context', status: 'warn', message: 'Not initialized. Run `opentology context init`.' });
|
|
32
|
+
}
|
|
33
|
+
// 4. Hook scripts
|
|
34
|
+
const hookDir = join(process.cwd(), '.opentology', 'hooks');
|
|
35
|
+
const sessionHook = join(hookDir, 'session-start.mjs');
|
|
36
|
+
const preEditHook = join(hookDir, 'pre-edit.mjs');
|
|
37
|
+
const hooksExist = existsSync(sessionHook) && existsSync(preEditHook);
|
|
38
|
+
if (hooksExist) {
|
|
39
|
+
results.push({ name: 'Hooks', status: 'ok', message: 'session-start.mjs + pre-edit.mjs present' });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
results.push({ name: 'Hooks', status: 'warn', message: 'Hook scripts missing. Run `opentology context init`.' });
|
|
43
|
+
}
|
|
44
|
+
// 5. Hook registration in .claude/settings.json
|
|
45
|
+
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
46
|
+
if (existsSync(settingsPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
49
|
+
const hooks = settings.hooks ?? {};
|
|
50
|
+
const hasSession = (hooks.SessionStart ?? []).some((h) => h.command?.includes('session-start.mjs'));
|
|
51
|
+
const hasPreEdit = (hooks.PreToolUse ?? []).some((h) => h.command?.includes('pre-edit.mjs'));
|
|
52
|
+
if (hasSession && hasPreEdit) {
|
|
53
|
+
results.push({ name: 'Settings', status: 'ok', message: 'Both hooks registered in .claude/settings.json' });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const missing = [];
|
|
57
|
+
if (!hasSession)
|
|
58
|
+
missing.push('SessionStart');
|
|
59
|
+
if (!hasPreEdit)
|
|
60
|
+
missing.push('PreToolUse');
|
|
61
|
+
results.push({ name: 'Settings', status: 'warn', message: `Missing hooks: ${missing.join(', ')}. Run \`opentology context init\`.` });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
results.push({ name: 'Settings', status: 'warn', message: 'Cannot parse .claude/settings.json' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
results.push({ name: 'Settings', status: 'warn', message: 'No .claude/settings.json found. Run `opentology context init`.' });
|
|
70
|
+
}
|
|
71
|
+
// 6. CLAUDE.md
|
|
72
|
+
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
73
|
+
if (existsSync(claudeMdPath)) {
|
|
74
|
+
const content = readFileSync(claudeMdPath, 'utf-8');
|
|
75
|
+
if (content.includes('OPENTOLOGY:CONTEXT:BEGIN') || content.includes('OpenTology')) {
|
|
76
|
+
results.push({ name: 'CLAUDE.md', status: 'ok', message: 'Context section present' });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
results.push({ name: 'CLAUDE.md', status: 'warn', message: 'Exists but no OpenTology context section' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
results.push({ name: 'CLAUDE.md', status: 'warn', message: 'Not found. Run `opentology context init`.' });
|
|
84
|
+
}
|
|
85
|
+
// 7. Optional dependencies
|
|
86
|
+
const optDeps = [
|
|
87
|
+
{ name: 'ts-morph', desc: 'TypeScript deep scan' },
|
|
88
|
+
{ name: 'web-tree-sitter', desc: 'Multi-language deep scan' },
|
|
89
|
+
];
|
|
90
|
+
for (const dep of optDeps) {
|
|
91
|
+
try {
|
|
92
|
+
await import(dep.name);
|
|
93
|
+
results.push({ name: dep.name, status: 'ok', message: dep.desc });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
results.push({ name: dep.name, status: 'warn', message: `Not installed (optional — ${dep.desc})` });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
export function registerDoctor(program) {
|
|
102
|
+
program
|
|
103
|
+
.command('doctor')
|
|
104
|
+
.description('Check project health: config, store, context, hooks, dependencies')
|
|
105
|
+
.option('--format <type>', 'Output format: table, json', 'table')
|
|
106
|
+
.action(async (opts) => {
|
|
107
|
+
const results = await runDoctor();
|
|
108
|
+
if (opts.format === 'json') {
|
|
109
|
+
console.log(JSON.stringify(results, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(pc.bold('\nOpenTology Doctor\n'));
|
|
113
|
+
for (const r of results) {
|
|
114
|
+
const icon = r.status === 'ok' ? pc.green('✓') : r.status === 'warn' ? pc.yellow('!') : pc.red('✗');
|
|
115
|
+
const msg = r.status === 'fail' ? pc.red(r.message) : r.status === 'warn' ? pc.yellow(r.message) : r.message;
|
|
116
|
+
console.log(` ${icon} ${pc.bold(r.name)}: ${msg}`);
|
|
117
|
+
}
|
|
118
|
+
const fails = results.filter((r) => r.status === 'fail').length;
|
|
119
|
+
const warns = results.filter((r) => r.status === 'warn').length;
|
|
120
|
+
console.log('');
|
|
121
|
+
if (fails > 0) {
|
|
122
|
+
console.log(pc.red(` ${fails} error(s), ${warns} warning(s)`));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
else if (warns > 0) {
|
|
126
|
+
console.log(pc.yellow(` ${warns} warning(s), everything else looks good`));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log(pc.green(' All checks passed!'));
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -16,10 +16,11 @@ import { registerInfer } from './commands/infer.js';
|
|
|
16
16
|
import { registerPrefix } from './commands/prefix.js';
|
|
17
17
|
import { registerContext } from './commands/context.js';
|
|
18
18
|
import { registerViz } from './commands/viz.js';
|
|
19
|
+
import { registerDoctor } from './commands/doctor.js';
|
|
19
20
|
const program = new Command();
|
|
20
21
|
program
|
|
21
22
|
.name('opentology')
|
|
22
|
-
.version('0.
|
|
23
|
+
.version('0.2.2')
|
|
23
24
|
.description('CLI-managed RDF/SPARQL infrastructure — Supabase for RDF');
|
|
24
25
|
registerInit(program);
|
|
25
26
|
registerValidate(program);
|
|
@@ -37,4 +38,5 @@ registerInfer(program);
|
|
|
37
38
|
registerPrefix(program);
|
|
38
39
|
registerContext(program);
|
|
39
40
|
registerViz(program);
|
|
41
|
+
registerDoctor(program);
|
|
40
42
|
program.parse(process.argv);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { OpenTologyConfig } from './config.js';
|
|
2
|
+
export interface SyncResult {
|
|
3
|
+
sessionsRecovered: number;
|
|
4
|
+
modulesUpdated: boolean;
|
|
5
|
+
moduleStats: {
|
|
6
|
+
modules: number;
|
|
7
|
+
edges: number;
|
|
8
|
+
} | null;
|
|
9
|
+
actions: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Core sync logic: recover missed sessions and rescan modules.
|
|
13
|
+
* Persists via .ttl files for embedded mode compatibility.
|
|
14
|
+
*/
|
|
15
|
+
export declare function syncContext(config: OpenTologyConfig, projectRoot: string): Promise<SyncResult>;
|