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.
- package/LICENSE +661 -0
- package/README.fr.md +344 -0
- package/README.md +344 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +262 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/accuracy-gauge.js +70 -0
- package/dist/src/frontend/admin.html +1532 -0
- package/dist/src/frontend/annotations.js +585 -0
- package/dist/src/frontend/boot.js +101 -0
- package/dist/src/frontend/config.js +29 -0
- package/dist/src/frontend/confirm-modal.js +82 -0
- package/dist/src/frontend/context.html +1252 -0
- package/dist/src/frontend/dark-mode.js +20 -0
- package/dist/src/frontend/diagram/alignment.js +161 -0
- package/dist/src/frontend/diagram/clipboard.js +187 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/custom-shapes.js +104 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/drawio-export.js +649 -0
- package/dist/src/frontend/diagram/edge-panel.js +293 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/evidence.js +146 -0
- package/dist/src/frontend/diagram/grid.js +78 -0
- package/dist/src/frontend/diagram/groups.js +102 -0
- package/dist/src/frontend/diagram/history.js +157 -0
- package/dist/src/frontend/diagram/image-name-modal.js +48 -0
- package/dist/src/frontend/diagram/image-upload.js +36 -0
- package/dist/src/frontend/diagram/label-editor.js +115 -0
- package/dist/src/frontend/diagram/link-panel.js +144 -0
- package/dist/src/frontend/diagram/main.js +364 -0
- package/dist/src/frontend/diagram/network.js +2214 -0
- package/dist/src/frontend/diagram/node-panel.js +389 -0
- package/dist/src/frontend/diagram/node-rendering.js +964 -0
- package/dist/src/frontend/diagram/persistence.js +168 -0
- package/dist/src/frontend/diagram/ports.js +421 -0
- package/dist/src/frontend/diagram/selection-overlay.js +387 -0
- package/dist/src/frontend/diagram/state.js +43 -0
- package/dist/src/frontend/diagram/t.js +3 -0
- package/dist/src/frontend/diagram/toast.js +21 -0
- package/dist/src/frontend/diagram/unlock-hold.js +206 -0
- package/dist/src/frontend/diagram/zoom.js +20 -0
- package/dist/src/frontend/diagram-link-modal.js +137 -0
- package/dist/src/frontend/diagram.html +1494 -0
- package/dist/src/frontend/documents.js +479 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/file-attach.js +178 -0
- package/dist/src/frontend/files-modal.js +243 -0
- package/dist/src/frontend/i18n/en.json +624 -0
- package/dist/src/frontend/i18n/fr.json +624 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +126 -0
- package/dist/src/frontend/index.html +2806 -0
- package/dist/src/frontend/local-search.js +476 -0
- package/dist/src/frontend/metadata.js +318 -0
- package/dist/src/frontend/misc.js +92 -0
- package/dist/src/frontend/new-doc-modal.js +285 -0
- package/dist/src/frontend/new-folder-modal.js +169 -0
- package/dist/src/frontend/search.js +194 -0
- package/dist/src/frontend/shape-editor.html +685 -0
- package/dist/src/frontend/sidebar-helpers.js +96 -0
- package/dist/src/frontend/sidebar-resize.js +98 -0
- package/dist/src/frontend/sidebar.js +351 -0
- package/dist/src/frontend/snippet-detect.js +25 -0
- package/dist/src/frontend/snippet-table.js +85 -0
- package/dist/src/frontend/snippet-tree.js +94 -0
- package/dist/src/frontend/snippets.js +1146 -0
- package/dist/src/frontend/state.js +46 -0
- package/dist/src/frontend/utils.js +21 -0
- package/dist/src/frontend/validate.js +107 -0
- package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
- package/dist/src/frontend/wordcloud.js +693 -0
- package/dist/src/lib/config.d.ts +26 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +195 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/hash.d.ts +2 -0
- package/dist/src/lib/hash.d.ts.map +1 -0
- package/dist/src/lib/hash.js +18 -0
- package/dist/src/lib/hash.js.map +1 -0
- package/dist/src/lib/metadata.d.ts +31 -0
- package/dist/src/lib/metadata.d.ts.map +1 -0
- package/dist/src/lib/metadata.js +128 -0
- package/dist/src/lib/metadata.js.map +1 -0
- package/dist/src/lib/parser.d.ts +11 -0
- package/dist/src/lib/parser.d.ts.map +1 -0
- package/dist/src/lib/parser.js +111 -0
- package/dist/src/lib/parser.js.map +1 -0
- package/dist/src/lib/status.d.ts +9 -0
- package/dist/src/lib/status.d.ts.map +1 -0
- package/dist/src/lib/status.js +72 -0
- package/dist/src/lib/status.js.map +1 -0
- package/dist/src/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +2046 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +82 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +594 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +44 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +186 -0
- package/dist/src/mcp/tools/documents.js.map +1 -0
- package/dist/src/mcp/tools/git.d.ts +10 -0
- package/dist/src/mcp/tools/git.d.ts.map +1 -0
- package/dist/src/mcp/tools/git.js +217 -0
- package/dist/src/mcp/tools/git.js.map +1 -0
- package/dist/src/mcp/tools/metadata.d.ts +57 -0
- package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
- package/dist/src/mcp/tools/metadata.js +222 -0
- package/dist/src/mcp/tools/metadata.js.map +1 -0
- package/dist/src/mcp/tools/source.d.ts +29 -0
- package/dist/src/mcp/tools/source.d.ts.map +1 -0
- package/dist/src/mcp/tools/source.js +196 -0
- package/dist/src/mcp/tools/source.js.map +1 -0
- package/dist/src/routes/annotations.d.ts +3 -0
- package/dist/src/routes/annotations.d.ts.map +1 -0
- package/dist/src/routes/annotations.js +83 -0
- package/dist/src/routes/annotations.js.map +1 -0
- package/dist/src/routes/browse-source.d.ts +3 -0
- package/dist/src/routes/browse-source.d.ts.map +1 -0
- package/dist/src/routes/browse-source.js +79 -0
- package/dist/src/routes/browse-source.js.map +1 -0
- package/dist/src/routes/browse.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +91 -0
- package/dist/src/routes/browse.js.map +1 -0
- package/dist/src/routes/config.d.ts +3 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +145 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/context.d.ts +3 -0
- package/dist/src/routes/context.d.ts.map +1 -0
- package/dist/src/routes/context.js +287 -0
- package/dist/src/routes/context.js.map +1 -0
- package/dist/src/routes/diagrams.d.ts +3 -0
- package/dist/src/routes/diagrams.d.ts.map +1 -0
- package/dist/src/routes/diagrams.js +69 -0
- package/dist/src/routes/diagrams.js.map +1 -0
- package/dist/src/routes/documents.d.ts +11 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +450 -0
- package/dist/src/routes/documents.js.map +1 -0
- package/dist/src/routes/export.d.ts +3 -0
- package/dist/src/routes/export.d.ts.map +1 -0
- package/dist/src/routes/export.js +280 -0
- package/dist/src/routes/export.js.map +1 -0
- package/dist/src/routes/files.d.ts +3 -0
- package/dist/src/routes/files.d.ts.map +1 -0
- package/dist/src/routes/files.js +180 -0
- package/dist/src/routes/files.js.map +1 -0
- package/dist/src/routes/images.d.ts +3 -0
- package/dist/src/routes/images.d.ts.map +1 -0
- package/dist/src/routes/images.js +49 -0
- package/dist/src/routes/images.js.map +1 -0
- package/dist/src/routes/metadata.d.ts +3 -0
- package/dist/src/routes/metadata.d.ts.map +1 -0
- package/dist/src/routes/metadata.js +131 -0
- package/dist/src/routes/metadata.js.map +1 -0
- package/dist/src/routes/shape-libraries.d.ts +3 -0
- package/dist/src/routes/shape-libraries.d.ts.map +1 -0
- package/dist/src/routes/shape-libraries.js +118 -0
- package/dist/src/routes/shape-libraries.js.map +1 -0
- package/dist/src/routes/wordcloud.d.ts +3 -0
- package/dist/src/routes/wordcloud.d.ts.map +1 -0
- package/dist/src/routes/wordcloud.js +95 -0
- package/dist/src/routes/wordcloud.js.map +1 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +93 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starter-doc/.living-doc.json +52 -0
- package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
- package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc/WORKLOG/current-task.md +57 -0
- package/dist/starter-doc-fr/.living-doc.json +52 -0
- package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
- package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
- package/images/living_documentation.jpg +0 -0
- package/images/readme-extra-files.png +0 -0
- package/images/readme-filename-pattern.png +0 -0
- package/images/readme-intelligent-search-demo.jpg +0 -0
- package/images/readme-sidebar.png +0 -0
- 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
|
+
"&": "&",
|
|
434
|
+
"<": "<",
|
|
435
|
+
">": ">",
|
|
436
|
+
'"': """,
|
|
437
|
+
"'": "'",
|
|
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">📁</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">📄</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>
|