mcp-config-manager 2.1.1 → 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.
- package/README.md +198 -0
- package/package.json +1 -1
- package/public/index.html +98 -2
- package/public/js/clientView.js +437 -3
- package/public/js/main.js +2 -2
- package/public/js/skillsApi.js +135 -0
- package/public/style.css +877 -26
- package/src/commands-manager.js +268 -0
- package/src/server.js +300 -0
- package/src/skills-manager.js +555 -0
- package/src/skills-scopes.js +809 -0
package/public/js/clientView.js
CHANGED
|
@@ -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
|
+
}
|