opentology 0.1.0

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +609 -0
  3. package/dist/commands/context.d.ts +29 -0
  4. package/dist/commands/context.js +369 -0
  5. package/dist/commands/delete.d.ts +2 -0
  6. package/dist/commands/delete.js +46 -0
  7. package/dist/commands/diff.d.ts +2 -0
  8. package/dist/commands/diff.js +43 -0
  9. package/dist/commands/drop.d.ts +2 -0
  10. package/dist/commands/drop.js +41 -0
  11. package/dist/commands/graph.d.ts +2 -0
  12. package/dist/commands/graph.js +130 -0
  13. package/dist/commands/infer.d.ts +2 -0
  14. package/dist/commands/infer.js +47 -0
  15. package/dist/commands/init.d.ts +2 -0
  16. package/dist/commands/init.js +53 -0
  17. package/dist/commands/mcp.d.ts +2 -0
  18. package/dist/commands/mcp.js +9 -0
  19. package/dist/commands/prefix.d.ts +2 -0
  20. package/dist/commands/prefix.js +73 -0
  21. package/dist/commands/pull.d.ts +2 -0
  22. package/dist/commands/pull.js +43 -0
  23. package/dist/commands/push.d.ts +2 -0
  24. package/dist/commands/push.js +79 -0
  25. package/dist/commands/query.d.ts +2 -0
  26. package/dist/commands/query.js +119 -0
  27. package/dist/commands/shapes.d.ts +2 -0
  28. package/dist/commands/shapes.js +67 -0
  29. package/dist/commands/status.d.ts +2 -0
  30. package/dist/commands/status.js +47 -0
  31. package/dist/commands/validate.d.ts +2 -0
  32. package/dist/commands/validate.js +46 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +38 -0
  35. package/dist/lib/codebase-scanner.d.ts +41 -0
  36. package/dist/lib/codebase-scanner.js +360 -0
  37. package/dist/lib/config.d.ts +16 -0
  38. package/dist/lib/config.js +70 -0
  39. package/dist/lib/embedded-adapter.d.ts +45 -0
  40. package/dist/lib/embedded-adapter.js +202 -0
  41. package/dist/lib/http-adapter.d.ts +41 -0
  42. package/dist/lib/http-adapter.js +169 -0
  43. package/dist/lib/oxigraph.d.ts +62 -0
  44. package/dist/lib/oxigraph.js +323 -0
  45. package/dist/lib/reasoner.d.ts +19 -0
  46. package/dist/lib/reasoner.js +310 -0
  47. package/dist/lib/shacl.d.ts +22 -0
  48. package/dist/lib/shacl.js +105 -0
  49. package/dist/lib/sparql-utils.d.ts +28 -0
  50. package/dist/lib/sparql-utils.js +217 -0
  51. package/dist/lib/store-adapter.d.ts +50 -0
  52. package/dist/lib/store-adapter.js +1 -0
  53. package/dist/lib/store-factory.d.ts +9 -0
  54. package/dist/lib/store-factory.js +71 -0
  55. package/dist/lib/validator.d.ts +10 -0
  56. package/dist/lib/validator.js +40 -0
  57. package/dist/mcp/server.d.ts +3 -0
  58. package/dist/mcp/server.js +1020 -0
  59. package/dist/templates/claude-md-context.d.ts +4 -0
  60. package/dist/templates/claude-md-context.js +104 -0
  61. package/dist/templates/otx-ontology.d.ts +2 -0
  62. package/dist/templates/otx-ontology.js +31 -0
  63. package/dist/templates/session-start-hook.d.ts +1 -0
  64. package/dist/templates/session-start-hook.js +94 -0
  65. package/dist/templates/slash-commands.d.ts +5 -0
  66. package/dist/templates/slash-commands.js +108 -0
  67. package/package.json +58 -0
