opentology 0.3.2 → 0.3.4

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.
Files changed (44) hide show
  1. package/README.md +6 -0
  2. package/dist/index.js +0 -30
  3. package/dist/{commands → lib}/doctor.d.ts +1 -4
  4. package/dist/{commands → lib}/doctor.js +2 -35
  5. package/dist/lib/store-factory.js +0 -4
  6. package/dist/mcp/server.d.ts +27 -0
  7. package/dist/mcp/server.js +122 -1
  8. package/dist/templates/post-error-hook.d.ts +1 -0
  9. package/dist/templates/post-error-hook.js +146 -0
  10. package/dist/templates/user-prompt-hook.d.ts +1 -0
  11. package/dist/templates/user-prompt-hook.js +139 -0
  12. package/package.json +1 -1
  13. package/dist/commands/context.d.ts +0 -29
  14. package/dist/commands/context.js +0 -616
  15. package/dist/commands/delete.d.ts +0 -2
  16. package/dist/commands/delete.js +0 -46
  17. package/dist/commands/diff.d.ts +0 -2
  18. package/dist/commands/diff.js +0 -49
  19. package/dist/commands/drop.d.ts +0 -2
  20. package/dist/commands/drop.js +0 -43
  21. package/dist/commands/graph.d.ts +0 -2
  22. package/dist/commands/graph.js +0 -130
  23. package/dist/commands/infer.d.ts +0 -2
  24. package/dist/commands/infer.js +0 -47
  25. package/dist/commands/prefix.d.ts +0 -2
  26. package/dist/commands/prefix.js +0 -73
  27. package/dist/commands/pull.d.ts +0 -2
  28. package/dist/commands/pull.js +0 -43
  29. package/dist/commands/push.d.ts +0 -2
  30. package/dist/commands/push.js +0 -79
  31. package/dist/commands/rollback.d.ts +0 -2
  32. package/dist/commands/rollback.js +0 -75
  33. package/dist/commands/shapes.d.ts +0 -2
  34. package/dist/commands/shapes.js +0 -67
  35. package/dist/commands/status.d.ts +0 -2
  36. package/dist/commands/status.js +0 -47
  37. package/dist/commands/validate.d.ts +0 -2
  38. package/dist/commands/validate.js +0 -46
  39. package/dist/commands/viz.d.ts +0 -2
  40. package/dist/commands/viz.js +0 -53
  41. package/dist/lib/http-adapter.d.ts +0 -45
  42. package/dist/lib/http-adapter.js +0 -199
  43. package/dist/lib/oxigraph.d.ts +0 -62
  44. package/dist/lib/oxigraph.js +0 -323
