gitnexus 1.4.1 → 1.4.5

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 (169) hide show
  1. package/README.md +215 -194
  2. package/dist/cli/ai-context.d.ts +2 -1
  3. package/dist/cli/ai-context.js +117 -90
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +57 -30
  6. package/dist/cli/augment.js +1 -1
  7. package/dist/cli/eval-server.d.ts +1 -1
  8. package/dist/cli/eval-server.js +1 -1
  9. package/dist/cli/index.js +18 -25
  10. package/dist/cli/lazy-action.d.ts +6 -0
  11. package/dist/cli/lazy-action.js +18 -0
  12. package/dist/cli/mcp.js +1 -1
  13. package/dist/cli/setup.js +42 -32
  14. package/dist/cli/skill-gen.d.ts +26 -0
  15. package/dist/cli/skill-gen.js +549 -0
  16. package/dist/cli/status.js +13 -4
  17. package/dist/cli/tool.d.ts +1 -1
  18. package/dist/cli/tool.js +2 -2
  19. package/dist/cli/wiki.js +2 -2
  20. package/dist/config/ignore-service.d.ts +25 -0
  21. package/dist/config/ignore-service.js +76 -0
  22. package/dist/config/supported-languages.d.ts +1 -0
  23. package/dist/config/supported-languages.js +1 -1
  24. package/dist/core/augmentation/engine.js +99 -72
  25. package/dist/core/embeddings/embedder.d.ts +1 -1
  26. package/dist/core/embeddings/embedder.js +1 -1
  27. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  28. package/dist/core/embeddings/embedding-pipeline.js +74 -47
  29. package/dist/core/embeddings/types.d.ts +1 -1
  30. package/dist/core/graph/types.d.ts +5 -2
  31. package/dist/core/ingestion/ast-cache.js +3 -2
  32. package/dist/core/ingestion/call-processor.d.ts +6 -7
  33. package/dist/core/ingestion/call-processor.js +560 -282
  34. package/dist/core/ingestion/call-routing.d.ts +53 -0
  35. package/dist/core/ingestion/call-routing.js +108 -0
  36. package/dist/core/ingestion/cluster-enricher.js +16 -16
  37. package/dist/core/ingestion/constants.d.ts +16 -0
  38. package/dist/core/ingestion/constants.js +16 -0
  39. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  40. package/dist/core/ingestion/entry-point-scoring.js +94 -24
  41. package/dist/core/ingestion/export-detection.d.ts +18 -0
  42. package/dist/core/ingestion/export-detection.js +231 -0
  43. package/dist/core/ingestion/filesystem-walker.js +4 -3
  44. package/dist/core/ingestion/framework-detection.d.ts +5 -1
  45. package/dist/core/ingestion/framework-detection.js +48 -8
  46. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  47. package/dist/core/ingestion/heritage-processor.js +109 -55
  48. package/dist/core/ingestion/import-processor.d.ts +16 -20
  49. package/dist/core/ingestion/import-processor.js +202 -696
  50. package/dist/core/ingestion/language-config.d.ts +46 -0
  51. package/dist/core/ingestion/language-config.js +167 -0
  52. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  53. package/dist/core/ingestion/mro-processor.js +369 -0
  54. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  55. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  56. package/dist/core/ingestion/parsing-processor.d.ts +3 -11
  57. package/dist/core/ingestion/parsing-processor.js +82 -181
  58. package/dist/core/ingestion/pipeline.d.ts +5 -1
  59. package/dist/core/ingestion/pipeline.js +192 -116
  60. package/dist/core/ingestion/process-processor.js +2 -1
  61. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  62. package/dist/core/ingestion/resolution-context.js +132 -0
  63. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  64. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  65. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  66. package/dist/core/ingestion/resolvers/go.js +42 -0
  67. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  68. package/dist/core/ingestion/resolvers/index.js +13 -0
  69. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  70. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  71. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  72. package/dist/core/ingestion/resolvers/php.js +35 -0
  73. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  74. package/dist/core/ingestion/resolvers/python.js +52 -0
  75. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  76. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  77. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  78. package/dist/core/ingestion/resolvers/rust.js +73 -0
  79. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  80. package/dist/core/ingestion/resolvers/standard.js +123 -0
  81. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  82. package/dist/core/ingestion/resolvers/utils.js +122 -0
  83. package/dist/core/ingestion/symbol-table.d.ts +15 -1
  84. package/dist/core/ingestion/symbol-table.js +20 -12
  85. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -11
  86. package/dist/core/ingestion/tree-sitter-queries.js +642 -485
  87. package/dist/core/ingestion/type-env.d.ts +49 -0
  88. package/dist/core/ingestion/type-env.js +559 -0
  89. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  90. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  91. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  92. package/dist/core/ingestion/type-extractors/csharp.js +369 -0
  93. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  94. package/dist/core/ingestion/type-extractors/go.js +436 -0
  95. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  96. package/dist/core/ingestion/type-extractors/index.js +31 -0
  97. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  98. package/dist/core/ingestion/type-extractors/jvm.js +654 -0
  99. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  100. package/dist/core/ingestion/type-extractors/php.js +411 -0
  101. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  102. package/dist/core/ingestion/type-extractors/python.js +392 -0
  103. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  104. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  105. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  106. package/dist/core/ingestion/type-extractors/rust.js +436 -0
  107. package/dist/core/ingestion/type-extractors/shared.d.ts +132 -0
  108. package/dist/core/ingestion/type-extractors/shared.js +571 -0
  109. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  110. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  111. package/dist/core/ingestion/type-extractors/types.d.ts +95 -0
  112. package/dist/core/ingestion/type-extractors/types.js +1 -0
  113. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  114. package/dist/core/ingestion/type-extractors/typescript.js +480 -0
  115. package/dist/core/ingestion/utils.d.ts +98 -0
  116. package/dist/core/ingestion/utils.js +1064 -9
  117. package/dist/core/ingestion/workers/parse-worker.d.ts +38 -4
  118. package/dist/core/ingestion/workers/parse-worker.js +248 -359
  119. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  120. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
  121. package/dist/core/{kuzu → lbug}/csv-generator.js +20 -4
  122. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
  123. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +82 -82
  124. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  125. package/dist/core/{kuzu → lbug}/schema.js +304 -289
  126. package/dist/core/search/bm25-index.d.ts +4 -4
  127. package/dist/core/search/bm25-index.js +17 -16
  128. package/dist/core/search/hybrid-search.d.ts +2 -2
  129. package/dist/core/search/hybrid-search.js +9 -9
  130. package/dist/core/tree-sitter/parser-loader.js +9 -2
  131. package/dist/core/wiki/generator.d.ts +4 -52
  132. package/dist/core/wiki/generator.js +53 -552
  133. package/dist/core/wiki/graph-queries.d.ts +4 -46
  134. package/dist/core/wiki/graph-queries.js +103 -282
  135. package/dist/core/wiki/html-viewer.js +192 -192
  136. package/dist/core/wiki/llm-client.js +11 -73
  137. package/dist/core/wiki/prompts.d.ts +8 -52
  138. package/dist/core/wiki/prompts.js +86 -200
  139. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  140. package/dist/mcp/compatible-stdio-transport.js +200 -0
  141. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +7 -9
  142. package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +77 -79
  143. package/dist/mcp/local/local-backend.d.ts +6 -6
  144. package/dist/mcp/local/local-backend.js +153 -146
  145. package/dist/mcp/resources.js +42 -42
  146. package/dist/mcp/server.js +18 -19
  147. package/dist/mcp/tools.js +103 -104
  148. package/dist/server/api.js +12 -12
  149. package/dist/server/mcp-http.d.ts +1 -1
  150. package/dist/server/mcp-http.js +1 -1
  151. package/dist/storage/repo-manager.d.ts +20 -2
  152. package/dist/storage/repo-manager.js +55 -1
  153. package/dist/types/pipeline.d.ts +1 -1
  154. package/hooks/claude/gitnexus-hook.cjs +238 -155
  155. package/hooks/claude/pre-tool-use.sh +79 -79
  156. package/hooks/claude/session-start.sh +42 -42
  157. package/package.json +98 -96
  158. package/scripts/patch-tree-sitter-swift.cjs +74 -74
  159. package/skills/gitnexus-cli.md +82 -82
  160. package/skills/gitnexus-debugging.md +89 -89
  161. package/skills/gitnexus-exploring.md +78 -78
  162. package/skills/gitnexus-guide.md +64 -64
  163. package/skills/gitnexus-impact-analysis.md +97 -97
  164. package/skills/gitnexus-pr-review.md +163 -163
  165. package/skills/gitnexus-refactoring.md +121 -121
  166. package/vendor/leiden/index.cjs +355 -355
  167. package/vendor/leiden/utils.cjs +392 -392
  168. package/dist/core/wiki/diagrams.d.ts +0 -27
  169. package/dist/core/wiki/diagrams.js +0 -163
