project-athena 0.2.0 → 0.3.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 (148) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +19 -11
  3. package/dist/activity-P4UUFHWV.js +77 -0
  4. package/dist/agents-3HTSQGL2.js +69 -0
  5. package/dist/ai-ASL4FFQ7.js +96 -0
  6. package/dist/analyze-F3YOILNL.js +49 -0
  7. package/dist/chunk-2DY4PUR4.js +465 -0
  8. package/dist/chunk-2R6PXY6Z.js +33 -0
  9. package/dist/chunk-2RS6TA3D.js +908 -0
  10. package/dist/chunk-32PRS2MG.js +169 -0
  11. package/dist/chunk-3IS2X2Z7.js +89 -0
  12. package/dist/chunk-65TSRDQ3.js +124 -0
  13. package/dist/chunk-673EPYYH.js +105 -0
  14. package/dist/chunk-AD4OZCU2.js +34 -0
  15. package/dist/chunk-CXUTYCZX.js +238 -0
  16. package/dist/chunk-DOQMAZL5.js +436 -0
  17. package/dist/chunk-EF6GHOIN.js +343 -0
  18. package/dist/chunk-ELKFRTZT.js +221 -0
  19. package/dist/chunk-EUPETUWA.js +14 -0
  20. package/dist/chunk-FQEGEVLY.js +38 -0
  21. package/dist/chunk-GHFRQAYT.js +112 -0
  22. package/dist/chunk-GR4DL2VM.js +47 -0
  23. package/dist/chunk-INT3IGMQ.js +111 -0
  24. package/dist/chunk-IQQBAWSM.js +90 -0
  25. package/dist/chunk-JCGDR4UV.js +182 -0
  26. package/dist/chunk-JY6BFIJJ.js +95 -0
  27. package/dist/chunk-KBO7XRBW.js +26 -0
  28. package/dist/chunk-LEMPCNAY.js +209 -0
  29. package/dist/chunk-LMOFZNTQ.js +379 -0
  30. package/dist/chunk-OLPSEQS5.js +264 -0
  31. package/dist/chunk-PANVPNYF.js +2892 -0
  32. package/dist/chunk-PDFQ5BFG.js +378 -0
  33. package/dist/chunk-PR53DT7I.js +179 -0
  34. package/dist/chunk-PVCIGUIT.js +64 -0
  35. package/dist/chunk-RXNRMIZP.js +390 -0
  36. package/dist/chunk-S7T6VX56.js +61 -0
  37. package/dist/chunk-SHKXELUX.js +295 -0
  38. package/dist/chunk-SJOOXO34.js +904 -0
  39. package/dist/chunk-TGJUUB44.js +399 -0
  40. package/dist/chunk-VSYBC3VY.js +28 -0
  41. package/dist/chunk-XB66PZ6Z.js +182 -0
  42. package/dist/chunk-XFN7UZCS.js +35 -0
  43. package/dist/chunk-XW3MLHI2.js +32 -0
  44. package/dist/chunk-Z6QWMJLK.js +124 -0
  45. package/dist/chunk-ZB7LC6FP.js +221 -0
  46. package/dist/clean-S7PKCOLM.js +23 -0
  47. package/dist/cli.js +18 -9795
  48. package/dist/context-AH5MFHRG.js +147 -0
  49. package/dist/doctor-RSJMLE2P.js +62 -0
  50. package/dist/event-TNT6CQZR.js +101 -0
  51. package/dist/git-hook-YZNZGAAP.js +252 -0
  52. package/dist/init-2V34DVRQ.js +37 -0
  53. package/dist/mcp-T2GRTEX5.js +255 -0
  54. package/dist/open-2QSZKKUG.js +1259 -0
  55. package/dist/program-V2USHGXN.js +148 -0
  56. package/dist/review-R3R2J5BJ.js +115 -0
  57. package/dist/rules-4FXJUCXB.js +83 -0
  58. package/dist/security-PI4LXCOB.js +175 -0
  59. package/dist/staged-check-HHIBKKMR.js +500 -0
  60. package/dist/status-6M73WX5I.js +75 -0
  61. package/dist/sync-3ZNJDSSU.js +216 -0
  62. package/dist/watch-USN5CQIV.js +121 -0
  63. package/dist/web/assets/{Editor-CL6A82dQ.js → Editor-BI56-HX5.js} +1 -1
  64. package/dist/web/assets/{abnfDiagram-O67JEVCF-BuFfjTfx.js → abnfDiagram-O67JEVCF-y_9px2ob.js} +1 -1
  65. package/dist/web/assets/architecture-7GRP2DOG-DyxkwYiW.js +1 -0
  66. package/dist/web/assets/{architectureDiagram-NJMV4G6O-BTsv87pA.js → architectureDiagram-NJMV4G6O-lB6fcVzU.js} +1 -1
  67. package/dist/web/assets/{blockDiagram-BEXU5L5S-CNXqyKIx.js → blockDiagram-BEXU5L5S-DcM5pEjM.js} +1 -1
  68. package/dist/web/assets/{c4Diagram-YGBWAQC7-kksFXQoJ.js → c4Diagram-YGBWAQC7-CJgqxTY2.js} +1 -1
  69. package/dist/web/assets/channel-BvDD7AMO.js +1 -0
  70. package/dist/web/assets/{chunk-3FUC2YCW-BY99XYBs.js → chunk-3FUC2YCW-B8l_VuWR.js} +1 -1
  71. package/dist/web/assets/{chunk-5DYCD2WN-h-PRThMP.js → chunk-5DYCD2WN-CZecgNhf.js} +1 -1
  72. package/dist/web/assets/{chunk-742MDFTN-BIxfyBUj.js → chunk-742MDFTN-BeZ7P5jB.js} +1 -1
  73. package/dist/web/assets/{chunk-7INBJB4K-DZtctDdk.js → chunk-7INBJB4K-IKy0pr5m.js} +1 -1
  74. package/dist/web/assets/{chunk-7M6MHVWA-BX9fUUbL.js → chunk-7M6MHVWA-Dg_QqJEX.js} +1 -1
  75. package/dist/web/assets/{chunk-7PRAP22T-X1s3e81n.js → chunk-7PRAP22T-DqQNS8oq.js} +1 -1
  76. package/dist/web/assets/{chunk-GTNCS2PH-Bh8eluU-.js → chunk-GTNCS2PH-x4sM51nJ.js} +1 -1
  77. package/dist/web/assets/{chunk-GWA4HPMP-D1KATAi0.js → chunk-GWA4HPMP-CE5GcwVN.js} +1 -1
  78. package/dist/web/assets/{chunk-MBY4JIJT-CEErv5kP.js → chunk-MBY4JIJT-DA_D1hjF.js} +1 -1
  79. package/dist/web/assets/{chunk-NETBCI7D-BXiH4aPa.js → chunk-NETBCI7D-CeJ3XBZ2.js} +1 -1
  80. package/dist/web/assets/{chunk-O7XYJQB3-Dede62yi.js → chunk-O7XYJQB3-BH2gESvO.js} +1 -1
  81. package/dist/web/assets/{chunk-UA2S7LBM-KZ9aWRTa.js → chunk-UA2S7LBM-BWr0xCux.js} +1 -1
  82. package/dist/web/assets/{chunk-WEXAMYUT-xf-3L2zU.js → chunk-WEXAMYUT-B4QwUZPM.js} +1 -1
  83. package/dist/web/assets/{chunk-Z7XXMR3K-DLpRdZ8n.js → chunk-Z7XXMR3K-BxcA-y5B.js} +1 -1
  84. package/dist/web/assets/{chunk-ZIGJFQKS-BdJ0_J-U.js → chunk-ZIGJFQKS-BZYvP63G.js} +1 -1
  85. package/dist/web/assets/{classDiagram-v2-NBCMYWYE-C5VzGDGq.js → classDiagram-v2-NBCMYWYE-BS3P_NXb.js} +1 -1
  86. package/dist/web/assets/{cynefin-OW5HDTMX-BqOgbE7U.js → cynefin-OW5HDTMX-BALD4Pve.js} +1 -1
  87. package/dist/web/assets/{cynefinDiagram-VND7K2PF-CCC_feG5.js → cynefinDiagram-VND7K2PF-BM2y4W6d.js} +1 -1
  88. package/dist/web/assets/{dagre-6A5THRUB-SLP-94sd.js → dagre-6A5THRUB-PTfOrIdF.js} +1 -1
  89. package/dist/web/assets/{diagram-22UHCM2B-DpHwEBYM.js → diagram-22UHCM2B-CpXEHhUl.js} +1 -1
  90. package/dist/web/assets/{diagram-3UASUU5V-iGFwuMEk.js → diagram-3UASUU5V-DPHfXzr6.js} +1 -1
  91. package/dist/web/assets/{diagram-ATOU4E4O-Csx9hj0z.js → diagram-ATOU4E4O-DMBNXvAC.js} +1 -1
  92. package/dist/web/assets/{diagram-CDSNMT55-yGKrLRO5.js → diagram-CDSNMT55-CDLe1M7G.js} +1 -1
  93. package/dist/web/assets/{diagram-MLGK6HIB-mDpCormD.js → diagram-MLGK6HIB-D6L8oE7j.js} +1 -1
  94. package/dist/web/assets/{diagram-MPIPVDR6-BJHdatrF.js → diagram-MPIPVDR6-CP-S7rrQ.js} +1 -1
  95. package/dist/web/assets/{ebnfDiagram-ZINNZB2B-ClJbCBBu.js → ebnfDiagram-ZINNZB2B-CkUc63TK.js} +1 -1
  96. package/dist/web/assets/{elk-276RUBZZ-C5tFZMYm.js → elk-276RUBZZ-C1CAJLH_.js} +1 -1
  97. package/dist/web/assets/{erDiagram-OPXOYQCR-u7_UFZot.js → erDiagram-OPXOYQCR-DXBnaTHm.js} +1 -1
  98. package/dist/web/assets/eventmodeling-NTZA5JFV-cLIzvgDp.js +1 -0
  99. package/dist/web/assets/flowDiagram-KWPJA3E3-D7Zq63VQ.js +1 -0
  100. package/dist/web/assets/{ganttDiagram-FUAMR5RP-B4hrLcLf.js → ganttDiagram-FUAMR5RP-BVm8SiGg.js} +1 -1
  101. package/dist/web/assets/{gitGraph-4MIJSDKK-Dxx-Bl-U.js → gitGraph-4MIJSDKK-Dm7xBrW0.js} +1 -1
  102. package/dist/web/assets/{gitGraphDiagram-X574FWY7-DKjDV5Kr.js → gitGraphDiagram-X574FWY7-D_PIKKhx.js} +1 -1
  103. package/dist/web/assets/index-CeOIBpu2.js +45 -0
  104. package/dist/web/assets/{info-A6RAGUB7-CgqIdgQA.js → info-A6RAGUB7-CLGrx8pn.js} +1 -1
  105. package/dist/web/assets/{infoDiagram-VRGFBTTK-D-qjWkwy.js → infoDiagram-VRGFBTTK-BlBjaKm7.js} +1 -1
  106. package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-CzpqV6WN.js → ishikawaDiagram-OU5B5YK6-Kxe7GGS3.js} +1 -1
  107. package/dist/web/assets/{journeyDiagram-ZHPQQLJL-BBnOUDzw.js → journeyDiagram-ZHPQQLJL-wCMoHK4M.js} +1 -1
  108. package/dist/web/assets/{kanban-definition-PNTS6WVX-Di9sTFB1.js → kanban-definition-PNTS6WVX-BxdCz9Ko.js} +1 -1
  109. package/dist/web/assets/{line-B8ehGiwF.js → line-Zf11FnWF.js} +1 -1
  110. package/dist/web/assets/{mermaid-parser.core-DWsT24e5.js → mermaid-parser.core-BvFFklev.js} +3 -3
  111. package/dist/web/assets/{mermaid.core-DzYpaVzI.js → mermaid.core-Cd_Ssti9.js} +4 -4
  112. package/dist/web/assets/{mindmap-definition-NLK3R4M7-CxagBsH3.js → mindmap-definition-NLK3R4M7-BGPQl-GY.js} +1 -1
  113. package/dist/web/assets/{packet-AYTQ26CC-CmxbS0qf.js → packet-AYTQ26CC-6lI3cEX_.js} +1 -1
  114. package/dist/web/assets/{pegDiagram-GJSIUBJH-_3jnD3q8.js → pegDiagram-GJSIUBJH-BPIjlSAj.js} +1 -1
  115. package/dist/web/assets/{pie-WAS4IAKB-DppCWOqP.js → pie-WAS4IAKB-D6Dzj3Ja.js} +1 -1
  116. package/dist/web/assets/{pieDiagram-5QR66LMP-DTVThTqz.js → pieDiagram-5QR66LMP-DtA-vZmv.js} +1 -1
  117. package/dist/web/assets/{quadrantDiagram-O4NWA36T-BJ_OFnht.js → quadrantDiagram-O4NWA36T-EettyqQn.js} +1 -1
  118. package/dist/web/assets/{radar-RG4KPBEZ-B6quSMjs.js → radar-RG4KPBEZ-xmRc3Dl8.js} +1 -1
  119. package/dist/web/assets/{railroad-74A4TZTK-CErU7RG3.js → railroad-74A4TZTK-BLB7dhOn.js} +1 -1
  120. package/dist/web/assets/railroad-abnf-HS5TGJTU-DBFym6g8.js +1 -0
  121. package/dist/web/assets/railroad-ebnf-LZEXJU2U-CkG28ig8.js +1 -0
  122. package/dist/web/assets/railroad-peg-WCYAUIDC-Df_Ifaad.js +1 -0
  123. package/dist/web/assets/{railroadDiagram-XR7U4H2S-Zu0N_D-D.js → railroadDiagram-XR7U4H2S-PBw4We6b.js} +1 -1
  124. package/dist/web/assets/{requirementDiagram-PLB6GJNP-Bl6CN3ul.js → requirementDiagram-PLB6GJNP-DjQpaUFA.js} +1 -1
  125. package/dist/web/assets/{sankeyDiagram-IPEJSGJF-DdpKzZV8.js → sankeyDiagram-IPEJSGJF-CBW-_AoF.js} +1 -1
  126. package/dist/web/assets/{sequenceDiagram-PO4LG4MO-BwUqrl9F.js → sequenceDiagram-PO4LG4MO-DEoPYqY9.js} +1 -1
  127. package/dist/web/assets/{stateDiagram-v2-GCMORJYK-C3vZGpAi.js → stateDiagram-v2-GCMORJYK-UZq5rPud.js} +1 -1
  128. package/dist/web/assets/{swimlanes-2SLR337P-BjoMa2HW.js → swimlanes-2SLR337P-CACfuXJR.js} +1 -1
  129. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-CDSrqdgx.js +8 -0
  130. package/dist/web/assets/{timeline-definition-EJHVYXUP-0apcf2l2.js → timeline-definition-EJHVYXUP-CYJl7mP-.js} +1 -1
  131. package/dist/web/assets/{treeView-Q6P3EWNA-BOPWzyg3.js → treeView-Q6P3EWNA-DtZ5nkPQ.js} +1 -1
  132. package/dist/web/assets/{treemap-WGGIJYW6-CC6rU6dD.js → treemap-WGGIJYW6-m49GDM-M.js} +1 -1
  133. package/dist/web/assets/{usecaseDiagram-POWQR4AR-BFclVNBG.js → usecaseDiagram-POWQR4AR-C2Vg20UK.js} +1 -1
  134. package/dist/web/assets/{vennDiagram-UO4OBE2U-D2XPv-zb.js → vennDiagram-UO4OBE2U-C07v5OGI.js} +1 -1
  135. package/dist/web/assets/{wardley-WFR3VGLG-Btz6ID8P.js → wardley-WFR3VGLG-C9Z7EZtz.js} +1 -1
  136. package/dist/web/assets/{wardleyDiagram-VNRHLVJA-DFYPvGpS.js → wardleyDiagram-VNRHLVJA-DdA8CcSC.js} +1 -1
  137. package/dist/web/assets/{xychartDiagram-PMCCYNJV-DMkQD-sV.js → xychartDiagram-PMCCYNJV-C9EZw07b.js} +1 -1
  138. package/dist/web/index.html +1 -1
  139. package/package.json +5 -2
  140. package/dist/web/assets/architecture-7GRP2DOG-CGSEE8po.js +0 -1
  141. package/dist/web/assets/channel-Dneho6u-.js +0 -1
  142. package/dist/web/assets/eventmodeling-NTZA5JFV-CA-vzJb3.js +0 -1
  143. package/dist/web/assets/flowDiagram-KWPJA3E3-Ca_1cbbS.js +0 -1
  144. package/dist/web/assets/index-s6lwoj84.js +0 -45
  145. package/dist/web/assets/railroad-abnf-HS5TGJTU-CWsEvTRR.js +0 -1
  146. package/dist/web/assets/railroad-ebnf-LZEXJU2U-Cyq9ELNn.js +0 -1
  147. package/dist/web/assets/railroad-peg-WCYAUIDC-BX1hI1hV.js +0 -1
  148. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BGNz1HGG.js +0 -8
