labgate 0.5.13 → 0.5.14
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/dist/cli.js +115 -0
- package/dist/cli.js.map +1 -1
- package/dist/lib/config.d.ts +12 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/container.js +22 -3
- package/dist/lib/container.js.map +1 -1
- package/dist/lib/dataset-mcp.js +18 -1
- package/dist/lib/dataset-mcp.js.map +1 -1
- package/dist/lib/ui.html +150 -2
- package/dist/lib/ui.js +129 -0
- package/dist/lib/ui.js.map +1 -1
- package/dist/mcp-bundles/dataset-mcp.bundle.mjs +17 -2
- package/package.json +1 -1
package/dist/lib/ui.html
CHANGED
|
@@ -627,6 +627,59 @@
|
|
|
627
627
|
line-height: 1.4;
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
.dataset-card-stats {
|
|
631
|
+
display: flex;
|
|
632
|
+
align-items: center;
|
|
633
|
+
gap: 6px;
|
|
634
|
+
padding-left: 42px;
|
|
635
|
+
margin-bottom: 8px;
|
|
636
|
+
font-size: 0.75rem;
|
|
637
|
+
color: var(--text-muted);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.dataset-card-stats .stat-dot {
|
|
641
|
+
color: var(--border-color);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.dataset-card-stats .stat-warning {
|
|
645
|
+
color: #d97706;
|
|
646
|
+
font-weight: 500;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.dataset-card-stats .stat-loading {
|
|
650
|
+
color: var(--text-muted);
|
|
651
|
+
font-style: italic;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.dataset-card-stats .stat-scan-btn {
|
|
655
|
+
background: none;
|
|
656
|
+
border: 1px solid var(--border-color);
|
|
657
|
+
color: var(--text-secondary);
|
|
658
|
+
font-size: 0.6875rem;
|
|
659
|
+
font-family: inherit;
|
|
660
|
+
padding: 1px 8px;
|
|
661
|
+
border-radius: 4px;
|
|
662
|
+
cursor: pointer;
|
|
663
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.dataset-card-stats .stat-scan-btn:hover {
|
|
667
|
+
background: var(--bg-secondary);
|
|
668
|
+
color: var(--text-primary);
|
|
669
|
+
border-color: #B8CBB8;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.dataset-card-stats .stat-scan-btn:disabled {
|
|
673
|
+
opacity: 0.5;
|
|
674
|
+
cursor: default;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.dataset-card-stats .stat-scanned-at {
|
|
678
|
+
color: var(--text-muted);
|
|
679
|
+
font-size: 0.6875rem;
|
|
680
|
+
margin-left: 2px;
|
|
681
|
+
}
|
|
682
|
+
|
|
630
683
|
.dataset-card-paths {
|
|
631
684
|
display: flex;
|
|
632
685
|
align-items: center;
|
|
@@ -3173,7 +3226,7 @@ function renderDatasets() {
|
|
|
3173
3226
|
var descHtml = ds.description
|
|
3174
3227
|
? '<div class="dataset-card-desc">' + escapeHtml(ds.description) + '</div>'
|
|
3175
3228
|
: '';
|
|
3176
|
-
return '<div class="dataset-card">'
|
|
3229
|
+
return '<div class="dataset-card" data-ds-name="' + escapeHtml(ds.name) + '">'
|
|
3177
3230
|
+ '<div class="dataset-card-header">'
|
|
3178
3231
|
+ '<div class="dataset-card-icon">' + dbIcon + '</div>'
|
|
3179
3232
|
+ '<div class="dataset-card-title">'
|
|
@@ -3183,6 +3236,7 @@ function renderDatasets() {
|
|
|
3183
3236
|
+ '<button class="remove-btn" data-index="' + i + '" title="Remove dataset">×</button>'
|
|
3184
3237
|
+ '</div>'
|
|
3185
3238
|
+ descHtml
|
|
3239
|
+
+ '<div class="dataset-card-stats"><span class="stat-loading">Loading stats\u2026</span></div>'
|
|
3186
3240
|
+ '<div class="dataset-card-paths">'
|
|
3187
3241
|
+ '<span class="dataset-path">' + escapeHtml(ds.path) + '</span>'
|
|
3188
3242
|
+ '<span class="dataset-path-arrow">' + arrowIcon + '</span>'
|
|
@@ -3198,6 +3252,98 @@ function renderDatasets() {
|
|
|
3198
3252
|
removeDataset(idx);
|
|
3199
3253
|
});
|
|
3200
3254
|
});
|
|
3255
|
+
fetchDatasetStats();
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
function renderStatsContent(statsEl, name, s) {
|
|
3259
|
+
if (!s.exists) {
|
|
3260
|
+
statsEl.innerHTML = '<span class="stat-warning">Path not found</span>';
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
if (!s.scanned_at) {
|
|
3264
|
+
statsEl.innerHTML = '<span style="color:var(--text-muted)">Not initialized</span>'
|
|
3265
|
+
+ ' <button class="stat-scan-btn" data-scan-name="' + escapeHtml(name) + '" title="Initialize dataset">Scan</button>'
|
|
3266
|
+
+ '<span class="stat-scanned-at" style="margin-left:4px">or run: <code style="font-size:0.6875rem">labgate dataset init ' + escapeHtml(name) + '</code></span>';
|
|
3267
|
+
bindScanButtons(statsEl);
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
var ago = timeAgo(s.scanned_at);
|
|
3271
|
+
statsEl.innerHTML = '<span>' + s.file_count + ' file' + (s.file_count !== 1 ? 's' : '') + '</span>'
|
|
3272
|
+
+ '<span class="stat-dot">·</span>'
|
|
3273
|
+
+ '<span>' + escapeHtml(s.total_size_formatted) + '</span>'
|
|
3274
|
+
+ '<span class="stat-scanned-at">(' + ago + ')</span>'
|
|
3275
|
+
+ ' <button class="stat-scan-btn" data-scan-name="' + escapeHtml(name) + '" title="Rescan">↻</button>';
|
|
3276
|
+
bindScanButtons(statsEl);
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
function bindScanButtons(root) {
|
|
3280
|
+
root.querySelectorAll('.stat-scan-btn[data-scan-name]').forEach(function(btn) {
|
|
3281
|
+
btn.addEventListener('click', function() {
|
|
3282
|
+
scanDataset(btn.getAttribute('data-scan-name'));
|
|
3283
|
+
});
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
function timeAgo(isoStr) {
|
|
3288
|
+
var diff = Date.now() - new Date(isoStr).getTime();
|
|
3289
|
+
var sec = Math.floor(diff / 1000);
|
|
3290
|
+
if (sec < 60) return 'just now';
|
|
3291
|
+
var min = Math.floor(sec / 60);
|
|
3292
|
+
if (min < 60) return min + 'm ago';
|
|
3293
|
+
var hr = Math.floor(min / 60);
|
|
3294
|
+
if (hr < 24) return hr + 'h ago';
|
|
3295
|
+
var days = Math.floor(hr / 24);
|
|
3296
|
+
return days + 'd ago';
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
function scanDataset(name) {
|
|
3300
|
+
var card = document.querySelector('.dataset-card[data-ds-name="' + name + '"]');
|
|
3301
|
+
if (!card) return;
|
|
3302
|
+
var statsEl = card.querySelector('.dataset-card-stats');
|
|
3303
|
+
if (statsEl) statsEl.innerHTML = '<span class="stat-loading">Scanning\u2026</span>';
|
|
3304
|
+
|
|
3305
|
+
fetch('/api/dataset-scan', {
|
|
3306
|
+
method: 'POST',
|
|
3307
|
+
headers: apiWriteHeaders(),
|
|
3308
|
+
body: JSON.stringify({ name: name })
|
|
3309
|
+
})
|
|
3310
|
+
.then(function(r) { return r.json(); })
|
|
3311
|
+
.then(function(data) {
|
|
3312
|
+
if (!statsEl) return;
|
|
3313
|
+
if (!data.ok) {
|
|
3314
|
+
statsEl.innerHTML = '<span class="stat-warning">' + escapeHtml(data.error || 'Scan failed') + '</span>';
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
renderStatsContent(statsEl, name, {
|
|
3318
|
+
exists: true,
|
|
3319
|
+
file_count: data.stats.file_count,
|
|
3320
|
+
total_size_formatted: data.stats.total_size_formatted,
|
|
3321
|
+
scanned_at: data.stats.scanned_at
|
|
3322
|
+
});
|
|
3323
|
+
showToast('Dataset "' + name + '" scanned: ' + data.stats.file_count + ' files, ' + data.stats.total_size_formatted, 'success');
|
|
3324
|
+
})
|
|
3325
|
+
.catch(function() {
|
|
3326
|
+
if (statsEl) statsEl.innerHTML = '<span class="stat-warning">Scan failed</span>';
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
function fetchDatasetStats() {
|
|
3331
|
+
fetch('/api/dataset-stats', { headers: apiReadHeaders() })
|
|
3332
|
+
.then(function(r) { return r.json(); })
|
|
3333
|
+
.then(function(data) {
|
|
3334
|
+
if (!data || !data.stats) return;
|
|
3335
|
+
var cards = document.querySelectorAll('.dataset-card[data-ds-name]');
|
|
3336
|
+
cards.forEach(function(card) {
|
|
3337
|
+
var name = card.getAttribute('data-ds-name');
|
|
3338
|
+
var statsEl = card.querySelector('.dataset-card-stats');
|
|
3339
|
+
if (!statsEl || !name || !data.stats[name]) return;
|
|
3340
|
+
renderStatsContent(statsEl, name, data.stats[name]);
|
|
3341
|
+
});
|
|
3342
|
+
})
|
|
3343
|
+
.catch(function() {
|
|
3344
|
+
var els = document.querySelectorAll('.dataset-card-stats .stat-loading');
|
|
3345
|
+
els.forEach(function(el) { el.textContent = ''; });
|
|
3346
|
+
});
|
|
3201
3347
|
}
|
|
3202
3348
|
|
|
3203
3349
|
function addDataset() {
|
|
@@ -3565,12 +3711,14 @@ function checkDatasetRestartNeeded() {
|
|
|
3565
3711
|
var notice = document.getElementById('datasetNotice');
|
|
3566
3712
|
var text = notice ? notice.querySelector('.dataset-notice-text') : null;
|
|
3567
3713
|
if (needsRestart.length > 0) {
|
|
3714
|
+
if (notice) notice.style.display = '';
|
|
3568
3715
|
if (text) text.textContent = needsRestart.length + ' active session' + (needsRestart.length > 1 ? 's' : '') + ' running with outdated config. Restart them now to apply dataset changes.';
|
|
3569
3716
|
if (btn) { btn.style.display = ''; btn.disabled = false; btn.textContent = 'Restart Sessions Now'; }
|
|
3570
3717
|
} else if (sessions.length > 0) {
|
|
3571
|
-
if (
|
|
3718
|
+
if (notice) notice.style.display = 'none';
|
|
3572
3719
|
if (btn) btn.style.display = 'none';
|
|
3573
3720
|
} else {
|
|
3721
|
+
if (notice) notice.style.display = '';
|
|
3574
3722
|
if (text) text.textContent = 'No active sessions. Dataset changes will be used the next time you start a session.';
|
|
3575
3723
|
if (btn) btn.style.display = 'none';
|
|
3576
3724
|
}
|
package/dist/lib/ui.js
CHANGED
|
@@ -1502,6 +1502,129 @@ async function handleValidatePath(req, res) {
|
|
|
1502
1502
|
json(res, { valid: false, error: err.message ?? String(err) }, 400);
|
|
1503
1503
|
}
|
|
1504
1504
|
}
|
|
1505
|
+
// ── Dataset stats helpers ───────────────────────────────
|
|
1506
|
+
function formatBytesUI(bytes) {
|
|
1507
|
+
if (bytes === 0)
|
|
1508
|
+
return '0 B';
|
|
1509
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
1510
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
1511
|
+
const val = bytes / Math.pow(1024, i);
|
|
1512
|
+
return `${val.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
1513
|
+
}
|
|
1514
|
+
function walkDirShallow(dir, maxDepth, depth = 0) {
|
|
1515
|
+
let files = 0;
|
|
1516
|
+
let totalSize = 0;
|
|
1517
|
+
if (depth > maxDepth)
|
|
1518
|
+
return { files, totalSize };
|
|
1519
|
+
let entries;
|
|
1520
|
+
try {
|
|
1521
|
+
entries = (0, fs_1.readdirSync)(dir);
|
|
1522
|
+
}
|
|
1523
|
+
catch {
|
|
1524
|
+
return { files, totalSize };
|
|
1525
|
+
}
|
|
1526
|
+
for (const entry of entries) {
|
|
1527
|
+
if (entry.startsWith('.') && depth === 0)
|
|
1528
|
+
continue;
|
|
1529
|
+
const fullPath = (0, path_1.join)(dir, entry);
|
|
1530
|
+
try {
|
|
1531
|
+
const st = (0, fs_1.statSync)(fullPath);
|
|
1532
|
+
if (st.isDirectory()) {
|
|
1533
|
+
if (depth < maxDepth) {
|
|
1534
|
+
const sub = walkDirShallow(fullPath, maxDepth, depth + 1);
|
|
1535
|
+
files += sub.files;
|
|
1536
|
+
totalSize += sub.totalSize;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
else {
|
|
1540
|
+
files++;
|
|
1541
|
+
totalSize += st.size;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
catch { /* skip inaccessible entries */ }
|
|
1545
|
+
}
|
|
1546
|
+
return { files, totalSize };
|
|
1547
|
+
}
|
|
1548
|
+
function handleGetDatasetStats(_req, res) {
|
|
1549
|
+
const config = (0, config_js_1.loadConfig)();
|
|
1550
|
+
const datasets = config.datasets || [];
|
|
1551
|
+
const stats = {};
|
|
1552
|
+
for (const ds of datasets) {
|
|
1553
|
+
const hostPath = ds.path.replace(/^~/, (0, os_1.homedir)());
|
|
1554
|
+
const pathExists = (0, fs_1.existsSync)(hostPath);
|
|
1555
|
+
if (ds.stats) {
|
|
1556
|
+
stats[ds.name] = {
|
|
1557
|
+
exists: pathExists,
|
|
1558
|
+
file_count: ds.stats.file_count,
|
|
1559
|
+
total_size: ds.stats.total_size,
|
|
1560
|
+
total_size_formatted: ds.stats.total_size_formatted,
|
|
1561
|
+
scanned_at: ds.stats.scanned_at,
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
stats[ds.name] = {
|
|
1566
|
+
exists: pathExists,
|
|
1567
|
+
file_count: 0,
|
|
1568
|
+
total_size: 0,
|
|
1569
|
+
total_size_formatted: '0 B',
|
|
1570
|
+
scanned_at: null,
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
json(res, { stats });
|
|
1575
|
+
}
|
|
1576
|
+
async function handlePostDatasetScan(req, res) {
|
|
1577
|
+
try {
|
|
1578
|
+
const body = await readBody(req);
|
|
1579
|
+
const { name } = JSON.parse(body);
|
|
1580
|
+
if (!name || typeof name !== 'string') {
|
|
1581
|
+
json(res, { ok: false, error: 'Missing dataset name' }, 400);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
const configPath = (0, config_js_1.getConfigPath)();
|
|
1585
|
+
let obj = {};
|
|
1586
|
+
if ((0, fs_1.existsSync)(configPath)) {
|
|
1587
|
+
const rawText = (0, fs_1.readFileSync)(configPath, 'utf-8');
|
|
1588
|
+
const stripped = rawText
|
|
1589
|
+
.split('\n')
|
|
1590
|
+
.filter(line => !line.trimStart().startsWith('//'))
|
|
1591
|
+
.join('\n');
|
|
1592
|
+
try {
|
|
1593
|
+
obj = JSON.parse(stripped);
|
|
1594
|
+
}
|
|
1595
|
+
catch {
|
|
1596
|
+
obj = {};
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const datasets = (obj.datasets || []);
|
|
1600
|
+
const idx = datasets.findIndex((d) => d?.name && String(d.name).toLowerCase() === name.toLowerCase());
|
|
1601
|
+
if (idx < 0) {
|
|
1602
|
+
json(res, { ok: false, error: `Dataset "${name}" not found` }, 404);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const ds = datasets[idx];
|
|
1606
|
+
const hostPath = String(ds.path || '').replace(/^~/, (0, os_1.homedir)());
|
|
1607
|
+
if (!(0, fs_1.existsSync)(hostPath)) {
|
|
1608
|
+
json(res, { ok: false, error: `Path does not exist: ${hostPath}` }, 400);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const walked = walkDirShallow(hostPath, 1);
|
|
1612
|
+
const statsObj = {
|
|
1613
|
+
file_count: walked.files,
|
|
1614
|
+
total_size: walked.totalSize,
|
|
1615
|
+
total_size_formatted: formatBytesUI(walked.totalSize),
|
|
1616
|
+
scanned_at: new Date().toISOString(),
|
|
1617
|
+
};
|
|
1618
|
+
ds.stats = statsObj;
|
|
1619
|
+
obj.datasets = datasets;
|
|
1620
|
+
(0, fs_1.writeFileSync)(configPath, JSON.stringify(obj, null, 2) + '\n', { encoding: 'utf-8', mode: config_js_1.PRIVATE_FILE_MODE });
|
|
1621
|
+
(0, config_js_1.ensurePrivateFile)(configPath);
|
|
1622
|
+
json(res, { ok: true, stats: statsObj });
|
|
1623
|
+
}
|
|
1624
|
+
catch (err) {
|
|
1625
|
+
json(res, { ok: false, error: err.message ?? String(err) }, 500);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1505
1628
|
function handleGetLogs(_req, res) {
|
|
1506
1629
|
const config = (0, config_js_1.loadConfig)();
|
|
1507
1630
|
if (!config.audit.enabled) {
|
|
@@ -2700,6 +2823,12 @@ function startUI(optsOrPort = {}, standaloneArg = true) {
|
|
|
2700
2823
|
else if (pathname === '/api/validate-path' && method === 'POST') {
|
|
2701
2824
|
await handleValidatePath(req, res);
|
|
2702
2825
|
}
|
|
2826
|
+
else if (pathname === '/api/dataset-stats' && method === 'GET') {
|
|
2827
|
+
handleGetDatasetStats(req, res);
|
|
2828
|
+
}
|
|
2829
|
+
else if (pathname === '/api/dataset-scan' && method === 'POST') {
|
|
2830
|
+
await handlePostDatasetScan(req, res);
|
|
2831
|
+
}
|
|
2703
2832
|
else if (/^\/api\/sessions\/[^/]+\/instructions$/.test(pathname) && method === 'GET') {
|
|
2704
2833
|
handleGetSessionInstructions(pathname, reqUrl, res);
|
|
2705
2834
|
}
|