@@ -24,84 +24,111 @@ const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
24
24
  * - Exact tool commands with parameters — vague directives get ignored
25
25
  * - Self-review checklist — forces model to verify its own work
26
26
  */
27
- function generateGitNexusContent(projectName, stats) {
28
- return `${GITNEXUS_START_MARKER}
29
- # GitNexus Code Intelligence
30
-
31
- This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
32
-
33
- > If any GitNexus tool warns the index is stale, run \`npx gitnexus analyze\` in terminal first.
34
-
35
- ## Always Do
36
-
37
- - **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`gitnexus_impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.
38
- - **MUST run \`gitnexus_detect_changes()\` before committing** to verify your changes only affect expected symbols and execution flows.
39
- - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
40
- - When exploring unfamiliar code, use \`gitnexus_query({query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
41
- - When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use \`gitnexus_context({name: "symbolName"})\`.
42
-
43
- ## When Debugging
44
-
45
- 1. \`gitnexus_query({query: "<error or symptom>"})\` — find execution flows related to the issue
46
- 2. \`gitnexus_context({name: "<suspect function>"})\` — see all callers, callees, and process participation
47
- 3. \`READ gitnexus://repo/${projectName}/process/{processName}\` — trace the full execution flow step by step
48
- 4. For regressions: \`gitnexus_detect_changes({scope: "compare", base_ref: "main"})\` see what your branch changed
49
-
50
- ## When Refactoring
51
-
52
- - **Renaming**: MUST use \`gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})\` first. Review the preview graph edits are safe, text_search edits need manual review. Then run with \`dry_run: false\`.
53
- - **Extracting/Splitting**: MUST run \`gitnexus_context({name: "target"})\` to see all incoming/outgoing refs, then \`gitnexus_impact({target: "target", direction: "upstream"})\` to find all external callers before moving code.
54
- - After any refactor: run \`gitnexus_detect_changes({scope: "all"})\` to verify only expected files changed.
55
-
56
- ## Never Do
57
-
58
- - NEVER edit a function, class, or method without first running \`gitnexus_impact\` on it.
59
- - NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
60
- - NEVER rename symbols with find-and-replace — use \`gitnexus_rename\` which understands the call graph.
61
- - NEVER commit changes without running \`gitnexus_detect_changes()\` to check affected scope.
62
-
63
- ## Tools Quick Reference
64
-
65
- | Tool | When to use | Command |
66
- |------|-------------|---------|
67
- | \`query\` | Find code by concept | \`gitnexus_query({query: "auth validation"})\` |
68
- | \`context\` | 360-degree view of one symbol | \`gitnexus_context({name: "validateUser"})\` |
69
- | \`impact\` | Blast radius before editing | \`gitnexus_impact({target: "X", direction: "upstream"})\` |
70
- | \`detect_changes\` | Pre-commit scope check | \`gitnexus_detect_changes({scope: "staged"})\` |
71
- | \`rename\` | Safe multi-file rename | \`gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})\` |
72
- | \`cypher\` | Custom graph queries | \`gitnexus_cypher({query: "MATCH ..."})\` |
73
-
74
- ## Impact Risk Levels
75
-
76
- | Depth | Meaning | Action |
77
- |-------|---------|--------|
78
- | d=1 | WILL BREAK direct callers/importers | MUST update these |
79
- | d=2 | LIKELY AFFECTED indirect deps | Should test |
80
- | d=3 | MAY NEED TESTING transitive | Test if critical path |
81
-
82
- ## Resources
83
-
84
- | Resource | Use for |
85
- |----------|---------|
86
- | \`gitnexus://repo/${projectName}/context\` | Codebase overview, check index freshness |
87
- | \`gitnexus://repo/${projectName}/clusters\` | All functional areas |
88
- | \`gitnexus://repo/${projectName}/processes\` | All execution flows |
89
- | \`gitnexus://repo/${projectName}/process/{name}\` | Step-by-step execution trace |
90
-
91
- ## Self-Check Before Finishing
92
-
93
- Before completing any code modification task, verify:
94
- 1. \`gitnexus_impact\` was run for all modified symbols
95
- 2. No HIGH/CRITICAL risk warnings were ignored
96
- 3. \`gitnexus_detect_changes()\` confirms changes match expected scope
97
- 4. All d=1 (WILL BREAK) dependents were updated
98
-
99
- ## CLI
100
-
101
- - Re-index: \`npx gitnexus analyze\`
102
- - Check freshness: \`npx gitnexus status\`
103
- - Generate docs: \`npx gitnexus wiki\`
104
-
27
+ function generateGitNexusContent(projectName, stats, generatedSkills) {
28
+ const generatedRows = (generatedSkills && generatedSkills.length > 0)
29
+ ? generatedSkills.map(s => `| Work in the ${s.label} area (${s.symbolCount} symbols) | \`.claude/skills/generated/${s.name}/SKILL.md\` |`).join('\n')
30
+ : '';
31
+ const skillsTable = `| Task | Read this skill file |
32
+ |------|---------------------|
33
+ | Understand architecture / "How does X work?" | \`.claude/skills/gitnexus/gitnexus-exploring/SKILL.md\` |
34
+ | Blast radius / "What breaks if I change X?" | \`.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md\` |
35
+ | Trace bugs / "Why is X failing?" | \`.claude/skills/gitnexus/gitnexus-debugging/SKILL.md\` |
36
+ | Rename / extract / split / refactor | \`.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md\` |
37
+ | Tools, resources, schema reference | \`.claude/skills/gitnexus/gitnexus-guide/SKILL.md\` |
38
+ | Index, status, clean, wiki CLI commands | \`.claude/skills/gitnexus/gitnexus-cli/SKILL.md\` |${generatedRows ? '\n' + generatedRows : ''}`;
39
+ return `${GITNEXUS_START_MARKER}
40
+ # GitNexus Code Intelligence
41
+
42
+ This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
43
+
44
+ > If any GitNexus tool warns the index is stale, run \`npx gitnexus analyze\` in terminal first.
45
+
46
+ ## Always Do
47
+
48
+ - **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`gitnexus_impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.
49
+ - **MUST run \`gitnexus_detect_changes()\` before committing** to verify your changes only affect expected symbols and execution flows.
50
+ - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
51
+ - When exploring unfamiliar code, use \`gitnexus_query({query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
52
+ - When you need full context on a specific symbolcallers, callees, which execution flows it participates in use \`gitnexus_context({name: "symbolName"})\`.
53
+
54
+ ## When Debugging
55
+
56
+ 1. \`gitnexus_query({query: "<error or symptom>"})\` — find execution flows related to the issue
57
+ 2. \`gitnexus_context({name: "<suspect function>"})\` — see all callers, callees, and process participation
58
+ 3. \`READ gitnexus://repo/${projectName}/process/{processName}\` trace the full execution flow step by step
59
+ 4. For regressions: \`gitnexus_detect_changes({scope: "compare", base_ref: "main"})\` see what your branch changed
60
+
61
+ ## When Refactoring
62
+
63
+ - **Renaming**: MUST use \`gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})\` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with \`dry_run: false\`.
64
+ - **Extracting/Splitting**: MUST run \`gitnexus_context({name: "target"})\` to see all incoming/outgoing refs, then \`gitnexus_impact({target: "target", direction: "upstream"})\` to find all external callers before moving code.
65
+ - After any refactor: run \`gitnexus_detect_changes({scope: "all"})\` to verify only expected files changed.
66
+
67
+ ## Never Do
68
+
69
+ - NEVER edit a function, class, or method without first running \`gitnexus_impact\` on it.
70
+ - NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
71
+ - NEVER rename symbols with find-and-replace use \`gitnexus_rename\` which understands the call graph.
72
+ - NEVER commit changes without running \`gitnexus_detect_changes()\` to check affected scope.
73
+
74
+ ## Tools Quick Reference
75
+
76
+ | Tool | When to use | Command |
77
+ |------|-------------|---------|
78
+ | \`query\` | Find code by concept | \`gitnexus_query({query: "auth validation"})\` |
79
+ | \`context\` | 360-degree view of one symbol | \`gitnexus_context({name: "validateUser"})\` |
80
+ | \`impact\` | Blast radius before editing | \`gitnexus_impact({target: "X", direction: "upstream"})\` |
81
+ | \`detect_changes\` | Pre-commit scope check | \`gitnexus_detect_changes({scope: "staged"})\` |
82
+ | \`rename\` | Safe multi-file rename | \`gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})\` |
83
+ | \`cypher\` | Custom graph queries | \`gitnexus_cypher({query: "MATCH ..."})\` |
84
+
85
+ ## Impact Risk Levels
86
+
87
+ | Depth | Meaning | Action |
88
+ |-------|---------|--------|
89
+ | d=1 | WILL BREAK — direct callers/importers | MUST update these |
90
+ | d=2 | LIKELY AFFECTED — indirect deps | Should test |
91
+ | d=3 | MAY NEED TESTING — transitive | Test if critical path |
92
+
93
+ ## Resources
94
+
95
+ | Resource | Use for |
96
+ |----------|---------|
97
+ | \`gitnexus://repo/${projectName}/context\` | Codebase overview, check index freshness |
98
+ | \`gitnexus://repo/${projectName}/clusters\` | All functional areas |
99
+ | \`gitnexus://repo/${projectName}/processes\` | All execution flows |
100
+ | \`gitnexus://repo/${projectName}/process/{name}\` | Step-by-step execution trace |
101
+
102
+ ## Self-Check Before Finishing
103
+
104
+ Before completing any code modification task, verify:
105
+ 1. \`gitnexus_impact\` was run for all modified symbols
106
+ 2. No HIGH/CRITICAL risk warnings were ignored
107
+ 3. \`gitnexus_detect_changes()\` confirms changes match expected scope
108
+ 4. All d=1 (WILL BREAK) dependents were updated
109
+
110
+ ## Keeping the Index Fresh
111
+
112
+ After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:
113
+
114
+ \`\`\`bash
115
+ npx gitnexus analyze
116
+ \`\`\`
117
+
118
+ If the index previously included embeddings, preserve them by adding \`--embeddings\`:
119
+
120
+ \`\`\`bash
121
+ npx gitnexus analyze --embeddings
122
+ \`\`\`
123
+
124
+ To check whether embeddings exist, inspect \`.gitnexus/meta.json\` — the \`stats.embeddings\` field shows the count (0 means no embeddings). **Running analyze without \`--embeddings\` will delete any previously generated embeddings.**
125
+
126
+ > Claude Code users: A PostToolUse hook handles this automatically after \`git commit\` and \`git merge\`.
127
+
128
+ ## CLI
129
+
130
+ ${skillsTable}
131
+
105
132
  ${GITNEXUS_END_MARKER}`;
