bluera-knowledge 0.11.20 → 0.12.1

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 (43) hide show
  1. package/.claude/council-cache/1a43ed5977b8f29afc79a9bf5c4082ee5ad8338c42ab991a4241a48f80c1e46d.json +7 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +64 -5
  5. package/commands/crawl.md +7 -7
  6. package/commands/search.md +9 -2
  7. package/commands/skill-activation.md +130 -0
  8. package/dist/{chunk-MQGRQ2EG.js → chunk-C4SYGLAI.js} +27 -7
  9. package/dist/chunk-C4SYGLAI.js.map +1 -0
  10. package/dist/{chunk-ZSKQIMD7.js → chunk-CC6EGZ4D.js} +48 -8
  11. package/dist/chunk-CC6EGZ4D.js.map +1 -0
  12. package/dist/{chunk-Q2ZGPJ66.js → chunk-QCSFBMYW.js} +2 -2
  13. package/dist/index.js +64 -12
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp/server.js +2 -2
  16. package/dist/workers/background-worker-cli.js +2 -2
  17. package/hooks/hooks.json +28 -1
  18. package/hooks/pretooluse-bk-reminder.py +97 -0
  19. package/hooks/skill-activation.py +190 -0
  20. package/hooks/skill-rules.json +122 -0
  21. package/package.json +1 -1
  22. package/src/analysis/code-graph.test.ts +30 -0
  23. package/src/analysis/code-graph.ts +10 -2
  24. package/src/cli/commands/store.test.ts +78 -0
  25. package/src/cli/commands/store.ts +19 -0
  26. package/src/cli/commands/sync.test.ts +1 -1
  27. package/src/cli/commands/sync.ts +50 -1
  28. package/src/mcp/commands/sync.commands.test.ts +94 -6
  29. package/src/mcp/commands/sync.commands.ts +36 -6
  30. package/src/mcp/handlers/search.handler.ts +3 -1
  31. package/src/mcp/handlers/store.handler.test.ts +3 -0
  32. package/src/mcp/handlers/store.handler.ts +5 -2
  33. package/src/mcp/schemas/index.test.ts +36 -0
  34. package/src/mcp/schemas/index.ts +6 -0
  35. package/src/mcp/server.ts +11 -0
  36. package/src/services/code-graph.service.ts +11 -1
  37. package/src/services/job.service.test.ts +23 -0
  38. package/src/services/job.service.ts +10 -6
  39. package/src/services/watch.service.test.ts +14 -11
  40. package/vitest.config.ts +1 -1
  41. package/dist/chunk-MQGRQ2EG.js.map +0 -1
  42. package/dist/chunk-ZSKQIMD7.js.map +0 -1
  43. /package/dist/{chunk-Q2ZGPJ66.js.map → chunk-QCSFBMYW.js.map} +0 -0
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMCPServer,
3
3
  runMCPServer
4
- } from "../chunk-ZSKQIMD7.js";
5
- import "../chunk-MQGRQ2EG.js";
4
+ } from "../chunk-CC6EGZ4D.js";
5
+ import "../chunk-C4SYGLAI.js";
6
6
  import "../chunk-HRQD3MPH.js";
7
7
  export {
8
8
  createMCPServer,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  IntelligentCrawler
4
- } from "../chunk-Q2ZGPJ66.js";
4
+ } from "../chunk-QCSFBMYW.js";
5
5
  import {
6
6
  JobService,
7
7
  createDocumentId,
@@ -9,7 +9,7 @@ import {
9
9
  createServices,
10
10
  createStoreId,
11
11
  shutdownLogger
12
- } from "../chunk-MQGRQ2EG.js";
12
+ } from "../chunk-C4SYGLAI.js";
13
13
  import "../chunk-HRQD3MPH.js";
14
14
 
15
15
  // src/workers/background-worker.ts
