mcp-config-manager 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { listClientsApi, getClientConfigApi, deleteServerApi } from './api.js';
2
+ import { fetchClientTabs, fetchTabItems, fetchItem, createItem, updateItem, deleteItem, fetchItemFiles, fetchItemFile, updateItemFile } from './skillsApi.js';
2
3
  import { showServerModal, copyServer, copyToClipboard, exportServer, deleteServer, removeFromAll, updateBulkActions, selectAllServers, deselectAllServers, deleteSelected } from './modals.js';
3
4
  import { getServerLogo, getInitials } from './logoService.js';
4
5
 
@@ -6,6 +7,8 @@ let currentClient = null;
6
7
  let clients = [];
7
8
  let loadClientsCallback = null; // Callback to main.js to reload all clients
8
9
  let serverListListenerAttached = false; // Track if event listener is already attached
10
+ let currentClientTab = 'servers'; // Current tab within client view
11
+ let clientTabs = []; // Tabs available for current client
9
12
 
10
13
  export function initClientView(allClients, currentClientId, loadClientsFn) {
11
14
  clients = allClients;
@@ -64,6 +67,8 @@ export function renderClientList() {
64
67
 
65
68
  export async function selectClient(clientId) {
66
69
  currentClient = clientId;
70
+ currentClientTab = 'servers'; // Reset to servers tab
71
+
67
72
  // Update the global currentClient in main.js
68
73
  if (window.setCurrentClient) {
69
74
  window.setCurrentClient(clientId);
@@ -80,9 +85,441 @@ export async function selectClient(clientId) {
80
85
  document.getElementById('clientName').textContent = client.name;
81
86
  document.getElementById('configPath').textContent = client.configPath;
82
87
 
88
+ // Load tabs for this client
89
+ await loadClientTabs();
90
+
91
+ // Load servers by default
83
92
  await loadClientServers();
84
93
  }
85
94
 
95
+ async function loadClientTabs() {
96
+ try {
97
+ const result = await fetchClientTabs(currentClient);
98
+ clientTabs = result.tabs || [];
99
+ renderClientTabs();
100
+ } catch (error) {
101
+ console.error('Failed to load client tabs:', error);
102
+ clientTabs = [];
103
+ renderClientTabs();
104
+ }
105
+ }
106
+
107
+ function renderClientTabs() {
108
+ // Remove existing tabs container if present
109
+ const existingTabs = document.getElementById('clientContentTabs');
110
+ if (existingTabs) {
111
+ existingTabs.remove();
112
+ }
113
+
114
+ // Create tabs container
115
+ const tabsContainer = document.createElement('div');
116
+ tabsContainer.id = 'clientContentTabs';
117
+ tabsContainer.className = 'client-content-tabs';
118
+
119
+ // Always add Servers tab
120
+ const serversTab = document.createElement('button');
121
+ serversTab.className = `client-tab-btn ${currentClientTab === 'servers' ? 'active' : ''}`;
122
+ serversTab.dataset.tab = 'servers';
123
+ serversTab.innerHTML = '📡 Servers';
124
+ serversTab.addEventListener('click', () => switchClientTab('servers'));
125
+ tabsContainer.appendChild(serversTab);
126
+
127
+ // Add skill/prompt tabs based on client capabilities
128
+ clientTabs.forEach(tab => {
129
+ const tabBtn = document.createElement('button');
130
+ tabBtn.className = `client-tab-btn ${currentClientTab === tab.id ? 'active' : ''}`;
131
+ tabBtn.dataset.tab = tab.id;
132
+ tabBtn.innerHTML = `${tab.icon} ${tab.name}`;
133
+ tabBtn.title = tab.description;
134
+ tabBtn.addEventListener('click', () => switchClientTab(tab.id));
135
+ tabsContainer.appendChild(tabBtn);
136
+ });
137
+
138
+ // Insert tabs after client header
139
+ const clientHeader = document.querySelector('.client-header');
140
+ if (clientHeader) {
141
+ clientHeader.after(tabsContainer);
142
+ }
143
+
144
+ // Update action buttons visibility based on tab
145
+ updateActionButtons();
146
+ }
147
+
148
+ function updateActionButtons() {
149
+ const addServerBtn = document.getElementById('addServerBtn');
150
+ const addRemoteServerBtn = document.getElementById('addRemoteServerBtn');
151
+ const exportConfigBtn = document.getElementById('exportConfigBtn');
152
+ const importConfigBtn = document.getElementById('importConfigBtn');
153
+
154
+ const isServersTab = currentClientTab === 'servers';
155
+
156
+ if (addServerBtn) addServerBtn.style.display = isServersTab ? 'inline-block' : 'none';
157
+ if (addRemoteServerBtn) addRemoteServerBtn.style.display = isServersTab ? 'inline-block' : 'none';
158
+ if (exportConfigBtn) exportConfigBtn.style.display = isServersTab ? 'inline-block' : 'none';
159
+ if (importConfigBtn) importConfigBtn.style.display = isServersTab ? 'inline-block' : 'none';
160
+ }
161
+
162
+ async function switchClientTab(tabId) {
163
+ currentClientTab = tabId;
164
+
165
+ // Update tab button states
166
+ document.querySelectorAll('.client-tab-btn').forEach(btn => {
167
+ btn.classList.toggle('active', btn.dataset.tab === tabId);
168
+ });
169
+
170
+ updateActionButtons();
171
+
172
+ if (tabId === 'servers') {
173
+ await loadClientServers();
174
+ } else {
175
+ await loadTabContent(tabId);
176
+ }
177
+ }
178
+
179
+ async function loadTabContent(tabId) {
180
+ const serverList = document.getElementById('serverList');
181
+ const tab = clientTabs.find(t => t.id === tabId);
182
+
183
+ if (!tab) {
184
+ serverList.innerHTML = '<p class="no-servers">Tab not found</p>';
185
+ return;
186
+ }
187
+
188
+ serverList.innerHTML = '<p class="loading">Loading...</p>';
189
+
190
+ try {
191
+ const result = await fetchTabItems(currentClient, tabId);
192
+ const items = result.items || [];
193
+ renderTabItems(items, tab);
194
+ } catch (error) {
195
+ console.error('Failed to load tab content:', error);
196
+ serverList.innerHTML = `<p class="error">Failed to load ${tab.name}: ${error.message}</p>`;
197
+ }
198
+ }
199
+
200
+ function renderTabItems(items, tab) {
201
+ const serverList = document.getElementById('serverList');
202
+
203
+ // Add button for creating new items
204
+ const headerHtml = `
205
+ <div class="tab-content-header">
206
+ <button class="btn btn-primary" id="addItemBtn">+ Add ${tab.name.slice(0, -1)}</button>
207
+ </div>
208
+ `;
209
+
210
+ if (items.length === 0) {
211
+ serverList.innerHTML = headerHtml + `<p class="no-servers">No ${tab.name.toLowerCase()} configured</p>`;
212
+ attachTabItemEventListeners(tab);
213
+ return;
214
+ }
215
+
216
+ let html = headerHtml;
217
+
218
+ items.forEach(item => {
219
+ const typeBadge = item.type === 'complex'
220
+ ? '<span class="item-badge item-badge-complex">📁 Complex</span>'
221
+ : item.type === 'single'
222
+ ? '<span class="item-badge item-badge-single">📄 Single</span>'
223
+ : '<span class="item-badge item-badge-simple">⚡ Simple</span>';
224
+
225
+ html += `
226
+ <div class="server-card skill-card" data-item-name="${item.name}">
227
+ <div class="server-header">
228
+ <div class="server-info">
229
+ <span class="server-name">${item.metadata?.name || item.name}</span>
230
+ ${typeBadge}
231
+ </div>
232
+ <div class="server-actions">
233
+ <button class="btn btn-small btn-secondary edit-item-btn" data-item-name="${item.name}">Edit</button>
234
+ <button class="btn btn-small btn-secondary duplicate-item-btn" data-item-name="${item.name}">Duplicate</button>
235
+ <button class="btn btn-small btn-danger delete-item-btn" data-item-name="${item.name}">Delete</button>
236
+ </div>
237
+ </div>
238
+ <div class="server-details">
239
+ ${item.metadata?.description ? `<div class="detail-row">${item.metadata.description}</div>` : ''}
240
+ ${item.metadata?.model ? `<div class="detail-row"><strong>Model:</strong> ${item.metadata.model}</div>` : ''}
241
+ ${item.structure?.hasWorkflows ? '<span class="structure-badge">workflows/</span>' : ''}
242
+ ${item.structure?.hasReferences ? '<span class="structure-badge">references/</span>' : ''}
243
+ </div>
244
+ </div>
245
+ `;
246
+ });
247
+
248
+ serverList.innerHTML = html;
249
+ attachTabItemEventListeners(tab);
250
+ }
251
+
252
+ function attachTabItemEventListeners(tab) {
253
+ const addItemBtn = document.getElementById('addItemBtn');
254
+ if (addItemBtn) {
255
+ addItemBtn.addEventListener('click', () => showItemModal(null, tab));
256
+ }
257
+
258
+ document.querySelectorAll('.edit-item-btn').forEach(btn => {
259
+ btn.addEventListener('click', async (e) => {
260
+ const itemName = e.target.dataset.itemName;
261
+ try {
262
+ const item = await fetchItem(currentClient, tab.id, itemName);
263
+ showItemModal(item, tab);
264
+ } catch (error) {
265
+ alert('Failed to load item: ' + error.message);
266
+ }
267
+ });
268
+ });
269
+
270
+ document.querySelectorAll('.duplicate-item-btn').forEach(btn => {
271
+ btn.addEventListener('click', async (e) => {
272
+ const itemName = e.target.dataset.itemName;
273
+ const newName = prompt(`Enter name for the duplicate:`, `${itemName}-copy`);
274
+ if (newName && newName !== itemName) {
275
+ try {
276
+ await fetch(`/api/clients/${encodeURIComponent(currentClient)}/tabs/${encodeURIComponent(tab.id)}/items/${encodeURIComponent(itemName)}/copy`, {
277
+ method: 'POST',
278
+ headers: { 'Content-Type': 'application/json' },
279
+ body: JSON.stringify({ newName })
280
+ });
281
+ await loadTabContent(tab.id);
282
+ } catch (error) {
283
+ alert('Failed to duplicate item: ' + error.message);
284
+ }
285
+ }
286
+ });
287
+ });
288
+
289
+ document.querySelectorAll('.delete-item-btn').forEach(btn => {
290
+ btn.addEventListener('click', async (e) => {
291
+ const itemName = e.target.dataset.itemName;
292
+ if (confirm(`Delete "${itemName}"?`)) {
293
+ try {
294
+ await deleteItem(currentClient, tab.id, itemName);
295
+ await loadTabContent(tab.id);
296
+ } catch (error) {
297
+ alert('Failed to delete item: ' + error.message);
298
+ }
299
+ }
300
+ });
301
+ });
302
+ }
303
+
304
+ async function showItemModal(item, tab) {
305
+ const modal = document.getElementById('skill-modal');
306
+ const modalContent = document.getElementById('skill-modal-content');
307
+ const titleEl = document.getElementById('skill-modal-title');
308
+ const simpleView = document.getElementById('skill-simple-view');
309
+ const complexView = document.getElementById('skill-complex-view');
310
+
311
+ // Determine if complex (has directory structure)
312
+ const isComplex = item && item.type === 'complex';
313
+
314
+ // Update modal title
315
+ const itemTypeName = tab.name.slice(0, -1);
316
+ titleEl.textContent = item ? `Edit ${itemTypeName}` : `Add ${itemTypeName}`;
317
+
318
+ // Show modal first
319
+ modal.style.display = 'flex';
320
+
321
+ if (isComplex) {
322
+ await showComplexView(item, tab);
323
+ } else {
324
+ showSimpleView(item, tab);
325
+ }
326
+
327
+ // Simple view - metadata + content in one pane
328
+ function showSimpleView(item, tab) {
329
+ simpleView.style.display = 'block';
330
+ complexView.style.display = 'none';
331
+ modalContent.classList.remove('modal-large');
332
+ modalContent.classList.add('modal-medium');
333
+
334
+ const form = document.getElementById('skill-form-simple');
335
+ const nameInput = document.getElementById('skill-name');
336
+ const descInput = document.getElementById('skill-description');
337
+ const modelInput = document.getElementById('skill-model');
338
+ const contentArea = document.getElementById('skill-content');
339
+
340
+ // Populate form
341
+ if (item) {
342
+ nameInput.value = item.name;
343
+ nameInput.disabled = true;
344
+ descInput.value = item.metadata?.description || '';
345
+ modelInput.value = item.metadata?.model || '';
346
+ contentArea.value = item.content || '';
347
+ } else {
348
+ nameInput.value = '';
349
+ nameInput.disabled = false;
350
+ descInput.value = '';
351
+ modelInput.value = '';
352
+ contentArea.value = '';
353
+ }
354
+
355
+ // Handle form submission
356
+ form.onsubmit = async (e) => {
357
+ e.preventDefault();
358
+
359
+ const data = {
360
+ type: 'simple',
361
+ metadata: {
362
+ name: nameInput.value,
363
+ description: descInput.value,
364
+ },
365
+ content: contentArea.value
366
+ };
367
+
368
+ if (modelInput.value) {
369
+ data.metadata.model = modelInput.value;
370
+ }
371
+
372
+ try {
373
+ if (item) {
374
+ await updateItem(currentClient, tab.id, item.name, data);
375
+ } else {
376
+ await createItem(currentClient, tab.id, nameInput.value, data);
377
+ }
378
+ modal.style.display = 'none';
379
+ await loadTabContent(tab.id);
380
+ } catch (error) {
381
+ alert('Failed to save: ' + error.message);
382
+ }
383
+ };
384
+
385
+ // Cancel button
386
+ document.getElementById('skill-cancel-btn').onclick = () => {
387
+ modal.style.display = 'none';
388
+ };
389
+ }
390
+
391
+ // Complex view - file tree + editor
392
+ async function showComplexView(item, tab) {
393
+ simpleView.style.display = 'none';
394
+ complexView.style.display = 'block';
395
+ modalContent.classList.remove('modal-medium');
396
+ modalContent.classList.add('modal-large');
397
+
398
+ const fileList = document.getElementById('skill-file-list');
399
+ const fileEditor = document.getElementById('skill-file-editor');
400
+ const currentFileLabel = document.getElementById('skill-current-file');
401
+ const saveFileBtn = document.getElementById('skill-save-file-btn');
402
+
403
+ let currentFile = 'SKILL.md';
404
+ let files = [];
405
+
406
+ // Load file list
407
+ try {
408
+ console.log('Loading files for:', currentClient, tab.id, item.name);
409
+ const result = await fetchItemFiles(currentClient, tab.id, item.name);
410
+ console.log('Files result:', result);
411
+ files = result.files || [];
412
+ renderFileTree(files);
413
+ } catch (error) {
414
+ console.error('Failed to load files:', error);
415
+ fileList.innerHTML = `<div class="file-item">Failed to load files: ${error.message}</div>`;
416
+ }
417
+
418
+ // Load initial file (SKILL.md)
419
+ await loadFile('SKILL.md');
420
+
421
+ function renderFileTree(files) {
422
+ // Separate root files and build directory structure
423
+ const rootFiles = [];
424
+ const directories = {};
425
+
426
+ files.forEach(f => {
427
+ if (f.type === 'directory') {
428
+ // Skip directory entries, we'll use them as groupings
429
+ if (!directories[f.name]) {
430
+ directories[f.name] = { name: f.name, children: [] };
431
+ }
432
+ } else {
433
+ // It's a file
434
+ const parts = f.path.split('/');
435
+ if (parts.length === 1) {
436
+ // Root level file
437
+ rootFiles.push(f);
438
+ } else {
439
+ // File in a directory
440
+ const dir = parts[0];
441
+ if (!directories[dir]) {
442
+ directories[dir] = { name: dir, children: [] };
443
+ }
444
+ directories[dir].children.push(f);
445
+ }
446
+ }
447
+ });
448
+
449
+ let html = '';
450
+
451
+ // Render root files first (like SKILL.md)
452
+ rootFiles.sort((a, b) => a.name.localeCompare(b.name));
453
+ rootFiles.forEach(f => {
454
+ const isActive = f.path === currentFile;
455
+ html += `<div class="file-item ${isActive ? 'active' : ''}" data-path="${f.path}">
456
+ <span class="file-item-icon">📄</span>
457
+ <span class="file-item-name">${f.name}</span>
458
+ </div>`;
459
+ });
460
+
461
+ // Render directories with their children
462
+ const sortedDirs = Object.values(directories).sort((a, b) => a.name.localeCompare(b.name));
463
+ sortedDirs.forEach(dir => {
464
+ html += `<div class="file-item is-directory">
465
+ <span class="file-item-icon">📁</span>
466
+ <span class="file-item-name">${dir.name}/</span>
467
+ </div>`;
468
+ dir.children.sort((a, b) => a.name.localeCompare(b.name));
469
+ dir.children.forEach(child => {
470
+ const isActive = child.path === currentFile;
471
+ html += `<div class="file-item ${isActive ? 'active' : ''}" data-path="${child.path}">
472
+ <span class="file-item-indent"></span>
473
+ <span class="file-item-icon">📄</span>
474
+ <span class="file-item-name">${child.name}</span>
475
+ </div>`;
476
+ });
477
+ });
478
+
479
+ fileList.innerHTML = html || '<div class="file-item">No files found</div>';
480
+
481
+ // Add click handlers
482
+ fileList.querySelectorAll('.file-item[data-path]').forEach(el => {
483
+ el.onclick = () => loadFile(el.dataset.path);
484
+ });
485
+ }
486
+
487
+ async function loadFile(filePath) {
488
+ currentFile = filePath;
489
+ currentFileLabel.textContent = filePath;
490
+
491
+ // Update active state
492
+ fileList.querySelectorAll('.file-item').forEach(el => {
493
+ el.classList.toggle('active', el.dataset.path === filePath);
494
+ });
495
+
496
+ try {
497
+ const result = await fetchItemFile(currentClient, tab.id, item.name, filePath);
498
+ fileEditor.value = result.content || '';
499
+ } catch (error) {
500
+ fileEditor.value = `// Failed to load file: ${error.message}`;
501
+ }
502
+ }
503
+
504
+ // Save file button
505
+ saveFileBtn.onclick = async () => {
506
+ try {
507
+ await updateItemFile(currentClient, tab.id, item.name, currentFile, fileEditor.value);
508
+ saveFileBtn.textContent = 'Saved!';
509
+ setTimeout(() => { saveFileBtn.textContent = 'Save File'; }, 1500);
510
+ } catch (error) {
511
+ alert('Failed to save: ' + error.message);
512
+ }
513
+ };
514
+
515
+ // Close button
516
+ document.getElementById('skill-complex-cancel-btn').onclick = () => {
517
+ modal.style.display = 'none';
518
+ loadTabContent(tab.id); // Refresh list
519
+ };
520
+ }
521
+ }
522
+
86
523
  export async function loadClientServers() {
87
524
  try {
88
525
  const config = await getClientConfigApi(currentClient);
@@ -238,6 +675,3 @@ export const removeFromAllFromClientView = (serverName) => {
238
675
  removeFromAll(serverName, window.loadClients, loadClientServers, window.loadClients);
239
676
  };
240
677
 
241
- export const renameServerFromClientView = (serverName) => {
242
- showRenameServerModal(serverName, loadClientsCallback, currentClient);
243
- };
package/public/js/main.js CHANGED
@@ -31,7 +31,7 @@ document.addEventListener('DOMContentLoaded', () => {
31
31
  async function loadClients() {
32
32
  try {
33
33
  clients = await listClientsApi();
34
-
34
+
35
35
  // Initialize modules with current client data
36
36
  initClientView(clients, currentClient, loadClients);
37
37
  initKanbanView(clients, loadClients);
@@ -153,4 +153,4 @@ document.addEventListener('DOMContentLoaded', () => {
153
153
 
154
154
  // Load clients on page load
155
155
  loadClients();
156
- });
156
+ });
@@ -0,0 +1,135 @@
1
+ // Skills API - Per-Client Architecture
2
+
3
+ async function apiRequest(url, options = {}) {
4
+ const response = await fetch(url, options);
5
+ if (!response.ok) {
6
+ const text = await response.text();
7
+ throw new Error(`API error ${response.status}: ${text}`);
8
+ }
9
+ return response.json();
10
+ }
11
+
12
+ // Get tabs for a specific client
13
+ export async function fetchClientTabs(clientId) {
14
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/tabs`);
15
+ }
16
+
17
+ // Check if client has skills support
18
+ export async function clientHasSkills(clientId) {
19
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/has-skills`);
20
+ }
21
+
22
+ // List items in a client's tab
23
+ export async function fetchTabItems(clientId, tabId) {
24
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/tabs/${encodeURIComponent(tabId)}/items`);
25
+ }
26
+
27
+ // Get single item
28
+ export async function fetchItem(clientId, tabId, name) {
29
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/tabs/${encodeURIComponent(tabId)}/items/${encodeURIComponent(name)}`);
30
+ }
31
+
32
+ // Create item
33
+ export async function createItem(clientId, tabId, name, data) {
34
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/tabs/${encodeURIComponent(tabId)}/items/${encodeURIComponent(name)}`, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify(data),
38
+ });
39
+ }
40
+
41
+ // Update item
42
+ export async function updateItem(clientId, tabId, name, data) {
43
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/tabs/${encodeURIComponent(tabId)}/items/${encodeURIComponent(name)}`, {
44
+ method: 'PUT',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify(data),
47
+ });
48
+ }
49
+
50
+ // Delete item
51
+ export async function deleteItem(clientId, tabId, name) {
52
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/tabs/${encodeURIComponent(tabId)}/items/${encodeURIComponent(name)}`, {
53
+ method: 'DELETE',
54
+ });
55
+ }
56
+
57
+ // List files in a complex item
58
+ export async function fetchItemFiles(clientId, tabId, name) {
59
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/tabs/${encodeURIComponent(tabId)}/items/${encodeURIComponent(name)}/files`);
60
+ }
61
+
62
+ // Read file from complex item
63
+ export async function fetchItemFile(clientId, tabId, name, filePath) {
64
+ // Don't encode filePath slashes - they're part of the path structure
65
+ const encodedPath = filePath.split('/').map(p => encodeURIComponent(p)).join('/');
66
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/tabs/${encodeURIComponent(tabId)}/items/${encodeURIComponent(name)}/files/${encodedPath}`);
67
+ }
68
+
69
+ // Write file in complex item
70
+ export async function updateItemFile(clientId, tabId, name, filePath, content) {
71
+ // Don't encode filePath slashes - they're part of the path structure
72
+ const encodedPath = filePath.split('/').map(p => encodeURIComponent(p)).join('/');
73
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/tabs/${encodeURIComponent(tabId)}/items/${encodeURIComponent(name)}/files/${encodedPath}`, {
74
+ method: 'PUT',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify({ content }),
77
+ });
78
+ }
79
+
80
+ // Copy item within same tab
81
+ export async function copyItem(clientId, tabId, name, newName) {
82
+ return apiRequest(`/api/clients/${encodeURIComponent(clientId)}/tabs/${encodeURIComponent(tabId)}/items/${encodeURIComponent(name)}/copy`, {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ newName }),
86
+ });
87
+ }
88
+
89
+ // Commands API (legacy - still uses global commands path)
90
+
91
+ export async function fetchCommands() {
92
+ return apiRequest('/api/commands');
93
+ }
94
+
95
+ export async function fetchCommand(name) {
96
+ return apiRequest(`/api/commands/${encodeURIComponent(name)}`);
97
+ }
98
+
99
+ export async function createCommand(name, data) {
100
+ return apiRequest(`/api/commands/${encodeURIComponent(name)}`, {
101
+ method: 'POST',
102
+ headers: { 'Content-Type': 'application/json' },
103
+ body: JSON.stringify(data),
104
+ });
105
+ }
106
+
107
+ export async function updateCommand(name, data) {
108
+ return apiRequest(`/api/commands/${encodeURIComponent(name)}`, {
109
+ method: 'PUT',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: JSON.stringify(data),
112
+ });
113
+ }
114
+
115
+ export async function deleteCommand(name) {
116
+ return apiRequest(`/api/commands/${encodeURIComponent(name)}`, {
117
+ method: 'DELETE',
118
+ });
119
+ }
120
+
121
+ export async function renameCommand(oldName, newName) {
122
+ return apiRequest(`/api/commands/${encodeURIComponent(oldName)}/rename`, {
123
+ method: 'POST',
124
+ headers: { 'Content-Type': 'application/json' },
125
+ body: JSON.stringify({ newName }),
126
+ });
127
+ }
128
+
129
+ export async function duplicateCommand(name, newName) {
130
+ return apiRequest(`/api/commands/${encodeURIComponent(name)}/duplicate`, {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify({ newName }),
134
+ });
135
+ }