106
133
  }
107
134
  /**
@@ -193,16 +220,16 @@ async function installSkills(repoPath) {
193
220
  }
194
221
  catch {
195
222
  // Fallback: generate minimal skill content
196
- skillContent = `---
197
- name: ${skill.name}
198
- description: ${skill.description}
199
- ---
200
-
201
- # ${skill.name.charAt(0).toUpperCase() + skill.name.slice(1)}
202
-
203
- ${skill.description}
204
-
205
- Use GitNexus tools to accomplish this task.
223
+ skillContent = `---
224
+ name: ${skill.name}
225
+ description: ${skill.description}
226
+ ---
227
+
228
+ # ${skill.name.charAt(0).toUpperCase() + skill.name.slice(1)}
229
+
230
+ ${skill.description}
231
+
232
+ Use GitNexus tools to accomplish this task.
206
233
  `;
207
234
  }
208
235
  await fs.writeFile(skillPath, skillContent, 'utf-8');
@@ -218,8 +245,8 @@ Use GitNexus tools to accomplish this task.
218
245
  /**
219
246
  * Generate AI context files after indexing
220
247
  */
221
- export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats) {
222
- const content = generateGitNexusContent(projectName, stats);
248
+ export async function generateAIContextFiles(repoPath, _storagePath, projectName, stats, generatedSkills) {
249
+ const content = generateGitNexusContent(projectName, stats, generatedSkills);
223
250
  const createdFiles = [];
224
251
  // Create AGENTS.md (standard for Cursor, Windsurf, OpenCode, Cline, etc.)
225
252
  const agentsPath = path.join(repoPath, 'AGENTS.md');
@@ -6,5 +6,7 @@
6
6
  export interface AnalyzeOptions {
7
7
  force?: boolean;
8
8
  embeddings?: boolean;
9
+ skills?: boolean;
10
+ verbose?: boolean;
9
11
  }
10
12
  export declare const analyzeCommand: (inputPath?: string, options?: AnalyzeOptions) => Promise<void>;
@@ -8,14 +8,15 @@ import { execFileSync } from 'child_process';
8
8
  import v8 from 'v8';
9
9
  import cliProgress from 'cli-progress';
10
10
  import { runPipelineFromRepo } from '../core/ingestion/pipeline.js';
11
- import { initKuzu, loadGraphToKuzu, getKuzuStats, executeQuery, executeWithReusedStatement, closeKuzu, createFTSIndex, loadCachedEmbeddings } from '../core/kuzu/kuzu-adapter.js';
11
+ import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, createFTSIndex, loadCachedEmbeddings } from '../core/lbug/lbug-adapter.js';
12
12
  // Embedding imports are lazy (dynamic import) so onnxruntime-node is never
13
13
  // loaded when embeddings are not requested. This avoids crashes on Node
14
14
  // versions whose ABI is not yet supported by the native binary (#89).
15
15
  // disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38)
16
- import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath } from '../storage/repo-manager.js';
16
+ import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath, cleanupOldKuzuFiles } from '../storage/repo-manager.js';
17
17
  import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
