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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenTology
2
2
 
3
- > CLI-managed RDF/SPARQL infrastructure with RDFS reasoning and SHACL validation -- Supabase for knowledge graphs
3
+ > CLI-managed RDF/SPARQL infrastructure with RDFS reasoning, SHACL validation, and interactive graph visualization -- Supabase for knowledge graphs
4
4
 
5
5
  [English](#english) | [한국어](#한국어)
6
6
 
@@ -22,7 +22,7 @@ graph TB
22
22
  end
23
23
 
24
24
  subgraph MCP["MCP Server"]
25
- Tools[18 Tools]
25
+ Tools[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 query, all from your terminal. It ships an MCP server so AI assistants can manage your knowledge graph directly. It runs in embedded mode with zero Docker dependency, or connects to an Oxigraph server over HTTP.
45
+ Existing ontology tools have terrible developer experience. OpenTology gives you managed RDF with a simple CLI -- initialize a project, write Turtle, validate with SHACL, push with automatic RDFS inference, query, and visualize your graph in an interactive web UI, all from your terminal. It ships an MCP server so AI assistants can manage your knowledge graph directly. It runs in embedded mode with zero Docker dependency, or connects to an Oxigraph server over HTTP.
46
46
 
47
47
  ### System Requirements
48
48
 
@@ -97,7 +97,7 @@ graph LR
97
97
  | Docker required | No (embedded mode) | Yes | Yes |
98
98
  | RDFS reasoning | Automatic on push | Manual SPARQL CONSTRUCT | Not native |
99
99
  | SHACL validation | Built-in | Manual tooling | N/A |
100
- | AI integration | MCP server with 18 tools | None | Plugin ecosystem |
100
+ | AI integration | MCP server with 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
- - 14 CLI commands covering the full RDF lifecycle
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 18 tools and 1 resource
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
- **18 Tools:**
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 14 commands
328
- - [x] MCP server with 18 tools and 1 resource
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
- - [ ] Web UI for graph exploration
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[18 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 쿼리까지 CLI 하나로 처리합니다. MCP 서버를 내장하고 있어 AI 어시스턴트가 지식 그래프를 직접 다룰 수 있고, Docker 없이 임베디드 모드로 바로 시작할 수 있습니다.
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 서버 18개 도구 | 없음 | 플러그인 생태계 |
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 전체 생애주기를 다루는 14개 CLI 명령어
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
- - 18개 도구와 1개 리소스를 제공하는 MCP 서버
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
- **18개 도구:**
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] 14개 CLI 명령어
675
- - [x] 18개 도구와 1개 리소스를 갖춘 MCP 서버
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
- - [ ] 그래프 탐색 웹 UI
716
+ - [x] 인터랙티브 그래프 시각화 웹 UI
689
717
 
690
718
  ### 기여 방법
691
719
 
@@ -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
- // Print hook registration instructions
142
- console.log('');
143
- console.log(pc.bold('Add this to your project .claude/settings.json:'));
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
- console.log(JSON.stringify({
146
- hooks: {
147
- SessionStart: [{
148
- type: 'command',
149
- command: 'node .opentology/hooks/session-start.mjs',
150
- }],
151
- },
152
- }, null, 2));
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.1.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>;