glancey 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +435 -0
  3. package/dist/__tests__/ast-chunker.test.d.ts +2 -0
  4. package/dist/__tests__/ast-chunker.test.d.ts.map +1 -0
  5. package/dist/__tests__/ast-chunker.test.js +307 -0
  6. package/dist/__tests__/ast-chunker.test.js.map +1 -0
  7. package/dist/__tests__/config.test.d.ts +2 -0
  8. package/dist/__tests__/config.test.d.ts.map +1 -0
  9. package/dist/__tests__/config.test.js +242 -0
  10. package/dist/__tests__/config.test.js.map +1 -0
  11. package/dist/__tests__/dashboard/beads.test.d.ts +2 -0
  12. package/dist/__tests__/dashboard/beads.test.d.ts.map +1 -0
  13. package/dist/__tests__/dashboard/beads.test.js +151 -0
  14. package/dist/__tests__/dashboard/beads.test.js.map +1 -0
  15. package/dist/__tests__/dashboard/index.test.d.ts +2 -0
  16. package/dist/__tests__/dashboard/index.test.d.ts.map +1 -0
  17. package/dist/__tests__/dashboard/index.test.js +116 -0
  18. package/dist/__tests__/dashboard/index.test.js.map +1 -0
  19. package/dist/__tests__/dashboard/routes.test.d.ts +2 -0
  20. package/dist/__tests__/dashboard/routes.test.d.ts.map +1 -0
  21. package/dist/__tests__/dashboard/routes.test.js +125 -0
  22. package/dist/__tests__/dashboard/routes.test.js.map +1 -0
  23. package/dist/__tests__/dashboard/server.test.d.ts +2 -0
  24. package/dist/__tests__/dashboard/server.test.d.ts.map +1 -0
  25. package/dist/__tests__/dashboard/server.test.js +75 -0
  26. package/dist/__tests__/dashboard/server.test.js.map +1 -0
  27. package/dist/__tests__/dashboard/state.test.d.ts +2 -0
  28. package/dist/__tests__/dashboard/state.test.d.ts.map +1 -0
  29. package/dist/__tests__/dashboard/state.test.js +124 -0
  30. package/dist/__tests__/dashboard/state.test.js.map +1 -0
  31. package/dist/__tests__/dashboard/token-tracking.test.d.ts +2 -0
  32. package/dist/__tests__/dashboard/token-tracking.test.d.ts.map +1 -0
  33. package/dist/__tests__/dashboard/token-tracking.test.js +315 -0
  34. package/dist/__tests__/dashboard/token-tracking.test.js.map +1 -0
  35. package/dist/__tests__/embeddings/factory.test.d.ts +2 -0
  36. package/dist/__tests__/embeddings/factory.test.d.ts.map +1 -0
  37. package/dist/__tests__/embeddings/factory.test.js +157 -0
  38. package/dist/__tests__/embeddings/factory.test.js.map +1 -0
  39. package/dist/__tests__/embeddings/gemini.test.d.ts +2 -0
  40. package/dist/__tests__/embeddings/gemini.test.d.ts.map +1 -0
  41. package/dist/__tests__/embeddings/gemini.test.js +178 -0
  42. package/dist/__tests__/embeddings/gemini.test.js.map +1 -0
  43. package/dist/__tests__/embeddings/ollama.test.d.ts +2 -0
  44. package/dist/__tests__/embeddings/ollama.test.d.ts.map +1 -0
  45. package/dist/__tests__/embeddings/ollama.test.js +273 -0
  46. package/dist/__tests__/embeddings/ollama.test.js.map +1 -0
  47. package/dist/__tests__/embeddings/rate-limiter.test.d.ts +2 -0
  48. package/dist/__tests__/embeddings/rate-limiter.test.d.ts.map +1 -0
  49. package/dist/__tests__/embeddings/rate-limiter.test.js +163 -0
  50. package/dist/__tests__/embeddings/rate-limiter.test.js.map +1 -0
  51. package/dist/__tests__/embeddings/retry.test.d.ts +2 -0
  52. package/dist/__tests__/embeddings/retry.test.d.ts.map +1 -0
  53. package/dist/__tests__/embeddings/retry.test.js +334 -0
  54. package/dist/__tests__/embeddings/retry.test.js.map +1 -0
  55. package/dist/__tests__/embeddings/types.test.d.ts +2 -0
  56. package/dist/__tests__/embeddings/types.test.d.ts.map +1 -0
  57. package/dist/__tests__/embeddings/types.test.js +31 -0
  58. package/dist/__tests__/embeddings/types.test.js.map +1 -0
  59. package/dist/__tests__/mocks/embedding-backend.mock.d.ts +10 -0
  60. package/dist/__tests__/mocks/embedding-backend.mock.d.ts.map +1 -0
  61. package/dist/__tests__/mocks/embedding-backend.mock.js +39 -0
  62. package/dist/__tests__/mocks/embedding-backend.mock.js.map +1 -0
  63. package/dist/__tests__/mocks/fetch.mock.d.ts +49 -0
  64. package/dist/__tests__/mocks/fetch.mock.d.ts.map +1 -0
  65. package/dist/__tests__/mocks/fetch.mock.js +102 -0
  66. package/dist/__tests__/mocks/fetch.mock.js.map +1 -0
  67. package/dist/__tests__/mocks/lancedb.mock.d.ts +38 -0
  68. package/dist/__tests__/mocks/lancedb.mock.d.ts.map +1 -0
  69. package/dist/__tests__/mocks/lancedb.mock.js +63 -0
  70. package/dist/__tests__/mocks/lancedb.mock.js.map +1 -0
  71. package/dist/__tests__/search/clustering.test.d.ts +2 -0
  72. package/dist/__tests__/search/clustering.test.d.ts.map +1 -0
  73. package/dist/__tests__/search/clustering.test.js +230 -0
  74. package/dist/__tests__/search/clustering.test.js.map +1 -0
  75. package/dist/__tests__/search/hybrid-search.test.d.ts +2 -0
  76. package/dist/__tests__/search/hybrid-search.test.d.ts.map +1 -0
  77. package/dist/__tests__/search/hybrid-search.test.js +279 -0
  78. package/dist/__tests__/search/hybrid-search.test.js.map +1 -0
  79. package/dist/__tests__/search/indexer.test.d.ts +2 -0
  80. package/dist/__tests__/search/indexer.test.d.ts.map +1 -0
  81. package/dist/__tests__/search/indexer.test.js +1305 -0
  82. package/dist/__tests__/search/indexer.test.js.map +1 -0
  83. package/dist/__tests__/search/tree-sitter-chunker.test.d.ts +2 -0
  84. package/dist/__tests__/search/tree-sitter-chunker.test.d.ts.map +1 -0
  85. package/dist/__tests__/search/tree-sitter-chunker.test.js +228 -0
  86. package/dist/__tests__/search/tree-sitter-chunker.test.js.map +1 -0
  87. package/dist/__tests__/setup.d.ts +2 -0
  88. package/dist/__tests__/setup.d.ts.map +1 -0
  89. package/dist/__tests__/setup.js +11 -0
  90. package/dist/__tests__/setup.js.map +1 -0
  91. package/dist/__tests__/tools/clustering-handlers.test.d.ts +2 -0
  92. package/dist/__tests__/tools/clustering-handlers.test.d.ts.map +1 -0
  93. package/dist/__tests__/tools/clustering-handlers.test.js +286 -0
  94. package/dist/__tests__/tools/clustering-handlers.test.js.map +1 -0
  95. package/dist/__tests__/tools/commit-handlers.test.d.ts +2 -0
  96. package/dist/__tests__/tools/commit-handlers.test.d.ts.map +1 -0
  97. package/dist/__tests__/tools/commit-handlers.test.js +153 -0
  98. package/dist/__tests__/tools/commit-handlers.test.js.map +1 -0
  99. package/dist/__tests__/tools/index-handlers.test.d.ts +2 -0
  100. package/dist/__tests__/tools/index-handlers.test.d.ts.map +1 -0
  101. package/dist/__tests__/tools/index-handlers.test.js +184 -0
  102. package/dist/__tests__/tools/index-handlers.test.js.map +1 -0
  103. package/dist/__tests__/tools/instructions-handlers.test.d.ts +2 -0
  104. package/dist/__tests__/tools/instructions-handlers.test.d.ts.map +1 -0
  105. package/dist/__tests__/tools/instructions-handlers.test.js +53 -0
  106. package/dist/__tests__/tools/instructions-handlers.test.js.map +1 -0
  107. package/dist/__tests__/tools/memory-handlers.test.d.ts +2 -0
  108. package/dist/__tests__/tools/memory-handlers.test.d.ts.map +1 -0
  109. package/dist/__tests__/tools/memory-handlers.test.js +175 -0
  110. package/dist/__tests__/tools/memory-handlers.test.js.map +1 -0
  111. package/dist/__tests__/tools/search-handlers.test.d.ts +2 -0
  112. package/dist/__tests__/tools/search-handlers.test.d.ts.map +1 -0
  113. package/dist/__tests__/tools/search-handlers.test.js +229 -0
  114. package/dist/__tests__/tools/search-handlers.test.js.map +1 -0
  115. package/dist/__tests__/tools/symbol-handlers.test.d.ts +2 -0
  116. package/dist/__tests__/tools/symbol-handlers.test.d.ts.map +1 -0
  117. package/dist/__tests__/tools/symbol-handlers.test.js +348 -0
  118. package/dist/__tests__/tools/symbol-handlers.test.js.map +1 -0
  119. package/dist/__tests__/tools/worktree-handlers.test.d.ts +2 -0
  120. package/dist/__tests__/tools/worktree-handlers.test.d.ts.map +1 -0
  121. package/dist/__tests__/tools/worktree-handlers.test.js +297 -0
  122. package/dist/__tests__/tools/worktree-handlers.test.js.map +1 -0
  123. package/dist/__tests__/utils/cache.test.d.ts +2 -0
  124. package/dist/__tests__/utils/cache.test.d.ts.map +1 -0
  125. package/dist/__tests__/utils/cache.test.js +147 -0
  126. package/dist/__tests__/utils/cache.test.js.map +1 -0
  127. package/dist/__tests__/utils/concurrency.test.d.ts +2 -0
  128. package/dist/__tests__/utils/concurrency.test.d.ts.map +1 -0
  129. package/dist/__tests__/utils/concurrency.test.js +83 -0
  130. package/dist/__tests__/utils/concurrency.test.js.map +1 -0
  131. package/dist/__tests__/utils/errors.test.d.ts +2 -0
  132. package/dist/__tests__/utils/errors.test.d.ts.map +1 -0
  133. package/dist/__tests__/utils/errors.test.js +136 -0
  134. package/dist/__tests__/utils/errors.test.js.map +1 -0
  135. package/dist/__tests__/utils/logger.test.d.ts +2 -0
  136. package/dist/__tests__/utils/logger.test.d.ts.map +1 -0
  137. package/dist/__tests__/utils/logger.test.js +131 -0
  138. package/dist/__tests__/utils/logger.test.js.map +1 -0
  139. package/dist/__tests__/utils/type-guards.test.d.ts +2 -0
  140. package/dist/__tests__/utils/type-guards.test.d.ts.map +1 -0
  141. package/dist/__tests__/utils/type-guards.test.js +80 -0
  142. package/dist/__tests__/utils/type-guards.test.js.map +1 -0
  143. package/dist/__tests__/worktree/worktree-manager.test.d.ts +2 -0
  144. package/dist/__tests__/worktree/worktree-manager.test.d.ts.map +1 -0
  145. package/dist/__tests__/worktree/worktree-manager.test.js +403 -0
  146. package/dist/__tests__/worktree/worktree-manager.test.js.map +1 -0
  147. package/dist/config.d.ts +191 -0
  148. package/dist/config.d.ts.map +1 -0
  149. package/dist/config.js +788 -0
  150. package/dist/config.js.map +1 -0
  151. package/dist/dashboard/beads.d.ts +35 -0
  152. package/dist/dashboard/beads.d.ts.map +1 -0
  153. package/dist/dashboard/beads.js +102 -0
  154. package/dist/dashboard/beads.js.map +1 -0
  155. package/dist/dashboard/events.d.ts +64 -0
  156. package/dist/dashboard/events.d.ts.map +1 -0
  157. package/dist/dashboard/events.js +211 -0
  158. package/dist/dashboard/events.js.map +1 -0
  159. package/dist/dashboard/index.d.ts +69 -0
  160. package/dist/dashboard/index.d.ts.map +1 -0
  161. package/dist/dashboard/index.js +93 -0
  162. package/dist/dashboard/index.js.map +1 -0
  163. package/dist/dashboard/routes.d.ts +6 -0
  164. package/dist/dashboard/routes.d.ts.map +1 -0
  165. package/dist/dashboard/routes.js +505 -0
  166. package/dist/dashboard/routes.js.map +1 -0
  167. package/dist/dashboard/server.d.ts +27 -0
  168. package/dist/dashboard/server.d.ts.map +1 -0
  169. package/dist/dashboard/server.js +72 -0
  170. package/dist/dashboard/server.js.map +1 -0
  171. package/dist/dashboard/state.d.ts +172 -0
  172. package/dist/dashboard/state.d.ts.map +1 -0
  173. package/dist/dashboard/state.js +362 -0
  174. package/dist/dashboard/state.js.map +1 -0
  175. package/dist/dashboard/token-tracking.d.ts +143 -0
  176. package/dist/dashboard/token-tracking.d.ts.map +1 -0
  177. package/dist/dashboard/token-tracking.js +363 -0
  178. package/dist/dashboard/token-tracking.js.map +1 -0
  179. package/dist/dashboard/ui.d.ts +6 -0
  180. package/dist/dashboard/ui.d.ts.map +1 -0
  181. package/dist/dashboard/ui.js +2710 -0
  182. package/dist/dashboard/ui.js.map +1 -0
  183. package/dist/embeddings/gemini.d.ts +28 -0
  184. package/dist/embeddings/gemini.d.ts.map +1 -0
  185. package/dist/embeddings/gemini.js +165 -0
  186. package/dist/embeddings/gemini.js.map +1 -0
  187. package/dist/embeddings/index.d.ts +34 -0
  188. package/dist/embeddings/index.d.ts.map +1 -0
  189. package/dist/embeddings/index.js +120 -0
  190. package/dist/embeddings/index.js.map +1 -0
  191. package/dist/embeddings/ollama.d.ts +22 -0
  192. package/dist/embeddings/ollama.d.ts.map +1 -0
  193. package/dist/embeddings/ollama.js +179 -0
  194. package/dist/embeddings/ollama.js.map +1 -0
  195. package/dist/embeddings/rate-limiter.d.ts +75 -0
  196. package/dist/embeddings/rate-limiter.d.ts.map +1 -0
  197. package/dist/embeddings/rate-limiter.js +145 -0
  198. package/dist/embeddings/rate-limiter.js.map +1 -0
  199. package/dist/embeddings/retry.d.ts +20 -0
  200. package/dist/embeddings/retry.d.ts.map +1 -0
  201. package/dist/embeddings/retry.js +227 -0
  202. package/dist/embeddings/retry.js.map +1 -0
  203. package/dist/embeddings/types.d.ts +103 -0
  204. package/dist/embeddings/types.d.ts.map +1 -0
  205. package/dist/embeddings/types.js +23 -0
  206. package/dist/embeddings/types.js.map +1 -0
  207. package/dist/index.d.ts +12 -0
  208. package/dist/index.d.ts.map +1 -0
  209. package/dist/index.js +2028 -0
  210. package/dist/index.js.map +1 -0
  211. package/dist/memory/index.d.ts +63 -0
  212. package/dist/memory/index.d.ts.map +1 -0
  213. package/dist/memory/index.js +168 -0
  214. package/dist/memory/index.js.map +1 -0
  215. package/dist/search/ast-chunker.d.ts +29 -0
  216. package/dist/search/ast-chunker.d.ts.map +1 -0
  217. package/dist/search/ast-chunker.js +236 -0
  218. package/dist/search/ast-chunker.js.map +1 -0
  219. package/dist/search/chunk-utils.d.ts +24 -0
  220. package/dist/search/chunk-utils.d.ts.map +1 -0
  221. package/dist/search/chunk-utils.js +41 -0
  222. package/dist/search/chunk-utils.js.map +1 -0
  223. package/dist/search/clustering.d.ts +77 -0
  224. package/dist/search/clustering.d.ts.map +1 -0
  225. package/dist/search/clustering.js +455 -0
  226. package/dist/search/clustering.js.map +1 -0
  227. package/dist/search/indexer.d.ts +377 -0
  228. package/dist/search/indexer.d.ts.map +1 -0
  229. package/dist/search/indexer.js +1557 -0
  230. package/dist/search/indexer.js.map +1 -0
  231. package/dist/search/tree-sitter-chunker.d.ts +64 -0
  232. package/dist/search/tree-sitter-chunker.d.ts.map +1 -0
  233. package/dist/search/tree-sitter-chunker.js +412 -0
  234. package/dist/search/tree-sitter-chunker.js.map +1 -0
  235. package/dist/symbols/index.d.ts +14 -0
  236. package/dist/symbols/index.d.ts.map +1 -0
  237. package/dist/symbols/index.js +19 -0
  238. package/dist/symbols/index.js.map +1 -0
  239. package/dist/symbols/name-path.d.ts +113 -0
  240. package/dist/symbols/name-path.d.ts.map +1 -0
  241. package/dist/symbols/name-path.js +194 -0
  242. package/dist/symbols/name-path.js.map +1 -0
  243. package/dist/symbols/pattern-search.d.ts +14 -0
  244. package/dist/symbols/pattern-search.d.ts.map +1 -0
  245. package/dist/symbols/pattern-search.js +224 -0
  246. package/dist/symbols/pattern-search.js.map +1 -0
  247. package/dist/symbols/reference-finder.d.ts +38 -0
  248. package/dist/symbols/reference-finder.d.ts.map +1 -0
  249. package/dist/symbols/reference-finder.js +376 -0
  250. package/dist/symbols/reference-finder.js.map +1 -0
  251. package/dist/symbols/symbol-editor.d.ts +81 -0
  252. package/dist/symbols/symbol-editor.d.ts.map +1 -0
  253. package/dist/symbols/symbol-editor.js +257 -0
  254. package/dist/symbols/symbol-editor.js.map +1 -0
  255. package/dist/symbols/symbol-extractor.d.ts +49 -0
  256. package/dist/symbols/symbol-extractor.d.ts.map +1 -0
  257. package/dist/symbols/symbol-extractor.js +593 -0
  258. package/dist/symbols/symbol-extractor.js.map +1 -0
  259. package/dist/symbols/symbol-renamer.d.ts +81 -0
  260. package/dist/symbols/symbol-renamer.d.ts.map +1 -0
  261. package/dist/symbols/symbol-renamer.js +204 -0
  262. package/dist/symbols/symbol-renamer.js.map +1 -0
  263. package/dist/symbols/types.d.ts +234 -0
  264. package/dist/symbols/types.d.ts.map +1 -0
  265. package/dist/symbols/types.js +106 -0
  266. package/dist/symbols/types.js.map +1 -0
  267. package/dist/tools/clustering-handlers.d.ts +70 -0
  268. package/dist/tools/clustering-handlers.d.ts.map +1 -0
  269. package/dist/tools/clustering-handlers.js +150 -0
  270. package/dist/tools/clustering-handlers.js.map +1 -0
  271. package/dist/tools/commit-handlers.d.ts +77 -0
  272. package/dist/tools/commit-handlers.d.ts.map +1 -0
  273. package/dist/tools/commit-handlers.js +251 -0
  274. package/dist/tools/commit-handlers.js.map +1 -0
  275. package/dist/tools/index-handlers.d.ts +31 -0
  276. package/dist/tools/index-handlers.d.ts.map +1 -0
  277. package/dist/tools/index-handlers.js +53 -0
  278. package/dist/tools/index-handlers.js.map +1 -0
  279. package/dist/tools/instructions-handlers.d.ts +25 -0
  280. package/dist/tools/instructions-handlers.d.ts.map +1 -0
  281. package/dist/tools/instructions-handlers.js +36 -0
  282. package/dist/tools/instructions-handlers.js.map +1 -0
  283. package/dist/tools/memory-handlers.d.ts +95 -0
  284. package/dist/tools/memory-handlers.d.ts.map +1 -0
  285. package/dist/tools/memory-handlers.js +126 -0
  286. package/dist/tools/memory-handlers.js.map +1 -0
  287. package/dist/tools/search-handlers.d.ts +51 -0
  288. package/dist/tools/search-handlers.d.ts.map +1 -0
  289. package/dist/tools/search-handlers.js +128 -0
  290. package/dist/tools/search-handlers.js.map +1 -0
  291. package/dist/tools/symbol-handlers.d.ts +171 -0
  292. package/dist/tools/symbol-handlers.d.ts.map +1 -0
  293. package/dist/tools/symbol-handlers.js +456 -0
  294. package/dist/tools/symbol-handlers.js.map +1 -0
  295. package/dist/tools/types.d.ts +32 -0
  296. package/dist/tools/types.d.ts.map +1 -0
  297. package/dist/tools/types.js +17 -0
  298. package/dist/tools/types.js.map +1 -0
  299. package/dist/tools/worktree-handlers.d.ts +96 -0
  300. package/dist/tools/worktree-handlers.d.ts.map +1 -0
  301. package/dist/tools/worktree-handlers.js +186 -0
  302. package/dist/tools/worktree-handlers.js.map +1 -0
  303. package/dist/utils/cache.d.ts +77 -0
  304. package/dist/utils/cache.d.ts.map +1 -0
  305. package/dist/utils/cache.js +134 -0
  306. package/dist/utils/cache.js.map +1 -0
  307. package/dist/utils/concurrency.d.ts +32 -0
  308. package/dist/utils/concurrency.d.ts.map +1 -0
  309. package/dist/utils/concurrency.js +57 -0
  310. package/dist/utils/concurrency.js.map +1 -0
  311. package/dist/utils/errors.d.ts +36 -0
  312. package/dist/utils/errors.d.ts.map +1 -0
  313. package/dist/utils/errors.js +91 -0
  314. package/dist/utils/errors.js.map +1 -0
  315. package/dist/utils/logger.d.ts +37 -0
  316. package/dist/utils/logger.d.ts.map +1 -0
  317. package/dist/utils/logger.js +114 -0
  318. package/dist/utils/logger.js.map +1 -0
  319. package/dist/utils/type-guards.d.ts +17 -0
  320. package/dist/utils/type-guards.d.ts.map +1 -0
  321. package/dist/utils/type-guards.js +25 -0
  322. package/dist/utils/type-guards.js.map +1 -0
  323. package/dist/worktree/index.d.ts +6 -0
  324. package/dist/worktree/index.d.ts.map +1 -0
  325. package/dist/worktree/index.js +6 -0
  326. package/dist/worktree/index.js.map +1 -0
  327. package/dist/worktree/types.d.ts +101 -0
  328. package/dist/worktree/types.d.ts.map +1 -0
  329. package/dist/worktree/types.js +6 -0
  330. package/dist/worktree/types.js.map +1 -0
  331. package/dist/worktree/worktree-manager.d.ts +80 -0
  332. package/dist/worktree/worktree-manager.d.ts.map +1 -0
  333. package/dist/worktree/worktree-manager.js +407 -0
  334. package/dist/worktree/worktree-manager.js.map +1 -0
  335. package/package.json +87 -0
  336. package/scripts/postinstall.js +48 -0