18
18
  import { generateAIContextFiles } from './ai-context.js';
19
+ import { generateSkillFiles } from './skill-gen.js';
19
20
  import fs from 'fs/promises';
20
21
  const HEAP_MB = 8192;
21
22
  const HEAP_FLAG = `--max-old-space-size=${HEAP_MB}`;
@@ -50,7 +51,7 @@ const PHASE_LABELS = {
50
51
  communities: 'Detecting communities',
51
52
  processes: 'Detecting processes',
52
53
  complete: 'Pipeline complete',
53
- kuzu: 'Loading into KuzuDB',
54
+ lbug: 'Loading into LadybugDB',
54
55
  fts: 'Creating search indexes',
55
56
  embeddings: 'Generating embeddings',
56
57
  done: 'Done',
@@ -58,6 +59,9 @@ const PHASE_LABELS = {
58
59
  export const analyzeCommand = async (inputPath, options) => {
59
60
  if (ensureHeap())
60
61
  return;
62
+ if (options?.verbose) {
63
+ process.env.GITNEXUS_VERBOSE = '1';
64
+ }
61
65
  console.log('\n GitNexus Analyzer\n');
62
66
  let repoPath;
63
67
  if (inputPath) {
@@ -77,13 +81,22 @@ export const analyzeCommand = async (inputPath, options) => {
77
81
  process.exitCode = 1;
78
82
  return;
79
83
  }
80
- const { storagePath, kuzuPath } = getStoragePaths(repoPath);
84
+ const { storagePath, lbugPath } = getStoragePaths(repoPath);
85
+ // Clean up stale KuzuDB files from before the LadybugDB migration.
86
+ // If kuzu existed but lbug doesn't, we're doing a migration re-index — say so.
87
+ const kuzuResult = await cleanupOldKuzuFiles(storagePath);
88
+ if (kuzuResult.found && kuzuResult.needsReindex) {
89
+ console.log(' Migrating from KuzuDB to LadybugDB — rebuilding index...\n');
90
+ }
81
91
  const currentCommit = getCurrentCommit(repoPath);
82
92
  const existingMeta = await loadMeta(storagePath);
83
- if (existingMeta && !options?.force && existingMeta.lastCommit === currentCommit) {
93
+ if (existingMeta && !options?.force && !options?.skills && existingMeta.lastCommit === currentCommit) {
84
94
  console.log(' Already up to date\n');
85
95
  return;
86
96
  }
97
+ if (process.env.GITNEXUS_NO_GITIGNORE) {
98
+ console.log(' GITNEXUS_NO_GITIGNORE is set — skipping .gitignore (still reading .gitnexusignore)\n');
99
+ }
87
100
  // Single progress bar for entire pipeline
88
101
  const bar = new cliProgress.SingleBar({
89
102
  format: ' {bar} {percentage}% | {phase}',
@@ -104,7 +117,7 @@ export const analyzeCommand = async (inputPath, options) => {
104
117
  aborted = true;
105
118
  bar.stop();
106
119
  console.log('\n Interrupted — cleaning up...');
107
- closeKuzu().catch(() => { }).finally(() => process.exit(130));
120
+ closeLbug().catch(() => { }).finally(() => process.exit(130));
108
121
  };
109
122
  process.on('SIGINT', sigintHandler);
110
123
  // Route all console output through bar.log() so the bar doesn't stamp itself
@@ -150,15 +163,15 @@ export const analyzeCommand = async (inputPath, options) => {
150
163
  if (options?.embeddings && existingMeta && !options?.force) {
151
164
  try {
152
165
  updateBar(0, 'Caching embeddings...');
153
- await initKuzu(kuzuPath);
166
+ await initLbug(lbugPath);
154
167
  const cached = await loadCachedEmbeddings();
155
168
  cachedEmbeddingNodeIds = cached.embeddingNodeIds;
156
169
  cachedEmbeddings = cached.embeddings;
157
- await closeKuzu();
170
+ await closeLbug();
158
171
  }
159
172
  catch {
160
173
  try {
161
- await closeKuzu();
174
+ await closeLbug();
162
175
  }
163
176
  catch { }
164
177
  }
@@ -169,26 +182,26 @@ export const analyzeCommand = async (inputPath, options) => {
169
182
  const scaled = Math.round(progress.percent * 0.6);
170
183
  updateBar(scaled, phaseLabel);
171
184
  });
172
- // ── Phase 2: KuzuDB (60–85%) ──────────────────────────────────────
173
- updateBar(60, 'Loading into KuzuDB...');
174
- await closeKuzu();
175
- const kuzuFiles = [kuzuPath, `${kuzuPath}.wal`, `${kuzuPath}.lock`];
176
- for (const f of kuzuFiles) {
185
+ // ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────────
186
+ updateBar(60, 'Loading into LadybugDB...');
187
+ await closeLbug();
188
+ const lbugFiles = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.lock`];
189
+ for (const f of lbugFiles) {
177
190
  try {
178
191
  await fs.rm(f, { recursive: true, force: true });
179
192
  }
180
193
  catch { }
181
194
  }
182
- const t0Kuzu = Date.now();
183
- await initKuzu(kuzuPath);
184
- let kuzuMsgCount = 0;
185
- const kuzuResult = await loadGraphToKuzu(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
186
- kuzuMsgCount++;
187
- const progress = Math.min(84, 60 + Math.round((kuzuMsgCount / (kuzuMsgCount + 10)) * 24));
195
+ const t0Lbug = Date.now();
196
+ await initLbug(lbugPath);
197
+ let lbugMsgCount = 0;
198
+ const lbugResult = await loadGraphToLbug(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
199
+ lbugMsgCount++;
200
+ const progress = Math.min(84, 60 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 24));
188
201
  updateBar(progress, msg);
189
202
  });
190
- const kuzuTime = ((Date.now() - t0Kuzu) / 1000).toFixed(1);
191
- const kuzuWarnings = kuzuResult.warnings;
203
+ const lbugTime = ((Date.now() - t0Lbug) / 1000).toFixed(1);
204
+ const lbugWarnings = lbugResult.warnings;
192
205
  // ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
193
206
  updateBar(85, 'Creating search indexes...');
194
207
  const t0Fts = Date.now();
@@ -217,7 +230,7 @@ export const analyzeCommand = async (inputPath, options) => {
217
230
  }
218
231
  }
219
232
  // ── Phase 4: Embeddings (90–98%) ──────────────────────────────────
220
- const stats = await getKuzuStats();
233
+ const stats = await getLbugStats();
221
234
  let embeddingTime = '0.0';
222
235
  let embeddingSkipped = true;
223
236
  let embeddingSkipReason = 'off (use --embeddings to enable)';
@@ -242,6 +255,13 @@ export const analyzeCommand = async (inputPath, options) => {
242
255
  }
243
256
  // ── Phase 5: Finalize (98–100%) ───────────────────────────────────
244
257
  updateBar(98, 'Saving metadata...');
258
+ // Count embeddings in the index (cached + newly generated)
259
+ let embeddingCount = 0;
260
+ try {
261
+ const embResult = await executeQuery(`MATCH (e:CodeEmbedding) RETURN count(e) AS cnt`);
262
+ embeddingCount = embResult?.[0]?.cnt ?? 0;
263
+ }
264
+ catch { /* table may not exist if embeddings never ran */ }
245
265
  const meta = {
246
266
  repoPath,
247
267
  lastCommit: currentCommit,
@@ -252,6 +272,7 @@ export const analyzeCommand = async (inputPath, options) => {
252
272
  edges: stats.edges,
253
273
  communities: pipelineResult.communityResult?.stats.totalCommunities,
254
274
  processes: pipelineResult.processResult?.stats.totalProcesses,
275
+ embeddings: embeddingCount,
255
276
  },
256
277
  };
257
278
  await saveMeta(storagePath, meta);
@@ -267,6 +288,12 @@ export const analyzeCommand = async (inputPath, options) => {
267
288
  }
268
289
  aggregatedClusterCount = Array.from(groups.values()).filter(count => count >= 5).length;
269
290
  }
291
+ let generatedSkills = [];
292
+ if (options?.skills && pipelineResult.communityResult) {
293
+ updateBar(99, 'Generating skill files...');
294
+ const skillResult = await generateSkillFiles(repoPath, projectName, pipelineResult);
295
+ generatedSkills = skillResult.skills;
296
+ }
270
297
  const aiContext = await generateAIContextFiles(repoPath, storagePath, projectName, {
271
298
  files: pipelineResult.totalFileCount,
272
299
  nodes: stats.nodes,
@@ -274,8 +301,8 @@ export const analyzeCommand = async (inputPath, options) => {
274
301
  communities: pipelineResult.communityResult?.stats.totalCommunities,
275
302
  clusters: aggregatedClusterCount,
276
303
  processes: pipelineResult.processResult?.stats.totalProcesses,
277
- });
278
- await closeKuzu();
304
+ }, generatedSkills);
305
+ await closeLbug();
279
306
  // Note: we intentionally do NOT call disposeEmbedder() here.
280
307
  // ONNX Runtime's native cleanup segfaults on macOS and some Linux configs.
281
308
  // Since the process exits immediately after, Node.js reclaims everything.
@@ -291,18 +318,18 @@ export const analyzeCommand = async (inputPath, options) => {
291
318
  const embeddingsCached = cachedEmbeddings.length > 0;
292
319
  console.log(`\n Repository indexed successfully (${totalTime}s)${embeddingsCached ? ` [${cachedEmbeddings.length} embeddings cached]` : ''}\n`);
293
320
  console.log(` ${stats.nodes.toLocaleString()} nodes | ${stats.edges.toLocaleString()} edges | ${pipelineResult.communityResult?.stats.totalCommunities || 0} clusters | ${pipelineResult.processResult?.stats.totalProcesses || 0} flows`);
294
- console.log(` KuzuDB ${kuzuTime}s | FTS ${ftsTime}s | Embeddings ${embeddingSkipped ? embeddingSkipReason : embeddingTime + 's'}`);
321
+ console.log(` LadybugDB ${lbugTime}s | FTS ${ftsTime}s | Embeddings ${embeddingSkipped ? embeddingSkipReason : embeddingTime + 's'}`);
295
322
  console.log(` ${repoPath}`);
296
323
  if (aiContext.files.length > 0) {
297
324
  console.log(` Context: ${aiContext.files.join(', ')}`);
298
325
  }
299
326
  // Show a quiet summary if some edge types needed fallback insertion
300
- if (kuzuWarnings.length > 0) {
301
- const totalFallback = kuzuWarnings.reduce((sum, w) => {
327
+ if (lbugWarnings.length > 0) {
328
+ const totalFallback = lbugWarnings.reduce((sum, w) => {
302
329
  const m = w.match(/\((\d+) edges\)/);
303
330
  return sum + (m ? parseInt(m[1]) : 0);
304
331
  }, 0);
305
- console.log(` Note: ${totalFallback} edges across ${kuzuWarnings.length} types inserted via fallback (schema will be updated in next release)`);
332
+ console.log(` Note: ${totalFallback} edges across ${lbugWarnings.length} types inserted via fallback (schema will be updated in next release)`);
306
333
  }
307
334
  try {
308
335
  await fs.access(getGlobalRegistryPath());
@@ -311,7 +338,7 @@ export const analyzeCommand = async (inputPath, options) => {
311
338
  console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
312
339
  }
313
340
  console.log('');
314
- // KuzuDB's native module holds open handles that prevent Node from exiting.
341
+ // LadybugDB's native module holds open handles that prevent Node from exiting.
315
342
  // ONNX Runtime also registers native atexit hooks that segfault on some
316
343
  // platforms (#38, #40). Force-exit to ensure clean termination.
317
344
  process.exit(0);
@@ -19,7 +19,7 @@ export async function augmentCommand(pattern) {
19
19
  const result = await augment(pattern, process.cwd());
20
20
  if (result) {
21
21
  // IMPORTANT: Write to stderr, NOT stdout.
22
- // KuzuDB's native module captures stdout fd at OS level during init,
22
+ // LadybugDB's native module captures stdout fd at OS level during init,
23
23
  // which makes stdout permanently broken in subprocess contexts.
24
24
  // stderr is never captured, so it works reliably everywhere.
25
25
  // The hook reads from the subprocess's stderr.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Eval Server — Lightweight HTTP server for SWE-bench evaluation
3
3
  *
4
- * Keeps KuzuDB warm in memory so tool calls from the agent are near-instant.
4
+ * Keeps LadybugDB warm in memory so tool calls from the agent are near-instant.
5
5
  * Designed to run inside Docker containers during SWE-bench evaluation.
6
6
  *
7
7
  * KEY DESIGN: Returns LLM-friendly text, not raw JSON.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Eval Server — Lightweight HTTP server for SWE-bench evaluation
3
3
  *
4
- * Keeps KuzuDB warm in memory so tool calls from the agent are near-instant.
4
+ * Keeps LadybugDB warm in memory so tool calls from the agent are near-instant.
5
5
  * Designed to run inside Docker containers during SWE-bench evaluation.
6
6
  *
7
7
  * KEY DESIGN: Returns LLM-friendly text, not raw JSON.
package/dist/cli/index.js CHANGED
@@ -2,18 +2,8 @@
2
2
  // Heap re-spawn removed — only analyze.ts needs the 8GB heap (via its own ensureHeap()).
3
3
  // Removing it from here improves MCP server startup time significantly.
4
4
  import { Command } from 'commander';
5
- import { analyzeCommand } from './analyze.js';
6
- import { serveCommand } from './serve.js';
7
- import { listCommand } from './list.js';
8
- import { statusCommand } from './status.js';
9
- import { mcpCommand } from './mcp.js';
10
- import { cleanCommand } from './clean.js';
11
- import { setupCommand } from './setup.js';
12
- import { augmentCommand } from './augment.js';
13
- import { wikiCommand } from './wiki.js';
14
- import { queryCommand, contextCommand, impactCommand, cypherCommand } from './tool.js';
15
- import { evalServerCommand } from './eval-server.js';
16
5
  import { createRequire } from 'node:module';
6
+ import { createLazyAction } from './lazy-action.js';
17
7
  const _require = createRequire(import.meta.url);
18
8
  const pkg = _require('../../package.json');
19
9
  const program = new Command();
@@ -24,37 +14,40 @@ program
24
14
  program
25
15
  .command('setup')
26
16
  .description('One-time setup: configure MCP for Cursor, Claude Code, OpenCode')
27
- .action(setupCommand);
17
+ .action(createLazyAction(() => import('./setup.js'), 'setupCommand'));
28
18
  program
29
19
  .command('analyze [path]')
30
20
  .description('Index a repository (full analysis)')
31
21
  .option('-f, --force', 'Force full re-index even if up to date')
32
22
  .option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
33
- .action(analyzeCommand);
23
+ .option('--skills', 'Generate repo-specific skill files from detected communities')
24
+ .option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)')
25
+ .addHelpText('after', '\nEnvironment variables:\n GITNEXUS_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .gitnexusignore)')
26
+ .action(createLazyAction(() => import('./analyze.js'), 'analyzeCommand'));
34
27
  program
35
28
  .command('serve')
36
29
  .description('Start local HTTP server for web UI connection')
37
30
  .option('-p, --port <port>', 'Port number', '4747')
38
31
  .option('--host <host>', 'Bind address (default: 127.0.0.1, use 0.0.0.0 for remote access)')
39
- .action(serveCommand);
32
+ .action(createLazyAction(() => import('./serve.js'), 'serveCommand'));
40
33
  program
41
34
  .command('mcp')
42
35
  .description('Start MCP server (stdio) — serves all indexed repos')
43
- .action(mcpCommand);
36
+ .action(createLazyAction(() => import('./mcp.js'), 'mcpCommand'));
44
37
  program
45
38
  .command('list')
46
39
  .description('List all indexed repositories')
47
- .action(listCommand);
40
+ .action(createLazyAction(() => import('./list.js'), 'listCommand'));
48
41
  program
49
42
  .command('status')
50
43
  .description('Show index status for current repo')
51
- .action(statusCommand);
44
+ .action(createLazyAction(() => import('./status.js'), 'statusCommand'));
52
45
  program
53
46
  .command('clean')
54
47
  .description('Delete GitNexus index for current repo')
55
48
  .option('-f, --force', 'Skip confirmation prompt')
56
49
  .option('--all', 'Clean all indexed repos')
57
- .action(cleanCommand);
50
+ .action(createLazyAction(() => import('./clean.js'), 'cleanCommand'));
58
51
  program
59
52
  .command('wiki [path]')
60
53
  .description('Generate repository wiki from knowledge graph')
@@ -64,11 +57,11 @@ program
64
57
  .option('--api-key <key>', 'LLM API key (saved to ~/.gitnexus/config.json)')
65
58
  .option('--concurrency <n>', 'Parallel LLM calls (default: 3)', '3')
66
59
  .option('--gist', 'Publish wiki as a public GitHub Gist after generation')
67
- .action(wikiCommand);
60
+ .action(createLazyAction(() => import('./wiki.js'), 'wikiCommand'));
68
61
  program
69
62
  .command('augment <pattern>')
70
63
  .description('Augment a search pattern with knowledge graph context (used by hooks)')
71
- .action(augmentCommand);
64
+ .action(createLazyAction(() => import('./augment.js'), 'augmentCommand'));
72
65
  // ─── Direct Tool Commands (no MCP overhead) ────────────────────────
73
66
  // These invoke LocalBackend directly for use in eval, scripts, and CI.
74
67
  program
@@ -79,7 +72,7 @@ program
79
72
  .option('-g, --goal <text>', 'What you want to find')
80
73
  .option('-l, --limit <n>', 'Max processes to return (default: 5)')
81
74
  .option('--content', 'Include full symbol source code')
82
- .action(queryCommand);
75
+ .action(createLazyAction(() => import('./tool.js'), 'queryCommand'));
83
76
  program
84
77
  .command('context [name]')
85
78
  .description('360-degree view of a code symbol: callers, callees, processes')
@@ -87,7 +80,7 @@ program
87
80
  .option('-u, --uid <uid>', 'Direct symbol UID (zero-ambiguity lookup)')
88
81
  .option('-f, --file <path>', 'File path to disambiguate common names')
89
82
  .option('--content', 'Include full symbol source code')
90
- .action(contextCommand);
83
+ .action(createLazyAction(() => import('./tool.js'), 'contextCommand'));
91
84
  program
92
85
  .command('impact <target>')
93
86
  .description('Blast radius analysis: what breaks if you change a symbol')
@@ -95,17 +88,17 @@ program
95
88
  .option('-r, --repo <name>', 'Target repository')
96
89
  .option('--depth <n>', 'Max relationship depth (default: 3)')
97
90
  .option('--include-tests', 'Include test files in results')
98
- .action(impactCommand);
91
+ .action(createLazyAction(() => import('./tool.js'), 'impactCommand'));
99
92
  program
100
93
  .command('cypher <query>')
101
94
  .description('Execute raw Cypher query against the knowledge graph')
102
95
  .option('-r, --repo <name>', 'Target repository')
103
- .action(cypherCommand);
96
+ .action(createLazyAction(() => import('./tool.js'), 'cypherCommand'));
104
97
  // ─── Eval Server (persistent daemon for SWE-bench) ─────────────────
105
98
  program
106
99
  .command('eval-server')
107
100
  .description('Start lightweight HTTP server for fast tool calls during evaluation')
108
101
  .option('-p, --port <port>', 'Port number', '4848')
109
102
  .option('--idle-timeout <seconds>', 'Auto-shutdown after N seconds idle (0 = disabled)', '0')
110
- .action(evalServerCommand);
103
+ .action(createLazyAction(() => import('./eval-server.js'), 'evalServerCommand'));
111
104
  program.parse(process.argv);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Creates a lazy-loaded CLI action that defers module import until invocation.
3
+ * The generic constraints ensure the export name is a valid key of the module
4
+ * at compile time — catching typos when used with concrete module imports.
5
+ */
6
+ export declare function createLazyAction<TModule extends Record<string, unknown>, TKey extends string & keyof TModule>(loader: () => Promise<TModule>, exportName: TKey): (...args: unknown[]) => Promise<void>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Creates a lazy-loaded CLI action that defers module import until invocation.
3
+ * The generic constraints ensure the export name is a valid key of the module
4
+ * at compile time — catching typos when used with concrete module imports.
5
+ */
6
+ function isCallable(value) {
7
+ return typeof value === 'function';
8
+ }
9
+ export function createLazyAction(loader, exportName) {
10
+ return async (...args) => {
11
+ const module = await loader();
12
+ const action = module[exportName];
13
+ if (!isCallable(action)) {
14
+ throw new Error(`Lazy action export not found: ${exportName}`);
15
+ }
16
+ await action(...args);
17
+ };
18
+ }