living-ai-documentation 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/LICENSE +661 -0
  2. package/README.fr.md +344 -0
  3. package/README.md +344 -0
  4. package/dist/bin/cli.d.ts +3 -0
  5. package/dist/bin/cli.d.ts.map +1 -0
  6. package/dist/bin/cli.js +262 -0
  7. package/dist/bin/cli.js.map +1 -0
  8. package/dist/src/frontend/accuracy-gauge.js +70 -0
  9. package/dist/src/frontend/admin.html +1532 -0
  10. package/dist/src/frontend/annotations.js +585 -0
  11. package/dist/src/frontend/boot.js +101 -0
  12. package/dist/src/frontend/config.js +29 -0
  13. package/dist/src/frontend/confirm-modal.js +82 -0
  14. package/dist/src/frontend/context.html +1252 -0
  15. package/dist/src/frontend/dark-mode.js +20 -0
  16. package/dist/src/frontend/diagram/alignment.js +161 -0
  17. package/dist/src/frontend/diagram/clipboard.js +187 -0
  18. package/dist/src/frontend/diagram/constants.js +109 -0
  19. package/dist/src/frontend/diagram/custom-shapes.js +104 -0
  20. package/dist/src/frontend/diagram/debug.js +43 -0
  21. package/dist/src/frontend/diagram/drawio-export.js +649 -0
  22. package/dist/src/frontend/diagram/edge-panel.js +293 -0
  23. package/dist/src/frontend/diagram/edge-rendering.js +12 -0
  24. package/dist/src/frontend/diagram/evidence.js +146 -0
  25. package/dist/src/frontend/diagram/grid.js +78 -0
  26. package/dist/src/frontend/diagram/groups.js +102 -0
  27. package/dist/src/frontend/diagram/history.js +157 -0
  28. package/dist/src/frontend/diagram/image-name-modal.js +48 -0
  29. package/dist/src/frontend/diagram/image-upload.js +36 -0
  30. package/dist/src/frontend/diagram/label-editor.js +115 -0
  31. package/dist/src/frontend/diagram/link-panel.js +144 -0
  32. package/dist/src/frontend/diagram/main.js +364 -0
  33. package/dist/src/frontend/diagram/network.js +2214 -0
  34. package/dist/src/frontend/diagram/node-panel.js +389 -0
  35. package/dist/src/frontend/diagram/node-rendering.js +964 -0
  36. package/dist/src/frontend/diagram/persistence.js +168 -0
  37. package/dist/src/frontend/diagram/ports.js +421 -0
  38. package/dist/src/frontend/diagram/selection-overlay.js +387 -0
  39. package/dist/src/frontend/diagram/state.js +43 -0
  40. package/dist/src/frontend/diagram/t.js +3 -0
  41. package/dist/src/frontend/diagram/toast.js +21 -0
  42. package/dist/src/frontend/diagram/unlock-hold.js +206 -0
  43. package/dist/src/frontend/diagram/zoom.js +20 -0
  44. package/dist/src/frontend/diagram-link-modal.js +137 -0
  45. package/dist/src/frontend/diagram.html +1494 -0
  46. package/dist/src/frontend/documents.js +479 -0
  47. package/dist/src/frontend/export.js +338 -0
  48. package/dist/src/frontend/file-attach.js +178 -0
  49. package/dist/src/frontend/files-modal.js +243 -0
  50. package/dist/src/frontend/i18n/en.json +624 -0
  51. package/dist/src/frontend/i18n/fr.json +624 -0
  52. package/dist/src/frontend/i18n.js +32 -0
  53. package/dist/src/frontend/image-paste.js +126 -0
  54. package/dist/src/frontend/index.html +2806 -0
  55. package/dist/src/frontend/local-search.js +476 -0
  56. package/dist/src/frontend/metadata.js +318 -0
  57. package/dist/src/frontend/misc.js +92 -0
  58. package/dist/src/frontend/new-doc-modal.js +285 -0
  59. package/dist/src/frontend/new-folder-modal.js +169 -0
  60. package/dist/src/frontend/search.js +194 -0
  61. package/dist/src/frontend/shape-editor.html +685 -0
  62. package/dist/src/frontend/sidebar-helpers.js +96 -0
  63. package/dist/src/frontend/sidebar-resize.js +98 -0
  64. package/dist/src/frontend/sidebar.js +351 -0
  65. package/dist/src/frontend/snippet-detect.js +25 -0
  66. package/dist/src/frontend/snippet-table.js +85 -0
  67. package/dist/src/frontend/snippet-tree.js +94 -0
  68. package/dist/src/frontend/snippets.js +1146 -0
  69. package/dist/src/frontend/state.js +46 -0
  70. package/dist/src/frontend/utils.js +21 -0
  71. package/dist/src/frontend/validate.js +107 -0
  72. package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
  73. package/dist/src/frontend/wordcloud.js +693 -0
  74. package/dist/src/lib/config.d.ts +26 -0
  75. package/dist/src/lib/config.d.ts.map +1 -0
  76. package/dist/src/lib/config.js +195 -0
  77. package/dist/src/lib/config.js.map +1 -0
  78. package/dist/src/lib/hash.d.ts +2 -0
  79. package/dist/src/lib/hash.d.ts.map +1 -0
  80. package/dist/src/lib/hash.js +18 -0
  81. package/dist/src/lib/hash.js.map +1 -0
  82. package/dist/src/lib/metadata.d.ts +31 -0
  83. package/dist/src/lib/metadata.d.ts.map +1 -0
  84. package/dist/src/lib/metadata.js +128 -0
  85. package/dist/src/lib/metadata.js.map +1 -0
  86. package/dist/src/lib/parser.d.ts +11 -0
  87. package/dist/src/lib/parser.d.ts.map +1 -0
  88. package/dist/src/lib/parser.js +111 -0
  89. package/dist/src/lib/parser.js.map +1 -0
  90. package/dist/src/lib/status.d.ts +9 -0
  91. package/dist/src/lib/status.d.ts.map +1 -0
  92. package/dist/src/lib/status.js +72 -0
  93. package/dist/src/lib/status.js.map +1 -0
  94. package/dist/src/mcp/server.d.ts +3 -0
  95. package/dist/src/mcp/server.d.ts.map +1 -0
  96. package/dist/src/mcp/server.js +2046 -0
  97. package/dist/src/mcp/server.js.map +1 -0
  98. package/dist/src/mcp/tools/diagrams.d.ts +82 -0
  99. package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
  100. package/dist/src/mcp/tools/diagrams.js +594 -0
  101. package/dist/src/mcp/tools/diagrams.js.map +1 -0
  102. package/dist/src/mcp/tools/documents.d.ts +44 -0
  103. package/dist/src/mcp/tools/documents.d.ts.map +1 -0
  104. package/dist/src/mcp/tools/documents.js +186 -0
  105. package/dist/src/mcp/tools/documents.js.map +1 -0
  106. package/dist/src/mcp/tools/git.d.ts +10 -0
  107. package/dist/src/mcp/tools/git.d.ts.map +1 -0
  108. package/dist/src/mcp/tools/git.js +217 -0
  109. package/dist/src/mcp/tools/git.js.map +1 -0
  110. package/dist/src/mcp/tools/metadata.d.ts +57 -0
  111. package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
  112. package/dist/src/mcp/tools/metadata.js +222 -0
  113. package/dist/src/mcp/tools/metadata.js.map +1 -0
  114. package/dist/src/mcp/tools/source.d.ts +29 -0
  115. package/dist/src/mcp/tools/source.d.ts.map +1 -0
  116. package/dist/src/mcp/tools/source.js +196 -0
  117. package/dist/src/mcp/tools/source.js.map +1 -0
  118. package/dist/src/routes/annotations.d.ts +3 -0
  119. package/dist/src/routes/annotations.d.ts.map +1 -0
  120. package/dist/src/routes/annotations.js +83 -0
  121. package/dist/src/routes/annotations.js.map +1 -0
  122. package/dist/src/routes/browse-source.d.ts +3 -0
  123. package/dist/src/routes/browse-source.d.ts.map +1 -0
  124. package/dist/src/routes/browse-source.js +79 -0
  125. package/dist/src/routes/browse-source.js.map +1 -0
  126. package/dist/src/routes/browse.d.ts +3 -0
  127. package/dist/src/routes/browse.d.ts.map +1 -0
  128. package/dist/src/routes/browse.js +91 -0
  129. package/dist/src/routes/browse.js.map +1 -0
  130. package/dist/src/routes/config.d.ts +3 -0
  131. package/dist/src/routes/config.d.ts.map +1 -0
  132. package/dist/src/routes/config.js +145 -0
  133. package/dist/src/routes/config.js.map +1 -0
  134. package/dist/src/routes/context.d.ts +3 -0
  135. package/dist/src/routes/context.d.ts.map +1 -0
  136. package/dist/src/routes/context.js +287 -0
  137. package/dist/src/routes/context.js.map +1 -0
  138. package/dist/src/routes/diagrams.d.ts +3 -0
  139. package/dist/src/routes/diagrams.d.ts.map +1 -0
  140. package/dist/src/routes/diagrams.js +69 -0
  141. package/dist/src/routes/diagrams.js.map +1 -0
  142. package/dist/src/routes/documents.d.ts +11 -0
  143. package/dist/src/routes/documents.d.ts.map +1 -0
  144. package/dist/src/routes/documents.js +450 -0
  145. package/dist/src/routes/documents.js.map +1 -0
  146. package/dist/src/routes/export.d.ts +3 -0
  147. package/dist/src/routes/export.d.ts.map +1 -0
  148. package/dist/src/routes/export.js +280 -0
  149. package/dist/src/routes/export.js.map +1 -0
  150. package/dist/src/routes/files.d.ts +3 -0
  151. package/dist/src/routes/files.d.ts.map +1 -0
  152. package/dist/src/routes/files.js +180 -0
  153. package/dist/src/routes/files.js.map +1 -0
  154. package/dist/src/routes/images.d.ts +3 -0
  155. package/dist/src/routes/images.d.ts.map +1 -0
  156. package/dist/src/routes/images.js +49 -0
  157. package/dist/src/routes/images.js.map +1 -0
  158. package/dist/src/routes/metadata.d.ts +3 -0
  159. package/dist/src/routes/metadata.d.ts.map +1 -0
  160. package/dist/src/routes/metadata.js +131 -0
  161. package/dist/src/routes/metadata.js.map +1 -0
  162. package/dist/src/routes/shape-libraries.d.ts +3 -0
  163. package/dist/src/routes/shape-libraries.d.ts.map +1 -0
  164. package/dist/src/routes/shape-libraries.js +118 -0
  165. package/dist/src/routes/shape-libraries.js.map +1 -0
  166. package/dist/src/routes/wordcloud.d.ts +3 -0
  167. package/dist/src/routes/wordcloud.d.ts.map +1 -0
  168. package/dist/src/routes/wordcloud.js +95 -0
  169. package/dist/src/routes/wordcloud.js.map +1 -0
  170. package/dist/src/server.d.ts +7 -0
  171. package/dist/src/server.d.ts.map +1 -0
  172. package/dist/src/server.js +93 -0
  173. package/dist/src/server.js.map +1 -0
  174. package/dist/starter-doc/.living-doc.json +52 -0
  175. package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
  176. package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
  177. package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
  178. package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
  179. package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
  180. package/dist/starter-doc/AI/default/AGENTS.md +31 -0
  181. package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
  182. package/dist/starter-doc/AI/default/MEMORY.md +24 -0
  183. package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
  184. package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
  185. package/dist/starter-doc/WORKLOG/current-task.md +57 -0
  186. package/dist/starter-doc-fr/.living-doc.json +52 -0
  187. package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
  188. package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
  189. package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
  190. package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
  191. package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
  192. package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
  193. package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
  194. package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
  195. package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
  196. package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
  197. package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
  198. package/images/living_documentation.jpg +0 -0
  199. package/images/readme-extra-files.png +0 -0
  200. package/images/readme-filename-pattern.png +0 -0
  201. package/images/readme-intelligent-search-demo.jpg +0 -0
  202. package/images/readme-sidebar.png +0 -0
  203. package/package.json +72 -0