package/hooks/hooks.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
- "description": "bluera-knowledge plugin hooks - automatic dependency checking and job status monitoring",
2
+ "description": "bluera-knowledge plugin hooks - dependency checking, job monitoring, and BK suggestions",
3
3
  "hooks": {
4
+ "PreToolUse": [
5
+ {
6
+ "matcher": "Grep",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse-bk-reminder.py",
11
+ "timeout": 3
12
+ }
13
+ ]
14
+ },
15
+ {
16
+ "matcher": "Read",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/pretooluse-bk-reminder.py",
21
+ "timeout": 3
22
+ }
23
+ ]
24
+ }
25
+ ],
4
26
  "SessionStart": [
5
27
  {
6
28
  "hooks": [
@@ -19,6 +41,11 @@
19
41
  "type": "command",
20
42
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/job-status-hook.sh",
21
43
  "timeout": 5
44
+ },
45
+ {
46
+ "type": "command",
47
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/skill-activation.py",
48
+ "timeout": 5
22
49
  }
23
50
  ]
24
51
  }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PreToolUse hook for bluera-knowledge plugin.
4
+ Fires when Claude is about to Grep/Read in dependency directories,
5
+ reminding to consider using BK instead.
6
+ """
7
+
8
+ import json
9
+ import re
10
+ import sys
11
+ from typing import Any
12
+
13
+ # Patterns indicating library/dependency code
14
+ LIBRARY_PATH_PATTERNS = [
15
+ r"node_modules/",
16
+ r"vendor/",
17
+ r"site-packages/",
18
+ r"\.venv/",
19
+ r"venv/",
20
+ r"bower_components/",
21
+ r"packages/.*/node_modules/",
22
+ r"\.npm/",
23
+ r"\.cargo/registry/",
24
+ r"go/pkg/mod/",
25
+ ]
26
+
27
+ # Compile patterns for efficiency
28
+ LIBRARY_PATTERNS_RE = re.compile("|".join(LIBRARY_PATH_PATTERNS), re.IGNORECASE)
29
+
30
+
31
+ def check_grep_tool(tool_input: dict[str, Any]) -> str | None:
32
+ """Check if Grep is targeting library code."""
33
+ path = tool_input.get("path", "")
34
+ pattern = tool_input.get("pattern", "")
35
+
36
+ # Check if searching in library directories
37
+ if path and LIBRARY_PATTERNS_RE.search(path):
38
+ return f"grep in `{path}`"
39
+
40
+ # Check if pattern suggests library-specific search
41
+ # (searching for common library internals)
42
+ return None
43
+
44
+
45
+ def check_read_tool(tool_input: dict[str, Any]) -> str | None:
46
+ """Check if Read is targeting library code."""
47
+ file_path = tool_input.get("file_path", "")
48
+
49
+ if file_path and LIBRARY_PATTERNS_RE.search(file_path):
50
+ return f"read `{file_path}`"
51
+
52
+ return None
53
+
54
+
55
+ def main() -> int:
56
+ try:
57
+ stdin_data = sys.stdin.read()
58
+ if not stdin_data.strip():
59
+ return 0
60
+ hook_input = json.loads(stdin_data)
61
+ except json.JSONDecodeError:
62
+ return 0
63
+
64
+ tool_name = hook_input.get("tool_name", "")
65
+ tool_input = hook_input.get("tool_input", {})
66
+
67
+ trigger_reason = None
68
+
69
+ if tool_name == "Grep":
70
+ trigger_reason = check_grep_tool(tool_input)
71
+ elif tool_name == "Read":
72
+ trigger_reason = check_read_tool(tool_input)
73
+
74
+ if not trigger_reason:
75
+ return 0
76
+
77
+ # Output reminder
78
+ reminder = f"""<system-reminder>
79
+ BLUERA-KNOWLEDGE SUGGESTION
80
+
81
+ You're about to {trigger_reason} which appears to be dependency/library code.
82
+
83
+ Consider querying Bluera Knowledge instead:
84
+ - Use MCP tool `search` with a relevant query
85
+ - Or invoke `/bluera-knowledge:search <query>`
86
+
87
+ BK provides indexed, searchable access to library sources - faster and more context-efficient than grepping through node_modules.
88
+
89
+ If you don't have this library indexed, continue with your current approach.
90
+ </system-reminder>"""
91
+
92
+ print(reminder)
93
+ return 0
94
+
95
+
96
+ if __name__ == "__main__":
97
+ raise SystemExit(main())
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Skill activation hook for bluera-knowledge plugin.
4
+ Matches user prompts against skill rules and injects activation reminders.
5
+
6
+ Runs on UserPromptSubmit to detect users who would benefit from learning
7
+ about BK skills, while excluding users who already know BK terminology.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ CONFIG_DIR = Path.home() / ".local" / "share" / "bluera-knowledge"
18
+ CONFIG_FILE = CONFIG_DIR / "skill-activation.json"
19
+ DEFAULT_CONFIG: dict[str, Any] = {
20
+ "enabled": True,
21
+ "threshold": 1,
22
+ "skills": {
23
+ "knowledge-search": True,
24
+ "when-to-query": True,
25
+ "search-optimization": True,
26
+ "advanced-workflows": True,
27
+ "store-lifecycle": True,
28
+ },
29
+ }
30
+
31
+
32
+ def load_config() -> dict[str, Any]:
33
+ """Load skill activation configuration."""
34
+ if not CONFIG_FILE.exists():
35
+ return DEFAULT_CONFIG.copy()
36
+ try:
37
+ with open(CONFIG_FILE, encoding="utf-8") as f:
38
+ return json.load(f)
39
+ except (json.JSONDecodeError, IOError):
40
+ return DEFAULT_CONFIG.copy()
41
+
42
+
43
+ def load_rules(plugin_root: Path) -> dict[str, Any]:
44
+ """Load skill rules from plugin hooks directory."""
45
+ rules_path = plugin_root / "hooks" / "skill-rules.json"
46
+ if not rules_path.exists():
47
+ return {"skills": [], "threshold": 1, "globalExclusions": []}
48
+ with open(rules_path, encoding="utf-8") as f:
49
+ return json.load(f)
50
+
51
+
52
+ def matches_condition(prompt: str, condition: dict[str, Any]) -> bool:
53
+ """Check if prompt matches a single condition (keyword or regex)."""
54
+ prompt_lower = prompt.lower()
55
+ if "keyword" in condition:
56
+ return condition["keyword"].lower() in prompt_lower
57
+ if "regex" in condition:
58
+ return bool(re.search(condition["regex"], prompt, flags=re.IGNORECASE))
59
+ return False
60
+
61
+
62
+ def check_exclusions(
63
+ prompt: str, exclusions: list[dict[str, Any]]
64
+ ) -> bool:
65
+ """Check if any exclusion pattern matches. Returns True if excluded."""
66
+ for exc in exclusions:
67
+ if matches_condition(prompt, exc):
68
+ return True
69
+ return False
70
+
71
+
72
+ def score_skill(
73
+ prompt: str, skill: dict[str, Any]
74
+ ) -> tuple[int, list[str]]:
75
+ """Score a skill against the user prompt. Returns (score, reasons)."""
76
+ reasons: list[str] = []
77
+ score = 0
78
+
79
+ # Check skill-specific exclusions first
80
+ if check_exclusions(prompt, skill.get("exclusions", [])):
81
+ return 0, []
82
+
83
+ for trigger in skill.get("triggers", []):
84
+ if matches_condition(prompt, trigger):
85
+ weight = trigger.get("weight", 1)
86
+ score += weight
87
+ if "keyword" in trigger:
88
+ reasons.append(f'keyword "{trigger["keyword"]}"')
89
+ elif "regex" in trigger:
90
+ reasons.append(f'pattern match')
91
+
92
+ return score, reasons
93
+
94
+
95
+ def generate_reminder(
96
+ matches: list[tuple[str, int, list[str], str]]
97
+ ) -> str:
98
+ """Generate the system-reminder for matched skills."""
99
+ lines = [
100
+ "<system-reminder>",
101
+ "BLUERA-KNOWLEDGE SKILL ACTIVATION",
102
+ "",
103
+ "The user's prompt suggests they may benefit from these skills.",
104
+ "For EACH skill below, decide YES/NO:",
105
+ "- YES: Invoke via Skill tool: Skill(skill='bluera-knowledge:<skill-name>')",
106
+ "- NO: Skip (user doesn't need this guidance)",
107
+ "",
108
+ "Candidate skills (ranked by relevance):",
109
+ ]
110
+
111
+ for name, score, reasons, description in matches:
112
+ lines.append(f"")
113
+ lines.append(f" [{name}] (score={score})")
114
+ lines.append(f" Purpose: {description}")
115
+ lines.append(f" Matched: {', '.join(reasons[:3])}")
116
+
117
+ lines.append("")
118
+ lines.append("Evaluate quickly, then answer the user's question.")
119
+ lines.append("</system-reminder>")
120
+
121
+ return "\n".join(lines)
122
+
123
+
124
+ def main() -> int:
125
+ # Load configuration
126
+ config = load_config()
127
+
128
+ # Check if skill activation is enabled
129
+ if not config.get("enabled", True):
130
+ return 0
131
+
132
+ # Get plugin root from environment
133
+ plugin_root_env = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
134
+ if not plugin_root_env:
135
+ return 0
136
+ plugin_root = Path(plugin_root_env)
137
+
138
+ # Read hook input from stdin
139
+ try:
140
+ stdin_data = sys.stdin.read()
141
+ if not stdin_data.strip():
142
+ return 0
143
+ hook_input = json.loads(stdin_data)
144
+ except json.JSONDecodeError:
145
+ return 0
146
+
147
+ prompt = hook_input.get("prompt", "")
148
+ if not prompt.strip():
149
+ return 0
150
+
151
+ # Load rules
152
+ rules = load_rules(plugin_root)
153
+
154
+ # Check global exclusions first
155
+ if check_exclusions(prompt, rules.get("globalExclusions", [])):
156
+ return 0
157
+
158
+ threshold = config.get("threshold", rules.get("threshold", 1))
159
+ enabled_skills = config.get("skills", {})
160
+
161
+ # Score each skill
162
+ matches: list[tuple[str, int, list[str], str]] = []
163
+
164
+ for skill in rules.get("skills", []):
165
+ name = skill["name"]
166
+
167
+ # Skip disabled skills
168
+ if not enabled_skills.get(name, True):
169
+ continue
170
+
171
+ score, reasons = score_skill(prompt, skill)
172
+ if score >= threshold:
173
+ matches.append((name, score, reasons, skill.get("description", "")))
174
+
175
+ # No matches - silent exit
176
+ if not matches:
177
+ return 0
178
+
179
+ # Sort by score (highest first)
180
+ matches.sort(key=lambda t: t[1], reverse=True)
181
+
182
+ # Generate and output the reminder
183
+ reminder = generate_reminder(matches)
184
+ print(reminder)
185
+
186
+ return 0
187
+
188
+
189
+ if __name__ == "__main__":
190
+ raise SystemExit(main())
@@ -0,0 +1,122 @@
1
+ {
2
+ "description": "bluera-knowledge skill activation rules - technology-agnostic patterns for development scenarios",
3
+ "version": 2,
4
+ "globalExclusions": [
5
+ { "keyword": "bluera-knowledge" },
6
+ { "keyword": "bluera knowledge" },
7
+ { "keyword": "/bluera-knowledge:" },
8
+ { "regex": "mcp__.*bluera" }
9
+ ],
10
+ "threshold": 2,
11
+ "skills": [
12
+ {
13
+ "name": "knowledge-search",
14
+ "description": "How to query Bluera Knowledge for library/dependency questions",
15
+ "triggers": [
16
+ { "regex": "the\\s+\\w+(-\\w+)*\\s+(package|library|module|framework|dependency)", "weight": 3 },
17
+ { "regex": "\\w+(-\\w+)*\\s+(package|library|module)\\s+(is|does|keeps|isn't|won't|doesn't)", "weight": 3 },
18
+ { "regex": "\\w+(-\\w+)*\\s+(documentation|docs)\\b", "weight": 2 },
19
+ { "regex": "error\\s+(from|in|with)\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library|module)", "weight": 3 },
20
+ { "regex": "(package|library|dependency|module)\\s+(is\\s+)?(throwing|throws|error|failing)", "weight": 3 },
21
+ { "regex": "how\\s+does\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library|module|framework)\\s+(handle|work|process)", "weight": 3 },
22
+ { "regex": "what\\s+does\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library)\\s+(do|return|accept)", "weight": 3 },
23
+ { "regex": "why\\s+does\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library|module)", "weight": 3 },
24
+ { "regex": "(configure|config|settings)\\s+(for\\s+)?(the\\s+)?\\w+(-\\w+)*\\s+(package|library)", "weight": 3 },
25
+ { "regex": "(upgraded?|updated?)\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library|dependency)", "weight": 2 },
26
+ { "regex": "integrate\\s+(the\\s+)?\\w+(-\\w+)*\\s+(package|library)", "weight": 2 },
27
+ { "regex": "dependency\\s+(is|keeps|isn't|won't|error|issue|problem)", "weight": 2 },
28
+ { "regex": "third[- ]party\\s+(library|package|code)", "weight": 2 }
29
+ ],
30
+ "exclusions": [
31
+ { "keyword": "my code" },
32
+ { "keyword": "our code" },
33
+ { "keyword": "this function" },
34
+ { "keyword": "this file" },
35
+ { "keyword": "this component" },
36
+ { "keyword": "this class" },
37
+ { "keyword": "this project" },
38
+ { "keyword": "I wrote" },
39
+ { "keyword": "we wrote" },
40
+ { "keyword": "my implementation" },
41
+ { "keyword": "store" },
42
+ { "keyword": "index" }
43
+ ]
44
+ },
45
+ {
46
+ "name": "when-to-query",
47
+ "description": "Decision guide for Bluera Knowledge vs Grep/Read",
48
+ "triggers": [
49
+ { "keyword": "should i grep", "weight": 2 },
50
+ { "keyword": "where should i look", "weight": 2 },
51
+ { "keyword": "grep or search", "weight": 2 },
52
+ { "keyword": "search or grep", "weight": 2 },
53
+ { "regex": "where\\s+(should|do)\\s+i\\s+(find|look)\\s+.*(library|package|dependency)", "weight": 3 },
54
+ { "regex": "is\\s+there\\s+a\\s+better\\s+way\\s+to\\s+(search|find)", "weight": 2 },
55
+ { "regex": "should\\s+i\\s+(use\\s+)?grep\\s+(for|to)", "weight": 2 },
56
+ { "regex": "how\\s+(do|should)\\s+i\\s+find.*(in|from)\\s+(a\\s+)?(library|package|dependency)", "weight": 3 }
57
+ ],
58
+ "exclusions": [
59
+ { "keyword": "store" },
60
+ { "keyword": "index" }
61
+ ]
62
+ },
63
+ {
64
+ "name": "search-optimization",
65
+ "description": "Optimizing search parameters and token usage",
66
+ "triggers": [
67
+ { "keyword": "too many results", "weight": 2 },
68
+ { "keyword": "too few results", "weight": 2 },
69
+ { "keyword": "limit results", "weight": 2 },
70
+ { "keyword": "reduce tokens", "weight": 2 },
71
+ { "keyword": "token usage", "weight": 2 },
72
+ { "keyword": "optimize search", "weight": 2 },
73
+ { "keyword": "detail level", "weight": 2 },
74
+ { "regex": "\\b(minimal|contextual|full)\\s+detail", "weight": 2 },
75
+ { "regex": "\\b(vector|fts|hybrid)\\s+(search|mode)", "weight": 2 },
76
+ { "regex": "narrow\\s+(down\\s+)?(the\\s+)?results", "weight": 2 },
77
+ { "regex": "search\\s+(is\\s+)?(returning|giving)\\s+(too\\s+)?(many|few)", "weight": 2 }
78
+ ],
79
+ "exclusions": [
80
+ { "regex": "--?(limit|detail|mode|threshold)\\s*=" }
81
+ ]
82
+ },
83
+ {
84
+ "name": "advanced-workflows",
85
+ "description": "Multi-tool orchestration patterns",
86
+ "triggers": [
87
+ { "keyword": "multi-step", "weight": 2 },
88
+ { "keyword": "orchestration", "weight": 2 },
89
+ { "keyword": "job monitoring", "weight": 2 },
90
+ { "keyword": "background job", "weight": 2 },
91
+ { "keyword": "combine tools", "weight": 2 },
92
+ { "keyword": "chain operations", "weight": 2 },
93
+ { "regex": "chain.*searches", "weight": 2 },
94
+ { "regex": "multiple.*searches", "weight": 2 },
95
+ { "regex": "search.*then\\s+(summarize|extract|filter)", "weight": 2 },
96
+ { "regex": "for\\s+each\\s+(search\\s+)?(result|match)", "weight": 2 }
97
+ ],
98
+ "exclusions": []
99
+ },
100
+ {
101
+ "name": "store-lifecycle",
102
+ "description": "Managing knowledge stores",
103
+ "triggers": [
104
+ { "keyword": "add store", "weight": 2 },
105
+ { "keyword": "create store", "weight": 2 },
106
+ { "keyword": "delete store", "weight": 2 },
107
+ { "keyword": "remove store", "weight": 2 },
108
+ { "keyword": "index store", "weight": 2 },
109
+ { "keyword": "re-index", "weight": 2 },
110
+ { "keyword": "reindex", "weight": 2 },
111
+ { "keyword": "knowledge store", "weight": 2 },
112
+ { "regex": "add\\s+(a\\s+)?(repo|repository|folder|directory)\\s+(to|for)\\s+(knowledge|indexing|search)", "weight": 3 },
113
+ { "regex": "index\\s+(a|the|my)\\s+(repo|repository|folder|directory|library|package)", "weight": 2 },
114
+ { "regex": "set\\s+up.*(knowledge|search)\\s*(store|index)", "weight": 2 },
115
+ { "regex": "(backup|snapshot|archive).*(knowledge|search)\\s*(store|index)", "weight": 2 }
116
+ ],
117
+ "exclusions": [
118
+ { "regex": "/bluera-knowledge:(add-repo|add-folder|remove-store|index)" }
119
+ ]
120
+ }
121
+ ]
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluera-knowledge",
3
- "version": "0.11.20",
3
+ "version": "0.12.1",
4
4
  "description": "CLI tool for managing knowledge stores with semantic search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -328,6 +328,36 @@ describe('CodeGraph', () => {
328
328
  expect(edgeTypes).toContain('calls');
329
329
  });
330
330
 
331
+ it('includes confidence in serialized edges', () => {
332
+ const graph = new CodeGraph();
333
+ const nodes: CodeNode[] = [
334
+ {
335
+ type: 'function',
336
+ name: 'fn',
337
+ exported: false,
338
+ startLine: 1,
339
+ endLine: 2,
340
+ },
341
+ ];
342
+
343
+ graph.addNodes(nodes, '/src/test.ts');
344
+ graph.addImport('/src/test.ts', 'module', ['util']); // confidence: 1.0
345
+ graph.analyzeCallRelationships('other();', '/src/test.ts', 'fn'); // confidence: 0.5
346
+
347
+ const json = graph.toJSON();
348
+
349
+ // All edges should have confidence property preserved
350
+ expect(json.edges.every((e) => typeof e.confidence === 'number')).toBe(true);
351
+
352
+ // Import edges have confidence 1.0
353
+ const importEdge = json.edges.find((e) => e.type === 'imports');
354
+ expect(importEdge?.confidence).toBe(1.0);
355
+
356
+ // Call edges from regex detection have confidence 0.5
357
+ const callEdge = json.edges.find((e) => e.type === 'calls');
358
+ expect(callEdge?.confidence).toBe(0.5);
359
+ });
360
+
331
361
  it('handles empty graph', () => {
332
362
  const graph = new CodeGraph();
333
363
  const json = graph.toJSON();
@@ -227,7 +227,10 @@ export class CodeGraph {
227
227
  return importPath;
228
228
  }
229
229
 
230
- toJSON(): { nodes: GraphNode[]; edges: Array<{ from: string; to: string; type: string }> } {
230
+ toJSON(): {
231
+ nodes: GraphNode[];
232
+ edges: Array<{ from: string; to: string; type: string; confidence: number }>;
233
+ } {
231
234
  const allEdges: GraphEdge[] = [];
232
235
  for (const edges of this.edges.values()) {
233
236
  allEdges.push(...edges);
@@ -235,7 +238,12 @@ export class CodeGraph {
235
238
 
236
239
  return {
237
240
  nodes: Array.from(this.nodes.values()),
238
- edges: allEdges.map((e) => ({ from: e.from, to: e.to, type: e.type })),
241
+ edges: allEdges.map((e) => ({
242
+ from: e.from,
243
+ to: e.to,
244
+ type: e.type,
245
+ confidence: e.confidence,
246
+ })),
239
247
  };
240
248
  }
241
249
  }
@@ -17,8 +17,23 @@ interface MockStoreService {
17
17
  delete: MockInstance;
18
18
  }
19
19
 
20
+ interface MockLanceService {
21
+ deleteStore: MockInstance;
22
+ }
23
+
24
+ interface MockCodeGraphService {
25
+ deleteGraph: MockInstance;
26
+ }
27
+
28
+ interface MockConfigService {
29
+ resolveDataDir: MockInstance;
30
+ }
31
+
20
32
  interface MockServices {
21
33
  store: MockStoreService;
34
+ lance: MockLanceService;
35
+ codeGraph: MockCodeGraphService;
36
+ config: MockConfigService;
22
37
  }
23
38
 
24
39
  describe('store command execution', () => {
@@ -38,6 +53,15 @@ describe('store command execution', () => {
38
53
  create: vi.fn(),
39
54
  delete: vi.fn(),
40
55
  },
56
+ lance: {
57
+ deleteStore: vi.fn().mockResolvedValue(undefined),
58
+ },
59
+ codeGraph: {
60
+ deleteGraph: vi.fn().mockResolvedValue(undefined),
61
+ },
62
+ config: {
63
+ resolveDataDir: vi.fn().mockReturnValue('/tmp/test-data'),
64
+ },
41
65
  };
42
66
 
43
67
  vi.mocked(createServices).mockResolvedValue(mockServices);
@@ -366,6 +390,51 @@ describe('store command execution', () => {
366
390
  });
367
391
  });
368
392
 
393
+ it('creates repo store with branch option', async () => {
394
+ const mockStore: RepoStore = {
395
+ id: createStoreId('new-store-6'),
396
+ name: 'branched-repo',
397
+ type: 'repo',
398
+ path: '/path/to/cloned/repo',
399
+ url: 'https://github.com/user/repo',
400
+ branch: 'develop',
401
+ createdAt: new Date(),
402
+ updatedAt: new Date(),
403
+ };
404
+
405
+ mockServices.store.create.mockResolvedValue({
406
+ success: true,
407
+ data: mockStore,
408
+ });
409
+
410
+ const command = createStoreCommand(getOptions);
411
+ const createCommand = command.commands.find((c) => c.name() === 'create');
412
+ const actionHandler = createCommand?._actionHandler;
413
+
414
+ createCommand.parseOptions([
415
+ '--type',
416
+ 'repo',
417
+ '--source',
418
+ 'https://github.com/user/repo',
419
+ '--branch',
420
+ 'develop',
421
+ ]);
422
+ await actionHandler!(['branched-repo']);
423
+
424
+ expect(mockServices.store.create).toHaveBeenCalledWith({
425
+ name: 'branched-repo',
426
+ type: 'repo',
427
+ path: undefined,
428
+ url: 'https://github.com/user/repo',
429
+ branch: 'develop',
430
+ description: undefined,
431
+ tags: undefined,
432
+ });
433
+ expect(consoleLogSpy).toHaveBeenCalledWith(
434
+ expect.stringContaining('Created store: branched-repo')
435
+ );
436
+ });
437
+
369
438
  it('outputs JSON when format is json', async () => {
370
439
  const mockStore: FileStore = {
371
440
  id: createStoreId('new-store-5'),
@@ -860,6 +929,15 @@ describe('store command execution', () => {
860
929
  expect(tagsOption?.mandatory).toBe(false);
861
930
  });
862
931
 
932
+ it('create subcommand has --branch option for repo type', () => {
933
+ const command = createStoreCommand(getOptions);
934
+ const createCommand = command.commands.find((c) => c.name() === 'create');
935
+ const branchOption = createCommand?.options.find((o) => o.long === '--branch');
936
+
937
+ expect(branchOption).toBeDefined();
938
+ expect(branchOption?.mandatory).toBe(false);
939
+ });
940
+
863
941
  it('delete subcommand has force and yes options', () => {
864
942
  const command = createStoreCommand(getOptions);
865
943
  const deleteCommand = command.commands.find((c) => c.name() === 'delete');