package/dist/index.js ADDED
@@ -0,0 +1,2028 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import { exec, spawn } from 'child_process';
6
+ import { promisify } from 'util';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ const execAsync = promisify(exec);
10
+ /**
11
+ * Execute a git command safely using spawn with array arguments.
12
+ * Avoids shell interpolation vulnerabilities.
13
+ * @param args Array of arguments to pass to git
14
+ * @param options Options including cwd and optional stdin
15
+ * @returns Promise with stdout and stderr
16
+ */
17
+ function gitSpawn(args, options) {
18
+ return new Promise((resolve, reject) => {
19
+ const proc = spawn('git', args, {
20
+ cwd: options.cwd,
21
+ stdio: ['pipe', 'pipe', 'pipe'],
22
+ });
23
+ let stdout = '';
24
+ let stderr = '';
25
+ proc.stdout.on('data', (data) => {
26
+ stdout += data.toString();
27
+ });
28
+ proc.stderr.on('data', (data) => {
29
+ stderr += data.toString();
30
+ });
31
+ proc.on('error', (err) => {
32
+ reject(err);
33
+ });
34
+ proc.on('close', (code) => {
35
+ if (code === 0) {
36
+ resolve({ stdout, stderr });
37
+ }
38
+ else {
39
+ const error = new Error(`git ${args[0]} failed with code ${code}: ${stderr}`);
40
+ error.code = code;
41
+ error.stdout = stdout;
42
+ error.stderr = stderr;
43
+ reject(error);
44
+ }
45
+ });
46
+ // Write stdin if provided (for commit message)
47
+ if (options.stdin !== undefined) {
48
+ proc.stdin.write(options.stdin);
49
+ proc.stdin.end();
50
+ }
51
+ else {
52
+ proc.stdin.end();
53
+ }
54
+ });
55
+ }
56
+ import { createRequire } from 'module';
57
+ const require = createRequire(import.meta.url);
58
+ const packageJson = require('../package.json');
59
+ import { createEmbeddingBackend } from './embeddings/index.js';
60
+ import { CodeIndexer } from './search/indexer.js';
61
+ import { isStringArray, isString, isNumber, isBoolean } from './utils/type-guards.js';
62
+ import { logError, formatErrorResponse, wrapError, GlanceyError } from './utils/errors.js';
63
+ import { logger } from './utils/logger.js';
64
+ import { loadConfig, loadSecrets, getInstructions, getDashboardConfig } from './config.js';
65
+ import { startDashboard, stopDashboard, dashboardState, findAvailablePort, isDashboardRunning, getDashboardUrl, } from './dashboard/index.js';
66
+ // Symbolic analysis imports
67
+ import { searchForPattern, formatPatternSearchResults, ReferenceFinder, formatReferencesResult, SymbolEditor, SymbolRenamer, formatRenameResult, } from './symbols/index.js';
68
+ import { MemoryManager, formatMemoryList } from './memory/index.js';
69
+ import { WorktreeManager, formatWorktreeInfo, formatWorktreeList } from './worktree/index.js';
70
+ // Tool handlers for token tracking
71
+ import { handleSearchCode, parseSearchCodeArgs, handleSearchSimilar, parseSearchSimilarArgs, } from './tools/search-handlers.js';
72
+ import { handleGetSymbolsOverview, parseGetSymbolsOverviewArgs, handleFindSymbol, parseFindSymbolArgs, } from './tools/symbol-handlers.js';
73
+ import { handleSummarizeCodebase, parseSummarizeCodebaseArgs, handleListConcepts, parseListConceptsArgs, handleSearchByConcept, parseSearchByConceptArgs, } from './tools/clustering-handlers.js';
74
+ /**
75
+ * Check if browser was recently opened (within the last hour)
76
+ */
77
+ function wasBrowserRecentlyOpened(projectPath) {
78
+ const flagFile = path.join(projectPath, '.glancey', 'browser-opened');
79
+ try {
80
+ if (fs.existsSync(flagFile)) {
81
+ const timestamp = parseInt(fs.readFileSync(flagFile, 'utf-8'), 10);
82
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
83
+ return timestamp > oneHourAgo;
84
+ }
85
+ }
86
+ catch {
87
+ // Ignore errors
88
+ }
89
+ return false;
90
+ }
91
+ /**
92
+ * Record that browser was opened
93
+ */
94
+ function recordBrowserOpened(projectPath) {
95
+ const flagFile = path.join(projectPath, '.glancey', 'browser-opened');
96
+ try {
97
+ fs.writeFileSync(flagFile, Date.now().toString());
98
+ }
99
+ catch {
100
+ // Ignore errors
101
+ }
102
+ }
103
+ /**
104
+ * Open a URL in the user's default browser (cross-platform)
105
+ */
106
+ function openBrowser(url, projectPath, force = false) {
107
+ // Don't open if already opened recently (unless forced)
108
+ if (!force && wasBrowserRecentlyOpened(projectPath)) {
109
+ console.error('[glancey] Dashboard was recently opened, skipping');
110
+ return;
111
+ }
112
+ const platform = process.platform;
113
+ let command;
114
+ switch (platform) {
115
+ case 'darwin':
116
+ command = `open "${url}"`;
117
+ break;
118
+ case 'win32':
119
+ command = `start "" "${url}"`;
120
+ break;
121
+ default:
122
+ // Linux and others
123
+ command = `xdg-open "${url}"`;
124
+ }
125
+ console.error(`[glancey] Opening browser with command: ${command}`);
126
+ exec(command, (error) => {
127
+ if (error) {
128
+ console.error('[glancey] Failed to open browser:', error.message);
129
+ }
130
+ else {
131
+ console.error('[glancey] Browser opened successfully');
132
+ recordBrowserOpened(projectPath);
133
+ }
134
+ });
135
+ }
136
+ const PROJECT_PATH = path.resolve(process.env.GLANCEY_PROJECT || process.cwd());
137
+ /**
138
+ * Brief guidance appended to tool responses to reinforce tool selection preferences.
139
+ */
140
+ const TOOL_GUIDANCE = `
141
+
142
+ ---
143
+ **Tip:** New to this codebase? Start with \`summarize_codebase\` → \`list_concepts\`. For code search, prefer \`search_code\` over grep/find.`;
144
+ /**
145
+ * Server instructions provided at MCP initialization.
146
+ * These guide Claude on when to use glancey tools vs alternatives.
147
+ */
148
+ const SERVER_INSTRUCTIONS = `# glancey - Semantic Code Search & Codebase Understanding
149
+
150
+ ## Workflow Triggers - When to Use Each Tool
151
+
152
+ ### 🚀 Starting Work on a New Codebase
153
+ 1. **\`summarize_codebase\`** - Get the big picture: file stats, languages, concept areas
154
+ 2. **\`list_concepts\`** - Discover semantic groupings (auth, database, API handlers, etc.)
155
+ 3. **\`search_by_concept\`** - Explore specific concept areas in depth
156
+
157
+ ### 🔍 Finding Code (Use BEFORE grep/pattern search)
158
+ - **\`search_code\`** - Natural language search. Use when:
159
+ - You're unsure of exact names ("how does auth work?")
160
+ - You would use wildcards or regex
161
+ - Your first search attempt failed
162
+ - Searching by concept, not exact identifier
163
+
164
+ ### 🔁 Refactoring or Looking for Duplication
165
+ - **\`search_similar\`** - Find semantically similar code. Use when:
166
+ - Looking for duplicate logic to consolidate
167
+ - Finding similar implementations across the codebase
168
+ - Checking if a pattern exists elsewhere before creating it
169
+
170
+ ### 📝 Committing Changes
171
+ - **\`commit\`** - **ALWAYS use instead of \`git commit\`** (MANDATORY)
172
+
173
+ ### 🌳 Parallel Development
174
+ - **\`create_worktree\`** - Create isolated worktree for parallel agent work
175
+
176
+ ## Tool Reference
177
+
178
+ ### Semantic Search (glancey exclusive)
179
+ | Tool | When to Use |
180
+ |------|-------------|
181
+ | \`search_code\` | Finding code by concept, uncertain of exact names |
182
+ | \`search_similar\` | Finding duplicate/related patterns for refactoring |
183
+ | \`summarize_codebase\` | **First thing** when exploring a new codebase |
184
+ | \`list_concepts\` | Understanding codebase organization and themes |
185
+ | \`search_by_concept\` | Deep-diving into a specific area |
186
+
187
+ ### Git & Index
188
+ | Tool | When to Use |
189
+ |------|-------------|
190
+ | \`commit\` | **ALWAYS** instead of raw \`git commit\` |
191
+ | \`index_codebase\` | After major file changes, or if search seems stale |
192
+ | \`get_index_status\` | Check if reindexing is needed |
193
+
194
+ ### Worktrees
195
+ | Tool | When to Use |
196
+ |------|-------------|
197
+ | \`create_worktree\` | Starting parallel work that needs isolation |
198
+ | \`list_worktrees\` | See active worktrees |
199
+ | \`worktree_status\` | Check a worktree's git state |
200
+ | \`remove_worktree\` | Clean up after parallel work |
201
+
202
+ ## CRITICAL: Always Use the \`commit\` Tool
203
+
204
+ **NEVER use raw \`git commit\`** - ALWAYS use the \`commit\` MCP tool.
205
+
206
+ The \`commit\` tool:
207
+ - Validates you're on a feature branch (not main)
208
+ - Checks message format (≤72 chars, imperative mood)
209
+ - Enforces single responsibility per commit
210
+ - Prevents common mistakes
211
+
212
+ A post-commit hook will warn if commits bypass this tool.
213
+
214
+ ## Signs You Should Have Used glancey Tools
215
+
216
+ **Use \`search_code\` if you:**
217
+ - Used wildcards or regex alternation
218
+ - Made multiple search attempts to find something
219
+ - Pattern-based search returned nothing
220
+ - Were searching for a concept, not an exact identifier
221
+
222
+ **Use \`summarize_codebase\` + \`list_concepts\` if you:**
223
+ - Are starting work on an unfamiliar codebase
224
+ - Need to understand the overall architecture
225
+ - Want to find where certain functionality lives
226
+
227
+ **Use \`search_similar\` if you:**
228
+ - Are about to write code and want to check for existing similar patterns
229
+ - Are refactoring and looking for duplicate logic
230
+ - Want to understand how similar problems were solved elsewhere`;
231
+ /**
232
+ * Priority instructions returned by get_project_instructions
233
+ */
234
+ const PRIORITY_INSTRUCTIONS = `
235
+ ## Workflow-Based Tool Selection
236
+
237
+ ### 🚀 New to this codebase? Start here:
238
+ 1. **summarize_codebase** → Get overview: file stats, languages, concept areas
239
+ 2. **list_concepts** → See semantic groupings (auth, database, API, etc.)
240
+ 3. **search_by_concept** → Explore a specific concept area
241
+
242
+ ### 🔍 Finding code? Use semantic search:
243
+ - **search_code** - Natural language search (preferred over grep/find)
244
+ - **search_similar** - Find duplicate or related patterns
245
+
246
+ ### 📝 Committing? Use the commit tool:
247
+ - **commit** - **ALWAYS use instead of \`git commit\`** (MANDATORY)
248
+ - Validates feature branch, message format, single responsibility
249
+
250
+ ### ⚠️ Signs you should have used glancey:
251
+ - You used wildcards or regex alternation
252
+ - Multiple search attempts to find something
253
+ - Pattern-based search returned nothing
254
+ - You're searching by concept, not exact identifier
255
+ - You're exploring an unfamiliar codebase
256
+
257
+ `;
258
+ // Package version - read from package.json
259
+ const PACKAGE_VERSION = packageJson.version;
260
+ /**
261
+ * Check for updates from npm registry (non-blocking).
262
+ * Logs a warning if a newer version is available.
263
+ */
264
+ async function checkForUpdates() {
265
+ try {
266
+ // Use npm view command to get latest version
267
+ const { stdout } = await execAsync('npm view glancey version 2>/dev/null', {
268
+ timeout: 5000, // 5 second timeout
269
+ });
270
+ const latestVersion = stdout.trim();
271
+ if (latestVersion && latestVersion !== PACKAGE_VERSION) {
272
+ // Simple semver comparison: split and compare major.minor.patch
273
+ const current = PACKAGE_VERSION.split('.').map(Number);
274
+ const latest = latestVersion.split('.').map(Number);
275
+ const isOutdated = latest[0] > current[0] ||
276
+ (latest[0] === current[0] && latest[1] > current[1]) ||
277
+ (latest[0] === current[0] && latest[1] === current[1] && latest[2] > current[2]);
278
+ if (isOutdated) {
279
+ logger.warn(`Update available: ${PACKAGE_VERSION} → ${latestVersion}`, 'version');
280
+ logger.warn('Run: npx glancey@latest (or npm update -g glancey)', 'version');
281
+ }
282
+ }
283
+ }
284
+ catch {
285
+ // Silently ignore update check failures (network issues, npm not available, etc.)
286
+ }
287
+ }
288
+ let indexerPromise = null;
289
+ let configPromise = null;
290
+ async function getConfig() {
291
+ if (!configPromise) {
292
+ configPromise = loadConfig(PROJECT_PATH);
293
+ }
294
+ return configPromise;
295
+ }
296
+ async function getIndexer() {
297
+ if (!indexerPromise) {
298
+ indexerPromise = (async () => {
299
+ // Load config and secrets to configure embedding backend
300
+ const config = await getConfig();
301
+ const secrets = await loadSecrets(PROJECT_PATH);
302
+ // Determine API key based on configured backend
303
+ const configuredBackend = config.embedding?.backend;
304
+ let apiKey;
305
+ if (configuredBackend === 'gemini') {
306
+ apiKey = secrets.geminiApiKey || process.env.GEMINI_API_KEY;
307
+ }
308
+ else {
309
+ // For auto-selection, use Gemini key if available
310
+ apiKey = secrets.geminiApiKey || process.env.GEMINI_API_KEY;
311
+ }
312
+ const { backend, fallback } = await createEmbeddingBackend({
313
+ backend: configuredBackend,
314
+ apiKey,
315
+ // Note: Don't pass indexing.batchSize here - that's for progress reporting batches.
316
+ // Ollama backend has its own DEFAULT_BATCH_SIZE (100) for API request batching.
317
+ concurrency: config.embedding?.ollamaConcurrency,
318
+ });
319
+ const idx = new CodeIndexer(PROJECT_PATH, backend);
320
+ await idx.initialize();
321
+ // Share indexer and config with dashboard state
322
+ dashboardState.setIndexer(idx);
323
+ dashboardState.setConfig(config);
324
+ dashboardState.setProjectPath(PROJECT_PATH);
325
+ // Track backend fallback if it occurred, or clear if successful
326
+ if (fallback) {
327
+ dashboardState.setBackendFallback(fallback);
328
+ }
329
+ else {
330
+ dashboardState.clearBackendFallback();
331
+ }
332
+ return idx;
333
+ })();
334
+ }
335
+ return indexerPromise;
336
+ }
337
+ /**
338
+ * Config file names to watch for changes
339
+ */
340
+ const CONFIG_FILES_TO_WATCH = ['.glancey.json', 'lance-context.config.json', '.glancey.local.json'];
341
+ /**
342
+ * Invalidate config and indexer caches to force reload on next access.
343
+ * Call this when config files change.
344
+ */
345
+ export function invalidateCaches() {
346
+ configPromise = null;
347
+ indexerPromise = null;
348
+ console.error('[glancey] Config caches invalidated - will reload on next operation');
349
+ }
350
+ /**
351
+ * Reload config and reinitialize the indexer.
352
+ * Returns true if reload was successful, false otherwise.
353
+ */
354
+ export async function reloadConfig() {
355
+ try {
356
+ // Clear caches
357
+ invalidateCaches();
358
+ // Reload config and indexer
359
+ const config = await getConfig();
360
+ const indexer = await getIndexer();
361
+ // Update dashboard state with new config/indexer
362
+ dashboardState.setConfig(config);
363
+ dashboardState.setIndexer(indexer);
364
+ console.error('[glancey] Config reloaded successfully');
365
+ // Check if reindex is needed due to backend change
366
+ const status = await indexer.getStatus();
367
+ if (status.backendMismatch) {
368
+ console.error(`[glancey] ${status.backendMismatchReason}`);
369
+ console.error('[glancey] Starting automatic reindex with new backend...');
370
+ dashboardState.onIndexingStart();
371
+ indexer
372
+ .indexCodebase(undefined, undefined, true, (progress) => {
373
+ dashboardState.onProgress(progress);
374
+ })
375
+ .then((result) => {
376
+ dashboardState.onIndexingComplete(result);
377
+ console.error(`[glancey] Reindex complete: ${result.filesIndexed} files, ${result.chunksCreated} chunks`);
378
+ })
379
+ .catch((error) => {
380
+ console.error('[glancey] Reindex failed:', error);
381
+ });
382
+ }
383
+ return true;
384
+ }
385
+ catch (error) {
386
+ console.error('[glancey] Failed to reload config:', error);
387
+ return false;
388
+ }
389
+ }
390
+ /**
391
+ * Set up file watchers for config files.
392
+ * When config files change, automatically reload the config.
393
+ */
394
+ function watchConfigFiles() {
395
+ // Debounce to avoid multiple reloads for rapid changes
396
+ let reloadTimeout = null;
397
+ const debounceMs = 500;
398
+ const scheduleReload = (filename) => {
399
+ if (reloadTimeout) {
400
+ clearTimeout(reloadTimeout);
401
+ }
402
+ reloadTimeout = setTimeout(async () => {
403
+ console.error(`[glancey] Config file changed: ${filename}`);
404
+ await reloadConfig();
405
+ }, debounceMs);
406
+ };
407
+ for (const configFile of CONFIG_FILES_TO_WATCH) {
408
+ const configPath = path.join(PROJECT_PATH, configFile);
409
+ try {
410
+ // Check if file exists before watching
411
+ if (fs.existsSync(configPath)) {
412
+ fs.watch(configPath, (eventType) => {
413
+ if (eventType === 'change') {
414
+ scheduleReload(configFile);
415
+ }
416
+ });
417
+ console.error(`[glancey] Watching config file: ${configFile}`);
418
+ }
419
+ }
420
+ catch {
421
+ // File doesn't exist or can't be watched, skip silently
422
+ }
423
+ }
424
+ // Also watch for new config files being created
425
+ try {
426
+ fs.watch(PROJECT_PATH, (eventType, filename) => {
427
+ if (filename && CONFIG_FILES_TO_WATCH.includes(filename) && eventType === 'rename') {
428
+ const configPath = path.join(PROJECT_PATH, filename);
429
+ if (fs.existsSync(configPath)) {
430
+ console.error(`[glancey] New config file detected: ${filename}`);
431
+ // Set up watcher for the new file
432
+ fs.watch(configPath, (evt) => {
433
+ if (evt === 'change') {
434
+ scheduleReload(filename);
435
+ }
436
+ });
437
+ scheduleReload(filename);
438
+ }
439
+ }
440
+ });
441
+ }
442
+ catch {
443
+ // Can't watch project directory, skip silently
444
+ }
445
+ }
446
+ /**
447
+ * Claude settings file paths
448
+ */
449
+ const CLAUDE_SETTINGS_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'settings.json');
450
+ /**
451
+ * Serena plugin identifier in Claude settings
452
+ */
453
+ const SERENA_PLUGIN_ID = 'serena@claude-plugins-official';
454
+ /**
455
+ * Disable Serena plugin in Claude settings and kill any running Serena processes.
456
+ * This allows lance-context to replace Serena as the primary code analysis tool.
457
+ */
458
+ async function disableSerena() {
459
+ // Step 1: Modify Claude settings to disable Serena
460
+ try {
461
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
462
+ const settingsContent = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf-8');
463
+ const settings = JSON.parse(settingsContent);
464
+ if (settings.enabledPlugins && settings.enabledPlugins[SERENA_PLUGIN_ID] === true) {
465
+ settings.enabledPlugins[SERENA_PLUGIN_ID] = false;
466
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
467
+ console.error('[glancey] Disabled Serena plugin in Claude settings');
468
+ }
469
+ }
470
+ }
471
+ catch (error) {
472
+ console.error('[glancey] Failed to disable Serena in settings:', error);
473
+ }
474
+ // Step 2: Kill any running Serena processes
475
+ try {
476
+ // Find and kill serena processes (works on macOS/Linux)
477
+ const { stdout } = await execAsync('pgrep -f "serena" 2>/dev/null || true');
478
+ const pids = stdout.trim().split('\n').filter(Boolean);
479
+ for (const pid of pids) {
480
+ // Don't kill our own process
481
+ if (pid !== String(process.pid)) {
482
+ try {
483
+ await execAsync(`kill ${pid} 2>/dev/null || true`);
484
+ console.error(`[glancey] Killed Serena process ${pid}`);
485
+ }
486
+ catch {
487
+ // Process may have already exited
488
+ }
489
+ }
490
+ }
491
+ }
492
+ catch {
493
+ // pgrep not available or no processes found, silently continue
494
+ }
495
+ }
496
+ const server = new Server({
497
+ name: 'glancey',
498
+ version: PACKAGE_VERSION,
499
+ }, {
500
+ capabilities: {
501
+ tools: {},
502
+ },
503
+ instructions: SERVER_INSTRUCTIONS,
504
+ });
505
+ // List available tools
506
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
507
+ return {
508
+ tools: [
509
+ {
510
+ name: 'index_codebase',
511
+ description: 'Index the codebase for semantic search. Creates vector embeddings of all code files. Supports incremental indexing - only changed files are re-indexed unless forceReindex is true.',
512
+ inputSchema: {
513
+ type: 'object',
514
+ properties: {
515
+ patterns: {
516
+ type: 'array',
517
+ items: { type: 'string' },
518
+ description: 'Glob patterns for files to index (default: common code files)',
519
+ },
520
+ excludePatterns: {
521
+ type: 'array',
522
+ items: { type: 'string' },
523
+ description: 'Glob patterns for files to exclude (default: node_modules, dist, .git)',
524
+ },
525
+ forceReindex: {
526
+ type: 'boolean',
527
+ description: 'Force a full reindex, ignoring cached file modification times (default: false)',
528
+ },
529
+ autoRepair: {
530
+ type: 'boolean',
531
+ description: 'Automatically repair a corrupted index by forcing a full reindex (default: false)',
532
+ },
533
+ },
534
+ },
535
+ },
536
+ {
537
+ name: 'search_code',
538
+ description: 'Search the codebase using natural language. Returns relevant code snippets. PREFER THIS TOOL when: (1) you are unsure of exact symbol/function names, (2) you would need wildcards or regex to find something, (3) you are exploring code by concept rather than exact identifier, (4) your first search attempt failed or returned nothing. Semantic search handles name uncertainty naturally - one call here replaces multiple pattern-based searches.',
539
+ inputSchema: {
540
+ type: 'object',
541
+ properties: {
542
+ query: {
543
+ type: 'string',
544
+ description: 'Natural language query to search for',
545
+ },
546
+ limit: {
547
+ type: 'number',
548
+ description: 'Maximum number of results (default: 10)',
549
+ },
550
+ pathPattern: {
551
+ type: 'string',
552
+ description: "Glob pattern to filter results by file path (e.g., 'src/**/*.ts', '!**/*.test.ts')",
553
+ },
554
+ languages: {
555
+ type: 'array',
556
+ items: { type: 'string' },
557
+ description: "Filter results to specific languages (e.g., ['typescript', 'javascript'])",
558
+ },
559
+ },
560
+ required: ['query'],
561
+ },
562
+ },
563
+ {
564
+ name: 'get_index_status',
565
+ description: 'Get the current status of the code index. USE THIS when search results seem stale or before searching after major file changes.',
566
+ inputSchema: {
567
+ type: 'object',
568
+ properties: {},
569
+ },
570
+ },
571
+ {
572
+ name: 'clear_index',
573
+ description: 'Clear the code index.',
574
+ inputSchema: {
575
+ type: 'object',
576
+ properties: {},
577
+ },
578
+ },
579
+ {
580
+ name: 'get_project_instructions',
581
+ description: 'Get project-specific instructions from the .glancey.json config file. Returns instructions for how to work with this codebase.',
582
+ inputSchema: {
583
+ type: 'object',
584
+ properties: {},
585
+ },
586
+ },
587
+ {
588
+ name: 'search_similar',
589
+ description: 'Find code semantically similar to a given code snippet or file location. USE THIS BEFORE writing new code to check for existing patterns, or when refactoring to find duplicate logic that could be consolidated.',
590
+ inputSchema: {
591
+ type: 'object',
592
+ properties: {
593
+ filepath: {
594
+ type: 'string',
595
+ description: 'File path (relative to project root) to find similar code for. Use with startLine/endLine to specify a range.',
596
+ },
597
+ startLine: {
598
+ type: 'number',
599
+ description: 'Starting line number (1-indexed). Requires filepath.',
600
+ },
601
+ endLine: {
602
+ type: 'number',
603
+ description: 'Ending line number (1-indexed). Requires filepath.',
604
+ },
605
+ code: {
606
+ type: 'string',
607
+ description: 'Code snippet to find similar code for. Alternative to filepath - provide either code or filepath, not both.',
608
+ },
609
+ limit: {
610
+ type: 'number',
611
+ description: 'Maximum number of results (default: 10)',
612
+ },
613
+ threshold: {
614
+ type: 'number',
615
+ description: 'Minimum similarity score 0-1 (default: 0)',
616
+ },
617
+ excludeSelf: {
618
+ type: 'boolean',
619
+ description: 'Exclude the source chunk from results (default: true)',
620
+ },
621
+ },
622
+ },
623
+ },
624
+ {
625
+ name: 'summarize_codebase',
626
+ description: 'USE THIS FIRST when starting work on a new or unfamiliar codebase. Generates a comprehensive summary including file statistics, language distribution, and discovered concept areas. Follow with list_concepts to explore specific areas.',
627
+ inputSchema: {
628
+ type: 'object',
629
+ properties: {
630
+ numClusters: {
631
+ type: 'number',
632
+ description: 'Target number of concept clusters (default: auto-determined based on codebase size)',
633
+ },
634
+ },
635
+ },
636
+ },
637
+ {
638
+ name: 'list_concepts',
639
+ description: 'USE AFTER summarize_codebase to explore the codebase organization. Lists semantic concept clusters (e.g., authentication, database, API handlers) with labels, sizes, and representative code. Use search_by_concept to dive into a specific cluster.',
640
+ inputSchema: {
641
+ type: 'object',
642
+ properties: {
643
+ forceRecluster: {
644
+ type: 'boolean',
645
+ description: 'Force reclustering even if cached results exist (default: false)',
646
+ },
647
+ },
648
+ },
649
+ },
650
+ {
651
+ name: 'search_by_concept',
652
+ description: 'Deep-dive into a specific concept cluster. USE AFTER list_concepts to explore a particular area (e.g., all authentication code, all database code). Can combine with a semantic query to search within the cluster.',
653
+ inputSchema: {
654
+ type: 'object',
655
+ properties: {
656
+ conceptId: {
657
+ type: 'number',
658
+ description: 'The cluster ID to search within (from list_concepts)',
659
+ },
660
+ query: {
661
+ type: 'string',
662
+ description: 'Optional semantic query to search within the cluster. If not provided, returns representative chunks.',
663
+ },
664
+ limit: {
665
+ type: 'number',
666
+ description: 'Maximum number of results (default: 10)',
667
+ },
668
+ },
669
+ required: ['conceptId'],
670
+ },
671
+ },
672
+ {
673
+ name: 'commit',
674
+ description: 'Create a git commit with validation. USE THIS TOOL instead of running git commit directly. This tool enforces project commit rules: (1) validates you are on a feature branch (not main), (2) checks commit message format (<=72 chars, imperative mood, single responsibility), (3) returns commit rules as a reminder. Prevents common mistakes like committing to main or multi-responsibility commits.',
675
+ inputSchema: {
676
+ type: 'object',
677
+ properties: {
678
+ message: {
679
+ type: 'string',
680
+ description: 'The commit message. Must be <=72 characters, imperative mood, single responsibility.',
681
+ },
682
+ files: {
683
+ type: 'array',
684
+ items: { type: 'string' },
685
+ description: 'Files to stage before committing. If not provided, commits already-staged files.',
686
+ },
687
+ },
688
+ required: ['message'],
689
+ },
690
+ },
691
+ // --- Symbolic Analysis Tools ---
692
+ {
693
+ name: 'get_symbols_overview',
694
+ description: "Get a high-level overview of code symbols in a file. Returns symbols grouped by kind (Class, Function, Method, etc.) in a compact format. Use this to understand a file's structure before diving into specific symbols.",
695
+ inputSchema: {
696
+ type: 'object',
697
+ properties: {
698
+ relative_path: {
699
+ type: 'string',
700
+ description: 'The relative path to the file to analyze.',
701
+ },
702
+ depth: {
703
+ type: 'number',
704
+ description: 'Depth of descendants to retrieve (0 = top-level only). Default: 0.',
705
+ },
706
+ max_answer_chars: {
707
+ type: 'number',
708
+ description: 'Maximum response size in characters. Default: 50000.',
709
+ },
710
+ },
711
+ required: ['relative_path'],
712
+ },
713
+ },
714
+ {
715
+ name: 'find_symbol',
716
+ description: 'Find symbols by name path pattern. Supports: (1) simple name "myFunction", (2) relative path "MyClass/myMethod", (3) absolute path "/MyClass/myMethod", (4) glob pattern "get*" with substring_matching. Returns symbol locations and optionally their source code body.',
717
+ inputSchema: {
718
+ type: 'object',
719
+ properties: {
720
+ name_path_pattern: {
721
+ type: 'string',
722
+ description: 'The name path pattern to search for (e.g., "MyClass/myMethod", "get*").',
723
+ },
724
+ relative_path: {
725
+ type: 'string',
726
+ description: 'Optional. Restrict search to this file or directory.',
727
+ },
728
+ depth: {
729
+ type: 'number',
730
+ description: 'Depth of descendants to retrieve (e.g., 1 for class methods). Default: 0.',
731
+ },
732
+ include_body: {
733
+ type: 'boolean',
734
+ description: "Whether to include the symbol's source code. Default: false.",
735
+ },
736
+ include_info: {
737
+ type: 'boolean',
738
+ description: 'Whether to include additional info (docstring, signature). Default: false.',
739
+ },
740
+ substring_matching: {
741
+ type: 'boolean',
742
+ description: 'If true, use substring matching for the last element of the pattern. Default: false.',
743
+ },
744
+ include_kinds: {
745
+ type: 'array',
746
+ items: { type: 'number' },
747
+ description: 'LSP symbol kind integers to include. If not provided, all kinds are included.',
748
+ },
749
+ exclude_kinds: {
750
+ type: 'array',
751
+ items: { type: 'number' },
752
+ description: 'LSP symbol kind integers to exclude. Takes precedence over include_kinds.',
753
+ },
754
+ max_answer_chars: {
755
+ type: 'number',
756
+ description: 'Maximum response size in characters. Default: 50000.',
757
+ },
758
+ },
759
+ required: ['name_path_pattern'],
760
+ },
761
+ },
762
+ {
763
+ name: 'find_referencing_symbols',
764
+ description: 'Find all references to a symbol across the codebase. Returns code snippets showing where the symbol is used.',
765
+ inputSchema: {
766
+ type: 'object',
767
+ properties: {
768
+ name_path: {
769
+ type: 'string',
770
+ description: 'Name path of the symbol to find references for (e.g., "MyClass/myMethod").',
771
+ },
772
+ relative_path: {
773
+ type: 'string',
774
+ description: 'The relative path to the file containing the symbol.',
775
+ },
776
+ include_info: {
777
+ type: 'boolean',
778
+ description: 'Whether to include additional info about referencing symbols. Default: false.',
779
+ },
780
+ include_kinds: {
781
+ type: 'array',
782
+ items: { type: 'number' },
783
+ description: 'LSP symbol kind integers to include.',
784
+ },
785
+ exclude_kinds: {
786
+ type: 'array',
787
+ items: { type: 'number' },
788
+ description: 'LSP symbol kind integers to exclude.',
789
+ },
790
+ max_answer_chars: {
791
+ type: 'number',
792
+ description: 'Maximum response size in characters. Default: 50000.',
793
+ },
794
+ },
795
+ required: ['name_path', 'relative_path'],
796
+ },
797
+ },
798
+ {
799
+ name: 'search_for_pattern',
800
+ description: 'Search for a regex pattern in the codebase. Returns matched lines with optional context. Useful for finding code patterns, TODO comments, specific strings, etc.',
801
+ inputSchema: {
802
+ type: 'object',
803
+ properties: {
804
+ substring_pattern: {
805
+ type: 'string',
806
+ description: 'Regular expression pattern to search for.',
807
+ },
808
+ relative_path: {
809
+ type: 'string',
810
+ description: 'Restrict search to this file or directory. Default: entire project.',
811
+ },
812
+ restrict_search_to_code_files: {
813
+ type: 'boolean',
814
+ description: 'Only search in code files (not config, docs). Default: false.',
815
+ },
816
+ paths_include_glob: {
817
+ type: 'string',
818
+ description: 'Glob pattern for files to include (e.g., "*.py", "src/**/*.ts").',
819
+ },
820
+ paths_exclude_glob: {
821
+ type: 'string',
822
+ description: 'Glob pattern for files to exclude (e.g., "*test*", "**/*_generated.py").',
823
+ },
824
+ context_lines_before: {
825
+ type: 'number',
826
+ description: 'Number of context lines before each match. Default: 0.',
827
+ },
828
+ context_lines_after: {
829
+ type: 'number',
830
+ description: 'Number of context lines after each match. Default: 0.',
831
+ },
832
+ max_answer_chars: {
833
+ type: 'number',
834
+ description: 'Maximum response size in characters. Default: 50000.',
835
+ },
836
+ },
837
+ required: ['substring_pattern'],
838
+ },
839
+ },
840
+ // --- Memory Tools ---
841
+ {
842
+ name: 'write_memory',
843
+ description: 'Write information about this project to a named memory file. Memories persist across sessions and can be read later. Useful for storing architectural decisions, patterns, or project-specific context.',
844
+ inputSchema: {
845
+ type: 'object',
846
+ properties: {
847
+ memory_file_name: {
848
+ type: 'string',
849
+ description: 'The name of the memory (will be saved as .md file).',
850
+ },
851
+ content: {
852
+ type: 'string',
853
+ description: 'The markdown content to write to the memory.',
854
+ },
855
+ max_answer_chars: {
856
+ type: 'number',
857
+ description: 'Maximum response size in characters. Default: 50000.',
858
+ },
859
+ },
860
+ required: ['memory_file_name', 'content'],
861
+ },
862
+ },
863
+ {
864
+ name: 'read_memory',
865
+ description: 'Read the content of a memory file. Only read memories that are relevant to the current task.',
866
+ inputSchema: {
867
+ type: 'object',
868
+ properties: {
869
+ memory_file_name: {
870
+ type: 'string',
871
+ description: 'The name of the memory to read.',
872
+ },
873
+ max_answer_chars: {
874
+ type: 'number',
875
+ description: 'Maximum response size in characters. Default: 50000.',
876
+ },
877
+ },
878
+ required: ['memory_file_name'],
879
+ },
880
+ },
881
+ {
882
+ name: 'list_memories',
883
+ description: 'List all available memory files for this project. Use this to discover what project context has been saved.',
884
+ inputSchema: {
885
+ type: 'object',
886
+ properties: {},
887
+ },
888
+ },
889
+ {
890
+ name: 'delete_memory',
891
+ description: 'Delete a memory file. Only delete memories when explicitly requested by the user.',
892
+ inputSchema: {
893
+ type: 'object',
894
+ properties: {
895
+ memory_file_name: {
896
+ type: 'string',
897
+ description: 'The name of the memory to delete.',
898
+ },
899
+ },
900
+ required: ['memory_file_name'],
901
+ },
902
+ },
903
+ {
904
+ name: 'edit_memory',
905
+ description: 'Edit a memory file using find/replace. Supports both literal string and regex replacement.',
906
+ inputSchema: {
907
+ type: 'object',
908
+ properties: {
909
+ memory_file_name: {
910
+ type: 'string',
911
+ description: 'The name of the memory to edit.',
912
+ },
913
+ needle: {
914
+ type: 'string',
915
+ description: 'The string or regex pattern to search for.',
916
+ },
917
+ repl: {
918
+ type: 'string',
919
+ description: 'The replacement string.',
920
+ },
921
+ mode: {
922
+ type: 'string',
923
+ enum: ['literal', 'regex'],
924
+ description: 'How to interpret the needle: "literal" for exact match, "regex" for regex pattern.',
925
+ },
926
+ },
927
+ required: ['memory_file_name', 'needle', 'repl', 'mode'],
928
+ },
929
+ },
930
+ // --- Symbol Editing Tools ---
931
+ {
932
+ name: 'replace_symbol_body',
933
+ description: 'Replace the entire body of a symbol (function, class, method, etc.) with new content. Use this for significant rewrites of symbol definitions.',
934
+ inputSchema: {
935
+ type: 'object',
936
+ properties: {
937
+ name_path: {
938
+ type: 'string',
939
+ description: 'Name path of the symbol to replace (e.g., "MyClass/myMethod").',
940
+ },
941
+ relative_path: {
942
+ type: 'string',
943
+ description: 'The relative path to the file containing the symbol.',
944
+ },
945
+ body: {
946
+ type: 'string',
947
+ description: 'The new body content for the symbol (including signature line for functions).',
948
+ },
949
+ },
950
+ required: ['name_path', 'relative_path', 'body'],
951
+ },
952
+ },
953
+ {
954
+ name: 'insert_before_symbol',
955
+ description: 'Insert code before a symbol definition. Useful for adding new functions, classes, or imports before an existing symbol.',
956
+ inputSchema: {
957
+ type: 'object',
958
+ properties: {
959
+ name_path: {
960
+ type: 'string',
961
+ description: 'Name path of the symbol to insert before.',
962
+ },
963
+ relative_path: {
964
+ type: 'string',
965
+ description: 'The relative path to the file containing the symbol.',
966
+ },
967
+ body: {
968
+ type: 'string',
969
+ description: 'The content to insert before the symbol.',
970
+ },
971
+ },
972
+ required: ['name_path', 'relative_path', 'body'],
973
+ },
974
+ },
975
+ {
976
+ name: 'insert_after_symbol',
977
+ description: 'Insert code after a symbol definition. Useful for adding new functions, classes, or code after an existing symbol.',
978
+ inputSchema: {
979
+ type: 'object',
980
+ properties: {
981
+ name_path: {
982
+ type: 'string',
983
+ description: 'Name path of the symbol to insert after.',
984
+ },
985
+ relative_path: {
986
+ type: 'string',
987
+ description: 'The relative path to the file containing the symbol.',
988
+ },
989
+ body: {
990
+ type: 'string',
991
+ description: 'The content to insert after the symbol.',
992
+ },
993
+ },
994
+ required: ['name_path', 'relative_path', 'body'],
995
+ },
996
+ },
997
+ {
998
+ name: 'rename_symbol',
999
+ description: 'Rename a symbol throughout the entire codebase. Updates the symbol definition and all references.',
1000
+ inputSchema: {
1001
+ type: 'object',
1002
+ properties: {
1003
+ name_path: {
1004
+ type: 'string',
1005
+ description: 'Name path of the symbol to rename.',
1006
+ },
1007
+ relative_path: {
1008
+ type: 'string',
1009
+ description: 'The relative path to the file containing the symbol definition.',
1010
+ },
1011
+ new_name: {
1012
+ type: 'string',
1013
+ description: 'The new name for the symbol.',
1014
+ },
1015
+ dry_run: {
1016
+ type: 'boolean',
1017
+ description: 'If true, preview changes without making them. Default: false.',
1018
+ },
1019
+ },
1020
+ required: ['name_path', 'relative_path', 'new_name'],
1021
+ },
1022
+ },
1023
+ // --- Worktree Tools ---
1024
+ {
1025
+ name: 'create_worktree',
1026
+ description: 'Create an isolated git worktree for parallel agent work. Prevents file conflicts when multiple agents work simultaneously. Creates a new branch and optionally installs dependencies.',
1027
+ inputSchema: {
1028
+ type: 'object',
1029
+ properties: {
1030
+ short_name: {
1031
+ type: 'string',
1032
+ description: 'Short descriptive name for the worktree (e.g., "add-auth", "fix-login"). Used in branch name.',
1033
+ },
1034
+ issue_id: {
1035
+ type: 'string',
1036
+ description: 'Optional issue ID (e.g., "bd-123"). Combined with short_name for naming.',
1037
+ },
1038
+ prefix: {
1039
+ type: 'string',
1040
+ enum: ['feature', 'fix', 'refactor', 'docs', 'test'],
1041
+ description: 'Branch prefix (default: "feature").',
1042
+ },
1043
+ base_branch: {
1044
+ type: 'string',
1045
+ description: 'Base branch to create from (default: main or current branch).',
1046
+ },
1047
+ install_deps: {
1048
+ type: 'boolean',
1049
+ description: 'Whether to install dependencies after creation (default: true).',
1050
+ },
1051
+ package_manager: {
1052
+ type: 'string',
1053
+ enum: ['npm', 'yarn', 'pnpm', 'bun'],
1054
+ description: 'Package manager to use (default: auto-detect from lock file).',
1055
+ },
1056
+ },
1057
+ required: ['short_name'],
1058
+ },
1059
+ },
1060
+ {
1061
+ name: 'list_worktrees',
1062
+ description: 'List all agent worktrees and their status. Shows branch, commit, dirty state, and ahead/behind counts.',
1063
+ inputSchema: {
1064
+ type: 'object',
1065
+ properties: {},
1066
+ },
1067
+ },
1068
+ {
1069
+ name: 'remove_worktree',
1070
+ description: 'Remove an agent worktree. Optionally deletes the associated branch. Fails if worktree has uncommitted changes unless force is true.',
1071
+ inputSchema: {
1072
+ type: 'object',
1073
+ properties: {
1074
+ name: {
1075
+ type: 'string',
1076
+ description: 'Name of the worktree to remove.',
1077
+ },
1078
+ delete_branch: {
1079
+ type: 'boolean',
1080
+ description: 'Whether to also delete the branch (default: false).',
1081
+ },
1082
+ force: {
1083
+ type: 'boolean',
1084
+ description: 'Force removal even if worktree has uncommitted changes (default: false).',
1085
+ },
1086
+ },
1087
+ required: ['name'],
1088
+ },
1089
+ },
1090
+ {
1091
+ name: 'worktree_status',
1092
+ description: 'Get detailed status of a specific worktree including branch, commit, and dirty state.',
1093
+ inputSchema: {
1094
+ type: 'object',
1095
+ properties: {
1096
+ name: {
1097
+ type: 'string',
1098
+ description: 'Name of the worktree to get status for.',
1099
+ },
1100
+ },
1101
+ required: ['name'],
1102
+ },
1103
+ },
1104
+ // --- Dashboard Tools ---
1105
+ {
1106
+ name: 'open_dashboard',
1107
+ description: 'Open the glancey dashboard in the default browser. Starts the dashboard server if not already running. Works even when dashboard auto-start is disabled in config.',
1108
+ inputSchema: {
1109
+ type: 'object',
1110
+ properties: {
1111
+ force: {
1112
+ type: 'boolean',
1113
+ description: 'Force open the browser even if it was recently opened. Bypasses the 1-hour cooldown.',
1114
+ },
1115
+ },
1116
+ },
1117
+ },
1118
+ ],
1119
+ };
1120
+ });
1121
+ // Handle tool calls
1122
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1123
+ const { name, arguments: args } = request.params;
1124
+ // Record command usage for dashboard
1125
+ const validCommands = [
1126
+ 'index_codebase',
1127
+ 'search_code',
1128
+ 'search_similar',
1129
+ 'get_index_status',
1130
+ 'clear_index',
1131
+ 'get_project_instructions',
1132
+ 'commit',
1133
+ // Symbolic analysis
1134
+ 'get_symbols_overview',
1135
+ 'find_symbol',
1136
+ 'find_referencing_symbols',
1137
+ 'search_for_pattern',
1138
+ 'replace_symbol_body',
1139
+ 'insert_before_symbol',
1140
+ 'insert_after_symbol',
1141
+ 'rename_symbol',
1142
+ // Memory
1143
+ 'write_memory',
1144
+ 'read_memory',
1145
+ 'list_memories',
1146
+ 'delete_memory',
1147
+ 'edit_memory',
1148
+ // Worktree
1149
+ 'create_worktree',
1150
+ 'list_worktrees',
1151
+ 'remove_worktree',
1152
+ 'worktree_status',
1153
+ // Clustering
1154
+ 'list_concepts',
1155
+ 'search_by_concept',
1156
+ 'summarize_codebase',
1157
+ // Dashboard
1158
+ 'open_dashboard',
1159
+ ];
1160
+ if (validCommands.includes(name)) {
1161
+ dashboardState.recordCommandUsage(name);
1162
+ }
1163
+ try {
1164
+ const idx = await getIndexer();
1165
+ switch (name) {
1166
+ case 'index_codebase': {
1167
+ const patterns = isStringArray(args?.patterns) ? args.patterns : undefined;
1168
+ const excludePatterns = isStringArray(args?.excludePatterns)
1169
+ ? args.excludePatterns
1170
+ : undefined;
1171
+ const forceReindex = isBoolean(args?.forceReindex) ? args.forceReindex : false;
1172
+ const autoRepair = isBoolean(args?.autoRepair) ? args.autoRepair : false;
1173
+ // Notify dashboard of indexing start
1174
+ dashboardState.onIndexingStart();
1175
+ const result = await idx.indexCodebase(patterns, excludePatterns, forceReindex, (progress) => {
1176
+ // Emit progress events to dashboard
1177
+ dashboardState.onProgress(progress);
1178
+ }, autoRepair);
1179
+ // Notify dashboard of indexing completion
1180
+ dashboardState.onIndexingComplete(result);
1181
+ const mode = result.repaired
1182
+ ? 'Repaired (corruption detected)'
1183
+ : result.incremental
1184
+ ? 'Incremental update'
1185
+ : 'Full reindex';
1186
+ return {
1187
+ content: [
1188
+ {
1189
+ type: 'text',
1190
+ text: `${mode}: Indexed ${result.filesIndexed} files, total ${result.chunksCreated} chunks.${TOOL_GUIDANCE}`,
1191
+ },
1192
+ ],
1193
+ };
1194
+ }
1195
+ case 'search_code': {
1196
+ const searchContext = {
1197
+ indexer: idx,
1198
+ projectPath: PROJECT_PATH,
1199
+ toolGuidance: TOOL_GUIDANCE,
1200
+ };
1201
+ const result = await handleSearchCode(parseSearchCodeArgs(args), searchContext);
1202
+ return { ...result };
1203
+ }
1204
+ case 'get_index_status': {
1205
+ const status = await idx.getStatus();
1206
+ let statusText = JSON.stringify(status, null, 2);
1207
+ // Add corruption warning if detected
1208
+ if (status.corrupted) {
1209
+ statusText =
1210
+ `**WARNING: Index corruption detected!**\n` +
1211
+ `Reason: ${status.corruptionReason}\n` +
1212
+ `\nTo repair, either:\n` +
1213
+ `1. Run \`index_codebase\` with \`autoRepair: true\`\n` +
1214
+ `2. Run \`clear_index\` followed by \`index_codebase\`\n\n` +
1215
+ statusText;
1216
+ }
1217
+ return {
1218
+ content: [
1219
+ {
1220
+ type: 'text',
1221
+ text: statusText + TOOL_GUIDANCE,
1222
+ },
1223
+ ],
1224
+ };
1225
+ }
1226
+ case 'clear_index': {
1227
+ await idx.clearIndex();
1228
+ return {
1229
+ content: [
1230
+ {
1231
+ type: 'text',
1232
+ text: 'Index cleared.' + TOOL_GUIDANCE,
1233
+ },
1234
+ ],
1235
+ };
1236
+ }
1237
+ case 'get_project_instructions': {
1238
+ const config = await loadConfig(PROJECT_PATH);
1239
+ const projectInstructions = getInstructions(config);
1240
+ const fullInstructions = PRIORITY_INSTRUCTIONS + (projectInstructions || '');
1241
+ return {
1242
+ content: [
1243
+ {
1244
+ type: 'text',
1245
+ text: fullInstructions ||
1246
+ 'No project instructions configured. Add an "instructions" field to .glancey.json.',
1247
+ },
1248
+ ],
1249
+ };
1250
+ }
1251
+ case 'search_similar': {
1252
+ const searchContext = {
1253
+ indexer: idx,
1254
+ projectPath: PROJECT_PATH,
1255
+ toolGuidance: TOOL_GUIDANCE,
1256
+ };
1257
+ const result = await handleSearchSimilar(parseSearchSimilarArgs(args), searchContext);
1258
+ return { ...result };
1259
+ }
1260
+ case 'summarize_codebase': {
1261
+ const clusterContext = {
1262
+ indexer: idx,
1263
+ toolGuidance: TOOL_GUIDANCE,
1264
+ };
1265
+ const result = await handleSummarizeCodebase(parseSummarizeCodebaseArgs(args), clusterContext);
1266
+ return { ...result };
1267
+ }
1268
+ case 'list_concepts': {
1269
+ const clusterContext = {
1270
+ indexer: idx,
1271
+ toolGuidance: TOOL_GUIDANCE,
1272
+ };
1273
+ const result = await handleListConcepts(parseListConceptsArgs(args), clusterContext);
1274
+ return { ...result };
1275
+ }
1276
+ case 'search_by_concept': {
1277
+ const clusterContext = {
1278
+ indexer: idx,
1279
+ toolGuidance: TOOL_GUIDANCE,
1280
+ };
1281
+ const result = await handleSearchByConcept(parseSearchByConceptArgs(args), clusterContext);
1282
+ return { ...result };
1283
+ }
1284
+ case 'commit': {
1285
+ const message = isString(args?.message) ? args.message : '';
1286
+ const files = isStringArray(args?.files) ? args.files : [];
1287
+ if (!message) {
1288
+ throw new GlanceyError('message is required', 'validation', { tool: 'commit' });
1289
+ }
1290
+ // Commit rules to return with every response
1291
+ const COMMIT_RULES = `
1292
+ ## Commit Rules Reminder
1293
+
1294
+ 1. **Branch**: Must be on a feature branch, not main/master
1295
+ 2. **Message length**: Subject line must be ≤72 characters
1296
+ 3. **Imperative mood**: "Add feature" not "Added feature"
1297
+ 4. **Single responsibility**: One logical change per commit
1298
+ 5. **Body format**: Only "Co-Authored-By: Claude <noreply@anthropic.com>"
1299
+
1300
+ **Signs of multi-responsibility** (split into separate commits):
1301
+ - Message contains "and" connecting actions
1302
+ - Message lists multiple changes with commas
1303
+ - Changes span unrelated files/features
1304
+ `;
1305
+ const errors = [];
1306
+ const warnings = [];
1307
+ // Check 1: Not on main/master branch
1308
+ let currentBranch = '';
1309
+ try {
1310
+ const { stdout } = await execAsync('git branch --show-current', { cwd: PROJECT_PATH });
1311
+ currentBranch = stdout.trim();
1312
+ if (currentBranch === 'main' || currentBranch === 'master') {
1313
+ errors.push(`Cannot commit directly to ${currentBranch}. Create a feature branch first:\n git checkout -b feature/your-feature-name`);
1314
+ }
1315
+ }
1316
+ catch {
1317
+ errors.push('Failed to determine current branch. Are you in a git repository?');
1318
+ }
1319
+ // Check 2: Message length
1320
+ const subjectLine = message.split('\n')[0];
1321
+ if (subjectLine.length > 72) {
1322
+ errors.push(`Subject line is ${subjectLine.length} characters (max 72). Shorten it.`);
1323
+ }
1324
+ // Check 3: Imperative mood (heuristic - check for common past tense patterns)
1325
+ const pastTensePatterns = /^(Added|Fixed|Updated|Changed|Removed|Implemented|Created|Deleted|Modified|Refactored|Merged)\b/i;
1326
+ if (pastTensePatterns.test(subjectLine)) {
1327
+ warnings.push(`Subject may not be imperative mood. Use "Add" not "Added", "Fix" not "Fixed", etc.`);
1328
+ }
1329
+ // Check 4: Single responsibility (heuristic - check for "and" or multiple verbs)
1330
+ const multiResponsibilityPatterns = /\b(and|,)\s+(add|fix|update|change|remove|implement|create|delete|modify|refactor)\b/i;
1331
+ if (multiResponsibilityPatterns.test(subjectLine)) {
1332
+ errors.push(`Message suggests multiple responsibilities. Split into separate commits.`);
1333
+ }
1334
+ // If there are blocking errors, return them without committing
1335
+ if (errors.length > 0) {
1336
+ return {
1337
+ content: [
1338
+ {
1339
+ type: 'text',
1340
+ text: `## Commit Blocked\n\n**Errors:**\n${errors.map((e) => `- ${e}`).join('\n')}\n${warnings.length > 0 ? `\n**Warnings:**\n${warnings.map((w) => `- ${w}`).join('\n')}` : ''}\n${COMMIT_RULES}`,
1341
+ },
1342
+ ],
1343
+ isError: true,
1344
+ };
1345
+ }
1346
+ // Track files we staged for potential rollback
1347
+ const stagedByUs = [];
1348
+ // Stage files if provided (using spawn to avoid shell injection)
1349
+ if (files.length > 0) {
1350
+ try {
1351
+ // Use -- to prevent files starting with - being interpreted as options
1352
+ await gitSpawn(['add', '--', ...files], { cwd: PROJECT_PATH });
1353
+ stagedByUs.push(...files);
1354
+ }
1355
+ catch (e) {
1356
+ throw wrapError('Failed to stage files', 'git', e, { files });
1357
+ }
1358
+ }
1359
+ // Check if there are staged changes
1360
+ try {
1361
+ const { stdout } = await gitSpawn(['diff', '--cached', '--name-only'], {
1362
+ cwd: PROJECT_PATH,
1363
+ });
1364
+ if (!stdout.trim()) {
1365
+ throw new GlanceyError('No staged changes to commit. Stage files first or pass files parameter.', 'git');
1366
+ }
1367
+ }
1368
+ catch (e) {
1369
+ if (e instanceof GlanceyError) {
1370
+ throw e;
1371
+ }
1372
+ throw wrapError('Failed to check staged changes', 'git', e);
1373
+ }
1374
+ // Build commit message with Co-Authored-By
1375
+ const fullMessage = `${message}\n\nCo-Authored-By: Claude <noreply@anthropic.com>`;
1376
+ // Write marker file to indicate commit is via MCP tool (for post-commit hook)
1377
+ const markerPath = path.join(PROJECT_PATH, '.git', 'MCP_COMMIT_MARKER');
1378
+ try {
1379
+ fs.writeFileSync(markerPath, Date.now().toString());
1380
+ }
1381
+ catch {
1382
+ // Ignore marker write failures - non-critical
1383
+ }
1384
+ // Execute commit using -F - to read message from stdin (avoids shell escaping issues)
1385
+ try {
1386
+ const { stdout } = await gitSpawn(['commit', '-F', '-'], {
1387
+ cwd: PROJECT_PATH,
1388
+ stdin: fullMessage,
1389
+ });
1390
+ let response = `## Commit Successful\n\n${stdout.trim()}`;
1391
+ if (warnings.length > 0) {
1392
+ response += `\n\n**Warnings:**\n${warnings.map((w) => `- ${w}`).join('\n')}`;
1393
+ }
1394
+ response += `\n${COMMIT_RULES}`;
1395
+ return {
1396
+ content: [
1397
+ {
1398
+ type: 'text',
1399
+ text: response,
1400
+ },
1401
+ ],
1402
+ };
1403
+ }
1404
+ catch (e) {
1405
+ // Rollback: unstage files we staged if commit failed
1406
+ if (stagedByUs.length > 0) {
1407
+ try {
1408
+ await gitSpawn(['reset', 'HEAD', '--', ...stagedByUs], { cwd: PROJECT_PATH });
1409
+ }
1410
+ catch {
1411
+ // Ignore rollback failures - best effort
1412
+ }
1413
+ }
1414
+ throw wrapError('Git commit failed', 'git', e, { message });
1415
+ }
1416
+ }
1417
+ // --- Symbolic Analysis Tools ---
1418
+ case 'get_symbols_overview': {
1419
+ const symbolContext = {
1420
+ projectPath: PROJECT_PATH,
1421
+ toolGuidance: TOOL_GUIDANCE,
1422
+ };
1423
+ const result = await handleGetSymbolsOverview(parseGetSymbolsOverviewArgs(args), symbolContext);
1424
+ return { ...result };
1425
+ }
1426
+ case 'find_symbol': {
1427
+ const symbolContext = {
1428
+ projectPath: PROJECT_PATH,
1429
+ toolGuidance: TOOL_GUIDANCE,
1430
+ };
1431
+ const result = await handleFindSymbol(parseFindSymbolArgs(args), symbolContext);
1432
+ return { ...result };
1433
+ }
1434
+ case 'find_referencing_symbols': {
1435
+ const namePath = isString(args?.name_path) ? args.name_path : '';
1436
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1437
+ if (!namePath || !relativePath) {
1438
+ throw new GlanceyError('name_path and relative_path are required', 'validation', {
1439
+ tool: 'find_referencing_symbols',
1440
+ });
1441
+ }
1442
+ const includeInfo = isBoolean(args?.include_info) ? args.include_info : false;
1443
+ const includeKinds = Array.isArray(args?.include_kinds)
1444
+ ? args.include_kinds
1445
+ : undefined;
1446
+ const excludeKinds = Array.isArray(args?.exclude_kinds)
1447
+ ? args.exclude_kinds
1448
+ : undefined;
1449
+ const finder = new ReferenceFinder(PROJECT_PATH);
1450
+ const references = await finder.findReferences({
1451
+ namePath,
1452
+ relativePath,
1453
+ includeInfo,
1454
+ includeKinds,
1455
+ excludeKinds,
1456
+ });
1457
+ return {
1458
+ content: [
1459
+ {
1460
+ type: 'text',
1461
+ text: formatReferencesResult(references) + TOOL_GUIDANCE,
1462
+ },
1463
+ ],
1464
+ };
1465
+ }
1466
+ case 'search_for_pattern': {
1467
+ const substringPattern = isString(args?.substring_pattern) ? args.substring_pattern : '';
1468
+ if (!substringPattern) {
1469
+ throw new GlanceyError('substring_pattern is required', 'validation', {
1470
+ tool: 'search_for_pattern',
1471
+ });
1472
+ }
1473
+ const result = await searchForPattern(PROJECT_PATH, {
1474
+ substringPattern,
1475
+ relativePath: isString(args?.relative_path) ? args.relative_path : undefined,
1476
+ restrictSearchToCodeFiles: isBoolean(args?.restrict_search_to_code_files)
1477
+ ? args.restrict_search_to_code_files
1478
+ : false,
1479
+ pathsIncludeGlob: isString(args?.paths_include_glob)
1480
+ ? args.paths_include_glob
1481
+ : undefined,
1482
+ pathsExcludeGlob: isString(args?.paths_exclude_glob)
1483
+ ? args.paths_exclude_glob
1484
+ : undefined,
1485
+ contextLinesBefore: isNumber(args?.context_lines_before) ? args.context_lines_before : 0,
1486
+ contextLinesAfter: isNumber(args?.context_lines_after) ? args.context_lines_after : 0,
1487
+ maxAnswerChars: isNumber(args?.max_answer_chars) ? args.max_answer_chars : 50000,
1488
+ });
1489
+ return {
1490
+ content: [
1491
+ {
1492
+ type: 'text',
1493
+ text: formatPatternSearchResults(result) + TOOL_GUIDANCE,
1494
+ },
1495
+ ],
1496
+ };
1497
+ }
1498
+ // --- Memory Tools ---
1499
+ case 'write_memory': {
1500
+ const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
1501
+ const content = isString(args?.content) ? args.content : '';
1502
+ if (!memoryFileName || !content) {
1503
+ throw new GlanceyError('memory_file_name and content are required', 'validation', {
1504
+ tool: 'write_memory',
1505
+ });
1506
+ }
1507
+ const memoryManager = new MemoryManager(PROJECT_PATH);
1508
+ await memoryManager.writeMemory(memoryFileName, content);
1509
+ return {
1510
+ content: [
1511
+ {
1512
+ type: 'text',
1513
+ text: `Memory "${memoryFileName}" saved successfully.` + TOOL_GUIDANCE,
1514
+ },
1515
+ ],
1516
+ };
1517
+ }
1518
+ case 'read_memory': {
1519
+ const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
1520
+ if (!memoryFileName) {
1521
+ throw new GlanceyError('memory_file_name is required', 'validation', {
1522
+ tool: 'read_memory',
1523
+ });
1524
+ }
1525
+ const memoryManager = new MemoryManager(PROJECT_PATH);
1526
+ const content = await memoryManager.readMemory(memoryFileName);
1527
+ return {
1528
+ content: [
1529
+ {
1530
+ type: 'text',
1531
+ text: `## Memory: ${memoryFileName}\n\n${content}` + TOOL_GUIDANCE,
1532
+ },
1533
+ ],
1534
+ };
1535
+ }
1536
+ case 'list_memories': {
1537
+ const memoryManager = new MemoryManager(PROJECT_PATH);
1538
+ const memories = await memoryManager.listMemories();
1539
+ return {
1540
+ content: [
1541
+ {
1542
+ type: 'text',
1543
+ text: formatMemoryList(memories) + TOOL_GUIDANCE,
1544
+ },
1545
+ ],
1546
+ };
1547
+ }
1548
+ case 'delete_memory': {
1549
+ const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
1550
+ if (!memoryFileName) {
1551
+ throw new GlanceyError('memory_file_name is required', 'validation', {
1552
+ tool: 'delete_memory',
1553
+ });
1554
+ }
1555
+ const memoryManager = new MemoryManager(PROJECT_PATH);
1556
+ await memoryManager.deleteMemory(memoryFileName);
1557
+ return {
1558
+ content: [
1559
+ {
1560
+ type: 'text',
1561
+ text: `Memory "${memoryFileName}" deleted successfully.` + TOOL_GUIDANCE,
1562
+ },
1563
+ ],
1564
+ };
1565
+ }
1566
+ case 'edit_memory': {
1567
+ const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
1568
+ const needle = isString(args?.needle) ? args.needle : '';
1569
+ const repl = isString(args?.repl) ? args.repl : '';
1570
+ const mode = isString(args?.mode) ? args.mode : '';
1571
+ if (!memoryFileName || !needle || mode === '') {
1572
+ throw new GlanceyError('memory_file_name, needle, repl, and mode are required', 'validation', { tool: 'edit_memory' });
1573
+ }
1574
+ if (mode !== 'literal' && mode !== 'regex') {
1575
+ throw new GlanceyError('mode must be "literal" or "regex"', 'validation', {
1576
+ tool: 'edit_memory',
1577
+ });
1578
+ }
1579
+ const memoryManager = new MemoryManager(PROJECT_PATH);
1580
+ const result = await memoryManager.editMemory(memoryFileName, needle, repl, mode);
1581
+ return {
1582
+ content: [
1583
+ {
1584
+ type: 'text',
1585
+ text: `Memory "${memoryFileName}" edited. ${result.matchCount} replacement(s) made.` +
1586
+ TOOL_GUIDANCE,
1587
+ },
1588
+ ],
1589
+ };
1590
+ }
1591
+ // --- Symbol Editing Tools ---
1592
+ case 'replace_symbol_body': {
1593
+ const namePath = isString(args?.name_path) ? args.name_path : '';
1594
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1595
+ const body = isString(args?.body) ? args.body : '';
1596
+ if (!namePath || !relativePath || !body) {
1597
+ throw new GlanceyError('name_path, relative_path, and body are required', 'validation', {
1598
+ tool: 'replace_symbol_body',
1599
+ });
1600
+ }
1601
+ const editor = new SymbolEditor(PROJECT_PATH);
1602
+ const result = await editor.replaceSymbolBody({ namePath, relativePath, body });
1603
+ if (!result.success) {
1604
+ return {
1605
+ content: [
1606
+ {
1607
+ type: 'text',
1608
+ text: `Failed to replace symbol: ${result.error}` + TOOL_GUIDANCE,
1609
+ },
1610
+ ],
1611
+ isError: true,
1612
+ };
1613
+ }
1614
+ return {
1615
+ content: [
1616
+ {
1617
+ type: 'text',
1618
+ text: `Symbol "${result.symbolName}" replaced in ${result.filepath}.\n` +
1619
+ `New location: lines ${result.newRange?.startLine}-${result.newRange?.endLine}` +
1620
+ TOOL_GUIDANCE,
1621
+ },
1622
+ ],
1623
+ };
1624
+ }
1625
+ case 'insert_before_symbol': {
1626
+ const namePath = isString(args?.name_path) ? args.name_path : '';
1627
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1628
+ const body = isString(args?.body) ? args.body : '';
1629
+ if (!namePath || !relativePath || !body) {
1630
+ throw new GlanceyError('name_path, relative_path, and body are required', 'validation', {
1631
+ tool: 'insert_before_symbol',
1632
+ });
1633
+ }
1634
+ const editor = new SymbolEditor(PROJECT_PATH);
1635
+ const result = await editor.insertBeforeSymbol({ namePath, relativePath, body });
1636
+ if (!result.success) {
1637
+ return {
1638
+ content: [
1639
+ {
1640
+ type: 'text',
1641
+ text: `Failed to insert before symbol: ${result.error}` + TOOL_GUIDANCE,
1642
+ },
1643
+ ],
1644
+ isError: true,
1645
+ };
1646
+ }
1647
+ return {
1648
+ content: [
1649
+ {
1650
+ type: 'text',
1651
+ text: `Content inserted before "${result.symbolName}" in ${result.filepath}.\n` +
1652
+ `Inserted at: lines ${result.newRange?.startLine}-${result.newRange?.endLine}` +
1653
+ TOOL_GUIDANCE,
1654
+ },
1655
+ ],
1656
+ };
1657
+ }
1658
+ case 'insert_after_symbol': {
1659
+ const namePath = isString(args?.name_path) ? args.name_path : '';
1660
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1661
+ const body = isString(args?.body) ? args.body : '';
1662
+ if (!namePath || !relativePath || !body) {
1663
+ throw new GlanceyError('name_path, relative_path, and body are required', 'validation', {
1664
+ tool: 'insert_after_symbol',
1665
+ });
1666
+ }
1667
+ const editor = new SymbolEditor(PROJECT_PATH);
1668
+ const result = await editor.insertAfterSymbol({ namePath, relativePath, body });
1669
+ if (!result.success) {
1670
+ return {
1671
+ content: [
1672
+ {
1673
+ type: 'text',
1674
+ text: `Failed to insert after symbol: ${result.error}` + TOOL_GUIDANCE,
1675
+ },
1676
+ ],
1677
+ isError: true,
1678
+ };
1679
+ }
1680
+ return {
1681
+ content: [
1682
+ {
1683
+ type: 'text',
1684
+ text: `Content inserted after "${result.symbolName}" in ${result.filepath}.\n` +
1685
+ `Inserted at: lines ${result.newRange?.startLine}-${result.newRange?.endLine}` +
1686
+ TOOL_GUIDANCE,
1687
+ },
1688
+ ],
1689
+ };
1690
+ }
1691
+ case 'rename_symbol': {
1692
+ const namePath = isString(args?.name_path) ? args.name_path : '';
1693
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1694
+ const newName = isString(args?.new_name) ? args.new_name : '';
1695
+ const dryRun = isBoolean(args?.dry_run) ? args.dry_run : false;
1696
+ if (!namePath || !relativePath || !newName) {
1697
+ throw new GlanceyError('name_path, relative_path, and new_name are required', 'validation', { tool: 'rename_symbol' });
1698
+ }
1699
+ const renamer = new SymbolRenamer(PROJECT_PATH);
1700
+ const result = await renamer.renameSymbol({ namePath, relativePath, newName, dryRun });
1701
+ if (!result.success) {
1702
+ return {
1703
+ content: [
1704
+ {
1705
+ type: 'text',
1706
+ text: `Failed to rename symbol: ${result.error}` + TOOL_GUIDANCE,
1707
+ },
1708
+ ],
1709
+ isError: true,
1710
+ };
1711
+ }
1712
+ const modeLabel = dryRun ? ' (dry run)' : '';
1713
+ return {
1714
+ content: [
1715
+ {
1716
+ type: 'text',
1717
+ text: formatRenameResult(result) + modeLabel + TOOL_GUIDANCE,
1718
+ },
1719
+ ],
1720
+ };
1721
+ }
1722
+ // --- Worktree Tools ---
1723
+ case 'create_worktree': {
1724
+ const shortName = isString(args?.short_name) ? args.short_name : '';
1725
+ if (!shortName) {
1726
+ throw new GlanceyError('short_name is required', 'validation', {
1727
+ tool: 'create_worktree',
1728
+ });
1729
+ }
1730
+ const worktreeManager = new WorktreeManager(PROJECT_PATH);
1731
+ const result = await worktreeManager.createWorktree({
1732
+ shortName,
1733
+ issueId: isString(args?.issue_id) ? args.issue_id : undefined,
1734
+ prefix: isString(args?.prefix)
1735
+ ? args.prefix
1736
+ : undefined,
1737
+ baseBranch: isString(args?.base_branch) ? args.base_branch : undefined,
1738
+ installDeps: isBoolean(args?.install_deps) ? args.install_deps : true,
1739
+ packageManager: isString(args?.package_manager)
1740
+ ? args.package_manager
1741
+ : undefined,
1742
+ });
1743
+ if (!result.success) {
1744
+ return {
1745
+ content: [
1746
+ {
1747
+ type: 'text',
1748
+ text: `Failed to create worktree: ${result.error}` + TOOL_GUIDANCE,
1749
+ },
1750
+ ],
1751
+ isError: true,
1752
+ };
1753
+ }
1754
+ const worktree = result.worktree;
1755
+ const parts = [];
1756
+ parts.push('## Worktree Created\n');
1757
+ parts.push(`**Name:** ${worktree?.name}`);
1758
+ parts.push(`**Path:** ${worktree?.path}`);
1759
+ parts.push(`**Branch:** ${worktree?.branch}`);
1760
+ if (result.depsInstalled !== undefined) {
1761
+ const depsStatus = result.depsInstalled ? 'installed' : 'skipped/failed';
1762
+ const timeInfo = result.depsInstallTime ? ` (${result.depsInstallTime}ms)` : '';
1763
+ parts.push(`**Dependencies:** ${depsStatus}${timeInfo}`);
1764
+ }
1765
+ parts.push('\n**Usage:** Spawn agent with `cwd: "' + (worktree?.path ?? '') + '"`');
1766
+ return {
1767
+ content: [
1768
+ {
1769
+ type: 'text',
1770
+ text: parts.join('\n') + TOOL_GUIDANCE,
1771
+ },
1772
+ ],
1773
+ };
1774
+ }
1775
+ case 'list_worktrees': {
1776
+ const worktreeManager = new WorktreeManager(PROJECT_PATH);
1777
+ const result = await worktreeManager.listWorktrees();
1778
+ return {
1779
+ content: [
1780
+ {
1781
+ type: 'text',
1782
+ text: formatWorktreeList(result) + TOOL_GUIDANCE,
1783
+ },
1784
+ ],
1785
+ };
1786
+ }
1787
+ case 'remove_worktree': {
1788
+ const worktreeName = isString(args?.name) ? args.name : '';
1789
+ if (!worktreeName) {
1790
+ throw new GlanceyError('name is required', 'validation', {
1791
+ tool: 'remove_worktree',
1792
+ });
1793
+ }
1794
+ const worktreeManager = new WorktreeManager(PROJECT_PATH);
1795
+ const result = await worktreeManager.removeWorktree({
1796
+ name: worktreeName,
1797
+ deleteBranch: isBoolean(args?.delete_branch) ? args.delete_branch : false,
1798
+ force: isBoolean(args?.force) ? args.force : false,
1799
+ });
1800
+ if (!result.success) {
1801
+ return {
1802
+ content: [
1803
+ {
1804
+ type: 'text',
1805
+ text: `Failed to remove worktree: ${result.error}` + TOOL_GUIDANCE,
1806
+ },
1807
+ ],
1808
+ isError: true,
1809
+ };
1810
+ }
1811
+ const parts = [];
1812
+ parts.push('## Worktree Removed\n');
1813
+ parts.push(`**Name:** ${worktreeName}`);
1814
+ if (result.branch) {
1815
+ parts.push(`**Branch:** ${result.branch}`);
1816
+ parts.push(`**Branch deleted:** ${result.branchDeleted ? 'yes' : 'no'}`);
1817
+ }
1818
+ return {
1819
+ content: [
1820
+ {
1821
+ type: 'text',
1822
+ text: parts.join('\n') + TOOL_GUIDANCE,
1823
+ },
1824
+ ],
1825
+ };
1826
+ }
1827
+ case 'worktree_status': {
1828
+ const worktreeName = isString(args?.name) ? args.name : '';
1829
+ if (!worktreeName) {
1830
+ throw new GlanceyError('name is required', 'validation', {
1831
+ tool: 'worktree_status',
1832
+ });
1833
+ }
1834
+ const worktreeManager = new WorktreeManager(PROJECT_PATH);
1835
+ const info = await worktreeManager.getWorktreeInfo(worktreeName);
1836
+ if (!info) {
1837
+ return {
1838
+ content: [
1839
+ {
1840
+ type: 'text',
1841
+ text: `Worktree "${worktreeName}" not found.` + TOOL_GUIDANCE,
1842
+ },
1843
+ ],
1844
+ isError: true,
1845
+ };
1846
+ }
1847
+ return {
1848
+ content: [
1849
+ {
1850
+ type: 'text',
1851
+ text: formatWorktreeInfo(info) + TOOL_GUIDANCE,
1852
+ },
1853
+ ],
1854
+ };
1855
+ }
1856
+ // --- Dashboard Tools ---
1857
+ case 'open_dashboard': {
1858
+ const force = isBoolean(args?.force) ? args.force : false;
1859
+ // Check if dashboard is already running
1860
+ if (isDashboardRunning()) {
1861
+ const url = getDashboardUrl();
1862
+ if (url) {
1863
+ openBrowser(url, PROJECT_PATH, force);
1864
+ return {
1865
+ content: [
1866
+ {
1867
+ type: 'text',
1868
+ text: `Dashboard already running at ${url}. Opening in browser.` + TOOL_GUIDANCE,
1869
+ },
1870
+ ],
1871
+ };
1872
+ }
1873
+ }
1874
+ // Dashboard not running, start it
1875
+ try {
1876
+ const config = await getConfig();
1877
+ const dashboard = await startDashboard({
1878
+ config,
1879
+ projectPath: PROJECT_PATH,
1880
+ version: PACKAGE_VERSION,
1881
+ });
1882
+ openBrowser(dashboard.url, PROJECT_PATH, force);
1883
+ return {
1884
+ content: [
1885
+ {
1886
+ type: 'text',
1887
+ text: `Dashboard started at ${dashboard.url}. Opening in browser.` + TOOL_GUIDANCE,
1888
+ },
1889
+ ],
1890
+ };
1891
+ }
1892
+ catch (error) {
1893
+ throw wrapError('Failed to start dashboard', 'internal', error);
1894
+ }
1895
+ }
1896
+ default:
1897
+ throw new GlanceyError(`Unknown tool: ${name}`, 'validation', { tool: name });
1898
+ }
1899
+ }
1900
+ catch (error) {
1901
+ // Log full error details server-side for debugging
1902
+ logError(error, name);
1903
+ return {
1904
+ content: [
1905
+ {
1906
+ type: 'text',
1907
+ text: formatErrorResponse(error),
1908
+ },
1909
+ ],
1910
+ isError: true,
1911
+ };
1912
+ }
1913
+ });
1914
+ // Start server
1915
+ async function main() {
1916
+ // Disable Serena plugin (glancey replaces it)
1917
+ await disableSerena();
1918
+ // Check for updates in background (non-blocking)
1919
+ checkForUpdates();
1920
+ // Set up config file watchers for hot reload
1921
+ watchConfigFiles();
1922
+ // Load config to check if dashboard is enabled
1923
+ const config = await getConfig();
1924
+ const dashboardConfig = getDashboardConfig(config);
1925
+ // Initialize the indexer eagerly so dashboard has data
1926
+ let indexer = null;
1927
+ try {
1928
+ indexer = await getIndexer();
1929
+ }
1930
+ catch (error) {
1931
+ console.error('[glancey] Failed to initialize indexer:', error);
1932
+ }
1933
+ // Auto-index if project is not yet indexed or backend has changed
1934
+ if (indexer) {
1935
+ const status = await indexer.getStatus();
1936
+ const needsIndex = !status.indexed;
1937
+ const needsReindex = status.indexed && status.backendMismatch;
1938
+ if (needsIndex) {
1939
+ console.error('[glancey] Project not indexed, starting auto-index...');
1940
+ }
1941
+ else if (needsReindex) {
1942
+ console.error(`[glancey] ${status.backendMismatchReason}`);
1943
+ console.error('[glancey] Starting automatic reindex with new backend...');
1944
+ }
1945
+ if (needsIndex || needsReindex) {
1946
+ dashboardState.onIndexingStart();
1947
+ // Run indexing in background so server can start immediately
1948
+ // Force reindex if backend changed to rebuild all vectors
1949
+ indexer
1950
+ .indexCodebase(undefined, undefined, needsReindex, (progress) => {
1951
+ dashboardState.onProgress(progress);
1952
+ })
1953
+ .then((result) => {
1954
+ dashboardState.onIndexingComplete(result);
1955
+ console.error(`[glancey] Auto-index complete: ${result.filesIndexed} files, ${result.chunksCreated} chunks`);
1956
+ })
1957
+ .catch((error) => {
1958
+ console.error('[glancey] Auto-index failed:', error);
1959
+ });
1960
+ }
1961
+ }
1962
+ // Start dashboard if enabled
1963
+ console.error(`[glancey] Dashboard config: enabled=${dashboardConfig.enabled}, openBrowser=${dashboardConfig.openBrowser}, port=${dashboardConfig.port}`);
1964
+ if (dashboardConfig.enabled) {
1965
+ const configuredPort = dashboardConfig.port || 24300;
1966
+ try {
1967
+ const availablePort = await findAvailablePort(configuredPort);
1968
+ if (availablePort !== configuredPort) {
1969
+ console.error(`[glancey] Configured port ${configuredPort} unavailable, using ${availablePort}`);
1970
+ }
1971
+ const dashboard = await startDashboard({
1972
+ port: availablePort,
1973
+ config,
1974
+ projectPath: PROJECT_PATH,
1975
+ version: PACKAGE_VERSION,
1976
+ });
1977
+ console.error(`[glancey] Dashboard started at ${dashboard.url}`);
1978
+ // Open dashboard in user's default browser if configured
1979
+ if (dashboardConfig.openBrowser) {
1980
+ openBrowser(dashboard.url, PROJECT_PATH);
1981
+ }
1982
+ else {
1983
+ console.error('[glancey] Browser auto-open disabled in config');
1984
+ }
1985
+ }
1986
+ catch (error) {
1987
+ // findAvailablePort throws if no port found in range, or startDashboard may fail
1988
+ console.error('[glancey] Failed to start dashboard:', error);
1989
+ }
1990
+ }
1991
+ else {
1992
+ console.error('[glancey] Dashboard disabled in config');
1993
+ }
1994
+ const transport = new StdioServerTransport();
1995
+ await server.connect(transport);
1996
+ console.error('[glancey] MCP server started');
1997
+ }
1998
+ /**
1999
+ * Gracefully shutdown the server and cleanup resources
2000
+ */
2001
+ async function shutdown(signal) {
2002
+ console.error(`[glancey] Received ${signal}, shutting down gracefully...`);
2003
+ try {
2004
+ // Stop the dashboard server
2005
+ await stopDashboard();
2006
+ console.error('[glancey] Dashboard stopped');
2007
+ }
2008
+ catch (error) {
2009
+ console.error('[glancey] Error stopping dashboard:', error);
2010
+ }
2011
+ // Close the MCP server connection
2012
+ try {
2013
+ await server.close();
2014
+ console.error('[glancey] MCP server closed');
2015
+ }
2016
+ catch (error) {
2017
+ console.error('[glancey] Error closing MCP server:', error);
2018
+ }
2019
+ process.exit(0);
2020
+ }
2021
+ // Register signal handlers for graceful shutdown
2022
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
2023
+ process.on('SIGINT', () => shutdown('SIGINT'));
2024
+ main().catch((error) => {
2025
+ console.error('[glancey] Fatal error:', error);
2026
+ process.exit(1);
2027
+ });
2028
+ //# sourceMappingURL=index.js.map