seer-mcp 0.1.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 (371) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/LICENSE +176 -0
  3. package/README.md +272 -0
  4. package/README_dev.md +199 -0
  5. package/dist/bundle/ci.d.ts +47 -0
  6. package/dist/bundle/ci.d.ts.map +1 -0
  7. package/dist/bundle/ci.js +113 -0
  8. package/dist/bundle/ci.js.map +1 -0
  9. package/dist/bundle/contract.d.ts +111 -0
  10. package/dist/bundle/contract.d.ts.map +1 -0
  11. package/dist/bundle/contract.js +352 -0
  12. package/dist/bundle/contract.js.map +1 -0
  13. package/dist/bundle/export.d.ts +36 -0
  14. package/dist/bundle/export.d.ts.map +1 -0
  15. package/dist/bundle/export.js +152 -0
  16. package/dist/bundle/export.js.map +1 -0
  17. package/dist/bundle/external.d.ts +66 -0
  18. package/dist/bundle/external.d.ts.map +1 -0
  19. package/dist/bundle/external.js +238 -0
  20. package/dist/bundle/external.js.map +1 -0
  21. package/dist/bundle/format.d.ts +94 -0
  22. package/dist/bundle/format.d.ts.map +1 -0
  23. package/dist/bundle/format.js +42 -0
  24. package/dist/bundle/format.js.map +1 -0
  25. package/dist/bundle/import.d.ts +49 -0
  26. package/dist/bundle/import.d.ts.map +1 -0
  27. package/dist/bundle/import.js +116 -0
  28. package/dist/bundle/import.js.map +1 -0
  29. package/dist/cli/index.d.ts +3 -0
  30. package/dist/cli/index.d.ts.map +1 -0
  31. package/dist/cli/index.js +1402 -0
  32. package/dist/cli/index.js.map +1 -0
  33. package/dist/cli/init.d.ts +48 -0
  34. package/dist/cli/init.d.ts.map +1 -0
  35. package/dist/cli/init.js +284 -0
  36. package/dist/cli/init.js.map +1 -0
  37. package/dist/db/schema.d.ts +3 -0
  38. package/dist/db/schema.d.ts.map +1 -0
  39. package/dist/db/schema.js +616 -0
  40. package/dist/db/schema.js.map +1 -0
  41. package/dist/db/store.d.ts +1011 -0
  42. package/dist/db/store.d.ts.map +1 -0
  43. package/dist/db/store.js +3888 -0
  44. package/dist/db/store.js.map +1 -0
  45. package/dist/graph/pagerank.d.ts +9 -0
  46. package/dist/graph/pagerank.d.ts.map +1 -0
  47. package/dist/graph/pagerank.js +47 -0
  48. package/dist/graph/pagerank.js.map +1 -0
  49. package/dist/indexer/architecture.d.ts +72 -0
  50. package/dist/indexer/architecture.d.ts.map +1 -0
  51. package/dist/indexer/architecture.js +112 -0
  52. package/dist/indexer/architecture.js.map +1 -0
  53. package/dist/indexer/behavior.d.ts +75 -0
  54. package/dist/indexer/behavior.d.ts.map +1 -0
  55. package/dist/indexer/behavior.js +395 -0
  56. package/dist/indexer/behavior.js.map +1 -0
  57. package/dist/indexer/boundaries.d.ts +60 -0
  58. package/dist/indexer/boundaries.d.ts.map +1 -0
  59. package/dist/indexer/boundaries.js +366 -0
  60. package/dist/indexer/boundaries.js.map +1 -0
  61. package/dist/indexer/churn.d.ts +15 -0
  62. package/dist/indexer/churn.d.ts.map +1 -0
  63. package/dist/indexer/churn.js +49 -0
  64. package/dist/indexer/churn.js.map +1 -0
  65. package/dist/indexer/classify.d.ts +9 -0
  66. package/dist/indexer/classify.d.ts.map +1 -0
  67. package/dist/indexer/classify.js +90 -0
  68. package/dist/indexer/classify.js.map +1 -0
  69. package/dist/indexer/context.d.ts +176 -0
  70. package/dist/indexer/context.d.ts.map +1 -0
  71. package/dist/indexer/context.js +193 -0
  72. package/dist/indexer/context.js.map +1 -0
  73. package/dist/indexer/continuity.d.ts +67 -0
  74. package/dist/indexer/continuity.d.ts.map +1 -0
  75. package/dist/indexer/continuity.js +288 -0
  76. package/dist/indexer/continuity.js.map +1 -0
  77. package/dist/indexer/detectchanges.d.ts +32 -0
  78. package/dist/indexer/detectchanges.d.ts.map +1 -0
  79. package/dist/indexer/detectchanges.js +74 -0
  80. package/dist/indexer/detectchanges.js.map +1 -0
  81. package/dist/indexer/discovery.d.ts +37 -0
  82. package/dist/indexer/discovery.d.ts.map +1 -0
  83. package/dist/indexer/discovery.js +136 -0
  84. package/dist/indexer/discovery.js.map +1 -0
  85. package/dist/indexer/externaldeps.d.ts +18 -0
  86. package/dist/indexer/externaldeps.d.ts.map +1 -0
  87. package/dist/indexer/externaldeps.js +288 -0
  88. package/dist/indexer/externaldeps.js.map +1 -0
  89. package/dist/indexer/freshness.d.ts +48 -0
  90. package/dist/indexer/freshness.d.ts.map +1 -0
  91. package/dist/indexer/freshness.js +128 -0
  92. package/dist/indexer/freshness.js.map +1 -0
  93. package/dist/indexer/git.d.ts +144 -0
  94. package/dist/indexer/git.d.ts.map +1 -0
  95. package/dist/indexer/git.js +444 -0
  96. package/dist/indexer/git.js.map +1 -0
  97. package/dist/indexer/index.d.ts +145 -0
  98. package/dist/indexer/index.d.ts.map +1 -0
  99. package/dist/indexer/index.js +930 -0
  100. package/dist/indexer/index.js.map +1 -0
  101. package/dist/indexer/modules.d.ts +62 -0
  102. package/dist/indexer/modules.d.ts.map +1 -0
  103. package/dist/indexer/modules.js +293 -0
  104. package/dist/indexer/modules.js.map +1 -0
  105. package/dist/indexer/preflight.d.ts +154 -0
  106. package/dist/indexer/preflight.d.ts.map +1 -0
  107. package/dist/indexer/preflight.js +399 -0
  108. package/dist/indexer/preflight.js.map +1 -0
  109. package/dist/indexer/protoScanner.d.ts +34 -0
  110. package/dist/indexer/protoScanner.d.ts.map +1 -0
  111. package/dist/indexer/protoScanner.js +133 -0
  112. package/dist/indexer/protoScanner.js.map +1 -0
  113. package/dist/indexer/risk.d.ts +115 -0
  114. package/dist/indexer/risk.d.ts.map +1 -0
  115. package/dist/indexer/risk.js +194 -0
  116. package/dist/indexer/risk.js.map +1 -0
  117. package/dist/indexer/serviceHostScanner.d.ts +25 -0
  118. package/dist/indexer/serviceHostScanner.d.ts.map +1 -0
  119. package/dist/indexer/serviceHostScanner.js +95 -0
  120. package/dist/indexer/serviceHostScanner.js.map +1 -0
  121. package/dist/indexer/serviceLinks.d.ts +105 -0
  122. package/dist/indexer/serviceLinks.d.ts.map +1 -0
  123. package/dist/indexer/serviceLinks.js +509 -0
  124. package/dist/indexer/serviceLinks.js.map +1 -0
  125. package/dist/indexer/shapehash.d.ts +98 -0
  126. package/dist/indexer/shapehash.d.ts.map +1 -0
  127. package/dist/indexer/shapehash.js +354 -0
  128. package/dist/indexer/shapehash.js.map +1 -0
  129. package/dist/indexer/skeleton.d.ts +15 -0
  130. package/dist/indexer/skeleton.d.ts.map +1 -0
  131. package/dist/indexer/skeleton.js +136 -0
  132. package/dist/indexer/skeleton.js.map +1 -0
  133. package/dist/indexer/symbolhistory.d.ts +41 -0
  134. package/dist/indexer/symbolhistory.d.ts.map +1 -0
  135. package/dist/indexer/symbolhistory.js +124 -0
  136. package/dist/indexer/symbolhistory.js.map +1 -0
  137. package/dist/indexer/watcher.d.ts +68 -0
  138. package/dist/indexer/watcher.d.ts.map +1 -0
  139. package/dist/indexer/watcher.js +179 -0
  140. package/dist/indexer/watcher.js.map +1 -0
  141. package/dist/mcp/server.d.ts +80 -0
  142. package/dist/mcp/server.d.ts.map +1 -0
  143. package/dist/mcp/server.js +1610 -0
  144. package/dist/mcp/server.js.map +1 -0
  145. package/dist/parser/index.d.ts +8 -0
  146. package/dist/parser/index.d.ts.map +1 -0
  147. package/dist/parser/index.js +33 -0
  148. package/dist/parser/index.js.map +1 -0
  149. package/dist/parser/languages/cpp.d.ts +3 -0
  150. package/dist/parser/languages/cpp.d.ts.map +1 -0
  151. package/dist/parser/languages/cpp.js +350 -0
  152. package/dist/parser/languages/cpp.js.map +1 -0
  153. package/dist/parser/languages/csharp.d.ts +3 -0
  154. package/dist/parser/languages/csharp.d.ts.map +1 -0
  155. package/dist/parser/languages/csharp.js +239 -0
  156. package/dist/parser/languages/csharp.js.map +1 -0
  157. package/dist/parser/languages/go.d.ts +3 -0
  158. package/dist/parser/languages/go.d.ts.map +1 -0
  159. package/dist/parser/languages/go.js +259 -0
  160. package/dist/parser/languages/go.js.map +1 -0
  161. package/dist/parser/languages/java.d.ts +3 -0
  162. package/dist/parser/languages/java.d.ts.map +1 -0
  163. package/dist/parser/languages/java.js +391 -0
  164. package/dist/parser/languages/java.js.map +1 -0
  165. package/dist/parser/languages/python.d.ts +3 -0
  166. package/dist/parser/languages/python.d.ts.map +1 -0
  167. package/dist/parser/languages/python.js +396 -0
  168. package/dist/parser/languages/python.js.map +1 -0
  169. package/dist/parser/languages/rust.d.ts +3 -0
  170. package/dist/parser/languages/rust.d.ts.map +1 -0
  171. package/dist/parser/languages/rust.js +159 -0
  172. package/dist/parser/languages/rust.js.map +1 -0
  173. package/dist/parser/languages/typescript.d.ts +3 -0
  174. package/dist/parser/languages/typescript.d.ts.map +1 -0
  175. package/dist/parser/languages/typescript.js +1442 -0
  176. package/dist/parser/languages/typescript.js.map +1 -0
  177. package/dist/parser/parserContext.d.ts +77 -0
  178. package/dist/parser/parserContext.d.ts.map +1 -0
  179. package/dist/parser/parserContext.js +354 -0
  180. package/dist/parser/parserContext.js.map +1 -0
  181. package/dist/parser/walker.d.ts +81 -0
  182. package/dist/parser/walker.d.ts.map +1 -0
  183. package/dist/parser/walker.js +217 -0
  184. package/dist/parser/walker.js.map +1 -0
  185. package/dist/parser/worker.d.ts +66 -0
  186. package/dist/parser/worker.d.ts.map +1 -0
  187. package/dist/parser/worker.js +129 -0
  188. package/dist/parser/worker.js.map +1 -0
  189. package/dist/parser/workerpool.d.ts +107 -0
  190. package/dist/parser/workerpool.d.ts.map +1 -0
  191. package/dist/parser/workerpool.js +383 -0
  192. package/dist/parser/workerpool.js.map +1 -0
  193. package/dist/scip/format.d.ts +87 -0
  194. package/dist/scip/format.d.ts.map +1 -0
  195. package/dist/scip/format.js +31 -0
  196. package/dist/scip/format.js.map +1 -0
  197. package/dist/scip/import.d.ts +37 -0
  198. package/dist/scip/import.d.ts.map +1 -0
  199. package/dist/scip/import.js +180 -0
  200. package/dist/scip/import.js.map +1 -0
  201. package/dist/types.d.ts +392 -0
  202. package/dist/types.d.ts.map +1 -0
  203. package/dist/types.js +4 -0
  204. package/dist/types.js.map +1 -0
  205. package/docs/architecture.md +105 -0
  206. package/docs/benchmarks/methodology.md +134 -0
  207. package/docs/benchmarks/raw-results.md +71 -0
  208. package/docs/benchmarks.md +74 -0
  209. package/docs/cli.md +148 -0
  210. package/docs/examples/behavior-tests.md +70 -0
  211. package/docs/examples/change-history.md +85 -0
  212. package/docs/examples/pre-edit-context.md +81 -0
  213. package/docs/examples/service-links.md +88 -0
  214. package/docs/examples.md +80 -0
  215. package/docs/faq.md +70 -0
  216. package/docs/internals.md +104 -0
  217. package/docs/languages.md +70 -0
  218. package/docs/limits.md +52 -0
  219. package/docs/mcp.md +199 -0
  220. package/docs/quickstart.md +119 -0
  221. package/docs/testing.md +123 -0
  222. package/docs/tools.md +115 -0
  223. package/package.json +52 -0
  224. package/research-codebase.md +578 -0
  225. package/seer-cli-docs.md +326 -0
  226. package/seer-master-guide.md +246 -0
  227. package/src/bundle/ci.ts +141 -0
  228. package/src/bundle/contract.ts +387 -0
  229. package/src/bundle/export.ts +175 -0
  230. package/src/bundle/external.ts +285 -0
  231. package/src/bundle/format.ts +92 -0
  232. package/src/bundle/import.ts +157 -0
  233. package/src/cli/index.ts +1249 -0
  234. package/src/cli/init.ts +389 -0
  235. package/src/db/schema.ts +614 -0
  236. package/src/db/store.ts +4306 -0
  237. package/src/graph/pagerank.ts +53 -0
  238. package/src/indexer/architecture.ts +148 -0
  239. package/src/indexer/behavior.ts +466 -0
  240. package/src/indexer/boundaries.ts +374 -0
  241. package/src/indexer/churn.ts +58 -0
  242. package/src/indexer/classify.ts +96 -0
  243. package/src/indexer/context.ts +340 -0
  244. package/src/indexer/continuity.ts +322 -0
  245. package/src/indexer/detectchanges.ts +94 -0
  246. package/src/indexer/discovery.ts +176 -0
  247. package/src/indexer/externaldeps.ts +243 -0
  248. package/src/indexer/freshness.ts +166 -0
  249. package/src/indexer/git.ts +453 -0
  250. package/src/indexer/index.ts +1092 -0
  251. package/src/indexer/modules.ts +358 -0
  252. package/src/indexer/preflight.ts +548 -0
  253. package/src/indexer/protoScanner.ts +147 -0
  254. package/src/indexer/risk.ts +304 -0
  255. package/src/indexer/serviceHostScanner.ts +92 -0
  256. package/src/indexer/serviceLinks.ts +543 -0
  257. package/src/indexer/shapehash.ts +370 -0
  258. package/src/indexer/skeleton.ts +169 -0
  259. package/src/indexer/symbolhistory.ts +172 -0
  260. package/src/indexer/watcher.ts +206 -0
  261. package/src/mcp/server.ts +1659 -0
  262. package/src/parser/index.ts +37 -0
  263. package/src/parser/languages/cpp.ts +361 -0
  264. package/src/parser/languages/csharp.ts +235 -0
  265. package/src/parser/languages/go.ts +259 -0
  266. package/src/parser/languages/java.ts +382 -0
  267. package/src/parser/languages/python.ts +370 -0
  268. package/src/parser/languages/rust.ts +164 -0
  269. package/src/parser/languages/typescript.ts +1435 -0
  270. package/src/parser/parserContext.ts +392 -0
  271. package/src/parser/walker.ts +306 -0
  272. package/src/parser/worker.ts +181 -0
  273. package/src/parser/workerpool.ts +448 -0
  274. package/src/scip/format.ts +83 -0
  275. package/src/scip/import.ts +216 -0
  276. package/src/types.ts +457 -0
  277. package/tests/benchmark-service-links.ts +244 -0
  278. package/tests/bug-regressions.ts +626 -0
  279. package/tests/filters.ts +264 -0
  280. package/tests/fixtures/Counter.tsx +38 -0
  281. package/tests/fixtures/caller.ts +7 -0
  282. package/tests/fixtures/collisions.ts +23 -0
  283. package/tests/fixtures/local_helper.ts +5 -0
  284. package/tests/fixtures/overloads.java +17 -0
  285. package/tests/fixtures/remote_helper.ts +4 -0
  286. package/tests/fixtures/sample.c +15 -0
  287. package/tests/fixtures/sample.cpp +47 -0
  288. package/tests/fixtures/sample.cs +62 -0
  289. package/tests/fixtures/sample.go +68 -0
  290. package/tests/fixtures/sample.h +30 -0
  291. package/tests/fixtures/sample.java +85 -0
  292. package/tests/fixtures/sample.py +46 -0
  293. package/tests/fixtures/sample.rs +78 -0
  294. package/tests/fixtures/sample.ts +76 -0
  295. package/tests/fixtures-service/HttpClients.cs +30 -0
  296. package/tests/fixtures-service/HttpClients.java +24 -0
  297. package/tests/fixtures-service/billing.ts +15 -0
  298. package/tests/fixtures-service/docker-compose.yml +15 -0
  299. package/tests/fixtures-service/gateway.ts +10 -0
  300. package/tests/fixtures-service/get_user.ts +11 -0
  301. package/tests/fixtures-service/graphql_client.ts +63 -0
  302. package/tests/fixtures-service/graphql_server.ts +30 -0
  303. package/tests/fixtures-service/grpc_client.go +30 -0
  304. package/tests/fixtures-service/http_clients.go +23 -0
  305. package/tests/fixtures-service/http_clients.py +38 -0
  306. package/tests/fixtures-service/http_clients.ts +49 -0
  307. package/tests/fixtures-service/k8s/payment-service.yaml +22 -0
  308. package/tests/fixtures-service/k8s_calls.ts +20 -0
  309. package/tests/fixtures-service/messaging.ts +87 -0
  310. package/tests/fixtures-service/trpc_client.ts +39 -0
  311. package/tests/fixtures-service/trpc_server.ts +39 -0
  312. package/tests/fixtures-service/user_service.proto +33 -0
  313. package/tests/fixtures-trackcd/Cargo.toml +11 -0
  314. package/tests/fixtures-trackcd/SpringController.java +36 -0
  315. package/tests/fixtures-trackcd/auth_service.ts +19 -0
  316. package/tests/fixtures-trackcd/complex_module.py +50 -0
  317. package/tests/fixtures-trackcd/express_app.js +30 -0
  318. package/tests/fixtures-trackcd/fastapi_app.py +49 -0
  319. package/tests/fixtures-trackcd/fastify_object_routes.js +32 -0
  320. package/tests/fixtures-trackcd/go.mod +8 -0
  321. package/tests/fixtures-trackcd/package.json +15 -0
  322. package/tests/fixtures-trackcd/requirements.txt +4 -0
  323. package/tests/fixtures-trackcd/tests/auth_service.test.ts +13 -0
  324. package/tests/fixtures-tracke/auth/AuthService.ts +23 -0
  325. package/tests/fixtures-tracke/auth/crypto.ts +7 -0
  326. package/tests/fixtures-tracke/billing/Billing.ts +20 -0
  327. package/tests/fixtures-tracke/billing/Invoice.ts +10 -0
  328. package/tests/fixtures-tracke/billing/server.ts +17 -0
  329. package/tests/fixtures-tracke/package.json +7 -0
  330. package/tests/fixtures-tracke/tests/auth.test.ts +23 -0
  331. package/tests/fixtures-tracke/tests/billing.test.ts +14 -0
  332. package/tests/fixtures-trackf/package.json +5 -0
  333. package/tests/fixtures-trackf/src/auth.ts +26 -0
  334. package/tests/fixtures-trackf/src/handlers.ts +35 -0
  335. package/tests/fixtures-tracki/billing/routes.ts +12 -0
  336. package/tests/fixtures-tracki/gateway/client.ts +13 -0
  337. package/tests/git-features.ts +267 -0
  338. package/tests/init.ts +141 -0
  339. package/tests/mcp-jit.ts +130 -0
  340. package/tests/mcp-smoke.ts +191 -0
  341. package/tests/mcp-trackcd.ts +169 -0
  342. package/tests/mcp-tracke.ts +229 -0
  343. package/tests/mcp-trackf.ts +330 -0
  344. package/tests/mcp-trackg.ts +219 -0
  345. package/tests/mcp-tracki.ts +174 -0
  346. package/tests/mcp-watcher.ts +126 -0
  347. package/tests/optspec.ts +194 -0
  348. package/tests/parallel-index.ts +333 -0
  349. package/tests/parallel-read.ts +125 -0
  350. package/tests/parallel-recovery.ts +241 -0
  351. package/tests/perf-callers.ts +145 -0
  352. package/tests/query-parity.ts +184 -0
  353. package/tests/query-perf.ts +55 -0
  354. package/tests/scale-parallel-parity.ts +225 -0
  355. package/tests/scale-test.ts +523 -0
  356. package/tests/smoke.ts +396 -0
  357. package/tests/trackcd.ts +325 -0
  358. package/tests/tracke-collisions.ts +255 -0
  359. package/tests/tracke.ts +314 -0
  360. package/tests/trackf-bugs.ts +406 -0
  361. package/tests/trackf.ts +390 -0
  362. package/tests/trackg.ts +1372 -0
  363. package/tests/tracki-boundaries.ts +202 -0
  364. package/tests/tracki-continuity.ts +253 -0
  365. package/tests/tracki-contract-diff.ts +249 -0
  366. package/tests/tracki-external-bundles.ts +341 -0
  367. package/tests/tracki-preflight.ts +251 -0
  368. package/tests/verify-roles.ts +51 -0
  369. package/tests/worker-parity.ts +286 -0
  370. package/tests/worker-pool.ts +262 -0
  371. package/tsconfig.json +20 -0
