attocode-code-intel 0.1.0__py3-none-any.whl

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 (345) hide show
  1. attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
  2. attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
  3. attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
  4. attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
  5. attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. attocode_intel/GUIDELINES.md +432 -0
  7. attocode_intel/__init__.py +52 -0
  8. attocode_intel/_internal/__init__.py +1 -0
  9. attocode_intel/_internal/errors/__init__.py +126 -0
  10. attocode_intel/_internal/integrations/__init__.py +1 -0
  11. attocode_intel/_internal/integrations/context/__init__.py +1 -0
  12. attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
  13. attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
  14. attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
  15. attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
  16. attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
  17. attocode_intel/_internal/integrations/context/cas.py +466 -0
  18. attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
  19. attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
  20. attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
  21. attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
  22. attocode_intel/_internal/integrations/context/cross_references.py +616 -0
  23. attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
  24. attocode_intel/_internal/integrations/context/embeddings.py +282 -0
  25. attocode_intel/_internal/integrations/context/frecency.py +427 -0
  26. attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
  27. attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
  28. attocode_intel/_internal/integrations/context/hydration.py +90 -0
  29. attocode_intel/_internal/integrations/context/index_repair.py +87 -0
  30. attocode_intel/_internal/integrations/context/index_store.py +586 -0
  31. attocode_intel/_internal/integrations/context/memory_store.py +602 -0
  32. attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
  33. attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
  34. attocode_intel/_internal/integrations/context/query_history.py +417 -0
  35. attocode_intel/_internal/integrations/context/reranker.py +351 -0
  36. attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
  37. attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
  38. attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
  39. attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
  40. attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
  41. attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
  42. attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
  43. attocode_intel/_internal/integrations/context/vector_store.py +795 -0
  44. attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
  45. attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
  46. attocode_intel/_internal/integrations/lsp/client.py +1140 -0
  47. attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
  48. attocode_intel/_internal/integrations/mcp/client.py +253 -0
  49. attocode_intel/_internal/integrations/security/__init__.py +1 -0
  50. attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
  51. attocode_intel/_internal/integrations/security/dataflow.py +522 -0
  52. attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
  53. attocode_intel/_internal/integrations/security/matcher.py +44 -0
  54. attocode_intel/_internal/integrations/security/patterns.py +724 -0
  55. attocode_intel/_internal/integrations/security/scanner.py +351 -0
  56. attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
  57. attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
  58. attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
  59. attocode_intel/_internal/tricks/__init__.py +1 -0
  60. attocode_intel/_internal/tricks/recursive_context.py +357 -0
  61. attocode_intel/_internal/types/__init__.py +1 -0
  62. attocode_intel/_internal/types/events.py +289 -0
  63. attocode_intel/_internal/types/severity.py +15 -0
  64. attocode_intel/_shared.py +341 -0
  65. attocode_intel/analysis_coverage.py +56 -0
  66. attocode_intel/api/__init__.py +1 -0
  67. attocode_intel/api/access.py +32 -0
  68. attocode_intel/api/app.py +247 -0
  69. attocode_intel/api/auth/__init__.py +128 -0
  70. attocode_intel/api/auth/api_keys.py +61 -0
  71. attocode_intel/api/auth/context.py +34 -0
  72. attocode_intel/api/auth/jwt.py +66 -0
  73. attocode_intel/api/auth/oauth.py +146 -0
  74. attocode_intel/api/auth/passwords.py +15 -0
  75. attocode_intel/api/deps.py +351 -0
  76. attocode_intel/api/knowledge_bridge.py +13 -0
  77. attocode_intel/api/mcp_transport.py +51 -0
  78. attocode_intel/api/middleware.py +411 -0
  79. attocode_intel/api/models.py +698 -0
  80. attocode_intel/api/providers/__init__.py +1 -0
  81. attocode_intel/api/providers/db_provider.py +1106 -0
  82. attocode_intel/api/providers/local_provider.py +476 -0
  83. attocode_intel/api/providers/remote_provider.py +740 -0
  84. attocode_intel/api/repository_source.py +21 -0
  85. attocode_intel/api/routes/__init__.py +1 -0
  86. attocode_intel/api/routes/_pin_helper.py +158 -0
  87. attocode_intel/api/routes/activity.py +148 -0
  88. attocode_intel/api/routes/adr.py +102 -0
  89. attocode_intel/api/routes/analysis.py +515 -0
  90. attocode_intel/api/routes/api_keys.py +170 -0
  91. attocode_intel/api/routes/auth.py +428 -0
  92. attocode_intel/api/routes/branches.py +262 -0
  93. attocode_intel/api/routes/cross_repo_search.py +198 -0
  94. attocode_intel/api/routes/embeddings.py +386 -0
  95. attocode_intel/api/routes/files.py +132 -0
  96. attocode_intel/api/routes/files_v2.py +388 -0
  97. attocode_intel/api/routes/gc.py +322 -0
  98. attocode_intel/api/routes/git_v2.py +433 -0
  99. attocode_intel/api/routes/graph.py +174 -0
  100. attocode_intel/api/routes/graph_viz.py +160 -0
  101. attocode_intel/api/routes/health.py +62 -0
  102. attocode_intel/api/routes/history.py +175 -0
  103. attocode_intel/api/routes/jobs.py +84 -0
  104. attocode_intel/api/routes/learning.py +205 -0
  105. attocode_intel/api/routes/lsp.py +272 -0
  106. attocode_intel/api/routes/notify.py +623 -0
  107. attocode_intel/api/routes/operations.py +27 -0
  108. attocode_intel/api/routes/orgs.py +924 -0
  109. attocode_intel/api/routes/preferences.py +100 -0
  110. attocode_intel/api/routes/presence.py +42 -0
  111. attocode_intel/api/routes/projects.py +116 -0
  112. attocode_intel/api/routes/repos.py +47 -0
  113. attocode_intel/api/routes/search.py +180 -0
  114. attocode_intel/api/routes/snapshots.py +614 -0
  115. attocode_intel/api/routes/webhooks.py +313 -0
  116. attocode_intel/api/routes/websocket.py +116 -0
  117. attocode_intel/api/static/graph.html +239 -0
  118. attocode_intel/api/utils.py +96 -0
  119. attocode_intel/artifacts/__init__.py +45 -0
  120. attocode_intel/artifacts/hashing.py +230 -0
  121. attocode_intel/artifacts/provenance.py +190 -0
  122. attocode_intel/artifacts/retrieval_pin.py +159 -0
  123. attocode_intel/audit.py +59 -0
  124. attocode_intel/bug_finder.py +206 -0
  125. attocode_intel/bundle.py +80 -0
  126. attocode_intel/catalog.py +242 -0
  127. attocode_intel/change_review.py +75 -0
  128. attocode_intel/cli.py +3062 -0
  129. attocode_intel/community.py +63 -0
  130. attocode_intel/confidence/__init__.py +144 -0
  131. attocode_intel/confidence/jev.py +234 -0
  132. attocode_intel/confidence/llm.py +330 -0
  133. attocode_intel/confidence/redact.py +44 -0
  134. attocode_intel/confidence/settings.py +74 -0
  135. attocode_intel/config.py +256 -0
  136. attocode_intel/crypto.py +31 -0
  137. attocode_intel/db/__init__.py +7 -0
  138. attocode_intel/db/base.py +33 -0
  139. attocode_intel/db/engine.py +61 -0
  140. attocode_intel/db/models.py +529 -0
  141. attocode_intel/entrypoint.py +185 -0
  142. attocode_intel/focused_evidence.py +130 -0
  143. attocode_intel/freshness.py +154 -0
  144. attocode_intel/gateway.py +734 -0
  145. attocode_intel/git/__init__.py +7 -0
  146. attocode_intel/git/credentials.py +75 -0
  147. attocode_intel/git/manager.py +493 -0
  148. attocode_intel/git/models.py +96 -0
  149. attocode_intel/git/storage.py +98 -0
  150. attocode_intel/graph_query_parser.py +761 -0
  151. attocode_intel/helpers.py +712 -0
  152. attocode_intel/indexing/__init__.py +29 -0
  153. attocode_intel/indexing/debouncer.py +136 -0
  154. attocode_intel/indexing/delta_indexer.py +151 -0
  155. attocode_intel/indexing/full_indexer.py +323 -0
  156. attocode_intel/indexing/incremental.py +240 -0
  157. attocode_intel/indexing/parser.py +403 -0
  158. attocode_intel/indexing/summarizer.py +264 -0
  159. attocode_intel/installer.py +1896 -0
  160. attocode_intel/knowledge.py +269 -0
  161. attocode_intel/local_knowledge.py +77 -0
  162. attocode_intel/migrations/alembic.ini +36 -0
  163. attocode_intel/migrations/env.py +71 -0
  164. attocode_intel/migrations/script.py.mako +27 -0
  165. attocode_intel/migrations/versions/001_initial_schema.py +219 -0
  166. attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
  167. attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
  168. attocode_intel/migrations/versions/004_audit_events.py +38 -0
  169. attocode_intel/migrations/versions/005_user_preferences.py +30 -0
  170. attocode_intel/migrations/versions/006_google_oauth.py +27 -0
  171. attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
  172. attocode_intel/migrations/versions/008_symbol_references.py +37 -0
  173. attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
  174. attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
  175. attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
  176. attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
  177. attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
  178. attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
  179. attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
  180. attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
  181. attocode_intel/migrations/versions/017_provenance_table.py +102 -0
  182. attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
  183. attocode_intel/migrations/versions/019_symref_caller.py +42 -0
  184. attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
  185. attocode_intel/onboarding.py +258 -0
  186. attocode_intel/output.py +178 -0
  187. attocode_intel/precision.py +133 -0
  188. attocode_intel/presence.py +160 -0
  189. attocode_intel/probe.py +110 -0
  190. attocode_intel/project_dir.py +97 -0
  191. attocode_intel/pubsub.py +113 -0
  192. attocode_intel/query_ranking.py +242 -0
  193. attocode_intel/readiness.py +972 -0
  194. attocode_intel/redis.py +51 -0
  195. attocode_intel/reference_pages.py +60 -0
  196. attocode_intel/remote.py +131 -0
  197. attocode_intel/repo_ranker.py +246 -0
  198. attocode_intel/request_context.py +82 -0
  199. attocode_intel/rules/__init__.py +28 -0
  200. attocode_intel/rules/ci.py +320 -0
  201. attocode_intel/rules/combinators.py +293 -0
  202. attocode_intel/rules/enricher.py +127 -0
  203. attocode_intel/rules/evolution.py +452 -0
  204. attocode_intel/rules/executor.py +531 -0
  205. attocode_intel/rules/filters/__init__.py +5 -0
  206. attocode_intel/rules/filters/pipeline.py +151 -0
  207. attocode_intel/rules/formatter.py +198 -0
  208. attocode_intel/rules/hygiene.py +242 -0
  209. attocode_intel/rules/importers/__init__.py +1 -0
  210. attocode_intel/rules/importers/semgrep.py +382 -0
  211. attocode_intel/rules/loader.py +292 -0
  212. attocode_intel/rules/marketplace.py +375 -0
  213. attocode_intel/rules/metavar.py +181 -0
  214. attocode_intel/rules/model.py +317 -0
  215. attocode_intel/rules/packs/__init__.py +1 -0
  216. attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
  217. attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
  218. attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
  219. attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
  220. attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
  221. attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
  222. attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
  223. attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
  224. attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
  225. attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
  226. attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
  227. attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
  228. attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
  229. attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
  230. attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
  231. attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
  232. attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
  233. attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
  234. attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
  235. attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
  236. attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
  237. attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
  238. attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
  239. attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
  240. attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
  241. attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
  242. attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
  243. attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
  244. attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
  245. attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
  246. attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
  247. attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
  248. attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
  249. attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
  250. attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
  251. attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
  252. attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
  253. attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
  254. attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
  255. attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
  256. attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
  257. attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
  258. attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
  259. attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
  260. attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
  261. attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
  262. attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
  263. attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
  264. attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
  265. attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
  266. attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
  267. attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
  268. attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
  269. attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
  270. attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
  271. attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
  272. attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
  273. attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
  274. attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
  275. attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
  276. attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
  277. attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
  278. attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
  279. attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
  280. attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
  281. attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
  282. attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
  283. attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
  284. attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
  285. attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
  286. attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
  287. attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
  288. attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
  289. attocode_intel/rules/packs/pack_loader.py +244 -0
  290. attocode_intel/rules/plugins/__init__.py +1 -0
  291. attocode_intel/rules/plugins/plugin_loader.py +132 -0
  292. attocode_intel/rules/profiling.py +332 -0
  293. attocode_intel/rules/registry.py +213 -0
  294. attocode_intel/rules/sarif.py +187 -0
  295. attocode_intel/rules/synthesis.py +420 -0
  296. attocode_intel/rules/testing.py +326 -0
  297. attocode_intel/server.py +734 -0
  298. attocode_intel/service.py +3416 -0
  299. attocode_intel/sharing.py +57 -0
  300. attocode_intel/storage/__init__.py +6 -0
  301. attocode_intel/storage/branch_overlay.py +352 -0
  302. attocode_intel/storage/content_store.py +220 -0
  303. attocode_intel/storage/dependency_store.py +143 -0
  304. attocode_intel/storage/diff_engine.py +187 -0
  305. attocode_intel/storage/embedding_store.py +594 -0
  306. attocode_intel/storage/provenance_store.py +137 -0
  307. attocode_intel/storage/security_scanner_db.py +164 -0
  308. attocode_intel/storage/symbol_store.py +160 -0
  309. attocode_intel/structural_evidence.py +144 -0
  310. attocode_intel/symbol_inspection.py +141 -0
  311. attocode_intel/symbol_links.py +167 -0
  312. attocode_intel/telemetry.py +42 -0
  313. attocode_intel/test_ranking.py +73 -0
  314. attocode_intel/testing/__init__.py +43 -0
  315. attocode_intel/testing/fixtures.py +122 -0
  316. attocode_intel/testing/helpers.py +75 -0
  317. attocode_intel/testing/mocks.py +99 -0
  318. attocode_intel/tools/__init__.py +2 -0
  319. attocode_intel/tools/adr_tools.py +628 -0
  320. attocode_intel/tools/analysis_tools.py +382 -0
  321. attocode_intel/tools/composite_tools.py +695 -0
  322. attocode_intel/tools/cross_mode_tools.py +173 -0
  323. attocode_intel/tools/dead_code_tools.py +582 -0
  324. attocode_intel/tools/distill_tools.py +353 -0
  325. attocode_intel/tools/frecency_tools.py +220 -0
  326. attocode_intel/tools/fuzzy_tools.py +255 -0
  327. attocode_intel/tools/history_tools.py +486 -0
  328. attocode_intel/tools/learning_tools.py +222 -0
  329. attocode_intel/tools/lsp_tools.py +218 -0
  330. attocode_intel/tools/maintenance_tools.py +1662 -0
  331. attocode_intel/tools/navigation_tools.py +220 -0
  332. attocode_intel/tools/overlay_tools.py +540 -0
  333. attocode_intel/tools/pin_store.py +432 -0
  334. attocode_intel/tools/pin_tools.py +266 -0
  335. attocode_intel/tools/query_constraints_tools.py +262 -0
  336. attocode_intel/tools/query_history_tools.py +158 -0
  337. attocode_intel/tools/readiness_tools.py +44 -0
  338. attocode_intel/tools/rule_tools.py +992 -0
  339. attocode_intel/tools/search_tools.py +770 -0
  340. attocode_intel/tools/snapshot_tools.py +624 -0
  341. attocode_intel/workers/__init__.py +1 -0
  342. attocode_intel/workers/job_utils.py +111 -0
  343. attocode_intel/workers/jobs.py +573 -0
  344. attocode_intel/workers/run.py +30 -0
  345. attocode_intel/workers/settings.py +61 -0
