claudeck 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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/cli.js +2 -0
  4. package/config/agent-chains.json +16 -0
  5. package/config/agent-dags.json +16 -0
  6. package/config/agents.json +46 -0
  7. package/config/bot-prompt.json +3 -0
  8. package/config/folders.json +66 -0
  9. package/config/prompts.json +92 -0
  10. package/config/repos.json +86 -0
  11. package/config/telegram-config.json +17 -0
  12. package/config/workflows.json +90 -0
  13. package/db.js +1198 -0
  14. package/package.json +55 -0
  15. package/plugins/claude-editor/client.css +171 -0
  16. package/plugins/claude-editor/client.js +183 -0
  17. package/plugins/event-stream/client.css +207 -0
  18. package/plugins/event-stream/client.js +271 -0
  19. package/plugins/linear/client.css +345 -0
  20. package/plugins/linear/client.js +380 -0
  21. package/plugins/linear/config.json +5 -0
  22. package/plugins/linear/server.js +312 -0
  23. package/plugins/repos/client.css +549 -0
  24. package/plugins/repos/client.js +663 -0
  25. package/plugins/repos/server.js +232 -0
  26. package/plugins/sudoku/client.css +196 -0
  27. package/plugins/sudoku/client.js +329 -0
  28. package/plugins/tasks/client.css +414 -0
  29. package/plugins/tasks/client.js +394 -0
  30. package/plugins/tasks/server.js +116 -0
  31. package/plugins/tic-tac-toe/client.css +167 -0
  32. package/plugins/tic-tac-toe/client.js +241 -0
  33. package/public/css/core/components.css +232 -0
  34. package/public/css/core/layout.css +330 -0
  35. package/public/css/core/print.css +18 -0
  36. package/public/css/core/reset.css +36 -0
  37. package/public/css/core/responsive.css +378 -0
  38. package/public/css/core/theme.css +116 -0
  39. package/public/css/core/variables.css +93 -0
  40. package/public/css/features/agent-monitor.css +297 -0
  41. package/public/css/features/agent-sidebar.css +525 -0
  42. package/public/css/features/agents.css +996 -0
  43. package/public/css/features/analytics.css +181 -0
  44. package/public/css/features/background-sessions.css +321 -0
  45. package/public/css/features/cost-dashboard.css +168 -0
  46. package/public/css/features/home.css +313 -0
  47. package/public/css/features/retro-terminal.css +88 -0
  48. package/public/css/features/telegram.css +127 -0
  49. package/public/css/features/tour.css +148 -0
  50. package/public/css/features/voice-input.css +60 -0
  51. package/public/css/features/welcome.css +241 -0
  52. package/public/css/panels/assistant-bot.css +442 -0
  53. package/public/css/panels/dev-docs.css +292 -0
  54. package/public/css/panels/file-explorer.css +322 -0
  55. package/public/css/panels/git-panel.css +221 -0
  56. package/public/css/panels/mcp-manager.css +199 -0
  57. package/public/css/panels/tips-feed.css +353 -0
  58. package/public/css/ui/commands.css +273 -0
  59. package/public/css/ui/context-gauge.css +76 -0
  60. package/public/css/ui/file-picker.css +69 -0
  61. package/public/css/ui/image-attachments.css +106 -0
  62. package/public/css/ui/messages.css +884 -0
  63. package/public/css/ui/modals.css +122 -0
  64. package/public/css/ui/parallel.css +217 -0
  65. package/public/css/ui/permissions.css +110 -0
  66. package/public/css/ui/right-panel.css +481 -0
  67. package/public/css/ui/sessions.css +689 -0
  68. package/public/css/ui/status-bar.css +425 -0
  69. package/public/css/ui/toolbox.css +206 -0
  70. package/public/data/tips.json +218 -0
  71. package/public/icons/favicon.png +0 -0
  72. package/public/icons/icon-192.png +0 -0
  73. package/public/icons/icon-512.png +0 -0
  74. package/public/icons/whaly.png +0 -0
  75. package/public/index.html +1140 -0
  76. package/public/js/core/api.js +591 -0
  77. package/public/js/core/constants.js +3 -0
  78. package/public/js/core/dom.js +270 -0
  79. package/public/js/core/events.js +10 -0
  80. package/public/js/core/plugin-loader.js +153 -0
  81. package/public/js/core/store.js +39 -0
  82. package/public/js/core/utils.js +25 -0
  83. package/public/js/core/ws.js +64 -0
  84. package/public/js/features/agent-monitor.js +222 -0
  85. package/public/js/features/agents.js +1209 -0
  86. package/public/js/features/analytics.js +397 -0
  87. package/public/js/features/attachments.js +251 -0
  88. package/public/js/features/background-sessions.js +475 -0
  89. package/public/js/features/chat.js +589 -0
  90. package/public/js/features/cost-dashboard.js +152 -0
  91. package/public/js/features/dag-editor.js +399 -0
  92. package/public/js/features/easter-egg.js +46 -0
  93. package/public/js/features/home.js +270 -0
  94. package/public/js/features/projects.js +372 -0
  95. package/public/js/features/prompts.js +228 -0
  96. package/public/js/features/sessions.js +332 -0
  97. package/public/js/features/telegram.js +131 -0
  98. package/public/js/features/tour.js +210 -0
  99. package/public/js/features/voice-input.js +185 -0
  100. package/public/js/features/welcome.js +43 -0
  101. package/public/js/features/workflows.js +277 -0
  102. package/public/js/main.js +51 -0
  103. package/public/js/panels/assistant-bot.js +445 -0
  104. package/public/js/panels/dev-docs.js +380 -0
  105. package/public/js/panels/file-explorer.js +486 -0
  106. package/public/js/panels/git-panel.js +285 -0
  107. package/public/js/panels/mcp-manager.js +311 -0
  108. package/public/js/panels/tips-feed.js +303 -0
  109. package/public/js/ui/commands.js +114 -0
  110. package/public/js/ui/context-gauge.js +100 -0
  111. package/public/js/ui/diff.js +124 -0
  112. package/public/js/ui/disabled-tools.js +36 -0
  113. package/public/js/ui/export.js +74 -0
  114. package/public/js/ui/formatting.js +206 -0
  115. package/public/js/ui/header-dropdowns.js +72 -0
  116. package/public/js/ui/input-meta.js +71 -0
  117. package/public/js/ui/max-turns.js +21 -0
  118. package/public/js/ui/messages.js +387 -0
  119. package/public/js/ui/model-selector.js +20 -0
  120. package/public/js/ui/notifications.js +232 -0
  121. package/public/js/ui/parallel.js +176 -0
  122. package/public/js/ui/permissions.js +168 -0
  123. package/public/js/ui/right-panel.js +173 -0
  124. package/public/js/ui/shortcuts.js +143 -0
  125. package/public/js/ui/sidebar-toggle.js +29 -0
  126. package/public/js/ui/status-bar.js +172 -0
  127. package/public/js/ui/tab-sdk.js +623 -0
  128. package/public/js/ui/theme.js +38 -0
  129. package/public/manifest.json +13 -0
  130. package/public/offline.html +190 -0
  131. package/public/style.css +42 -0
  132. package/public/sw.js +91 -0
  133. package/server/agent-loop.js +385 -0
  134. package/server/dag-executor.js +265 -0
  135. package/server/orchestrator.js +514 -0
  136. package/server/paths.js +61 -0
  137. package/server/plugin-mount.js +56 -0
  138. package/server/push-sender.js +31 -0
  139. package/server/routes/agents.js +294 -0
  140. package/server/routes/bot.js +45 -0
  141. package/server/routes/exec.js +35 -0
  142. package/server/routes/files.js +218 -0
  143. package/server/routes/mcp.js +82 -0
  144. package/server/routes/messages.js +36 -0
  145. package/server/routes/notifications.js +37 -0
  146. package/server/routes/projects.js +207 -0
  147. package/server/routes/prompts.js +53 -0
  148. package/server/routes/sessions.js +103 -0
  149. package/server/routes/stats.js +143 -0
  150. package/server/routes/telegram.js +71 -0
  151. package/server/routes/tips.js +135 -0
  152. package/server/routes/workflows.js +81 -0
  153. package/server/summarizer.js +55 -0
  154. package/server/telegram-poller.js +205 -0
  155. package/server/telegram-sender.js +304 -0
  156. package/server/ws-handler.js +926 -0
  157. package/server.js +179 -0