@@ -0,0 +1,1249 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { Indexer } from '../indexer/index.js';
6
+ import { Store } from '../db/store.js';
7
+ import { rankedBehavior } from '../indexer/behavior.js';
8
+ import { computeRisk } from '../indexer/risk.js';
9
+ import { buildContext } from '../indexer/context.js';
10
+ import { runInit, ClientId } from './init.js';
11
+
12
+ const VERSION = '0.1.0';
13
+
14
+ const KNOWN_CLIENTS: ClientId[] = ['claude', 'cursor', 'vscode', 'codex', 'gemini', 'antigravity'];
15
+
16
+ function resolveDb(repoPath: string, customDb?: string): string {
17
+ if (customDb) return path.resolve(customDb);
18
+ const seerDir = path.join(path.resolve(repoPath), '.seer');
19
+ if (!fs.existsSync(seerDir)) fs.mkdirSync(seerDir, { recursive: true });
20
+ return path.join(seerDir, 'graph.db');
21
+ }
22
+
23
+ function openStore(dbPath: string, mutable = false): Store {
24
+ if (!fs.existsSync(dbPath)) {
25
+ console.error(`No index found at ${dbPath}. Run "seer index <path>" first.`);
26
+ process.exit(1);
27
+ }
28
+ return mutable ? new Store(dbPath) : Store.openReadOnly(dbPath);
29
+ }
30
+
31
+ // ── Program ────────────────────────────────────────────────────────────────────
32
+
33
+ const program = new Command();
34
+
35
+ program
36
+ .name('seer')
37
+ .description('Local-first AI codebase explainer')
38
+ .version(VERSION);
39
+
40
+ // ── seer index ───────────────────────────────────────────────────────────────
41
+
42
+ program
43
+ .command('index <repo-path>')
44
+ .description('Index a repository into a local SQLite graph')
45
+ .option('--db <path>', 'Custom database path (default: <repo>/.seer/graph.db)')
46
+ .option('-v, --verbose', 'Show per-file progress')
47
+ .option('--reset', 'Delete existing index before re-indexing')
48
+ .option('--max-file-kb <kb>', 'Skip files larger than this (KiB). 0 = no cap (default).', '0')
49
+ .option('--include-vendor', 'Index vendor/ vendored/ thirdparty/ directories')
50
+ .option('--include-generated', 'Index *.generated.* / *.pb.* / *.gen.* files')
51
+ .option('--mode <mode>', 'Discovery mode: full | standard | fast (default: standard).', 'standard')
52
+ .option('--parallel', 'Force worker-thread parsing even for tiny repositories')
53
+ .option('--no-parallel', 'Disable worker-thread parsing; auto mode uses workers for normal/large repos')
54
+ .option('--jobs <n>', 'Worker thread count when worker parsing is active (default: cores - 1, capped at 8)')
55
+ .action(async (repoPath: string, opts: { db?: string; verbose?: boolean; reset?: boolean; maxFileKb: string; includeVendor?: boolean; includeGenerated?: boolean; mode?: string; parallel?: boolean; jobs?: string }) => {
56
+ const absRepo = path.resolve(repoPath);
57
+ if (!fs.existsSync(absRepo)) {
58
+ console.error(`Path not found: ${absRepo}`);
59
+ process.exit(1);
60
+ }
61
+ const dbPath = resolveDb(absRepo, opts.db);
62
+ if (opts.reset && fs.existsSync(dbPath)) {
63
+ fs.unlinkSync(dbPath);
64
+ console.log(` Removed existing index: ${dbPath}`);
65
+ }
66
+ console.log(`\nSeer Index`);
67
+ console.log(` Repo: ${absRepo}`);
68
+ console.log(` DB: ${dbPath}\n`);
69
+ const store = new Store(dbPath);
70
+ const indexer = new Indexer(store);
71
+ try {
72
+ const maxKb = parseInt(opts.maxFileKb, 10);
73
+ const maxFileBytes = isNaN(maxKb) || maxKb <= 0 ? 0 : maxKb * 1024;
74
+ const mode = parseMode(opts.mode);
75
+ // `--parallel` / `--no-parallel` force the parser mode; otherwise the
76
+ // indexer uses auto mode (workers for normal/large repos, serial for tiny).
77
+ const jobsN = opts.jobs ? parseInt(opts.jobs, 10) : undefined;
78
+ const result = await indexer.indexDirectory(absRepo, {
79
+ verbose: opts.verbose,
80
+ reset: opts.reset,
81
+ maxFileBytes,
82
+ includeVendor: opts.includeVendor,
83
+ includeGenerated: opts.includeGenerated,
84
+ mode,
85
+ parallel: opts.parallel,
86
+ jobs: jobsN != null && !isNaN(jobsN) && jobsN > 0 ? jobsN : undefined,
87
+ });
88
+ console.log(`\n ✓ Indexed ${result.filesIndexed.toLocaleString()} files`);
89
+ if (result.filesReusedFromCache > 0) console.log(` ${result.filesReusedFromCache.toLocaleString()} reused from cache`);
90
+ if (result.filesSkipped > 0) console.log(` ${result.filesSkipped.toLocaleString()} skipped`);
91
+ if (result.filesSkippedTooLarge > 0) console.log(` ${result.filesSkippedTooLarge.toLocaleString()} skipped (too large)`);
92
+ if (result.filesParseError > 0) console.log(` ${result.filesParseError.toLocaleString()} parse errors`);
93
+ if (result.wasmResets > 0) console.log(` ${result.wasmResets} WASM reset(s)`);
94
+ console.log(` ✓ ${result.symbols.toLocaleString()} symbols`);
95
+ console.log(` ✓ ${result.edges.toLocaleString()} edges (${result.resolvedEdges.toLocaleString()} resolved)`);
96
+ console.log(` ✓ ${result.resolvedImports.toLocaleString()} imports resolved`);
97
+ if ((result.routesResolved ?? 0) > 0) console.log(` ✓ ${result.routesResolved} routes linked to handlers`);
98
+ if ((result.testEdgesAdded ?? 0) > 0) console.log(` ✓ ${result.testEdgesAdded} test edges synthesized`);
99
+ if ((result.externalDependencies ?? 0) > 0)console.log(` ✓ ${result.externalDependencies} external deps`);
100
+ if (result.pagerankRecomputed) console.log(` ✓ PageRank computed`);
101
+ else console.log(` ↻ PageRank reused (graph unchanged)`);
102
+ console.log(`\n Done in ${(result.elapsedMs / 1000).toFixed(1)}s`);
103
+ } finally {
104
+ store.close();
105
+ }
106
+ });
107
+
108
+ // ── seer init ──────────────────────────────────────────────────────────────
109
+
110
+ program
111
+ .command('init [workspace]')
112
+ .description('Wire Seer in as an MCP server for your AI agents and write an AGENTS.md usage guide')
113
+ .option('--db <path>', 'Custom database path passed through to the MCP launcher')
114
+ .option('--client <names>', 'Comma-separated clients: claude,cursor,vscode,codex,gemini,antigravity,all (default: claude,cursor,vscode,codex,gemini)')
115
+ .option('--global', 'Write user-level config instead of project-local config')
116
+ .option('--npx', 'Emit a portable "npx -y <pkg> mcp" launcher instead of an absolute node path')
117
+ .option('--pkg <name>', 'npm package name used by the --npx launcher', 'seer-mcp')
118
+ .option('--command <cmd>', 'Override the launch command entirely (advanced)')
119
+ .option('--no-agents', 'Do not write the AGENTS.md guidance block')
120
+ .option('--print', 'Print the plan without writing any files')
121
+ .option('--force', 'Overwrite an existing seer entry / agents block')
122
+ .action((workspace: string | undefined, opts: {
123
+ db?: string; client?: string; global?: boolean; npx?: boolean; pkg?: string;
124
+ command?: string; agents?: boolean; print?: boolean; force?: boolean;
125
+ }) => {
126
+ const ws = path.resolve(workspace ?? process.cwd());
127
+ if (!fs.existsSync(ws)) { console.error(`Workspace not found: ${ws}`); process.exit(1); }
128
+
129
+ let clients: ClientId[] | undefined;
130
+ if (opts.client) {
131
+ const names = opts.client.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
132
+ if (names.includes('all')) {
133
+ clients = KNOWN_CLIENTS;
134
+ } else {
135
+ const bad = names.filter((n) => !KNOWN_CLIENTS.includes(n as ClientId));
136
+ if (bad.length) {
137
+ console.error(`Unknown client(s): ${bad.join(', ')}. Known: ${KNOWN_CLIENTS.join(', ')}, all`);
138
+ process.exit(1);
139
+ }
140
+ clients = names as ClientId[];
141
+ }
142
+ }
143
+
144
+ const result = runInit({
145
+ workspace: ws,
146
+ clients,
147
+ global: opts.global,
148
+ npx: opts.npx,
149
+ pkg: opts.pkg,
150
+ command: opts.command,
151
+ agents: opts.agents,
152
+ print: opts.print,
153
+ force: opts.force,
154
+ db: opts.db,
155
+ });
156
+
157
+ console.log(`\nSeer Init ${opts.print ? '(dry run — nothing written)' : ''}`);
158
+ console.log(` Workspace: ${ws}`);
159
+ console.log(` Launcher: ${result.launch.command} ${result.launch.args.join(' ')}\n`);
160
+
161
+ const mark: Record<string, string> = opts.print
162
+ ? { wrote: '+ would write ', updated: '~ would update', skipped: '· would skip ', manual: '! manual ' }
163
+ : { wrote: '✓ wrote ', updated: '✓ updated', skipped: '· skipped', manual: '! manual ' };
164
+ for (const e of result.entries) {
165
+ console.log(` ${mark[e.action] ?? e.action} ${e.label.padEnd(28)} ${e.file}`);
166
+ if (e.note) console.log(` ${e.note}`);
167
+ if (e.snippet && (opts.print || e.action === 'manual')) {
168
+ console.log(e.snippet.split('\n').map((l) => ' ' + l).join('\n'));
169
+ }
170
+ }
171
+ if (result.agents) {
172
+ console.log(` ${mark[result.agents.action] ?? result.agents.action} ${'AGENTS.md (agent guide)'.padEnd(28)} ${result.agents.file}`);
173
+ }
174
+
175
+ console.log(`\n Next:`);
176
+ console.log(` 1. Reload / restart your agent so it picks up the new MCP server.`);
177
+ console.log(` 2. Seer indexes this workspace automatically on first query.`);
178
+ console.log(` 3. Ask your agent to call seer_health to confirm it is connected.\n`);
179
+ });
180
+
181
+ // ── seer callers / callees / symbols / stats / health ──────────────────────────
182
+
183
+ program
184
+ .command('callers <symbol>')
185
+ .description('Find all callers of a symbol')
186
+ .option('--db <path>', 'Database path')
187
+ .option('-n, --limit <n>', 'Max results', '40')
188
+ // Callers query is keyed by `edges.to_name`, not by symbol_role / vendor /
189
+ // test flags. The include-* options are accepted for surface consistency
190
+ // with the rest of the CLI but don't currently change results.
191
+ .action((symbol: string, opts: { db?: string; limit: string }) => {
192
+ const dbPath = opts.db ?? findDbFromCwd();
193
+ const store = openStore(dbPath);
194
+ try {
195
+ const total = store.countCallers(symbol);
196
+ if (total === 0) { console.log(`No callers found for "${symbol}"`); return; }
197
+ const limit = Math.max(1, parseInt(opts.limit, 10) || 40);
198
+ const callers = store.findCallers(symbol, limit);
199
+ console.log(`\nCallers of '${symbol}' (${total} found)\n`);
200
+ for (const c of callers) {
201
+ const loc = `${c.callerFile}:${c.callerLine + 1}`;
202
+ console.log(` ${c.callerName.padEnd(32)} ${c.callerKind.padEnd(12)} ${loc}`);
203
+ }
204
+ if (total > callers.length) console.log(` … and ${total - callers.length} more`);
205
+ } finally { store.close(); }
206
+ });
207
+
208
+ program
209
+ .command('callees <symbol>')
210
+ .description('Find all symbols called by a symbol')
211
+ .option('--db <path>', 'Database path')
212
+ .option('-n, --limit <n>', 'Max results', '40')
213
+ .action((symbol: string, opts: { db?: string; limit: string }) => {
214
+ const dbPath = opts.db ?? findDbFromCwd();
215
+ const store = openStore(dbPath);
216
+ try {
217
+ const callees = store.findCallees(symbol);
218
+ if (callees.length === 0) { console.log(`No callees found for "${symbol}"`); return; }
219
+ console.log(`\nCallees of '${symbol}' (${callees.length} found)\n`);
220
+ const limit = Math.min(parseInt(opts.limit, 10), callees.length);
221
+ for (const c of callees.slice(0, limit)) {
222
+ const loc = c.calleeFile ? `${c.calleeFile}:${(c.calleeLineStart ?? 0) + 1}` : '(unresolved)';
223
+ const kind = c.calleeKind ?? '?';
224
+ console.log(` ${c.calleeName.padEnd(32)} ${kind.padEnd(12)} ${loc}`);
225
+ }
226
+ if (callees.length > limit) console.log(` … and ${callees.length - limit} more`);
227
+ } finally { store.close(); }
228
+ });
229
+
230
+ program
231
+ .command('symbols [query]')
232
+ .description('Search symbols by name, or list top symbols by PageRank')
233
+ .option('--db <path>', 'Database path')
234
+ .option('--file <path>', 'Filter to symbols in a specific file')
235
+ .option('-n, --top <n>', 'Show top N symbols by PageRank (default: 20)', '20')
236
+ .option('--include-vendor', 'Include vendored code (off by default)')
237
+ .option('--include-generated', 'Include generated code (off by default)')
238
+ .option('--include-tests', 'Include symbols from test files (off by default)')
239
+ .option('--include-declarations', 'Include forward / class-body declarations (off by default)')
240
+ .option('--include-type-refs', 'Include bare type-reference rows (off by default; not yet emitted)')
241
+ .action((query: string | undefined, opts: { db?: string; file?: string; top: string; includeVendor?: boolean; includeGenerated?: boolean; includeTests?: boolean; includeDeclarations?: boolean; includeTypeRefs?: boolean }) => {
242
+ const dbPath = opts.db ?? findDbFromCwd();
243
+ const store = openStore(dbPath);
244
+ try {
245
+ const limit = parseInt(opts.top, 10);
246
+ const includeOpts = {
247
+ includeVendor: opts.includeVendor,
248
+ includeGenerated: opts.includeGenerated,
249
+ includeTests: opts.includeTests,
250
+ includeDeclarations: opts.includeDeclarations,
251
+ includeTypeRefs: opts.includeTypeRefs,
252
+ };
253
+ let symbols;
254
+ if (opts.file) { symbols = store.listSymbolsInFile(opts.file, limit); console.log(`\nSymbols in ${opts.file}\n`); }
255
+ else if (query) { symbols = store.findSymbols(query, includeOpts); console.log(`\nSymbols matching '${query}'\n`); }
256
+ else { symbols = store.getTopSymbols(limit, includeOpts); console.log(`\nTop ${limit} symbols by PageRank\n`); }
257
+ if (symbols.length === 0) { console.log(' (none found)'); return; }
258
+ console.log(` ${'Name'.padEnd(32)} ${'Kind'.padEnd(12)} ${'Line'.padEnd(6)} ${'PageRank'.padEnd(10)} ${'Role'.padEnd(11)} File`);
259
+ console.log(' ' + '─'.repeat(102));
260
+ for (const s of symbols) {
261
+ const pr = s.pagerank.toFixed(4);
262
+ const loc = String(s.lineStart + 1).padEnd(6);
263
+ const relFile = s.filePath.replace(/\\/g, '/');
264
+ const role = (s.symbolRole ?? 'definition').padEnd(11);
265
+ console.log(` ${s.name.padEnd(32)} ${s.kind.padEnd(12)} ${loc} ${pr.padEnd(10)} ${role} ${relFile}`);
266
+ }
267
+ } finally { store.close(); }
268
+ });
269
+
270
+ program
271
+ .command('stats')
272
+ .description('Show index statistics')
273
+ .option('--db <path>', 'Database path')
274
+ .action((opts: { db?: string }) => {
275
+ const dbPath = opts.db ?? findDbFromCwd();
276
+ const store = openStore(dbPath);
277
+ try {
278
+ const stats = store.getStats();
279
+ console.log('\nSeer Index Stats');
280
+ console.log('──────────────────');
281
+ console.log(` Files: ${stats.files.toLocaleString()}`);
282
+ console.log(` Symbols: ${stats.symbols.toLocaleString()}`);
283
+ console.log(` Edges: ${stats.edges.toLocaleString()}`);
284
+ console.log(` Resolved edges: ${stats.resolvedEdges.toLocaleString()}`);
285
+ if (stats.routes != null) console.log(` Routes: ${stats.routes.toLocaleString()}`);
286
+ if (stats.externalDependencies != null) console.log(` External deps: ${stats.externalDependencies.toLocaleString()}`);
287
+ if (stats.configKeys != null) console.log(` Config keys: ${stats.configKeys.toLocaleString()}`);
288
+ if (stats.symbolHistory != null) console.log(` Symbol history: ${stats.symbolHistory.toLocaleString()}`);
289
+ console.log(` Languages:`);
290
+ for (const [lang, count] of Object.entries(stats.languages).sort((a, b) => b[1] - a[1])) {
291
+ console.log(` ${lang.padEnd(14)} ${count}`);
292
+ }
293
+ console.log(`\n DB: ${dbPath}`);
294
+ } finally { store.close(); }
295
+ });
296
+
297
+ program
298
+ .command('health')
299
+ .description('Show Seer index health')
300
+ .option('--db <path>', 'Database path')
301
+ .action((opts: { db?: string }) => {
302
+ const dbPath = opts.db ?? findDbFromCwd();
303
+ const store = openStore(dbPath);
304
+ try {
305
+ const schema = store.schemaInfo();
306
+ const stats = store.getStats();
307
+ console.log('\nSeer Health');
308
+ console.log('─────────────');
309
+ console.log(` DB path: ${dbPath}`);
310
+ console.log(` Read-only: ${store.isReadOnly()}`);
311
+ console.log(` Schema version: ${schema.dbVersion} (build expects ${schema.buildVersion})`);
312
+ if (!schema.current) console.log(` ⚠ Schema is behind. Run \`seer index <path>\` to migrate.`);
313
+ else console.log(` ✓ Schema is up to date.`);
314
+ console.log(` Files: ${stats.files.toLocaleString()}`);
315
+ console.log(` Symbols: ${stats.symbols.toLocaleString()}`);
316
+ console.log(` Edges: ${stats.edges.toLocaleString()} (${stats.resolvedEdges.toLocaleString()} resolved)`);
317
+ if (stats.roles) {
318
+ const t = stats.roles.project + stats.roles.vendor + stats.roles.generated + stats.roles.test;
319
+ console.log(` File roles: project ${stats.roles.project} vendor ${stats.roles.vendor} generated ${stats.roles.generated} test ${stats.roles.test} (${t} total)`);
320
+ }
321
+ if (stats.routes != null && stats.routes > 0) console.log(` Routes: ${stats.routes.toLocaleString()}`);
322
+ if (stats.externalDependencies != null && stats.externalDependencies > 0) console.log(` External deps: ${stats.externalDependencies.toLocaleString()}`);
323
+ if (stats.configKeys != null && stats.configKeys > 0) console.log(` Config keys: ${stats.configKeys.toLocaleString()}`);
324
+ if (stats.symbolHistory != null && stats.symbolHistory > 0) console.log(` Symbol history: ${stats.symbolHistory.toLocaleString()} rows`);
325
+ } finally { store.close(); }
326
+ });
327
+
328
+ // ── seer routes ──────────────────────────────────────────────────────────────
329
+
330
+ program
331
+ .command('routes')
332
+ .description('List HTTP routes detected in the codebase')
333
+ .option('--db <path>', 'Database path')
334
+ .option('--method <m>', 'Filter by HTTP method (GET/POST/...)')
335
+ .option('--framework <f>', 'Filter by framework (express/fastapi/flask/spring)')
336
+ .option('--path <substr>', 'Filter by path substring')
337
+ .option('-n, --limit <n>', 'Max results', '50')
338
+ .action((opts: { db?: string; method?: string; framework?: string; path?: string; limit: string }) => {
339
+ const dbPath = opts.db ?? findDbFromCwd();
340
+ const store = openStore(dbPath);
341
+ try {
342
+ const rows = store.listRoutes({
343
+ method: opts.method,
344
+ framework: opts.framework,
345
+ pathSubstr: opts.path,
346
+ limit: parseInt(opts.limit, 10) || 50,
347
+ });
348
+ if (rows.length === 0) { console.log('No routes found.'); return; }
349
+ console.log(`\nRoutes (${rows.length} shown)\n`);
350
+ for (const r of rows) {
351
+ const h = r.handlerSymbol ? `→ ${r.handlerSymbol}` : (r.handlerName ? `→ ${r.handlerName} (unresolved)` : '');
352
+ console.log(` ${r.method.padEnd(6)} ${r.path.padEnd(40)} ${r.framework.padEnd(10)} ${h}`);
353
+ }
354
+ } finally { store.close(); }
355
+ });
356
+
357
+ // ── seer service-calls / service-links / trace-service ──────────────────────
358
+
359
+ program
360
+ .command('service-calls')
361
+ .description('List outbound HTTP/service client calls detected in the codebase')
362
+ .option('--db <path>', 'Database path')
363
+ .option('--protocol <p>', 'Filter by protocol (http)')
364
+ .option('--method <m>', 'Filter by HTTP method (GET/POST/...)')
365
+ .option('--framework <f>', 'Filter by client framework (fetch/axios/requests/...)')
366
+ .option('--path <substr>', 'Filter by normalized path substring')
367
+ .option('--min-confidence <c>', 'Minimum confidence 0..1', '0')
368
+ .option('-n, --limit <n>', 'Max results', '100')
369
+ .option('--offset <n>', 'Skip first N results', '0')
370
+ .action((opts: {
371
+ db?: string; protocol?: string; method?: string; framework?: string;
372
+ path?: string; minConfidence: string; limit: string; offset: string;
373
+ }) => {
374
+ const dbPath = opts.db ?? findDbFromCwd();
375
+ const store = openStore(dbPath);
376
+ try {
377
+ const rows = store.listServiceCalls({
378
+ protocol: opts.protocol,
379
+ method: opts.method,
380
+ framework: opts.framework,
381
+ pathSubstr: opts.path,
382
+ minConfidence: parseFloat(opts.minConfidence) || 0,
383
+ limit: parseInt(opts.limit, 10) || 100,
384
+ offset: parseInt(opts.offset, 10) || 0,
385
+ });
386
+ if (rows.length === 0) { console.log('No service calls found.'); return; }
387
+ console.log(`\nService calls (${rows.length} shown)\n`);
388
+ for (const r of rows) {
389
+ const caller = r.callerQualifiedName ?? r.callerName ?? '(module-level)';
390
+ const target = r.normalizedPath ?? r.rawTarget;
391
+ const host = r.hostHint ? ` host=${r.hostHint}` : '';
392
+ const env = r.envKey ? ` env=${r.envKey}` : '';
393
+ console.log(` ${(r.method ?? 'ANY').padEnd(6)} ${target.padEnd(40)} ` +
394
+ `${r.framework.padEnd(12)} ${caller.padEnd(28)} ${r.filePath}:${r.line + 1}${host}${env}`);
395
+ }
396
+ } finally { store.close(); }
397
+ });
398
+
399
+ program
400
+ .command('service-links')
401
+ .description('List deterministic service-link rendezvous between client calls and route handlers')
402
+ .option('--db <path>', 'Database path')
403
+ .option('--protocol <p>', 'Filter by protocol (http)')
404
+ .option('--method <m>', 'Filter by HTTP method')
405
+ .option('--path <substr>', 'Filter by call/route path substring')
406
+ .option('--match-kind <k>', 'Filter by match_kind (literal_path/env_base/route_pattern)')
407
+ .option('--min-confidence <c>', 'Minimum confidence 0..1', '0')
408
+ .option('-n, --limit <n>', 'Max results', '100')
409
+ .option('--offset <n>', 'Skip first N results', '0')
410
+ .action((opts: {
411
+ db?: string; protocol?: string; method?: string; path?: string;
412
+ matchKind?: string; minConfidence: string; limit: string; offset: string;
413
+ }) => {
414
+ const dbPath = opts.db ?? findDbFromCwd();
415
+ const store = openStore(dbPath);
416
+ try {
417
+ const rows = store.listServiceLinks({
418
+ protocol: opts.protocol,
419
+ method: opts.method,
420
+ pathSubstr: opts.path,
421
+ matchKind: opts.matchKind,
422
+ minConfidence: parseFloat(opts.minConfidence) || 0,
423
+ limit: parseInt(opts.limit, 10) || 100,
424
+ offset: parseInt(opts.offset, 10) || 0,
425
+ });
426
+ if (rows.length === 0) { console.log('No service links found.'); return; }
427
+ console.log(`\nService links (${rows.length} shown)\n`);
428
+ for (const r of rows) {
429
+ const caller = r.callerQualifiedName ?? r.callerName ?? '(module-level)';
430
+ const handler = r.handlerQualifiedName ?? r.handlerName ?? '(no handler)';
431
+ const route = r.routePath ?? r.callNormalizedPath ?? r.callRawTarget;
432
+ console.log(
433
+ ` ${(r.callMethod ?? 'ANY').padEnd(6)} ${(route ?? '').padEnd(36)} ` +
434
+ `${caller.padEnd(22)} → ${handler.padEnd(22)} ` +
435
+ `[${r.matchKind} ${r.confidence.toFixed(2)}]`,
436
+ );
437
+ }
438
+ } finally { store.close(); }
439
+ });
440
+
441
+ program
442
+ .command('trace-service <from> <to>')
443
+ .description('Find a shortest service-link path between two symbols (bounded BFS)')
444
+ .option('--db <path>', 'Database path')
445
+ .option('--depth <n>', 'Max BFS depth', '6')
446
+ .action((from: string, to: string, opts: { db?: string; depth: string }) => {
447
+ const dbPath = opts.db ?? findDbFromCwd();
448
+ const store = openStore(dbPath);
449
+ try {
450
+ const fromRows = store.getDefinition(from);
451
+ const toRows = store.getDefinition(to);
452
+ if (fromRows.length === 0) { console.log(`Source symbol "${from}" not found.`); return; }
453
+ if (toRows.length === 0) { console.log(`Target symbol "${to}" not found.`); return; }
454
+ const path = store.traceServicePath(
455
+ fromRows[0].id, toRows[0].id,
456
+ parseInt(opts.depth, 10) || 6,
457
+ );
458
+ if (path.length === 0) { console.log('No service-link path found.'); return; }
459
+ console.log(`\nService path (${path.length} hops):\n`);
460
+ for (const id of path) {
461
+ const row = store.rawDb().prepare(
462
+ `SELECT qualified_name, name FROM symbols WHERE id = ?`
463
+ ).get(id) as { qualified_name: string | null; name: string } | undefined;
464
+ const label = row?.qualified_name ?? row?.name ?? `#${id}`;
465
+ console.log(` → ${label}`);
466
+ }
467
+ } finally { store.close(); }
468
+ });
469
+
470
+ // ── seer deps ────────────────────────────────────────────────────────────────
471
+
472
+ program
473
+ .command('deps')
474
+ .description('List external dependencies declared in manifests')
475
+ .option('--db <path>', 'Database path')
476
+ .option('--ecosystem <e>', 'Filter (npm/cargo/pypi/go)')
477
+ .option('--name <substr>', 'Filter by name substring')
478
+ .option('-n, --limit <n>', 'Max results', '100')
479
+ .action((opts: { db?: string; ecosystem?: string; name?: string; limit: string }) => {
480
+ const dbPath = opts.db ?? findDbFromCwd();
481
+ const store = openStore(dbPath);
482
+ try {
483
+ const rows = store.listExternalDeps({
484
+ ecosystem: opts.ecosystem, nameSubstr: opts.name,
485
+ limit: parseInt(opts.limit, 10) || 100,
486
+ });
487
+ if (rows.length === 0) { console.log('No external dependencies indexed.'); return; }
488
+ console.log(`\nExternal dependencies (${rows.length} shown)\n`);
489
+ for (const r of rows) {
490
+ console.log(` ${r.ecosystem.padEnd(8)} ${r.name.padEnd(40)} ${r.versionRange ?? ''}${r.isDev ? ' (dev)' : ''}`);
491
+ }
492
+ } finally { store.close(); }
493
+ });
494
+
495
+ // ── seer config ──────────────────────────────────────────────────────────────
496
+
497
+ program
498
+ .command('config')
499
+ .description('List config / env reads detected in the codebase')
500
+ .option('--db <path>', 'Database path')
501
+ .option('--key <substr>', 'Filter by key substring')
502
+ .option('-n, --limit <n>', 'Max results', '50')
503
+ .action((opts: { db?: string; key?: string; limit: string }) => {
504
+ const dbPath = opts.db ?? findDbFromCwd();
505
+ const store = openStore(dbPath);
506
+ try {
507
+ const rows = store.listConfigKeys({ key: opts.key, limit: parseInt(opts.limit, 10) || 50 });
508
+ if (rows.length === 0) { console.log('No config keys indexed.'); return; }
509
+ console.log(`\nConfig keys (${rows.length} shown)\n`);
510
+ for (const r of rows) {
511
+ console.log(` ${r.source.padEnd(6)} ${r.key.padEnd(30)} ${r.filePath}:${r.line + 1} ${r.symbolName ?? ''}`);
512
+ }
513
+ } finally { store.close(); }
514
+ });
515
+
516
+ // ── seer churn (file-level git churn pass) ──────────────────────────────────
517
+
518
+ program
519
+ .command('churn')
520
+ .description('Collect file-level git churn (commits, last commit, top authors)')
521
+ .option('--db <path>', 'Database path')
522
+ .option('--workspace <path>', 'Workspace path (defaults to cwd)')
523
+ .action(async (opts: { db?: string; workspace?: string }) => {
524
+ const workspace = path.resolve(opts.workspace ?? process.cwd());
525
+ const dbPath = opts.db ?? findDbFromCwd();
526
+ if (!fs.existsSync(dbPath)) { console.error(`No index at ${dbPath}`); process.exit(1); }
527
+ const store = new Store(dbPath);
528
+ try {
529
+ const { collectChurn } = await import('../indexer/churn.js');
530
+ const r = await collectChurn(workspace, store);
531
+ console.log(`\nChurn pass: ${r.filesWithChurn}/${r.filesAnalyzed} files have history (HEAD ${r.headSha?.slice(0, 8) ?? '—'}), ${r.elapsedMs}ms`);
532
+ } finally { store.close(); }
533
+ });
534
+
535
+ // ── seer history (Track D) ──────────────────────────────────────────────────
536
+
537
+ program
538
+ .command('history <symbol>')
539
+ .description('Show per-symbol commit history (requires `seer symbol-history` to have run)')
540
+ .option('--db <path>', 'Database path')
541
+ .option('-n, --limit <n>', 'Max commits', '20')
542
+ .action((symbol: string, opts: { db?: string; limit: string }) => {
543
+ const dbPath = opts.db ?? findDbFromCwd();
544
+ const store = openStore(dbPath);
545
+ try {
546
+ const matches = store.getDefinition(symbol);
547
+ if (matches.length === 0) { console.log(`No symbol named "${symbol}"`); return; }
548
+ const limit = parseInt(opts.limit, 10) || 20;
549
+ for (const m of matches.slice(0, 3)) {
550
+ const history = store.getSymbolHistory(m.id, { limit });
551
+ const total = store.countSymbolHistory(m.id);
552
+ console.log(`\n${m.qualifiedName ?? m.name} (${m.kind}) ${m.filePath}:${m.lineStart + 1}`);
553
+ if (history.length === 0) { console.log(` (no history — run \`seer symbol-history\` first)`); continue; }
554
+ console.log(` ${total} commits in history${total > history.length ? ` (showing ${history.length})` : ''}`);
555
+ for (const h of history) {
556
+ const date = new Date(h.committedAt * 1000).toISOString().slice(0, 10);
557
+ const author = h.authorName ?? '?';
558
+ const pr = h.prNumber ? ` #${h.prNumber}` : '';
559
+ const msg = (h.message ?? '').split('\n')[0].slice(0, 60);
560
+ console.log(` ${h.commitSha.slice(0, 8)} ${date} +${h.linesAdded}/-${h.linesRemoved}${pr} ${author.padEnd(20)} ${msg}`);
561
+ }
562
+ }
563
+ } finally { store.close(); }
564
+ });
565
+
566
+ program
567
+ .command('symbol-history')
568
+ .description('Index per-symbol git history (opt-in; can take a few minutes)')
569
+ .option('--db <path>', 'Database path')
570
+ .option('--workspace <path>', 'Workspace path (defaults to cwd)')
571
+ .option('--max-commits <n>', 'Max commits per file', '200')
572
+ .option('--force', 'Re-run even if HEAD unchanged')
573
+ .action(async (opts: { db?: string; workspace?: string; maxCommits: string; force?: boolean }) => {
574
+ const workspace = path.resolve(opts.workspace ?? process.cwd());
575
+ const dbPath = opts.db ?? findDbFromCwd();
576
+ if (!fs.existsSync(dbPath)) { console.error(`No index at ${dbPath}`); process.exit(1); }
577
+ const store = new Store(dbPath);
578
+ try {
579
+ const { buildSymbolHistory } = await import('../indexer/symbolhistory.js');
580
+ const r = await buildSymbolHistory(workspace, store, {
581
+ maxCommitsPerFile: parseInt(opts.maxCommits, 10) || 200,
582
+ skipIfHeadUnchanged: !opts.force,
583
+ log: (m) => console.log(` ${m}`),
584
+ });
585
+ console.log(`\nSymbol history: ${r.historyRowsInserted} rows across ${r.filesProcessed} files (${r.elapsedMs}ms)`);
586
+ } finally { store.close(); }
587
+ });
588
+
589
+ // ── seer continuity (rename/move continuity evidence) ────────────────────
590
+
591
+ program
592
+ .command('continuity <symbol>')
593
+ .description('v10 — Show rename/move continuity evidence (advisory; confidence-labelled).')
594
+ .option('--db <path>', 'Database path')
595
+ .action(async (symbol: string, opts: { db?: string }) => {
596
+ const dbPath = opts.db ?? findDbFromCwd();
597
+ const store = openStore(dbPath);
598
+ try {
599
+ const { getContinuityForSymbol } = await import('../indexer/continuity.js');
600
+ const defs = store.getDefinition(symbol);
601
+ if (defs.length === 0) { console.log(`No symbol "${symbol}"`); return; }
602
+ for (const d of defs.slice(0, 3)) {
603
+ console.log(`\n${d.qualifiedName ?? d.name} (${d.kind}) ${d.filePath}:${d.lineStart + 1}`);
604
+ const rows = getContinuityForSymbol(store, d.id);
605
+ if (rows.length === 0) {
606
+ console.log(` (no continuity candidates)`);
607
+ continue;
608
+ }
609
+ for (const r of rows) {
610
+ console.log(` ← previous: ${r.previousName.padEnd(28)} conf=${r.confidence.toFixed(2)} [${r.matchReasons.join(', ')}]`);
611
+ console.log(` in: ${r.previousFile}`);
612
+ }
613
+ }
614
+ } finally { store.close(); }
615
+ });
616
+
617
+ // ── seer architecture ──────────────────────────────────────────────────────
618
+
619
+ program
620
+ .command('architecture')
621
+ .alias('arch')
622
+ .description('Show a one-page architecture snapshot of the codebase')
623
+ .option('--db <path>', 'Database path')
624
+ .action(async (opts: { db?: string }) => {
625
+ const dbPath = opts.db ?? findDbFromCwd();
626
+ const store = openStore(dbPath);
627
+ try {
628
+ const { buildArchitecture } = await import('../indexer/architecture.js');
629
+ const a = buildArchitecture(path.dirname(path.dirname(dbPath)), store);
630
+ console.log(`\nArchitecture snapshot`);
631
+ console.log(`─────────────────────`);
632
+ console.log(` Workspace: ${a.workspace}`);
633
+ console.log(` Totals: files=${a.totals.files} symbols=${a.totals.symbols} edges=${a.totals.edges} routes=${a.totals.routes} deps=${a.totals.externalDependencies} configKeys=${a.totals.configKeys}`);
634
+ console.log(`\n Languages:`);
635
+ for (const l of a.languages) console.log(` ${l.language.padEnd(14)} files=${l.files} symbols=${l.symbols}`);
636
+ console.log(`\n Top modules:`);
637
+ for (const m of a.topModules) console.log(` ${m.name.padEnd(20)} files=${m.files} symbols=${m.symbols}`);
638
+ console.log(`\n Top symbols:`);
639
+ for (const s of a.topSymbols.slice(0, 10)) console.log(` ${s.pagerank.toFixed(4)} ${(s.qualifiedName ?? s.name).padEnd(40)} (${s.kind})`);
640
+ if (a.entryPoints.length > 0) { console.log(`\n Entry points:`); for (const e of a.entryPoints) console.log(` ${(e.qualifiedName ?? e.name).padEnd(30)} ${e.file}`); }
641
+ if (a.hotspots.length > 0) { console.log(`\n Hotspots:`); for (const h of a.hotspots.slice(0, 10)) console.log(` ${h.commits.toString().padStart(5)} commits ${h.file}`); }
642
+ if (a.routes.total > 0) console.log(`\n Routes by framework: ${JSON.stringify(a.routes.byFramework)}`);
643
+ } finally { store.close(); }
644
+ });
645
+
646
+ // ── seer detect-changes ────────────────────────────────────────────────────
647
+
648
+ program
649
+ .command('detect-changes')
650
+ .description('Show blast radius of an uncommitted (or between-refs) diff')
651
+ .option('--db <path>', 'Database path')
652
+ .option('--workspace <path>', 'Workspace path (defaults to cwd)')
653
+ .option('--from <ref>', 'From ref (default: working tree)')
654
+ .option('--to <ref>', 'To ref')
655
+ .option('--depth <n>', 'Reverse-caller depth', '2')
656
+ .action(async (opts: { db?: string; workspace?: string; from?: string; to?: string; depth: string }) => {
657
+ const workspace = path.resolve(opts.workspace ?? process.cwd());
658
+ const dbPath = opts.db ?? findDbFromCwd();
659
+ const store = openStore(dbPath);
660
+ try {
661
+ const { detectChanges } = await import('../indexer/detectchanges.js');
662
+ const r = detectChanges(workspace, store, {
663
+ fromRef: opts.from, toRef: opts.to,
664
+ callerDepth: parseInt(opts.depth, 10) || 2,
665
+ });
666
+ console.log(`\nDetected ${r.changedFiles.length} changed file(s), ${r.directlyChanged.length} directly-changed symbol(s)`);
667
+ for (const f of r.changedFiles) {
668
+ console.log(`\n ${f.path} (${f.hunks} hunk(s))`);
669
+ for (const s of f.symbols) {
670
+ console.log(` → ${(s.symbol.qualifiedName ?? s.symbol.name).padEnd(40)} ${s.symbol.kind}`);
671
+ }
672
+ }
673
+ if (r.transitivelyAffected.length > 0) {
674
+ console.log(`\n Transitively-affected (top 15 by PageRank):`);
675
+ for (const s of r.transitivelyAffected.slice(0, 15)) {
676
+ console.log(` ${s.pagerank.toFixed(4)} ${(s.qualifiedName ?? s.name).padEnd(40)} ${s.kind} ${s.filePath}`);
677
+ }
678
+ }
679
+ console.log(`\n ${r.elapsedMs}ms`);
680
+ } finally { store.close(); }
681
+ });
682
+
683
+ // ── Track-E: modules / behavior / risk / context ──────────────────────────
684
+
685
+ program
686
+ .command('modules')
687
+ .description('List clustered modules (Louvain) by centrality / size / label')
688
+ .option('--db <path>', 'Database path')
689
+ .option('-n, --limit <n>', 'Max results', '40')
690
+ .option('--sort <by>', 'centrality | size | label', 'centrality')
691
+ .action((opts: { db?: string; limit: string; sort: string }) => {
692
+ const dbPath = opts.db ?? findDbFromCwd();
693
+ const store = openStore(dbPath);
694
+ try {
695
+ const sortBy = opts.sort === 'size' || opts.sort === 'label' ? opts.sort : 'centrality';
696
+ const rows = store.listModules({ limit: parseInt(opts.limit, 10) || 40, sortBy });
697
+ if (rows.length === 0) { console.log('No modules — run `seer index` to build the clustering.'); return; }
698
+ console.log(`\nModules (${rows.length} shown, sorted by ${sortBy})\n`);
699
+ console.log(` ${'Label'.padEnd(28)} ${'Files'.padStart(5)} ${'Symbols'.padStart(7)} ${'Lang'.padEnd(12)} ${'Cohesion'.padStart(8)} ${'Central'.padStart(8)}`);
700
+ console.log(' ' + '─'.repeat(80));
701
+ for (const m of rows) {
702
+ console.log(
703
+ ` ${m.label.padEnd(28)} ${String(m.sizeFiles).padStart(5)} ${String(m.sizeSymbols).padStart(7)} ${(m.primaryLanguage ?? '').padEnd(12)} ${m.cohesion.toFixed(2).padStart(8)} ${m.centrality.toFixed(4).padStart(8)}`,
704
+ );
705
+ }
706
+ } finally { store.close(); }
707
+ });
708
+
709
+ program
710
+ .command('module <label>')
711
+ .description('Show files and top symbols inside a module (by label or id)')
712
+ .option('--db <path>', 'Database path')
713
+ .option('-n, --files <n>', 'Max files', '50')
714
+ .option('-s, --symbols <n>', 'Max symbols', '20')
715
+ .action((label: string, opts: { db?: string; files: string; symbols: string }) => {
716
+ const dbPath = opts.db ?? findDbFromCwd();
717
+ const store = openStore(dbPath);
718
+ try {
719
+ const asId = parseInt(label, 10);
720
+ const mod = !isNaN(asId) && String(asId) === label
721
+ ? store.getModuleById(asId)
722
+ : store.getModuleByLabel(label);
723
+ if (!mod) { console.log(`No module "${label}"`); return; }
724
+ console.log(`\nModule "${mod.label}" id=${mod.id} files=${mod.sizeFiles} symbols=${mod.sizeSymbols} cohesion=${mod.cohesion.toFixed(2)} centrality=${mod.centrality.toFixed(4)}`);
725
+ const files = store.listModuleMembers(mod.id, parseInt(opts.files, 10) || 50);
726
+ console.log(`\n Files (${files.length}):`);
727
+ for (const f of files) console.log(` ${f.language.padEnd(12)} ${f.role.padEnd(9)} ${f.relPath}`);
728
+ const syms = store.listModuleTopSymbols(mod.id, parseInt(opts.symbols, 10) || 20);
729
+ console.log(`\n Top symbols (${syms.length}):`);
730
+ for (const s of syms) console.log(` ${s.pagerank.toFixed(4)} ${(s.qualifiedName ?? s.name).padEnd(40)} ${s.kind} ${s.filePath}`);
731
+ const out = store.moduleDependencies(mod.id, { direction: 'out', limit: 10 });
732
+ const inn = store.moduleDependencies(mod.id, { direction: 'in', limit: 10 });
733
+ if (out.length > 0) {
734
+ console.log(`\n Depends on (out):`);
735
+ for (const d of out) console.log(` ${d.label.padEnd(28)} kind=${d.kind.padEnd(8)} weight=${d.weight}`);
736
+ }
737
+ if (inn.length > 0) {
738
+ console.log(`\n Depended on by (in):`);
739
+ for (const d of inn) console.log(` ${d.label.padEnd(28)} kind=${d.kind.padEnd(8)} weight=${d.weight}`);
740
+ }
741
+ } finally { store.close(); }
742
+ });
743
+
744
+ program
745
+ .command('behavior <symbol>')
746
+ .description('Show ranked behavioral contract (tests) for a symbol')
747
+ .option('--db <path>', 'Database path')
748
+ .option('-n, --limit <n>', 'Max results', '20')
749
+ .option('--depth <n>', 'BFS depth for indirect coverage', '2')
750
+ .action((symbol: string, opts: { db?: string; limit: string; depth: string }) => {
751
+ const dbPath = opts.db ?? findDbFromCwd();
752
+ const store = openStore(dbPath);
753
+ try {
754
+ const r = rankedBehavior(store, symbol, {
755
+ limit: parseInt(opts.limit, 10) || 20,
756
+ indirectDepth: parseInt(opts.depth, 10) || 2,
757
+ });
758
+ if (!r) { console.log(`No symbol "${symbol}"`); return; }
759
+ console.log(`\nBehavior for ${r.symbol.qualifiedName ?? r.symbol.name} (${r.symbol.kind}) ${r.symbol.file}`);
760
+ console.log(` direct=${r.direct} indirect=${r.indirect} naming=${r.namingMatches} same-file=${r.sameFileMatches}\n`);
761
+ for (const t of r.tests) {
762
+ const dist = t.graphDistance == null ? ' ' : String(t.graphDistance).padStart(2);
763
+ console.log(
764
+ ` spec=${t.specificity.toString().padStart(4)} d=${dist} asserts=${String(t.assertionCount).padStart(2)} ${t.relationship.padEnd(18)} ${(t.testSymbol.qualifiedName ?? t.testSymbol.name).padEnd(40)} ${t.testSymbol.file}:${t.testSymbol.lineStart + 1}`,
765
+ );
766
+ }
767
+ } finally { store.close(); }
768
+ });
769
+
770
+ program
771
+ .command('risk <symbol>')
772
+ .description('Deterministic edit-risk profile for a symbol')
773
+ .option('--db <path>', 'Database path')
774
+ .option('--depth <n>', 'BFS depth for transitive callers', '3')
775
+ .action((symbol: string, opts: { db?: string; depth: string }) => {
776
+ const dbPath = opts.db ?? findDbFromCwd();
777
+ const store = openStore(dbPath);
778
+ try {
779
+ const r = computeRisk(store, symbol, { callerDepth: parseInt(opts.depth, 10) || 3 });
780
+ if (!r) { console.log(`No symbol "${symbol}"`); return; }
781
+ console.log(`\nRisk: ${r.risk.toUpperCase()} (score ${r.score.toFixed(2)})`);
782
+ console.log(` ${r.symbol.qualifiedName ?? r.symbol.name} (${r.symbol.kind}) ${r.symbol.file}:${r.symbol.lineStart + 1}`);
783
+ if (r.module) console.log(` module=${r.module.label}`);
784
+ console.log(`\n Signal contributions:`);
785
+ for (const c of r.signalContributions) {
786
+ const sign = c.contribution > 0 ? '+' : '';
787
+ console.log(` ${c.signal.padEnd(28)} value=${String(c.value).padEnd(8)} ${sign}${c.contribution.toFixed(2)}`);
788
+ }
789
+ if (r.signals.routes.length > 0) {
790
+ console.log(` Routes:`);
791
+ for (const rt of r.signals.routes) console.log(` ${rt.method} ${rt.path} (${rt.framework})`);
792
+ }
793
+ } finally { store.close(); }
794
+ });
795
+
796
+ program
797
+ .command('context <symbol>')
798
+ .description('One compact pre-edit packet: definition, callers, callees, routes, config, behavior, history, complexity, module, blast radius, risk')
799
+ .option('--db <path>', 'Database path')
800
+ .option('--file <path>', 'Disambiguate by file')
801
+ .action((symbol: string, opts: { db?: string; file?: string }) => {
802
+ const dbPath = opts.db ?? findDbFromCwd();
803
+ const store = openStore(dbPath);
804
+ try {
805
+ const c = buildContext(store, symbol, { filePath: opts.file });
806
+ if (!c) { console.log(`No symbol "${symbol}"`); return; }
807
+ console.log(`\nContext for ${c.symbol.qualifiedName ?? c.symbol.name} (${c.symbol.kind}) ${c.symbol.file}:${c.symbol.lineStart + 1}`);
808
+ if (c.module) console.log(` Module: ${c.module.label}`);
809
+ console.log(` Complexity: loc=${c.complexity.loc ?? '—'} cyclomatic=${c.complexity.cyclomatic ?? '—'} cognitive=${c.complexity.cognitive ?? '—'}`);
810
+ console.log(` Callers: ${c.callers.total} total; Callees: ${c.callees.total}; Blast radius (depth ${c.blastRadius.maxDepth}): direct=${c.blastRadius.directCallers}, transitive=${c.blastRadius.transitiveCallers}`);
811
+ console.log(` Behavior: direct=${c.behavior.direct} indirect=${c.behavior.indirect} naming=${c.behavior.namingMatches} same-file=${c.behavior.sameFileMatches}`);
812
+ console.log(` Routes: ${c.routes.length} Config: ${c.configKeys.length} History: ${c.recentHistory.total}`);
813
+ console.log(` Risk: ${c.risk.risk.toUpperCase()} (score ${c.risk.score.toFixed(2)})`);
814
+ console.log(`\n Signal contributions:`);
815
+ for (const sc of c.risk.signalContributions) {
816
+ const sign = sc.contribution > 0 ? '+' : '';
817
+ console.log(` ${sc.signal.padEnd(28)} ${sign}${sc.contribution.toFixed(2)}`);
818
+ }
819
+ } finally { store.close(); }
820
+ });
821
+
822
+ // ── Track-F: bundle export/import + CI pipeline ──────────────────────────
823
+
824
+ const bundleCmd = program
825
+ .command('bundle')
826
+ .description('Portable .seer index bundles (export, import, info)');
827
+
828
+ bundleCmd
829
+ .command('export')
830
+ .description('Export the current index as a portable .seerbundle file')
831
+ .option('--workspace <path>', 'Workspace path (defaults to cwd)')
832
+ .option('--db <path>', 'Database path')
833
+ .option('--out <path>', 'Output bundle path (default: <workspace>/.seer/index.seerbundle)')
834
+ .option('--level <n>', 'Gzip compression level 0-9 (default: 6)', '6')
835
+ .option('--built-at <ms>', 'Pin manifest.builtAt to a fixed Unix-millis value for reproducible bundles')
836
+ .action(async (opts: { workspace?: string; db?: string; out?: string; level: string; builtAt?: string }) => {
837
+ const workspace = path.resolve(opts.workspace ?? process.cwd());
838
+ const dbPath = opts.db ?? path.join(workspace, '.seer', 'graph.db');
839
+ const { exportBundle } = await import('../bundle/export.js');
840
+ const level = Math.max(0, Math.min(9, parseInt(opts.level, 10) || 6));
841
+ const builtAt = opts.builtAt ? parseInt(opts.builtAt, 10) : undefined;
842
+ const r = await exportBundle(dbPath, workspace, {
843
+ out: opts.out, compressionLevel: level,
844
+ builtAt: (builtAt != null && !isNaN(builtAt)) ? builtAt : undefined,
845
+ log: (m) => console.log(` ${m}`),
846
+ });
847
+ console.log(`\n ✓ Bundle exported to ${r.bundlePath}`);
848
+ console.log(` ${r.bytes.toLocaleString()} bytes schemaVersion=${r.manifest.schemaVersion} symbols=${r.manifest.index.symbols} edges=${r.manifest.index.edges}`);
849
+ console.log(` DB sha256=${r.manifest.dbSha256.slice(0, 16)}... built in ${r.elapsedMs}ms`);
850
+ });
851
+
852
+ bundleCmd
853
+ .command('import <bundle>')
854
+ .description('Import a .seerbundle into a workspace. Add --external to import additively as a peer-repo evidence layer (does not replace the local DB).')
855
+ .option('--workspace <path>', 'Workspace path (defaults to cwd)')
856
+ .option('--db <path>', 'Database path (default: <workspace>/.seer/graph.db)')
857
+ .option('--overwrite', 'Allow overwriting an existing index')
858
+ .option('--skip-integrity-check', 'Skip sha256 check (forensics only)')
859
+ .option('--skip-schema-check', 'Skip schemaVersion compatibility check (use only if you KNOW the bundle is safe)')
860
+ .option('--external', 'Additive external import — adds routes/service endpoints as a read-only external layer, never replaces local rows.')
861
+ .option('--alias <name>', 'Optional alias for the external bundle (defaults to manifest.gitBranch or filename).')
862
+ .option('--force', 'Force re-import even if the same hash is already present (external mode only).')
863
+ .action(async (bundle: string, opts: { workspace?: string; db?: string; overwrite?: boolean; skipIntegrityCheck?: boolean; skipSchemaCheck?: boolean; external?: boolean; alias?: string; force?: boolean }) => {
864
+ const workspace = path.resolve(opts.workspace ?? process.cwd());
865
+ if (opts.external) {
866
+ const dbPath = opts.db ?? path.join(workspace, '.seer', 'graph.db');
867
+ if (!fs.existsSync(dbPath)) {
868
+ console.error(`No index at ${dbPath}. Run "seer index <path>" first before importing an external bundle.`);
869
+ process.exit(1);
870
+ }
871
+ const { importExternalBundle } = await import('../bundle/external.js');
872
+ const store = new Store(dbPath);
873
+ try {
874
+ const r = await importExternalBundle(path.resolve(bundle), store, {
875
+ alias: opts.alias, force: opts.force,
876
+ log: (m) => console.log(` ${m}`),
877
+ });
878
+ if (r.alreadyImported) {
879
+ console.log(`\n ↻ External bundle already imported (hash unchanged); no-op.`);
880
+ } else {
881
+ console.log(`\n ✓ External bundle imported as layer #${r.bundleId} (${r.externalProject ?? 'unnamed'}).`);
882
+ console.log(` routes=${r.routesImported} serviceEndpoints=${r.serviceEndpointsImported} schemaVersion=${r.schemaVersion}`);
883
+ console.log(` hash=${r.externalHash.slice(0, 12)}... took ${r.elapsedMs}ms`);
884
+ }
885
+ } catch (err) {
886
+ console.error(`\n ✗ External import failed: ${(err as Error).message}`);
887
+ process.exit(1);
888
+ } finally { store.close(); }
889
+ return;
890
+ }
891
+ const { importBundle } = await import('../bundle/import.js');
892
+ try {
893
+ const r = await importBundle(path.resolve(bundle), {
894
+ repoRoot: workspace,
895
+ dbOut: opts.db,
896
+ overwrite: opts.overwrite,
897
+ skipIntegrityCheck: opts.skipIntegrityCheck,
898
+ skipSchemaCheck: opts.skipSchemaCheck,
899
+ log: (m) => console.log(` ${m}`),
900
+ });
901
+ console.log(`\n ✓ Bundle imported to ${r.dbPath}`);
902
+ console.log(` builtAt=${new Date(r.manifest.builtAt).toISOString()} schemaVersion=${r.manifest.schemaVersion}`);
903
+ console.log(` symbols=${r.manifest.index.symbols} edges=${r.manifest.index.edges} modules=${r.manifest.index.modules}`);
904
+ console.log(` Took ${r.elapsedMs}ms`);
905
+ } catch (err) {
906
+ console.error(`\n ✗ Import failed: ${(err as Error).message}`);
907
+ process.exit(1);
908
+ }
909
+ });
910
+
911
+ // ── seer boundaries (Feature 4: monorepo boundary detection) ─────────────
912
+
913
+ program
914
+ .command('boundaries')
915
+ .description('List monorepo package/service boundaries detected at index time.')
916
+ .option('--db <path>', 'Database path')
917
+ .option('-n, --limit <n>', 'Max results', '50')
918
+ .action((opts: { db?: string; limit: string }) => {
919
+ const dbPath = opts.db ?? findDbFromCwd();
920
+ const store = openStore(dbPath);
921
+ try {
922
+ const rows = store.listBoundaries(parseInt(opts.limit, 10) || 50);
923
+ if (rows.length === 0) {
924
+ console.log('No boundaries detected — workspace has no nested package manifests or convention dirs.');
925
+ return;
926
+ }
927
+ console.log(`\nBoundaries (${rows.length} shown)\n`);
928
+ for (const b of rows) {
929
+ const eco = b.ecosystem ? `[${b.ecosystem}]` : '';
930
+ console.log(` ${b.kind.padEnd(16)} ${String(b.sizeFiles).padStart(5)} ${b.label.padEnd(20)} ${eco} ${b.rootRelPath || '.'}`);
931
+ }
932
+ } finally { store.close(); }
933
+ });
934
+
935
+ // ── seer preflight ────────────────────────────────────────────────────────
936
+
937
+ program
938
+ .command('preflight')
939
+ .description('One compact "should I edit this?" evidence packet for an agent. Pass --symbol <X> for a single-symbol packet, or --from <ref> --to <ref> for a diff-range packet.')
940
+ .option('--db <path>', 'Database path')
941
+ .option('--workspace <path>', 'Workspace path (defaults to cwd)')
942
+ .option('--symbol <name>', 'Build a packet for the named symbol.')
943
+ .option('--file <path>', 'Optional file to disambiguate the symbol.')
944
+ .option('--from <ref>', 'Build a range packet from this git ref.')
945
+ .option('--to <ref>', 'Build a range packet to this git ref.')
946
+ .option('--old-bundle <path>', 'Optional old .seerbundle to include contract changes.')
947
+ .option('--new-bundle <path>', 'Optional new .seerbundle to include contract changes.')
948
+ .option('--max-symbols <n>', 'Cap on touched symbols (default 12)', '12')
949
+ .option('--max-tests <n>', 'Cap on likely tests (default 8)', '8')
950
+ .option('--max-history <n>', 'Cap on history rows (default 8)', '8')
951
+ .option('--json', 'Print machine-readable JSON.')
952
+ .action(async (opts: {
953
+ db?: string; workspace?: string;
954
+ symbol?: string; file?: string;
955
+ from?: string; to?: string;
956
+ oldBundle?: string; newBundle?: string;
957
+ maxSymbols: string; maxTests: string; maxHistory: string;
958
+ json?: boolean;
959
+ }) => {
960
+ const dbPath = opts.db ?? findDbFromCwd();
961
+ const workspace = path.resolve(opts.workspace ?? process.cwd());
962
+ const store = openStore(dbPath);
963
+ try {
964
+ const { preflight } = await import('../indexer/preflight.js');
965
+ const r = await preflight(store, {
966
+ symbol: opts.symbol,
967
+ filePath: opts.file,
968
+ fromRef: opts.from,
969
+ toRef: opts.to,
970
+ workspace,
971
+ oldBundle: opts.oldBundle,
972
+ newBundle: opts.newBundle,
973
+ maxSymbols: parseInt(opts.maxSymbols, 10) || 12,
974
+ maxTests: parseInt(opts.maxTests, 10) || 8,
975
+ maxHistory: parseInt(opts.maxHistory, 10) || 8,
976
+ });
977
+ if (opts.json) {
978
+ process.stdout.write(JSON.stringify(r, null, 2) + '\n');
979
+ } else {
980
+ printPreflight(r);
981
+ }
982
+ // Advisory: never raise non-zero exit when preflight finds risk.
983
+ process.exit(r.ok ? 0 : 1);
984
+ } finally { store.close(); }
985
+ });
986
+
987
+ function printPreflight(r: import('../indexer/preflight.js').PreflightResult): void {
988
+ if (!r.ok) {
989
+ console.log(`\n ✗ preflight failed: ${r.reason}`);
990
+ return;
991
+ }
992
+ console.log(`\nPreflight (${r.mode})`);
993
+ if (r.symbol) {
994
+ console.log(` Symbol: ${r.symbol.qualifiedName ?? r.symbol.name} ${r.symbol.file}:${r.symbol.lineStart + 1}`);
995
+ }
996
+ if (r.range) {
997
+ console.log(` Range: ${r.range.fromRef ?? '(working tree)'} → ${r.range.toRef ?? 'HEAD'}`);
998
+ console.log(` ${r.range.changedFiles} file(s), ${r.range.directHunkCount} hunk(s)`);
999
+ }
1000
+ console.log(` Risk: ${r.risk.overall.toUpperCase()}`);
1001
+ for (const r2 of r.risk.perSymbol.slice(0, 5)) {
1002
+ console.log(` - ${r2.symbol.qualifiedName ?? r2.symbol.name} score=${r2.score.toFixed(2)} ${r2.risk}`);
1003
+ }
1004
+ if (r.likelyTests.length > 0) {
1005
+ console.log(` Likely tests (${r.likelyTests.length}):`);
1006
+ for (const t of r.likelyTests.slice(0, 8)) {
1007
+ console.log(` • ${(t.testSymbol.qualifiedName ?? t.testSymbol.name).padEnd(40)} [${t.relationship}] spec=${t.specificity}`);
1008
+ }
1009
+ }
1010
+ if (r.serviceImpact.inbound.length + r.serviceImpact.outbound.length > 0) {
1011
+ console.log(` Service impact: in=${r.serviceImpact.inbound.length} out=${r.serviceImpact.outbound.length}`);
1012
+ }
1013
+ if (r.history.length > 0) {
1014
+ console.log(` Recent commits (${r.history.length}):`);
1015
+ for (const h of r.history.slice(0, 5)) {
1016
+ const date = new Date(h.committedAt * 1000).toISOString().slice(0, 10);
1017
+ console.log(` ${h.sha.slice(0, 8)} ${date} ${(h.author ?? '?').slice(0, 24).padEnd(24)} ${(h.message ?? '').split('\n')[0].slice(0, 60)}`);
1018
+ }
1019
+ }
1020
+ if (r.warnings.length > 0) {
1021
+ console.log(` Warnings:`);
1022
+ for (const w of r.warnings) console.log(` ⚠ ${w}`);
1023
+ }
1024
+ }
1025
+
1026
+ // ── seer contract diff ────────────────────────────────────────────────────
1027
+
1028
+ const contractCmd = program
1029
+ .command('contract')
1030
+ .description('API/service contract diffing across exported .seerbundle files (advisory).');
1031
+
1032
+ contractCmd
1033
+ .command('diff <old-bundle> <new-bundle>')
1034
+ .description('Diff API/service contracts (routes, tRPC/GraphQL/gRPC ops, topics, queues) between two bundles. Exit 0 even when breaking changes appear — advisory only.')
1035
+ .option('--json', 'Emit machine-readable JSON instead of a compact table.')
1036
+ .option('--include-callers', 'Include affectedCallers using service-link evidence from both bundles.')
1037
+ .action(async (oldBundle: string, newBundle: string, opts: { json?: boolean; includeCallers?: boolean }) => {
1038
+ const { contractDiff, formatContractDiffTable } = await import('../bundle/contract.js');
1039
+ try {
1040
+ const diff = await contractDiff(
1041
+ path.resolve(oldBundle),
1042
+ path.resolve(newBundle),
1043
+ { includeAffectedCallers: opts.includeCallers },
1044
+ );
1045
+ if (opts.json) {
1046
+ process.stdout.write(JSON.stringify(diff, null, 2) + '\n');
1047
+ } else {
1048
+ process.stdout.write(formatContractDiffTable(diff));
1049
+ }
1050
+ // Advisory: always exit 0.
1051
+ process.exit(0);
1052
+ } catch (err) {
1053
+ console.error(`\n ✗ contract diff failed: ${(err as Error).message}`);
1054
+ process.exit(1);
1055
+ }
1056
+ });
1057
+
1058
+ bundleCmd
1059
+ .command('external')
1060
+ .description('List external bundle layers imported into this workspace.')
1061
+ .option('--db <path>', 'Database path')
1062
+ .action((opts: { db?: string }) => {
1063
+ const dbPath = opts.db ?? findDbFromCwd();
1064
+ const store = openStore(dbPath);
1065
+ try {
1066
+ const rows = store.listExternalBundles();
1067
+ if (rows.length === 0) {
1068
+ console.log('No external bundles imported.');
1069
+ return;
1070
+ }
1071
+ console.log(`\nExternal bundle layers (${rows.length}):\n`);
1072
+ for (const r of rows) {
1073
+ console.log(` #${r.id} ${r.externalProject ?? '(unnamed)'} routes=${r.routesImported}`);
1074
+ console.log(` path=${r.bundlePath}`);
1075
+ console.log(` hash=${(r.externalHash ?? '').slice(0, 12)}... imported=${new Date(r.importedAt).toISOString()}`);
1076
+ }
1077
+ } finally { store.close(); }
1078
+ });
1079
+
1080
+ bundleCmd
1081
+ .command('info <bundle>')
1082
+ .description('Show a bundle\'s manifest without unpacking the DB')
1083
+ .action(async (bundle: string) => {
1084
+ const { readBundleManifest } = await import('../bundle/import.js');
1085
+ try {
1086
+ const manifest = readBundleManifest(path.resolve(bundle));
1087
+ console.log(JSON.stringify(manifest, null, 2));
1088
+ } catch (err) {
1089
+ console.error(`\n ✗ ${(err as Error).message}`);
1090
+ process.exit(1);
1091
+ }
1092
+ });
1093
+
1094
+ const ciCmd = program
1095
+ .command('ci')
1096
+ .description('CI helpers: bundle generation, workflow templates');
1097
+
1098
+ ciCmd
1099
+ .command('bundle')
1100
+ .description('Fresh-index the repo and emit a portable bundle (designed for CI)')
1101
+ .option('--workspace <path>', 'Repo to index (defaults to cwd)')
1102
+ .option('--out <path>', 'Output path (default: <workspace>/.seer/index.seerbundle)')
1103
+ .option('--mode <mode>', 'Discovery mode: full | standard | fast (default: standard)', 'standard')
1104
+ .option('--no-reset', 'Keep existing DB before indexing (default: wipe)')
1105
+ .option('--no-parallel', 'Disable parallel parsing')
1106
+ .option('--git-head <sha>', 'Override gitHead in the manifest')
1107
+ .option('--git-branch <name>', 'Override gitBranch in the manifest')
1108
+ .option('--built-at <ms>', 'Pin manifest.builtAt to a fixed Unix-millis value for reproducible bundles')
1109
+ .action(async (opts: { workspace?: string; out?: string; mode?: string; reset?: boolean; parallel?: boolean; gitHead?: string; gitBranch?: string; builtAt?: string }) => {
1110
+ const workspace = path.resolve(opts.workspace ?? process.cwd());
1111
+ const { buildCiBundle } = await import('../bundle/ci.js');
1112
+ try {
1113
+ const builtAt = opts.builtAt ? parseInt(opts.builtAt, 10) : undefined;
1114
+ const r = await buildCiBundle({
1115
+ repoRoot: workspace, out: opts.out,
1116
+ mode: parseMode(opts.mode),
1117
+ reset: opts.reset, parallel: opts.parallel,
1118
+ gitHead: opts.gitHead, gitBranch: opts.gitBranch,
1119
+ builtAt: (builtAt != null && !isNaN(builtAt)) ? builtAt : undefined,
1120
+ });
1121
+ console.log(`\n ✓ CI bundle: ${r.bundle.bundlePath}`);
1122
+ console.log(` ${r.index.symbols.toLocaleString()} symbols / ${r.index.edges.toLocaleString()} edges in ${r.totalElapsedMs}ms`);
1123
+ } catch (err) {
1124
+ console.error(`\n ✗ CI bundle failed: ${(err as Error).message}`);
1125
+ process.exit(1);
1126
+ }
1127
+ });
1128
+
1129
+ ciCmd
1130
+ .command('workflow')
1131
+ .description('Emit a ready-to-paste GitHub Actions workflow that builds a bundle on push')
1132
+ .action(async () => {
1133
+ const { workflowTemplate } = await import('../bundle/ci.js');
1134
+ process.stdout.write(workflowTemplate());
1135
+ });
1136
+
1137
+ // ── Track-F: SCIP import ────────────────────────────────────────────────
1138
+
1139
+ program
1140
+ .command('scip-import <scip-path>')
1141
+ .description('Import a SCIP precision index. Adds source-labelled precise edges over the tree-sitter baseline.')
1142
+ .option('--workspace <path>', 'Workspace path (defaults to cwd)')
1143
+ .option('--db <path>', 'Database path')
1144
+ .option('--require-file-in-index', 'Skip SCIP docs whose file isn\'t already indexed (default: on)')
1145
+ .option('--no-require-file-in-index', 'Accept SCIP docs for files outside the tree-sitter index')
1146
+ .action(async (scipPath: string, opts: { workspace?: string; db?: string; requireFileInIndex?: boolean }) => {
1147
+ const workspace = path.resolve(opts.workspace ?? process.cwd());
1148
+ const dbPath = opts.db ?? path.join(workspace, '.seer', 'graph.db');
1149
+ if (!fs.existsSync(dbPath)) {
1150
+ console.error(`No index at ${dbPath}. Run "seer index <path>" first.`);
1151
+ process.exit(1);
1152
+ }
1153
+ const { importScip } = await import('../scip/import.js');
1154
+ const store = new Store(dbPath);
1155
+ try {
1156
+ const r = await importScip(path.resolve(scipPath), store, {
1157
+ repoRoot: workspace,
1158
+ requireFileInIndex: opts.requireFileInIndex ?? true,
1159
+ log: (m) => console.log(` ${m}`),
1160
+ });
1161
+ console.log(`\n ✓ SCIP import done in ${r.elapsedMs}ms`);
1162
+ console.log(` docs=${r.documentsProcessed} symbols=${r.symbolsInserted} new, ${r.symbolsMerged} merged`);
1163
+ console.log(` edges=${r.edgesInserted} filesMissing=${r.filesMissing}`);
1164
+ console.log(` tool=${r.tool ?? '—'} sha=${r.sha256.slice(0, 12)}...`);
1165
+ } finally { store.close(); }
1166
+ });
1167
+
1168
+ // ── Track-F: duplicate detection ────────────────────────────────────────
1169
+
1170
+ program
1171
+ .command('duplicates')
1172
+ .alias('dupes')
1173
+ .description('Find clusters of structurally-similar functions (SimHash)')
1174
+ .option('--db <path>', 'Database path')
1175
+ .option('--max-distance <n>', 'Max Hamming distance for two symbols to cluster (default: 6)', '6')
1176
+ .option('--min-loc <n>', 'Minimum lines-of-code to consider (default: 4)', '4')
1177
+ .option('--include-tests', 'Include test files (off by default)')
1178
+ .option('-n, --limit <n>', 'Max clusters to show', '40')
1179
+ .action(async (opts: { db?: string; maxDistance: string; minLoc: string; includeTests?: boolean; limit: string }) => {
1180
+ const dbPath = opts.db ?? findDbFromCwd();
1181
+ const store = openStore(dbPath);
1182
+ try {
1183
+ const { findDuplicates } = await import('../indexer/shapehash.js');
1184
+ const clusters = findDuplicates(store, {
1185
+ maxDistance: parseInt(opts.maxDistance, 10) || 6,
1186
+ minLoc: parseInt(opts.minLoc, 10) || 4,
1187
+ includeTests: opts.includeTests,
1188
+ maxClusters: parseInt(opts.limit, 10) || 40,
1189
+ });
1190
+ if (clusters.length === 0) {
1191
+ console.log('No duplicate clusters found (have you run `seer index`? — shape hashes are built during indexing).');
1192
+ return;
1193
+ }
1194
+ console.log(`\nFound ${clusters.length} duplicate cluster(s):\n`);
1195
+ for (const c of clusters) {
1196
+ console.log(` Cluster (${c.symbols.length} symbols, fingerprint=${c.fingerprint.toString(16).slice(0, 8)}...)`);
1197
+ for (const s of c.symbols) {
1198
+ console.log(` [d=${s.hammingFromAnchor.toString().padStart(2)}] ${(s.qualifiedName ?? s.name).padEnd(40)} ${s.kind.padEnd(10)} loc=${(s.loc ?? '?').toString().padStart(3)} ${s.file}:${s.lineStart + 1}`);
1199
+ }
1200
+ console.log();
1201
+ }
1202
+ } finally { store.close(); }
1203
+ });
1204
+
1205
+ // ── seer mcp ─────────────────────────────────────────────────────────────────
1206
+
1207
+ program
1208
+ .command('mcp')
1209
+ .description('Run an MCP server (stdio JSON-RPC) over the index.')
1210
+ .option('--workspace <path>', 'Workspace path (defaults to current directory)')
1211
+ .option('--db <path>', 'Custom database path')
1212
+ .option('--no-watch', 'Disable the background file watcher')
1213
+ .option('--no-jit', 'Disable JIT freshness checks before each query')
1214
+ .action(async (opts: { workspace?: string; db?: string; watch?: boolean; jit?: boolean }) => {
1215
+ const workspace = path.resolve(opts.workspace ?? process.cwd());
1216
+ if (!fs.existsSync(workspace)) { console.error(`Workspace not found: ${workspace}`); process.exit(1); }
1217
+ const { runMcp } = await import('../mcp/server.js');
1218
+ await runMcp({
1219
+ workspace,
1220
+ dbPath: opts.db,
1221
+ watch: opts.watch !== false,
1222
+ jit: opts.jit !== false,
1223
+ });
1224
+ });
1225
+
1226
+ // ── DB auto-detection ──────────────────────────────────────────────────────────
1227
+
1228
+ function parseMode(input: string | undefined): 'full' | 'standard' | 'fast' | undefined {
1229
+ if (!input) return undefined;
1230
+ const v = input.toLowerCase();
1231
+ if (v === 'full' || v === 'standard' || v === 'fast') return v;
1232
+ console.error(`Invalid --mode: ${input}.`);
1233
+ process.exit(1);
1234
+ }
1235
+
1236
+ function findDbFromCwd(): string {
1237
+ let dir = process.cwd();
1238
+ for (let i = 0; i < 6; i++) {
1239
+ const candidate = path.join(dir, '.seer', 'graph.db');
1240
+ if (fs.existsSync(candidate)) return candidate;
1241
+ const parent = path.dirname(dir);
1242
+ if (parent === dir) break;
1243
+ dir = parent;
1244
+ }
1245
+ console.error('Could not find .seer/graph.db. Run "seer index <path>" first.');
1246
+ process.exit(1);
1247
+ }
1248
+
1249
+ program.parse(process.argv);