@@ -0,0 +1,1140 @@
1
+ """Language Server Protocol client manager.
2
+
3
+ Manages LSP clients for multiple programming languages,
4
+ providing definitions, completions, hover info, references,
5
+ and diagnostics via the LSP JSON-RPC protocol.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import logging
13
+ import os
14
+ import re
15
+ import shutil
16
+ from collections.abc import Callable
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from attocode_intel._internal.errors import ConfigurationError, ToolError
22
+ from attocode_intel._internal.types.events import SimpleEventListener
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # =============================================================================
27
+ # Types
28
+ # =============================================================================
29
+
30
+
31
+ @dataclass
32
+ class LSPPosition:
33
+ """Position in a document (0-indexed)."""
34
+
35
+ line: int
36
+ character: int
37
+
38
+
39
+ @dataclass
40
+ class LSPRange:
41
+ """Range in a document."""
42
+
43
+ start: LSPPosition
44
+ end: LSPPosition
45
+
46
+
47
+ @dataclass
48
+ class LSPLocation:
49
+ """Location in a document."""
50
+
51
+ uri: str
52
+ range: LSPRange
53
+
54
+
55
+ @dataclass
56
+ class LSPDiagnostic:
57
+ """Diagnostic message from a language server."""
58
+
59
+ range: LSPRange
60
+ message: str
61
+ severity: str = "error" # error, warning, information, hint
62
+ source: str | None = None
63
+ code: str | int | None = None
64
+
65
+
66
+ @dataclass
67
+ class LSPCompletion:
68
+ """Completion item from a language server."""
69
+
70
+ label: str
71
+ kind: str = "text"
72
+ detail: str | None = None
73
+ documentation: str | None = None
74
+ insert_text: str | None = None
75
+
76
+
77
+ @dataclass
78
+ class WSSymbol:
79
+ """A symbol found via workspace/symbol."""
80
+
81
+ name: str
82
+ kind: str # "class", "function", "method", "variable", etc.
83
+ uri: str
84
+ range: LSPRange
85
+ container_name: str | None = None
86
+ detail: str | None = None
87
+
88
+
89
+ @dataclass
90
+ class LanguageServerConfig:
91
+ """Configuration for a language server."""
92
+
93
+ command: str
94
+ args: list[str] = field(default_factory=list)
95
+ extensions: list[str] = field(default_factory=list)
96
+ language_id: str = ""
97
+
98
+
99
+ @dataclass
100
+ class LSPConfig:
101
+ """LSP manager configuration."""
102
+
103
+ enabled: bool = True
104
+ servers: dict[str, LanguageServerConfig] = field(default_factory=dict)
105
+ auto_detect: bool = True
106
+ timeout: float = 30.0
107
+ root_uri: str = ""
108
+
109
+
110
+ LSPEventListener = SimpleEventListener
111
+
112
+
113
+ # =============================================================================
114
+ # Built-in Server Configs
115
+ # =============================================================================
116
+
117
+ BUILTIN_SERVERS: dict[str, LanguageServerConfig] = {
118
+ "typescript": LanguageServerConfig(
119
+ command="typescript-language-server",
120
+ args=["--stdio"],
121
+ extensions=[".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"],
122
+ language_id="typescript",
123
+ ),
124
+ "python": LanguageServerConfig(
125
+ command="pyright-langserver",
126
+ args=["--stdio"],
127
+ extensions=[".py", ".pyi"],
128
+ language_id="python",
129
+ ),
130
+ "rust": LanguageServerConfig(
131
+ command="rust-analyzer",
132
+ args=[],
133
+ extensions=[".rs"],
134
+ language_id="rust",
135
+ ),
136
+ "go": LanguageServerConfig(
137
+ command="gopls",
138
+ args=[],
139
+ extensions=[".go"],
140
+ language_id="go",
141
+ ),
142
+ "json": LanguageServerConfig(
143
+ command="vscode-json-language-server",
144
+ args=["--stdio"],
145
+ extensions=[".json", ".jsonc"],
146
+ language_id="json",
147
+ ),
148
+ }
149
+
150
+ COMPLETION_KIND_MAP: dict[int, str] = {
151
+ 1: "text", 2: "method", 3: "function", 4: "constructor",
152
+ 5: "field", 6: "variable", 7: "class", 8: "interface",
153
+ 9: "module", 10: "property", 14: "keyword", 15: "snippet",
154
+ }
155
+
156
+
157
+ # =============================================================================
158
+ # LSP Client (Internal)
159
+ # =============================================================================
160
+
161
+
162
+ class _LSPClient:
163
+ """Internal LSP client for a single language server.
164
+
165
+ Communicates via JSON-RPC 2.0 over stdio using the
166
+ Content-Length header framing protocol.
167
+ """
168
+
169
+ def __init__(
170
+ self,
171
+ config: LanguageServerConfig,
172
+ root_uri: str,
173
+ timeout: float = 30.0,
174
+ on_diagnostics: Callable[[str, list[LSPDiagnostic]], None] | None = None,
175
+ ) -> None:
176
+ self._config = config
177
+ self._root_uri = root_uri
178
+ self._timeout = timeout
179
+ self._on_diagnostics = on_diagnostics
180
+
181
+ self._process: asyncio.subprocess.Process | None = None
182
+ self._request_id = 0
183
+ self._pending: dict[int, asyncio.Future[Any]] = {}
184
+ self._buffer = b""
185
+ self._notifications: set[asyncio.Task] = set()
186
+ self._initialized = False
187
+ self._reader_task: asyncio.Task[None] | None = None
188
+ self._stderr_task: asyncio.Task[None] | None = None
189
+ self._is_stopping = False # Suppress spurious errors during shutdown
190
+ self._trace_enabled = False # Protocol tracing (set via feature flag)
191
+
192
+ @property
193
+ def language_id(self) -> str:
194
+ return self._config.language_id
195
+
196
+ @property
197
+ def is_initialized(self) -> bool:
198
+ return self._initialized
199
+
200
+ def enable_protocol_trace(self, enabled: bool = True) -> None:
201
+ """Enable verbose LSP protocol tracing (logs sent/received messages)."""
202
+ self._trace_enabled = enabled
203
+
204
+ async def start(self) -> None:
205
+ """Start the language server and initialize it."""
206
+ self._process = await asyncio.create_subprocess_exec( # noqa: S603
207
+ self._config.command,
208
+ *self._config.args,
209
+ stdin=asyncio.subprocess.PIPE,
210
+ stdout=asyncio.subprocess.PIPE,
211
+ stderr=asyncio.subprocess.PIPE,
212
+ )
213
+
214
+ if not self._process.stdout or not self._process.stdin:
215
+ raise ToolError(f"Failed to spawn {self._config.command}", tool_name="lsp")
216
+
217
+ # Start reading responses in background
218
+ self._reader_task = asyncio.create_task(self._read_loop())
219
+ self._stderr_task = asyncio.create_task(self._drain_stderr())
220
+
221
+ # Initialize
222
+ await self._request("initialize", {
223
+ "processId": os.getpid(),
224
+ "rootUri": self._root_uri,
225
+ "capabilities": {
226
+ "textDocument": {
227
+ "completion": {"completionItem": {"snippetSupport": True}},
228
+ "hover": {},
229
+ "definition": {},
230
+ "references": {},
231
+ "publishDiagnostics": {},
232
+ },
233
+ },
234
+ })
235
+ self._notify("initialized", {})
236
+ self._initialized = True
237
+
238
+ async def stop(self) -> None:
239
+ """Full LSP server shutdown lifecycle.
240
+
241
+ Sequence (CC pattern):
242
+ 1. Send shutdown request (clean stop request to server)
243
+ 2. Send exit notification (server should terminate gracefully)
244
+ 3. Close stdin
245
+ 4. Wait for process to exit (with timeout)
246
+ 5. Kill if still alive
247
+ 6. Cancel pending requests
248
+
249
+ The _is_stopping flag prevents spurious errors from pending requests
250
+ during the shutdown sequence.
251
+ """
252
+ if not self._process or self._is_stopping:
253
+ return
254
+
255
+ self._is_stopping = True
256
+
257
+ try:
258
+ # Step 1: Send shutdown request
259
+ try:
260
+ await self._request("shutdown", None)
261
+ logger.debug("LSP %s: shutdown request sent", self._config.language_id)
262
+ except TimeoutError:
263
+ logger.warning("LSP %s: shutdown request timed out", self._config.language_id)
264
+ except Exception as exc:
265
+ logger.debug(
266
+ "LSP %s: shutdown request error (non-fatal): %s",
267
+ self._config.language_id, exc,
268
+ )
269
+
270
+ # Step 2: Send exit notification
271
+ try:
272
+ self._notify("exit", None)
273
+ logger.debug("LSP %s: exit notification sent", self._config.language_id)
274
+ except Exception as exc:
275
+ logger.debug(
276
+ "LSP %s: exit notification error (non-fatal): %s",
277
+ self._config.language_id, exc,
278
+ )
279
+
280
+ # Brief pause to let server clean up
281
+ await asyncio.sleep(0.1)
282
+
283
+ # Step 3: Close stdin
284
+ try:
285
+ if self._process.stdin:
286
+ self._process.stdin.close()
287
+ except Exception:
288
+ pass
289
+
290
+ # Step 4: Wait for graceful exit
291
+ try:
292
+ retcode = await asyncio.wait_for(
293
+ self._process.wait(),
294
+ timeout=self._timeout,
295
+ )
296
+ logger.debug(
297
+ "LSP %s: exited gracefully with code %s",
298
+ self._config.language_id, retcode,
299
+ )
300
+ # Reader loop should drain EOF naturally, but cancel if still alive
301
+ try:
302
+ await asyncio.wait_for(self._reader_task, timeout=1.0)
303
+ except TimeoutError:
304
+ if self._reader_task and not self._reader_task.done():
305
+ self._reader_task.cancel()
306
+ try:
307
+ await self._reader_task
308
+ except asyncio.CancelledError:
309
+ pass
310
+ except TimeoutError:
311
+ logger.warning("LSP %s: did not exit gracefully, killing", self._config.language_id)
312
+
313
+ # Step 5: Kill if still alive, then cancel the reader
314
+ killed = False
315
+ try:
316
+ self._process.kill()
317
+ killed = True
318
+ except Exception as exc:
319
+ logger.warning("LSP %s: kill failed: %s", self._config.language_id, exc)
320
+
321
+ if killed:
322
+ # Give reader loop a moment to notice EOF, then cancel it
323
+ try:
324
+ await asyncio.wait_for(self._reader_task, timeout=1.0)
325
+ except TimeoutError:
326
+ if self._reader_task and not self._reader_task.done():
327
+ self._reader_task.cancel()
328
+ try:
329
+ await asyncio.wait_for(self._reader_task, timeout=2.0)
330
+ except (TimeoutError, asyncio.CancelledError):
331
+ pass
332
+ except asyncio.CancelledError:
333
+ pass
334
+ try:
335
+ await asyncio.wait_for(self._process.wait(), timeout=1.0)
336
+ logger.info("LSP %s: killed", self._config.language_id)
337
+ except TimeoutError:
338
+ logger.warning("LSP %s: process still alive after kill + cancel", self._config.language_id)
339
+
340
+ finally:
341
+ if self._stderr_task is not None:
342
+ self._stderr_task.cancel()
343
+ await asyncio.gather(self._stderr_task, return_exceptions=True)
344
+ self._stderr_task = None
345
+ self._process = None
346
+ self._initialized = False
347
+ self._reader_task = None
348
+ self._is_stopping = False
349
+
350
+ # Step 6: Cancel pending requests
351
+ for future in self._pending.values():
352
+ if not future.done():
353
+ future.cancel()
354
+ self._pending.clear()
355
+
356
+ async def get_definition(
357
+ self, uri: str, line: int, character: int
358
+ ) -> LSPLocation | None:
359
+ """Get symbol definition location."""
360
+ if not self._initialized:
361
+ return None
362
+
363
+ result = await self._request("textDocument/definition", {
364
+ "textDocument": {"uri": uri},
365
+ "position": {"line": line, "character": character},
366
+ })
367
+ if not result:
368
+ return None
369
+
370
+ loc = result[0] if isinstance(result, list) else result
371
+ if not loc or not isinstance(loc, dict):
372
+ return None
373
+
374
+ return LSPLocation(
375
+ uri=loc["uri"],
376
+ range=_parse_range(loc["range"]),
377
+ )
378
+
379
+ async def get_completions(
380
+ self, uri: str, line: int, character: int
381
+ ) -> list[LSPCompletion]:
382
+ """Get completion items at position."""
383
+ if not self._initialized:
384
+ return []
385
+
386
+ result = await self._request("textDocument/completion", {
387
+ "textDocument": {"uri": uri},
388
+ "position": {"line": line, "character": character},
389
+ })
390
+ if not result:
391
+ return []
392
+
393
+ items: list[Any]
394
+ if isinstance(result, list):
395
+ items = result
396
+ elif isinstance(result, dict):
397
+ items = result.get("items", [])
398
+ else:
399
+ items = []
400
+
401
+ completions: list[LSPCompletion] = []
402
+ for item in items:
403
+ if not isinstance(item, dict):
404
+ continue
405
+ doc = item.get("documentation")
406
+ if isinstance(doc, dict):
407
+ doc = doc.get("value", "")
408
+ completions.append(LSPCompletion(
409
+ label=str(item.get("label", "")),
410
+ kind=COMPLETION_KIND_MAP.get(item.get("kind", 1), "text"),
411
+ detail=item.get("detail"),
412
+ documentation=doc if isinstance(doc, str) else None,
413
+ insert_text=item.get("insertText") or item.get("label"),
414
+ ))
415
+ return completions
416
+
417
+ async def get_hover(
418
+ self, uri: str, line: int, character: int
419
+ ) -> str | None:
420
+ """Get hover information at position."""
421
+ if not self._initialized:
422
+ return None
423
+
424
+ result = await self._request("textDocument/hover", {
425
+ "textDocument": {"uri": uri},
426
+ "position": {"line": line, "character": character},
427
+ })
428
+ if not result or not isinstance(result, dict):
429
+ return None
430
+
431
+ contents = result.get("contents")
432
+ if not contents:
433
+ return None
434
+ if isinstance(contents, str):
435
+ return contents
436
+ if isinstance(contents, list):
437
+ parts = []
438
+ for c in contents:
439
+ if isinstance(c, str):
440
+ parts.append(c)
441
+ elif isinstance(c, dict):
442
+ parts.append(c.get("value", ""))
443
+ return "\n".join(parts)
444
+ if isinstance(contents, dict):
445
+ return contents.get("value")
446
+ return None
447
+
448
+ async def get_references(
449
+ self,
450
+ uri: str,
451
+ line: int,
452
+ character: int,
453
+ include_declaration: bool = True,
454
+ ) -> list[LSPLocation]:
455
+ """Get all references to a symbol."""
456
+ if not self._initialized:
457
+ return []
458
+
459
+ result = await self._request("textDocument/references", {
460
+ "textDocument": {"uri": uri},
461
+ "position": {"line": line, "character": character},
462
+ "context": {"includeDeclaration": include_declaration},
463
+ })
464
+ if not result or not isinstance(result, list):
465
+ return []
466
+
467
+ return [
468
+ LSPLocation(uri=loc["uri"], range=_parse_range(loc["range"]))
469
+ for loc in result
470
+ if isinstance(loc, dict) and "uri" in loc and "range" in loc
471
+ ]
472
+
473
+ # LSP kind numbers: 1=text, 2=method, 3=function, 4=class, 5=interface,
474
+ # 6=enum, 7=keyword, 9=variable, 11=constant, 12=struct,
475
+ # 14=property, 23=namespace, 24=package, 25=typeParameter
476
+ _SYMBOL_KIND_MAP: dict[int, str] = {
477
+ 1: "text", 2: "method", 3: "function", 4: "class", 5: "interface",
478
+ 6: "enum", 7: "keyword", 9: "variable", 11: "constant", 12: "struct",
479
+ 14: "property", 23: "namespace", 24: "package", 25: "typeParameter",
480
+ }
481
+
482
+ async def search_symbols(
483
+ self, query: str, max_results: int = 50
484
+ ) -> list[WSSymbol]:
485
+ """Search workspace symbols by name using workspace/symbol.
486
+
487
+ Uses the LSP's indexed symbol search — much faster and more
488
+ accurate than grep for finding function/class/constant definitions.
489
+
490
+ Args:
491
+ query: Search query (partial name match).
492
+ max_results: Cap on results to avoid large responses.
493
+
494
+ Returns:
495
+ List of matching WSSymbol objects.
496
+ """
497
+ if not self._initialized:
498
+ return []
499
+
500
+ result = await self._request("workspace/symbol", {
501
+ "query": query,
502
+ })
503
+ if not result or not isinstance(result, list):
504
+ return []
505
+
506
+ symbols: list[WSSymbol] = []
507
+ for item in result[:max_results]:
508
+ if not isinstance(item, dict):
509
+ continue
510
+ symbols.append(WSSymbol(
511
+ name=str(item.get("name", "")),
512
+ kind=self._SYMBOL_KIND_MAP.get(
513
+ item.get("kind", 1), str(item.get("kind", 1))
514
+ ),
515
+ uri=str(item.get("location", {}).get("uri", "")),
516
+ range=_parse_range(item.get("location", {}).get("range", {})),
517
+ container_name=item.get("containerName"),
518
+ detail=item.get("detail"),
519
+ ))
520
+ return symbols
521
+
522
+ async def prepare_call_hierarchy(
523
+ self, uri: str, line: int, character: int
524
+ ) -> list[dict[str, Any]] | None:
525
+ """Step 1 of call hierarchy: prepare the symbol at position.
526
+
527
+ Returns raw call hierarchy items from the server, or None if
528
+ the server doesn't support callHierarchy.
529
+ """
530
+ if not self._initialized:
531
+ return None
532
+
533
+ try:
534
+ result = await self._request("textDocument/prepareCallHierarchy", {
535
+ "textDocument": {"uri": uri},
536
+ "position": {"line": line, "character": character},
537
+ })
538
+ except Exception:
539
+ return None
540
+
541
+ if not result:
542
+ return None
543
+ items: list[dict[str, Any]] = result if isinstance(result, list) else [result]
544
+ return [item for item in items if isinstance(item, dict)]
545
+
546
+ async def incoming_calls(
547
+ self, item: dict[str, Any]
548
+ ) -> list[dict[str, Any]]:
549
+ """Step 2a: get incoming calls — who calls this symbol."""
550
+ if not self._initialized:
551
+ return []
552
+
553
+ try:
554
+ result = await self._request("callHierarchy/incomingCalls", {
555
+ "item": item,
556
+ })
557
+ except Exception:
558
+ return []
559
+
560
+ return result if isinstance(result, list) else []
561
+
562
+ async def outgoing_calls(
563
+ self, item: dict[str, Any]
564
+ ) -> list[dict[str, Any]]:
565
+ """Step 2b: get outgoing calls — what this symbol calls."""
566
+ if not self._initialized:
567
+ return []
568
+
569
+ try:
570
+ result = await self._request("callHierarchy/outgoingCalls", {
571
+ "item": item,
572
+ })
573
+ except Exception:
574
+ return []
575
+
576
+ return result if isinstance(result, list) else []
577
+
578
+ def notify_document_open(self, uri: str, text: str) -> None:
579
+ """Notify server about file open."""
580
+ if not self._initialized:
581
+ return
582
+ self._notify("textDocument/didOpen", {
583
+ "textDocument": {
584
+ "uri": uri,
585
+ "languageId": ("typescriptreact" if uri.endswith(".tsx") else
586
+ "javascriptreact" if uri.endswith(".jsx") else
587
+ "javascript" if uri.endswith((".js", ".mjs", ".cjs")) else
588
+ self._config.language_id),
589
+ "version": 1,
590
+ "text": text,
591
+ },
592
+ })
593
+
594
+ def notify_document_change(
595
+ self, uri: str, text: str, version: int = 1
596
+ ) -> None:
597
+ """Notify server about file change."""
598
+ if not self._initialized:
599
+ return
600
+ self._notify("textDocument/didChange", {
601
+ "textDocument": {"uri": uri, "version": version},
602
+ "contentChanges": [{"text": text}],
603
+ })
604
+
605
+ def notify_document_close(self, uri: str) -> None:
606
+ """Notify server about file close."""
607
+ if not self._initialized:
608
+ return
609
+ self._notify("textDocument/didClose", {
610
+ "textDocument": {"uri": uri},
611
+ })
612
+
613
+ # Internal protocol methods
614
+
615
+ async def _request(self, method: str, params: Any) -> Any:
616
+ """Send a JSON-RPC request and wait for response."""
617
+ if not self._process or not self._process.stdin:
618
+ raise ToolError("LSP server not running", tool_name="lsp")
619
+
620
+ self._request_id += 1
621
+ req_id = self._request_id
622
+
623
+ loop = asyncio.get_running_loop()
624
+ future: asyncio.Future[Any] = loop.create_future()
625
+ self._pending[req_id] = future
626
+
627
+ message = {"jsonrpc": "2.0", "id": req_id, "method": method, "params": params}
628
+ try:
629
+ # initialized/didOpen/didChange must reach the server before the query.
630
+ if self._notifications:
631
+ await asyncio.gather(*tuple(self._notifications))
632
+ await self._send_message(message)
633
+ return await asyncio.wait_for(future, timeout=self._timeout)
634
+ except TimeoutError as exc:
635
+ raise ToolError(f"Request {method} timed out", tool_name="lsp") from exc
636
+ finally:
637
+ self._pending.pop(req_id, None)
638
+
639
+ def _notify(self, method: str, params: Any) -> None:
640
+ """Send a JSON-RPC notification (no response expected)."""
641
+ message = {"jsonrpc": "2.0", "method": method, "params": params}
642
+ task = asyncio.ensure_future(self._send_message(message))
643
+ self._notifications.add(task)
644
+ task.add_done_callback(self._notification_done)
645
+
646
+ def _notification_done(self, task: asyncio.Task) -> None:
647
+ self._notifications.discard(task)
648
+ if not task.cancelled() and (error := task.exception()) is not None:
649
+ logger.debug("LSP notification failed: %s", error)
650
+
651
+ async def _drain_stderr(self) -> None:
652
+ # An unread pipe can block an otherwise healthy language server.
653
+ if self._process is None or self._process.stderr is None:
654
+ return
655
+ try:
656
+ while chunk := await self._process.stderr.read(4096):
657
+ logger.debug("LSP stderr: %s", chunk.decode(errors="replace")[:1000])
658
+ except (OSError, asyncio.CancelledError):
659
+ pass
660
+
661
+ async def _send_message(self, message: dict[str, Any]) -> None:
662
+ """Send a message with Content-Length header framing.
663
+
664
+ When _trace_enabled is True, logs the sent message at DEBUG level
665
+ (method name and params, no full content).
666
+ """
667
+ if not self._process or not self._process.stdin:
668
+ return
669
+ content = json.dumps(message).encode()
670
+ header = f"Content-Length: {len(content)}\r\n\r\n".encode()
671
+ self._process.stdin.write(header + content)
672
+ await self._process.stdin.drain()
673
+
674
+ if self._trace_enabled:
675
+ method = message.get("method", "?")
676
+ params = message.get("params", {})
677
+ # Truncate params for logging
678
+ params_str = json.dumps(params, default=str)
679
+ if len(params_str) > 200:
680
+ params_str = params_str[:197] + "..."
681
+ logger.debug("LSP OUT: %s %s", method, params_str)
682
+
683
+ async def _read_loop(self) -> None:
684
+ """Background loop reading LSP responses.
685
+
686
+ Also captures stderr for server diagnostics when tracing is enabled.
687
+ """
688
+ if self._process is None:
689
+ return
690
+
691
+ # Start stderr reader if tracing is enabled
692
+ stderr_task: asyncio.Task[None] | None = None
693
+ if self._process.stderr:
694
+ async def _read_stderr() -> None:
695
+ try:
696
+ stderr_lines: list[str] = []
697
+ while True:
698
+ line = await self._process.stderr.readline()
699
+ if not line:
700
+ break
701
+ decoded = line.decode(errors="replace").strip()
702
+ if decoded:
703
+ stderr_lines.append(decoded)
704
+ if self._trace_enabled and stderr_lines:
705
+ logger.debug("LSP STDERR: %s", decoded)
706
+ except Exception:
707
+ pass
708
+
709
+ stderr_task = asyncio.create_task(_read_stderr())
710
+
711
+ try:
712
+ if self._process.stdout:
713
+ while True:
714
+ data = await self._process.stdout.read(8192)
715
+ if not data:
716
+ break
717
+ self._buffer += data
718
+ self._process_buffer()
719
+ except asyncio.CancelledError:
720
+ pass
721
+ finally:
722
+ if stderr_task:
723
+ stderr_task.cancel()
724
+ try:
725
+ await stderr_task
726
+ except asyncio.CancelledError:
727
+ pass
728
+
729
+ def _process_buffer(self) -> None:
730
+ """Parse complete messages from the buffer."""
731
+ while True:
732
+ header_end = self._buffer.find(b"\r\n\r\n")
733
+ if header_end == -1:
734
+ break
735
+
736
+ header = self._buffer[:header_end].decode(errors="replace")
737
+ match = re.search(r"Content-Length:\s*(\d+)", header, re.IGNORECASE)
738
+ if not match:
739
+ self._buffer = self._buffer[header_end + 4:]
740
+ continue
741
+
742
+ content_length = int(match.group(1))
743
+ message_start = header_end + 4
744
+ message_end = message_start + content_length
745
+
746
+ if len(self._buffer) < message_end:
747
+ break # Not enough data yet
748
+
749
+ content = self._buffer[message_start:message_end]
750
+ self._buffer = self._buffer[message_end:]
751
+
752
+ try:
753
+ msg = json.loads(content)
754
+ self._handle_message(msg)
755
+ except json.JSONDecodeError:
756
+ pass
757
+
758
+ def _handle_message(self, message: dict[str, Any]) -> None:
759
+ """Handle an incoming JSON-RPC message.
760
+
761
+ When _trace_enabled is True, logs received messages at DEBUG level.
762
+ """
763
+ if self._trace_enabled:
764
+ method = message.get("method", "?")
765
+ result = message.get("result")
766
+ error = message.get("error")
767
+ msg_id = message.get("id")
768
+ if method == "?":
769
+ # Response — log result/error summary
770
+ if error:
771
+ logger.debug("LSP IN [resp]: id=%s error=%s", msg_id, error)
772
+ else:
773
+ result_str = json.dumps(result, default=str)
774
+ if len(result_str) > 200:
775
+ result_str = result_str[:197] + "..."
776
+ logger.debug("LSP IN [resp]: id=%s result=%s", msg_id, result_str)
777
+ else:
778
+ # Notification
779
+ logger.debug("LSP IN [notif]: %s", method)
780
+
781
+ msg_id = message.get("id")
782
+ if msg_id is not None and "method" in message:
783
+ method = message["method"]
784
+ params = message.get("params") or {}
785
+ response = {"jsonrpc": "2.0", "id": msg_id}
786
+ if method == "workspace/configuration":
787
+ response["result"] = [None for _ in params.get("items", [])]
788
+ elif method == "workspace/workspaceFolders":
789
+ response["result"] = [{"uri": self._root_uri, "name": "workspace"}]
790
+ elif method == "workspace/applyEdit":
791
+ response["result"] = {"applied": False, "failureReason": "Analysis client is read-only"}
792
+ elif method in {"client/registerCapability", "client/unregisterCapability", "window/workDoneProgress/create"}:
793
+ response["result"] = None
794
+ else:
795
+ response["error"] = {"code": -32601, "message": "Method not supported"}
796
+ task = asyncio.ensure_future(self._send_message(response))
797
+ self._notifications.add(task)
798
+ task.add_done_callback(self._notification_done)
799
+ elif msg_id is not None and msg_id in self._pending:
800
+ future = self._pending.pop(msg_id)
801
+ if future.done():
802
+ return
803
+ error = message.get("error")
804
+ if error:
805
+ future.set_exception(
806
+ ToolError(error.get("message", "Unknown error"), tool_name="lsp")
807
+ )
808
+ else:
809
+ future.set_result(message.get("result"))
810
+ elif "method" in message:
811
+ self._handle_notification(message["method"], message.get("params"))
812
+
813
+ def _handle_notification(self, method: str, params: Any) -> None:
814
+ """Handle server notification."""
815
+ if method == "textDocument/publishDiagnostics" and self._on_diagnostics:
816
+ uri = params.get("uri", "")
817
+ raw_diags = params.get("diagnostics", [])
818
+ severity_map = {1: "error", 2: "warning", 3: "information", 4: "hint"}
819
+ diagnostics = []
820
+ for d in raw_diags:
821
+ r = d.get("range", {})
822
+ diagnostics.append(LSPDiagnostic(
823
+ range=_parse_range(r),
824
+ message=d.get("message", ""),
825
+ severity=severity_map.get(d.get("severity", 1), "error"),
826
+ source=d.get("source"),
827
+ code=d.get("code"),
828
+ ))
829
+ self._on_diagnostics(uri, diagnostics)
830
+
831
+
832
+ # Alias for external use
833
+ LSPClient = _LSPClient
834
+
835
+
836
+ def _parse_range(r: dict[str, Any]) -> LSPRange:
837
+ """Parse an LSP range from a dict."""
838
+ start = r.get("start", {})
839
+ end = r.get("end", {})
840
+ return LSPRange(
841
+ start=LSPPosition(
842
+ line=start.get("line", 0),
843
+ character=start.get("character", 0),
844
+ ),
845
+ end=LSPPosition(
846
+ line=end.get("line", 0),
847
+ character=end.get("character", 0),
848
+ ),
849
+ )
850
+
851
+
852
+ # =============================================================================
853
+ # LSP Manager
854
+ # =============================================================================
855
+
856
+
857
+ class LSPManager:
858
+ """Manages multiple LSP clients for different languages.
859
+
860
+ Auto-detects project languages and starts appropriate servers.
861
+ Provides unified API for definitions, completions, hover, and refs.
862
+ """
863
+
864
+ def __init__(self, config: LSPConfig | None = None) -> None:
865
+ cfg = config or LSPConfig()
866
+ self._enabled = cfg.enabled
867
+ self._servers = {**BUILTIN_SERVERS, **cfg.servers}
868
+ self._auto_detect = cfg.auto_detect
869
+ self._timeout = cfg.timeout
870
+ self._root_uri = cfg.root_uri or f"file://{os.getcwd()}"
871
+
872
+ self._clients: dict[str, _LSPClient] = {}
873
+ self._listeners: set[LSPEventListener] = set()
874
+ self._diagnostics_cache: dict[str, list[LSPDiagnostic]] = {}
875
+ # Optional callback: (tool_name, file_path, results, query) -> None.
876
+ # Fired after get_definition/get_references to feed results back
877
+ # into the cross-reference index. ``query`` carries the original
878
+ # request position (``{"line": int, "col": int}``) so the consumer
879
+ # can resolve which symbol was queried — needed to attribute LSP
880
+ # references to caller→callee call-graph edges.
881
+ self.on_result_callback: (
882
+ Callable[[str, str, list, dict | None], None] | None
883
+ ) = None
884
+
885
+ def has_clients(self) -> bool:
886
+ """Return True if any LSP clients are running."""
887
+ return bool(self._clients)
888
+
889
+ async def auto_start(self, workspace_root: str | None = None) -> list[str]:
890
+ """Auto-detect and start LSP servers for detected languages."""
891
+ if not self._enabled or not self._auto_detect:
892
+ return []
893
+
894
+ root_uri = f"file://{workspace_root}" if workspace_root else self._root_uri
895
+ root_path = workspace_root or os.getcwd()
896
+
897
+ detected = self._detect_languages(root_path)
898
+ started: list[str] = []
899
+
900
+ for lang_id in detected:
901
+ try:
902
+ await self.start_server(lang_id, root_uri)
903
+ started.append(lang_id)
904
+ except Exception as exc:
905
+ self._emit("lsp.error", {
906
+ "language_id": lang_id, "error": str(exc),
907
+ })
908
+
909
+ return started
910
+
911
+ async def start_server(
912
+ self, language_id: str, root_uri: str | None = None
913
+ ) -> None:
914
+ """Start a specific language server."""
915
+ if language_id in self._clients:
916
+ return
917
+
918
+ server_config = self._servers.get(language_id)
919
+ if not server_config:
920
+ raise ConfigurationError(
921
+ f"No server configuration for language: {language_id}"
922
+ )
923
+
924
+ if not shutil.which(server_config.command):
925
+ raise ConfigurationError(
926
+ f"Language server not found: {server_config.command}"
927
+ )
928
+
929
+ def on_diags(uri: str, diags: list[LSPDiagnostic]) -> None:
930
+ self._diagnostics_cache[uri] = diags
931
+ self._emit("lsp.diagnostics", {"uri": uri, "diagnostics": diags})
932
+
933
+ client = _LSPClient(
934
+ server_config,
935
+ root_uri or self._root_uri,
936
+ self._timeout,
937
+ on_diags,
938
+ )
939
+
940
+ # Wire protocol tracing from feature flag
941
+ from attocode_intel._internal.integrations.feature_flags import feature
942
+ if feature("LSP_PROTOCOL_TRACE"):
943
+ client.enable_protocol_trace(True)
944
+
945
+ try:
946
+ await client.start()
947
+ except BaseException:
948
+ await client.stop()
949
+ raise
950
+ self._clients[language_id] = client
951
+ self._emit("lsp.started", {
952
+ "language_id": language_id,
953
+ "command": server_config.command,
954
+ })
955
+
956
+ async def stop_server(self, language_id: str) -> None:
957
+ """Stop a specific language server."""
958
+ client = self._clients.pop(language_id, None)
959
+ if client:
960
+ await client.stop()
961
+ self._emit("lsp.stopped", {"language_id": language_id})
962
+
963
+ async def stop_all(self) -> None:
964
+ """Stop all language servers."""
965
+ tasks = [self.stop_server(lid) for lid in list(self._clients)]
966
+ await asyncio.gather(*tasks, return_exceptions=True)
967
+
968
+ async def get_definition(
969
+ self, file: str, line: int, col: int
970
+ ) -> LSPLocation | None:
971
+ """Get symbol definition."""
972
+ client = self._get_client_for_file(file)
973
+ if not client:
974
+ return None
975
+ uri = self._to_uri(file)
976
+ result = await client.get_definition(uri, line, col)
977
+ if result and self.on_result_callback:
978
+ try:
979
+ self.on_result_callback(
980
+ "definition", file, [result], {"line": line, "col": col},
981
+ )
982
+ except Exception:
983
+ pass # don't let callback errors break LSP flow
984
+ return result
985
+
986
+ async def get_completions(
987
+ self, file: str, line: int, col: int
988
+ ) -> list[LSPCompletion]:
989
+ """Get completions at position."""
990
+ client = self._get_client_for_file(file)
991
+ if not client:
992
+ return []
993
+ uri = self._to_uri(file)
994
+ return await client.get_completions(uri, line, col)
995
+
996
+ async def get_hover(
997
+ self, file: str, line: int, col: int
998
+ ) -> str | None:
999
+ """Get hover information."""
1000
+ client = self._get_client_for_file(file)
1001
+ if not client:
1002
+ return None
1003
+ uri = self._to_uri(file)
1004
+ return await client.get_hover(uri, line, col)
1005
+
1006
+ async def get_references(
1007
+ self, file: str, line: int, col: int,
1008
+ include_declaration: bool = True,
1009
+ ) -> list[LSPLocation]:
1010
+ """Get all references to a symbol."""
1011
+ client = self._get_client_for_file(file)
1012
+ if not client:
1013
+ return []
1014
+ uri = self._to_uri(file)
1015
+ results = await client.get_references(uri, line, col, include_declaration)
1016
+ if results and self.on_result_callback:
1017
+ try:
1018
+ self.on_result_callback(
1019
+ "references", file, results, {"line": line, "col": col},
1020
+ )
1021
+ except Exception:
1022
+ pass # don't let callback errors break LSP flow
1023
+ return results
1024
+
1025
+ async def search_symbols(
1026
+ self, query: str, max_results: int = 50
1027
+ ) -> list[WSSymbol]:
1028
+ """Search workspace symbols across all running language servers.
1029
+
1030
+ Queries each active LSP client and aggregates results.
1031
+ Falls back to the first available client if no file is specified.
1032
+
1033
+ Args:
1034
+ query: Search string (partial name match, case-insensitive).
1035
+ max_results: Maximum results per server.
1036
+
1037
+ Returns:
1038
+ Combined list of matching WSSymbol objects, sorted by name.
1039
+ """
1040
+ all_symbols: list[WSSymbol] = []
1041
+
1042
+ for lang_id, client in self._clients.items():
1043
+ if not client.is_initialized:
1044
+ continue
1045
+ try:
1046
+ symbols = await client.search_symbols(query, max_results)
1047
+ all_symbols.extend(symbols)
1048
+ except Exception:
1049
+ # One server failed — keep going
1050
+ pass
1051
+
1052
+ # Sort by name for consistent output
1053
+ all_symbols.sort(key=lambda s: s.name)
1054
+ return all_symbols[:max_results]
1055
+
1056
+ def get_diagnostics(self, file: str) -> list[LSPDiagnostic]:
1057
+ """Get cached diagnostics for a file."""
1058
+ uri = self._to_uri(file)
1059
+ return self._diagnostics_cache.get(uri, [])
1060
+
1061
+ def notify_file_opened(self, file: str, content: str) -> None:
1062
+ """Notify about file open."""
1063
+ client = self._get_client_for_file(file)
1064
+ if client:
1065
+ client.notify_document_open(self._to_uri(file), content)
1066
+
1067
+ def notify_file_changed(
1068
+ self, file: str, content: str, version: int = 1
1069
+ ) -> None:
1070
+ """Notify about file change."""
1071
+ client = self._get_client_for_file(file)
1072
+ if client:
1073
+ client.notify_document_change(self._to_uri(file), content, version)
1074
+
1075
+ def notify_file_closed(self, file: str) -> None:
1076
+ """Notify about file close."""
1077
+ client = self._get_client_for_file(file)
1078
+ if client:
1079
+ client.notify_document_close(self._to_uri(file))
1080
+
1081
+ def get_active_servers(self) -> list[str]:
1082
+ """Get list of active language server IDs."""
1083
+ return list(self._clients.keys())
1084
+
1085
+ def is_server_running(self, language_id: str) -> bool:
1086
+ """Check if a language server is running."""
1087
+ client = self._clients.get(language_id)
1088
+ return client.is_initialized if client else False
1089
+
1090
+ def on(self, listener: LSPEventListener) -> Callable[[], None]:
1091
+ """Subscribe to LSP events. Returns unsubscribe function."""
1092
+ self._listeners.add(listener)
1093
+ return lambda: self._listeners.discard(listener)
1094
+
1095
+ async def cleanup(self) -> None:
1096
+ """Stop all servers and clean up resources."""
1097
+ await self.stop_all()
1098
+ self._listeners.clear()
1099
+ self._diagnostics_cache.clear()
1100
+
1101
+ # Internal helpers
1102
+
1103
+ @staticmethod
1104
+ def _to_uri(file: str) -> str:
1105
+ """Convert file path to URI."""
1106
+ if file.startswith("file://"):
1107
+ return file
1108
+ return f"file://{os.path.abspath(file)}"
1109
+
1110
+ def _get_client_for_file(self, file: str) -> _LSPClient | None:
1111
+ """Find the appropriate client for a file extension."""
1112
+ ext = Path(file).suffix.lower()
1113
+ for lang_id, server_config in self._servers.items():
1114
+ if ext in server_config.extensions:
1115
+ return self._clients.get(lang_id)
1116
+ return None
1117
+
1118
+ @staticmethod
1119
+ def _detect_languages(root_path: str) -> list[str]:
1120
+ """Detect languages based on project files."""
1121
+ detected: set[str] = set()
1122
+ check_files = [
1123
+ ("package.json", "typescript"),
1124
+ ("tsconfig.json", "typescript"),
1125
+ ("pyproject.toml", "python"),
1126
+ ("requirements.txt", "python"),
1127
+ ("Cargo.toml", "rust"),
1128
+ ("go.mod", "go"),
1129
+ ]
1130
+ for filename, language in check_files:
1131
+ if (Path(root_path) / filename).exists():
1132
+ detected.add(language)
1133
+ return list(detected)
1134
+
1135
+ def _emit(self, event: str, data: dict[str, Any]) -> None:
1136
+ for listener in self._listeners:
1137
+ try:
1138
+ listener(event, data)
1139
+ except Exception:
1140
+ pass