@@ -0,0 +1,285 @@
1
+ // Git Panel — status, staging, commit, branch, log
2
+ import { $ } from "../core/dom.js";
3
+ import { on } from "../core/events.js";
4
+ import { on as onState } from "../core/store.js";
5
+ import { execCommand } from "../core/api.js";
6
+
7
+ let currentCwd = null;
8
+
9
+ function getCwd() {
10
+ return $.projectSelect.value || null;
11
+ }
12
+
13
+ function escapeHtml(str) {
14
+ const div = document.createElement("div");
15
+ div.textContent = str;
16
+ return div.innerHTML;
17
+ }
18
+
19
+ // ── Branch ──────────────────────────────────────────────
20
+
21
+ async function loadBranches() {
22
+ const cwd = getCwd();
23
+ if (!cwd) return;
24
+
25
+ try {
26
+ const result = await execCommand("git branch --no-color", cwd);
27
+ if (result.error) return;
28
+
29
+ const lines = result.stdout.split("\n").filter(Boolean);
30
+ $.gitBranchSelect.innerHTML = "";
31
+
32
+ for (const line of lines) {
33
+ const isCurrent = line.startsWith("*");
34
+ const name = line.replace(/^\*?\s*/, "").trim();
35
+ if (!name) continue;
36
+ const opt = document.createElement("option");
37
+ opt.value = name;
38
+ opt.textContent = name;
39
+ if (isCurrent) opt.selected = true;
40
+ $.gitBranchSelect.appendChild(opt);
41
+ }
42
+ } catch {}
43
+ }
44
+
45
+ async function switchBranch(branch) {
46
+ const cwd = getCwd();
47
+ if (!cwd || !branch) return;
48
+
49
+ try {
50
+ const result = await execCommand(`git checkout "${branch}"`, cwd);
51
+ if (result.error && result.stderr) {
52
+ console.error("Branch switch error:", result.stderr);
53
+ }
54
+ await refreshAll();
55
+ } catch {}
56
+ }
57
+
58
+ // ── Status ──────────────────────────────────────────────
59
+
60
+ function parseStatusCode(x, y) {
61
+ if (x === "?" && y === "?") return { group: "untracked", badge: "?", cls: "untracked" };
62
+ if (x === "A") return { group: "staged", badge: "A", cls: "added" };
63
+ if (x === "M") return { group: "staged", badge: "M", cls: "modified" };
64
+ if (x === "D") return { group: "staged", badge: "D", cls: "deleted" };
65
+ if (x === "R") return { group: "staged", badge: "R", cls: "renamed" };
66
+ if (y === "M") return { group: "changes", badge: "M", cls: "modified" };
67
+ if (y === "D") return { group: "changes", badge: "D", cls: "deleted" };
68
+ return { group: "changes", badge: x || y, cls: "modified" };
69
+ }
70
+
71
+ async function loadStatus() {
72
+ const cwd = getCwd();
73
+ if (!cwd) {
74
+ $.gitStatusList.innerHTML = `
75
+ <div class="git-empty">
76
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
77
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
78
+ </svg>
79
+ <span>Select a project to view git status</span>
80
+ </div>`;
81
+ return;
82
+ }
83
+
84
+ try {
85
+ const result = await execCommand("git status --porcelain=v1", cwd);
86
+ if (result.error) {
87
+ $.gitStatusList.innerHTML = `
88
+ <div class="git-empty">
89
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
90
+ <circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>
91
+ </svg>
92
+ <span>Not a git repository</span>
93
+ </div>`;
94
+ return;
95
+ }
96
+
97
+ const lines = result.stdout.split("\n").filter(Boolean);
98
+ const groups = { staged: [], changes: [], untracked: [] };
99
+
100
+ for (const line of lines) {
101
+ const x = line[0];
102
+ const y = line[1];
103
+ const file = line.slice(3);
104
+ const info = parseStatusCode(x, y);
105
+ groups[info.group].push({ file, badge: info.badge, cls: info.cls });
106
+
107
+ // Files with both staged and unstaged changes
108
+ if (x !== " " && x !== "?" && y !== " " && y !== "?") {
109
+ groups.changes.push({ file, badge: y === "M" ? "M" : y, cls: y === "D" ? "deleted" : "modified" });
110
+ }
111
+ }
112
+
113
+ $.gitStatusList.innerHTML = "";
114
+
115
+ if (groups.staged.length === 0 && groups.changes.length === 0 && groups.untracked.length === 0) {
116
+ $.gitStatusList.innerHTML = `
117
+ <div class="git-empty">
118
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
119
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>
120
+ </svg>
121
+ <span>Working tree clean</span>
122
+ </div>`;
123
+ return;
124
+ }
125
+
126
+ renderGroup("Staged Changes", groups.staged, "unstage");
127
+ renderGroup("Changes", groups.changes, "stage");
128
+ renderGroup("Untracked", groups.untracked, "stage");
129
+ } catch {
130
+ $.gitStatusList.innerHTML = `<div class="git-empty">Failed to load status</div>`;
131
+ }
132
+ }
133
+
134
+ function renderGroup(title, files, action) {
135
+ if (files.length === 0) return;
136
+
137
+ const group = document.createElement("div");
138
+ group.className = "git-status-group";
139
+ group.innerHTML = `<div class="git-status-group-title">${title} (${files.length})</div>`;
140
+
141
+ for (const f of files) {
142
+ const row = document.createElement("div");
143
+ row.className = "git-status-file";
144
+ row.innerHTML = `
145
+ <span class="git-status-badge ${f.cls}">${f.badge}</span>
146
+ <span class="git-status-name" title="${escapeHtml(f.file)}">${escapeHtml(f.file)}</span>
147
+ <button class="git-status-action" title="${action === "stage" ? "Stage" : "Unstage"}">${action === "stage" ? "+" : "\u2212"}</button>
148
+ `;
149
+
150
+ row.querySelector(".git-status-action").addEventListener("click", async () => {
151
+ const cwd = getCwd();
152
+ if (!cwd) return;
153
+ const cmd = action === "stage"
154
+ ? `git add "${f.file}"`
155
+ : `git reset HEAD "${f.file}"`;
156
+ await execCommand(cmd, cwd);
157
+ await loadStatus();
158
+ });
159
+
160
+ group.appendChild(row);
161
+ }
162
+
163
+ $.gitStatusList.appendChild(group);
164
+ }
165
+
166
+ // ── Commit ──────────────────────────────────────────────
167
+
168
+ async function handleCommit() {
169
+ const cwd = getCwd();
170
+ const msg = $.gitCommitMsg.value.trim();
171
+ if (!cwd || !msg) return;
172
+
173
+ $.gitCommitBtn.disabled = true;
174
+ $.gitCommitBtn.textContent = "Committing...";
175
+
176
+ // Remove old error
177
+ const oldErr = $.gitCommitBtn.parentElement.querySelector(".git-commit-error");
178
+ if (oldErr) oldErr.remove();
179
+
180
+ try {
181
+ const escaped = msg.replace(/"/g, '\\"');
182
+ const result = await execCommand(`git commit -m "${escaped}"`, cwd);
183
+ if (result.error || (result.stderr && result.stderr.includes("nothing to commit"))) {
184
+ showCommitError(result.stderr || result.stdout || "Nothing to commit");
185
+ } else {
186
+ $.gitCommitMsg.value = "";
187
+ await refreshAll();
188
+ }
189
+ } catch {
190
+ showCommitError("Commit failed");
191
+ } finally {
192
+ $.gitCommitBtn.disabled = false;
193
+ $.gitCommitBtn.textContent = "Commit";
194
+ }
195
+ }
196
+
197
+ function showCommitError(msg) {
198
+ const oldErr = $.gitCommitBtn.parentElement.querySelector(".git-commit-error");
199
+ if (oldErr) oldErr.remove();
200
+
201
+ const errEl = document.createElement("div");
202
+ errEl.className = "git-commit-error";
203
+ errEl.textContent = msg;
204
+ $.gitCommitBtn.parentElement.appendChild(errEl);
205
+ }
206
+
207
+ // ── Log ─────────────────────────────────────────────────
208
+
209
+ async function loadLog() {
210
+ const cwd = getCwd();
211
+ if (!cwd) return;
212
+
213
+ try {
214
+ const result = await execCommand('git log --oneline --no-color -10 --format="%h|%s|%ar"', cwd);
215
+ if (result.error) {
216
+ $.gitLogList.innerHTML = "";
217
+ return;
218
+ }
219
+
220
+ const lines = result.stdout.split("\n").filter(Boolean);
221
+ $.gitLogList.innerHTML = "";
222
+
223
+ for (const line of lines) {
224
+ const [hash, subject, time] = line.split("|");
225
+ if (!hash) continue;
226
+ const item = document.createElement("div");
227
+ item.className = "git-log-item";
228
+ item.innerHTML = `
229
+ <span class="git-log-hash">${escapeHtml(hash)}</span>
230
+ <span class="git-log-subject">${escapeHtml(subject || "")}</span>
231
+ <span class="git-log-time">${escapeHtml(time || "")}</span>
232
+ `;
233
+ $.gitLogList.appendChild(item);
234
+ }
235
+ } catch {}
236
+ }
237
+
238
+ // ── Refresh ─────────────────────────────────────────────
239
+
240
+ async function refreshAll() {
241
+ $.gitRefreshBtn.classList.add("spinning");
242
+ try {
243
+ await Promise.all([loadBranches(), loadStatus(), loadLog()]);
244
+ } finally {
245
+ $.gitRefreshBtn.classList.remove("spinning");
246
+ }
247
+ }
248
+
249
+ function initGitPanel() {
250
+ $.gitRefreshBtn.addEventListener("click", () => refreshAll());
251
+ $.gitCommitBtn.addEventListener("click", () => handleCommit());
252
+ $.gitBranchSelect.addEventListener("change", (e) => switchBranch(e.target.value));
253
+
254
+ // Load when Git tab opens
255
+ on("rightPanel:opened", (tab) => {
256
+ if (tab === "git") refreshAll();
257
+ });
258
+ on("rightPanel:tabChanged", (tab) => {
259
+ if (tab === "git") refreshAll();
260
+ });
261
+
262
+ // Reset and reload on project switch
263
+ $.projectSelect.addEventListener("change", () => {
264
+ currentCwd = null;
265
+ $.gitStatusList.innerHTML = "";
266
+ $.gitLogList.innerHTML = "";
267
+ $.gitBranchSelect.innerHTML = "";
268
+ if (isGitTabActive()) refreshAll();
269
+ });
270
+
271
+ // Load when projects data arrives (covers initial page load)
272
+ // setTimeout(0) defers until after loadProjects() finishes populating the select
273
+ onState("projectsData", () => {
274
+ setTimeout(() => {
275
+ if (isGitTabActive()) refreshAll();
276
+ }, 0);
277
+ });
278
+ }
279
+
280
+ function isGitTabActive() {
281
+ const pane = $.rightPanel?.querySelector('.right-panel-pane[data-tab="git"]');
282
+ return pane && pane.classList.contains("active") && !$.rightPanel.classList.contains("hidden");
283
+ }
284
+
285
+ initGitPanel();
@@ -0,0 +1,311 @@
1
+ // MCP Server Management — CRUD modal for global + per-project mcpServers
2
+ import { $ } from "../core/dom.js";
3
+ import { fetchMcpServers, saveMcpServer, deleteMcpServer } from "../core/api.js";
4
+ import { registerCommand } from "../ui/commands.js";
5
+
6
+ let editingName = null;
7
+ let editingScope = "global";
8
+
9
+ function escapeHtml(str) {
10
+ const div = document.createElement("div");
11
+ div.textContent = str;
12
+ return div.innerHTML;
13
+ }
14
+
15
+ function getCurrentProject() {
16
+ return $.projectSelect?.value || null;
17
+ }
18
+
19
+ // ── Modal ───────────────────────────────────────────────
20
+
21
+ function openModal() {
22
+ $.mcpModal.classList.remove("hidden");
23
+ hideForm();
24
+ loadServers();
25
+ }
26
+
27
+ function closeModal() {
28
+ $.mcpModal.classList.add("hidden");
29
+ hideForm();
30
+ }
31
+
32
+ // ── Server List ─────────────────────────────────────────
33
+
34
+ function renderSectionHeader(label, count, icon) {
35
+ const el = document.createElement("div");
36
+ el.className = "mcp-scope-header";
37
+ el.innerHTML = `<span>${icon} ${label}</span><span class="mcp-scope-count">${count}</span>`;
38
+ return el;
39
+ }
40
+
41
+ function renderEmpty(text) {
42
+ const el = document.createElement("div");
43
+ el.className = "mcp-empty mcp-empty-compact";
44
+ el.textContent = text;
45
+ return el;
46
+ }
47
+
48
+ async function loadServers() {
49
+ $.mcpServerList.innerHTML = `<div class="mcp-empty">Loading...</div>`;
50
+
51
+ try {
52
+ const projectPath = getCurrentProject();
53
+
54
+ // Fetch both scopes in parallel
55
+ const [globalData, projectData] = await Promise.all([
56
+ fetchMcpServers(),
57
+ projectPath ? fetchMcpServers(projectPath) : Promise.resolve({ servers: {} }),
58
+ ]);
59
+
60
+ const globalServers = globalData.servers || {};
61
+ const projectServers = projectData.servers || {};
62
+
63
+ $.mcpServerList.innerHTML = "";
64
+
65
+ // Project section (only when a project is selected)
66
+ if (projectPath) {
67
+ const projectNames = Object.keys(projectServers);
68
+ $.mcpServerList.appendChild(renderSectionHeader("Project", projectNames.length, "\u{1F4C1}"));
69
+ if (projectNames.length === 0) {
70
+ $.mcpServerList.appendChild(renderEmpty("No project-level MCP servers"));
71
+ } else {
72
+ for (const name of projectNames) {
73
+ $.mcpServerList.appendChild(renderCard(name, projectServers[name], "project"));
74
+ }
75
+ }
76
+ }
77
+
78
+ // Global section
79
+ const globalNames = Object.keys(globalServers);
80
+ $.mcpServerList.appendChild(renderSectionHeader("Global", globalNames.length, "\u{1F310}"));
81
+ if (globalNames.length === 0) {
82
+ $.mcpServerList.appendChild(renderEmpty("No global MCP servers"));
83
+ } else {
84
+ for (const name of globalNames) {
85
+ $.mcpServerList.appendChild(renderCard(name, globalServers[name], "global"));
86
+ }
87
+ }
88
+ } catch {
89
+ $.mcpServerList.innerHTML = `<div class="mcp-empty">Failed to load servers</div>`;
90
+ }
91
+ }
92
+
93
+ function getServerType(config) {
94
+ if (config.command) return "stdio";
95
+ if (config.url && config.url.includes("/sse")) return "sse";
96
+ if (config.url) return "http";
97
+ return config.type || "stdio";
98
+ }
99
+
100
+ function getServerDetail(config) {
101
+ if (config.command) {
102
+ const args = config.args ? " " + config.args.join(" ") : "";
103
+ return config.command + args;
104
+ }
105
+ if (config.url) return config.url;
106
+ return "";
107
+ }
108
+
109
+ function renderCard(name, config, scope) {
110
+ const type = getServerType(config);
111
+ const detail = getServerDetail(config);
112
+
113
+ const card = document.createElement("div");
114
+ card.className = "mcp-server-card";
115
+ card.innerHTML = `
116
+ <div class="mcp-server-info">
117
+ <div class="mcp-server-name">
118
+ ${escapeHtml(name)}
119
+ <span class="mcp-server-type">${type}</span>
120
+ </div>
121
+ <div class="mcp-server-detail" title="${escapeHtml(detail)}">${escapeHtml(detail)}</div>
122
+ </div>
123
+ <div class="mcp-server-actions">
124
+ <button class="edit" title="Edit">&#9998;</button>
125
+ <button class="delete" title="Delete">&times;</button>
126
+ </div>
127
+ `;
128
+
129
+ card.querySelector(".edit").addEventListener("click", () => showEditForm(name, config, scope));
130
+ card.querySelector(".delete").addEventListener("click", () => handleDelete(name, scope));
131
+
132
+ return card;
133
+ }
134
+
135
+ // ── Scope Selector ─────────────────────────────────────
136
+
137
+ function ensureScopeSelector() {
138
+ let sel = document.getElementById("mcp-scope");
139
+ if (sel) return sel;
140
+
141
+ // Create label + select, insert before the Type label
142
+ const label = document.createElement("label");
143
+ label.setAttribute("for", "mcp-scope");
144
+ label.textContent = "Scope";
145
+ label.id = "mcp-scope-label";
146
+
147
+ sel = document.createElement("select");
148
+ sel.id = "mcp-scope";
149
+
150
+ const typeLabel = $.mcpType.previousElementSibling;
151
+ $.mcpForm.insertBefore(sel, typeLabel);
152
+ $.mcpForm.insertBefore(label, sel);
153
+
154
+ sel.addEventListener("change", () => { editingScope = sel.value; });
155
+ return sel;
156
+ }
157
+
158
+ function updateScopeSelector(disabled) {
159
+ const sel = ensureScopeSelector();
160
+ const label = document.getElementById("mcp-scope-label");
161
+ const projectPath = getCurrentProject();
162
+
163
+ sel.innerHTML = `<option value="global">Global (~/.claude)</option>`;
164
+ if (projectPath) {
165
+ sel.innerHTML += `<option value="project">Project (.claude/)</option>`;
166
+ }
167
+ sel.value = editingScope;
168
+ sel.disabled = !!disabled;
169
+
170
+ // Hide scope selector entirely if no project selected
171
+ const hidden = !projectPath;
172
+ sel.style.display = hidden ? "none" : "";
173
+ label.style.display = hidden ? "none" : "";
174
+ }
175
+
176
+ // ── Form ────────────────────────────────────────────────
177
+
178
+ function showAddForm() {
179
+ editingName = null;
180
+ editingScope = "global";
181
+ $.mcpFormTitle.textContent = "Add Server";
182
+ $.mcpForm.reset();
183
+ $.mcpName.disabled = false;
184
+ updateTypeFields();
185
+ updateScopeSelector(false);
186
+ $.mcpFormContainer.classList.remove("hidden");
187
+ $.mcpAddBtn.classList.add("hidden");
188
+ $.mcpName.focus();
189
+ }
190
+
191
+ function showEditForm(name, config, scope) {
192
+ editingName = name;
193
+ editingScope = scope;
194
+ $.mcpFormTitle.textContent = "Edit Server";
195
+ $.mcpName.value = name;
196
+ $.mcpName.disabled = true;
197
+
198
+ const type = getServerType(config);
199
+ $.mcpType.value = type;
200
+ updateTypeFields();
201
+ updateScopeSelector(true); // can't change scope when editing
202
+
203
+ if (config.command) {
204
+ $.mcpCommand.value = config.command || "";
205
+ $.mcpArgs.value = (config.args || []).join("\n");
206
+ const envLines = Object.entries(config.env || {}).map(([k, v]) => `${k}=${v}`);
207
+ $.mcpEnv.value = envLines.join("\n");
208
+ }
209
+ if (config.url) {
210
+ $.mcpUrl.value = config.url;
211
+ }
212
+
213
+ $.mcpFormContainer.classList.remove("hidden");
214
+ $.mcpAddBtn.classList.add("hidden");
215
+ }
216
+
217
+ function hideForm() {
218
+ $.mcpFormContainer.classList.add("hidden");
219
+ $.mcpAddBtn.classList.remove("hidden");
220
+ editingName = null;
221
+ }
222
+
223
+ function updateTypeFields() {
224
+ const type = $.mcpType.value;
225
+ if (type === "stdio") {
226
+ $.mcpStdioFields.classList.remove("hidden");
227
+ $.mcpUrlFields.classList.add("hidden");
228
+ } else {
229
+ $.mcpStdioFields.classList.add("hidden");
230
+ $.mcpUrlFields.classList.remove("hidden");
231
+ }
232
+ }
233
+
234
+ async function handleSubmit(e) {
235
+ e.preventDefault();
236
+ const name = editingName || $.mcpName.value.trim();
237
+ if (!name) return;
238
+
239
+ const type = $.mcpType.value;
240
+ let config = {};
241
+
242
+ if (type === "stdio") {
243
+ config.command = $.mcpCommand.value.trim();
244
+ const argsText = $.mcpArgs.value.trim();
245
+ if (argsText) config.args = argsText.split("\n").map((s) => s.trim()).filter(Boolean);
246
+ const envText = $.mcpEnv.value.trim();
247
+ if (envText) {
248
+ config.env = {};
249
+ for (const line of envText.split("\n")) {
250
+ const eq = line.indexOf("=");
251
+ if (eq > 0) {
252
+ config.env[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
253
+ }
254
+ }
255
+ }
256
+ } else {
257
+ config.url = $.mcpUrl.value.trim();
258
+ config.type = type;
259
+ }
260
+
261
+ const projectPath = editingScope === "project" ? getCurrentProject() : undefined;
262
+
263
+ $.mcpFormSave.disabled = true;
264
+ $.mcpFormSave.textContent = "Saving...";
265
+
266
+ try {
267
+ await saveMcpServer(name, config, projectPath);
268
+ hideForm();
269
+ await loadServers();
270
+ } catch {
271
+ $.mcpFormSave.textContent = "Failed";
272
+ } finally {
273
+ $.mcpFormSave.disabled = false;
274
+ $.mcpFormSave.textContent = "Save";
275
+ }
276
+ }
277
+
278
+ async function handleDelete(name, scope) {
279
+ if (!confirm(`Delete MCP server "${name}"?`)) return;
280
+
281
+ const projectPath = scope === "project" ? getCurrentProject() : undefined;
282
+ try {
283
+ await deleteMcpServer(name, projectPath);
284
+ await loadServers();
285
+ } catch {}
286
+ }
287
+
288
+ // ── Init ────────────────────────────────────────────────
289
+
290
+ function initMcpManager() {
291
+ $.mcpToggleBtn.addEventListener("click", () => openModal());
292
+ $.mcpModalClose.addEventListener("click", () => closeModal());
293
+ $.mcpModal.addEventListener("click", (e) => {
294
+ if (e.target === $.mcpModal) closeModal();
295
+ });
296
+ $.mcpAddBtn.addEventListener("click", () => showAddForm());
297
+ $.mcpFormCancel.addEventListener("click", () => hideForm());
298
+ $.mcpType.addEventListener("change", () => updateTypeFields());
299
+ $.mcpForm.addEventListener("submit", handleSubmit);
300
+
301
+ // Register /mcp slash command
302
+ registerCommand("mcp", {
303
+ category: "app",
304
+ description: "Manage MCP servers",
305
+ execute() {
306
+ openModal();
307
+ },
308
+ });
309
+ }
310
+
311
+ initMcpManager();