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.
- package/README.md +6 -0
- package/dist/index.js +0 -30
- package/dist/{commands → lib}/doctor.d.ts +1 -4
- package/dist/{commands → lib}/doctor.js +2 -35
- package/dist/lib/store-factory.js +0 -4
- package/dist/mcp/server.d.ts +27 -0
- package/dist/mcp/server.js +122 -1
- package/dist/templates/post-error-hook.d.ts +1 -0
- package/dist/templates/post-error-hook.js +146 -0
- package/dist/templates/user-prompt-hook.d.ts +1 -0
- package/dist/templates/user-prompt-hook.js +139 -0
- package/package.json +1 -1
- package/dist/commands/context.d.ts +0 -29
- package/dist/commands/context.js +0 -616
- package/dist/commands/delete.d.ts +0 -2
- package/dist/commands/delete.js +0 -46
- package/dist/commands/diff.d.ts +0 -2
- package/dist/commands/diff.js +0 -49
- package/dist/commands/drop.d.ts +0 -2
- package/dist/commands/drop.js +0 -43
- package/dist/commands/graph.d.ts +0 -2
- package/dist/commands/graph.js +0 -130
- package/dist/commands/infer.d.ts +0 -2
- package/dist/commands/infer.js +0 -47
- package/dist/commands/prefix.d.ts +0 -2
- package/dist/commands/prefix.js +0 -73
- package/dist/commands/pull.d.ts +0 -2
- package/dist/commands/pull.js +0 -43
- package/dist/commands/push.d.ts +0 -2
- package/dist/commands/push.js +0 -79
- package/dist/commands/rollback.d.ts +0 -2
- package/dist/commands/rollback.js +0 -75
- package/dist/commands/shapes.d.ts +0 -2
- package/dist/commands/shapes.js +0 -67
- package/dist/commands/status.d.ts +0 -2
- package/dist/commands/status.js +0 -47
- package/dist/commands/validate.d.ts +0 -2
- package/dist/commands/validate.js +0 -46
- package/dist/commands/viz.d.ts +0 -2
- package/dist/commands/viz.js +0 -53
- package/dist/lib/http-adapter.d.ts +0 -45
- package/dist/lib/http-adapter.js +0 -199
- package/dist/lib/oxigraph.d.ts +0 -62
- package/dist/lib/oxigraph.js +0 -323
package/dist/commands/context.js
DELETED
|
@@ -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
|
-
}
|
package/dist/commands/delete.js
DELETED
|
@@ -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
|
-
}
|
package/dist/commands/diff.d.ts
DELETED