@@ -0,0 +1,369 @@
1
+ import pc from 'picocolors';
2
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { loadConfig, saveConfig } from '../lib/config.js';
5
+ import { createReadyAdapter } from '../lib/store-factory.js';
6
+ import { OTX_BOOTSTRAP_TURTLE } from '../templates/otx-ontology.js';
7
+ import { generateContextSection, updateClaudeMd } from '../templates/claude-md-context.js';
8
+ import { generateHookScript } from '../templates/session-start-hook.js';
9
+ import { generateSlashCommands } from '../templates/slash-commands.js';
10
+ export function registerContext(program) {
11
+ const context = program
12
+ .command('context')
13
+ .description('Manage project context graph for Claude Code integration');
14
+ // --- context init ---
15
+ context
16
+ .command('init')
17
+ .description('Initialize project context graph and Claude Code hook')
18
+ .option('--force', 'Regenerate hook script and CLAUDE.md even if they exist')
19
+ .action(async (opts) => {
20
+ let config;
21
+ try {
22
+ config = loadConfig();
23
+ }
24
+ catch {
25
+ console.error(pc.red('Error: No .opentology.json found. Run `opentology init` first.'));
26
+ process.exit(1);
27
+ }
28
+ const graphs = config.graphs ?? {};
29
+ const contextUri = `${config.graphUri}/context`;
30
+ const sessionsUri = `${config.graphUri}/sessions`;
31
+ let createdHook = false;
32
+ let createdClaudeMd = false;
33
+ try {
34
+ // Step 1: Create graphs in config
35
+ let graphsChanged = false;
36
+ if (!graphs['context']) {
37
+ graphs['context'] = contextUri;
38
+ graphsChanged = true;
39
+ console.log(pc.green(` Created graph 'context' -> ${contextUri}`));
40
+ }
41
+ else {
42
+ console.log(pc.dim(` Graph 'context' already exists — skipped`));
43
+ }
44
+ if (!graphs['sessions']) {
45
+ graphs['sessions'] = sessionsUri;
46
+ graphsChanged = true;
47
+ console.log(pc.green(` Created graph 'sessions' -> ${sessionsUri}`));
48
+ }
49
+ else {
50
+ console.log(pc.dim(` Graph 'sessions' already exists — skipped`));
51
+ }
52
+ if (graphsChanged) {
53
+ config.graphs = graphs;
54
+ }
55
+ // Step 2: Bootstrap ontology — persist as tracked file for embedded mode
56
+ const ontologyDir = join(process.cwd(), '.opentology');
57
+ const ontologyPath = join(ontologyDir, 'ontology.ttl');
58
+ if (!existsSync(ontologyPath) || opts.force) {
59
+ mkdirSync(ontologyDir, { recursive: true });
60
+ writeFileSync(ontologyPath, OTX_BOOTSTRAP_TURTLE, 'utf-8');
61
+ // Track ontology file so embedded adapter loads it automatically
62
+ if (!config.files)
63
+ config.files = {};
64
+ if (!config.files[contextUri])
65
+ config.files[contextUri] = [];
66
+ const relPath = '.opentology/ontology.ttl';
67
+ if (!config.files[contextUri].includes(relPath)) {
68
+ config.files[contextUri].push(relPath);
69
+ }
70
+ console.log(pc.green(' Bootstrapped otx ontology vocabulary (6 classes, 12 properties)'));
71
+ }
72
+ else {
73
+ console.log(pc.dim(' Ontology already bootstrapped — skipped'));
74
+ }
75
+ // Create adapter after config is updated with tracked files
76
+ const adapter = await createReadyAdapter(config);
77
+ // Step 3: Write hook script
78
+ const hookDir = join(process.cwd(), '.opentology', 'hooks');
79
+ const hookPath = join(hookDir, 'session-start.mjs');
80
+ if (!existsSync(hookPath) || opts.force) {
81
+ mkdirSync(hookDir, { recursive: true });
82
+ writeFileSync(hookPath, generateHookScript(), 'utf-8');
83
+ createdHook = true;
84
+ console.log(pc.green(` Generated hook script: .opentology/hooks/session-start.mjs`));
85
+ }
86
+ else {
87
+ console.log(pc.dim(' Hook script already exists — skipped (use --force to regenerate)'));
88
+ }
89
+ // Step 4: Update CLAUDE.md
90
+ const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
91
+ const section = generateContextSection(config.projectId, config.graphUri);
92
+ if (!existsSync(claudeMdPath) || opts.force) {
93
+ updateClaudeMd(claudeMdPath, section);
94
+ createdClaudeMd = true;
95
+ console.log(pc.green(' Updated CLAUDE.md with context management instructions'));
96
+ }
97
+ else {
98
+ // Check for markers — update if present, append if not
99
+ const { readFileSync } = await import('node:fs');
100
+ const content = readFileSync(claudeMdPath, 'utf-8');
101
+ if (content.includes('OPENTOLOGY:CONTEXT:BEGIN')) {
102
+ updateClaudeMd(claudeMdPath, section);
103
+ createdClaudeMd = true;
104
+ console.log(pc.green(' Updated CLAUDE.md context section'));
105
+ }
106
+ else {
107
+ updateClaudeMd(claudeMdPath, section);
108
+ createdClaudeMd = true;
109
+ console.log(pc.green(' Appended context section to CLAUDE.md'));
110
+ }
111
+ }
112
+ // Step 5: Generate slash commands
113
+ const commandsDir = join(process.cwd(), '.claude', 'commands');
114
+ const slashCommands = generateSlashCommands();
115
+ const expectedFilenames = new Set(slashCommands.map((c) => c.filename));
116
+ let slashCreated = 0;
117
+ mkdirSync(commandsDir, { recursive: true });
118
+ // Clean up stale slash command files from previous naming conventions
119
+ if (existsSync(commandsDir)) {
120
+ for (const file of readdirSync(commandsDir)) {
121
+ if (file.endsWith('.md') && !expectedFilenames.has(file) && file.includes('context-')) {
122
+ unlinkSync(join(commandsDir, file));
123
+ }
124
+ }
125
+ }
126
+ for (const cmd of slashCommands) {
127
+ const cmdPath = join(commandsDir, cmd.filename);
128
+ if (!existsSync(cmdPath) || opts.force) {
129
+ writeFileSync(cmdPath, cmd.content, 'utf-8');
130
+ slashCreated++;
131
+ }
132
+ }
133
+ if (slashCreated > 0) {
134
+ console.log(pc.green(` Generated ${slashCreated} slash commands in .claude/commands/`));
135
+ }
136
+ else {
137
+ console.log(pc.dim(' Slash commands already exist — skipped'));
138
+ }
139
+ // Step 6: Save config LAST (atomic commit point)
140
+ saveConfig(config);
141
+ // Print hook registration instructions
142
+ console.log('');
143
+ console.log(pc.bold('Add this to your project .claude/settings.json:'));
144
+ 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));
153
+ console.log('');
154
+ console.log(pc.dim('Consider adding .opentology/hooks/ to version control so team members share the hook.'));
155
+ }
156
+ catch (err) {
157
+ // Rollback: clean up files created in this run
158
+ if (createdHook) {
159
+ const hookPath = join(process.cwd(), '.opentology', 'hooks', 'session-start.mjs');
160
+ try {
161
+ unlinkSync(hookPath);
162
+ }
163
+ catch { /* ignore */ }
164
+ }
165
+ if (createdClaudeMd) {
166
+ // Only delete if we created a new file (not appended to existing)
167
+ const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
168
+ // Don't delete existing CLAUDE.md — too risky. Just warn.
169
+ }
170
+ console.error(pc.red(`Error during context init: ${err.message}`));
171
+ console.error(pc.dim('Config was NOT modified. Fix the error and retry.'));
172
+ process.exit(1);
173
+ }
174
+ });
175
+ // --- context load ---
176
+ context
177
+ .command('load')
178
+ .description('Load project context (recent sessions, open issues, decisions)')
179
+ .option('--format <type>', 'Output format: table, json', 'table')
180
+ .action(async (opts) => {
181
+ let config;
182
+ try {
183
+ config = loadConfig();
184
+ }
185
+ catch {
186
+ console.error('Error: No .opentology.json found. Run `opentology init` first.');
187
+ process.exit(1);
188
+ }
189
+ const graphs = config.graphs ?? {};
190
+ if (!graphs['context'] || !graphs['sessions']) {
191
+ console.error('Error: Context not initialized. Run `opentology context init` first.');
192
+ process.exit(1);
193
+ }
194
+ const contextUri = graphs['context'];
195
+ const sessionsUri = graphs['sessions'];
196
+ const output = {
197
+ projectId: config.projectId,
198
+ graphUri: config.graphUri,
199
+ sessions: [],
200
+ openIssues: [],
201
+ recentDecisions: [],
202
+ meta: {
203
+ contextTripleCount: 0,
204
+ sessionsTripleCount: 0,
205
+ loadedAt: new Date().toISOString(),
206
+ },
207
+ warnings: [],
208
+ };
209
+ try {
210
+ const adapter = await createReadyAdapter(config);
211
+ // Query 1: Recent sessions
212
+ try {
213
+ const sessionsResult = await adapter.sparqlQuery(`
214
+ PREFIX otx: <https://opentology.dev/vocab#>
215
+ SELECT ?session ?title ?date ?nextTodo WHERE {
216
+ GRAPH <${sessionsUri}> {
217
+ ?session a otx:Session ;
218
+ otx:title ?title ;
219
+ otx:date ?date .
220
+ OPTIONAL { ?session otx:nextTodo ?nextTodo }
221
+ }
222
+ } ORDER BY DESC(?date) LIMIT 3
223
+ `);
224
+ output.sessions = sessionsResult.results.bindings.map((b) => ({
225
+ uri: b['session']?.value ?? '',
226
+ title: b['title']?.value ?? '',
227
+ date: b['date']?.value ?? '',
228
+ ...(b['nextTodo']?.value ? { nextTodo: b['nextTodo'].value } : {}),
229
+ }));
230
+ }
231
+ catch (err) {
232
+ output.warnings.push(`Failed to query sessions: ${err.message}`);
233
+ }
234
+ // Query 2: Open issues
235
+ try {
236
+ const issuesResult = await adapter.sparqlQuery(`
237
+ PREFIX otx: <https://opentology.dev/vocab#>
238
+ SELECT ?issue ?title ?date WHERE {
239
+ GRAPH <${contextUri}> {
240
+ ?issue a otx:Issue ;
241
+ otx:title ?title ;
242
+ otx:date ?date ;
243
+ otx:status "open" .
244
+ }
245
+ } ORDER BY DESC(?date) LIMIT 10
246
+ `);
247
+ output.openIssues = issuesResult.results.bindings.map((b) => ({
248
+ uri: b['issue']?.value ?? '',
249
+ title: b['title']?.value ?? '',
250
+ date: b['date']?.value ?? '',
251
+ }));
252
+ }
253
+ catch (err) {
254
+ output.warnings.push(`Failed to query issues: ${err.message}`);
255
+ }
256
+ // Query 3: Recent decisions
257
+ try {
258
+ const decisionsResult = await adapter.sparqlQuery(`
259
+ PREFIX otx: <https://opentology.dev/vocab#>
260
+ SELECT ?decision ?title ?date ?reason WHERE {
261
+ GRAPH <${contextUri}> {
262
+ ?decision a otx:Decision ;
263
+ otx:title ?title ;
264
+ otx:date ?date .
265
+ OPTIONAL { ?decision otx:reason ?reason }
266
+ }
267
+ } ORDER BY DESC(?date) LIMIT 3
268
+ `);
269
+ output.recentDecisions = decisionsResult.results.bindings.map((b) => ({
270
+ uri: b['decision']?.value ?? '',
271
+ title: b['title']?.value ?? '',
272
+ date: b['date']?.value ?? '',
273
+ ...(b['reason']?.value ? { reason: b['reason'].value } : {}),
274
+ }));
275
+ }
276
+ catch (err) {
277
+ output.warnings.push(`Failed to query decisions: ${err.message}`);
278
+ }
279
+ // Meta: triple counts
280
+ try {
281
+ output.meta.contextTripleCount = await adapter.getGraphTripleCount(contextUri);
282
+ }
283
+ catch { /* ignore */ }
284
+ try {
285
+ output.meta.sessionsTripleCount = await adapter.getGraphTripleCount(sessionsUri);
286
+ }
287
+ catch { /* ignore */ }
288
+ // Clean up empty warnings
289
+ if (output.warnings.length === 0)
290
+ delete output.warnings;
291
+ }
292
+ catch (err) {
293
+ console.error(`Error: ${err.message}`);
294
+ process.exit(1);
295
+ }
296
+ if (opts.format === 'json') {
297
+ console.log(JSON.stringify(output, null, 2));
298
+ }
299
+ else {
300
+ // Table format
301
+ console.log(pc.bold(`Project: ${output.projectId}`));
302
+ console.log('');
303
+ if (output.sessions.length > 0) {
304
+ console.log(pc.bold('Recent Sessions:'));
305
+ for (const s of output.sessions) {
306
+ console.log(` ${pc.dim(s.date)} ${s.title}`);
307
+ if (s.nextTodo)
308
+ console.log(` ${pc.dim('Next:')} ${s.nextTodo}`);
309
+ }
310
+ console.log('');
311
+ }
312
+ if (output.openIssues.length > 0) {
313
+ console.log(pc.bold(`Open Issues (${output.openIssues.length}):`));
314
+ for (const i of output.openIssues) {
315
+ console.log(` ${pc.dim(i.date)} ${i.title}`);
316
+ }
317
+ console.log('');
318
+ }
319
+ if (output.recentDecisions.length > 0) {
320
+ console.log(pc.bold('Recent Decisions:'));
321
+ for (const d of output.recentDecisions) {
322
+ console.log(` ${pc.dim(d.date)} ${d.title}`);
323
+ }
324
+ console.log('');
325
+ }
326
+ console.log(pc.dim(`Context: ${output.meta.contextTripleCount} triples | Sessions: ${output.meta.sessionsTripleCount} triples`));
327
+ }
328
+ });
329
+ // --- context status ---
330
+ context
331
+ .command('status')
332
+ .description('Show context initialization status')
333
+ .action(async () => {
334
+ let config;
335
+ try {
336
+ config = loadConfig();
337
+ }
338
+ catch {
339
+ console.log('Context: ' + pc.red('not initialized') + ' (no .opentology.json)');
340
+ return;
341
+ }
342
+ const graphs = config.graphs ?? {};
343
+ const hasContext = !!graphs['context'];
344
+ const hasSessions = !!graphs['sessions'];
345
+ const initialized = hasContext && hasSessions;
346
+ console.log('Context: ' + (initialized ? pc.green('initialized') : pc.red('not initialized')));
347
+ if (initialized) {
348
+ const adapter = await createReadyAdapter(config);
349
+ const contextCount = await adapter.getGraphTripleCount(graphs['context']).catch(() => 0);
350
+ const sessionsCount = await adapter.getGraphTripleCount(graphs['sessions']).catch(() => 0);
351
+ console.log('Graphs:');
352
+ console.log(` context ${graphs['context']} (${contextCount} triples)`);
353
+ console.log(` sessions ${graphs['sessions']} (${sessionsCount} triples)`);
354
+ }
355
+ const hookPath = join(process.cwd(), '.opentology', 'hooks', 'session-start.mjs');
356
+ const hookExists = existsSync(hookPath);
357
+ console.log('Hook: .opentology/hooks/session-start.mjs ' + (hookExists ? pc.green('(exists)') : pc.red('(missing)')));
358
+ const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
359
+ if (!existsSync(claudeMdPath)) {
360
+ console.log('CLAUDE.md: ' + pc.red('file missing'));
361
+ }
362
+ else {
363
+ const { readFileSync } = await import('node:fs');
364
+ const content = readFileSync(claudeMdPath, 'utf-8');
365
+ const hasMarkers = content.includes('OPENTOLOGY:CONTEXT:BEGIN');
366
+ console.log('CLAUDE.md: ' + (hasMarkers ? pc.green('markers present') : pc.yellow('markers missing')));
367
+ }
368
+ });
369
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerDelete(program: Command): void;
@@ -0,0 +1,46 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerDiff(program: Command): void;
@@ -0,0 +1,43 @@
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 registerDiff(program) {
6
+ program
7
+ .command('diff <file>')
8
+ .description('Show differences between a local Turtle file and the remote graph')
9
+ .option('--graph <name>', 'Target a specific named graph')
10
+ .action(async (file, opts) => {
11
+ let config;
12
+ try {
13
+ config = loadConfig();
14
+ }
15
+ catch (err) {
16
+ console.error(`Error: ${err.message}`);
17
+ process.exit(1);
18
+ }
19
+ const graphUri = opts.graph ? resolveGraphUri(config, opts.graph) : config.graphUri;
20
+ try {
21
+ const adapter = await createReadyAdapter(config);
22
+ const turtle = readFileSync(file, 'utf-8');
23
+ const result = await adapter.diffGraph(graphUri, turtle);
24
+ for (const triple of result.added) {
25
+ console.log(pc.green(`+ ${triple}`));
26
+ }
27
+ for (const triple of result.removed) {
28
+ console.log(pc.red(`- ${triple}`));
29
+ }
30
+ console.log(`\n${result.added.length} added, ${result.removed.length} removed, ${result.unchanged} unchanged`);
31
+ }
32
+ catch (err) {
33
+ const message = err.message;
34
+ if (message.includes('fetch failed') || message.includes('ECONNREFUSED')) {
35
+ console.error(`Cannot connect to Oxigraph at ${config.endpoint ?? 'unknown'}. Is it running? Start with: docker compose up -d`);
36
+ }
37
+ else {
38
+ console.error(`Error: ${message}`);
39
+ }
40
+ process.exit(1);
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerDrop(program: Command): void;
@@ -0,0 +1,41 @@
1
+ import pc from 'picocolors';
2
+ import { loadConfig, resolveGraphUri, saveConfig } from '../lib/config.js';
3
+ import { createReadyAdapter } from '../lib/store-factory.js';
4
+ export function registerDrop(program) {
5
+ program
6
+ .command('drop')
7
+ .description('Drop (delete) the entire project graph')
8
+ .option('--force', 'Skip confirmation and drop immediately')
9
+ .option('--graph <name>', 'Target a specific named graph')
10
+ .action(async (opts) => {
11
+ let config;
12
+ try {
13
+ config = loadConfig();
14
+ }
15
+ catch (err) {
16
+ console.error(`Error: ${err.message}`);
17
+ process.exit(1);
18
+ }
19
+ const graphUri = opts.graph ? resolveGraphUri(config, opts.graph) : config.graphUri;
20
+ if (!opts.force) {
21
+ console.error(pc.red(`This will delete all triples in ${graphUri}. Use --force to confirm.`));
22
+ process.exit(1);
23
+ }
24
+ try {
25
+ const adapter = await createReadyAdapter(config);
26
+ await adapter.dropGraph(graphUri);
27
+ // In embedded mode, clear tracked files for this graph
28
+ if (config.mode === 'embedded') {
29
+ if (!config.files)
30
+ config.files = {};
31
+ config.files[graphUri] = [];
32
+ saveConfig(config);
33
+ }
34
+ console.log(pc.green(`Dropped graph ${graphUri}`));
35
+ }
36
+ catch (err) {
37
+ console.error(`Error: ${err.message}`);
38
+ process.exit(1);
39
+ }
40
+ });
41
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerGraph(program: Command): void;
@@ -0,0 +1,130 @@
1
+ import pc from 'picocolors';
2
+ import { loadConfig, saveConfig, resolveGraphUri } from '../lib/config.js';
3
+ import { createReadyAdapter } from '../lib/store-factory.js';
4
+ export function registerGraph(program) {
5
+ const graph = program
6
+ .command('graph')
7
+ .description('Manage named graphs for the project');
8
+ graph
9
+ .command('list')
10
+ .description('List all named graphs for the project')
11
+ .action(async () => {
12
+ let config;
13
+ try {
14
+ config = loadConfig();
15
+ }
16
+ catch (err) {
17
+ console.error(`Error: ${err.message}`);
18
+ process.exit(1);
19
+ }
20
+ try {
21
+ const adapter = await createReadyAdapter(config);
22
+ // Query for all graphs that start with the project's base URI
23
+ const results = await adapter.sparqlQuery(`SELECT DISTINCT ?g (COUNT(*) AS ?count) WHERE { GRAPH ?g { ?s ?p ?o } } GROUP BY ?g`);
24
+ const baseUri = config.graphUri;
25
+ const remoteGraphs = new Map();
26
+ for (const binding of results.results.bindings) {
27
+ const g = binding['g']?.value;
28
+ const count = binding['count']?.value;
29
+ if (g && g.startsWith(baseUri)) {
30
+ remoteGraphs.set(g, count ? parseInt(count, 10) : 0);
31
+ }
32
+ }
33
+ // Merge with config.graphs
34
+ const configGraphs = config.graphs ?? {};
35
+ const allUris = new Set([
36
+ baseUri,
37
+ ...remoteGraphs.keys(),
38
+ ...Object.values(configGraphs),
39
+ ]);
40
+ // Build name lookup: URI -> logical name
41
+ const uriToName = new Map();
42
+ uriToName.set(baseUri, '(default)');
43
+ for (const [name, uri] of Object.entries(configGraphs)) {
44
+ uriToName.set(uri, name);
45
+ }
46
+ // Print table
47
+ console.log(`${'Name'.padEnd(20)} ${'URI'.padEnd(60)} ${'Triples'.padStart(8)}`);
48
+ console.log(`${'-'.repeat(20)} ${'-'.repeat(60)} ${'-'.repeat(8)}`);
49
+ for (const uri of allUris) {
50
+ const name = uriToName.get(uri) ?? '?';
51
+ const count = remoteGraphs.get(uri);
52
+ const tripleStr = count !== undefined ? String(count) : '?';
53
+ console.log(`${name.padEnd(20)} ${uri.padEnd(60)} ${tripleStr.padStart(8)}`);
54
+ }
55
+ }
56
+ catch (err) {
57
+ const message = err.message;
58
+ if (message.includes('fetch failed') || message.includes('ECONNREFUSED')) {
59
+ console.error(`Cannot connect to Oxigraph at ${config.endpoint ?? 'unknown'}. Is it running? Start with: docker compose up -d`);
60
+ }
61
+ else {
62
+ console.error(`Error: ${message}`);
63
+ }
64
+ process.exit(1);
65
+ }
66
+ });
67
+ graph
68
+ .command('create <name>')
69
+ .description('Create a new named graph')
70
+ .action(async (name) => {
71
+ let config;
72
+ try {
73
+ config = loadConfig();
74
+ }
75
+ catch (err) {
76
+ console.error(`Error: ${err.message}`);
77
+ process.exit(1);
78
+ }
79
+ const graphs = config.graphs ?? {};
80
+ if (graphs[name]) {
81
+ console.error(`Graph '${name}' already exists: ${graphs[name]}`);
82
+ process.exit(1);
83
+ }
84
+ const uri = `${config.graphUri}/${name}`;
85
+ graphs[name] = uri;
86
+ config.graphs = graphs;
87
+ saveConfig(config);
88
+ console.log(pc.green(`Created graph '${name}' -> ${uri}`));
89
+ });
90
+ graph
91
+ .command('drop <name>')
92
+ .description('Drop a named graph')
93
+ .option('--force', 'Skip confirmation and drop immediately')
94
+ .action(async (name, opts) => {
95
+ let config;
96
+ try {
97
+ config = loadConfig();
98
+ }
99
+ catch (err) {
100
+ console.error(`Error: ${err.message}`);
101
+ process.exit(1);
102
+ }
103
+ let graphUri;
104
+ try {
105
+ graphUri = resolveGraphUri(config, name);
106
+ }
107
+ catch (err) {
108
+ console.error(`Error: ${err.message}`);
109
+ process.exit(1);
110
+ return;
111
+ }
112
+ if (!opts.force) {
113
+ console.error(pc.red(`This will delete all triples in graph '${name}' (${graphUri}). Use --force to confirm.`));
114
+ process.exit(1);
115
+ }
116
+ try {
117
+ const adapter = await createReadyAdapter(config);
118
+ await adapter.dropGraph(graphUri);
119
+ const graphs = config.graphs ?? {};
120
+ delete graphs[name];
121
+ config.graphs = Object.keys(graphs).length > 0 ? graphs : undefined;
122
+ saveConfig(config);
123
+ console.log(pc.green(`Dropped graph '${name}' (${graphUri})`));
124
+ }
125
+ catch (err) {
126
+ console.error(`Error: ${err.message}`);
127
+ process.exit(1);
128
+ }
129
+ });
130
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerInfer(program: Command): void;