@@ -1,616 +0,0 @@
1
- import pc from 'picocolors';
2
- import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, readdirSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { createInterface } from 'node:readline';
5
- import { loadConfig, saveConfig } from '../lib/config.js';
6
- import { createReadyAdapter } from '../lib/store-factory.js';
7
- import { startGraphServer } from '../lib/graph-server.js';
8
- import { syncContext } from '../lib/context-sync.js';
9
- import { OTX_BOOTSTRAP_TURTLE } from '../templates/otx-ontology.js';
10
- import { generateContextSection, updateClaudeMd } from '../templates/claude-md-context.js';
11
- import { generateHookScript } from '../templates/session-start-hook.js';
12
- import { generatePreEditHookScript } from '../templates/pre-edit-hook.js';
13
- import { generateSlashCommands } from '../templates/slash-commands.js';
14
- import { normalizeModuleUri } from '../lib/module-uri.js';
15
- function ask(question) {
16
- const rl = createInterface({ input: process.stdin, output: process.stdout });
17
- return new Promise((resolve) => {
18
- rl.question(question, (answer) => {
19
- rl.close();
20
- resolve(answer.trim().toLowerCase());
21
- });
22
- });
23
- }
24
- export function registerContext(program) {
25
- const context = program
26
- .command('context')
27
- .description('Manage project context graph for Claude Code integration');
28
- // --- context init ---
29
- context
30
- .command('init')
31
- .description('Initialize project context graph and Claude Code hook')
32
- .option('--force', 'Regenerate hook script and CLAUDE.md even if they exist')
33
- .action(async (opts) => {
34
- let config;
35
- try {
36
- config = loadConfig();
37
- }
38
- catch {
39
- console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
40
- process.exit(1);
41
- }
42
- const graphs = config.graphs ?? {};
43
- const contextUri = `${config.graphUri}/context`;
44
- const sessionsUri = `${config.graphUri}/sessions`;
45
- let createdHook = false;
46
- let createdClaudeMd = false;
47
- try {
48
- // Step 1: Create graphs in config
49
- let graphsChanged = false;
50
- if (!graphs['context']) {
51
- graphs['context'] = contextUri;
52
- graphsChanged = true;
53
- console.log(pc.green(` Created graph 'context' -> ${contextUri}`));
54
- }
55
- else {
56
- console.log(pc.dim(` Graph 'context' already exists — skipped`));
57
- }
58
- if (!graphs['sessions']) {
59
- graphs['sessions'] = sessionsUri;
60
- graphsChanged = true;
61
- console.log(pc.green(` Created graph 'sessions' -> ${sessionsUri}`));
62
- }
63
- else {
64
- console.log(pc.dim(` Graph 'sessions' already exists — skipped`));
65
- }
66
- if (graphsChanged) {
67
- config.graphs = graphs;
68
- }
69
- // Step 2: Bootstrap ontology — persist as tracked file for embedded mode
70
- const ontologyDir = join(process.cwd(), '.opentology');
71
- const ontologyPath = join(ontologyDir, 'ontology.ttl');
72
- if (!existsSync(ontologyPath) || opts.force) {
73
- mkdirSync(ontologyDir, { recursive: true });
74
- writeFileSync(ontologyPath, OTX_BOOTSTRAP_TURTLE, 'utf-8');
75
- // Track ontology file so embedded adapter loads it automatically
76
- if (!config.files)
77
- config.files = {};
78
- if (!config.files[contextUri])
79
- config.files[contextUri] = [];
80
- const relPath = '.opentology/ontology.ttl';
81
- if (!config.files[contextUri].includes(relPath)) {
82
- config.files[contextUri].push(relPath);
83
- }
84
- console.log(pc.green(' Bootstrapped otx ontology vocabulary (6 classes, 12 properties)'));
85
- }
86
- else {
87
- console.log(pc.dim(' Ontology already bootstrapped — skipped'));
88
- }
89
- // Create adapter after config is updated with tracked files
90
- const adapter = await createReadyAdapter(config);
91
- // Step 3: Write hook script
92
- const hookDir = join(process.cwd(), '.opentology', 'hooks');
93
- const hookPath = join(hookDir, 'session-start.mjs');
94
- if (!existsSync(hookPath) || opts.force) {
95
- mkdirSync(hookDir, { recursive: true });
96
- writeFileSync(hookPath, generateHookScript(), 'utf-8');
97
- createdHook = true;
98
- console.log(pc.green(` Generated hook script: .opentology/hooks/session-start.mjs`));
99
- }
100
- else {
101
- console.log(pc.dim(' Hook script already exists — skipped (use --force to regenerate)'));
102
- }
103
- // Step 3b: Write pre-edit hook script
104
- const preEditHookPath = join(hookDir, 'pre-edit.mjs');
105
- if (!existsSync(preEditHookPath) || opts.force) {
106
- mkdirSync(hookDir, { recursive: true });
107
- writeFileSync(preEditHookPath, generatePreEditHookScript(), 'utf-8');
108
- console.log(pc.green(` Generated hook script: .opentology/hooks/pre-edit.mjs`));
109
- }
110
- else {
111
- console.log(pc.dim(' Pre-edit hook already exists — skipped'));
112
- }
113
- // Step 4: Update CLAUDE.md
114
- const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
115
- const section = generateContextSection(config.projectId, config.graphUri);
116
- if (!existsSync(claudeMdPath) || opts.force) {
117
- updateClaudeMd(claudeMdPath, section);
118
- createdClaudeMd = true;
119
- console.log(pc.green(' Updated CLAUDE.md with context management instructions'));
120
- }
121
- else {
122
- // Check for markers — update if present, append if not
123
- const { readFileSync } = await import('node:fs');
124
- const content = readFileSync(claudeMdPath, 'utf-8');
125
- if (content.includes('OPENTOLOGY:CONTEXT:BEGIN')) {
126
- updateClaudeMd(claudeMdPath, section);
127
- createdClaudeMd = true;
128
- console.log(pc.green(' Updated CLAUDE.md context section'));
129
- }
130
- else {
131
- updateClaudeMd(claudeMdPath, section);
132
- createdClaudeMd = true;
133
- console.log(pc.green(' Appended context section to CLAUDE.md'));
134
- }
135
- }
136
- // Step 5: Generate slash commands
137
- const commandsDir = join(process.cwd(), '.claude', 'commands');
138
- const slashCommands = generateSlashCommands();
139
- const expectedFilenames = new Set(slashCommands.map((c) => c.filename));
140
- let slashCreated = 0;
141
- mkdirSync(commandsDir, { recursive: true });
142
- // Clean up stale slash command files from previous naming conventions
143
- if (existsSync(commandsDir)) {
144
- for (const file of readdirSync(commandsDir)) {
145
- if (file.endsWith('.md') && !expectedFilenames.has(file) && file.includes('context-')) {
146
- unlinkSync(join(commandsDir, file));
147
- }
148
- }
149
- }
150
- for (const cmd of slashCommands) {
151
- const cmdPath = join(commandsDir, cmd.filename);
152
- if (!existsSync(cmdPath) || opts.force) {
153
- writeFileSync(cmdPath, cmd.content, 'utf-8');
154
- slashCreated++;
155
- }
156
- }
157
- if (slashCreated > 0) {
158
- console.log(pc.green(` Generated ${slashCreated} slash commands in .claude/commands/`));
159
- }
160
- else {
161
- console.log(pc.dim(' Slash commands already exist — skipped'));
162
- }
163
- // Step 6: Save config LAST (atomic commit point)
164
- saveConfig(config);
165
- // Step 7: Register hooks in .claude/settings.json
166
- const settingsDir = join(process.cwd(), '.claude');
167
- const settingsPath = join(settingsDir, 'settings.json');
168
- mkdirSync(settingsDir, { recursive: true });
169
- let settings = {};
170
- if (existsSync(settingsPath)) {
171
- try {
172
- settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
173
- }
174
- catch { /* start fresh */ }
175
- }
176
- const hooks = (settings.hooks ?? {});
177
- // Always register SessionStart hook
178
- const sessionStartCmd = 'node .opentology/hooks/session-start.mjs';
179
- if (!hooks.SessionStart)
180
- hooks.SessionStart = [];
181
- const hasSessionHook = hooks.SessionStart.some((h) => {
182
- const entry = h;
183
- const entryHooks = entry.hooks;
184
- return entryHooks?.some((hook) => hook.command === sessionStartCmd);
185
- });
186
- if (!hasSessionHook) {
187
- hooks.SessionStart.push({
188
- matcher: '',
189
- hooks: [{ type: 'command', command: sessionStartCmd }],
190
- });
191
- console.log(pc.green(' Registered SessionStart hook in .claude/settings.json'));
192
- }
193
- // Ask about PreToolUse impact hook
194
- console.log('');
195
- const enableImpact = await ask(pc.bold('Enable PreToolUse impact analysis hook? ') +
196
- pc.dim('(auto-shows dependents before Edit/Write)') +
197
- ' [Y/n] ');
198
- if (enableImpact !== 'n' && enableImpact !== 'no') {
199
- const preEditCmd = 'node .opentology/hooks/pre-edit.mjs';
200
- if (!hooks.PreToolUse)
201
- hooks.PreToolUse = [];
202
- const hasPreEditHook = hooks.PreToolUse.some((h) => {
203
- const entry = h;
204
- const entryHooks = entry.hooks;
205
- return entryHooks?.some((hook) => hook.command === preEditCmd);
206
- });
207
- if (!hasPreEditHook) {
208
- hooks.PreToolUse.push({
209
- matcher: 'Edit|Write',
210
- hooks: [{ type: 'command', command: preEditCmd }],
211
- });
212
- }
213
- console.log(pc.green(' Registered PreToolUse impact hook in .claude/settings.json'));
214
- }
215
- else {
216
- console.log(pc.dim(' Skipped PreToolUse hook registration'));
217
- }
218
- settings.hooks = hooks;
219
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
220
- // Ensure .opentology/snapshots/ is in .gitignore
221
- const gitignorePath = join(process.cwd(), '.gitignore');
222
- const snapshotIgnore = '.opentology/snapshots/';
223
- if (existsSync(gitignorePath)) {
224
- const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
225
- if (!gitignoreContent.includes(snapshotIgnore)) {
226
- writeFileSync(gitignorePath, gitignoreContent.trimEnd() + '\n' + snapshotIgnore + '\n', 'utf-8');
227
- console.log(pc.green(' Added .opentology/snapshots/ to .gitignore'));
228
- }
229
- }
230
- else {
231
- writeFileSync(gitignorePath, snapshotIgnore + '\n', 'utf-8');
232
- console.log(pc.green(' Created .gitignore with .opentology/snapshots/'));
233
- }
234
- console.log('');
235
- console.log(pc.dim('Consider adding .opentology/hooks/ to version control so team members share the hook.'));
236
- }
237
- catch (err) {
238
- // Rollback: clean up files created in this run
239
- if (createdHook) {
240
- const hookPath = join(process.cwd(), '.opentology', 'hooks', 'session-start.mjs');
241
- try {
242
- unlinkSync(hookPath);
243
- }
244
- catch { /* ignore */ }
245
- }
246
- if (createdClaudeMd) {
247
- // Only delete if we created a new file (not appended to existing)
248
- const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
249
- // Don't delete existing CLAUDE.md — too risky. Just warn.
250
- }
251
- console.error(pc.red(`Error during context init: ${err.message}`));
252
- console.error(pc.dim('Config was NOT modified. Fix the error and retry.'));
253
- process.exit(1);
254
- }
255
- });
256
- // --- context load ---
257
- context
258
- .command('load')
259
- .description('Load project context (recent sessions, open issues, decisions)')
260
- .option('--format <type>', 'Output format: table, json', 'table')
261
- .action(async (opts) => {
262
- let config;
263
- try {
264
- config = loadConfig();
265
- }
266
- catch {
267
- console.error('Error: No .opentology.json found. Run `opentology init` first.');
268
- process.exit(1);
269
- }
270
- const graphs = config.graphs ?? {};
271
- if (!graphs['context'] || !graphs['sessions']) {
272
- console.error('Error: Context not initialized. Run `opentology context init` first.');
273
- process.exit(1);
274
- }
275
- const contextUri = graphs['context'];
276
- const sessionsUri = graphs['sessions'];
277
- const output = {
278
- projectId: config.projectId,
279
- graphUri: config.graphUri,
280
- sessions: [],
281
- openIssues: [],
282
- recentDecisions: [],
283
- meta: {
284
- contextTripleCount: 0,
285
- sessionsTripleCount: 0,
286
- loadedAt: new Date().toISOString(),
287
- },
288
- warnings: [],
289
- };
290
- try {
291
- const adapter = await createReadyAdapter(config);
292
- // Query 1: Recent sessions
293
- try {
294
- const sessionsResult = await adapter.sparqlQuery(`
295
- PREFIX otx: <https://opentology.dev/vocab#>
296
- SELECT ?session ?title ?date ?nextTodo WHERE {
297
- GRAPH <${sessionsUri}> {
298
- ?session a otx:Session ;
299
- otx:title ?title ;
300
- otx:date ?date .
301
- OPTIONAL { ?session otx:nextTodo ?nextTodo }
302
- }
303
- } ORDER BY DESC(?date) LIMIT 3
304
- `);
305
- output.sessions = sessionsResult.results.bindings.map((b) => ({
306
- uri: b['session']?.value ?? '',
307
- title: b['title']?.value ?? '',
308
- date: b['date']?.value ?? '',
309
- ...(b['nextTodo']?.value ? { nextTodo: b['nextTodo'].value } : {}),
310
- }));
311
- }
312
- catch (err) {
313
- output.warnings.push(`Failed to query sessions: ${err.message}`);
314
- }
315
- // Query 2: Open issues
316
- try {
317
- const issuesResult = await adapter.sparqlQuery(`
318
- PREFIX otx: <https://opentology.dev/vocab#>
319
- SELECT ?issue ?title ?date WHERE {
320
- GRAPH <${contextUri}> {
321
- ?issue a otx:Issue ;
322
- otx:title ?title ;
323
- otx:date ?date ;
324
- otx:status "open" .
325
- }
326
- } ORDER BY DESC(?date) LIMIT 10
327
- `);
328
- output.openIssues = issuesResult.results.bindings.map((b) => ({
329
- uri: b['issue']?.value ?? '',
330
- title: b['title']?.value ?? '',
331
- date: b['date']?.value ?? '',
332
- }));
333
- }
334
- catch (err) {
335
- output.warnings.push(`Failed to query issues: ${err.message}`);
336
- }
337
- // Query 3: Recent decisions
338
- try {
339
- const decisionsResult = await adapter.sparqlQuery(`
340
- PREFIX otx: <https://opentology.dev/vocab#>
341
- SELECT ?decision ?title ?date ?reason WHERE {
342
- GRAPH <${contextUri}> {
343
- ?decision a otx:Decision ;
344
- otx:title ?title ;
345
- otx:date ?date .
346
- OPTIONAL { ?decision otx:reason ?reason }
347
- }
348
- } ORDER BY DESC(?date) LIMIT 3
349
- `);
350
- output.recentDecisions = decisionsResult.results.bindings.map((b) => ({
351
- uri: b['decision']?.value ?? '',
352
- title: b['title']?.value ?? '',
353
- date: b['date']?.value ?? '',
354
- ...(b['reason']?.value ? { reason: b['reason'].value } : {}),
355
- }));
356
- }
357
- catch (err) {
358
- output.warnings.push(`Failed to query decisions: ${err.message}`);
359
- }
360
- // Meta: triple counts
361
- try {
362
- output.meta.contextTripleCount = await adapter.getGraphTripleCount(contextUri);
363
- }
364
- catch { /* ignore */ }
365
- try {
366
- output.meta.sessionsTripleCount = await adapter.getGraphTripleCount(sessionsUri);
367
- }
368
- catch { /* ignore */ }
369
- // Clean up empty warnings
370
- if (output.warnings.length === 0)
371
- delete output.warnings;
372
- }
373
- catch (err) {
374
- console.error(`Error: ${err.message}`);
375
- process.exit(1);
376
- }
377
- if (opts.format === 'json') {
378
- console.log(JSON.stringify(output, null, 2));
379
- }
380
- else {
381
- // Table format
382
- console.log(pc.bold(`Project: ${output.projectId}`));
383
- console.log('');
384
- if (output.sessions.length > 0) {
385
- console.log(pc.bold('Recent Sessions:'));
386
- for (const s of output.sessions) {
387
- console.log(` ${pc.dim(s.date)} ${s.title}`);
388
- if (s.nextTodo)
389
- console.log(` ${pc.dim('Next:')} ${s.nextTodo}`);
390
- }
391
- console.log('');
392
- }
393
- if (output.openIssues.length > 0) {
394
- console.log(pc.bold(`Open Issues (${output.openIssues.length}):`));
395
- for (const i of output.openIssues) {
396
- console.log(` ${pc.dim(i.date)} ${i.title}`);
397
- }
398
- console.log('');
399
- }
400
- if (output.recentDecisions.length > 0) {
401
- console.log(pc.bold('Recent Decisions:'));
402
- for (const d of output.recentDecisions) {
403
- console.log(` ${pc.dim(d.date)} ${d.title}`);
404
- }
405
- console.log('');
406
- }
407
- console.log(pc.dim(`Context: ${output.meta.contextTripleCount} triples | Sessions: ${output.meta.sessionsTripleCount} triples`));
408
- }
409
- });
410
- // --- context status ---
411
- context
412
- .command('status')
413
- .description('Show context initialization status')
414
- .action(async () => {
415
- let config;
416
- try {
417
- config = loadConfig();
418
- }
419
- catch {
420
- console.log('Context: ' + pc.red('not initialized') + ' (no .opentology.json)');
421
- return;
422
- }
423
- const graphs = config.graphs ?? {};
424
- const hasContext = !!graphs['context'];
425
- const hasSessions = !!graphs['sessions'];
426
- const initialized = hasContext && hasSessions;
427
- console.log('Context: ' + (initialized ? pc.green('initialized') : pc.red('not initialized')));
428
- if (initialized) {
429
- const adapter = await createReadyAdapter(config);
430
- const contextCount = await adapter.getGraphTripleCount(graphs['context']).catch(() => 0);
431
- const sessionsCount = await adapter.getGraphTripleCount(graphs['sessions']).catch(() => 0);
432
- console.log('Graphs:');
433
- console.log(` context ${graphs['context']} (${contextCount} triples)`);
434
- console.log(` sessions ${graphs['sessions']} (${sessionsCount} triples)`);
435
- }
436
- const hookPath = join(process.cwd(), '.opentology', 'hooks', 'session-start.mjs');
437
- const hookExists = existsSync(hookPath);
438
- console.log('Hook: .opentology/hooks/session-start.mjs ' + (hookExists ? pc.green('(exists)') : pc.red('(missing)')));
439
- const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
440
- if (!existsSync(claudeMdPath)) {
441
- console.log('CLAUDE.md: ' + pc.red('file missing'));
442
- }
443
- else {
444
- const { readFileSync } = await import('node:fs');
445
- const content = readFileSync(claudeMdPath, 'utf-8');
446
- const hasMarkers = content.includes('OPENTOLOGY:CONTEXT:BEGIN');
447
- console.log('CLAUDE.md: ' + (hasMarkers ? pc.green('markers present') : pc.yellow('markers missing')));
448
- }
449
- });
450
- // --- context impact ---
451
- context
452
- .command('impact')
453
- .description('Analyze impact of modifying a file (dependents, dependencies, related context)')
454
- .requiredOption('--file <path>', 'Relative file path to analyze')
455
- .option('--format <type>', 'Output format: table, json', 'table')
456
- .action(async (opts) => {
457
- let config;
458
- try {
459
- config = loadConfig();
460
- }
461
- catch {
462
- console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
463
- process.exit(1);
464
- }
465
- const graphs = config.graphs ?? {};
466
- if (!graphs['context']) {
467
- console.error(pc.red('Error: Context not initialized. Run `opentology context init` first.'));
468
- process.exit(1);
469
- }
470
- const contextUri = graphs['context'];
471
- const OTX = 'https://opentology.dev/vocab#';
472
- const moduleUriStr = normalizeModuleUri(opts.file);
473
- try {
474
- const adapter = await createReadyAdapter(config);
475
- // Dependents (reverse deps)
476
- const dependentsResult = await adapter.sparqlQuery(`
477
- SELECT ?dependent WHERE {
478
- GRAPH <${contextUri}> {
479
- ?dependent <${OTX}dependsOn> <${moduleUriStr}> .
480
- }
481
- }`);
482
- const dependents = (dependentsResult.results?.bindings ?? []).map((b) => b.dependent?.value?.replace('urn:module:', '') ?? '').filter(Boolean);
483
- // Dependencies
484
- const depsResult = await adapter.sparqlQuery(`
485
- SELECT ?dep WHERE {
486
- GRAPH <${contextUri}> {
487
- <${moduleUriStr}> <${OTX}dependsOn> ?dep .
488
- }
489
- }`);
490
- const dependencies = (depsResult.results?.bindings ?? []).map((b) => b.dep?.value?.replace('urn:module:', '') ?? '').filter(Boolean);
491
- // Related context entries
492
- let related = [];
493
- try {
494
- const relatedResult = await adapter.sparqlQuery(`
495
- SELECT ?type ?title ?status ?date WHERE {
496
- GRAPH <${contextUri}> {
497
- ?s <${OTX}body> ?body .
498
- ?s a ?type .
499
- ?s <${OTX}title> ?title .
500
- OPTIONAL { ?s <${OTX}status> ?status }
501
- OPTIONAL { ?s <${OTX}date> ?date }
502
- FILTER(CONTAINS(?body, "${opts.file}"))
503
- }
504
- } LIMIT 10`);
505
- related = (relatedResult.results?.bindings ?? []).map((b) => ({
506
- type: b.type?.value?.replace(OTX, '') ?? '',
507
- title: b.title?.value ?? '',
508
- status: b.status?.value,
509
- date: b.date?.value,
510
- }));
511
- }
512
- catch {
513
- // FILTER/CONTAINS may not be supported
514
- }
515
- const impact = dependents.length === 0 ? 'low' : dependents.length <= 3 ? 'medium' : 'high';
516
- if (opts.format === 'json') {
517
- console.log(JSON.stringify({ filePath: opts.file, dependents, dependencies, related, impact }, null, 2));
518
- }
519
- else {
520
- console.log(pc.bold(`Impact Analysis: ${opts.file}`));
521
- console.log(`Impact level: ${impact === 'high' ? pc.red(impact) : impact === 'medium' ? pc.yellow(impact) : pc.green(impact)}`);
522
- console.log('');
523
- if (dependents.length > 0) {
524
- console.log(pc.bold(`Dependents (${dependents.length}):`));
525
- for (const d of dependents)
526
- console.log(` ${d}`);
527
- console.log('');
528
- }
529
- if (dependencies.length > 0) {
530
- console.log(pc.bold(`Dependencies (${dependencies.length}):`));
531
- for (const d of dependencies)
532
- console.log(` ${d}`);
533
- console.log('');
534
- }
535
- if (related.length > 0) {
536
- console.log(pc.bold('Related context:'));
537
- for (const r of related) {
538
- console.log(` [${r.type}] ${r.title}${r.status ? ` (${r.status})` : ''}`);
539
- }
540
- }
541
- if (dependents.length === 0 && dependencies.length === 0) {
542
- console.log(pc.dim('No module dependencies found. Run `opentology context scan` to populate.'));
543
- }
544
- }
545
- }
546
- catch (err) {
547
- console.error(pc.red(`Error: ${err.message}`));
548
- process.exit(1);
549
- }
550
- });
551
- // --- context sync ---
552
- context
553
- .command('sync')
554
- .description('Auto-sync context graph: recover missed sessions from git log, rescan module dependencies')
555
- .option('--format <type>', 'Output format: table, json', 'table')
556
- .action(async (opts) => {
557
- let config;
558
- try {
559
- config = loadConfig();
560
- }
561
- catch {
562
- console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
563
- process.exit(1);
564
- }
565
- try {
566
- const result = await syncContext(config, process.cwd());
567
- if (opts.format === 'json') {
568
- console.log(JSON.stringify(result, null, 2));
569
- }
570
- else {
571
- console.log(pc.bold('Context Sync'));
572
- for (const action of result.actions) {
573
- console.log(` ${pc.green('•')} ${action}`);
574
- }
575
- }
576
- }
577
- catch (err) {
578
- console.error(pc.red(`Error: ${err.message}`));
579
- process.exit(1);
580
- }
581
- });
582
- // --- context graph ---
583
- context
584
- .command('graph')
585
- .description('Open interactive graph visualization in the browser')
586
- .option('--port <number>', 'Server port (default: auto)', parseInt)
587
- .action(async (opts) => {
588
- let config;
589
- try {
590
- config = loadConfig();
591
- }
592
- catch {
593
- console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
594
- process.exit(1);
595
- }
596
- const graphs = config.graphs ?? {};
597
- if (!graphs['context']) {
598
- console.error(pc.red('Error: Context not initialized. Run `opentology context init` first.'));
599
- process.exit(1);
600
- }
601
- try {
602
- const { port } = await startGraphServer({ port: opts.port });
603
- const url = `http://localhost:${port}`;
604
- console.log(pc.green(`Graph server running at ${url}`));
605
- // Open browser
606
- const { exec } = await import('node:child_process');
607
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
608
- exec(`${cmd} ${url}`);
609
- console.log(pc.dim('Press Ctrl+C to stop the server.'));
610
- }
611
- catch (err) {
612
- console.error(pc.red(`Error: ${err.message}`));
613
- process.exit(1);
614
- }
615
- });
616
- }
@@ -1,2 +0,0 @@
1
- import { Command } from 'commander';
2
- export declare function registerDelete(program: Command): void;
@@ -1,46 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import pc from 'picocolors';
3
- import { loadConfig, resolveGraphUri } from '../lib/config.js';
4
- import { createReadyAdapter } from '../lib/store-factory.js';
5
- export function registerDelete(program) {
6
- program
7
- .command('delete [file]')
8
- .description('Delete specific triples from the project graph')
9
- .option('--where <pattern>', 'SPARQL WHERE pattern for pattern-based deletion')
10
- .option('--graph <name>', 'Target a specific named graph')
11
- .action(async (file, opts) => {
12
- let config;
13
- try {
14
- config = loadConfig();
15
- }
16
- catch (err) {
17
- console.error(`Error: ${err.message}`);
18
- process.exit(1);
19
- }
20
- if (!file && !opts.where) {
21
- console.error('Provide a Turtle file or --where pattern');
22
- process.exit(1);
23
- }
24
- if (config.mode === 'embedded') {
25
- console.error('Delete is not supported in embedded mode — edit your .ttl files directly.');
26
- process.exit(1);
27
- }
28
- const graphUri = opts.graph ? resolveGraphUri(config, opts.graph) : config.graphUri;
29
- try {
30
- const adapter = await createReadyAdapter(config);
31
- if (file) {
32
- const content = readFileSync(file, 'utf-8');
33
- await adapter.deleteTriples(graphUri, { turtle: content });
34
- console.log(pc.green(`Deleted triples from ${file}`));
35
- }
36
- else if (opts.where) {
37
- await adapter.deleteTriples(graphUri, { where: opts.where });
38
- console.log(pc.green('Deleted triples matching pattern'));
39
- }
40
- }
41
- catch (err) {
42
- console.error(`Error: ${err.message}`);
43
- process.exit(1);
44
- }
45
- });
46
- }
@@ -1,2 +0,0 @@
1
- import { Command } from 'commander';
2
- export declare function registerDiff(program: Command): void;