@@ -0,0 +1,1259 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ configureAgents,
4
+ listAgents,
5
+ removeAgents
6
+ } from "./chunk-JY6BFIJJ.js";
7
+ import {
8
+ getRelevantContext,
9
+ graphSummary,
10
+ refreshGraph
11
+ } from "./chunk-CXUTYCZX.js";
12
+ import {
13
+ runSecurityScan
14
+ } from "./chunk-LMOFZNTQ.js";
15
+ import {
16
+ reviewChanges
17
+ } from "./chunk-EF6GHOIN.js";
18
+ import {
19
+ buildStatus
20
+ } from "./chunk-ZB7LC6FP.js";
21
+ import {
22
+ INSTANCE_FILE,
23
+ findRunningInstance,
24
+ runDoctor
25
+ } from "./chunk-PR53DT7I.js";
26
+ import {
27
+ aiStatus
28
+ } from "./chunk-RXNRMIZP.js";
29
+ import "./chunk-VSYBC3VY.js";
30
+ import {
31
+ contentHash,
32
+ docAtRevision,
33
+ docHistory,
34
+ getRules,
35
+ listDocs,
36
+ readDoc,
37
+ rulesAdd,
38
+ rulesRemove,
39
+ rulesUpdate,
40
+ saveDoc,
41
+ searchDocs
42
+ } from "./chunk-LEMPCNAY.js";
43
+ import "./chunk-3IS2X2Z7.js";
44
+ import {
45
+ ACTIVITY_FILE,
46
+ activityFile,
47
+ parseEventLines,
48
+ readRecentEvents,
49
+ rotatedActivityFile,
50
+ summarizeSessions
51
+ } from "./chunk-65TSRDQ3.js";
52
+ import {
53
+ printHeader
54
+ } from "./chunk-SHKXELUX.js";
55
+ import {
56
+ watchProject
57
+ } from "./chunk-32PRS2MG.js";
58
+ import {
59
+ AnalysisScheduler
60
+ } from "./chunk-XB66PZ6Z.js";
61
+ import {
62
+ c,
63
+ dim,
64
+ isJson,
65
+ json,
66
+ line,
67
+ ok,
68
+ setInterruptMessage,
69
+ warn
70
+ } from "./chunk-Z6QWMJLK.js";
71
+ import {
72
+ requireProjectRoot
73
+ } from "./chunk-XFN7UZCS.js";
74
+ import "./chunk-KBO7XRBW.js";
75
+ import {
76
+ applySync,
77
+ ignorePlan,
78
+ staleForCheck
79
+ } from "./chunk-TGJUUB44.js";
80
+ import {
81
+ projectSession,
82
+ releaseProjectSession
83
+ } from "./chunk-PDFQ5BFG.js";
84
+ import "./chunk-GR4DL2VM.js";
85
+ import {
86
+ runPipeline
87
+ } from "./chunk-SJOOXO34.js";
88
+ import {
89
+ SCAN_FILE,
90
+ saveScan
91
+ } from "./chunk-PANVPNYF.js";
92
+ import "./chunk-ELKFRTZT.js";
93
+ import {
94
+ ATHENA_VERSION
95
+ } from "./chunk-2R6PXY6Z.js";
96
+ import "./chunk-PVCIGUIT.js";
97
+ import "./chunk-OLPSEQS5.js";
98
+ import "./chunk-JCGDR4UV.js";
99
+ import "./chunk-2RS6TA3D.js";
100
+ import "./chunk-GHFRQAYT.js";
101
+ import "./chunk-673EPYYH.js";
102
+ import {
103
+ KNOWLEDGE_DOCS,
104
+ RELEVANCE_MAP
105
+ } from "./chunk-FQEGEVLY.js";
106
+ import {
107
+ athenaDir
108
+ } from "./chunk-2DY4PUR4.js";
109
+ import {
110
+ readTextIfExists
111
+ } from "./chunk-IQQBAWSM.js";
112
+ import {
113
+ AthenaError,
114
+ notFound
115
+ } from "./chunk-AD4OZCU2.js";
116
+ import "./chunk-INT3IGMQ.js";
117
+ import "./chunk-XW3MLHI2.js";
118
+ import "./chunk-EUPETUWA.js";
119
+
120
+ // src/cli/commands/open.ts
121
+ import { execFile } from "child_process";
122
+
123
+ // src/server/instance.ts
124
+ import { promises as fs6 } from "fs";
125
+ import path4 from "path";
126
+ import { fileURLToPath } from "url";
127
+
128
+ // src/server/app.ts
129
+ import crypto3 from "crypto";
130
+ import Fastify from "fastify";
131
+
132
+ // src/server/context.ts
133
+ var STATUS_TTL_MS = 3e3;
134
+ var WATCHED_STATUS_TTL_MS = 3e4;
135
+ function createServerContext(init) {
136
+ const { root } = init;
137
+ let statusCache = null;
138
+ let statusInflight = null;
139
+ let statusGen = 0;
140
+ const state = { analysisRunning: false, scanRunning: false, graphBuilding: false, syncBusy: false, currentPlan: null, lastCheckedAt: null, watcher: null };
141
+ const ctx = {
142
+ ...init,
143
+ session: projectSession(root),
144
+ recentWrites: /* @__PURE__ */ new Map(),
145
+ state,
146
+ scheduler: new AnalysisScheduler(root),
147
+ async getStatus() {
148
+ const ttl = state.watcher ? WATCHED_STATUS_TTL_MS : STATUS_TTL_MS;
149
+ if (statusCache && Date.now() - statusCache.at < ttl) return statusCache.report;
150
+ if (statusInflight && statusInflight.gen === statusGen) return statusInflight.report;
151
+ const gen = statusGen;
152
+ const started = Date.now();
153
+ const inflight = { gen, report: buildStatus(root) };
154
+ statusInflight = inflight;
155
+ const report = await inflight.report.finally(() => {
156
+ if (statusInflight === inflight) statusInflight = null;
157
+ });
158
+ if (gen === statusGen) statusCache = { at: started, report };
159
+ return report;
160
+ },
161
+ invalidateStatus() {
162
+ statusCache = null;
163
+ statusGen++;
164
+ },
165
+ setPlan(plan) {
166
+ state.lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
167
+ state.currentPlan = plan && !plan.upToDate ? plan : null;
168
+ },
169
+ async checkNow() {
170
+ if (state.watcher) return state.watcher.planNow();
171
+ const plan = await ctx.scheduler.request({ reason: "check" });
172
+ ctx.setPlan(plan);
173
+ return plan;
174
+ },
175
+ replanSoon() {
176
+ if (state.currentPlan) void ctx.checkNow().catch(() => {
177
+ });
178
+ }
179
+ };
180
+ return ctx;
181
+ }
182
+
183
+ // src/server/events.ts
184
+ import crypto from "crypto";
185
+ var EventBus = class {
186
+ constructor(capacity = 500) {
187
+ this.capacity = capacity;
188
+ }
189
+ capacity;
190
+ buffer = [];
191
+ listeners = /* @__PURE__ */ new Set();
192
+ current = { state: "IDLE", actor: "none", task: null, reading: [], since: (/* @__PURE__ */ new Date()).toISOString() };
193
+ settleTimer = null;
194
+ emit(e) {
195
+ const event = { id: e.id ?? crypto.randomUUID(), ts: e.ts ?? (/* @__PURE__ */ new Date()).toISOString(), ...e };
196
+ this.buffer.push(event);
197
+ if (this.buffer.length > this.capacity) this.buffer.splice(0, this.buffer.length - this.capacity);
198
+ for (const l of this.listeners) l(event);
199
+ return event;
200
+ }
201
+ /** Add historical events (e.g. from the agent log) without notifying listeners. */
202
+ seed(events) {
203
+ this.buffer.push(...events);
204
+ this.buffer.sort((a, b) => a.ts.localeCompare(b.ts));
205
+ if (this.buffer.length > this.capacity) this.buffer.splice(0, this.buffer.length - this.capacity);
206
+ }
207
+ has(id) {
208
+ return this.buffer.some((e) => e.id === id);
209
+ }
210
+ recent(limit = 200) {
211
+ return this.buffer.slice(-limit);
212
+ }
213
+ activity() {
214
+ return this.current;
215
+ }
216
+ setActivity(next, settleToIdleMs) {
217
+ if (this.settleTimer) clearTimeout(this.settleTimer);
218
+ this.settleTimer = null;
219
+ this.current = { ...next, since: (/* @__PURE__ */ new Date()).toISOString() };
220
+ for (const l of this.listeners) l({ type: "__activity", activity: this.current });
221
+ if (settleToIdleMs) {
222
+ this.settleTimer = setTimeout(() => this.setActivity({ state: "IDLE", actor: "none", task: null, reading: [] }), settleToIdleMs);
223
+ this.settleTimer.unref();
224
+ }
225
+ }
226
+ subscribe(l) {
227
+ this.listeners.add(l);
228
+ return () => this.listeners.delete(l);
229
+ }
230
+ close() {
231
+ if (this.settleTimer) clearTimeout(this.settleTimer);
232
+ this.listeners.clear();
233
+ }
234
+ };
235
+
236
+ // src/server/routes/activity.ts
237
+ import { promises as fs } from "fs";
238
+ var AGENT_IDLE_MS = 5 * 60 * 1e3;
239
+ function registerActivityRoutes({ app, root, events }) {
240
+ app.get("/api/activity", async () => {
241
+ const agents = await listAgents(root);
242
+ const observing = agents.filter((a) => a.activityObservation === "hooks");
243
+ return {
244
+ activity: events.activity(),
245
+ events: events.recent(),
246
+ sessions: summarizeSessions(await readRecentEvents(root, 500)),
247
+ agentObservation: {
248
+ available: observing.length > 0,
249
+ agents: observing.map((a) => a.name),
250
+ reason: observing.length ? "Events below come from hooks the agent fired. Athena sees which tool ran and on which files \u2014 never the agent's reasoning." : "No agent is reporting activity yet. Configure an agent with hooks (Claude Code or Cursor) to see its actions here."
251
+ }
252
+ };
253
+ });
254
+ }
255
+ function createAgentActivityFeed({ root, events, state }) {
256
+ let offset = 0;
257
+ let ino = null;
258
+ const toAthenaEvent = (e) => ({
259
+ id: e.id,
260
+ ts: e.ts,
261
+ source: "agent",
262
+ type: `agent.${e.kind}`,
263
+ level: e.kind === "stop" ? "success" : "info",
264
+ message: `${e.agent}: ${e.message}`,
265
+ data: { agent: e.agent, session: e.session, hook: e.hook, files: e.files, command: e.command, tool: e.tool }
266
+ });
267
+ const apply = (e, live) => {
268
+ if (events.has(e.id)) return;
269
+ if (live) events.emit(toAthenaEvent(e));
270
+ else events.seed([{ ...toAthenaEvent(e) }]);
271
+ if (!live) return;
272
+ if (e.state === "IDLE") {
273
+ if (events.activity().actor === "agent") events.setActivity({ state: "IDLE", actor: "none", task: null, reading: [] });
274
+ return;
275
+ }
276
+ if (state.analysisRunning) return;
277
+ events.setActivity({ state: e.state, actor: "agent", task: `${e.agent}: ${e.message}`, reading: e.files.slice(0, 4) }, e.state === "SUCCESS" ? 1e4 : AGENT_IDLE_MS);
278
+ };
279
+ const readLinesFrom = async (file, from, size) => {
280
+ if (size <= from) return from;
281
+ const handle = await fs.open(file, "r");
282
+ try {
283
+ const length = size - from;
284
+ const buf = Buffer.alloc(length);
285
+ await handle.read(buf, 0, length, from);
286
+ const text = buf.toString("utf8");
287
+ const lastNewline = text.lastIndexOf("\n");
288
+ if (lastNewline === -1) return from;
289
+ for (const e of parseEventLines(text.slice(0, lastNewline + 1))) apply(e, true);
290
+ return from + Buffer.byteLength(text.slice(0, lastNewline + 1));
291
+ } finally {
292
+ await handle.close();
293
+ }
294
+ };
295
+ return {
296
+ async seed() {
297
+ const history = await readRecentEvents(root, 200);
298
+ for (const e of history) apply(e, false);
299
+ const st = await fs.stat(activityFile(root)).catch(() => null);
300
+ offset = st?.size ?? 0;
301
+ ino = st?.ino ?? null;
302
+ },
303
+ async readNew() {
304
+ try {
305
+ const file = activityFile(root);
306
+ const st = await fs.stat(file).catch(() => null);
307
+ if (!st) return;
308
+ if (ino !== null && st.ino !== ino) {
309
+ const old = await fs.stat(rotatedActivityFile(root)).catch(() => null);
310
+ if (old && old.ino === ino) await readLinesFrom(rotatedActivityFile(root), offset, old.size).catch(() => {
311
+ });
312
+ offset = 0;
313
+ } else if (st.size < offset) offset = 0;
314
+ ino = st.ino;
315
+ offset = await readLinesFrom(file, offset, st.size);
316
+ } catch {
317
+ }
318
+ }
319
+ };
320
+ }
321
+
322
+ // src/server/routes/agents.ts
323
+ var agentsBody = { schema: { body: { type: "object", required: ["agents"], additionalProperties: false, properties: { agents: { type: "array", maxItems: 10, items: { type: "string", maxLength: 40 } } } } } };
324
+ function registerAgentRoutes({ app, root, events }) {
325
+ app.get("/api/agents", async () => listAgents(root));
326
+ app.post("/api/agents/configure", agentsBody, async (req) => {
327
+ const r = await configureAgents(root, req.body.agents);
328
+ events.emit({ source: "web-ui", type: "agents.configured", level: "success", message: `Configured ${r.map((x) => x.id).join(", ")}`, data: { files: r.flatMap((x) => x.files.map((f) => f.path)) } });
329
+ return listAgents(root);
330
+ });
331
+ app.post("/api/agents/remove", agentsBody, async (req) => {
332
+ const removed = await removeAgents(root, req.body.agents);
333
+ events.emit({ source: "web-ui", type: "agents.removed", level: "info", message: `Removed ${req.body.agents.join(", ")} integration${removed.length ? ` (${removed.join(", ")})` : ""}` });
334
+ return listAgents(root);
335
+ });
336
+ }
337
+
338
+ // src/server/routes/analysis.ts
339
+ var STAGE_LABELS = { scan: "Scanning files", detect: "Detecting technologies, schema and routes", git: "Reading Git metadata", write: "Writing knowledge", agents: "Updating agent integrations" };
340
+ function registerAnalysisRoutes(ctx) {
341
+ const { app, root, events, state } = ctx;
342
+ app.post(
343
+ "/api/analyze",
344
+ { schema: { body: { type: "object", additionalProperties: false, properties: { force: { type: "boolean" } } } } },
345
+ async (req, reply) => {
346
+ if (state.analysisRunning) throw new AthenaError("An analysis is already running.", void 0, 1, "busy");
347
+ state.analysisRunning = true;
348
+ const started = Date.now();
349
+ events.setActivity({ state: "ANALYZING", actor: "athena", task: "Analyzing project", reading: [] });
350
+ events.emit({ source: "athena", type: "analysis.started", level: "info", message: "Analysis started (requested from web UI)" });
351
+ void ctx.scheduler.exclusive(
352
+ () => runPipeline({
353
+ root,
354
+ mode: "analyze",
355
+ force: req.body?.force,
356
+ onStage: (stage) => {
357
+ if (STAGE_LABELS[stage]) events.setActivity({ state: "ANALYZING", actor: "athena", task: STAGE_LABELS[stage], reading: [] });
358
+ }
359
+ })
360
+ ).then((result) => {
361
+ for (const d of result.docs) ctx.recentWrites.set(d.file, d.contentHash);
362
+ const changed = result.docs.filter((d) => d.status !== "unchanged").map((d) => d.file);
363
+ const preserved = result.docs.filter((d) => d.preservedModified.length).map((d) => d.file);
364
+ ctx.invalidateStatus();
365
+ state.currentPlan = null;
366
+ events.emit({
367
+ source: "athena",
368
+ type: "analysis.completed",
369
+ level: "success",
370
+ message: `Analysis completed in ${((Date.now() - started) / 1e3).toFixed(1)}s \u2014 ${changed.length ? `updated ${changed.join(", ")}` : "knowledge already up to date"}`,
371
+ data: { changed, preserved, filesScanned: result.analysis.model.stats.filesScanned, warnings: result.analysis.model.warnings.length }
372
+ });
373
+ events.setActivity({ state: "SUCCESS", actor: "athena", task: "Analysis complete", reading: [] }, 4e3);
374
+ }).catch((err) => {
375
+ events.emit({ source: "athena", type: "analysis.failed", level: "error", message: `Analysis failed: ${err.message}` });
376
+ events.setActivity({ state: "ERROR", actor: "athena", task: "Analysis failed", reading: [] }, 8e3);
377
+ }).finally(() => {
378
+ state.analysisRunning = false;
379
+ });
380
+ return reply.code(202).send({ accepted: true });
381
+ }
382
+ );
383
+ }
384
+
385
+ // src/server/routes/context.ts
386
+ var MAX_CONTEXT_CHARS = 2e5;
387
+ function clampContextChars(raw) {
388
+ const n = raw ? Number(raw) : NaN;
389
+ return Number.isFinite(n) ? Math.min(MAX_CONTEXT_CHARS, Math.max(500, Math.floor(n))) : void 0;
390
+ }
391
+ function registerContextRoutes({ app, root }) {
392
+ app.get("/api/context", async (req) => {
393
+ const task = String(req.query.task ?? "").slice(0, 500);
394
+ return getRelevantContext(root, task, { maxChars: clampContextChars(req.query.maxChars) });
395
+ });
396
+ }
397
+
398
+ // src/server/routes/docs.ts
399
+ function registerDocsRoutes(ctx) {
400
+ const { app, root, events } = ctx;
401
+ app.get("/api/docs", async () => {
402
+ const [docs, status] = await Promise.all([listDocs(root), ctx.getStatus().catch(() => null)]);
403
+ const affected = new Map(status?.affectedDocuments.map((a) => [a.file, a.reasons]) ?? []);
404
+ return docs.map((d) => ({ ...d, sync: syncState(d.id, d.present, affected.get(d.file)), affectedBy: affected.get(d.file) ?? [] }));
405
+ });
406
+ const withSync = async (doc) => {
407
+ const status = await ctx.getStatus().catch(() => null);
408
+ const reasons = status?.affectedDocuments.find((a) => a.file === doc.file)?.reasons;
409
+ return { ...doc, sync: syncState(doc.id, true, reasons), affectedBy: reasons ?? [] };
410
+ };
411
+ app.get("/api/docs/:id", async (req) => withSync(await readDoc(root, req.params.id)));
412
+ app.put(
413
+ "/api/docs/:id",
414
+ { schema: { body: { type: "object", required: ["content", "baseHash"], additionalProperties: false, properties: { content: { type: "string" }, baseHash: { type: ["string", "null"] } } } } },
415
+ async (req) => {
416
+ const doc = await saveDoc(root, req.params.id, req.body.content, req.body.baseHash);
417
+ ctx.recentWrites.set(doc.file, doc.hash);
418
+ ctx.invalidateStatus();
419
+ events.emit({ source: "web-ui", type: "knowledge.saved", level: "success", message: `Saved ${doc.file}`, data: { file: doc.file } });
420
+ ctx.replanSoon();
421
+ return withSync(doc);
422
+ }
423
+ );
424
+ app.get("/api/docs/:id/history", async (req) => docHistory(root, req.params.id));
425
+ app.get("/api/docs/:id/history/:sha", async (req) => ({ content: await docAtRevision(root, req.params.id, req.params.sha) }));
426
+ app.get("/api/search", async (req) => searchDocs(root, String(req.query.q ?? "").slice(0, 200)));
427
+ }
428
+ function syncState(id, present, affectedReasons) {
429
+ if (!present) return "missing";
430
+ if (id === "rules") return "developer-owned";
431
+ return affectedReasons?.length ? "may-be-outdated" : "synchronized";
432
+ }
433
+
434
+ // src/server/security.ts
435
+ import crypto2 from "crypto";
436
+ import os from "os";
437
+ function generateToken() {
438
+ return crypto2.randomBytes(32).toString("base64url");
439
+ }
440
+ function tokensEqual(a, b) {
441
+ const ab = Buffer.from(a);
442
+ const bb = Buffer.from(b);
443
+ return ab.length === bb.length && crypto2.timingSafeEqual(ab, bb);
444
+ }
445
+ function isLoopbackHost(host) {
446
+ return host === "127.0.0.1" || host === "::1" || host === "localhost";
447
+ }
448
+ var SECURITY_HEADERS = {
449
+ // Scripts only from our own origin. Inline styles are required by CodeMirror and Mermaid SVG output.
450
+ "Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
451
+ "X-Content-Type-Options": "nosniff",
452
+ "X-Frame-Options": "DENY",
453
+ "Referrer-Policy": "no-referrer",
454
+ "Cross-Origin-Opener-Policy": "same-origin",
455
+ "Cross-Origin-Resource-Policy": "same-origin",
456
+ "Permissions-Policy": "camera=(), microphone=(), geolocation=()"
457
+ };
458
+ var UNSAFE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
459
+ function makeGuard(opts) {
460
+ return async function guard(req, reply) {
461
+ const host = (req.headers.host ?? "").toLowerCase();
462
+ if (opts.allowedHosts.size && !opts.allowedHosts.has(host)) {
463
+ await reply.code(421).send({ error: "Host not allowed" });
464
+ return;
465
+ }
466
+ const url = req.url.split("?")[0];
467
+ if (!url.startsWith("/api/") || url === "/api/health") return;
468
+ const auth = req.headers.authorization ?? "";
469
+ const token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
470
+ if (!token || !tokensEqual(token, opts.token)) {
471
+ await reply.code(401).send({ error: "Unauthorized", hint: "Open the URL printed by `athena open`." });
472
+ return;
473
+ }
474
+ if (UNSAFE_METHODS.has(req.method)) {
475
+ const origin = req.headers.origin;
476
+ if (origin && opts.allowedOrigins.size && !opts.allowedOrigins.has(origin.toLowerCase())) {
477
+ await reply.code(403).send({ error: "Origin not allowed" });
478
+ return;
479
+ }
480
+ const ct = req.headers["content-type"] ?? "";
481
+ const hasBody = Number(req.headers["content-length"] ?? 0) > 0 || req.headers["transfer-encoding"];
482
+ if (hasBody && !ct.toLowerCase().startsWith("application/json")) {
483
+ await reply.code(415).send({ error: "Content-Type must be application/json" });
484
+ return;
485
+ }
486
+ }
487
+ };
488
+ }
489
+ var LOOPBACK_NAMES = ["127.0.0.1", "localhost", "[::1]"];
490
+ var bracket = (h) => h.includes(":") && !h.startsWith("[") ? `[${h}]` : h;
491
+ function allowedHostsFor(host, port, opts = {}) {
492
+ const names = new Set(LOOPBACK_NAMES);
493
+ if (!isLoopbackHost(host)) {
494
+ if (!opts.remote) names.clear();
495
+ if (host === "0.0.0.0" || host === "::") {
496
+ for (const list of Object.values(os.networkInterfaces())) for (const a of list ?? []) names.add(bracket(a.address));
497
+ names.add(os.hostname());
498
+ } else names.add(bracket(host));
499
+ }
500
+ for (const h of opts.extraHosts ?? []) names.add(bracket(h));
501
+ return new Set([...names].map((n) => `${n}:${port}`.toLowerCase()));
502
+ }
503
+ function allowedOriginsFor(host, port, opts = {}) {
504
+ return new Set([...allowedHostsFor(host, port, opts)].map((h) => `http://${h}`));
505
+ }
506
+ var DEFAULT_RATE_LIMIT = { perSecond: 30, burst: 120 };
507
+ function makeRateLimiter(opts, now = Date.now) {
508
+ const buckets = /* @__PURE__ */ new Map();
509
+ const MAX_CLIENTS = 1e4;
510
+ return function take(key) {
511
+ const t = now();
512
+ let b = buckets.get(key);
513
+ if (!b) {
514
+ if (buckets.size >= MAX_CLIENTS) {
515
+ for (const [k, v] of buckets) if (v.tokens + (t - v.at) / 1e3 * opts.perSecond >= opts.burst) buckets.delete(k);
516
+ if (buckets.size >= MAX_CLIENTS) buckets.delete(buckets.keys().next().value);
517
+ }
518
+ b = { tokens: opts.burst, at: t };
519
+ buckets.set(key, b);
520
+ }
521
+ b.tokens = Math.min(opts.burst, b.tokens + (t - b.at) / 1e3 * opts.perSecond);
522
+ b.at = t;
523
+ if (b.tokens < 1) return false;
524
+ b.tokens -= 1;
525
+ return true;
526
+ };
527
+ }
528
+
529
+ // src/server/routes/events.ts
530
+ function registerEventRoutes(app, hub) {
531
+ app.addHook("preClose", async () => {
532
+ hub.close();
533
+ });
534
+ app.get("/api/events", (req, reply) => {
535
+ if (hub.full) {
536
+ reply.header("Retry-After", "5");
537
+ reply.code(503).send({ error: "Too many open event streams", hint: "Close other Athena tabs and reload." });
538
+ return;
539
+ }
540
+ reply.hijack();
541
+ const res = reply.raw;
542
+ res.writeHead(200, { ...SECURITY_HEADERS, "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-store", Connection: "keep-alive", "X-Accel-Buffering": "no" });
543
+ const detach = hub.add({
544
+ write: (chunk) => res.write(chunk),
545
+ once: (event, listener) => res.once(event, listener),
546
+ end: () => {
547
+ res.end();
548
+ res.socket?.destroy();
549
+ }
550
+ });
551
+ req.raw.on("close", detach);
552
+ });
553
+ }
554
+
555
+ // src/server/routes/graph.ts
556
+ function registerGraphRoutes({ app, root, events, state }) {
557
+ app.get("/api/graph", async () => ({ ...await graphSummary(root), building: state.graphBuilding }));
558
+ app.post("/api/graph/build", async () => {
559
+ if (state.graphBuilding) throw new AthenaError("The graph is already being built.", void 0, 1, "busy");
560
+ state.graphBuilding = true;
561
+ try {
562
+ const graph = await refreshGraph(root);
563
+ events.emit({ source: "web-ui", type: "graph.built", level: "success", message: `Project graph built: ${graph.stats.nodes} nodes, ${graph.stats.edges} relationships` });
564
+ return { built: true, builtAt: graph.builtAt, stats: graph.stats };
565
+ } finally {
566
+ state.graphBuilding = false;
567
+ }
568
+ });
569
+ }
570
+
571
+ // src/server/routes/review.ts
572
+ function registerReviewRoutes({ app, root, scheduler }) {
573
+ app.get("/api/review", async (req) => {
574
+ const base = typeof req.query.base === "string" && req.query.base ? req.query.base : void 0;
575
+ return reviewChanges(root, { base, checkSync: async () => staleForCheck(await scheduler.request({ reason: "review" })).length === 0 });
576
+ });
577
+ }
578
+
579
+ // src/server/routes/rules.ts
580
+ function registerRulesRoutes({ app, root, events, recentWrites }) {
581
+ const ruleAfter = (message) => {
582
+ events.emit({ source: "web-ui", type: "rules.changed", level: "success", message });
583
+ };
584
+ app.get("/api/rules", async () => getRules(root));
585
+ app.post(
586
+ "/api/rules",
587
+ { schema: { body: { type: "object", required: ["section", "text"], additionalProperties: false, properties: { section: { type: "string", maxLength: 200 }, text: { type: "string", maxLength: 1e3 }, baseHash: { type: "string" } } } } },
588
+ async (req) => {
589
+ const v = await rulesAdd(root, req.body.baseHash, req.body.section, req.body.text);
590
+ recentWrites.set("rules.md", v.hash);
591
+ ruleAfter(`Added rule to "${req.body.section}"`);
592
+ return v;
593
+ }
594
+ );
595
+ app.patch(
596
+ "/api/rules/:index",
597
+ { schema: { body: { type: "object", required: ["baseHash"], additionalProperties: false, properties: { text: { type: "string", maxLength: 1e3 }, enabled: { type: "boolean" }, baseHash: { type: "string" } } } } },
598
+ async (req) => {
599
+ const index = parseIndex(req.params.index);
600
+ const v = await rulesUpdate(root, req.body.baseHash, index, { text: req.body.text, enabled: req.body.enabled });
601
+ recentWrites.set("rules.md", v.hash);
602
+ ruleAfter(req.body.enabled === void 0 ? `Edited rule #${index}` : `${req.body.enabled ? "Enabled" : "Disabled"} rule #${index}`);
603
+ return v;
604
+ }
605
+ );
606
+ app.delete("/api/rules/:index", async (req) => {
607
+ const index = parseIndex(req.params.index);
608
+ if (!req.query.baseHash) throw new AthenaError("baseHash is required");
609
+ const v = await rulesRemove(root, req.query.baseHash, index);
610
+ recentWrites.set("rules.md", v.hash);
611
+ ruleAfter(`Deleted rule #${index}`);
612
+ return v;
613
+ });
614
+ }
615
+ function parseIndex(raw) {
616
+ const n = Number(raw);
617
+ if (!Number.isInteger(n) || n < 1) throw new AthenaError(`Invalid rule index: ${raw.slice(0, 20)}`);
618
+ return n;
619
+ }
620
+
621
+ // src/server/routes/security.ts
622
+ function registerSecurityRoutes(ctx) {
623
+ const { app, root, events, state, session } = ctx;
624
+ app.get("/api/security", async () => ({ scan: await session.scan(), running: state.scanRunning }));
625
+ app.post(
626
+ "/api/security/scan",
627
+ { schema: { body: { type: "object", additionalProperties: false, properties: { skipAudit: { type: "boolean" } } } } },
628
+ async (req, reply) => {
629
+ if (state.scanRunning) throw new AthenaError("A security scan is already running.", void 0, 1, "busy");
630
+ state.scanRunning = true;
631
+ let handedOff = false;
632
+ try {
633
+ const model = await session.requireModel();
634
+ events.emit({ source: "web-ui", type: "security.started", level: "info", message: "Security scan started" });
635
+ events.setActivity({ state: "REVIEWING", actor: "athena", task: "Scanning dependencies", reading: [] });
636
+ handedOff = true;
637
+ void runSecurityScan(root, model, { skipAudit: req.body?.skipAudit, onTool: (tool) => events.setActivity({ state: "REVIEWING", actor: "athena", task: `Running ${tool}`, reading: [] }) }).then(async (scan) => {
638
+ await saveScan(root, scan);
639
+ await session.remember(SCAN_FILE, scan);
640
+ const findings = scan.tools.reduce((n, t) => n + t.findings.length, 0);
641
+ events.emit({
642
+ source: "athena",
643
+ type: "security.completed",
644
+ level: findings || scan.secrets.count ? "warn" : "success",
645
+ message: `Security scan finished \u2014 ${findings} dependency finding(s), ${scan.secrets.count} potential secret(s)`,
646
+ data: { findings, secrets: scan.secrets.count }
647
+ });
648
+ events.setActivity({ state: "SUCCESS", actor: "athena", task: "Security scan complete", reading: [] }, 4e3);
649
+ ctx.replanSoon();
650
+ }).catch((err) => {
651
+ events.emit({ source: "athena", type: "security.failed", level: "error", message: `Security scan failed: ${err.message}` });
652
+ events.setActivity({ state: "ERROR", actor: "athena", task: "Security scan failed", reading: [] }, 8e3);
653
+ }).finally(() => {
654
+ state.scanRunning = false;
655
+ });
656
+ } finally {
657
+ if (!handedOff) state.scanRunning = false;
658
+ }
659
+ return reply.code(202).send({ accepted: true });
660
+ }
661
+ );
662
+ }
663
+
664
+ // src/services/overview.ts
665
+ async function getOverview(root) {
666
+ const session = projectSession(root);
667
+ const st = await session.state();
668
+ if (st.kind === "missing") throw new AthenaError("Athena is not initialized in this project.", "Run `athena init`.", 3);
669
+ if (st.kind === "corrupted") throw new AthenaError(`.athena/state.json is corrupted (${st.reason}).`, "Run `athena analyze` to rebuild it.");
670
+ const state = st.state;
671
+ const model = await session.model();
672
+ const uniq = (xs) => [...new Set(xs)];
673
+ return {
674
+ athenaVersion: ATHENA_VERSION,
675
+ project: { name: state.projectName, description: model?.description ?? null, root },
676
+ analyzedAt: state.analyzedAt,
677
+ analysisDurationMs: state.analysisDurationMs,
678
+ git: { isRepo: state.git.isRepo, branch: state.git.branch ?? null, head: state.git.head ?? null },
679
+ summary: model && {
680
+ filesAnalyzed: model.stats.filesScanned,
681
+ languages: model.languages.slice(0, 6).map((l) => ({ name: l.name, files: l.files })),
682
+ frameworks: model.frameworks.map((f) => ({ name: f.name, category: f.category, root: f.root, status: f.provenance.status, confidence: f.provenance.confidence })),
683
+ monorepo: { isMonorepo: model.workspace.isMonorepo, tool: model.workspace.tool ?? null, packages: model.workspace.packages.length },
684
+ databases: uniq(model.databases.map((d) => d.name)),
685
+ entities: model.dbEntities.length,
686
+ migrations: model.migrations.reduce((n, m) => n + m.count, 0),
687
+ routes: model.routes.length,
688
+ apiSpecs: model.apiSpecs.length,
689
+ auth: uniq(model.auth.map((a) => a.name)),
690
+ testFrameworks: uniq(model.tests.frameworks.map((t) => t.name)),
691
+ testFiles: model.tests.testFileCount,
692
+ ci: uniq(model.ci.map((c2) => c2.system)),
693
+ containers: model.containers.dockerfiles.length + model.containers.services.length,
694
+ hosting: uniq(model.infrastructure.filter((i) => i.kind === "hosting").map((i) => i.name)),
695
+ envVars: model.env.vars.length,
696
+ potentialSecrets: model.security.secrets.length,
697
+ securityControls: model.security.controls.length,
698
+ warnings: model.warnings
699
+ }
700
+ };
701
+ }
702
+
703
+ // src/server/routes/session.ts
704
+ function registerSessionRoutes({ app, root, instanceId, getStatus }) {
705
+ app.get("/api/health", async () => ({ ok: true, instanceId, version: ATHENA_VERSION }));
706
+ app.get("/api/session", async () => ({ ok: true, instanceId, version: ATHENA_VERSION, root, docs: KNOWLEDGE_DOCS, relevanceMap: RELEVANCE_MAP }));
707
+ app.get("/api/overview", async () => getOverview(root));
708
+ app.get("/api/status", async () => getStatus());
709
+ app.get("/api/doctor", async () => runDoctor(root));
710
+ app.get("/api/ai", async () => aiStatus(root));
711
+ }
712
+
713
+ // src/server/routes/static.ts
714
+ import { promises as fs3 } from "fs";
715
+
716
+ // src/server/static.ts
717
+ import { promises as fs2 } from "fs";
718
+ import path from "path";
719
+ var TYPES = {
720
+ ".html": "text/html; charset=utf-8",
721
+ ".js": "text/javascript; charset=utf-8",
722
+ ".mjs": "text/javascript; charset=utf-8",
723
+ ".css": "text/css; charset=utf-8",
724
+ ".svg": "image/svg+xml",
725
+ ".png": "image/png",
726
+ ".ico": "image/x-icon",
727
+ ".json": "application/json; charset=utf-8",
728
+ ".woff2": "font/woff2",
729
+ ".txt": "text/plain; charset=utf-8",
730
+ ".map": "application/json; charset=utf-8"
731
+ };
732
+ async function buildAssetMap(webDir) {
733
+ try {
734
+ await fs2.access(path.join(webDir, "index.html"));
735
+ } catch {
736
+ return null;
737
+ }
738
+ const map = /* @__PURE__ */ new Map();
739
+ async function walk(dir) {
740
+ for (const ent of await fs2.readdir(dir, { withFileTypes: true })) {
741
+ const abs = path.join(dir, ent.name);
742
+ if (ent.isDirectory()) await walk(abs);
743
+ else if (ent.isFile()) {
744
+ const ext = path.extname(ent.name).toLowerCase();
745
+ const type = TYPES[ext];
746
+ if (!type) continue;
747
+ const urlPath = `/${path.relative(webDir, abs).split(path.sep).join("/")}`;
748
+ map.set(urlPath, { abs, type, immutable: urlPath.startsWith("/assets/") });
749
+ }
750
+ }
751
+ }
752
+ await walk(webDir);
753
+ return map;
754
+ }
755
+ var MISSING_UI_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Athena</title></head>
756
+ <body style="font-family:system-ui;background:#0b0d10;color:#e6e8eb;padding:48px">
757
+ <h1>Athena web UI assets not found</h1>
758
+ <p>The API is running, but the UI has not been built. From the Athena repository run <code>npm run build</code>.</p>
759
+ </body></html>`;
760
+
761
+ // src/server/routes/static.ts
762
+ function registerStaticRoutes(app, assets) {
763
+ const sendAsset = async (reply, urlPath) => {
764
+ if (!assets) return reply.code(503).type("text/html; charset=utf-8").send(MISSING_UI_HTML);
765
+ const asset = assets.get(urlPath) ?? assets.get("/index.html");
766
+ const body = await fs3.readFile(asset.abs);
767
+ reply.header("Cache-Control", asset.immutable ? "public, max-age=31536000, immutable" : "no-cache");
768
+ return reply.type(asset.type).send(body);
769
+ };
770
+ app.route({
771
+ method: "GET",
772
+ url: "/*",
773
+ handler: async (req, reply) => {
774
+ let urlPath;
775
+ try {
776
+ urlPath = decodeURIComponent(req.url.split("?")[0]);
777
+ } catch {
778
+ return reply.code(400).send("Bad request");
779
+ }
780
+ if (urlPath.startsWith("/api/")) return reply.code(404).send({ error: "Not found" });
781
+ if (assets && !assets.has(urlPath) && /\.[a-z0-9]+$/i.test(urlPath)) return reply.code(404).send("Not found");
782
+ return sendAsset(reply, urlPath === "/" ? "/index.html" : urlPath);
783
+ }
784
+ });
785
+ }
786
+
787
+ // src/server/routes/sync.ts
788
+ import { promises as fs4 } from "fs";
789
+ import path2 from "path";
790
+ function summarizePlan(plan) {
791
+ if (!plan) return null;
792
+ return { ...plan, documents: plan.documents.map(({ diff: _diff, ...rest }) => rest) };
793
+ }
794
+ var DOC_IDS = new Set(KNOWLEDGE_DOCS.map((d) => d.id));
795
+ var planIdSchema = { schema: { body: { type: "object", required: ["planId"], additionalProperties: false, properties: { planId: { type: "string", pattern: "^[a-f0-9]{16}$" } } } } };
796
+ var docParamsSchema = {
797
+ schema: {
798
+ params: { type: "object", required: ["doc"], additionalProperties: false, properties: { doc: { type: "string", pattern: "^[a-z][a-z-]{0,39}$" } } },
799
+ querystring: { type: "object", additionalProperties: false, properties: { planId: { type: "string", pattern: "^[a-f0-9]{16}$" } } }
800
+ }
801
+ };
802
+ function registerSyncRoutes(ctx) {
803
+ const { app, root, events, state } = ctx;
804
+ app.get("/api/sync", async () => ({ watching: Boolean(state.watcher), lastCheckedAt: state.lastCheckedAt, plan: summarizePlan(state.currentPlan) }));
805
+ app.get("/api/sync/:doc", docParamsSchema, async (req) => {
806
+ const id = req.params.doc;
807
+ if (!DOC_IDS.has(id)) throw notFound(`Unknown document: ${id}`);
808
+ const plan = state.currentPlan;
809
+ if (req.query.planId && plan?.id !== req.query.planId) throw new AthenaError("This proposal is out of date.", "Check for changes again and review the new proposal.", 1, "conflict");
810
+ const doc = plan?.documents.find((d) => d.id === id);
811
+ if (!plan || !doc) throw notFound(`No proposed update for ${id}.`);
812
+ return { planId: plan.id, id: doc.id, file: doc.file, diff: doc.diff, diffTruncated: doc.diffTruncated, additions: doc.additions, deletions: doc.deletions };
813
+ });
814
+ app.post("/api/sync/check", async () => {
815
+ const plan = await ctx.checkNow();
816
+ if (!state.watcher) ctx.setPlan(plan);
817
+ return { watching: Boolean(state.watcher), lastCheckedAt: state.lastCheckedAt, plan: state.currentPlan, upToDate: plan?.upToDate ?? true };
818
+ });
819
+ app.post("/api/sync/apply", planIdSchema, async (req) => {
820
+ if (!state.currentPlan || state.currentPlan.id !== req.body.planId) throw new AthenaError("This proposal is out of date.", "Check for changes again and review the new proposal.", 1, "conflict");
821
+ if (state.syncBusy || state.analysisRunning) throw new AthenaError("Another update is in progress.", void 0, 1, "busy");
822
+ state.syncBusy = true;
823
+ const plan = state.currentPlan;
824
+ try {
825
+ const result = await ctx.scheduler.exclusive(() => applySync(root, plan));
826
+ for (const d of plan.documents) {
827
+ const text = await fs4.readFile(path2.join(athenaDir(root), d.file), "utf8").catch(() => null);
828
+ if (text !== null) ctx.recentWrites.set(d.file, contentHash(text));
829
+ }
830
+ state.currentPlan = null;
831
+ state.lastCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
832
+ ctx.invalidateStatus();
833
+ events.emit({ source: "web-ui", type: "sync.applied", level: "success", message: `Knowledge updated: ${result.applied.join(", ")}`, data: { files: result.applied, preserved: result.preserved } });
834
+ events.setActivity({ state: "SUCCESS", actor: "athena", task: "Knowledge synchronized", reading: [] }, 4e3);
835
+ return result;
836
+ } catch (err) {
837
+ if (err instanceof AthenaError && err.kind === "conflict") ctx.replanSoon();
838
+ throw err;
839
+ } finally {
840
+ state.syncBusy = false;
841
+ }
842
+ });
843
+ app.post("/api/sync/ignore", planIdSchema, async (req) => {
844
+ if (!state.currentPlan || state.currentPlan.id !== req.body.planId) throw new AthenaError("This proposal is out of date.", void 0, 1, "conflict");
845
+ await ignorePlan(root, req.body.planId);
846
+ state.currentPlan = { ...state.currentPlan, ignored: true };
847
+ events.emit({ source: "web-ui", type: "sync.ignored", level: "info", message: `Ignored proposed update to ${state.currentPlan.documents.map((d) => d.file).join(", ")}` });
848
+ return { ok: true };
849
+ });
850
+ }
851
+
852
+ // src/server/sse.ts
853
+ var SSE_DEFAULTS = {
854
+ coalesceMs: 250,
855
+ maxClients: 16,
856
+ maxPending: 100,
857
+ maxPendingBytes: 256 * 1024,
858
+ heartbeatMs: 2e4
859
+ };
860
+ var sseFrame = (event, data) => `event: ${event}
861
+ data: ${JSON.stringify(data)}
862
+
863
+ `;
864
+ var SseHub = class {
865
+ constructor(bus, opts = {}) {
866
+ this.bus = bus;
867
+ this.opts = { ...SSE_DEFAULTS, ...opts };
868
+ this.unsubscribe = bus.subscribe((e) => {
869
+ if (e.type === "__activity") this.enqueue(sseFrame("activity", e.activity), true);
870
+ else this.enqueue(sseFrame("event", e), false);
871
+ });
872
+ this.heartbeat = setInterval(() => {
873
+ for (const c2 of this.clients.values()) if (!c2.congested) this.write(c2, ": ping\n\n");
874
+ }, this.opts.heartbeatMs);
875
+ this.heartbeat.unref();
876
+ }
877
+ bus;
878
+ clients = /* @__PURE__ */ new Map();
879
+ opts;
880
+ flushTimer = null;
881
+ heartbeat;
882
+ unsubscribe;
883
+ get size() {
884
+ return this.clients.size;
885
+ }
886
+ get full() {
887
+ return this.clients.size >= this.opts.maxClients;
888
+ }
889
+ /** Start streaming to `sink` (the current activity is sent immediately). Returns a function that detaches it. */
890
+ add(sink) {
891
+ const client = { sink, pending: [], pendingBytes: 0, activity: null, dropped: 0, congested: false };
892
+ this.clients.set(sink, client);
893
+ this.write(client, sseFrame("activity", this.bus.activity()));
894
+ return () => {
895
+ this.clients.delete(sink);
896
+ };
897
+ }
898
+ /** Write everything queued now (normally done by the coalescing timer). */
899
+ flush() {
900
+ if (this.flushTimer) clearTimeout(this.flushTimer);
901
+ this.flushTimer = null;
902
+ for (const c2 of this.clients.values()) if (!c2.congested) this.send(c2);
903
+ }
904
+ /** End every stream and stop listening to the bus. */
905
+ close() {
906
+ if (this.flushTimer) clearTimeout(this.flushTimer);
907
+ this.flushTimer = null;
908
+ clearInterval(this.heartbeat);
909
+ this.unsubscribe();
910
+ for (const c2 of this.clients.values()) c2.sink.end();
911
+ this.clients.clear();
912
+ }
913
+ enqueue(frame, isActivity) {
914
+ if (!this.clients.size) return;
915
+ for (const c2 of this.clients.values()) {
916
+ if (isActivity) {
917
+ c2.activity = frame;
918
+ continue;
919
+ }
920
+ c2.pending.push(frame);
921
+ c2.pendingBytes += frame.length;
922
+ while (c2.pending.length > this.opts.maxPending || c2.pendingBytes > this.opts.maxPendingBytes && c2.pending.length > 1) {
923
+ c2.pendingBytes -= c2.pending.shift().length;
924
+ c2.dropped++;
925
+ }
926
+ }
927
+ this.flushTimer ??= setTimeout(() => this.flush(), this.opts.coalesceMs);
928
+ }
929
+ send(c2) {
930
+ let out = "";
931
+ if (c2.dropped) {
932
+ out += sseFrame("resync", { dropped: c2.dropped });
933
+ c2.dropped = 0;
934
+ }
935
+ if (c2.pending.length) out += c2.pending.join("");
936
+ if (c2.activity) out += c2.activity;
937
+ c2.pending = [];
938
+ c2.pendingBytes = 0;
939
+ c2.activity = null;
940
+ if (out) this.write(c2, out);
941
+ }
942
+ write(c2, chunk) {
943
+ if (c2.sink.write(chunk)) return;
944
+ c2.congested = true;
945
+ c2.sink.once("drain", () => {
946
+ c2.congested = false;
947
+ if (this.clients.get(c2.sink) === c2) this.send(c2);
948
+ });
949
+ }
950
+ };
951
+
952
+ // src/server/watchers.ts
953
+ import { promises as fs5, watch } from "fs";
954
+ import path3 from "path";
955
+ function watchKnowledgeDir(ctx, feed) {
956
+ const { events, recentWrites } = ctx;
957
+ let docsWatcher = null;
958
+ const pending = /* @__PURE__ */ new Map();
959
+ try {
960
+ const dir = athenaDir(ctx.root);
961
+ docsWatcher = watch(dir, { persistent: false }, (_type, filename) => {
962
+ const name = filename?.toString();
963
+ if (name === ACTIVITY_FILE) {
964
+ void feed.readNew();
965
+ return;
966
+ }
967
+ if (name === "state.json") {
968
+ ctx.invalidateStatus();
969
+ return;
970
+ }
971
+ if (!name || !KNOWLEDGE_DOCS.some((d) => d.file === name)) return;
972
+ clearTimeout(pending.get(name));
973
+ pending.set(
974
+ name,
975
+ setTimeout(async () => {
976
+ pending.delete(name);
977
+ const text = await fs5.readFile(path3.join(dir, name), "utf8").catch(() => null);
978
+ const hash = text === null ? null : contentHash(text);
979
+ if (hash && recentWrites.get(name) === hash) return;
980
+ ctx.invalidateStatus();
981
+ events.emit({ source: "filesystem", type: "knowledge.external-change", level: "info", message: text === null ? `${name} was deleted on disk` : `${name} changed on disk`, data: { file: name, hash } });
982
+ }, 150)
983
+ );
984
+ });
985
+ docsWatcher.on("error", () => {
986
+ });
987
+ } catch {
988
+ docsWatcher = null;
989
+ }
990
+ return () => {
991
+ docsWatcher?.close();
992
+ for (const t of pending.values()) clearTimeout(t);
993
+ };
994
+ }
995
+ async function watchProjectFiles(ctx, debounceMs) {
996
+ const { events, state } = ctx;
997
+ try {
998
+ state.watcher = await watchProject(ctx.root, {
999
+ debounceMs,
1000
+ scheduler: ctx.scheduler,
1001
+ onEvent: (e) => {
1002
+ switch (e.type) {
1003
+ case "changes":
1004
+ ctx.invalidateStatus();
1005
+ break;
1006
+ case "planning":
1007
+ if (!state.analysisRunning) events.setActivity({ state: "ANALYZING", actor: "athena", task: e.paths?.length ? `Checking ${e.paths.length} changed file${e.paths.length === 1 ? "" : "s"}` : "Checking for changes", reading: [] });
1008
+ break;
1009
+ case "plan": {
1010
+ const plan = e.plan;
1011
+ const prevId = state.currentPlan?.id;
1012
+ ctx.setPlan(plan);
1013
+ if (!state.analysisRunning) events.setActivity({ state: "IDLE", actor: "none", task: null, reading: [] });
1014
+ if (!plan.upToDate && !plan.ignored && plan.id !== prevId) {
1015
+ events.emit({ source: "athena", type: "sync.proposed", level: "warn", message: `Knowledge update proposed for ${plan.documents.map((d) => d.file).join(", ")}`, data: { planId: plan.id, files: plan.documents.map((d) => d.file) } });
1016
+ }
1017
+ if (plan.upToDate && prevId) events.emit({ source: "athena", type: "sync.up-to-date", level: "info", message: "Proposal no longer needed \u2014 knowledge is up to date" });
1018
+ break;
1019
+ }
1020
+ case "refreshed":
1021
+ ctx.setPlan(null);
1022
+ ctx.invalidateStatus();
1023
+ if (!state.analysisRunning) events.setActivity({ state: "IDLE", actor: "none", task: null, reading: [] });
1024
+ events.emit({ source: "athena", type: "sync.up-to-date", level: "info", message: "Files changed \u2014 knowledge already up to date (index refreshed)" });
1025
+ break;
1026
+ case "git-head":
1027
+ ctx.invalidateStatus();
1028
+ events.emit({ source: "filesystem", type: "git.head", level: "info", message: `Git HEAD moved ${e.head?.from?.slice(0, 8) ?? "?"} \u2192 ${e.head?.to?.slice(0, 8) ?? "?"}` });
1029
+ break;
1030
+ case "error":
1031
+ if (!state.analysisRunning) events.setActivity({ state: "ERROR", actor: "athena", task: "Change check failed", reading: [] }, 6e3);
1032
+ events.emit({ source: "athena", type: "sync.error", level: "error", message: `Change check failed: ${e.error?.message ?? "unknown error"}` });
1033
+ break;
1034
+ default:
1035
+ break;
1036
+ }
1037
+ }
1038
+ });
1039
+ void state.watcher.planNow();
1040
+ } catch (err) {
1041
+ events.emit({ source: "athena", type: "sync.error", level: "error", message: `File watching unavailable: ${err.message}` });
1042
+ }
1043
+ }
1044
+
1045
+ // src/server/app.ts
1046
+ var STATUS = {
1047
+ invalid: 400,
1048
+ "not-found": 404,
1049
+ conflict: 409,
1050
+ "not-initialized": 409,
1051
+ busy: 423,
1052
+ unprocessable: 422,
1053
+ forbidden: 403,
1054
+ internal: 500
1055
+ };
1056
+ async function createServer(opts) {
1057
+ const root = opts.root;
1058
+ const events = new EventBus();
1059
+ const instanceId = crypto3.randomUUID();
1060
+ const app = Fastify({
1061
+ logger: opts.logger ?? false,
1062
+ bodyLimit: 2 * 1024 * 1024,
1063
+ trustProxy: false,
1064
+ // Hijacked SSE responses would otherwise keep close() waiting forever.
1065
+ forceCloseConnections: true,
1066
+ // Strict validation: no type coercion, reject (don't silently strip) unknown fields.
1067
+ ajv: { customOptions: { coerceTypes: false, removeAdditional: false, allErrors: false } }
1068
+ });
1069
+ const assets = await buildAssetMap(opts.webDir);
1070
+ const takeToken = makeRateLimiter(opts.rateLimit ?? DEFAULT_RATE_LIMIT);
1071
+ app.addHook("onRequest", async (req, reply) => {
1072
+ if (!req.url.startsWith("/api/")) return;
1073
+ if (!takeToken(req.ip || "unknown")) {
1074
+ reply.header("Retry-After", "1");
1075
+ await reply.code(429).send({ error: "Too many requests" });
1076
+ }
1077
+ });
1078
+ app.addHook("onRequest", makeGuard({
1079
+ token: opts.token,
1080
+ allowedHosts: allowedHostsFor(opts.host, opts.port, { remote: opts.allowRemote }),
1081
+ allowedOrigins: allowedOriginsFor(opts.host, opts.port, { remote: opts.allowRemote })
1082
+ }));
1083
+ app.addHook("onSend", async (req, reply, payload) => {
1084
+ for (const [k, v] of Object.entries(SECURITY_HEADERS)) reply.header(k, v);
1085
+ if (req.url.startsWith("/api/")) reply.header("Cache-Control", "no-store");
1086
+ return payload;
1087
+ });
1088
+ app.setErrorHandler((err, _req, reply) => {
1089
+ if (err instanceof AthenaError) {
1090
+ return reply.code(STATUS[err.kind]).send({ error: err.message, hint: err.hint, kind: err.kind, details: err.details });
1091
+ }
1092
+ const e = err;
1093
+ if (e.statusCode && e.statusCode < 500) return reply.code(e.statusCode).send({ error: e.message });
1094
+ app.log.error(err);
1095
+ return reply.code(500).send({ error: "Internal error" });
1096
+ });
1097
+ const ctx = createServerContext({ app, root, events, instanceId });
1098
+ const feed = createAgentActivityFeed(ctx);
1099
+ const hub = new SseHub(events, opts.sse);
1100
+ registerSessionRoutes(ctx);
1101
+ registerDocsRoutes(ctx);
1102
+ registerRulesRoutes(ctx);
1103
+ registerAgentRoutes(ctx);
1104
+ registerAnalysisRoutes(ctx);
1105
+ registerSyncRoutes(ctx);
1106
+ registerSecurityRoutes(ctx);
1107
+ registerReviewRoutes(ctx);
1108
+ registerContextRoutes(ctx);
1109
+ registerGraphRoutes(ctx);
1110
+ registerActivityRoutes(ctx);
1111
+ registerEventRoutes(app, hub);
1112
+ registerStaticRoutes(app, assets);
1113
+ const stopDocsWatcher = watchKnowledgeDir(ctx, feed);
1114
+ await feed.seed();
1115
+ if (opts.watch) await watchProjectFiles(ctx, opts.watchDebounceMs);
1116
+ return {
1117
+ app,
1118
+ events,
1119
+ instanceId,
1120
+ scheduler: ctx.scheduler,
1121
+ async close() {
1122
+ await ctx.state.watcher?.close();
1123
+ ctx.state.watcher = null;
1124
+ ctx.scheduler.close();
1125
+ stopDocsWatcher();
1126
+ events.close();
1127
+ await app.close();
1128
+ hub.close();
1129
+ releaseProjectSession(root);
1130
+ }
1131
+ };
1132
+ }
1133
+
1134
+ // src/server/port.ts
1135
+ import net from "net";
1136
+ var DEFAULT_PORT = 7432;
1137
+ function isPortFree(port, host) {
1138
+ return new Promise((resolve) => {
1139
+ const srv = net.createServer();
1140
+ srv.unref();
1141
+ srv.once("error", () => resolve(false));
1142
+ srv.listen({ port, host, exclusive: true }, () => srv.close(() => resolve(true)));
1143
+ });
1144
+ }
1145
+ async function findAvailablePort(host, start = DEFAULT_PORT, attempts = 100) {
1146
+ for (let p = start; p < start + attempts && p <= 65535; p++) {
1147
+ if (await isPortFree(p, host)) return p;
1148
+ }
1149
+ throw new Error(`No free port found in ${start}-${start + attempts - 1} on ${host}`);
1150
+ }
1151
+
1152
+ // src/server/instance.ts
1153
+ function defaultWebDir() {
1154
+ const here = path4.dirname(fileURLToPath(import.meta.url));
1155
+ return path4.basename(here) === "dist" ? path4.join(here, "web") : path4.resolve(here, "../../dist/web");
1156
+ }
1157
+ function browserUrl(info) {
1158
+ const host = info.host.includes(":") ? `[${info.host}]` : info.host;
1159
+ return `http://${host}:${info.port}/#token=${info.token}`;
1160
+ }
1161
+ async function startServer(opts) {
1162
+ const host = opts.host ?? "127.0.0.1";
1163
+ if (!isLoopbackHost(host) && !opts.allowRemote) {
1164
+ throw new Error(`Refusing to bind to non-loopback address ${host} without --allow-remote.`);
1165
+ }
1166
+ const port = opts.port ?? await findAvailablePort(host, DEFAULT_PORT);
1167
+ const token = generateToken();
1168
+ const server = await createServer({ root: opts.root, token, host, port, webDir: opts.webDir ?? defaultWebDir(), allowRemote: opts.allowRemote, watch: opts.watch ?? true });
1169
+ await server.app.listen({ host, port });
1170
+ const info = { pid: process.pid, host, port, instanceId: server.instanceId, token, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
1171
+ const file = path4.join(athenaDir(opts.root), INSTANCE_FILE);
1172
+ await fs6.writeFile(file, `${JSON.stringify(info, null, 2)}
1173
+ `, { mode: 384 });
1174
+ await ensureGitignored(opts.root, INSTANCE_FILE);
1175
+ return {
1176
+ server,
1177
+ info,
1178
+ url: browserUrl(info),
1179
+ async close() {
1180
+ const current = await readTextIfExists(file);
1181
+ if (current && current.includes(server.instanceId)) await fs6.rm(file, { force: true });
1182
+ await server.close();
1183
+ }
1184
+ };
1185
+ }
1186
+ async function ensureGitignored(root, entry) {
1187
+ const file = path4.join(athenaDir(root), ".gitignore");
1188
+ const text = await readTextIfExists(file) ?? "";
1189
+ if (text.split(/\r?\n/).includes(entry)) return;
1190
+ await fs6.writeFile(file, `${text}${text.endsWith("\n") || !text ? "" : "\n"}${entry}
1191
+ `);
1192
+ }
1193
+
1194
+ // src/cli/commands/open.ts
1195
+ function openBrowser(url) {
1196
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
1197
+ const child = execFile(cmd, args, { windowsHide: true }, () => {
1198
+ });
1199
+ child.on("error", () => {
1200
+ });
1201
+ child.unref();
1202
+ }
1203
+ async function openCommand(opts) {
1204
+ const root = await requireProjectRoot(opts);
1205
+ const shouldOpen = opts.open !== false;
1206
+ const existing = await findRunningInstance(root);
1207
+ if (existing) {
1208
+ const url = browserUrl(existing);
1209
+ if (isJson()) json({ running: true, reused: true, url, port: existing.port, pid: existing.pid });
1210
+ else {
1211
+ ok(`Athena is already running for this project ${dim(`(pid ${existing.pid})`)}`);
1212
+ line();
1213
+ line(`Local: ${c.cyan(url)}`);
1214
+ }
1215
+ if (shouldOpen) openBrowser(url);
1216
+ return;
1217
+ }
1218
+ let port;
1219
+ if (opts.port !== void 0) {
1220
+ port = Number(opts.port);
1221
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new AthenaError(`Invalid port: ${opts.port}`);
1222
+ }
1223
+ const host = opts.host ?? "127.0.0.1";
1224
+ let running;
1225
+ try {
1226
+ running = await startServer({ root, host, port, allowRemote: opts.allowRemote, watch: opts.watch !== false });
1227
+ } catch (err) {
1228
+ const e = err;
1229
+ if (e.code === "EADDRINUSE") throw new AthenaError(`Port ${port} is already in use.`, "Omit --port to pick a free port automatically.");
1230
+ throw new AthenaError(e.message);
1231
+ }
1232
+ if (isJson()) json({ running: true, reused: false, url: running.url, port: running.info.port, pid: process.pid });
1233
+ else {
1234
+ printHeader();
1235
+ line(c.bold(c.green("Athena is running")));
1236
+ line();
1237
+ line(`Local: ${c.cyan(running.url)}`);
1238
+ if (opts.allowRemote) {
1239
+ warn(c.yellow(`Bound to ${host} \u2014 reachable from other machines. The access token in the URL is required for every API call.`));
1240
+ warn(c.yellow("This is plain HTTP: the token and your project knowledge cross the network unencrypted. Use it only on a network you trust, or put a TLS proxy / SSH tunnel in front. Requests must use this machine's address (Host header allowlist)."));
1241
+ }
1242
+ line();
1243
+ line(dim(opts.watch === false ? "File watching is off." : "Watching for changes \u2014 knowledge updates are proposed in the UI, never applied without your review."));
1244
+ line(dim("The link contains a private access token. Press Ctrl+C to stop."));
1245
+ }
1246
+ if (shouldOpen) openBrowser(running.url);
1247
+ setInterruptMessage("Stopping Athena server...");
1248
+ await new Promise((resolve) => {
1249
+ if (opts.signal.aborted) resolve();
1250
+ opts.signal.addEventListener("abort", () => resolve(), { once: true });
1251
+ });
1252
+ await running.close();
1253
+ if (!isJson()) line(dim("Athena server stopped."));
1254
+ }
1255
+ export {
1256
+ openBrowser,
1257
+ openCommand
1258
+ };
1259
+ //# sourceMappingURL=open-2QSZKKUG.js.map