@@ -0,0 +1,1252 @@
1
+ <!doctype html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AI Context</title>
7
+ <script src="/i18n.js"></script>
8
+ <script src="/confirm-modal.js"></script>
9
+ <script src="https://cdn.tailwindcss.com?plugins=typography"></script>
10
+ <script>
11
+ tailwind.config = { darkMode: "class", theme: { extend: {} } };
12
+ </script>
13
+ </head>
14
+ <body
15
+ class="min-h-screen bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100"
16
+ >
17
+ <header
18
+ class="h-14 border-b border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900 flex items-center gap-3 px-4"
19
+ >
20
+ <a
21
+ href="/"
22
+ class="text-sm font-medium text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400"
23
+ data-i18n="context.back_to_docs"
24
+ >Back to docs</a
25
+ >
26
+ <h1 class="text-base font-semibold" data-i18n="context.title">
27
+ AI Context
28
+ </h1>
29
+ <span id="status" class="ml-auto text-xs text-gray-500"></span>
30
+ </header>
31
+
32
+ <main class="max-w-7xl mx-auto px-4 py-5 space-y-6">
33
+ <section class="space-y-1">
34
+ <h2 class="text-xl font-semibold" data-i18n="context.orientation_title">
35
+ AI orientation
36
+ </h2>
37
+ <p
38
+ class="text-sm text-gray-500 dark:text-gray-400"
39
+ data-i18n="context.orientation_intro"
40
+ >
41
+ Stable instructions and rules for AI-assisted development.
42
+ </p>
43
+ </section>
44
+
45
+ <section
46
+ class="grid min-w-0 grid-cols-1 xl:grid-cols-[minmax(0,1fr)_24rem] gap-5"
47
+ >
48
+ <div class="min-w-0 space-y-5">
49
+ <section
50
+ class="min-w-0 rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900 p-4"
51
+ >
52
+ <div class="flex items-center justify-between gap-3 mb-3">
53
+ <h2
54
+ class="text-sm font-semibold"
55
+ data-i18n="context.instructions_title"
56
+ >
57
+ Instruction files
58
+ </h2>
59
+ <div class="flex items-center gap-3">
60
+ <span
61
+ id="instructionCount"
62
+ class="text-xs text-gray-500"
63
+ ></span>
64
+ <button
65
+ id="btnToggleInstructionBrowser"
66
+ class="rounded-md border border-gray-300 px-2 py-1 text-xs font-semibold text-gray-600 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
67
+ data-i18n="context.add_instruction_file"
68
+ >
69
+ Add AI instruction file
70
+ </button>
71
+ </div>
72
+ </div>
73
+ <div
74
+ id="instructionBrowser"
75
+ class="hidden mb-3 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
76
+ >
77
+ <div
78
+ class="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"
79
+ >
80
+ <button
81
+ id="instructionBrowseUp"
82
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0"
83
+ >
84
+ <span data-i18n="common.up">↑ Up</span>
85
+ </button>
86
+ <span
87
+ id="instructionBrowsePath"
88
+ class="font-mono text-xs text-gray-400 dark:text-gray-500 truncate flex-1 text-right"
89
+ ></span>
90
+ </div>
91
+ <div
92
+ id="instructionBrowseList"
93
+ class="divide-y divide-gray-100 dark:divide-gray-800 max-h-52 overflow-y-auto"
94
+ ></div>
95
+ </div>
96
+ <div id="instructionList" class="space-y-2"></div>
97
+ </section>
98
+
99
+ <section
100
+ class="min-w-0 rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900 p-4"
101
+ >
102
+ <div class="flex items-center justify-between gap-3 mb-3">
103
+ <h2 class="text-sm font-semibold" data-i18n="context.rules_title">
104
+ AI rules
105
+ </h2>
106
+ <span
107
+ id="rulesFolder"
108
+ class="font-mono text-xs text-gray-400"
109
+ ></span>
110
+ </div>
111
+ <div id="ruleList" class="space-y-2"></div>
112
+ </section>
113
+
114
+ <section
115
+ class="min-w-0 rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900 p-4 space-y-3"
116
+ >
117
+ <div class="flex items-center justify-between gap-3">
118
+ <h2 class="text-sm font-semibold" data-i18n="context.mcp_title">
119
+ MCP explorer
120
+ </h2>
121
+ <button
122
+ id="btnRefreshMcp"
123
+ class="rounded-md border border-gray-300 px-2 py-1 text-xs font-semibold text-gray-600 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
124
+ data-i18n="context.mcp_refresh"
125
+ >
126
+ Refresh
127
+ </button>
128
+ </div>
129
+ <p
130
+ class="text-xs text-gray-500 dark:text-gray-400"
131
+ data-i18n="context.mcp_intro"
132
+ >
133
+ Verify the local Living Documentation MCP endpoint exposed to AI
134
+ tools.
135
+ </p>
136
+ <div class="grid grid-cols-2 gap-2 text-xs">
137
+ <div
138
+ class="rounded-md border border-gray-100 dark:border-gray-800 px-2 py-2"
139
+ >
140
+ <div class="text-gray-400" data-i18n="context.mcp_status">
141
+ Status
142
+ </div>
143
+ <div id="mcpStatus" class="mt-1 font-semibold text-gray-500">
144
+ -
145
+ </div>
146
+ </div>
147
+ <div
148
+ class="rounded-md border border-gray-100 dark:border-gray-800 px-2 py-2"
149
+ >
150
+ <div class="text-gray-400" data-i18n="context.mcp_transport">
151
+ Transport
152
+ </div>
153
+ <div
154
+ id="mcpTransport"
155
+ class="mt-1 font-mono text-gray-700 dark:text-gray-200 truncate"
156
+ >
157
+ -
158
+ </div>
159
+ </div>
160
+ <div
161
+ class="col-span-2 rounded-md border border-gray-100 dark:border-gray-800 px-2 py-2"
162
+ >
163
+ <div class="text-gray-400" data-i18n="context.mcp_endpoint">
164
+ Endpoint
165
+ </div>
166
+ <div
167
+ id="mcpEndpoint"
168
+ class="mt-1 font-mono text-gray-700 dark:text-gray-200 break-all"
169
+ >
170
+ -
171
+ </div>
172
+ </div>
173
+ </div>
174
+ <div>
175
+ <div class="mb-2 flex items-center justify-between gap-3">
176
+ <h3
177
+ class="text-xs font-semibold text-gray-500"
178
+ data-i18n="context.mcp_tools"
179
+ >
180
+ Tools
181
+ </h3>
182
+ <div class="flex items-center gap-3">
183
+ <span id="mcpToolCount" class="text-xs text-gray-400"></span>
184
+ <button
185
+ id="btnRunAllTools"
186
+ type="button"
187
+ class="rounded-md border border-gray-300 px-2 py-1 text-xs font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
188
+ data-i18n="context.mcp_run_all_tools"
189
+ >
190
+ Run all tools
191
+ </button>
192
+ </div>
193
+ </div>
194
+ <div id="mcpToolList" class="space-y-2"></div>
195
+ </div>
196
+ <div>
197
+ <div class="mb-2 flex items-center justify-between gap-3">
198
+ <h3
199
+ class="text-xs font-semibold text-gray-500"
200
+ data-i18n="context.mcp_prompts"
201
+ >
202
+ Prompts
203
+ </h3>
204
+ <div class="flex items-center gap-3">
205
+ <span
206
+ id="mcpPromptCount"
207
+ class="text-xs text-gray-400"
208
+ ></span>
209
+ <button
210
+ id="btnGetAllPrompts"
211
+ type="button"
212
+ class="rounded-md border border-gray-300 px-2 py-1 text-xs font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
213
+ data-i18n="context.mcp_get_all_prompts"
214
+ >
215
+ Get all prompts
216
+ </button>
217
+ </div>
218
+ </div>
219
+ <div id="mcpPromptList" class="space-y-2"></div>
220
+ </div>
221
+ </section>
222
+ </div>
223
+
224
+ <aside class="min-w-0 max-w-full space-y-5">
225
+ <section
226
+ class="min-w-0 rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900 p-4 space-y-3"
227
+ >
228
+ <h2
229
+ class="text-sm font-semibold"
230
+ data-i18n="context.add_rule_title"
231
+ >
232
+ Add AI rule
233
+ </h2>
234
+ <div>
235
+ <label
236
+ for="ruleTitle"
237
+ class="block text-xs font-semibold text-gray-500 mb-1"
238
+ data-i18n="context.rule_title_label"
239
+ >Title</label
240
+ >
241
+ <input
242
+ id="ruleTitle"
243
+ class="min-w-0 w-full h-9 rounded-md border border-gray-300 bg-white px-2 text-sm dark:border-gray-700 dark:bg-gray-950"
244
+ data-i18n-placeholder="context.rule_title_placeholder"
245
+ placeholder="Avoid magic numbers"
246
+ />
247
+ </div>
248
+ <div>
249
+ <label
250
+ for="ruleSeverity"
251
+ class="block text-xs font-semibold text-gray-500 mb-1"
252
+ data-i18n="context.rule_severity_label"
253
+ >Severity</label
254
+ >
255
+ <select
256
+ id="ruleSeverity"
257
+ class="min-w-0 w-full h-9 rounded-md border border-gray-300 bg-white px-2 text-sm dark:border-gray-700 dark:bg-gray-950"
258
+ >
259
+ <option
260
+ value="guideline"
261
+ data-i18n="context.rule_severity_guideline"
262
+ >
263
+ Guideline
264
+ </option>
265
+ <option
266
+ value="warning"
267
+ data-i18n="context.rule_severity_warning"
268
+ >
269
+ Warning
270
+ </option>
271
+ <option
272
+ value="required"
273
+ data-i18n="context.rule_severity_required"
274
+ >
275
+ Required
276
+ </option>
277
+ </select>
278
+ </div>
279
+ <div>
280
+ <label
281
+ for="ruleDescription"
282
+ class="block text-xs font-semibold text-gray-500 mb-1"
283
+ data-i18n="context.rule_description_label"
284
+ >Description</label
285
+ >
286
+ <input
287
+ id="ruleDescription"
288
+ class="min-w-0 w-full h-9 rounded-md border border-gray-300 bg-white px-2 text-sm dark:border-gray-700 dark:bg-gray-950"
289
+ data-i18n-placeholder="context.rule_description_placeholder"
290
+ placeholder="Name domain constants instead of repeating raw values."
291
+ />
292
+ </div>
293
+ <div>
294
+ <label
295
+ for="ruleTags"
296
+ class="block text-xs font-semibold text-gray-500 mb-1"
297
+ data-i18n="context.rule_tags_label"
298
+ >Tags</label
299
+ >
300
+ <input
301
+ id="ruleTags"
302
+ class="min-w-0 w-full h-9 rounded-md border border-gray-300 bg-white px-2 text-sm dark:border-gray-700 dark:bg-gray-950"
303
+ data-i18n-placeholder="context.rule_tags_placeholder"
304
+ placeholder="code-quality, maintainability"
305
+ />
306
+ </div>
307
+ <div>
308
+ <label
309
+ for="ruleAppliesTo"
310
+ class="block text-xs font-semibold text-gray-500 mb-1"
311
+ data-i18n="context.rule_applies_to_label"
312
+ >Applies to</label
313
+ >
314
+ <input
315
+ id="ruleAppliesTo"
316
+ class="min-w-0 w-full h-9 rounded-md border border-gray-300 bg-white px-2 text-sm dark:border-gray-700 dark:bg-gray-950"
317
+ data-i18n-placeholder="context.rule_applies_to_placeholder"
318
+ placeholder="src/**/*.ts, src/frontend/**/*.js"
319
+ />
320
+ </div>
321
+ <div>
322
+ <label
323
+ for="ruleBody"
324
+ class="block text-xs font-semibold text-gray-500 mb-1"
325
+ data-i18n="context.rule_body_label"
326
+ >Rule body</label
327
+ >
328
+ <textarea
329
+ id="ruleBody"
330
+ rows="7"
331
+ class="min-w-0 w-full rounded-md border border-gray-300 bg-white px-2 py-2 text-sm dark:border-gray-700 dark:bg-gray-950"
332
+ data-i18n-placeholder="context.rule_body_placeholder"
333
+ placeholder="Numeric constants with domain meaning should be named."
334
+ ></textarea>
335
+ </div>
336
+ <button
337
+ id="btnAddRule"
338
+ class="w-full h-9 rounded-md bg-blue-600 text-sm font-semibold text-white hover:bg-blue-700"
339
+ data-i18n="context.add_rule_button"
340
+ >
341
+ Add rule
342
+ </button>
343
+ </section>
344
+ </aside>
345
+ </section>
346
+ </main>
347
+
348
+ <div
349
+ id="confirm-modal"
350
+ class="hidden fixed inset-0 z-[60] bg-black/50 flex items-center justify-center p-4"
351
+ onclick="_confirmModalBackdrop(event)"
352
+ >
353
+ <div
354
+ class="w-full max-w-md bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-2xl p-5 flex flex-col gap-3"
355
+ onclick="event.stopPropagation()"
356
+ >
357
+ <h3
358
+ id="confirm-modal-title"
359
+ class="font-semibold text-gray-900 dark:text-gray-100"
360
+ ></h3>
361
+ <p
362
+ id="confirm-modal-message"
363
+ class="text-sm text-gray-700 dark:text-gray-200"
364
+ ></p>
365
+ <p
366
+ id="confirm-modal-detail"
367
+ class="text-xs text-gray-500 dark:text-gray-400 italic break-all"
368
+ ></p>
369
+ <div class="flex justify-end gap-2 mt-2">
370
+ <button
371
+ id="confirm-modal-cancel"
372
+ type="button"
373
+ class="text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
374
+ ></button>
375
+ <button
376
+ id="confirm-modal-ok"
377
+ type="button"
378
+ class="text-sm px-4 py-1.5 rounded-lg text-white font-semibold transition-colors"
379
+ ></button>
380
+ </div>
381
+ </div>
382
+ </div>
383
+
384
+ <script>
385
+ const els = {
386
+ status: document.getElementById("status"),
387
+ instructionList: document.getElementById("instructionList"),
388
+ instructionCount: document.getElementById("instructionCount"),
389
+ instructionBrowser: document.getElementById("instructionBrowser"),
390
+ instructionBrowseUp: document.getElementById("instructionBrowseUp"),
391
+ instructionBrowsePath: document.getElementById("instructionBrowsePath"),
392
+ instructionBrowseList: document.getElementById("instructionBrowseList"),
393
+ btnToggleInstructionBrowser: document.getElementById(
394
+ "btnToggleInstructionBrowser",
395
+ ),
396
+ ruleList: document.getElementById("ruleList"),
397
+ rulesFolder: document.getElementById("rulesFolder"),
398
+ btnRefreshMcp: document.getElementById("btnRefreshMcp"),
399
+ mcpStatus: document.getElementById("mcpStatus"),
400
+ mcpTransport: document.getElementById("mcpTransport"),
401
+ mcpEndpoint: document.getElementById("mcpEndpoint"),
402
+ mcpToolCount: document.getElementById("mcpToolCount"),
403
+ mcpToolList: document.getElementById("mcpToolList"),
404
+ mcpPromptCount: document.getElementById("mcpPromptCount"),
405
+ mcpPromptList: document.getElementById("mcpPromptList"),
406
+ btnRunAllTools: document.getElementById("btnRunAllTools"),
407
+ btnGetAllPrompts: document.getElementById("btnGetAllPrompts"),
408
+ ruleTitle: document.getElementById("ruleTitle"),
409
+ ruleSeverity: document.getElementById("ruleSeverity"),
410
+ ruleDescription: document.getElementById("ruleDescription"),
411
+ ruleTags: document.getElementById("ruleTags"),
412
+ ruleAppliesTo: document.getElementById("ruleAppliesTo"),
413
+ ruleBody: document.getElementById("ruleBody"),
414
+ btnAddRule: document.getElementById("btnAddRule"),
415
+ };
416
+ let orientation = null;
417
+ let instructionBrowseCurrent = "";
418
+ let instructionBrowseParent = "";
419
+ const mcpItemsByKind = {
420
+ tool: new Map(),
421
+ prompt: new Map(),
422
+ };
423
+
424
+ function setStatus(message) {
425
+ els.status.textContent = message || "";
426
+ }
427
+
428
+ function escapeHtml(value) {
429
+ return String(value ?? "").replace(
430
+ /[&<>"']/g,
431
+ (char) =>
432
+ ({
433
+ "&": "&amp;",
434
+ "<": "&lt;",
435
+ ">": "&gt;",
436
+ '"': "&quot;",
437
+ "'": "&#39;",
438
+ })[char],
439
+ );
440
+ }
441
+
442
+ function splitList(value) {
443
+ return value
444
+ .split(",")
445
+ .map((item) => item.trim())
446
+ .filter(Boolean);
447
+ }
448
+
449
+ function basename(value) {
450
+ return (
451
+ String(value || "")
452
+ .split(/[\\/]/)
453
+ .pop() || ""
454
+ );
455
+ }
456
+
457
+ function documentHref(docPath) {
458
+ const docId = encodeURIComponent(
459
+ String(docPath || "").replace(/\.md$/i, ""),
460
+ );
461
+ return "/?doc=" + encodeURIComponent(docId);
462
+ }
463
+
464
+ function formatMessage(key, values) {
465
+ return Object.entries(values || {}).reduce(
466
+ (message, [name, value]) => message.replaceAll(`{${name}}`, value),
467
+ window.t(key),
468
+ );
469
+ }
470
+
471
+ function rootLabel(value) {
472
+ return window.t(`context.root.${value}`);
473
+ }
474
+
475
+ function typeLabel(value) {
476
+ return window.t(`context.type.${value}`);
477
+ }
478
+
479
+ function severityLabel(value) {
480
+ return window.t(`context.rule_severity_${value}`) ===
481
+ `context.rule_severity_${value}`
482
+ ? value
483
+ : window.t(`context.rule_severity_${value}`);
484
+ }
485
+
486
+ function renderEmpty(container, key) {
487
+ container.innerHTML = `<p class="text-sm text-gray-400">${window.t(key)}</p>`;
488
+ }
489
+
490
+ function renderInstructionFiles(items) {
491
+ els.instructionCount.textContent = `${items.filter((item) => item.exists).length}/${items.length}`;
492
+ if (!items.length) {
493
+ renderEmpty(els.instructionList, "context.no_instructions");
494
+ return;
495
+ }
496
+ els.instructionList.innerHTML = items
497
+ .map((item) => {
498
+ const statusClass = item.exists
499
+ ? "text-green-600 dark:text-green-400"
500
+ : "text-red-600 dark:text-red-400";
501
+ const statusLabel = item.exists
502
+ ? window.t("context.exists")
503
+ : window.t("context.missing");
504
+ const size =
505
+ item.size === null ? "" : ` · ${Math.round(item.size / 1024)} KB`;
506
+ return `
507
+ <div class="flex items-start justify-between gap-3 rounded-md border border-gray-100 dark:border-gray-800 px-3 py-2">
508
+ <div class="min-w-0">
509
+ <a href="${escapeHtml(documentHref(item.path))}" class="font-mono text-xs break-all text-blue-600 hover:underline dark:text-blue-400">${escapeHtml(item.path)}</a>
510
+ <div class="text-xs text-gray-400">${escapeHtml(rootLabel(item.root))} · ${escapeHtml(typeLabel(item.type))}${size}</div>
511
+ </div>
512
+ <div class="flex items-center gap-3 shrink-0">
513
+ <span class="text-xs font-semibold ${statusClass}">${statusLabel}</span>
514
+ ${
515
+ item.exists
516
+ ? `<button data-filename="${escapeHtml(basename(item.path))}" class="instruction-remove text-xs text-red-500 hover:text-red-700 dark:hover:text-red-400">${window.t("common.remove")}</button>`
517
+ : ""
518
+ }
519
+ </div>
520
+ </div>
521
+ `;
522
+ })
523
+ .join("");
524
+ els.instructionList
525
+ .querySelectorAll(".instruction-remove")
526
+ .forEach((button) => {
527
+ button.addEventListener("click", () =>
528
+ removeInstructionFile(button.dataset.filename),
529
+ );
530
+ });
531
+ }
532
+
533
+ async function loadInstructionBrowse(dirPath) {
534
+ els.instructionBrowseList.innerHTML = `<p class="px-3 py-4 text-xs text-gray-400 text-center">${window.t("common.loading")}</p>`;
535
+ try {
536
+ const data = await fetch(
537
+ "/api/browse?path=" + encodeURIComponent(dirPath),
538
+ ).then((res) => res.json());
539
+ instructionBrowseCurrent = data.current;
540
+ instructionBrowseParent = data.parent;
541
+ els.instructionBrowsePath.textContent = data.current;
542
+ els.instructionBrowseUp.disabled = !data.parent;
543
+
544
+ const instructionNames = new Set(
545
+ ((orientation && orientation.instructions) || []).map((item) =>
546
+ basename(item.path),
547
+ ),
548
+ );
549
+ const rows = [];
550
+ for (const dir of data.dirs || []) {
551
+ rows.push(`
552
+ <button data-path="${escapeHtml(dir.path)}" class="instruction-dir w-full flex items-center gap-2 px-3 py-2 text-sm text-left hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
553
+ <span class="text-gray-400 shrink-0">&#128193;</span>
554
+ <span class="text-gray-700 dark:text-gray-300 truncate">${escapeHtml(dir.name)}</span>
555
+ </button>
556
+ `);
557
+ }
558
+ for (const file of data.files || []) {
559
+ const added = instructionNames.has(file.name);
560
+ rows.push(`
561
+ <div class="flex items-center justify-between gap-2 px-3 py-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-800">
562
+ <span class="flex items-center gap-2 text-gray-700 dark:text-gray-300 min-w-0">
563
+ <span class="text-gray-400 shrink-0">&#128196;</span>
564
+ <span class="truncate">${escapeHtml(file.name)}</span>
565
+ </span>
566
+ ${
567
+ added
568
+ ? `<span class="text-xs text-green-600 dark:text-green-400 shrink-0">${window.t("context.added")}</span>`
569
+ : `<button data-path="${escapeHtml(file.path)}" class="instruction-add text-xs text-blue-600 dark:text-blue-400 hover:underline shrink-0">${window.t("context.add_file_action")}</button>`
570
+ }
571
+ </div>
572
+ `);
573
+ }
574
+
575
+ els.instructionBrowseList.innerHTML = rows.length
576
+ ? rows.join("")
577
+ : `<p class="px-3 py-4 text-xs text-gray-400 text-center">${window.t("common.empty_dir")}</p>`;
578
+
579
+ els.instructionBrowseList
580
+ .querySelectorAll(".instruction-dir")
581
+ .forEach((button) => {
582
+ button.addEventListener("click", () =>
583
+ loadInstructionBrowse(button.dataset.path),
584
+ );
585
+ });
586
+ els.instructionBrowseList
587
+ .querySelectorAll(".instruction-add")
588
+ .forEach((button) => {
589
+ button.addEventListener("click", () =>
590
+ addInstructionFile(button.dataset.path),
591
+ );
592
+ });
593
+ } catch {
594
+ els.instructionBrowseList.innerHTML = `<p class="px-3 py-4 text-xs text-red-400 text-center">${window.t("common.cannot_read_dir")}</p>`;
595
+ }
596
+ }
597
+
598
+ function toggleInstructionBrowser() {
599
+ const isHidden = els.instructionBrowser.classList.toggle("hidden");
600
+ els.btnToggleInstructionBrowser.textContent = window.t(
601
+ isHidden
602
+ ? "context.add_instruction_file"
603
+ : "context.hide_instruction_browser",
604
+ );
605
+ if (
606
+ !isHidden &&
607
+ !instructionBrowseCurrent &&
608
+ orientation &&
609
+ orientation.sourceRoot
610
+ ) {
611
+ loadInstructionBrowse(orientation.sourceRoot);
612
+ }
613
+ }
614
+
615
+ async function addInstructionFile(filePath) {
616
+ setStatus(window.t("context.adding_instruction_file"));
617
+ const res = await fetch("/api/context/instructions", {
618
+ method: "POST",
619
+ headers: { "Content-Type": "application/json" },
620
+ body: JSON.stringify({ path: filePath }),
621
+ });
622
+ if (!res.ok) throw new Error(await res.text());
623
+ await loadOrientation();
624
+ if (!els.instructionBrowser.classList.contains("hidden")) {
625
+ await loadInstructionBrowse(instructionBrowseCurrent);
626
+ }
627
+ }
628
+
629
+ async function removeInstructionFile(filename) {
630
+ const ok = await window.showConfirm({
631
+ title: window.t("context.remove_instruction_title"),
632
+ message: formatMessage("context.remove_instruction_message", {
633
+ name: filename,
634
+ }),
635
+ detail: window.t("context.remove_instruction_detail"),
636
+ confirmLabel: window.t("common.remove"),
637
+ danger: true,
638
+ });
639
+ if (!ok) return;
640
+
641
+ setStatus(window.t("context.removing_instruction_file"));
642
+ const res = await fetch(
643
+ "/api/context/instructions/" + encodeURIComponent(filename),
644
+ {
645
+ method: "DELETE",
646
+ },
647
+ );
648
+ if (!res.ok) throw new Error(await res.text());
649
+ await loadOrientation();
650
+ if (!els.instructionBrowser.classList.contains("hidden")) {
651
+ await loadInstructionBrowse(instructionBrowseCurrent);
652
+ }
653
+ }
654
+
655
+ function renderRules(items) {
656
+ if (!items.length) {
657
+ renderEmpty(els.ruleList, "context.no_rules");
658
+ return;
659
+ }
660
+ els.ruleList.innerHTML = items
661
+ .map(
662
+ (rule) => `
663
+ <article class="rounded-md border border-gray-100 dark:border-gray-800 px-3 py-2">
664
+ <div class="flex items-start justify-between gap-3">
665
+ <div class="min-w-0">
666
+ <h3 class="text-sm font-semibold">
667
+ <a href="${escapeHtml(documentHref(rule.path))}" class="text-blue-600 hover:underline dark:text-blue-400">${escapeHtml(rule.title)}</a>
668
+ </h3>
669
+ <a href="${escapeHtml(documentHref(rule.path))}" class="font-mono text-xs text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 break-all">${escapeHtml(rule.path)}</a>
670
+ </div>
671
+ <span class="rounded bg-gray-100 px-2 py-1 text-xs font-semibold text-gray-600 dark:bg-gray-800 dark:text-gray-300">${escapeHtml(severityLabel(rule.severity))}</span>
672
+ </div>
673
+ <p class="mt-2 text-sm text-gray-600 dark:text-gray-300">${escapeHtml(rule.description)}</p>
674
+ <div class="mt-2 flex flex-wrap gap-1 text-xs text-blue-600 dark:text-blue-400">
675
+ ${(rule.tags || []).map((tag) => `<span>${escapeHtml(tag)}</span>`).join("")}
676
+ </div>
677
+ <div class="mt-2 font-mono text-xs text-gray-400 break-all">${escapeHtml((rule.appliesTo || []).join(", "))}</div>
678
+ </article>
679
+ `,
680
+ )
681
+ .join("");
682
+ }
683
+
684
+ function formatJson(value) {
685
+ if (!value || typeof value !== "object") return "";
686
+ return JSON.stringify(value, null, 2);
687
+ }
688
+
689
+ function exampleValueForSchema(schema) {
690
+ if (!schema || typeof schema !== "object") return null;
691
+ if (schema.type === "string") return "";
692
+ if (schema.type === "number" || schema.type === "integer") return 0;
693
+ if (schema.type === "boolean") return false;
694
+ if (schema.type === "array") return [];
695
+ if (schema.type === "object" || schema.properties) {
696
+ const result = {};
697
+ const properties = schema.properties || {};
698
+ const required = Array.isArray(schema.required)
699
+ ? schema.required
700
+ : [];
701
+ const keys = required.length ? required : [];
702
+ keys.forEach((key) => {
703
+ result[key] = exampleValueForSchema(properties[key]) ?? "";
704
+ });
705
+ return result;
706
+ }
707
+ return null;
708
+ }
709
+
710
+ function exampleToolArguments(item) {
711
+ if (!item || typeof item !== "object") return {};
712
+ const example = exampleValueForSchema(item.inputSchema);
713
+ return example && typeof example === "object" && !Array.isArray(example)
714
+ ? example
715
+ : {};
716
+ }
717
+
718
+ function examplePromptArguments(item) {
719
+ if (!item || typeof item !== "object" || !Array.isArray(item.arguments))
720
+ return {};
721
+ return item.arguments.reduce((acc, argument) => {
722
+ if (argument.required) acc[argument.name] = "";
723
+ return acc;
724
+ }, {});
725
+ }
726
+
727
+ function renderMcpArgument(argument) {
728
+ const required = argument.required
729
+ ? window.t("context.mcp_required")
730
+ : window.t("context.mcp_optional");
731
+ return `
732
+ <li class="rounded border border-gray-100 px-2 py-1 dark:border-gray-800">
733
+ <div class="flex flex-wrap items-center gap-2">
734
+ <span class="font-mono text-xs font-semibold text-gray-800 dark:text-gray-100">${escapeHtml(argument.name || "-")}</span>
735
+ <span class="text-[11px] uppercase tracking-wide text-gray-400">${escapeHtml(required)}</span>
736
+ </div>
737
+ ${argument.description ? `<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">${escapeHtml(argument.description)}</p>` : ""}
738
+ </li>
739
+ `;
740
+ }
741
+
742
+ function renderMcpItems(container, items, emptyKey, kind) {
743
+ if (mcpItemsByKind[kind]) {
744
+ mcpItemsByKind[kind].clear();
745
+ items.forEach((item) => {
746
+ const label = typeof item === "string" ? item : item.name;
747
+ if (label) mcpItemsByKind[kind].set(label, item);
748
+ });
749
+ }
750
+ if (!items.length) {
751
+ container.innerHTML = `<p class="text-xs text-gray-400">${window.t(emptyKey)}</p>`;
752
+ return;
753
+ }
754
+ container.innerHTML = items
755
+ .map((item) => {
756
+ const label = typeof item === "string" ? item : item.name;
757
+ const description =
758
+ typeof item === "string" ? "" : item.description;
759
+ const schema =
760
+ typeof item === "string" ? "" : formatJson(item.inputSchema);
761
+ const args = Array.isArray(item.arguments) ? item.arguments : [];
762
+ const exampleArgs =
763
+ kind === "tool"
764
+ ? exampleToolArguments(item)
765
+ : examplePromptArguments(item);
766
+ return `
767
+ <details class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950">
768
+ <summary class="cursor-pointer select-none font-mono text-xs font-semibold text-blue-700 dark:text-blue-300">${escapeHtml(label)}</summary>
769
+ ${description ? `<p class="mt-2 whitespace-pre-wrap text-xs leading-5 text-gray-600 dark:text-gray-300">${escapeHtml(description)}</p>` : ""}
770
+ ${
771
+ kind === "tool" && schema
772
+ ? `
773
+ <div class="mt-3">
774
+ <h4 class="mb-1 text-xs font-semibold text-gray-500">${window.t("context.mcp_input_schema")}</h4>
775
+ <pre class="max-h-56 overflow-auto rounded bg-white p-2 text-xs text-gray-700 dark:bg-gray-900 dark:text-gray-200"><code>${escapeHtml(schema)}</code></pre>
776
+ </div>
777
+ `
778
+ : ""
779
+ }
780
+ ${
781
+ kind === "prompt" && args.length
782
+ ? `
783
+ <div class="mt-3">
784
+ <h4 class="mb-1 text-xs font-semibold text-gray-500">${window.t("context.mcp_arguments")}</h4>
785
+ <ul class="space-y-1">${args.map(renderMcpArgument).join("")}</ul>
786
+ </div>
787
+ `
788
+ : ""
789
+ }
790
+ <div class="mt-3 border-t border-gray-200 pt-3 dark:border-gray-800">
791
+ <label class="mb-1 block text-xs font-semibold text-gray-500">${window.t("context.mcp_request_json")}</label>
792
+ <textarea data-mcp-args="${escapeHtml(label)}" data-mcp-kind="${escapeHtml(kind)}" rows="4" spellcheck="false" class="w-full rounded border border-gray-200 bg-white px-2 py-2 font-mono text-xs text-gray-800 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100">${escapeHtml(JSON.stringify(exampleArgs, null, 2))}</textarea>
793
+ <div class="mt-2 flex items-center justify-between gap-3">
794
+ <span data-mcp-error="${escapeHtml(label)}" data-mcp-kind="${escapeHtml(kind)}" class="text-xs text-red-500"></span>
795
+ <button data-mcp-run="${escapeHtml(label)}" data-mcp-kind="${escapeHtml(kind)}" type="button" class="rounded-md bg-blue-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-blue-700">
796
+ ${window.t(kind === "tool" ? "context.mcp_run_tool" : "context.mcp_get_prompt")}
797
+ </button>
798
+ </div>
799
+ <div data-mcp-result="${escapeHtml(label)}" data-mcp-kind="${escapeHtml(kind)}" class="mt-2 hidden text-xs"></div>
800
+ </div>
801
+ </details>
802
+ `;
803
+ })
804
+ .join("");
805
+ container.querySelectorAll("[data-mcp-run]").forEach((button) => {
806
+ button.addEventListener("click", () =>
807
+ runMcpItem(button.dataset.mcpKind, button.dataset.mcpRun),
808
+ );
809
+ });
810
+ }
811
+
812
+ function parseSseJson(text) {
813
+ const match = text.match(/^data:\s*(\{.*\})\s*$/m);
814
+ if (!match)
815
+ throw new Error(text.slice(0, 300) || "No MCP response body");
816
+ return JSON.parse(match[1]);
817
+ }
818
+
819
+ async function mcpRpc(id, method, params) {
820
+ const res = await fetch("/mcp", {
821
+ method: "POST",
822
+ headers: {
823
+ "Content-Type": "application/json",
824
+ Accept: "application/json, text/event-stream",
825
+ },
826
+ body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
827
+ });
828
+ const text = await res.text();
829
+ if (!res.ok) throw new Error(text || `HTTP ${res.status}`);
830
+ return parseSseJson(text);
831
+ }
832
+
833
+ async function initializeMcpForTest() {
834
+ await mcpRpc(1, "initialize", {
835
+ protocolVersion: "2025-03-26",
836
+ capabilities: {},
837
+ clientInfo: {
838
+ name: "living-ai-documentation-context",
839
+ version: "1.0.0",
840
+ },
841
+ });
842
+ }
843
+
844
+ function findMcpElement(selector, kind, name) {
845
+ return document.querySelector(
846
+ `${selector}[data-mcp-kind="${CSS.escape(kind)}"][${selector.includes("args") ? "data-mcp-args" : selector.includes("error") ? "data-mcp-error" : "data-mcp-result"}="${CSS.escape(name)}"]`,
847
+ );
848
+ }
849
+
850
+ function wrapCodeBlock(value, language = "") {
851
+ const text = String(value ?? "").replaceAll("```", "`\\`\\`");
852
+ return `\`\`\`${language}\n${text}\n\`\`\``;
853
+ }
854
+
855
+ function wrapJsonBlock(value) {
856
+ return wrapCodeBlock(JSON.stringify(value, null, 2), "json");
857
+ }
858
+
859
+ function formatMaybeJsonText(value) {
860
+ if (typeof value !== "string") return wrapJsonBlock(value);
861
+ try {
862
+ return wrapJsonBlock(JSON.parse(value));
863
+ } catch {
864
+ return value;
865
+ }
866
+ }
867
+
868
+ function formatMcpDisplayValue(value) {
869
+ if (typeof value === "string") return value;
870
+ if (!value || typeof value !== "object") return String(value ?? "");
871
+ if (value.error) return wrapJsonBlock(value.error);
872
+
873
+ const result = value.result;
874
+ if (!result || typeof result !== "object") return wrapJsonBlock(value);
875
+
876
+ if (Array.isArray(result.messages)) {
877
+ return result.messages
878
+ .map((message) => {
879
+ const role = message.role ? `[${message.role}]` : "";
880
+ const content =
881
+ message.content?.text ?? message.content ?? message;
882
+ return `${role}\n${formatMaybeJsonText(content)}`.trim();
883
+ })
884
+ .join("\n\n");
885
+ }
886
+
887
+ if (Array.isArray(result.content)) {
888
+ return result.content
889
+ .map((part) => {
890
+ if (
891
+ part &&
892
+ part.type === "text" &&
893
+ typeof part.text === "string"
894
+ ) {
895
+ return formatMaybeJsonText(part.text);
896
+ }
897
+ return wrapJsonBlock(part);
898
+ })
899
+ .join("\n\n");
900
+ }
901
+
902
+ return wrapJsonBlock(result);
903
+ }
904
+
905
+ function parseJsonIfPossible(value) {
906
+ if (typeof value !== "string") return value;
907
+ try {
908
+ return JSON.parse(value);
909
+ } catch {
910
+ return value;
911
+ }
912
+ }
913
+
914
+ function extractToolResultForMarkdown(envelope) {
915
+ const result = envelope?.result;
916
+ if (!result || typeof result !== "object") return envelope;
917
+ if (!Array.isArray(result.content)) return result;
918
+
919
+ const parts = result.content.map((part) => {
920
+ if (part && part.type === "text" && typeof part.text === "string") {
921
+ return parseJsonIfPossible(part.text);
922
+ }
923
+ return part;
924
+ });
925
+ return parts.length === 1 ? parts[0] : parts;
926
+ }
927
+
928
+ function buildToolRunMarkdown(item, name, args, envelope) {
929
+ const description =
930
+ item && typeof item === "object" && item.description
931
+ ? String(item.description)
932
+ : window.t("context.empty");
933
+ const inputSchema =
934
+ item && typeof item === "object" && item.inputSchema
935
+ ? item.inputSchema
936
+ : {};
937
+ const request = {
938
+ jsonrpc: "2.0",
939
+ method: "tools/call",
940
+ params: {
941
+ name,
942
+ arguments: args,
943
+ },
944
+ };
945
+ const result = extractToolResultForMarkdown(envelope);
946
+ const resultIsJson = typeof result !== "string";
947
+
948
+ return [
949
+ `# MCP tool: \`${name}\``,
950
+ "",
951
+ "## Description",
952
+ "",
953
+ description,
954
+ "",
955
+ "## Schéma d'entrée",
956
+ "",
957
+ wrapJsonBlock(inputSchema),
958
+ "",
959
+ "## Requête effectuée",
960
+ "",
961
+ wrapJsonBlock(request),
962
+ "",
963
+ "## Résultat",
964
+ "",
965
+ resultIsJson ? wrapJsonBlock(result) : String(result),
966
+ "",
967
+ ].join("\n");
968
+ }
969
+
970
+ function stringifyMcpErrorDetail(value) {
971
+ if (typeof value === "string") return value;
972
+ if (!value || typeof value !== "object") return String(value ?? "");
973
+ if (value.error) return JSON.stringify(value.error, null, 2);
974
+
975
+ const result = value.result;
976
+ if (result?.isError) {
977
+ const parts = Array.isArray(result.content)
978
+ ? result.content
979
+ .map((part) => {
980
+ if (
981
+ part &&
982
+ part.type === "text" &&
983
+ typeof part.text === "string"
984
+ )
985
+ return part.text;
986
+ return JSON.stringify(part, null, 2);
987
+ })
988
+ .filter(Boolean)
989
+ : [];
990
+ return parts.join("\n\n") || JSON.stringify(result, null, 2);
991
+ }
992
+
993
+ return JSON.stringify(value, null, 2);
994
+ }
995
+
996
+ function setMcpRunning(kind, name) {
997
+ const container = findMcpElement("[data-mcp-result]", kind, name);
998
+ const error = findMcpElement("[data-mcp-error]", kind, name);
999
+ if (error) error.textContent = "";
1000
+ if (!container) return;
1001
+ container.classList.remove("hidden");
1002
+ container.innerHTML = `<span class="text-gray-500 dark:text-gray-400">${escapeHtml(window.t("context.mcp_running"))}</span>`;
1003
+ }
1004
+
1005
+ function setMcpResultLink(kind, name, docPath) {
1006
+ const container = findMcpElement("[data-mcp-result]", kind, name);
1007
+ const error = findMcpElement("[data-mcp-error]", kind, name);
1008
+ if (error) error.textContent = "";
1009
+ if (!container) return;
1010
+ const href = documentHref(docPath);
1011
+ container.classList.remove("hidden");
1012
+ container.innerHTML = `
1013
+ <div class="text-gray-500 dark:text-gray-400 mb-1">${escapeHtml(window.t("context.mcp_saved_to"))}</div>
1014
+ <a href="${escapeHtml(href)}" target="_blank" rel="noopener" class="inline-flex items-center gap-1.5 rounded-md bg-green-50 px-3 py-1.5 font-mono text-xs font-semibold text-green-700 hover:bg-green-100 dark:bg-green-950 dark:text-green-300 dark:hover:bg-green-900">
1015
+ <span>📄</span><span>${escapeHtml(docPath)}</span>
1016
+ </a>
1017
+ `;
1018
+ }
1019
+
1020
+ function setMcpResultError(kind, name, message, detail) {
1021
+ const container = findMcpElement("[data-mcp-result]", kind, name);
1022
+ const error = findMcpElement("[data-mcp-error]", kind, name);
1023
+ const detailText = String(detail || "").trim();
1024
+ if (container) {
1025
+ if (detailText) {
1026
+ container.classList.remove("hidden");
1027
+ container.innerHTML = `
1028
+ <pre class="max-h-56 overflow-auto whitespace-pre-wrap break-words rounded-md border border-red-200 bg-red-50 p-2 font-mono text-xs leading-5 text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-200"><code>${escapeHtml(detailText)}</code></pre>
1029
+ `;
1030
+ } else {
1031
+ container.classList.add("hidden");
1032
+ container.innerHTML = "";
1033
+ }
1034
+ }
1035
+ if (error) error.textContent = message || window.t("context.mcp_error");
1036
+ }
1037
+
1038
+ async function saveMcpResult(name, content, kind) {
1039
+ const res = await fetch("/api/context/mcp-result", {
1040
+ method: "POST",
1041
+ headers: { "Content-Type": "application/json" },
1042
+ body: JSON.stringify({ name, content, kind }),
1043
+ });
1044
+ if (!res.ok)
1045
+ throw new Error((await res.text()) || `HTTP ${res.status}`);
1046
+ return res.json();
1047
+ }
1048
+
1049
+ async function runAllMcpItems(kind) {
1050
+ const container = kind === "tool" ? els.mcpToolList : els.mcpPromptList;
1051
+ const buttons = Array.from(
1052
+ container.querySelectorAll("[data-mcp-run]"),
1053
+ );
1054
+ if (!buttons.length) return;
1055
+
1056
+ els.btnRunAllTools.disabled = true;
1057
+ els.btnGetAllPrompts.disabled = true;
1058
+ const total = buttons.length;
1059
+ try {
1060
+ for (let i = 0; i < buttons.length; i++) {
1061
+ const name = buttons[i].dataset.mcpRun;
1062
+ buttons[i].closest("details")?.setAttribute("open", "");
1063
+ setStatus(
1064
+ formatMessage("context.mcp_running_all", {
1065
+ current: i + 1,
1066
+ total,
1067
+ }),
1068
+ );
1069
+ await runMcpItem(kind, name);
1070
+ }
1071
+ setStatus(formatMessage("context.mcp_done_all", { total }));
1072
+ } finally {
1073
+ els.btnRunAllTools.disabled = false;
1074
+ els.btnGetAllPrompts.disabled = false;
1075
+ }
1076
+ }
1077
+
1078
+ async function runMcpItem(kind, name) {
1079
+ const textarea = findMcpElement("[data-mcp-args]", kind, name);
1080
+ const error = findMcpElement("[data-mcp-error]", kind, name);
1081
+ if (error) error.textContent = "";
1082
+ let args = {};
1083
+ try {
1084
+ args =
1085
+ textarea && textarea.value.trim() ? JSON.parse(textarea.value) : {};
1086
+ } catch {
1087
+ if (error) error.textContent = window.t("context.mcp_invalid_json");
1088
+ return;
1089
+ }
1090
+
1091
+ setMcpRunning(kind, name);
1092
+ try {
1093
+ await initializeMcpForTest();
1094
+ const envelope =
1095
+ kind === "tool"
1096
+ ? await mcpRpc(2, "tools/call", { name, arguments: args })
1097
+ : await mcpRpc(2, "prompts/get", { name, arguments: args });
1098
+
1099
+ if (envelope.error || envelope.result?.isError) {
1100
+ setMcpResultError(
1101
+ kind,
1102
+ name,
1103
+ window.t("context.mcp_error"),
1104
+ stringifyMcpErrorDetail(envelope),
1105
+ );
1106
+ return;
1107
+ }
1108
+
1109
+ const item = mcpItemsByKind[kind]?.get(name);
1110
+ const markdown =
1111
+ kind === "tool"
1112
+ ? buildToolRunMarkdown(item, name, args, envelope)
1113
+ : formatMcpDisplayValue(envelope);
1114
+ const { path: docPath } = await saveMcpResult(name, markdown, kind);
1115
+ setMcpResultLink(kind, name, docPath);
1116
+ } catch (err) {
1117
+ setMcpResultError(kind, name, err.message || String(err));
1118
+ }
1119
+ }
1120
+
1121
+ function renderMcpExplorer(data) {
1122
+ const tools = Array.isArray(data.tools) ? data.tools : [];
1123
+ const prompts = Array.isArray(data.prompts) ? data.prompts : [];
1124
+ els.mcpStatus.textContent = window.t("context.mcp_available");
1125
+ els.mcpStatus.className =
1126
+ "mt-1 font-semibold text-green-600 dark:text-green-400";
1127
+ els.mcpTransport.textContent = data.transport || "-";
1128
+ els.mcpEndpoint.textContent = data.endpoint || "POST /mcp";
1129
+ els.mcpToolCount.textContent = String(tools.length);
1130
+ els.mcpPromptCount.textContent = String(prompts.length);
1131
+ renderMcpItems(els.mcpToolList, tools, "context.mcp_no_tools", "tool");
1132
+ renderMcpItems(
1133
+ els.mcpPromptList,
1134
+ prompts,
1135
+ "context.mcp_no_prompts",
1136
+ "prompt",
1137
+ );
1138
+ }
1139
+
1140
+ function renderMcpUnavailable() {
1141
+ els.mcpStatus.textContent = window.t("context.mcp_unavailable");
1142
+ els.mcpStatus.className =
1143
+ "mt-1 font-semibold text-red-600 dark:text-red-400";
1144
+ els.mcpTransport.textContent = "-";
1145
+ els.mcpEndpoint.textContent = "/mcp";
1146
+ els.mcpToolCount.textContent = "0";
1147
+ els.mcpPromptCount.textContent = "0";
1148
+ renderMcpItems(els.mcpToolList, [], "context.mcp_no_tools", "tool");
1149
+ renderMcpItems(
1150
+ els.mcpPromptList,
1151
+ [],
1152
+ "context.mcp_no_prompts",
1153
+ "prompt",
1154
+ );
1155
+ }
1156
+
1157
+ async function loadMcpExplorer() {
1158
+ try {
1159
+ const res = await fetch("/mcp");
1160
+ if (!res.ok) throw new Error(await res.text());
1161
+ renderMcpExplorer(await res.json());
1162
+ } catch {
1163
+ renderMcpUnavailable();
1164
+ }
1165
+ }
1166
+
1167
+ function renderOrientation(data) {
1168
+ orientation = data;
1169
+ renderInstructionFiles(data.instructions || []);
1170
+ renderRules(data.rules || []);
1171
+ els.rulesFolder.textContent = data.rulesFolder || "";
1172
+ }
1173
+
1174
+ async function loadOrientation() {
1175
+ setStatus(window.t("context.loading"));
1176
+ const res = await fetch("/api/context/orientation");
1177
+ if (!res.ok) throw new Error(await res.text());
1178
+ renderOrientation(await res.json());
1179
+ setStatus("");
1180
+ }
1181
+
1182
+ async function addRule() {
1183
+ const payload = {
1184
+ title: els.ruleTitle.value.trim(),
1185
+ severity: els.ruleSeverity.value,
1186
+ description: els.ruleDescription.value.trim(),
1187
+ tags: splitList(els.ruleTags.value),
1188
+ appliesTo: splitList(els.ruleAppliesTo.value),
1189
+ body: els.ruleBody.value.trim(),
1190
+ };
1191
+ if (!payload.title) {
1192
+ setStatus(window.t("context.rule_title_required"));
1193
+ return;
1194
+ }
1195
+ setStatus(window.t("context.saving_rule"));
1196
+ const res = await fetch("/api/context/rules", {
1197
+ method: "POST",
1198
+ headers: { "Content-Type": "application/json" },
1199
+ body: JSON.stringify(payload),
1200
+ });
1201
+ if (!res.ok) throw new Error(await res.text());
1202
+ els.ruleTitle.value = "";
1203
+ els.ruleDescription.value = "";
1204
+ els.ruleTags.value = "";
1205
+ els.ruleAppliesTo.value = "";
1206
+ els.ruleBody.value = "";
1207
+ await loadOrientation();
1208
+ }
1209
+
1210
+ async function loadContextI18n() {
1211
+ try {
1212
+ const cfgRes = await fetch("/api/config");
1213
+ const cfg = cfgRes.ok ? await cfgRes.json() : {};
1214
+ await window.initI18n(cfg.language || "en");
1215
+ } catch {
1216
+ await window.initI18n("en");
1217
+ }
1218
+ window.applyI18n();
1219
+ document.title = window.t("context.title");
1220
+ }
1221
+
1222
+ async function init() {
1223
+ await loadContextI18n();
1224
+ await loadOrientation();
1225
+ await loadMcpExplorer();
1226
+ }
1227
+
1228
+ els.btnAddRule.addEventListener("click", () => {
1229
+ addRule().catch((err) => setStatus(err.message));
1230
+ });
1231
+ els.btnToggleInstructionBrowser.addEventListener(
1232
+ "click",
1233
+ toggleInstructionBrowser,
1234
+ );
1235
+ els.btnRefreshMcp.addEventListener("click", () => {
1236
+ loadMcpExplorer().catch(() => renderMcpUnavailable());
1237
+ });
1238
+ els.btnRunAllTools.addEventListener("click", () => {
1239
+ runAllMcpItems("tool").catch((err) => setStatus(err.message));
1240
+ });
1241
+ els.btnGetAllPrompts.addEventListener("click", () => {
1242
+ runAllMcpItems("prompt").catch((err) => setStatus(err.message));
1243
+ });
1244
+ els.instructionBrowseUp.addEventListener("click", () => {
1245
+ if (instructionBrowseParent)
1246
+ loadInstructionBrowse(instructionBrowseParent);
1247
+ });
1248
+
1249
+ init().catch((err) => setStatus(err.message));
1250
+ </script>
1251
+ </body>
1252
+ </html>