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
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  > Ontology-powered project memory for AI coding assistants — your codebase as a knowledge graph
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/opentology)](https://www.npmjs.com/package/opentology) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Live Demo](https://img.shields.io/badge/Live_Demo-opentology.dev-6c63ff)](https://opentology.dev)
6
+
5
7
  [English](#english) | [한국어](#한국어)
6
8
 
7
9
  ---
@@ -14,6 +16,8 @@ When you connect OpenTology to Claude Code (or any MCP client), it doesn't just
14
16
 
15
17
  The result: an AI assistant that remembers across sessions, understands your codebase structure, and thinks before it acts.
16
18
 
19
+ **[See the live knowledge graph at opentology.dev](https://opentology.dev)** — OpenTology scanning its own codebase: 67 modules, 234 call relations, fully interactive.
20
+
17
21
  ### How It Works
18
22
 
19
23
  ```
@@ -294,6 +298,8 @@ OpenTology를 Claude Code(또는 MCP 호환 클라이언트)에 연결하면,
294
298
 
295
299
  결과: 세션을 넘어 기억하고, 코드베이스 구조를 이해하며, 행동 전에 생각하는 AI 어시스턴트.
296
300
 
301
+ **[opentology.dev에서 라이브 지식 그래프 확인](https://opentology.dev)** — OpenTology가 자기 자신의 코드베이스를 스캔한 결과: 67개 모듈, 234개 호출 관계, 인터랙티브 탐색 가능.
302
+
297
303
  ### 작동 방식
298
304
 
299
305
  ```
package/dist/index.js CHANGED
@@ -1,44 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { registerInit } from './commands/init.js';
4
- import { registerValidate } from './commands/validate.js';
5
- import { registerPush } from './commands/push.js';
6
4
  import { registerQuery } from './commands/query.js';
7
- import { registerStatus } from './commands/status.js';
8
- import { registerPull } from './commands/pull.js';
9
- import { registerDrop } from './commands/drop.js';
10
- import { registerDelete } from './commands/delete.js';
11
5
  import { registerMcp } from './commands/mcp.js';
12
- import { registerShapes } from './commands/shapes.js';
13
- import { registerDiff } from './commands/diff.js';
14
- import { registerGraph } from './commands/graph.js';
15
- import { registerInfer } from './commands/infer.js';
16
- import { registerPrefix } from './commands/prefix.js';
17
- import { registerContext } from './commands/context.js';
18
- import { registerViz } from './commands/viz.js';
19
- import { registerDoctor } from './commands/doctor.js';
20
- import { registerRollback } from './commands/rollback.js';
21
6
  const program = new Command();
22
7
  program
23
8
  .name('opentology')
24
9
  .version('0.2.4')
25
10
  .description('CLI-managed RDF/SPARQL infrastructure — Supabase for RDF');
26
11
  registerInit(program);
27
- registerValidate(program);
28
- registerPush(program);
29
12
  registerQuery(program);
30
- registerStatus(program);
31
- registerPull(program);
32
- registerDrop(program);
33
- registerDelete(program);
34
13
  registerMcp(program);
35
- registerShapes(program);
36
- registerDiff(program);
37
- registerGraph(program);
38
- registerInfer(program);
39
- registerPrefix(program);
40
- registerContext(program);
41
- registerViz(program);
42
- registerDoctor(program);
43
- registerRollback(program);
44
14
  program.parse(process.argv);
@@ -1,9 +1,6 @@
1
- import { Command } from 'commander';
2
- interface CheckResult {
1
+ export interface CheckResult {
3
2
  name: string;
4
3
  status: 'ok' | 'warn' | 'fail';
5
4
  message: string;
6
5
  }
7
6
  export declare function runDoctor(): Promise<CheckResult[]>;
8
- export declare function registerDoctor(program: Command): void;
9
- export {};
@@ -1,8 +1,7 @@
1
- import pc from 'picocolors';
2
1
  import { existsSync, readFileSync } from 'node:fs';
3
2
  import { join } from 'node:path';
4
- import { configExists, loadConfig } from '../lib/config.js';
5
- import { createReadyAdapter } from '../lib/store-factory.js';
3
+ import { configExists, loadConfig } from './config.js';
4
+ import { createReadyAdapter } from './store-factory.js';
6
5
  export async function runDoctor() {
7
6
  const results = [];
8
7
  // 1. Config check
@@ -98,35 +97,3 @@ export async function runDoctor() {
98
97
  }
99
98
  return results;
100
99
  }
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
- }
@@ -1,7 +1,6 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { getTrackedFiles } from './config.js';
4
- import { HttpAdapter } from './http-adapter.js';
5
4
  import { EmbeddedAdapter } from './embedded-adapter.js';
6
5
  import { materializeInferences } from './reasoner.js';
7
6
  // Singleton cache for embedded mode — keeps data alive across MCP tool calls.
@@ -16,9 +15,6 @@ export function resetAdapterCache() {
16
15
  loadedFileKeys = new Set();
17
16
  }
18
17
  export async function createReadyAdapter(config) {
19
- if (config.mode !== 'embedded') {
20
- return new HttpAdapter(config.endpoint ?? 'http://localhost:7878');
21
- }
22
18
  // Reuse cached adapter so data persists across MCP tool calls.
23
19
  if (!cachedAdapter) {
24
20
  cachedAdapter = new EmbeddedAdapter();
@@ -1,5 +1,32 @@
1
1
  import type { OpenTologyConfig } from '../lib/config.js';
2
2
  import type { StoreAdapter } from '../lib/store-adapter.js';
3
+ export interface ContextLoadOutput {
4
+ projectId: string;
5
+ graphUri: string;
6
+ sessions: Array<{
7
+ uri: string;
8
+ title: string;
9
+ date: string;
10
+ nextTodo?: string;
11
+ }>;
12
+ openIssues: Array<{
13
+ uri: string;
14
+ title: string;
15
+ date: string;
16
+ }>;
17
+ recentDecisions: Array<{
18
+ uri: string;
19
+ title: string;
20
+ date: string;
21
+ reason?: string;
22
+ }>;
23
+ meta: {
24
+ contextTripleCount: number;
25
+ sessionsTripleCount: number;
26
+ loadedAt: string;
27
+ };
28
+ warnings?: string[];
29
+ }
3
30
  export declare const MAX_TRIPLES_PER_PUSH = 100;
4
31
  export declare function assertTripleLimit(tripleCount: number): void;
5
32
  /**
@@ -18,8 +18,10 @@ import { OTX_BOOTSTRAP_TURTLE } from '../templates/otx-ontology.js';
18
18
  import { generateContextSection, updateClaudeMd } from '../templates/claude-md-context.js';
19
19
  import { generateHookScript } from '../templates/session-start-hook.js';
20
20
  import { generatePreEditHookScript } from '../templates/pre-edit-hook.js';
21
+ import { generateUserPromptHookScript } from '../templates/user-prompt-hook.js';
22
+ import { generatePostErrorHookScript } from '../templates/post-error-hook.js';
21
23
  import { generateSlashCommands } from '../templates/slash-commands.js';
22
- import { runDoctor } from '../commands/doctor.js';
24
+ import { runDoctor } from '../lib/doctor.js';
23
25
  import { syncContext } from '../lib/context-sync.js';
24
26
  import { scanCodebase } from '../lib/codebase-scanner.js';
25
27
  import { startGraphServer } from '../lib/graph-server.js';
@@ -482,6 +484,18 @@ async function handleContextInit(args) {
482
484
  writeFileSync(preEditHookPath, generatePreEditHookScript(), 'utf-8');
483
485
  actions.push('Generated hook: .opentology/hooks/pre-edit.mjs');
484
486
  }
487
+ const userPromptHookPath = join(hookDir, 'user-prompt.mjs');
488
+ if (!existsSync(userPromptHookPath) || force) {
489
+ mkdirSync(hookDir, { recursive: true });
490
+ writeFileSync(userPromptHookPath, generateUserPromptHookScript(), 'utf-8');
491
+ actions.push('Generated hook: .opentology/hooks/user-prompt.mjs');
492
+ }
493
+ const postErrorHookPath = join(hookDir, 'post-error.mjs');
494
+ if (!existsSync(postErrorHookPath) || force) {
495
+ mkdirSync(hookDir, { recursive: true });
496
+ writeFileSync(postErrorHookPath, generatePostErrorHookScript(), 'utf-8');
497
+ actions.push('Generated hook: .opentology/hooks/post-error.mjs');
498
+ }
485
499
  // Update CLAUDE.md
486
500
  const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
487
501
  const section = generateContextSection(config.projectId, config.graphUri);
@@ -552,6 +566,38 @@ async function handleContextInit(args) {
552
566
  });
553
567
  hooksChanged = true;
554
568
  }
569
+ // UserPromptSubmit: keyword-based context search
570
+ const userPromptCmd = 'node .opentology/hooks/user-prompt.mjs';
571
+ if (!hooks.UserPromptSubmit)
572
+ hooks.UserPromptSubmit = [];
573
+ const hasUserPromptHook = hooks.UserPromptSubmit.some((h) => {
574
+ const entry = h;
575
+ const entryHooks = entry.hooks;
576
+ return entryHooks?.some((hook) => hook.command === userPromptCmd);
577
+ });
578
+ if (!hasUserPromptHook) {
579
+ hooks.UserPromptSubmit.push({
580
+ matcher: '',
581
+ hooks: [{ type: 'command', command: userPromptCmd }],
582
+ });
583
+ hooksChanged = true;
584
+ }
585
+ // PostToolUse: error pattern matching on Bash
586
+ const postErrorCmd = 'node .opentology/hooks/post-error.mjs';
587
+ if (!hooks.PostToolUse)
588
+ hooks.PostToolUse = [];
589
+ const hasPostErrorHook = hooks.PostToolUse.some((h) => {
590
+ const entry = h;
591
+ const entryHooks = entry.hooks;
592
+ return entryHooks?.some((hook) => hook.command === postErrorCmd);
593
+ });
594
+ if (!hasPostErrorHook) {
595
+ hooks.PostToolUse.push({
596
+ matcher: 'Bash',
597
+ hooks: [{ type: 'command', command: postErrorCmd }],
598
+ });
599
+ hooksChanged = true;
600
+ }
555
601
  if (hooksChanged) {
556
602
  settings.hooks = hooks;
557
603
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
@@ -704,6 +750,54 @@ async function handleContextLoad() {
704
750
  delete output.warnings;
705
751
  return output;
706
752
  }
753
+ async function handleContextSearch(args) {
754
+ const keywords = args.keywords;
755
+ if (!keywords || keywords.length === 0)
756
+ throw new Error('keywords is required (array of strings)');
757
+ const types = args.types ?? ['Issue', 'Decision', 'Knowledge', 'Pattern'];
758
+ const limit = args.limit ?? 10;
759
+ const config = loadConfig();
760
+ const contextUri = `${config.graphUri}/context`;
761
+ const OTX = 'https://opentology.dev/vocab#';
762
+ const adapter = await createReadyAdapter(config);
763
+ const typeFilter = types.map(t => `<${OTX}${t}>`).join(', ');
764
+ const keywordFilters = keywords.map(kw => {
765
+ const escaped = kw.toLowerCase().replace(/"/g, '\\"');
766
+ return `CONTAINS(LCASE(?title), "${escaped}") || CONTAINS(LCASE(COALESCE(?body, "")), "${escaped}")`;
767
+ });
768
+ const filterClause = keywordFilters.join(' || ');
769
+ const sparql = `
770
+ PREFIX otx: <${OTX}>
771
+ SELECT ?type ?title ?body ?status ?date ?solution ?cause ?reason WHERE {
772
+ GRAPH <${contextUri}> {
773
+ ?s a ?type .
774
+ ?s otx:title ?title .
775
+ OPTIONAL { ?s otx:body ?body }
776
+ OPTIONAL { ?s otx:status ?status }
777
+ OPTIONAL { ?s otx:date ?date }
778
+ OPTIONAL { ?s otx:solution ?solution }
779
+ OPTIONAL { ?s otx:cause ?cause }
780
+ OPTIONAL { ?s otx:reason ?reason }
781
+ FILTER(
782
+ ?type IN (${typeFilter})
783
+ && (${filterClause})
784
+ )
785
+ }
786
+ } ORDER BY DESC(?date) LIMIT ${limit}
787
+ `;
788
+ const result = await adapter.sparqlQuery(sparql);
789
+ const results = (result.results?.bindings ?? []).map((b) => ({
790
+ type: b.type?.value ?? '',
791
+ title: b.title?.value ?? '',
792
+ body: b.body?.value,
793
+ status: b.status?.value,
794
+ date: b.date?.value,
795
+ solution: b.solution?.value,
796
+ cause: b.cause?.value,
797
+ reason: b.reason?.value,
798
+ }));
799
+ return { keywords, results };
800
+ }
707
801
  async function handleContextImpact(args) {
708
802
  const filePath = args.filePath;
709
803
  if (!filePath)
@@ -1196,6 +1290,30 @@ export async function startMcpServer() {
1196
1290
  properties: {},
1197
1291
  },
1198
1292
  },
1293
+ {
1294
+ name: 'context_search',
1295
+ description: 'Search the knowledge graph by keywords. Searches across Issues, Decisions, Knowledge, and Patterns by matching keywords against titles and bodies. Use this to find relevant context before investigating code.',
1296
+ inputSchema: {
1297
+ type: 'object',
1298
+ properties: {
1299
+ keywords: {
1300
+ type: 'array',
1301
+ items: { type: 'string' },
1302
+ description: 'Keywords to search for (matched with OR logic against title and body).',
1303
+ },
1304
+ types: {
1305
+ type: 'array',
1306
+ items: { type: 'string' },
1307
+ description: 'OTX types to search (default: Issue, Decision, Knowledge, Pattern). Use short names without prefix.',
1308
+ },
1309
+ limit: {
1310
+ type: 'number',
1311
+ description: 'Maximum results (default: 10).',
1312
+ },
1313
+ },
1314
+ required: ['keywords'],
1315
+ },
1316
+ },
1199
1317
  {
1200
1318
  name: 'context_status',
1201
1319
  description: 'Check whether project context is initialized. Shows graph triple counts, hook script presence, and CLAUDE.md marker status.',
@@ -1398,6 +1516,9 @@ export async function startMcpServer() {
1398
1516
  case 'context_load':
1399
1517
  result = await handleContextLoad();
1400
1518
  break;
1519
+ case 'context_search':
1520
+ result = await handleContextSearch(args);
1521
+ break;
1401
1522
  case 'context_status':
1402
1523
  result = await handleContextStatus();
1403
1524
  break;
@@ -0,0 +1 @@
1
+ export declare function generatePostErrorHookScript(): string;
@@ -0,0 +1,146 @@
1
+ export function generatePostErrorHookScript() {
2
+ return `#!/usr/bin/env node
3
+ // Generated by: opentology context init
4
+ // PostToolUse hook (Bash) — matches error output against known patterns
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { join, resolve, dirname } from 'node:path';
8
+
9
+ const TIMEOUT = parseInt(process.env.OPENTOLOGY_HOOK_TIMEOUT || '3000', 10);
10
+
11
+ // Error indicators that trigger pattern search
12
+ const ERROR_PATTERNS = [
13
+ /\\bError\\b/i,
14
+ /\\bERR!\\b/,
15
+ /\\bFailed\\b/i,
16
+ /\\bfatal\\b/i,
17
+ /\\bENOENT\\b/,
18
+ /\\bENOTDIR\\b/,
19
+ /\\bEACCES\\b/,
20
+ /\\bSyntaxError\\b/,
21
+ /\\bTypeError\\b/,
22
+ /\\bReferenceError\\b/,
23
+ /\\bModuleNotFoundError\\b/,
24
+ /\\bexited with code [1-9]/,
25
+ /\\bcommand not found\\b/,
26
+ /\\bCannot find module\\b/,
27
+ /\\bTS\\d{4}:/,
28
+ ];
29
+
30
+ function findProjectRoot(startDir) {
31
+ let dir = resolve(startDir);
32
+ while (dir !== dirname(dir)) {
33
+ if (existsSync(join(dir, '.opentology.json'))) return dir;
34
+ dir = dirname(dir);
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function findBin(projectRoot) {
40
+ const local = join(projectRoot, 'node_modules', '.bin', 'opentology');
41
+ if (existsSync(local)) return { bin: local, args: [] };
42
+ return { bin: 'npx', args: ['opentology'] };
43
+ }
44
+
45
+ function run(projectRoot, cmdArgs) {
46
+ const { bin, args } = findBin(projectRoot);
47
+ return execFileSync(bin, [...args, ...cmdArgs], {
48
+ cwd: projectRoot,
49
+ timeout: TIMEOUT,
50
+ encoding: 'utf-8',
51
+ stdio: ['pipe', 'pipe', 'pipe'],
52
+ });
53
+ }
54
+
55
+ function extractErrorKeywords(output) {
56
+ const keywords = new Set();
57
+
58
+ // Extract error class names (TypeError, SyntaxError, etc.)
59
+ const errorClasses = output.match(/\\b[A-Z][a-zA-Z]*Error\\b/g);
60
+ if (errorClasses) errorClasses.forEach(e => keywords.add(e));
61
+
62
+ // Extract TS error codes
63
+ const tsCodes = output.match(/TS\\d{4}/g);
64
+ if (tsCodes) tsCodes.forEach(c => keywords.add(c));
65
+
66
+ // Extract "Cannot find module 'xxx'"
67
+ const modules = output.match(/Cannot find module ['"]([^'"]+)['"]/g);
68
+ if (modules) modules.forEach(m => {
69
+ const name = m.match(/['"]([^'"]+)['"]/)?.[1];
70
+ if (name) keywords.add(name);
71
+ });
72
+
73
+ // Extract file paths from error output
74
+ const paths = output.match(/(?:src|lib)\\/[\\w/.-]+\\.\\w+/g);
75
+ if (paths) paths.slice(0, 3).forEach(p => keywords.add(p));
76
+
77
+ // Extract npm package names from errors
78
+ const npmPkgs = output.match(/(?:ERR!|error)\\s+([a-z@][a-z0-9@/.-]+)/gi);
79
+ if (npmPkgs) npmPkgs.slice(0, 2).forEach(p => {
80
+ const name = p.replace(/^(?:ERR!|error)\\s+/i, '');
81
+ if (name.length >= 3) keywords.add(name);
82
+ });
83
+
84
+ return [...keywords].slice(0, 5);
85
+ }
86
+
87
+ try {
88
+ const projectRoot = findProjectRoot(process.cwd());
89
+ if (!projectRoot) process.exit(0);
90
+
91
+ // Read tool output from stdin
92
+ let input = '';
93
+ try {
94
+ input = readFileSync('/dev/stdin', 'utf-8');
95
+ } catch {
96
+ process.exit(0);
97
+ }
98
+
99
+ let toolOutput = '';
100
+ let toolName = '';
101
+ try {
102
+ const parsed = JSON.parse(input);
103
+ toolName = parsed.tool_name || '';
104
+ toolOutput = parsed.tool_output || parsed.stdout || parsed.stderr || '';
105
+ } catch {
106
+ toolOutput = input;
107
+ }
108
+
109
+ // Only process Bash tool output
110
+ if (toolName && toolName !== 'Bash') process.exit(0);
111
+
112
+ // Check if output contains error indicators
113
+ if (!toolOutput || toolOutput.length < 10) process.exit(0);
114
+ const hasError = ERROR_PATTERNS.some(p => p.test(toolOutput));
115
+ if (!hasError) process.exit(0);
116
+
117
+ const keywords = extractErrorKeywords(toolOutput);
118
+ if (keywords.length === 0) process.exit(0);
119
+
120
+ const raw = run(projectRoot, [
121
+ 'context', 'search',
122
+ '--keywords', keywords.join(','),
123
+ '--types', 'Pattern,Issue,Knowledge',
124
+ '--format', 'json',
125
+ ]);
126
+ const data = JSON.parse(raw);
127
+
128
+ if (!data.results || data.results.length === 0) process.exit(0);
129
+
130
+ const lines = [];
131
+ lines.push('[Known Pattern Match] Similar errors found in project history:');
132
+ for (const r of data.results.slice(0, 3)) {
133
+ const tag = r.type.replace('https://opentology.dev/vocab#', '');
134
+ lines.push(\`- [\${tag}] \${r.title}\${r.status ? ' (' + r.status + ')' : ''}\`);
135
+ if (r.solution) lines.push(\` Solution: \${r.solution}\`);
136
+ if (r.cause) lines.push(\` Cause: \${r.cause}\`);
137
+ if (r.body && r.body.length <= 150) lines.push(\` \${r.body}\`);
138
+ }
139
+
140
+ console.log(lines.join('\\n'));
141
+ } catch {
142
+ // Silent exit — do not block command execution
143
+ process.exit(0);
144
+ }
145
+ `;
146
+ }
@@ -0,0 +1 @@
1
+ export declare function generateUserPromptHookScript(): string;
@@ -0,0 +1,139 @@
1
+ export function generateUserPromptHookScript() {
2
+ return `#!/usr/bin/env node
3
+ // Generated by: opentology context init
4
+ // UserPromptSubmit hook — extracts keywords from user message and queries related context
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { join, resolve, dirname } from 'node:path';
8
+
9
+ const TIMEOUT = parseInt(process.env.OPENTOLOGY_HOOK_TIMEOUT || '3000', 10);
10
+
11
+ // Stop words (EN + KO common particles/connectors)
12
+ const STOP_WORDS = new Set([
13
+ // English
14
+ 'the','a','an','is','are','was','were','be','been','being','have','has','had',
15
+ 'do','does','did','will','would','shall','should','may','might','must','can','could',
16
+ 'this','that','these','those','it','its','i','you','he','she','we','they','me','him',
17
+ 'her','us','them','my','your','his','our','their','what','which','who','whom','when',
18
+ 'where','why','how','not','no','nor','but','and','or','if','then','else','for','from',
19
+ 'with','without','about','above','after','before','between','into','through','during',
20
+ 'to','of','in','on','at','by','as','so','than','too','very','just','also','now','here',
21
+ 'there','all','each','every','both','few','more','most','other','some','such','only',
22
+ 'same','let','please','want','need','like','make','get','use','try','look','see','know',
23
+ 'think','help','show','tell','give','take','come','go','run','file','code','check',
24
+ // Korean particles/connectors
25
+ '은','는','이','가','을','를','에','에서','으로','로','와','과','의','도','만',
26
+ '부터','까지','에게','한테','보다','라고','이라고','하고','해서','해줘','해주세요',
27
+ '좀','그','저','것','거','수','때','중','후','전','안','못','잘','더','다',
28
+ '뭐','어떻게','왜','어디','언제','누가','뭘','걸','건','데','게','줘',
29
+ ]);
30
+
31
+ function findProjectRoot(startDir) {
32
+ let dir = resolve(startDir);
33
+ while (dir !== dirname(dir)) {
34
+ if (existsSync(join(dir, '.opentology.json'))) return dir;
35
+ dir = dirname(dir);
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function findBin(projectRoot) {
41
+ const local = join(projectRoot, 'node_modules', '.bin', 'opentology');
42
+ if (existsSync(local)) return { bin: local, args: [] };
43
+ return { bin: 'npx', args: ['opentology'] };
44
+ }
45
+
46
+ function run(projectRoot, cmdArgs) {
47
+ const { bin, args } = findBin(projectRoot);
48
+ return execFileSync(bin, [...args, ...cmdArgs], {
49
+ cwd: projectRoot,
50
+ timeout: TIMEOUT,
51
+ encoding: 'utf-8',
52
+ stdio: ['pipe', 'pipe', 'pipe'],
53
+ });
54
+ }
55
+
56
+ function extractKeywords(text) {
57
+ const keywords = [];
58
+
59
+ // Step 1: Split mixed Korean+English tokens (e.g. "persistGraph에서" → "persistGraph", "에서")
60
+ const raw = text.replace(/[\\n\\r]/g, ' ');
61
+ const mixedSplit = raw.replace(/([a-zA-Z0-9_./]+)([\\uAC00-\\uD7AF\\u3130-\\u318F]+)/g, '$1 $2')
62
+ .replace(/([\\uAC00-\\uD7AF\\u3130-\\u318F]+)([a-zA-Z0-9_./]+)/g, '$1 $2');
63
+
64
+ const tokens = mixedSplit
65
+ .split(/[\\s,.:;!?()\\[\\]{}'"]+/)
66
+ .filter(Boolean);
67
+
68
+ for (const token of tokens) {
69
+ const lower = token.toLowerCase();
70
+ if (STOP_WORDS.has(lower)) continue;
71
+ // English identifiers (camelCase, snake_case, PascalCase) — 4+ chars
72
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(token) && token.length >= 4) {
73
+ keywords.push(lower);
74
+ }
75
+ // Korean content words (3+ chars to skip particles like 에러가→에러)
76
+ if (/[\\uAC00-\\uD7AF]/.test(token) && token.length >= 3 && !STOP_WORDS.has(token)) {
77
+ keywords.push(token);
78
+ }
79
+ // File paths, module references, Error class names
80
+ if (token.includes('/') || token.includes('.') || /Error$/.test(token)) {
81
+ keywords.push(token);
82
+ }
83
+ }
84
+
85
+ // Deduplicate and limit
86
+ return [...new Set(keywords)].slice(0, 5);
87
+ }
88
+
89
+ try {
90
+ const projectRoot = findProjectRoot(process.cwd());
91
+ if (!projectRoot) process.exit(0);
92
+
93
+ // Read user prompt from stdin
94
+ let input = '';
95
+ try {
96
+ input = readFileSync('/dev/stdin', 'utf-8');
97
+ } catch {
98
+ process.exit(0);
99
+ }
100
+
101
+ let userMessage = '';
102
+ try {
103
+ const parsed = JSON.parse(input);
104
+ userMessage = parsed.user_prompt || parsed.message || '';
105
+ } catch {
106
+ // Plain text input
107
+ userMessage = input;
108
+ }
109
+
110
+ if (!userMessage || userMessage.length < 5) process.exit(0);
111
+
112
+ const keywords = extractKeywords(userMessage);
113
+ if (keywords.length === 0) process.exit(0);
114
+
115
+ const raw = run(projectRoot, ['context', 'search', '--keywords', keywords.join(','), '--format', 'json']);
116
+ const data = JSON.parse(raw);
117
+
118
+ if (!data.results || data.results.length === 0) process.exit(0);
119
+
120
+ const lines = [];
121
+ lines.push('[Graph Context] Related to your query:');
122
+ for (const r of data.results.slice(0, 5)) {
123
+ const tag = r.type.replace('https://opentology.dev/vocab#', '');
124
+ let line = \`- [\${tag}] \${r.title}\`;
125
+ if (r.status) line += \` (\${r.status})\`;
126
+ if (r.date) line += \` [\${r.date}]\`;
127
+ lines.push(line);
128
+ if (r.solution) lines.push(\` Solution: \${r.solution}\`);
129
+ if (r.reason) lines.push(\` Reason: \${r.reason}\`);
130
+ if (r.body && r.body.length <= 200) lines.push(\` \${r.body}\`);
131
+ }
132
+
133
+ console.log(lines.join('\\n'));
134
+ } catch {
135
+ // Silent exit — do not block prompt submission
136
+ process.exit(0);
137
+ }
138
+ `;
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opentology",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Ontology-powered project memory for AI coding assistants — your codebase as a knowledge graph",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,29 +0,0 @@
1
- import { Command } from 'commander';
2
- export interface ContextLoadOutput {
3
- projectId: string;
4
- graphUri: string;
5
- sessions: Array<{
6
- uri: string;
7
- title: string;
8
- date: string;
9
- nextTodo?: string;
10
- }>;
11
- openIssues: Array<{
12
- uri: string;
13
- title: string;
14
- date: string;
15
- }>;
16
- recentDecisions: Array<{
17
- uri: string;
18
- title: string;
19
- date: string;
20
- reason?: string;
21
- }>;
22
- meta: {
23
- contextTripleCount: number;
24
- sessionsTripleCount: number;
25
- loadedAt: string;
26
- };
27
- warnings?: string[];
28
- }
29
- export declare function registerContext(program: Command): void;